# `MishkaCmsCore.Install.StarterContent`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/install/starter_content.ex#L5)

The site a first install answers with: a theme, the logo, a header, a footer, a layout, a home
page and an error page, written through the ordinary create actions.

## Why it is plain HTML

The installer lets the user deselect every UI component (`MishkaCmsCore.Install.Bootstrap`), so a
starter page that drew `chelekom-card` would render the red "Component failed to render!" box on
exactly the install that chose least. Everything below is HTML plus `<.link>`, which a compiled
page has whatever is installed — and NOT the host's `<.input>`, whose class names come from a CSS
framework no bundle here ships.

What the pages DO have is the site's own `:theme` stylesheet, so `bg-page`, `text-fg`,
`border-edge` and the rest are real utilities in the site's bundle. That is the one dependency,
and it is seeded here beside them. Each colour of the home page has a `-dark` twin, and the
markup pairs them with `dark:`, which the site's stylesheet answers to `data-theme="dark"` on the
document; the header's toggle sets that attribute through the host's `phx:set-theme` event, the
same one the sign-in layout uses.

## Every site has it

The installer writes it whatever its first screen said (`MishkaCmsCore.Install.Bootstrap`), and
a site created later in the admin gets the same from its `"provision"` job
(`MishkaCmsCore.Runtime.Workers.SiteRuntimeWorker`), so every site of an install has a home
page, the theme, the header, the footer and the `site-main` layout.

## What belongs to the layout and what to the page

ONE LAYOUT FOR EVERY PAGE. The header and the footer are components, drawn by the `site-main`
layout, so every page the site has, the blog's and the docs' among them, has the home page's
header and footer without asking. The page at `/:lang` is everything between them. What the
layout draws, the content column a page is drawn in and what a page hands the layout (its
languages and the bar under the header) are `MishkaCmsCore.Runtime.SiteLayout`'s.

The layout also links, from the document's `<head>`, the two typefaces the theme does not ship,
Geist and Geist Mono, from Google Fonts, which the public Content-Security-Policy already
allows; Newsreader comes with the host's own `/fonts/fonts.css`.

## What the header changes with

Three things, each handed to the `site-header` by the layout and each read from what the runtime
already assigns, so no page and no helper of ours looks anything up:

  * `@current_user` — a signed-in visitor sees their email and a menu in place of Sign in: the
    console and their account settings when `MishkaCmsCore.Sidebar.landing_path/1` says they may
    open it, and Sign out, which is the DELETE the sign-out page's own form sends. Naming the
    visitor in the layout is also what makes the runtime load one for pages that use it
    (`MishkaCmsCore.Runtime.Compilers.UserClosure`).
  * `@page_languages` — the languages the page is written in, each at its own address, as
    `MishkaCmsCore.Runtime.SiteLayout` resolves them: the page's own editions, or the record's
    translations an extension answers with. With two or more the header has a language
    switcher: a flag (`Languages.flag/1`) and the entry's `name` for each, the current one
    marked. A page in one language has none. An entry whose `path` is a path on the site is a
    live `navigate` link, so changing language does not reload the page, and the layout sets
    `<html lang dir>` when the new page mounts; an `http(s)://` one is a plain `href`.
  * The version the home page's pill shows is `MishkaCmsCore.Helpers.host_version/0`, the
    application's own, read when the page is drawn.

## The header on a phone

The bar is the design's from `lg` (1024 px) up. Below it the links, the language switcher, the
dark toggle and the Sign in button or the visitor's menu are the same elements, in a panel under
the header that a menu button opens: the logo and the button are all the bar holds, so nothing
can run over the logo or off the screen. `lg` and not `md`, because a site that adds links of its
own (the dev sites had up to six, before their links went into one dropdown) and two languages is
wider than 768 px.

The button works as the header's two dropdowns do, with the host's own `Phoenix.LiveView.JS`: it
toggles the panel's `max-lg:hidden` class and its own `aria-expanded`, a click away from the
header or Escape closes the panel and Escape gives the focus back to the button. The switcher and
the visitor's menu open in the panel, in the list, rather than over it.

