# SEO - SSR websites

> What search engines and social networks read of the pages of an SSR website - title and title template, description, Open Graph and Twitter cards, canonical addresses, noindex, sitemap.xml, robots.txt and JSON-LD.

Source: https://docs.apimaker.dev/v4/docs/ssr/seo.html

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.

```ts
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 :

```ts
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 :

```ts
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.

```tsx
<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**

```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](https://docs.apimaker.dev/v4/docs/ssr/deployment.html).
