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 whenMishkaCmsCore.Sidebar.landing_path/1says 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, asMishkaCmsCore.Runtime.SiteLayoutresolves 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'snamefor each, the current one marked. A page in one language has none. An entry whosepathis a path on the site is a livenavigatelink, so changing language does not reload the page, and the layout sets<html lang dir>when the new page mounts; anhttp(s)://one is a plainhref.- 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 (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
lgup 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 samePhoenix.LiveView.JScommands. Its menu opens under the button. - Below
lgit 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
-darktwin (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-1totint-6, withtint-N-ink, andtint-N-dark,tint-N-ink-darkfor 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;
- six tints, each a surface and its ink:
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_inorsign_out"visitors_only"—trueon every page butsign_out, so a signed-in visitor is sent away"signed_in_only"—trueonsign_outalone, 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 reportensure_auth!/1returns, withsite_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
@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() }
@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
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.
@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.
@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.
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.
@spec error_root_layout() :: String.t()
The error page's default root layout, which links the site's compiled stylesheet.
@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).
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.
@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.