Zum Inhalt springen
Blume
Deutsch
Esc
navigierenöffnen⌘Jvorschau
Auf dieser Seite

KI

Mach deine Dokumentation maschinenlesbar mit llms.txt und einer per OpenAPI beschriebenen JSON-API, ergänze einen optionalen "Ask AI"-Assistenten direkt auf der Seite und stelle einen gehosteten MCP-Server für Coding-Agents bereit.

Blume bietet einige KI-Funktionen: maschinenlesbare Dokumentation für externe Werkzeuge (llms.txt und eine JSON-API mit OpenAPI-Beschreibung, beide standardmäßig aktiviert), einen Ask AI-Assistenten direkt auf der Seite und einen gehosteten MCP-Server für Coding-Agents. Ask AI und MCP sind optional zuschaltbar, und statische Dokumentation bleibt vollständig statisch, bis du eine Funktion aktivierst.

llms.txt

Blume erzeugt maschinenlesbare Versionen deiner Dokumentation, die Coding-Agents und Chat-Assistenten verarbeiten können. Dies ist standardmäßig aktiviert; setze llmsTxt: false, um es abzuschalten:

ai: {
  llmsTxt: false,
}

Solange die Funktion aktiviert ist, schreibt blume build zwei Dateien in das Stammverzeichnis deiner Website:

  • /llms.txt — ein kompakter Index: Titel und Beschreibung deiner Website, gefolgt von einer verlinkten Liste aller Seiten mit ihrer Zusammenfassung, gegliedert in Abschnitte, die deine Seitenleiste widerspiegeln — Ordner und Gruppen werden zu Überschriften, sodass ein Agent die Struktur der Dokumentation sieht und nicht einen einzigen flachen Block.
  • /llms-full.txt — der gesamte Korpus: der vollständige Markdown-Text jeder Seite samt ihrer Quell-URL, in einer einzigen Datei.

Entwurfsseiten werden ausgeschlossen. Setze deployment.site, damit die Links und Quell-URLs auf absolute Adressen aufgelöst werden.

llmsTxt akzeptiert außerdem eine Objektform mit Stellschrauben dafür, was die Dateien enthalten. Wenn deine API-Referenz eine Platzhalter- oder Beispielspezifikation dokumentiert, setze openapi: false, um die daraus generierten Seiten aus beiden Dateien herauszuhalten:

ai: {
  llmsTxt: {
    enabled: true, // default
    openapi: false, // exclude generated API reference pages
  },
}

Die Objektform akzeptiert außerdem details: Markdown, das in llms.txt direkt nach Titel und Zusammenfassung steht, vor den Seitenabschnitten — der freiformige “details”-Block der llms.txt-Spezifikation. Hier ist der Ort, um Agents zu sagen, wann sie zu deinem Produkt greifen sollen und wie sie es aufrufen, wonach Readiness-Scanner ausdrücklich suchen; ein Installationsbefehl und der Paketname gehören ebenfalls hierher:

ai: {
  llmsTxt: {
    details: [
      "## When to use Acme",
      "",
      "Reach for Acme when a project needs hosted feature flags. Install the CLI with `npm install -g acme`; the API reference below covers every endpoint.",
    ].join("\n"),
  },
}

llms.txt schließt mit zwei generierten Abschnitten, die keinerlei Konfiguration benötigen. Agent skills listet jeden über ai.skills veröffentlichten Skill samt seiner Beschreibung auf (dort sagt ein Skill, wann er zu verwenden ist). Agent resources verlinkt jedes maschinenlesbare Artefakt, das der Build erzeugt — llms-full.txt, den seitenweisen Roh-Markdown-Spiegel, den MCP-Server und sein Discovery-Dokument, den Skills-Index, den API-Katalog, agent-readability.json und die Sitemap — jeweils nur dann, wenn es existiert, sodass ein Agent, der nichts als llms.txt liest, dennoch die gesamte Oberfläche findet.

Um eine einzelne Seite aus beiden Dateien herauszuhalten, setze ai.exclude in deren Frontmatter:

---
title: Internal notes
ai:
  exclude: true
---

Die Seite wird weiterhin gerendert, bleibt in der Suche und behält ihren Platz in der Sitemap — nur die llms.txt-Dateien überspringen sie.

Um die volle Kontrolle über eine der beiden Dateien zu übernehmen, lege deine eigene llms.txt oder llms-full.txt in deinem public/-Ordner ab. Wie ein eigenes Favicon wird sie automatisch übernommen und anstelle der generierten Datei ausgeliefert — überschreibst du eine, generiert Blume die andere trotzdem.

Rohes Markdown

Hänge .md oder .mdx an die URL einer beliebigen Seite an, um deren rohe Markdown-Quelle abzurufen — perfekt für LLMs, Coding-Agents und “Als Markdown kopieren”-Workflows. Das steht für jede Seite zur Verfügung, in der Entwicklung wie in der Produktion, ohne jede Konfiguration.

URL Liefert
/quickstart Die gerenderte Seite
/quickstart.md Reines Markdown, mit umgewandelten Komponenten
/quickstart.mdx Die rohe MDX-Quelle, exakt wie geschrieben

Verschachtelte Routen funktionieren genauso (/content/syntax.md), und die Startseite wird unter /index.md ausgeliefert.

Die .md-Variante reduziert Komponenten auf reines Markdown für Konsumenten, die kein JSX interpretieren können: <TypeTable> wird zu einer Markdown-Tabelle, <Callout> zu einem beschrifteten Blockzitat, <Steps> zu einer nummerierten Liste, <Tabs> zu fett beschrifteten Abschnitten, <Card> zu seinem Titel als Link über seinem Rumpf (und <CardGroup> zu den Karten, die sie enthält) und <YouTube> zu einem Link. Props werden mit dem frontmatter der Seite im Gültigkeitsbereich ausgewertet, sodass ein Prop wie title={frontmatter.status} denselben Wert ergibt, den die gerenderte Seite anzeigt. Alles, was sich nicht originalgetreu umwandeln lässt — eine eigene Komponente oder ein aus einem Import berechnetes Prop — bleibt unverändert, und Komponenten-Markup innerhalb von Code-Blöcken wird nie angetastet. Dieselbe Umwandlung gilt für llms-full.txt und das get_page-Tool des MCP-Servers, sodass jede an Agents gerichtete Oberfläche sauberes Markdown liefert. Wenn du die untransformierte Quelle möchtest, verwende die .mdx-Variante.

