June 10, 2026
Social cards, generated per page
By Phil Scott
Pennington now generates an OpenGraph image for every page and emits the
og:image and twitter:image tags that point at it.
What you write
You write one function, Render, that takes a page's title, description, date,
and front matter and returns the image bytes (return null to skip a page). Around
that, Pennington discovers a /social-cards/{page}.png route per page, serves
each one in dev, bakes them to PNGs in the static build, and emits the og:image,
twitter:image, and twitter:card tags.
builder.Services.AddDocSite(() => new DocSiteOptions
{
SiteTitle = "My Docs",
CanonicalBaseUrl = "https://example.com",
SocialCards = new SocialCardOptions
{
Render = (request, services, ct) =>
Task.FromResult<byte[]?>(Paint(request.Title, request.Width, request.Height)),
},
});
You pick the image library: ImageSharp, SkiaSharp, or a headless browser. This docs site uses Ashcroft to lay text over a background image.
A few details
The card routes produce no content records, so they never show up in search, the
sitemap, or llms.txt. A page that sets its own og:image keeps it; the generated
card only fills the gap, through the same head
subsystem everything else in the <head>
goes through.
One thing you have to set: OpenGraph wants an absolute image URL. Set
CanonicalBaseUrl or the tags come out root-relative, which works in dev but
won't unfurl when shared. The social cards
how-to has a full painter.