# `MishkaDocument.Plugins.PageSeo`
[🔗](https://github.com/mishka-group/mishka_document/blob/v0.0.1/lib/mishka_document/plugins/page_seo.ex#L5)

Gives each document and each page of one its own head on the documentation page that serves it,
through the `runtime:page:seo` mishka_installer hook `MishkaCmsWeb.RuntimeLive` fires in
`handle_params/3` (see `MishkaCmsCore.Runtime.Seo` for the map it is handed and what the head
builds from it).

A page is matched by the kind of record it says it shows (`record`, its `extra`'s `"record"`):
`"docs:document"`, a document's front page, or `"docs:page"`, one page of a document — the kinds
`MishkaDocument.Plugins.PageRecords` names. Its path is a document's own address,
`/chelekom/docs` and `/chelekom/docs/*section` (`MishkaDocument.Address`), which names that
document; or anything that captures `workspace`, and `*section` for a page, whose slug names a
document with no address of its own. Every other page is handed back untouched before anything
is read. A kind has one owner, so the answer
for one of these halts the chain: no later plugin is asked.

## What is read

Nothing, on a page a visitor reads: `MishkaCmsWeb.RuntimeLive` hands it the rows the page read
once for its head, its frame and its blocks, as the visitor (`source`,
`MishkaDocument.PageSource`): the active document whose own address the page is at, else the one
the `workspace` capture names, in the page's language; its published versions; the approved pages
of the one shown — the version shown is the one `?v=` names, else the one marked latest, else the
newest published — and the page at the address the `section` capture holds, `headless/switch`,
with its own text and lock. A slug names only a document with no address of its own: one that
has an address is read there and nowhere else, so `/docs/<its slug>` names nothing. A version's
front page is at the document's own address and nowhere under it (`MishkaDocument.Address`), so a
`section` naming its slug names nothing; a group is only a heading in the contents, and nothing is
named at its slug either — unless it has a page of its own (`group_page`), which is named there
as any page is.

A document or a page the visitor's own read left out is answered with its lock alone, read as the
system, so the address answers them 404 and nothing of it reaches them.

An editor's preview and a share card hand no rows, and the same document, versions and page are
read here, once, through `:public_list` as the system (`MishkaDocument.PageSource.workspace/3`).

## What it returns

Every value is a default: what the page sets in its own SEO — a title, a description, a tag,
structured data — wins, and may name the variables below, `{{doc.title}} · {{workspace.name}}`.

  * `title`, `description` — the document's name and description on its front page, whether it
    draws the page its version marks as its front page there or the landing; the page's title and
    the start of its text on one of its pages;
  * `type` — `"article"`;
  * `tags` — the document's or the page's own `seo_tags`; a page that holds no robots meta of its
    own takes its document's, as a CMS page takes its layout's;
  * `schema` — its own `schema` when it holds one, else a `TechArticle`; then a
    `BreadcrumbList`, Documentation → the document → the page. The `TechArticle`'s
    `description` is the description meta the record stores, else the one above; its `image`
    is `{{page.image}}`; its `author` is the `Person` the page's `author` meta names, else its
    document's, and left out when neither names one; its `publisher` is the site, the
    `Organization` at `<site>/#organization`, with the `site.logo` setting as its `logo`. A
    record whose robots meta (`tags`) says `noindex` or `none` has no `TechArticle`: a stored
    robots meta of `noindex, follow` keeps a page that is not ready yet out of an index, and out
    of the sitemap (`MishkaDocument.Plugins.SitemapUrls`);
  * `url` — the canonical address: the document's own address where it has one, with the page's
    under it, so the head names the one address a reader and a crawler are sent to; else the
    address asked;
  * `vars` — `doc.title`, `doc.description`, `doc.url`, `doc.version`, `doc.group`,
    `doc.published_at`, `doc.modified_at` (the front page or the page of it this address shows;
    a page's, like its `TechArticle`'s `dateModified`, is when it last changed —
    `MishkaDocument.Resources.Calculations.LastModified`), and `workspace.name`,
    `workspace.description`, `workspace.url` (the document);
  * `card` — what its share card (`MishkaCmsCore.Runtime.ShareCard`) shows besides the title and
    description: "Documentation" above a document's front page, the document's name above one of
    its pages — `Guide · Navigation` under a group — and the version, on the front page the one
    a reader lands on;
  * `var_docs` — those variables in the current locale, whether or not this request filled them.
    Called with no `params` — an editor asking which variables a page has — that is all it
    changes.

`permissions` is not a default: the document's on its front page; on one of its pages, the page's
own, else its nearest parent page's, else its document's
(`MishkaDocument.Resources.Calculations.EffectivePermissions`). `MishkaCmsWeb.RuntimeLive` opens
the address only for whoever holds one of them, after the page's own, and the head keeps it out
of an index. That is why a record the visitor's read left out has its lock read as the system: a
read as the visitor leaves a locked record out for exactly the visitor the page has to refuse.

## What plugins add to it

The answer is handed to the `document:page:seo` mishka_installer event (`event/0`) through
`MishkaInstaller.Event.Hook.call/3`, and what it replies is the answer; a plugin may give the page
a share card of its own — `image` with `card: false` — or change any other default. A plugin that
opts in with `call/2` is handed, as `meta`:

    %{
      site_id: "…",
      document: %{
        name: "Guide",
        slug: "guide",
        path: "/guide/docs",
        language: "en",
        description: "How to use it.",
        url: "https://example.com/guide/docs"
      },
      version: "2.0.0",
      page: %{
        slug: "breadcrumb",
        title: "Breadcrumb",
        description: "The start of its text.",
        path: "navigation/breadcrumb",
        parents: [%{slug: "navigation", title: "Navigation", group: true}]
      },
      url: "https://example.com/guide/docs/breadcrumb",
      contents: [
        %{title: "Breadcrumb", slug: "breadcrumb", group: false, current: true},
        %{title: "Menu", slug: "menu", group: false, current: false}
      ]
    }

The answer keeps `redraw` as core handed it (`MishkaCmsCore.Runtime.Seo.given/2`): `true` when a
forced look at the share card asks for the head (`MishkaCmsCore.Runtime.ShareCard.specs/3`). A
plugin that draws a card of its own draws it again over its old file then.

`page` is `nil` on a document's front page, and `version` when it has no published version. A
`description` is the description meta the document or the page stores, else the one the head
is given here.
`parents` are the pages and groups the page is filed under, nearest last. `contents` are up to
seven entries of the page's level of the contents, in their order, the page among them marked
`current`; on a front page, the document's top level, its version's front page marked. A plugin
replies with the answer:

    use MishkaInstaller.Event.Hook, event: "document:page:seo"

    def call(seo, %{page: %{title: title}}), do: {:reply, %{seo | title: title <> " · Docs"}}
    def call(seo, _meta), do: {:reply, seo}

A capture that names nothing a reader may open changes nothing else but `card`, `false`: the page
answers 404 as it does without this, and has no share card to draw. A note is drawn inside its
page and has no address of its own, so its own SEO is not read here.

A module because a mishka_installer hook is one. Registered in MishkaDocument.Application.

# `error_return`

```elixir
@type error_return() ::
  {:error, [%{action: atom(), field: atom(), message: String.t()}]}
```

# `okey_return`

```elixir
@type okey_return() :: {:ok, struct() | map() | module() | [any()]}
```

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `config`

```elixir
@spec config() :: keyword()
```

# `config`

```elixir
@spec config(atom()) :: any()
```

# `enable`

```elixir
@spec enable() :: okey_return() | error_return()
```

# `event`

```elixir
@spec event() :: String.t()
```

The mishka_installer event a documentation page's answer is handed to: `"document:page:seo"`.

# `get`

```elixir
@spec get() :: keyword()
```

# `health_check`

```elixir
@spec health_check() :: :ok | {:degraded, term()} | {:error, term()}
```

Optional. See `c:MishkaInstaller.Event.Hook.health_check/0`. Override to report health.

# `on_dependency_error`

```elixir
@spec on_dependency_error(term()) :: term()
```

Optional. See `c:MishkaInstaller.Event.Hook.on_dependency_error/1`. Override to react.

# `register`

```elixir
@spec register() :: okey_return() | error_return()
```

# `restart`

```elixir
@spec restart() :: okey_return() | error_return()
```

# `start`

```elixir
@spec start() :: okey_return() | error_return()
```

# `start_link`

# `stop`

```elixir
@spec stop() :: okey_return() | error_return()
```

# `unregister`

```elixir
@spec unregister() :: okey_return() | error_return()
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
