Complemento del blog · Artículos

Los Artículos: Markdown que se vuelve página

La pieza central del blog. Cada entrada es un archivo Markdown con unos metadatos al inicio; la plantilla la convierte en una página completa —con su SEO, sus fechas y sus datos estructurados—. El autor escribe; el sistema arma el resto.

Esta página no es una ficha técnica: es el complemento entero, abierto y explicado. Qué es un artículo aquí, de qué partes se compone, cómo se comporta en el teléfono, dónde encaja y, al final, cómo está construido —del archivo .mdx al Article JSON-LD que emite el layout—.

Con una idea que conviene marcar desde el principio: contenido y diseño van separados a propósito. El .mdx solo lleva el texto y unos metadatos tipados; la presentación, el SEO y la estructura los resuelve la plantilla una sola vez. Publicar es agregar un archivo —el listado, la paginación y la página de detalle se generan solas—.

Definición

¿Qué es un artículo del blog?

Un archivo Markdown (.mdx) con dos partes: un frontmatter tipado con los metadatos y el cuerpo del texto. La plantilla lo convierte en una página completa.

Un artículo, en esta plantilla, es un único archivo .mdx dentro de la colección de contenido. Arriba lleva el frontmatter —un bloque entre --- con los metadatos: título, descripción, categoría, fecha, imagen, tags—; debajo, el cuerpo en Markdown: encabezados, párrafos, listas, citas, enlaces, código. Nada de HTML ni de plantilla: solo contenido y datos.

No se inventó aquí: es el patrón estándar del contenido en sitios modernos —Markdown con metadatos al frente—. Lo propio es que ese frontmatter está tipado y validado: forma parte de una Content Collection con esquema Zod, así que el build comprueba cada campo. Y al ser .mdx, el cuerpo puede incluir componentes de Astro cuando una entrada necesita algo más que texto.

Función e importancia

¿Para qué sirve?

Hace tres trabajos: separa el contenido del diseño, valida los datos en build para evitar errores, y deja el SEO y la estructura ya resueltos por la plantilla.

Su función es que escribir un artículo no exija saber maquetar. Quien redacta abre un .mdx, llena unos metadatos y escribe en Markdown; no toca HTML, CSS ni el layout. La presentación se decide una vez, en la plantilla, y vale para todos los artículos. Cambiar el diseño del blog no obliga a reeditar una sola entrada.

Y lo hace sin sacrificar rigor ni SEO. El frontmatter pasa por Zod, así que un campo mal escrito detiene el build antes de publicar; y cada artículo emite su Article JSON-LD, sus breadcrumbs y sus meta a partir de ese mismo frontmatter, sin trabajo extra. Tres efectos —autoría simple, datos correctos, SEO de fábrica— en una sola pieza.

Contenido separado del diseño

El autor escribe en Markdown —texto, encabezados, listas, enlaces— sin tocar HTML ni CSS ni el layout. La presentación la resuelve la plantilla una sola vez; el .mdx solo lleva el contenido. Quien redacta no necesita saber Astro, y un cambio de diseño no obliga a reeditar cada artículo.

Validación que evita errores

Cada frontmatter pasa por Zod en build. Un título demasiado largo, una categoría inventada, una fecha mal escrita o una imagen sin la ruta correcta detienen la compilación con un mensaje claro. Los errores se atrapan antes de publicar, no cuando ya están en producción rompiendo el SEO.

SEO y estructura ya resueltos

Cada artículo emite su Article JSON-LD, sus breadcrumbs, su bloque de relacionados y sus meta —todo derivado del frontmatter, sin trabajo extra—. El autor se concentra en escribir bien; la plantilla se encarga de que Google entienda fecha, autor, sección e imagen de cada entrada.

Anatomía

¿Qué lleva un artículo?

Cinco piezas: el frontmatter tipado, el cuerpo en Markdown/MDX, la Content Collection que lo valida, la ruta dinámica que lo renderiza y el Article JSON-LD que emite el layout.

Cada pieza cumple un papel claro. El frontmatter aporta los datos; el cuerpo, el texto. La colección valida ese frontmatter y agrupa todos los artículos; la ruta dinámica los convierte en páginas; y el layout emite el JSON-LD y los meta. Dos piezas visibles en el archivo (frontmatter y cuerpo), tres invisibles que las orquestan —pero todas necesarias—.

