How to generate an Open Graph image from an SVG template
Paste a link to your site into Slack, X, LinkedIn, or iMessage, and the platform unfurls a preview card with a big image. That image is the Open Graph image (“OG image”), and if you don’t provide one you get a blank card or, worse, a random cropped screenshot. A good OG image is free marketing on every share, so it is worth getting right.
This site has one committed at public/og-image.png, and the blog has its own at blog/public/og-image.png. Both are exactly 1200x630 pixels. Let me explain what that file is, how the page points at it, and how I produce it from an SVG template.
What an OG image actually is
“Open Graph” is a small set of <meta> tags in your HTML <head> that tell social platforms how to render a link. They live alongside the rest of your SEO metadata — see adding RSS, a sitemap, and SEO metadata to Astro. The image tag is the star of the show. Here is what this site declares in index.html:
<!-- index.html -->
<meta property="og:image" content="https://developerehsan.com/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:type" content="image/png" />The og:image points at a real, publicly reachable PNG. Because it lives in public/, the build copies it to the site root, so https://developerehsan.com/og-image.png just works. The width/height tags let platforms lay out the card before the image finishes downloading.
Why 1200x630? That is the standard OG aspect ratio (1.91:1). Every major platform crops to roughly that, so designing at exactly 1200x630 means nothing important gets chopped off. Confirm your file with any image tool:
file public/og-image.png
# public/og-image.png: PNG image data, 1200 x 630, 8-bit/color RGBAWhy design it as an SVG, then export to PNG
Platforms want a raster image — a PNG or JPG. They will not render an SVG in a preview card. So why start with SVG?
Because SVG is text you can edit. When my tagline or role changes, I do not want to reopen a design tool, nudge pixels, and re-export. I want to change one line of markup. SVG gives you:
- Crisp text at any size (it is vector, so no blur).
- Version-control-friendly diffs (it is just XML).
- Trivial edits — change a string, swap a color token.
Then a one-time export bakes it into the PNG the platforms need.
Design the template
An OG image is mostly a background, a headline, and a subtitle. Here is the shape of a template sized to the exact canvas:
<!-- scratchpad/og-image.svg -->
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630"
viewBox="0 0 1200 630">
<rect width="1200" height="630" fill="#0a0a0a" />
<text x="80" y="300" fill="#fafafa"
font-family="JetBrains Mono, monospace"
font-size="72" font-weight="700">
Ehsan Shahid
</text>
<text x="80" y="380" fill="#a1a1aa"
font-family="JetBrains Mono, monospace" font-size="36">
Full-stack developer
</text>
</svg>Points worth internalizing as a beginner:
- The root
width,height, andviewBoxare all1200 x 630. Match them so nothing scales unexpectedly. - Coordinates are top-left origin.
y="300"on<text>is the baseline of the text, not its top — so large text needs a fairly largey. - Keep meaningful content away from the edges (I start at
x="80"). Some platforms overlay UI or round the corners. - Match your brand fonts and colors. This site’s aesthetic is a dark terminal look with JetBrains Mono, so the template mirrors that.
Render the SVG to a 1200x630 PNG
Here is the honest part, because the style guide I write to demands accuracy: there is no committed script in this repo that generates the OG image. The public/og-image.png and blog/public/og-image.png files are committed artifacts, and the project’s own guidance describes regenerating the OG image from a scratchpad SVG — a manual step, not part of bun run build. I am not going to pretend a package.json script exists when it does not.
So the workflow is: keep the SVG in a scratch location, and export it to PNG whenever the design changes. Any of these does the job.
Using a headless browser or an SVG renderer library (sharp is the common choice in Node):
bun add -d sharp// one-off export script (not committed)
import sharp from "sharp"
import fs from "node:fs"
const svg = fs.readFileSync("scratchpad/og-image.svg")
await sharp(svg)
.resize(1200, 630)
.png()
.toFile("public/og-image.png")sharp reads the SVG buffer, enforces the 1200x630 canvas, and writes a PNG. If you would rather avoid a dependency, most vector editors (Inkscape, Figma) and even the browser can export an SVG to PNG at a fixed size:
# with Inkscape installed
inkscape scratchpad/og-image.svg \
--export-width=1200 --export-height=630 \
--export-filename=public/og-image.pngWhichever tool you use, the output is the same: a static PNG dropped into public/ so the og:image URL resolves.
Verify it works
After deploying, confirm the tags and image are live. Curl the page and look for the meta tags, then check the image loads:
curl -s https://developerehsan.com | grep 'og:image'
curl -sI https://developerehsan.com/og-image.png | grep -i content-typeFor the real preview, paste your URL into a social platform’s share box, or use a link-preview debugger. Note that platforms cache aggressively — if you update the image, the old one may show until the cache expires.
What to remember
- An OG image is the 1200x630 PNG social platforms show in link previews; you wire it up with
og:imagemeta tags in your<head>. - Design at exactly 1200x630, keep important content away from the edges, and match your brand fonts and colors.
- Author it as an SVG template so edits are one-line text changes, then export to PNG — platforms need the raster file.
- In this repo the PNGs are committed artifacts and regenerating them from an SVG is a manual step; there is no build script that does it automatically.
- Serve the PNG from
public/so its public URL resolves, and remember platforms cache previews.
Related reading: the OG tags round out the structured metadata in adding Person, ProfilePage, WebSite and FAQPage JSON-LD to a static site, and the committed PNG gets a year-long immutable cache via hardening a Vercel deploy with security and cache headers.