Content Negotiation

Agents müssen die .md-Konvention nicht kennen: Wird die eigentliche URL einer Seite mit einem Accept: text/markdown-Header angefragt, wird die Markdown-Variante unter derselben Adresse ausgeliefert, mit Vary: Accept, damit Caches die beiden auseinanderhalten. Der Entwicklungsserver berücksichtigt den Header von Haus aus, und ein Vercel- oder Cloudflare-Server-Build verdrahtet dieselbe Aushandlung automatisch in das Deployment — Routing-Regeln auf Vercel, ein generierter Worker auf Cloudflare — ganz ohne Konfiguration. Die Startseite handelt immer aus, selbst wenn sie eine eigene Landing Page statt einer Inhaltsseite ist: Ihr Markdown-Spiegel fällt auf den llms.txt-Index zurück, sodass ein Agent, der beim Website-Stamm nach Markdown fragt, die maschinenlesbare Karte der Website erhält. Markdown-Antworten tragen außerdem einen x-markdown-tokens-Header — eine geschätzte Token-Anzahl (~4 Zeichen pro Token), gemäß der Konvention von Cloudflares Markdown for Agents — auf jeder Oberfläche, auf der Blume die Antwort-Header kontrolliert: dem Entwicklungsserver, serverseitig gerenderten Antworten und der ausgehandelten Startseite auf Vercel und Cloudflare. Andere Deploy-Ziele liefern vorgerenderte Seiten aus einer statischen Schicht ohne Hook zur Anfragezeit aus, sodass Agents dort die .md-URL direkt abrufen; das Agent-Readability-Manifest weist contentNegotiation nur auf Deployments aus, die den Header berücksichtigen.

Auch fehlende Seiten handeln aus. Jeder Build erzeugt eine Markdown-404-Seite unter /404.md — die Nicht-gefunden-Meldung, gefolgt von Rettungslinks zu jedem Abschnitt der obersten Ebene, zur Sitemap und zu llms.txt — und auf Vercel erhält eine Anfrage an eine nicht existierende URL, die Markdown bevorzugt, oder jede .md-URL ohne Seite dahinter, genau diesen Rumpf mit einem echten 404-Status statt der HTML-Hülle.

Eigene Komponenten-Serializer

Gib deinen eigenen Komponenten mit ai.markdownComponents eine Markdown-Form — eine Zuordnung von JSX-Name zu Serializer. Jeder Serializer erhält die props der Komponente (statisch aus den MDX-Attributen ausgewertet, mit dem frontmatter der Seite im Gültigkeitsbereich), ihre children (bereits auf Markdown reduziert) und die frontmatter-Daten der Seite und gibt den Ersatz zurück — oder null, um das JSX unverändert zu lassen:

import { defineConfig } from "blume";
import type { ComponentMarkdown } from "blume";

const chart: ComponentMarkdown = ({ props }) =>
  `![${props.title}](/charts/${props.slug}.png)`;

export default defineConfig({
  ai: {
    markdownComponents: {
      Chart: chart,
    },
  },
});

Für Container-Komponenten extrahiert childComponents("Name") direkte Kinder anhand des Tags — genauso, wie der eingebaute <Steps>-Serializer seine <Step>-Elemente sammelt —, und childBlocks() gibt jedes direkte Kind der Reihe nach zurück, Komponenten wie Prosa, jeweils bereits auf einen Markdown-Block heruntergestuft (der eingebaute <CardGroup>-Serializer ist einfach nur diese Blöcke, durch Leerzeilen verbunden). Ein gleichnamiger Eintrag ersetzt einen eingebauten Serializer, sodass du umgestalten kannst, wie <Callout> heruntergestuft wird — oder null zurückgeben, um eine Komponente vollständig auszunehmen.

Serializer leben in blume.config.ts, nicht in components.tsx: Die Konfigurationsdatei wird zur Build-Zeit ausgeführt, während die Komponentendatei nur statisch analysiert wird (sie darf .astro-Dateien importieren, die außerhalb des Website-Builds nicht laufen können). Deine Komponenten selbst bleiben genau wie zuvor in components.tsx registriert — markdownComponents ergänzt lediglich deren an Agents gerichtete Markdown-Form.

Als Markdown kopieren

Jede Seite verfügt über eine Aktion Als Markdown kopieren — bei den Seitenaktionen unterhalb des Inhaltsverzeichnisses —, die das rohe Markdown der Seite in die Zwischenablage kopiert. Es ist dieselbe Quelle, die unter der .md-URL oben ausgeliefert wird, bereit zum Einfügen in ein LLM, ein Issue oder deine Notizen. Sie ist auf jeder Seite verfügbar, in der Entwicklung wie in der Produktion, ohne jede Konfiguration.

Wo die Clipboard-API nicht verfügbar ist oder der Browser sie verweigert — In-App-Browser, WebViews, unsichere Origins —, greift die Aktion auf den althergebrachten Kopierbefehl zurück, und landet nichts in der Zwischenablage, meldet die Schaltfläche Kopieren fehlgeschlagen (lokalisiert über actions.copyFailed), statt stumm zu bleiben. Dieselbe Rückfallebene stützt jede Kopieren-Schaltfläche, die Blume rendert.

Im Chat öffnen

