MishkaCmsCore.Install.StarterContent (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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.

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 (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 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
PathRole
/:lang/authsign_in
/:lang/auth/registerregister
/:lang/auth/forgot-passwordforgot_password
/:lang/auth/magic-linkmagic_link
/:lang/auth/password-reset/:tokenpassword_reset
/:lang/auth/confirm-new-user/:tokenconfirm_new_user
/:lang/auth/magic-link/:tokenmagic_link_sign_in
/:lang/auth/sign-outsign_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.

Summary

Types

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.

Functions

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!/1, answering {:error, :site_not_found} for a site that is not there instead of raising. Any other refusal still raises.

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.

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.

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

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.

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.

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

Types

auth_report()

@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()

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

nav_link()

@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()

@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()]
}

Functions

auth_page_code(role)

@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(site_id)

@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!(site_id)

@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(site_id)

@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()

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

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

seed(site, opts \\ [])

@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!(site, opts \\ [])

@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()

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