Abajo, el ejemplo en vivo —un .mdx en miniatura, anotado—. Cada punto numerado se desglosa en su tarjeta: qué resuelve y de qué dato sale. El frontmatter va entre los guiones de apertura; el cuerpo, debajo; y las tres piezas de sistema (colección, ruta, schema) trabajan detrás para que publicar sea solo agregar el archivo.

1

Frontmatter tipado

El bloque entre --- al inicio del .mdx: los metadatos del artículo. title, description, category, heroImage y pubDate son la columna vertebral; tags, updatedDate, author y los relacionados afinan el resto. No es texto libre: cada campo lo valida Zod en build.

Dato title · description · category · pubDate · tags…

2

El cuerpo (Markdown/MDX)

Lo que va debajo del frontmatter: el artículo en sí, en Markdown. Encabezados, listas, citas, enlaces y código se escriben en texto plano. Al ser .mdx, además admite componentes de Astro cuando una entrada necesita algo interactivo —un callout, una demo—.

Dato Markdown → admite <Componentes /> (MDX)

3

La Content Collection

La colección articulos (content.config.ts): un glob loader que recoge cada .mdx de src/content/articulos/ y un esquema Zod que valida su frontmatter. Si falta un campo o tiene mal tipo, el build se detiene con un error claro —los datos malos no llegan a producción—.

Dato glob() + z.object().strict() en content.config.ts

4

La ruta dinámica

src/pages/blog/[...slug].astro: una sola ruta que sirve TODOS los artículos. getStaticPaths recorre la colección y genera una página por entrada en build; render(articulo) devuelve el <Content /> que pinta el cuerpo dentro de ArticleLayout. Publicar es agregar un .mdx, no tocar código.

Dato [...slug].astro → render() → <Content /> → ArticleLayout

5

El Article JSON-LD + meta

ArticleLayout arma el schemaData con las claves del Article schema —datePublished, dateModified, author y section (la categoría)— a partir del frontmatter. De ahí salen el JSON-LD que entiende el buscador y los meta og:article:*. Datos estructurados sin escribir una línea de schema a mano.

Dato schemaData.article · datePublished/dateModified/author/section

Variantes

Otros diseños y aplicaciones

El de esta plantilla es un .mdx con frontmatter tipado y cuerpo Markdown, pero el artículo cambia de cara según lo que pida: imagen de cabecera, componentes MDX, índice, borrador o relacionados.

No hay un único tipo de artículo: hay una misma estructura —metadatos + cuerpo— que cada caso aprovecha distinto. Una guía corta vive de texto bien estructurado; una de cabecera abre con su heroImage; un tutorial técnico incrusta componentes; una pieza de referencia suma índice; y un borrador (draft) ni siquiera se publica.

Abajo, seis variantes —casi todas son configuraciones reales del mismo esquema (cambiando campos del frontmatter o el contenido del cuerpo), y un par son extensiones sobre el layout, como el índice—. Cada una con su réplica en vivo y el tipo de artículo donde rinde mejor.

  • Artículo estándar (guía)

    Blog · Guía

    La entrada típica: frontmatter mínimo (title, description, category, heroImage, pubDate) y cuerpo en Markdown con encabezados y listas. Es el 90% de los artículos del blog —texto bien estructurado, sin nada interactivo—. Configuración real de la colección.

  • Con imagen de cabecera

    Editorial · Destacado

    El mismo artículo, con un heroImage que abre la lectura. En esta plantilla la cabecera es obligatoria (heroImage en el schema); el matiz es que hoy se sustituye por una imagen del pool —ver «¿Dónde va?»—. La figura va a ancho completo y alimenta la og:image.

  • Con componentes MDX

    Tutorial · Técnico

    Cuando el texto pide algo más que prosa: un callout de aviso, un bloque de código resaltado, una tabla o una mini-demo. Al ser .mdx, el cuerpo importa y usa componentes de Astro entre el Markdown. Soporte de fábrica —no requiere tocar la ruta ni el layout—.

  • Guía larga con índice

    Referencia · Pilar

    Artículos extensos —los de referencia— donde conviene un índice del contenido (anclas a cada H2/H3) que acompañe el scroll. Es el cuerpo Markdown de siempre; el índice se deriva de los encabezados. Extensión sobre el layout, sin cambiar el frontmatter.

  • Borrador (no se publica)

    Editorial · WIP

    Un .mdx con draft: true. La ruta dinámica filtra los borradores en getStaticPaths, así que no genera página ni aparece en el listado: el artículo existe en el repo pero no en el sitio. Útil para escribir con calma antes de publicar. Soporte de fábrica.

  • Con relacionados manuales

    Cross-sell · Negocio

    Frontmatter con relatedProducts o relatedServices: referencias tipadas a otras colecciones. Conectan el artículo con las fichas que cotizan —del contenido editorial al negocio— y las valida Zod por slug. Campos opcionales del mismo esquema.

