This documentation is also published as Markdown for efficient machine reading: the whole site is indexed at /llms.txt, and every page has a clean Markdown copy at the same URL with .md appended. These are generated from the same source and cost far fewer tokens to read than this rendered HTML.

Skip to main content

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.

csharp
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.