pagegraph

Add per-page Open Graph images

Declare an Open Graph image on each route, share one resolver between the plugin and createSeo, and fail CI when a page has no image.

This guide declares a social image per page, resolves images from one shared function, and configures the check that fails pages without one.

Declare an image on a page

Declare an image alongside a static page's title and description:

head: {
  title: "Pricing — Example",
  description: "Simple volume pricing.",
  image: { url: "/og/pricing.png", width: 1200, height: 630, alt: "Example pricing" },
}

For collection pages, return image from contentCollection's entry mapping and pass the same value to seoHead(ctx, { ...head, image }) at render time. Existing article.image strings remain supported. Precedence is head.image, then article.image, then the resolver.

An image emits og:image, optional og:image:width, og:image:height and og:image:alt, plus twitter:image and twitter:card=summary_large_image. Without an image, the card is summary. site.defaultImage remains the Article JSON-LD default. It does not satisfy social-image coverage.

Share a resolver

Share one resolver between the build plugin and the render API so every graph consumer and the rendered head use the same image:

// src/lib/seo/og-image.ts
import type { OgImageResolver } from "pagegraph";

export const ogImage: OgImageResolver = (node) => ({
  url: `/og/${node.kind}/${node.path === "/" ? "index" : node.path.slice(1)}.png`,
  width: 1200,
  height: 630,
  alt: node.head?.title ?? node.path,
});

// vite.config.ts
pagegraph({ origin: "https://example.com", ogImage /* other plugin options */ });

// src/lib/seo.ts
createSeo({ origin: "https://example.com", ogImage, site, organization, website });

The resolver is synchronous and returns an image or undefined. It receives a concrete node with its path, kind and head metadata (the shared OgImageNode shape). It honors head.canonicalPath, while graph path ownership stays unchanged. Keep it deterministic and safe to import in the server and browser runtime.

Your app renders the PNGs at build time. PageGraph only resolves metadata. The graph stores the absolute URL in node.head.image, which graph serialization preserves and pageHeads exposes for downstream head, llms and Markdown tooling. A generic buildSeoGraph caller supplies origin and ogImage to resolve images the same way.

Configure the check

pagegraph check fails sitemap-eligible pages without a resolved image. Same-origin images must name files under the app's public/ directory, including absolute URLs resolved from local paths. External URLs are not fetched. Generate the files before running the check.

export default defineSeoConfig({
  loadGraph: tanstackStartGraph({ root: import.meta.dirname }),
  ogImage: {
    severity: "structural", // default: error; "editorial" warns, "off" skips both checks
    publicDirectory: "public", // relative to the config directory; default: public
  },
});

Read next

Edit this page on GitHubView as Markdown