Volver
Desarrollo Web

Cómo crear un blog automatizado con n8n y Astro usando IA para generar posts

El problema: ideas sueltas que nunca se convierten en posts

Tengo un problema clásico: muchas ideas técnicas y poco tiempo para desarrollarlas.

Apunto cosas en Notion, en un bloc de notas… y ahí se quedan. El cuello de botella no es la idea. Es sentarme, estructurar, escribir el frontmatter, crear el archivo en la colección de Astro y dejarlo listo para publicar.

Así que hice lo que haría cualquier desarrollador con tendencia a automatizarlo todo: monté un flujo en n8n que toma una idea, la desarrolla con IA y genera directamente el .mdx dentro de src/content/blog en mi proyecto Astro.

Sin copiar y pegar. Sin abrir el editor.

Solo una idea. El resto lo hace el flujo.


Arquitectura general

El stack es simple:

  • Astro con @astrojs/content para gestionar la colección blog.
  • n8n como orquestador.
  • OpenAI (o modelo compatible) para generar el contenido.
  • Acceso al repo vía GitHub API.

El flujo hace esto:

  1. Recibe una idea (manual o desde webhook).
  2. La pasa a un prompt estructurado.
  3. La IA devuelve el post completo en Markdown, incluyendo frontmatter.
  4. n8n crea un archivo .md en la colección de Astro.
  5. Hace commit automático.

La estructura del proyecto en Astro es la típica:

src/
  content/
    blog/
      como-optimice-lcp.md
      arquitectura-frontend-modular.md
  content.config.ts

Yo uso colecciones tipadas. Algo así:

// src/content.config.ts
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    draft: z.boolean().default(false),
    tags: z.array(z.string()),
    categories: z.array(z.string()),
    lang: z.enum(['es', 'en'])
  })
});

export const collections = {
  blog
};

Si el frontmatter no cumple el schema, el build falla. Y eso es clave: la IA tiene que generar contenido válido.


Paso 1: Diseñar el prompt como si fuera una API

El mayor error al usar IA en automatizaciones es tratarla como un chat.

Aquí no quiero creatividad descontrolada. Quiero estructura rígida. Determinista dentro de lo posible.

Mi prompt base en n8n tiene esta forma:

Eres Álvaro Moreiro, desarrollador web senior...

[Instrucciones de estilo completas]

Genera un post técnico siguiendo EXACTAMENTE este formato:

1. Frontmatter YAML válido.
2. Contenido en markdown.
3. Sin texto fuera del markdown.
4. Empieza por --- y termina con el último párrafo.

Datos del post:
- Tema: {{$json.idea}}
- Idioma: es
- Nivel: intermedio
- Fecha: {{$now}}

No improviso aquí. El prompt es largo y específico.

Si no fuerzas el formato, tarde o temprano la IA mete una línea fuera del frontmatter y te rompe el build.


Paso 2: Crear el workflow en n8n

Mi flujo en n8n tiene estos nodos:

  1. Trigger
  2. Set / Transform
  3. OpenAI
  4. Function (limpieza opcional)
  5. GitHub
  6. (Opcional) Notificación

Trigger

Uso dos modos:

  • Manual trigger para pruebas.
  • Webhook para integración con otras herramientas.

Ejemplo de webhook:

POST /webhook/blog-idea
{
  "idea": "Cómo usar edge functions con Astro para personalización"
}

Ese idea viaja por todo el flujo.


Nodo OpenAI

En n8n configuro el nodo con:

  • Modelo: gpt-5.2 o equivalente.
  • Temperature: 0.7 (no la subo más).
  • Response format: texto plano.

El input es el prompt completo más la idea dinámica.

Depende de la versión del nodo, pero la idea es que salga un string enorme con todo el post.


Paso 3: Generar el nombre del archivo automáticamente (Opcional)

No quiero archivos tipo post-123.md. Quiero slugs limpios.

Así que después del nodo de IA, añado un Function node para:

  1. Extraer el title del frontmatter.
  2. Convertirlo en slug.
  3. Construir la ruta final.

Ejemplo:

const content = $json.post;

// Extraer título del frontmatter
const match = content.match(/title:\s*"(.+)"/);
if (!match) {
  throw new Error("No se pudo extraer el título");
}

const title = match[1];

