Runtime-API
createScribe und die komplette typisierte Client-Schnittstelle: Lesezugriffe, Routing-Hilfsfunktionen, Relationen, Sitemap, Weiterleitungen und Framework-Integration.
View as Markdownimport { createScribe } from "scribe-cms/runtime";
import config from "./scribe.config";
const scribe = createScribe(config);
createScribe() liefert einen typisierten Client zurück. Dieser bietet für jeden Inhaltstyp einen eigenen Accessor (etwa scribe.blog, scribe.author, …) sowie die Methode scribe.sitemap(). Sämtliche Lesezugriffe erfolgen synchron direkt im Arbeitsspeicher. Inhalte werden einmalig geladen, zwischengespeichert und während der Entwicklung automatisch aktualisiert, sobald sich Dateien oder der Übersetzungsspeicher ändern.
Der Client basiert auf reinem Node und kommt komplett ohne Framework-Abhängigkeiten aus. Er funktioniert in Next.js, Astro, Remix, SvelteKit oder in einem einfachen Build-Skript exakt gleich. Die folgenden Seitenbeispiele nutzen die Konventionen von Next.js lediglich zur Veranschaulichung.
Dokumente weisen folgende Struktur auf. Dabei ist frontmatter durch dein Zod-Schema typisiert:
{
slug: string; // Slug in der Sprache dieses Dokuments
enSlug: string; // Englischer übergeordneter Slug (== Slug für EN-Dokumente)
locale: string;
frontmatter: { ... };
content: string; // Unverarbeiteter MDX-Inhalt
publishedAt?: string;
updatedAt?: string;
noindex: boolean;
}
Lesezugriffe
list(locale?, options?)
Ruft alle Dokumente für eine bestimmte Sprache ab (Standardwert ist die Standardsprache), sortiert nach dem orderBy-Wert des jeweiligen Typs.
scribe.blog.list("fr");
scribe.blog.list("fr", { limit: 3 });
scribe.blog.list("fr", { orderBy: "slug" }); // Überschreibt die Sortierung für diesen Aufruf
Gibt ein leeres Array ([]) zurück, falls für die gewählte Sprache keine Dokumente existieren.
get(slug, locale?)
Führt eine exakte Slug-Suche in einer einzigen Sprache durch. Es gibt hierbei keinen Fallback und keine automatischen Weiterleitungen. Das Ergebnis ist das Dokument selbst oder null.
resolve(slug, locale)
Übernimmt die komplette Auflösung des Anfragepfads. Nutze diese Methode für deine Seiten:
const r = scribe.blog.resolve(slug, locale);
// {
// document: BlogDoc | null,
// actualLocale: string, // "en", wenn der EN-Fallback aktiv wurde
// shouldRedirectTo?: string, // 301-Ziel (Korrektur bei falscher Slug-Sprache)
// canonicalPath?: string, // Sprachspezifischer Pfadname für das Dokument
// }
Arbeitet folgende Schritte nacheinander ab: Direkttreffer, Weiterleitung bei einem Slug in der falschen Sprache, Fallback-Kette der Sprachen (standardmäßig aktiv) und zuletzt der englische Fallback (falls indexFallback: "en" gesetzt ist). Wenn eine Fallback-Sprache die Seite ausliefert, spiegelt actualLocale diese Sprache wider. Die Slug-Korrektur berücksichtigt dabei die gesamte Kette. Slug-Migrationen und entfernte Dokumente werden über _redirects.json-Regeln in deinem Proxy oder deiner statischen Weiterleitungs-Map abgewickelt und nicht über resolve(). Ein typischer Seiten-Handler sieht in Next.js beispielsweise so aus:
const resolved = scribe.blog.resolve(slug, locale);
if (resolved.shouldRedirectTo) permanentRedirect(resolved.shouldRedirectTo);
if (!resolved.document) notFound();
Routing-Hilfsfunktionen
staticParams(options?)
Liefert jedes { locale, slug }-Paar für das Prerendering. Du kannst dies direkt in den Static-Paths-Hook deines Frameworks einfügen (wie etwa generateStaticParams in Next.js, getStaticPaths in Astro oder entries in SvelteKit):
export function generateStaticParams() {
return scribe.blog.staticParams();
}
// Einschränkung auf bestimmte Sprachen (z. B. für eine OG-Image-Route, die nur lateinische Schriften unterstützt):
scribe.blog.staticParams({ locales: ["en", "fr", "de"] });
Fehlt für eine Sprache die Übersetzung, stammt der vorgerenderte Slug aus der ersten Sprache der Fallback-Kette, die einen passenden Eintrag aufweist (danach folgt der englische Slug). Dadurch stimmen die vorgerenderten URLs exakt mit den Ausgaben von resolve() überein.
alternates(doc)
Erstellt die Hreflang-Zuordnung für ein Dokument (Sprache zu relativem Pfad) für jede Sprache mit einer vorhandenen Übersetzung. Die Standardsprache ist dabei immer enthalten:
scribe.blog.alternates(doc);
// { en: "/blog/hello-world", fr: "/fr/blog/bonjour-le-monde" }
Übergib diese Daten an deine Hreflang-Tags, beispielsweise an metadata.alternates.languages in Next.js oder an manuell generierte Link-Elemente.
translation(doc, targetLocale)
Liefert dasselbe Dokument in einer anderen Sprache oder null im Falle einer fehlenden Übersetzung. Meistens greift man bei einem leeren Ergebnis einfach auf das bereits vorliegende Dokument zurück:
const frDoc = scribe.blog.translation(doc, "fr") ?? doc;
url(slug, locale)
Generiert einen Pfadnamen anhand der path-Vorlage des Typs:
scribe.blog.url("hello-world", "fr"); // "/fr/blog/hello-world"
Bei reinen Referenztypen ohne eigenen path werfen staticParams, alternates und url einen Fehler.
Relationen
related(doc, field, locale?)
Löst ein Frontmatter-Feld vom Typ field.relation() in das entsprechende Dokument oder die Dokumente des Zieltyps auf. Der Rückgabetyp wird direkt aus dem Schema abgeleitet:
scribe.blog.related(doc, "author"); // AuthorDoc (erforderlich, einzeln)
scribe.vertical.related(doc, "blogSlug"); // BlogDoc | null (optional, einzeln)
scribe.glossary.related(doc, "terms"); // GlossaryDoc[] (mehrere)
- Die Auflösung erfolgt in
locale ?? doc.locale. Ist das Ziel nicht übersetzt, wird auf das englische Dokument zurückgegriffen. - Eine erforderliche Relation gibt niemals
nullzurück. Der Befehlscribe validatebricht den Build-Vorgang ab, falls erforderliche Relationen ins Leere laufen. Sollte dennoch eine durchrutschen, wirftrelated()einen Fehler. - Optionale Relationen liefern
nullzurück. Bei Mehrfachrelationen werden fehlende Ziele stillschweigend ignoriert. - Es sind ausschließlich Schema-Felder auf oberster Ebene zugänglich. Verschachtelte Relationen werden zwar validiert, du musst sie jedoch manuell auflösen.
Sitemap
const entries = await scribe.sitemap({
baseUrl: "https://example.com",
typeDefaults: {
blog: { priority: 0.7, changeFrequency: "monthly" },
},
});
Gibt einfache JSON-Einträge zurück (url, lastModified, changeFrequency, priority sowie Hreflang-Daten unter alternates.languages inklusive x-default). Diese Struktur entspricht MetadataRoute.Sitemap in Next.js und lässt sich für jeden anderen Technologie-Stack direkt als Sitemap-XML serialisieren. Pro englischem Dokument über alle routingfähigen Typen hinweg wird ein Eintrag erstellt. Dokumente mit noindex und Quell-Slugs aus _redirects.json werden ignoriert.
Verfügbare Optionen: baseUrl (erforderlich), contentTypes, typeDefaults, resolveUrl, resolvePathname, excludeNoindex (standardmäßig true) und includeXDefault (standardmäßig true).
Introspektion
Neben den typspezifischen Accessoren stellt der Client auch die vollständig aufgelöste Konfiguration bereit:
scribe.config; // Die aufgelöste ScribeConfig
scribe.getType("blog"); // Ein einzelner aufgelöster Inhaltstyp
scribe.listTypes(); // Alle Inhaltstypen
scribe.listRoutableTypes(); // Nur Typen mit einer path-Vorlage
Dies ist besonders praktisch für generische Werkzeuge. Du kannst damit beispielsweise über jeden routingfähigen Typ iterieren, um Navigationselemente oder Feeds aufzubauen.
Weiterleitungen
Verwende für statisch exportierte Weiterleitungsregeln (wie _redirects.json oder sprachübergreifende Slugs) den folgenden Build-Skript-Einstiegspunkt:
// scripts/generate-redirects.ts (Ausführung vor dem App-Build)
import {
buildAllContentRedirects,
createProject,
createUrlBuilder,
loadConfigSync,
} from "scribe-cms";
const config = loadConfigSync();
const project = createProject(config);
const urlBuilder = createUrlBuilder(project.config);
const rules = buildAllContentRedirects(project, {
prefixedLocales: urlBuilder.prefixedLocales,
});
// [{ source, destination, permanent: true }, ...]
// Entspricht exakt der Weiterleitungs-Konfiguration von Next.js. Mappe dies je nach Bedarf auf nginx-Regeln, _redirects,
// vercel.json oder das entsprechende Äquivalent deines Frameworks.
Statische Rohdaten-Exporte
So erstellst du LLM- oder Crawler-freundliche reine MDX-Dateien, die als statische Assets (etwa /blog/my-post.mdx) bereitgestellt werden:
scribe export-static --out public
Alternativ funktioniert das auch programmatisch:
import { writeStaticRawExports, createProject, loadConfigSync } from "scribe-cms";
writeStaticRawExports(createProject(loadConfigSync()), { outDir: "public" });
Die Methode getStaticExportRoots(project) liefert die verwalteten Stammverzeichnisse (wie blog/ oder fr/blog/). Diese kannst du vor dem Schreiben bereinigen lassen. Dokumente, die in der Datei _redirects.json als Weiterleitungsquellen aufgeführt sind, werden übersprungen, sofern excludeRedirected den Wert true hat (dies ist der Standard). Dokumente mit noindex sind eingeschlossen, es sei denn, du setzt excludeNoindex. Weitere Informationen zu den Befehls-Flags findest du in der CLI-Referenz.
Notausstieg
Der Aufruf von scribe.<type>.load() gibt den rohen Index als Map<locale, { bySlug, byEnSlug }> zurück. Greife im Normalfall besser auf list, get oder resolve zurück. Die Methode load() ist primär für spezielle Tooling-Zwecke und ungewöhnliche Zugriffsmuster gedacht.
Framework-Integration
Scribe ist komplett unabhängig vom gewählten Framework. Die Runtime ist reines Node und funktioniert reibungslos in jedem serverseitig gerenderten oder statisch gebauten Stack. Dabei gelten überall drei Grundregeln:
- Nutze in deinem App-Code den Import von
scribe-cms/runtime. Dadurch werden die CLI- und Übersetzer-Codepfade beim Tracing durch den Bundler ausgeschlossen. In Build-Skripten verwendest du hingegenscribe-cms. - Halte das native Modul
better-sqlite3sowie Scribe aus deinem Bundler heraus. Nutze dafür beispielsweiseserverExternalPackagesin Next.js,ssr.externalin Vite oder Astro sowieexternalin esbuild. - Speichere den Client in einem Modul-Singleton zwischen:
// src/lib/scribe.ts
import { createScribe } from "scribe-cms/runtime";
import type { ScribeClient } from "scribe-cms/runtime";
import config from "../../scribe.config";
let cached: ScribeClient<typeof config> | null = null;
export function getScribe() {
return (cached ??= createScribe(config));
}
Mache deine Builds von validen Inhalten abhängig, indem du sie mit "build": "scribe validate && <framework build>" koppelst.
Beispiel für Next.js
// next.config.ts
const nextConfig = {
serverExternalPackages: ["better-sqlite3", "scribe-cms", "scribe-cms/runtime"],
// Erforderlich auf Vercel: Scribe liest content/ und .scribe/ zur Laufzeit.
outputFileTracingIncludes: {
"/**": ["./content/**/*", "./.scribe/**/*"],
},
// Ebenfalls erforderlich auf Vercel: Durch die Dateizugriffe der Scribe-Runtime
// kopiert der Tracer den Webpack-Build-Cache von Next in jede Serverless-Funktion.
// Dadurch wird das 250-MB-Limit gesprengt. Der Cache wird zur Laufzeit nie benötigt.
outputFileTracingExcludes: {
"/**": ["./.next/cache/**"],
},
};
Die Funktion staticParams() klinkt sich nahtlos in generateStaticParams() ein. Du verbindest alternates() mit metadata.alternates.languages, scribe.sitemap() mit app/sitemap.ts und buildAllContentRedirects() mit redirects().