Si ya te gusta escribir en Notion pero tu sitio está construido con Astro u otro stack estático file-first, aparece una pequeña brecha de workflow que se vuelve molesta antes de lo que debería.

Redactar en Notion es cómodo. Mover secciones es fácil, añadir imágenes también, y la página sigue siendo legible mientras el texto todavía está tomando forma. Después llega la publicación y, de repente, vuelves al repositorio: copiar contenido, arreglar frontmatter, generar una versión inglesa, ejecutar un build, hacer push y esperar al despliegue. La primera mitad parece escritura. La segunda parece trabajo de mantenimiento.

Ese era exactamente el hueco que quería eliminar. Quería mantener Notion como superficie de escritura, pero no convertir de nuevo el blog Astro en un sistema de contenido pesado con backend. Así terminé con una pipeline estrecha: escribir el artículo en Notion, dejar que GitHub Actions recupere la página, convertirla en archivos Markdown dentro del repositorio, generar la versión inglesa y el slug, validar el build y continuar después hacia el flujo de despliegue de producción que ya existía.

Si todavía no has movido tu blog a un modelo file-first, Astro vs WordPress: por qué prefiero Markdown y GitHub Actions para un blog de desarrollador es un mejor punto de partida. Si ya publicas directamente desde Markdown en el repositorio, Escribir Markdown en local y desplegar con git push: mi flujo de publicación bilingüe cubre ese lado. Este artículo está justo entre los dos: conservar la comodidad de escribir en Notion sin renunciar a la estructura y disciplina de release de un repositorio de contenido Astro.

Mi primer impulso fue el mismo que suele tener todo el mundo: hacerlo event-driven desde el principio. Publicar en Notion, lanzar un webhook, activar un Worker, escribir archivos y desplegar producción. Sobre el papel suena más limpio. Pero cuanto más dibujaba los caminos reales de fallo, más me alejaba de esa idea. Para un blog personal, lo más valioso de la primera versión no es la velocidad, sino la capacidad de depuración. Por eso mantuve el primer diseño deliberadamente sencillo. El cuerpo del artículo vive en el contenido de la página de Notion; las propiedades solo contienen metadata como título, resumen, categoría, tags, slug y estado; cada ejecución procesa una sola página; la regla de cola por defecto es publish = true más status = Ready; y actualizar un artículo existente exige reutilizar el slug original y permitir las actualizaciones de forma explícita.

Nada de esto parece espectacular, pero hace que la pipeline sea fácil de razonar. Si una página nunca entra en la cola, probablemente el metadata esté mal. Si falla la generación inglesa, ese es un nivel concreto. Si falla el build, vuelve al schema de contenido. Si el deploy nunca se ejecuta, mira el siguiente workflow. Prefiero una primera versión más lenta pero obvia de depurar que un sistema “en tiempo real” que se vuelve molesto en cuanto aparece el primer fallo.

Ese intercambio tiene todavía más sentido para un blog de una sola persona. Un equipo quizá necesite publicación instantánea, estados de review multi-editor y eventos de workflow más ricos. A mí me importa algo más simple: si termino un artículo por la noche, ¿puede aparecer en el sitio a la mañana siguiente sin riesgo de sobrescribir algo por accidente? El polling no es glamuroso, pero para este trabajo es difícil odiarlo.

Si eliminas los nombres de herramientas, el workflow es muy sencillo:

Notion page body
  -> mark page as Ready + publish
  -> GitHub Actions pulls markdown
  -> generate English title, description, body, and slug
  -> write files into src/content/posts/zh-cn and en-us
  -> run build validation
  -> push to main
  -> trigger the production deploy workflow
  -> write status, links, and commit SHA back to Notion

Había dos cosas que me importaban especialmente. La primera: Notion es solo la superficie de escritura, no el artefacto final publicado. El sitio en producción sigue saliendo de archivos Markdown dentro del repositorio. La segunda: automatizar no debería significar perder protección contra sobrescrituras. Por eso el repositorio sigue teniendo una forma de contenido muy simple:

src/content/posts/
├── zh-cn/
│   └── notion-to-astro-auto-publish-workflow.md
└── en-us/
    └── notion-to-astro-auto-publish-workflow.md

Ambas versiones comparten el mismo path, lo que mantiene limpios el routing y la agregación de Astro. En el lado de Notion, los inputs obligatorios también se mantienen limitados: título, resumen, categoría, tags, slug, estado de publicación, estado de cola, auto-translate y el flag explícito de update. El cuerpo permanece en el contenido de la página, no en una propiedad. Parece obvio, pero importa. El script obtiene Markdown exportado de la página. No reconstruye el artículo recorriendo bloques manualmente ni adivinando cómo serializarlos. Mientras Notion pueda exportar la página limpiamente, se puede transformar en un archivo normal de contenido sin demasiado trabajo extra.

Si la pregunta práctica es “¿cómo debería escribir dentro de Notion para que el artículo sincronice bien con un blog estático?”, ahora mi respuesta es simple: trata Notion como un editor Markdown cómodo, no como un playground de diseño visual. Eso significa niveles normales de heading, párrafos normales, bloques de código fenced para comandos y configuración, imágenes cerca del párrafo al que pertenecen y líneas independientes para links o vídeos que quizá quieras mejorar después. Cuanto más dependas de trucos de presentación exclusivos de Notion —bookmarks visuales pesados, layouts de varias columnas o bloques demasiado decorativos—, más limpieza tendrás que hacer tras exportar.

Lo que mejor me funciona tiene una forma muy sencilla. Empieza con una sección guiada por el problema. Lleva la explicación principal en unos cuantos párrafos largos. Añade bloques de código cuando los comandos o la configuración realmente importen. Mantén las imágenes cerca del párrafo relevante en lugar de tratarlas como una galería. Y si una referencia o vídeo importa, dale su propia línea. En ese punto la página de Notion ya se comporta bastante como una fuente Markdown limpia, que es precisamente por lo que la sincronización se vuelve fiable.

