{"id":303,"date":"2026-05-10T07:03:22","date_gmt":"2026-05-10T11:03:22","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=303"},"modified":"2026-05-10T09:35:44","modified_gmt":"2026-05-10T13:35:44","slug":"arquitectura-hibrida-2-0-como-automatice-la-inteligencia-de-mis-hubs-de-contenido","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/05\/arquitectura-hibrida-2-0-como-automatice-la-inteligencia-de-mis-hubs-de-contenido\/","title":{"rendered":"Arquitectura H\u00edbrida 2.0: C\u00f3mo automatic\u00e9 la inteligencia de mis Hubs de contenido"},"content":{"rendered":"\n<p>En el <a href=\"https:\/\/juredev.com\/blog\/2026\/05\/arquitectura-hibrida-blog-con-hugo-y-wordpress\/\">art\u00edculo anterior<\/a> expliqu\u00e9 c\u00f3mo rescat\u00e9 mis mejores art\u00edculos del cementerio cronol\u00f3gico de WordPress usando Hugo para crear Hubs de conocimiento. El resultado fue una secci\u00f3n<code> \/temas\/<\/code> r\u00e1pida, limpia y bien organizada.<\/p>\n\n\n\n<p>Pero hab\u00eda un problema que no mencion\u00e9: cada vez que publicaba un art\u00edculo nuevo, ten\u00eda que sentarme frente al ordenador, ejecutar el script manualmente y esperar a que Hugo compilara. Si estaba fuera de casa o solo con el m\u00f3vil, los Hubs se quedaban desactualizados.<\/p>\n\n\n\n<p>El contenido era din\u00e1mico. La arquitectura, demasiado est\u00e1tica.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">La pregunta correcta<\/h2>\n\n\n\n<p>Podr\u00eda haberlo resuelto con un webhook, con una GitHub Action o con cualquier servicio de CI\/CD. Pero el objetivo siempre ha sido el contrario: menos dependencias externas y m\u00e1s control propio.<\/p>\n\n\n\n<p>La pregunta no era \u00ab\u00bfqu\u00e9 servicio uso?\u00bb. \u00abEra \u00bfc\u00f3mo hago que el propio servidor sepa cu\u00e1ndo reconstruirse?\u00bb.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">El pegamento: Juredev Hubs Connector<\/h2>\n\n\n\n<p>Todo el sistema depende de un dato clave: saber a qu\u00e9 Hub pertenece cada art\u00edculo. Esa informaci\u00f3n no existe en WordPress por defecto, por lo que hay que crearla.<\/p>\n\n\n\n<p>El plugin Juredev Hubs Connector resuelve esto con tres hooks de WordPress. El primero a\u00f1ade un selector en el editor lateral de cada post. El segundo guarda la elecci\u00f3n como un metadato al publicar. El tercero inyecta autom\u00e1ticamente un banner al final del art\u00edculo con el enlace de vuelta al Hub.<\/p>\n\n\n\n<p>El <code>selector<\/code> es un con los diez Hubs disponibles en el panel lateral del editor. Cuando guardo el post, el plugin escribe el slug elegido en <code>wp_postmeta<\/code> bajo la clave <code>_juredev_selected_hub<\/code>. Un dato minimalista que hace exactamente una sola cosa.<\/p>\n\n\n\n<p>El banner que aparece al final de cada art\u00edculo usa <code>rel=\"tag\"<\/code> en el enlace, un detalle sem\u00e1ntico peque\u00f1o que refuerza la relaci\u00f3n entre el art\u00edculo y su Hub para los crawlers:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$banner_html = '&lt;p class=\"post-series\"&gt;\n    Tema Relacionado: &lt;a href=\"' . esc_url( $hub_url ) . '\" rel=\"tag\"&gt;' . esc_html( $hub_name ) . '&lt;\/a&gt;\n&lt;\/p&gt;';<\/code><\/pre>\n\n\n\n<p>Vale la pena mencionar que el plugin verifica tres condiciones antes de inyectar el banner: que sea una p\u00e1gina de art\u00edculo individual, que est\u00e9 dentro del loop principal y que sea la query principal. Sin esas comprobaciones, el banner podr\u00eda aparecer en widgets, excerpts o cualquier otro lugar donde WordPress renderice contenido.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">El cerebro del sistema: una query SQL<\/h2>\n\n\n\n<p>Antes de pensar en automatizaci\u00f3n, tuve que resolver un problema m\u00e1s fundamental: \u00bfqu\u00e9 datos necesita Hugo exactamente?<\/p>\n\n\n\n<p>La respuesta est\u00e1 en <code>SELECT-WP-POST.sql<\/code>, una consulta que hace varias cosas a la vez. Extrae t\u00edtulo, fecha, slug y resumen de cada art\u00edculo. Resuelve la imagen destacada navegando por la tabla de metadatos. Agrupa categor\u00edas y tags con <code>GROUP_CONCAT<\/code>. Y hace algo espec\u00edfico de esta arquitectura: lee el campo <code>_juredev_selected_hub<\/code>, el metadato que el plugin Juredev Hubs Connector escribe cuando asigno un art\u00edculo a un Hub desde el editor de WordPress.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>(SELECT meta_value FROM wp_postmeta \n WHERE post_id = p.ID \n AND meta_key = '_juredev_selected_hub') AS hub_slug,<\/code><\/pre>\n\n\n\n<p>Esa l\u00ednea es el puente entre los dos sistemas. Sin ella, Hugo no sabr\u00eda qu\u00e9 art\u00edculos pertenecen a qu\u00e9 Hub.<\/p>\n\n\n\n<p>Hay tambi\u00e9n un detalle de criterio editorial: los art\u00edculos etiquetados como <code>Opini\u00f3n<\/code> est\u00e1n excluidos expl\u00edcitamente. Los Hubs son colecciones t\u00e9cnicas. Las opiniones tienen su propio espacio en el feed cronol\u00f3gico. La query lo refleja:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>AND p.ID NOT IN (\n    SELECT tr.object_id FROM wp_term_relationships tr\n    INNER JOIN wp_term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id\n    INNER JOIN wp_terms t ON tt.term_id = t.term_id\n    WHERE tt.taxonomy = 'post_tag' AND t.name = 'Opini\u00f3n'\n)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">El exportador: PHP hablando directamente con la base de datos<\/h2>\n\n\n\n<p>El script <code>export-wp-to-hugo.php<\/code> no usa la API REST de WordPress. Se conecta directamente a la base de datos, ejecuta la query SQL y vuelca el resultado en <code>data\/posts.json<\/code>, el archivo que Hugo usa para construir cada Hub.<\/p>\n\n\n\n<p>La raz\u00f3n de ir directo a la DB en lugar de usar la API es simple: velocidad y control. No hay overhead de HTTP, no hay autenticaci\u00f3n y no hay plugins que puedan interferir. El script lee, transforma y escribe:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$row&#91;'categorias'] = explode(', ', $row&#91;'categorias']);\n$row&#91;'tags'] = explode(', ', $row&#91;'tags']);<\/code><\/pre>\n\n\n\n<p>Esas dos l\u00edneas convierten los strings de <code>GROUP_CONCAT<\/code> en arrays reales, que es el formato que espera Hugo en sus templates.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">El vigilante: Bash + WP-CLI + Cron<\/h2>\n\n\n\n<p>Con el exportador listo, el siguiente paso fue hacer que el servidor decidiera cu\u00e1ndo ejecutarlo. La soluci\u00f3n es un script bash que act\u00faa como vigilante y corre cada 15 minutos v\u00eda cron:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>*\/15 * * * * \/bin\/bash \/home\/temasadmin\/auto-deploy-hugo.sh<\/code><\/pre>\n\n\n\n<p>La l\u00f3gica es deliberadamente simple. El script pregunta a WordPress cu\u00e1l es el ID del \u00faltimo art\u00edculo publicado:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ID_ACTUAL=$(wp post list --post_type=post --post_status=publish \\\n  --posts_per_page=1 --field=ID \\\n  --path=$PATH_WP --skip-plugins --skip-themes --allow-root)<\/code><\/pre>\n\n\n\n<p>Lo compara con el \u00faltimo ID que registr\u00f3 en un archivo de texto plano. Si el n\u00famero creci\u00f3, hay contenido nuevo. Si no, no hace nada y el servidor queda en reposo. Sin polling innecesario, sin peticiones HTTP y sin dependencias externas.<\/p>\n\n\n\n<p>Cuando detecta un ID nuevo, ejecuta la cadena completa:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/usr\/bin\/php export-wp-to-hugo.php    # Actualiza el JSON desde la DB\n\/usr\/local\/bin\/hugo --gc --minify           # Recompila el sitio est\u00e1tico\n\/usr\/bin\/rsync -avzh --delete public\/ $WEB_DIR\/  # Sincroniza solo lo que cambi\u00f3\nchown -R www-data:www-data $WEB_DIR     # Ajusta permisos\necho $ID_ACTUAL > $LAST_ID_FILE         # Registra el nuevo ID<\/code><\/pre>\n\n\n\n<p>Todo queda registrado en un log con timestamp. Si algo falla, hay trazabilidad.<\/p>\n\n\n\n<p>El flujo completo queda as\u00ed:<\/p>\n\n\n\n<script type=\"module\">\n  import mermaid from 'https:\/\/cdn.jsdelivr.net\/npm\/mermaid@10\/dist\/mermaid.esm.min.mjs';\n  mermaid.initialize({ \n    startOnLoad: true, \n    theme: 'dark'\n  });\n<\/script>\n<div style=\"display: flex; justify-content: center; align-items: center; width: 100%; margin: 20px 0;\">\n    <pre class=\"mermaid\" style=\"background: transparent; border: none; color: transparent;\">\nflowchart TD\n    WP[(WordPress DB)] -- Publicas un Post --> WP\n    Cron[Cron cada 15min] -- Chequea ID --> WP\n    Cron -- \"ID nuevo detectado\" --> Export[PHP Export]\n    Export --> JSON[(posts.json)]\n    JSON --> Hugo[Hugo Build]\n    Hugo --> Deploy[rsync a \/temas\/]\n    Deploy --> Live((Sitio Actualizado))\n    <\/pre>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Lo que cambi\u00f3 en la pr\u00e1ctica<\/h2>\n\n\n\n<p>Antes publicaba un art\u00edculo y ten\u00eda que volver al ordenador a actualizar los Hubs. Ahora publico desde donde sea, el m\u00f3vil, una tablet, cualquier navegador, y en menos de 15 minutos los Hubs se actualizan solos, con el art\u00edculo correctamente asignado a su Hub, con sus metadatos completos y sin que yo haya tocado nada m\u00e1s.<\/p>\n\n\n\n<p>La arquitectura h\u00edbrida no es solo separar el frontend del backend. Es crear un ecosistema donde cada herramienta hace exactamente lo que mejor sabe hacer: WordPress gestiona el contenido, PHP extrae los datos, Hugo construye las p\u00e1ginas y rsync sincroniza. Nadie hace el trabajo del otro.<\/p>\n\n\n\n<p>Y cuando todo est\u00e1 bien separado, automatizarlo es trivial.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>En el art\u00edculo anterior expliqu\u00e9 c\u00f3mo rescat\u00e9 mis mejores art\u00edculos del cementerio cronol\u00f3gico de WordPress usando Hugo para crear Hubs de conocimiento. El resultado fue una secci\u00f3n \/temas\/ r\u00e1pida, limpia y bien organizada. Pero hab\u00eda un problema que no mencion\u00e9: cada vez que publicaba un art\u00edculo nuevo, ten\u00eda que sentarme frente al ordenador, ejecutar el [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[24,15,20,19],"class_list":["post-303","post","type-post","status-publish","format-standard","hentry","category-nota","tag-linux","tag-php","tag-sql","tag-wordpress"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/303","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=303"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/303\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=303"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=303"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=303"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}