A documentation manual embedded at compile time, so a Phoenix app can serve
it at a path of its own (/docs) instead of publishing a second site.
The host keeps a module (MyApp.Docs, say) whose body is nothing but this:
use Managoat.Docs,
root: Path.expand("../..", __DIR__), # where snippet includes resolve
docs_dir: "docs", # the pages, relative to root
nav: "docs/nav.yml", # the nav, relative to root
mount: "/docs", # the path the manual is served at
extra_resources: ["CHANGELOG.md"], # other files a page includes
languages: ~w(bash elixir json) # parsers the image bakesEvery option has the default shown except root, which is required. The
macro reads the nav and every page it names in the host module's body, so
each of them is an @external_resource of the host module, which is
where recompilation has to be triggered when a page changes. It generates:
| Function | Returns |
|---|---|
nav/0 | the sidebar: {title, slug} for a page, {section, [{title, slug}]} for a section |
nav_source/0 | the same nav in source shape, {title, file}, for the guardrails |
slugs/0 | every page slug (the home page is "") |
get/1 | {:ok, %{title: t, body: markdown}} or :error |
search_index/0 | one entry per page with its h2–h6 headings and their ids |
search_index_json/0 | the index pre-encoded, safe to inline in a <script> |
path_for_slug/1 | "" → the mount, "setup" → mount <> "/setup" |
external_resources/0 | every file read at compile time, relative to root |
root/0, docs_dir/0, mount/0, languages/0 | the options, resolved |
A page's body is plain markdown: the compile step (Managoat.Docs.Compiler)
has expanded snippet includes, turned admonitions into blockquotes and
rewritten relative .md links to paths under the mount. The host renders it
with Managoat.Docs.Markdown.to_trusted_html/1 and wraps the result in
Phoenix.HTML.raw/1.
The dialect
The markdown a manual may use beyond CommonMark and GFM tables is deliberately small, inherited from the MkDocs Material site Fountain's pages were published as before they moved in-app:
--8<-- "path/to/file"on a line of its own inlines that file, read relative toroot;!!! note "Title"followed by a four-space-indented body becomes a blockquote with a bold title line;[text](other-page.md#anchor)is rewritten tomount/other-page#anchor, resolved against the linking page's own directory.
Nothing else is rewritten. If a page starts using something beyond that, check it as served; there is no second renderer to disagree with.
The nav
nav.yml has a nav: block that is a hand-written list two levels deep:
nav:
- Home: index.md
- Setup: setup.md
- Guides:
- Deploy: guides/deploy.md
- Operate: guides/operate.mdManagoat.Docs.Compiler.parse_nav/1 reads exactly those two line shapes
and raises at compile time on anything else, a nested section
included: a page silently missing from the manual is the bug the parser
exists to make impossible, so an unreadable nav fails the compile rather
than shrinking the site.
The guardrails
What makes the manual safe to change is Managoat.Docs.GuardrailCase, a
case template the host uses in a test of its own. Each check exists
because of an incident in Fountain's history: every page the nav names
exists and every page on disk is named; every internal link and every
#anchor resolves against the rendered heading ids; every file read at
compile time is COPYd into the image's build stage, because a missing
one does not break a link, it kills mix release while CI stays green.