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

What a site needs before anybody can read its documentation: this extension's live components,
three sample pages built out of them, and the "latest documents" block saved as a snippet an
author can drop on a home page.

## Why a production site needs this at all

`MishkaDocument.Kit.install!/1` writes the components, and until now it had no caller at all. A
real install goes 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 `doc-` components in it and there was no page to put them
on. Workspaces, versions, sections and notes existed in the admin with no address a reader could
reach them at. `ensure!/1` is that missing step, and it runs in production two ways:
`MishkaCmsCore.Install.Extensions` fires `installer:extension:install`, MishkaDocument.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 documentation 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 "Documentation" at an address of its own gets
    `/:lang/docs` 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.
  * **The bar's words.** Registered in the site's translations
    (`MishkaDocument.Plugins.PageFrame.words/0`), where a row that is there is left as it is.

Nothing is ever updated, so nothing an author touched can be lost. A component or a sample page
that has moved on since the site was set up is the "Documentation components" card's, on the
document dashboard: `MishkaCmsCore.Runtime.Handlers.UiKitUpgrade` says which component rows are
behind the bundle and which of them carry an edit, and writes only the ones the admin chooses,
and `MishkaCmsCore.Runtime.PageSamples` 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 the run
and the card read the pages from.

The pages below are written against the bundle in this checkout. An attribute a page hands a
component whose row does not declare it is swept by
`MishkaCmsCore.Runtime.Compilers.ComponentCompiler` 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. A site whose components are behind the bundle takes the newer ones
from that card. What neither the run nor the card does is what `MishkaDocument.Kit.install!/1`
does, upsert every component the bundle ships over whatever is there.
`MishkaDocument.ProvisionTest` holds the templates and the bundle together.

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

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

    /:lang/docs                        every document the site publishes
    /:lang/docs/:workspace             one document's front page and its contents
    /:lang/docs/:workspace/*section    one page of it, with its notes — headless/switch for the
                                       page switch filed under headless (`MishkaDocument.Address`)

A document given an address of its own is read there instead, on two pages of the same design
written when the address is set — see `ensure_address!/3`.

No static route in the host application's router answers any of them — the hand-written marketing
the user keeps now sits under `mish-`, `/mish-chelekom/docs` and
`/mish-cms/extensions/documentation` — and the `/:lang` prefix is what guarantees it stays that
way. `MishkaCmsWeb.DocumentProvisionRoutesTest` derives the router's own list rather than
repeating it here.

The bare `/docs` is NOT provisioned, and the prefix is not what made it free: no static route
ever answered `/docs`, which is why `MishkaCmsCore.Dev.Seeds` could already publish a page there.
A row of ours 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.

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/docs` answers on a site that publishes Persian
and falls through to the 404 on one that does not.

## One document, one URL

Every block is given `lang={@page.language}` and a `params` map the language has been taken out
of — the snippet included, on a page belonging to somebody else — which is one rule rather than
two: a page's language is read off the page, and no block is left to find it in a query string.
`MishkaCmsCore.Runtime.PageEditions.language/3` is what makes that one rule enough for a page this
module has never seen: it is the `:lang` capture where the path has one and the page row's own
language where it has none, so there is no page whose language a block has to guess at. A
workspace slug belongs to ONE edition, so `/fa/docs/<an-english-slug>` draws "That page is not
published." rather than the English document inside a Persian, right-to-left shell.

Handing `@query_params` through whole would work today — `MishkaCmsWeb.RuntimeLive` merges the
captured `lang` over the query string precisely so `/fa/docs?lang=xyz` cannot undo the check that
let the page answer — but it would work by that precedence rather than by anything these pages
say, and a block would then be taking its language from a key a visitor also writes. `lang_param`
is left at its default, because on a base that names the language as a segment it decides nothing:
`doc_href/5` carries the code in the path and writes no query key at all.

The bases the blocks build links from keep the capture IN them — `doc_base="/:lang/docs"`, not
`"/" <> @page.language <> "/docs"`. A 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`): `/docs/<slug>` in a first language the site leaves
unprefixed, `/fa/docs/<slug>` in Persian. A base with the page's own language baked in would offer
every translation under the language the reader is leaving.

`base`, which only the index's pager has, stays a real URL: nothing appends a slug to it, and its
`?page=2` has to be somewhere a reader could have typed.

## The header, the footer, the language nav and the bar are the layout's

Every page below hangs on the site's `site-main` layout when the site has one, and draws only what
the design shows between its header and its footer, straight into the layout's column
(`MishkaCmsCore.Runtime.SiteLayout`): no container, no `max-w-*` and no side padding of its own,
since the column is 1160 pixels with its gutters, and a second one would double them. The logo,
the Docs and GitHub links, the sign-in menu, the dark-mode switch and the language switcher are
`site-header`'s, and the footer is `site-footer`'s.

The documentation has no switcher of its own. The header lists the languages a page is in
(`@page_languages`), and on the library, `/:lang/docs`, that is this same page's URL with the
`lang` capture swapped, which is exactly right. On the two document paths it is not: the
`:workspace` and `:section` captures are slugs of one edition, and the Persian edition of a
document has Persian slugs, so the core's default would offer `/fa/docs/<english-slug>`, a URL
its own page answers as not published. `MishkaDocument.Plugins.PageFrame` answers for them on
`runtime:page:frame`: the languages the document or the page really exists in, each at its own
address, and the bar under the header — Docs, the document and the page as a trail, and the
version in a menu (`doc-switcher`). A page of a document at an address of its own
(`ensure_address!/3`) is answered the same way.

Every template stays inside what BOTH renderers assign: `@page`, `@query_params` and
`@page_languages`. The admin's Preview tab assigns no visitor and no captures (see
`MishkaCmsCore.Runtime.RuntimePreview`), which is why no URL here is built from a capture — only
from `@page.language`, which a preview does assign.

# `report`

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

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

# `address_permissions_missing`

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

The names `actor` is missing to give a document at `address` its pages on `site_id` — `[]` when
the site already has a page at both paths, so nothing would be written.

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

# `docs_base`

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

The base the documents with no address of their own are read under, by their slug:
`/:lang/docs`. A document's own address may not be under it (`MishkaDocument.Address.check/1`).

# `ensure!`

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

Gives `site` everything the documentation ships and it is missing, and returns what happened as
`%{permissions: names, components: names, pages: paths, snippets: names, renamed_pages: renamed,
skipped_pages: held}` — `permissions` empty once any site has installed it, the next three empty
on a site that already has it all, `renamed_pages` naming every sample page written under a name
of its own because the one it asked for was taken, and `skipped_pages` naming every one something
already there holds, so the caller can say which rather than leave it to the logs. The bar's
words are registered in the site's translations too (see "Idempotent" above).

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`.

# `ensure_address!`

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

Gives a document at `address` the two pages it is read at — its front page, and every page of it
(`MishkaDocument.Address.page_paths/1`) — on `site`, where the site has no page at that path yet.
A page the site already has at either path, its own design among them, is left as it is.

Written as `opts[:actor]`, who needs what `MishkaCmsCore.Runtime.ExtensionProvision.required/1`
names; leave it out and the system writes. Returns the report `ensure!/3` gives.

# `item_paths`

```elixir
@spec item_paths() :: %{workspace: String.t(), section: String.t()}
```

The paths of the two sample pages that read one document: `:workspace` its front page, `:section`
one page of it. Each is filled with a document's language and slugs by
`MishkaCmsCore.Runtime.Languages.url/3` — what `MishkaDocument.Plugins.SitemapUrls` lists.

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

# `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 snippet is called in the Studio's list.

---

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