Scribe

API en tiempo de ejecución

createScribe y toda la superficie del cliente tipado: lectura, ayudantes de enrutamiento, relaciones, mapa del sitio, redirecciones e integración con frameworks.

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

const scribe = createScribe(config);

createScribe() devuelve un cliente tipado con un método de acceso por cada tipo de contenido (scribe.blog, scribe.author, ...) además de scribe.sitemap(). Todas las lecturas son síncronas y se realizan en memoria: el contenido se carga una vez, se guarda en caché y (en desarrollo) se revalida cuando cambian los archivos o el almacén de traducciones.

El cliente es Node puro, sin dependencias de frameworks. Funciona exactamente igual en Next.js, Astro, Remix, SvelteKit o en un script de compilación; los ejemplos de páginas a continuación usan las convenciones de Next.js puramente como ilustración.

Los documentos tienen esta estructura; frontmatter se tipa a partir de tu esquema de Zod:

{
  slug: string;        // slug in this document's locale
  enSlug: string;      // English parent slug (== slug for EN docs)
  locale: string;
  frontmatter: { ... };
  content: string;     // raw MDX body
  publishedAt?: string;
  updatedAt?: string;
  noindex: boolean;
}

Lectura

list(locale?, options?)

Todos los documentos para un idioma (por defecto, el idioma predeterminado), ordenados por el orderBy del tipo de contenido.

scribe.blog.list("fr");
scribe.blog.list("fr", { limit: 3 });
scribe.blog.list("fr", { orderBy: "slug" });   // per-call override

Devuelve [] para un idioma sin documentos.

get(slug, locale?)

Búsqueda exacta del slug en un solo idioma. No hay alternativas de respaldo ni gestión de redirecciones. Devuelve el documento o null.

resolve(slug, locale)

La resolución completa de la ruta de la solicitud; úsala en las páginas:

const r = scribe.blog.resolve(slug, locale);
// {
//   document: BlogDoc | null,
//   actualLocale: string,        // "en" when EN fallback kicked in
//   shouldRedirectTo?: string,   // 301 target (wrong-locale slug correction)
//   canonicalPath?: string,      // locale-aware pathname for the document
// }

Gestiona, en este orden: acierto directo, redirección por slug en el idioma incorrecto, cadena de idiomas de respaldo (activada por defecto) e idioma inglés de respaldo (cuando indexFallback: "en"). Cuando un idioma de respaldo sirve la página, actualLocale es ese idioma y la corrección del slug tiene en cuenta la cadena completa. Las migraciones de slugs y los documentos retirados se gestionan mediante las reglas de _redirects.json en tu proxy o mapa de redirecciones estáticas, no a través de resolve(). Un manejador de páginas típico (ejemplo con Next.js):

const resolved = scribe.blog.resolve(slug, locale);
if (resolved.shouldRedirectTo) permanentRedirect(resolved.shouldRedirectTo);
if (!resolved.document) notFound();

Ayudantes de enrutamiento

staticParams(options?)

Todos los pares { locale, slug } para pre-renderizar; colócalo en el hook de rutas estáticas de tu framework (generateStaticParams en Next.js, getStaticPaths en Astro, entries en SvelteKit, ...):

export function generateStaticParams() {
  return scribe.blog.staticParams();
}

// Restrict locales (e.g. an OG-image route that only supports Latin scripts):
scribe.blog.staticParams({ locales: ["en", "fr", "de"] });

Para un idioma sin traducción, el slug pre-renderizado proviene del primer idioma en la cadena de idiomas de respaldo que tenga uno (y luego el slug en inglés), de modo que las URL pre-renderizadas coincidan con lo que sirve resolve().

alternates(doc)

El mapa hreflang para un documento: idioma relativo a la ruta, para cada idioma que tenga una traducción (el idioma por defecto siempre se incluye):

scribe.blog.alternates(doc);
// { en: "/blog/hello-world", fr: "/fr/blog/bonjour-le-monde" }

Pásalo a tus etiquetas hreflang, por ejemplo, en metadata.alternates.languages de Next.js o en elementos de enlace renderizados a mano.

translation(doc, targetLocale)

El mismo documento en otro idioma, o null si no está traducido. Quienes lo llaman suelen recurrir al documento que ya tienen:

const frDoc = scribe.blog.translation(doc, "fr") ?? doc;

url(slug, locale)

Construye la ruta a partir de la plantilla path del tipo:

scribe.blog.url("hello-world", "fr");  // "/fr/blog/hello-world"

staticParams, alternates y url lanzan un error para los tipos que son solo de referencia (sin path).

Relaciones

related(doc, field, locale?)

Extrae la referencia de un campo frontmatter field.relation() hacia los documentos del tipo de destino. El tipo de retorno se infiere del esquema:

