#!/usr/bin/env node /** * General-purpose screenshot tool for the blog article. * * Takes a screenshot of any section or element in the rendered blog post. * Useful for agent debugging: visually verify how content renders. * * Usage: * node scripts/screenshot.mjs [options] * * Options: * --target CSS selector or #anchor to screenshot (default: viewport) * --output Output file path (default: ../../assets/screenshot.png) * --width Viewport width (default: 1400) * --height Viewport height (default: 900) * --full-page Capture the full scrollable page * --padding Extra padding around the target element (default: 40) * --url Page URL (default: http://localhost:4321/) * --wait Extra wait time after navigation in ms (default: 2000) * --dark Use dark theme * * Examples: * # Screenshot the full viewport (above the fold) * node scripts/screenshot.mjs * * # Screenshot a specific section by heading anchor * node scripts/screenshot.mjs --target "#experiments" * * # Screenshot a specific figure by its id * node scripts/screenshot.mjs --target "#baselines-comparison" * * # Screenshot a CSS selector with extra padding * node scripts/screenshot.mjs --target ".mermaid" --padding 80 * * # Full-page screenshot in dark mode * node scripts/screenshot.mjs --full-page --dark * * # Custom output path * node scripts/screenshot.mjs --target "#infrastructure" --output ./infra-shot.png */ import { chromium } from 'playwright'; import { fileURLToPath } from 'url'; import { dirname, join, isAbsolute } from 'path'; import { parseArgs } from 'util'; const __dirname = dirname(fileURLToPath(import.meta.url)); const { values: args } = parseArgs({ options: { target: { type: 'string', default: '' }, output: { type: 'string', default: '' }, width: { type: 'string', default: '1400' }, height: { type: 'string', default: '900' }, 'full-page': { type: 'boolean', default: false }, padding: { type: 'string', default: '40' }, url: { type: 'string', default: 'http://localhost:4321/' }, wait: { type: 'string', default: '2000' }, dark: { type: 'boolean', default: false }, }, }); const target = args.target; const width = parseInt(args.width, 10); const height = parseInt(args.height, 10); const fullPage = args['full-page']; const padding = parseInt(args.padding, 10); const url = args.url; const waitMs = parseInt(args.wait, 10); const dark = args.dark; // Build output path: default uses target name or "viewport" const defaultName = target ? `screenshot-${target.replace(/^#/, '').replace(/[^a-zA-Z0-9_-]/g, '_')}.png` : 'screenshot.png'; const output = args.output ? (isAbsolute(args.output) ? args.output : join(process.cwd(), args.output)) : join(__dirname, '..', '..', 'assets', defaultName); async function main() { const browser = await chromium.launch(); const page = await browser.newPage(); await page.setViewportSize({ width, height }); // Navigate await page.goto(url, { waitUntil: 'domcontentloaded' }); // Apply dark theme if requested if (dark) { await page.evaluate(() => { document.documentElement.setAttribute('data-theme', 'dark'); }); } // Wait for async content (mermaid, embeds) to render await page.waitForTimeout(waitMs); if (fullPage) { await page.screenshot({ path: output, fullPage: true }); console.log(`Full-page screenshot saved to: ${output}`); await browser.close(); return; } if (!target) { // No target: screenshot the current viewport await page.screenshot({ path: output, fullPage: false }); console.log(`Viewport screenshot saved to: ${output}`); await browser.close(); return; } // Resolve target: if it starts with #, try clicking a TOC link first for smooth scroll if (target.startsWith('#')) { const tocLink = page.locator(`a[href="${target}"]`).first(); if (await tocLink.count() > 0) { await tocLink.click(); await page.waitForTimeout(500); } else { // Fallback: scroll the element into view directly await page.evaluate((sel) => { const el = document.querySelector(sel); el?.scrollIntoView({ behavior: 'instant', block: 'start' }); }, target); await page.waitForTimeout(300); } } // Find the target element const locator = page.locator(target).first(); const box = await locator.boundingBox(); if (box) { const clip = { x: Math.max(0, box.x - padding), y: Math.max(0, box.y - padding), width: Math.min(width, box.width + padding * 2), height: box.height + padding * 2, }; await page.screenshot({ path: output, clip, fullPage: false }); console.log(`Screenshot of "${target}" saved to: ${output}`); } else { console.warn(`Target "${target}" not found, falling back to viewport screenshot.`); await page.screenshot({ path: output, fullPage: false }); console.log(`Viewport screenshot saved to: ${output}`); } await browser.close(); } main().catch((err) => { console.error(err); process.exit(1); });