GamendWeb.DocsLive behaviour (gamend_web v1.0.1216)

Copy Markdown View Source

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.")
end

Options:

  • :collection — the Gamend.Content registered 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 :redirect back to the index. Only ever use :redirect off 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

empty_message()

@callback empty_message() :: String.t()

Shown when the collection's directory is missing or empty.

index_subtitle()

@callback index_subtitle() :: String.t() | nil

The line under the index heading, or nil for none.

index_title()

@callback index_title() :: String.t()

The heading and <title> of the index page.

Functions

index(assigns)

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 to nil.
  • current_path (:string) - Defaults to nil.
  • categories (:list) (required)
  • item_path (:string) (required)
  • title (:string) (required)
  • subtitle (:string) - Defaults to nil.
  • empty_message (:string) (required)

show(assigns)

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 to nil.
  • current_path (:string) - Defaults to nil.
  • guide (:map) (required)
  • category (:map) - Defaults to nil.
  • html (:string) - Defaults to nil.
  • prev (:map) - Defaults to nil.
  • next (:map) - Defaults to nil.
  • siblings (:list) - Defaults to [].
  • index_path (:string) (required)
  • item_path (:string) (required)

siblings(category, slug)

@spec siblings(map() | nil, String.t()) :: [map()]

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.

with_layout_assigns(assigns)

@spec with_layout_assigns(map()) :: map()

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.