Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| /** | |
| * 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 <selector> CSS selector or #anchor to screenshot (default: viewport) | |
| * --output <path> Output file path (default: ../../assets/screenshot.png) | |
| * --width <px> Viewport width (default: 1400) | |
| * --height <px> Viewport height (default: 900) | |
| * --full-page Capture the full scrollable page | |
| * --padding <px> Extra padding around the target element (default: 40) | |
| * --url <url> Page URL (default: http://localhost:4321/) | |
| * --wait <ms> 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); | |
| }); | |