Each dropdown listens for the click away from the `div` that holds its button, not from its menu:
the click that is meant to close an open menu is on its button, which is "away" from the menu
alone, so the menu hid and the button's own toggle showed it again.

## Grouped links

A site with many links of its own (the dev sites have up to eight) can put them in a group, one
entry of the `:nav` option (`t:nav_entry/0`) that holds the links instead of being one. The
group is written into the layout, beside the plain links, and the `site-header` component is
not told about it.

  * From `lg` up it is a dropdown: a button with the group's label and a chevron, drawn as the
    language switcher's is and opened, closed (by its button, a click away or Escape) and
    marked (`aria-expanded`) with the same `Phoenix.LiveView.JS` commands. Its menu opens
    under the button.
  * Below `lg` it is a labelled group in the menu panel, its links in the list and no button
    to open: the panel is already something a visitor opened.

They are two sets of elements, one hidden at each width, so each has the semantics its width
needs (a menu button; a group with a label) and neither is restyled into the other. The links
are on the page twice, and only one set is ever shown.

## The theme

Three blocks of design tokens and one utility:

  * the admin palette the error pages and the dev seeds' pages are written in (`ground`, `ink`,
    `accent`, …);
  * the home page's colours, each that changes in the dark with a `-dark` twin (`page`, `fg`,
    `dim`, `surface`, `edge`, …), and its three typefaces;
  * the colours the extensions' pages share, so a blog and a documentation kit are written in
    the site's tokens and not in hex values of their own:
    * six tints, each a surface and its ink: `tint-1` to `tint-6`, with `tint-N-ink`, and
      `tint-N-dark`, `tint-N-ink-dark` for the dark (teal, violet, amber, rose, blue, stone);
    * `chip-wash` (`-dark`), the wash behind a chosen chip or link;
    * `brand-dark`, the brand on a dark page;
    * `body` (`-dark`), the text of a long read;
    * `edge-hover`, the edge a hovered or focused card takes, the same in both;
    * `warn-ink` (`-dark`), the ink of a warning or a featured mark;
    * `warn-wash` (`-dark`), the wash behind a warning or a featured mark;
  * `site-bleed`, which lets a section run from one edge of the window to the other while its
    content stays in the layout's column (`MishkaCmsCore.Runtime.SiteLayout`).

## The logo

The mark in the header, the footer and the home page is a picture in the MASTER site's media
library, named `mishka-logo`, and every address of it is that row's `relative_path`. It is
stored the way an upload is (`MishkaCmsCore.Runtime.Media.validate_and_process_file/1`, then
`save_file_to_storage/3`), so it can be replaced, archived or deleted from the media manager like
any other file. A site that is not the master draws the master's picture, which is what a
dev site does. It ships as a PNG under `priv/install`: the library refuses SVG, because a vector
served from the site's own origin can carry a script.

## Why the dev seeds run it too

`MishkaCmsCore.Dev.Seeds` gives every dev site this same base before it adds what that site exists
to demonstrate, so the code a stranger's first install runs is the code this project's developers
look at every day. A production path nobody opens is a production path nobody has seen.

That includes the home page: a dev site's `/` is the one written here, template and stamps, and
the dev seeds add their pages beside it (`:extra_pages`), never in its place.

## Running it again

It writes ONLY WHAT IS MISSING, the rule the whole seeding surface follows: a row found by its
name — the key the database keeps — is left exactly as it is, edits and all, and a page is asked
about through `MishkaCmsCore.Runtime.PageProvision`, which knows that a held NAME and a held
ADDRESS are two different answers.

## The addresses

`/:lang` — the path mode `MishkaCmsCore.Runtime.Languages` validates, so `/fa` answers and `/xyz`
does not. It collides with no route this application answers itself, and it answers every
language the site publishes in (`PageProvision.serving_all_languages/1`).

