SPEC — Système de Sidebar Bimodale Starlight & Indexation Algolia Séparée
SPEC — Système de Sidebar Bimodale Starlight & Indexation Algolia Séparée
Spécification technique détaillée de la bimodalité public/membre pour la navigation et la recherche Starlight
Cycle : DOC → SPEC → CODE → TEST → INTÉG
Composant : Sidebar Bimodale Starlight & Recherche Algolia Séparée
Fichier :docs/L2_Presentation/astro/starlight/SPEC-Starlight-Dual-Sidebar-Algolia.md
Statut : SPEC
Date : 2026-10-12
1. Périmètre et Objectifs
1.1. Objectifs1/2
- Fournir une navigation Starlight fluide et accueillante pour les visiteurs non authentifiés, affichant exclusivement les documents publics sans aucun lien verrouillé ni redirection intempestive.
- Fournir aux membres authentifiés via Better Auth un accès complet à l’ensemble de la documentation (publique et privée) avec un indicateur visuel clair (badge “Privé”).
1.1. Objectifs2/2
- Assainir la pagination bas de page (
prev/next) pour éviter toute navigation accidentelle vers un document privé pour un utilisateur non connecté. - Rediriger la route racine
/docsvers la page de documentation publique principale au lieu d’un code 404. - Séparer physiquement les index de recherche Algolia DocSearch en deux index distincts (
publicetall) afin d’interdire toute fuite de contenu sensible via les clés de recherche publiques.
1.2. Contraintes Architecturales
- Utiliser l’API officielle Starlight
routeMiddlewareviadefineRouteMiddleware(@astrojs/starlight/route-data). - Ne pas modifier ni écraser les composants internes Starlight (
Sidebar.astro), afin de garantir la pérennité lors des mises à jour du framework. - Maintenir le principe de sécurité par défaut : tout document dont la visibilité n’est pas explicitement
'public'est considéré'private'. - Zéro fuite de données : aucun titre, slug ou extrait privé dans l’index Algolia public.
2. Spécification des Types et Contrats de Données
2.1. Types Starlight Route Data1/2
D’après la documentation officielle Starlight (@astrojs/starlight/route-data) :
2.1. Types Starlight Route Data2/2
export interface Badge { text: string; variant: 'note' | 'danger' | 'success' | 'caution' | 'tip' | 'default'; class?: string;}
export interface SidebarLink { type: 'link'; label: string; href: string; isCurrent: boolean; attrs: Record<string, string | number | boolean | undefined>; badge?: Badge; autogenerate?: { directory: string };}
export interface SidebarGroup { type: 'group'; label: string; entries: Array<SidebarLink | SidebarGroup>; collapsed: boolean; badge?: Badge; autogenerate?: { directory: string };}
export type SidebarEntry = SidebarLink | SidebarGroup;
export interface StarlightRouteData { sidebar: SidebarEntry[]; pagination: { prev?: SidebarLink; next?: SidebarLink; }; entry: { id: string; slug: string; data: { title: string; description?: string; visibility?: 'public' | 'private'; [key: string]: unknown; }; }; [key: string]: unknown;}2.2. Contexte d’Exécution SSR1/2
Dans le pipeline Astro SSR :
apps/web/src/middleware.tss’exécute en premier :- Extrait le token de session Better Auth depuis les cookies (
better-auth.session_token). - Valide la session et peuple
context.locals.useravec l’utilisateur authentifié (ounull/undefined).
- Extrait le token de session Better Auth depuis les cookies (
- Starlight initialise
context.locals.starlightRoute.
2.2. Contexte d’Exécution SSR2/2
- Le middleware de route Starlight (
apps/web/src/starlightRouteData.ts) s’exécute avant le rendu des composants :- Lit
context.locals.userpour déterminer si la requête est authentifiée. - Modifie par mutation contrôlée
context.locals.starlightRoute.sidebaretcontext.locals.starlightRoute.pagination.
- Lit
3. Spécification de l’Algorithme de Filtrage de la Sidebar
3.1. Résolution de la Visibilité depuis l’URL (href)1/2
Pour chaque lien de navigation avec href (ex. /docs/l1_contenu/sanity/visual-editing/... ou /gettingstarted/01-setup-local/) :
- Normaliser le chemin en retirant les slashes initiaux/finaux.
- Mapper vers l’identifiant de la collection
docs:- Ex.
/docs/l1_contenu/sanity/.../->l1_contenu/sanity/... - Ex.
/gettingstarted/01-setup-local/->gettingstarted/01-setup-local
- Ex.
3.1. Résolution de la Visibilité depuis l’URL (href)2/2
- Vérifier la visibilité du document :
- Requêter la collection de documents via
getCollection('docs')ougetEntry('docs', id). - Si
visibility === 'public'-> Visibilité Publique. - Sinon (ou en cas d’erreur/absence) -> Visibilité Privée (Secure by default).
- Requêter la collection de documents via
3.2. Filtrage Récursif des Éléments (filterSidebar)
Pour un utilisateur non authentifié (!locals.user) :
function filterSidebarItems( items: SidebarEntry[], visibilityMap: Map<string, 'public' | 'private'>): SidebarEntry[] { const result: SidebarEntry[] = [];
for (const item of items) { if (item.type === 'link') { const isPublic = isHrefPublic(item.href, visibilityMap); if (isPublic) { result.push(item); } } else if (item.type === 'group') { // Filtrer récursivement les sous-éléments const filteredEntries = filterSidebarItems(item.entries, visibilityMap); // Élagage : ne conserver le groupe que s'il contient au moins un élément visible if (filteredEntries.length > 0) { result.push({ ...item, entries: filteredEntries, }); } } }
return result;}3.3. Décoration pour Utilisateur Authentifié
Pour un utilisateur authentifié (locals.user présent) :
- Tous les éléments de navigation sont conservés.
- Pour chaque
SidebarLink, si le document associé est privé (visibility !== 'public'), un badge est ajouté :if (!item.badge) {item.badge = {text: 'Privé',variant: 'caution',};}
3.4. Assainissement de la Pagination (pagination)
Starlight génère les liens prev et next basés sur l’ordre de la sidebar :
- Pour un visiteur non authentifié :
- Si
pagination.prevpointe vers un document privé ->pagination.prev = undefined. - Si
pagination.nextpointe vers un document privé ->pagination.next = undefined.
- Si
- Pour un utilisateur authentifié :
- Les liens
prevetnextrestent inchangés.
- Les liens
4. Spécification du Déclencheur d’Authentification (Header Starlight)
4.1. Problématique UX
Avec une sidebar et une pagination strictement épurées pour les visiteurs publics, un internaute ne rencontre plus jamais de redirection forcée vers /login. La démarche d’authentification doit donc être explicite et accessible en permanence sans perturber la lecture.
4.2. Composant de Surcharge HeaderAuth.astro
Starlight permet de surcharger le composant de barre d’outils supérieure SocialIcons situé dans le Header, à droite du champ de recherche et à gauche du sélecteur de thème.
-
Enregistrement dans la configuration (
apps/web/astro.config.mjs) :starlight({components: {SocialIcons: './src/components/starlight/HeaderAuth.astro',},}) -
Logique du composant
src/components/starlight/HeaderAuth.astro:- Réutilise le composant natif :
import Default from '@astrojs/starlight/components/SocialIcons.astro'; - Détecte l’état d’authentification via
Astro.locals.user. - Visiteur Public (
!locals.user) :- Affiche un bouton d’action
Connexionmenant à/login?callbackURL=${encodeURIComponent(Astro.url.pathname)}. - Permet une redirection post-connexion exacte vers la documentation en cours de consultation.
- Affiche un bouton d’action
- Membre Authentifié (
locals.userprésent) :- Affiche un badge discret contenant l’email de l’utilisateur (
user.email). - Affiche un bouton
DéconnexiondéclenchantauthClient.signOut()côté client, suivi d’un rechargement de la page pour repasser instantanément en vue publique.
- Affiche un badge discret contenant l’email de l’utilisateur (
- Rend les icônes sociales d’origine via
<Default><slot /></Default>.
- Réutilise le composant natif :
5. Spécification de la Route Racine /docs
5.1. Problème Actuel
Dans apps/web/src/pages/[slug].astro :
const reservedStarlightSlugs = ['docs', 'gettingstarted', 'getting-started'];if (reservedStarlightSlugs.includes(slug?.toLowerCase())) { return new Response(null, { status: 404 });}L’accès à http://localhost:8787/docs déclenche un code HTTP 404.
5.2. Comportement Spécifié
Dans apps/web/src/pages/[slug].astro :
- Si
slugfait partie des slugs réservés :- Pour
docs: Rediriger viaAstro.redirect('/docs/gettingstarted/01-setup-local/', 302)(ou le document d’accueil public convenu). - Pour
gettingstarted/getting-started: Rediriger vers/gettingstarted/01-setup-local/.
- Pour
- Dans
apps/web/src/middleware.ts:- La requête
/docsou/docs/est autorisée publiquement pour permettre la redirection fluide.
- La requête
6. Spécification de la Séparation des Index Algolia DocSearch
6.1. Problématique de Sécurité
Algolia DocSearch requiert une clé PUBLIC_ALGOLIA_SEARCH_API_KEY injectée dans le code JavaScript du navigateur. Si des enregistrements privés sont indexés dans un index partagé avec un simple tag visibility: 'private', n’importe quel internaute peut appeler directement l’API REST Algolia sans filtre et aspirer l’ensemble des titres, sous-titres, extraits et blocs de code privés.
6.2. Stratégie du Double Index
Deux index physiquement isolés sont créés et alimentés :
| Index | Variables d’Environnement | Contenu autorisé | Audience |
|---|---|---|---|
| Index Public | PUBLIC_ALGOLIA_INDEX_NAME_PUBLIC (défaut : amn_docs_public) |
Uniquement documents avec visibility: 'public' |
Tout visiteur anonyme |
| Index Membre | PUBLIC_ALGOLIA_INDEX_NAME_ALL (défaut : amn_docs_all) |
Documents publics + documents privés | Utilisateurs connectés Better Auth |
6.3. Spécification du Script d’Indexation (index-algolia-docs.ts)1/2
Le script de génération des records :
- Parcourt tous les fichiers
.mdet.mdxdeapps/web/src/content/docs/. - Extrait le frontmatter et détermine la visibilité (
publicouprivate). - Découpe le contenu en fragments DocSearch standard (
lvl0,lvl1,lvl2,lvl3,content,url). - Construit deux ensembles de données :
publicRecords: filtrerecord.visibility === 'public'allRecords: l’intégralité des enregistrements
6.3. Spécification du Script d’Indexation (index-algolia-docs.ts)2/2
- Envoie
publicRecordsà l’indexPUBLIC_ALGOLIA_INDEX_NAME_PUBLIC. - Envoie
allRecordsà l’indexPUBLIC_ALGOLIA_INDEX_NAME_ALL. - Configure les paramètres de pertinence et de recherche (
searchableAttributes,customRanking,highlightPreTag) sur les deux index de manière identique.
6.4. Sélection de l’Index dans docsearch.ts
Dans apps/web/src/config/docsearch.ts :
- Détection de l’état d’authentification :
- Vérification de la présence du cookie
better-auth.session_tokencôté client (ou lecture d’un attribut de données injecté sur<html>/<body>lors du SSR). - Si session active -> utiliser
PUBLIC_ALGOLIA_INDEX_NAME_ALL. - Si session inactive -> utiliser
PUBLIC_ALGOLIA_INDEX_NAME_PUBLIC.
- Vérification de la présence du cookie
7. Plan de Test et Critères d’Acceptation (TEST)
7.1. Critères d’Acceptation Visiteur Public (Non Authentifié)1/3
- Accès
/docs: Une requête GET surhttp://localhost:8787/docsredirige vers une page publique valide (200 OK) sans demander de connexion. - Déclencheur d’Authentification : Le bouton « Connexion » est visible dans la barre supérieure (Header). Au clic, il renvoie vers
/login?callbackURL=....
7.1. Critères d’Acceptation Visiteur Public (Non Authentifié)2/3
- Sidebar épurée :
- La sidebar ne contient AUCUN document privé.
- Aucun groupe vide n’est affiché.
- Tous les liens cliquables mènent à des pages avec code 200 OK (jamais 302 vers
/login).
7.1. Critères d’Acceptation Visiteur Public (Non Authentifié)3/3
- Pagination sécurisée : Les boutons “Page précédente” et “Page suivante” ne pointent jamais vers un document privé.
- Recherche Algolia :
- La recherche interroge l’index public.
- Aucune recherche ne retourne de résultat issu d’un document privé (ex: “SANITY_PROJECT_ID”, “Cloudflare Secret”, etc.).
7.2. Critères d’Acceptation Membre Authentifié (Connecté)1/2
- État du Header : Le Header affiche le badge de l’utilisateur (email) ainsi que le bouton « Déconnexion ». Le clic sur « Déconnexion » déconnecte l’utilisateur et recharge la page en mode public.
- Sidebar complète :
- La sidebar affiche tous les documents (publics et privés).
- Les documents privés affichent un badge visuel distinct “Privé”.
7.2. Critères d’Acceptation Membre Authentifié (Connecté)2/2
- Navigation fluide : Les documents privés s’affichent correctement en 200 OK.
- Recherche Algolia :
- La recherche interroge l’index complet.
- Les résultats incluent les documents privés avec leurs ancres.
7.3. Commandes de Validation Requises
pnpm --filter amn-astro build: compilation SSR sans erreur TypeScript ni rupture de contrat Starlight.task test:better-auth/vitest run: tests unitaires d’authentification opérationnels.task dev:wrangler: vérification manuelle via cURL et navigateur.
8. Fichiers Concernés pour la Phase 2 (CODE)1/2
apps/web/astro.config.mjs: Déclaration derouteMiddlewareet composant de surchargeSocialIcons: './src/components/starlight/HeaderAuth.astro'.apps/web/src/components/starlight/HeaderAuth.astro: Composant de bouton de connexion / déconnexion dans le Header.apps/web/src/starlightRouteData.ts: Création du middleware de route Starlight (defineRouteMiddleware).
8. Fichiers Concernés pour la Phase 2 (CODE)2/2
apps/web/src/pages/[slug].astro: Redirection propre de/docsvers la documentation publique.apps/web/scripts/index-algolia-docs.ts: Indexation double (public et complet).apps/web/src/config/docsearch.ts: Sélection dynamique de l’index selon l’authentification.GettingStarted/38-test-starlight-sidebar-dual-view.md: Guide d’instructions de test pour validation locale.