scribe.blog.related(doc, "author");        // AuthorDoc           (required single)
scribe.vertical.related(doc, "blogSlug");  // BlogDoc | null      (optional single)
scribe.glossary.related(doc, "terms");     // GlossaryDoc[]       (multiple)
  • Extrae la referencia en locale ?? doc.locale, recurriendo al documento en inglés cuando el destino no está traducido.
  • Una relación requerida nunca devuelve null: scribe validate bloquea la compilación si hay relaciones requeridas colgantes, y related() lanza un error si alguna se cuela.
  • Las relaciones opcionales devuelven null; las relaciones múltiples descartan silenciosamente los destinos faltantes.
  • Solo se exponen los campos del esquema de nivel superior (las relaciones anidadas se siguen validando, pero debes extraer sus referencias manualmente).

Mapa del sitio

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

Devuelve entradas JSON sencillas (url, lastModified, changeFrequency, priority, y alternates.languages para hreflang, incluyendo x-default). La estructura coincide con MetadataRoute.Sitemap de Next.js y se serializa directamente en un XML de mapa del sitio para cualquier otra pila tecnológica. Hay una entrada por documento en inglés en todos los tipos enrutables; omite los documentos noindex y los slugs de origen de redirección provenientes de _redirects.json.

Opciones: baseUrl (requerido), contentTypes, typeDefaults, resolveUrl, resolvePathname, excludeNoindex (verdadero por defecto), includeXDefault (verdadero por defecto).

Introspección

Más allá de los métodos de acceso por tipo, el cliente expone la configuración resuelta:

scribe.config;              // resolved ScribeConfig
scribe.getType("blog");     // one resolved content type
scribe.listTypes();         // all content types
scribe.listRoutableTypes(); // only types with a path template

Útil para herramientas genéricas, por ejemplo, iterar sobre cada tipo enrutable para construir la navegación o los feeds.

Redirecciones

Para las reglas de redirección exportadas estáticamente (_redirects.json, slugs multilingües), utiliza el punto de entrada del script de compilación:

// scripts/generate-redirects.ts (run before the 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 }, ...]
// Matches Next.js redirect config directly; map to nginx rules, _redirects,
// vercel.json, or your framework's equivalent as needed.

Exportaciones estáticas sin procesar

Para servir archivos MDX sin procesar como activos estáticos amigables con LLMs y rastreadores (por ejemplo, /blog/my-post.mdx):

scribe export-static --out public

O de forma programática:

import { writeStaticRawExports, createProject, loadConfigSync } from "scribe-cms";

writeStaticRawExports(createProject(loadConfigSync()), { outDir: "public" });

getStaticExportRoots(project) devuelve los directorios raíz gestionados para limpiarlos antes de escribir (por ejemplo, blog/, fr/blog/). Los documentos listados como orígenes de redirección en _redirects.json se omiten cuando excludeRedirected es verdadero (su valor por defecto). Los documentos noindex se incluyen a menos que se configure excludeNoindex. Consulta la referencia de la CLI para ver las banderas del comando.

Vía de escape

scribe.<type>.load() devuelve el índice sin procesar Map<locale, { bySlug, byEnSlug }>. Es preferible usar list, get o resolve; load() existe para herramientas específicas y patrones de acceso inusuales.

Integración con frameworks

Scribe es independiente del framework; el entorno de ejecución es Node puro y funciona en cualquier pila renderizada en el servidor o construida de forma estática. Se aplican tres reglas en todos los casos:

  1. Importa desde scribe-cms/runtime en el código de la aplicación (esto excluye las rutas de la CLI y del traductor del rastreo del empaquetador); usa scribe-cms en los scripts de compilación.
  2. Mantén better-sqlite3 (un módulo nativo) y Scribe como externos a tu empaquetador, por ejemplo usando serverExternalPackages en Next.js, ssr.external en Vite/Astro o external en esbuild.
  3. Guarda el cliente en la caché como un singleton del módulo:
// 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));
}

Condiciona las compilaciones a la integridad del contenido: "build": "scribe validate && <framework build>".

Ejemplo con Next.js

// next.config.ts
const nextConfig = {
  serverExternalPackages: ["better-sqlite3", "scribe-cms", "scribe-cms/runtime"],
  // Required on Vercel: Scribe reads content/ and .scribe/ at runtime.
  outputFileTracingIncludes: {
    "/**": ["./content/**/*", "./.scribe/**/*"],
  },
  // Also required on Vercel: Scribe's runtime file reads make the tracer
  // sweep Next's webpack build cache into every serverless function,
  // blowing past the 250 MB limit. The cache is never needed at runtime.
  outputFileTracingExcludes: {
    "/**": ["./.next/cache/**"],
  },
};

staticParams() se conecta con generateStaticParams(), alternates() con metadata.alternates.languages, scribe.sitemap() con app/sitemap.ts, y buildAllContentRedirects() con redirects().

API en tiempo de ejecución · Scribe