Capture a website screenshot with its title, favicon, and Open Graph metadata

Get a website screenshot, page title, favicon, and Open Graph tags in one ScreenshotOne request. Save the image and handle missing metadata safely in Node.js.

Blog post5 min read

Written by

Dmytro Krasun

Updated on

If you’re building a directory, a bookmarks app, or a research dashboard, a screenshot alone doesn’t make a good card. People recognize sites by their name and icon, and a one-line description helps.

ScreenshotOne can return that metadata together with the screenshot, so you don’t need a second scraper. Ask for a JSON response and turn on the fields you want:

Request optionYou get
response_type=jsonJSON with a screenshot_url plus whatever metadata you asked for
metadata_page_title=truemetadata.page_title
metadata_icon=truemetadata.icon
metadata_open_graph=truemetadata.open_graph

They’re all off by default. Details are in the metadata reference.

Build a card with Node.js

You’ll need Node.js 22+ and your access key in SCREENSHOTONE_ACCESS_KEY. Save this as website-card.mjs:

import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
const accessKey = process.env.SCREENSHOTONE_ACCESS_KEY;
const [inputUrl, outputDir = "website-card"] = process.argv.slice(2);
if (!accessKey || !inputUrl) {
throw new Error(
"Set SCREENSHOTONE_ACCESS_KEY, then run: node website-card.mjs URL [directory]"
);
}
const target = new URL(inputUrl);
const text = (value) => (typeof value === "string" ? value.trim() : "");
// Keep only http(s) URLs. This drops data: URIs, which the icon field can contain.
function webUrl(value) {
if (!text(value)) return null;
try {
const url = new URL(value, target);
return ["http:", "https:"].includes(url.protocol) ? url.href : null;
} catch {
return null;
}
}
const response = await fetch("https://api.screenshotone.com/take", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Access-Key": accessKey,
},
body: JSON.stringify({
url: target.href,
format: "png",
viewport_width: 1280,
viewport_height: 800,
response_type: "json",
metadata_page_title: true,
metadata_icon: true,
metadata_open_graph: true,
}),
signal: AbortSignal.timeout(120_000),
});
if (!response.ok) {
throw new Error(`Capture failed: HTTP ${response.status}`);
}
const result = await response.json();
if (typeof result.screenshot_url !== "string" || !result.screenshot_url.trim()) {
throw new Error("The response didn't include a screenshot URL.");
}
// The screenshot URL is temporary, so download the image right away.
const imageResponse = await fetch(result.screenshot_url, {
signal: AbortSignal.timeout(60_000),
});
if (!imageResponse.ok) {
throw new Error(`Image download failed: HTTP ${imageResponse.status}`);
}
const image = Buffer.from(await imageResponse.arrayBuffer());
const pngHeader = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
if (!image.subarray(0, 8).equals(pngHeader)) {
throw new Error("The downloaded file wasn't a PNG image.");
}
const metadata = result.metadata ?? {};
const og = metadata.open_graph ?? {};
const card = {
url: target.href,
title: text(og.title) || text(metadata.page_title) || target.hostname,
pageTitle: text(metadata.page_title) || null,
description: text(og.description),
faviconUrl: webUrl(metadata.icon?.url),
openGraphImageUrl: webUrl(og.image),
screenshotFile: "screenshot.png",
capturedAt: new Date().toISOString(),
};
await mkdir(outputDir, { recursive: true });
await writeFile(join(outputDir, "screenshot.png"), image);
await writeFile(join(outputDir, "card.json"), JSON.stringify(card, null, 2));
console.log(`Saved screenshot.png and card.json to ${outputDir}/`);

Run it:

Terminal window
node website-card.mjs 'https://example.com/' example-card

You end up with a folder containing screenshot.png and card.json, ready to import into your app. It captures the viewport rather than the full page because cards have a fixed image area anyway. Add full_page: true if you need the whole page.

A screenshot isn’t an Open Graph image

They’re easy to mix up, but they show different things:

  • The Open Graph image is chosen by the site owner. It could be a logo, an illustration, a product photo, or a generated social card.
  • The screenshot shows what the page actually looks like.

For a directory of websites, the screenshot is usually what people want to see. For a saved article, the author’s OG image is often the nicer thumbnail. The script keeps both, so you can decide per card or let users switch.

If you want to generate Open Graph images, that’s a different job. See the Open Graph images guide. Also, the metadata you extract won’t necessarily match what X or LinkedIn show, because each platform runs its own crawler and cache.

Missing metadata

Many sites have incomplete metadata, so the script falls back step by step: OG title, then the HTML <title>, then the hostname. Here are the fallbacks I’d use:

MissingFall back to
OG titleThe HTML page title
Both titlesThe hostname
DescriptionNothing. Leave it empty rather than making something up
FaviconA generic icon, or the site’s initials
OG imageYour screenshot

The webUrl helper keeps only http and https URLs. The API sometimes returns the icon as a data: URI, and the helper drops those. If you’d rather keep them, handle them separately.

Treat metadata as untrusted input

Whoever runs the site wrote the title, the description, and the image URLs, so treat them like any user input:

  • Render titles and descriptions as text, never as HTML.
  • If your server downloads icons or OG images, put that behind a proper image proxy that checks redirects, destination IPs, content type, and size. Otherwise a malicious og:image can point your server at its own internal network (SSRF).

screenshot_url is temporary: it lasts up to four hours unless you’ve set a longer cache TTL. That’s why the script downloads the image immediately instead of saving the link.

In a real app, upload the image to your own storage and keep the storage key next to the card data. If you want to keep the OG image as well, copy it too, assuming you have the right to store it.

If you’d rather stream the image directly and still get the metadata, the same fields come back as response headers, such as X-ScreenshotOne-Page-Title. I find JSON easier when I’m combining several fields into one record.

When a card looks wrong

To inspect the metadata returned in a page’s initial HTML, try the free link-preview checker. It shows social-card previews and image checks. Unlike the screenshot API used in this guide, the checker does not execute JavaScript.

The title says “Sign in” or “Access denied.” Look at the screenshot. It probably shows the same login page, because the renderer wasn’t logged in. The data matches what the renderer saw.

The metadata doesn’t match the page. Some sites set their title and OG tags with JavaScript after the page loads. Compare the returned values with the rendered page, not the raw HTML source.

The image is broken. Open the image URL directly. A longer delay won’t fix a 404 or a hotlink-protected image.

The card is outdated. Check your own stored copy and any API caching you’ve turned on before requesting a fresh capture.

If you need the page’s actual content and not just a card, you can also request rendered HTML or Markdown. For a card, I’d leave that off and keep the requests small.

Read more Screenshot rendering

Interviews, tips, guides, industry best practices, and news.

View all posts
How to screenshot websites in Next.js

How to screenshot websites in Next.js

There 3 simple ways to render website screenshots in Next.js—using Puppeteer, Cloudflare Browser Rendering, and a screenshot API like ScreenshotOne or similar.

Read more

Automate website screenshots

Exhaustive documentation, ready SDKs, no-code tools, and other automation to help you render website screenshots and outsource all the boring work related to that to us.