Core and pages
Routing, layouts, and how a Nexa page is declared top to bottom.
File-based routing
pages/index.tsx -> /
pages/about.tsx -> /about
pages/products/[slug].tsx -> /products/:slug
pages/[locale]/products/[slug].tsx -> /:locale/products/:slugEvery .tsx under src/pages/ is a real route. [slug].tsx captures a dynamic segment; [locale] is the special segment that turns on i18n for that whole branch.
A page, top to bottom
export const load = { url: "/products/:slug" };
export const paths = { url: "/products" };
export const seo = {
title: `${data.name} | Mi Tienda`,
canonical: `/products/${params.slug}`
};
function buy() { cart.add(data.id); }
export default function ProductPage() {
return (
<article>
<h1>{data.name}</h1>
<button onClick={buy}>Comprar</button>
</article>
);
}load/paths/seo/schema are literal objects/templates — never executed; nexa-parser recognizes them as fixed patterns and resolves them itself against real data at build/request time. That's why they can't hold logic: no conditionals, no .map(), no functions of your own.
load() — fetching data
export const load = { url: "/products/:slug" };
// GET NEXA_API_URL + /products/iphone-17
// data.* queda disponible en toda la páginaA real HTTP GET against NEXA_API_URL + url, with :params substituted by the route's real values. The backend can be any language — Nexa only expects JSON back.
paths() — pre-rendering dynamic routes
export const paths = { url: "/products" };
// backend responde: [{ "slug": "iphone-17" }, { "slug": "pixel-10" }]
// -> dist/products/iphone-17/index.html
// -> dist/products/pixel-10/index.htmlSame shape as load, but the backend responds with an array of parameter sets. nexa build generates a real index.html for each entry — plain HTML, with no dependency on the backend still running.
<For> — iterating lists
export const load = { url: "/articles" };
export default function Articles() {
return (
<ul>
<For each={data.items}>
{(item) => (
<li><a href={`/articles/${item.slug}`}>{item.title}</a></li>
)}
</For>
</ul>
);
}Not a component or an HTML element — a fixed syntactic pattern, just like t("key"). Rust classifies the callback's body once (it's a template) and renders it as real HTML once per array element.
Automatic active link
<nav>
<a href="/">Inicio</a>
<a href="/dashboard" data-nexa-match="prefix">Panel</a>
</nav>
<!-- en /dashboard/settings, el segundo <a> recibe aria-current="page" -->An <a href> whose href matches the current route gets aria-current="page" on its own — no conditional to write (a Nexa page has no logic of its own to write one in). data-nexa-match="prefix" turns it into prefix matching, with a segment boundary.
Shared layouts
export default function Layout() {
return (
<div class="site-shell">
<header><nav><a href="/">Inicio</a></nav></header>
<div data-nexa-slot></div>
<footer><p>© 2026</p></footer>
</div>
);
}src/layout.tsx, optional, wraps every project page's HTML. It compiles just like any page — this isn't composition: nexa-cli finds the single empty data-nexa-slot and inserts the page's already-rendered HTML there, verbatim.
export const head
export const head = {
icon: "/static/logo.svg",
appleTouchIcon: "/static/icon-180.png",
stylesheets: ["/static/site.css"],
meta: [{ name: "google-site-verification", content: "abc123" }]
};
export default function Layout() { /* ... */ }The only thing that actually reaches the document's real <head> (everything else the layout returns ends up inside <body>, via the slot splice). icon/appleTouchIcon/stylesheets/meta — a literal object, same pattern as seo/schema.
Header with real state: an island in the layout
// src/layout.tsx
<div data-nexa-island="siteHeader" data-nexa-strategy="load">
<header><nav><a href="/">Inicio</a></nav></header>
</div>
<div data-nexa-slot></div>A direct onClick in src/layout.tsx isn't supported (node ids would collide between the layout and the page). An island is — the client reads specifier/props/strategy from the DOM, with no activation-manifest entry, and survives SPA navigation without remounting.