ENbext.dev
DocumentationPRISM (natif bext)

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

Sur cette page

Cadre de modèle

Le package @bext-stack/framework est le framework intégré de bext pour la création de sites rendus par un serveur. Il rend les pages sous forme de chaînes HTML pures – pas de React, pas de DOM virtuel, pas d'hydratation. Les pages se chargent instantanément car elles ne contiennent aucun JavaScript côté client par défaut.

Chaque site bext (docs. bext.dev, blog. bext.dev, bext.dev lui-même) est construit avec ce framework.

Comment ça marche#

  1. Vous définissez des pages (contenu) et un modèle (shell de mise en page)
  2. createSite() les connecte ensemble et enregistre un gestionnaire globalThis.__fetch
  3. Le serveur Rust de bext appelle __fetch à partir de son pool de tâches V8 pour chaque requête
  4. La réponse est un BextResponse sérialisé JSON avec le statut, les en-têtes et le corps

Aucune étape de construction n'est requise — bext regroupe votre TypeScript avec Bun et l'évalue dans la V8. Mais vous pouvez ajouter une étape de construction pour Tailwind CSS ou un autre prétraitement.

Conseil

Si vous avez besoin de mises en page imbriquées ou de segments d'itinéraire dynamiques ( [slug], [...path] ), utilisez defineRoutes() à partir de la page Routage au lieu du tableau plat routes. L'option router dans createSite() est prioritaire sur pages + routes plat.

Installation#

Le framework est livré sous forme de package local dans votre projet bext. Référencez-le via l'alias de chemin :

json
// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@bext-stack/framework/*": ["../shared/framework/*"],
      "@bext-stack/framework": ["../shared/framework/src/index.ts"]
    }
  }
}

API createSite()#

Le point d’entrée pour chaque site modèle. Appelez-le dans votre server/entry.ts :

typescript
import { createSite } from "@bext-stack/framework/entry";
import * as pages from "./pages";
import * as template from "./template";

createSite({
  pages,
  template,
  hostname: "my-site.bext.dev",
});

Options de configuration complètes#

typescript
interface SiteConfig {
  /** Page content provider (getPage, getPagePaths). */
  pages?: PagesModule;

  /** Template that wraps pages in an HTML shell. */
  template?: TemplateModule;

  /** Hostname for SEO generation and URL resolution. */
  hostname: string;

  /** Flat route handlers (legacy). First non-null response wins. */
  routes?: RouteHandler[];

  /** File-based router with nested layouts (preferred over flat routes). */
  router?: { handle(ctx: RouteContext): BextResponse | null };

  /** SEO config for robots.txt and sitemap.xml. Set to false to disable. */
  seo?: SeoConfig | false;

  /** Default ISR cache TTL in milliseconds. */
  cacheTtlMs?: number;

  /** Generate auth HTML (login/logout buttons) from the current user. */
  authHtml?: (user: AuthUser | null) => string;

  /** Initialization function called once at startup. */
  init?: () => void;
}

Ordre de traitement des demandes#

