Designing Screenshot API Presets: Sizes and Defaults
Why our presets (og_card, twitter_card, link_preview) use the sizes and defaults they do, how they merge with your parameters, and measured file sizes.
A preset is one parameter that replaces five. preset=og_card sets a 1200x630 viewport, PNG output and a 24-hour cache in a single word, and you can still override any of them. This post explains how we chose the presets in RenderScreenshot, how a preset merges with the parameters you send, one merge rule that surprises people, and what the default format costs in bytes. It is part of how we built RenderScreenshot.
Last checked: 2026-10-15. Preset values come from our presets reference and the
Presetmodel in the API. File sizes were measured with local Chromium (Chrome headless shell 148.0.7778.97, Puppeteer 24.43.1, Node 22.18.0) on an Apple M1 Pro, not with the API. Facebook's image guidance was read on its developer site on the same date.
What a preset is, and what it is not
The API has a dozen parameter groups: viewport, capture, output, wait, cache and more. Most requests for a given job use the same handful of values. A social card is always the same size. A full-page capture always wants full-page mode. A preset is a named bundle of those values.
We set three rules for presets early:
- A preset only sets defaults. Anything you send explicitly wins. A preset never blocks a parameter.
- A preset names an output, not a customer.
og_cardis "an image forog:image", not "the settings for blogs". That keeps the list short and stable. - The values are public and machine-readable.
GET /v1/presetsreturns each preset's config, and the presets reference lists the same values. Nothing about a preset is hidden.
The full list today:
| Preset | Canvas | Ratio | Format | Other defaults | Cache TTL |
|---|---|---|---|---|---|
og_card |
1200x630 | 1.91:1 | PNG | 24 h | |
twitter_card |
1200x600 | 2:1 | PNG | 24 h | |
twitter_card_large |
1200x628 | 1.91:1 | PNG | 24 h | |
link_preview |
800x600 | 4:3 | PNG | 24 h | |
full_page |
1280x800 | PNG | capture.mode: full_page with scroll settings |
1 h | |
mobile |
390x844 | PNG | scale 3, mobile and touch emulation | 24 h | |
mobile_landscape |
844x390 | PNG | scale 3, mobile, touch, landscape | 24 h | |
desktop_hd |
1920x1080 | 16:9 | PNG | 24 h | |
desktop_4k |
3840x2160 | 16:9 | PNG | 24 h | |
pdf_document |
Letter paper | background graphics, 1 cm margins | 24 h |
Why these sizes
og_card: 1200x630
This is the one preset we did not have to think hard about. Facebook's image guidance for shared links asks for images of at least 1200x630 for high-resolution displays and an aspect ratio as close to 1.91:1 as possible, with a minimum of 200x200 and an 8 MB file limit (checked 2026-10-15). LinkedIn, Slack, Discord and most link unfurlers accept the same Open Graph image, so one 1200x630 card covers most platforms. That is why og_card is the default recommendation in our social card guide.
We render at exactly 1200x630 rather than at a larger size. A crawler downloads the image on first share, often on a short timeout, and a bigger canvas only adds bytes that the platform scales away.
twittercard and twittercard_large
These two are close relatives of og_card. twitter_card is a 2:1 canvas (1200x600). twitter_card_large is 1200x628, a 1.91:1 canvas two pixels shorter than og_card. X reads twitter:image and falls back to og:image, and it crops to its own layout, so the question is which ratio you design for. If you design one card for every platform, use og_card and keep important content away from the edges. If you design a card only for X, check X's current card documentation for the ratio of the card type you use. We could not load X's developer documentation on 2026-10-15, so we do not quote its limits here.
link_preview: 800x600
Chat apps and dashboards often show a small thumbnail of a page, not a wide banner. A 4:3 window at 800 px wide shows the top of a real page at a readable layout width, and the image is small enough to store many of them. Use it for "preview of this link" thumbnails, not for og:image.
full_page: a shorter cache
full_page is the only preset with a 1-hour cache instead of 24 hours. A full-page capture is the whole document, so any change anywhere on the page makes it stale, and full-page captures are often used for monitoring and archiving, where a fresh copy matters more than a cache hit. A social card shows the top of a page or a template, which changes less often.
Device presets
mobile and mobile_landscape match the iPhone 14 Pro viewport (390x844) at a scale factor of 3, with mobile and touch emulation. desktop_hd and desktop_4k are the two common desktop resolutions. For other phones and tablets, use viewport.device with an id from GET /v1/devices instead of a preset.
How a preset merges with your parameters
The merge is the part of preset design that matters most in practice, so here is the actual code from the API:
# app/controllers/api/v1/screenshots_controller.rb (excerpt) def deep_merge_params(preset_config, explicit_params) result = explicit_params.dup preset_config.each do |key, preset_value| if explicit_params[key].is_a?(Hash) && preset_value.is_a?(Hash) # For nested hashes, merge recursively with explicit values taking precedence result[key] = preset_value.merge(explicit_params[key]) elsif explicit_params[key].nil? # Use preset value if no explicit value provided result[key] = preset_value end # Otherwise keep explicit value (already in result from dup) end result end
Three consequences:
- Explicit values win at the group level.
preset: og_cardwithoutput: { format: "webp" }keeps the 1200x630 viewport and the 24-hour cache, and changes only the format. - Groups you do not send come from the preset. If you send no
cachegroup, you get the preset's TTL. - The merge is one level deep. Inside a group, your keys replace the preset's keys with the same name. A nested object inside a group is replaced as a whole, not merged. For example, with
preset: full_page, sendingcapture: { scroll: { delay: 800 } }replaces the preset's wholescrollobject, so the preset'ssimulate: trueis gone. If you override a nested object, send all of it.
The batch endpoint applies presets the same way, per item, so a batch can mix og_card and full_page items. An unknown name fails fast with invalid_preset before any render, so a typo never costs a credit.
We recommend not combining a preset with viewport.device in one request. Both set viewport values, and a request is easier to read when one of them owns the viewport.
Overriding a preset
The same override in a GET request (flat parameters) and a POST request (nested JSON):
curl -sS --fail -o card.webp \ -H "Authorization: Bearer $RS_API_KEY" \ "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com&preset=og_card&format=webp&quality=85"
curl -sS --fail -o card.webp -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com", "preset": "og_card", "output": {"format": "webp", "quality": 85}}'
With the Node.js SDK:
import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); const image = await client.take( TakeOptions.url('https://example.com').preset('og_card').format('webp').quality(85), ); await writeFile('card.webp', image);
The SDK methods (preset, format, quality) are listed in the Node.js SDK reference. The other SDKs use the same TakeOptions builder pattern; see the SDK overview.
What the PNG default costs
We kept PNG as the default for every image preset. PNG is lossless, supports transparency, and is accepted by every crawler and chat app we know of. For an image in a meta tag that a third party fetches, compatibility is the right default.
But PNG is expensive for the images people usually put on cards: gradients, photos and large color fields. We rendered one card design (a gradient background with a title and a URL line, laid out in viewport units so it fills each canvas) at each social preset size, in three formats:
| Preset | Canvas | PNG | JPEG (q80) | WebP (q80) |
|---|---|---|---|---|
og_card |
1200x630 | 335 KB | 31 KB | 18 KB |
twitter_card |
1200x600 | 305 KB | 31 KB | 17 KB |
twitter_card_large |
1200x628 | 335 KB | 31 KB | 18 KB |
link_preview |
800x600 | 200 KB | 20 KB | 12 KB |
The PNG is about 18 times the size of the WebP for this design. A text-heavy page shows a smaller gap: in the same session, a plain documentation page at 1200x630 was 92 KB as PNG and 44 KB as WebP. So:
- Keep PNG when the image has transparency, sharp UI text you will zoom into, or a consumer whose format support you do not know.
- Use WebP for cards with gradients or photos when the consumers accept it. All current browsers do; for social platforms, test the ones you care about with their own preview tools before you switch.
- Use JPEG as the safe smaller option for photo-heavy cards.
This is the measurement script. It renders locally with Puppeteer, not through the API, so it isolates the format cost from network time:
// preset_sizes.mjs: render one responsive card at each social preset size; report bytes. // Node 22, [email protected]. Local Chromium, not the RenderScreenshot API. import puppeteer from 'puppeteer'; const PRESETS = { og_card: [1200, 630], twitter_card: [1200, 600], twitter_card_large: [1200, 628], link_preview: [800, 600], }; const html = `<!doctype html><meta charset="utf-8"><style> html,body{margin:0;height:100%} body{display:flex;flex-direction:column;justify-content:center;padding:0 6vw;box-sizing:border-box; background:linear-gradient(135deg,#0f172a,#1e3a8a 60%,#0ea5e9);color:#f8fafc;font-family:Helvetica,Arial,sans-serif} h1{font-size:5.6vw;line-height:1.1;margin:0 0 3vh}p{font-size:2.5vw;margin:0;opacity:.85} </style><h1>Designing screenshot presets</h1><p>renderscreenshot.com/blog</p>`; const browser = await puppeteer.launch({ headless: 'shell' }); try { const rows = []; for (const [name, [width, height]] of Object.entries(PRESETS)) { const page = await browser.newPage(); await page.setViewport({ width, height }); await page.setContent(html); const row = { preset: name, size: `${width}x${height}`, ratio: (width / height).toFixed(3) }; for (const [type, quality] of [['png'], ['jpeg', 80], ['webp', 80]]) { const buf = await page.screenshot({ type, ...(quality ? { quality } : {}) }); row[`${type}_kb`] = Math.round(buf.length / 1024); } rows.push(row); await page.close(); } console.table(rows); } catch (err) { console.error(err); process.exitCode = 1; } finally { await browser.close(); }
The bytes also matter for storage and delivery cost, which we model in R2 vs S3 for screenshot storage.
Design your template for the preset
A preset fixes the canvas. Your HTML template has to fill it:
- Set the body to the canvas size (
width:1200px;height:630px) or use viewport units, as the script above does, so one template can serve several presets. - Set a background on
body. Without one, the page canvas follows the browser's color scheme. - Embed or wait for your fonts. A card rendered before its web font arrives is a card in a fallback font. We measured how large that difference is in consistent font rendering.
- Escape user input that goes into the template, such as a post title. A title like
<script>must become text, not markup.
How we decide on a new preset
We get requests for more presets (Pinterest pins, Instagram squares, YouTube thumbnails). The test we apply:
- Is it a distinct output that many requests share, with a size documented by the platform that consumes it?
- Does it need more than a viewport size? If it is only a size,
viewport.widthandviewport.heightalready do the job, and a new preset adds a name to learn and nothing else. - Can we keep it stable? A preset's values are part of the API contract. Changing
og_cardfrom PNG to WebP would break consumers that assume PNG, so a change like that would be a new preset, not an edit.
If a preset you want passes those tests, tell us.
Try it
Create a free account and request https://example.com with preset=og_card, then the same with format=webp, and compare the Content-Length of the two responses. To put the card in a meta tag without exposing your key, use a signed URL.
Related in this topic
- How We Built a Screenshot API: Rails, Workers and R2 The architecture of RenderScreenshot: a Rails 8 API, a Cloudflare Worker that drives the browser, R2 storage, and the bugs and fixes that shaped it.
- Cloudflare Browser Rendering API: A Tested Tutorial (2026) Screenshots with Cloudflare Browser Rendering (now Browser Run): REST Quick Actions, the Workers binding, session reuse, limits and cost. Tested code.
- Screenshot API vs Self-Hosted Puppeteer: Real Cost A build-vs-buy cost model for screenshots: servers, Cloudflare browser time, engineering hours and API plans, with a calculator and a capacity script.
- Which Websites Block Headless Chrome? A 200-Site Study We loaded the top 200 Tranco domains in three headless Chrome profiles. 15% of sites blocked or challenged default Puppeteer. Raw data and script.
- How Long Does a Screenshot Take? Latency, Phase by Phase Where the seconds go in a web page screenshot: launch, DNS, TLS, load, wait, capture, encode and upload. A script that times each phase and a method to measure.
- Blocking Cookie Banners Automatically: How It Fails How our block.cookie_banners option clicks or hides consent banners in headless Chrome, the code behind it, and eight fixtures that show where it breaks.