Responsive y móvil

Cómo se comporta en el teléfono

En móvil lo que manda es la legibilidad del cuerpo: tipografía fluida y una medida cómoda, imagen de cabecera sin saltos, y código o tablas que se desplazan dentro de su caja en vez de desbordar.

Un artículo es, ante todo, texto para leer. En el teléfono eso significa una columna única donde la tipografía escala con clamp() y la medida no se dispara —una línea demasiado larga cansa—. El cuerpo (.prose) acota su ancho a unas 66 columnas para que la lectura sea cómoda con cualquier longitud de pantalla.

A partir de ahí, tres patrones que cuidan los puntos donde un artículo suele romperse en móvil: la imagen de cabecera fluida y sin CLS (reservando su hueco con width/height), y los bloques anchos —código y tablas— que scrollean dentro de su caja sin empujar el layout. Cada uno, abajo, con su vista en el teléfono y su receta.

1 · Tipografía fluida del cuerpo

El cuerpo (.prose) usa tamaños con clamp() y acota la medida a ~66 columnas, así la línea nunca queda demasiado larga. La lectura es cómoda lo mismo en un teléfono que en escritorio, sin saltos de tamaño.

CSS · tipografía fluida y medida
/* MOVIL · tipografia del articulo: legible y de medida comoda.
   El cuerpo (.prose) usa una medida acotada (~66ch) para que la
   linea no sea demasiado larga, y tamanos fluidos con clamp(). */

.prose {
  max-width: 66ch;                 /* medida de lectura comoda */
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.125rem);
  line-height: var(--leading-relaxed);
}
.prose h2 { font-size: clamp(1.4rem, 1.2rem + 1.4vw, 2rem); }

2 · Imagen de cabecera fluida (sin CLS)

La cabecera usa max-width:100% y height:auto para ocupar el ancho conservando su proporción. Los atributos width/height en el <img> reservan el hueco y evitan el salto de layout (CLS) al cargar.

CSS · imagen hero responsive
/* MOVIL · imagen de cabecera fluida y sin saltos de layout.
   width:100% + height:auto la hace responsive; los atributos
   width/height en el <img> reservan el espacio y evitan el CLS. */

.post__hero img {
  width: 100%;
  height: auto;            /* conserva la proporcion */
  border-radius: var(--radius-lg);
}
/* En el HTML: <img ... width="1200" height="630"> para reservar el hueco. */

3 · Código y tablas con overflow-x

Un <pre> o una <table> anchos no caben en un teléfono. Con min-width:0 en la columna y overflow-x:auto en el bloque, se desplazan DENTRO de su caja en vez de empujar y romper la página.

CSS · scroll local en bloques anchos
/* MOVIL · codigo y tablas que no caben se desplazan, no desbordan.
   En el grid del articulo, el contenido va con min-width:0 para que
   un <pre> o una <table> anchos scrolleen DENTRO de su caja. */

.prose { min-width: 0; }   /* la columna puede encoger bajo el min-content */

.prose pre,
.prose table {
  display: block;
  max-width: 100%;
  overflow-x: auto;        /* scroll horizontal local, sin romper la pagina */
}

Posición

¿Dónde vive un artículo?

