Routing and Pages

Vext keeps URL ownership in src/routes/**. Page files are render targets, not automatic URL definitions.

Mental Model

request URL
  -> src/routes/** handler
  -> app.services / business data
  -> res.render(page, props, options)
  -> src/frontend/pages/** page component
  -> SSR HTML + browser hydration

This keeps service access on the server and keeps the browser graph limited to frontend files.

Page IDs

Page ids are relative to src/frontend/pages/**:

FilePage id
src/frontend/pages/index.tsxindex
src/frontend/pages/about.tsxabout
src/frontend/pages/admin/dashboard.tsxadmin/dashboard
src/frontend/pages/error/default.tsxerror/default

Rendering From a Route

export default (app) => {
  app.get("/users/:id", {}, async (req, res) => {
    const user = await app.services.users.get(req.params.id);
    res.render("users/detail", { user }, {
      head: {
        title: `${user.name} - Users`,
      },
    });
  });
};

res.render(page, props?, options?) means:

ArgumentMeaning
pagePage id under src/frontend/pages/**, without extension.
propsJSON-safe data prepared on the server and reused by hydration.
optionsStatus, head, layout data, locale/messages, nonce, and render behavior.

Route Files Stay Server-only

Do this:

// src/routes/dashboard.ts
const metrics = await app.services.metrics.summary();
res.render("dashboard", { metrics });

Do not do this:

// src/frontend/pages/dashboard.tsx
import { db } from "../../services/db";

When frontend.build.diagnostics.leakScan is enabled, Vext reports the importer, import specifier, resolved path, and a plain-language fix.