Aller au contenu

Espace client

Registre des sections de l'espace client, page qui les rend sous /compte, navigation, menu du compte et liens vers les sections.

features/account sert l'espace client : les pages qu'un compte connecté voit hors de l'admin, sous /compte par défaut. Les modules y enregistrent des sections, et la route du projet les rend. Règle absolue : une section s'enregistre depuis la sous-entrée ./account du module, jamais depuis register-server.js.

Pour mettre l'espace en place de bout en bout, voir Ajouter un espace client.

La page livrée par @fraza/core/features/account/server est un espace de base. Un site qui veut le sien, dans son design, écrit sa propre page avec getVisibleAccountSections(), le Component de chaque section et, au besoin, AccountNav, AccountMenu et renderAccountSummaries() : voir Construire son propre espace client. Les sections des modules s'y rendent de la même façon.

Import

// Barrel neutre : registre et liens
import {
  registerAccountSection,
  getAccountSections,
  getVisibleAccountSections,
  getAccountSummaries,
  getAccountBasePath,
  accountHref,
} from '@fraza/core/features/account';

// Page racine RSC de la route de l'espace client, et rendu des cartes d'accueil
export { default, generateMetadata } from '@fraza/core/features/account/server';
import { renderAccountSummaries } from '@fraza/core/features/account/server';

// Briques de cadre, pour un espace construit par le site
import AccountNav from '@fraza/core/features/account/nav';    // neutre
import AccountMenu from '@fraza/core/features/account/menu';  // client

@fraza/core/features/account est neutre : il n'importe ni Next.js ni la base. C'est ce qui permet à la sous-entrée ./account d'un module de l'importer sans tirer la session. @fraza/core/features/account/server est serveur : il lit la session et la base.

@fraza/core/features/account/nav est neutre et se rend côté serveur. @fraza/core/features/account/menu est un composant client ('use client') : un composant serveur le rend en lui passant des props sérialisables. Tous deux s'importent un fichier à la fois, sans tirer le jeu d'icônes ni l'éditeur de blocs dans une page client.

getAccountBasePath() et accountHref() lisent process.env. Elles s'appellent côté serveur ; un composant client reçoit le chemin en prop.

API

FonctionRetourDescription
registerAccountSection(section, { source, override? })voidEnregistre une section.
getAccountSections()Section[]Toutes les sections enregistrées, triées.
getVisibleAccountSections(session)Promise<Section[]>Les sections que cette session peut voir.
getAccountSummaries(session)Promise<Section[]>Les sections visibles qui apportent un Summary.
renderAccountSummaries(session, { basePath?, sections? })Promise<{ slug, label, node }[]>Les cartes d'accueil rendues. Exporté par ./server.
AccountNavcomposantNavigation de l'espace. Sous-entrée ./nav.
AccountMenucomposant clientMenu du compte, l'avatar en déclencheur. Sous-entrée ./menu.
getAccountBasePath()stringChemin de l'espace client, normalisé.
accountHref(...segments)stringLien vers une page de l'espace client.

registerAccountSection({ slug, label, order?, Component, isVisible?, icon?, Summary? }, { source, override? })

// src/account.js, exposé par la sous-entrée ./account
import { registerAccountSection } from '@fraza/core/features/account';
import { SOURCE } from './source.js';
import { FileIcon } from '@fraza/icons/FileIcon';
import InvoicesSection from './account/InvoicesSection.server.js';
import InvoicesSummary from './account/InvoicesSummary.server.js';

registerAccountSection({
  slug: 'factures',
  label: 'Factures',
  order: 20,
  icon: FileIcon,
  Component: InvoicesSection,
  Summary: InvoicesSummary,
}, { source: SOURCE });
ChampTypeRequisRôle
slugstringouiSegment d'URL après le chemin de l'espace : /compte/factures. Minuscules, chiffres et tirets, 64 caractères au plus, en français comme toute route.
labelstringouiLibellé dans la navigation, titre de la page et titre d'onglet du navigateur.
ordernumbernonRang dans la navigation, 100 par défaut. À rang égal, l'ordre alphabétique du libellé décide.
ComponentcomposantouiComposant serveur qui rend la section.
isVisible({ session }) => boolean | Promise<boolean>nonDécide si la session voit la section. Absente, la section est visible de tout compte connecté.
iconcomposant d'icônenonIcône de @fraza/icons, affichée à côté du libellé dans la navigation. Absente, l'entrée n'a que son libellé.
SummarycomposantnonComposant serveur, la carte de la section sur l'accueil de l'espace. Absent, la section n'a pas de carte.

Le registre est strict, comme les autres : un slug pris par une autre source lève à l'enregistrement, sauf override: true. Voir Une clé, une source.

Le composant reçoit :