Every link to another page of the site is written with `site_path/2`
(`MishkaCmsCore.Runtime.LiveViewHelpers.site_path/2`), in the language of the page it is on, so
it follows the site's URL mode: `/auth` and `/fa/auth`, or `/en/auth` and `/fa/auth`. A page that
knows no language links in the site's first.

## The sign-in pages

`ensure_auth!/1` gives a site the `auth` layout and one page per sign-in flow, whatever else the
site holds. Every site gets them: the installer, the dev seeds and a site created in the admin
(the `"provision"` job of `MishkaCmsCore.Runtime.Workers.SiteRuntimeWorker`) all call it. Each
page answers every language of the site and is marked in `extra`:

  * `"auth_role"` — which flow the page is: `sign_in`, `register`, `forgot_password`,
    `magic_link`, `password_reset`, `confirm_new_user`, `magic_link_sign_in` or `sign_out`
  * `"visitors_only"` — `true` on every page but `sign_out`, so a signed-in visitor is sent away
  * `"signed_in_only"` — `true` on `sign_out` alone, so a visitor who is not signed in is sent to
    the sign-in page

| Path                                  | Role                 |
| ------------------------------------- | -------------------- |
| `/:lang/auth`                         | `sign_in`            |
| `/:lang/auth/register`                | `register`           |
| `/:lang/auth/forgot-password`         | `forgot_password`    |
| `/:lang/auth/magic-link`              | `magic_link`         |
| `/:lang/auth/password-reset/:token`   | `password_reset`     |
| `/:lang/auth/confirm-new-user/:token` | `confirm_new_user`   |
| `/:lang/auth/magic-link/:token`       | `magic_link_sign_in` |
| `/:lang/auth/sign-out`                | `sign_out`           |

The pages read these assigns besides the ones every runtime page has: `current_tenant` (the site
a form signs into, `nil` for a master), `client_request` (`%{remote_ip: String.t() | nil}`, what
the rate limits count), `query_params["token"]` on the token pages and
`query_params["return_to"]`, carried on when `MishkaCmsCoreWeb.ReturnTo.verify/2` accepts it.

Every word goes through `t/1`, so a page answers in any language the site publishes in. A language
with built-in words (Persian today) gets a row for each of them; any other reads the English until
its rows are filled in on the Translations screen.

## Events

  * `runtime:site:before_auth_pages` — `%{site_id: site_id, site_name: site_name}`, before the
    sign-in content is written.
  * `runtime:site:after_auth_pages` — the report `ensure_auth!/1` returns, with `site_id`.

# `auth_report`

```elixir
@type auth_report() :: %{
  layout_id: String.t(),
  layouts: [String.t()],
  pages: [String.t()],
  renamed_pages: [MishkaCmsCore.Runtime.PageProvision.renamed()],
  skipped_pages: [MishkaCmsCore.Runtime.PageProvision.held()],
  translations: non_neg_integer()
}
```

# `nav_entry`

```elixir
@type nav_entry() :: nav_link() | %{label: String.t(), children: [nav_link()]}
```

# `nav_link`

```elixir
@type nav_link() :: {String.t(), String.t()} | %{label: String.t(), path: String.t()}
```

A link the header carries, as the `:nav` option takes it: `{label, path}` (or the same as
`%{label:, path:}`), the path being what follows the language, or a group of them,
`%{label:, children:}`, which the header draws as a dropdown (see "Grouped links" above). A
group's children are links; a group does not hold another group.

# `report`

```elixir
@type report() :: %{
  layout_id: String.t(),
  stylesheets: [String.t()],
  media: [String.t()],
  components: [String.t()],
  layouts: [String.t()],
  pages: [String.t()],
  error_pages: [String.t()],
  renamed_pages: [MishkaCmsCore.Runtime.PageProvision.renamed()],
  skipped_pages: [MishkaCmsCore.Runtime.PageProvision.held()]
}
```

# `auth_page_code`

```elixir
@spec auth_page_code(String.t()) ::
  %{template: String.t(), events: list(), helpers: list()} | nil
```

