# `MishkaCmsCore.Runtime.PageBuilder.SeoSettings`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_builder/seo_settings.ex#L5)

The SEO a page or a layout stores, as the builder's settings modal edits it: a Gervaz form.

## A resource with no table behind it

An SEO setting is not a row anywhere. It is a `Page`'s `description`, `seo_tags` and `schema`, or
a `Layout`'s `seo_tags`. Gervaz builds a form from an Ash RESOURCE, so this is one with no data
layer — `Ash.DataLayer.Simple`, as `MishkaCmsCore.Runtime.PageBuilder.DesignToken` — whose `:read`
reads the document as the actor and whose `:update` writes it back through the document's own
action, so every policy and validation stays the document's.

Its id names the document: `"page:<id>"` or `"layout:<id>"` (`id/2`), and `"page:draft:<id>"` or
`"layout:draft:<id>"` for a draft of one, which the builder edits while it writes that draft.

## The record is the draft

What the document stores, the add-a-tag row, whether the JSON box is open, and the head the page
would render (`preview/1`). `change/3` is what one edit writes and `typed/2` what Save reads
from the fields as drawn; `MishkaCmsCoreWeb.Templates.SeoForm` draws the draft and keeps the
edited one on the form's component until Save sends `params/1`.

## What a page sets

  * `title` — a stored `title` tag; empty, the record's title on a page that serves records, else
    the page's own `title`;
  * `description` — the page's own `description`;
  * share image, canonical address, "hide from search engines" — stored `og:image`, `canonical`
    and `robots` tags;
  * structured data — one object of a type `MishkaCmsCore.Runtime.Seo.schema_types/0` offers,
    written in place among the page's others, or the whole of it as JSON.

Every one of them may name a variable, `{{post.title}}`; a plugin on
`MishkaCmsCore.Runtime.Seo.event/0` fills what the page leaves empty. A layout stores tags only.

## What the page shows

A page that serves one record per address names the kind in its `extra`, `"record" => "blog:post"`
— one of `MishkaCmsCore.Runtime.Seo.record_types/1`, which the installed extensions answer — and
the plugin that owns the kind fills its SEO, whatever its path. The page's General tab picks it
(`MishkaCmsCore.Runtime.Resources.Changes.SetRecordKind`); this reads it, for the variables and
for a preview drawn with one of the kind's samples instead of placeholders, and Save leaves it as
it is.

# `t`

```elixir
@type t() :: struct()
```

# `add_question`

```elixir
@spec add_question(t()) :: t()
```

`seo` with one more, empty, question.

# `add_tag`

```elixir
@spec add_tag(t()) :: t()
```

`seo` with the add-a-tag row written as a stored tag, when the row is complete.

# `change`

```elixir
@spec change(t(), map(), [String.t()]) :: t()
```

`seo` after one edit, its head drawn again. `params` are the form's fields as drawn and `target`
the one that changed (`["seo", "add"]`), which decides what else the edit writes — picking a tag
to add starts its row, picking a structured-data type writes its object, typing JSON replaces the
schema.

# `clear_image`

```elixir
@spec clear_image(t()) :: t()
```

`seo` without a share image of its own: each record's, or none, again.

# `default_short_name`

# `id`

```elixir
@spec id(String.t(), map()) :: String.t()
```

The id of a document's SEO: `"page:<id>"`, or `"page:draft:<id>"` for a draft.

# `input`

```elixir
@spec input(values :: map() | Keyword.t()) :: map() | no_return()
```

Validates that the keys in the provided input are valid for at least one action on the resource.

Raises a KeyError error at compile time if not. This exists because generally a struct should only ever
be created by Ash as a result of a successful action. You should not be creating records manually in code,
e.g `%MyResource{value: 1, value: 2}`. Generally that is fine, but often with embedded resources it is nice
to be able to validate the keys that are being provided, e.g

```elixir
Resource
|> Ash.Changeset.for_create(:create, %{embedded: EmbeddedResource.input(foo: 1, bar: 2)})
|> Ash.create()
```

# `input`

```elixir
@spec input(values :: map() | Keyword.t(), action :: atom()) :: map() | no_return()
```

Same as `input/1`, except restricts the keys to values accepted by the action provided.

# `mode`

```elixir
@spec mode(String.t()) :: :page | :tags | nil
```

Which form a section's SEO is: `:page` where its resource stores `schema` and `seo_tags`, `:tags`
where it stores `seo_tags` alone (a layout), `nil` where it stores neither.

# `params`

```elixir
@spec params(t()) :: map()
```

What Save sends the `:update` action for `seo`.

# `preview`

```elixir
@spec preview(t()) :: t()
```

`seo` with the head its page would render, asked of `MishkaCmsCore.Runtime.Seo.resolve/2` with
the draft in place of the stored fields; a dynamic page's plugin answers with its own variables
there too. A layout has no head of its own and is returned as it is.

# `primary_key_matches?`

# `put_view`

```elixir
@spec put_view(t(), String.t()) :: t()
```

`seo` previewed in `view`: `"google"`, `"x"` or `"facebook"`.

# `read`

```elixir
@spec read(Ash.Query.t(), map()) :: {:ok, [t()]}
```

The `:read` action: the document the id names, read as the actor, as its draft — none for an
unknown section, a document the actor may not read, or a query that names no id.

# `record_type`

```elixir
@spec record_type(t()) :: map() | nil
```

The kind of record `seo` shows, as its `record_types` describe it; `nil` for none.

# `remove_question`

```elixir
@spec remove_question(t(), String.t()) :: t()
```

`seo` without the question at `index`.

# `remove_tag`

```elixir
@spec remove_tag(t(), String.t()) :: t()
```

`seo` without the stored tag at `index`.

# `save`

```elixir
@spec save(Ash.Changeset.t(), map()) :: {:ok, t()} | {:error, term()}
```

The `:update` action: the draft's `description`, `seo_tags` and `schema` written through the
document's own update action, as the actor — `MishkaCmsCore.Runtime.PageBuilder.Helpers.save_section/5`,
the write every save in the editor takes. The saved document comes back as the record's `document`.

# `toggle_advanced`

```elixir
@spec toggle_advanced(t()) :: t()
```

`seo` with the Advanced section opened, or closed.

# `toggle_image_manual`

```elixir
@spec toggle_image_manual(t()) :: t()
```

`seo` with the share image's address field shown, or hidden.

# `typed`

```elixir
@spec typed(t(), map()) :: t()
```

`seo` with the fields every edit sends as they are drawn: the description, and the title, share
image and canonical address, each of which is one stored tag — a value writes it, a blank removes
it, an absent field leaves it.

---

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