PropTypeContenu
sessionobjectSession courante, jamais null : la page a déjà renvoyé un visiteur anonyme vers la connexion.
userobjectsession.user.
segmentsstring[]Segments après le slug : /compte/factures/2026-0001 donne ['2026-0001'].
searchParamsobjectChaîne de requête résolue, {} si l'URL n'en porte pas.
basePathstringChemin de l'espace, /compte par défaut, '' à la racine du site.
sectionPathstringChemin de la section, /compte/factures.

Le composant lit lui-même ses données, filtrées par user.id. La page ne vérifie que la session : ce qu'un compte a le droit de voir dans la section, c'est la section qui le décide.

Summary reçoit session, user, basePath et sectionPath, les mêmes props que Component sans segments ni searchParams. Il rend null quand il n'a rien à dire, et sa carte disparaît de l'accueil :

// src/account/InvoicesSummary.server.js
export default async function InvoicesSummary({ user, sectionPath }) {
  const unpaid = await countUnpaidInvoices(user.id);
  if (unpaid === 0) return null;
  return (
    <a href={sectionPath} className="block rounded-card border border-line bg-surface p-5">
      {unpaid} facture(s) à régler
    </a>
  );
}

icon et Summary sont contrôlés à l'enregistrement : une valeur qui n'est pas un composant, une chaîne par exemple, lève une TypeError.

getAccountSections()

Toutes les sections, triées par order puis par libellé, sans tenir compte de isVisible. La page s'en sert pour le titre d'onglet, qui ne dépend pas de la session.

getVisibleAccountSections(session)

Les sections sans isVisible, et celles dont isVisible({ session }) répond vrai. Un isVisible qui lève masque sa section au lieu de casser tout l'espace.

registerAccountSection({
  slug: 'abonnements',
  label: 'Abonnements',
  Component: SubscriptionsSection,
  isVisible: async ({ session }) => hasSubscriptions(session.user.id),
}, { source: SOURCE });

isVisible tourne à chaque affichage de l'espace, pour chaque section : la garder à une requête courte.

getAccountSummaries(session)

Les sections visibles de cette session qui apportent un Summary, dans l'ordre de la navigation. Elle ne rend rien : elle sert à une page qui rend elle-même les cartes, et décide de leur disposition.

renderAccountSummaries(session, { basePath?, sections? })

Rend le Summary de chaque section visible et retourne les cartes qui ont donné quelque chose, { slug, label, node }, dans l'ordre de la navigation. Exporté par @fraza/core/features/account/server, pour une page d'accueil construite par le site.

import { renderAccountSummaries } from '@fraza/core/features/account/server';

const cards = await renderAccountSummaries(session, { basePath, sections });
return (
  <div className="grid gap-4 lg:grid-cols-2">
    {cards.map(({ slug, node }) => <div key={slug}>{node}</div>)}
  </div>
);

basePath vaut getAccountBasePath() par défaut. sections évite une seconde lecture quand l'appelant a déjà les sections visibles : sans elle, la fonction appelle getVisibleAccountSections(session).

Un Summary qui lève est journalisé et écarté. Un module en panne retire sa carte, pas l'accueil entier. Un composant fonction, asynchrone ou non, est appelé directement pour que son erreur soit attrapée ici ; une référence client ou un memo est rendu comme un élément, et son erreur n'est pas attrapée par la fonction.

AccountNav

La navigation de l'espace : l'entrée Accueil, puis une entrée par section, avec son icône. Une colonne à partir de md, une rangée qui défile en dessous. L'entrée active porte aria-current="page". Chaque entrée est un lien : ouvrir une section est une navigation.

import AccountNav from '@fraza/core/features/account/nav';

<AccountNav sections={sections} activeSlug={active?.slug ?? null} basePath={basePath} />
PropTypeDéfautRôle
sections{ slug, label, icon? }[]requisLes entrées, typiquement getVisibleAccountSections(session).
activeSlugstring | nullrequisSlug de la section ouverte, null sur l'accueil.
basePathstringrequisChemin de l'espace, getAccountBasePath().
homeLabelstring'Accueil'Libellé de l'entrée d'accueil.
showHomebooleantrueAffiche l'entrée d'accueil. false pour un espace sans page d'accueil.
classNamestring''Classes ajoutées à la balise <nav>.

AccountMenu

Le menu du compte connecté, pour l'en-tête : l'avatar en déclencheur, puis dans le panneau le nom et le courriel, les liens passés, la bascule de thème et « Se déconnecter ». Composant client.

import AccountMenu from '@fraza/core/features/account/menu';

<AccountMenu
  user={{ name: user.name, email: user.email, image: user.image }}
  links={[{ label: 'Retour au site', href: '/' }]}
