GitHub Action para Publicar Posts de Hugo en Dev.to

Cómo construí un GitHub Action reutilizable para automatizar la publicación de posts de Hugo en Dev.to, eliminando el cross-posting manual y manteniendo el contenido sincronizado entre plataformas.

Lastmod: 2026-01-25

Cómo construí un GitHub Action reutilizable para automatizar la publicación de posts de Hugo en Dev.to, eliminando el cross-posting manual y manteniendo el contenido sincronizado entre plataformas.

Tabla de Contenidos

Introducción

Como desarrolladores que mantenemos blogs técnicos, frecuentemente enfrentamos un dilema común: ¿deberíamos publicar exclusivamente en nuestro propio sitio, o deberíamos hacer cross-posting a plataformas como Dev.to, Medium o Hashnode para alcanzar una audiencia más amplia?

La respuesta usualmente es “ambas”, pero eso crea un nuevo problema: el cross-posting manual es tedioso, propenso a errores y consume mucho tiempo. Escribes un post en Hugo, lo publicas en tu sitio, luego copias y pegas manualmente el contenido a Dev.to, ajustas el formato, agregas tags, configuras la URL canónica, y esperas no haber olvidado nada.

Experimenté esta fricción de primera mano con mi blog basado en Hugo en blog.walsen.website . Después de publicar varios posts y hacer cross-posting manual a Dev.to, me di cuenta de que este flujo de trabajo era insostenible. Tenía que haber una mejor manera.

Fue entonces cuando decidí construir hugo2devto : un GitHub Action que publica automáticamente posts de Hugo en Dev.to con soporte completo de frontmatter, URLs canónicas, y cero intervención manual.

El Problema: El Cross-Posting es Doloroso

Permítanme pintar una imagen del flujo de trabajo tradicional:

  1. Escribir tu post en Hugo con frontmatter YAML
  2. Construir y desplegar tu sitio Hugo
  3. Abrir Dev.to en tu navegador
  4. Copiar y pegar tu contenido markdown
  5. Configurar manualmente:
    • Título
    • Tags (máximo 4)
    • Imagen de portada
    • URL canónica
    • Estado publicado/borrador
    • Información de serie
  6. Previsualizar y publicar
  7. Repetir para cada actualización del post

Este proceso tiene varios problemas:

  • Consume tiempo: 10-15 minutos por post
  • Propenso a errores: Fácil olvidar URLs canónicas o tags
  • No escalable: Desalienta el cross-posting
  • Sin control de versiones: Los cambios no se rastrean
  • Sincronización manual: Las actualizaciones requieren repetir todo el proceso

La Solución: Automatización con GitHub Actions

La solución ideal debería:

  1. Detectar cuando un post de Hugo es creado o actualizado
  2. Extraer automáticamente los metadatos del frontmatter
  3. Publicar o actualizar el post en Dev.to
  4. Configurar la URL canónica correcta
  5. Manejar tags, imágenes de portada y series
  6. Funcionar perfectamente con flujos de trabajo Hugo existentes

Esto es exactamente lo que hace hugo2devto.

Construyendo el Action: Inmersión Técnica

Visión General de la Arquitectura

El action está construido con TypeScript y corre en Node.js 20. Aquí está la arquitectura de alto nivel:

flowchart TD subgraph trigger["Disparador del Workflow"] T1["push a main"] T2["workflow_dispatch"] end subgraph action["hugo2devto Action v1"] A1["1. Leer Archivo Markdown"] A2["2. Parsear Frontmatter YAML"] A3["3. Transformar Hugo → Dev.to"] A4["4. Llamar API de Dev.to"] A5["5. Retornar Outputs"] end subgraph output["Resultado"] O1["Publicado en Dev.to"] end trigger --> A1 A1 --> A2 A2 --> A3 A3 --> A4 A4 --> A5 A5 --> O1

Características Principales

1. Soporte Completo de Frontmatter Hugo

El action entiende el formato de frontmatter de Hugo nativamente:

---
title: "Mi Post Increíble"
description: "Una inmersión profunda en algo genial"
publishdate: 2026-01-25T22:23:37-04:00
draft: false
tags: ["hugo", "devto", "automatización"]
series: "Automatización Hugo"
eyecatch: "https://ejemplo.com/portada.png"
---

Mapea automáticamente estos campos al formato de la API de Dev.to:

  • titletitle
  • descriptiondescription
  • tagstags (limitado a 4)
  • seriesseries
  • eyecatch / cover_imagemain_image
  • draftpublished (invertido)

2. Generación Automática de URL Canónica

Una de las consideraciones SEO más importantes al hacer cross-posting es configurar la URL canónica para que apunte a tu post original. El action genera esto automáticamente:

const canonicalUrl = `${baseUrl}/${language}/posts/${slug}/`

