@clauzier/astro-kit (0.15.2)

Published 2026-08-30 04:45:54 +00:00 by 0x346e3730

Installation

@clauzier:registry=
npm install @clauzier/astro-kit@0.15.2
"@clauzier/astro-kit": "0.15.2"

About this package

@clauzier/astro-kit

Couche front partagée de la plateforme multi-clients : client typé vers cms-core, couche contenu, réglages et libellés, gabarit de site, composants Astro, styles socle et modules verticaux opt-in (esport, twitch).

Modules

Entrypoint Contenu
client createCmsClient : client typé minimal vers l'API cms-core (dont getSiteSettings)
entities createContentSource, createSiteSettings, normalisation, libellés de schémas
labels createLabels, interpolate : textes d'interface surchargeables depuis les réglages
layout buildSiteHead, pageTitle, footerLine : tête de page depuis les réglages
pages cmsPagePaths, cmsPageSeo, renderCmsPage, notFoundSeo : route fourre-tout et 404
blocks Registre de renderers + rendu HTML sûr (escapeHtml, sanitizeHtml, safeUrl)
seo Balises meta/OG, URLs absolues, robotsTxt
esport Opt-in. withEsport, createEsportBlockRegistry, helpers de fiche équipe
twitch Opt-in. renderWebTv, createWebTvRenderer, statut Twitch ; twitch/island (îlot)
members Opt-in. Espace membres : session par cookie, routes d'auth, page réservée ; members/island
components/* Composants Astro : SiteLayout, Header, Footer, NotFound, TeamHero, …
styles/* base.css (socle), esport.css, twitch.css, members.css : tout en tokens

Le barrel racine et ./entities/./blocks ne contiennent rien d'esport, de twitch ni de members : un site qui n'active pas ces presets n'en embarque rien.

Couche contenu

Une coquille cliente ne fournit que sa configuration et ses fixtures ; lecture API, normalisation snake_case → camelCase, tris et garde anti-repli viennent du paquet.

import { createContentSource, createSiteSettings } from '@clauzier/astro-kit/entities';
import { withEsport } from '@clauzier/astro-kit/esport';

const source = withEsport(
  createContentSource({
    baseUrl: import.meta.env.CMS_BASE_URL,
    fixtures: { site: createSiteSettings({ name: 'Mon club' }), pages, staff, socials, menus },
    avatarColors: ['#1226aa', '#e63946'],
  }),
  { teams, players, matches, achievements },
);

export const { getSiteSettings, getLabels, listPageEntries, listTeams, listPalmares } = source;

Sans baseUrl, les fixtures sont servies (CI, previews) et fixtures.site est obligatoire. Avec une API configurée, toute erreur de lecture fait échouer le build — y compris une fiche Site absente (« créez et publiez la fiche Site en back-office ») : un repli silencieux publierait un site plausible mais faux.

Les libellés de schéma (rôles, plateformes…) viennent de /api/content-schemas via schemaLabels() — ils ne sont jamais recopiés côté front.

Réglages et libellés

GET /api/site sert la fiche singleton site : identité, contact, SEO et surcharges de textes. getSiteSettings() la normalise (SiteSettings, champs absents à null, champs hors contrat dans extra, ex. twitch_channel) et la lit une fois par build.

Chaque texte humain a une clé de libellé, avec un défaut dans le module qui le rend et une surcharge possible depuis les réglages :

const labels = await getLabels(); // surcharges API > défauts TS > clé
labels.t('footer.copyright', { year: 2030, name: 'Mon club' }); // « © 2030 Mon club »
labels.t('header.menu'); // « Menu »

t() rend du texte brut, toujours échappé à l'affichage : une surcharge <b>…</b> s'affiche telle quelle. Les défauts génériques vivent dans labels/defaults.ts, ceux des modules dans esport/labels.ts et twitch/labels.ts ; withEsport étend getLabels() avec les siens, et chaque renderer garantit les siens quoi qu'on lui passe. La liste des clés est figée par tests/labelsCatalog.test.ts, miroir du LabelCatalog de cms-core : ajouter une clé se fait des deux côtés, jamais dans une coquille.

Les libellés s'injectent par closure : createCoreBlockRegistry(source, { labels }), createEsportBlockRegistry(source, { labels }), createWebTvRenderer({ labels }), createAwesomeFormsRenderer({ labels }), prop labels des composants, PartContext.labels des renderers de fragments.

Registres de briques : défauts + core + modules

Une coquille compose son registre par familles : les briques par défaut (heading, paragraph, hero, section, image, form), celles du preset core (staff_grid, socials_grid, depuis listStaff/listClubSocials), puis les modules qu'elle active.

staff_grid groupe les fiches par section quand le schéma en déclare, dans l'ordre du contrat et sous les libellés qu'il porte ; sans section déclarée la grille reste plate. teams_grid fait de même avec le champ « Jeu » d'une équipe, et affiche le libellé du contrat plutôt que la valeur stockée.

Aucun chemin du site n'est supposé par le socle : la coquille fournit sa base d'URL d'équipe et ses liens de repli, et sans lien il n'y a pas de bouton.

import { createCoreBlockRegistry, createDefaultBlockRegistry } from '@clauzier/astro-kit/blocks';
import { createEsportBlockRegistry } from '@clauzier/astro-kit/esport';
import { createWebTvRenderer } from '@clauzier/astro-kit/twitch';

const registry = createDefaultBlockRegistry({ layout: 'section', form });

createCoreBlockRegistry(source, { registry, labels });

createEsportBlockRegistry(source, {
  registry,
  labels,
  clubName: settings.name,
  teamBase: '/sections/',
  links: { calendar: '/calendrier/', tv: '/direct/' },
});

registry.register(
  createWebTvRenderer({
    apiBaseUrl: source.apiBaseUrl,
    channel: String(settings.extra.twitch_channel ?? ''),
    labels,
  }),
);

Briques esport : teams_grid, matches (mode: results, display: rows pour la liste détaillée plutôt que la bande de pastilles, filter_by_game), palmares (mode: tournaments, group_by_year, filter_by_game), next_match. Le filtre par jeu se passe de JavaScript et reste scopé à l'instance du bloc ; sur les matchs, la puce emprunte son logo à l'équipe qui joue le jeu, un match n'en portant pas. Fiche équipe : splitRoster(roster, staffRoles) et teamNote(team, staff, labels). L'encadrement se reconnaît à la case « Encadrement » de la fiche joueur ; staffRoles (['coach'] par défaut, comparé à la casse et aux espaces près) ne sert que de repli pour les fiches antérieures à cette case.

Les trois visuels d'une équipe ont chacun leur place : banner en fond de carte et de bandeau, logo en pastille, game_logo en filigrane de carte — et en pastille de repli tant qu'aucun logo d'équipe n'est renseigné.

Bloc webtv : coquille statique (jamais de faux LIVE), hydratée côté client par import '@clauzier/astro-kit/twitch/island' dans le gabarit — l'îlot lit ses textes en data-label-* et hydrate aussi les comptes à rebours. Sans canal ni origine d'API, seule la coquille est rendue. La pastille flottante est twitch/components/LivePill.astro. Les feuilles esport.css et twitch.css sont importées explicitement par la coquille ; twitch.css attend le token --live.

Gabarit de site et pages CMS

SiteLayout.astro rend la tête SEO (titre suffixé, favicon, OG de repli), l'en-tête (logo et CTA des réglages), le lien d'évitement, <main id="contenu"> et le pied de page (« © année nom · mention légale », ou footer_text). Il n'importe aucune feuille de style : la coquille charge sa palette puis base.css dans son propre gabarit.

---
import '@/styles/theme.css';
import '@clauzier/astro-kit/styles/base.css';
import SiteLayout from '@clauzier/astro-kit/components/SiteLayout.astro';
import { getLabels, getSiteSettings, listClubSocials, listMenu } from '@/lib/content.js';

const [settings, labels, header, footer, socials] = await Promise.all([
  getSiteSettings(),
  getLabels(),
  listMenu('header'),
  listMenu('footer'),
  listClubSocials(),
]);
---

<SiteLayout
  {settings}
  {labels}
  {socials}
  menus={{ header, footer }}
  brandSplit={3}
  {...Astro.props}
>
  <slot />
</SiteLayout>

Route fourre-tout des pages composées en back-office, en une quinzaine de lignes :

---
import { cmsPagePaths, cmsPageSeo, renderCmsPage } from '@clauzier/astro-kit/pages';

export async function getStaticPaths() {
  return cmsPagePaths({ source, homeSlug: 'accueil', reserved: ['404', 'sections'] });
}

const { entry } = Astro.props;
const seo = cmsPageSeo(entry);
const html = await renderCmsPage(entry, { registry, path: Astro.url.pathname });
---

<Base title={seo.title} description={seo.description} ogImage={seo.ogImage}>
  <Fragment set:html={html} />
</Base>

reserved accepte des préfixes ou un prédicat asynchrone ; un slug réservé ou un accueil absent (avec API) cassent le build avec un message explicite. NotFound.astro (settings, labels?, links?) et notFoundSeo() couvrent la 404 ; robotsTxt({ siteUrl }) sert robots.txt.

Espace membres (opt-in)

Le module members rend les pages réservées à la requête : le site passe en hybride (@astrojs/node, output: 'static'), les pages publiques restent pré-rendues et le serveur Node ne sert que l'espace membres. La session est un cookie httpOnly (cms_member) qui porte le token Sanctum émis par cms-core ; aucun store côté site, un rebuild ne déconnecte personne.

Côté coquille, tout tient dans un module et quatre fichiers de route :

// src/lib/members.ts
import { createMemberRoutes, createMembersClient } from '@clauzier/astro-kit/members';

// import.meta.env est figé au build : l'espace membres lit l'API à la requête.
const baseUrl = process.env.CMS_BASE_URL ?? import.meta.env.CMS_BASE_URL;

export const membersClient = createMembersClient({ baseUrl });
export const memberRoutes = createMemberRoutes({ client: membersClient, accountPath: '/compte/' });
// src/pages/session/login.ts — idem callback.ts (GET), logout.ts (POST), me.ts (GET)
export const prerender = false;
export const GET = memberRoutes.login;

prerender = false doit être écrit littéralement dans chaque fichier de route : Astro le détecte par lecture du source, pas via un réexport. Les chemins (/compte/, /session/login…) appartiennent à la coquille ; le socle ne les connaît que par ses props. Le préfixe des pages réservées et celui des routes d'auth s'ajoutent au reserved de cmsPagePaths, qui ignore de toute façon les pages visibility: 'members'.

Parcours : login pose un nonce (cookie sous le dossier des routes) et part vers /api/auth/discord/redirect ; l'API revient sur callback avec un code à usage unique, échangé contre le token seulement si le nonce correspond ; logout (POST uniquement) révoque côté API et efface le cookie ; me rend { user } en JSON, jamais mis en cache, sans appeler l'API quand il n'y a pas de cookie. safeReturnTo n'accepte qu'un chemin relatif au site.

Page réservée :

---
export const prerender = false;
import { loadMemberPage, memberHeaders } from '@clauzier/astro-kit/members';
import MemberGate from '@clauzier/astro-kit/members/components/MemberGate.astro';
import { passthroughResolver } from '@clauzier/astro-kit/blocks';

memberHeaders(Astro.response.headers);
const result = await loadMemberPage(Astro, { client: membersClient, slug });
if (result.state === 'missing') return new Response(null, { status: 404 });
if (result.state === 'forbidden') Astro.response.status = 403;
const html = result.state === 'ok' ? await renderCmsPage(result.entry, { registry, path }) : '';
---

MemberGate (state: anonymous | forbidden | unavailable, loginHref?) rend l'état bloquant ; sans CMS_BASE_URL (fixtures) le client n'appelle rien et la coquille rend unavailable. Les médias d'une page réservée passent par passthroughResolver (media du registre) : pas d'optimisation à la requête. MemberAccount (user, roles du catalogue via memberSettings(settings), inviteUrl, logoutAction) rend la page de compte, avec l'invitation Discord quand guildMember === false.

En-tête : SiteLayout expose le slot header-end ; la coquille y place MemberWidget (loginHref, accountHref, logoutAction, meHref), rendu anonyme puis mis à jour par import '@clauzier/astro-kit/members/island'. La feuille members.css s'importe après base.css. Le cookie cms_member_ui (nom, avatar ; lisible par le navigateur, sans secret) évite le flash de « Se connecter » : le widget le rend avant toute requête, puis me confirme.

Derrière un proxy, poser security.allowedDomains dans astro.config.mjs (depuis SITE_URL) : sans lui Astro.url reste en http://localhost et le contrôle d'origine refuse le POST de déconnexion. Le drapeau Secure des cookies se déduit d'Astro.site, jamais d'Astro.url.

Composants et styles

Le Header ne charge aucun script : le burger mobile et les sous-menus sont des <details>/<summary>, dont l'état d'ouverture est exposé nativement aux technologies d'assistance. Au-dessus de 900 px, [open] est neutralisé par la feuille socle et les sous-menus reviennent au survol. L'entrée parente d'un sous-menu n'est plus un lien — ajouter un « Tout voir » en tête de ses enfants si elle porte une page propre.

base.css porte le reset global du document et n'écrit aucune couleur en dur : tout passe par des tokens (--bg, --panel, --accent, --muted, --font-display…) que la coquille définit dans sa propre feuille de palette (tokens.css en donne le contrat et un point de départ neutre).

Développement

bun install
bun run lint && bun run format:check && bun run typecheck && bun run test

Dependencies

Dependencies

ID Version
sanitize-html ^2.17.7

Development dependencies

ID Version
@eslint/js ^10.0.0
@types/node ^26.0.0
@types/sanitize-html ^2.16.1
astro ^7.2.4
eslint ^10.0.0
eslint-plugin-astro ^1.3.0
jsdom ^30.0.1
prettier ^3.9.0
prettier-plugin-astro ^0.14.0
typescript ~5.9.0
typescript-eslint ^8.40.0
vitest ^4.0.0

Peer dependencies

ID Version
astro ^7.0.0
Details
npm
2026-08-30 04:45:54 +00:00
75
UNLICENSED
73 KiB
Assets (1)
Versions (30) View all
0.26.0 2026-09-09
0.25.0 2026-09-07
0.24.0 2026-09-07
0.23.0 2026-09-05
0.22.0 2026-09-04