ichisieben.dev/botanica · ETL en Python · atlas estático en Astro

Arquitectura del Atlas Botánico del Perú

Un ETL reproducible convierte descargas de GBIF, Kew y geoBoundaries en unos pocos JSON pequeños; un sitio estático los lee en el build y el navegador solo trae archivos. Cinco vistas: contenedores, flujos, datos, despliegue y decisiones. Haz clic en cualquier caja; reproduce un flujo para ver el recorrido.

este repo persona u otra herramienta servicio externo tráfico
Detalle

Haz clic en un contenedor

Cada caja es una pieza que corre por separado: una fuente externa, el ETL, una carpeta de datos, el build del sitio o el navegador de quien visita. Las líneas son llamadas reales: protocolo y qué viaja.

Por qué todo se calcula antes

El sitio no tiene servidor de aplicación ni llama a ninguna API al ser visitado. El ETL deja los números hechos y el build los lee de archivos. Una visita cuesta lo que cuesta servir archivos, y una cifra del sitio se puede rastrear hasta la descarga que la produjo.

Por qué nombres aceptados

Los registros de GBIF mezclan sinónimos y errores de tipeo. El ETL los cruza contra el backbone de nombres aceptados antes de contar, por eso los totales son menores que los de fuentes que cuentan nombres crudos. Lo que no resuelve se reporta, no se descarta en silencio.

Por qué cada descarga lleva DOI

La descarga reproducible de GBIF devuelve un download key y un DOI citable. El manifiesto los guarda junto al sha256 del archivo, así que cualquier figura apunta a un extracto exacto y no a una consulta viva que cambie sin avisar.

Secuencias · una por flujo · los mismos pasos que reproduce el C4
atlas.duckdb · esquema estrella y marts · pasa el cursor por una relación

Sin base de datos de aplicación: las carpetas son el contrato

data/raw
Descargas crudas y manifest.json. Ignorado por git; se regenera con el ETL.
atlas.duckdb
Base de trabajo local. Ni el sitio ni el navegador la tocan.
data/exports
Marts en JSON, versionados. Es la única entrada del build del sitio. Los Parquet quedan fuera de git.
web/public/data
Lo que baja el navegador. Índice de especies columnar: las familias, órdenes y departamentos van en tablas de búsqueda y cada especie guarda enteros.
geometrías
Dos a propósito. La detallada asigna puntos a departamentos con ST_Contains y un ajuste de hasta 5 km; la simple solo dibuja el mapa.
orden
Cada export lleva un ORDER BY por todas las columnas, con NULLS LAST, para que dos corridas sobre los mismos datos escriban el mismo archivo.
Botanica/
  etl/                    pasos del pipeline y CLI atlas
  data/
    raw/                  descargas + manifest.json   (git: no)
    atlas.duckdb          base de trabajo             (git: no)
    exports/
      mart_*.json         8 JSON con los marts        (git: si)
      *.parquet                                       (git: no)
  web/
    public/data/          species-*, facets-*, protologue-*
    src/lib/data.ts       lee data/exports en el build
    dist/                 salida del build            (git: no)
Despliegue · lo que hay y lo que cambiaría

Hoy una subcarpeta del Landing. El plan escrito es un subdominio propio, sin tocar el ETL.

El atlas es un directorio de archivos. El build corre en el PC del autor, su salida se copia al repo del Landing y el auto-deploy de ese repo lo publica. Mover el atlas a otro destino cambia la base y la URL del sitio, no el pipeline de datos.

Hoy · subcarpeta /botanica/

uv run atlas data: ETL en el PCDuckDB local
npm run build en web/ (base /botanica/)Astro estático
copia de web/dist a public/botanica/ del Landingmanual
push a main del Landing y build en Hostingerauto-deploy
.htaccess: immutable para JSON con hash, sin rewritesApache
gates de Playwright, axe y Lighthouse antes de copiarnpm run gate

La salida lleva dos copias de cada JSON: con hash para la caché larga y sin hash como respaldo para pestañas abiertas antes de un deploy.

→ mismo dist/ →

Plan escrito · subdominio propio

mismo ETL y mismos martssin cambios
base de Astro pasa de /botanica/ a /astro.config
site, canonical y hreflang al subdominiocabecera de cada página
docroot propio, subido por la API de hostingsin copia al Landing
301 desde /botanica/ en el Landing.htaccess del Landing
comprobar que no quede /botanica/ en HTML ni en JSgrep del dist

Qué gana: un despliegue independiente y sin copia manual. Qué no cambia: las cifras, que salen del ETL.

Decisiones · con la alternativa descartada

Cada decisión dice qué se eligió, qué se dejó y por qué

DecisiónPor quéDescartado
Estático primero: el sitio lee marts JSON en el buildSin servidor ni APIs en vivo, el costo por visita es el de servir archivos y el sitio no puede caerse por una dependenciaAlojar en Vercel o Cloudflare, o consultar los datos en tiempo de ejecución
Contar solo nombres aceptadosLos sinónimos y errores de tipeo inflan la diversidad; resolverlos antes de contar da cifras defendiblesContar los nombres crudos de GBIF
Esquema estrella en DuckDBSepara el grano de registro (esfuerzo de colecta) del de distribución (presencia) y deja un mart por vistaQue el sitio lea tablas crudas o de hechos
Dos geometrías desacopladasLa simplificada recorta cientos de metros de costa y mandaría puntos legítimos a sin asignar; la detallada hace el join y la simple solo dibujaUsar un solo GeoJSON para el join y para el render
Descarga de GBIF con DOI y manifiesto selladoCada cifra apunta a un extracto exacto y citable; el manifiesto guarda key, DOI y sha256El search paginado como fuente final: no tiene DOI ni es reproducible
Export ordenado por todas las columnasSin ORDER BY el orden de filas queda a criterio del motor y cada corrida ensucia el repo con diffs que no significan nada. Inferida; que la corrida repetida salga idéntica sigue por confirmarDejar el orden por defecto de DuckDB
Mapa familia a orden derivado de datosSale del árbol de Tree of Life y del backbone de GBIF; los conflictos se resuelven por soporte y se reportan. El orden es la unidad de color del cladoUna tabla escrita a mano
JSON con hash en el nombre y caché immutableUn export nuevo es una URL nueva y el CDN puede servirlo de caché un año; el nombre sin hash queda como respaldoServir los JSON sin cabecera de caché, como antes
Primeras filas de especies dentro del HTMLLa primera fila esperaba un JSON de 1,6 MB; con 26 filas en el build aparece antes de bajar nadaUn Web Worker para parsear: no quita la descarga del camino
Three.js solo como chunk perezoso del árbolLa página principal no carga ECharts ni Three.js; el 3D es opcional y cae al sunburst en dispositivos modestos o con movimiento reducidoCargar el motor 3D en el bundle inicial