Runtime API
Метод createScribe и весь типизированный клиентский интерфейс: чтение, хелперы для маршрутизации, связи, sitemap, редиректы и интеграция с фреймворками.
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):
{
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 } для пререндера. Просто передайте их в хук вашего фреймворка (например, generateStaticParams в Next.js, getStaticPaths в Astro, entries в SvelteKit):
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 (например, в metadata.alternates.languages для Next.js или при ручном рендере элементов 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"
Методы staticParams, alternates и url выбрасывают ошибку для ссылочных типов контента без свойства path.
Связи
related(doc, field, locale?)
Разрешает поле frontmatter field.relation() в документы целевого типа. Возвращаемый тип выводится из схемы:
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. Множественные связи просто игнорируют недостающие цели. - Доступны только поля схемы верхнего уровня (вложенные связи валидируются, но разрешать их нужно вручную).
Карта сайта
const entries = await scribe.sitemap({
baseUrl: "https://example.com",
typeDefaults: {
blog: { priority: 0.7, changeFrequency: "monthly" },
},
});
Возвращает обычные JSON-записи (url, lastModified, changeFrequency, priority, hreflang alternates.languages включая x-default). Формат полностью соответствует MetadataRoute.Sitemap из Next.js и напрямую сериализуется в XML для любого другого стека. Создается одна запись на каждый английский документ среди всех маршрутизируемых типов. Игнорируются документы с noindex и исходные slug редиректов из _redirects.json.
Опции включают baseUrl (обязательно), contentTypes, typeDefaults, resolveUrl, resolvePathname, excludeNoindex (по умолчанию true) и includeXDefault (по умолчанию true).
Интроспекция
Помимо доступа к конкретным типам, клиент предоставляет доступ к разрешенной конфигурации:
scribe.config; // разрешенный ScribeConfig
scribe.getType("blog"); // один разрешенный тип контента
scribe.listTypes(); // все типы контента
scribe.listRoutableTypes(); // только типы с шаблоном пути
Это полезно для создания общих инструментов (например, для итерации по всем маршрутизируемым типам при создании навигации или фидов).
Редиректы
Для статически экспортируемых правил редиректа (_redirects.json или кросс-локальных slug) используйте скрипт на этапе сборки:
// 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
Для генерации чистых файлов MDX в виде статических ассетов (например, /blog/my-post.mdx), которые хорошо индексируются поисковиками и LLM:
scribe export-static --out public
Или программно:
import { writeStaticRawExports, createProject, loadConfigSync } from "scribe-cms";
writeStaticRawExports(createProject(loadConfigSync()), { outDir: "public" });
Функция getStaticExportRoots(project) возвращает управляемые корневые директории для очистки перед записью (например, blog/, fr/blog/). Документы, указанные как источники редиректа в _redirects.json, пропускаются, если excludeRedirected установлено в true (по умолчанию). Документы с noindex включаются, если не задан флаг excludeNoindex. Подробнее смотрите в справочнике CLI.
Прямой доступ
Метод scribe.<type>.load() возвращает сырой индекс в виде Map<locale, { bySlug, byEnSlug }>. Рекомендуется использовать list, get или resolve. Метод load() существует исключительно для инструментов и нестандартных сценариев доступа.
Интеграция с фреймворками
Scribe не привязан к конкретному фреймворку. Runtime написан на чистом Node и работает в любом стеке с серверным рендерингом или статической сборкой. Везде действуют три правила:
- Импортируйте из
scribe-cms/runtimeв коде приложения (это исключает CLI и код переводчика из трассировки бандлера). В скриптах сборки используйтеscribe-cms. - Убедитесь, что
better-sqlite3(нативный модуль) и Scribe исключены из сборки бандлера (например, черезserverExternalPackagesв Next.js,ssr.externalв Vite/Astro илиexternalв esbuild). - Кэшируйте клиент с помощью синглтона в модуле:
// 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 в runtime заставляет трассировщик
// тянуть кэш сборки webpack Next в каждую бессерверную функцию.
// Это быстро превышает лимит в 250 МБ. Кэш никогда не нужен во время выполнения.
outputFileTracingExcludes: {
"/**": ["./.next/cache/**"],
},
};
Метод staticParams() легко интегрируется с generateStaticParams(), alternates() — с metadata.alternates.languages, scribe.sitemap() отлично подходит для app/sitemap.ts, а buildAllContentRedirects() встраивается в redirects().