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

What a site needs before anybody can look at its blog: this extension's live components, nine
sample pages built out of them, and the sidebar saved as a snippet an author can drop anywhere.

## Why a production site needs this at all

`MishkaBlog.Kit.install!/1` writes the components, and until now its only caller was the dev seed.
A real install went through `MishkaCmsCore.Install.Bootstrap`, which offers Chelekom and knows
nothing about this extension, and a site created later in the admin gets nothing until somebody asks — so the
builder's palette on those sites had no blog components in it and there was no page to put them
on. `ensure!/1` is that missing step, and it runs in production two ways:
`MishkaCmsCore.Install.Extensions` fires `installer:extension:install`, MishkaBlog.Plugins.Installer
answers to its own key, and this is what it runs. Two screens fire it: the install wizard, for the
extensions an operator ticked, and the "Set up blog pages" button on this extension's dashboard,
for a site that already exists.

## Idempotent, and that is the whole design

Running this twice must be indistinguishable from running it once, because both callers can run it
again: an admin can press the button whenever they like, and the wizard and the button can both reach a site
again.

  * **Components.** Only the ones the site has no row for at all — active or archived — are
    installed, through `selection: %{components: missing}`.
    `MishkaCmsCore.Runtime.Handlers.UiKitHandler.install_kit!/2` UPSERTS, so handing it a name the
    site already has would overwrite whatever the admin edited into it, silently. A component the
    admin archived stays archived for the same reason: it is there, and the admin put it where it
    is.
  * **Pages.** Only where every identity the page row has is free, which
    `MishkaCmsCore.Runtime.PageProvision` reads off the resource rather than this module naming
    them. A held ADDRESS is a page left to whoever holds it while the rest of the run happens; a
    held NAME is not — a site holding a page called "Blog" at an address of its own, which is
    exactly what `MishkaCmsCore.Dev.Seeds` writes at `/blog`, gets `/:lang/blog` under the next
    free name and is told which. An archived page counts as holding both, which is stricter than
    the indexes are — they are partial on `(archived_at IS NULL)` — because a page an admin
    archived is an answer, not an absence.
  * **The snippet.** Only when nothing of that name is saved for this site's page editor.

Nothing is ever updated, so nothing an author touched can be lost. What a site that was set up
months ago does about the sample pages having moved on since is
`MishkaCmsCore.Runtime.PageSamples`, behind the same card as the component update: it offers each
outdated page as a DRAFT of itself, which the admin previews in the builder and publishes when
they want it. `sample_pages/1` is what both read the pages from.

WHICH MEANS A SITE PROVISIONED TODAY CAN GET PAGES NEWER THAN ITS COMPONENTS. The pages below are
written against the bundle in this checkout, and a site that already has the components keeps the
ones it has — so an attribute added to a component since that site was seeded reaches a row that
does not declare it, and `MishkaCmsCore.Runtime.Compilers.ComponentCompiler` sweeps every
undeclared key into the component's global attr: it is rendered onto the element as an HTML
attribute, read by nothing, and the behaviour the page asked for is silently the old one. Such a
site takes the newer components from the "Blog components" card on the blog dashboard, which is
`MishkaCmsCore.Runtime.Handlers.UiKitUpgrade`: it says which rows are behind the bundle and which
of them carry an edit, and writes only the ones the admin chooses. What it does NOT do is what
`MishkaBlog.Kit.install!/1` does — upsert every component the bundle ships over whatever is there,
the dev seed's own call — and neither does the button here, because an admin asking for pages has
not asked to lose an edit. `MishkaBlog.ProvisionTest` holds the templates and the bundle together;
no test can see a row installed months ago.

## The pages, and why they look the way they do

Nine pattern pages in `/:lang` PATH mode, which is what seeded and starter content uses here:

    /:lang/blog                  the search, the featured strip and the paged list
    /:lang/blog/:slug            one post, its conversation and what to read next
    /:lang/blog/category/:slug   the same screen, narrowed to that category's posts
    /:lang/blog/tag/:slug        the same screen, narrowed to that tag's posts
    /:lang/blog/categories       every category, as cards
    /:lang/blog/tags             the whole tag cloud
    /:lang/blog/author/:slug     one blogger, their teams and their posts
    /:lang/blog/team/:slug       one team, its members and their posts
    /:lang/blog/people           every team with its members, and the bloggers in none

