npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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, Base y Seo.
  • Lo que el kit garantiza no es un marcado sino unas reglas, y sumaq-check-site las 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 (h1h4, 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, mediaUrl y compañía.
  • Behaviours genéricos en scripts/behaviours/: header-scroll, nav, reveal, scrollspy, to-top. Base los ejecuta todos salvo que behaviours diga otra cosa. reveal solo observa lo que el sitio marca, con .reveal o data-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 bloques

Una 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:

  1. no tiene <html lang>;
  2. no tiene exactamente un <h1>;
  3. tiene un <img> sin atributo alt (vacío vale para una imagen decorativa);
  4. tiene un src/srcset que apunta a /media/, es decir, una clave del CMS sin resolver;
  5. no tiene <link rel="canonical">; o el sitio tiene varios idiomas y la página no declara hreflang, o declara uno que apunta a una página que no existe en dist/;
  6. tiene <p>, <li>, <ul>, <ol>, <dl>, <h1><h6> o <section> vacíos;
  7. tiene un <section> sin aria-labelledby, sin aria-label y 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 en BlockRenderer, Card y los perfiles profiles/*.yaml;
  • getTeamMembers y getShowcaseCards (quedan loadEntries y applyOrder);
  • los behaviours de un solo sitio: gallery-swap, room-gallery, room-transition, team-filter y lightbox;
  • el contrato de clases: class-inventory.json, block-props.json, sumaq-check-blocks y la opción checkLocalBlocks, que ahora solo avisa;
  • reveal deja de animar las tarjetas sq-* de los bloques; lo que deba aparecer lleva .reveal o data-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 + props

Para 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.