Enlaces Enriquecidos para Desarrolladores Perezosos

Cuando comparto un enlace de mi blog en Twitter o Slack, aparece como texto plano. Sin imagen de vista previa. Solo una URL como granda.org/en/2026/01/02/claude-code-on-the-go/.

Necesitaba imágenes Open Graph. El enfoque estándar: crear manualmente una imagen de 1200x630 para cada entrada. Eso es tedioso. Le pedí a Claude que lo automatizara.

flowchart LR
    Push[git push] --> GHA[GitHub Actions]
    GHA --> Hugo[Hugo Server]
    GHA --> PW[Playwright]
    PW -->|screenshot| Hugo
    PW --> IMG[OG Image]
    IMG --> Commit[git commit]
    Commit --> Deploy[Deploy]
flowchart TB
    Push[git push] --> GHA[GitHub Actions]
    GHA --> Hugo[Hugo Server]
    GHA --> PW[Playwright]
    PW -->|screenshot| Hugo
    PW --> IMG[OG Image]
    IMG --> Commit[git commit]
    Commit --> Deploy[Deploy]

La Configuración

Le expliqué el problema a Claude: las entradas necesitan imágenes de vista previa social, pero no quiero crearlas manualmente. Captura el contenido del artículo, guárdalo como imagen OG, actualiza el frontmatter automáticamente.

Lo Que Claude Construyó

ComponentePropósito
generate-og-image.jsScript de Playwright para capturar pantallas de las entradas
generate-og-images.ymlGitHub Action activada por cambios en las entradas
Cambios en baseof.htmlEtiquetas meta og:image condicionales

El script de Playwright lanza Chrome sin interfaz, navega a la entrada, fuerza el modo claro, oculta el encabezado y el pie de página, y captura la pantalla a 1200x630:

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  viewport: { width: 1200, height: 630 },
  deviceScaleFactor: 2, // Retina for crisp text
});

// Force light mode, hide nav
await page.evaluate(() => {
  document.documentElement.setAttribute("data-theme", "light");
  document.querySelector("header").style.display = "none";
  document.querySelector("footer").style.display = "none";
});

await page.screenshot({ path: outputPath, type: "png" });

La GitHub Action se activa con cualquier cambio en archivos .en.md, inicia Hugo, ejecuta el script y hace commit de la imagen generada de vuelta al repositorio:

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

steps:
  - name: Start Hugo server
    run: hugo server --bind 0.0.0.0 --port 1313 &

  - name: Generate OG images
    run: node scripts/generate-og-image.js "$file"

  - name: Commit changes
    run: |
      git add static/images/posts/ content/posts/
      git commit -m "Generate Open Graph images for blog posts"
      git push

La plantilla incluye condicionalmente la imagen cuando el frontmatter tiene un campo image:

{{- if .Params.image -}}
<meta property="og:image" content="{{ $canonical }}{{ .Params.image }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="{{ $canonical }}{{ .Params.image }}">
{{- else -}}
<meta name="twitter:card" content="summary">
{{- end -}}

404: Slug No Encontrado

Primera ejecución. Playwright capturó una página en blanco. La URL devolvía 404.

El script había construido la URL a partir del nombre del archivo: automatic-blog-translations.en.md/en/2025/12/23/automatic-blog-translations/

Pero Hugo no usa el nombre del archivo para los slugs. Usa el título. La URL real era /en/2025/12/23/automatic-blog-translations-with-claude-and-github-actions/.

Podría haber reimplementado el algoritmo slugify de Hugo en JavaScript. En su lugar: preguntarle a Hugo.

const output = execSync("hugo list all", { encoding: "utf8" });
// CSV output includes the exact permalink Hugo generates

Una línea. Sin casos especiales. Hugo ya conoce sus propias URLs.

Qué Sigue

La implementación actual solo genera imágenes para entradas en inglés. Las versiones traducidas referencian la misma ruta de imagen, lo cual funciona, pero significa que la imagen OG siempre muestra texto en inglés incluso cuando se comparte una versión en español o japonés.

El verdadero soporte i18n llegará en el próximo PR. Por ahora, cada entrada obtiene una imagen social, lo cual es mejor que ninguna.

La Conclusión

Esto tomó 14 minutos construir y depurar. Ahora cada entrada obtiene automáticamente una imagen de vista previa social. Sin trabajo manual por entrada. La automatización se paga a sí misma después de un puñado de entradas.

Lanza la versión funcional, descubre las brechas, itera.


Entradas relacionadas: