topcoat 0.12.1

A modular, batteries-included Rust web framework for server-rendered apps.
Documentation
Page metadata gives browsers, search engines, and social networks information about a page, such as its title, description, and preview image. [`Metadata`] holds these values. Layouts provide defaults, and pages replace the fields they need to change.

The `metadata` feature is enabled by default. Read the combined values with [`metadata(cx)`](fn@metadata), or render them as HTML tags with [`tags`].

# Setting metadata

Add `Metadata` to a layout or page with `with(...)`. The router makes these values available before rendering, so an outer layout can read the metadata of the page it wraps:

```rust
use topcoat::{
    Result,
    context::Cx,
    metadata::Metadata,
    router::{Router, Slot, layout, page},
    view::{View, view},
};

#[layout("/", with(
    Metadata::new()
        .title("Acme")
        .description("Everything you need for tea.")
))]
async fn shell(cx: &Cx, slot: Slot<'_>) -> Result<impl View> {
    Ok(view! {
        <!DOCTYPE html>
        <html>
            <head>topcoat::metadata::tags()</head>
            <body>(slot)</body>
        </html>
    })
}

#[page("/products", with(Metadata::new().title("Products")))]
async fn products(cx: &Cx) -> Result<impl View> {
    Ok(view! { <h1>"Products"</h1> })
}

let router = Router::builder().layout(shell).page(products).build();
```

The products page has the title "Products" and the description "Everything you need for tea." Setting the title leaves the inherited description in place.