Die Aktion Im Chat öffnen öffnet die aktuelle Seite in einem KI-Assistenten — v0, ChatGPT, Claude, T3 Chat, Scira oder Cursor — vorbefüllt mit einem Prompt, der ihn auf das rohe Markdown der Seite verweist, sodass er Fragen zu dem beantworten kann, was du gerade liest:

Read https://your-site/this-page.md so I can ask you questions about this page.

Wie “Als Markdown kopieren” erfordert dies keine Einrichtung. Der Assistent ruft die Seite über ihre öffentliche URL ab, es funktioniert also, sobald die Seite bereitgestellt ist.

Der Prompt ist Teil des UI-Wörterbuchs (actions.openInChatPrompt), sodass lokalisierte Websites ihn in ihrer Sprache senden, und i18n.ui kann den Wortlaut überschreiben — behalte dabei den Platzhalter {url}, der durch die Roh-Markdown-URL der Seite ersetzt wird.

Um die Aktion anzupassen, setze ai.openInChat. false blendet sie vollständig aus, und ein Array von Anbieter-Schlüsseln — "v0", "chatgpt", "claude", "t3", "scira", "cursor" — zeigt genau diese Anbieter, in der Reihenfolge, in der du sie aufführst:

ai: {
  openInChat: ["claude", "chatgpt", "cursor"],
}

Um einen kopierfertigen Prompt direkt in deine Inhalte einzubetten — statt einer seitenweiten Aktion —, verwende die Prompt-Komponente, die eine beschriftete Zeile mit einer Schaltfläche Prompt kopieren und einem optionalen Link zum Öffnen in Cursor rendert.

Ask AI

Füge einen Assistenten hinzu, der Leserfragen in einem Chat-Panel auf der Seite beantwortet, gestützt auf einen streamenden Server-Endpunkt und das AI SDK:

ai: {
  ask: {
    enabled: true,
    provider: "gateway", // default
    model: "openai/gpt-5.5",
  },
}

Vorgeschlagene Fragen

Befülle den leeren Zustand mit einigen Einstiegs-Prompts. Jeder wird als anklickbarer Vorschlag gerendert — ein Klick sendet ihn ab — mit einem optionalen Lucide-Icon neben der Beschriftung:

ai: {
  ask: {
    enabled: true,
    suggestions: [
      { label: "What is Blume?", icon: "rocket" },
      { label: "How do I write a docs page?", icon: "file-text" },
      { label: "How do I configure the theme?", icon: "settings" },
    ],
  },
}

label ist die Frage, die gestellt wird; icon ist optional. Lässt du suggestions ungesetzt (oder leer), öffnet sich das Panel mit einem schlichten Eingabefeld.

Eigene Anweisungen

Ergänze mit instructions deinen eigenen System-Prompt-Text — Identität, Sprache, Tonfall oder alles andere, was der Assistent im Hinterkopf behalten soll:

ai: {
  ask: {
    enabled: true,
    instructions:
      "You are Bloomy, the Acme docs assistant. Answer in the language the question was asked in, and keep answers under three paragraphs.",
  },
}

Dein Text wird den eingebauten Anweisungen hinzugefügt, statt sie zu ersetzen: Der eingebaute Teil trägt den Fundierungs-Vertrag — nur aus den abgerufenen Seiten antworten, sie als Markdown-Links zitieren —, von dem die Zitate im Chat-Panel abhängen, er bleibt also unangetastet, was auch immer du ergänzt.

Fundierung

Ask AI ist in deiner Dokumentation fundiert. Für jede Frage ruft es die relevantesten Seiten ab — über denselben lexikalischen Orama-Index, der auch die Suche auf der Seite antreibt — und fügt sie in den System-Prompt des Modells ein, sodass Antworten aus deinen Inhalten stammen statt aus dem Eigenwissen des Modells. Dem Assistenten wird aufgetragen, nur aus den abgerufenen Seiten zu antworten, zu sagen, wenn etwas nicht abgedeckt ist, und die Seiten zu zitieren, aus denen er geschöpft hat.

Die Seite, auf der sich der Leser gerade befindet, wird dem Kontext zuerst hinzugefügt und dient dazu, den Abruf auf die Sprache dieser Seite einzugrenzen, sodass Antworten dort relevant bleiben, wo der Leser sich in der Dokumentation befindet. Der Abruf läuft zur Anfragezeit aus einem in den Build eingebackenen Snapshot, funktioniert also unabhängig von deinem Such-Anbieter — selbst wenn die Suche auf none gesetzt ist — und benötigt keine Konfiguration.

Die Fundierung ist für jedes Backend aktiv außer Inkeep, das seinen eigenen Abruf über die in seinem Dashboard indizierten Inhalte durchführt.

Abrufumfang

Wie viel Dokumentation eine Frage mit sich trägt, ist der größte Hebel dafür, wie lange der Leser auf das erste Wort wartet: Das Modell liest jedes eingefügte Zeichen, bevor es ein Token ausgibt. Bei einem gehosteten Spitzenmodell ist das unsichtbar, bei einem selbst gehosteten Backend dominiert es. retrieval bemisst ihn:

ai: {
  ask: {
    enabled: true,
    retrieval: {
      maxResults: 3, // fewer pages retrieved per question
      excerptChars: 1200, // shorter excerpt from each one
      contextBudget: 3000, // smaller total injection
    },
  },
}
Option Standard Beschreibung
maxResults 6 Pro Frage abgerufene Dokumente.
excerptChars 2000 Zeichen, die von jeder abgerufenen Seite behalten werden.
contextBudget 10000 Insgesamt eingefügte Zeichen, über alle Auszüge hinweg.

Die drei sind nicht austauschbar. contextBudget deckelt die gesamte Einfügung, excerptChars entscheidet, wie tief der Auszug in eine einzelne lange Seite hineinreicht — erhöhe es, wenn eine Seite die ganze Antwort enthält und der Auszug sie abschneidet —, und maxResults deckelt, wie viele Seiten der Abruf hinzufügt. Die Seite, die der Leser gerade betrachtet, wird zusätzlich zu den abgerufenen eingefügt, sodass eine Antwort bis zu eine Seite mehr zitieren kann, als maxResults angibt.

