Scribe

运行时 API

createScribe 及完整的类型化客户端接口:读取、路由辅助函数、关联关系、站点地图、重定向和框架集成。

View as Markdown
import { createScribe } from "scribe-cms/runtime";
import config from "./scribe.config";

const scribe = createScribe(config);

createScribe() 会返回一个带有类型提示的客户端,每个内容类型都有一个专门的访问器(例如 scribe.blogscribe.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),调用 staticParamsalternates 以及 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 环境下解引用。如果目标语言环境下没有翻译版本,将回退至英文文档。
  • 必填关系绝不会返回 nullscribe validate 会在构建时拦截所有悬空的必填关联关系。如有漏网之鱼,related() 会直接抛出错误。
  • 选填关系在找不到目标时返回 null;多项关系则会静默剔除丢失的目标。
  • 仅暴露顶层 schema 字段(嵌套关系依然接受验证,但需要手动处理解引用)。

站点地图 (Sitemap)

const entries = await scribe.sitemap({
  baseUrl: "https://example.com",
  typeDefaults: {
    blog: { priority: 0.7, changeFrequency: "monthly" },
  },
});

返回基础 JSON 条目(包含 urllastModifiedchangeFrequencypriority 以及包含 x-default 的 hreflang alternates.languages)。其数据结构完全契合 Next.js 的 MetadataRoute.Sitemap 格式,其他技术栈也可直接将其序列化为站点地图 XML。涵盖了所有支持路由类型中的每一篇英文文档,同时跳过被标记为 noindex 的文档以及位于 _redirects.json 中的重定向来源 slug。

可用选项:baseUrl(必填)、contentTypestypeDefaultsresolveUrlresolvePathnameexcludeNoindex(默认 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 }> 索引数据。在绝大多数情况下,建议优先使用 listgetresolveload() 是专为开发工具及应对不寻常的访问模式而保留的。

框架集成

Scribe 是框架无关的;运行时基于纯 Node 构建,适用于任何服务端渲染或者静态构建方案。不论在何处部署,始终需遵循这三条准则:

  1. 在业务代码中请从 scribe-cms/runtime 引入依赖(这能阻止打包工具追踪 CLI 和翻译工具的代码路径);在构建脚本中则引入 scribe-cms
  2. better-sqlite3(原生模块)与 Scribe 配置为打包器的外部依赖。比如 Next.js 中的 serverExternalPackages、Vite/Astro 的 ssr.external 或是 esbuild 的 external 配置项。
  3. 在单例模块中缓存客户端实例:
// 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.languagesscribe.sitemap() 对应 app/sitemap.ts,而 buildAllContentRedirects() 则是为 redirects() 量身定做的。

运行时 API · Scribe