The shared renderer for a markdown guide collection: an index and a page.
A guide is a markdown file and nothing else — the folder gives its category,
the numeric filename prefix its order, the first heading its title, and
optional front matter its heroicon. A folder's _category.md names the
category and gives it an icon and a colour, which its guides inherit so each
section reads as one group. Adding one takes no Elixir change.
Why this is in core
Three copies of this page existed: gamend's public /docs, Polyglot
Pirates' player guide at /guide, and its admin engineering docs at
/admin/docs — the last two forked from the first, along with a second copy
of the loader in Gamend.Content. They drifted in both directions. The
guide grew pill badges, prev/next navigation, a coloured title icon and the
standard Back button; the docs kept the sibling list, :persistent_term
caching and a real 404. Every host wanted the union and no host had it.
Why a use macro rather than a configured route
A host needs its own <title> copy, its own gettext domain and its own base
path, and live/4 gives a LiveView nowhere to put any of that. A module per
collection also keeps GamendHost.PageMeta and the router referring to a
real module name, the way they already do.
defmodule MyAppWeb.GuideLive do
use GamendWeb.DocsLive,
collection: :guide,
index_path: "/guide",
item_path: "/guide"
def index_title, do: gettext("Player guide")
def index_subtitle, do: gettext("How the game actually works.")
endOptions:
:collection— theGamend.Contentregistered path name. Default:docs.:index_path— where the index lives, for the "all guides" link.:item_path— the prefix a guide's own URL is built from.:not_found—:raise(a 404, the default) or:redirectback to the index. Only ever use:redirectoff a public URL knowingly: a bad slug answering 200 is a soft 404, which is worse for a crawler than a hard one.
Navigation carries both shapes
Prev/next and the sibling list. A collection written to be read front to back needs the first; one read as reference needs the second; and a reader who wants neither loses nothing by their being there. Picking one per collection was the alternative, and it is a knob that exists only because two authors happened to write two pages.
Summary
Callbacks
Shown when the collection's directory is missing or empty.
The line under the index heading, or nil for none.
The heading and <title> of the index page.
Functions
The index: every category, every guide's title and summary.
One guide: title, body, then both navigations.
The other guides in a guide's category, in reading order.
Fills in the assigns the layout reads but a LiveView does not always have.
Callbacks
Functions
The index: every category, every guide's title and summary.
Titles and summaries only. Repeating the bodies here would make every guide duplicate content competing with itself, and make the index the longest page in the collection.
Attributes
flash(:map) (required)current_scope(:any) - Defaults tonil.current_path(:string) - Defaults tonil.categories(:list) (required)item_path(:string) (required)title(:string) (required)subtitle(:string) - Defaults tonil.empty_message(:string) (required)
One guide: title, body, then both navigations.
No category eyebrow above the title. The category is on the title icon as a colour and named again at the foot of the page, where "More in Operations" heads a list you can act on — an uppercase band between the breadcrumb and the heading said it a third time and linked nowhere.
Attributes
flash(:map) (required)current_scope(:any) - Defaults tonil.current_path(:string) - Defaults tonil.guide(:map) (required)category(:map) - Defaults tonil.html(:string) - Defaults tonil.prev(:map) - Defaults tonil.next(:map) - Defaults tonil.siblings(:list) - Defaults to[].index_path(:string) (required)item_path(:string) (required)
The other guides in a guide's category, in reading order.
Empty rather than nil when the guide is alone in its category — or has no
_category.md at all — so the template's :if reads as a list check.
Fills in the assigns the layout reads but a LiveView does not always have.
current_path is set by a plug that not every host mounts, and
current_scope is absent on a public page with no session. Both are optional
to the layout and neither can be defaulted by attr here — see render/1.