El archivo, en src/content/articulos/; la página resultante, en /blog/<slug>. Una sola ruta dinámica los sirve a todos —no hay un .astro por artículo—.

El artículo no es una página suelta: es un .mdx dentro de la colección, en src/content/articulos/. De ahí lo recoge el glob loader, lo valida Zod y lo sirve la ruta dinámica [...slug].astro como /blog/<slug>. Su cara en el listado es la tarjeta de artículo; su cierre, el bloque de relacionados; y su posición en el sitio la declara el breadcrumb.

Un matiz real de esta plantilla conviene marcarlo: el frontmatter exige heroImage, pero hoy ese valor se sustituye por blogImage(id) en la ruta dinámica, porque las imágenes .avif de la demo todavía no existen. El campo sigue siendo obligatorio en el esquema (un sitio real pondrá su ruta), pero la imagen que se pinta sale del pool compartido mientras tanto.

Piezas cercanas: la tarjeta de artículo (su cara en el listado), los relacionados (cómo cierra cada artículo) y los breadcrumbs (la jerarquía que declara su posición).

Implementación

Cómo está construido

Un .mdx con frontmatter tipado, una colección con glob loader + Zod que lo valida, y una ruta dinámica que hace getStaticPaths sobre la colección, render(articulo) → <Content /> y lo pasa a ArticleLayout.

El flujo va de archivo a página sin intermediarios. El autor escribe el .mdx (frontmatter + cuerpo). La colección articulos —glob loader sobre src/content/articulos/ + esquema Zod .strict()— recoge cada archivo y valida su frontmatter en build: si algo falta o tiene mal tipo, el build se detiene. Los datos buenos quedan disponibles para todo el sitio.

La ruta [...slug].astro cierra el círculo: getStaticPaths recorre la colección (filtrando borradores) y genera una página por entrada; render(articulo) devuelve el componente <Content /> que pinta el cuerpo; y ArticleLayout lo envuelve y emite el Article JSON-LD (datePublished, dateModified, author, section) más los meta. Publicar es agregar un archivo —el resto se genera solo—.

Markdown · un .mdx de ejemplo (frontmatter + cuerpo)
---
title: "Migas de pan en Astro: guía paso a paso"
description: "Cómo construir un componente de breadcrumbs accesible en Astro y conectarlo al BreadcrumbList JSON-LD, sin duplicar el emisor."
category: "guias"
heroImage: "/images/articulos/migas-de-pan-astro.avif"
pubDate: 2026-03-12
updatedDate: 2026-04-02
author: "Ejemplos.mx"
tags: ["astro", "breadcrumbs", "seo"]
relatedServices: ["diseno-web"]
---

El componente de migas resuelve dos cosas a la vez: orienta al lector
y le declara a Google la jerarquia de la pagina. Aqui lo construimos
desde cero.

## Por que importa

Una barra de migas convierte una URL opaca en un rastro legible...

## El componente

Para mostrar codigo con llaves o etiquetas, usa un bloque cercado
(como este) — NUNCA backticks en linea con esos simbolos en .mdx.
TypeScript · el esquema Zod de la colección (content.config.ts)
// content.config.ts · la coleccion 'articulos' (extracto).
import { defineCollection, reference, z } from 'astro:content'
import { glob } from 'astro/loaders'

const ARTICLE_CATEGORIES = ['guias', 'novedades', 'general'] as const

