Markdown con tipado de frontmatter nativo en Astro: cómo funciona y ejemplos

DUGLAS MORENODUGLAS MORENO 👁 3

Introducción

Astro es un framework de componentes web que ha asumido rápidamente la posición de "el framework que procesa Markdown de forma nativa". Podés crear páginas de contenido en Markdown y obtener frontmatter fuertemente tipado sin necesidad de plugins externos ni configuraciones adicionales. En este artículo, te guiaré a través del proceso paso a paso, desde cómo Astro extrae y parsea Markdown hasta cómo podés definir tipos TypeScript exactos para el frontmatter, y cómo usar esos tipos al renderizar contenido.

Este artículo está orientado a desarrolladores que ya están familiarizados con Astro (o con frameworks similares como Svelte o Vue) y que quieren llevar sus páginas Markdown al siguiente nivel con tipado estático. Si recién empezás con Astro, te recomiendo primero leer la documentación sobre Markdown de Astro para familiarizarte con la sintaxis básica.

Al final, tendrás un patrón reutilizable que podés copiar en cualquier proyecto Astro, con ejemplos de frontmatter simple y complejo, y verás el HTML real que se genera en cada caso.

Cómo Astro procesa Markdown

Soporte nativo de Markdown en Astro

Astro trata cada archivo .md como un componente de Astro, al igual que un archivo .astro. Cuando un archivo Markdown es importado, Astro ejecuta tres etapas principales:

  1. Parseo – El motor de Markdown de Astro (basado en remark) convierte el texto plano en un AST.
  2. Procesamiento – Los plugins remark/rehype aplican transformaciones (por ejemplo, enlaces automáticos, sintaxis de código resaltado, tablas).
  3. Renderizado – El AST se transforma en HTML, y el frontmatter (el bloque YAML/JSON al principio del archivo) se extrae como un objeto JavaScript plain.

Todo esto es así nativamente; no necesitas instalar @astrojs/markdown o @astrojs/remark. Solo tenés que colocar el bloque frontmatter entre triple guion (---).

El papel de remark y rehype

Astro soporta plugins de remark a través del archivo de configuración astro.config.mjs. Podés agregar remark plugins para generar HTML extra, pero para la mayoría de los casos de uso, los defaults son suficientes. La extracción del frontmatter es hecha por el parser propio de Astro, no por remark, por lo que el frontmatter permanece intacto independientemente de los plugins de remark que agregues.

Extracción del frontmatter

Cuando importas un archivo .md en TypeScript (o JavaScript), obtienes un módulo con dos exportaciones:

import { Content, frontmatter } from './mi-post.md';
  • Content – Un componente de Astro que, cuando se renderiza, inyecta el HTML del cuerpo del markdown.
  • frontmatter – Un objeto literal con las claves y valores del frontmatter. El tipo de este objeto es inferido automáticamente por la herramienta de tipos de Astro, siempre que tengas TypeScript habilitado.

Si querés ver cómo Astro almacena la información internamente, podés abrir la carpeta .astro de tu proyecto durante la compilación (no es necesario en desarrollo). Las cadenas del frontmatter se mantienen tal cual aparecen en el archivo, conservando tipos como Date, boolean, string[], etc. Esto es crucial para el tipado estricto.

Tipado de frontmatter en TypeScript

Habilitando TypeScript en tu proyecto Astro

La mayoría de los proyectos Astro ya vienen con TypeScript preconfigurado (tsconfig.json). Si no es así, podés ejecutar:

npm install --save-dev typescript @types/node ts-node
npm i -D astro@latest

Luego, ejecutá npx astro add typescript (o editá manualmente astro.config.mjs). Esto agregará un archivo tsconfig.json e habilitará la comprobación de tipos en el servidor de desarrollo.

Definición de tipos para módulos Markdown

Cuando imports un archivo .md, Astro exporta una variable frontmatter cuyo tipo es desconocido para TypeScript. Podés darle forma con una declaración interface o un tipo de tipo import:

1️⃣ Uso de inferencia directa (más simple)

---
// src/components/ResumenPost.astro
import { frontmatter } from '../content/articulo-nuevo.md';

// El tipo se infiere automáticamente:
const { title, author, publishedAt } = frontmatter;
---
<h1>{title}</h1>
<p>Publicado por {author} el {publishedAt}</p>

Si articulo-nuevo.md tiene un frontmatter como:

---
title: Mi artículo de blog
author: María López
date: 2023-11-20
published: true
---

Contenido del artículo...

El editor de TypeScript completará automáticamente title, author, date y published. El tipo inferido es algo como:

type Frontmatter = {
  title: string;
  author: string;
  date: string; // En realidad un Date, porque Astro lo parsea
  published: boolean;
};

2️⃣ Declaración de interfaz explícita (recomendado para esquemas complejos)

Cuando tenés muchas claves, valores anidados o campos opcionales, es más seguro definir una interfaz que describa exactamente el shape del frontmatter. Podés crear un archivo de tipos compartido (src/types/frontmatter.ts) y reexportarlo:

