Skip to content
Blume is now publicly available.
Blume
Esc
navigateopen⌘Jpreview
On this page

SEO

Metadata, Open Graph images, RSS feeds, and JSON-LD — Blume's discoverability layer, grouped under one seo config.

Blume handles the discoverability layer for you: page metadata, social share images, feeds, and structured data. The configurable features live under the seo key in blume.config.ts; metadata is driven by your content.

seo: {
  og: { enabled: true },
  rss: { enabled: true, types: ["blog", "changelog"] },
  sitemap: true,
  robots: true,
  structuredData: true,
  x: { handle: "@acme" },
}

Most of this is sharper with an absolute site URL — set deployment.site so feeds, OG images, canonicals, the sitemap, and JSON-LD can emit full URLs.

Metadata

Every page renders the standard <head> tags from your config and frontmatter:

  • <title> — the page title plus your site title.
  • <meta name="description"> and og:description — the page description, falling back to the site description.
  • og:title and og:site_name — the page title and your site title.
  • <link rel="canonical"> and og:url — the page’s absolute URL (when deployment.site is set).
  • og:typearticle on blog posts and changelog entries, website elsewhere. Article pages also emit article:published_time and article:modified_time from the page’s date and last-modified timestamp.
  • og:image — the OG image for the page. A generated card also declares its og:image:width, og:image:height, og:image:type, and og:image:alt, so a crawler can lay the card out without fetching it first; an seo.image you supply yourself declares none of these, since its size and format are unknown.
  • twitter:card, twitter:title, twitter:description, twitter:image — the X card. Pages with an image get the wide summary_large_image variant; pages without one still get the compact summary card rather than rendering as a bare link.

X attribution

X reads everything else on the card from the og:* tags, so the only values it can’t infer are the accounts to credit. Set them under seo.x and Blume emits twitter:site (your site’s account) and twitter:creator (the author’s). The @ is optional — acme and @acme both work.

seo: {
  x: { handle: "@acme", creator: "@jane" },
}

A page can claim its own author, which is what you want for a guest post:

---
title: How we shipped it
seo:
  x:
    creator: "@guestauthor"
---

Override any of the other tags per page with seo frontmatter:

---
title: Pricing
description: Plans and pricing for every team size.
seo:
  title: Pricing — Acme
  canonical: https://acme.com/pricing
  noindex: false
---
PropType
seo.title?string

Override the <title> and og:title for this page.

Typestring
seo.description?string

Override the meta + og:description.

Typestring
seo.image?string

Custom social image (see Open Graph).

Typestring
seo.canonical?string

Override the canonical URL.

Typestring
seo.noindex?boolean

Emit robots noindex and skip structured data.

Typeboolean
seo.x.creator?string

Credit this page to an X account (twitter:creator), overriding seo.x.creator from your config.

Typestring

Open Graph images

Blume can render a 1200×630 social card for every page at build time — no headless browser, thanks to Takumi, so builds stay fast. On by default once deployment.site is set or auto-detected (the og:image URL has to be absolute to be useful to crawlers), and off otherwise. Set enabled to override that either way:

seo: {
  og: { enabled: true }, // or false to opt out even with a site set
}

Brand the generated card

Set a local SVG and color palette to match the generated card to your brand. The logo can live in public/ or at the project root. Omit any palette value to keep its default.

seo: {
  og: {
    logo: "/logo/og.svg",
    palette: {
      accent: "#ff5410",
      background: "#1d1d1d",
      foreground: "#fff6f2",
      muted: "#a6a19f",
      border: "#323232",
    },
  },
}

By default, each card is derived from your content and theme — the page title as the headline, your site title as the eyebrow, and your theme accent for the mark. Images are served at /og/<slug>.png, mirroring each route, and are prerendered as static files even in server mode:

Page route Image URL
/ /og/index.png
/quickstart /og/quickstart.png
/configuration/ai /og/configuration/ai.png

Override the generated card for any page with seo.image — a file in public/ or an external URL. It takes precedence over the generated card and works even when og is off, so you can mix custom images with generated ones:

---
title: Pricing
seo:
  image: /og/pricing-custom.png
---

Emoji in a page title or site title render as Twemoji glyphs, fetched from a CDN while the card renders — so a build whose titles contain emoji needs network access. Each glyph is fetched once per build, however many pages use it.

Show, hide, or override card layers

Beyond the headline, the card carries three optional layers: the brand mark in the top-left (your logo, or an accent tile with the site title’s initial), the subtitle under the headline (your site description), and a footer with your repo slug (from github) and the site’s URL — the deployment site’s host plus deployment.base, so a GitHub Pages project site reads user.github.io/repo. Override any of them with a string of your own, or hide one with false:

seo: {
  og: {
    site: "docs.acme.com", // footer URL text, or false to hide it
    description: false, // hide the subtitle; a string overrides it
    logo: false, // no brand mark at all — not even the initial tile
  },
}

Card fonts

By default the card renders in Takumi’s built-in font, which covers only Latin glyphs — a title in another script (Japanese, Chinese, Korean, Arabic, …) would render as tofu, empty boxes.

