Benutzerdefinierte Seiten
Binde vollständig benutzerdefinierte .astro-Routen neben deiner Dokumentation ein und lies Konfiguration, Navigation und Inhalte deiner Website aus dem Modul blume:data.
Der Großteil einer Blume-Website besteht aus Markdown, aber manchmal benötigst du eine Route, die kein Dokument ist — eine Landingpage, eine Preisseite, einen handgebauten Blog- oder Changelog-Index oder ein interaktives Dashboard. Lege eine .astro-Datei in deinem pages-Ordner ab, und Blume bindet sie als echte Route ein, direkt neben deinen Inhalten.
Eine Seite hinzufügen
Erstelle im Stammverzeichnis deines Projekts einen pages/-Ordner und füge eine .astro-Datei hinzu:
---
import data from "blume:data";
---
<h1>Pricing for {data.config.title}</h1>
blume dev erkennt sie sofort, und blume build rendert sie vorab zu statischem HTML. Der Ordnername ist über content.pages konfigurierbar (Standard "pages").
Benutzerdefinierte Seiten behalten ihren ursprünglichen Speicherort auf der Festplatte, sodass relative Importe, Komponentenimporte und getStaticPaths genau so funktionieren wie in einem einfachen Astro-Projekt — Blume bindet jede Datei dort ein, wo sie liegt, statt sie zu kopieren.
Dateien und Routen
Der Pfad jeder Datei innerhalb des pages-Ordners wird zu ihrer Route. index verweist auf den übergeordneten Ordner, und dynamische [param]-Segmente bleiben erhalten:
| Datei | Route |
|---|---|
pages/pricing.astro |
/pricing |
pages/blog/index.astro |
/blog |
pages/blog/[slug].astro |
/blog/:slug |
pages/changelog.astro |
/changelog |
Eine benutzerdefinierte Seite gewinnt gegenüber einer generierten Route mit demselben Pfad. Fügt man beispielsweise pages/changelog.astro hinzu, ersetzt das Blumes generierte Changelog-Zeitleiste durch deine eigene.
Website-Daten lesen
Importiere blume:data, um dieselbe aufgelöste Konfiguration, Navigation, Routen und Feeds zu lesen, die auch der Rest der Website verwendet:
---
import data from "blume:data";
---
<h1>All pages</h1>
<ul>
{
data.routes
.filter((route) => route.indexable)
.map((route) => (
<li>
<a href={route.path}>{route.title}</a>
</li>
))
}
</ul>
Innerhalb eines Blume-Projekts ist das Modul automatisch typisiert. Du kannst die Struktur mit import type { BlumeData } from "blume" auch explizit einbinden — für typisierte Hilfsfunktionen, Props oder deine eigene tsconfig:
import type { BlumeData, BlumeRoute } from "blume";
const indexable = (data: BlumeData): BlumeRoute[] =>
data.routes.filter((route) => route.indexable);
Das Modul stellt Folgendes bereit:
configBlumeDataConfig
Aufgelöste Website-Einstellungen: title, description, logo, favicon, appleIcon, banner, theme, site, repoUrl, github (owner, repo, host und die REST-API-Basis — null, wenn nicht gesetzt), search, i18n, mcp, ask, og, analytics, feedback, structuredData, toc, codeThemes, codeWrap und imageZoom.
BlumeDataConfignavigationNavigation
Die aus deinen Inhalten abgeleitete Seitenleiste, Tabs und Auswahlelemente (Standard-Locale).
NavigationnavigationByLocaleRecord<string, Navigation>
Navigationsbäume pro Locale, indiziert nach Locale-Code. Leer, sofern i18n nicht konfiguriert ist.
Record<string, Navigation>routesBlumeRoute[]
Jede Inhaltsseite: { id, path, title, locale, indexable, hidden, draft, fallback, editUrl, lastModified, alternates, collection, entryId }.
BlumeRoute[]feedsBlumeFeed[]
Generierte RSS-Feeds: { href, title }.
BlumeFeed[]fontCssVarsstring[]
CSS-Variablennamen für die konfigurierten Schriftarten (Astro-<Font>-Integration).
string[]uiUIStrings
Aufgelöste UI-Chrome-Zeichenketten für die Standard-Locale (Beschriftungen für Suche, Seitenleiste und Fußzeile).
UIStringsuiByLocaleRecord<string, UIStrings>
UI-Zeichenketten pro Locale, indiziert nach Locale-Code. Leer, sofern i18n nicht konfiguriert ist.
Record<string, UIStrings>routes enthält Seiten-Metadaten, aber kein Frontmatter wie type oder date. Um eine nach Inhaltstyp gefilterte Liste zu erstellen — einen Blog- oder Changelog-Index — kombiniere es mit Astros docs-Inhaltssammlung, die das Frontmatter enthält:
---
import { getCollection } from "astro:content";
import data from "blume:data";
// Each route's id matches its collection entry id.
const routeById = new Map(data.routes.map((route) => [route.id, route.path]));
const posts = (await getCollection("docs"))
.filter((entry) => entry.data.type === "blog" && !entry.data.draft)
.map((entry) => ({
description: entry.data.description,
href: routeById.get(entry.id),
title: entry.data.title,
}));
---
<ul>
{
posts.map((post) => (
<li>
<a href={post.href}>{post.title}</a>
<p>{post.description}</p>
</li>
))
}
</ul>
Laufzeit-Hilfsfunktionen
blume/runtime bündelt die gängigen Datenmuster, sodass du nicht auf die Interna von blume:data zugreifen musst.
getBlumeCollection(data, query?) wählt Inhaltsrouten aus — gefiltert nach Sammlung, Locale oder Pfadpräfix, wobei Entwürfe und ausgeblendete Seiten ausgeschlossen und die Ergebnisse nach Pfad sortiert werden — genau das, was ein benutzerdefinierter Index braucht:
---
import data from "blume:data";
import { getBlumeCollection } from "blume/runtime";
const posts = getBlumeCollection(data, { prefix: "/blog" });
---
<ul>
{posts.map((post) => (
<li><a href={post.path}>{post.title}</a></li>
))}
</ul>
<BlumePage> rendert den Textkörper eines Inhaltseintrags innerhalb einer benutzerdefinierten Seite, wobei Blumes integrierte MDX-Komponenten (Hinweisboxen, Karten, Schritte …) bereits eingebunden sind — um ein Dokument auf einer Landingpage hervorzuheben oder einen maßgeschneiderten Index zu bauen, der echte Inhalte zeigt:
---
import BlumePage from "blume/components/BlumePage.astro";
import data from "blume:data";
import { getBlumeCollection } from "blume/runtime";
const [intro] = getBlumeCollection(data, { prefix: "/docs" });
---
{intro && <BlumePage id={intro.entryId} />}
Übergib components, um eigene Überschreibungen oder Islands hinzuzufügen (die in der generierten Laufzeitumgebung liegen und standardmäßig nicht importiert werden), und collection, um aus einer anderen Sammlung als "docs" zu lesen.
Das Website-Layout verwenden
RootLayout verleiht einer benutzerdefinierten Seite das komplette Dokumentations-Chrome — Kopfzeile, Seitenleiste, Suche, Inhaltsverzeichnis und Theme —, indem es sie in dasselbe dreispaltige Raster einbettet, das auch die generierten Seiten verwenden. Für eine Landing- oder Marketingseite steht dieses Raster im Weg, greif deshalb stattdessen zu PageLayout: Es liefert die Dokumenthülle, die Kopfzeile, das Theme und die Schriftarten und dann einen einzelnen <slot /> über die volle Breite (keine Seitenleiste, kein Fließtext, kein Inhaltsverzeichnis). Ein optionaler footer-Slot wird nach <main> gerendert:
---
import PageLayout from "blume/components/layout/PageLayout.astro";
import data from "blume:data";
import Footer from "./_home/Footer.astro";
const { config } = data;
---
<PageLayout
site={{ title: config.title, description: config.description }}
logo={config.logo}
banner={config.banner}
analytics={config.analytics}
navigation={data.navigation}
favicon={config.favicon}
fontCssVars={data.fontCssVars}
themeMode={config.theme.mode}
searchEnabled={config.search.enabled}
siteUrl={config.site}
ogEnabled={config.og.enabled}
page={{ title: "Acme — the fastest docs", description: config.description }}
>
<section class="mx-auto max-w-5xl px-6 py-24">
<h1>Build docs that fly</h1>
</section>
<Footer slot="footer" />
</PageLayout>
Die Kopfzeile, die eine benutzerdefinierte Seite erhält, ist dieselbe wie bei den Dokumentationsseiten, sodass das darin enthaltene Chrome mitkommt: Suche, der Theme-Umschalter, der Sprachwechsler und — wenn Ask AI konfiguriert ist — der Ask-AI-Auslöser. Nichts davon muss pro Seite eingerichtet werden. Übergib askEnabled={false}, um den Ask-Auslöser auf einer Seite wegzulassen und ihn überall sonst beizubehalten.
Die Übergabe von siteUrl (und ogEnabled) leitet automatisch das canonical der Seite und ein generiertes og:image ab: Blume rendert für jede statische benutzerdefinierte Seite eine Open-Graph-Karte — einschließlich der Startseite, der am häufigsten geteilten URL —, ausgeliefert unter /og/<route>.png (/og/index.png für /). Die Startseiten-Karte verwendet den Website-Titel mit der Beschreibung als Kopfzeile; eine tiefer liegende Seite wird nach ihrem letzten Pfadsegment benannt. Setze ogImage oder canonical explizit, um eines von beiden zu überschreiben. ogImage erwartet einen wurzelrelativen Pfad — eine Datei in public/, die anhand von deployment.site zu der absoluten URL aufgelöst wird, die Crawler benötigen — oder eine externe URL, die unverändert durchgereicht wird:
<PageLayout
siteUrl={config.site}
ogEnabled={config.og.enabled}
ogImage="/opengraph-image.png"
ogImageAlt="Acme — the fastest docs"
ogImageSize={{ width: 1200, height: 630 }}
page={{ title: config.title }}
>
<!-- page content -->
</PageLayout>
Nur diese eine Seite ändert sich — jede andere Route behält ihre generierte Karte —, und so gibst du allein der Startseite ein maßgeschneidertes Teilen-Bild. Die generierte Karte gibt ihre Größe und ihren Alternativtext von sich aus an Crawler weiter; übergib bei deinem eigenen ogImage zusätzlich ogImageAlt und ogImageSize, damit die Teilen-Karte dieselbe Behandlung erhält.
Die Seite gibt außerdem schema.org-JSON-LD aus — denselben WebSite-Graphen, den auch die Dokumentationsseiten mitführen, damit die Startseite (üblicherweise eine benutzerdefinierte Seite) nicht die eine URL ohne strukturierte Daten ist. Übergib structuredDataEnabled={config.structuredData}, um sie mit der structuredData-Konfiguration synchron zu halten, oder structuredDataEnabled={false}, um sie für eine Seite abzuschalten.
page.title wird wortwörtlich als Dokumenttitel verwendet (ohne - siteTitle-Suffix), da Marketingseiten üblicherweise ihren eigenen festlegen. Um einer benutzerdefinierten Seite stattdessen das komplette Dokumentations-Chrome zu geben — Seitenleiste, Inhaltsverzeichnis und alles Weitere —, bette sie in RootLayout ein, das Layout, das auch die generierten Seiten verwenden. Beziehe die erforderlichen Props direkt aus blume:data:
---
import RootLayout from "blume/components/layout/RootLayout.astro";
import data from "blume:data";
---
<RootLayout
site={{ title: data.config.title, description: data.config.description }}
logo={data.config.logo}
banner={data.config.banner}
navigation={data.navigation}
page={{ title: "Pricing", route: "/pricing" }}
headings={[]}
themeMode={data.config.theme.mode}
searchEnabled={data.config.search.enabled}
indexable={true}
>
<h1>Pricing</h1>
</RootLayout>
404-Seite
Blume liefert ab Werk eine standardmäßige Nicht gefunden-Seite mit: eine zentrierte „404“-Meldung, eingebettet in das Website-Chrome (Kopfzeile, Suche, Theme), die für jede nicht zugeordnete URL ausgeliefert wird. blume build schreibt sie nach 404.html, was statische Hoster automatisch ausliefern, und blume dev zeigt sie für unbekannte Routen an. Unterhalb der Meldung verlinkt eine Liste Wo du als Nächstes nachsehen kannst jeden Abschnitt der obersten Ebene sowie die Indizes sitemap.xml und llms.txt, sofern vorhanden, damit eine lesende Person — oder ein Agent, der einer veralteten URL gefolgt ist — einen Weg zurück hat.
Die Seite hat außerdem ein Markdown-Gegenstück unter /404.md und ein JSON-Gegenstück unter /404.json (RFC 9457-Problemdetails) mit denselben Rückkehr-Links (absolute URLs, sobald deployment.site gesetzt ist), dazu die openapi.json-Beschreibung, wenn die JSON-API aktiviert ist. Bei einem Vercel-Server-Build erhält eine Anfrage nach einer fehlenden Seite, die Accept: text/markdown sendet oder eine .md-URL anfordert, hinter der keine Seite steht, diesen Markdown-Textkörper mit dem Status 404 statt der HTML-Hülle; eine Anfrage, die Accept: application/json sendet oder eine .json-URL anfordert, hinter der keine Datei steht, erhält das Problemdokument — sodass ein Agent nie eine Seite voller Chrome parsen muss, um zu erfahren, wohin es weitergeht.
Um sie durch deine eigene zu ersetzen, füge eine pages/404.astro hinzu. Sie übernimmt die /404-Route genauso, wie pages/changelog.astro den Changelog übernimmt — deine Seite gewinnt, und die Standardseite entfällt. Bau sie wie jede andere benutzerdefinierte Seite, in PageLayout oder RootLayout:
---
import PageLayout from "blume/components/layout/PageLayout.astro";
import data from "blume:data";
---
<PageLayout
site={{ title: data.config.title, description: data.config.description }}
logo={data.config.logo}
navigation={data.navigation}
themeMode={data.config.theme.mode}
searchEnabled={data.config.search.enabled}
page={{ title: "Page not found", route: "/404" }}
noindex={true}
>
<section class="mx-auto max-w-2xl px-6 py-24 text-center">
<h1>This page took a wrong turn</h1>
<a href="/">Back to home</a>
</section>
</PageLayout>
Um das Standarddesign beizubehalten, aber seine Formulierungen zu ändern — auch für andere Sprachen —, überschreibe die notFound-UI-Zeichenketten (title, description, home) über i18n.ui.
Interaktive Seiten
Benutzerdefinierte Seiten sind gewöhnliches Astro, sodass du React- (oder Framework-beliebige) Islands mit einer Hydratations-Direktive einbinden kannst. React wird automatisch aktiviert, sobald dein Projekt eine .tsx- oder .jsx-Datei enthält.