Skip to content
This page View Markdown Open in ChatGPT Open in Claude

Pages and routing

The files of pages/ are the routes of a website. The path of a file is the path it answers.

File Path What it is
pages/index.tsx / A page
pages/about.tsx /about A page
pages/blog/index.tsx /blog A page
pages/blog/[slug].tsx /blog/hello A page with one part which takes a value : ctx.params.slug
pages/docs/[...path].tsx /docs/a/b One part or more : ctx.params.path is ['a', 'b']
pages/docs/[[...path]].tsx /docs, /docs/a/b Any number of parts, none too
pages/(shop)/cart.tsx /cart A folder between brackets is no part of the path : it gathers pages under a layout
pages/api/items.ts /api/items An endpoint : functions named after methods
pages/rss.xml.ts /rss.xml An endpoint with a dot in its path
pages/_layout.tsx The HTML around the pages of its folder and of the folders below it
pages/_middleware.ts Code which runs before the pages and endpoints of its folder
pages/_404.tsx, pages/_error.tsx What a path no page answers shows ; what a page which failed shows
pages/_parts/… A folder whose name starts with _ holds no routes

When two routes take a path, the one whose parts are fixed earliest wins : /blog/new is answered by blog/new.tsx before blog/[slug].tsx, which comes before blog/[...rest].tsx, which comes before blog/[[...rest]].tsx. Two files of the same route (about.tsx and about/index.tsx) are a problem of the website, which the panel shows.

A page

A .tsx file of pages/ exports its component as default. It may export :

Export What it is
default The component : a function of { data, ctx, params, actionData } which returns JSX. It may be async.
loader(ctx) Reads on the server what the page shows ; what it returns is data. See data.
action(ctx) Takes a form the page posts. See forms.
config Its title, its description, who may open it, how long it is kept, the UI libraries it loads. See reference.
head(props) What it adds to the head : meta, links, or JSX. See SEO.
pages/blog/[slug].tsx
import { notFound } from 'am-ssr';
import type { PageConfig, PageContext, PageProps } from 'am-ssr';

export async function loader(ctx: PageContext) {
    const [post] = await ctx.db.query({ instance: 'mongodb', database: 'blog', table: 'posts', find: { slug: String(ctx.params.slug) }, limit: 1 });
    if (!post) notFound('This post does not exist.');
    return { post };
}

// The title can be made from the data of the page.
export const config: PageConfig = { title: (data) => data.post.title };

export default function Post({ data }: PageProps<typeof loader>) {
    return (
        <article>
            <h1>{data.post.title}</h1>
            <p>{data.post.summary}</p>
        </article>
    );
}

JSX

JSX is HTML with values in braces, compiled to templates when the file is saved : no virtual DOM, nothing walked when the page renders.

  • class takes a text, a list (['btn', active && 'active']) or an object ({ active: true }). className is the same.
  • style takes a text or an object : { color: 'red', fontSize: 14 } (px added to numbers).
  • A boolean attribute is written for true and left out for false : <option selected={x === y}>.
  • false, null and undefined write nothing ; 0 writes 0.
  • A function given to an attribute (onclick={...}) is left out : nothing runs in the browser unless a script does it.
  • <script> and <style> take their text as it is (a script gets the nonce of the page by itself) ; every other text is escaped.
  • <>…</> writes its children without an element around them.
  • <Tag> where Tag is a variable holding a name ('h2') writes that element.

Layouts

_layout.tsx wraps every page of its folder and of the folders below it : children is the page, or the layouts inside this one. The layout at the root writes <html> : the server writes the title, the description, the CSRF token and the UI libraries into its head.

pages/_layout.tsx
import { asset } from 'am-ssr';
import type { LayoutProps } from 'am-ssr';

export default function Layout({ children, ctx }: LayoutProps) {
    return (
        <html lang="en">
            <head>
                <meta charset="utf-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <link rel="stylesheet" href={asset('/styles.css')} />
            </head>
            <body>
                <nav><a href="/">Home</a> <a href="/blog">Blog</a></nav>
                {ctx.flashes.map((flash) => <p class={'flash ' + flash.type}>{flash.text}</p>)}
                <main>{children}</main>
            </body>
        </html>
    );
}

A layout may have a loader (its data), a config (it applies to every page inside it : access, cache, libraries) and a head. The loaders of the layouts and of the page run at the same time.

