Configuration file
Every option in blume.config.ts, from site metadata and content sources to the links that lead into each individual feature configuration guide.
Blume reads blume.config.ts from your project root. Wrap your config in defineConfig for autocomplete and type-checking — every field is optional, with a sensible default.
import { defineConfig } from "blume";
export default defineConfig({
title: "My Docs",
description: "Documentation for my project.",
});
A complete example
A broader example touching the most common options (see each feature’s guide for the rest):
import partytown from "@astrojs/partytown";
import { defineConfig } from "blume";
import { orama } from "blume/search";
export default defineConfig({
// Site
title: "My Docs",
description: "Documentation for my project.",
logo: "/logo.svg",
// Astro integrations — installed and versioned by this site
integrations: [partytown()],
// Content
content: {
root: "docs",
},
// Theme — see the Theming guide
theme: {
accent: "teal",
radius: "md",
mode: "system",
},
// Search — an adapter from "blume/search"; see the Search guide
search: orama(),
// Markdown features
markdown: {
imageZoom: true,
externalLinks: false, // open https:// links in a new tab
code: {
icons: true, // language icon in the code-block header
wrap: false, // wrap long lines instead of scrolling
theme: {
light: "github-light", // bundled name or custom Shiki theme object
dark: "github-dark",
},
},
},
// Agents — llms.txt, MCP, the AI catalog; see the Discoverability section
agents: {
llmsTxt: true,
// AI Catalog / ARD manifest at /.well-known/ai-catalog.json (needs deployment.site)
catalog: true,
// Your site's agent skill at /skill.md (needs deployment.site)
skillMd: true,
// MCP server (needs server output)
mcp: {
enabled: false,
route: "/mcp",
},
},
// SEO — OG images, feeds, sitemap, structured data; see the Discoverability section
seo: {
og: { enabled: true },
rss: { enabled: true, types: ["blog", "changelog"] },
sitemap: true,
robots: true,
structuredData: true,
},
// Deployment — a static build here; see the Deployment guide for host adapters
deployment: {
site: "https://docs.example.com",
},
});
Site
| Option | Default | Description |
|---|---|---|
title |
"Documentation" |
Site name — shown in the header, page titles, OG cards. |
description |
— | Default meta description, used for SEO and OG. |
logo |
— | Brand mark and/or wordmark shown in the header. |
banner |
— | Site-wide announcement bar above the header. |
footer |
— | A copyright line, a row of links, and social profile icons below the content. |
Logo
Point logo at an SVG and Blume inlines it, so a currentColor logo follows the light and dark theme automatically:
logo: "/logo.svg",
The SVG can live at your project root or in public/. The brand is a mark (image) plus a wordmark (text); the object form lets you set them independently:
logo: {
image: "/logo.svg", // string, or { light, dark, alt } for themed raster art
text: "Acme", // wordmark beside the mark
href: "/", // overrides the brand link (defaults to "/")
},
image takes the same value as the shorthand — a single path, or { light, dark, alt } for separate light/dark artwork (raster images must live in public/).
href is a default-locale path like a tab path: on a multi-locale site the brand link moves into the reader’s locale (/ becomes /en under /en/…) whenever that locale serves the route, so clicking the logo doesn’t leave the language they were reading. A route only the default locale serves — a custom page or the generated changelog index — keeps its own path instead of pointing at a localized URL that would 404. An absolute URL is used as-is.
text controls the wordmark independently of the mark:
- Omit
textand the brand uses your sitetitle(the default). - Set
text: ""to show the mark alone — handy when the logo image already includes the wordmark. Screen readers then announce the brand link by the image’salt, or by your sitetitlewhen it has none. - Set
textwith noimagefor a text-only logo.
Favicon
There’s no favicon option — Blume auto-detects one by filename, the way Next.js does. Drop an icon or favicon file (.svg, .png, or .ico) in your project root or public/ directory and it becomes the browser tab icon:
my-docs/
├─ blume.config.ts
├─ icon.png ← picked up automatically
└─ docs/
SVG wins over PNG over ICO when several are present, and a file in public/ is preferred over one at the root. If Blume finds no icon, it falls back to its own mark.
A dark mark disappears against dark browser chrome, so you can ship a second file for dark mode. Add a -dark sibling of your icon file — the same name and directory, with -dark before the extension (icon.png → icon-dark.png) — and Blume emits both icons behind a prefers-color-scheme media query, plus a plain light tag for browsers and crawlers that ignore media queries on icons:
my-docs/
├─ blume.config.ts
├─ icon.png ← light mode
├─ icon-dark.png ← dark mode
└─ docs/
Only the sibling of the icon Blume picked is used — a -dark file with a different name stays ignored, so an unrelated file can’t pair with your mark by accident. The dark file is optional; with only one icon, Blume emits a single tag as before. Blume’s own fallback mark ships both variants.
Apple touch icon
The icon iOS uses when someone adds your site to their home screen is detected the same way. Drop an apple-icon file (.png, .jpg, or .jpeg) — or an apple-touch-icon.png, the name most favicon generators emit — in your project root or public/ directory and Blume wires up <link rel="apple-touch-icon"> for you. There’s no default; if no file is found, no tag is emitted.
my-docs/
├─ blume.config.ts
├─ apple-icon.png ← picked up automatically
└─ docs/
Put the file in public/ rather than the project root: iOS ignores the inlined data URI Blume uses for a root-level icon, so only a public/ file (served at /apple-icon.png) reliably reaches the home screen. Unlike the favicon, there’s no -dark sibling here — iOS ignores media queries on home-screen icons, so a dark variant could never be served.
Banner
Show a site-wide announcement bar above the header. Pass a string, or an object with a link and a dismiss button:
banner: "Docs are in beta — expect changes.",
banner: {
content: "Version 2.0 is here!",
link: { text: "Read the release notes", href: "/changelog" },
dismissible: true,
id: "v2",
},
When dismissible is on, the bar shows a close button and stays hidden for that visitor afterward. The dismissal key defaults to the content text, so editing the message brings the banner back; set a stable id to keep it dismissed across edits.
On a multilingual site, content and the link’s text also take a map of locale code to text. Each page shows its own locale’s entry, falling back to the default locale’s, and the link moves into the reader’s locale whenever that locale serves the page:
banner: {
content: { en: "Version 2.0 is here!", fr: "La version 2.0 est là !" },
link: {
text: { en: "Read the release notes", fr: "Lire les notes de version" },
href: "/changelog",
},
},
A translated banner keys dismissal on the default locale’s text, so one dismissal hides it in every language.
Footer
Every page ends with a footer: one row, as tall as the header, with your links on one side, social profile icons on the other, and a copyright line centered between them. The footer is the only place Blume links your social profiles and repository, so the github repository comes first among the icons whenever it’s set. footer adds the rest:
footer: {
copyright: "© 2026 Acme, Inc.",
links: [
{ label: "Pricing", href: "https://acme.dev/pricing" },
{ label: "Changelog", href: "/changelog" },
{ label: "Blog", href: "https://acme.dev/blog" },
],
socials: {
x: "https://x.com/acme",
discord: "https://discord.gg/acme",
},
},
copyright is plain text, so write © itself rather than ©. For a year that keeps itself current, use a template literal: `© ${new Date().getFullYear()} Acme, Inc.`. links appear in the order you write them. Write internal links as if mounted at root; an external link opens in a new tab. On a multilingual site, copyright and a label can each be a map of locale code to text, and an internal link moves into the reader’s locale whenever that locale serves the page. socials maps a platform to its profile URL, and the icons appear in the order you write them. The platforms are bluesky, discord, facebook, github, hacker-news, instagram, linkedin, medium, podcast, reddit, slack, telegram, threads, website, x, and youtube. A github social replaces the repository link; hide that link with navigation.repo.
A site with no copyright, links, profiles, or repository has no footer. Custom pages built on PageLayout show it too, unless they fill the layout’s own footer slot. To replace it entirely, override the Footer layout slot.
Content
Where your content lives and how Blume discovers it. See Pages for how files become routes.
content: {
root: "docs",
}
| Option | Default | Description |
|---|---|---|
root |
"docs" |
Folder Blume scans for content. Shorthand for a single filesystem() source; not allowed beside sources. |
include |
["**/*.{md,mdx}"] |
Globs that match content files. Shorthand, like root. |
exclude |
["**/_*", "**/.*"] |
Globs to ignore. Yours add to the defaults, which skip underscore- and dot-files; a ! entry drops a pattern, so "!**/_*" publishes _-prefixed files. Shorthand, like root. |
sources |
one filesystem() |
Content source adapters from blume/sources, replacing the shorthand. See Content sources. |
pages |
"pages" |
Folder for custom .astro pages. |
defaultType |
"doc" |
Page type used when frontmatter omits it. |
types |
{} |
Per-type content definitions — custom frontmatter keys scoped to pages of one type. See Frontmatter. |
Static assets live in public/ — a file at public/logo.png is served at /logo.png, so a reference like  resolves against public/images/create.png. Images referenced by relative path () live next to your content instead, and are optimized at build time.
Images
Local images referenced by relative path are optimized automatically at build time — compressed, converted to WebP, and given intrinsic width/height attributes so the layout doesn’t shift while they load. There’s nothing to configure; see Links and images for authoring guidance.
Remote images are served untouched by default. To have Blume download and optimize them at build time too, authorize their hosts:
image: {
domains: ["cdn.example.com"],
remotePatterns: [{ protocol: "https", hostname: "**.example.com" }],
}
| Option | Default | Description |
|---|---|---|
domains |
[] |
Hostnames whose remote images may be optimized. |
remotePatterns |
[] |
Pattern-based authorization (protocol, hostname, port, pathname); hostnames accept *. (one level) and **. (any depth) wildcards. |
Frontmatter
Page frontmatter is strictly validated — an unknown key fails the build, so typos are caught early. To carry project-specific metadata (an owner, a review date), declare the extra keys under frontmatter.extend, each mapped to a schema you supply:
import { defineConfig } from "blume";
import { z } from "zod";
export default defineConfig({
frontmatter: {
extend: {
owner: z.string(),
reviewedAt: z.coerce.date().optional(),
},
},
});
Any Standard Schema library works — Zod (whichever version your project installs), Valibot, ArkType. Keys outside the extension stay strictly validated, so typo-catching is unchanged. See Custom keys for the validation semantics.
Keys under extend apply site-wide. To require keys only on pages of one content type — an RFC’s status, a runbook’s service — declare them per type under content.types instead:
import { defineConfig } from "blume";
import { z } from "zod";
export default defineConfig({
content: {
types: {
rfc: {
facets: ["domain", "status"],
frontmatter: {
domain: z.string(),
status: z.enum(["draft", "review", "enforced"]),
},
},
},
},
});
A key can be declared site-wide or per-type, not both. See Per-type keys for how the scoping resolves.
facets names the custom keys whose values become filterable metadata: they ride along on search documents (blume-search.json and the MCP index), and the MCP tools accept a filters input matching against them, so an agent can retrieve, say, only enforced RFCs in the architecture domain. Each facet must be a declared custom key — per-type or site-wide — and only string (or stringified number/boolean) values facet.
GitHub
Point Blume at your repository with github. It powers the footer’s repository link and the Edit on GitHub page action:
github: {
owner: "acme",
repo: "docs",
}
| Option | Default | Description |
|---|---|---|
owner |
— | GitHub account or organization that owns the repository. |
repo |
— | Repository name. |
branch |
"main" |
Branch that edit links point at. |
dir |
— | Path from the repo root to the project root (for monorepos). |
host |
"https://github.com" |
Origin of the GitHub instance, for Enterprise installations. Must be HTTP(S); normalized to its origin. |
api |
derived from host |
REST API base, for <GithubInfo> against an Enterprise instance. Must be HTTP(S); normalized to an origin and path. |
GitHub Enterprise
Docs whose repository lives on a GitHub Enterprise instance set host, and every repo-derived link — the header mark, edit links, the agent manifest — points at that instance instead of the public site:
github: {
host: "https://github.acme.com",
owner: "acme",
repo: "docs",
}
The REST API base that <GithubInfo> queries is derived from host: an Enterprise Cloud tenant with data residency (acme.ghe.com) is served from its api. subdomain, and any other host is treated as Enterprise Server (/api/v3). Set api explicitly when your instance sits somewhere else.
Last modified
Show a “Last updated on …” line at the bottom of each page. Off by default; set lastModified to "git" to derive each page’s date from its git history:
lastModified: "git",
| Value | Description |
|---|---|
false |
Disabled (default). |
"git" |
Read the date from git history (commit dates). |
"frontmatter" |
Never run git — use only the lastModified frontmatter field. |
The git source reads the most recent commit that touched each file, so it works in any git repository — including monorepos — and needs the repo’s history at build time. It follows renames, as git log --follow does: renaming page.md to page.mdx, or moving a file to another folder under the same content root, keeps the page’s date as long as the commit leaves its content unchanged. A rename that also edits the file dates the page at that commit, and so does moving a file in from outside the content root. CI platforms usually check out a shallow clone, which silently drops most dates (the build warns with BLUME_SHALLOW_GIT_HISTORY when that happens): on Vercel, set the VERCEL_DEEP_CLONE=true environment variable; with actions/checkout, set fetch-depth: 0. A page’s own lastModified frontmatter always wins, which is handy for pinning a date or for files that aren’t committed yet:
---
title: My page
lastModified: 2026-06-20
---
When enabled, the date is also emitted as schema.org dateModified in the page’s structured data.
Date format
Both the “Last updated” stamp and the changelog timeline render their dates through the same dateFormat, so they read alike. Dates always render in the site’s locale; dateFormat controls the shape. It defaults to the long form (July 21, 2026, 2026年7月21日):
dateFormat: { dateStyle: "long" },
dateFormat is a pass-through to Intl.DateTimeFormat options. Use a dateStyle preset for a length:
dateFormat: { dateStyle: "medium" },
Or the individual component fields for a numeric house style like 2026/07/21:
dateFormat: { year: "numeric", month: "2-digit", day: "2-digit" },
| Option | Description |
|---|---|
dateStyle |
Preset length: "full", "long", "medium", or "short". Can’t be combined with the component fields. |
weekday, era, year, month, day |
Individual components, e.g. year: "numeric", month: "2-digit". |
timeZone |
IANA time zone. Defaults to UTC, so a date reads the same regardless of where the site builds. |
calendar, numberingSystem |
Calendar system (e.g. "japanese") and numbering system (e.g. "arab"). |
timeZone, calendar, and numberingSystem don’t change the shape: a dateFormat that sets only those keeps the long form.
SEO and agents
Metadata, Open Graph images, RSS feeds, JSON-LD, the sitemap, and robots.txt live under seo; llms.txt, raw Markdown, the JSON API, the MCP server, and the discovery manifests live under agents. Both are covered page by page in the Discoverability section, which treats search engines and AI agents as two audiences for the same machine-readable layer. The reader-facing model features — the assistant and the Open in chat action — live under ai.
seo: {
og: { enabled: true },
rss: { enabled: true, types: ["blog", "changelog"] },
sitemap: true,
robots: true,
structuredData: true,
}
| Option | Default | Description |
|---|---|---|
og.enabled |
auto | Per-page Open Graph images — on when a site URL is set. |
rss.enabled |
true |
Build feeds for blog and changelog content (needs deployment.site). |
rss.types |
["blog", "changelog"] |
Content types that each get a feed. |
rss.limit |
50 |
Maximum items per feed. |
sitemap |
true |
Generate sitemap.xml (needs deployment.site). |
robots |
true |
Generate robots.txt with a Sitemap link. |
structuredData |
true |
Emit schema.org JSON-LD in each page’s head. |
These work best with an absolute deployment.site for full URLs.
Table of contents
The on-this-page outline is on by default and lists H2–H3 headings. Turn it off, or change the heading range, with toc:
export default defineConfig({
toc: false, // hide it everywhere
});
Or narrow the heading range instead:
export default defineConfig({
toc: { minHeadingLevel: 2, maxHeadingLevel: 4 },
});
Page feedback
Each docs page ends with a “Was this page helpful?” rating. It’s on by default; set feedback to false to hide it everywhere:
export default defineConfig({
feedback: false,
});
A reader’s answer is sent as a feedback custom event — with helpful ("yes" or "no"), path, and title — through every configured analytics adapter that has an event API, and as a blume:track event on window. Fathom, Microsoft Clarity, and Hotjar take an event’s name alone, without its properties, so they count ratings but can’t tell a yes from a no. Without an analytics adapter, the answer isn’t recorded anywhere, and with cookie consent on, the rating only shows for readers who allowed analytics, since from anyone else an answer would go nowhere. The question and thank-you text are UI strings you can translate with i18n.ui.
Written feedback
To hear more than yes or no, turn on comments. After rating a page, the reader gets a box to say what worked or what’s missing:
export default defineConfig({
feedback: { comments: true },
});
A comment is sent as a feedback_comment event with the same helpful, path, and title as the rating, plus the reader’s comment (up to 1,000 characters). It goes through the same adapters as the rating, so read comments where you read events. Providers cap how long a property value can be, so a long comment may not arrive whole: Google Analytics keeps 100 characters, Mixpanel 255 bytes (fewer characters in a script that takes several bytes a character, like Japanese), and Vercel Web Analytics takes values up to 255 characters, while PostHog and Segment pass comments whole. Fathom, Clarity, and Hotjar get no comment text at all. Plausible groups identical values rather than listing each comment, so it suits ratings better than comments. Readers can write anything in the box, so treat comments as they might contain personal details.
The box’s label and button are UI strings too (feedback.comment and feedback.send).
Feature options
Each of these has its own guide. The config field is the entry point:
| Field | What it configures | Guide |
|---|---|---|
theme |
Accent color, corner radius, fonts, light/dark mode | Theming |
navigation |
Explicit sidebar and header tabs | Navigation |
search |
Provider (Orama, Pagefind, Algolia, and more) and indexing | Search |
markdown |
Markdown rendering options — code blocks, heading anchors, image zoom | Syntax |
agents |
llms.txt, Markdown mirrors, the JSON API, the hosted MCP server, skills, and discovery manifests for coding agents |
SEO and GEO |
ai |
The in-page assistant and the Open in chat action | Assistant |
rateLimit |
How often one reader can call the assistant, the playground proxy, and server-side search, with an adapter from blume/ratelimit (on by default) |
Rate limiting |
narration |
The “Listen to this page” player, with browser voices or voices generated at build | Narration |
reference |
API references: openapi(), asyncapi(), graphql(), and scalar() adapters from blume/reference |
OpenAPI, AsyncAPI, GraphQL, Scalar |
api |
The server, auth, and Try it panel of hand-written endpoint pages | Hand-written API pages |
analytics |
Adapters from blume/analytics — PostHog, Google Analytics, Plausible, Mixpanel, Segment, and more — plus custom scripts |
Analytics |
consent |
A cookie consent adapter from blume/consent — Blume’s own banner, Osano, or Ethyca — that holds analytics until the reader allows it |
Cookie consent |
seo |
Metadata, OG images, feeds, structured data, sitemap, robots | SEO and GEO |
deployment |
A host adapter from blume/deploy, or { site, base } for a static build |
Deployment |
redirects |
Permanent and temporary redirects | Deployment |
poweredBy |
The X-Powered-By: Blume response header (default true) |
Deployment |
integrations |
Astro integrations appended after Blume’s built-ins | Customization |
basePath |
A path every generated route mounts under (e.g. /docs), invisible to the sidebar |
Deployment |
i18n |
Locales, the default locale, and translated UI strings | Internationalization |
versions |
Frozen snapshots of older docs with a version switcher | Versioning |
export |
Reader-facing PDF and EPUB downloads | Export |
examples |
Where <Component path> example previews live, and the CSS injected into their frames |
Component |
react |
React island behavior — the React Compiler’s auto-memoization | Islands |
variables |
Values {{name}} reads in any page |
Variables |
feedback |
The “Was this page helpful?” rating at the foot of each page (default true), and optional written comments after it |
Page feedback |
changelog |
The title and description of the generated /changelog index, each a string or a per-locale map |
Changelogs |
Precedence
Settings resolve from lowest to highest priority, so you only override what you need:
Blume defaults
blume.config.ts
Folder meta
meta.ts for a section’s title and ordering.
Page frontmatter