# Every language's page, for a search engine — hreflang, canonical, sitemap

> An app that speaks more than one language tells search engines where each page lives in each one, with nothing written in its source: every public page's head names its address in every language and `x-default`, its canonical is the address in the language it is served in, and a served sitemap lists each page once per language with its alternates. What a page's head says about itself — its title, description and structured data — is in the served language, because the head runs in it.

<!-- id: i18n-search-engines · area: i18n · stability: stable · html: https://osysharp.com/reference/i18n/search-engines/ -->

## Summary        {#summary}
Each language of a page has one address: the source language's is the page's own path (`/p/kiambu`), every other
language's is the same path under its code (`/sv/p/kiambu`), served in place ([Which language a reader gets — and switching it](https://osysharp.com/reference/i18n/language/)). Search engines are
told so in three places, by the platform, for every app that speaks more than one language.

## Signature      {#signature}
```html syntax
<!-- the head of every public page, served as /sv/p/kiambu -->
<link rel="alternate" hreflang="en" href="https://shop.example.com/p/kiambu">
<link rel="alternate" hreflang="sv" href="https://shop.example.com/sv/p/kiambu">
<link rel="alternate" hreflang="x-default" href="https://shop.example.com/p/kiambu">
<link rel="canonical" href="https://shop.example.com/sv/p/kiambu">

<!-- a served sitemap: each <url> once per language, each carrying the whole set -->
<url>
  <loc>https://shop.example.com/sv/p/kiambu</loc>
  <xhtml:link rel="alternate" hreflang="en" href="https://shop.example.com/p/kiambu"/>
  <xhtml:link rel="alternate" hreflang="sv" href="https://shop.example.com/sv/p/kiambu"/>
  <xhtml:link rel="alternate" hreflang="x-default" href="https://shop.example.com/p/kiambu"/>
</url>
```

## Description    {#description}
**The head of every public page** — one a signed-out visitor may open, which is all a crawler ever reads — names the
page in every language the app speaks and `x-default` (the unprefixed page, which chooses by `Accept-Language`), as
absolute addresses. A page with no `head { }` of its own gets them too. A client-side navigation re-applies them with
the rest of the head, so they always describe the page on screen.

**The canonical follows the served language.** A page's `head { Canonical = … }` is written once, as the source
language's address (`"/p/" + slug`). Served as `/sv/p/kiambu`, the platform writes it — and `og:url` — as
`/sv/p/kiambu`: a Swedish page whose canonical named the English one would be folded into it, and never shown to a
Swedish searcher. A canonical on another host is the page's own statement and is left as written.

**What the head says is in the served language.** A head runs in the language the page is served in, so a
`[Translatable]` member ([[Translatable] — an entity's own words in every language](https://osysharp.com/reference/i18n/translatable/)) reads as that language: the title, `og:title`, the description and the
structured data of `/sv/p/kiambu` are the product's Swedish words. An address a head builds for itself — a product's
`url` in its structured data — should be the page in that language too; `Session.Language` says which
(`Osysharp.Shop`'s `ProductUrl` does this).

**A served sitemap** (`[Serve("/sitemap.xml")]`, [[Serve]](https://osysharp.com/reference/api/serve/)) is written as for one language — a `<url>` per page, its
`<loc>` absolute — and the platform lists each page once per language, every copy carrying the full set of
`<xhtml:link rel="alternate" hreflang>` and whatever else the `<url>` held (`lastmod`, `priority`). A `<url>` that
already names its alternates is left as written.

**An app in one language** gets none of this: its pages, heads and sitemap are exactly what its source says.

## Examples       {#examples}
```osy title="a product page and a sitemap, written once — the platform names every language's address" test app=i18n-search-engines
entity Coffee {
  [Required, MaxLength(80)] string Slug;
  [Required, MaxLength(120), Translatable] string Name;
  [MaxLength(300), Translatable] string? Notes;
  security { allow read when IsAnonymous || IsAuthenticated; }
}

[Route("/coffee/{slug}")]
[AllowAnonymous]
[Title("Coffee")]
component CoffeePage(string slug) {
  var coffee = Coffee.Where(c => c.Slug == slug).FirstOrDefault();
  head {
    Title = coffee == null ? "Not found" : coffee.Name;
    Description = coffee?.Notes;
    Canonical = "/coffee/" + slug;          // the source language's address — moved into the served one
  }
  render { Text(coffee?.Name ?? "Not found"); }
}

[Serve("/sitemap.xml")]
[AllowAnonymous]
string Sitemap() {
  var xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">";
  foreach (var c in Coffee.OrderBy(c => c.Slug).ToList()) {
    xml = xml + "<url><loc>" + App.BaseUrl + "/coffee/" + c.Slug + "</loc></url>";
  }
  return xml + "</urlset>";
}

translations sv {
  title "Coffee" => "Kaffe";
}
```

## See also       {#see-also}
- [Which language a reader gets — and switching it](https://osysharp.com/reference/i18n/language/) — which language a reader gets, and the `/sv/` address
- [head](https://osysharp.com/reference/ui/head/) — a page's `head { }`
- [[Serve]](https://osysharp.com/reference/api/serve/) — a served sitemap
- [[Translatable] — an entity's own words in every language](https://osysharp.com/reference/i18n/translatable/) — data read in the served language