Die Standardwerte passen zu einem gehosteten Modell. Senke sie, wenn du von eigener Hardware ausspielst und die Zeit bis zum ersten Token wichtiger ist als die Trefferabdeckung; Antworten bleiben so oder so fundiert, und dem Assistenten wird aufgetragen, zu sagen, wenn etwas nicht abgedeckt ist, statt die Lücke zu füllen.

Externer Endpunkt

Du hast bereits ein API-Backend für KI? Richte das Panel darauf aus und halte den Dokumentations-Build statisch:

ai: {
  ask: {
    enabled: true,
    endpoint: "https://api.example.com/v1/docs/ask",
  },
}

Blume sendet denselben POST-Body wie seine eingebaute Route:

{
  "messages": [{ "role": "user", "content": "How do I deploy?" }],
  "page": { "path": "/deployment" }
}

Gib eine erfolgreiche Antwort zurück, deren Body ein reiner UTF-8-Textstream ist. Liegt der Endpunkt auf einem anderen Origin, erlaube den Dokumentations-Origin per CORS: Akzeptiere OPTIONS und POST, lass den content-type-Request-Header zu und gib die CORS-Header sowohl beim Preflight als auch bei der gestreamten Antwort zurück. Mit gesetztem endpoint erzeugt Blume die Chat-Oberfläche, aber keine Server-Route, keinen Fundierungs-Snapshot, keine Anbieter-Abhängigkeit und keine Warnung zu Anbieter-Geheimnissen; dein Backend verantwortet Abruf, Authentifizierung, Ratenbegrenzung, Modellzugriff und Zitate.

Server-Ausgabe erforderlich

Blumes eingebautes Ask-AI-Backend ist eine Server-Route (POST /api/ask) und kann daher nicht auf einem statischen Build laufen. Wechsle zur Server-Ausgabe und wähle einen Adapter:

deployment: {
  output: "server",
  adapter: "vercel",
}

Ein statischer Build mit aktiviertem Ask AI und ohne externen endpoint schlägt sofort fehl, mit einer Meldung, die dich auffordert, deployment.output auf server zu setzen. Siehe Deployment für die Adapter.

Backends

Standardmäßig läuft Ask AI über das Vercel AI Gateway: model ist eine provider/model-Zeichenkette, du wechselst Modelle also durch deren Änderung (openai/gpt-5.5, anthropic/claude-sonnet-4-5 und so weiter), ohne ein Anbieter-SDK installieren zu müssen. Das Gateway liest AI_GATEWAY_API_KEY aus deiner Umgebung und wird bei einem Deployment auf Vercel automatisch verdrahtet.

Setze provider, um Ask AI anderswohin zu leiten. Jedes Backend liest seinen API-Schlüssel aus einer Umgebungsvariablen und streamt über ein Anbieter-SDK, das du in deinem Projekt installierst — nur dasjenige, das du verwendest:

provider model Umgebungsvariable für den API-Schlüssel Zu installierendes SDK
gateway (Standard) eine provider/model-Zeichenkette über das AI Gateway AI_GATEWAY_API_KEY keines — wird mit Blume mitgeliefert
openrouter ein beliebiges OpenRouter-Modell OPENROUTER_API_KEY @openrouter/ai-sdk-provider
llmgateway ein beliebiges LLMGateway-Modell LLMGATEWAY_API_KEY @ai-sdk/openai-compatible
inkeep ein Inkeep-QA-Modell INKEEP_API_KEY @ai-sdk/openai-compatible
openai-compatible was auch immer dein Endpunkt ausliefert mit apiKeyEnv gesetzt @ai-sdk/openai-compatible

Die SDKs sind optionale Peer-Abhängigkeiten, füge deinem Projekt also dasjenige hinzu, das dein Backend benötigt (z. B. npm install @openrouter/ai-sdk-provider). Fehlt es, warnt der Build mit dem exakten Paketnamen, bevor Vite den Import nicht auflösen könnte.

Um zum Beispiel OpenRouter zu verwenden:

ai: {
  ask: {
    enabled: true,
    provider: "openrouter",
    model: "anthropic/claude-sonnet-4-5",
  },
}

Jeder OpenAI-kompatible Endpunkt funktioniert über openai-compatible — gib die baseUrl und die Umgebungsvariable an, die seinen Schlüssel enthält:

ai: {
  ask: {
    enabled: true,
    provider: "openai-compatible",
    baseUrl: "https://my-gateway.example.com/v1",
    apiKeyEnv: "MY_GATEWAY_API_KEY",
    model: "gpt-4o",
  },
}

Setze apiKeyEnv (und, bei den benannten Anbietern, baseUrl) bei jedem Backend, um auf eine andere Umgebungsvariable oder einen Proxy zu verweisen.

Schlüssel werden mit process.env gelesen, was die Adapter Node, Vercel und Netlify abdeckt. Auf Cloudflare stellst du den Schlüssel über das Runtime-Binding der Plattform bereit. Das Aktivieren von Ask AI schaltet außerdem React für die Insel auf der Seite ein — siehe Anpassung.

Ratenbegrenzung

Der Endpunkt POST /api/ask ist nicht authentifiziert — er muss es sein, damit der Assistent auf der Seite ihn aufrufen kann. Blume validiert jede Anfrage — weist fehlerhafte Bodys ab, begrenzt sie auf 1–40 Nachrichten und akzeptiert nur die Rollen user/assistant, sodass ein Aufrufer keinen eigenen System-Prompt einschleusen und die Route nicht als allgemeinen LLM-Proxy zweckentfremden kann —, um zu begrenzen, wie viel ein einzelner Aufruf gegen dein Modell verbrauchen kann; es kann jedoch niemanden davon abhalten, den Endpunkt wiederholt aufzurufen. Falls Kostenmissbrauch ein Anliegen ist, stelle der Route eine Ratenbegrenzung voran — die Edge-Ratenbegrenzung deines Hosters (z. B. die von Vercel), eine Middleware oder die Ausgabelimits pro Schlüssel deines Modellanbieters.

