Routage & Rendu
Les routes vivent dans src/app/ sous la forme d'un arbre fondé sur les fichiers. L'imbrication des
répertoires correspond à l'imbrication des URL, et des noms de fichiers spéciaux définissent ce que
rend chaque segment.
Fichiers de route
| Fichier | Rôle |
|---|---|
page.tsx |
La page. L'export par défaut est le composant Page ; props { params, searchParams, data?, actionData? }. |
layout.tsx |
Enveloppe son sous-arbre : ({ children, route }) => Renderable, où route vaut { pathname, host }. S'imbrique par répertoire. Le layout racine possède <html>. |
route.ts |
Gestionnaire HTTP. Exportez GET / POST / PUT / PATCH / DELETE / OPTIONS / HEAD, chacune de type (request, { params }). Voir « Routes API » ci-dessous. |
loading.tsx |
Fallback Suspense pour le segment. |
error.tsx |
Frontière d'erreur : ({ error, reset, route }). |
not-found.tsx |
Rendue dans les layouts quand un loader ou une action retourne ou lance une Response 404, ou lance notFound(). |
template.tsx |
Comme un layout, enveloppe la page à l'intérieur des layouts. |
Pour loading.tsx, error.tsx, not-found.tsx et template.tsx, c'est le fichier le plus proche,
en remontant depuis le répertoire de la page, qui s'applique. Un fichier de page peut aussi exporter
des fonctions de données (loader / action), voir Loaders & Actions.
Un répertoire dont le nom commence par _ ou . n'est jamais routé : il peut contenir des
composants ou des utilitaires propres à un segment.
Le composant Page
L'export par défaut de page.tsx reçoit :
/** @jsxImportSource @bext-stack/framework */
export default function Page({ params, searchParams, data, actionData }) {
return <h1>Hello {params.slug}</h1>;
}
searchParams associe chaque paramètre de requête à une chaîne, ou à un tableau de chaînes quand il
est répété. La Page ne voit jamais la request brute : seuls loader et action la voient.
Lisez-y les cookies et en-têtes, puis transmettez ce dont vous avez besoin via data / actionData.
Routes API
Un route.ts placé sous src/app/api/ répond à l'URL correspondante :
src/app/api/posts/route.ts répond à /api/posts, src/app/api/posts/[id]/route.ts à
/api/posts/42.
// src/app/api/posts/[id]/route.ts
export async function GET(request, { params }) {
return { id: params.id }; // envoyé en JSON, statut 200
}
export async function POST(request, { params }) {
const body = await request.json();
return new Response(JSON.stringify({ id: params.id, ...body }), {
status: 201,
headers: { "content-type": "application/json" },
});
}
request se lit comme une Request Web : method, url, headers.get(), json(), text(),
formData() (urlencoded et multipart) et arrayBuffer().
- Une méthode sans export reçoit un
405avec{ "error": "method <MÉTHODE> not allowed" }, sauf si le fichier a un export par défaut, qui traite alors toutes les méthodes. - Retourner une
Responsel'envoie telle quelle. Retourner un objet{ status, headers, body }construit par les helpers de réponse (voir plus bas) l'envoie aussi. Toute autre valeur part en réponse JSON200. - Une erreur non interceptée devient une réponse JSON
500{ "error": "<message>" }. - Une URL sous
/api/qu'aucunroute.tsne couvre renvoie un 404, sans essayer les pages.
Un route.ts placé hors de src/app/api/ (par exemple src/app/flux.xml/route.ts) sert de
repli : il ne répond que si aucune page ne correspond à l'URL, et seulement pour les méthodes qu'il
exporte. Une autre méthode reçoit un 404, pas un 405. Un route.ts
posé à côté d'un page.tsx est ignoré : la page garde l'URL.
Routes dynamiques, catch-all, groupes et parallèles
| Motif | Signification | URL d'exemple |
|---|---|---|
[slug] |
Segment dynamique → params.slug |
/posts/hello |
[...slug] |
Catch-all → params.slug, le reste du chemin en une seule chaîne ("a/b/c") |
/docs/a/b/c |
(group) |
Regroupement uniquement, aucun effet sur les URL des pages | (marketing)/posts → /posts |
@name/page.tsx |
Emplacement de route parallèle nommée | transmis au layout dans la prop name |
La priorité se décide segment par segment, de gauche à droite : un segment littéral passe avant
[param], qui passe avant [...rest]. La route qui en dit le plus sur le début de l'URL l'emporte.
Un catch-all répond aussi à son propre préfixe (docs/[...slug] répond à /docs avec
params.slug === ""), mais seulement quand aucune autre route ne correspond exactement.
La page d'un emplacement parallèle reçoit les mêmes props que la page, est rendue en HTML puis
transmise au layout le plus profond dans une prop portant le nom de l'emplacement (pour @modal, le
layout lit ({ children, modal })). Un emplacement dont le rendu échoue reste vide.
Les routes d'interception de Next.js ((.)dossier, (..)dossier) ne sont pas prises en charge :
les pages qu'elles contiennent ne sont pas routées.
Métadonnées
Ajoutez des balises head avec un export metadata, ou generateMetadata() quand elles dépendent de
l'URL :
export const metadata = {
title: "Posts",
description: "All posts",
};
// or, dynamic:
export async function generateMetadata({ params, searchParams, route, status }) {
const post = await getPost(params.slug);
return { title: post.title };
}
generateMetadata reçoit { params, searchParams, route, status }, pas les données du loader.
route vaut { pathname, host } ; status vaut 200 pour un rendu normal, 404 pour le rendu de
not-found.tsx et 500 pour celui d'error.tsx.
Les layouts peuvent aussi exporter metadata ou generateMetadata. Les niveaux sont fusionnés du
layout racine vers la page : pour chaque clé, le niveau le plus profond l'emporte, et un objet
(openGraph, robots…) est remplacé en entier, pas fusionné. title accepte une chaîne ou
{ default, template, absolute } : un layout qui déclare template: "%s | Mon site" formate les
titres des niveaux en dessous. Pour une 404 ou une erreur, seuls les layouts fournissent les
métadonnées.
Quand un même fichier exporte les deux, le metadata statique l'emporte. Si generateMetadata
lève une erreur, ce niveau est ignoré et la page se rend quand même avec les métadonnées des autres
niveaux.
Les fichiers statiques icon.*, apple-icon.*, opengraph-image.* et twitter-image.* placés dans
src/app/ (ou un sous-répertoire) sont servis à l'URL correspondante, et la balise <link> ou
<meta> associée est ajoutée au head des pages concernées. sitemap.xml et robots.txt à la racine
de src/app/ sont servis tels quels.
Modes de rendu
Le mode de rendu du site se définit dans bext.config.toml :
[rendering]
mode = "isr" # seule la valeur "isr" active le cache de pages
revalidate = 60 # en secondes, 60 si absent
- Sans
mode = "isr", chaque requête est rendue. isr: régénération statique incrémentale. Le rendu est mis en cache et revalidé aprèsrevalidatesecondes ; pendant une courte fenêtre après expiration, la copie périmée est servie tandis qu'un nouveau rendu s'exécute en arrière-plan.
Sur un site ISR, une route peut fixer sa propre durée de cache avec export const revalidate = 30
(en secondes), et sa fenêtre de copie périmée avec export const swr = 120 (en secondes, pris en
compte avec revalidate).
En ISR, les requêtes qui portent un cookie de session ou un en-tête Authorization ne sont ni
servies depuis le cache de pages ni stockées dedans : les visiteurs connectés obtiennent toujours
un rendu frais.
Indépendamment du mode, le résultat d'un loader est aussi mis en cache jusqu'à 15 secondes pour les
requêtes GET sans aucun cookie, quand le loader ne lit aucun en-tête de requête et retourne une
valeur sérialisable en JSON. Excluez une route avec force-dynamic (ci-dessous) quand elle doit
toujours être à jour, par exemple une page atteinte par un lien à jeton.
Désactiver la mise en cache
export const dynamic = "force-dynamic";
// or
export const revalidate = false;
L'un ou l'autre export exclut la route du cache de pages et du cache automatique des loaders
(revalidate = 0 exclut aussi la route du cache des loaders).
Streaming
Une route diffuse son HTML en streaming dès que l'une de ces conditions est vraie :
- elle importe
Suspensedepuis@bext-stack/framework/streaming, - elle a un
loading.tsxà côté de sonpage.tsx, - son export par défaut est un générateur asynchrone (
export default async function* Page()), ou - elle exporte
export const renderingMode = "streaming".
export const renderingMode = "buffered" désactive le streaming pour une route qui serait sinon
diffusée. force-dynamic ne désactive pas le streaming, seulement la mise en cache.
Un loader ou une action qui redirige reste respecté : la réponse n'est engagée qu'une fois le loader et l'action exécutés.
Helpers de réponse
Le framework exporte des helpers qui construisent des objets de réponse simples
({ status, headers, body }) :
import { json, html, text, xml, redirect, notFound } from "@bext-stack/framework";
| Helper | Signature |
|---|---|
json |
json(data, status = 200) |
html, text, xml |
html(body, status = 200), et de même pour text et xml |
redirect |
redirect(location, status = 302), statut parmi 301, 302, 307, 308 |
notFound |
notFound(body = "Not Found") |
Une route API (route.ts) peut retourner ces objets directement. Ce ne sont pas des instances de
Response : dans un loader ou une action, lancez-les (throw redirect("/login")) pour
court-circuiter le rendu ; retournés, ils deviennent simplement data ou actionData. Voir
Loaders & Actions.
Pour supprimer des entrées en cache par tag, utilisez revalidateTag du point d'entrée cache :
import { revalidateTag } from "@bext-stack/framework/cache";
revalidateTag("products"); // returns the number of entries dropped
Et ensuite
| Page | Ce qu'elle couvre |
|---|---|
| Loaders & Actions | Chargement de données, mutations, accès à la requête/session |
| Présentation du SDK | Structure d'un site, le client createSdk, l'intégration de packages |
| Référence CLI | bext dev, bext build, bext routes |
| Référence de configuration | bext.config.toml |