Lorsqu'une demande arrive, createSite la traite dans cet ordre :

  1. Itinéraires SEO — /robots.txt et /sitemap.xml (si seo n'est pas false )
  2. Routeur — config.router.handle(ctx) (si un routeur est configuré)
  3. Itinéraires plats — chaque gestionnaire config.routes[], premières victoires non nulles
  4. Pages statiques — config.pages.getPage(path) rendues via config.template
  5. 404 — page de secours introuvable

contrat pages.ts#

Le module Pages fournit du contenu pour votre site. Il doit exporter getPage() et éventuellement getPagePaths() :

typescript
import type { Page, PageMap } from "@bext-stack/framework";

const pages: PageMap = {
  "/": {
    title: "Home",
    description: "Welcome to my site",
    html: "<h1>Hello World</h1><p>This is my site.</p>",
  },
  "/about": {
    title: "About",
    description: "About us",
    html: "<h1>About</h1><p>We build things.</p>",
  },
  "/contact": {
    title: "Contact",
    description: "Get in touch",
    html: "<h1>Contact</h1><p>Email us at hello@example.com</p>",
  },
};

export function getPage(path: string): Page | null {
  return pages[path] ?? null;
}

export function getPagePaths(): string[] {
  return Object.keys(pages);
}

Type de page#

typescript
interface Page {
  title: string;
  description: string;
  html: string;  // Raw HTML content (no layout wrapper)
}

contrat template.ts#

Le module de modèle enveloppe le contenu de la page dans un shell de document HTML complet :

typescript
import type { Page, RenderContext } from "@bext-stack/framework";

export function renderPage(page: Page, ctx: RenderContext): string {
  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>${page.title} — My Site</title>
  <meta name="description" content="${page.description}">
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <nav>
    <a href="/" class="${ctx.path === "/" ? "active" : ""}">Home</a>
    <a href="/about" class="${ctx.path === "/about" ? "active" : ""}">About</a>
    <a href="/contact" class="${ctx.path === "/contact" ? "active" : ""}">Contact</a>
    ${ctx.authHtml}
  </nav>
  <main>${page.html}</main>
  <footer>&copy; 2026 My Site</footer>
</body>
</html>`;
}

Type de contexte de rendu#

typescript
interface RenderContext {
  /** Current request path (e.g., "/about"). */
  path: string;

  /** Pre-rendered auth HTML (login/logout buttons). */
  authHtml: string;

  /** Authenticated user, or null for anonymous visitors. */
  user: AuthUser | null;

  /** Full request object with headers, method, body, etc. */
  request: BextRequest;
}

Gestionnaires de routes#

Pour les routes dynamiques (API, soumissions de formulaires, redirections), utilisez des gestionnaires de routes :

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

const apiProducts: RouteHandler = (ctx) => {
  if (ctx.path !== "/api/products") return null;

  if (ctx.request.method === "GET") {
    return json([
      { id: 1, name: "Widget" },
      { id: 2, name: "Gadget" },
    ]);
  }

  if (ctx.request.method === "POST") {
    const body = JSON.parse(ctx.request.body ?? "{}");
    return json({ created: true, name: body.name }, 201);
  }

  return null;
};

const legacyRedirects: RouteHandler = (ctx) => {
  if (ctx.path === "/old-page") return redirect("/new-page", 301);
  if (ctx.path === "/legacy") return redirect("/");
  return null;
};

Connectez-les à votre site :

typescript
createSite({
  pages,
  template,
  hostname: "my-site.bext.dev",
  routes: [apiProducts, legacyRedirects],
});

Type de contexte de route#

typescript
interface RouteContext {
  request: BextRequest;
  path: string;
  url: URL;
  user: AuthUser | null;
  authHtml: string;
  query: URLSearchParams;
  params: Record<string, string>;
}

Type de requête bext#

typescript
interface BextRequest {
  method: string;
  url: string;
  pathname: string;
  headers: [string, string][];
  body: string | null;
  tenant_id: string | null;
  site_id: string | null;
  auth_user: AuthUser | null;
  client_ip: string | null;
  locale: string | null;
  is_bot: boolean;
}

Aides à la réponse#

Le framework fournit des fonctions d'usine pour les types de réponses courants :

typescript
import { json, html, text, xml, redirect, notFound, cached } from "@bext-stack/framework";

// JSON response (200 by default)
json({ message: "ok" })
json({ error: "not found" }, 404)

// HTML response
html("<h1>Hello</h1>")
html("<h1>Error</h1>", 500)

// Plain text
text("Hello, world")

// XML (for feeds, APIs)
xml("<rss>...</rss>")

// Redirects (302 by default, supports 301/307/308)
redirect("/new-location")
redirect("/permanent", 301)

// 404 with optional body
notFound()
notFound("Custom not found message")

// Wrap any response with ISR cache hints
cached(json({ data: "expensive" }), 60_000)             // 60s TTL
cached(html("<h1>Cached</h1>"), 3600_000, ["page:home"]) // 1h TTL with cache tags

Type de réponse bext#

typescript
interface BextResponse {
  status: number;
  headers: [string, string][];
  body: string;
  cache?: CacheHint;
}

interface CacheHint {
  enabled: boolean;
  ttl_ms?: number;    // Time-to-live in milliseconds
  swr_ms?: number;    // Stale-while-revalidate window
  tags?: string[];    // Cache tags for targeted invalidation
}

Génération automatique SEO#

Lorsque seo est configuré (ou est par défaut { hostname } lorsqu'il n'est pas explicitement défini), bext génère automatiquement :

  • /robots.txt — User-agent: *, Allow: /, référence du plan du site et chemins Disallow facultatifs
  • /sitemap.xml — tous les chemins de page de getPagePaths() plus tout additionalPaths
typescript
createSite({
  pages,
  template,
  hostname: "my-site.bext.dev",
  seo: {
    hostname: "my-site.bext.dev",
    additionalPaths: ["/api/feed"],       // Extra URLs for the sitemap
    disallowPaths: ["/admin", "/api"],    // Block from robots.txt
  },
});

Pour désactiver complètement la génération SEO :

typescript
createSite({
  // ...
  seo: false,
});

Exemple de site minimal (3 fichiers)#

Un site de modèles bext complet en trois fichiers :

serveur/entrée.ts#

typescript
import { createSite } from "@bext-stack/framework/entry";
import * as pages from "./pages";
import * as template from "./template";

createSite({
  pages,
  template,
  hostname: "hello.bext.dev",
});

serveur/pages.ts#

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

const content: Record<string, Page> = {
  "/": {
    title: "Home",
    description: "A minimal bext site",
    html: "<h1>Hello from bext</h1><p>This site renders in under 50 microseconds.</p>",
  },
  "/about": {
    title: "About",
    description: "About this site",
    html: "<h1>About</h1><p>Built with @bext-stack/framework — zero JavaScript shipped to the browser.</p>",
  },
};

export function getPage(path: string): Page | null {
  return content[path] ?? null;
}

export function getPagePaths(): string[] {
  return Object.keys(content);
}

serveur/template.ts#

typescript
import type { Page, RenderContext } from "@bext-stack/framework";

export function renderPage(page: Page, ctx: RenderContext): string {
  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>${page.title}</title>
  <meta name="description" content="${page.description}">
</head>
<body>
  <nav><a href="/">Home</a> | <a href="/about">About</a></nav>
  <main>${page.html}</main>
</body>
</html>`;
}

Site complexe avec itinéraires#

Un site avec des routes API, une authentification, une mise en cache et des redirections :

typescript
import { createSite } from "@bext-stack/framework/entry";
import { json, redirect, cached, html, notFound } from "@bext-stack/framework";
import type { RouteHandler, AuthUser } from "@bext-stack/framework";
import { dbQuery } from "@bext-stack/framework/bridge";
import * as pages from "./pages";
import * as template from "./template";

// API: list products from SQLite
const productsApi: RouteHandler = (ctx) => {
  if (!ctx.path.startsWith("/api/products")) return null;

  if (ctx.path === "/api/products" && ctx.request.method === "GET") {
    const { rows } = dbQuery("./data.db", "SELECT * FROM products LIMIT 50");
    return cached(json(rows), 30_000, ["products"]);
  }

  // Single product
  const match = ctx.path.match(/^\/api\/products\/(\d+)$/);
  if (match) {
    const { rows } = dbQuery("./data.db", "SELECT * FROM products WHERE id = ?", [match[1]]);
    if (!rows?.length) return notFound("Product not found");
    return json(rows[0]);
  }

  return null;
};

// Auth-gated admin
const adminRoute: RouteHandler = (ctx) => {
  if (!ctx.path.startsWith("/admin")) return null;
  if (!ctx.user) return redirect("/login");
  if (!ctx.user.isSuperAdmin) return json({ error: "Forbidden" }, 403);
  return html("<h1>Admin Dashboard</h1><p>Welcome, " + ctx.user.firstName + "</p>");
};

// Legacy URL redirects
const redirects: RouteHandler = (ctx) => {
  const map: Record<string, string> = {
    "/old-pricing": "/pricing",
    "/v1/docs": "/docs",
  };
  const target = map[ctx.path];
  return target ? redirect(target, 301) : null;
};

createSite({
  pages,
  template,
  hostname: "my-app.bext.dev",
  routes: [productsApi, adminRoute, redirects],
  cacheTtlMs: 60_000,
  authHtml: (user) =>
    user
      ? `<span>Hi, ${user.firstName}</span> <a href="/logout">Logout</a>`
      : `<a href="/login">Login</a>`,
});

## Related

- [Routing & Layouts](/frameworks/routing) — `defineRoutes()` for nested file-based routing
- [Native IO Bridge](/frameworks/bridge) — `dbQuery`, `readFile`, and other server-side IO in route handlers
- [Tailwind CSS](/frameworks/tailwind) — generating CSS from your template source files
- [Client Runtime](/frameworks/client-runtime) — SPA-like navigation and live reload for template sites
- [PRISM Framework](/frameworks/prism) — the JSX-first alternative to string-template rendering
Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.