El patrón más conservador se parece a esto:

# Title

Open with two or three paragraphs that explain the problem and the decision.

## One key section

Keep writing.

```bash
pnpm build
node scripts/notion-auto-publish.mjs process
```

Then add pitfalls, limits, or recovery notes.

https://example.com/some-reference

Cuando escribes así, el paso de sincronización deja de sentirse como “convertir una página elegante de Notion en un blog” y empieza a sentirse como “tomar un borrador fuente limpio desde Notion y colocarlo en el repositorio”. Ese cambio mental importa bastante.

Cómo se divide el script: tres comandos, tres trabajos

Los entrypoints del script también se mantienen estrechos:

node scripts/notion-auto-publish.mjs process
node scripts/notion-auto-publish.mjs finalize-success
node scripts/notion-auto-publish.mjs finalize-failure

process realiza el trabajo real. Selecciona una página elegible, obtiene el cuerpo, genera el lado inglés, escribe ambos archivos Markdown y después deja que la cadena de build y release continúe. Los callbacks de éxito y fallo solo manejan el write-back hacia Notion. Prefiero esa separación porque mantiene cada etapa honesta. No termina convertido en un script gigantesco que intenta consultar Notion, traducir contenido, validar el build, hacer push de Git y parchear estados todo a la vez.

Cuanto más utilicé este sistema, menos me preocupaba la traducción en sí y más me preocupaba la protección de slugs. Una vez automatizas la publicación, el mayor riesgo no suele ser “hice clic una vez de más”, sino “pensaba que publicaba un post nuevo, pero el sistema entendió que quería reemplazar uno antiguo”. Ese riesgo es muy real cuando los títulos se parecen y los slugs se generan automáticamente.

Por eso la regla es deliberadamente estricta. Un post nuevo debería recibir un slug único siempre que sea posible. Si el repositorio ya contiene ese slug, falla por defecto. Solo permite reemplazarlo cuando el permiso de update esté activado explícitamente. Así la pipeline es conservadora a propósito. Prefiere detenerse y quejarse antes que adivinar en silencio que querías sobrescribir algo. Para un blog, creo que ese sesgo es correcto. Recuperarse de una publicación fallida es sencillo. Recuperarse de un reemplazo accidental suele ser más molesto.

Otra cosa que dio forma a esta configuración fue el comportamiento de gateways compatibles con OpenAI. Ya he visto casos incómodos: un 200 OK sin cuerpo utilizable, una respuesta no streaming que llegaba vacía mientras streaming sí funcionaba o un schema aparentemente aceptado que devolvía campos incorrectos. Por eso la capa de traducción tiene varios fallbacks. No porque quisiera un script inteligente, sino porque cuando la generación inglesa y la generación del slug están dentro del camino de publicación, ese nivel no puede ser frágil. No trato el modelo como un editor mágico. Lo trato como un paso de transformación reemplazable dentro de un sistema mayor. Puede ayudar con borrador inglés, título y descripción, pero los límites duros siguen perteneciendo al repositorio: schema de contenido, validación del build y protección contra sobrescritura.

Los límites que realmente importan

Después de ejecutar de verdad esta pipeline, no creo que las partes peligrosas estén en el cuerpo del artículo. Están en los límites. Crees que actualizas un post viejo, pero cambiaste el slug. Crees que las propiedades contienen el cuerpo, pero el script nunca las lee para eso. Das por hecho que Published significa que el despliegue está completamente verificado, cuando en esta versión realmente significa que la pipeline de contenido alcanzó correctamente el trigger de deployment de producción. O supones que automatizar elimina la necesidad de pruebas manuales, cuando los fallos de build todavía deben depurarse dentro del repositorio.

También existe otro malentendido habitual. La gente oye “escribe en Notion y publica automáticamente en el blog” y supone que puede dar cualquier formato a la página y confiar en que la pipeline la convertirá mágicamente en un artículo limpio. Ya no lo veo así. En este sistema Notion actúa realmente como una superficie más cómoda para crear Markdown. Desde que empecé a tratarlo así, todo mejoró. Dejé de preocuparme por si cada bloque se veía bonito dentro de Notion y empecé a preocuparme más por una estructura limpia de headings, fences correctos de código, imágenes en el lugar adecuado y links importantes en sus propias líneas.

Por eso sigo prefiriendo una ruta de depuración conservadora. Si necesito probar una página, ejecuto el workflow manualmente, paso un page_id concreto y mantengo dry_run activado primero. Así todavía recupera la página de Notion, genera el lado inglés, escribe output temporal y ejecuta validación del build, pero no hace push ni cambia producción. Es mucho más fácil confiar en la automatización cuando ya has visto cómo atraviesa las capas intermedias más riesgosas.

Si tuviera que reducir las reglas a un conjunto muy pequeño serían estas: escribe el cuerpo dentro del contenido de la página de Notion, no en propiedades; da a los posts nuevos su propio slug siempre que puedas; reutiliza el slug original y permite actualizaciones explícitamente cuando edites uno antiguo; deja primero la fuente china en estado publicable y después permite que la automatización genere el inglés; y cuando una ejecución falle, no te quedes mirando el exit 1 final: busca el primer paso que se rompió de verdad.

Por eso terminé con este diseño. No es la arquitectura de blog más ambiciosa del mundo. Es simplemente una línea limpia entre tres cosas que quería conservar: Notion como superficie de escritura, Astro como objetivo file-first de publicación y GitHub Actions como la capa que se come el trabajo repetitivo de en medio.

Para mí, eso es bastante más útil que añadir otro botón de administración.