ENbext.dev
DocumentationPRISM (natif bext)

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

Routage et mises en page imbriqués

Le routeur du bext fournit un routage imbriqué de type système de fichiers avec une composition de mise en page. Définissez les routes de manière déclarative avec defineRoutes() ou impérativement avec createRouter(), puis passez le routeur à createSite().

Architecture#

Le routeur mappe les chemins d'URL vers les composants de page et les gestionnaires d'API, en encapsulant les pages dans une pile de composants de mise en page du plus externe au plus interne :

code
Request: GET /blog/hello-world

Matched: /blog/[slug]

Layout composition:
  RootLayout          ← wraps everything
    └─ BlogLayout     ← wraps /blog/* pages
        └─ BlogPost   ← the page component

Le routeur restitue d'abord la page, puis l'enveloppe dans des mises en page allant du plus interne au plus externe. Le résultat final est une chaîne HTML complète.

createRouter() — API impérative#

Construisez un routeur étape par étape :

typescript
import { createRouter } from "@bext-stack/framework";
import type { RouteContext } from "@bext-stack/framework";

const router = createRouter();

// Root layout wraps all pages
router.layout(({ children, ctx }) => `
  <!DOCTYPE html>
  <html lang="en">
  <head><title>My App</title></head>
  <body>
    <nav><a href="/">Home</a> <a href="/blog">Blog</a></nav>
    <main>${children}</main>
  </body>
  </html>
`);

// Pages
router.page("/", (ctx) => `<h1>Welcome</h1><p>Hello, world!</p>`);
router.page("/about", (ctx) => `<h1>About</h1><p>About us.</p>`);

// Dynamic page
router.page("/blog/[slug]", (ctx) => {
  const slug = ctx.params.slug;
  const post = getPost(slug);
  return `<article><h1>${post.title}</h1>${post.html}</article>`;
});

// API route (no layout wrapping)
router.api("/api/health", (ctx) => ({
  status: 200,
  headers: [["content-type", "application/json"]],
  body: JSON.stringify({ ok: true }),
}));

Méthodes de routeur#

typescript
class Router {
  /** Set the root layout (wraps all pages). */
  layout(component: LayoutComponent): this;

  /** Register a page at a URL pattern with optional extra layouts. */
  page(pattern: string, component: PageComponent, layouts?: LayoutComponent[]): this;

  /** Register an API handler (no layout wrapping). */
  api(pattern: string, handler: ApiHandler): this;

  /** Match a request and return the response. */
  handle(ctx: RouteContext): BextResponse | null;
}

DefineRoutes() — API déclarative#

Pour les applications plus volumineuses, defineRoutes() vous permet de déclarer l'intégralité de l'arborescence des routes en tant qu'objet imbriqué :

typescript
import { defineRoutes } from "@bext-stack/framework";

const router = defineRoutes({
  layout: RootLayout,
  routes: {
    "/": { page: HomePage },
    "/about": { page: AboutPage },
    "/blog": {
      layout: BlogLayout,
      page: BlogListPage,
      children: {
        "/[slug]": { page: BlogPostPage },
      },
    },
    "/docs": {
      layout: DocsLayout,
      children: {
        "/[...path]": { page: DocsPage },
      },
    },
    "/api/users": { handler: usersHandler },
    "/api/users/[id]": { handler: userByIdHandler },
  },
});

Types RouteTree et RouteNode#

typescript
interface RouteTree {
  /** Root layout that wraps all pages. */
  layout?: LayoutComponent;
  /** Route definitions. Keys are URL patterns. */
  routes: Record<string, RouteNode>;
}

interface RouteNode {
  /** Page component for this route. */
  page?: PageComponent;
  /** API handler (mutually exclusive with page). */
  handler?: ApiHandler;
  /** Layout that wraps this route and its children. */
  layout?: LayoutComponent;
  /** Child routes (paths are concatenated with parent). */
  children?: Record<string, RouteNode>;
}

Segments dynamiques#

Paramètres nommés : [id]#

Un segment entre parenthèses correspond à n'importe quel segment d'URL et le capture en tant que paramètre :

typescript
router.page("/users/[id]", (ctx) => {
  const userId = ctx.params.id;  // "42" for /users/42
  return `<h1>User ${userId}</h1>`;
});

router.page("/blog/[slug]", (ctx) => {
  const slug = ctx.params.slug;  // "hello-world" for /blog/hello-world
  return `<h1>${slug}</h1>`;
});

Fourre-tout : [...path]#

Un segment fourre-tout correspond au reste de l'URL :

typescript
router.page("/docs/[...path]", (ctx) => {
  const docPath = ctx.params.path;  // "getting-started/installation" for /docs/getting-started/installation
  const content = loadDoc(docPath);
  return `<article>${content}</article>`;
});

Paramètres multiples#

typescript
router.page("/repos/[owner]/[repo]", (ctx) => {
  const { owner, repo } = ctx.params;
  // owner = "benfavre", repo = "bext" for /repos/benfavre/bext
  return `<h1>${owner}/${repo}</h1>`;
});

Dispositions imbriquées#

