Todos los cambios notables en este proyecto serán documentados en este archivo bajo el estándar Keep a Changelog.
.gitignore: Configuración base para omitir dependencias de Node, archivos de entorno local y directorios generados por los builds de Astro.LICENSE.md: Implementación de los términos de propiedad intelectual con reserva absoluta de derechos y validez legal bajo la normativa colombiana.README.md: Creación de la documentación inicial del proyecto, detallando la arquitectura de software, stack tecnológico e identidad de marca.CHANGELOG.md: Inicialización de este archivo para el seguimiento estructurado del ciclo de vida y control de versiones del repositorio.
- Estructura base del framework Astro configurada para Generación de Sitios Estáticos (SSG).
- Entorno de TypeScript estricto mediante el archivo
tsconfig.json. - Archivo de directrices operativas
CLAUDE.mdpara el contexto de asistentes de IA.
- Guía de inicialización en la documentación para reflejar el uso de pnpm en lugar de npm.
- Corregida la sección de estructura del repositorio en el
README.mdpara adaptarla a la configuración actual del entorno.
- Actualizada la sección del stack tecnológico en el
README.mdpara remover Tailwind CSS y referencias a plataformas externas de despliegue, reflejando el enfoque de CSS nativo y pnpm.
src/content.config.ts: Definición de Astro Content Collections para gestionar contenido multiidioma con esquema tipado (título, descripción, nombre de página, idioma, imagen OG).src/pages/index.astro: Enrutador principal de idiomas con detección automática basada enaccept-languageheader (defecto español). Incluye:- Diseño visual con gradiente atardecer-océano (8 pasos: púrpura oscuro → naranja → azul cielo)
- Esferas flotantes animadas con blur y opacidad
- Logo prominente (200x200px) con animación pulse
- Títulos y subtítulos con fuentes Plus Jakarta Sans e Inter Variable
- Botones de acción para rutas localizadas (es/en)
- Redirección automática comentada para activar en producción
scripts/builders/og-base.ts: Template actualizado para generación de imágenes OpenGraph con estética coherente:- Fondo con gradiente matching del sitio (135deg, 8 pasos)
- Esferas de fondo difuminadas (naranja y azul)
- Logo centrado de 120px con drop-shadow
- Tipografía Plus Jakarta Sans con gradiente de texto para headlines
- Layout vertical centrado
CLAUDE.md: Corregido gestor de paquetes depnpmaBuncon todos los comandos actualizados:bun install(instalar dependencias)bun run dev(servidor de desarrollo)bun run build(build para producción)bun run preview(previsualización)bun run prebuild(generar imágenes OG)
README.md:- Actualizado gestor de paquetes a Bun con descripción de ventajas
- Todos los comandos de pnpm → bun run
- Añadida sección "Generar Imágenes OG" con comando prebuild
- Completada y detallada la sección "Estructura del Repositorio" con árbol visual del proyecto incluyendo:
- Rutas localizadas (
en/,es/) - Estructura completa de
src/(layouts, pages, components, data, styles, assets) - Carpeta
scripts/con builders de OG images - Directorio
public/images/og/para imágenes generadas - Explicación de directorios clave
- Rutas localizadas (
- TypeScript estricto en todo el código
- Paleta de colores: Atardecer/Océano (#051c36, #0a1929, #5a2a60, #d65a3a, #ff8c42, #87ceeb)
- Animaciones CSS:
float(20s-30s) para esferas,pulse-logo(3s) para logo - Responsive: Media queries en 768px y 480px
src/components/ui/RouterPage.astroysrc/styles/router-page.css: nueva experiencia visual de transición inicial con fondo ambientado, marca, acciones de idioma y estado de detección del navegador.src/pages/404.astro: página 404 personalizada con la misma estética del router y acciones de retorno al inicio.public/images/og/en/home.pngypublic/images/og/es/home.png: actualización de las imágenes Open Graph de la home para reflejar la nueva identidad visual.
src/pages/index.astro: reemplazada la landing anterior por una vista de enrutamiento inicial que detecta el idioma del navegador y redirige a la ruta correspondiente.src/layouts/Layout.astro: ajuste del layout principal para incluir metadatos SEO más completos, tags de Open Graph/Twitter, carga de fuentes y configuración básica de Google Ads.src/styles/global.css: añadidos estilos base para el esquema de color oscuro, tipografía general y reset visual del documento.README.md: documentación actualizada para reflejar la estructura y los scripts del proyecto en su estado actual.package.json: configuración del proyecto ajustada para incluir dependencias y scripts alineados con la nueva experiencia y la generación de assets.
- Se incorporaron nuevas dependencias para tipografías avanzadas y Partytown.
- Se reorganizó la estructura de páginas para soportar una entrada de idioma más consistente en el flujo de navegación.
- Sistema de diseño "atardecer" modular en
src/styles/, repartido por responsabilidad para localizar cada cosa con facilidad:tokens.css: fuente de verdad del sistema — paleta cruda, temperatura (gradientes y resplandores del atardecer), color semántico, escala de spacing «marea», tipografía fluida, radios y elevación, movimiento (duraciones y curvas), grid de 12 columnas con breakout y capas z-index.reset.css: reset moderno del documento.base.css: estilos de elementos base — titulares con jerarquía real, cuerpo, enlaces con subrayado que «atardece», código/mono y foco visible accesible.layout.css: primitivas de composición (.u-gridcon breakout,.stack,.cluster,.section).tones.css: temperatura de sección ([data-tone]night/sea/sand) para que el sitio «atardezca» al hacer scroll.identity.css: helpers de identidad de marca (.text-sunset,.eyebrow).motion.css: accesibilidad del movimiento (prefers-reduced-motion).
src/styles/global.css: convertido en punto de entrada (barrel) que solo orquesta los parciales del sistema en orden de cascada; ya no contiene reglas propias.src/layouts/Layout.astro: carga de tipografías alineada a la variante variable de Plus Jakarta Sans.package.json: reemplazada la dependencia estática@fontsource/plus-jakarta-sanspor la variable@fontsource-variable/plus-jakarta-sans.
- Resolución de la tipografía Plus Jakarta Sans: el paquete variable no estaba declarado y el
@importcon specifier pelado fallaba en postcss (ENOENT), lo que rompía el renderizado de cualquier página que cargaraglobal.css. Ahora la dependencia se declara y las fuentes se importan por ruta explícita (.../index.css).
- La separación en parciales conserva el mismo orden de cascada que el archivo único anterior, sin cambios visuales.
- Los estilos específicos de componentes se cargan desde su propio componente (patrón ya usado por
router-page.css), no desde el barrel global.
src/components/global/Navbar.astroysrc/styles/navbar.css: cabecera responsiva del sitio — barra sticky translúcida con filo de atardecer, navegación con voz mono y numeración01/02/03, switch de idioma (ES↔EN) y CTA. En móvil (<60rem) la navegación se colapsa en un overlay a pantalla completa con bloqueo de scroll del documento; accesible por teclado (aria-expanded,inert, cierre conEscape).src/components/ui/CTA.astroysrc/styles/cta.css: botón de acción de marca «el faro del atardecer». Componente parametrizado (href/size) pero de estilo fijo: en reposo el atardecer orbita el contorno, una brasa arde en el interior, un haz de luz barre la superficie y un sol late junto al texto; al pasar/enfocar «rompe el molde» (se descuadra e inclina con una onda expansiva). Contraste AA en ambos estados y respetaprefers-reduced-motion.src/components/pages/Home.astroysrc/styles/home.css: composición de la página de inicio como única fuente de la verdad, parametrizada por idioma.
src/assets/logo.svg,public/favicon.svgypublic/favicon.ico: llaves separadas del sol para que el conjunto respire y quede más cuadrado y simétrico (antes parecía un «ojo» con pupila vertical).src/pages/es/index.astroysrc/pages/en/index.astro: reducidas a delegar en el componenteHomepasando el idioma, eliminando la estructura y los estilos duplicados entre ambas.src/data/i18n.tsysrc/data/locales/{es,en}.ts: ampliado el esquema connav.cta,nav.openMenu,nav.closeMenuyhero.eyebrow.
- Los estilos de cada componente se movieron a su propia hoja en
src/styles/importada desde el componente (patrón derouter-page.css); los componentes ya no llevan<style>inline. - El comportamiento de cliente (toggle del menú, bloqueo de scroll, medición de la altura de la barra) permanece colocado en el
<script>del propioNavbar.astro.
src/components/pages/NotFound.astroysrc/styles/not-found.css: página 404 «el náufrago». El «0» del 404 es el sol del logo hundiéndose en el horizonte; los «4» flotan como boyas y el mar (panel conbackdrop-filter) sumerge la mitad inferior. Textos y coordenadas de puerto en voz mono; una sola ruta con textos ES/EN según el navegador.src/components/pages/Router.astroysrc/styles/router.css: nueva página de entrada «la carta de navegación». Una brújula construida con el sol del logo (sus 8 rayos son la rosa de los vientos) busca el rumbo y, al detectar el idioma, la aguja se asienta (resorte amortiguado) sobre la orillaes/en. Responsive con contenedor acotado al viewport.src/styles/identity.css: helper.brand-mono— marca mínima «{ ctgcode }» para páginas fuera del flujo (router, 404); las llaves se abren al hover y se recogen al pulsar.src/styles/base.css: barra de scroll con estética de atardecer (pulgar ámbar→coral→ocaso sobre carril de mar profundo).src/components/ui/CTA.astroysrc/styles/cta.css: refracción térmica («aire caliente») sobre el texto del CTA al hover, con ruido de turbulencia que muta en el tiempo (SVGfeTurbulence+feDisplacementMap).src/data/i18n.tsysrc/data/locales/{es,en}.ts: seccionesnotFoundyrouteren el esquema y ambos idiomas.
src/pages/404.astroysrc/pages/index.astro: reducidas a delegar en los componentesNotFoundyRouter.src/pages/index.astro: la entrada detecta el idioma del navegador, orienta la brújula y redirige automáticamente (window.location.replace) a la ruta localizada.
- Bloqueo de la interacción en móvil por el long-press nativo de los
<a>: al mantener pulsado un enlace, el callout/preview y el arrastre nativos podían quedar colgados y capturar los toques, dejando el sitio inservible hasta recargar. Cortado a nivel global en eladebase.css(-webkit-touch-callout,-webkit-user-drag,touch-action: manipulation); los controles añadenuser-select: noneydraggable="false".
src/components/ui/RouterPage.astroysrc/styles/router-page.css: reemplazados por la nueva página de entrada (Router.astro/router.css).
- Las páginas fuera del flujo (
404, entrada) siguen el patrón del proyecto: ruta delgada → componente encomponents/pages/→ hoja propia ensrc/styles/. - Toda animación respeta
prefers-reduced-motion; en el CTA se apaga además el filtro SVG (que el@mediaglobal de movimiento no alcanza al SMIL).
src/components/global/Footer.astroysrc/styles/footer.css: cierre del sitio «el fondo del mar» — el opuesto de la navbar. El sol del logo se hunde tras el filo superior, la navegación son marcas de profundidad de una sonda (numeración01–04), un botón de luz fría (bioluminiscencia turquesa) vuelve a la superficie, y al fondo la marca «CTGCODE» yace medio hundida, recortada y disuelta por un mask hacia la oscuridad. Incluye la invitación de cierre con el CTA.src/styles/tones.css: nuevo tono de sección[data-tone="abyss"](un paso más hondo que la noche) para el footer.src/data/i18n.tsysrc/data/locales/{es,en}.ts: secciónfooteren el esquema y ambos idiomas.
src/data/locales/{es,en}.ts: elheroadopta el eslogan oficial de la marca — «Construyendo el futuro digital desde el Caribe» (título) y una descripción alineada («Desarrollo de software a la medida y soluciones web de alto rendimiento desde Cartagena para el mundo»). El<title>SEO de la home pasa a reflejar el eslogan.src/components/pages/Home.astro: monta elFooteral pie de la página.README.md: eslogan oficial en la introducción.
.github/workflows/deploy.yml: workflow de despliegue continuo a GitHub Pages (build de Astro y publicación automática en cada push amain).
astro.config.mjs:output: "static"explícito para el build de GitHub Pages.
- CI: dependencia
libasoundajustada para compatibilidad con la imagen reciente de Ubuntu en el runner de despliegue.
- Hero de la home «la mesa de trabajo a la hora dorada» (
src/components/pages/Home.astro+src/styles/home.css): la primera cara del sitio, con composición asimétrica sobre una rejilla de 12 columnas. Titular fragmentado en tres voces y tres sangrías (mono → display en gradiente → remate con cursor que respira) y una ventana de código desfasada en diagonal donde un snippet TypeScript se escribe en vivo (resaltado de sintaxis hecho en build, sin JS de librerías) y, al terminar, «compila»: el sol tras la ventana se enciende, la luz de estado vira a turquesa y la salida reporta200 OK. Decoradores de mesa de trabajo — sello circular giratorio, stickers de código pegados torcidos, coordenadas de Cartagena al margen y línea de horizonte —, microinteracciones con propósito y entrada escalonada; todo respetaprefers-reduced-motion. src/data/i18n.tsysrc/data/locales/{es,en}.ts: elherocrece con el snippet de código, el título segmentado, los estados de compilación, la salida del build y los chips.
- Responsive del hero: se saca el hero de
u-gridy se acota con su propio contenedor, de modo que la ventana de código ya no desborda en móvil (su scroll horizontal queda contenido dentro de la ventana). El titular en gradiente deja de recortar los descendentes (la «g» de «digital»).
- Ruteo por idioma sin redirección (SEO): el español (idioma por defecto) pasa a vivir en la raíz
/como contenido real e indexable, y los idiomas adicionales bajo/<lang>(por ahora/en). Se elimina la página de enrutamiento con redirección automática, que impedía a Google rastrear el contenido real del sitio. src/layouts/Layout.astro: el idioma llega por prop (no porAstro.params), lo que da URLs canónicas correctas por idioma; se añaden alternativashreflang(es-CO,en,x-default) yog:localedinámico para SEO multiidioma.theme-coloractualizado a#0a1c2b(el fondo real del sitio).NavbaryFooter: los enlaces y el intercambio de idioma se reconstruyen con el nuevo helper de rutas.src/data/i18n.ts: nuevo helperlocalizedPath(lang, subpath)como fuente única de rutas localizadas.
src/components/global/LangNotice.astroysrc/styles/lang-notice.css: aviso discreto que, solo si el idioma del navegador difiere del de la página y existe esa versión, invita a cambiar de idioma —en el idioma de destino—. No redirige de forma automática: solo cambia si el usuario pulsa, y recuerda el descarte.src/data/locales/{es,en}.ts: secciónlangNotice.
src/components/pages/Router.astroysrc/styles/router.css: la página de enrutamiento por redirección, ya innecesaria. También la rutasrc/pages/es/(el español ahora es la raíz) y la secciónrouterdel esquema i18n.
src/components/global/CookieBanner.astroysrc/styles/cookie-banner.css: banner de consentimiento de cookies. Aparece solo si el usuario aún no ha decidido; al aceptar o rechazar actualiza el consentimiento en gtag y guarda la preferencia (no se vuelve a preguntar). No bloquea la interacción y habla el idioma de la página.src/data/locales/{es,en}.ts: seccióncookies.
src/layouts/Layout.astro: implementado Google Consent Mode v2. El consentimiento arranca denegado (ad_storage,ad_user_data,ad_personalization,analytics_storage) antes de cargargtag.js; solo se concede si el usuario acepta, y la preferencia guardada se reaplica en cada visita. Los scripts de Google Tag pasan al hilo principal (el orden que exige el consentimiento no lo garantiza Partytown). El banner se monta en todas las páginas.src/components/global/LangNotice.astro: espera a que se resuelva el consentimiento de cookies antes de mostrarse, para no solapar dos avisos.
- Rendimiento · fuentes: se cargan solo por el eje
wght(wght.css; se descartan italic/opsz que no se usaban) y una sola vez (se elimina la importación duplicada entreLayoutyglobal.css). Se precargan (<link rel="preload">) los.woff2latin del contenido visible para mejorar LCP y CLS. El subconjunto latin ya se descargaba solo víaunicode-range. - Rendimiento · Google Tag:
gtag.jsse carga ahora de forma diferida (requestIdleCallbacko primera interacción). Elconsent default(denegado) y los comandosconfigse encolan de inmediato, así que no se pierde medición y el arranque deja de ejecutar ~90 KB de terceros en el hilo principal.
@astrojs/partytown: quedó sin uso al diferirgtag.js; se retira la integración deastro.config.mjsy la dependencia (menos peso muerto).
- Accesibilidad · contraste (AA):
- El
eyebrowsobre secciones claras ([data-tone="sand"]) usaba ámbar sobre crema (ilegible); ahora vira a un ocaso oscuro mediante el nuevo token--color-eyebrow. - El selector de idioma de la navbar sube de
--color-text-subtlea--color-text-muted. - Los índices numerados (
01/02…) de navbar y footer suben su opacidad de 0.55 a 0.75.
- El
- Accesibilidad · árbol para agentes: el aviso de idioma dejaba
role="status"sobre un<aside>(rol no apropiado para ese elemento); pasa a<div role="status">.
- SEO · dominio de producción:
siteConfig.urlpasa dehttp://localhost:4321ahttps://ctgcode.com. Las URLs canónicas, loshreflangy losog:urldejan de apuntar alocalhosty usan el dominio real; así casan con elsitemap-index.xmly elrobots.txtya generados.
- El footer absorbe el contacto y se convierte en el cierre de venta (
Footer.astro+footer.css): bajo «¿Zarpamos?» vive ahora el formulario de contacto (nombre, correo y «tu idea»), cuyo submit es el propio CTA «el faro del atardecer». Los campos son pozos en el lecho marino que se encienden en ámbar al enfocarlos, con etiquetas en la voz mono (// nombre). Al lado, el directorio en columnas: navegación (la sonda01–04), canales de atención (WhatsApp Business, Instagram, Facebook, LinkedIn y correo, con códigoswa/ig/fb/in/@) e información legal (Política de privacidad y Términos y condiciones, con rutas localizadas por idioma). - El ancla
#contactvive ahora en el footer: los CTA de la navbar y del hero aterrizan directamente en el formulario, en cualquier página que use el footer. src/components/pages/Home.astro: se elimina la sección independiente de contacto (absorbida por el footer).src/data/i18n.tsysrc/data/locales/{es,en}.ts: elfootercrece con el formulario (etiquetas y placeholders), los encabezados de grupo y los textos/slugs legales.
- Sección «nosotros» — «los cuatro principios (
src/components/sections/About.astro+src/styles/about.css): los valores corporativos como rosa de los vientos — Confianza es el norte (000°), Excelencia el este por donde sale el sol (090°), Resiliencia el sur de mar abierto (180°) e Innovación el oeste hacia lo que viene (270°). Bento asimétrico sobre el cielo ligeramente estrellado; cada carta lleva grados y apodo en voz mono más una aguja apuntando a su rumbo. La celda destacada es el compromiso con el cliente: una carta cálida (tonosand) que se lee mirando al cielo — «Tu éxito es nuestro puerto» — con las soluciones (robustas, escalables, de alto impacto y rendimiento, con tecnologías y estándares modernos) y la firma de terminal$ rumbo fijado → tu éxito. src/data/i18n.tsysrc/data/locales/{es,en}.ts: secciónabouten el esquema y ambos idiomas.src/components/sections/: nueva carpeta para las secciones de página reutilizables.
- Rediseño «el descenso» — del espacio a la arena de Cartagena: la home se recorre ahora como un viaje vertical al hacer scroll, un atardecer que cae mientras bajas. Cinco altitudes encadenadas por sus temperaturas de sección: cosmos (Hero) → cirros (About) → horizonte de fuego (Projects) → mar (Services) → arena de noche (Footer/Contacto). Las costuras entre secciones son continuas: cada
[data-tone]cierra en el color exacto con el que abre el siguiente, y algunos elementos (el sello del estudio, el rescoldo del sol, los cirros) cruzan la frontera para que nada se corte en seco. src/components/backdrop/: nueva familia de componentes de escenografía atmosférica, aislados, reutilizables yaria-hidden, con su CSS y su script co-ubicados.Starfield(carta estelar de navegación con Crux y Polaris; al tocar el cielo nace una estrella),Cirrus(cirros encendidos que derivan),Sun(el sol desciende con el scroll medido por un anillo de sextante, con senda de reflejos),Waves(el mar de frente: olas que vienen creciendo en perspectiva y rompen, resacas que vuelven, y cuya geometría se remodela en el viaje),CompassRose(rosa de los vientos como marca de agua) yNightSand(la orilla del puerto: rompiente que va y viene, boya y grano de arena).src/styles/tokens.css: ampliación de paleta muestreada del atardecer real de Cartagena (índigo, violeta, magenta, rosa, carmesí, escarlata, agua-abismo, arena-noche, espuma) y nuevo gradiente maestro--gradient-descent(la columna del atardecer).
- Estilos co-ubicados (
src/components/**+src/styles/): cada componente pasa a vivir en su carpeta con su hoja al lado (Nombre/Nombre.astro+Nombre.css); ensrc/styles/queda solo el sistema de diseño transversal. Las rutas de importación de páginas y componentes se ajustan al nuevo árbol. - Nuevos tonos del descenso (
src/styles/tones.css):cosmos,dusk,horizonysand-night(mássea, redefinido a azul profundo para que lea «mar»). El cremasandqueda reservado a páginas-documento. Todos cumplen contraste AA. - Hero, About y Footer: rediseño completo (escena atmosférica + contenido) sobre el nuevo lenguaje. El Hero aprovecha el ancho en pantallas grandes (consola y titular mayores) sin abultarse en portátiles chicos.
src/styles/tokens.css:--gradient-sunsety--palette-nightse rehacen hacia el arco oscuro/rojizo del concepto; superficies y sombras se retiñen del azul al púrpura-noche.
- Projects y Services se montan en este pase solo con su escena de fondo y su altura reservada (sin contenido ni copy inventados); se rellenarán sección por sección dentro de la cáscara ya coherente.
- Toda la escenografía anima solo
transform/opacity; el descenso del sol interpola porrequestAnimationFrame(movimiento sedoso, no a saltos de scroll) y la remodelación de las olas usa SMIL (<animate>sobre el atributod). Cada escena honraprefers-reduced-motioncon una «postal» estática equivalente (se congela, no se apaga), y el SMIL se pausa por script porque la regla global de movimiento reducido no lo alcanza. La coreografía se calcula por el progreso propio de cada sección, así el contenido futuro de Projects/Services podrá cambiar de altura sin desincronizar sol ni olas.
- Páginas legales con contenido real (
src/components/sections/LegalDoc/): la Política de Privacidad y los Términos y Condiciones dejan de ser cáscaras vacías. Un único componenteLegalDocrenderiza ambos documentos en los dos idiomas desde los locales. Estética sobria: sin escenografía ni movimiento, medida de lectura corta y jerarquía tipográfica limpia, pero dentro de la paleta oscura del sitio (aquí no hay modo claro); la marca solo asoma en el eyebrow mono, la numeración01/02de los artículos y el filo de atardecer bajo el título. src/data/legal.ts: datos del responsable (nombre, NIT, correo y fecha de revisión) centralizados en un único punto que alimenta los cuatro documentos. ExponelegalDataPending, que saca las páginas del índice mientras falten datos.- Imágenes OG para todas las páginas:
home,privacy,termsy404en español e inglés (8 tarjetas). Plantilla rediseñada con el lenguaje «descenso» —el atardecer completo, el sol posado en el horizonte y su senda de reflejos—; la 404 usa una escenanightpropia (el sol ya se hundió). - Datos estructurados JSON-LD (
src/data/schema.ts): grafo conOrganization(Cartagena de Indias, fundación 2026, correo, teléfono, redes y materias que domina el estudio),WebSite—de donde sale el nombre del sitio que muestra Google—,SiteNavigationElementy el tipo específico de cada página (WebPage,PrivacyPolicyPage,TermsOfServicePage) con sus migas. public/images/brand/logo.png: logo en ráster, requisito de Google para la propiedadlogodeOrganization(no admite SVG). Se reutiliza comoapple-touch-icon.
public/robots.txt: rastreo declarado agente por agente para GEO/AIO (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-SearchBot, anthropic-ai, Google-Extended, PerplexityBot, Applebot-Extended, CCBot y demás). Varios de ellos no heredan el permiso deUser-agent: *, así que sin nombrarlos el sitio quedaba fuera de las respuestas generativas.- Metadatos completos (
src/layouts/Layout.astro):og:site_name,og:image:type/width/height/alt,og:locale:alternate,twitter:image:alty una directivarobotsconmax-image-preview:largeymax-snippet:-1(permiten la tarjeta con imagen grande y que los motores citen con contexto). - Generación de OG (
scripts/builders/): las fuentes se embeben en base64 desde los paquetes@fontsource-variableya declarados, en vez de descargarlas de Google Fonts; el prebuild deja de depender de la red. Las tarjetas se renderizan a 1200×630 exactos para que pesen por debajo del umbral que WhatsApp necesita para mostrar la vista previa.
- Canónicas y
hreflangde las páginas legales: se construían con el identificador interno de página, así que la privacidad en español declarabactgcode.com/privacy/cuando su ruta real es/privacidad/(y/terms/frente a/terminos/). Ahora se resuelve el slug real de cada idioma. - Títulos y descripciones duplicados: Privacidad y Términos anunciaban el título y la descripción del Home, dejando tres URLs con los mismos metadatos. Cada documento tiene ya los suyos.
- Barra final inconsistente: las canónicas emitían
/privacidadmientras el sitemap listaba/privacidad/; para Google eran dos URLs distintas de la misma página.localizedPathañade ahora la barra final a las rutas (nunca a las anclas). - La 404 se declara
noindex: antes era indexable como cualquier otra página.
- Los identificadores de página (
pageName) siguen siendo estables e internos; la traducción a slug real vive enpageSlug()(src/data/i18n.ts), único punto donde se resuelve la equivalencia. El JSON-LD se omite por completo en las páginas fuera del índice y escapa<para que su contenido no pueda cerrar el<script>que lo aloja.
- Sección de proyectos en la Home — «el escaparate» (
src/components/sections/Projects/): con un solo trabajo entregable, la pieza NO es una rejilla esperando compañía, sino un caso destacado a ancho completo. El argumento son cifras verificables —rendimiento, SEO y accesibilidad medidos con PageSpeed Insights, citando herramienta y fecha— en vez de adjetivos. Dos salidas deliberadas: el sitio en producción del cliente (la prueba) y la página que engloba todo el trabajo (la continuidad). src/components/ui/ScrollShot/: componente reutilizable que recorre el sitio del cliente sin salir del nuestro. Una captura de página completa vive dentro de un marco con barra de navegador y se traslada conforme bajas; el teléfono se le superpone en una esquina y avanza a otra velocidad, porque sincronizados se leen mecánicos y desfasados dan profundidad. Sirve las capturas con<picture>en cadena de respaldo AVIF → WebP → PNG: el navegador se queda con el primer formato que entienda (642 KB en AVIF frente a 3.3 MB del PNG de reserva).src/data/i18n.tsysrc/data/locales/{es,en}.ts: secciónprojectsen el esquema y en ambos idiomas, con el slug localizado de la página índice (/proyectos/·/projects/).
src/components/pages/Home/Home.astro: la sección#projectsdeja de ser una cáscara con escena reservada y pasa a montar el caso destacado; la escena del sol viaja ahora dentro del propio componente.
- El recorrido de las capturas se ata al marco, no a la sección: vale 0 justo cuando el marco asoma en pantalla. Atarlo a la sección —mucho más alta y con el marco a media altura— hacía que al llegar a verlo el progreso fuera ya del 45 %, y el visitante se perdía la cabecera del sitio capturado. Además el recorrido se limita en unidades de viewport, porque estas capturas son larguísimas (la móvil ronda 1:19) y recorrerlas enteras las hacía pasar volando: con el tope, la velocidad queda por debajo de 1× respecto al scroll. Que no se llegue al pie es aceptable; perderse la cabecera, no.
- El contraste sobre la banda de fuego se resuelve apartando el sol en móvil (se corre a estribor y encoge) y con un velo oscuro bajo la tarjeta, no con sombras detrás del texto.
- Cabecera y caso comparten una sola rejilla en escritorio, de modo que la tarjeta arranca a la altura del titular sin márgenes negativos que se descuadren al cambiar el texto. Desde 90rem la tarjeta se lleva algo más de ancho (6/5 en vez de 7/5) para que las cuatro cifras entren en una sola fila.
- Sección de servicios en la Home — «la hondura» (
src/components/sections/Services/): el hueco reservado del mar deja de ser solo escena. La composición no es una rejilla de tarjetas iguales, porque los servicios no pesan igual: una pieza a ancho completo para el servicio insignia y, debajo, una retícula desnivelada 7/5 con los dos servicios de apoyo a babor —texto corrido con un filo que se enciende, sin cajas— y las preguntas frecuentes a estribor, en un aparte de filo punteado. - Acordeón de FAQ sin una línea de JavaScript:
<details>/<summary>con el atributonamecompartido, que lo hace excluyente de forma nativa (al abrir una pregunta se cierra la anterior). El signo+se convierte en−recogiendo su propia barra vertical. - Nodo
FAQPageen el grafo JSON-LD (src/data/schema.ts): se inyecta solo en la Home, con las mismas cinco preguntas que lee una persona en el acordeón. Nunca en las páginas legales, donde ese contenido no está. - Logotipos del stack (
src/assets/): once marcas —Astro, CSS, FastAPI, htmx, JavaScript, Jinja, PostgreSQL, Python, REST API, Rust y TypeScript— sustituyen a las píldoras de texto en los tres servicios. El registroTECHdel componente resuelve cada clave a su logo y su nombre; los locales solo guardan las claves, porque las marcas no se traducen. - Rótulo propio al pasar sobre cada logotipo: reemplaza al tooltip del navegador, que llega tarde y se pinta con los colores del sistema. Voz mono ámbar sobre superficie de noche, con su puntita de anclaje.
- La insignia del servicio destacado: un gallardete con la estrella dentro, dibujado en SVG con el mismo trazo abierto que el resto de instrumentos del sitio.
- Reflectores sobre el recuadro insignia: dos haces que cuelgan de lo alto y se cruzan sobre la pieza —cálido por babor, frío por estribor—, sumados con
screeny respirando desfasados. - Estrella al pulsar dentro del recuadro insignia: misma mecánica que el cielo del hero, pero con una estrella de cinco puntas de verdad en vez de un punto con destellos en cruz.
src/data/i18n.tsysrc/data/locales/{es,en}.ts: secciónservicesen el esquema y en ambos idiomas, con el slug localizado de la página índice (/servicios/·/services/).
src/components/pages/Home/Home.astro: la sección#servicesdeja de ser una cáscara con escena reservada; las olas y la rosa de los vientos viajan ahora dentro del propio componente.- La rosa de los vientos navega a estribor (
Home.css): ocupa el hueco que el titular deja a su derecha en vez de quedar debajo del texto. Su anclaje pasa de porcentaje arem, porque en porcentaje se descolgaba hacia el medio al crecer la sección. src/assets/astro.svg: el logotipo llega en un solopathmonocromo; se separa en dos para recuperar la llama naranja de la marca.
- Los logotipos se pintan con
<img>e importación?url, no incrustados en línea.postgresql,rest-apiyrusttraen su propia hoja<style>con clases genéricas (.st0,.st1) ypythondefine degradados con idsA/B: conviviendo en un mismo documento esas clases e ids son globales y se pisan entre sí. Dentro de un<img>cada SVG es un documento aparte. - Las placas claras bajo los logotipos no son decoración: cinco de los once vienen en negro o casi (
astro,jinjayrest-apino declaranfill;rustes#252422;htmx,#111111) y sobre el azul de la sección desaparecerían. La placa en tono arena los hace legibles a todos y unifica formatos que van deviewBox 0 0 24 24a0 0 630 630. astro.svgtraía dentro un@media (prefers-color-scheme: dark)que pintaba el logo de blanco. Dentro de un<img>esa consulta obedece al tema del sistema operativo, no al de la página: cualquiera con el SO en modo oscuro habría visto el logo blanco sobre la placa clara. Los colores quedan fijos en el propio archivo.- Los titulares de la sección usan
text-wrap: prettyy no elbalanceque heredan del sistema: repartir las líneas a la par dejaba media caja vacía a la derecha en móvil (el título del servicio destacado aprovechaba el 59 % del ancho; ahora, el 94 %). El título insignia lleva además su propia curva de tamaño, porque el mínimo de--text-2xlsolo dejaba entrar dos palabras por línea a 390 px; el tope en escritorio sigue siendo el mismo. - El recuadro insignia declara
grid-template-rowsexplícitas: sin ellas, el-1con el que la columna de logotipos llega hasta abajo no tiene última línea a la que apuntar —se resuelve como2 / 1— y los mandaba a la fila del rótulo. - Los haces van en
z-index: 0con el contenido por encima. No podían ir enz-index: -1: elbackdrop-filterde la tarjeta crea contexto de apilamiento y habrían quedado detrás de su propio fondo. - La apertura del acordeón se anima con
::details-contenteinterpolate-size: allow-keywordsacotado al bloque de FAQ, sin tocar la raíz del documento; donde no hay soporte, la respuesta aparece de golpe. - Sobre el
FAQPage: desde 2023 Google reserva el resultado enriquecido de FAQ a sitios gubernamentales y de salud, así que no va a pintar el acordeón en la SERP de un estudio de software. Se declara igual porque cumple el otro propósito del grafo: dar a los motores generativos respuestas atribuibles sobre plazos, costos, stack, soporte y cobertura en vez de dejar que las improvisen. - El enlace al catálogo apunta a
/servicios/·/services/, que todavía no existen (mismo estado que/proyectos/). Los slugs ya viven en los locales, así que crear la página no obligará a tocar el componente.
- Página de proyectos — «la bitácora» (
src/components/pages/Projects/, rutas/proyectos/·/en/projects/): el cuaderno abierto entero, asiento por asiento, con lo que hay a bordo. No clona el Home: el atardecer (sol, olas, cirros, la rosa que gira) es la escena del Home y aquí no se repite; solo se hereda el cielo estrellado de la portada. El resto vive sobre la noche y se ordena por composición: numeración de asientos, voz mono, acentos cálidos y asimetría en zigzag. - Cuatro asientos, cada uno en su punto: lo entregado (Recogras, con la captura recortada a su primer pliegue); Helio, el producto propio en construcción, cuyo visual es el plano (wireframe) de su panel dibujándose bajo una línea de escaneo —lo honesto cuando aún no hay captura—; las tres plantillas web, en filas intercaladas (imagen a un lado, ficha al otro, empezando y terminando con la imagen a babor); y las tres automatizaciones, con un diagrama de flujo (disparadores → núcleo → salida) por el que viajan los pulsos.
- Textura de fondo: la carta náutica en frío (
.log-backdrop): rejilla de carta, dos abanicos de líneas de rumbo desde puntos opuestos, lavados fríos que respiran y un cielo que tiñe la franja alta y se disuelve en la noche —la costura portada → primer asiento es un solo lienzo, sin corte. - Migas de pan, visibles (
Inicio / Proyectos) y en el grafo JSON-LD (BreadcrumbList), en ambos idiomas; más un nodoItemList(src/data/schema.ts) que enumera el inventario de la bitácora en el orden en que se lee. StackList(src/components/pages/Projects/StackList.astro): el stack de una pieza con la regla todo íconos o todo texto, nunca mezclado —si toda la lista tiene logo se pintan las placas (como en Servicios del Home); si alguna no lo tiene, la lista entera cae a texto.- Enlace «Proyectos» en la navbar y en el footer (numeración
01…04, idioma al05), con su etiqueta ennav.projectsde ambos locales. - Imágenes de las tres plantillas (
src/assets/plantilla_web_*): servicios profesionales, negocios locales y startups, en la cadena AVIF → WebP → PNG. - Builder de la imagen OG de la página (
scripts/builders/projects-og.ts), registrado enrun-builders.ts. - Micro-interacciones: revelado escalonado al entrar en viewport, el wireframe que se dibuja solo, los pulsos del diagrama de flujo y la aguja del cierre que oscila —todo se apaga con
prefers-reduced-motion.
Navbar.astrorecibepageNamey con él traduce el slug en el conmutador de idioma; suma «Proyectos» a la navegación numerada.Footer.astrosuma «Proyectos» al mapa del sitio y corre el idioma al índice05.src/data/i18n.tsylocales/{es,en}.ts: esquemaprojectsPageen ambos idiomas,nav.projectsy el slug localizado (/proyectos/·/projects/) resuelto porpageSlug.- El nombre de la miga en el grafo se limpia de la coletilla de marca:
«Proyectos — CTG Code»→«Proyectos».
- El conmutador de idioma llevaba a rutas inexistentes: desde
/privacidad/saltaba a/en/privacidad/(404) porque no traducía el slug —la ruta real es/en/privacy/. Ahora la navbar resuelve el slug porpageName, así que cada idioma cae en su URL real.
- El CTA del cierre baja al
#contactde la misma página (el footer vive en toda página) con scroll suave, en vez de navegar al Home y bajar allí —eso no tenía sentido; una flecha↓lo anuncia. - Las capturas de plantilla miden
1905×770(alguna771): el marco fija la relación1905/770y recorta conoverflow, así el píxel de más queda fuera. La de Recogras es una página larga (1905×7368), que se recorta a su primer pliegue conaspect-ratio+ máscara de fundido: se lee «esto sigue» en vez de mostrar la página entera. - Un
grid-columnsin acotar (.bt-head--rightdel bloque de Helio) se filtraba a la cabecera de automatizaciones y le rompía la rejilla en 12 columnas fantasma; las colocaciones de rejilla quedan acotadas a su sección. - La página se compone con el patrón del sistema (ruta delgada → componente en
components/pages/Projects/→ hoja co-ubicada); textos en los locales bajoLocaleSchema; ninguna escena debackdrop/del descenso salvoStarfielden la portada.
- Pipeline de despliegue centralizado multilingüe (
.github/workflows/deploy.yml): automatización en GitHub Actions para compilar la aplicación principalctgcode.comy realizar la clonación y doble compilación (Español e Inglés) de las tres plantillas independientes (plantilla-negocio-local,plantilla-servicios-profesionales,plantilla-producto-startup). - Soporte para el evento
repository_dispatch(update-projects) que permite disparar el despliegue automático desde los repositorios individuales de cada plantilla.
- Integración de variables de entorno
BASE_PATHyPUBLIC_LOCALEpara generar las salidas estáticas en subdirectorios coordinados (/plantilla-*/y/en/template-*/) compilados directamente a la carpetadist/para GitHub Pages.
- LCP móvil disparado por la animación de entrada del hero (PageSpeed móvil ~3.7 s de LCP frente a 99 en escritorio): el elemento LCP de la portada es el texto
.hero-lead, que entraba con un fundido (opacityde 0 a 1) tras unanimation-delay. Como el reloj de la animación es de pared, en móvil (CPU y red limitados) el pintado del texto quedaba a la espera del fundido y el LCP se apilaba sobre el FCP. Ahora el texto se pinta visible en el primer frame.
@keyframes hero-rise(Home.css) anima solotranslate(capa de composición), nuncaopacity: el «subir y asentarse» escalonado de la portada se conserva idéntico, pero ni el elemento LCP ni el resto del contenido esperan al fundido para hacerse visibles. Medido con throttling móvil, el render delay del LCP baja de ~1459 ms a ~600 ms sin tocar el diseño ni el CLS (se mantiene en 0). Las animaciones de entrada del sistema deben movertranslate/transform, noopacity, en cualquier contenido que pueda ser el elemento LCP.
- Las secciones bajo el pliegue del Home (About, Projects, Services) difieren su render con
content-visibility: auto+contain-intrinsic-size: el navegador salta el maquetado y pintado de esas escenas —cargadas de SVG decorativo (carta náutica, cirros, sol, olas, rosa de los vientos, diagramas de flujo)— hasta que se acercan al viewport. El primer render solo trabaja el hero.automemoriza el alto real de cada sección tras su primer pintado, así que el reparto sigue sin saltos (CLS 0) y las revelaciones al hacer scroll y los anclajes (#services,#contact) funcionan igual.
- Medido con CPU 4×, el maquetado inicial baja de 364 ms (1251 nodos) a 153 ms (291 nodos) y el recálculo de estilo de 315 ms a 45 ms —el hilo principal ya no procesa toda la página antes del primer pintado—, mejorando LCP y Speed Index en móvil. Es solo salto de trabajo fuera de pantalla: ni la estructura, ni los estilos, ni el orden visual cambian.
- Error en consola por CSP: el beacon de Cloudflare Web Analytics quedaba bloqueado. La
Content-Security-Policy(Layout.astro) no incluía el origen de Cloudflare Insights, así questatic.cloudflareinsights.com/beacon.min.jsviolabascript-srcy no cargaba —bajaba la nota de Prácticas recomendadas (errores de navegador en consola). Se añadehttps://static.cloudflareinsights.comascript-srcyhttps://cloudflareinsights.comaconnect-src(el destino del envío de métricas).
- CSP endurecida contra XSS (auditoría de Prácticas recomendadas): se añaden
base-uri 'self'(impide inyectar etiquetas<base>que secuestren rutas relativas) yobject-src 'none'(prohíbe<object>/<embed>/plugins). Sin fallback adefault-src,base-uriquedaba abierto; ahora la política cubre ambos vectores que Lighthouse marcaba.
- Notificación automática a IndexNow tras cada despliegue (
.github/workflows/deploy.yml, jobindexnow): al terminar el deploy, un job lee elsitemap-index.xmlya publicado, extrae las 14 URLs (páginas ES/EN + las seis de plantillas) y las envía awww.bing.com/indexnow(cualquier endpoint IndexNow comparte el aviso con Yandex y demás, pero se usa el de Bing porque es donde se verifica en Bing Webmaster Tools). Así el recrawl se dispara al instante en vez de esperar al rastreo periódico, complementando los sitemaps ya registrados. La clave de verificación vive enpublic/234f3a26e66c418f8923f549ce9fd3a9.txt.
- El job
indexnowcorre conneeds: deploy(solo si el despliegue tuvo éxito). Reintenta la lectura del sitemap hasta 6 veces (por la propagación del CDN) y registra en el log el payload y la respuesta de IndexNow; si no logra reunir URLs o IndexNow responde un código distinto de 200/202, falla de forma visible (::error::+exit 1) para poder diagnosticarlo —el despliegue ya se publicó en un job aparte, así que esto no revierte nada—. No usaset -ea propósito: rompía el reintento ([ -n "$urls" ] && breakabortaba el job con el sitemap aún sin propagar). El payload JSON se arma conjq; se envían todas las URLs del sitemap en cada despliegue (el sitio es pequeño e IndexNow admite reenvíos).
- El sol con su sextante (y su resplandor) se cortaban en seco en la costura entre secciones del Home. La causa:
content-visibility: auto(añadido en 0.23.2) imponecontain: paint, que recorta cualquier desbordamiento a la caja de la sección —y las escenas de Projects (el sol) y Services (olas + rosa de los vientos) están hechas justo para desbordar y fluir hacia la sección vecina, sinoverflow: hidden—. Se retiracontent-visibilityde.projectsy.services; las escenas vuelven a cruzar la costura sin corte. Se conserva en.about, que ya recortaba conoverflow: hidden(misma caja, sin regresión visual) y sigue difiriendo su render.
- Banda oscura horizontal en la costura entre «Sobre nosotros» y «Proyectos». Era el último resto del
content-visibility: autode 0.23.2, que seguía en.about: aunque la sección ya recortaba conoverflow: hidden, la contención de pintado creaba un borde de composición que no mezclaba con la sección vecina y dibujaba una línea oscura en la costura. Se retira también de.about, completando la reversión:content-visibilityes incompatible con el sistema de escenas continuas del descenso (ninguna sección del Home debe llevarlo). La costura vuelve a ser un degradado continuo.
- Navegación reordenada y sin enlace de contacto (
Navbar.astroyFooter.astro). El orden y la numeración ahora siguen el recorrido real de la página:01 Inicio · 02 Proyectos · 03 Servicios—Proyectos va antes que Servicios, como ya lo numeran los eyebrows de cada sección—. Antes la barra listaba02 Servicios · 03 Proyectos, invertido respecto al scroll. En el footer, el idioma pasa de05a04. - Se retira el enlace «Contacto» de la barra y del footer: era un ancla a
#contact(el propio footer), redundante porque el CTA «Cotiza tu proyecto» de la barra ya lleva allí y el formulario de contacto vive en ese mismo footer. El ancla#contacty el CTA se mantienen intactos.
- Señales «agent-ready» (GEO/AIO) a nivel estático, para que los asistentes descubran, lean y CITEN el sitio con ventaja:
Content-Signalenrobots.txt(grupoUser-agent: *): declara la preferencia de uso del contenido separada del acceso —search=yes, ai-input=yes, ai-train=no(sí a buscadores y a ser fuente citable; se reserva el entrenamiento de modelos). Es señal de preferencia, no control de acceso.<link rel="api-catalog">en el<head>+public/.well-known/api-catalog(linkset JSON en formato RFC 9264): punto de descubrimiento machine-readable que apunta alsitemap-index.xml(rel: sitemap) y a los datos estructurados schema.org de cada página (rel: describedby).public/auth.md: documento que declara explícitamente que el sitio es público y estático, sin autenticación, API keys ni OAuth para acceder a páginas, contenido o sitemaps.
- Se descartaron por diseño tres artefactos propuestos que no aplican a un sitio estático: la MCP
server-card.jsony la A2Aagent-card.jsonanuncian un servidor/agente invocable (con endpoint y transporte) que el sitio no expone, yagent-skills/index.jsonqueda huérfano sin ese agente. Publicar metadata de servicios inexistentes es engañoso y resta credibilidad; se añadirán como cards reales solo si se llega a exponer un endpoint de agente/MCP. - El
<link rel="sitemap">ya existía; NO se añadió elrel="service-doc"propuesto hacia el sitemap (un sitemap no es documentación de servicio). El formato del linkset se corrigió al de RFC 9264 (la relación como clave con array de destinos), en vez del objeto plano{anchor, rel, href}propuesto.
- Agent Skills Discovery (RFC de Cloudflare, v0.2.0):
public/.well-known/agent-skills/index.jsonque enumera una skill real y honesta —submit-lead— cuyo artefacto essubmit-lead/SKILL.md: documenta cómo un agente envía un lead (nombre, email, mensaje) al formulario público mediante el endpoint Formspree del sitio (https://formspree.io/f/xnjeaorj), con guía de consentimiento. El index referencia elSKILL.mdcon sudigestSHA-256 real, verificado contra el archivo desplegado. public/llms.txt(llmstxt.org): resumen del estudio, servicios, proyectos y canales de contacto en la voz de la marca, con enlaces a ambos idiomas, para que los LLMs entiendan y citen el sitio de un vistazo.public/.well-known/agent-card.json(descriptivo, enlazado conrel="describedby"desde el<head>): tarjeta que describe a la entidad CTG Code y remite a la skillsubmit-leady a los canales de contacto. Es un documento descriptivo, no un endpoint de agente invocable (así se declara en su propia descripción).robots.txt:Disallow: /cdn-cgi/en el grupoUser-agent: *— las rutas de infraestructura de Cloudflare no son contenido y no deben rastrearse.
Content-Signalabierto por completo:ai-trainpasa denoayes. Coherente con la filosofía de máxima visibilidad delrobots.txt(todos los agentes conAllow: /): ahorasearch=yes, ai-input=yes, ai-train=yes—que nos lean, nos citen y nos aprendan—.public/auth.md: el H1 pasa a# Auth.md — Authenticating with CTG Codepara incluir el literal «Auth.md» que esperan los validadores de descubrimiento de autenticación.
- Rectificaciones sobre la propuesta original: el
$schemacorrecto de Agent Skills eshttps://schemas.agentskills.io/discovery/0.2.0/schema.json(noagentskills.io/schema/...); una skill del estándar es un artefactoSKILL.mdcondigest, no un endpoint —el URL de Formspree vive DENTRO delSKILL.md, no en el campourldel index—; y el nombre debe sersubmit-lead(minúsculas y guiones;submit_leadcon guion bajo es inválido). El digest se calcula sobre los bytes LF (como los sirve GitHub Pages). No se crea la MCPserver-card.json: seguiría anunciando un servidor inexistente.
public/auth.mdreescrito para describir explícitamente la postura de autenticación y registro de agentes (lo que buscan los validadores tipo WorkOSauth.md), pero en negación veraz: declara punto por punto que no hay autenticación, ni registro de agentes/register_uri(RFC 7591 no aplica), ni recursos protegidos, ni servidor OAuth (/.well-known/oauth-authorization-serverRFC 8414 ni/.well-known/oauth-protected-resourceRFC 9728), y explica cómo un agente debe acceder (fetch directo, sitemap,llms.txt, la skillsubmit-lead). El H1 conserva el literal «Auth.md».
- NO se fabrica infraestructura OAuth: publicar un
/.well-known/oauth-authorization-servercon unregister_urio unoauth-protected-resourceque apuntan a servicios inexistentes sería metadata falsa, no funcional y un anti-patrón de seguridad. El sitio es público y estático: la respuesta correcta a «¿cómo registro un agente?» es «no hace falta», y así se documenta. Si un validador exige endpoints OAuth reales, ese check no aplica a este tipo de sitio y su rojo es un falso negativo.
- Fondo marino compartido
Seascape(src/components/backdrop/Seascape/): nuevo componente de escenografía que unifica el mar con burbujas, el cielo del atardecer y el sol de la marca en un único backdrop reutilizable. Aceptavariant(fullpara vistas con cielo + mar,waterpara solo el agua) de modo que cada vista (Home, Proyectos, Servicios, páginas legales) monte exactamente el mismo mar sin duplicar CSS ni lógica. IncluyeSkyHeroFx(src/components/backdrop/SkyHeroFx/), que pinta la franja de cielo con el sol posado en el horizonte. - Hero reutilizable
ViewHero(src/components/sections/ViewHero/): portada compartida con miga de pan, eyebrow, título, entradilla y manifiesto (índice en voz mono). Sustituye al hero artesanal que Projects tenía en línea (portada + cielo + estrellas) y se usará igual en Servicios. - Cabecera de sección
SectionHead(src/components/sections/SectionHead/): bloque de cabecera reutilizable (eyebrow + título + entradilla) para las secciones internas de las páginas. - Token
--wrap-view(src/styles/tokens.css): ancho estándar de las vistas (hero + cuerpo), para que Proyectos y Servicios compartan esqueleto.
- Proyectos (
Projects.astro+Projects.css): la portada propia (cielo estrellado, carta náutica, líneas de rumbo, lavados fríos) se retira y se sustituye por<Seascape variant="full">+<ViewHero>. Se eliminan ~420 líneas de CSS de fondo artesanal y ~90 de Astro; el resultado visual es el mismo mar del Home con la portada compartida. - Páginas legales (
LegalDoc.astro+LegalDoc.css): el gradiente índigo propio se sustituye por<Seascape variant="water">(solo agua, sin cielo). Las burbujas del Seascape se contienen conoverflow-x: clipy el documento se apila sobre el agua conz-index: 1.
- El refactor no cambia el resultado visual: cada vista sigue viéndose igual, pero la escenografía sale de un solo punto. Facilita que las futuras vistas (Servicios, etc.) monten su fondo con una sola línea (
<Seascape variant="full" />).
- Página de servicios — «la carta completa» (
src/components/pages/Services/, rutas/servicios/·/en/services/): la sección del Home enseña el mapa (tres servicios en una línea y las preguntas); esta página abre la carta entera. Tres bloques: el catálogo (cada servicio a fondo —qué incluye, para quién, stack—, con la clave de inventarioSFT/WEB/AUTen voz mono), el proceso (cómo trabajamos por hitos, de la brújula a producción) y los compromisos (hechos, no adornos: propiedad total del código, sin ataduras, soporte real). Cierre con CTA al formulario del footer. - Builder de imagen OG para la página (
scripts/builders/services-og.ts), registrado enrun-builders.ts. src/data/i18n.tsylocales/{es,en}.ts: esquemaservicesPagecompleto con catálogo, proceso y compromisos en ambos idiomas.src/data/schema.ts: la página de servicios se tipifica comoCollectionPagey su slug se resuelve porpageSlug.- Rutas
/servicios.astroy/en/services.astro(patrón ruta delgada → componente).
- Navbar y Footer: el enlace «Servicios» deja de apuntar al ancla
#servicesdel Home y lleva a la página propia (/servicios/·/en/services/), con slug resuelto porlocalizedPath.
- La página monta
<Seascape variant="full">+<ViewHero>del refactor anterior: una sola línea para el fondo y la portada. Los textos salen íntegramente de los locales; el componente no hardcodea copy. - Las plantillas de la sección intercalada de la página de Proyectos siguen el patrón «ladrillo» correcto: imagen grande (7fr) + texto pequeño (5fr), texto pequeño (5fr) + imagen grande (7fr), imagen grande (7fr) + texto pequeño (5fr). La imagen de negocios locales es del mismo tamaño que las otras dos, solo cambia de lado.
- Microsoft Clarity (mapas de calor / grabaciones de sesión) integrado con Consent Mode v2 (
src/layouts/Layout.astro): la colaclarity(...)se crea de inmediato, pero la librería solo se inyecta si hay consentimiento (lectura dectg-cookie-consentenlocalStorage) o cuando el banner lo concede (eventoctg-consent-resolved). Así no se contradice la Política de Privacidad (todo denegado por defecto).
- CSP ampliada (
Layout.astro): se añadenhttps://*.clarity.msyhttps://c.bing.comascript-src,connect-srceimg-srcpara que Clarity no viole la Content-Security-Policy.
- Ocultamiento del sol tras el horizonte (
src/components/backdrop/Sun/,Seascape.astro,Home.astro): se corrige el comportamiento del sol para que descienda y se oculte gradualmente tras la línea del horizonte en lugar de flotar o deslizarse sobre el cuerpo del mar. Al trasponer el horizonte, la masa del astro y el limbo del sextante se recortan medianteclip-pathtras el agua, mientras que el resplandor cálido (.ss-haze) y la línea de fuego (.ss-horizon) permanecen en el borde brillando con intensidad continua. Seascape.astro: integra la escena<Sun />dentro de la franja del cielo (.sea-sky) en la variantefull, encapsulando el ocaso en el backdrop compartido.Home.astroyHome.css: se elimina la implementación duplicada de.hhero-sunfixed en el hero que flotaba sobre el cuerpo de la página, unificando la escenografía en el backdrop compartido.
- La caída del sol se sincroniza tanto mediante CSS
animation-timeline: scroll()en el hilo del Compositor GPU como mediante interpolación fluida (rAF lerp--sun) cross-browser. Conprefers-reduced-motion: reduce, se congela en una postal estática con el sol posado en la línea del horizonte.
- Ajustes de acoplamiento visual del footer (
NightSand,Seascape,Footer): refactor de los patrones de fondo y capas de la arena de noche para extender las olas y texturas fuera del marco de página (inset: -4rem+overflow: hidden) y pulir el desvanecido continuo entre el lecho marino y la playa.
- Máscara gradual sobre
.ns-shorepara suavizar filos superiores y extensión del backdrop en los laterales.
- Microsoft Clarity solo se carga tras el consentimiento (
Layout.astro): la etiqueta se inyectaba siempre y el permiso se pasaba únicamente por su Consent API. Se comprobó en producción que esa vía suprime la grabación de la sesión —Clarity lo declara en consola— pero no impide que deje cinco cookies en el dominio de Microsoft (SM,MR,ANONCHK,CLID,MUID) ni que sincronice elMUIDcon Bing, todo ello antes de que el visitante decida. Ahora la etiqueta no se descarga hasta que hay permiso, y en una visita de vuelta con el permiso ya dado se carga de inmediato. - El oyente del consentimiento estaba en el blanco equivocado (
Layout.astro): escuchabactg-consent-resolvedenwindowmientras el banner lo despacha endocument. UnCustomEventsinbubblesdespachado endocumentnunca alcanzawindow, así que la llamada a la Consent API era código muerto y el fallo no producía ningún error en consola.
- La cola de
clarity()se sigue instalando siempre, de modo que cualquier llamada previa a la carga se encola y la etiqueta la procesa al arrancar. - Verificado contra el build de producción en un contexto de navegador limpio: sin aceptar, cero peticiones a
clarity.msy ninguna cookie de tercera parte; tras aceptar, la etiqueta y el motor cargan sin violar la CSP. - Aviso operativo: la inyección estática existía para que los verificadores de Bing Webmaster Tools y Clarity detectaran la etiqueta sin interacción. Con el cambio, un bot que ejecute JS ya no la verá cargarse; la integración debe estar verificada antes de desplegar.
- El resultado del formulario de contacto no se anunciaba y el foco se perdía (
Footer.astro): al enviar, el<form>se oculta condisplay: nonellevándose consigo el botón que tenía el foco, que caía a<body>. Los bloques de éxito y error no tenían ningún atributo de anuncio, así que quien usa lector de pantalla no recibía confirmación ni aviso de fallo en la única acción del sitio. Ahora el bloque de éxito esrole="status", el de errorrole="alert"—un fallo interrumpe, no informa de pasada— y el foco aterriza en el que aparece. Al reiniciar el formulario vuelve al primer campo. - Contraste insuficiente en la barra de URL del navegador simulado (
ScrollShot.css):--color-text-subtledaba 4.32:1 contra el fondo del marco, por debajo del 4.5:1 que exige AA para texto pequeño (12px). Se compone con el tonomutedpara subir el contraste sin llevarlo al texto pleno: la barra sigue leyéndose como cromo del navegador y no como contenido. - Nombre accesible de la marca (
Navbar.astro): el enlace declarabaaria-label="CTG Code"mientras en pantalla se lee «CTGCode». El nombre accesible debe contener el texto visible (WCAG 2.5.3), y el espacio lo rompía: el control por voz no encontraba el enlace. Se retira la etiqueta y el nombre pasa a salir del propio texto.
- Lighthouse en móvil sube de 97 a 100 en Accesibilidad, sin auditorías fallidas.
- Reflow forzado en el bucle del sol (
Sun.astro): el rAF que publica--sunleía la geometría del documento y escribía estilo en el mismo fotograma —layout thrashing— sesenta veces por segundo. Eran dos lecturas:closest(".sea-sky")congetBoundingClientRect().height, que devuelve un valor que no cambia al hacer scroll, ywindow.scrollY || document.documentElement.scrollTop, cuyo respaldo entra en juego justo con la página arriba (scrollYvale 0) y también obliga a rehacer el layout. Ahora la altura del cielo se resuelve fuera del bucle y la posición de scroll se cachea en el oyente; el bucle solo hace cuentas y escribe.
- La altura se refresca con un
ResizeObserversobre el cielo en vez de con un oyente deresize: cubre también la carga de fuentes y los cambios de contenido, y dispara una primera medición con el layout ya resuelto. - La posición de scroll se guarda en el oyente
scrollpasivo, que se despacha antes del rAF del mismo fotograma, así que no se pierde sincronía. - En la traza de producción (móvil, CPU 4x, Fast 4G) el script del sol era el primer culpable del reflow forzado con 2.496 ms; tras el cambio desaparece por completo de la atribución. La animación conserva su interpolación:
--sunrecorre 0 → 0,52 → 0,84 al descender.
- Canónica y
hreflangen las páginas fuera del índice (Layout.astro): la 404 declaraba<link rel="canonical">haciahttps://ctgcode.com/404/y alternativas de idioma hacia/404/y/en/404/. Ninguna de las tres URL existe: el SSG emite404.htmly el alojamiento lo sirve directamente, sin ruta propia. Ahora las páginasnoindexno declaran ubicación canónica ni alternativas, con el mismo criterio que ya se aplicaba al JSON-LD.
- El origen es el
defaultdepageSlug(), que devuelve elpageNametal cual cuando no lo reconoce:404se convertía en/404/sin que nada fallara en el build. Es el modo de fallo silencioso que permite el esquema de slugs traducidos, y conviene tenerlo presente al añadir vistas nuevas.
- La carta de servicios entra en el resumen para agentes (
llms.txt): el archivo no mencionaba/servicios/ni/en/services/. Los tres servicios apuntaban al ancla#servicesdel inicio, escrita cuando servicios vivía solo como sección; desde entonces es una vista completa con catálogo, fases y compromisos, y quedaba invisible para los motores generativos. Ahora los tres enlazan a la página, se añade una entrada propia para la carta completa y la versión en inglés figura entre los enlaces secundarios.
- Nada valida este archivo —ni el build ni el sistema de tipos—, así que conviene repasarlo cada vez que se añada o renombre una vista.
- Precisión recortada en los iconos de valores (
src/assets/{trust,excellence,resilience,innovation}.svg): entre el 96% y el 98% de cada archivo eran datos de trazado, con coordenadas de hasta cinco decimales sobre unviewBoxde 512. Se redondean a dos. Los cuatro archivos pasan de 77.298 a 42.320 bytes (45% menos) y, como el componenteAboutlos incrusta en línea —lo exige el recoloreado concurrentColoren:hovery:focus-within—, el documento del inicio baja de 62.006 a 43.446 bytes comprimidos: 18.560 bytes menos, el 30% del HTML de la página. - El redondeo es visualmente inocuo y acotado: los trazados usan solo comandos absolutos (cero relativos), así que el error no se acumula. El máximo por coordenada es de 0,005 unidades sobre 512, es decir 0,000625 px a 64 px de render.
- El guardián de visibilidad del sol era inerte (
Sun.astro): elIntersectionObserverobservaba<main>, que abarca el documento entero y por tanto intersecta el viewport a cualquier altura de scroll.inViewnunca pasaba afalse, así que el bucle de animación seguía pidiendo fotogramas hasta el pie de página, donde el sol lleva rato oculto tras el horizonte. Ahora observa el cielo (.sea-sky), que es la única franja donde el astro significa algo. - El sol entraba deslizándose al volver a subir: como consecuencia de lo anterior, al detenerse de verdad el bucle la posición interpolada quedaba congelada, y al reaparecer el cielo el sol se desplazaba desde ese valor viejo hasta el correcto. Al parar se invalida la interpolación, de modo que el primer fotograma tras reanudar se sitúa directamente en el objetivo, sin barrido (mismo criterio que en
ui/ScrollShot).
- Verificado en navegador: con el cielo fuera de vista el script del sol desaparece de las peticiones de fotograma, y al volver arriba
--sunvuelve a 0 en menos de 150 ms sin transición perceptible. - No se detectó ninguna fuga de memoria en el recorrido del sitio: seis pasadas completas arriba y abajo dejan el heap en 3.814 KB frente a los 3.732 KB iniciales, oscilando en vez de crecer.
- Los documentos legales seguían en el sitemap con los datos del responsable sin confirmar (
astro.config.mjs): el comentario dedata/legal.tsprometía quelegalDataPendinglos sacaría del índice y del sitemap, pero la integración no tenía filtro. El interruptor movía elmeta robotsanoindex, mostraba el aviso y omitía el JSON-LD, y aun así el sitio pedía indexar en el sitemap lo que prohibía indexar en la propia página. Ahora unfilterretira las cuatro URL legales mientras el interruptor esté activo.
- Las URL se derivan de los mismos slugs que usan el Layout y el pie (
footer.privacySlug/footer.termsSlugde cada idioma), no se escriben a mano en la configuración. - Comprobado en los dos estados: con los datos completos el sitemap lista 16 URL; con el interruptor activo, 12.
- Las tres demos de plantilla salen del índice: cada demo simula un negocio que no existe —un asadero en Bogotá, una consultora, un SaaS de facturación— con su nombre, su dirección y su teléfono. Un buscador podía tomarlas por fichas legítimas de negocios reales. Ahora se construyen con
PUBLIC_DEMO=true(ver eldeploy.yml) y esa variable, en cada repositorio de plantilla, hace tres cosas: emite<meta name="robots" content="noindex, follow">, omite el JSON-LD del negocio (Restaurant,ProfessionalService,SoftwareApplication) y no genera sitemap propio. astro.config.mjsdeja de inyectar las seis URL de las demos en el sitemap: desaparecen el bloquecustomPagesy la funciónserializeentera, que existía solo para asignarleshreflangcruzado. El sitio pedía indexar en el sitemap justo lo que ahora se prohíbe indexar en la propia página.
followy nonofollowa propósito: la demo enlaza de vuelta al sitio y ese enlace debe seguir contando. El propnoindexque ya tenían las plantillas sigue emitiendonoindex, nofollowpara el caso de un sitio de cliente.- Comprobado construyendo cada plantilla en los dos modos: con
PUBLIC_DEMOsalennoindex, follow, cero bloquesld+jsony cero sitemaps; sin la variable, sinrobots, dos bloquesld+jsony dos sitemaps.
- Una página dedicada por plantilla, en los dos idiomas (
/plantillas/…y/en/templates/…, seis rutas reales). Las demos son ahora la prueba, no la vitrina: quedaron fuera del índice, y hacía falta algo que sí posicione, se comparta y pueda citar un asistente. Cada página abre la plantilla entera: para quién es, el inventario real de sus secciones, hasta dónde llega la personalización antes de ser desarrollo a medida, qué incluye el encargo y qué se cotiza aparte, precio en pesos y dólares, plazo, y seis preguntas frecuentes propias. - El prefijo
plantillas/templateses deliberado: no puede confundirse con la URL de la demo (/plantilla-negocio-local/). - La de servicios profesionales publica su caso en producción: Recogras nació de esa misma base. Sus cifras se citan con herramienta y fecha (
metricsSource), y se publica el rango observado en tres pasadas, no la mejor.
- Un solo componente (
components/pages/Template/) sirve las seis rutas: el idioma llega porlangy la plantilla por una clave estable del locale, no por segmento dinámico. - Las tres invariantes silenciosas, cubiertas: caso en
pageSlug()para los trespageNamenuevos, seis imágenes OG con su builder (scripts/builders/templates-og.ts) enganchado alprebuild, y toda la copia enLocaleSchema.ViewHerogana una miga intermedia opcional: estas páginas cuelgan de la bitácora, no del inicio. - El sitemap las recoge solo, por ser rutas reales. Verificado sobre
dist/: canónica autorreferencial,hreflangrecíproco másx-default, y títulos y descripciones únicos frente a las otras once páginas del sitio.
- Las fichas de plantilla de la bitácora llevan a su página, no a la demo: tanto la captura como el enlace de
/proyectos/apuntan ahora a la página dedicada, que es la que enlaza la demo. El rótulo pasa de «Ver demo» a «Ver la plantilla». - El
ItemListde la bitácora deja de apuntar a anclas de sí misma: las tres plantillas se declaraban con#slug, así que el inventario estructurado señalaba a un trozo de/proyectos/en vez de a la URL que queremos que se indexe y se cite. llms.txtgana su sección de plantillas y se repasa entero: las tres páginas con su alcance, precio y plazo, las condiciones comunes del encargo, los enlaces en inglés, y una advertencia explícita de que las demos están fuera del índice y no deben citarse como negocios reales. Las cifras de Recogras se actualizan a lo medido hoy.
FAQPagepor URL y nodoServicecon su oferta: hasta ahora el grafo solo emitía FAQ en el inicio. Cada página de plantilla declara las suyas —las mismas seis que lee una persona en el acordeón, palabra por palabra— y unServicecon dosAggregateOffer, una por moneda. Se usaServicey noProductporque lo que se contrata es un encargo, no un artículo con existencias.- Migas de tres niveles:
breadcrumb()solo sabía hacer inicio → página. Las páginas de plantilla cuelgan de la bitácora, y ahora la miga lo dice igual que el hero.
- Los importes del
AggregateOfferse parsean de la cadena ya formateada en vez de duplicarse como números en el locale: dos copias del mismo dato acaban divergiendo en silencio. Si el formato cambiara y dejara de haber dos importes, esa oferta no se emite en lugar de publicar una falsa. - Verificado sobre
dist/: JSON-LD válido en las seis, 7 nodos y 0@idcolgando en cada una, y las preguntas y respuestas del grafo idénticas carácter a carácter a las del acordeón. - Lighthouse sobre el build servido: 100 en accesibilidad, SEO y prácticas recomendadas en las seis páginas nuevas, y sin regresión en
/proyectos/. Cero scroll horizontal en 8 páginas × 5 anchos (390, 768, 1280, 1366, 1920).
- Cambiar de idioma sacaba de la plantilla (
Template.astro): alNavbarse le pasabapageName="projects"para que la barra se leyera dentro de la bitácora, pero ese identificador es justo lo que usa el switch de idioma para traducir el slug. Pulsar «EN» en/plantillas/negocio-local/te dejaba en/en/projects/en vez de en la ficha en inglés. Ahora recibe elpageNamereal, que es el único uso que le da el componente. - La nota de la frontera hablaba de «la derecha» en móvil: los dos platillos se ponen lado a lado desde 48em y uno debajo del otro por debajo, donde no hay derecha que valga. Hay dos redacciones y el CSS enseña la que corresponde a la disposición real; la que sobra se oculta con
display:none, que además la saca del árbol de accesibilidad para que no se lea dos veces.
- Los perfiles de «para quién es» van todos a la misma sangría: estaban escalonados, y el escalón se leía como jerarquía —como si «Abogados» dependiera de «Contadores»—. Son oficios distintos con la misma dificultad y ahora se presentan en pie de igualdad.
- Arrastre al señalar un perfil: en vez del hover clásico que mueve solo el elemento apuntado, el señalado se desplaza y TIRA de los vecinos con fuerza decreciente (8 / 12 / 24 / 12 / 8 px). El gesto es lo que comunica que la lista es una cadena, ahora que la sangría ya no lo insinúa.
- El stack de cada plantilla se pinta con logotipos (Astro, Tailwind, Lucide) en lugar de nombres escritos, con la misma voz que la bitácora. Lo que no es una marca con logo —la salida estática, no llevar framework— pasa a una nota debajo, para no colar palabras sueltas entre las placas.
/plantillas/y/en/templates/dejan de dar 404: redirigen a la sección de plantillas de la bitácora. No los enlaza nadie ni están en el sitemap, pero quien recorte la URL a mano ya no se queda sin salida.
- El registro de tecnologías sale de
StackListadata/tech.ts, que ahora comparten la bitácora y las páginas de plantilla.sections/Servicesconserva su copia —y ya divergía, no conocetailwind—; migrarla toca una página del inicio y se deja para su propio commit. - Tres trampas del arrastre.
:has()hereda la especificidad de su argumento, así que los cuatro grados de tirón necesitan un prefijo común o la regla general le gana al elemento señalado y este se queda quieto. Lightningcss —el minificador del build— rompe la compilación entera ante un combinador inicial suelto (:has(+ x)), de ahí el:scopeexplícito, que es la forma larga de lo mismo. Y el peldaño y su revelado al scroll tienen que vivir en dos elementos distintos: ambos animantransformy la regla del revelado, más específica, dejaba el arrastre en cero. - Las redirecciones se declaran en
astro.config.mjsy no en Cloudflare: Astro emite una página conmeta refresh,noindexy canónica al destino, y el sitemap la ignora. Para una ruta sin enlaces entrantes es equivalente en la práctica, y la regla queda versionada con el código. - Verificado en las seis páginas: Lighthouse 100 en accesibilidad, prácticas recomendadas, SEO y navegación agéntica, con 0 auditorías fallidas; 100 de rendimiento en escritorio y 94–98 en móvil, en línea con
/proyectos/. LCP 0,5 s en escritorio y 2,1 s en móvil, CLS 0. Cero scroll horizontal en 8 páginas × 5 anchos.
- Los logotipos del stack no seguían el formato del sitio: se pintaban como píldoras con el nombre escrito al lado, cuando en el resto del sitio —la bitácora y la carta de servicios— un logotipo es un chip cuadrado de 2.5rem con fondo arena y el nombre en un globo que solo aparece al pasar el cursor. Se copia esa variante en vez de inventar una tercera. Comprobado propiedad a propiedad contra el chip de la bitácora: idénticos en las nueve que definen la pieza.
sections/Servicesdeja de tener su propio registro de tecnologías y usa el compartido dedata/tech.ts, que ya consumían la bitácora y las páginas de plantilla. La copia había divergido —no conocíatailwind—, así que un mismo logotipo podía salir en una página y faltar en otra. El comentario dedata/tech.tspasa a decir que es la única fuente, para que no vuelva a copiarse. Verificado comparando el HTML construido antes y después: los 16 logotipos del inicio salen idénticos, mismo orden y mismos archivos.
- Las tres plantillas publican sus cifras medidas. La de servicios profesionales ya enseñaba las de Recogras; ahora negocio local y producto/startup tienen su propio tramo. Como esas dos no tienen todavía un cliente en producción, el bloque no finge uno: dice explícitamente que lo medido es su demo —el mismo código que recibe el cliente— y enlaza a ella. Se publica el rango observado de tres pasadas en móvil y tres en escritorio, no la mejor.
- Las dos páginas sin tramo «en producción» se numeraban mal. Los índices de «Qué incluye» y «Hasta dónde llega» salen de un objeto compartido y estaban fijados en 04 y 05, pensados para la página de servicios, que sí tiene un tercer tramo. En las otras dos se leía 01, 02, 04, 05 —sin 03— mientras el índice de portada decía otra cosa. Al añadirles el tramo medido la secuencia cuadra sola, y la FAQ pasa de 06 a 07.
- Recogras sube a 100 de accesibilidad (venía de 96) tras corregir en su repositorio el contraste de los textos pequeños del pie y el de la insignia de economía circular. Se actualizan su bloque de cifras en los dos idiomas y su línea en
llms.txt, con la fecha de la nueva medición.
- La nota de procedencia de las dos plantillas nuevas explica por qué NO se publica su puntuación de SEO: la demo lleva
noindexa propósito —simula un negocio que no existe— y Lighthouse la deja en 66-69 solo poris-crawlable; el resto de sus auditorías de SEO pasan. Publicar un 100 ahí sería falso y publicar el 69 sin contexto, engañoso. El bloque de servicios sí la conserva porque mide Recogras, que sí se indexa. - Verificado sobre
dist/: 18 páginas, títulos y descripciones únicos, canónica autorreferencial (salvo las dos redirecciones, que apuntan a su destino por diseño), hreflang recíproco conx-default, JSON-LD válido con los 49@idcerrados, las ocho FAQ del grafo idénticas a las del acordeón y los seis PNG de OG de plantilla. En navegador contrabun run preview: cero scroll horizontal en 390, 768, 1280, 1366 y 1920, y 100 en accesibilidad, prácticas recomendadas, SEO y navegación agéntica, con 0 auditorías fallidas.
- Las tres automatizaciones ya tienen demo navegable. El tramo de automatizaciones de la bitácora enseñaba las tres piezas con el botón de demo apagado —«demo en preparación»— desde que existe. Ahora cada una enlaza a un panel real: control de acceso en
/automatizacion-control-acceso, motor de reservas en/automatizacion-reservasy alertas de facturación en/automatizacion-alertas-facturacion, con sus gemelas en inglés. Las construye este repositorio en el despliegue, dos veces por pieza —una por idioma—, con el mismo mecanismo que ya usaban las tres plantillas.
- Las demos viven en sus propios repositorios (
control-acceso-smart,gestion-reservas-clases,alertas-facturacion-recurrente) y contienen solo la demo: un panel en Astro que simula el comportamiento del módulo en el navegador. La lógica real de cada servicio se trasladó a repositorios privados, porque es el producto que se vende y no algo que haya que regalar para poder enseñarlo. - Salen con
PUBLIC_DEMO=true, así que llevannoindex, followy no entran en ningún sitemap: simulan negocios que no existen y un buscador podría tomarlas por fichas legítimas. A diferencia de las plantillas, aquí no hay página dedicada que posicione en su lugar —quien lo hace es la propia sección de automatizaciones de/proyectos, que es la que las enlaza—.llms.txtgana la nota equivalente a la que ya tenían las demos de plantilla. - Estas demos sí llevan un framework de JavaScript, a diferencia de las plantillas: Astro 5 con Tailwind 4, shadcn/ui y una isla de React 19 para el panel. Son ~85 KB comprimidos que las plantillas no pagan. Es una decisión deliberada —un panel con estado compartido entre tarjetas, tabla y bitácora es justo lo que React resuelve bien—, pero conviene tenerla presente antes de comparar sus cifras con las de las plantillas.
- El comentario del filtro del sitemap en
astro.config.mjspasa de seis a doce URL de demo y separa las dos familias.
- Ajuste del sitemap generado por Astro para agrupar cada ruta localizada con su homólogo en inglés y emitir alternancias de idioma correctas en el build.
- Actualización de imágenes OG generadas durante el build para reflejar el estado actual de la compilación.
- Systemslab entra en la bitácora. El asiento «01 · Entregado» deja de ser una ficha suelta y pasa a ser una lista: un asiento por sitio en producción, alternando el lado de la captura (Recogras a estribor, Systemslab a babor) para que la sección baje en zigzag como el resto de la página. La ficha del nuevo cliente explica que el sitio se entregó con un panel de administración propio —categorías, productos y las cintas de la portada, con republicación en un clic—, y ese renglón vive aparte del resumen a propósito: la URL del panel no se publica, solo lo que el cliente hace con él.
llms.txty elItemListde datos estructurados ganan su entrada.
- Recogras sube a 100 en las cuatro cifras (rendimiento móvil venía de 98). Se actualizan el bloque de cifras en los dos idiomas, la fecha de la medición —PageSpeed Insights, 5 de agosto de 2026— y su línea en
llms.txt. - El stack se enseña igual en todo el sitio. La sección de proyectos del inicio era la única pieza que lo pintaba como etiquetas de texto mientras el resto usaba placas de logotipo. Ahora las tres vistas comparten
<StackList>(ui/StackList), que ya existía dentro de la bitácora y sube a componente compartido con su propia hoja. Se retiran las copias de.pj-stacky.bt-stack. - Las capturas caen a JPG progresivo en el último eslabón de la cadena de respaldo (AVIF → WebP → JPG), en lugar de PNG: mismo píxel a la vista y una quinta parte del peso para el navegador que no entienda ninguno de los dos primeros. Los PNG salen del repositorio.
- La bitácora enseña el primer pliegue recortado en origen, no la página entera recortada por CSS. Antes servía la captura completa —3,4 MB de alto larguísimo— y tiraba el 90% con un
object-fit; ahora carga un recorte de la cabecera del sitio de menos de 100 kB y lo enseña con su proporción real.
projects.featuredyprojects.testimonialse funden enprojects.delivered[], una lista de sitios entregados con su reseña dentro (opcional: hay clientes contentos que no dejan una pública). El inicio enseña el primero, la bitácora los abre todos y el JSON-LD los recorre: una cifra corregida se corrige en un único sitio.- El registro de tecnologías (
data/tech.ts) ganamysql,phpyshadcn. - Las clases de
<StackList>llevan prefijotechstack-y nostack-:.stackya existe enstyles/layout.csscomo utilidad de apilado vertical y, al no declarar la hoja nueva su propioflex-direction, ganaba la de allí y las placas salían en columna.
- El sol vuelve a animarse sin JavaScript. La coreografía del atardecer —el descenso, el giro del sextante y la formación del resplandor— está escrita para correr en el compositor con
animation-timeline: scroll(), pero llevaba tiempo muerta en producción: el minificador plegabaanimation-timelinedentro del atajo y emitíaanimation: linear both ss-sun-descent scroll(root), que ningún navegador acepta —el timeline se sacó del atajo en la especificación—, así que el navegador descartaba la declaración entera y toda esa vía quedaba inerte. Todo el movimiento lo sostenía elrequestAnimationFramede respaldo, en el hilo principal. Declarado en longhands, el minificador ya no puede plegarlo. - El sextante deja de quedar debajo de la barra de navegación. El arco graduado sobresale un 6% por encima del disco, y el astro arrancaba tan pegado al tope que la barra le cortaba la corona de marcas: 35 px en escritorio, 23 px en móvil.
- El sol arranca más abajo (de
6vha12vh), lo justo para que el limbo del sextante despeje la barra sin que el núcleo se meta en el texto del hero. - En el teléfono el astro se encoge de 8,5rem a 6rem. Entre la barra (77 px) y el eyebrow (174 px) hay 97 px y el conjunto sol+sextante medía 152 px: no cabía, y de ahí venía el recorte de origen. Encogerlo es la única forma de que quepan los dos despejes —quedan ~10 px por lado—; en escritorio conserva su tamaño, porque allí vive a estribor y lejos del texto.
- El halo se aviva con el descenso también sin JavaScript. Era la única pieza que faltaba en el bloque nativo, y por eso el respaldo seguía siendo imprescindible aunque el resto funcionara.
- El respaldo en
requestAnimationFramesolo arranca donde no hayanimation-timeline. Antes se ejecutaba siempre, también donde la vía nativa ya estaba animando: trabajo tirado a la basura para la gran mayoría del tráfico. - El respaldo mide el recorrido contra
85vh, el mismoanimation-rangeque el CSS. Antes normalizaba contra la altura del cielo (~704 px frente a ~612 px en un viewport de 720): con una sola vía viva la diferencia no se veía, con las dos la misma página caería a distinto ritmo según el navegador. Medido tras el cambio, a 22vh: 0,258 en el respaldo y 0,259 en el nativo. - Desaparece el
getBoundingClientRect()por fotograma del respaldo. Al no depender ya de la altura del cielo, no hay nada que medir dentro del bucle. .ss-haloentra en el bloque deprefers-reduced-motioncon opacidad propia: sin ella la postal estática se quedaba con el valor del sol alto y salía apagada, con el astro ya posado en el horizonte.- Verificado en navegador: siete animaciones sobre
ScrollTimeliney dos por tiempo, el respaldo sin escribir--sundonde hay soporte nativo, y el CSS compilado sin un solo atajo plegado.
- Las capturas del escaparate se recortan a la franja que el paseo llega a enseñar. El
<ScrollShot>traslada la imagen dentro de su marco hasta un tope (--max-travel), así que del pie de una captura de página completa no se ve un solo píxel ni en la pantalla más alta: se estaban sirviendo megas por nada. La de escritorio pasa de 1905×7368 a 1905×4789 y la móvil, de 850×16384 a 480×4789 —el mismo alto exacto que la de escritorio, y con el ancho ajustado además, porque el teléfono nunca se pinta a más de 172 px y llegaban 850—. En AVIF, que es lo que recibe casi todo el tráfico, las dos juntas bajan de 1594 KiB a 260 KiB.
- El recorte se calculó contra el recorrido real, no a ojo: marco (
--frame-h) más--max-travel, medido en los anchos que se prueban, y con margen de sobra por encima. El paseo se conserva entero en escritorio —a 1920 quedan 1689 px de imagen bajo el marco para 918 de recorrido— y en el marco del teléfono en cualquier ancho. El único punto donde se acorta es el marco de escritorio en viewport de móvil, que hace el 72% del recorrido: es justo donde ese marco va detrás del teléfono y donde los megas más pesan. min(100% - var(--frame-h), var(--max-travel))ata el recorrido a lo que quede de imagen bajo el marco, de modo que recortar de más no atasca el paseo: lo acorta en silencio. Queda anotado junto a la regla, para quien vuelva a tocar las capturas.- Los tres formatos se reencodan desde el original —AVIF q52, WebP q78 y JPG progresivo con mozjpeg q74—, no se recortan los ficheros ya optimizados.
- El formulario de contacto ofrece salida por WhatsApp. En la misma fila del botón de enviar, al otro extremo y alineado con el borde derecho del formulario, «¿Algo más directo?» y un enlace que abre el chat con el motivo ya redactado en el idioma de la página: quien pulsa solo tiene que darle a enviar. Va ahí y no escondido entre los canales del pie porque es donde tiene sentido: si rellenar tres campos da pereza, la alternativa tiene que estar donde se está pidiendo el esfuerzo.
- El botón no trae estética propia: reusa
.ft-surface, la misma pieza que «volver a la superficie», con un punto más de cuerpo y de aire —46 px de alto contra los 55 del CTA— para que levante la vista sin hacerle sombra al envío. La marca va escrita tal cual, «WhatsApp»: en versalitas se lee como una etiqueta cualquiera y deja de reconocerse de un vistazo. Como el rótulo visible es solo la marca, el nombre accesible lo ponewhatsappAria, que la contiene (WCAG 2.5.3). - El número de atención pasa a
siteConfig.whatsapp, en E.164 sin el «+» (el formato que exigewa.me). De ahí beben el atajo del formulario, el canal del pie y eltelephonedel JSON-LD, que lo publica con el «+». Estaba escrito a mano en tres sitios. - El enlace vive dentro del
<form>, así que al enviarse con éxito se oculta con él. Quien ya escribió no necesita el atajo, y el WhatsApp de la columna de canales sigue ahí. - Contraste sobre la arena de noche: 5,4:1 en reposo y 5,5:1 en ámbar al pasar. Verificado en los dos idiomas y en 390, 600, 768, 900 y 1280: el borde derecho del botón cuadra con el del formulario hasta que la fila se parte, y en móvil el atajo baja a su propia línea y vuelve al margen izquierdo.
- La 404 deja de generar su propia tarjeta social. Nunca se llegó a ver ni se verá: quien comparte un enlace roto hace que el rastreador pida esa URL, reciba un 404 y ahí no se monta previsualización —ni en WhatsApp, ni en Facebook, ni en X—. La única forma de que se pintara sería que el hosting devolviera 200 en la página de error, que es un fallo de configuración, no un caso a soportar. Se retiran el builder y los dos PNG de 275 kB que se rehacían con puppeteer en cada
prebuildpara no enseñarse jamás.
siteConfig.fallbackOgImageempieza a usarse: estaba declarado desde que existen las tarjetas por página, con su comentario explicando que es el respaldo de las vistas sin tarjeta propia, pero no lo leía nadie. Ahora elLayoutlo sirve a las páginas deOG_LESS_PAGES—hoy solo la 404— en vez de apuntar a un PNG que ya no existe.
- El sitio sale de
0.x.0.y.zsignifica, literalmente, «desarrollo inicial: cualquier cosa puede cambiar en cualquier momento». Hace tiempo que eso es falso —el sitio está en línea, indexado, recibiendo tráfico y enseñando trabajo de clientes reales— y además dejaba el proyecto sin escalón MAJOR: añadir una página a la bitácora y rehacer la identidad visual entera se anotaban en el mismo dígito. Esta versión no toca una sola línea de la interfaz: marca que lo publicado es la versión estable y devuelve los tres niveles a su sitio.
- De aquí en adelante, la «API pública» de un sitio son sus URL y lo que un visitante que vuelve da por hecho:
- MAJOR — se rompe la continuidad hacia fuera: cambian slugs o rutas, desaparecen páginas, cambia la identidad visual o cambia lo que el sitio ofrece.
- MINOR — se añade sin romper: una página, una sección, una plantilla, un proyecto entrando en la bitácora.
- PATCH — correcciones, textos, rendimiento, accesibilidad y refactores invisibles desde fuera.
- Es el mismo criterio que se venía aplicando, corrido un dígito:
0.33.0(Systemslab entra en la bitácora) habría sido1.1.0, y0.33.2(el sol vuelve a animarse sin JavaScript),1.0.1. - El número no sale a ninguna parte: no lo lee nada de
src/, no aparece en el HTML compilado ni enllms.txtni en los datos estructurados. Vive entrepackage.jsony este archivo, así que el salto no tiene efecto hacia fuera.