// src/types/frontmatter.ts
export interface MarkdownFrontmatter {
  /** Título de la publicación; requerido */
  title: string;
  /** Nombre del autor; requerido */
  author: string;
  /** Fecha de publicación en formato ISO; opcional */
  publishedAt?: string;
  /** Tags para SEO; opcional, array de strings */
  tags?: string[];
  /** Booleano para indicar si está publicado; por defecto false */
  draft?: boolean;
  /** Campos adicionales para metadatos personalizados */
  [key: string]: unknown;
}

Luego, en cualquier componente Astro, podés hacer:

---
// src/components/ArticuloResumen.astro
import type { MarkdownFrontmatter } from '../types/frontmatter';
import { frontmatter } from '../content/articulo-nuevo.md';

// Forzar un tipo de seguridad extra
const typedFM = frontmatter as MarkdownFrontmatter;
const { title, author, draft } = typedFM;
---
<div class="articulo">
  <h2>{title}</h2>
  <p>Escrito por {author}</p>
  {draft && <span class="borrador">Borrador</span>}
</div>

¿Por qué hacer el cast? Porque Astro solo puede inferir el tipo si hay una declaración de tipo que coincida con las exportaciones del módulo. Al importar type { MarkdownFrontmatter } (en lugar de import { … }), estás diciendo: "TypeScript, este es el shape esperado". El cast asegura que los valores coincidan en tiempo de compilación y evita errores molestos.

3️⃣ Uso de genéricos para reutilizar en múltiples archivos Markdown

Si manejas muchos posts de blog, podés abstraer el shape en un tipo genérico que se pueda importar en cada componente:

// src/utils/article-types.ts
export type ArticleFrontmatter<T extends string = string> = {
  title: string;
  slug: T;
  author: string;
  excerpt?: string;
  publishedAt: Date;
  tags?: string[];
};

Luego, en un componente:

---
// src/components/ArticuloFinal.astro
import type { ArticleFrontmatter } from '../utils/article-types';
import { frontmatter } from '../content/articulo-final.md';

const article = frontmatter as ArticleFrontmatter<'articulo-final'>;
const { title, slug, author } = article;
---
<h1>{title}</h1>
<p>Autor: {author}</p>
<p>Slug: {slug}</p>

El genérico slug es una ilustración de cómo podés parametrizar el tipo para cada publicación, asegurando que el slug que usás en la URL coincida con el declarado en el frontmatter.

Renderizando contenido Markdown con frontmatter tipado

Creando un componente Astro para renderizar Markdown

Una vez que tenés el frontmatter tipado, podés renderizar el cuerpo del markdown usando el export Content del módulo. Content es un componente de Astro que acepta el frontmatter como Astro.props. El componente sabe cómo renderizar HTML sin necesidad de set:html manualmente.

Ejemplo: Tarjeta de blog con imagen

Archivo Markdown (src/content/post-1.md)

---
title: Primeros pasos con Astro
tags: ["astro", "markdown", "beginner"]
publishedAt: 2024-01-05
author: Juan Pérez
image: /blog/astro-hero.webp
---

Astro hace que escribir Markdown sea casi tan fácil como escribir HTML. En esta publicación, verás cómo crear una página con **Markdown nativo**, usar frontmatter tipado y vincular a otras páginas sin problemas.

Componente Astro (src/components/BlogCard.astro)

---
import { Content, frontmatter } from '../content/post-1.md';
import type { MarkdownFrontmatter } from '../types/frontmatter';

// Forzar el tipo
const typedFM = frontmatter as MarkdownFrontmatter;
const { title, author, image, tags } = typedFM;
---
<div class="tarjeta">
  <img src={image} alt=`Portada de ${title}` />
  <h2>{title}</h2>
  <p>Por {author}</p>
  <ul>
    {tags?.map(tag => <li>{tag}</li>)}
  </ul>
  <Content />
</div>

Salida HTML esperada

Al renderizar <BlogCard/> en una página, el navegador ve:

<div class="tarjeta">
  <img src="/blog/astro-hero.webp" alt="Portada de Primeros pasos con Astro" />
  <h2>Primeros pasos con Astro</h2>
  <p>Por Juan Pérez</p>
  <ul>
    <li>astro</li>
    <li>markdown</li>
    <li>beginner</li>
  </ul>
  <p>Astro hace que escribir Markdown sea casi tan fácil como escribir HTML. En esta publicación, verás cómo crear una página con <strong>Markdown nativo</strong>, usar frontmatter tipado y vincular a otras páginas sin problemas.</p>
</div>

El <Content /> inyecta el HTML del markdown directamente, por lo que no hay necesidad de set:html o dangerouslySetInnerHTML.

Ejemplo avanzado: Lista de artículos con metadatos dinámicos

Imagina que tienes un array de archivos Markdown (posts/) y querés generar una página de índice. Astro provee import.meta.glob para importar múltiples módulos Markdown a la vez, manteniendo los tipos:

// src/pages/articulo/index.astro
---
// Importa todos los archivos .md dentro de src/content/
const markdownModules = import.meta.glob('./content/*.md', { eager: true });

// Define un tipo para cada entrada de módulo
interface PostModule {
  frontmatter: MarkdownFrontmatter;
  default: astroComponentConstructor;
}

// Convierte el objeto a array
const posts = Object.entries(markdownModules).map(([path, module]) => {
  const { frontmatter } = module as PostModule;
  return { path, frontmatter };
});

// Filtra solo los publicados
const publishedPosts = posts.filter(post => !post.frontmatter.draft);
---

<h1>Artículos publicados</h1>
<ul>
  {publishedPosts.map(({ frontmatter, path }) => (
    <li>
      <a href={frontmatter.slug + '.html'}>{frontmatter.title}</a> – {frontmatter.author}
    </li>
  ))}
</ul>

Puedes ver la salida HTML resultante (simplificada):

<h1>Artículos publicados</h1>
<ul>
  <li><a href="primeros-pasos-con-astro.html">Primeros pasos con Astro</a> – Juan Pérez</li>
  <li><a href="guia-avanzada-de-markdown.html">Guía avanzada de Markdown</a> – María Gómez</li>
</ul>

Cada post mantiene su frontmatter tipado gracias a MarkdownFrontmatter, evitando errores como tratar un Date como un string.

Buenas prácticas y errores comunes

Mantener el esquema de frontmatter consistente

  • Reutiliza interfaces – En lugar de reescribir interface en cada componente, crea un archivo centralizado (por ejemplo, src/types/frontmatter.ts).
  • Documenta cada campo – Usa comentarios de JSDoc para describir el propósito, las opciones permitidas y los valores por defecto.
  • Valida en tiempo de compilación – Utiliza zod o valibot a través de un script de prebuild si necesitas validación en tiempo de ejecución.

Usar configuración de TypeScript estricta

{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true
  }
}

Estas opciones ayudan a detectar accesos no seguros a undefined, y te avisan cuando usás frontmatter sin haber hecho un cast.

Aprovecha los tipos de utilidad de TypeScript

  • Partial<T> – Cuando estás editando un frontmatter y querés hacer solo algunas claves obligatorias.
  • Pick<T, K> – Si solo necesitás un subconjunto de campos (por ejemplo, solo title y slug).
  • Record<K, V> – Útil para permitir claves personalizadas ([key: string]: unknown). Luego podés usar keyof para iterar de forma segura sobre claves desconocidas.

Evita la coerción de tipos en tiempo de ejecución

Astro parsea las fechas como Date automáticamente; no las conviertas a cadena a menos que realmente necesites una cadena. De lo contrario, podrás encontrar errores como:

// Malo – frontmatter.date es Date, pero lo tratas como string
const iso = frontmatter.date.toISOString(); // en realidad, frontmatter.date es Date, no string

En su lugar:

const iso = frontmatter.date instanceof Date ? frontmatter.date.toISOString() : frontmatter.date;

Errores comunes

  1. Olvidar hacer un cast – Si intentas desestructurar frontmatter sin un cast, TypeScript a veces infiere any. Esto puede ocultar errores en tiempo de ejecución.
  2. Usar --- en lugar de --- – Algunas personas piensan que necesitan un separador especial. Astro usa --- por defecto; ---astro es para componentes .astro.
  3. ** Mezclar YAML y JSON ** – Astro espera YAML. Si usas JSON, necesitarás un adaptador extra.
  4. Esperar que frontmatter sea un propfrontmatter es una exportación del módulo, no un prop de componente. Pasar frontmatter como Astro.props solo funciona si estás renderizando el Content component.

Conclusión

Astro hace que el Markdown sea fácil, y el frontmatter tipado es prácticamente gratuito. Al seguir los pasos aquí presentados, podés:

  • Configurar TypeScript en el proyecto (si aún no lo tienes).
  • Definir una o más interfaces que reflejen el shape de tu frontmatter.
  • Usar importaciones con cast para asegurarte de que cada componente vea el mismo shape.
  • Renderizar Markdown con <Content /> sin sacrificar el autocompletado de tipos.

Ahora tenés un flujo de trabajo reproducible que combina la simplicidad del Markdown con la seguridad del tipado estático. El resultado son páginas más mantenibles, mejoradas por SEO y menos errores molestos en tiempo de ejecución.

Próximos pasos:

  • Experimenta con remark plugins para agregar tabla de contenidos o numeración de heading.
  • Integra un esquema de validación como zod para garantizar que cada frontmatter cumpla con el estándar.
  • Considera usar astro:content (próximamente) para queries tipadas en contenido Markdown.

Si seguís estos patrones, tus publicaciones en Markdown serán tan potentes como cualquier componente de framework, con la tranquilidad adicional de TypeScript vigiando cada clave del frontmatter.

Referencias


Si tenés alguna pregunta o quieres ver más ejemplos, dejame un comentario abajo. ¡Feliz programación!

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario