Open Graph images with Astro and Typst
51 points by eaj
51 points by eaj
oh this is excellent, I gotta put this on my own blog now too, nice writeup thanks!!!
Nice! Let me know how it goes :)
Live now, works, seems the command + template you suggested generates images at sizes twice that of 1200x630 tho? maybe needs to be 72ppi instead https://github.com/p0lyw0lf/website/blob/f5c454e3fc94b49edf6e93837f5246a0e99dce17/src/runtime/opengraph/generatePng.js
Nice!
And I think that's right, as points (pt) are by definition 1/72 inch. This typst markup compiles to 1:
#(72pt).inches()
So using 72 ppi means 1pt = 1px, but I found the output to be of a low quality and used the typst default of 144 instead.
This is pretty cool, Typst works quite well for this. The Rust package repository crates.io also uses Typst though they do it at runtime since there is quite a lot of packages which are not often accessed.
That makes sense. If I was tasked to setup this for a more dynamic site with user-generated content I'd still reach for typst, but probably with a cache in front.
Looks like they split it out into its own crate at some point: https://github.com/rust-lang/crates_io_og_image
I think they have some backend queue system that runs it and pushes the images to a CDN, though I don't know the details about that part.
I agree that typst is a wonderful tool to reach for, and you can get it to run quite fast as well which is nice.
You can do the same with LaTeX, you just have to write hundreds of lines of recursively self-expanding macros.
With that title, I was expecting actual Open Graph (you know, served from a triplestore, with an endpoint serving Turtle content for instance). Alas! It’s just automated image generation for social media previews.
I have my static site generator generate my OpenGraph cards (e.g. https://nytpu.com/gemlog/2025-12-21.og-card.png) using this lovely ImageMagick command I put together XD, this post might just be a bit more elegant (converted to shell syntax for clarity but my SSG invokes with a list of arguments which is actually more readable than all the escapes here…):
magick -size 1200x630 canvas:lightblue -fill black -font Ubuntu \
\( \
-size 1100x550 -background none -gravity center "caption:${post_title}" \
\( +clone -background navy -shadow 60x0+4+4 \) +swap -background none -layers merge +repage -extent 1100x550 \
\) \
-gravity north -composite \
-gravity southeast -pointsize 50 -annotate +20+25 'by nytpu' \
${output_file}
I have been meaning to try typst for a while but I never thought about this use case (image generation). Pretty clever idea and the results seem excellent. I wonder how much effort would be required to spice things up a little more: add decorations, background images, that kind of stuff ...
On my static sites I have been recently using satori with pretty decent results but I admit in the past I did stuff by rendering to HTML and taking snapshots with a headless browsers. It can work surprisingly well but the whole idea seems hacky and it isn't a performant solution if you have lots of pages and don't use caching wisely!
Anyway, thanks once again for sharing this and for giving one more reminder to try typst!
Nice write-up! Typst is a fantastic tool and can be used for all kinds of generated documents and images.
One tiny suggestion: I noticed you used the long form of new Response() in your API route, manually setting the headers and encoding the body.
You could simplify the code by using the static method Response.json() to achieve the same result with less code and reduced risk for typos.
Looks cool! One part I didn't immediately grasp regarding static site vs json api and this 'content collection': Is this basically like the whole post dumped in a file and your client-side api queries that file for the title, plus some rewrite magic. So it's a js-heavy static site?
Not quite. The Content Collection reads all the article files and parses the markdown and the metadata, and returns a Javascript array of objects, each object representing an article. When building the site, this is how I interact with the posts.
The important part is that this only happens at build time: The finished site is a bunch of HTML-files served by Caddy in my case. There is no client API, and the only JS shipped to the client is a tiny snippet from a third party analytics provider.