Los scripts del mirror (00-90) vivian solo en el disco. Van al repo; los datos que generan no (16 GB entre crawl, snapshot del origen y Joomla restaurado) -> .gitignore. Nuevo 91-repone-assets404.sh: repone los ficheros que el crawl no capturo porque se referencian SOLO desde CSS y el crawler seguia enlaces HTML (system.css, los fondos de fe_adulta_1, ratingstars.gif de K2). Salian como 404 en los logs de nginx del Hetzner. Descarga por HTTP desde el Joomla local aislado, nunca del filesystem -- mismo principio que el crawl, para no arrastrar los .php comprometidos del #183 -- y escanea PHP embebido antes de copiar a site/. Resultado sobre las 286 rutas unicas con 404 del log: 196 repuestas y verificadas en produccion (196/196 en 200 tras el rsync), 82 que dan 301->404 tambien en el origen (ya estaban rotas en la web original) y 8 rutas basura /%22/... de HTML mal formado. Refs #180 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mirror-antiguo — mirror estático del Joomla legacy de feadulta
Construcción del mirror HTML read-only de antiguo.feadulta.com (issue
rafa/feadulta#180, remediación de #183).
Regla de diseño: el mirror se genera solo por HTTP, nunca copiando el filesystem. Así es
imposible arrastrar los .php comprometidos del incidente. Lo que se captura es lo que Joomla
renderiza.
Origen del crawl
No es producción: es el Joomla legacy restaurado en local.
| Contenedor | joomla-mirror-web (php:7.4-apache), red joomla-migration_joomla-net, IP 172.20.0.5 |
| Acceso | http://127.0.0.1:8086 o http://antiguo.feadulta.com (entrada en /etc/hosts → 172.20.0.5) |
| BD | joomla_mirror dentro del contenedor joomla-mysql |
| Snapshot fuente | source/antiguo-20260729.tar.gz + source/fejoomla3-20260729.sql.gz, hashes en MANIFEST-source.sha256 |
⚠️ La imagen base necesitó mysqli, pdo_mysql, gd, zip y mod_rewrite instalados en caliente:
se pierden si el contenedor se recrea, no si solo se para/arranca. Para cambiar memoria o política
de reinicio usar docker update, nunca docker rm + docker run.
⚠️ WSL2 se apaga cuando no queda ninguna sesión abierta desde Windows, y al apagarse para los contenedores. Antes de un proceso de horas, dejar un proceso ancla vivo en WSL.
Datos del sitio
- SEF:
sef=1,sef_rewrite=1,sef_suffix=1→ las URLs terminan en.html. - Un solo idioma de contenido publicado:
es-ES(sefes); todo el contenido eslanguage='*'. El sitio hace 301 de/x→/es/x, así que el inventario se genera ya con/es/. - Solo 2 categorías K2 y un único ítem de menú de K2:
buscadoravanzado(Itemid 138). Las URLs de ítem son/es/buscadoravanzado/item/<id>-<alias>.html. - 16.269 ítems K2 publicados · 9.079 artículos com_content · 216 ítems de menú · 68 categorías.
Método
El inventario no se reconstruye a mano: lo genera el propio router de Joomla. _genurls.php
(en la raíz del sitio restaurado) arranca el framework por HTTP y llama a JRoute::_() y
K2HelperRoute::getItemRoute(), así que las URLs son idénticas a las que el sitio imprime.
Cuatro pases, todos acotados por listas — nunca por recursión libre (ver post-mortem del 2026-07-29 en #180 comment-497):
| Pase | Qué captura | Script |
|---|---|---|
| A | Las 25.437 URLs del inventario | 21-crawl-html.sh |
| B | Recursos (css/js/img/mp3/pdf) referenciados por el HTML capturado | 30-extract-links.py + 31-fetch-assets.sh |
| C | Huecos: rutas de menú alternativas, páginas de autor de K2, /anterior, /ediciones |
43-pendientes.sh |
| D | /anterior, la web estática anterior a Joomla (HTML plano, recursión finita) |
37-pase-d-anterior.sh |
raw/ es inmutable. site/ es el derivado servible (45-normalize-links.py).
Trampas encontradas (y cómo se resuelven)
- Paginación de K2: los enlaces acumulan
&start=en vez de reemplazarlo → espacio de URLs infinito. Es lo que tumbó la VM el 29-jul. Se evita capturando por inventario, y el--reject-regexincluyestart|limitstart|limit|print|tmpl|format|searchword|task|orderby|filter. - Alias con
?literal: ~198 artículos tienen el signo de interrogación dentro del alias (...-dios-nos-ama?.html). Para cualquier cliente HTTP eso es el separador de query, así que la ruta real es la parte anterior al?y wget guarda un fichero sin extensión. Es correcto para servirlo estáticamente, pero nginx necesitadefault_type text/htmlen esa ubicación o el navegador se lo descargará en vez de mostrarlo. - Assets con cache-busting (
core.js?a32fb…): wget mete la query en el nombre del fichero. El paso de normalización deja también una copia con el nombre limpio, que es la que pedirá el servidor estático. - Menús cuyo componente ya no existe (
com_surveys,com_breezingforms):JRouteno les construye ruta y devuelven 404. Ya son 404 en producción; para el resto de ítems de menú la ruta autoritativa es la columnapathde#__menu(41-menu-paths.sh).
Scripts
Numerados por orden de ejecución en scripts/. 23-estado.sh da el estado en cualquier momento;
22-monitor.sh vigila el crawl y lo aborta si el servidor pasa de 100 respuestas 500 o si la RAM
libre baja de 800 MB. 60-restaurar-entorno.sh rearranca los contenedores parados durante el crawl.