Skip to content

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

  1. 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.
  2. 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

  1. Assainir la pagination bas de page (prev / next) pour éviter toute navigation accidentelle vers un document privé pour un utilisateur non connecté.
  2. Rediriger la route racine /docs vers la page de documentation publique principale au lieu d’un code 404.
  3. Séparer physiquement les index de recherche Algolia DocSearch en deux index distincts (public et all) afin d’interdire toute fuite de contenu sensible via les clés de recherche publiques.

1.2. Contraintes Architecturales

  • Utiliser l’API officielle Starlight routeMiddleware via defineRouteMiddleware (@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 :

  1. apps/web/src/middleware.ts s’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.user avec l’utilisateur authentifié (ou null/undefined).
  2. Starlight initialise context.locals.starlightRoute.

2.2. Contexte d’Exécution SSR2/2

  1. Le middleware de route Starlight (apps/web/src/starlightRouteData.ts) s’exécute avant le rendu des composants :
    • Lit context.locals.user pour déterminer si la requête est authentifiée.
    • Modifie par mutation contrôlée context.locals.starlightRoute.sidebar et context.locals.starlightRoute.pagination.

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/) :

  1. Normaliser le chemin en retirant les slashes initiaux/finaux.
  2. 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

3.1. Résolution de la Visibilité depuis l’URL (href)2/2

  1. Vérifier la visibilité du document :
    • Requêter la collection de documents via getCollection('docs') ou getEntry('docs', id).
    • Si visibility === 'public' -> Visibilité Publique.
    • Sinon (ou en cas d’erreur/absence) -> Visibilité Privée (Secure by default).

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.prev pointe vers un document privé -> pagination.prev = undefined.
    • Si pagination.next pointe vers un document privé -> pagination.next = undefined.
  • Pour un utilisateur authentifié :
    • Les liens prev et next restent inchangés.

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.

  1. Enregistrement dans la configuration (apps/web/astro.config.mjs) :

    starlight({
    components: {
    SocialIcons: './src/components/starlight/HeaderAuth.astro',
    },
    })
  2. 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 Connexion menant à /login?callbackURL=${encodeURIComponent(Astro.url.pathname)}.
      • Permet une redirection post-connexion exacte vers la documentation en cours de consultation.
    • Membre Authentifié (locals.user présent) :
      • Affiche un badge discret contenant l’email de l’utilisateur (user.email).
      • Affiche un bouton Déconnexion déclenchant authClient.signOut() côté client, suivi d’un rechargement de la page pour repasser instantanément en vue publique.
    • Rend les icônes sociales d’origine via <Default><slot /></Default>.

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 :

  1. Si slug fait partie des slugs réservés :
    • Pour docs : Rediriger via Astro.redirect('/docs/gettingstarted/01-setup-local/', 302) (ou le document d’accueil public convenu).
    • Pour gettingstarted / getting-started : Rediriger vers /gettingstarted/01-setup-local/.
  2. Dans apps/web/src/middleware.ts :
    • La requête /docs ou /docs/ est autorisée publiquement pour permettre la redirection fluide.

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 :

  1. Parcourt tous les fichiers .md et .mdx de apps/web/src/content/docs/.
  2. Extrait le frontmatter et détermine la visibilité (public ou private).
  3. Découpe le contenu en fragments DocSearch standard (lvl0, lvl1, lvl2, lvl3, content, url).
  4. Construit deux ensembles de données :
    • publicRecords : filtre record.visibility === 'public'
    • allRecords : l’intégralité des enregistrements

6.3. Spécification du Script d’Indexation (index-algolia-docs.ts)2/2

  1. Envoie publicRecords à l’index PUBLIC_ALGOLIA_INDEX_NAME_PUBLIC.
  2. Envoie allRecords à l’index PUBLIC_ALGOLIA_INDEX_NAME_ALL.
  3. 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_token cô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.

7. Plan de Test et Critères d’Acceptation (TEST)

7.1. Critères d’Acceptation Visiteur Public (Non Authentifié)1/3

  1. Accès /docs : Une requête GET sur http://localhost:8787/docs redirige vers une page publique valide (200 OK) sans demander de connexion.
  2. 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

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

  1. Pagination sécurisée : Les boutons “Page précédente” et “Page suivante” ne pointent jamais vers un document privé.
  2. 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

  1. É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.
  2. 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

  1. Navigation fluide : Les documents privés s’affichent correctement en 200 OK.
  2. 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

  1. apps/web/astro.config.mjs : Déclaration de routeMiddleware et composant de surcharge SocialIcons: './src/components/starlight/HeaderAuth.astro'.
  2. apps/web/src/components/starlight/HeaderAuth.astro : Composant de bouton de connexion / déconnexion dans le Header.
  3. apps/web/src/starlightRouteData.ts : Création du middleware de route Starlight (defineRouteMiddleware).

8. Fichiers Concernés pour la Phase 2 (CODE)2/2

  1. apps/web/src/pages/[slug].astro : Redirection propre de /docs vers la documentation publique.
  2. apps/web/scripts/index-algolia-docs.ts : Indexation double (public et complet).
  3. apps/web/src/config/docsearch.ts : Sélection dynamique de l’index selon l’authentification.
  4. GettingStarted/38-test-starlight-sidebar-dual-view.md : Guide d’instructions de test pour validation locale.
Switch to normal mode

Contents