@sumaq/site-kit
v1.1.0
Published
Base layout, atoms, helpers and Astro config factory for Sumaq client sites.
Readme
@sumaq/site-kit
Layout base, átomos, utilidades y configuración de Astro para los sitios de Sumaq.
pnpm add @sumaq/[email protected]Público en npmjs.com. Cada sitio www-* fija una
versión exacta, nunca un rango.
El principio
Cada sitio es dueño de su diseño y de sus secciones; el kit comparte reglas y átomos.
- El sitio escribe sus secciones en
src/sections/con sus propias clases y su CSS. Un sitio entregado no cambia de diseño: no hay un catálogo común que, al actualizarse, mueva el marcado de todos a la vez. - El kit aporta lo que tiene lógica y hace falta en más de un sitio: la regla de vacío, enlaces
tel:/mailto:, imágenes del CMS, accesibilidad y comportamiento en el navegador. Además,BaseySeo. - Lo que el kit garantiza no es un marcado sino unas reglas, y
sumaq-check-sitelas comprueba en el HTML construido.
La decisión y su porqué: plan-kit-atomos.md.
Reglas comunes
| # | Regla | Verificación |
|---|---|---|
| R1 | Todo texto visible viene de content/*.json según el schema | revisión |
| R2 | Campo vacío → no se emite nada | átomos + sumaq-check-site (elementos vacíos) |
| R3 | Imágenes del CMS siempre por Media | validateSiteContent + sumaq-check-site (sin /media/ crudo en dist/) |
| R4 | Accesibilidad mínima: lang, un h1, alt, secciones etiquetadas | sumaq-check-site |
| R5 | SEO: canonical; hreflang si hay varios idiomas | sumaq-check-site |
| R6 | Un átomo del kit no se copia: se extiende por props o slot, o se envuelve | revisión |
| R7 | Clases, CSS y estructura de secciones son del sitio | libre |
Criterio de admisión de un átomo: lo necesitan al menos dos sitios reales y lleva lógica (regla de vacío, enlaces, imágenes, accesibilidad, JS). Lo demás vive en el sitio. Si un segundo sitio necesita algo que otro ya escribió, se propone subirlo al kit.
Catálogo
Layout y chrome
| Componente | Qué es |
|---|---|
| Base | <html> con SEO, cabecera, pie, skip link, back-to-top opcional, slot head para fuentes, el tracker de Ñahui cuando hay PUBLIC_NAHUI_SITE, hreflang en sitios multiidioma (ver abajo), y siempre scripts/lqip.js |
| Seo | título, descripción, canonical, Open Graph, Twitter, JSON-LD opcional |
| SiteHeader / SiteFooter / Logo | el chrome que Base pinta a partir de header / footer del contenido; slots para extenderlo sin sustituirlo (ver la cabecera de Base.astro) |
| ScrollToTop | control flotante de vuelta arriba |
Estructura de sección
| Componente | Qué es |
|---|---|
| Section | <section class="sq-section sq-{name}"> + contenedor; no emite nada si el cuerpo queda vacío. base={false} para aperturas a sangre, container="none", as para <header>/<aside> |
| SectionHeader / SectionHeading | etiqueta, título (+ acento), entradilla y nota, con y sin <header>. SectionHeader acepta un slot tras el encabezado |
Átomos
Reglas para todos: una sola raíz. class y cualquier otro atributo van a la raíz. Sin contenido
no emiten nada. cms es opcional y solo escribe data-cms, que hoy no consume nada. Donde hay una
clase sq-* de gancho, class se añade a ella, no la sustituye.
| Átomo | Props | HTML |
|---|---|---|
| Text | text, as (p·span·div·small·strong, def. p), class, cms | <p class>text</p> |
| Heading | title, accent, as (h1–h4, def. h2), id, class, accentClass, cms, accentCms; slot por defecto tras el acento | <h2 class>title<span class="sq-accent">accent</span></h2> |
| Button | cta o label/href/variant/external; class, primaryClass (def. sq-btn--primary), secondaryClass (def. sq-btn--ghost), cms | <a class="sq-btn …">, con target/rel si external |
| Actions | ctas, class (def. sq-section__actions), primaryClass, secondaryClass, buttonClass, primaryFirst, cms | <div class> con un Button por CTA |
| DataList | items: {label?, value?, href?, type?: "email"\|"phone"\|"url", external?}[], as (dl·ul), class, itemClass, labelClass, valueClass, cms | dl>div>dt+dd o ul>li>span+span |
| TagList | items: (string\|{name?}\|{text?})[], as (ul·div), class, tagClass, cms, field (def. name) | ul>li.sq-tag o div>span.sq-tag |
| ContactLinks | email, phone, website, websiteLabel, as (ul·none), class, cms | ul>li>a, o <a> hermanos con as="none" |
| Media | ver Imágenes | <img> |
| Prose | markdown o HTML en sq-prose | <div class="sq-prose"> |
Las props exactas de cada componente, derivadas del código, están en
@sumaq/site-kit/component-props.json (pnpm props). Es lo que lee la skill del generador antes de
componer una sección.
Despachador
BlockRenderer convierte los bloques de una página del CMS en las secciones del sitio. No trae
catálogo propio: el mapa components es el único registro, y un type que no está en él para el build.
<BlockRenderer blocks={data.blocks} components={{ hero: Hero, services: Services }} />Utilidades y behaviours
loadEntries/applyOrder(colecciones),telHref,md,hasText/list/hasItems/hasImage/hasAny/cmsKey,mediaUrly compañía.- Behaviours genéricos en
scripts/behaviours/:header-scroll,nav,reveal,scrollspy,to-top.Baselos ejecuta todos salvo quebehavioursdiga otra cosa.revealsolo observa lo que el sitio marca, con.revealodata-reveal.
Secciones del sitio: src/sections/ y src/ui/
src/
sections/ Hero.astro, Services.astro, Contact.astro … — una por `type` del schema
ui/ envoltorios opcionales que fijan las clases del sitio sobre un átomo
pages/ componen secciones; BlockRenderer donde la página sea una lista de bloquesUna sección es Section + átomos + el marcado propio que el diseño pida:
---
import { Section, Text, Heading, Actions, DataList, hasText } from "@sumaq/site-kit";
const { eyebrow, title, titleAccent, sectionSubtitle, ctas, stats } = Astro.props;
---
<Section name="hero" base={false} container="none" labelledby={hasText(title) ? "hero-title" : undefined}>
<div class="sq-container sq-hero__content">
<Text class="sq-eyebrow" text={eyebrow} />
<Heading as="h1" id="hero-title" class="sq-hero__title" title={title} accent={titleAccent} />
<Text class="sq-hero__lede" text={sectionSubtitle} />
<Actions ctas={ctas} class="sq-hero__actions" />
<DataList as="dl" class="sq-hero__stats" items={stats} />
</div>
</Section>src/ui/ es para cuando el sitio repite la misma combinación de clases sobre un átomo, por ejemplo
un SiteButton.astro que envuelve Button con class="btn btn--pill". Es opcional. Lo que no se
hace es copiar el átomo (R6): se envuelve o se extiende por props.
Nada obliga a usar el prefijo sq- en las clases propias. El kit ya no mantiene un inventario de
clases: las sq-* que emiten Section, el chrome y los ganchos de los átomos son las únicas del kit.
sumaq-check-site
Recorre dist/**/*.html y falla, en español, si alguna página:
- no tiene
<html lang>; - no tiene exactamente un
<h1>; - tiene un
<img>sin atributoalt(vacío vale para una imagen decorativa); - tiene un
src/srcsetque apunta a/media/, es decir, una clave del CMS sin resolver; - no tiene
<link rel="canonical">; o el sitio tiene varios idiomas y la página no declarahreflang, o declara uno que apunta a una página que no existe endist/; - tiene
<p>,<li>,<ul>,<ol>,<dl>,<h1>–<h6>o<section>vacíos; - tiene un
<section>sinaria-labelledby, sinaria-labely sin encabezado dentro.
defineSumaqSite lo ejecuta al terminar el build (checkSite, activo por defecto). A mano:
pnpm exec sumaq-check-site dist [--multilang]Varios idiomas: hreflang
Con i18n en defineSumaqSite y más de un idioma, Base emite un
<link rel="alternate" hreflang> por cada versión de la página que existe, y x-default hacia
la del idioma por defecto. «Existe» se decide por el fichero: la versión en de /team/[slug] es
src/pages/en/team/[slug].astro. En rutas dinámicas eso no garantiza cada entrada traducida, y
sumaq-check-site falla si un hreflang apunta a una página que no está en dist/.
<Base alternates={false}> no emite ninguno, y alternates={[{ hreflang, href }]} sustituye al
cálculo.
Imágenes
Un sitio guarda sus imágenes en ./media/, en la raíz del repo, junto a schema/ y content/,
no dentro de public/. El CMS publica ahí y Astro las procesa: hash de contenido en el nombre, un
srcset con varios anchos y una URL dist/_astro/… que cambia cuando cambian los bytes.
Por eso la cadena de content/*.json es una clave, no una URL:
"image": { "alt": "Retrato", "src": "/media/portrait-a3f9c1d2.webp" }Nada sirve /media/portrait-a3f9c1d2.webp. Media busca la clave en un glob de build y pinta el
asset que Astro emitió. Interpolar esa cadena en un src= a mano da un 404 mudo, y
sumaq-check-site lo detecta. Usa <Media>, o mediaUrl() donde solo cabe una cadena (un
og:image, un data- que lee un script).
| src | Qué pasa |
|---|---|
| /media/x.webp presente en ./media/ | <Image> con srcset a 640/960/1280/1600/1920 (limitado al original) y el LQIP del manifiesto como fondo |
| /media/x.svg | <img> con la URL emitida |
| https://… o cualquier ruta de public/ | <img> tal cual |
| un import ImageMetadata | <Image> |
| vacío | nada, salvo que se pase fallback |
media/manifest.json, que escribe el CMS, lleva { alt, width, height, lqip, alpha } por clave.
El LQIP se pinta como fondo del propio <img> y se quita en cuanto carga la imagen real: si se
quedara, asomaría por los píxeles transparentes o por los bordes cuando la caja CSS y el fichero no
tienen la misma proporción. Una imagen marcada alpha: true no lleva LQIP. La limpieza está en
scripts/lqip.js, que Base carga siempre. Un sitio con su propio layout en lugar de Base tiene que
importarla él:
<script>import "@sumaq/site-kit/scripts/lqip.js";</script>Media tiene una sola raíz: todo atributo que no sea una prop suya (data-*, aria-*,
transition:name) llega al <img>.
<Media fallback /> pinta un marcador de posición en vez de nada cuando src está vacío. Es opcional
a propósito: en una apertura queda mejor un hueco que una caja gris. Un sitio puede usar el suyo
dejando media/placeholder.webp en el repo.
Los PDF y demás adjuntos van a public/files/ y se enlazan como /files/tarifas-a3f9c1d2.pdf:
se copian tal cual y su URL no cambia.
Migrar desde 0.x
Los sitios en 0.x siguen fijados en su versión: un sitio entregado no se migra (diseño congelado). 1.0 es para sitios nuevos. Lo que desaparece respecto a 0.14:
- los bloques de
blocks/*y su catálogo enBlockRenderer,Cardy los perfilesprofiles/*.yaml; getTeamMembersygetShowcaseCards(quedanloadEntriesyapplyOrder);- los behaviours de un solo sitio:
gallery-swap,room-gallery,room-transition,team-filterylightbox; - el contrato de clases:
class-inventory.json,block-props.json,sumaq-check-blocksy la opcióncheckLocalBlocks, que ahora solo avisa; revealdeja de animar las tarjetassq-*de los bloques; lo que deba aparecer lleva.revealodata-reveal.
Si un sitio 0.x tuviera que pasar a 1.0 algún día, cada bloque que use se convierte en una sección de
src/sections/. El HTML de la 0.13 es el punto de partida, y los átomos lo reproducen: la plantilla
lo hace con Hero, Services y Contact.
Trabajar en el kit
pnpm typecheck # tsc sobre la parte TS
pnpm props # regenera src/tokens/component-props.json
pnpm test # typecheck + propsPara probarlo en un sitio sin publicar: scripts/link-into.sh ../sites/www-foo --only site-kit. Al
terminar, devuelve el sitio a una versión exacta publicada.