MCP-Server

Hoste einen Model-Context-Protocol-Server, damit Coding-Agents (Claude Code, Cursor, VS Code, claude.ai-Connectors) deine Dokumentation direkt durchsuchen und lesen können — ohne Scraping:

ai: {
  mcp: {
    enabled: true,
    route: "/mcp", // where the server is mounted
  },
}
Option Standard Beschreibung
enabled false Den MCP-Server erzeugen und hosten.
route /mcp Pfad, unter dem der Streamable-HTTP-Endpunkt hängt.
name Titel Clients angezeigter Servername (Standard: Titel).
instructions Optionaler System-Hinweis für verbindende Agents.

Der Server stellt schreibgeschützte Tools bereit — search_docs, get_page, list_pages und get_navigation — sowie jede Seite als MCP-Ressource (resources/list zählt die Seiten unter ihren ausgelieferten URLs mit dem Typ text/markdown auf; resources/read gibt das Agent-Markdown der Seite zurück, dieselbe Ausgabe wie get_page), sodass Clients, die Kontext per URI anhängen, die Dokumentation durchstöbern können, ohne ein Tool aufzurufen. Er veröffentlicht Discovery-Dokumente unter /.well-known/mcp.json und /.well-known/mcp/server-card.json. Die Server Card folgt dem Server-Card-Erweiterungsschema SEP-2127 (Reverse-DNS-name, remotes-Transport-Endpunkte), mit initialize-förmigen Kompatibilitätsfeldern (serverInfo, capabilities, transports) für Scanner, die gegen die frühere Revision des Vorschlags gebaut wurden. Das Menü Mit MCP verbinden jeder Seite bietet eine Kopieren-und-loslegen-Installation für Claude Code, Cursor, VS Code und Codex (angezeigt, sobald deployment.site gesetzt ist).

search_docs betreibt seinen eigenen Volltextindex und funktioniert damit unabhängig von deinem Such-Anbieter — und sogar, wenn die Suche auf none gesetzt ist. Der MCP-Server ist eine von der Suche auf der Seite getrennte Funktion.

search_docs und list_pages akzeptieren beide einen optionalen contentTypes-Filter, der die Ergebnisse auf Seiten der angegebenen Frontmatter-types eingrenzt — ["rfc"], ["blog", "changelog"] —, sodass ein Agent, der mit einer Website arbeitet, die Dokumentation mit RFCs, Runbooks oder Richtlinien mischt, den Abruf auf die Art von Seite eingrenzen kann, die er benötigt. Jedes Ergebnis nennt seinen Inhaltstyp, und die Ausgabe von list_pages zeigt die verwendeten Typen.

Beide Tools akzeptieren außerdem ein filters-Objekt, das gegen die Facetten abgleicht, die eine Website pro Inhaltstyp deklariert (content.types.<type>.facets) — eigene Frontmatter-Schlüssel, deren Werte zu filterbaren Metadaten werden:

{
  "query": "OpenAPI request schemas",
  "contentTypes": ["rfc"],
  "filters": { "domain": "architecture", "status": "enforced" }
}

Jeder filters-Eintrag muss zutreffen (Ergebnisse tragen ihre Facettenwerte, und list_pages zeigt die jeder Seite), sodass eine Wissensdatenbank Agent-Workflows mit progressiver Offenlegung antreiben kann — die verbindlichen Standards aufzählen, nur innerhalb dieser suchen — ganz ohne eigenen Server.

Server-Ausgabe erforderlich

Der MCP-Server ist ein aktiver Endpunkt (/mcp) und kann daher nicht auf einem statischen Build laufen. Wechsle zur Server-Ausgabe und wähle einen Adapter:

deployment: {
  output: "server",
  adapter: "node", // or "vercel" | "netlify" | "cloudflare"
  site: "https://docs.example.com",
}

Ein statischer Build mit ai.mcp.enabled schlägt sofort fehl, mit einer Meldung, die dich auffordert, deployment.output auf server zu setzen. Siehe Deployment für die Adapter. Nach dem Deployment verbindest du dich aus Claude Code heraus mit:

claude mcp add --transport http my-docs https://docs.example.com/mcp

JSON-API

Jede Blume-Website liefert ihre Dokumentation außerdem über eine kleine schreibgeschützte JSON-API aus — das REST-Pendant zu den Tools des MCP-Servers, über denselben Seiten-Snapshot, für Agents und Function-Calling-Frameworks, die schlichtes HTTP sprechen statt MCP. Sie ist standardmäßig aktiviert und benötigt keinerlei Konfiguration:

Endpunkt Liefert
/api/docs/pages.json Jede Seite mit ihrer Route, ihrem Titel, ihrer Beschreibung, ihrem Inhaltstyp, ihrem Locale, ihren Facetten und den URLs ihrer gerenderten, ihrer Markdown- und ihrer JSON-Form.
/api/docs/pages/{route}.json Eine Seite: ihr Indexeintrag plus das Agent-Markdown (derselbe Rumpf, den get_page zurückgibt). {route} ist die Seitenroute ohne führenden Schrägstrich, index für die Startseite.
/api/docs/navigation.json Der Navigationsbaum — Header-Tabs und die Hierarchie der Seitenleiste.
/api/docs/search?q= Volltextsuche, mit derselben Eingrenzung über limit, contentTypes, locale, version und filters[key] wie bei search_docs. Nur bei Server-Ausgabe.
/openapi.json Die OpenAPI-3.1-Beschreibung der gesamten maschinenlesbaren Oberfläche.

