OG Images the Hard Way (And Other Options)
Open Graph images act as the visual handshake for every URL on the site. Rather than rely on manually curated screenshots or hosted rendering services, this project generates images alongside the rest of the static build. The stack is modest—Astro API routes, Satori for SVG generation, and Resvg for rasterization—but each layer is wired carefully so that every page receives a predictable, cacheable PNG even as the content changes. The following sections document the routing, templating, rendering, and caching strategies that keep the system deterministic.
The Open Graph protocol and social sharing
Facebook introduced the Open Graph protocol in 2010 as a metadata standard enabling web pages to appear as rich link previews when shared on social platforms.1 The protocol defines how platforms should crawl and display shared content, transforming plain URLs into visual cards with titles, descriptions, and images. Four properties form the required minimum: og:title, og:type, og:url, and og:image. Without these tags, platforms fall back to heuristics—scraping the first image on the page or truncating arbitrary text—producing inconsistent and often misleading previews.2
The og:image property holds particular weight in social contexts because visual content drives engagement. Studies consistently show that posts with images receive significantly higher click-through rates than text-only links.2 However, quality matters: images should measure at least 1200×630 pixels to retain clarity across devices and platforms, with file sizes under 8 MB to ensure fast loading.3 These constraints shape the technical requirements for any automated generation system.
Best practices recommend creating custom OG images for every shareable page rather than reusing a generic site logo.3 Custom images convey specific information about the content—a blog post title, a product name, key statistics—making the preview more informative and increasing the likelihood that users will click through. This principle motivates build-time generation: every post deserves a tailored visual representation that reflects its actual content.
Routing model and data sources
Astro treats any *.png.ts endpoint under src/pages as an executable route, which means OG assets can be produced with the same primitives as JSON APIs. Blog posts feed into src/pages/og/[...slug].png.ts, which first calls getCollection("blog") to enumerate every published slug and returns a params/props tuple for each entry. Astro then prerenders a static path at /og/<slug>.png during the production build. Because the route relies exclusively on content collections, it automatically inherits the validation guarantees in src/content/config.ts and never needs to hit external services at runtime.
Non-blog pages use src/pages/og/page/[id].png.ts, a sibling endpoint that currently exposes a single home identifier but can accommodate additional IDs such as about or projects without structural changes. Each entry defines title, description, and pageUrl props explicitly rather than pulling from collections. This distinction keeps the implementation flexible: routable pages can still have tailored OG art even if they live outside the content collections (for example, dynamic tag archives).
Both routes share design constants such as background colors, border styles, and typography. They also load the same JetBrains Mono font files from the /fonts directory via readFile and resolve, which guarantees typographic parity between the OG art and the in-browser experience.
Template composition with Satori
Satori is Vercel’s “enlightened library to convert HTML and CSS to SVG,” accepting JSX elements and returning SVG strings.4 Unlike browser-based rendering engines, Satori operates in pure JavaScript environments—Node.js, edge functions, or static build pipelines—without requiring a headless browser. This makes it dramatically faster and more resource-efficient than Puppeteer-based approaches, which must spawn entire Chromium instances to capture screenshots.5
The library accepts only pure, stateless JSX elements, with no support for React hooks or lifecycle methods.4 This constraint aligns perfectly with static site generation: OG images don’t need interactivity or dynamic updates, just a declarative description of the visual layout. Satori uses the same Flexbox layout engine as React Native, meaning developers familiar with web layout patterns can compose complex designs without learning new positioning systems.4 However, Satori intentionally implements only a subset of CSS—enough for typical card layouts but not a complete specification.
Each OG route constructs an element tree manually to retain full control over spacing and layering. The structure is intentionally verbose:
- A root
divestablishes the dimensions (1200×630), flex layout, and background color that echoes the site’s base theme. - A radial gradient overlay adds depth without relying on external assets.
- A window-like container recreates the macOS traffic lights, reinforcing the “terminal” motif present throughout the UI.
- Body sections render metadata: the faux command prompt string (
$ cat ~/blog/<date>.md), the title, and the description. The template clamps each text block to prevent overflow and ensures multi-line text stays legible. - The footer displays both the canonical URL for the page and a static domain string for branding consistency.
Satori’s inline style objects make the layout deterministic. There are no external CSS files to import, so the OG build step cannot break due to missing class names or Tailwind purges. The colors and typographic sizes directly mirror the site’s Tailwind theme, which keeps the brand consistent even if the theme evolves. When that happens, updating the constants inside these templates is sufficient.
Font handling requires attention. Satori supports TTF, OTF, and WOFF formats but explicitly excludes WOFF2, necessitating font file conversions for projects using modern web fonts.4 The templates load JetBrains Mono from the local /fonts directory via Node’s readFile and resolve APIs, guaranteeing typographic parity with the live site. This local loading also avoids network requests during builds, keeping the process fast and reproducible.
Rasterization with Resvg
Satori outputs SVG, but most social platforms expect PNG assets. Resvg (via @resvg/resvg-js) bridges that gap by rendering the SVG buffer into a PNG byte array with predictable results. The library originated as a Rust project addressing the complexity inherent in SVG’s nearly 900-page specification, which effectively requires a full web browser to interpret completely.6 Resvg deliberately targets only the static SVG subset, excluding animations, scripting, and interactive elements, making it substantially lighter than browser-based renderers while maintaining high fidelity for typical graphics.6
A key differentiator is reproducibility. Because Resvg compiles to WebAssembly and avoids system-level graphics libraries, it produces identical output across platforms: rendering an SVG on x86 Windows yields pixel-perfect matches to ARM macOS or Linux environments.6 This property matters for build systems that may run on different CI infrastructure or developer machines. With browser-based renderers like Puppeteer, subtle differences in font rendering or anti-aliasing can produce inconsistent results, complicating visual regression testing.
Resvg’s test suite includes approximately 1,600 SVG-to-PNG regression tests, providing confidence that updates to the library won’t introduce unexpected rendering changes.6 The CLI application weighs less than 3 MB and carries zero external dependencies, contrasting sharply with Puppeteer installations that download hundreds of megabytes of Chromium binaries.6 For static site builds, this efficiency translates to faster CI runs and lower storage costs.
Because Resvg is deterministic, re-running the build with identical props yields identical PNGs. The final Response object sets the Content-Type header to image/png and applies Cache-Control: public, max-age=31536000, immutable, enabling CDN and browser caches to treat the files as versioned artifacts. Any time the underlying template or content changes, Astro’s build emits a new file, and the updated deployment invalidates the old cache entry by virtue of the new hash.
Integration with page templates
Pages and posts opt into OG coverage through the ogImage prop exposed by src/layouts/Base.astro. Blog posts compute the prop per slug (const ogImage =/og/${post.slug}.png;) and pass it to the layout, which then resolves the relative path against Astro.site before writing <meta property="og:image"> and <meta name="twitter:image"> tags. Non-blog pages point to /og/page/home.png (or future IDs). This centralization ensures that every route emits the correct metadata without scattering <meta> tags across templates.
Because the OG files reside under /og/, they are accessible both to crawlers and to humans curious about the share cards. Additionally, keeping the URLs stable allows analytics tools to attribute engagement accurately when platforms prefetch these assets.
Build-time determinism
The OG pipeline runs during the same astro build invocation that produces HTML, so there is no divergence between the markup and its associated share card. When a post is edited, the blog page and the OG image both rebuild from source, ensuring that the title, description, and date displayed inside the PNG stay in sync with the textual content. This property is especially important when scheduling posts: the OG asset reflects the final metadata on launch day without manual effort.
In environments where builds run in parallel (for example, CI pipelines), the approach remains deterministic because each OG route reads only local files and does not depend on environment variables beyond Astro.site for canonical URLs. Reproducibility simplifies debugging; if a visual glitch ever surfaces in a share preview, reproducing it locally is as simple as running astro build and opening the emitted PNG.
Performance characteristics
Generating one PNG per blog post adds a constant amount of work to the build. Resvg processes each SVG quickly because the scenes are simple (no complex gradients or filters), and the absence of remote asset loading eliminates network latency. On the client side, there is no runtime cost: OG images behave like static files served from the CDN, and the Base layout simply references them via metadata tags. This division of labor underscores that OG generation is part of the static build rather than an edge function, ensuring images are available immediately upon deployment.
Maintenance and extensibility
Each OG template isolates its styling inside a single function, so evolving the visual system means touching one file per template. For example, introducing a dark/light variant could involve passing a theme prop from the content collection into the OG route and swapping colors accordingly. Similarly, adding per-post background art would involve referencing new props in the Satori tree, provided the assets exist locally or can be derived procedurally.
If the site ever adds translation support or alternate titles, the OG routes already accept title and description props from getStaticPaths, making it straightforward to inject localized strings. Because Satori renders UTF-8 by default and JetBrains Mono supports a wide range of glyphs, no additional font work is necessary unless the language requires specialized typefaces.
Comparison with hosted solutions
Hosted renderers such as Vercel OG, Cloudinary, or Bannerbear provide dynamic templates accessible via query parameters. They remove the need to manage fonts or rendering code but introduce runtime dependencies, potential cold starts, and vendor-specific rate limits.7 Maintaining the pipeline in-repo ensures full control over release cadence, styling, and caching. The trade-off is that developers must edit TypeScript and manage fonts manually, but the benefit is independence from third-party outages.
Hosted API services
Services like Bannerbear, Placid, and Robolly excel at bulk image generation, offering design templates in responsive web-based environments accessible through REST APIs or no-code integrations like Zapier.7 Bannerbear’s pricing starts at $49/month for 1,000 API credits, while Placid offers plans from $19/month for 500 renders up to $189/month for power users. For teams generating thousands of OG images monthly across multiple projects, centralized services can simplify billing and infrastructure management.
However, per-image costs accumulate. A blog publishing 50 posts monthly would consume 600 API credits annually just for initial generation, not counting regenerations triggered by content updates or design tweaks. Over three years, hosted service costs could exceed $2,000, whereas self-hosted solutions incur only the marginal compute costs of longer build times—typically seconds per image on modest CI runners.
Hosted services also introduce latency and reliability concerns. Dynamic OG generation typically runs at the edge or via serverless functions, meaning the first request to a new URL may experience cold starts of several seconds while the service spins up and renders the image.7 Subsequent requests benefit from caching, but cache evictions or TTL expirations force users to wait again. Self-hosted static generation eliminates this variability: every image pre-exists before deployment, guaranteeing instant availability.
Self-hosted with Satori
Satori positions itself as “fast, flexible, and serverless-friendly,” converting HTML/CSS to SVG without external dependencies.5 The library works anywhere JavaScript runs—Vercel Edge Functions, AWS Lambda, Cloudflare Workers, or local Node.js processes. Almost every recent tutorial on OG image generation defaults to Satori because it balances ease of use with performance, though it does create some coupling to Vercel’s ecosystem if using @vercel/og directly.5
The primary advantage is cost: after initial implementation, additional images cost nothing beyond incremental build time. Static site generators like Astro can hook into the build process to generate OG images and emit them to the dist folder, where they’re served as plain files.5 For projects already using SSG, this approach imposes negligible overhead compared to serverless invocations on every page load.
Developer experience differs markedly. Hosted services provide visual template builders where designers can drag elements and preview results instantly, then generate images by passing variables through API calls. Self-hosted solutions require writing JSX or HTML templates in code editors, testing by running local builds, and debugging layout issues through trial and error.7 Teams with strong front-end skills may prefer the precision and version control benefits of code-based templates, while marketing teams might favor hosted visual editors.
Hybrid approaches
Some projects combine strategies: use Satori for common page types with predictable layouts (blog posts, product pages) and rely on hosted services for user-generated content or complex designs requiring frequent iteration. This maximizes control over core content while leveraging external services for edge cases. The key is maintaining consistent metadata handling so the og:image tags point to the correct source regardless of generation method.
Alternative approaches
Although the current pipeline is code-driven, two alternatives remain viable:
- Headless-browser screenshots: Tools like Playwright can render a hidden page at build time and capture a screenshot. This approach excels when the desired design already exists as live HTML, but it increases resource consumption and introduces complexity around viewport management.
- Static asset libraries: For evergreen pages, handcrafted PNGs stored in
public/og/eliminate computation entirely. This works well for legal disclosures or docs that rarely change, and the rest of the system can reference them via frontmatter (ogImage: "/og/page/home.png").
Both options still benefit from the Base layout’s consistent metadata handling, reinforcing the idea that OG integration is first-class regardless of how the art is produced.
Quality assurance
Testing OG outputs involves two steps: verifying that Astro generated the expected files and confirming that social platforms render them correctly. Locally, inspecting dist/og/*.png ensures the build produced assets for every slug. For external validation, Facebook’s Sharing Debugger and Twitter’s Card Validator can fetch the URLs and display the parsed metadata. Because the assets are static, these tools should always see the latest deploy without further configuration.
Automated tests can reinforce coverage by asserting that getStaticPaths in src/pages/og/[...slug].png.ts returns as many entries as there are published blog posts, preventing missing OG cards when content is added. Linters can also flag missing ogImage props in page templates, though in practice the centralized Base layout already handles the default for non-blog routes.
Conclusion
A custom OG pipeline might appear overbuilt at first glance, but it guarantees that every link preview reflects the same care invested in the site itself. By rendering images during the static build, using Satori for declarative templates, and rasterizing with Resvg, the system remains predictable, debuggable, and easy to evolve. For teams that prioritize brand coherence, owning this stack is often simpler—and ultimately faster—than delegating the responsibility to third-party services.
Footnotes
-
“Open Graph Protocol - What is it and how does it work?” — getstream.io/glossary ↩
-
“Open Graph Meta Tags: Everything You Need to Know” — ahrefs.com/blog ↩ ↩2
-
“How to Use Open Graph Tags to Boost Social Engagement” — semrush.com/blog ↩ ↩2
-
GitHub - vercel/satori — github.com/vercel/satori ↩ ↩2 ↩3 ↩4
-
“Dynamic Open Graph Images with Satori and Astro” — knaap.dev/posts ↩ ↩2 ↩3 ↩4
-
GitHub - linebender/resvg: An SVG rendering library — github.com/linebender/resvg ↩ ↩2 ↩3 ↩4 ↩5
-
“What is og:Image? How to Automatically Generate OG Image?” — popupsmart.com/blog ↩ ↩2 ↩3 ↩4