Auditoría de MarkeUP: estado, rendimiento, SEO y dependencias
El estado técnico de MarkeUP: rendimiento, dependencias, SEO y accesibilidad, con el histórico de novedades y correcciones de cada auditoría desde julio de 2026.
sw.js v6: offline parcial. HTML network-only, /_astro+fuentes cache-first, /img/* network-first (su nombre no lleva hash y el contenido se regenera en el mismo path), pagefind network-first
🖼️ Imágenes siempre frescas
_headers v10: /img/* y /img/og/* pasan de 30 días con stale-while-revalidate a max-age=300, must-revalidate, y sw.js v6 deja /img/* en network-first (antes cache-first, que congelaba la foto antigua para siempre). ⚠️ Aquello era una segunda capa del problema: la causa raíz estaba en el pipeline — con dos fuentes del mismo nombre (foto.jpeg + foto.webp) la imagen publicada dependía del orden de procesado, y producción servía la del clon limpio (el .jpeg ampliado) mientras local servía la última editada. Arreglado archivando las fuentes que sobran en src/content/posts/img/_originales/ (una sola fuente por nombre), con prioridad determinista como red de seguridad y npm run images:check bloqueante en CI
🌐 Dominio corregido
site now https://markeup.pages.dev (antes mybrain-limpio inexistente — canonicals rotos)
/privacy, /terms y /faq creadas y enlazadas en footer; cookies: banner CookieConsent (sin trackers, localStorage solos esenciales).
🔗 OpenGraph/autor completo
rel=canonical auto, article:published/modified/author/section/tag, twitter:creator, meta author, Schema.org Person[] + publisher con logo.
🧭 Sitemap + RSS + Robots
changefreq × entrada, /llms.txt y /api/posts.json en sitemap; feed RSS con <image>/autor/idioma; robots por crawler + usuario.
♿ Accesibilidad
skip-link visible, focus-visible, un único <main> por página (limpieza <main> anidados), page 404 con sugerencias + noindex, a11y-check.mjs ampliado.
🪄 Rich previews
public/preview.js (defer global): hover-tooltip sobre wikilinks/enlaces internos con imagen+metadatos y coreografía GSAP «la tarjeta se materializa y su foto se abre» (≈0,8 s, beats solapados). Contrato de suavidad: solo se animan transform, opacity y clip-path — ni una propiedad de layout (animar el tamaño era la causa de los tirones) —, sin backdrop-filter en la tarjeta, tamaño medido una sola vez y will-change retirado al terminar (clase .lp-anim). Secuencia: tarjeta (opacity + scale + y desde el lado del enlace) y bloom → metadatos + título palabra a palabra (stagger 24 ms, ya con la tarjeta: si no, se vería un panel vacío) → la foto se abre por cortina (clip-path: inset(100%…)→inset(0%)) mientras la miniatura se asienta (scale 1,14→1) y un reflejo la cruza una vez → descripción y CTA con su flecha. El estado por defecto del CSS es el FINAL: sin GSAP, sin clip-path: inset() o con prefers-reduced-motion, la tarjeta aparece completa (plan B sin código). Alcance: cuerpo (.prose), wikilinks y cualquier enlace con data-preview="on" —incluidas las píldoras de «Posts que enlazan a este artículo», que viven fuera del cuerpo y por eso van marcadas (plantilla + setBacklinks)—. Cambio de enlace: se reutiliza la misma tarjeta, que viaja al enlace nuevo (transform puro desde la posición anterior) y vuelve a revelar su contenido — un solo nodo, sin parpadeo ni contenedores a medias; la timeline es única por tarjeta (killTl(node)) y underPop() pregunta qué hay debajo para atender un enlace tapado. Colocación con recorte final al viewport (nunca se sale). Medido en Chrome real: p50 17 ms / p95 18 ms (60 fps, sin picos). Verificación: npm run verify:preview (CDP: tamaño constante, cortina, cascada del texto, frame pacing, escena 2 de dos enlaces contiguos y escena 3 de backlinks) + src/lib/preview-choreography.test.ts.
🤖 llm.txt
/llms.txt estándar (página + posts) y manifiesto /api/posts.json para tooltips/cards/agentes.
⚙️ Share
Componente SocialShare en posts: X, LinkedIn, Facebook, WhatsApp, Telegram y copiar enlace.
Era destructivo (leía solo .md en el primer nivel → borraba las 207 imágenes). Ahora: dry-run por defecto, --apply obligatorio, guardarraíl que aborta con 0 posts, protege /img/og/, escanea frontmatter + markdown + wikilinks + código (`src/**/*.astro
Cloudflare Web Analytics vía PUBLIC_CF_ANALYTICS_TOKEN (sin cookies, GDPR-friendly). Sin token no emite nada.
🔒 CSP hardening
Añadidas script-src-elem, style-src-elem, media-src, manifest-src; dominio de Cloudflare Insights permitido.
⚡ Rendimiento
BackgroundImage con image-set() responsive; tooltip de Graphify usa variante -640; preload de fuentes (Roboto Flex + Material Symbols); cache de HTML en _headers.
♿ a11y-check ampliado
Verifica <h1> único, labels de inputs (incl. implícitos), rel=noopener en target=_blank, texto accesible en enlaces/botones, preload de fuentes. Arreglados <h1> faltante en /search y aria-label del buscador.
🧹 Repositorio
.git 304→207 MB (purga de 28 ficheros pesados borrados vía filter-repo); hermes-agent/ y embeddings de Obsidian archivados fuera del working tree; rama main-backup obsoleta eliminada.
📖 Docs
Nuevos README.md y CONTRIBUTING.md; doc-arquitectura/doc-operacion/doc-guia actualizados a Tailwind v4.
Los enlaces propagaban una ruta relativa (/posts/slug) → WhatsApp/LinkedIn/Facebook compartían un enlace roto (WhatsApp compartía literalmente /posts/caretas-mascaras sin dominio). Ahora se comparte la URL absoluta (https://markeup.pages.dev/posts/slug).
🧩 Nuevo src/lib/social-share.ts
Lógica pura y testeable (buildShareLinks, buildShareText). Lanza si recibe una URL no absoluta → el bug no puede repetirse.
📱 Web Share API
En móvil (táctil) los botones abren el panel nativo de compartir con la URL completa; degrada a los enlaces por red en escritorio.
twitter:site y twitter:creator = @splitzforx (constante TWITTER_HANDLE en BaseLayout). Se eliminó el twitter:creator = "Admin" inválido que X ignoraba.
🏷️ og:site_name corregido
MarkUP → MarkeUP, coherente con Footer y RSS.
📐 Ratio OG 1.91:1
Por defecto se declara 1200×630 (ratio ideal para summary_large_image); las imágenes de fallback 1920×1080 se reescalan dentro de ese marco.
🔗 Schema.org autor
author[].sameAs = ["https://x.com/splitzforx"] y article:author:profile.
✅ Validador nuevo
scripts/mjs/validate-share.mjs (npm run validate:share): audita OG + Twitter Cards + URLs absolutas de compartir en dist/. Falla con código 1 → apto para CI.
🧪 Tests
src/lib/social-share.test.ts (9 casos). Total Vitest: 30 tests.
Auditoría de limpieza del sitio: archivos caducos, duplicaciones, artefactos
generados versionados e inconsistencias de marca.
Cambio
Descripción
🏷️ Marca unificada
El <title> de los posts decía | Multiblog mientras og:site_name decía MarkeUP. Se unificó todo a MarkeUP (17 Multiblog + 6 MarkUP → 0). Señales de marca contradictorias para buscadores y redes.
🐛 Preload duplicado
roboto-flex.woff2 se precargaba dos veces en BaseLayout → petición y parseo repetidos. Ahora se declara una sola vez.
♻️ Helper compartido
getCategoryImage() estaba duplicada literalmente en /category/:cat y /category/:cat/page/:n. Extraída a resolveCategoryImage() en src/lib/images.ts.
🗑️ Artefacto generado versionado
public/pagefind/ (147 ficheros) estaba en git con un índice obsoleto (129 fragmentos / page_count 129) frente al build real (203 fragmentos). Es una caché que regenera scripts/dev-search.js; sale del control de versiones y entra en .gitignore.
🧹 Archivo histórico
cline-instructions.md, task_progress.md, GIT-HISTORY-CLEANUP.md y hermes/ movidos a docs/archive/ (obsoletos, conservados por trazabilidad). Eliminados directorios vacíos, 32 .DS_Store y __pycache__/.
🧭 Sitemap
Se quitó /llms.txt (un .txt no es una URL rastreable).
🤫 Ruido en tests
cleanSlug() emitía un console.warn en el caso esperado de slug vacío; ahora es silencioso.
🔍 Rutas demo
/animated-sections y /admin/* marcadas noIndex y desenlazadas de la home (siguen accesibles, fuera de índices).
🧪 Linter de wikilinks reparado
validate-wikilinks.mjs tenía 4 bugs que generaban 121 falsos positivos: analizaba el frontmatter como prosa, confundía el interior de [[wikilink]] con un corchete suelto, marcaba enlaces de referencia [texto][2] y reportaba líneas relativas al cuerpo. Ahora 121 → 0, con líneas exactas.
📖 Docs sincronizadas
Corregidos conteos desactualizados (76 tests, 74 posts, 385 páginas) y referencias a ficheros inexistentes (optimize-images.js, graph-2d.astro).
Auditoría completa de las animaciones del sitio (nueve subsistemas) medida en
navegador real: gates de movimiento, de tooltip y uno nuevo de grafos, más sondas de
preestado, accesibilidad y ritmo de frames.
Cambio
Descripción
🐛 opacity pisada en las tarjetas
La cortina de salida de los bloques (m-block-exit) se aplicaba también a los elementos que son bloque y beat a la vez (.post-card, el hero del artículo…). Una animación gana al style en línea, así que la tarjeta aparecía a opacidad 1 antes de entrar y perdía el fundido de entrada. Medido en el mismo elemento: opacity: 1 + animation-name: m-block-exit → ahora 0 + none.
🐛 Tres caminos rotos en el grafo 2D
canvas.dispatch('zoom', …) se llamaba sobre el elemento del DOM en vez de sobre la selección de d3: el encuadre automático (a los ~14 s de cargar), el atajo de teclado r y el botón «↺ Reset todo» lanzaban TypeError y no hacían nada.
♿ Movimiento reducido
El latido de la cabecera de cada artículo (breathe, 10 s en bucle) y el rótulo de Ventajas seguían animando con la preferencia activada; los grafos no la miraban en absoluto (física, partículas y latido continuos). Con reduce la simulación se resuelve de golpe y nada queda en bucle.
⚡ Tooltip más fino
La miniatura se descarga al detectar el enlace y se espera su decodificación antes de abrir la tarjeta, en vez de dentro de la animación: el pico de frame baja de 70/34/54 ms a 49/30/32 ms (p95 18-20 ms) y el gate deja de dar rojo.
🧹 Código muerto
Retirados el parallax y la cortina de imagen (0 usos en el sitio), la regla inerte #Tabla-de-contenido y dos selectores que nunca llegaban a aplicarse en la página de demostración.
🔧 Build reparado
El hook de pre-commit prettificaba el GSAP minificado de public/vendor/, de modo que npm run build fallaba en su segundo paso. Esa carpeta queda fuera de Prettier.
🧪 Gates
validate cubre ahora el listado y una categoría; nace un gate para los grafos 2D/3D y un test de contrato entre el CSS de movimiento y sus módulos.
🧠 Grafos con un solo cerebro
El 2D y el 3D decidían por su cuenta qué pintar. Ahora comparten estado (src/lib/graph-state.ts): vistas (Notas por defecto, Blogs, Tags, Huérfanos, Todo), consulta con sintaxis (tag:, path:, type:, -excluir, OR), foco con profundidad y política de etiquetas. La vista por defecto dibuja 105 nodos y 722 enlaces en vez de 416 y 1200: los tags son el 75 % de los nodos y ahogaban la estructura.
🔗 Vistas compartibles
El estado va en la URL (?v=todo&q=tag:yoga&f=post:el-aura&d=2), así que una vista concreta se puede enlazar. Lo que no se entiende se ignora: una URL no puede romper el grafo.
🏷️ Etiquetas del 3D
Solo se ven las del nodo y sus vecinos al pasar el ratón (más las 5 categorías): antes estaban TODAS encendidas y tapaban el dibujo. Se encienden mutando la visibilidad del sprite, sin reconstruir los objetos.
🛡️ Contrato de los datos del grafo
npm run validate:graph (en el build) comprueba ids únicos, extremos de enlace existentes y números finitos en las 12 páginas que embeben un grafo: un solo dato raro podía dejar el lienzo en negro sin que fallara ningún validador.
Verificación: 326 tests, movimiento 40/40 (portada, listado con 691 beats, una
categoría, un artículo y dos pasadas de movimiento reducido), tooltip 3/3, grafos 15/15,
CLS 0 y p95 de 16,7 ms.
Arquitectura limpia: Separación clara en capas (lib → pages → components → layout). Sin dependencias circulares.
CSS modular:@layer para control de cascada, overrides DaisyUI fuera de layer, 0% CSS duplicado.
SEO completo: BreadcrumbList (Schema.org), OG tags con dimensiones de imagen, Twitter Cards, preload de LCP, fetchpriority="high".
Imágenes responsivas: Pipeline completo con Sharp: crop 1920×1080 + 3 tamaños (1920/1280/640) en WebP calidad 82. <picture> + srcset + sizes (100vw en hero, 33vw en cards) + aspect-ratio en todos los componentes. Cloudflare sirve los archivos tal cual (no los reoptimiza).
Fuentes locales: Roboto Flex + Material Symbols cargadas sin CDN externo, font-display: swap.
Build automatizado:npm run build = images + astro + pagefind en un solo comando.
192 páginas estáticas: Sin servidor, sin base de datos, hosting en CDN global.
Búsqueda offline e inteligente:Pagefind indexa solo los 73 artículos (data-pagefind-body), se enriquece con el grafo de wikilinks/aliases y se reordena con una jerarquía estricta (título → contenido → contiene). Ver doc-buscador.
Grafo interactivo: 3D (Three.js) y 2D (D3.js) para visualizar relaciones entre contenido.
Scroll suave: Lenis 1.3 con soporte táctil y re-inicialización con View Transitions.
Generación automática de posts: Script generar_articulo.py que, a partir de una URL, extrae contenido, detecta categoría, genera tags y produce un artículo Markdown completo vía Ollama (qwen2.5-coder:7b-64k) con frontmatter validado contra el schema Zod del blog.
Detección inteligente de categorías: El script analiza el texto extraído contra un diccionario de palabras clave por categoría (Humano, Documentación, Máquina, Opinión, Literatura) y asigna automáticamente la más relevante.
Normalización de tags: Sistema de extracción de palabras frecuentes con filtrado de stopwords en español, normalización a formato palabra-compuesta y aseguramiento de cantidad mínima (3) y máxima (8) de etiquetas.
Estas dependencias son independientes del ecosistema npm y no afectan al build de Astro. Se gestionan con uv pip install y no aparecen en package.json.
Para profundizar en este ámbito, 5 Ritmos desarrolla gabrielle Roth y la Danza de los 5 Ritmos (5Rhythms): una práctica de movimiento meditativo y somático basada en las olas.
Este sitio no rastrea ni te muestra publicidad. Solo guardamos en tu navegador el tema visual y que has visto este aviso. Consulta los detalles en laPolítica de privacidad.