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

Gives each post, category, tag, blogger and team its own head on the blog 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"`):
`"blog:post"`, `"blog:category"`, `"blog:tag"`, `"blog:author"` or `"blog:team"`, the kinds
`MishkaBlog.Plugins.PageRecords`
names. Its path may be anything that captures the `slug`. 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.

Everything returned 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, `{{post.title}} | Blog`.

## 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`, `MishkaBlog.PageSource`),
and the record below is the one there, with what that read loaded. A record the visitor's read
left out is answered with its lock alone, read here as the system, so the address answers them
404 and nothing else of it reaches them. An editor's preview and a share card hand no rows, and
the record is read here, once: a post, a category or a tag as the system, and a blogger or a
team, which carry no lock of their own, as a guest, through the public action.

## A post, `"blog:post"`

The post the `slug` capture names in the page's language, published — read through
`:public_list` as `blog-post-detail` reads it, on the page's site, with its authors, categories,
tags and its other published editions:

  * `title`, `description`, `image` — the post's title, excerpt and featured image;
  * `type` — `"article"`;
  * `tags` — `author` (the authors' names), a `twitter:creator` with the X account of the first
    author, in the authors' order, who has one, `article:published_time`, `article:modified_time`,
    `article:section` (the first category), one `article:tag` per tag, the pairs Slack and
    Discord show under a link — `twitter:label1`/`twitter:data1`, then `twitter:label2`/
    `twitter:data2`: "Written by" the authors' names, "Reading time" the post's, each only when
    known and numbered from 1 — and a `rel="alternate"` link per language the post is published
    in among the site's languages, with `x-default` at the site's primary one, when there are two
    or more. The post's own `seo_tags` come last and replace any of these they name;
  * `schema` — the post's own `schema` when it holds one, else a `BlogPosting` whose `author` is
    a `Person` per author — their name, their address on the site's `"blog:author"` page and
    `sameAs` their social links — whose `articleSection` is the first category, and whose
    `publisher` is the site's Organization by its `@id`, `<site address>/#organization`; then a
    `BreadcrumbList`, Home → Blog → the post;
  * `vars` — `post.title`, `post.excerpt`, `post.image`, `post.author`, `post.published_at`,
    `post.modified_at`, `post.url`, `post.collection`, `post.tags`, and `post.rating` (the
    readers' average, one decimal) with `post.rating_count` once a reader has rated it.

`card` — what its share card shows (`MishkaCmsCore.Runtime.ShareCard`): the first category above
the title, the authors' photos and names, their teams, the date and the reading time, over the
featured image.

## A category, `"blog:category"`

The active category the `slug` capture names in the page's language: its name and description,
its own `seo_tags`, its own `schema` (else a `CollectionPage`) and a
`BreadcrumbList`, Home → Blog → the category. `vars` — `collection.name`,
`collection.description`, `collection.url`. Its share card says "Category" above the name.

## A tag, `"blog:tag"`

The tag the `slug` capture names in the page's language: its name and description, its own
`seo_tags`, a `CollectionPage` and a `BreadcrumbList`, Home → Blog → the tag. A public tag says
`robots` `noindex, follow`, unless its own `seo_tags` hold a `robots`; the sitemap lists exactly the
tags that say `index` (`MishkaBlog.Plugins.SitemapUrls`). `vars` — `tag.name`, `tag.description`,
`tag.url`. Its share card says "Tag" above the name.

## A blogger, `"blog:author"`

The blogger the `slug` capture names: their name, their bio as plain text, their avatar, a
`twitter:creator` with their X account when they have one, a
`ProfilePage` in the language of the page whose `mainEntity` is the `Person` — with `sameAs`
their social links and `knowsAbout` their expertise — and a `BreadcrumbList`, Home → Blog → the
blogger. `vars` —
`author.name`, `author.bio`, `author.image`, `author.url`. Its share card says "Author" above the
name, with their photo and their teams. A blogger is the same row in every language, so the slug
alone names them.

## A team, `"blog:team"`

The active team the `slug` capture names: its name, description and avatar, a `ProfilePage` in
the language of the page whose `mainEntity` is the `Organization` with its members as `Person`s, and a `BreadcrumbList`,
Home → Blog → the team. `vars` — `team.name`, `team.description`, `team.url`. Its share card
says "Team" above the name, with the members' photos and how many they are.

## Where "Blog" points

The breadcrumb's middle item is the blog's home, read off the page's own path: the post page's
parent, the parent of every other kind's parent — `/blog/:slug` and `/blog/tag/:slug` both lead
to `/blog`, `/:lang/blog/:slug` to `/:lang/blog` in the page's language.

## What every matched page returns

`permissions` — what the record opens with: named, `MishkaCmsWeb.RuntimeLive` opens the address
only for whoever holds one of them, and the head keeps it out of an index. A tag opens with its
own; a category with its own, else its nearest parent category's; a post with its own, else its
categories' (`MishkaBlog.Resources.Calculations.EffectivePermissions`).

`var_docs` lists the page's 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. A slug that names no row a reader may open changes nothing else.

A module because a mishka_installer hook is one. Registered in `MishkaBlog.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()
```

# `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*
