# head

> A routed page's document head — its title, description, canonical address, share image, structured data, whether it may be indexed, and whether it has moved. Each value may read the page's own data, and the head is written into the page's first response whatever the page's render mode, so a crawler reads it without running anything.

<!-- id: ui-head · area: ui · stability: stable · html: https://osysharp.com/reference/ui/head/ -->

## Summary        {#summary}
`head { }` is what a page says about itself to everything that is not a person looking at it: the browser tab, a
bookmark, a search result, a link preview in a chat, a crawler reading structured data. A product page says which
product it is; a moved page says where it went.

```osy syntax
[Route("/p/{slug}")]
[AllowAnonymous]
component ProductPage(string slug) {
  var product = Product.Where(p => p.Slug == slug).FirstOrDefault();
  head {
    Title = product == null ? "Not found" : product.Name;
    Description = product?.Summary;
    Canonical = "/p/" + slug;
  }
  render { … }
}
```

## Signature      {#signature}
```osy syntax
head {
  Title          = <string?>;   // the <title> — the tab, a bookmark, a search result's headline (also og:title)
  Description    = <string?>;   // <meta name="description"> — a search result's snippet (also og:description)
  Canonical      = <string?>;   // <link rel="canonical"> — the one address this content lives at (also og:url)
  Image          = <string?>;   // og:image — the picture a shared link shows
  StructuredData = <JsonLd?>;   // JSON-LD — an Osysharp.Seo block, JsonLd.Raw(json), or [a, b] for several
  NoIndex        = <bool>;      // <meta name="robots" content="noindex">
  RedirectTo     = <string?>;   // answer 301 to this path instead of rendering the page
}
```

Every setting is optional, and each may be set once. A `null` value writes no tag. The block belongs to a **routed**
page (one with `[Route("/…")]`); on any other component it is a compile error, because nothing else has a document.

## Description    {#description}

### It reads the page's own data   {#reads-data}
A setting is an ordinary expression, and it may read the page's **parameters** and its **members** — a
`var product = Product.Where(…).FirstOrDefault();` declared on the page is readable from the head exactly as it is
from `render`. Only the members the head actually reads are computed for it, so a member that only makes sense in the
browser does not stop a head that never looks at it.

A wrong type is the ordinary assignment error, at the line that wrote it: `NoIndex = "yes";` is refused because
`NoIndex` is a `bool`.

### It is in the first response — whatever `[Render]` says   {#first-response}
A crawler reads the head of the first response and runs nothing. So the head is computed **on the server** for every
page a signed-out visitor may open (`[AllowAnonymous]`), including a `[Render(CSR)]` page that paints nothing on the
server at all. Relative `Canonical` and `Image` values are made absolute against the address the request came to,
because the machines that read them do not resolve a relative address the way a browser does.

A page that requires a sign-in is reached by a hard navigation that carries no credential, so its head is not computed
there: the tab shows its `[Title]` until the client has signed in, and then the head is applied as below. Nothing the
head would have computed reaches a visitor the page itself refuses.

### And it follows the visitor around the app   {#client-navigation}
Every page after the first is reached by the client router, which never asks for a new document. After each such
navigation the client asks the server for the new page's head — computed **as the signed-in caller**, and refused to a
caller the page itself refuses (`[AllowAnonymous]` / `[Authorize]` apply exactly as they do to the page) — and replaces
the previous page's head tags with it. A page with no head clears them, so a canonical address or structured data never
outlives the page it described. A page with no `head { }` costs no request.