Les mises en page reçoivent children (le contenu interne rendu) et ctx (le contexte de l'itinéraire). Ils enveloppent les enfants dans du HTML supplémentaire :

typescript
import type { RouteContext } from "@bext-stack/framework";

// Root layout — HTML shell
function RootLayout({ children, ctx }: { children: string; ctx: RouteContext }) {
  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>My App</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <header>
    <nav>
      <a href="/">Home</a>
      <a href="/dashboard">Dashboard</a>
      <a href="/settings">Settings</a>
      ${ctx.authHtml}
    </nav>
  </header>
  ${children}
  <footer>&copy; 2026</footer>
</body>
</html>`;
}

// Dashboard layout — sidebar + content area
function DashboardLayout({ children, ctx }: { children: string; ctx: RouteContext }) {
  return `<div class="dashboard">
  <aside class="sidebar">
    <a href="/dashboard" class="${ctx.path === "/dashboard" ? "active" : ""}">Overview</a>
    <a href="/dashboard/analytics" class="${ctx.path === "/dashboard/analytics" ? "active" : ""}">Analytics</a>
    <a href="/dashboard/orders" class="${ctx.path === "/dashboard/orders" ? "active" : ""}">Orders</a>
  </aside>
  <main class="content">${children}</main>
</div>`;
}

// Settings layout — tabs
function SettingsLayout({ children, ctx }: { children: string; ctx: RouteContext }) {
  return `<div class="settings">
  <div class="tabs">
    <a href="/settings/profile" class="${ctx.path.startsWith("/settings/profile") ? "active" : ""}">Profile</a>
    <a href="/settings/billing" class="${ctx.path.startsWith("/settings/billing") ? "active" : ""}">Billing</a>
    <a href="/settings/team" class="${ctx.path.startsWith("/settings/team") ? "active" : ""}">Team</a>
  </div>
  <div class="tab-content">${children}</div>
</div>`;
}

Itinéraires API#

Les routes API utilisent handler au lieu de page. Ils renvoient directement un BextResponse et ne sont jamais enveloppés dans des mises en page :

typescript
import { json, notFound } from "@bext-stack/framework";
import type { RouteContext, BextResponse } from "@bext-stack/framework";

function usersHandler(ctx: RouteContext): BextResponse | null {
  if (ctx.request.method === "GET") {
    const users = getAllUsers();
    return json(users);
  }
  if (ctx.request.method === "POST") {
    const body = JSON.parse(ctx.request.body ?? "{}");
    const user = createUser(body);
    return json(user, 201);
  }
  return json({ error: "Method not allowed" }, 405);
}

function userByIdHandler(ctx: RouteContext): BextResponse | null {
  const id = ctx.params.id;
  const user = getUserById(id);
  if (!user) return notFound("User not found");
  return json(user);
}

Intégration avec createSite()#

Passez le routeur à createSite() via l'option router :

typescript
import { createSite } from "@bext-stack/framework/entry";
import { defineRoutes } from "@bext-stack/framework";

const router = defineRoutes({
  layout: RootLayout,
  routes: {
    "/": { page: HomePage },
    "/about": { page: AboutPage },
    "/blog": {
      layout: BlogLayout,
      page: BlogListPage,
      children: {
        "/[slug]": { page: BlogPostPage },
      },
    },
    "/api/health": { handler: healthHandler },
  },
});

createSite({
  hostname: "my-app.bext.dev",
  router,
});

Lorsqu'un routeur est configuré, il est prioritaire sur pages et routes. L'ordre de traitement est le suivant : itinéraires SEO, routeur, itinéraires plats, pages statiques.

Vous pouvez également utiliser un routeur avec des pages statiques pour une approche hybride :

typescript
createSite({
  pages,              // Static content pages
  template,           // Template for static pages
  hostname: "my-app.bext.dev",
  router,             // Dynamic routes with layouts
  routes: [redirects], // Legacy redirects
});

Exemple complet : application de tableau de bord#

Une application de tableau de bord complète avec des mises en page imbriquées :

typescript
// server/entry.ts
import { createSite } from "@bext-stack/framework/entry";
import { defineRoutes, json, redirect } from "@bext-stack/framework";
import type { RouteContext, BextResponse } from "@bext-stack/framework";

// ─── Layouts ───────────────────────────────────────────────────────────────

function RootLayout({ children, ctx }: { children: string; ctx: RouteContext }) {
  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Acme Dashboard</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <header class="topbar">
    <a href="/" class="logo">Acme</a>
    <nav>
      <a href="/dashboard">Dashboard</a>
      <a href="/settings/profile">Settings</a>
    </nav>
    ${ctx.authHtml}
  </header>
  ${children}
</body>
</html>`;
}

function DashboardLayout({ children, ctx }: { children: string; ctx: RouteContext }) {
  return `<div class="dash-shell">
  <aside class="dash-nav">
    <a href="/dashboard" class="${ctx.path === "/dashboard" ? "active" : ""}">Overview</a>
    <a href="/dashboard/analytics" class="${ctx.path === "/dashboard/analytics" ? "active" : ""}">Analytics</a>
    <a href="/dashboard/orders" class="${ctx.path.startsWith("/dashboard/orders") ? "active" : ""}">Orders</a>
    <a href="/dashboard/customers" class="${ctx.path === "/dashboard/customers" ? "active" : ""}">Customers</a>
  </aside>
  <main class="dash-content">${children}</main>
</div>`;
}

function SettingsLayout({ children, ctx }: { children: string; ctx: RouteContext }) {
  return `<div class="settings-shell">
  <div class="settings-tabs">
    <a href="/settings/profile" class="${ctx.path === "/settings/profile" ? "active" : ""}">Profile</a>
    <a href="/settings/billing" class="${ctx.path === "/settings/billing" ? "active" : ""}">Billing</a>
    <a href="/settings/team" class="${ctx.path === "/settings/team" ? "active" : ""}">Team</a>
  </div>
  <div class="settings-body">${children}</div>
</div>`;
}

// ─── Pages ─────────────────────────────────────────────────────────────────

const HomePage = (ctx: RouteContext) =>
  `<section class="hero"><h1>Welcome to Acme</h1><p>Your all-in-one business platform.</p></section>`;

const DashOverview = (ctx: RouteContext) =>
  `<h1>Overview</h1><div class="stats-grid">
    <div class="stat"><strong>$12,340</strong><span>Revenue</span></div>
    <div class="stat"><strong>342</strong><span>Orders</span></div>
    <div class="stat"><strong>89</strong><span>Customers</span></div>
  </div>`;

const DashAnalytics = (ctx: RouteContext) =>
  `<h1>Analytics</h1><p>Charts and metrics go here.</p>`;

const DashOrders = (ctx: RouteContext) =>
  `<h1>Orders</h1><p>Recent orders listed here.</p>`;

const DashOrderDetail = (ctx: RouteContext) =>
  `<h1>Order #${ctx.params.id}</h1><p>Order details for ${ctx.params.id}.</p>`;

const DashCustomers = (ctx: RouteContext) =>
  `<h1>Customers</h1><p>Customer list here.</p>`;

const SettingsProfile = (ctx: RouteContext) =>
  `<h1>Profile Settings</h1><form><label>Name<input value="${ctx.user?.firstName ?? ""}"></label></form>`;

const SettingsBilling = (ctx: RouteContext) =>
  `<h1>Billing</h1><p>Manage your subscription.</p>`;

const SettingsTeam = (ctx: RouteContext) =>
  `<h1>Team Members</h1><p>Invite and manage team members.</p>`;

// ─── API ───────────────────────────────────────────────────────────────────

function ordersApi(ctx: RouteContext): BextResponse | null {
  if (ctx.request.method === "GET") {
    return json([
      { id: "ORD-001", total: 99.99, status: "shipped" },
      { id: "ORD-002", total: 249.00, status: "processing" },
    ]);
  }
  return json({ error: "Method not allowed" }, 405);
}

// ─── Router ────────────────────────────────────────────────────────────────

const router = defineRoutes({
  layout: RootLayout,
  routes: {
    "/": { page: HomePage },
    "/dashboard": {
      layout: DashboardLayout,
      page: DashOverview,
      children: {
        "/analytics": { page: DashAnalytics },
        "/orders": { page: DashOrders },
        "/orders/[id]": { page: DashOrderDetail },
        "/customers": { page: DashCustomers },
      },
    },
    "/settings": {
      layout: SettingsLayout,
      children: {
        "/profile": { page: SettingsProfile },
        "/billing": { page: SettingsBilling },
        "/team": { page: SettingsTeam },
      },
    },
    "/api/orders": { handler: ordersApi },
  },
});

// ─── Site ──────────────────────────────────────────────────────────────────

createSite({
  hostname: "acme.bext.dev",
  router,
  authHtml: (user) =>
    user
      ? `<span class="user">${user.firstName}</span>`
      : `<a href="/login">Sign in</a>`,
});

Table de routage pour cet exemple#

URL Pile de mise en page Composant
/ Disposition racine Page d'accueil
/dashboard RootLayout > Tableau de bordLayout DashVue d'ensemble
/dashboard/analytics RootLayout > Tableau de bordLayout DashAnalytics
/dashboard/orders RootLayout > Tableau de bordLayout Commandes Dash
/dashboard/orders/ORD-001 RootLayout > Tableau de bordLayout DashOrderDétail
/dashboard/customers RootLayout > Tableau de bordLayout Clients Dash
/settings/profile RootLayout > ParamètresLayout ParamètresProfil
/settings/billing RootLayout > ParamètresLayout ParamètresFacturation
/settings/team RootLayout > ParamètresLayout ParamètresÉquipe
/api/orders (aucun) commandesApi
Conseil

Les routes API (enregistrées avec handler plutôt que page ) ne sont jamais encapsulées dans des mises en page et ne reçoivent aucun shell HTML. Renvoyez un BextResponse directement à l’aide des assistants json(), html() ou redirect().

Remarque

Lorsqu'un router est configuré via createSite(), il est prioritaire sur le tableau plat routes et Module pages. Utilisez router pour les nouveaux projets et réservez des itinéraires plats uniquement pour les anciennes redirections.

En rapport#

Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.