
MDX não é só texto quando gera uma rota pública
Um arquivo MDX pode parecer inofensivo: título, parágrafos e um botão no final.
Até ele definir uma URL que vai para o sitemap, aparecer em uma listagem pública, gerar metadados e apontar alguém para um agendamento. A partir daí, o artigo virou parte do sistema em produção.
A consequência é simples: revisão editorial continua necessária, mas não basta. O post precisa passar pelo mesmo tipo de verificação que protege uma mudança de rota: contrato de entrada, build e smoke do artefato final.
O caminho do arquivo já é uma decisão pública
Neste site, o padrão de arquivos em content/posts não serve só para organizar
pastas. A configuração do Velite
aceita apenas posts no formato posts/AAAA/MM/DD/slug/index.mdx e deriva a URL a
partir desse caminho.
posts: {
name: "Post",
pattern: "posts/**/index{,.en}.mdx",
schema: postsSchema.transform((data, { meta }) => {
const filePath = (meta.path ?? "").replace(/\\/g, "/");
const isEn = /\.en\.mdx$/.test(filePath);
const locale: Locale = isEn ? "en" : "pt";
const match = filePath.match(
/content\/posts\/(\d{4})\/(\d{2})\/(\d{2})\/([^/]+)\/index(?:\.en)?\.mdx$/,
);
if (!match) {
throw new Error(
`Post path does not match expected pattern: ${filePath}`,
);
}
const [, year, month, day, slug] = match;
const permalink =
locale === "pt"
? `/${year}/${month}/${day}/${slug}`
: `/en/${year}/${month}/${day}/${slug}`;
const rawBody = (meta.content ?? "") as string;
const rt = readingTime(rawBody);
const stripped = rawBody
.replace(/```[\s\S]*?```/g, "")
.replace(/[#*_>`~\-[\]()!]/g, "")
.replace(/\s+/g, " ")
.trim();
const excerpt =
data.description && data.description.length > 0
? data.description
: stripped.slice(0, 160) + (stripped.length > 160 ? "…" : "");
return {
...data,
body: rawBody,
locale,
slug,
permalink,
readingTime: Math.max(1, Math.round(rt.minutes)),
excerpt,
};
}),
},
},
prepare: ({ posts }) => {
const bySlug: Record<string, { pt?: string; en?: string }> = {};
for (const p of posts) {
bySlug[p.slug] ??= {};
bySlug[p.slug][p.locale as Locale] = p.permalink;
}
for (const p of posts) {
const sib = bySlug[p.slug];
p.translationSlug = p.locale === "pt" ? sib?.en : sib?.pt;
}
},content/posts/2026/10/01/mdx-nao-e-so-texto/index.mdx
→ /2026/10/01/mdx-nao-e-so-textoO frontmatter completa o contrato. title, description, date, draft, tags e
comentários não são anotações que o renderer pode ignorar. São dados que entram na
coleção de posts e alimentam páginas, listagens e metadados.
Por isso, trocar um slug no fim da escrita não é uma revisão estética. É trocar um endereço público. Antes de considerar o texto pronto, confirme o caminho final, links internos e os campos que o build exige.
Publicar não é só trocar draft para false
O estado público deste blog depende de duas condições: o post não pode ser draft e a data precisa já ter chegado. A regra está explícita na função de visibilidade:
export function isScheduled(
post: Schedulable,
now: Date = new Date(),
): boolean {
const at = new Date(post.date).getTime();
if (Number.isNaN(at)) return false;
return at > now.getTime();
}
/** Público: publicado (não-draft) e com a data já vencida. */
export function isPublic(post: Publishable, now: Date = new Date()): boolean {
return !post.draft && !isScheduled(post, now);
}Isso evita dois erros de operação diferentes:
- colocar no ar um rascunho que ainda não terminou;
- vazar um post agendado porque algum trecho de código filtrou apenas
draft.
A data precisa ter timezone explícito. 2026-10-01T11:00:00-03:00 descreve um
instante; “1º de outubro às 11h” depende de quem está lendo e de qual servidor
está calculando a visibilidade.
A mesma separação aparece em automações que escrevem fora do sistema. No post sobre idempotência em efeitos externos, retry e efeito são tratados como coisas diferentes. Em conteúdo, rascunho, agendamento e publicação também precisam ser estados distintos. Misturá-los produz publicação acidental ou um post pronto que nunca aparece.
PT-only também é uma decisão de release
Traduzir um post é útil quando a versão em inglês mantém os fatos e os links. Mas criar uma rota EN só para parecer bilíngue deixa uma falha pública para alguém encontrar.
O pipeline deste site só preenche translationSlug quando encontra o arquivo irmão
no mesmo slug. O
sitemap só anuncia hreflang quando essa contraparte existe. Sem par, o post PT continua
válido e não ganha uma alternância para uma página inexistente.
const allPosts = rawPosts as unknown as Post[];
for (const p of allPosts.filter((x) => isPublic(x, now))) {
const selfUrl = `${siteConfig.url}${p.permalink}`;
// Only advertise alternates when a real translation exists — otherwise we'd
// emit a self-referential hreflang for the "other" language, which is a
// non-reciprocal pair (mirrors the buildMetadata head fix).
let alternates: Entry["alternates"];
if (p.translationSlug) {
// translationSlug is an absolute path (starts with `/`), prepend site URL.
const otherUrl = `${siteConfig.url}${p.translationSlug}`;
alternates = {
languages:
p.locale === "pt"
? { "pt-BR": selfUrl, en: otherUrl }
: { en: selfUrl, "pt-BR": otherUrl },
};
}
entries.push({
url: selfUrl,
lastModified: p.updated ? new Date(p.updated) : new Date(p.date),
...(alternates ? { alternates } : {}),
});A regra prática é menos elegante em planilha e mais segura no navegador:
- publique PT quando a fonte e a revisão estiverem fechadas;
- publique EN apenas quando houver paridade factual real;
- teste as duas URLs quando o par existir;
- não derive rota por string e torça para ela resolver.
O build é onde texto encontra runtime
Um arquivo pode estar bem escrito e ainda falhar na única condição que importa: ser renderizado no build que vai para produção.
No repositório deste site, pnpm build executa primeiro a geração do Velite e depois
o build do Next.js, como definido no package.json:
"scripts": {
"predev": "velite build",
"build": "velite build && next build"
}Esse é o ponto de encontro entre frontmatter, MDX, componentes, tipos e rotas.
Depois do build, ainda falta a verificação que o comando não enxerga sozinho:
- iniciar o artefato final;
- pedir a URL nova por HTTP;
- confirmar
200, título e trecho principal; - conferir canonical e, se houver, alternates de idioma;
- seguir o CTA e validar o destino real.
Uma revisão que só abre o arquivo fonte pode aprovar um link quebrado, uma rota inexistente ou uma CTA que perdeu o destino. Um smoke só de HTTP pode aprovar uma página vazia. Os dois gates se complementam.
Um release pequeno, mas completo
Para conteúdo que cria superfície pública, o fluxo mínimo é este:
fonte verificável
→ MDX com contrato válido
→ links e frontmatter revisados
→ lint, tipo, testes e build
→ smoke da rota gerada
→ PR, CI e deploy
→ smoke em produçãoIsso não transforma cada artigo em um projeto de engenharia de duas semanas. Só elimina a suposição perigosa de que texto não quebra produção.
Fontes
- Velite config do site (
velite.config.ts, trecho acima) — schema, padrão de caminho, URL derivada e pareamento de idiomas. - Regra de visibilidade (
features/blog/lib/visibility.ts, trecho acima) — draft e data como condições independentes de publicação. - Sitemap do site (
app/sitemap.ts, trecho acima) — inclusão de posts públicos e alternates apenas para pares reais. - Scripts de build (
package.json, trecho acima) — geração de conteúdo antes do build do Next.js.
Próximo passo
Se sua equipe já publica documentação ou conteúdo que cria rotas, integrações e CTAs, mas a aprovação termina no texto, traga esse workflow para um diagnóstico de 30 minutos.
Posts relacionados
O retry não é o problema. Repetir o efeito é.
Um timeout não diz se a escrita externa aconteceu. Antes de automatizar uma ação com IA, defina a chave que representa a intenção, o registro do efeito e o caminho para conflito.
Os 5 registros que mostram se seu agente está operando — ou só respondendo
Resposta final não explica uma execução. Para recuperar um workflow com IA, registre correlação, decisão, chamada de ferramenta, transição de estado e handoff — sem despejar dados sensíveis no log.
Um agente não amadurece no prompt
Internet instável, um canal bloqueado e um QR Code impresso mostraram a diferença entre testar IA e operar um agente de verdade.