What the sign-in page of `role` (`"sign_in"`, `"register"`, …, its `extra["auth_role"]`) is
written with today: `%{template:, events:, helpers:}`, as `ensure_auth!/1` writes them. Nothing
here updates a page a site already has; this is for a script that brings one up to date, written
with `context: %{page_code: :package}` as these are. `nil` for a role there is no page for.

# `ensure_auth`

```elixir
@spec ensure_auth(map() | String.t()) ::
  {:ok, auth_report()} | {:error, :site_not_found}
```

`ensure_auth!/1`, answering `{:error, :site_not_found}` for a site that is not there instead of
raising. Any other refusal still raises.

# `ensure_auth!`

```elixir
@spec ensure_auth!(map() | String.t()) :: auth_report()
```

Gives `site` the sign-in content it is missing — the built-in words for each language the site
publishes in that has them (Persian today), the `auth` layout and the eight sign-in pages — and
returns what was written. `layout_id` is
the `auth` layout whether this call wrote it or found it.

Takes the site record or its id. A row found is left exactly as it is: a page held at its path is
reported in `skipped_pages`, one whose name only is taken is written under the next free name and
reported in `renamed_pages`, and a translation row that exists keeps its value.

Raises when the site is not there, or when a write is refused for any reason but a row being
taken. See `ensure_auth/1` for the answer a job reads.

# `ensure_offline_entry`

```elixir
@spec ensure_offline_entry(map() | String.t()) :: :added | :held | :no_error_page
```

Gives `site`'s Error Page the offline entry a new site's gets, after the entries it holds, when it
holds none. Answers `:added`, `:held` when it holds one, or `:no_error_page` when the site has no
Error Page that is not archived.

For the dev seeds, so a site seeded before the entry existed picks it up on the next run. The
installer does not call it, so an entry an admin deleted is not handed back.

# `error_root_layout`

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

The error page's default root layout, which links the site's compiled stylesheet.

# `seed`

```elixir
@spec seed(map() | String.t(), keyword()) ::
  {:ok, report()} | {:error, :site_not_found | :no_master_site}
```

`seed!/2`, answering `{:error, :site_not_found}` for a site that is not there and
`{:error, :no_master_site}` for a site that is not the master on an install that has none,
where the logo would be kept, instead of raising. Any other refusal still raises.

For the `"provision"` job of a site created in the admin
(`MishkaCmsCore.Runtime.Workers.SiteRuntimeWorker`).

# `seed!`

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

Gives `site` the starter content it is missing, and returns what was written — plus `layout_id`,
which is the `site-main` row whether this call wrote it or found it, so a caller adding pages of
its own can hang them on the same layout.

Takes the site record or its id. Options:

  * `:nav` — extra links for the header, as `[t:nav_entry/0]`: `{label, path}` where the path is
    what follows the language (`{"Pricing", "/pricing"}`), or a group of them,
    `%{label: "Dev", children: [{"Pricing", "/pricing"}, …]}` — see "Grouped links" above. The dev
    seeds use it to reach the pages they add.
  * `:brand` — the name to print in the header. Defaults to the site's name, which the create
    action has slugified.
  * `:extra_pages` — page definitions (`%{name:, title:, path:, description:, template:}`) written
    after the home page, stamped and held the way the home is. The dev seeds hand it the about,
    contact and privacy pages a first install no longer has, and so every dev site's `/` is the
    home a first install writes, not a copy of it.
  * `:root_layout_template` — the error page's root layout, for a caller that ships its own.

Every page is drawn in the same column, the layout's (`MishkaCmsCore.Runtime.SiteLayout`), so
there is no option for its width: the dev sites are drawn as a stranger's install is.

The logo goes in the master site's media library, `site` or not — see "The logo" above — and
`media` in the report names it when this call stored it. Raises when the site is not there, when
`site` is not the master and there is none, or when a write is refused for any reason but a page
being taken — the installer shows the failure on its own progress list. `seed/2` answers the
first two instead.

# `theme_css`

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

The stylesheet a new site's `site-theme` holds: the design tokens and the `site-bleed` utility.

For whatever checks the colours an extension's markup names against the theme's.

---

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