运行时 API
createScribe 及完整的类型化客户端接口:读取、路由辅助函数、关联关系、站点地图、重定向和框架集成。
View as Markdownimport { createScribe } from "scribe-cms/runtime";
import config from "./scribe.config";
const scribe = createScribe(config);
createScribe() 会返回一个带有类型提示的客户端,每个内容类型都有一个专门的访问器(例如 scribe.blog、scribe.author 等),外加一个 scribe.sitemap() 方法。所有的读取操作都是同步的并且在内存中完成:内容只加载一次,随后被缓存。在开发环境下,当文件或翻译存储发生变化时会自动重新验证。
该客户端是纯 Node 实现,不依赖任何特定框架。不论是在 Next.js、Astro、Remix、SvelteKit 还是普通的构建脚本中,它的运行表现完全一致;下文出现的页面示例单纯为了演示而使用了 Next.js 规范。
文档的数据结构如下,其中 frontmatter 会自动根据你的 Zod schema 推导类型:
{
slug: string; // 当前文档语言环境下的 slug
enSlug: string; // 英文父级 slug(如果是英文文档,则等同于 slug)
locale: string;
frontmatter: { ... };
content: string; // 原始 MDX 内容
publishedAt?: string;
updatedAt?: string;
noindex: boolean;
}
读取数据
list(locale?, options?)
获取指定语言环境(未指定则使用默认语言环境)下的所有文档,并根据该内容类型的 orderBy 设置进行排序。
scribe.blog.list("fr");
scribe.blog.list("fr", { limit: 3 });
scribe.blog.list("fr", { orderBy: "slug" }); // 单次调用的覆盖设置
如果指定语言环境下没有对应的文档,则返回 []。
get(slug, locale?)
在单一语言环境下进行精确的 slug 匹配查找。此方法没有回退机制,也不处理重定向。直接返回匹配的文档或 null。
resolve(slug, locale)
处理完整的请求路径解析流程,这是在页面代码中最常使用的方法:
const r = scribe.blog.resolve(slug, locale);
// {
// document: BlogDoc | null,
// actualLocale: string, // 当触发英语回退时,此处为 "en"
// shouldRedirectTo?: string, // 301 重定向目标(修正错误语言环境下的 slug)
// canonicalPath?: string, // 包含语言环境的规范化文档路径
// }
该方法按以下顺序进行处理:精准匹配 → 错误语言环境 slug 重定向 → 语言环境回退链(默认开启) → 英语回退(前提是配置了 indexFallback: "en")。如果最终由回退语言环境提供了页面,actualLocale 将返回该语言环境,并且 slug 修正逻辑能够感知回退链的状态。需要注意的是,Slug 的历史迁移以及下线文档是由代理或静态重定向映射表中的 _redirects.json 规则来处理的,而不是交由 resolve() 负责。一个典型的页面处理逻辑如下(以 Next.js 为例):
const resolved = scribe.blog.resolve(slug, locale);
if (resolved.shouldRedirectTo) permanentRedirect(resolved.shouldRedirectTo);
if (!resolved.document) notFound();
路由辅助函数
staticParams(options?)
返回所有需要预渲染的 { locale, slug } 键值对,你可以直接将它丢给框架的静态路径生成钩子(例如 Next.js 的 generateStaticParams、Astro 的 getStaticPaths 或 SvelteKit 的 entries 等):
export function generateStaticParams() {
return scribe.blog.staticParams();
}
// 限制生成的语言环境(例如:只支持拉丁语系的 OG 图片路由):
scribe.blog.staticParams({ locales: ["en", "fr", "de"] });
如果某个语言环境缺少对应的翻译内容,预渲染使用的 slug 会取自语言环境回退链中首个拥有翻译的语言环境(随后才是英语 slug),这保证了预渲染的 URL 结构与 resolve() 的处理结果完全一致。
alternates(doc)
生成某篇文档的 hreflang 映射关系:即 语言环境 → 相对路径。这涵盖了所有提供了翻译版本的语言环境(默认语言环境必然包含在内):
scribe.blog.alternates(doc);
// { en: "/blog/hello-world", fr: "/fr/blog/bonjour-le-monde" }
你可以把它的返回值直接用于渲染 hreflang 标签,比如在 Next.js 中的 metadata.alternates.languages,或者是手动组装 link 元素。
translation(doc, targetLocale)
获取同一文档在其他语言环境下的版本,如果没有翻译则返回 null。调用者通常会优雅降级,回退到已获取到的文档:
const frDoc = scribe.blog.translation(doc, "fr") ?? doc;
url(slug, locale)
依据该类型的 path 模板构建完整的路径名:
scribe.blog.url("hello-world", "fr"); // "/fr/blog/hello-world"
对于仅用作引用的类型(未定义 path),调用 staticParams、alternates 以及 url 会抛出异常。
关联关系
related(doc, field, locale?)
将通过 field.relation() 声明的 frontmatter 字段解引用为目标类型的实际文档对象。其返回类型会根据 schema 自动推断:
scribe.blog.related(doc, "author"); // AuthorDoc (必填的单项)
scribe.vertical.related(doc, "blogSlug"); // BlogDoc | null (选填的单项)
scribe.glossary.related(doc, "terms"); // GlossaryDoc[] (多项)
- 在
locale ?? doc.locale环境下解引用。如果目标语言环境下没有翻译版本,将回退至英文文档。 - 必填关系绝不会返回
null:scribe validate会在构建时拦截所有悬空的必填关联关系。如有漏网之鱼,related()会直接抛出错误。 - 选填关系在找不到目标时返回
null;多项关系则会静默剔除丢失的目标。 - 仅暴露顶层 schema 字段(嵌套关系依然接受验证,但需要手动处理解引用)。
站点地图 (Sitemap)
const entries = await scribe.sitemap({
baseUrl: "https://example.com",
typeDefaults: {
blog: { priority: 0.7, changeFrequency: "monthly" },
},
});
返回基础 JSON 条目(包含 url、lastModified、changeFrequency、priority 以及包含 x-default 的 hreflang alternates.languages)。其数据结构完全契合 Next.js 的 MetadataRoute.Sitemap 格式,其他技术栈也可直接将其序列化为站点地图 XML。涵盖了所有支持路由类型中的每一篇英文文档,同时跳过被标记为 noindex 的文档以及位于 _redirects.json 中的重定向来源 slug。
可用选项:baseUrl(必填)、contentTypes、typeDefaults、resolveUrl、resolvePathname、excludeNoindex(默认 true)及 includeXDefault(默认 true)。
数据自省
除了针对具体类型的访问器,客户端还暴露出了解析后的配置对象:
scribe.config; // 解析后的 ScribeConfig
scribe.getType("blog"); // 单个解析后的内容类型
scribe.listTypes(); // 所有的内容类型
scribe.listRoutableTypes(); // 仅包含配置了 path 模板的类型
这对开发通用工具来说非常实用,例如遍历每一个支持路由的内容类型以生成导航栏或订阅源。
重定向
针对静态导出的重定向规则(例如 _redirects.json 或跨语言环境的 slugs),请使用这个用于构建脚本的入口点:
// scripts/generate-redirects.ts (在应用构建前执行)
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 }, ...]
// 完全匹配 Next.js 重定向配置项;你可以视需要将其映射为 nginx 规则、_redirects、
// vercel.json 或是你选用框架对应的类似配置。
原始静态导出
如果需要将原始 MDX 文件以静态资源的形式提供(方便 LLM 获取或爬虫抓取,如 /blog/my-post.mdx):
scribe export-static --out public
或者通过代码调用:
import { writeStaticRawExports, createProject, loadConfigSync } from "scribe-cms";
writeStaticRawExports(createProject(loadConfigSync()), { outDir: "public" });
getStaticExportRoots(project) 会返回所有由工具托管的根目录,供写入前清理使用(如 blog/、fr/blog/)。当 excludeRedirected 设置为 true 时(默认开启),在 _redirects.json 中列为重定向源的文档将被跳过。除非配置了 excludeNoindex,否则 noindex 文档也会被包括在内。请参阅命令行参考了解具体的命令参数标志。
逃生舱接口
scribe.<type>.load() 可以让你获取最底层的 Map<locale, { bySlug, byEnSlug }> 索引数据。在绝大多数情况下,建议优先使用 list、get 及 resolve;load() 是专为开发工具及应对不寻常的访问模式而保留的。
框架集成
Scribe 是框架无关的;运行时基于纯 Node 构建,适用于任何服务端渲染或者静态构建方案。不论在何处部署,始终需遵循这三条准则:
- 在业务代码中请从
scribe-cms/runtime引入依赖(这能阻止打包工具追踪 CLI 和翻译工具的代码路径);在构建脚本中则引入scribe-cms。 - 将
better-sqlite3(原生模块)与 Scribe 配置为打包器的外部依赖。比如 Next.js 中的serverExternalPackages、Vite/Astro 的ssr.external或是 esbuild 的external配置项。 - 在单例模块中缓存客户端实例:
// 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));
}
让构建流程依赖内容状态的健康度验证:"build": "scribe validate && <framework build>"。
Next.js 示例
// next.config.ts
const nextConfig = {
serverExternalPackages: ["better-sqlite3", "scribe-cms", "scribe-cms/runtime"],
// Vercel 部署必需:Scribe 会在运行时读取 content/ 与 .scribe/ 目录。
outputFileTracingIncludes: {
"/**": ["./content/**/*", "./.scribe/**/*"],
},
// Vercel 部署必需:Scribe 在运行时的文件读取会触发追踪器将 Next 的
// webpack 构建缓存带入每个 Serverless 函数中,这通常会突破 250 MB 限制。
// 运行时其实根本不需要这些缓存。
outputFileTracingExcludes: {
"/**": ["./.next/cache/**"],
},
};
staticParams() 完美对应 generateStaticParams(),alternates() 对应 metadata.alternates.languages,scribe.sitemap() 对应 app/sitemap.ts,而 buildAllContentRedirects() 则是为 redirects() 量身定做的。