doc-arquitectura — MarkUP
Última revisión: 2026-06-16 LOC total: ~3,500+ líneas en 35+ archivos fuente (src/)
Este documento describe la arquitectura del proyecto MarkUP en detalle: el árbol completo de archivos, cómo fluyen los datos durante el build, cómo se relacionan los módulos entre sí, cómo está organizado el CSS, la configuración de las herramientas y el pipeline de CI/CD.
1. Árbol completo del proyecto
A continuación se muestra el árbol completo del proyecto con una breve descripción de cada archivo/directorio. Los elementos marcados con ⚠️ son archivos huérfanos o problemáticos.
markeup/
├── .astro/ ← Generado por Astro (types, settings)
│
├── .github/ ← CI/CD
│ ├── lighthouse/
│ │ └── lighthouserc.json ← Configuración de Lighthouse CI (umbrales de rendimiento)
│ └── workflows/
│ ├── deploy.yml ← Workflow de deploy a Cloudflare Pages
│ └── lighthouse.yml ← Workflow de Lighthouse CI automático
│
├── .obsidian/ ← Configuración del vault Obsidian en Mac
│ ├── app.json ← Configuración general de la app Obsidian
│ ├── appearance.json ← Tema y apariencia de Obsidian
│ ├── core-plugins.json ← Plugins core activados en Obsidian
│ └── workspace.json ← Estado del workspace (paneles abiertos, etc.)
│
├── .vscode/
│ └── settings.json ← Configuración de VSCode específica del proyecto (+ Cline)
│
├── public/ ← Archivos estáticos servidos en la raíz del sitio
│ ├── img/ ← 40 imágenes estáticas en formato WebP (~40 archivos)
│ └── pagefind/ ← Índice de búsqueda generado por Pagefind durante el build
│
├── scripts/ ← Scripts auxiliares (Node.js)
│ ├── dev-search.js ← Genera el índice Pagefind para desarrollo
│ ├── export-blog.js ← Exporta un blog por categoría a un directorio de salida
│ ├── optimize-images.js ← Convierte imágenes a WebP usando sharp
│ └── sync-obsidian.js ← ⚠️ OBSOLETO — Watcher con chokidar (reemplazado por watchexec)
│
├── src/
│ ├── components/ ← Componentes reutilizables de Astro
│ │ ├── blog/ ← Componentes relacionados con el listado de posts
│ │ │ ├── Pagination.astro ← Paginación responsive (anterior/siguiente)
│ │ │ ├── Pagination.css ← Estilos de la paginación
│ │ │ ├── PostCard.astro ← Tarjeta clickable de post (con hover y overlay)
│ │ │ ├── PostCard.css ← Estilos de la tarjeta (con glassmorphism desde Junio 2026)
│ │ │ ├── PostHero.astro ← Hero de post individual (con o sin imagen de portada)
│ │ │ └── PostHero.css ← Estilos del hero
│ │ ├── common/ ← Componentes compartidos (header, footer, botones, tema)
│ │ │ ├── Button.astro ← Botón reutilizable (6 variantes × 5 tamaños)
│ │ │ ├── Footer.astro ← Footer simple con enlaces
│ │ │ ├── Footer.css ← Estilos del footer
│ │ │ ├── Header.astro ← Menú responsive con glassmorphism
│ │ │ ├── Header.css ← Estilos del header (sin overflow:hidden desde Junio 2026)
│ │ │ ├── ThemeSelector.astro ← Selector de 6 temas DaisyUI
│ │ │ └── ThemeSelector.css ← Estilos del selector de tema
│ │ └── graph/
│ │ └── ForceGraph3D.astro ← Grafo 3D interactivo (3d-force-graph + Three.js)
│ │
│ ├── content/ ← Contenido del sitio
│ │ ├── posts/ ← ✅ Colección de posts (markdown con frontmatter)
│ │ │ ├── Biodanza/ ← 12 posts + 4 subposts (cuatro-elementos)
│ │ │ ├── Literatura/ ← 2 posts (Fernando Pessoa)
│ │ │ ├── documentacion/ ← 5 archivos de documentación del sistema
│ │ │ ├── img/ ← ⚠️ Imágenes duplicadas de public/img/
│ │ │ ├── sample-folder-based-post/ ← Post demo con imágenes adjuntas
│ │ │ └── *.md ← 7 posts de tecnología + 2 de prueba
│ │ └── content.config.ts ← Schema Zod + loader glob (define la colección "posts")
│ │
│ ├── layouts/
│ │ └── BaseLayout.astro ← Layout base: View Transitions, Header, Footer, slot
│ │
│ ├── lib/ ← Lógica del negocio
│ │ ├── graph.ts ← generateGraphData() — genera datos para el grafo 3D (~180 LOC)
│ │ ├── posts.ts ← CRUD de posts: getAllPosts, getPostsByCategory, etc.
│ │ ├── posts.test.ts ← Tests unitarios (6 tests con Vitest)
│ │ ├── remark-wikilink.js ← Plugin Remark: procesa [[wikilinks]] → /posts/slug
│ │ └── tags.ts ← getAllTags(), getPostsByTag()
│ │
│ ├── pages/ ← 15 rutas que definen la URL structure
│ │ ├── 404.astro ← Página de error 404
│ │ ├── admin/
│ │ │ ├── index.astro ← Panel de administración (stats + rebuild)
│ │ │ └── login.astro ← Login con JWT
│ │ ├── animated-sections.astro ← Página demo con secciones animadas
│ │ ├── categories/index.astro ← Listado de todas las categorías (con glassmorphism desde Junio 2026)
│ │ ├── category/[category]/
│ │ │ ├── index.astro ← Posts filtrados por categoría
│ │ │ └── page/[page].astro ← Paginación de categoría
│ │ ├── feed.xml.ts ← RSS feed global
│ │ ├── graph.astro ← Página del grafo 3D (con glassmorphism desde Junio 2026)
│ │ ├── index.astro ← Página principal (con hero glassmorphism unificado)
│ │ ├── posts/
│ │ │ ├── [...slug].astro ← Página de post individual
│ │ │ └── index.astro ← Listado de todos los posts (con glassmorphism desde Junio 2026)
│ │ ├── search.astro ← Página de búsqueda con Pagefind (262 LOC)
│ │ └── tags/
│ │ ├── [tag].astro ← Posts filtrados por tag
│ │ └── index.astro ← Tag cloud (nube de tags)
│ │
│ ├── styles/
│ │ └── global.css ← 202 LOC: grids fluidos, tipografía responsive, wikilinks
│ │
│ ├── types/
│ │ └── pagefind.d.ts ← Tipos TypeScript para la API de Pagefind
│ │
│ ├── content.config.ts ← Schema Zod + loader glob (duplicado para claridad)
│ └── env.d.ts ← Tipos de entorno de Astro
│
├── auto-commit.sh ← Script ejecutado por watchexec: git add → commit → push
├── astro.config.mjs ← Configuración de Astro: site, aliases, plugins remark
├── cline-instructions.md ← Instrucciones/prompts para la IA Cline
├── package.json ← Dependencias npm y scripts
├── tailwind.config.cjs ← Configuración de Tailwind + DaisyUI (6 temas)
├── tsconfig.json ← TypeScript strict + path aliases
├── vitest.config.ts ← Configuración de Vitest
│
├── nohup.out ← ⚠️ Log de nohup (archivo huérfano, se puede eliminar)
├── watch-native.log ← ⚠️ Log de watchexec (vacío, se puede eliminar)
└── watch-native-error.log ← ⚠️ Log de error de watcher (contiene error de sintaxis)
2. Flujo de datos en build time
Cuando ejecutas npm run build, ocurre lo siguiente:
Obsidian vault (notas.md + imágenes)
↓ Syncthing (sincronización en tiempo real)
src/content/posts/ (~30 posts .md)
↓ Build time (Astro 4.16 → Node.js)
├── 1. content.config.ts:
│ Zod valida el frontmatter de cada post (título, slug, fecha, etc.)
│ Si algún post tiene frontmatter inválido, el build falla con ZodError
│
├── 2. remark-wikilink.js:
│ Procesa los [[enlaces]] estilo Obsidian y los convierte en
│ enlaces HTML a /posts/slug-correspondiente
│ Ejemplo: [[el-agua]] → <a href="/posts/el-agua">el-agua</a>
│
├── 3. posts.ts:
│ Filtra posts (draft=false), los ordena por fecha,
│ los agrupa por categoría, prepara la paginación
│
├── 4. graph.ts:
│ Lee las relaciones de cada post (enlaces entre posts)
│ y genera graph-data.json para el grafo 3D
│
└── 5. sharp (scripts/optimize-images.js):
Opcional — convierte imágenes a WebP para optimizar rendimiento
↓
~150 páginas HTML estáticas
+ graph-data.json (datos para 3d-force-graph)
+ feed.xml (RSS con todos los posts)
+ pagefind/ (índice de búsqueda full-text)
↓ GitHub Actions → Cloudflare Pages (deploy automático)
¿Qué pasa si el build falla?
- ZodError: Frontmatter inválido (el error muestra qué archivo y qué campo)
- Error de import: Algún componente Astro tiene un error de sintaxis o import incorrecto
- Error de dependencia: Algún paquete npm falta o está corrupto
En todos los casos, GitHub Actions muestra el error en los logs y no despliega. No hay notificación automática (ver doc-auditoria sección 4 sobre vulnerabilidades).
3. Dependencias entre módulos
El código de src/ está organizado en capas. Cada capa solo depende de las que están debajo:
content.config.ts (Zod schema, loader glob)
↓ Define la colección "posts" y valida el frontmatter
posts.ts (getAllPosts, getPostsByCategory, getAllCategories,
getFeaturedPostsByCategory, paginate, cleanSlug)
↓ Exporta funciones que leen la colección definida por content.config.ts
tags.ts (getAllTags, getPostsByTag) ← depende de posts.ts
graph.ts (generateGraphData) ← depende de posts.ts
remark-wikilink.js ([[slug]] → /posts/slug) ← independiente (solo procesa markdown)
↓
pages/* (15 rutas Astro que importan funciones de lib/)
Regla importante: Ningún módulo de lib/ importa directamente de pages/ o components/. La dirección de las dependencias es siempre hacia abajo.
Layout y componentes (árbol de renderizado)
BaseLayout.astro (View Transitions, flex layout, slot para contenido)
├── Header.astro
│ ├── Header.css
│ ├── ThemeSelector.astro + ThemeSelector.css
│ └── Script de menú hamburguesa (responsive)
├── Footer.astro + Footer.css
└── <slot /> ← Aquí se inyecta el contenido de cada página
¿Qué es el <slot />? En Astro, el slot es donde se renderiza el contenido de la página que usa el layout. Por ejemplo, si index.astro usa BaseLayout, todo el HTML de index.astro se inserta en el slot.
4. Páginas y dependencias directas
Cada página importa funciones de lib/ y componentes de components/. A continuación se muestra qué usa cada una:
| Página (ruta) | LOC | Dependencias |
|---|---|---|
index.astro (/) | 42 | BaseLayout |
404.astro | 16 | BaseLayout |
search.astro (/search) | 262 | BaseLayout, Pagefind (librería externa) |
graph.astro (/graph) | 36 | BaseLayout, ForceGraph3D, generateGraphData |
feed.xml.ts (/feed.xml) | 18 | getAllPosts |
posts/index.astro (/posts) | 25 | BaseLayout, getAllPosts, PostCard |
posts/[...slug].astro (/posts/:slug) | 24 | BaseLayout, getAllPosts, PostHero |
categories/index.astro (/categories) | 27 | BaseLayout, getAllCategories |
category/[category]/index.astro | 39 | BaseLayout, getAllCategories, getPostsByCategory, PostCard, Pagination |
category/[category]/page/[page].astro | 49 | BaseLayout, getAllCategories, getPostsByCategory, PostCard, Pagination |
tags/index.astro (/tags) | 42 | BaseLayout, getAllTags |
tags/[tag].astro (/tags/:tag) | 63 | BaseLayout, getPostsByTag, getAllTags, PostCard |
admin/index.astro (/admin) | 97 | BaseLayout (JS cliente para stats y rebuild) |
admin/login.astro (/admin/login) | 72 | BaseLayout (JWT login) |
animated-sections.astro | — | BaseLayout (página demo) |
Todas las páginas usan BaseLayout, que a su vez incluye Header y Footer. Esto significa que Header y Footer se renderizan en todas las páginas.
5. Componentes y sus estados
Cada componente Astro tiene una función específica y un conjunto de estados visuales:
| Componente | Archivos | LOC total | Función | Estados |
|---|---|---|---|---|
| PostCard | .astro + .css | 104 + 181 = 285 | Tarjeta clickable que muestra un post | Normal, hover (700ms), con overlay de categoría |
| PostHero | .astro + .css | 107 + 235 = 342 | Hero del post (título, metadatos, imagen) | Con imagen de fondo, sin imagen, responsive (móvil/escritorio) |
| Pagination | .astro + .css | 54 + 84 = 138 | Navegación anterior/siguiente entre páginas | Normal, deshabilitado (cuando no hay anterior/siguiente), sin páginas suficientes |
| Header | .astro + .css | 140 + 311 = 451 | Menú de navegación principal | Escritorio (menú horizontal), móvil (hamburguesa), con dropdowns |
| Footer | .astro + .css | 11 + 44 = 55 | Pie de página simple | Único estado |
| Button | .astro | 52 | Botón reutilizable | 6 variantes × 5 tamaños = 30 combinaciones |
| ThemeSelector | .astro + .css | 66 + 128 = 194 | Selector de tema DaisyUI | Cerrado, abierto (dropdown), con tema activo |
| ForceGraph3D | .astro | 73 | Grafo 3D interactivo | Cargando (mientras Three.js inicializa), renderizado, error (si no hay datos) |
| BaseLayout | .astro | 38 | Layout base que envuelve todo | Único estado (contiene Header y Footer) |
Ciclo de vida de un componente como PostCard
- Renderizado en servidor: Astro obtiene los datos del post (título, slug, imagen, categoría) y genera el HTML estático.
- Hidratación en cliente: El CSS se aplica, los hovers funcionan, los enlaces son navegables.
- Interacción: El usuario hace hover (700ms de transición), ve el overlay de categoría, y hace clic para ir al post.
6. Arquitectura CSS
Regla fundamental
Todo el CSS vive en archivos .css independientes (uno por componente). Está prohibido usar clases inline de Tailwind en los templates .astro. Esto mantiene el CSS limpio, reutilizable y fácil de mantener.
¿Por qué CSS separado y no Tailwind inline?
- Mantenibilidad: Cambiar el estilo de un componente solo requiere editar un archivo
.css, no buscar clases en múltiples templates. - Reutilización: Las clases CSS se pueden reutilizar en varios componentes sin duplicar código.
- Rendimiento: El CSS se extrae y optimiza durante el build (Astro lo purga automáticamente).
Archivos CSS (7 archivos, ~1,185 LOC total)
| Archivo | LOC | Contenido |
|---|---|---|
global.css | 202 | Grids fluidos (5 variantes), tipografía responsive con clamp(), estilos de wikilinks, animaciones globales |
PostCard.css | 181 | Variables CSS, card clickable, overlay de categorías con glassmorphism, transiciones hover |
PostHero.css | 235 | Hero con/sin imagen de fondo, badges de categorías y tags, metadatos responsive |
Pagination.css | 84 | Navegación responsive, oculta páginas en móvil, centrado |
Header.css | 311 | Variables CSS, navbar, botones glassmorphism, dropdowns, menú hamburguesa para móvil |
Footer.css | 44 | Footer simple con enlaces, centrado, padding |
ThemeSelector.css | 128 | Dropdown glassmorphism, items con secondary badges, animaciones de entrada/salida |
Patrón BEM utilizado
.bloque → Componente principal (ej: .site-header)
.bloque__elemento → Elemento hijo (ej: .site-header__btn)
.bloque--modificador → Variante (ej: .site-header--transparent)
Glassmorphism unificado
El efecto glassmorphism (fondo semitransparente con blur) se usa en varios componentes. Todos siguen el mismo patrón:
| Selector | Background | Blur | Hover |
|---|---|---|---|
.site-header__btn | oklch(var(--p) / 0.5) | 24px | oklch(var(--p) / 1) |
.site-header__hamburger | oklch(var(--p) / 0.5) | 24px | oklch(var(--p) / 1) |
.theme-selector__btn | oklch(var(--p) / 0.5) | 24px | oklch(var(--p) / 1) |
.post-card .btn.btn-primary | oklch(var(--p) / 0.5) | 24px | oklch(var(--p) / 1) |
.site-header__dropdown-content | hsl(var(--b1) / 0.7) | 24px | — |
.theme-selector__content | hsl(var(--b1) / 0.7) | 24px | — |
.site-header__dropdown-item | oklch(var(--s) / 0.5) | — | oklch(var(--s) / 1) |
.theme-selector__item | oklch(var(--s) / 0.5) | — | oklch(var(--s) / 1) |
Glassmorphism en botones de acción (Hero, CTA) - Junio 2026
Desde la actualización de Junio 2026, todos los botones de la home (index.astro) y las páginas de categorías/posts siguen este patrón unificado con blur máximo:
| Selector | Background | Blur | Hover |
|---|---|---|---|
.btn-primary (Hero) | oklch(var(--p) / 0.5) | 48px | Opaco (hover DaisyUI) |
.btn-ghost (Hero) | hsl(var(--b2) / 0.4) | 48px | Ghost hover (DaisyUI) |
.btn-outline (CTA) | hsl(var(--b2) / 0.4) | 48px | Outline hover (DaisyUI) |
| Badge Hero | oklch(var(--p) / 0.4) | 48px | — |
| Stats Hero | hsl(var(--b1) / 0.4) | 24px | — |
| Cards seccion About | bg-base-200/60 | — | Borde primario |
| PostCard | hsl(var(--b1) / 0.5) | 24px | Imagen scale(1.25) |
Principios del glassmorphism unificado:
- Transparencia: Todos los botones tienen
background-colorcon opacidad 0.4-0.5 para que el fondo con imagen se vea a traves - Blur maximo: 48px en botones principales; 24px en elementos secundarios (stats, cards)
- Borde sutil:
border: 1px solid oklch(var(--p) / 0.3)para botones primarios - Transiciones suaves:
transition-all duration-300en todos los botones interactivos - Sin overflow:hidden: Se elimino del navbar y navbar-center para que los dropdowns no queden recortados
Nota tecnica: oklch() es un espacio de color moderno que permite transparencias predecibles. hsl() se usa en algunos casos donde oklch no es compatible con ciertas variables de DaisyUI.
7. Configuración
astro.config.mjs
- Site:
https://mybrain-limpio.pages.dev(usado para sitemap y RSS) - trailingSlash:
never(las URLs no tienen barra final:/posts/el-aguaen lugar de/posts/el-agua/) - Integraciones: TailwindCSS
- Aliases Vite:
@→src/,@layouts,@components,@lib,@styles - Remark plugins:
remarkWikilinkconvalidSlugs(lee todos los slugs del sistema de archivos)
tailwind.config.cjs
- Plugins: DaisyUI +
@tailwindcss/typography(estilos tipográficos prolijos para el contenido markdown) - Temas: light, cupcake (personalizado), corporate (personalizado), dark, dim, sunset (personalizado)
- darkTheme: “dark” (tema oscuro por defecto cuando el sistema usa modo oscuro)
tsconfig.json
- Extiende
astro/tsconfigs/strict(TypeScript en modo estricto) - Paths aliases:
@/*(src/),@layouts/*,@components/*,@lib/*,@styles/*
vitest.config.ts
- Aliases (mismos que tsconfig)
- Environment:
node globals: true(no necesitas importardescribe,it,expect)- Include:
src/**/*.test.ts
8. CI/CD
GitHub Actions — deploy.yml
# Se ejecuta en cada push a main
on: push → branches: [main]
jobs:
- Checkout del repositorio
- Setup Node.js 20
- npm ci (working-dir: multiblog-obsidian)
- npm run build (con variables de entorno: ADMIN_PASSWORD, JWT_SECRET)
- cloudflare/wrangler-action: pages deploy → mybrain-limpio
⚠️ Nota importante: El working-directory en deploy.yml usa multiblog-obsidian. Este es el nombre que tenía el proyecto originalmente. En GitHub Actions, el directorio se crea con el nombre del repositorio en GitHub, que podría ser markeup o multiblog-obsidian. Si el build falla en GitHub Actions, esta es la primera causa a revisar.
GitHub Actions — lighthouse.yml
# Se ejecuta en cada push y pull request a main
on: push + pull_request → branches: [main]
steps:
- Build del sitio
- treosh/lighthouse-ci-action con lighthouserc.json
- Umbrales mínimos:
performance: ≥ 0.8
accessibility: ≥ 0.9
best-practices: ≥ 0.9
seo: ≥ 0.9
9. Scripts npm
| Comando | Qué hace | Cuándo usarlo |
|---|---|---|
npm run dev | Inicia servidor de desarrollo en localhost:4321 | Mientras desarrollas o pruebas cambios |
npm run build | Build completo: astro build + pagefind | Antes de hacer push para verificar que todo compila |
npm run build:astro | Solo build de Astro → genera dist/ | Si solo quieres ver el HTML sin búsqueda |
npm run build:search | Solo indexar búsqueda con Pagefind (requiere dist/) | Si ya tienes dist/ y solo quieres regenerar el índice |
npm run preview | Previsualizar el build de producción localmente | Para ver cómo queda el sitio antes de publicar |
npm test | Ejecuta Vitest (6 tests) | Después de modificar lib/posts.ts |
npm run test:watch | Vitest en modo watch (se re-ejecuta al guardar) | Mientras desarrollas cambios en la lógica |
npm run export <cat> <dir> | Exporta un blog por categoría a un directorio | Para compartir contenido offline |
npm run sync | ⚠️ OBSOLETO — Usar watchexec + launchd en su lugar | No usar |
npm run optimize-images | Convierte imágenes a WebP con sharp | Cuando añades imágenes nuevas al blog |