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

Examples

Short recipes for what websites do most. Each is a whole file you can paste : the templates have many more, with comments.

A list with a search and pages

pages/products/index.tsx
import { paginate } from 'am-ssr';
import type { PageConfig, PageContext, PageProps } from 'am-ssr';

const TABLE = { instance: 'mongodb', database: 'shop', table: 'products' };

export const config: PageConfig = {
    title: 'Products',
    cache: { maxAge: 60, scope: 'public', tags: ['products'], query: ['q', 'page'] },
};

export async function loader(ctx: PageContext) {
    const q = String(ctx.query.q ?? '').trim().slice(0, 60);
    // A text of a visitor is a value of the query, never an operator.
    const find = q ? { name: { $regex: q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' } } : {};
    const total = await ctx.db.count({ ...TABLE, find });
    const pages = paginate({ page: String(ctx.query.page ?? '1'), perPage: 12, total });
    const products = await ctx.db.query({ ...TABLE, find, sort: { name: 1 }, skip: pages.skip, limit: pages.limit });
    return { q, pages, products };
}

export default function Products({ data }: PageProps<typeof loader>) {
    const link = (page: number) => `/products?q=${encodeURIComponent(data.q)}&page=${page}`;
    return (
        <>
            <form method="get"><input type="search" name="q" value={data.q} /> <button>Search</button></form>
            <ul>{data.products.map((product) => <li><a href={`/products/${product._id}`}>{product.name}</a></li>)}</ul>
            <nav>
                {data.pages.items.map((item) =>
                    item === '…' ? <span>…</span> : <a href={link(item)} class={item === data.pages.page ? 'current' : undefined}>{item}</a>,
                )}
            </nav>
        </>
    );
}

The page of a record

pages/products/[id].tsx
import { HttpError, notFound } from 'am-ssr';
import type { PageConfig, PageContext, PageProps } from 'am-ssr';

export async function loader(ctx: PageContext) {
    try {
        const product = await ctx.db.getById({ instance: 'mongodb', database: 'shop', table: 'products', id: String(ctx.params.id) });
        if (!product) notFound();
        return { product };
    } catch (error) {
        // An id which is no id, or no such record : the page of 404.
        if (error instanceof HttpError && error.status < 500) notFound();
        throw error;
    }
}

export const config: PageConfig = { title: (data) => data.product.name, description: (data) => data.product.description };

export default function Product({ data }: PageProps<typeof loader>) {
    return <h1>{data.product.name}</h1>;
}

The data of each customer, nobody else's

site.config.ts
data: {
    read: [{ 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'] }],
},
pages/account/orders.tsx
export const config: PageConfig = { title: 'Your orders', access: 'authenticated' };

export async function loader(ctx: PageContext) {
    // `find: {}` : the rule adds `customer_id: <the visitor>` to the query.
    return { orders: await ctx.db.query({ instance: 'mongodb', database: 'shop', table: 'orders', find: {}, sort: { createdAt: -1 } }) };
}

A saved order gets customer_id of the visitor whatever the page sent ; getById of another customer's order finds nothing.

A part of the website for one group

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

// Every page of admin/ : the group admin only. The others get 403 before any code of these pages runs.
export const config: PageConfig = { access: { groups: ['admin'] } };

export default function AdminLayout({ children }: LayoutProps) {
    return <section class="admin">{children}</section>;
}

The whole website behind a sign-in

site.config.ts
auth: { providers: ['staff'], loginPath: '/login' },
defaultAccess: 'authenticated',
pages/login.tsx
export const config: PageConfig = { title: 'Sign in', access: 'public' };

Who may open a page, decided by code

export const config: PageConfig = {
    access: {
        authenticated: true,
        // Run before the loaders : `false` is a 403.
        check: async (ctx) => (await ctx.db.count({ instance: 'mongodb', database: 'app', table: 'members', find: { team: String(ctx.params.team) } })) > 0,
    },
};

The rule of the table members (where: { user_id: '$user._id' }) keeps the count to the visitor's own memberships.

A JSON API for the scripts of the website

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

export async function GET(ctx: PageContext) {
    return { items: JSON.parse(decodeURIComponent(ctx.cookies.get('cart') ?? '%5B%5D')) };
}

export async function POST(ctx: PageContext) {
    // The server checked the token in `x-csrf-token` before this runs.
    const slug = String(ctx.body?.slug ?? '');
    if (!/^[a-z0-9-]{1,80}$/.test(slug)) return json({ message: 'No such product.' }, { status: 400 });
    // ...
    return json({ added: slug }, { status: 201 });
}
public/app.js
const { base, csrf } = window.__AM_SSR__;
await fetch(base + '/api/cart', {
    method: 'POST',
    headers: { 'content-type': 'application/json', 'x-csrf-token': csrf },
    body: JSON.stringify({ slug: 'basalt-mug' }),
});

A webhook of another service

pages/hooks/payments.ts
import type { PageConfig, PageContext } from 'am-ssr';

// Other servers call it : no CSRF token, a secret of its own.
export const config: PageConfig = { csrf: false, access: 'public' };

export async function POST(ctx: PageContext) {
    const secret = await ctx.cache.get<string>('payments-secret');
    if (!secret || ctx.headers['x-signature'] !== secret) ctx.error(401, 'Unknown sender.');
    await ctx.db.updateMany({ instance: 'mongodb', database: 'shop', table: 'orders', find: { paymentId: String(ctx.body?.id) }, updateData: { status: 'paid' } });
    return { ok: true };
}

Uploading a picture

pages/account/avatar.tsx
export const config: PageConfig = { access: 'authenticated' };

export async function action(ctx: PageContext) {
    const file = ctx.files.find((one) => one.fieldname === 'avatar');
    if (!file || !/^image\/(png|jpeg|webp)$/.test(file.mimetype) || file.size > 2_000_000) {
        ctx.status(422);
        return { error: 'A PNG, JPEG or WebP image of 2 MB at most.' };
    }
    ctx.flash('success', 'Saved.');
    ctx.redirect('/account', 303);
}

export default function Avatar({ actionData }: PageProps<undefined, { error: string }>) {
    return (
        <form method="post" enctype="multipart/form-data">
            {actionData?.error && <p class="error">{actionData.error}</p>}
            <input type="file" name="avatar" accept="image/png,image/jpeg,image/webp" required />
            <button type="submit">Upload</button>
        </form>
    );
}

A part of a page from the server, with htmx

libraries.json
{ "libraries": [{ "id": "htmx", "pages": ["/tickets"] }] }
pages/tickets/index.tsx
<input type="search" name="q" hx-get="/tickets/rows" hx-trigger="input changed delay:300ms" hx-target="#rows" />
<table><tbody id="rows"><TicketRows tickets={data.tickets} /></tbody></table>
pages/tickets/rows.ts
import TicketRows from '@/components/TicketRows';

export async function GET(ctx: PageContext) {
    const tickets = await ctx.db.query({ instance: 'mongodb', database: 'desk', table: 'tickets', find: {}, limit: 20 });
    return ctx.html(await TicketRows({ tickets }));
}

hx-get gets the base path of the website by itself, as href does.

A chart

export const config: PageConfig = { libraries: ['chartjs'] };

export default function Sales({ data }: PageProps<typeof loader>) {
    return (
        <>
            <canvas id="sales"></canvas>
            <script>{`document.addEventListener('DOMContentLoaded', () => new Chart(document.getElementById('sales'), {
                type: 'bar', data: ${serialize({ labels: data.labels, datasets: [{ label: 'Sales', data: data.values }] })},
            }));`}</script>
        </>
    );
}

Pages in the language of the visitor

lib/i18n.ts
import texts from '@/data/texts.json';

export function t(ctx: PageContext, key: string): string {
    const lang = ctx.locale.startsWith('fr') ? 'fr' : 'en';
    return (texts as Record<string, Record<string, string>>)[lang]?.[key] ?? key;
}

A page which reads ctx.locale is never kept for everyone : use paths per language (pages/fr/...) for pages you keep.

A feed

pages/feed.xml.ts
import { escape } from 'am-ssr';

export async function GET(ctx: PageContext) {
    const home = ctx.site.origin + ctx.site.base;
    const posts = await ctx.db.query({ instance: 'mongodb', database: 'blog', table: 'posts', sort: { date: -1 }, limit: 20 });
    const items = posts.map((post) => `<item><title>${escape(post.title)}</title><link>${home}/posts/${post.slug}</link></item>`).join('');
    ctx.setHeader('content-type', 'application/rss+xml; charset=utf-8');
    return ctx.text(`<?xml version="1.0"?><rss version="2.0"><channel><title>${escape(ctx.site.title ?? '')}</title>${items}</channel></rss>`);
}

Old addresses, and a pause

site.config.ts
redirects: [
    { from: '/shop/*', to: '/products', status: 301 },
    { from: '/twitter', to: 'https://x.com/apimaker', status: 302 },
],
// The website answers 503 with this message, but to the group admin.
maintenance: { message: 'Back at 10:00 UTC.', allowGroups: ['admin'] },

A redirect to another site

// Refused without `external` : a `?next=` a visitor wrote never sends to another site.
ctx.redirect('https://checkout.stripe.com/c/pay/' + session.id, 303, { external: true });

Calling a custom API as the visitor

export async function action(ctx: PageContext) {
    // The access of the API, its hooks, and the token of the visitor.
    const quote = await ctx.api.customApi('/shop/quote', { method: 'POST', body: { items: ctx.form.items } });
    return { quote };
}

Values kept between requests

const weather = await ctx.cache.getOrSet('weather:paris', 600, async () => {
    const answer = await ctx.fetch('https://api.example.com/weather?city=paris');
    return answer.json;
});