Ajouter un espace client
Monter l'espace /compte dans un projet, y ajouter une section depuis un module, et y ramener le client après sa connexion.
Un client reçoit une facture par courriel et veut retrouver les précédentes. L'admin ne lui sert à rien : elle demande admin.access et montre tout le site. Ce guide monte un espace client, des pages réservées aux comptes connectés hors de l'admin, puis montre comment un module y ajoute sa section et comment le client y revient après s'être connecté.
Fraza fournit l'admin ; l'espace client, lui, appartient au site. Le core en livre une version de base, prête à l'emploi, pour un site qui n'a pas besoin de plus. Le plus souvent, un site construit le sien, dans son propre design, et y rend les sections des modules : voir Construire son propre espace client.
Prérequis
- Les pages d'authentification sont en place :
app/(auth)/auth/[...auth]/page.js. Voir Authentification. - Un module installé qui expose une sous-entrée
./account, ou le vôtre : voir Écrire une section.
Monter la route
Cette route monte l'espace de base du core. L'espace client est facultatif : @fraza/start ne le crée pas, et fraza-doctor ne l'exige pas. Créer la route dans le projet :
// app/compte/[[...section]]/page.js
import '../../.fraza/account.server.js';
export { default, generateMetadata } from '@fraza/core/features/account/server';Le segment optionnel [[...section]] sert à la fois /compte et /compte/<section>/…. La page demande une session et aucune permission : tout compte connecté y entre, et un visiteur anonyme est renvoyé vers la connexion.
L'import du manifeste est obligatoire. Une section est retrouvée au rendu par une lecture de registre, pas par un import. Le compilateur RSC de la route ne peut donc pas deviner qu'elle existe, et les composants 'use client' qu'elle rend resteraient hors du React Client Manifest : le rendu échouerait sur « Could not find the module … in the React Client Manifest ». app/.fraza/account.server.js, écrit par fraza-modules sync, importe la sous-entrée ./account de chaque module activé, et crée l'arête statique qui manque. C'est le même mécanisme que pour les pages publiques des modules.
Le manifeste se régénère tout seul en postinstall, en dev et au build. Pour le régénérer à la main après l'ajout d'un module :
npx fraza-modules syncChoisir le chemin
L'espace vit sous /compte par défaut. Pour le monter ailleurs, déplacer le dossier de la route et poser la variable au même chemin :
.env.local
FRAZA_ACCOUNT_PATH=/espace-clientLa route devient alors app/espace-client/[[...section]]/page.js, avec le même contenu. Une valeur vide ou / place l'espace à la racine du site.
Les deux doivent concorder parce que 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 construire la navigation, le retour après connexion et les liens des modules. Un dossier déplacé sans la variable donne une navigation qui mène à une 404.
Écrire une section depuis un module
Une section est un composant serveur, enregistré depuis une quatrième sous-entrée du module, ./account :
{
"exports": {
".": { "import": "./dist/index.js" },
"./client": { "import": "./dist/client.js" },
"./account": { "import": "./dist/account.js" }
}
}// src/account.js
import { registerAccountSection } from '@fraza/core/features/account';
import { SOURCE } from './source.js';
import InvoicesSection from './account/InvoicesSection.server.js';
registerAccountSection({
slug: 'factures',
label: 'Factures',
order: 20,
Component: InvoicesSection,
}, { source: SOURCE });src/account.js ne commence pas par 'use client' : registerAccountSection() s'exécute sur le serveur. Ses composants enfants, eux, peuvent être client, c'est la raison d'être de la sous-entrée. fraza-module check refuse une entrée ./account marquée 'use client', et check-bundle une entrée qui atteint un barrel lourd du core. Jamais d'appel depuis register-server.js : il est atteint par instrumentation.js, une autre compilation que celle de la route.
Le slug est le segment d'URL, en français : /compte/factures. Le registre est strict : un slug déjà pris par un autre module fait échouer l'enregistrement.
Ajouter une icône et une carte d'accueil
/compte sans section ouvre l'accueil de l'espace : une salutation, puis une carte par section qui en apporte une. Deux champs facultatifs habillent la section :
// src/account.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 });icon s'affiche à côté du libellé dans la navigation. Summary est un composant serveur qui reçoit session, user, basePath et sectionPath. Il résume la section en une carte, par exemple le nombre de factures à régler avec un lien vers sectionPath, et rend null quand il n'a rien à dire. Un Summary qui lève retire sa carte, pas l'accueil.
Lire les données du compte
Le composant reçoit la session, le compte, et la suite de l'URL. Il lit ses propres données, filtrées par le compte connecté :
// src/account/InvoicesSection.server.js
import { accountHref } from '@fraza/core/features/account';
import { listInvoicesForUser, getInvoiceForUser } from '../invoices.server.js';
import InvoiceTable from './InvoiceTable.client.js';
import InvoiceDetail from './InvoiceDetail.js';
export default async function InvoicesSection({ user, segments }) {
// /compte/factures/2026-0001 → segments = ['2026-0001']
if (segments[0]) {
const invoice = await getInvoiceForUser(user.id, segments[0]);
if (!invoice) return <p>Cette facture est introuvable.</p>;
return <InvoiceDetail invoice={invoice} backHref={accountHref('factures')} />;
}
const invoices = await listInvoicesForUser(user.id);
return <InvoiceTable invoices={invoices} basePath={accountHref('factures')} />;
}Filtrer par user.id dans chaque requête. La page ne vérifie que la session, pas ce que le compte a le droit de voir : un identifiant lu dans segments vient de l'URL, n'importe quel compte connecté peut le changer.
Un composant client ne lit pas process.env : il reçoit le chemin en prop, comme basePath ci-dessus. Les props complètes sont listées dans Espace client.
Masquer une section
Une section qui n'a rien à montrer à un compte se masque avec isVisible. Un client sans abonnement n'a pas besoin d'une entrée « Abonnements » vide :
registerAccountSection({
slug: 'abonnements',
label: 'Abonnements',
Component: SubscriptionsSection,
isVisible: async ({ session }) => hasSubscriptions(session.user.id),
}, { source: SOURCE });Une section masquée disparaît de la navigation et de l'accueil, et son URL répond 404, la même réponse qu'un slug inconnu. isVisible tourne à chaque affichage : la garder à une requête courte. Si elle lève, la section est masquée et le reste de l'espace s'affiche.
Construire son propre espace client
La page du core n'est qu'une façon de rendre les sections. Un site qui veut son en-tête, sa navigation et sa mise en page écrit sa propre page, avec les mêmes briques :
| Brique | Rôle |
|---|---|
getSession() de @fraza/core/features/auth/actions | La session. Sans elle, renvoyer vers /auth/login?next=<chemin demandé>. |
getVisibleAccountSections(session) | Les sections que ce compte voit, triées : la navigation. |
getAccountSections() | Toutes les sections, sans la session : le titre de l'onglet du navigateur. |
getAccountBasePath() | Le chemin de l'espace, pour construire les liens. |
section.Component | Le composant serveur de la section, à rendre avec ses props. |
renderAccountSummaries(session, { basePath, sections }) de @fraza/core/features/account/server | Les cartes Summary rendues, pour une page d'accueil. Une carte qui lève est écartée. |
AccountNav de @fraza/core/features/account/nav | La navigation de la page de base, avec l'entrée Accueil et les icônes. Neutre, rendue côté serveur. |
AccountMenu de @fraza/core/features/account/menu | Le menu de l'avatar : courriel, liens, thème, déconnexion. Composant client. |
Les deux composants sont facultatifs : un site qui a déjà sa navigation et son en-tête les ignore. Ils servent à garder l'apparence de la page de base dans un cadre propre au site, par exemple l'en-tête et le pied de page de fraza.ca autour de la navigation du core.
// app/(public)/compte/[[...section]]/page.js
import '../../../.fraza/account.server.js'; // les sections des modules
import '@/components/compte/sections'; // les sections du site
import { notFound, redirect } from 'next/navigation';
import { getAccountBasePath, getVisibleAccountSections } from '@fraza/core/features/account';
import { renderAccountSummaries } from '@fraza/core/features/account/server';
import AccountNav from '@fraza/core/features/account/nav';
import AccountMenu from '@fraza/core/features/account/menu';
import { getSession } from '@fraza/core/features/auth/actions';
import { MonCadre } from '@/components/compte/mon-cadre';
export default async function Compte({ params, searchParams }) {
const { section: segments = [] } = await params;
const basePath = getAccountBasePath();
const session = await getSession();
if (!session?.user) redirect(`/auth/login?next=${encodeURIComponent(`${basePath}/${segments.join('/')}`)}`);
const sections = await getVisibleAccountSections(session);
const [slug, ...rest] = segments;
const active = slug ? sections.find((s) => s.slug === slug) : null;
if (slug && !active) notFound();
const { name, email, image } = session.user;
const nav = <AccountNav sections={sections} activeSlug={active?.slug ?? null} basePath={basePath} />;
const menu = <AccountMenu user={{ name, email, image }} />;
// Sans section : l'accueil, avec les cartes des sections.
if (!active) {
const cards = await renderAccountSummaries(session, { basePath, sections });
return (
<MonCadre nav={nav} menu={menu}>
{cards.map(({ slug: key, node }) => <div key={key}>{node}</div>)}
</MonCadre>
);
}
const Section = active.Component;
return (
<MonCadre nav={nav} menu={menu}>
<Section
session={session}
user={session.user}
segments={rest}
searchParams={(await searchParams) ?? {}}
basePath={basePath}
sectionPath={`${basePath}/${active.slug}`}
/>
</MonCadre>
);
}AccountMenu est un composant client : lui passer les seuls champs du compte qu'il affiche, sérialisables, plutôt que la ligne entière.
Les règles de la page de base restent celles de la vôtre :
- L'import du manifeste reste obligatoire, pour la même raison qu'avec la page du core.
- Un slug inconnu ou masqué répond 404, sans distinguer les deux.
- Les métadonnées posent
noindex: ces pages sont privées. FRAZA_ACCOUNT_PATHsuit le dossier de la route,/pour un espace à la racine du site.
Une section propre au site s'enregistre comme celle d'un module, depuis un fichier que la route importe, avec la source du projet :
// components/compte/sections.js
import { registerAccountSection } from '@fraza/core/features/account';
import ProfilSection from './profil-section.server';
registerAccountSection({ slug: 'profil', label: 'Profil', order: 90, Component: ProfilSection }, { source: 'mon-site' });Une section Profil s'écrit sur les routes de profil du core : PUT /za/api/users/profile pour le nom, POST /za/api/users/profile/email pour l'adresse, POST /za/api/users/profile/password pour le mot de passe, DELETE /za/api/users/profile/sessions pour fermer les sessions. Voir Authentification. Le lien de confirmation d'une nouvelle adresse ouvre /auth/confirm-email-change, que tout compte connecté atteint : passer next: accountHref('profil') dans la demande pour que le client revienne sur sa section. Voir Changement d'adresse.
compte.fraza.ca est bâti ainsi : un espace à la racine, dans le design de fraza.ca, avec les sections Abonnements de Facturation, Projets de Licences, et une section Profil propre au site.
Envoyer le client vers son espace
Un lien vers l'espace se construit avec accountHref(), qui suit FRAZA_ACCOUNT_PATH :
import { getPublicBaseUrl } from '@fraza/core';
import { accountHref } from '@fraza/core/features/account';
const url = `${getPublicBaseUrl()}${accountHref('factures', invoice.number)}`;Ce lien suffit, même pour un client qui n'est pas connecté. La page le renvoie vers /auth/login?next=/compte/factures/2026-0001, et la connexion le ramène à cette adresse au lieu de FRAZA_AUTH_REDIRECT_AFTER_LOGIN, qui vaut /admin dans un projet généré.
Le paramètre survit à une inscription. Le lien « S'inscrire » de la connexion le garde, et le courriel de vérification le porte. Le code à 6 chiffres saisi après l'inscription, comme le lien du courriel, ouvre la session : le nouveau compte arrive directement sur la page d'où il est parti, sans repasser par la connexion.
Pour envoyer quelqu'un se connecter depuis une page du site, construire le lien soi-même :
const loginHref = `/auth/login?next=${encodeURIComponent(accountHref())}`;Seul un chemin du site est suivi : une adresse vers un autre domaine, ou une page sous /auth, est ignorée au profit de la destination par défaut. Voir Retour après connexion.
Vérifier
- Lancer
npm run dev, qui régénère les manifestes. - Ouvrir
app/.fraza/account.server.js: chaque module activé qui expose./accounty est importé. - Ouvrir
/comptedans une fenêtre privée : la page de connexion s'affiche, avec?next=%2Fcomptedans l'adresse. - Se connecter avec un compte sans rôle d'administration : l'espace s'ouvre sur son accueil, avec une carte pour chaque section visible qui apporte un
Summary.
Notes
Un module absent du manifeste n'a pas de section. Vérifier qu'il est activé par sa variable FRAZA_MODULE_*, et que son package.json#exports déclare bien ./account.
Le registre est local à la route. Depuis une autre page du site, getAccountSections() retourne un tableau vide. Pour savoir depuis ailleurs si un compte a quelque chose à voir, interroger directement les données du module.