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

What a site's sample pages are against the ones this checkout ships, and the one write that offers
the newer ones without taking an admin's page away from them.

## The gap this closes

An extension's "Set up …" button writes only the pages a site has no row for, deliberately — see
`MishkaCmsCore.Runtime.PageProvision` — so a site provisioned once keeps the pages it was
provisioned with for ever. `MishkaCmsCore.Runtime.Handlers.UiKitUpgrade` closed the same gap for
COMPONENTS, and closing it left the other half wide open: the blog's featured strip became a
component of its own, the index page that draws it was rewritten to place it, and no site already
serving that page ever saw the new one.

## Why a draft, and not an update

A live page is the admin's. They may have rewritten it, hung their own blocks on it, or simply be
serving it to readers right now — and nothing here can tell a page that is BEHIND from one
somebody EDITED, because a page carries no fingerprint of what it was created from. An update that
guessed would publish this module's markup over somebody's work, live, with no step in between.

So the offer is a `MishkaCmsCore.Runtime.PageDraft` of the page itself: it appears in the
builder's draft picker next to the admin's own drafts, they open it, look at it, and publish it if
they want it. Nothing live changes until they do, and publishing keeps a version of what was there
through the page's own paper trail.

## The answers

`plan/2` sorts every page an extension ships into:

  * `:current` — the row holds exactly the template this checkout ships;
  * `:outdated` — it does not, and no draft of this version's page is waiting on it. This is what
    the button writes;
  * `:offered` — a draft carrying this version's template is already pending on that page, so
    there is nothing to write. This is what makes a second press do nothing;
  * `:blocked` — it is behind, and the page this version ships draws a component this site cannot
    draw. See below;
  * `:taken` — a page that is not ours answers at that address;
  * `:archived` — a page of ours is at that address and the admin archived it;
  * `:absent` — nothing is at that address at all, which is the "Set up …" button's job and never
    this one's.

The last three are one answer in the site's eyes and three different dead ends in the admin's: the
"Set up …" button writes the absent one and skips the other two, so a card that called all three
absent would send an admin to a button that does nothing for two of them.

A row is ours when its ADDRESS and its SECTION are both the definition's. The address alone would
claim a page an admin wrote at the same path as their own work and offer to replace it; the
section is what an extension stamps every page it writes with, and a page whose section an admin
has changed is one this cannot recognise — so it is left alone, which is the safe way round.

## Why a page can be held back

A page is markup that names components by name, and the name resolves against the site's own rows
at render time: `MishkaCmsCore.Runtime.LiveViewHelpers` draws a red "Component failed to render!"
line in place of one it cannot build. This version's blog index draws `blog-featured-posts`, which
is newer than the sites the offer exists for, and no update installs a component a site does not
have — the "Set up …" button does. So offering that page to a site without it would be offering an
admin a draft whose whole purpose is to put an error message in front of their readers.

`:blocked` is the answer instead, naming the components that are not there. Nothing is written for
such a page at any setting.

## Pressing it twice

`offer!/3` reads the plan again inside the call rather than taking it from the caller, and writes
one draft per `:outdated` page. On the second press those pages are `:offered` and nothing is
written. An admin who then EDITS the draft moves it off this version's template, so the offer is
made again — that is the honest answer to "is a draft of the shipped page waiting here", and the
second draft costs them nothing but a row in the picker.

# `entry`

```elixir
@type entry() :: %{
  name: String.t(),
  standing: standing(),
  page_id: String.t() | nil,
  needs: [String.t()]
}
```

One shipped page and its standing. `name` is the page's ADDRESS: that is what an admin recognises
a sample page by, and what every report of one is written from. `needs` names the components this
version's page draws that the site cannot draw, and is empty wherever there are none.

# `plan`

```elixir
@type plan() :: %{
  site_id: String.t(),
  serves_pages?: boolean(),
  current: [entry()],
  outdated: [entry()],
  offered: [entry()],
  blocked: [entry()],
  taken: [entry()],
  archived: [entry()],
  absent: [entry()]
}
```

# `report`

```elixir
@type report() :: %{
  drafted: [String.t()],
  waiting: [String.t()],
  current: [String.t()],
  blocked: [String.t()],
  taken: [String.t()],
  archived: [String.t()],
  absent: [String.t()]
}
```

# `standing`

```elixir
@type standing() ::
  :current | :outdated | :offered | :blocked | :taken | :archived | :absent
```

Where one shipped page stands on this site. `:offered` is not a kind of `:outdated` — it is the
work already done, and a button that offered it again would leave the picker full of copies.

# `message`

```elixir
@spec message(report()) :: String.t()
```

What one offer did, worded from what was actually written.

The sentence about nothing living having changed is not decoration: an admin who pressed a button
on a dashboard and was told "3 pages" has every reason to think their site just changed, and the
whole point of this path is that it did not.

# `note`

```elixir
@spec note(entry()) :: String.t()
```

The line a dashboard shows beside one page, saying what its standing is.

An `:outdated` page is not called old. Nothing here can tell a row that is behind from one an
admin rewrote, and a card that called somebody's own work outdated would be lying to them in the
one sentence that has to be trusted.

# `offer!`

```elixir
@spec offer!(String.t(), [map()], keyword()) :: report()
```

Offers every outdated page as a draft of itself and says what it did.

`:label` is what the drafts are called in the builder's picker; it defaults to `"Sample page"`.

Writes nothing but `MishkaCmsCore.Runtime.PageDraft` rows. No live page is touched here at any
setting — publishing one is the admin's own gesture, in the builder, one page at a time.

Pass `actor: user` and every draft is written as them, so its history says who forked the page.

# `plan`

```elixir
@spec plan(String.t(), [map()], map() | nil) :: plan()
```

Where every page in `definitions` stands on `site_id`. Reads, and writes nothing at all: this is
what a dashboard shows an admin BEFORE anything happens.

`definitions` are the attribute maps a page create takes — the same ones the extension hands
`MishkaCmsCore.Runtime.PageProvision` — so the two can never be describing different pages.

---

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