Blog Markdown (multilingüe, con etiquetas)
Un blog donde el contenido son archivos, no filas. Todo el diseño gira en torno a una idea:
la identidad de un artículo es su translationKey, no su slug. Los slugs se localizan
para SEO y difieren por idioma; la clave es lo que hace que tres archivos sean un solo artículo,
y es lo que impulsa hreflang, el selector de idioma, los artículos relacionados y las
redirecciones entre locales. Si aciertas con ese campo, todo lo demás son búsquedas.
Escrito por el ingeniero que ha enviado este módulo. La implementación anterior contra la que
se auditó fue un blog de marketing multilingüe, generado estáticamente, con su sitemap y
superficie SEO. Los hechos y reglas a continuación son lo que la suite de la capa de contenido
y la compilación verifican; references/provenance.md contiene el registro.
Cuándo usar
- Contenido redactado como markdown en el repositorio, desplegado con la aplicación
- Más de un idioma, con diferentes slugs por idioma
- Páginas de etiquetas, artículos relacionados, feeds o hreflang están dentro del alcance
- Deseas que todo se genere estáticamente en tiempo de compilación
Cuándo NO usar
| En lugar de esto | Usa |
|---|---|
| Un CMS alojado es la fuente de verdad | el SDK de ese CMS; guarda solo references/content-model.md |
| Un sitio de documentación / centro de ayuda (categorías, barra lateral, búsqueda) | un skill de centro de ayuda — modelo de navegación diferente |
| Un blog monolingüe de menos de 10 artículos | fs + gray-matter en línea; esto es sobrecarga |
| Renderizar markdown enviado por usuarios | un renderizador sanitizante; consulta la nota XSS en references/rendering.md |
Arquitectura
content/blog/<locale>/<localized-slug>.md # frontmatter + cuerpo
|
v
parseFrontmatter --YAML, recurre a un analizador línea por línea en comillas mal formadas
|
v
getAllPosts(locale) <-- memoizado por locale en producción
|
+--> translationKey ----> alternates / hreflang / cambio de idioma
+--> tags -> slug de etiqueta ---> páginas de etiqueta (el slug es la identidad, no la etiqueta)
+--> related (slugs EN) -> artículos relacionados, con fallback a EN
|
v
/blog /blog/[slug] /blog/tag/[slug] sitemap.xml feed.xml
Hechos críticos
- Las relaciones se escriben una vez, en el locale predeterminado, como slugs del locale predeterminado.
Un archivo traducido puede omitir
relatedpor completo y aun así obtener los artículos relacionados correctos, resueltos a través detranslationKey, de modo que cada traducción renderiza la misma sección de relacionados. La prueba de fallback lo verifica. - La identidad de una etiqueta es su slug, no su etiqueta textual.
"AI Agents"y"AI agents"apuntan al mismo slug y URL. Agrupa por slug y empareja artículos por slug, de modo que todas las variantes ortográficas de una etiqueta aparezcan en la misma página de etiqueta. Dos pruebas lo verifican. gray-matterlanza una excepción en un escalar YAML con comillas dobles que contienen caracteres de comillas literales — las comillas tipográficas en un extracto traducido lo provocan. Sin un analizador de respaldo, la compilación se detiene en ese archivo; el analizador línea por línea de respaldo lo mantiene cargando. Verificado contra la librería y cubierto por una prueba.gray-matteralmacena en caché un parseo fallido como resultado vacío. Escribe su entrada de caché antes de analizar, por lo que la segunda y posteriores lecturas de un archivo que falló devuelven{}— sin error, sin frontmatter. Siempre llama amatter(contents, {}); cualquier objeto de opciones desactiva la caché.- La carga es O(artículos x locales) por página a menos que memoices. Memoiza por locale para que cada archivo
de contenido se lea una vez por compilación; resolver alternates en
generateMetadatade otra manera resulta en una amplificación de lectura de archivos de 71x, medida en la implementación anterior. getPostSlugsdebe filtrar.md. El filtro evita que un.DS_Storesuelto se convierta en una lectura de.DS_Store.mdy unENOENT. Una prueba lo verifica.- Las páginas de etiqueta no tienen identidad entre locales. Las etiquetas son texto libre por locale, por lo que el selector de idioma debe recurrir al índice del blog en una URL de etiqueta en lugar de construir una URL que dé 404.
Reglas estrictas
Nunca uses el slug como clave para búsquedas entre locales. Los slugs están localizados intencionadamente.
translationKeyes la columna de unión; una coincidencia de slug entre locales es una coincidencia, no una relación.
Nunca permitas que
getPostBySlugdecida la visibilidad de borradores implícitamente. Filtra los borradores en el cargador;dynamicParams = falselos oculta solo como efecto secundario y deja de hacerlo en cuanto la bandera cambia. La prueba de borradores verifica el filtro.
Nunca renderices una constante «5 min de lectura». Calcúlala a partir del cuerpo; cuesta un
split, y la prueba de tiempo de lectura lo verifica.
Nunca derives la URL de una página de etiqueta a partir de la etiqueta en tiempo de lectura y la etiqueta a partir de la URL en tiempo de renderizado sin una comprobación de colisión. Ese viaje de ida y vuelta solo es sin pérdidas mientras cada etiqueta se convierta en un slug único.
Inicio rápido
- Define el contrato de locale y el esquema de frontmatter — content-model.md
- Construye el cargador (memoizado, filtrado por
.md, analizador de respaldo) — content-loader.md - Conecta locales, alternates y redirecciones entre locales — i18n-and-routing.md
- Añade agrupación de etiquetas y páginas de etiqueta — tags.md
- Construye las tres rutas, metadatos y JSON-LD — pages-and-seo.md
- Renderiza cuerpos y portadas — rendering.md
- Añade el script de validación y lee las brechas operativas — operations.md
- Ejecuta los fixtures y las pruebas — testing.md
¿Portando esto a una aplicación existente? Completa primero la tabla de costuras en adaptation.md — lleva diez minutos y evita un renombrado.
Directorio de referencias
| Escenario | Palabras clave de activación | Referencia |
|---|---|---|
| Campos de frontmatter, borradores, claves de traducción, relaciones | frontmatter, schema, translationKey, draft, related, excerpt | content-model.md |
| Lectura de archivos, caché, fallos de YAML, tiempo de lectura | gray-matter, fs, cache, memoize, ENOENT, YAMLException, parse | content-loader.md |
| Locales, hreflang, alternates, enlaces a locale incorrecto | i18n, locale, hreflang, alternates, canonical, language switcher, redirect | i18n-and-routing.md |
| URLs de etiquetas, colisiones, páginas de etiqueta ligeras | tag, slug, diacritics, collision, tag page, taxonomy | tags.md |
| Las rutas, metadatos, JSON-LD, sitemap, RSS | page.tsx, generateStaticParams, generateMetadata, dynamicParams, sitemap, RSS, feed, BlogPosting | pages-and-seo.md |
| Markdown a React, bloques de código, imágenes de portada | react-markdown, remark-gfm, syntax highlighting, cover, motif, accent, XSS | rendering.md |
| Validación de contenido, coste de compilación, flujo de trabajo del editor | validation, CI check, build time, drafts, authoring, operator | operations.md |
| Fixtures y la suite de pruebas que pasa | test, fixture, vitest, node:test, regression, assert | testing.md |
| Integración en una aplicación anfitriona: costuras, renombres, prueba | adapt, port, host, seam, rename, CMS, integrate | adaptation.md |
| El registro de auditoría: qué cambió, qué se mantuvo y qué se añadió vs. la implementación anterior | provenance, deviations, fidelity, audit, ledger | provenance.md |