Núcleo y páginas
Rutas, layouts, y cómo se declara una página de Nexa de arriba a abajo.
Rutas por archivo
pages/index.tsx -> /
pages/about.tsx -> /about
pages/products/[slug].tsx -> /products/:slug
pages/[locale]/products/[slug].tsx -> /:locale/products/:slugCada .tsx bajo src/pages/ es una ruta real. [slug].tsx captura un segmento dinámico; [locale] es el segmento especial que activa i18n en toda la rama.
Una página, de arriba a abajo
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 son objetos/plantillas literales — nunca se ejecutan, nexa-parser los reconoce como patrones y los resuelve él mismo contra datos reales en build/request. Por eso no pueden tener lógica: sin condicionales, sin .map(), sin funciones propias.
load() — traer datos
export const load = { url: "/products/:slug" };
// GET NEXA_API_URL + /products/iphone-17
// data.* queda disponible en toda la páginaUn GET HTTP real contra NEXA_API_URL + url, con :params sustituidos por los valores reales de la ruta. El backend puede ser cualquier lenguaje — Nexa solo espera JSON de vuelta.
paths() — pre-renderizar rutas dinámicas
export const paths = { url: "/products" };
// backend responde: [{ "slug": "iphone-17" }, { "slug": "pixel-10" }]
// -> dist/products/iphone-17/index.html
// -> dist/products/pixel-10/index.htmlMisma forma que load, pero el backend responde con un array de sets de parámetros. nexa build genera un index.html real por cada entrada — HTML puro, sin depender de que el backend siga corriendo.
<For> — iterar listas
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>
);
}No es un componente ni un elemento HTML — es un patrón sintáctico fijo, igual que t("clave"). Rust clasifica el cuerpo del callback una sola vez (es una plantilla) y lo renderiza como HTML real una vez por elemento.
Link activo automático
<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" -->Un <a href> cuyo href coincide con la ruta actual recibe aria-current="page" solo — sin escribir ningún condicional (una página Nexa no admite lógica propia). data-nexa-match="prefix" lo vuelve por prefijo, con límite de segmento.
Layouts compartidos
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, opcional, envuelve el HTML de toda página del proyecto. Se compila igual que cualquier página — no es composición: nexa-cli busca el único data-nexa-slot vacío y ahí inserta, tal cual, el HTML ya renderizado de la página.
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() { /* ... */ }Lo único que sí llega al <head> real del documento (todo lo demás del layout termina dentro de <body>, por el splice del slot). icon/appleTouchIcon/stylesheets/meta — un objeto literal, mismo patrón que seo/schema.
Header con estado real: una isla en el 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>Un onClick directo en src/layout.tsx no se soporta (ids de nodo colisionarían entre layout y página). Una isla sí — el cliente lee specifier/props/estrategia del DOM, sin entrada en el manifiesto de activación, y sobrevive la navegación SPA sin remount.