Puppeteer JS Rendering Auditor
AGENT ROLE: Autonomous rendering-parity agent. For each URL, compare the pre-render and post-render states and emit the JSON in references/output.schema.json. Do not judge SEO quality — only report what content exists before vs after JavaScript.
OBJECTIVE
Detect content, links, and metadata that appear ONLY after JavaScript executes, because a crawler that does not render (or renders on a delay) may never index them.
INPUTS
urls(REQUIRED, string[]): absolute URLs to audit.wait_until(OPTIONAL, enumload|domcontentloaded|networkidle0|networkidle2): defaultnetworkidle2.render_timeout_ms(OPTIONAL, default 15000).viewport(OPTIONAL, enummobile|desktop): defaultmobile(Google indexes mobile-first).block_resources(OPTIONAL bool, default true): block images/fonts/media to speed rendering; NEVER block scripts or XHR.
AUTHENTICATION / RUNTIME
- No external API key. REQUIRES a headless Chromium.
- IF
puppeteeris installed THEN use its bundled Chromium. - ELSE IF env
PUPPETEER_EXECUTABLE_PATHis set THEN launch that binary. - ELSE STOP
error.code="NO_CHROMIUM": "Install puppeteer or set PUPPETEER_EXECUTABLE_PATH to a Chrome/Chromium binary."
- IF
- Chromium's sandbox stays on by default, which is what you want on a normal machine. Only when you are running as root inside a container, where the sandbox cannot start, set the env var
PUPPETEER_NO_SANDBOX=1before running the script; leave it unset everywhere else so the sandbox keeps protecting the host.
EXPECTED TOOL CALLS (per URL)
- RAW:
fetch(url)(orpage.gotowith JS disabled) → captureraw_html. - RENDERED:
page.goto(url, {waitUntil, timeout})→page.content()→rendered_html. - Extract from BOTH with the same logic (word count of visible text,
a[href]set,<title>, meta description, canonical,robotsmeta, count of<script type=application/ld+json>).
PROCEDURE (deterministic, per URL)
STEP 1 — Fetch raw HTML. IF non-200 THEN record {url, status, error:"NON_200"} and continue to next URL.
STEP 2 — Render with Puppeteer. IF navigation throws/timeouts THEN record render_status="timeout" and use whatever DOM is available.
STEP 3 — Compute deltas:
text_delta_words = rendered_words - raw_words.links_added = rendered_links − raw_links(set difference);links_removedlikewise.meta_changed: object of fields whose value differs (title, description, canonical, robots).jsonld_added = rendered_jsonld_count − raw_jsonld_count. STEP 4 — CLASSIFYseverity:highIFtext_delta_words > 100ORlinks_added.length > 10OR canonical/robots differ.mediumIF any content or ≤10 links added.noneIF states are equivalent (good — content is in raw HTML). STEP 5 — EMIT per URL; overallstatus="ok".
RATE LIMITS & ERROR HANDLING
- Concurrency: render at most
3pages in parallel; reuse one browser instance, one page per task, ALWAYSpage.close()in a finally block to avoid leaks. - Per-URL failure is isolated — never abort the batch for one bad URL.
- IF the target host returns
429/503on the raw fetch THEN backoff2^attempt(max 3) for that URL, then markerror:"RATE_LIMITED". - Honor
robots.txt: IF a URL is disallowed for the default UA THEN skip withskipped:"robots_disallow"unlessrespect_robots=falseis explicitly passed.
MISSING / INSUFFICIENT DATA
- IF
rendered_htmlequalsraw_htmlbyte-for-byte THENseverity="none",note="no client-side rendering detected". - Never infer indexation status; only report the observable render delta.
OUTPUT
One JSON object per references/output.schema.json. No prose.
FILES
scripts/render_audit.js— Puppeteer reference implementation (raw vs rendered diff).references/output.schema.json— output contract.