/>
PropTypeDéfautRôle
user{ name?, email?, image? }requisLe compte affiché : avatar, nom et courriel.
links{ label, href, icon? }[][]Liens ajoutés entre l'en-tête du panneau et la bascule de thème.
logoutHrefstring'/auth/logout'Adresse de déconnexion.
showThemebooleantrueAffiche la bascule de thème, quand le thème du site permet de basculer.

La déconnexion passe par une vraie navigation, jamais un <Link> : Next.js précharge un lien, et précharger la déconnexion fermerait la session. Le composant étant client, user doit être sérialisable : passer les champs utiles plutôt que la ligne complète du compte. Un icon de links est un composant, ou un élément (<UserIcon />) depuis un composant serveur : une fonction ne traverse pas la frontière serveur-client, un élément si.

getAccountBasePath()

Retourne FRAZA_ACCOUNT_PATH, normalisé : une barre oblique en tête, aucune en fin. Une valeur vide ou / donne '', la racine du site. Une valeur qui n'est pas faite de segments simples (lettres, chiffres, ., _, ~, -) retombe sur /compte.

accountHref(...segments)

Construit un lien vers l'espace client. Chaque segment est encodé, les valeurs vides sont ignorées.

accountHref();                        // '/compte'
accountHref('factures');              // '/compte/factures'
accountHref('factures', '2026-0001'); // '/compte/factures/2026-0001'

C'est la forme à employer partout où un module pointe vers l'espace client, courriel compris : elle suit FRAZA_ACCOUNT_PATH, un chemin écrit en dur ne le suit pas. Dans un courriel, préfixer par getPublicBaseUrl().

Page de base de l'espace client

La route du projet réexporte la page et ses métadonnées, après avoir importé le manifeste des sections :

// app/compte/[[...section]]/page.js
import '../../.fraza/account.server.js';

export { default, generateMetadata } from '@fraza/core/features/account/server';

Ce que fait la page :

  • Sans session, elle redirige vers /auth/login?next=<chemin demandé>, chaîne de requête comprise. Le visiteur revient à la section demandée une fois connecté.
  • Avec session, elle ne demande aucune permission : tout compte connecté y accède, administrateur ou non.
  • Le premier segment choisit la section. Sans segment, l'accueil de l'espace s'ouvre : « Bonjour » suivi du prénom, le courriel du compte, puis la grille des cartes Summary des sections visibles, par renderAccountSummaries(). Sans aucune carte, l'accueil renvoie à la navigation.
  • Un slug inconnu ou masqué répond 404, la même réponse dans les deux cas : l'URL ne dit pas si la section existe pour un autre compte.
  • Sans aucune section, la page affiche un état vide plutôt qu'une erreur.

Les métadonnées posent le titre "<libellé> - <appName>", ou "Mon compte - <appName>" hors section, et noindex, nofollow : ces pages sont privées.

Apparence

Le cadre reprend la famille des pages publiques des modules : un client qui passe d'un lien de paiement à son compte ne doit pas avoir l'impression de changer de site.

  • une barre collante de 60 px : le logo du site, le lien « Mon compte », et le menu du compte (AccountMenu), qui porte le courriel, la bascule de thème et « Se déconnecter », vers /auth/logout ;
  • la navigation (AccountNav), une colonne à gauche du contenu à partir de md, une rangée qui défile sous l'en-tête en dessous. Elle apparaît dès la première section, avec l'entrée Accueil ;
  • le titre de la section active, ou la salutation de l'accueil.

Voir Design system.

Variables d'environnement

VariableDéfautEffet
FRAZA_ACCOUNT_PATH/compteChemin où le projet monte l'espace client. Une valeur vide ou / le place à la racine du site. Doit correspondre au dossier de la route.

Notes

Le dossier de la route et FRAZA_ACCOUNT_PATH doivent concorder. Next.js ne donne à une page aucun moyen de lire le chemin où elle est montée. La variable dit au core ce même chemin, pour la navigation, le retour après connexion et les liens des modules. Déplacer la route sans changer la variable casse ces trois liens.

Le registre est local à la route. Il est alimenté par le graphe RSC de la route de l'espace client, pas au démarrage du serveur : depuis une autre route, getAccountSections() retourne un tableau vide. C'est la même règle que pour les pages publiques. Voir Qui alimente un registre de composants.

L'espace client est facultatif. La route ne fait pas partie du contrat de projet de fraza-doctor, et @fraza/start ne la crée pas. Le manifeste app/.fraza/account.server.js est écrit de toute façon : sans route qui l'importe, il n'entre dans aucun bundle.

Sous Cache Components, la page se rend dans une frontière <Suspense>. La redirection vers la connexion arrive alors au navigateur comme une redirection côté client, et un notFound() répond 200 avec noindex au lieu de 404. Voir Lectures de requête et Cache Components.

Sur cette page