const slug = title
  .toLowerCase()
  .normalize("NFD")
  .replace(/[\u0300-\u036f]/g, "")
  .replace(/[^a-z0-9\s-]/g, "")
  .trim()
  .replace(/\s+/g, "-");

return [
  {
    json: {
      content,
      slug,
      path: `src/content/blog/${slug}.md`
    }
  }
];

Esto evita caracteres raros y acentos.

Prefiero generar el slug desde el título real porque mantiene coherencia semántica. Si el título cambia, el slug también.


Paso 4: Crear el archivo en GitHub

Aquí uso el nodo oficial de GitHub.

Operación: Create File

Campos clave:

  • Repository
  • Branch: main
  • File Path: {{$json.path}}
  • Content: {{$json.content}}
  • Commit Message: feat(blog): add {{$json.slug}}

n8n convierte el contenido en base64 automáticamente si usas el nodo oficial.


Controlando errores reales

Aquí es donde esto deja de ser un experimento y se convierte en sistema.

1. Validación mínima del frontmatter

Antes de enviar a GitHub, añado otra verificación:

const requiredFields = [
  "title:",
  "description:",
  "pubDate:",
  "draft:",
  "tags:",
  "categories:",
  "lang:"
];

for (const field of requiredFields) {
  if (!content.includes(field)) {
    throw new Error(`Falta el campo ${field}`);
  }
}

No es perfecto. Pero evita commits rotos.


2. Evitar texto fuera del markdown

A veces el modelo añade algo como:

Aquí tienes el post:

Eso rompe todo.

Solución: forzar en prompt y, si hace falta, recortar desde la primera aparición de ---.

const startIndex = content.indexOf('---');
if (startIndex > 0) {
  content = content.slice(startIndex);
}

Prefiero prevenir en prompt antes que parchear después.


3. Controlar longitud

Si el post es demasiado corto, lo descarto.

if (content.length < 5000) {
  throw new Error("El contenido es demasiado corto");
}

No quiero posts mediocres publicados automáticamente.


Variante avanzada: flujo con revisión humana opcional

No siempre publico directo.

A veces prefiero que el flujo:

  1. Genere el archivo.
  2. Cree un Pull Request en vez de hacer commit en main.

Con el nodo de GitHub puedes crear un branch dinámico:

feature/auto-post-{{$json.slug}}

Luego:

  • Create file en ese branch.
  • Create Pull Request.

Así reviso el contenido antes de mergearlo.

Es el punto medio entre automatización total y control editorial.


Qué aprendí montando esto

La IA necesita límites claros

Cuanto más abierto el prompt, peor el resultado.

Cuando definí:

  • Formato exacto.
  • Estructura obligatoria.
  • Reglas de estilo.
  • Restricciones duras.

La calidad subió muchísimo.


n8n escala mejor de lo que parece

Al principio lo veía como herramienta low-code.

Pero con:

  • Function nodes
  • Expresiones JS
  • Webhooks
  • Integraciones con APIs

Se convierte en un backend de automatización muy serio, en futuros post veremos como se puede desplegar self hosted de manera super rapida y barata.

Para este caso, no necesito montar un microservicio en Node. n8n ya me resuelve la orquestación.


Astro encaja perfecto para esto

Astro + content collections es ideal para generación automática.

Porque:

  • El contenido es archivos.
  • El schema es tipado.
  • El build falla si algo no cuadra.

Si usara un CMS tradicional, tendría que validar contra API, estados, etc.

Aquí es Git + Markdown. Simple. Predecible.


Extensiones que tengo en mente

Esto es solo la base. Se puede llevar más lejos:

  • Generar imágenes destacadas con IA y guardarlas en /public/images.
  • Extraer automáticamente tags según el contenido.
  • Generar versión en inglés desde el mismo flujo.
  • Enviar el post a una newsletter.

Todo orquestado desde n8n.

Una vez que tienes el flujo, añadir nodos es trivial.


Conclusión

Automatizar mi blog no es para publicar más. Es para reducir fricción.

La idea ya la tengo. El criterio también. Lo que automatizo es lo mecánico: estructurar, formatear, crear el archivo, commitear.

n8n me da el pegamento. Astro me da estructura y validación. La IA hace el trabajo pesado de redacción.

El resultado: un sistema donde una idea se convierte en post publicado en minutos. Y eso cambia completamente la velocidad a la que puedo construir contenido técnico.