How Open Graph Images Are Dynamically Generated Using Satori for Roadmap Previews
The developer-roadmap repository uses Satori to convert Tailwind-styled HTML templates into SVGs, then rasterizes them to PNGs for dynamic Open Graph previews without requiring a headless browser.
The kamranahmedse/developer-roadmap project generates unique social sharing previews for every roadmap, best-practice guide, and article. Instead of maintaining thousands of static images, the codebase dynamically generates Open Graph images using Satori, a library that renders HTML-like components directly to SVG. This approach produces deterministic, high-quality previews during the build process while keeping the generation pipeline lightweight and fully server-side.
The Satori-Based OG Image Pipeline
The generation logic lives in scripts/generate-og-images.mjs, which orchestrates a four-step transformation from markdown metadata to final PNG assets.
Step 1: Extracting Metadata with gray-matter
The script first collects content metadata using the getAllRoadmaps function. It reads every markdown file in the repository, parsing front-matter fields—such as title, description, and optional image—with the gray-matter library.
// From scripts/generate-og-images.mjs
async function getAllRoadmaps() {
const roadmaps = await fs.readdir('./src/data/roadmaps');
return Promise.all(roadmaps.map(async (slug) => {
const content = await fs.readFile(`./src/data/roadmaps/${slug}/${slug}.md`, 'utf8');
const { data } = matter(content); // gray-matter parsing
return { slug, ...data };
}));
}
Step 2: Building Tailwind-Styled Templates
Rather than writing raw SVG markup, the pipeline uses satori-html to build type-safe HTML templates styled with Tailwind CSS utility classes. The script provides three template variants:
getRoadmapDefaultTemplate: Text-only layout for roadmaps without custom cover imagesgetRoadmapImageTemplate: Composite layout that embeds an existing cover image alongside textgetGuideTemplate: Specialized layout for best-practice guides and articles
// Template construction example
function getRoadmapDefaultTemplate({ title, description }) {
return html`
<div tw="bg-white flex flex-col h-full w-full p-[60px]">
<div tw="text-[70px] font-bold text-gray-900 leading-tight">
${title}
</div>
<div tw="mt-[16px] text-[30px] text-gray-600 leading-relaxed">
${description}
</div>
</div>
`;
}
Step 3: Rendering SVG with Satori
The core transformation happens in the generateOpenGraph function. It passes the HTML template to Satori along with dimensions (1200×630px) and a custom font configuration. Satori returns a pure SVG string representing the visual layout.
import satori from 'satori';
async function generateOpenGraph(htmlString, type, fileName) {
const svg = await satori(htmlString, {
width: 1200,
height: 630,
fonts: [
{
name: 'balsamiq',
data: await fs.readFile('public/fonts/BalsamiqSans-Regular.ttf'),
weight: 400,
style: 'normal',
},
],
});
// SVG is now ready for rasterization
return svg;
}
Step 4: Rasterizing to PNG with sharp or Resvg
The SVG string is converted to a PNG file using one of two renderers:
- sharp (default): High-performance Node.js image processing library
- Resvg (fallback): Used when content contains special characters that cause rendering artifacts in sharp
The final assets are written to public/og-images/<type>/<id>.png, making them available at predictable URLs like https://roadmap.sh/og/roadmap/full-stack.png.
import sharp from 'sharp';
import { Resvg } from '@resvg/resvg-js';
async function rasterize(svg, type, fileName, renderer = 'sharp') {
const outputPath = `public/og-images/${type}/${fileName}`;
if (renderer === 'resvg') {
const resvg = new Resvg(svg, { fitTo: { mode: 'width', value: 2500 } });
await fs.writeFile(outputPath, resvg.render().asPng());
} else {
await sharp(Buffer.from(svg), { density: 150 })
.png()
.toFile(outputPath);
}
}
Handling Edge Cases and Special Characters
The pipeline includes defensive logic for HTML entities that break SVG generation. The hasSpecialCharacters function detects characters like &, <, and >, then unescapes the HTML string before passing it to Satori. This prevents malformed SVG output when roadmap titles or descriptions contain reserved HTML characters.
Additionally, if a markdown file specifies a custom cover image in its front-matter, the getRoadmapImageTemplate function embeds that image directly into the OG template using an <img> tag with a tw attribute for sizing, creating a composite preview rather than a text-only card.
Runtime API Access and URL Generation
While the build script pre-generates images for static hosting, the repository also exposes runtime utilities in src/lib/open-graph.ts. The getOpenGraphImageUrl helper constructs canonical OG image URLs based on resource type and ID, ensuring that <meta property="og:image"> tags always point to valid assets.
For on-demand generation, the /v1-open-graph API endpoint leverages the same getResourceOpenGraph function from the library, allowing the server to generate fresh previews for newly created or updated content without requiring a full site rebuild.
Summary
- Satori converts Tailwind-styled HTML templates into SVG at build time, eliminating the need for headless browsers.
- The pipeline in
scripts/generate-og-images.mjsextracts front-matter with gray-matter, builds templates with satori-html, and rasterizes with sharp or Resvg. - Custom fonts (Balsamiq Sans) and defensive logic for special characters ensure consistent, high-quality renders.
- Generated PNGs are stored in
public/og-images/and served via predictable URLs constructed bysrc/lib/open-graph.ts.
Frequently Asked Questions
What is Satori and why is it used for Open Graph images?
Satori is an open-source library developed by Vercel that converts HTML and CSS into SVG. It is used in the developer-roadmap repository because it renders vector graphics without requiring a headless browser like Puppeteer or Playwright. This makes the build process faster, more memory-efficient, and fully deterministic, which is ideal for generating thousands of static OG images during deployment.
How does the pipeline handle markdown front-matter for image generation?
The script uses the gray-matter library to parse YAML front-matter from markdown files located in src/data/roadmaps/. It extracts fields like title, description, and optional image URLs. This metadata is then passed to template functions such as getRoadmapDefaultTemplate or getRoadmapImageTemplate, which construct the HTML layout that Satori will eventually render into an SVG.
What happens when a roadmap title contains special characters like ampersands?
The hasSpecialCharacters function in scripts/generate-og-images.mjs detects HTML entities such as &, <, and >. When these characters are present, the pipeline unescapes the HTML string before passing it to Satori. If rendering issues persist with the default sharp rasterizer, the system falls back to Resvg, which handles complex Unicode and special-character edge cases more robustly when converting the SVG to PNG.
Where are the generated Open Graph images stored and how are they served?
After rasterization, PNG files are written to public/og-images/<type>/<id>.png within the repository. The src/lib/open-graph.ts utility provides the getOpenGraphImageUrl function, which constructs canonical URLs like https://roadmap.sh/og/roadmap/full-stack.png. These URLs are embedded in <meta property="og:image"> tags, allowing social platforms to fetch the pre-generated previews directly from the static file system or via the /v1-open-graph API endpoint for on-demand generation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →