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

Guide for AI assistants

This page is written for an assistant which writes the files of a website of API Maker. Read it whole before you write a file.

The website

  • A website is files. Its pages are TSX, rendered on the server by QuickJS : no React, no hooks, no state, no useEffect. A component is a function of its props which returns JSX ; it may be async.
  • pages/ holds the routes : index.tsx is /, blog/[slug].tsx is /blog/:slug, docs/[...path].tsx takes several parts, (group)/ is no part of the path, a .ts file is an endpoint (export function GET(ctx)).
  • _layout.tsx wraps the pages of its folder (it gets children), _middleware.ts runs before them, _404.tsx and _error.tsx answer errors (they get error).
  • components/, lib/ : code the pages import with @/components/Card (@/ is the root of the website), ./x, ../x. data/*.json is imported as a module. public/ is sent as it is.
  • site.config.ts : export default defineSite({ ... }) with values only (no functions, no variables). libraries.json : the UI libraries of the browser.
  • The only modules a file imports are files of the website, 'am-ssr' and 'types'. There is no npm on the server side.

A page, complete

import { formatCurrency, notFound } from 'am-ssr';
import type { PageConfig, PageContext, PageProps } from 'am-ssr';

export const config: PageConfig = {
    title: (data) => data.product.name,          // a value, or (data, ctx) => value
    description: (data) => data.product.summary,
    access: 'public',                            // 'public' | 'authenticated' | { groups: ['admin'] }
    cache: { maxAge: 60, scope: 'public' },      // only for pages which show nothing of the visitor
};

export async function loader(ctx: PageContext) {
    const [product] = await ctx.db.query({
        instance: 'mongodb', database: 'shop', table: 'products',
        find: { slug: String(ctx.params.slug) },   // a value from the visitor, made a text
        limit: 1,
    });
    if (!product) notFound('No such product.');
    return { product };
}

export default function ProductPage({ data }: PageProps<typeof loader>) {
    return (
        <article>
            <h1>{data.product.name}</h1>
            <p>{formatCurrency(data.product.price, 'USD')}</p>
            <a href="/products">All products</a>
        </article>
    );
}

Rules

  1. Write addresses from the root of the website : href="/about", src="/logo.png", action="/login". The server adds the base path (/ssr/<apiPath>/<name>, or nothing behind a domain). Do not write /ssr/<apiPath>/<name> yourself. In a value which is no attribute, use ctx.link('/about').
  2. Static files : asset('/styles.css') (hash in the address, kept a year).
  3. Every value is escaped. Write {text} ; never raw() or dangerouslySetInnerHTML with what a visitor wrote.
  4. No event attributes : onclick="..." does not run (CSP). Add a <script> (it gets the nonce by itself) which listens : document.getElementById('x').addEventListener('click', …).
  5. Data for a script : <script>{const DATA = ${serialize(data)};}</script>.
  6. Forms post to their own page : <form method="post"> gets its CSRF token by itself ; the page exports action(ctx) ; read ctx.form ; return { values, errors } with ctx.status(422) on a mistake ; on success ctx.flash('success', '…') then redirect('/path', 303).
  7. Scripts which POST send x-csrf-token: window.__AM_SSR__.csrf and use window.__AM_SSR__.base + '/api/...'.
  8. Data : ctx.db.<method>({ instance, database, table, ... }) reaches only the tables site.config.ts lists in data.read / data.write ('instance/database/table' or { table, where, set, requireUser, fields, allow }). When you add a call to a new table, add the table to site.config.ts. For data of a visitor, add a rule where: { owner_id: '$user._id' } rather than filtering in the page.
  9. Never pass an object of the visitor as a query : find: { status: String(ctx.query.status) }, never find: ctx.query. Escape regular expressions.
  10. Who may open a page : config.access, or access of a _layout.tsx for a whole folder, or defaultAccess of the website. Do not check it only in the loader.
  11. Sign-in : await ctx.auth.login({ provider: '<name in auth.providers>', username, password }) in an action, inside try/catch (HttpError with status < 500 is a wrong password : answer 401 with one sentence for both cases). Sign-out is a POST endpoint calling ctx.auth.logout().
  12. Redirects to another host need { external: true }. A next of the query is a path : check next.startsWith('/') && !next.startsWith('//').
  13. Cache : cache: { maxAge, scope: 'public' } only for pages which show nothing of the visitor (no ctx.user, no cookies). Drop with tags + ctx.revalidate('tag') after a change.
  14. Never keep ctx or a visitor's data in a variable of a module : modules are shared by every visitor.
  15. No Node.js : no fs, process, Buffer, require('x') of npm, setTimeout. Use ctx.fetch (hosts listed in data.fetch), ctx.cache, crypto.randomUUID(), URL, TextEncoder.
  16. Endpoints return an object (JSON), a text, json(data, { status }), ctx.html(await Component(props)) for HTML without the layout, or nothing (204).
  17. Browser libraries (Bootstrap, Tailwind, Chart.js, htmx...) go in libraries.json ({ "id": "bootstrap" }) ; a page may limit them with config.libraries: ['chartjs']. Never add a <script src="https://cdn..."> by hand : the CSP refuses it.
  18. Types : PageProps<typeof loader>, LayoutProps, ErrorProps, PageContext, PageConfig, Head from 'am-ssr' (import type).

The checklist of a new page

  • The file is in pages/ at the path it answers ; a dynamic part reads ctx.params.<name> (a text, or a list for [...]).
  • config with title (and description for public pages), access when it is not public.
  • Every table it reads or writes is in site.config.ts, with a row rule for the data of visitors.
  • Values of the visitor are made texts or numbers before they reach a query.
  • A missing record is notFound().
  • Forms : method="post", an action, checks on the server, 303 after success.
  • Links are written from the root of the website.

site.config.ts, complete

import { defineSite } from 'am-ssr';

export default defineSite({
    title: 'My shop',
    titleTemplate: '%s · My shop',
    description: 'What the website is.',
    lang: 'en',
    domains: [],
    auth: { providers: ['customers'], loginPath: '/login', sessionDays: 30 },
    defaultAccess: 'public',
    data: {
        read: ['mongodb/shop/products', { table: 'mongodb/shop/orders', where: { customer_id: '$user._id' }, requireUser: true }],
        write: [{ table: 'mongodb/shop/orders', where: { customer_id: '$user._id' }, requireUser: true, allow: ['create'] }],
        fetch: [],
        allowSys: false,
    },
    security: { csp: 'strict' },
    seo: { sitemap: true, robots: 'allow' },
    redirects: [],
});

Where to look

  • Starter template : one page per feature, with comments.
  • Examples : recipes to copy.
  • Reference : every function and every member of ctx.
  • The declarations : am-ssr.d.ts next to each website in the git repository.