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

SEO

A page rendered by the server is read by a search engine as people read it : the text is in the HTML. The server writes its head from its settings.

export const config: PageConfig = {
    title: 'Pricing',                                     // <title>Pricing · My product</title> with titleTemplate '%s · My product'
    description: 'Free to start, simple when you grow.',  // <meta name="description"> and og:description
    image: '/og.png',                                     // og:image (a file of public/, or an address) and a large Twitter card
    canonical: '/pricing',                                // <link rel="canonical"> : the whole address
    index: false,                                         // <meta name="robots" content="noindex">
};

Each may be made from the data of the page :

export const config: PageConfig = {
    title: (data) => data.post.title,
    description: (data) => data.post.summary,
    canonical: (data) => `/posts/${data.post.slug}`,
};
Head From
<title> title of the page (else of its layouts, else title of the website), in titleTemplate of site.config.ts
description, og:description description
og:title title
og:image, twitter:card image, written as a whole address
canonical canonical, written as a whole address : the origin, the base path, the path
robots index: false of the page or of a layout, or seo.noIndex of the website

More in the head

head(props) of a page or a layout adds meta tags and links, or any JSX :

export function head({ data }: PageProps<typeof loader>): Head {
    return {
        meta: [
            { property: 'og:type', content: 'article' },
            { property: 'article:published_time', content: data.post.date },
        ],
        links: [{ rel: 'alternate', hreflang: 'fr', href: `/fr/posts/${data.post.slug}` }],
    };
}

Structured data

JSON-LD is a script of the page : serialize() writes JSON nothing can break out of.

<script type="application/ld+json">{serialize({
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: data.product.name,
    offers: { '@type': 'Offer', price: data.product.price, priceCurrency: 'USD' },
})}</script>

sitemap.xml and robots.txt

  • /sitemap.xml lists the pages of the website whose paths take no value, but the ones which say sitemap: false or index: false, and the pages for signed-in visitors. sitemap: { changefreq, priority } tells more.
  • /robots.txt allows every robot (seo.robots: 'allow'), none ('disallow'), or says your text ; it names the sitemap.
  • A website with pages whose paths take values (posts, products) writes its own : a page or an endpoint at pages/sitemap.xml.ts answers in place of the one of the server.
pages/sitemap.xml.ts
import { escape } from 'am-ssr';
import type { PageContext } 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', select: 'slug updatedAt', limit: 5000 });
    const urls = posts.map((post) => `<url><loc>${escape(home + '/posts/' + encodeURIComponent(post.slug))}</loc></url>`);
    ctx.setHeader('content-type', 'application/xml; charset=utf-8');
    return ctx.text(`<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.join('')}</urlset>`);
}

Addresses

  • seo.noIndex: true keeps the whole website out of search engines (a staging copy).
  • trailingSlash : /about/ goes to /about ('never', the default), the other way ('always'), or both answer ('ignore').
  • redirects of site.config.ts send old addresses to new ones (308 unless a status says another ; /old/* takes every path below /old).
  • Behind a domain of its own, the addresses of the pages are at the root of the domain : see deployment.