Metadata combines one field at a time. Router builder values are the defaults, outer layouts can replace them, inner layouts can replace those, and the page has the final say. Values added during a request with `cx.with(...)` take priority for code given that child context. See [route context](crate::context#route-context) for how these values reach the request.

An unset field inherits its value. A removal method such as [`no_description`](Metadata::no_description) blocks inheritance for that field, so a page can omit a description supplied by a layout. Other fields keep their values. The same rule applies to individual fields inside the robots, Open Graph, and X settings. The Open Graph image list is treated as one field and replaced as a whole.

# Reading metadata

Call [`metadata(cx).await?`](fn@metadata) to borrow the combined [`PageMetadata`]. You can read it in a page, layout, component, or helper. For example, a component can use the page title as a heading:

```rust
use topcoat::{
    Result,
    context::Cx,
    metadata::metadata,
    view::{View, component, view},
};

#[component]
async fn page_heading(cx: &Cx) -> Result<impl View> {
    let page = metadata(cx).await?;

    Ok(view! {
        if let Some(title) = page.title() {
            <h1>(title)</h1>
        }
    })
}
```

The accessor combines every `Metadata` value visible in the context. Reading `context::<Metadata>(cx)` would return only the closest entry, without inherited fields.

# Rendering tags

Use [`topcoat::metadata::tags()`](tags) to render the page's combined metadata. Place it first in the layout's `<head>`:

```rust
use topcoat::{
    Result,
    context::Cx,
    router::{Slot, layout},
    view::{View, view},
};

#[layout("/")]
async fn shell(cx: &Cx, slot: Slot<'_>) -> Result<impl View> {
    Ok(view! {
        <!DOCTYPE html>
        <html>
            <head>
                topcoat::metadata::tags()
            </head>
            <body>(slot)</body>
        </html>
    })
}
```

The component calls `metadata(cx)` and renders the matching HTML tags. It starts with `<meta charset="utf-8">`, which needs to appear within the document's first 1024 bytes. Fields with no value produce no tag. Some fields have defaults, such as the viewport and Open Graph type, so `tags()` still renders useful tags when no `Metadata` was added.

If you need to choose individual tags or supply custom markup, the components in [`tag`] render each kind separately. Read `metadata(cx).await?` once and pass the result to them, for example `tag::title(metadata: page)`.

# Loading metadata for a request

Values in `with(...)` are created when the router is built. Use [`Metadata::from_fn`] when metadata needs async work or request data, such as a database lookup. It takes an async function that receives `&Cx` and returns `Result<Metadata>`:

```rust
# fn main() {}
use topcoat::{
    Result,
    context::{Cx, memoize},
    metadata::Metadata,
    router::page,
    view::{View, view},
};

# struct Product { name: String, description: String }
# mod db {
#     pub async fn find_product(_: &topcoat::context::Cx) -> topcoat::Result<super::Product> {
#         Ok(super::Product { name: "Teapot".into(), description: "A six-cup teapot.".into() })
#     }
# }
#[memoize(as_ref)]
async fn find_product(cx: &Cx) -> Result<Product> {
    db::find_product(cx).await
}

#[page("/product", with(Metadata::from_fn(async |cx| {
    let product = find_product(cx).await?;
    Ok(Metadata::new()
        .title(product.name.clone())
        .description(product.description.clone()))
})))]
async fn product_page(cx: &Cx) -> Result<impl View> {
    let product = find_product(cx).await?;
    Ok(view! { <h1>(&product.name)</h1> })
}
```

The page and metadata loader share the lookup through [`#[memoize]`](crate::context::memoize). The first caller loads the product, and the other borrows the cached result. See [memoization](crate::context::memoize) for the caching rules.

Loaded fields follow the same inheritance rules as directly set fields. Within one metadata entry, directly set fields take priority. For example, `Metadata::from_fn(load).title("Special offer")` overrides the title returned by `load` while keeping its other fields.

`metadata(cx)` waits for these functions and returns the first error. `tags()` reads through that accessor, so it waits for the metadata before rendering its tags and passes on loading errors.

You do not need `from_fn` for every value that varies by request. Text fields also accept translations from `t!`, which use the locale in the rendering context. With `l10n-fluent` enabled and messages declared through [`messages!`](crate::l10n::fluent::messages), pass the translation directly:

```rust
# topcoat::l10n::fluent::messages!("l10n/{locale}.ftl");
use topcoat::metadata::Metadata;

let metadata = Metadata::new().title(t!(title));
```

See [localization](crate::l10n) for declaring messages and choosing the request's locale. Likewise, [preview images](#images) accept `asset!` directly because asset URLs resolve when rendered.

# Metadata fields

The following sections describe the fields you can set on `Metadata`, their defaults, and the tags they produce.

## Title and description

[`title`](Metadata::title) sets the text of `<title>`. [`description`](Metadata::description) sets `<meta name="description">`, a short summary that search engines may show below the page title:

```rust
use topcoat::metadata::Metadata;

let metadata = Metadata::new()
    .title("Teapots | Acme")
    .description("Browse teapots for everyday use.");
```

Both fields accept strings or translations. Neither has a default. Use [`no_description`](Metadata::no_description) to omit an inherited description.

## Canonical URL

[`canonical`](Metadata::canonical) sets `<link rel="canonical">`. This identifies the preferred URL when the same content is available at several addresses.

Use [`Url::Current`] to use the current request's URL without its query string. This is often a useful layout default:

```rust
use topcoat::{
    context::BaseUrl,
    metadata::{Metadata, Url},
    router::Router,
};

let router = Router::builder()
    .with(BaseUrl::new("https://example.com")?)
    .with(Metadata::new().canonical(Url::Current))
    .build();
# Ok::<(), topcoat::context::BaseUrlError>(())
```

You can also pass an absolute URL, a path relative to the site root such as `/products`, or [`Url::route(&handler)`](Url::route) for a page without path parameters. For a route with parameters, build its URL with [`href!`](macro@crate::router::href) in a [loading function](#loading-metadata-for-a-request).

Canonical tags request absolute URLs. Add a [`BaseUrl`](crate::context::BaseUrl) to the router's context so Topcoat can turn `/products` into `https://example.com/products`. URLs that are already absolute stay unchanged. Resolving a site-relative URL without the required base URL panics.

There is no default canonical URL. Use [`no_canonical`](Metadata::no_canonical) to omit an inherited one.

## Viewport

[`viewport`](Metadata::viewport) controls `<meta name="viewport">`, which tells mobile browsers how to size and scale the page. The default is `width=device-width, initial-scale=1`.

Pass a string to change it:

```rust
use topcoat::metadata::Metadata;

let metadata = Metadata::new()
    .viewport("width=device-width, initial-scale=1, viewport-fit=cover");
```

Use [`no_viewport`](Metadata::no_viewport) to omit the tag, including its default value.

## Search engine instructions

[`robots`](Metadata::robots) takes a [`Robots`] value that sets whether search engines should include the page in search results and follow its links:

```rust
use topcoat::metadata::{Metadata, Robots};

let metadata = Metadata::new().robots(Robots::new().index(false).follow(false));
```

`index(false)` adds `noindex` to the robots tag. `follow(false)` adds `nofollow`. Both settings default to `true`, and the tag is omitted when both are allowed.

The settings inherit separately. For example, a layout can set `index(false)` for a section, and a page can set `follow(false)` while keeping the layout's indexing choice. A page can also use `index(true)` to replace an inherited `false`.

## Theme color

[`theme_color`](Metadata::theme_color) sets `<meta name="theme-color">`. Some browsers use this color for their interface around the page:

```rust
use topcoat::metadata::Metadata;

let metadata = Metadata::new().theme_color("#0f766e");
```

There is no default theme color. Use [`no_theme_color`](Metadata::no_theme_color) to omit an inherited value.

## Open Graph

[`open_graph`](Metadata::open_graph) takes an [`OpenGraph`] value that sets the `og:*` tags used in social link previews. Its fields inherit separately, so a layout can set the website name while a page supplies its own image:

```rust
use topcoat::metadata::{Image, Metadata, OpenGraph, OpenGraphType};

let metadata = Metadata::new().open_graph(
    OpenGraph::new()
        .site_name("Acme")
        .kind(OpenGraphType::Article)
        .image(
            Image::new("https://example.com/images/brewing-tea.png")
                .size(1200, 630)
                .alt("A teapot beside two cups of tea"),
        ),
);
```

[`OpenGraph`] supports these fields:

| Method | Purpose | Value when unset |
| --- | --- | --- |
| [`title`]OpenGraph::title | Title shown in the preview | Page title |
| [`description`]OpenGraph::description | Summary shown in the preview | Page description |
| [`url`]OpenGraph::url | URL that identifies the page | Canonical URL |
| [`site_name`]OpenGraph::site_name | Name of the website | None |
| [`kind`]OpenGraph::kind | Content type, such as a website or article | [`OpenGraphType::Website`] |
| [`image`]OpenGraph::image | Image to show in the preview | No images |

These defaults apply after inheritance. For example, an Open Graph title set by a layout takes priority over the page's regular title. Leave the layout's Open Graph title unset if each preview should use its page's title.

### Images

Pass a URL directly to `image`, or use [`Image`] to add pixel dimensions and alternative text. The title, description, website name, and image alternative text accept translations just like the page title.

Bundled assets work directly too:

```rust
use topcoat::{
    asset::asset,
    metadata::{Metadata, OpenGraph},
};

let metadata = Metadata::new().open_graph(OpenGraph::new().image(asset!("./preview.png")));
```

The asset URL is resolved when the tags render, so this does not need `from_fn`. Set up [asset bundling](crate::asset) as usual. Open Graph page and image URLs request the absolute form, so site-relative URLs also need the router's base URL.

Call `image` several times to add several images to one metadata entry. This list replaces all inherited images. Use [`no_images`](OpenGraph::no_images) to remove the inherited list without adding a replacement.

## X previews

[`twitter`](Metadata::twitter) takes a [`Twitter`] value that sets the `twitter:*` tags for link previews on X. X uses the Open Graph title, description, and images for the preview content. The X settings choose the card format and identify the website and author accounts:

```rust
use topcoat::metadata::{Metadata, Twitter, TwitterCard};

let metadata = Metadata::new().twitter(
    Twitter::new()
        .card(TwitterCard::SummaryLargeImage)
        .site("@acme")
        .creator("@ada"),
);
```

[`card`](Twitter::card) selects the preview format. [`TwitterCard::Summary`] uses a small image beside the text, while [`TwitterCard::SummaryLargeImage`] uses a large image above it. If no card is set or inherited, pages with Open Graph images use the large image format, and pages without images use the summary format.

[`site`](Twitter::site) identifies the website's account, and [`creator`](Twitter::creator) identifies the page author's account. Include the `@` prefix. Neither account has a default, and each field inherits separately.