Por ejemplo, un post en content/en/posts/my-post.md se convierte en:

https://blog.walsen.website/en/posts/my-post/

3. Soporte Multi-Idioma

El action detecta el idioma desde la ruta del archivo:

content/en/posts/my-post.md  → Inglés
content/es/posts/mi-post.md  → Español

Esto es crucial para sitios Hugo con soporte i18n.

4. Soporte de Diagramas Mermaid (v1.1.0)

Hugo usa shortcodes para diagramas mermaid, pero Dev.to no soporta mermaid nativamente. El action convierte automáticamente los shortcodes de mermaid de Hugo a imágenes PNG renderizadas usando el servicio mermaid.ink:

<!-- Formato Hugo (en tu fuente) -->
{{< mermaid >}}
flowchart TD
    A --> B
{{< /mermaid >}}

<!-- Convertido a (en Dev.to) -->
![Diagrama Mermaid](https://mermaid.ink/img/base64encodeddiagram)

Esto significa que tus diagramas se renderizan hermosamente en ambas plataformas sin ninguna intervención manual.

5. Actualizaciones Idempotentes

El action verifica si un artículo ya existe en Dev.to (por URL canónica) y lo actualiza en lugar de crear un duplicado. Esto significa que puedes ejecutar el action múltiples veces de forma segura.

Aspectos Destacados de la Implementación

Aquí hay una versión simplificada de la lógica central:

// Leer y parsear el archivo markdown
const fileContent = fs.readFileSync(filePath, 'utf-8');
const { data: frontmatter, content } = matter(fileContent);

// Extraer metadatos
const title = frontmatter.title;
const description = frontmatter.description || '';
const tags = (frontmatter.tags || []).slice(0, 4); // Límite de Dev.to
const published = !frontmatter.draft;

// Generar URL canónica
const slug = path.basename(filePath, '.md')
  .toLowerCase()
  .replace(/\s+/g, '-');
const language = filePath.includes('/en/') ? 'en' : 'es';
const canonicalUrl = `${baseUrl}/${language}/posts/${slug}/`;

// Preparar artículo para Dev.to
const article = {
  title,
  body_markdown: content,
  published,
  tags,
  canonical_url: canonicalUrl,
  main_image: frontmatter.eyecatch || frontmatter.cover_image,
  series: frontmatter.series,
  description
};

// Verificar si el artículo existe
const existingArticle = await findArticleByCanonicalUrl(canonicalUrl);

if (existingArticle) {
  // Actualizar artículo existente
  await updateArticle(existingArticle.id, article);
} else {
  // Crear nuevo artículo
  await createArticle(article);
}

Usando el Action: Ejemplos Prácticos

Configuración Básica

Primero, obtén tu API key de Dev.to desde https://dev.to/settings/extensions y agrégala a los secrets de tu repositorio como DEVTO_API_KEY.

Luego crea .github/workflows/publish-devto.yml:

name: Publicar en Dev.to

on:
  push:
    branches: [main]
    paths:
      - 'content/*/posts/*.md'

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Publicar en Dev.to
        uses: Walsen/hugo2devto@v1
        with:
          api-key: ${{ secrets.DEVTO_API_KEY }}
          file-path: 'content/en/posts/my-post.md'
          base-url: 'https://blog.walsen.website'

Avanzado: Detección Automática de Posts Modificados

Para mi blog, quería que el action detectara automáticamente qué posts cambiaron y publicara solo esos:

name: Publicar en Dev.to

on:
  push:
    branches: [main]
    paths:
      - 'content/*/posts/*.md'

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      posts: ${{ steps.changed-files.outputs.posts }}
      has-changes: ${{ steps.changed-files.outputs.has-changes }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
      
      - name: Obtener archivos modificados
        id: changed-files
        run: |
          CHANGED_FILES=$(git diff --name-only HEAD^ HEAD | grep -E 'content/en/posts/.*\.md' || echo "")
          
          if [ -n "$CHANGED_FILES" ]; then
            POSTS_JSON=$(echo "$CHANGED_FILES" | jq -R -s -c 'split("\n") | map(select(length > 0))')
            echo "posts=$POSTS_JSON" >> $GITHUB_OUTPUT
            echo "has-changes=true" >> $GITHUB_OUTPUT
          else
            echo "posts=[]" >> $GITHUB_OUTPUT
            echo "has-changes=false" >> $GITHUB_OUTPUT
          fi          
  
  publish-changed:
    if: needs.detect-changes.outputs.has-changes == 'true'
    needs: detect-changes
    runs-on: ubuntu-latest
    strategy:
      matrix:
        post: ${{ fromJson(needs.detect-changes.outputs.posts) }}
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      
      - name: Publicar en Dev.to
        uses: Walsen/hugo2devto@v1
        with:
          api-key: ${{ secrets.DEVTO_API_KEY }}
          file-path: ${{ matrix.post }}
          base-url: 'https://blog.walsen.website'

Este workflow:

  1. Detecta qué archivos markdown cambiaron en el último commit
  2. Los convierte a un array JSON
  3. Usa una estrategia de matriz para publicar múltiples posts en paralelo
  4. Configura fail-fast: false para que un fallo no detenga los demás

Disparador Manual

También puedes disparar la publicación manualmente:

on:
  workflow_dispatch:
    inputs:
      post_path:
        description: 'Ruta al post'
        required: true
        type: string

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Publicar en Dev.to
        uses: Walsen/hugo2devto@v1
        with:
          api-key: ${{ secrets.DEVTO_API_KEY }}
          file-path: ${{ github.event.inputs.post_path }}
          base-url: 'https://blog.walsen.website'

Resultados en el Mundo Real

Desde que implementé este action en mi blog, los resultados han sido transformadores:

Antes:

  • ⏱️ 15 minutos por post para hacer cross-posting manual
  • 🐛 Errores frecuentes (URLs canónicas olvidadas, tags incorrectos)
  • 😓 Desalentado de actualizar posts en Dev.to
  • 📉 Presencia inconsistente en Dev.to

Después:

  • ⚡ Publicación automática en ~30 segundos
  • ✅ Cero intervención manual requerida
  • 🔄 Actualizaciones sincronizadas automáticamente
  • 📈 Cross-posting consistente a Dev.to
  • 🎯 Más tiempo para escribir, menos para publicar

Lecciones Aprendidas

Construir este action me enseñó varias lecciones valiosas:

1. Empieza con un Script, Luego Empaquétalo

Inicialmente construí un script TypeScript (publish-to-devto.ts) que funcionaba localmente. Una vez que estuvo estable, lo empaquete como un GitHub Action. Este enfoque iterativo hizo el debugging mucho más fácil.

2. El Mapeo de Frontmatter es Complicado

Hugo y Dev.to usan diferentes nombres de campos y formatos. Crear una capa de mapeo robusta requirió pruebas cuidadosas con varios formatos de posts.

3. La Idempotencia Importa

El action necesitaba manejar tanto posts nuevos como actualizaciones de forma elegante. Verificar artículos existentes por URL canónica fue crucial.

4. La Documentación es Clave

Creé múltiples archivos de documentación:

  • README.md - Visión general e inicio rápido
  • GETTING_STARTED.md - Guía de configuración en 5 minutos
  • SETUP.md - Instrucciones completas
  • HUGO_COMPATIBILITY.md - Detalles específicos de Hugo
  • API_KEY_SETUP.md - Mejores prácticas de seguridad

Esto hizo el action accesible para usuarios con diferentes necesidades.

5. El Feedback de la Comunidad es Invaluable

Publicar el action en el GitHub Marketplace lo expuso a casos de uso del mundo real que no había considerado. El feedback de usuarios ayudó a mejorar el manejo de errores y casos extremos.

Mejoras Futuras

Aunque el action funciona muy bien, siempre hay espacio para mejorar:

  1. Publicación por Lotes: Soportar publicación de múltiples posts en una sola invocación del action
  2. Modo Dry Run: Previsualizar qué se publicaría sin hacerlo realmente
  3. Mapeo de Campos Personalizado: Permitir a usuarios configurar sus propios mapeos de frontmatter
  4. Subida de Imágenes: Subir automáticamente imágenes locales a Dev.to
  5. Integración de Analytics: Rastrear métricas de publicación
  6. Soporte Multi-Plataforma: Extender a Medium, Hashnode, etc.
  7. Soporte de Shortcodes Adicionales: Transformar otros shortcodes de Hugo (embeds de YouTube, Twitter, etc.)

Conclusión

Construir hugo2devto resolvió un problema real que enfrentaba como blogger técnico: la fricción del cross-posting de contenido. Al automatizar el proceso a través de GitHub Actions, eliminé el trabajo manual, reduje errores, y hice que mantener presencia en Dev.to fuera sin esfuerzo.

El action es open source y está disponible para que cualquiera lo use. Ya sea que estés corriendo un blog Hugo, un sitio Jekyll, o cualquier plataforma basada en markdown, los conceptos centrales aplican: automatiza lo aburrido para que puedas enfocarte en escribir gran contenido.

Si estás interesado en probarlo o contribuir, revisa el repositorio:

Repositorio: https://github.com/Walsen/hugo2devto

GitHub Marketplace: https://github.com/marketplace/actions/hugo-to-dev-to-publisher

¡El futuro del blogging técnico es automatizado, y estoy emocionado de ver a dónde lleva este viaje. Feliz blogging!

Enlaces


comments powered by Disqus

Publicaciones recomendadas al azar