Der Seitenindex, die seitenweisen Dokumente und die Navigation werden vorgerendert, eine statische Website liefert sie also als Dateien von jedem beliebigen Hoster aus. Die Suche ist ein aktiver Endpunkt und existiert nur unter Server-Ausgabe, wo sie denselben Index betreibt wie search_docs. Fehler sind Problem Details nach RFC 9457 (application/problem+json) mit einem stabilen code, einem detail und einem resolution-Hinweis, der dem Agent sagt, wohin er als Nächstes gehen soll — eine fehlende Seite, eine leere Suchanfrage oder, bei Server-Ausgabe, jede /api/…-URL, die kein Endpunkt beantwortet:

{
  "code": "API_ROUTE_NOT_FOUND",
  "detail": "No API route exists at /api/nope.",
  "instance": "/api/nope",
  "links": [
    {
      "href": "https://docs.example.com/openapi.json",
      "label": "OpenAPI description"
    },
    {
      "href": "https://docs.example.com/api/docs/pages.json",
      "label": "Page index"
    }
  ],
  "resolution": "Discover the available operations through the OpenAPI description at https://docs.example.com/openapi.json, or list every page at https://docs.example.com/api/docs/pages.json.",
  "status": 404,
  "title": "API route not found",
  "type": "about:blank"
}

Das OpenAPI-Dokument unter /openapi.json wird pro Build aus deiner Konfiguration erzeugt, es beschreibt also nur das, was die bereitgestellte Website tatsächlich ausliefert: jeden JSON-Endpunkt mit eindeutiger operationId, typisierten Parametern und Antwortschemata, dazu die Text-Oberflächen daneben — die .md-Spiegel, llms.txt und llms-full.txt, agent-readability.json — sowie den MCP-Endpunkt, wenn er aktiviert ist. Frameworks, die Tools aus einer OpenAPI-Beschreibung bauen, erhalten dieselbe Reichweite wie ein MCP-Client. Das Dokument ist aus dem API-Katalog, dem Readability-Manifest, dem Link-Header der Startseite als rel="service-desc" und aus llms.txt verlinkt.

Nichts davon rührt an deine eigene API-Referenz: Eine dokumentierte Spezifikation wird in Seiten gerendert, niemals unter /openapi.json ausgeliefert, und der Katalog führt beide auf. Eine public/openapi.json, die du selbst ausliefert, übernimmt diese Route (die JSON-Endpunkte bleiben). Der /api/…-Catch-all tritt zur Seite, wenn ein Dokumentationsabschnitt aus dem /api-Namensraum ausgeliefert wird (content/api/overview.md) oder eine eigene Seite eine Rest-Route unter /api/ besitzt, sodass diese Seiten weiterhin gewinnen. Setze ai.api auf false, um nichts davon zu veröffentlichen:

ai: {
  api: false,
}

Agent-Lesbarkeit

Blume schreibt ein Manifest /agent-readability.json in den Stamm deiner Website, das die in dieser Seite beschriebene, an Agents gerichtete Oberfläche indexiert — sodass ein Agent sie mit einem einzigen Abruf entdecken kann, statt Konventionen zu erraten oder HTML zu scrapen. Wie llms.txt ist es standardmäßig aktiviert:

seo: {
  agentReadability: true,
}

Das Manifest listet nur auf, was du aktiviert hast — das Spiegelmuster für rohes Markdown, die JSON-API und ihre OpenAPI-Beschreibung, llms.txt und llms-full.txt, den MCP-Server und sein Discovery-Dokument, den Ask AI-Endpunkt, die Sitemap und RSS-Feeds — neben deinem Website-Namen, der Beschreibung, dem Quell-Repository und der Content-Signal-Nutzungsrichtlinie. URLs sind absolut, wenn deployment.site gesetzt ist, andernfalls stammrelativ:

{
  "artifacts": {
    "markdown": {
      "contentNegotiation": "text/markdown",
      "pattern": "https://docs.example.com/{route}.md"
    },
    "api": {
      "openapi": "https://docs.example.com/openapi.json",
      "pages": "https://docs.example.com/api/docs/pages.json",
      "search": "https://docs.example.com/api/docs/search"
    },
    "llmsFullTxt": "https://docs.example.com/llms-full.txt",
    "llmsTxt": "https://docs.example.com/llms.txt",
    "mcp": {
      "discovery": "https://docs.example.com/.well-known/mcp.json",
      "url": "https://docs.example.com/mcp"
    }
  },
  "description": "Docs for the Acme API.",
  "generator": "blume@1.0.0",
  "name": "Acme Docs",
  "site": "https://docs.example.com",
  "contentUsage": { "search": true, "ai-input": true, "ai-train": true },
  "repository": "https://github.com/acme/docs"
}

Das Feld contentNegotiation erscheint nur, wenn die bereitgestellte Website den Header Accept: text/markdown tatsächlich berücksichtigt — siehe Content Negotiation; bei jedem anderen Deployment weist das Manifest nur das .md-Spiegelmuster aus.

Setze seo.agentReadability auf false, um es zu überspringen, oder liefere deine eigene public/agent-readability.json aus, um zu übernehmen — Blume überschreibt niemals eine Datei, die du in public/ ablegst.

Agents, die eine Website sondieren, wissen nicht, dass sie nach dem Manifest suchen sollen — deshalb weist Blume es zusätzlich in einem Link-Antwort-Header nach RFC 8288 auf der Startseite aus, unter Verwendung IANA-registrierter Beziehungstypen:

Link: </.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+json",
  </openapi.json>; rel="service-desc"; type="application/json",
  </agent-readability.json>; rel="describedby"; type="application/json",
  </llms.txt>; rel="describedby"; type="text/plain",
  </index.md>; rel="alternate"; type="text/markdown"

