Page source: read a page's rows once
v0.0.1
In Mishka CMS, a page that shows a record has three readers: the plugin on runtime:page:seo that writes the head, the plugin on runtime:page:frame that fills the layout's frame, and the page's own blocks. Without a source, each of them asks the database for the same rows. A plugin on the runtime:page:source event reads the rows once for the address, as the visitor, and the CMS hands what it read to all three. This page lists the event's payload, where the answer goes, the rules a source keeps, the held key and the attribute a block declares to receive the rows. For the plugin that answers for the layout, see Page frame: languages and the section bar. For every other event, see Plugin events.
When the event fires
The host app serves database pages through MishkaCmsWeb.RuntimeLive. Its handle_params/3 calls MishkaCmsCore.Runtime.PageSource.assign_source/1 for every address, after the page, its language, @query_params and the visitor are known, and before the frame and the head. The call fires the event with the map PageSource.given/2 builds and assigns the answer as @page_source. The CMS then resolves the frame and the head and hands them the same rows.
The payload
| Key | Holds | In your answer |
|---|---|---|
page, section, record, extra, params, language, site_id | The same values the frame's plugins receive. See Page frame. | Read only |
current_user | The signed-in visitor, or nil for a guest. Read the rows as this visitor. | Read only |
held | What this LiveView's previous address read, for the same visitor, or nil. See Reuse the last read with held. | Read only |
source | nil. Put your rows here. | Read back |
What a plugin answers
- Put the rows you read under
source. The core never looks inside it, so its shape is yours. - The CMS keeps only
sourceof the answer. An answer that is not a map, or that has nosourcekey, givesnil. - Answer only for the pages you own, matched as a frame plugin matches them: by
record, bysectionor bypage.path. Return every other page untouched with{:reply, given}before you read anything. {:reply, :halt, given}keeps later plugins from answering for a page you own.- A plugin that raises, or an event whose plugins cannot run, leaves the page with no source. Every reader then reads for itself, as it does without a source.
Where the source goes
| Reader | How it receives the rows |
|---|---|
Plugins on runtime:page:seo | As source in their payload, unchanged. The CMS ignores a changed source in their answer. The editor's preview and a share card hand no source, so a plugin reads for itself there. |
Plugins on runtime:page:frame | As source in their payload, unchanged. A frame plugin that finds its record here does not read it again. |
| The page's blocks | As @page_source. The page's template passes it on to each block as the attribute page_source. |
The template passes the rows with page_source={assigns[:page_source]}:
<.component
component_name="shop-detail"
site="acme"
current_user={assigns[:current_user]}
page_source={assigns[:page_source]}
/>Write assigns[:page_source] and not @page_source. The admin's preview assigns no rows, and a template that reads a missing assign with @ raises.
The rules a source keeps
| Rule | What it means |
|---|---|
| Read as the visitor | Read through the public actions and the policies that the blocks read with: the same locks, the same lock inherited from what a record is filed under, and the same page lock. A source is never broader than what the visitor can see, so a reader handed it shows nothing that a read of its own would not. A read as the system belongs only where nothing of it reaches the visitor, such as the lock that makes a record's page answer 404. |
| Keyed to one visitor | A source says whom it was read for. MishkaCmsCore.Runtime.PageSource.visitor_id/1 gives the signed-in visitor's id, or nil for a guest. Keep it in your source. A block compares it with the id of its own current_user before it takes a row, and a block handed a source read for somebody else reads for itself. |
| Alive for one address | The CMS reads the source again in every handle_params/3. It lives in the LiveView's assigns and nowhere else. A task that a block starts receives what it needs as an argument, not the LiveView. |
| A block still stands alone | On the editor's canvas, in a preview, on a page whose template does not pass the rows, and for any read the block was not given, a block reads as it always did. A block handed a source takes only the rows read for exactly what it would read itself. |
Reuse the last read with held
assign_source/1 remembers whom each source was read for. On the next handle_params/3 of the same LiveView, held is the previous @page_source when it was read for the same visitor. In every other case it is nil, such as on the first address of the LiveView or when the last address was read for a different visitor.
- The core checks the visitor and nothing else. Before you reuse
held, compare the site, the language and the record it was read for. Mishka Blog compares the site, the visitor, the language, the kind of record and the slug. - A live patch that stays on the same record, such as the next page of a list or another version, takes from
heldwhat the new address did not change and reads only the rest.
A block must declare page_source
The runtime hands page_source only to a component that declares the attribute. It asks the component's own list of declared attributes each time it draws the component, and only when the template names page_source.
- A component that does not declare it, such as one an admin keeps on an older kit, gets no rows. The runtime draws it as if the template had not named the attribute. Without the check, the rows would fall into the component's global attributes, and the component would draw them onto its root element.
- When a kit adds the attribute, the component gets the rows as soon as it is compiled.
- Declare it with the type
anyand a default ofnil:
{"name": "page_source", "type": "any", "opts": {"default": null}}A block that ships with an extension reads a source by its shape, because its code calls nothing of the extension. It checks that rows_of names the extension, then that site_id is its own site and visitor is its own reader. Anything else it reads for itself.
Example: a shop product page
A shop extension marks its product pages with the record shop:product. This plugin reads the product once per address. MyShop.Catalog.product/3 stands for your own read, made as the visitor.
defmodule MyShop.Plugins.PageSource do
use MishkaInstaller.Event.Hook, event: "runtime:page:source", initial: %{priority: 50}
alias MishkaCmsCore.Runtime.PageSource
@impl true
def call(%{record: "shop:product", params: %{"slug" => slug}} = given) do
visitor = PageSource.visitor_id(given.current_user)
source =
case given.held do
%{rows_of: "my_shop", visitor: ^visitor, slug: ^slug} = held ->
held
_read_for_another_address ->
%{
rows_of: "my_shop",
site_id: given.site_id,
visitor: visitor,
slug: slug,
product: MyShop.Catalog.product(given.site_id, slug, given.current_user)
}
end
{:reply, :halt, %{given | source: source}}
end
def call(given), do: {:reply, given}
endThe page's template passes the rows to a product block, shop-detail, that declares page_source and current_user. The block takes the source with a helper named source_of, which has the argument assigns. It falls back to its own read when the helper returns nil.
site = __MODULE__.__component_information__([:private, :site_id])
visitor =
case assigns[:current_user] do
%{id: id} -> id
_guest -> nil
end
case assigns[:page_source] do
%{rows_of: "my_shop", site_id: ^site, visitor: ^visitor} = source -> source
_not_for_this_block -> nil
endThe results, address by address:
- A visitor with the id
u1opens/shop/mug.heldisnil, so the plugin reads the product. - The same LiveView patches to the same record.
heldis the first source, and the plugin returns it without a read. - The LiveView patches to
/shop/cup.heldwas read for another slug, so the plugin reads again. - The next address is read for another visitor,
u2.heldisnil, and the new source saysvisitor: "u2". - The block, drawn for a guest, takes a source whose
visitorisnil. It reads for itself when its reader is a different visitor, when the source is for another site, and when the page passes none.
What the blog and the document extension put under source
Each extension answers for its own kinds of record and keeps its rows in a struct. The shape is the extension's own, and its blocks rely on it.
| Extension | Answers for | Struct and keys | Blocks that declare page_source |
|---|---|---|---|
| Mishka Blog | blog:post, blog:category, blog:tag, blog:author and blog:team pages, and the front page, which is the page of the Posts place | MishkaBlog.PageSource: rows_of ("mishka_blog"), site_id, visitor, language, kind, slug, record, featured, total and listed | blog-post-list, blog-featured-posts, blog-post-search, blog-post-detail, blog-post-comments, blog-related-posts, blog-author-profile and blog-team-profile |
| Mishka Document | docs:document and docs:page pages | MishkaDocument.PageSource: rows_of ("mishka_document"), site_id, visitor, language, tried, found_by, workspace, versions, version, pages, pages_by_version, body and notes | doc-section, doc-sidebar, doc-toc and doc-switcher |
The struct of the document extension also holds the other editions of the document, which its frame plugin reads. The frame plugins of both extensions take their record from this source. See the Mishka Blog plugin events documentation and the Mishka Document plugin events documentation.