## What a page is drawn in

Every page is hung on the site's `site-main` layout (`MishkaCmsCore.Runtime.SiteLayout`), whose
header, bar and footer are the site's own: the logo, the language switcher, the sign-in menu and
the dark-mode switch; the blog's name with its four places (Posts, Categories, Tags and People),
which `MishkaBlog.Plugins.PageFrame` hands the layout; and the footer. A page therefore draws only
what lies between them, straight into the layout's column, with no container, width or side
padding of its own. The layout's header answers for the page's languages, which the same plugin
reads off the record a page shows.

The colours, type and radii of everything are the tokens of the site's theme, with a `dark:` twin
for each, and the screens are the design's: the search box with its count, the featured strip, the
row of categories above the cards, the pager that says which posts it shows, the article with its
conversation and what to read next, and the people with their posts. A site that skipped the
starter content has neither the layout nor the theme tokens the markup is written in
(`bg-surface`, `text-dim`, `font-newsreader` and the rest), and these pages are plain on it.

No static route in the host application's router answers any of them, which is the point of the
`/:lang` prefix: `/mish-blog/*` is hand-written marketing the user keeps, and a runtime page
underneath one of those routes would never be reached.

The bare `/blog` that prefix left free is NOT provisioned. A row there could not fill the `:lang`
segment the bases below are built from, so every link off it would name a language nobody asked
for; and the address is the site's own to publish at, which is why it was freed.

ONE ROW PER PAGE, not one per language. `:lang` is a capture, and `MishkaCmsCore.Runtime.Languages`
admits only the site's own languages in it — so `/fa/blog` answers on a site that publishes Persian
and falls through to the 404 on one that does not, and the template reads the language it was asked
for from `@page.language`.

## One article, one URL, and the links that follow from it

Every component on these pages is given `language={@page.language}`, blog-post-detail and
blog-post-comments included: a slug belongs to one edition, so `/fa/blog/<english-slug>` finds
nothing and says so rather than serving the English article inside a Persian, right-to-left page —
or publishing that article's conversation underneath the message saying it is not here.

That is also why the bases the components build links from are written with the capture still in
them — `post_base="/:lang/blog"`, not `"/" <> @page.language <> "/blog"`. The component fills the
`:lang` segment with the language of the ROW it is linking to, which is the language the reader
will be reading in when they arrive, and builds the address the site's URL mode gives it
(`site_path/2`, `MishkaCmsCore.Runtime.LiveViewHelpers`): `/blog/<slug>` in a first language the
site leaves unprefixed, `/fa/blog/<slug>` in Persian. A base with the page's own language baked in
would offer every other language's row under the language the reader is leaving. The languages of
a post itself are not a link of the page's: the layout's header lists them, from
`MishkaBlog.Plugins.PageFrame`, which gives each edition its own slug.

`base`, where a page has one, is the pager's and stays a real URL — nothing appends a slug to it,
and its `?page=2` has to be somewhere a reader could have typed.

## What `:slug` means depends on the page, and only the page knows

One capture name carries a category on `/:lang/blog/category/:slug`, a tag on
`/:lang/blog/tag/:slug`, a blogger on `/:lang/blog/author/:slug`, a team on `/:lang/blog/team/:slug`
and a post on `/:lang/blog/:slug` — while a category list and a tag cloud both read `params` under
the same default `selected_param` to mark the row the reader is on. So a capture is handed only to
the component whose kind of row it holds, and the sidebar snippet, which an author drops on a page
this module knows nothing about, is handed none and marks none.

Every template stays inside what BOTH renderers assign: `@page`, `@query_params`,
`assigns[:current_user]` and `assigns[:page_source]`. The admin's Preview tab assigns no visitor, no
rows and no captures (see `MishkaCmsCore.Runtime.RuntimePreview`), so `@current_user` would raise
there and a nil slug reached by `<>` would too — hence `assigns[:current_user]`,
`assigns[:page_source]` and `|| ""` on everything captured.

# `report`

```elixir
@type report() :: MishkaCmsCore.Runtime.ExtensionProvision.report()
```