Jeder Eintrag erscheint nur, wenn die zugehörige Funktion aktiv ist. Der alternate-Link verweist auf den Markdown-Spiegel der Startseite — das rohe Markdown der Seite selbst, wenn die Startroute eine Inhaltsseite ist, oder den synthetisierten llms.txt-Fallback, wenn sie eine Landing Page ist. Der service-desc-Link (RFC 8631) verweist auf die OpenAPI-Beschreibung der JSON-API, und api-catalog auf den generierten API-Katalog. Der Header wird auf jeder von Blume kontrollierten Oberfläche mitgeliefert: dem Entwicklungsserver (prüfe ihn mit curl -I localhost:4321), statischen Builds über die erzeugte _headers-Datei (Netlify und Cloudflare) und Vercel-Server-Builds über die Routing-Regeln des Deployments.

Nicht jeder Agent tritt jedoch über den Stamm ein — wer einem Suchergebnis oder einem geteilten Link folgt, landet auf einer tief liegenden Seite und sieht den Header der Startseite nie. Deshalb trägt jede gerenderte Seite dieselben Discovery-Links auch in ihrem HTML-<head>, unter Verwendung derselben IANA-registrierten Beziehungen:

<link
  rel="describedby"
  href="/agent-readability.json"
  type="application/json"
/>
<link rel="describedby" href="/llms.txt" type="text/plain" />
<link rel="alternate" href="/docs/example.md" type="text/markdown" />

Hier verweist der alternate-Link auf den Roh-Markdown-Spiegel der jeweiligen Seite, sodass ein Agent direkt von dem HTML, auf dem er gelandet ist, zur token-effizienten Version springen kann. Da die Head-Links mit dem vorgerenderten HTML mitreisen, funktionieren sie auch bei Hostern, die _headers ignorieren und überhaupt keine eigenen Antwort-Header senden können (GitHub Pages, S3) — ganz gleich, über welche Seite der Agent eintritt.

API-Katalog

Wenn die Website APIs veröffentlicht, generiert Blume einen API-Katalog nach RFC 9727 unter /.well-known/api-catalog — ein Linkset, das Agents ermöglicht, deine APIs allein von der Domain aus aufzuzählen, ausgeliefert mit seinem registrierten Medientyp application/linkset+json auf jeder Build-Oberfläche. Es gibt nichts zu konfigurieren: Der Katalog wird aus dem abgeleitet, was ohnehin in blume.config.ts steht. Jede OpenAPI- oder AsyncAPI-Referenz wird zu einem Eintrag, verankert an ihrer gerenderten Dokumentationsroute, wobei service-doc auf diese Dokumentation zeigt und service-desc auf die Spezifikation, sofern sie unter einer abrufbaren URL liegt; die eigene JSON-API der Website wird zu einem Eintrag, der durch ihre /openapi.json beschrieben wird; und der MCP-Server wird zu einem Eintrag mit seinem Discovery-Dokument als Dienstbeschreibung:

{
  "linkset": [
    {
      "anchor": "https://docs.example.com/reference",
      "service-doc": [
        { "href": "https://docs.example.com/reference", "type": "text/html" }
      ],
      "service-desc": [{ "href": "https://api.example.com/openapi.json" }]
    },
    {
      "anchor": "https://docs.example.com/api/docs",
      "service-desc": [
        {
          "href": "https://docs.example.com/openapi.json",
          "type": "application/json"
        }
      ],
      "service-doc": [
        { "href": "https://docs.example.com/", "type": "text/html" }
      ]
    },
    {
      "anchor": "https://docs.example.com/mcp",
      "service-desc": [
        {
          "href": "https://docs.example.com/.well-known/mcp.json",
          "type": "application/json"
        }
      ],
      "service-doc": [
        { "href": "https://docs.example.com/", "type": "text/html" }
      ]
    }
  ]
}

Eine Website ohne API-Referenzen, ohne MCP-Server und mit abgeschalteter JSON-API erzeugt keinen Katalog — es stünde nichts darin. Wie überall gewinnt eine Datei public/.well-known/api-catalog, die du selbst auslieferst, gegenüber der generierten.

WebMCP

WebMCP ist eine aufkommende Browser-API, mit der eine Seite Tools direkt bei einem agentischen Browser registrieren kann — ohne separate Serververbindung. Jede Blume-Seite registriert die schreibgeschützte Oberfläche der Dokumentation im Modellkontext der Seite: search_docs (Website-Suche), get_page (das rohe Markdown einer Seite) und list_pages (den llms.txt-Index). Das Skript ist winzig, lädt keinerlei Suchmaschinerie, bis ein Tool tatsächlich aufgerufen wird, und tut in jedem Browser ohne die API stillschweigend nichts — was heute auf alle außerhalb der Early Preview von Chrome zutrifft. Es registriert sich an derjenigen Oberfläche, die die im Fluss befindliche Spezifikation bereitstellt (navigator.modelContext oder document.modelContext), über provideContext oder das tool-weise registerTool.

Es ist standardmäßig aktiviert; setze webmcp: false, um es abzuwählen:

ai: {
  webmcp: false,
}

Skills-Discovery

Wenn dein Projekt Agent Skills mitliefert — das Blume-Repo selbst tut das —, richte ai.skills auf das Verzeichnis aus, das sie enthält, und der Build veröffentlicht sie zur Discovery gemäß dem Agent Skills Discovery RFC:

ai: {
  skills: "./skills",
}

Der Pfad wird relativ zu deinem Projektstamm aufgelöst, und jedes Unterverzeichnis mit einer SKILL.md wird zu einem veröffentlichten Skill. Ein Skill, der aus einer einzelnen SKILL.md besteht, wird wortgetreu nach /.well-known/agent-skills/<name>/SKILL.md kopiert (type: "skill-md"); ein Skill mit unterstützenden Ressourcen (scripts/, references/, assets/) wird in ein deterministisches .tar.gz gebündelt (type: "archive"), damit seine relativen Verweise nach dem Entpacken aufgelöst werden, unter Beibehaltung der Ausführungs-Bits von Skripten. Der Discovery-Index unter /.well-known/agent-skills/index.json trägt das $schema der Version v0.2.0 und pro Skill dessen Namen, Typ, Beschreibung (aus dem Frontmatter der SKILL.md), Artefakt-URL sowie den SHA-256-Digest, gegen den Clients Downloads verifizieren.

