Guía

Sitios estáticos

Una página HTML por archivo de src/routes/. stil run build deja el sitio listo para subir, y solo las páginas con islas llevan JavaScript.

Una línea en vite.config.ts

ts
import ascua from "vite-plugin-ascua";

export default { plugins: [ascua({ site: true })] };

Con eso, cada archivo de src/routes/ es una página, vite la sirve en desarrollo y vite build escribe en dist/ un HTML por ruta. No hay servidor que escribir ni entradas de cliente y servidor: queda una carpeta de archivos para Hull, busybox httpd, tower-http o cualquier CDN.

src/routes/index.ts            →  dist/index.html
src/routes/acerca.ts           →  dist/acerca/index.html
src/routes/pedidos/index.ts    →  dist/pedidos/index.html
src/routes/pedidos/[id].ts     →  dist/pedidos/7/index.html, una por id
src/routes/docs/[...rest].ts   →  dist/docs/guia/inicio/index.html, con sus barras
src/routes/404.ts              →  dist/404.html
src/routes/_layout.ts          envuelve todas las páginas
src/routes/pedidos/_layout.ts  envuelve las de pedidos/, dentro del de la raíz

Un archivo que empieza por _ no es una página. [...rest] va al final y atrapa lo que quede de la URL: /docs/guia/inicio le da rest = "guia/inicio". El nombre entre corchetes lo eliges tú: es el del parámetro que llega a la página.

Una página

ts
// src/routes/acerca.ts
export const title = "Acerca";

export default function Acerca() {
  return view`<section><h1>Acerca</h1></section>`;
}

El default es el componente de la página y title va al <title>. El código de las páginas corre al construir y no llega al navegador: queda como HTML.

Con parámetros

ts
// src/routes/pedidos/[id].ts
export const title = ({ id }: { id: string }) => `Pedido ${id}`;

export function paths() {
  return PEDIDOS.map((p) => ({ id: p.id }));
}

export default function Pedido({ id }: { id: string }) {
  return view`<article><h1>Pedido ${id}</h1></article>`;
}

[id] es un segmento variable, y la página lo recibe como prop. paths() dice qué páginas generar —una por objeto— y puede ser asíncrona. Es obligatoria: sin ella, el build no sabría qué ids existen.

Datos: load()

ts
// src/routes/pedidos/[id].ts
import { notFound } from "vite-plugin-ascua/site";

export async function load({ id }: { id: string }) {
  const pedido = await leerPedido(id);
  if (!pedido) throw notFound();
  return pedido;
}

type Props = { id: string; data: Awaited<ReturnType<typeof load>> };

export const title = ({ data }: Props) => `Pedido ${data.numero}`;
export const description = ({ data }: Props) => `${data.lineas.length} líneas`;

export default function Pedido({ data }: Props) {
  return view`<article><h1>Pedido ${data.numero}</h1></article>`;
}

load() corre al construir —en desarrollo, en cada petición— y lo que devuelve llega a la página, al título y a la descripción como data. Es el sitio para leer archivos, una base de datos o una API: nada de eso llega al navegador. description va a <meta name="description">.

throw notFound() responde con la página 404 en desarrollo y con servidor.

Rutas que no son páginas

Un archivo cuyo nombre, sin el .ts, conserva una extensión es ese archivo:

ts
// src/routes/sitemap.xml.ts   →  dist/sitemap.xml
export default function Mapa() {
  return `<?xml version="1.0"?><urlset>…</urlset>`;
}

El default devuelve el contenido, que puede ser asíncrono; con parámetros y load(), igual que una página. Sirve para un sitemap.xml, un feed.xml, un índice de búsqueda en .js o un .json.

Lo interactivo: islas

Una página es HTML. Lo que tenga que responder al usuario va en una isla, que la página usa como cualquier componente:

ts
// src/islands/contador.ts
export const IslaContador = defineIsland("contador", { inicial: p.number }, Contador);

// src/routes/index.ts
view`<section><IslaContador inicial=${3}/></section>`;

Las islas de src/islands/ se registran solas. Una página que usa alguna recibe el script que las hidrata y solo los módulos de las islas que usa, con sus chunks y sus hojas pedidos por delante; una que no usa ninguna sale sin JavaScript. Por eso conviene una isla por archivo: si dos islas se definen en el mismo módulo, la página que usa una descarga las dos. Cómo funcionan las islas está en SSR e islas.

El layout

ts
// src/routes/_layout.ts
import type { Children } from "ascua";

export default function Layout(props: { path: string; children: Children }) {
  const marco = view`<div><header>…</header><main></main></div>`;
  props.children(marco.querySelector("main")!);
  return marco;
}

Es un componente con hijos: recibe la ruta (path), sus parámetros (params) y dónde poner la página (children). Puede haber uno por carpeta, y se anidan: el de la raíz envuelve al de pedidos/, que envuelve a sus páginas.

Más cosas en el <head>

ts
export const head = ({ data }: Props) => `
  <meta property="og:title" content="${data.titulo}">
  <link rel="canonical" href="https://ejemplo.cl/pedidos/${data.id}/">`;

head —una cadena o una función de los props— va al <head> tal cual, sin escapar: es HTML que escribe la ruta, no texto de un usuario.

El esqueleto: shell

Si hay un index.html en la raíz, es el esqueleto de todas las páginas. Pasa por Vite como siempre, así que sus <link> a hojas globales salen con su hash. La página va donde esté <!--ascua-->, o al principio de <body>. Sin index.html, se usa uno mínimo.

Si la raíz del sitio es otra cosa —una portada hecha a mano—, el esqueleto puede ser otro archivo —la opción shell—, y index.html sigue siendo una página más de Vite. Así está hecha esta documentación:

ts
ascua({ site: { routes: "rutas", shell: "docs/esqueleto.html" } });

A cada página se le añaden su <title>, un <style> con los estilos de los componentes que aparecen en ella —y solo esos— y, si tiene islas, su script.

Al subirlo

/pedidos/7 es dist/pedidos/7/index.html, que cualquier servidor de archivos entrega como índice de la carpeta. Para la página de error, en busybox httpd:

E404:/404.html

en el httpd.conf del sitio.

Con servidor

Si las páginas tienen que renderizarse en cada petición —datos que cambian a cada rato, una página por usuario—, el mismo sitio sale como servidor:

ts
ascua({ site: { mode: "server" } });
sh
stil run build
node dist/server/index.mjs       # PORT=3000 por defecto

vite build deja el cliente en dist/client/ y el servidor en dist/server/index.mjs, con todo dentro: no necesita node_modules para arrancar. Sirve los archivos del cliente —con caché larga para los que llevan hash— y renderiza cada página al pedirla. Las rutas variables ya no necesitan paths(): cualquier valor llega a load(), que decide con notFound(). Para montarlo dentro de otro servidor, handle(url) devuelve el estado, el tipo y el cuerpo (serve(puerto) es el que arranca el suyo):

ts
import { handle } from "./dist/server/index.mjs";

const { status, type, body } = await handle("/pedidos/7");

Opciones

Por defecto
routes src/routes Dónde están las páginas
islands src/islands Lo que se hidrata; sin islas no hay script
shell index.html La base de cada página
mode static O server

Y lo que puede exportar una ruta:

default La página: un componente de los props. En una ruta-archivo, el contenido
title El <title>: una cadena o una función de los props
description <meta name="description">, igual
head HTML para el <head>, igual
paths() Qué páginas generar en una ruta variable
load() Los datos: llegan como data