Scribe

Runtime API

Метод createScribe и весь типизированный клиентский интерфейс: чтение, хелперы для маршрутизации, связи, sitemap, редиректы и интеграция с фреймворками.

View as Markdown
import { 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 и работает в любом стеке с серверным рендерингом или статической сборкой. Везде действуют три правила:

  1. Импортируйте из scribe-cms/runtime в коде приложения (это исключает CLI и код переводчика из трассировки бандлера). В скриптах сборки используйте scribe-cms.
  2. Убедитесь, что better-sqlite3 (нативный модуль) и Scribe исключены из сборки бандлера (например, через serverExternalPackages в Next.js, ssr.external в Vite/Astro или external в esbuild).
  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 в runtime заставляет трассировщик
  // тянуть кэш сборки webpack Next в каждую бессерверную функцию.
  // Это быстро превышает лимит в 250 МБ. Кэш никогда не нужен во время выполнения.
  outputFileTracingExcludes: {
    "/**": ["./.next/cache/**"],
  },
};

Метод staticParams() легко интегрируется с generateStaticParams(), alternates() — с metadata.alternates.languages, scribe.sitemap() отлично подходит для app/sitemap.ts, а buildAllContentRedirects() встраивается в redirects().

Runtime API · Scribe