Set theme.fonts and the card follows it. When your config picks its own fonts, the generated cards automatically render the headline in your display font and the description and footer in your body font, so shared links match the site — including non-Latin coverage, with nothing to configure here. (Families from non-Google providers are skipped — the card renderer can only fetch from Google Fonts — but local font files work.)

To use different fonts on cards than on the site, or to add script coverage without touching the theme, set og.fonts explicitly — it always wins over the theme-derived fonts:

seo: {
  og: {
    fonts: [
      "Noto Sans JP",
      { name: "Inter", weight: [400, 700] },
      { name: "Berkeley Mono", src: "./fonts/BerkeleyMono-Regular.woff2" },
    ],
  },
}

Each entry is a Google Fonts family name, an object pinning its weight (a number, a list, or a variable range like "100..900") and style ("normal", "italic", or both), or a local font file — src resolves from the project root, with optional weight and style when the file’s own metadata shouldn’t decide.

Google families are fetched at build — so a build that uses them needs network access — and the renderer only pulls the glyph subsets each title actually uses. Fallback is per-glyph, so adding a family only affects glyphs the other fonts can’t draw.

An explicit og.fonts: [] opts out entirely: cards keep the built-in font even when theme.fonts is set.

Custom page titles

A custom .astro page has no frontmatter to read, so its generated card is titled by humanizing the last URL segment of its route — /getting-started becomes “Getting Started”, but /cli becomes “Cli”. Name those cards explicitly with og.titles, keyed by route ("/" addresses the home, whose card otherwise carries the site title):

seo: {
  og: {
    titles: {
      "/cli": "CLI",
    },
  },
}

Entries only apply to custom pages — a content page’s card always takes its headline from the page title, so retitle those in frontmatter instead.

seo.image is frontmatter, so it only covers Markdown and MDX content. To give a custom .astro page its own social image — a marketing home or landing page, and the way to give the home page alone a bespoke share image — pass the ogImage prop to PageLayout.

RSS feeds

Blume builds an RSS feed for each content type in rss.typesblog and changelog by default — that has pages, served at /<type>/rss.xml. See Feeds for authoring blog and changelog entries with dates.

seo: {
  rss: {
    enabled: true,
    types: ["blog", "changelog"],
    limit: 50,
  },
}
Option Default Description
enabled true Generate feeds.
types ["blog", "changelog"] Content types that each get a feed.
limit 50 Maximum items per feed, newest first.

Blume injects <link rel="alternate"> tags so browsers and feed readers discover the feeds automatically.

Structured data

Blume emits schema.org JSON-LD in every page’s <head> so search engines understand your content. On by default:

seo: {
  structuredData: true,
}

Each page includes:

  • a WebSite node for site identity,
  • the page as an articleBlogPosting for blog posts, TechArticle for changelog and docs — with its description and publish date,
  • a BreadcrumbList built from the navigation trail.

URLs are absolute when deployment.site is set. Pages marked seo.noindex are skipped.

Sitemap

Blume writes a sitemap.xml of every indexable page at build time. It needs an absolute deployment.site and lists every page except drafts, hidden, and noindex pages. On by default:

seo: {
  sitemap: true,
}

Ship your own public/sitemap.xml to take over — Blume never overwrites a file you place in public/.

Robots

Blume writes a robots.txt that allows all crawlers, declares your content signals, and adds a Sitemap: line pointing to the sitemap when one is available. On by default:

seo: {
  robots: true,
}
User-agent: *
Content-Signal: search=yes, ai-input=yes, ai-train=yes
Allow: /

Sitemap: https://docs.example.com/sitemap.xml

Content signals

The Content-Signal line — the emerging content-usage convention — declares how AI crawlers may reuse your docs. Blume emits it on by default with every signal set to yes, matching its stance that docs are open to humans and agents alike:

  • search — traditional and AI search indexing
  • aiInput — grounding / RAG at answer time
  • aiTrain — model training

Restrict any signal by setting it to false; the ones you leave out stay yes:

seo: {
  contentSignals: {
    aiTrain: false, // opt out of training, keep search + grounding
  },
}
User-agent: *
Content-Signal: search=yes, ai-input=yes, ai-train=no
Allow: /

Set contentSignals: false to drop the declaration entirely:

seo: {
  contentSignals: false,
}
PropType
seo.contentSignals?boolean | object

Content-Signal declaration. true or omitted emits all signals as yes; false drops the line; an object sets signals individually.

Typeboolean | object
contentSignals.search?boolean

Allow use for search indexing (search). Default true.

Typeboolean
contentSignals.aiInput?boolean

Allow use for AI grounding / RAG at answer time (ai-input). Default true.

Typeboolean
contentSignals.aiTrain?boolean

Allow use for AI model training (ai-train). Default true.

Typeboolean

Content signals express a preference, not access control: they tell well-behaved crawlers how you’d like your content used, and it’s on the crawler to honor them.

Ship your own public/robots.txt to take over.

Last updated on August 3, 2026

Was this page helpful?