ferrovue 0.7.2

Vue templates compiled to Rust render functions: server-side rendering with no JavaScript at run time
Documentation
The page head: `useHead` and `useSeoMeta` from `@unhead/vue`, written into `<head>` as unhead's
server renderer writes it.

A component sets the title, meta tags and the rest of the head with
[unhead](https://unhead.unjs.io) (`@unhead/vue` 3), as Nuxt's `useHead` and `useSeoMeta` do. The
compiler translates each call in `<script setup>` into a push onto a [`Head`](crate::Head), in the
order Vue's server would run them: a parent's setup before its children's, the children in the
order they render, and a component `defineAsyncComponent` loads, with everything it renders, after
the components rendered without waiting, as Vue's server waits for it. Once the body is rendered, [`Head::render`](crate::Head::render) gives what
unhead's `renderSSRHead` gives for the same calls: the same tags, in the same order, deduplicated
and escaped the same way, with the title template applied. The suite holds it to unhead's own
renderer, over the conformance components and over hundreds of recorded heads
(`tests/vectors/head.json`).

A component that calls `useHead`, or a child that does, takes a `&Head` as its last parameter.
Build one per page render.

```vue
<!-- Article.vue -->
<script setup lang="ts">
import { useHead, useSeoMeta } from "@unhead/vue";

const props = defineProps<{ title: string; summary?: string }>();

useHead({
  title: () => props.title,
  titleTemplate: "%s · Books",
  meta: [{ name: "description", content: props.summary }],
  htmlAttrs: { lang: "en" },
});
useSeoMeta({ ogTitle: props.title });
</script>

<template>
  <article><h1>{{ title }}</h1></article>
</template>
```

```rust
# mod article {
# // @generated by ferrovue from components/Article.vue. Do not edit: change the `.vue` file and run
# // `ferrovue`.
#
# use std::borrow::Cow;
#
# use ferrovue as fv;
#
# /// The component's name, as `data-island` carries it.
# pub const NAME: &str = "Article";
#
# /// The props `Article.vue` declares.
# #[derive(Debug, Clone, serde::Serialize)]
# #[cfg_attr(test, derive(serde::Deserialize))]
# pub struct Props<'a> {
#     #[serde(rename = "title")]
#     pub title: Cow<'a, str>,
#     #[serde(rename = "summary", default, skip_serializing_if = "Option::is_none")]
#     pub summary: Option<Cow<'a, str>>,
# }
#
# impl<'a> Props<'a> {
#     /// Props with its required fields, every optional one absent.
#     pub fn new(title: impl Into<Cow<'a, str>>) -> Self {
#         Props { title: title.into(), summary: None }
#     }
#
#     /// Set `summary`, which is absent otherwise.
#     pub fn summary(mut self, summary: impl Into<Cow<'a, str>>) -> Self {
#         self.summary = Some(summary.into());
#         self
#     }
# }
#
#
/// Write the component's server render into `out`.
pub fn render(out: &mut String, props: &Props<'_>, fv_head: &fv::Head) {
    out.reserve(28 + props.title.len() + props.summary.as_deref().map_or(0, str::len));
    fv_head.push(fv::HeadValue::object([("title", fv::HeadValue::str(&props.title)), ("titleTemplate", fv::HeadValue::str("%s · Books")), ("meta", fv::HeadValue::array([fv::HeadValue::object([("name", fv::HeadValue::str("description")), ("content", if let Some(v) = props.summary.as_deref() { fv::HeadValue::str(v) } else { fv::HeadValue::Undefined })])])), ("htmlAttrs", fv::HeadValue::object([("lang", fv::HeadValue::str("en"))]))]));
    fv_head.push_seo_meta(fv::HeadValue::Object(Vec::new()), vec![("property", "og:title", fv::HeadValue::str(&props.title))]);
    out.push_str("<article><h1>");
    fv::escape_into(out, &props.title);
    out.push_str("</h1></article>");
}
#
# /// The component's markup, for a maud page that shows it.
# pub fn html<'p, 'a>(props: &'p Props<'a>, fv_head: &'p fv::Head) -> fv::Html<'p, Props<'a>, impl Fn(&mut String, &Props<'a>) + 'p> {
#     fv::Html::markup(props, move |out: &mut String, props: &Props<'a>| render(out, props, fv_head))
# }
# }
let head = ferrovue::Head::new();
let mut body = String::new();
let props = article::Props::new("Dune & Children").summary("A \"desert\" planet");
article::render(&mut body, &props, &head);

let tags = head.render();
let mut page = format!("<!doctype html><html{}><head>", tags.html_attrs);
page.push_str(&tags.head_tags);
page.push_str(&format!("</head><body{}>", tags.body_attrs));
page.push_str(&tags.body_tags_open);
page.push_str(&body);
page.push_str(&tags.body_tags);
page.push_str("</body></html>");

assert_eq!(tags.html_attrs, r#" lang="en""#);
assert_eq!(
    tags.head_tags,
    concat!(
        "<meta charset=\"utf-8\">\n",
        "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n",
        "<title>Dune &amp; Children · Books</title>\n",
        "<meta name=\"description\" content=\"A &quot;desert&quot; planet\">\n",
        "<meta property=\"og:title\" content=\"Dune & Children\">",
    )
);
```

The page renders the body first, then writes the head: the tags in [`HeadHtml`](crate::HeadHtml)
go where their names say, and `html_attrs` and `body_attrs` into the opening tags of `<html>` and
`<body>`. A streamed page renders its body up to its first hole before it sends anything, so the
head is complete by then; a component rendered later, into a hole, adds nothing to it.

[`Head::new`](crate::Head::new) is the head `createHead()` from `@unhead/vue/server` makes, with
unhead's defaults (`lang="en"`, `<meta charset="utf-8">` and the viewport);
[`Head::without_defaults`](crate::Head::without_defaults) is `createHead({ disableDefaults: true })`.
The page can push entries of its own with [`Head::push`](crate::Head::push), as a
`useHead` in the app's root would: pushed before the body renders, a site-wide `titleTemplate` or
stylesheet comes before every component's.

# What a component may pass

`useHead` (and `useServerHead`) takes an object literal, or a getter returning one, of these keys:

| Key | Value |
|---|---|
| `title` | a string, number or boolean, or a getter of one |
| `titleTemplate` | a string, where `%s` stands for the title |
| `meta`, `link`, `script`, `style`, `noscript` | an object literal, an array of them, or `list.map((x, i) => ({ … }))` |
| `base`, `htmlAttrs`, `bodyAttrs` | an object literal |

A tag's object holds attributes, whose values are strings, numbers, booleans, `null` and absent
values, as unhead reads each (an empty string or `"true"` is a boolean attribute; `false` and `null`
leave it out); `content` of a `meta` may be a list, which makes one tag per item; `class` and
`style` take a string, a list of strings or an object literal; `innerHTML` and `textContent` of a
`script` may be an object or array literal, written as JSON. Of unhead's own keys, `key` is any
value, and `tagPosition`, `tagPriority` and `tagDuplicateStrategy` are literals. Any value may be a
getter, `() => props.title`, which unhead calls on the server and ferrovue evaluates.

`useSeoMeta` (and `useServerSeoMeta`) takes an object literal of camel-case keys
(`description`, `ogTitle`, `twitterCard`, `robots`) whose values are strings, numbers, booleans,
`null` or absent, and `title` and `titleTemplate`.

The compiler refuses, each with a code of its own (see [`error_codes`](crate::guide::error_codes)):
a second argument to `useHead`; `useHeadSafe`; `templateParams` and `processTemplateParams`, which
need unhead's template params plugin; a `titleTemplate` function and an event handler (`onload`),
which run with arguments or on the client; a value the server does not compute; a `class` or
`style` that may be `null`, on which unhead's renderer throws; and an object or array given to a
`useSeoMeta` key, which unhead unpacks into tags of its own.

# On the client

The head is unhead's on the client. A component that calls `useHead` takes the [`Head`](crate::Head)
like any other render parameter, so it is never an island of its own; on the client it runs where
the page is hydrated as one app, by `mountPage`, which must give it unhead's client head:

```ts
import { createHead } from "@unhead/vue/client";
import { mountPage } from "ferrovue";

const head = createHead();
await mountPage(() => import("./components/Layout.vue"), islands, { pinia, router, plugins: [head] });
```

unhead's client head adopts the tags the server wrote, matching each by the key it deduplicates
by, so hydrating changes nothing in the document; from then on, `useHead` in the hydrated
components updates the head as it would in any Vue app. `@unhead/vue` is an optional peer of the
`ferrovue` package: install it in the application (`pnpm add @unhead/vue`).

unhead's server escapes a title as text, but an attribute value only for its quotes (`&` and `<`
are written as they are) and the content of a `script`, `style` or `noscript` only for its closing
tag. ferrovue writes exactly that. A value holding `&amp;` or markup in an attribute or a
`<style>` is then read back by the browser as something other than what unhead's client holds,
and the client rewrites that tag; keep text you do not control out of `style` and `script`
content, as with unhead itself.

# Testing

`conformanceSuite` from `ferrovue/testing` installs unhead's server head in each fixture's app when
`@unhead/vue` is installed, and records what it renders after the fixture's HTML, behind
`<!--fv-head-->`, as the generated `render_json` does. Hydrating a fixture installs the client head
over the recorded tags.