Blog Markdown (multilíngue, com tags)
Um blog onde o conteúdo são arquivos, não linhas em um banco de dados. Todo o design gira em torno de uma ideia:
a identidade de um post é sua translationKey, não seu slug. Os slugs são localizados
para SEO e diferem por idioma; a chave é o que faz três arquivos serem um único artigo,
e é ela que alimenta hreflang, o seletor de idioma, posts relacionados e
redirecionamentos entre localidades. Acertar esse campo resolve todo o resto.
Escrito pelo engenheiro que já enviou este módulo em produção. A implementação anterior
contra a qual foi auditado era um blog de marketing multilíngue, gerado estaticamente,
com seu sitemap e superfície de SEO. Os fatos e regras abaixo são o que o conjunto
de camadas de conteúdo e a build verificam; references/provenance.md contém o registro.
Quando usar
- Conteúdo escrito como markdown no repositório, implantado junto com o aplicativo
- Mais de um idioma, com slugs diferentes por idioma
- Páginas de tag, posts relacionados, feeds ou hreflang estão no escopo
- Você deseja que tudo seja gerado estaticamente no momento da build
Quando NÃO usar
| Em vez disso | Use |
|---|---|
| Um CMS hospedado é a fonte da verdade | o SDK desse CMS; mantenha apenas references/content-model.md |
| Um site de documentação / central de ajuda (categorias, barra lateral, busca) | um skill de central de ajuda — modelo de navegação diferente |
| Um blog de único local com <10 posts | fs puro + gray-matter inline; isso aqui é overhead |
| Renderizar markdown enviado por usuário | um renderizador com sanitização; veja a nota sobre XSS em references/rendering.md |
Arquitetura
content/blog/<locale>/<localized-slug>.md # frontmatter + corpo
|
v
parseFrontmatter --YAML, com fallback para um parser linha a linha em aspas malformadas
|
v
getAllPosts(locale) <-- memoizado por localidade em produção
|
+--> translationKey ----> alternates / hreflang / troca de idioma
+--> tags -> SLUG da tag ---> páginas de tag (slug é a identidade, não o rótulo)
+--> related (slugs EN) -> posts relacionados, com fallback para EN
|
v
/blog /blog/[slug] /blog/tag/[slug] sitemap.xml feed.xml
Fatos críticos
- As relações são escritas uma vez, no locale padrão, como slugs do locale padrão.
Um arquivo traduzido pode omitir
relatedcompletamente e ainda obter os posts relacionados corretos, resolvidos viatranslationKey, de modo que cada tradução renderize a mesma seção de relacionados. O teste de fallback garante isso. - A identidade de uma tag é seu slug, não seu rótulo.
"AI Agents"e"AI agents"resultam no mesmo slug e na mesma URL. Agrupe por slug e combine posts por slug, para que todas as variantes de grafia de um post apareçam na mesma página de tag. Dois testes garantem isso. gray-matterlança exceção em um escalar YAML entre aspas duplas contendo caracteres de aspas literais — aspas tipográficas em um excerto traduzido fazem isso. Sem um parser de fallback a build para naquele único arquivo; o fallback linha a linha mantém o carregamento. Verificado contra a biblioteca e coberto por um teste.gray-matterarmazena em cache uma análise com falha como resultado vazio. Ele escreve sua entrada de cache antes de analisar, então a segunda e todas as análises posteriores de um arquivo que lançou exceção retornam{}— sem erro, sem frontmatter. Sempre chamematter(contents, {}); qualquer objeto de opções desativa o cache.- O carregamento é O(posts x localidades) por página, a menos que você memorize.
Memoize por localidade para que cada arquivo de conteúdo seja lido uma vez por
build; resolver alternates em
generateMetadataresulta em uma amplificação de 71x de leitura de arquivo, medida na implementação anterior. getPostSlugsdeve filtrar.md. O filtro evita que um.DS_Storeperdido se torne uma leitura de.DS_Store.mde umENOENT. Um teste garante isso.- Páginas de tag não têm identidade entre localidades. Tags são texto livre por localidade, então o seletor de idioma deve cair no índice do blog em uma URL de tag, em vez de construir uma URL que retorne 404.
Regras rígidas
Nunca use o slug como chave para buscas entre localidades. Os slugs são localizados propositalmente.
translationKeyé a coluna de junção; uma correspondência de slug entre localidades é coincidência, não relação.
Nunca deixe
getPostBySlugdecidir a visibilidade de rascunhos implicitamente. Filtre rascunhos no carregador;dynamicParams = falseos esconde apenas como efeito colateral e para de fazê-lo assim que a flag mudar. O teste de rascunho garante o filtro.
Nunca renderize uma constante "5 min de leitura". Calcule a partir do corpo; custa um
split, e o teste de tempo de leitura garante isso.
Nunca derive a URL de uma página de tag do rótulo no momento da leitura e o rótulo da URL no momento da renderização sem uma verificação de colisão. Esse caminho de ida e volta só é sem perdas enquanto cada rótulo resultar em um slug único.
Início rápido
- Defina o contrato de localidade e o esquema de frontmatter — content-model.md
- Construa o carregador (memoizado, filtrado por
.md, parser de fallback) — content-loader.md - Conecte localidades, alternates e redirecionamentos entre localidades — i18n-and-routing.md
- Adicione o agrupamento de tags e páginas de tag — tags.md
- Construa as três rotas, metadados e JSON-LD — pages-and-seo.md
- Renderize corpos e capas — rendering.md
- Adicione o script de validação e leia as lacunas operacionais — operations.md
- Execute os fixtures e testes — testing.md
Portando isso para um aplicativo existente? Preencha primeiro a tabela de junção em adaptation.md — leva dez minutos e economiza uma renomeação.
Diretório de referência
| Cenário | Palavras-chave de gatilho | Referência |
|---|---|---|
| Campos de frontmatter, rascunhos, chaves de tradução, relações | frontmatter, schema, translationKey, draft, related, excerpt | content-model.md |
| Leitura de arquivos, cache, falhas de YAML, tempo de leitura | gray-matter, fs, cache, memoize, ENOENT, YAMLException, parse | content-loader.md |
| Localidades, hreflang, alternates, links de localidade errada | i18n, locale, hreflang, alternates, canonical, language switcher, redirect | i18n-and-routing.md |
| URLs de tag, colisões, páginas de tag enxutas | tag, slug, diacritics, collision, tag page, taxonomy | tags.md |
| As rotas, metadados, JSON-LD, sitemap, RSS | page.tsx, generateStaticParams, generateMetadata, dynamicParams, sitemap, RSS, feed, BlogPosting | pages-and-seo.md |
| Markdown para React, blocos de código, imagens de capa | react-markdown, remark-gfm, syntax highlighting, cover, motif, accent, XSS | rendering.md |
| Validando conteúdo, custo de build, fluxo do editor | validation, CI check, build time, drafts, authoring, operator | operations.md |
| Fixtures e o conjunto de testes aprovado | test, fixture, vitest, node:test, regression, assert | testing.md |
| Adaptando a um aplicativo host: junções, renomeações, sonda | adapt, port, host, seam, rename, CMS, integrate | adaptation.md |
| O registro de auditoria: o que mudou, foi mantido e adicionado vs. a implementação anterior | provenance, deviations, fidelity, audit, ledger | provenance.md |