Contenu public et templates
Servir du contenu à la racine du site par un résolveur, et l'envelopper dans un template de rendu.
Deux mécanismes distincts se combinent pour afficher un contenu CMS :
- Un résolveur décide quel module répond à une URL. Voir
core/public-content. - Un template décide de la mise en page du contenu résolu. Voir
core/templates.
Il existe un troisième namespace, /lrvo/<module>/…, réservé aux pages techniques d'un module. Voir Pages publiques.
Servir du contenu à la racine
Le projet généré par @lrvo/start expose un seul catch-all qui délègue au registre.
// app/(public)/[...slug]/page.js — généré par @lrvo/start
import { resolvePublicContent, resolvePublicMetadata } from '@lrvo/core/public-content';
import { notFound } from 'next/navigation';
export async function generateMetadata({ params }) {
const { slug = [] } = await params;
return (await resolvePublicMetadata(slug)) ?? {};
}
export default async function PublicPage({ params }) {
const { slug = [] } = await params;
const match = await resolvePublicContent(slug);
if (!match) notFound();
const { Component, props } = match;
return <Component {...props} />;
}Enregistrer un résolveur
Depuis register-server.js du module.
import { registerPublicContentResolver } from '@lrvo/core/public-content';
import { resolvePostPublic, resolvePostMetadata } from './public/PostPublicView.server.js';
registerPublicContentResolver({
name: 'posts',
priority: 10,
resolve: resolvePostPublic,
generateMetadata: resolvePostMetadata,
});// src/public/PostPublicView.server.js
import { getTemplate } from '@lrvo/core/templates';
import { getPublicPostBySlug } from '../db.server.js';
export async function resolvePostPublic(segments) {
if (segments.length !== 2) return null; // ne matche que [postType, slug]
const [postType, slug] = segments;
const post = await getPublicPostBySlug({ postType, slug });
if (!post) return null;
const template = getTemplate({ kind: 'post', id: post.template || 'default', postType });
if (!template) return null;
return {
Component: template.Component,
props: { post, blocks: post.blocks },
};
}Un résolveur retourne null quand il ne reconnaît pas le contenu. C'est ce qui laisse le suivant tenter sa chance. Un résolveur qui rendrait lui-même une page 404 court-circuiterait toute la chaîne, et aucun autre module ne pourrait plus répondre.
Priorités
Les résolveurs sont essayés par priorité décroissante.
| Priorité | Type | Exemple |
|---|---|---|
0 | Fallback hiérarchique, tente n'importe quel chemin. | @lrvo/module-pages, qui descend la chaîne parent_id. |
10 et plus | Préfixe spécifique, ne matche qu'une forme connue. | @lrvo/module-posts, qui ne matche qu'un premier segment correspondant à une clé de type déclarée. |
Un résolveur qui matche large doit avoir une priorité basse, sinon il capture les URL destinées aux autres.
À la fin de la chaîne, le catch-all répond notFound().
Enregistrer un template
Un template enveloppe le rendu : en-tête, mise en page, branding.
import { registerTemplate } from '@lrvo/core/templates';
import Content from '@/components/lrvo/templates/content';
registerTemplate({
id: 'default',
label: 'Par défaut',
kind: 'page',
Component: ({ page, blocks }) => (
<article className="mx-auto max-w-3xl px-6 py-16">
<h1 className="text-3xl font-semibold">{page.title}</h1>
<Content blocks={blocks} />
</article>
),
});kind | Props reçues |
|---|---|
'page' | { page, blocks, url, breadcrumb } |
'post' | { post, blocks } |
Un template de post exige en plus un postType.
Le template est responsable du rendu des blocs. Aucun children n'est injecté implicitement : il importe son propre <Content> et contrôle entièrement sa JSX.
Templates du projet hôte
Le projet ajoute ses templates en déposant un fichier dans components/lrvo/templates/<id>.js, exportant meta et un composant par défaut.
// components/lrvo/templates/landing.js
export const meta = { id: 'landing', label: 'Page d\'atterrissage', kind: 'page' };
export default function LandingTemplate({ page, blocks }) {
return (
<main>
<Hero title={page.title} />
<Content blocks={blocks} />
</main>
);
}npx lrvo-modules sync détecte le fichier et génère app/.lrvo/templates.server.js, qui appelle registerTemplate au boot.
Rendre les blocs
Trois niveaux, du plus simple au plus contrôlé.
Rendu par défaut : l'apparence fournie par le core.
import BlockListRenderer from '@lrvo/core/blocks-registry/render';
<BlockListRenderer blocks={blocks} /><Content> maison : la voie recommandée. Le projet écrit components/lrvo/templates/content.js et assemble les helpers du core avec sa propre JSX par type de bloc.
import { renderInline, groupListItems, anchorProps, CustomBlockRender, isCustomBlockType }
from '@lrvo/core/blocks-registry/render';Blocs personnalisés seuls : CustomBlockList, quand le dispatch est fait en amont.
Ne pas réutiliser le BlockEditor en lecture seule pour l'affichage public : il embarquerait tout le code d'édition dans le bundle et poserait une frontière client sur toute la page. Voir Registre de blocs.
Lister les templates dans l'admin
listTemplates retourne les métadonnées seules, sans le composant, ce qui convient à un <Select>.
const options = listTemplates({ kind: 'post', postType: 'blogue' });Ce registre étant purement serveur, le formulaire d'admin ne le lit pas depuis le navigateur : il passe par une route REST du module, qui appelle listTemplates côté serveur.
Pièges
Ne pas self-fetcher. Un résolveur tourne déjà sur le serveur : il lit la base directement. Un fetch vers /lrvo/api/* du même processus ajoute de la latence et se fait bloquer par le reverse proxy en production, ce qui fait disparaître la section en silence.
Un postType est obligatoire sur un template de post. registerTemplate lève au boot sans lui, plutôt que de laisser le premier rendu échouer.
Les images imbriquées dans des colonnes doivent être parcourues récursivement. Utiliser flattenBlocks et mapBlocksDeep de @lrvo/core/blocks-registry/walk, sinon le contenu des colonnes est silencieusement ignoré à la normalisation média.