Pages publiques

Registre des pages de module servies sous le namespace réservé /lrvo/<module>/...

core/public-pages sert le namespace /lrvo/<module>/.... Tout chemin /lrvo/<segment>/… autre que /lrvo/api/… est résolu vers le composant enregistré sous <segment>. Le module gère son routage interne à partir des segments restants.

C'est le namespace technique d'un module : page de confirmation, vue publique d'une ressource, retour de paiement. Pour servir du contenu à la racine du site, voir Contenu public.

Import

import {
  registerPublicModulePage,
  getPublicModulePage,
  getPublicModulePages,
} from '@lrvo/core/public-pages';

// Composant serveur de rendu, câblé par @lrvo/start
import PublicModulePage from '@lrvo/core/public-pages/server';

Module serveur.

API

registerPublicModulePage({ moduleName, Component, title?, generateMetadata? })

L'appel vit dans la sous-entrée ./public-page du module, jamais dans register-server.js. Voir Où placer l'appel plus bas : ce n'est pas une préférence de style, c'est ce qui décide si les composants client de la page fonctionnent.

// module-billing/src/public-page.js
import { registerPublicModulePage } from '@lrvo/core/public-pages';
import BillingRouter from './public/BillingRouter.js';

registerPublicModulePage({
  moduleName: 'billing',
  Component: BillingRouter,
  title: 'Facturation',
});
ChampTypeRequisRôle
moduleNamestringouiPremier segment servi sous /lrvo/.
ComponentcomposantouiRacine de rendu du namespace.
titlestringnonTitre d'onglet fixe, utilisé quand generateMetadata est absent ou retourne null.
generateMetadataasync ({ params, searchParams, segments }) => null | MetadatanonMétadonnées calculées à partir de l'URL.

Le composant reçoit :

PropTypeContenu
paramsobjectParamètres Next.js résolus, dont path.
searchParamsobjectChaîne de requête résolue, {} si l'URL n'en porte pas.
segmentsstring[]Segments après /lrvo/<moduleName>/.
function BillingRouter({ segments, searchParams }) {
  // /lrvo/billing/invoice/abc-123 → segments = ['invoice', 'abc-123']
  if (segments[0] === 'invoice') {
    return <InvoiceView id={segments[1]} outcome={searchParams.paiement} />;
  }
  return <BillingIndex />;
}

searchParams est la seule lecture serveur de la chaîne de requête sur ce namespace, et c'est ce qui rend lisible un retour de redirection externe : ?paiement=succes après un passage chez Stripe, ?code=… après un consentement OAuth. Comme toute donnée d'URL, elle est fournie par le visiteur : elle sert à choisir un affichage, jamais à autoriser quoi que ce soit.

Titre d'onglet et métadonnées

Le gabarit racine ne déclare aucun titre, et la page publique d'un module est retrouvée par lecture de registre : sans les métadonnées ci-dessous, l'onglet reste vide. C'est la route qui les expose à Next.js, d'où le second export du gabarit :

// app/lrvo/[...path]/page.js
import '../../.lrvo/public-pages.server.js';

export { default, generateMetadata } from '@lrvo/core/public-pages/server';

lrvo-doctor --fix remonte une route restée au export { default } seul.

Un title fixe suffit à une page unique. Un namespace qui sert plusieurs ressources passe par generateMetadata, qui reçoit le même contexte que le composant :

// module-billing/src/public/BillingPublicPage.server.js
export async function generateBillingMetadata({ segments }) {
  const invoice = await loadInvoice(segments[0]);
  if (!invoice) return { title: 'Facture' };
  return { title: `Facture ${invoice.invoice_number}`, robots: { index: false, follow: false } };
}

Le retour est un objet Metadata Next.js : robots, description, openGraph y passent aussi. Retourner null fait retomber sur le title de l'enregistrement, puis sur le nom d'application seul.

Le nom d'application est ajouté au titre, comme sur les pages d'admin : Facture 2026-0001 — Acary. Un module qui veut disposer du titre entier retourne la forme absolue de Next.js, { title: { absolute: 'Facture 2026-0001' } }.

Deux points de vigilance :

  • Une seule requête. Next.js appelle generateMetadata puis rend la page dans la même passe. Si les deux lisent la même ressource, envelopper la lecture dans le cache de React évite d'interroger la base deux fois par affichage.
  • Pas de notFound(). Le rendu s'en charge. Lever depuis les métadonnées transforme une 404 propre en page d'erreur. Sur une ressource protégée par un jeton d'URL, retourner un titre générique évite en plus de révéler si le jeton existe.

getPublicModulePage(moduleName) et getPublicModulePages()

Lecture du registre. Utilisées par le composant de rendu.

Le registre est local à la route /lrvo/[...path] : il est alimenté par le graphe RSC de cette route, pas au démarrage du serveur. Depuis n'importe où ailleurs, getPublicModulePages() retourne un tableau vide. C'est voulu, et la section suivante explique pourquoi.

Où placer l'appel

Le composant enregistré est rendu par la route /lrvo/[...path]. S'il atteint un fichier 'use client', ce fichier ne figure au React Client Manifest de la route que si le compilateur RSC y arrive depuis cette route. Un composant enregistré depuis instrumentation.js arrive par une autre compilation : le rendu échoue alors sur

Could not find the module "…/InvoiceView.client.js#default" in the React Client Manifest.

D'où le câblage : le module expose une sous-entrée ./public-page qui contient l'appel, npx lrvo-modules sync la référence depuis app/.lrvo/public-pages.server.js, et app/lrvo/[...path]/page.js importe ce manifeste. C'est le même mécanisme que blocks.server.js et templates.server.js, importés depuis app/layout.js.

// app/lrvo/[...path]/page.js — généré par @lrvo/start, réparable par lrvo-doctor --fix
import '../../.lrvo/public-pages.server.js';

export { default } from '@lrvo/core/public-pages/server';

Un seul écrivain dans le registre. Enregistrer à la fois dans register-server.js et dans public-page.js laisserait la copie compilée hors condition react-server gagner une course au rechargement à chaud, et la panne reviendrait par intermittence.

Restrictions

  • Le moduleName api est réservé aux routes API et lève à l'enregistrement.
  • Un seul composant par moduleName : un second appel écrase le premier.
  • Un module qui n'expose pas ./public-page ne peut rien enregistrer d'utile : son composant ne sera jamais atteint par le graphe RSC de la route.
  • Ne jamais placer un @lrvo/module-* dans serverExternalPackages : le package sortirait du bundler et ses frontières client disparaîtraient du manifeste.

Notes

Le routage interne appartient au module. Le core ne fait que router le premier segment, ce qui évite de figer une convention d'URL par module dans le core.

Sur cette page