const articulos = defineCollection({
  // glob loader: recoge cada .mdx de la carpeta de articulos.
  loader: glob({ pattern: '**/*.mdx', base: './src/content/articulos' }),
  schema: z
    .object({
      title: z.string().min(10).max(70),       // <=70 para el SERP
      description: z.string().min(70).max(160),
      category: z.enum(ARTICLE_CATEGORIES).default('general'),
      heroImage: z.string().regex(/^\/images\//), // ruta obligatoria
      pubDate: z.coerce.date(),
      updatedDate: z.coerce.date().optional(),
      author: z.string().default('Ejemplos.mx'),
      tags: z.array(z.string()).max(10).optional(),
      relatedProducts: z.array(reference('productos')).optional(),
      relatedServices: z.array(reference('servicios')).optional(),
      draft: z.boolean().default(false),
    })
    .strict(),  // rechaza campos desconocidos
})

export const collections = { articulos }
Astro · la ruta dinámica (getStaticPaths + render → ArticleLayout)
---
// src/pages/blog/[...slug].astro · sirve TODOS los articulos.
import { getCollection, render } from 'astro:content'
import ArticleLayout from '@layouts/ArticleLayout.astro'
import { blogImage } from '@lib/blogImages'

export async function getStaticPaths() {
  // Filtra borradores: un draft no genera pagina.
  const articulos = await getCollection('articulos', ({ data }) => !data.draft)
  return articulos.map((articulo) => ({
    params: { slug: articulo.id },     // /blog/<slug>
    props: { articulo },
  }))
}

const { articulo } = Astro.props
const { Content } = await render(articulo)  // compila el cuerpo .mdx
const d = articulo.data
---

<ArticleLayout
  title={d.title}
  description={d.description}
  category={d.category}
  pubDate={d.pubDate}
  updatedDate={d.updatedDate}
  author={d.author}
  image={blogImage(articulo.id)}  {/* hoy sustituye al heroImage */}
  tags={d.tags}
>
  <Content />   {/* el cuerpo del .mdx entra aqui */}
</ArticleLayout>

En concreto: la colección usa glob({ pattern: '**/*.mdx' }) para recoger cada archivo, y un esquema z.object().strict() que rechaza campos desconocidos —un hero_image: mal escrito no se ignora en silencio, detiene el build—. La ruta dinámica filtra con !data.draft en getStaticPaths, así que un borrador no genera página ni aparece en el listado.

El SEO es de fábrica y sale del mismo frontmatter: ArticleLayout arma el schemaData.article con datePublished, dateModified (por defecto, la fecha de publicación), author y section (la categoría), de donde el proyecto emite el Article JSON-LD una sola vez por página, junto con los breadcrumbs y los meta og:article:*. El cuerpo se inyecta por <slot> —lo rellena <Content />—, así que el texto y la estructura nunca se mezclan.

Buenas prácticas

Qué hacer y qué evitar

La diferencia entre un artículo que se publica limpio y uno que rompe el build cabe en un puñado de hábitos —empezando por un gotcha clásico de MDX—.

Ninguno de estos hábitos es capricho: salen de mirar dónde tropieza un blog en Markdown. Un artículo sano respeta los límites del esquema (título y descripción en su rango), usa la taxonomía real, pone alt a las imágenes y deja que la colección sea la única fuente. Uno que estorba inventa categorías, pega HTML suelto o cae en el gotcha de MDX con los backticks.

Ese gotcha merece subrayado porque rompe el build de forma desconcertante: en .mdx, el contenido dentro de backticks en la prosa se evalúa como JSX, así que unas llaves o una etiqueta entre backticks lanzan un error. La regla es simple —para código con símbolos, bloque cercado—. Abajo, lo que conviene y lo que conviene evitar, enfrentados.

Sí conviene

  • Mantén el title entre 10 y 70 caracteres y la description entre 70 y 160: lo exige Zod y lo agradece el SERP.
  • Una sola fuente: cada artículo es un .mdx de la colección; nada de páginas .astro sueltas (anti-patrón D3).
  • Usa los componentes de Astro en MDX para callouts o demos; el resto del texto, Markdown plano.
  • Pon alt descriptivo en cada imagen del cuerpo (y recuerda que el heroImage es obligatorio).
  • Rellena category y tags con la taxonomía real: alimentan archivos de categoría, etiquetas y el sidebar.

Mejor evita

  • En .mdx, NUNCA pongas llaves o etiquetas dentro de backticks en la prosa: MDX evalúa el contenido entre backticks como JSX y rompe el build. Para código con símbolos, usa un bloque de código cercado.
  • No inventes una categoría fuera del enum (guias, novedades, general): el build la rechaza.
  • No pegues HTML suelto por artículo para «maquetar» algo puntual: rompe la coherencia y el responsive.
  • No subas un .mdx sin description o sin heroImage: Zod detiene la compilación.
  • No dupliques estructura entre artículos a mano: el layout y el listado se generan solos desde la colección.
¿Necesitas ayuda?