A group folder gives some pages a layout of their own without changing their paths : pages/(account)/_layout.tsx wraps pages/(account)/orders.tsx, which is /orders.

Middlewares

_middleware.ts runs before every page and endpoint of its folder and of the folders below it, the outer ones first. Return an answer to stop there, nothing to go on.

pages/admin/_middleware.ts
import type { PageContext } from 'am-ssr';

export default async function middleware(ctx: PageContext) {
    ctx.setHeader('x-robots-tag', 'noindex');
    if (!ctx.auth.hasGroup('admin')) ctx.forbidden('This part of the website is for the group admin.');
}

Prefer access for who may open a page

access in the config of a page or a layout is read when the file is saved, and the server refuses a visitor before any code runs. A middleware is code : use it for what values can not say.

404 and error pages

_404.tsx answers a path nothing answers, and notFound(). _error.tsx answers what a page threw (error(400, '…'), forbidden()) or what failed. Both render in the layouts of their folder ; the nearest to the path answers. They get error : { status, message, stack, errors }.

  • A visitor sees the message of an error thrown on purpose (notFound('No such product.')) ; for anything else, a sentence. The preview of the panel shows the message and the stack of every error, with the lines of your files.
  • A request which asks for JSON (accept: application/json) or an endpoint gets { success: false, status, message }.
  • Without _404.tsx or _error.tsx, the server answers with a plain page of its own.
pages/_error.tsx
import type { ErrorProps, PageConfig } from 'am-ssr';

export const config: PageConfig = { title: 'Error', index: false };

export default function ErrorPage({ error }: ErrorProps) {
    return (
        <section>
            <h1>{error.status}</h1>
            <p>{error.message}</p>
            {error.stack && <pre>{error.stack}</pre>}
        </section>
    );
}

Endpoints

A .ts (or .js) file of pages/ is an endpoint : its functions are named after the methods they answer — GET, POST, PUT, PATCH, DELETE, or ALL for every method. HEAD is answered by GET. A method it has not is a 405 with Allow.

pages/api/notes.ts
import { json } from 'am-ssr';
import type { PageConfig, PageContext } from 'am-ssr';

export const config: PageConfig = { access: 'authenticated' };

export async function GET(ctx: PageContext) {
    // An object is JSON.
    return ctx.db.query({ instance: 'mongodb', database: 'app', table: 'notes', find: {}, limit: 50 });
}

export async function POST(ctx: PageContext) {
    const text = String(ctx.body?.text ?? '').trim();
    if (!text) return json({ message: '`text` is a text.' }, { status: 400 });
    return json(await ctx.db.saveSingleOrMultiple({ instance: 'mongodb', database: 'app', table: 'notes', saveData: { text } }), { status: 201 });
}
An endpoint returns The answer
An object, a list, a number JSON
A text text/plain
json(data, { status, headers }) JSON with that status
ctx.text(body, status), ctx.html(body, status) A text, or HTML (ctx.html(await Card({ ... })) sends a component without the layout)
redirect('/path', 303) A redirect
Nothing 204 No Content

An endpoint which takes POST, PUT, PATCH or DELETE asks for the CSRF token of the visitor (x-csrf-token, or the field _csrf of a form). An endpoint other servers call (a webhook) says csrf: false in its config and checks a secret of its own. See forms.

Public files

public/ holds files sent as they are : public/logo.png is /logo.png. A file of public/ wins over a page of the same path.

  • asset('/logo.png') writes its address with the hash of its content : /ssr/acme/shop/logo.png?v=3fa2c9e1. Browsers keep such an address for a year (immutable) and take the new one when the file changes.
  • Without the hash, the address is answered with an ETag : browsers ask again, and get 304 when nothing changed.
  • Texts are compressed (Brotli or gzip) the first time a browser asks for them.
  • The panel uploads images, fonts and any other file into public/ ; the code of the server never runs them.

Addresses

Write the addresses of the website from its root : href="/about". The server writes the base path in front of them by itself — /ssr/acme/shop/about, or /about behind a domain of its own — in href, src, action, formaction, poster and the hx-get… attributes of htmx.

  • In a value which is no attribute (a text of a script, a redirect of your own), use ctx.link('/about').
  • withoutBase('/api/custom-api/…') is an address of the server outside the website.
  • An address of another site (https://…) is written as it is ; javascript: is refused when the file is saved.