# 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](https://gitea.feadulta.com/rafa/feadulta/issues/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` (sef `es`); todo el contenido es `language='*'`.
  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)

1. **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-regex` incluye `start|limitstart|limit|print|tmpl|format|searchword|task|orderby|filter`.
2. **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 necesita `default_type text/html` en esa ubicación o el
   navegador se lo descargará en vez de mostrarlo.
3. **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.
4. **Menús cuyo componente ya no existe** (`com_surveys`, `com_breezingforms`): `JRoute` no les
   construye ruta y devuelven 404. Ya son 404 en producción; para el resto de ítems de menú la ruta
   autoritativa es la columna `path` de `#__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.
