Draw an extension's pages in the site layout
v0.0.1
A site draws its pages in one layout, site-main, apart from the sign-in pages. The layout holds the site's header and footer, with the page between them. An extension's pages join it when the extension names it on each page it ships. This guide hangs an extension's sample pages on site-main, draws their sections into the layout's column, and keeps the page's language right in a layout of your own.
Before you begin
- Have an extension with a provision, the code that writes its sample pages onto a site when the site sets the extension up.
MishkaBlog.ProvisionandMishkaDocument.Provisionare two. See Extensions and plugins and Install an extension. - Use a site that has the
site-mainlayout. Every site has it: the install wizard writes it with the first site, and a site you add later in the admin gets it too. To change it, see Design the site layout. - The header's language switcher and the bar under the header read each page's frame, which a plugin on the
runtime:page:frameevent fills in. See Page frame: languages and the section bar.
Hang a page on the layout
Call MishkaCmsCore.Runtime.SiteLayout.layout_id/1 with the site's id, and put the answer in each page's layout_id:
alias MishkaCmsCore.Runtime.SiteLayout
SiteLayout.layout_id(site.id)
#=> "b9c2239d-16c1-4eb0-9448-70a95b1ab3fe"layout_id/1 returns the id of the site's layout named site-main, as a string. It returns nil when the site has no such layout, when the layout is archived, and when the argument is not a site id. A page whose layout_id is nil has no layout of its own. It is drawn in the application's default layout, without the site's header and footer.
The id belongs to one site, so your provision builds its pages from a function of the site. Mishka Blog and Mishka Document both read the id once and write it on every page they ship. The example at the end of this page has the same shape.
A provision writes only the pages a site lacks and never updates a row. A page that already exists keeps the layout it has, and running the provision again does not move it.
A new site's site-main draws, from top to bottom:
- the
site-headercomponent; - the section bar, when the page has one;
- the flash messages;
<main id="site-main">, the content column that holds the page;- the
site-footercomponent.
The site's owner can change all of it, so your page draws only what lies between the header and the footer.
The sign-in pages are the exception. The pages at /:lang/auth and under it hang on the auth layout, which every site also has. Leave them there.
Draw sections into the column
The layout owns the content column. Its <main> element has the classes mx-auto w-full max-w-[1160px] flex-1 px-8 pb-24: a column up to 1160 px wide, 32 px gutters at the sides, and 96 px under the last section. The header, the bar, the messages and the footer use the same 1160 px and 32 px, so every edge lines up on a phone and on a desktop.
Draw the page's sections straight into the column, with no container of its own: no mx-auto, no max-w-* and no side padding around the whole page. A container of the page's own doubles the gutters.
The page does own its vertical rhythm, and the room above its first section in particular. Mishka Blog's list pages use pt-16.
<section class="flex flex-col gap-6 pt-16">
<h1 class="text-[44px] font-medium">Notes, guides and long reads.</h1>
<p class="text-body dark:text-body-dark">Short posts from the team.</p>
</section>Run a section edge to edge
A band with a background of its own, such as a colored or dotted one, adds site-bleed. The section then reaches both edges of the window, and what is inside it stays in the column. It needs no container either.
<section class="site-bleed bg-tint-1 pt-[72px] pb-24 dark:bg-tint-1-dark">
<div class="mx-auto max-w-[820px] text-center">
<h2 class="text-[32px] font-medium">Start here</h2>
</div>
</section>site-bleed is a utility in the site's site-theme stylesheet:
@utility site-bleed {
margin-inline: calc(50% - 50vw);
padding-inline: calc(50vw - 50%);
}The negative margin reaches the window's edges, and the padding brings the content back to the column's edges. Both are measured against the column the section sits in. Make a site-bleed section a direct child of the page. A section nested in a narrower box measures that box.
The outer element of site-main has overflow-x-clip. It clips what 100vw adds where the window has a scrollbar, so the page does not scroll sideways. It uses clip and not hidden, so a sticky element inside a page still sticks. Keep that class on the outer element of a layout of your own.
Keep the page's language right in a layout of your own
The root layout draws <html lang dir> once, with the HTTP response. In the host application, MishkaCmsWeb.RuntimeLive.html_lang/1 and html_dir/1 give it the page's language and that language's direction. A language change from the header's switcher is a live navigation: a link to an edition at a path on the site uses navigate, so the page, the header and the title swap inside the open connection and the document is not drawn again. A visit that moves from an English page to a Persian one would keep lang="en" dir="ltr" on a right-to-left page.
site-main fixes this with one hidden element. A layout of your own that lets visitors change language without a reload needs the same element. Put it first inside the layout's outer element:
<span
id={"site-lang-" <> lang(assigns)}
hidden
phx-mounted={
Phoenix.LiveView.JS.set_attribute({"lang", lang(assigns)}, to: "html")
|> Phoenix.LiveView.JS.set_attribute({"dir", dir(assigns)}, to: "html")
}
>
</span>- The element is empty and
hidden. It draws nothing. - Its
idissite-lang-followed by the page's language, such assite-lang-fa. A patch from one language to another replaces the element, and a new element mounts again. - Its
phx-mountedrunsPhoenix.LiveView.JS.set_attribute/2twice onhtml, once forlangand once fordir. It needs no hook and no script. On the first load it writes what the response already says.
The template calls two helper functions. Add them to the layout under Helper Functions, each with the argument assigns. This is the body of lang:
(assigns[:page] || %{})[:language] ||
MishkaCmsCore.Runtime.Languages.primary(MishkaCmsCore.Runtime.Translations.site())This is the body of dir:
MishkaCmsCore.Runtime.Languages.direction(lang(assigns))lang returns the language of this response, assigns[:page].language. On a pattern page such as /:lang/notes that is the captured language. With no page, it returns the site's first language. dir returns "rtl" for a right-to-left language such as Arabic, Persian, Hebrew or Urdu, and "ltr" for the rest.
Example: a notes page
A Notes extension ships one page. Its provision reads the layout id, then writes a tinted band that runs edge to edge, followed by a section in the column. Notes.Kit.bundle_path/0 stands for the path of the extension's UI kit bundle, as MishkaBlog.Kit.bundle_path/0 does for Mishka Blog.
defmodule Notes.Provision do
alias MishkaCmsCore.Runtime.{ExtensionProvision, SiteLayout}
def manifest do
%{
app: :notes,
bundle_path: Notes.Kit.bundle_path(),
section: "notes",
pages: &page_attrs_for/1
}
end
def ensure!(site), do: ExtensionProvision.ensure!(manifest(), site)
defp page_attrs_for(%{id: site_id}) do
layout_id = SiteLayout.layout_id(site_id)
[
%{
name: "Notes",
title: "Notes",
path: "/:lang/notes",
format: :heex,
template: """
<section class="site-bleed bg-tint-1 pt-[72px] pb-16 dark:bg-tint-1-dark">
<div class="mx-auto max-w-[820px] text-center">
<h1 class="text-[44px] font-medium text-tint-1-ink dark:text-tint-1-ink-dark">Notes</h1>
<p class="text-body dark:text-body-dark">Short posts from the team.</p>
</div>
</section>
<section class="flex flex-col gap-6 pt-16">
<h2 class="text-[28px] font-medium">Latest</h2>
<p class="text-body dark:text-body-dark">No notes yet.</p>
</section>
""",
layout_id: layout_id,
active: true
}
]
end
endRun it for a site that has site-main:
report = Notes.Provision.ensure!(site)
report.pages
#=> ["/:lang/notes"]The site now has a page at /:lang/notes, in the section notes, whose layout_id is the id of its site-main layout.
Next steps
- Page frame: languages and the section bar: hand the layout your pages' languages and the bar under the header.
- Page source: read a page's rows once: share one read of a page's rows between the frame, the head and your blocks.
- Translate an extension's words: register the words a plugin draws in the bar.
- Design the site layout: edit
site-mainor build another layout. - Add custom CSS, JS hooks and components: change the colors in
site-theme.