/download-webpage-as-pdf
Set to "false" (the recipe default) to force headless capture regardless of the host agent-browser config
$ npx -y skills add tenequm/skills --skill download-webpage-as-pdf --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.
- You can call itInvoke it directly when you want it.
- Slash command
/download-webpage-as-pdf
Context preview
The summary Claude sees to decide when to auto-load this skill.
Set to "false" (the recipe default) to force headless capture regardless of the host agent-browser config
SKILL.md
download-webpage-as-pdf.SKILL.mdname: download-webpage-as-pdf
description: Save a live webpage as a high-fidelity PDF that preserves the original layout AND every image (including lazy-loaded ones) using the agent-browser CLI. Use this whenever the user asks to "download this page as PDF", "save this article", "archive this URL", "fetch this page for reference", or otherwise wants a local PDF of a web page that looks like the browser version. Especially important on modern JS-heavy sites (engineering blogs, Next.js sites, anything with IntersectionObserver lazy loading) where naive `chrome --headless --print-to-pdf` or a bare `agent-browser pdf` produces blank rectangles or broken-image placeholders. Trigger this skill even when the user does not name the tool - any request to capture a webpage's full visual content as a PDF on disk should pull this in. For reader-mode/article-only output (no nav, no footer, no manual trimming) prefer percollate instead - see "When NOT to use this".
metadata:
version: "0.1.5"
upstream: "agent-browser@0.26.0"
openclaw:
homepage: https://github.com/tenequm/skills/tree/main/skills/download-webpage-as-pdf
emoji: "๐"
requires:
bins:
- agent-browser
install:
- kind: node
package: agent-browser
bins:
- agent-browser
envVars:
- name: AGENT_BROWSER_HEADED
required: false
description: Set to "false" (the recipe default) to force headless capture regardless of the host agent-browser configDownload a webpage as a PDF (agent-browser recipe)
The naive approaches fail on modern sites:
- `chrome --headless --print-to-pdf` captures only the initial viewport's images. Anything below the fold renders as a blank rectangle.
- `agent-browser pdf` immediately after `open` has the same problem - lazy-loaded images haven't decoded yet.
- Scrolling via JS and then waiting a fixed time is also unreliable - you don't know when each image actually finished.
The fix is one async script that strips lazy-load attributes, scrolls the page to trigger any IntersectionObserver-based loaders, and `await`s every `<img>` to decode. agent-browser's `eval` waits for the returned promise to resolve before exiting, so the subsequent `pdf` command sees a fully-loaded DOM.
The recipe
If multiple test/agent runs may share the host's agent-browser, isolate each invocation with `agent-browser --session <unique-name> ...` on every command in the pipeline. Single-user one-off captures can omit the flag and use the default session.
Set `AGENT_BROWSER_HEADED=false` in the environment before running so the skill launches headless even when the host's `~/.agent-browser/config.json` defaults to `"headed": true`. This avoids popping a real Chrome window on the user's desktop while an agent is working in the background. Do NOT use the CLI's `--headed false` flag - in agent-browser 0.26.0 it parses but corrupts the session context (subsequent commands see an empty document). The env var is the supported route. To watch the run for debugging, unset the variable or pass `--headed` instead.
export AGENT_BROWSER_HEADED=false
agent-browser open <URL>
agent-browser wait --load networkidle
agent-browser eval "(async () => {
const sleep = ms => new Promise(r => setTimeout(r, ms));
['#onetrust-banner-sdk','#onetrust-consent-sdk','.ot-sdk-container','#ot-sdk-btn-floating','[id*=cookie]','[id*=consent]','[id*=onetrust]'].forEach(s => document.querySelectorAll(s).forEach(e => e.remove()));
document.querySelectorAll('img').forEach(img => {
img.removeAttribute('loading');
img.removeAttribute('decoding');
if (img.dataset.src) img.src = img.dataset.src;
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
});
for (let y = 0; y < document.documentElement.scrollHeight + 2000; y += 400) {
window.scrollTo(0, y);
await sleep(200);
}
window.scrollTo(0, document.documentElement.scrollHeight);
await sleep(2000);
await Promise.all(Array.from(document.images).map(i =>
i.complete && i.naturalWidth ? null
: new Promise(r => { i.addEventListener('load', r, {once:true}); i.addEventListener('error', r, {once:true}); setTimeout(r, 5000); })
));
window.scrollTo(0, 0);
await sleep(500);
return Array.from(document.images).filter(i => !i.naturalWidth).length;
})()"
agent-browser pdf /tmp/page.pdf
agent-browser close
# Verify the result
pdfinfo /tmp/page.pdf | grep -E "Pages|File size"The `eval` returns the count of images that still failed to load. Expect `0`. If non-zero, the recipe didn't fully capture the page - investigate before trusting the PDF. The `pdfinfo` line is your standard end-of-recipe report (page count + bytes) so the agent has concrete numbers to relay back.
Why each step matters
- **`wait --load networkidle`** before the eval gives the page a chance to attach its IntersectionObservers and other JS hooks. Scrolling before observers attach defeats the trigger.
- **Removing the `loading` attribute** is the structural fix. This is the same trick percollate uses internally - the most reliable way to make Chromium eagerly fetch every image.
- **Scrolling the full height in 400px steps** triggers any observer-based loaders that watch for elements crossing the viewport. Some sites use observers even after `loading=lazy` is removed.
- **`await Promise.all` on every `<img>`** guarantees decoded pixels are in memory before the eval returns. agent-browser's `eval` is promise-aware - the next command (`pdf`) will not run until this resolves.
- **Returning the broken-image count** is your verification. If it is not 0, the recipe did not fully capture the page - do not trust the PDF.
Cleanup pipeline (optional but recommended)
agent-browser saves at letter size with the page's full footer (nav, newsletter signup, link sitemap). For a clean archive:
# 1. Inspect total page count and visually identify which trailing pages are footer
pdfinfo /tmp/page.pdf | grep Pa
Read more
name: download-webpage-as-pdf
description: Save a live webpage as a high-fidelity PDF that preserves the original layout AND every image (including lazy-loaded ones) using the agent-browser CLI. Use this whenever the user asks to "download this page as PDF", "save this article", "archive this URL", "fetch this page for reference", or otherwise wants a local PDF of a web page that looks like the browser version. Especially important on modern JS-heavy sites (engineering blogs, Next.js sites, anything with IntersectionObserver lazy loading) where naive `chrome --headless --print-to-pdf` or a bare `agent-browser pdf` produces blank rectangles or broken-image placeholders. Trigger this skill even when the user does not name the tool - any request to capture a webpage's full visual content as a PDF on disk should pull this in. For reader-mode/article-only output (no nav, no footer, no manual trimming) prefer percollate instead - see "When NOT to use this".
metadata:
version: "0.1.5"
upstream: "agent-browser@0.26.0"
openclaw:
homepage: https://github.com/tenequm/skills/tree/main/skills/download-webpage-as-pdf
emoji: "๐"
requires:
bins:
- agent-browser
install:
- kind: node
package: agent-browser
bins:
- agent-browser
envVars:
- name: AGENT_BROWSER_HEADED
required: false
description: Set to "false" (the recipe default) to force headless capture regardless of the host agent-browser configDownload a webpage as a PDF (agent-browser recipe)
The naive approaches fail on modern sites:
- `chrome --headless --print-to-pdf` captures only the initial viewport's images. Anything below the fold renders as a blank rectangle.
- `agent-browser pdf` immediately after `open` has the same problem - lazy-loaded images haven't decoded yet.
- Scrolling via JS and then waiting a fixed time is also unreliable - you don't know when each image actually finished.
The fix is one async script that strips lazy-load attributes, scrolls the page to trigger any IntersectionObserver-based loaders, and `await`s every `<img>` to decode. agent-browser's `eval` waits for the returned promise to resolve before exiting, so the subsequent `pdf` command sees a fully-loaded DOM.
The recipe
If multiple test/agent runs may share the host's agent-browser, isolate each invocation with `agent-browser --session <unique-name> ...` on every command in the pipeline. Single-user one-off captures can omit the flag and use the default session.
Set `AGENT_BROWSER_HEADED=false` in the environment before running so the skill launches headless even when the host's `~/.agent-browser/config.json` defaults to `"headed": true`. This avoids popping a real Chrome window on the user's desktop while an agent is working in the background. Do NOT use the CLI's `--headed false` flag - in agent-browser 0.26.0 it parses but corrupts the session context (subsequent commands see an empty document). The env var is the supported route. To watch the run for debugging, unset the variable or pass `--headed` instead.
export AGENT_BROWSER_HEADED=false
agent-browser open <URL>
agent-browser wait --load networkidle
agent-browser eval "(async () => {
const sleep = ms => new Promise(r => setTimeout(r, ms));
['#onetrust-banner-sdk','#onetrust-consent-sdk','.ot-sdk-container','#ot-sdk-btn-floating','[id*=cookie]','[id*=consent]','[id*=onetrust]'].forEach(s => document.querySelectorAll(s).forEach(e => e.remove()));
document.querySelectorAll('img').forEach(img => {
img.removeAttribute('loading');
img.removeAttribute('decoding');
if (img.dataset.src) img.src = img.dataset.src;
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
});
for (let y = 0; y < document.documentElement.scrollHeight + 2000; y += 400) {
window.scrollTo(0, y);
await sleep(200);
}
window.scrollTo(0, document.documentElement.scrollHeight);
await sleep(2000);
await Promise.all(Array.from(document.images).map(i =>
i.complete && i.naturalWidth ? null
: new Promise(r => { i.addEventListener('load', r, {once:true}); i.addEventListener('error', r, {once:true}); setTimeout(r, 5000); })
));
window.scrollTo(0, 0);
await sleep(500);
return Array.from(document.images).filter(i => !i.naturalWidth).length;
})()"
agent-browser pdf /tmp/page.pdf
agent-browser close
# Verify the result
pdfinfo /tmp/page.pdf | grep -E "Pages|File size"The `eval` returns the count of images that still failed to load. Expect `0`. If non-zero, the recipe didn't fully capture the page - investigate before trusting the PDF. The `pdfinfo` line is your standard end-of-recipe report (page count + bytes) so the agent has concrete numbers to relay back.
Why each step matters
- **`wait --load networkidle`** before the eval gives the page a chance to attach its IntersectionObservers and other JS hooks. Scrolling before observers attach defeats the trigger.
- **Removing the `loading` attribute** is the structural fix. This is the same trick percollate uses internally - the most reliable way to make Chromium eagerly fetch every image.
- **Scrolling the full height in 400px steps** triggers any observer-based loaders that watch for elements crossing the viewport. Some sites use observers even after `loading=lazy` is removed.
- **`await Promise.all` on every `<img>`** guarantees decoded pixels are in memory before the eval returns. agent-browser's `eval` is promise-aware - the next command (`pdf`) will not run until this resolves.
- **Returning the broken-image count** is your verification. If it is not 0, the recipe did not fully capture the page - do not trust the PDF.
Cleanup pipeline (optional but recommended)
agent-browser saves at letter size with the page's full footer (nav, newsletter signup, link sitemap). For a clean archive:
# 1. Inspect total page count and visually identify which trailing pages are footer pdfinfo /tmp/page.pdf | grep Pa
Showing the first part of this file.
Claude Code skills for founders, developers, and web3 builders. This repository publishes reusable skill folders under skills//, ships stable bundle downloads through GitHub Releases, and publishes changed skills to ClawHub.
Repo: tenequm/skills
Other skills on tenequm-skills.
- /audio-quality-check
Analyze audio recording quality - echo detection, loudness, speech intelligibility, SNR, spectral analysis. Use when the user wants to check a recording's quality, detect echo or duplication in audio files, measure speech clarity, compare original vs processed audio, diagnose
Open skill - /chrome-extension-wxt
Build Chrome extensions using WXT framework with TypeScript, React, Vue, or Svelte. Use when creating browser extensions, developing cross-browser add-ons, or working with Chrome Web Store projects. Triggers on phrases like "chrome extension", "browser extension", "WXT
Open skill - /cloudflare-workers
Cloudflare account ID, set as a CI secret for wrangler deploys.
Open skill - /command-skill-creator
Create automation command skills (slash commands) for Claude Code projects. Use when building `/slash-commands` that automate multi-step workflows - deploys, commits, releases, migrations, cross-repo operations, or any repeatable process. Triggers on "create a command", "make a
Open skill - /deep-research-glim
Conducts deep, multi-angle research using glim MCP tools and parallel subagents. Use for deep research, competitive landscape analysis, strategic intelligence, or /deep-research-glim [topic]. Triggers - deep research, deep dive on, competitive landscape, strategic intelligence,
Open skill - /effect-ts
OpenAI API key for Effect AI examples using the OpenAI provider.
Open skill