See `t:MishkaCmsCore.Runtime.ExtensionProvision.report/0`.

# `bundle_version`

```elixir
@spec bundle_version() :: String.t() | nil
```

The bundle version these pages are written against, stamped onto every page this writes.

It is the KIT's version rather than the extension's: the pages and the components are one release
together — a page drawing a component the site has not got renders an error where the block should
be — and the kit is what `MishkaCmsCore.Runtime.Handlers.UiKitUpgrade` already versions.

# `component_names`

```elixir
@spec component_names() :: [String.t()]
```

The component names this extension's bundle ships, read off the bundle itself.

# `ensure!`

```elixir
@spec ensure!(map() | String.t(), map(), keyword()) :: report()
```

Gives `site` everything the blog ships and it is missing, and returns what happened as
`%{permissions: names, components: names, pages: paths, snippets: names, renamed_pages: renamed,
skipped_pages: held}`, `MishkaCmsCore.Runtime.ExtensionProvision.ensure!/4`'s report.
`permissions` is empty once any site has installed it, the next three are empty on a site that
already has it all, `renamed_pages` names every sample page written under a name of its own
because the one it asked for was taken, and `skipped_pages` names every one something already
there holds, so the caller can say which rather than leave it to the logs.

Takes the site record the hook is handed, or its id. Raises when the site is not there, or when a
write is refused for any reason but a page being taken.

`opts` takes `:actor` — whoever asked for this. Every row of the site is then written as them,
through the action their own role may use, and the resource's policy answers for it; the
permission names, shared by every site, are written by the system. Leave it out and the whole run
writes as the system, which is what the installer wizard and the dev seeds do: they have no
session behind them. See `MishkaCmsCore.Runtime.ExtensionProvision.ensure!/4`.

# `featured_strip`

```elixir
@spec featured_strip() :: pos_integer()
```

How many posts the featured strip on the blog's front page draws, and the list under it leaves
out: what `MishkaBlog.PageSource` reads for the two of them, once.

# `item_paths`

```elixir
@spec item_paths() :: %{
  post: String.t(),
  category: String.t(),
  tag: String.t(),
  author: String.t(),
  team: String.t()
}
```

The paths of the sample pages that read one row each, by the kind of row their `:slug` capture
names — where a post, a category, a tag, a blogger and a team is read.

    Provision.item_paths().post
    #=> "/:lang/blog/:slug"

# `list_page`

```elixir
@spec list_page() :: pos_integer()
```

How many posts one page of the list holds on the blog's front page, a category's page and a
tag's: what `MishkaBlog.PageSource` reads of that list for its first paint, once.

# `manifest`

```elixir
@spec manifest() :: MishkaCmsCore.Runtime.ExtensionProvision.manifest()
```

What this extension declares about itself, for
`MishkaCmsCore.Runtime.ExtensionProvision` — the bundle, the section its pages belong to, the
pages themselves and the snippet.

`pages` and `snippet.source` take the SITE because a stored `<.component>` call names the site
whose component it draws, so the markup cannot be written until the site is known.

# `page_paths`

```elixir
@spec page_paths() :: [String.t()]
```

The paths of the sample pages, in the order they are created.

# `places`

```elixir
@spec places() :: [{:posts | :categories | :tags | :people, String.t()}]
```

The places of the blog, in the order its bar lists them, each with the path of the page that is
its front. A post, a category and a tag are in Posts, and a blogger and a team are in People.

    Provision.places()
    #=> [posts: "/:lang/blog", categories: "/:lang/blog/categories", tags: "/:lang/blog/tags",
    #    people: "/:lang/blog/people"]

# `sample_pages`

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

The sample pages as this checkout ships them, as the attribute maps a page create takes.

`ensure!/1` writes the ones the site has no row for; `MishkaCmsCore.Runtime.PageSamples` compares
the same maps against the rows a site already has, which is how an admin is offered a newer
version of a page they are already serving. Both read them from here, so the two can never be
describing different pages.

Takes the site record or its id, and the actor to read it as — `nil` for a caller with no session
behind it.

# `snippet_name`

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

What the sidebar snippet is called in the Studio's list.

---

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