---
title: "SEO and Sharing · RUAL Documentation"
description: "The head tags web_render writes for you: title, description, canonical, Open Graph, Twitter cards and JSON-LD, and how to add or override your own."
canonical: https://docs.rual.nl/web/seo
language: en
---

# SEO and Sharing

Every web page is complete HTML when it leaves the server, so search engines and link previews see exactly what visitors see. [`web_render`](https://docs.rual.nl/block-types/web/web_render) writes the title, description, canonical link, Open Graph tags, Twitter card and structured data from its pins, and leaves alone any tag your own markup already has.

In short

- Wire `title` and `description` and you have a page that shares well. Add `image` for a large card.

- The canonical link is the page's URL without its query, unless you set `canonical`.

- Add anything else with the `meta` pin, and structured data with `json_ld`.

- A tag your layout writes is not written twice.

## Each pin becomes head tags

| Pin | Writes | Good to know |
| --- | --- | --- |
| `title` | ` ` and `og:title` | The template can print it as `{{title}}`. |
| `description` | ` ` and `og:description` | The template can print it as `{{description}}`. |
| `image` | `og:image` | A `/path` becomes an absolute URL on the page's own host. With an image, `twitter:card` becomes `summary_large_image`. |
| `canonical` | ` ` and `og:url` | Defaults to the page's URL without its query. A `/path` becomes absolute. |
| `meta` | Any tag, from an object of name to content | See [below](#meta). |
| `json_ld` | ` ` | See [below](#json-ld). |

Some tags need no pin at all: `og:type` is `website`, `twitter:card` is `summary`, and `og:locale` and the `hreflang` links come from the page's language and its `l10n` URLs. See [Languages](https://docs.rual.nl/web/languages#hreflang).

The head tags read these pins, not the template's variables. A `title` field in the `data` object prints in your template, but it writes no ` `.

```
<title>Green tea</title>
<meta name="description" content="Light and grassy.">
<link rel="canonical" href="https://shop.example.com/shop/green-tea">
<meta property="og:description" content="Light and grassy.">
<meta property="og:image" content="https://shop.example.com/images/green-tea.jpg">
<meta property="og:locale" content="en_US">
<meta property="og:title" content="Green tea">
<meta property="og:type" content="website">
<meta property="og:url" content="https://shop.example.com/shop/green-tea">
<meta name="twitter:card" content="summary_large_image">
```

## The meta pin adds or overrides any tag

It takes an object of name to content, and writes one tag per key:

```
{
  "robots": "noindex",
  "og:type": "product",
  "twitter:image": "https://shop.example.com/images/green-tea.jpg"
}
```

- A key starting with `og:`, `article:` or `product:` is written as ` `. Any other as ` `.

- A key that matches a generated tag replaces its value, such as `og:type` above.

- The pins write `twitter:card` and no other Twitter tag. Add `twitter:image` here if you want one.

- A tag with an empty value is left out.

## Structured data goes on the json_ld pin

Wire an object into `json_ld`, and it lands in the head as JSON-LD. The data cannot close its own script element, because ` ` and `&` in it are written as escapes. An empty object writes nothing.

```
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Green tea",
  "description": "Light and grassy.",
  "offers": {"@type": "Offer", "price": "4.50", "priceCurrency": "EUR"}
}
```

You can also write it into your template, where the same safety applies: a value in a ` ` is written as JSON, so it takes no quotes.

```
<script type="application/ld+json">
  {"@context": "https://schema.org", "@type": "Product", "name": {{product.name}}, "offers": {"@type": "Offer", "price": {{product.price}}}}
</script>
```

## Status and headers are pins too

| Pin | Is |
| --- | --- |
| `status` | The HTTP status, 200 by default. Answer a missing product with a real `404` and its own template, so it does not get indexed as a page. |
| `headers` | Extra response headers as `[{"key": "...", "value": "..."}]`, or an object of name to value. One value per name. |
| `cache` | How many seconds shared caches may keep the page. See [Speed and Caching](https://docs.rual.nl/web/speed#cache-seconds). |

A header you set wins. That goes for `Content-Type`, `Cache-Control`, `Referrer-Policy` and `X-Content-Type-Options`, and for `Content-Security-Policy`: see [Escaping and Security](https://docs.rual.nl/web/security#csp) before you replace that one.

A flow can also skip [`web_render`](https://docs.rual.nl/block-types/web/web_render) and answer with an `httpconnection` block. That is how a [`web_page`](https://docs.rual.nl/block-types/web/web_page) with the event `sitemap.xml` can answer XML through [`httpconnection_set_xml`](https://docs.rual.nl/block-types/xml%2Chttp%20connection/httpconnection_set_xml).

## A tag your layout writes is not written twice

The block looks at the part of the document before ` ` and skips what it finds there. The layout can write its own title and still get everything else:

```
<!doctype html>
<html lang="{{language}}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="{{viewport}}">
  <title>{{title}} | My Shop</title>
  {{{head}}}
</head>
<body>{{{body}}}</body>
</html>
```

- It looks for `<title`, `name="description"`, `rel="canonical"`, `hreflang`, and each generated name in double quotes, such as `"og:image"`. Write your tags with double quotes, or the check misses them and you get both.

- An inline SVG's ` ` in the body is not the page's title, and does not count.

- `{{{head}}}` places the tags. Without it they go before ` `, and the document's [three shapes](https://docs.rual.nl/web/layouts-and-partials#which-document) all get them.

## Next steps

- [Languages](https://docs.rual.nl/web/languages): hreflang, og:locale and a URL per language.

- [Speed and Caching](https://docs.rual.nl/web/speed): ETags, cache seconds and prefetching.

- [Layouts and Partials](https://docs.rual.nl/web/layouts-and-partials): Where the head of a stored layout comes from.

## Frequently asked

**Does a RUAL web page get Open Graph tags and a canonical link?**

Yes, from the pins of web_render: the title, the meta description, a canonical link that defaults to the page URL without its query, Open Graph and Twitter tags, hreflang links and JSON-LD from the json_ld pin. Anything else goes through the meta pin, and a tag your layout already writes is not written twice.