Skills mit fehlendem oder spezifikationswidrigem name/description werden mit einer Build-Warnung übersprungen, statt fehlerhaft veröffentlicht zu werden, und eine public/.well-known/agent-skills/index.json, die du selbst auslieferst, übernimmt die gesamte Oberfläche.

DNS-basierte Discovery (DNS-AID)

DNS for AI Discovery ist ein aufkommender IETF-Entwurf, mit dem Agents die KI-Oberfläche einer Website entdecken können, bevor sie eine einzige HTTP-Anfrage stellen, indem sie ServiceMode-SVCB/HTTPS-Einträge an einem wohlbekannten DNS-Einstiegspunkt abfragen. DNS-Einträge liegen in deiner Zone, nicht im Build, weshalb dies die eine Discovery-Oberfläche ist, die Blume nicht für dich veröffentlichen kann — lege stattdessen einen Eintrag bei deinem DNS-Anbieter an:

_index._agents.docs.example.com. 3600 IN HTTPS 1 docs.example.com. alpn=h2

Verwende den Eintragstyp HTTPS, wenn dein Anbieter ihn anbietet (Vercel DNS tut das; den reinen SVCB-Typ unterstützt es nicht), andernfalls einen ServiceMode-SVCB-Eintrag mit den Parametern alpn und port. Der Entwurf empfiehlt außerdem, die Zone mit DNSSEC zu signieren, damit validierende Resolver authentifizierte Antworten zurückgeben — Anbieter wie Cloudflare aktivieren das mit einem Klick, während manche (darunter Vercel DNS) es überhaupt nicht unterstützen.

blume audit --url <origin> prüft das für dich: Wenn deployment.site gesetzt ist, fragt die Netzwerkstufe den Einstiegspunkt über DNS-over-HTTPS ab und meldet den exakt zu veröffentlichenden Eintrag, falls keiner existiert, sowie ob die Antworten DNSSEC-authentifiziert sind. Setze BLUME_DOH_URL, um die Abfrage auf deinen eigenen Resolver zu richten, falls dein Netzwerk die öffentlichen (Google, Cloudflare) blockiert.

Web Bot Auth

Web Bot Auth wirkt in die andere Richtung: Es geht nicht darum, dass Agents deine Dokumentation lesen, sondern darum, dass die Agents deiner Organisation sich ausweisen, wenn sie anderswo Anfragen stellen. Deine Agents signieren ihre Anfragen mit HTTP Message Signatures, und empfangende Websites verifizieren sie gegen ein auf deiner Domain veröffentlichtes Verzeichnis öffentlicher Schlüssel. Wenn deine Organisation Agents betreibt und deine Blume-Website unter der Domain liegt, als die sie sich ausweisen, veröffentliche deren öffentliche Schlüssel:

ai: {
  webBotAuth: {
    keys: [{ kty: "OKP", crv: "Ed25519", x: "JrQLj5P_89iXES9-vFgrIy29c…" }],
  },
}

Blume liefert das JWKS dann unter /.well-known/http-message-signatures-directory mit seinem registrierten Medientyp auf jeder Build-Oberfläche aus. Das Verzeichnis ist per Definition öffentlich, weshalb die Konfiguration nur öffentliche Schlüssel zulässt — ein JWK, das privates Material enthält (d, p, q, …), scheitert an der Validierung mit einem Fehler, statt eine geleakte Zugangsberechtigung auszuliefern. Erzeuge ein Ed25519-Paar mit:

node -e 'const { generateKeyPairSync } = require("node:crypto"); const { publicKey, privateKey } = generateKeyPairSync("ed25519"); console.log("public: ", JSON.stringify(publicKey.export({ format: "jwk" }))); console.log("private:", JSON.stringify(privateKey.export({ format: "jwk" })))'

Der öffentliche JWK kommt in die obige Konfiguration; der private dorthin, wo dein signierender Agent läuft (ein Secret Manager, niemals das Repo). Wenn deine Organisation keine Agents betreibt, überspringe dies — ein leeres Verzeichnis weist nichts aus, das der Verifikation wert wäre.

Da blume.config.ts zur Build-Zeit ausgeführt wird, muss der Schlüssel nicht fest einprogrammiert sein — lade ihn aus einer Build-Zeit-Umgebungsvariablen, um die Konfiguration frei von Schlüssel-Blobs zu halten und ohne Commit zu rotieren:

const webBotAuthKey = process.env.WEB_BOT_AUTH_PUBLIC_JWK;

export default defineConfig({
  ai: {
    webBotAuth: {
      keys: webBotAuthKey ? [JSON.parse(webBotAuthKey)] : [],
    },
  },
});

Umgebungen ohne die Variable veröffentlichen kein Verzeichnis, und ein so geladener Schlüssel wird genauso validiert wie ein inline angegebener — einschließlich der Prüfung auf privates Material. (Der öffentliche Schlüssel ist kein Geheimnis, ihn inline zu committen ist also genauso in Ordnung; die Umgebungsvariable ist eine Frage der Ergonomie, keine der Sicherheit.)

Agent Skill

Baust du eine Blume-Website mit Hilfe eines Coding-Agents? Installiere den Blume-Agent Skill, damit er weiß, wie Blume funktioniert, ohne dass du es ihm erklärst:

npx skills add haydenbleasel/blume

Der Skill vermittelt dem Agent, was Blume ist und wie man eine Website aufsetzt, schreibt und konfiguriert, und verweist ihn auf die vollständige, im installierten Paket mitgelieferte Dokumentation (das Verzeichnis docs/ innerhalb von blume, wo auch immer dein Paketmanager es installiert).

Er ist einer der von Blume mitgelieferten Agent Skills, neben einem Skill, der die Dokumentation aus einem geplanten Agent-Lauf heraus mit deinem Produkt synchron hält.

War diese Seite hilfreich?