### Title: the head's wins the tab   {#title}
The browser tab reads, most specific first: the head's `Title`, then the page's `[Title("…")]`, then the component's
own name. `[Title]` goes on naming the route for the app's own chrome — a breadcrumb, a tab strip reading
[`Navigation.Routes`](https://osysharp.com/reference/ui/navigation/) — so a page can be "Blue Kettle — Shop" in the browser and "Product" in its
breadcrumb.

### A moved page answers 301   {#redirect}
`RedirectTo` makes the page answer **301 Moved Permanently** before anything renders, which is what tells a search
engine to carry the old address's standing over to the new one. It accepts **a path on this app** — one that starts
with a single `/`. A value naming another host (`https://…`, or `//host`, which a browser also reads as another host)
is not followed: the page is served instead and the server logs why. That is deliberate — a head computes its value
from data and from the address, and a head that forwarded one would be a way for any link to send your visitors to
another site from your own domain.

After a client navigation the same rule applies, and the router replaces the address rather than adding one.

### Structured data: typed blocks, or your own JSON   {#structured-data}
`StructuredData` holds a `JsonLd` block — what search engines read to show a product's price and stock, a breadcrumb
trail or a site's name in a result. The [`Osysharp.Seo` kit](https://osysharp.com/reference/ui/seo-kit/) ships schema.org's shapes as classes that
derive from it (`SeoProduct`, `SeoOffer`, `SeoBreadcrumbList`, `SeoOrganization`, `SeoWebSite`), each writing its own
`@context` and `@type`. `JsonLd.Raw(json)` is JSON you wrote yourself, for anything the kit does not model. Several
blocks are a list — `StructuredData = [product, breadcrumbs];` — which is written as one JSON-LD array
(`JsonLd.Of(…)` spells the same thing).

It cannot break out of its script. The block's JSON is written into a `<script type="application/ld+json">` after it is
parsed — text that is not JSON is dropped, never written — and every `<` in it is written as the escape `\u003c`, which is the same character to a JSON
reader and can never end the script element. So a product whose name is `</script><script>…` shows up in the
structured data as that name, and not as script on your page.

### What a head is not   {#not}
- **Not per-visitor chrome.** It is computed for the page's address. A head that greets the signed-in user by name
  would show that name to a crawler reading the anonymous response — use the page's `render` for that.
- **Not on a generic page.** A head is computed from the address alone, and an address cannot choose a type argument,
  so `head { }` on `component Board<T>(…)` is refused.

## Examples       {#examples}

### A product page that names its product   {#example-product}
```osy title="a head computed from the page's own row" test app=ui-head
entity Product {
  [MaxLength(200)] string Name;
  [MaxLength(200)] string Slug;
  [MaxLength(500)] string? Summary;
  security { allow read when IsAnonymous || IsAuthenticated; }
}

[Route("/p/{slug}")]
[AllowAnonymous]
[Title("Product")]
component ProductPage(string slug) {
  var product = Product.Where(p => p.Slug == slug).FirstOrDefault();
  head {
    Title = product == null ? "Not found" : product.Name + " | Shop";
    Description = product == null ? null : product.Summary;
    Canonical = "/p/" + slug;
    Image = "/img/" + slug + ".png";
    StructuredData = product == null ? null
      : JsonLd.Raw("{\"@context\":\"https://schema.org\",\"@type\":\"Product\",\"name\":" + JsonSerializer.Serialize(product.Name) + "}");
    NoIndex = product == null;
    RedirectTo = slug == "kettle-blue" ? "/p/blue-kettle" : null;
  }
  render { Text(product == null ? "Not found" : product.Name); }
}
```

### What the compiler refuses   {#example-refused}
```osy syntax
[Route("/p/{slug}")]
component ProductPage(string slug) {
  head {
    Title = "Kettle";
    Keywords = "kettle, blue";   // ERROR: a head sets `Title` (string?), `Description` (string?), `Canonical` (string?),
  }                              //        `Image` (string?), `StructuredData` (JsonLd?), `NoIndex` (bool), `RedirectTo` (string?)
  render { … }
}

component Card() {
  head { Title = "Card"; }       // ERROR: only a routed page has a document head — this component has no [Route("/…")]
  render { … }
}
```

`meta { title = "…"; }`, the block this replaced, is refused by name with the rewrite: `head { Title = "…"; }`.

## See also       {#see-also}
- [routes and pages](https://osysharp.com/reference/ui/routing/) — how a component becomes a page, and what its address binds.
- [Navigation](https://osysharp.com/reference/ui/navigation/) — `Navigation.Routes` and the route titles an app's own chrome reads.
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — who may open a page, which is who may read its head.
- [component](https://osysharp.com/reference/ui/component/) — the members a head may read.
- [[Serve]](https://osysharp.com/reference/api/serve/) — `[Serve("/sitemap.xml")]`, for what a crawler reads that is not a page.
- [moved addresses (app.Redirects)](https://osysharp.com/reference/config/redirects/) — `app.Redirects`, for a move you can write down rather than one the data decides.
