finephrase / app /scripts /screenshot.mjs
joelniklaus's picture
joelniklaus HF Staff
added skill to take screenshots for better debugging with agents
17d0a5b
#!/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 <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);
});