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 sitetitle.<meta name="description">andog:description— the pagedescription, falling back to the sitedescription.og:titleandog:site_name— the page title and your sitetitle.<link rel="canonical">andog:url— the page’s absolute URL (whendeployment.siteis set).og:type—articleon blog posts and changelog entries,websiteelsewhere. Article pages also emitarticle:published_timeandarticle:modified_timefrom the page’sdateand last-modified timestamp.og:image— the OG image for the page. A generated card also declares itsog:image:width,og:image:height,og:image:type, andog:image:alt, so a crawler can lay the card out without fetching it first; anseo.imageyou 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 widesummary_large_imagevariant; pages without one still get the compactsummarycard 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
---
seo.title?string
Override the <title> and og:title for this page.
stringseo.description?string
Override the meta + og:description.
stringseo.image?string
Custom social image (see Open Graph).
stringseo.canonical?string
Override the canonical URL.
stringseo.noindex?boolean
Emit robots noindex and skip structured data.
booleanseo.x.creator?string
Credit this page to an X account (twitter:creator), overriding seo.x.creator from your config.
stringOpen 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
}
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
---
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.types — blog 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 article —
BlogPostingfor blog posts,TechArticlefor 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 indexingaiInput— grounding / RAG at answer timeaiTrain— 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,
}
seo.contentSignals?boolean | object
Content-Signal declaration. true or omitted emits all signals as yes; false drops the line; an object sets signals individually.
boolean | objectcontentSignals.search?boolean
Allow use for search indexing (search). Default true.
booleancontentSignals.aiInput?boolean
Allow use for AI grounding / RAG at answer time (ai-input). Default true.
booleancontentSignals.aiTrain?boolean
Allow use for AI model training (ai-train). Default true.
booleanContent 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.