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.
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.
Permítanme pintar una imagen del flujo de trabajo tradicional:
Este proceso tiene varios problemas:
La solución ideal debería:
Esto es exactamente lo que hace hugo2devto.
El action está construido con TypeScript y corre en Node.js 20. Aquí está la arquitectura de alto nivel:
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:
title → titledescription → descriptiontags → tags (limitado a 4)series → serieseyecatch / cover_image → main_imagedraft → published (invertido)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/
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.
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) -->

Esto significa que tus diagramas se renderizan hermosamente en ambas plataformas sin ninguna intervención manual.
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.
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);
}
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'
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:
fail-fast: false para que un fallo no detenga los demásTambié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'
Desde que implementé este action en mi blog, los resultados han sido transformadores:
Antes:
Después:
Construir este action me enseñó varias lecciones valiosas:
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.
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.
El action necesitaba manejar tanto posts nuevos como actualizaciones de forma elegante. Verificar artículos existentes por URL canónica fue crucial.
Creé múltiples archivos de documentación:
README.md - Visión general e inicio rápidoGETTING_STARTED.md - Guía de configuración en 5 minutosSETUP.md - Instrucciones completasHUGO_COMPATIBILITY.md - Detalles específicos de HugoAPI_KEY_SETUP.md - Mejores prácticas de seguridadEsto hizo el action accesible para usuarios con diferentes necesidades.
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.
Aunque el action funciona muy bien, siempre hay espacio para mejorar:
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!
Keyboard Shortcuts
| Command | Function |
|---|---|
| ? (Shift+/) | Bring up this help modal |
| g+h | Go to Home |
| g+p | Go to Posts |
| g+e | Open Editor page on GitHub in a new tab |
| g+s | Open Source page on GitHub in a new tab |
| r | Reload page |