@heroiclands/package-build 22.23.0 → 22.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -0
  3. package/docs/authoring/assets.md +15 -0
  4. package/docs/commands.md +18 -19
  5. package/docs/configuration.md +2 -6
  6. package/docs/guides/module-project.md +2 -2
  7. package/docs/guides/site.md +41 -0
  8. package/docs/project-setup.md +4 -4
  9. package/docs/reference/theme-pages.md +496 -0
  10. package/engine/art-slots.mjs +2 -1
  11. package/engine/foreign-catalog.mjs +2 -1
  12. package/engine/helpers.mjs +5 -3
  13. package/engine/site-config.mjs +19 -24
  14. package/hugo-theme/data/banners.yaml +69 -0
  15. package/hugo-theme/layouts/404.html +85 -0
  16. package/hugo-theme/layouts/_default/baseof.html +64 -0
  17. package/hugo-theme/layouts/_default/list.html +139 -0
  18. package/hugo-theme/layouts/_default/single.html +4 -0
  19. package/hugo-theme/layouts/blog/list.html +54 -0
  20. package/hugo-theme/layouts/blog-post/single.html +36 -0
  21. package/hugo-theme/layouts/index.html +75 -0
  22. package/hugo-theme/layouts/partials/banner-url.html +117 -0
  23. package/hugo-theme/layouts/partials/cdn-url.html +36 -0
  24. package/hugo-theme/layouts/partials/draft-notice.html +41 -0
  25. package/hugo-theme/layouts/partials/draft-state.html +26 -0
  26. package/hugo-theme/layouts/partials/footer.html +29 -0
  27. package/hugo-theme/layouts/partials/header.html +35 -0
  28. package/hugo-theme/layouts/partials/hero-banner.html +86 -0
  29. package/hugo-theme/layouts/partials/holdings/entry.html +21 -0
  30. package/hugo-theme/layouts/partials/holdings/kind-groups.html +71 -0
  31. package/hugo-theme/layouts/partials/holdings.html +61 -0
  32. package/hugo-theme/layouts/partials/infobox/entry.html +15 -0
  33. package/hugo-theme/layouts/partials/infobox/items.html +26 -0
  34. package/hugo-theme/layouts/partials/infobox/section.html +56 -0
  35. package/hugo-theme/layouts/partials/infobox/value.html +43 -0
  36. package/hugo-theme/layouts/partials/infobox.html +57 -0
  37. package/hugo-theme/layouts/partials/page-body.html +154 -0
  38. package/hugo-theme/layouts/partials/place-map.html +9 -0
  39. package/hugo-theme/layouts/partials/related.html +121 -0
  40. package/hugo-theme/layouts/partials/search.html +84 -0
  41. package/hugo-theme/layouts/partials/site-url.html +32 -0
  42. package/hugo-theme/layouts/partials/tags.html +30 -0
  43. package/hugo-theme/layouts/partials/toc.html +28 -0
  44. package/hugo-theme/layouts/partials/warn-missing-banner.html +3 -0
  45. package/hugo-theme/layouts/shortcodes/cartouche.html +64 -0
  46. package/hugo-theme/layouts/shortcodes/exturl.html +12 -0
  47. package/hugo-theme/layouts/shortcodes/glyph.html +43 -0
  48. package/hugo-theme/layouts/shortcodes/pagelist.html +63 -0
  49. package/hugo-theme/static/css/sohl-icons.css +57 -0
  50. package/hugo-theme/static/css/style.css +2953 -0
  51. package/hugo-theme/static/fonts/game-icons-kb.woff2 +0 -0
  52. package/hugo-theme/static/img/banners/takheperu-hieroglyphs.svg +1 -0
  53. package/hugo-theme/static/img/cartouches/takheperu.svg +1 -0
  54. package/hugo-theme/theme.toml +11 -0
  55. package/package.json +2 -1
  56. package/types/engine/art-slots.d.mts +2 -1
  57. package/types/engine/helpers.d.mts +5 -3
  58. package/types/engine/site-config.d.mts +9 -9
@@ -0,0 +1,154 @@
1
+ {{- /*
2
+ A note's body — the infobox rail, the content, and prev/next through its
3
+ catalog. Shared by `_default/single.html`, for a page, and `index.html`,
4
+ for the homepage note rendered as a page: the two are the same family, and
5
+ this is the shape they share. The caller renders the hero; this partial
6
+ starts at the article.
7
+
8
+ Context: the page.
9
+ */ -}}
10
+ {{- /* The boxes a page declares, drawn by one generic renderer. What each box
11
+ holds is decided by the build and carried in `infoboxes:` front matter;
12
+ this file decides only where the rail goes. A page declaring none gets
13
+ back an empty string and no rail. */ -}}
14
+ {{- $infobox := partial "infobox.html" . -}}
15
+ {{- $hasSidebar := ne (trim $infobox " \r\n\t") "" -}}
16
+
17
+ {{- /* The TOC partial only emits markup when the page has more than
18
+ 3 H2 headings. To avoid laying the page out as a two-column
19
+ grid for nothing, detect the same condition here so we can
20
+ apply the `.single-with-toc` class conditionally. A page that
21
+ already has a rail gets its contents list at the foot of that
22
+ rail rather than a second column. */ -}}
23
+ {{- $h2Count := len (findRE `(?m)^##\s+\S` (.RawContent | default "")) -}}
24
+ {{- $hasToc := gt $h2Count 3 -}}
25
+
26
+ {{- /* A draft page states it, which also decides the article's first grid
27
+ row — see the notice partial and `.single-with-notice`. */ -}}
28
+ {{- $isDraft := partial "draft-state.html" . -}}
29
+
30
+ <article class="single{{ if $hasSidebar }} single-with-sidebar{{ else if $hasToc }} single-with-toc{{ end }}{{ if $isDraft }} single-with-notice{{ end }}">
31
+ {{- /* A statement about the page rather than about its subject, so it leads
32
+ the article: above the rail and above the prose, at every width. It
33
+ spans the grid for that reason — inside `.single-content` it would sit
34
+ beside the boxes on a wide screen and below them on a narrow one,
35
+ where the rail comes first. */ -}}
36
+ {{ partial "draft-notice.html" . }}
37
+ {{- /* Filter values a search can narrow on, invisible and excluded from
38
+ indexing themselves — `data-pagefind-filter` is read wherever it
39
+ sits, `data-pagefind-body`/`data-pagefind-ignore` scoping included
40
+ (see the README's "Search" section). */ -}}
41
+ {{- with .Params.type }}<span data-pagefind-filter="type" hidden>{{ . }}</span>{{ end -}}
42
+ {{- with .Params.package }}<span data-pagefind-filter="package" hidden>{{ . }}</span>{{ end -}}
43
+ {{- if $hasSidebar }}
44
+ {{- /* The rail comes first in document order, because the infobox is
45
+ generated content prepended before the prose: on a narrow screen the
46
+ grid collapses and the boxes land where they were written, above the
47
+ text they summarise. */ -}}
48
+ <aside class="info-rail">
49
+ {{ $infobox }}
50
+ {{ if $hasToc }}{{ partial "toc.html" . }}{{ end }}
51
+ </aside>
52
+ {{- end }}
53
+ <div class="single-content">
54
+ {{- $tagChips := partial "tags.html" . -}}
55
+ {{- $beingSubtype := "" -}}
56
+ {{- if and (eq .Params.type "being") .Params.subType -}}
57
+ {{- $beingSubtype = cond (eq .Params.subType "npc") "NPC" (humanize .Params.subType) -}}
58
+ {{- end -}}
59
+ {{ if or .Params.date (ne (trim $tagChips " \r\n\t") "") $beingSubtype }}
60
+ <div class="single-meta">
61
+ {{ if .Params.date }}
62
+ <time class="single-date" datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
63
+ {{ end }}
64
+ {{- with $beingSubtype }}<span class="single-subtype">{{ . }}</span>{{ end }}
65
+ {{ $tagChips }}
66
+ </div>
67
+ {{ end }}
68
+
69
+ <div class="single-body" data-pagefind-body>
70
+ {{ .Content }}
71
+ </div>
72
+
73
+ {{ partial "holdings.html" . }}
74
+ {{ partial "place-map.html" . }}
75
+ {{ partial "related.html" . }}
76
+
77
+ {{- /*
78
+ Prev/next walk the page's own catalog, not its Hugo section (issue #48).
79
+ `.PrevInSection` / `.NextInSection` mean "the neighbouring page in this
80
+ directory", which was the same thing only while a page lived in a
81
+ directory named for its catalog. `@heroiclands/package-build`
82
+ (package-build#204) emits every content page flat under the content
83
+ mount, so a consumer's whole catalog is one section and these two links
84
+ would walk every page of every type — reading through the afflictions
85
+ silently left them.
86
+
87
+ So where a section holds more than one catalog, the sibling set is
88
+ narrowed to the pages sharing this page's catalog key. A section holding
89
+ a single catalog is left alone, because `.PrevInSection` already walks
90
+ exactly that catalog there — which is what makes this a no-op for a site
91
+ whose pages still sit in catalog-named directories. A page declaring no
92
+ catalog keeps the section-scoped result. The home page declares no
93
+ catalog and has no section siblings either way.
94
+
95
+ The catalog key: `type` for structured content, whose type *is* its
96
+ catalog (`affliction`, `skill`, `weapongear`, …); for `type: doc`, which
97
+ is prose rather than a catalog of its own, `subType` (what package-build
98
+ compiles from a note's classification) or, failing that, `category`
99
+ (what a documentation tree mounted from a repository directory
100
+ carries); "" for a page with no `type`, which has no catalog.
101
+
102
+ Ordering follows Hugo's: in a page collection `.PrevInSection` is the
103
+ *following* entry and `.NextInSection` the preceding one, so the
104
+ neighbours are taken at index+1 and index-1 respectively.
105
+ */ -}}
106
+ {{- $prev := .PrevInSection -}}
107
+ {{- $next := .NextInSection -}}
108
+ {{- $type := .Params.type | default "" -}}
109
+ {{- $key := $type -}}
110
+ {{- if eq $type "doc" -}}
111
+ {{- $key = .Params.subType | default (.Params.category | default "") -}}
112
+ {{- end -}}
113
+ {{- $siblings := slice -}}
114
+ {{- with .CurrentSection -}}
115
+ {{- if $key -}}
116
+ {{- $mixed := false -}}
117
+ {{- $matching := slice -}}
118
+ {{- range .RegularPages -}}
119
+ {{- $pageType := .Params.type | default "" -}}
120
+ {{- $pageKey := $pageType -}}
121
+ {{- if eq $pageType "doc" -}}
122
+ {{- $pageKey = .Params.subType | default (.Params.category | default "") -}}
123
+ {{- end -}}
124
+ {{- if eq $pageKey $key -}}
125
+ {{- $matching = $matching | append . -}}
126
+ {{- else if ne $pageKey "" -}}
127
+ {{- $mixed = true -}}
128
+ {{- end -}}
129
+ {{- end -}}
130
+ {{- if $mixed -}}{{- $siblings = $matching -}}{{- end -}}
131
+ {{- end -}}
132
+ {{- end -}}
133
+ {{- if $siblings -}}
134
+ {{- $index := -1 -}}
135
+ {{- range $i, $p := $siblings -}}
136
+ {{- if eq $p.Path $.Path -}}{{- $index = $i -}}{{- end -}}
137
+ {{- end -}}
138
+ {{- if ge $index 0 -}}
139
+ {{- $prev = "" -}}
140
+ {{- $next = "" -}}
141
+ {{- if lt (add $index 1) (len $siblings) -}}{{- $prev = index $siblings (add $index 1) -}}{{- end -}}
142
+ {{- if gt $index 0 -}}{{- $next = index $siblings (sub $index 1) -}}{{- end -}}
143
+ {{- end -}}
144
+ {{- end -}}
145
+ <nav class="single-nav">
146
+ {{ with $prev }}<a href="{{ .Permalink }}" class="nav-prev">&larr; {{ .Title }}</a>{{ end }}
147
+ {{ with $next }}<a href="{{ .Permalink }}" class="nav-next">{{ .Title }} &rarr;</a>{{ end }}
148
+ </nav>
149
+ </div>
150
+
151
+ {{ if and (not $hasSidebar) $hasToc }}
152
+ {{ partial "toc.html" . }}
153
+ {{ end }}
154
+ </article>
@@ -0,0 +1,9 @@
1
+ {{- /* A page bundle's generated itinerary. The SVG carries its own place links. */ -}}
2
+ {{- with .Params.map -}}
3
+ {{- with $.Resources.GetMatch . -}}
4
+ <section class="place-map" aria-labelledby="place-map-heading" data-pagefind-ignore>
5
+ <h2 id="place-map-heading" class="place-map-heading">From here</h2>
6
+ <div class="place-map-drawing">{{ .Content | safeHTML }}</div>
7
+ </section>
8
+ {{- end -}}
9
+ {{- end -}}
@@ -0,0 +1,121 @@
1
+ {{- /*
2
+ Related partial.
3
+
4
+ Renders a compact "Related" block at the bottom of a single page from the
5
+ page's `related` front matter, if it carries any.
6
+
7
+ Structure in front matter:
8
+ related:
9
+ backlinks: [{ title, url, type }, ...] # pages that link here
10
+ mentions: [{ title, url, type }, ...] # pages this page links to
11
+
12
+ Rendering strategy:
13
+ - Merge both directions into a single deduped pool (direction
14
+ doesn't matter to a reader — a connection is a connection).
15
+ - Group by the referenced page's `type`.
16
+ - Emit one labelled group per type, its entries laid out as a
17
+ CSS grid (`repeat(auto-fill, minmax(14em, 1fr))`) so a wide
18
+ screen shows two or three columns and a narrow one falls back
19
+ to a single column with no media query.
20
+ - The whole block is wrapped in a bordered, tinted card so it
21
+ visually sets off the end of the article.
22
+
23
+ Silent disappear: a page with no `related`, or whose two lists are both
24
+ empty, emits nothing at all rather than an empty heading. The rows are
25
+ computed first and the <section> is rendered only when there is something
26
+ to put in it.
27
+
28
+ `return` cannot be used to bail out early here. It sets the partial's
29
+ return value; it does not discard template text the partial has already
30
+ emitted, so a guard written that way still prints the empty shell. Hence
31
+ the emptiness test wraps the markup instead of preceding it.
32
+ */ -}}
33
+
34
+ {{- /* `default dict` keeps the lookups below safe on a page with no
35
+ `related` at all, so the whole computation stays flat. */ -}}
36
+ {{- $rel := .Params.related | default dict -}}
37
+ {{- $backlinks := or $rel.backlinks slice -}}
38
+ {{- $mentions := or $rel.mentions slice -}}
39
+
40
+ {{- /* Merge both directions, deduped by URL. `union` can't hash
41
+ slices of maps, so we walk the two lists in order and dedupe
42
+ by URL manually. Mentions come first so explicit author
43
+ choices win when a page appears in both directions. */ -}}
44
+ {{- $seen := slice -}}
45
+ {{- $merged := slice -}}
46
+ {{- range $mentions -}}
47
+ {{- if not (in $seen .url) -}}
48
+ {{- $seen = $seen | append .url -}}
49
+ {{- $merged = $merged | append . -}}
50
+ {{- end -}}
51
+ {{- end -}}
52
+ {{- range $backlinks -}}
53
+ {{- if not (in $seen .url) -}}
54
+ {{- $seen = $seen | append .url -}}
55
+ {{- $merged = $merged | append . -}}
56
+ {{- end -}}
57
+ {{- end -}}
58
+
59
+ {{- /* Plural, human-readable label for each type. Falls back to
60
+ `humanize` for any type not listed here. */ -}}
61
+ {{- $labels := dict
62
+ "affiliation" "Affiliations"
63
+ "being" "Beings"
64
+ "blog-post" "Posts"
65
+ "character" "People"
66
+ "continent" "Continents"
67
+ "creature" "Creatures"
68
+ "faith" "Faiths"
69
+ "language" "Languages"
70
+ "lore" "Lore"
71
+ "organization" "Organizations"
72
+ "page" "Pages"
73
+ "pantheon" "Pantheons"
74
+ "people" "Peoples"
75
+ "polity" "Polities"
76
+ "project-page" "Projects"
77
+ "region" "Regions"
78
+ "religion" "Religions"
79
+ "settlement" "Settlements"
80
+ "site" "Places"
81
+ "world" "Worlds"
82
+ -}}
83
+
84
+ {{- /* Group refs by type, preserving alphabetical order within a
85
+ group (the input lists arrive already sorted). */ -}}
86
+ {{- $groups := dict -}}
87
+ {{- range $merged -}}
88
+ {{- $t := .type | default "page" -}}
89
+ {{- $existing := index $groups $t | default slice -}}
90
+ {{- $groups = merge $groups (dict $t ($existing | append .)) -}}
91
+ {{- end -}}
92
+
93
+ {{- /* Sort the group keys alphabetically by their rendered label
94
+ so the list reads A → Z regardless of insertion order. */ -}}
95
+ {{- $rows := slice -}}
96
+ {{- range $type, $refs := $groups -}}
97
+ {{- $label := index $labels $type | default (humanize $type) -}}
98
+ {{- $rows = $rows | append (dict "label" $label "type" $type "refs" $refs) -}}
99
+ {{- end -}}
100
+ {{- if $rows -}}
101
+ {{- $rows = sort $rows "label" "asc" -}}
102
+ {{- end -}}
103
+
104
+ {{- /* Deliberately not indented under the `if`: keeping the markup at
105
+ column 0 leaves the emitted HTML byte-identical to what pages with
106
+ `related` rendered before the guard was added. */ -}}
107
+ {{- if $rows -}}
108
+ <section class="related" aria-labelledby="related-heading" data-pagefind-ignore>
109
+ <h2 id="related-heading" class="related-heading">Related</h2>
110
+ {{- range $rows }}
111
+ <div class="related-group">
112
+ <h3 class="related-group-label">{{ .label }}</h3>
113
+ <ul class="related-group-grid">
114
+ {{- range .refs }}
115
+ <li><a href="{{ .url }}">{{ .title }}</a></li>
116
+ {{- end }}
117
+ </ul>
118
+ </div>
119
+ {{- end }}
120
+ </section>
121
+ {{ end -}}
@@ -0,0 +1,84 @@
1
+ {{- /*
2
+ The header search box, rendered only when a consumer sets `params.search`
3
+ (silent-disappear otherwise — see the README's "Search" section).
4
+
5
+ `@heroiclands/package-build`'s `package-build site-root` step writes a
6
+ Pagefind index beside the rendered pages, at `pagefind/` under the site's
7
+ own base URL, when a consumer leaves `site.search` at its default of `true`
8
+ (see that package's `docs/configuration.md`, `site.search`). Pagefind's own
9
+ UI — `pagefind-ui.js` and `pagefind-ui.css` — ships inside that generated
10
+ directory, so nothing here is bundled or pinned to a Pagefind version; the
11
+ index and the UI that reads it travel together.
12
+
13
+ Both files are referenced the same way every other theme-bundled asset is
14
+ — `relURL`, exactly as `"css/style.css" | relURL` resolves in
15
+ `baseof.html` — so a package served under a prefix (`/thalorna/`) finds
16
+ its own index rather than another package's.
17
+
18
+ A `/search/` results page would need a consumer content file at that
19
+ address, and a site is its homepage and its pages with nothing generated
20
+ between them (package-build's `docs/configuration.md`, "A site is its
21
+ homepage and its pages") — so a dedicated results page is not this
22
+ theme's to invent. Pagefind's own panel UI needs no page of its own, so
23
+ the header box opens that instead.
24
+
25
+ Context: the page (only .Site is read, so this partial can also be called
26
+ with just `.Site` in a dict).
27
+ */ -}}
28
+ {{- if .Site.Params.search }}
29
+ <div class="site-search" data-pagefind-ignore>
30
+ <button
31
+ type="button"
32
+ class="search-toggle"
33
+ aria-label="Search"
34
+ aria-expanded="false"
35
+ aria-controls="site-search-panel"
36
+ ><i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i></button>
37
+ <div id="site-search-panel" class="search-panel" hidden>
38
+ <div class="search-panel-inner" id="search"></div>
39
+ </div>
40
+ </div>
41
+ <link rel="stylesheet" href="{{ "pagefind/pagefind-ui.css" | relURL }}">
42
+ <script src="{{ "pagefind/pagefind-ui.js" | relURL }}" defer></script>
43
+ <script>
44
+ (function () {
45
+ var toggle = document.querySelector(".search-toggle");
46
+ var panel = document.getElementById("site-search-panel");
47
+ if (!toggle || !panel) return;
48
+ var ui = null;
49
+
50
+ function open() {
51
+ panel.hidden = false;
52
+ toggle.setAttribute("aria-expanded", "true");
53
+ if (!ui && typeof PagefindUI !== "undefined") {
54
+ ui = new PagefindUI({
55
+ element: "#search",
56
+ showSubResults: true,
57
+ showImages: false
58
+ });
59
+ }
60
+ var input = panel.querySelector("input");
61
+ if (input) input.focus();
62
+ }
63
+
64
+ function close() {
65
+ panel.hidden = true;
66
+ toggle.setAttribute("aria-expanded", "false");
67
+ }
68
+
69
+ toggle.addEventListener("click", function () {
70
+ if (panel.hidden) { open(); } else { close(); }
71
+ });
72
+
73
+ document.addEventListener("keydown", function (e) {
74
+ if (e.key === "Escape" && !panel.hidden) close();
75
+ });
76
+
77
+ document.addEventListener("click", function (e) {
78
+ if (panel.hidden) return;
79
+ if (panel.contains(e.target) || toggle.contains(e.target)) return;
80
+ close();
81
+ });
82
+ })();
83
+ </script>
84
+ {{- end }}
@@ -0,0 +1,32 @@
1
+ {{- /*
2
+ Resolve a consumer-supplied link against the site it is rendered on.
3
+
4
+ The theme carries layout, not addresses, so every URL it renders comes from
5
+ a consuming site's configuration or front matter — and a package published
6
+ under a path prefix (`/sohl/`, `/thalorna/`) needs its own links to carry
7
+ that prefix without writing it down anywhere.
8
+
9
+ Resolution:
10
+ 1. An already-absolute URL (http://, https://, //) is returned as-is, so a
11
+ consumer can point at another host.
12
+ 2. A page fragment or query alone (#anchor, ?q=) is returned as-is.
13
+ 3. Anything else is treated as a path within this site and resolved with
14
+ `relURL`. A leading slash is trimmed first: `relURL` reads a leading
15
+ slash as "from the origin" and leaves the base path off, so trimming it
16
+ means a consumer writing the obvious "/section/" still gets a link
17
+ inside its own site when that site is served under a prefix.
18
+
19
+ Usage:
20
+ {{ partial "site-url.html" (dict "site" .Site "url" "kb/rules/") }}
21
+
22
+ Params:
23
+ site — the site context (.Site). Accepted for symmetry with cdn-url.html
24
+ and so this partial can never be tempted to read a page.
25
+ url — the address to resolve. An empty value resolves to the site root.
26
+ */ -}}
27
+ {{- $url := .url | default "" -}}
28
+ {{- $resolved := $url -}}
29
+ {{- if not (or (hasPrefix $url "http://") (hasPrefix $url "https://") (hasPrefix $url "//") (hasPrefix $url "#") (hasPrefix $url "?")) -}}
30
+ {{- $resolved = strings.TrimPrefix "/" $url | relURL -}}
31
+ {{- end -}}
32
+ {{- return $resolved -}}
@@ -0,0 +1,30 @@
1
+ {{- /*
2
+ A page's own subject-tag chips, rendered from .Params.tags.
3
+
4
+ `draft` is suppressed here: it is a presentation fact about the writing,
5
+ not a fact about the subject (package-build/engine/note-vocabulary.mjs,
6
+ `DRAFT_TAG`) — a draft compiles, validates, publishes and resolves like any
7
+ other note. It does not belong beside subject tags in a reader-facing tag
8
+ list; `draft-notice.html` is where the state is stated,
9
+ and `draft-state.html` is the one place either partial spells the tag.
10
+
11
+ The taxonomy itself is untouched: a draft page still carries the tag, still
12
+ lists on its term page, and the term still appears in a site-wide taxonomy
13
+ listing. This partial decides only what a page's own chip row shows.
14
+
15
+ Silent-disappear: a page whose only tag is `draft` emits nothing here, the
16
+ same as a page with no tags at all.
17
+
18
+ Context: the page.
19
+ */ -}}
20
+ {{- $tags := slice -}}
21
+ {{- range .Params.tags -}}
22
+ {{- if ne (lower .) "draft" -}}
23
+ {{- $tags = $tags | append . -}}
24
+ {{- end -}}
25
+ {{- end -}}
26
+ {{- with $tags }}
27
+ <div class="single-tags">
28
+ {{ range . }}<a href="{{ printf "tags/%s/" (. | urlize) | relURL }}" class="tag">{{ . }}</a>{{ end }}
29
+ </div>
30
+ {{ end -}}
@@ -0,0 +1,28 @@
1
+ {{- /*
2
+ Table of Contents partial.
3
+
4
+ Renders a sticky right-rail TOC on pages that have more than 3 H2
5
+ headings. We detect the threshold by grepping the raw content source
6
+ (`.RawContent`) for lines starting with "## " — this is faster and
7
+ more reliable than parsing the rendered HTML, and catches authored
8
+ headings regardless of markdown extensions that Hugo might apply.
9
+
10
+ The caller is responsible for:
11
+ - wrapping the page in a CSS grid when this partial renders
12
+ (see `.single.single-with-toc` in style.css)
13
+ - gating invocation with `partialCached` if desired
14
+
15
+ This partial prints nothing when the H2 count is 3 or less, so it's
16
+ safe to include unconditionally.
17
+ */ -}}
18
+
19
+ {{- $raw := .RawContent | default "" -}}
20
+ {{- $h2Count := len (findRE `(?m)^##\s+\S` $raw) -}}
21
+ {{- if gt $h2Count 3 -}}
22
+ <aside class="toc" aria-label="Table of contents" data-pagefind-ignore>
23
+ <div class="toc-inner">
24
+ <p class="toc-label">On this page</p>
25
+ {{ .TableOfContents }}
26
+ </div>
27
+ </aside>
28
+ {{- end -}}
@@ -0,0 +1,3 @@
1
+ {{- warnidf (printf "hero-banner-missing-%s" .name)
2
+ "hero-banner: no banner named %q in the shared artwork set; falling back to %s/%s. Draw it and add %q to data/banners.yaml, or name a hero image on the page — `data.banner` on a note, `banner:` on a hand-authored page. A blank `data.banner`, or `banner: none`, declines a hero image. First seen on %s."
3
+ .name .dir .fallback .name .context -}}
@@ -0,0 +1,64 @@
1
+ {{- /*
2
+ Cartouche shortcode — renders a named cartouche SVG as an ornamental
3
+ accent on culture-flavored pages. The canonical use is Kheperi
4
+ cartouches on Ta'Kheperu and Kemetian-pantheon pages, but the same
5
+ pipeline works for any "named seal" SVG.
6
+
7
+ Usage (shown with plain angle brackets so the Go template parser
8
+ doesn't try to close this comment). Hugo forbids MIXING positional
9
+ and named parameters in one call, so pick a form per call:
10
+
11
+ { {< cartouche "takheperu" >} } positional
12
+ { {< cartouche slug="takheperu" >} } named, centered
13
+ { {< cartouche slug="takheperu" align="right" >} } floated right
14
+ { {< cartouche slug="ra" size="inline" >} } inline
15
+ { {< cartouche slug="anubis" caption="Anpu" >} } figure + caption
16
+
17
+ Parameters (all optional except slug):
18
+ slug - The cartouche SVG basename. May also be passed positionally
19
+ (as the first positional argument) when no other params
20
+ are needed — Hugo disallows mixing the two styles.
21
+ size - "inline" to flow with text. Otherwise block-level.
22
+ align - "left" or "right" to float so prose wraps. Default centered.
23
+ height - CSS length override for the cartouche's height, e.g.
24
+ "10rem" or "160px". Default comes from .cartouche CSS
25
+ (13rem block, 9rem floated). Width scales from aspect ratio.
26
+ alt - Override screen-reader text. Defaults to humanized slug.
27
+ caption - Optional figcaption (block mode only).
28
+
29
+ SVG is expected at /static/img/cartouches/<slug>.svg. Design note:
30
+ cartouche SVGs should draw with stroke="currentColor" and fill="none"
31
+ (or a hard-coded brand color) so they tint cleanly against any
32
+ background.
33
+ */ -}}
34
+ {{- /* Accept slug as either the first positional arg or the named
35
+ "slug" parameter, so a caller can pick the style that fits the
36
+ rest of their arguments. */ -}}
37
+ {{- $slug := .Get "slug" -}}
38
+ {{- if not $slug }}{{ $slug = .Get 0 }}{{ end -}}
39
+ {{- if not $slug -}}
40
+ {{- errorf "cartouche shortcode at %s needs a slug (positional arg 0 or slug=\"…\")" .Position -}}
41
+ {{- end -}}
42
+ {{- $altDefault := humanize (replace $slug "-" " ") | title -}}
43
+ {{- $alt := .Get "alt" | default $altDefault -}}
44
+ {{- $size := .Get "size" | default "" -}}
45
+ {{- $align := .Get "align" | default "" -}}
46
+ {{- $cap := .Get "caption" | default "" -}}
47
+ {{- $height := .Get "height" | default "" -}}
48
+ {{- $src := printf "img/cartouches/%s.svg" $slug | relURL -}}
49
+ {{- $classes := slice "cartouche" -}}
50
+ {{- with $size }}{{ $classes = $classes | append (printf "cartouche-%s" .) }}{{ end -}}
51
+ {{- with $align }}{{ $classes = $classes | append (printf "cartouche-%s" .) }}{{ end -}}
52
+ {{- $class := delimit $classes " " -}}
53
+ {{- /* When `height` is given, apply it as an inline style directly on
54
+ the <img>. Inline styles on the target element are unambiguous —
55
+ they trump any .cartouche img CSS rule on specificity and don't
56
+ depend on custom-property inheritance. */ -}}
57
+ {{- if eq $size "inline" -}}
58
+ <span class="{{ $class }}"><img src="{{ $src }}" alt="{{ $alt }}" loading="lazy"{{ with $height }} style="height: {{ . }}; width: auto; max-width: none;"{{ end }} /></span>
59
+ {{- else -}}
60
+ <figure class="{{ $class }}">
61
+ <img src="{{ $src }}" alt="{{ $alt }}" loading="lazy"{{ with $height }} style="height: {{ . }}; width: auto; max-width: none;"{{ end }} />
62
+ {{- with $cap }}<figcaption>{{ . }}</figcaption>{{ end }}
63
+ </figure>
64
+ {{- end -}}
@@ -0,0 +1,12 @@
1
+ <span class="exturl">
2
+ <code class="exturl-text">{{ .Get 0 }}</code>
3
+ <button class="exturl-copy" onclick="navigator.clipboard.writeText('{{ .Get 0 }}').then(()=>{const b=this;b.classList.add('copied');setTimeout(()=>b.classList.remove('copied'),1500)})" title="Copy URL to clipboard" aria-label="Copy URL to clipboard">
4
+ <svg class="exturl-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
6
+ <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
7
+ </svg>
8
+ <svg class="exturl-check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
9
+ <polyline points="20 6 9 17 4 12"/>
10
+ </svg>
11
+ </button>
12
+ </span>
@@ -0,0 +1,43 @@
1
+ {{- /*
2
+ Glyph shortcode — renders a hieroglyphic deity-name cluster (or any
3
+ "icon-scale" SVG) from the project CDN. Parallel to cartouche.html
4
+ but for smaller, non-royal glyph groups: Kemetían deity names,
5
+ category icons, ornamental marks, etc.
6
+
7
+ Usage (Hugo disallows mixing positional and named params in one
8
+ call; pick a form per call):
9
+
10
+ { {< glyph "anubis" >} } inline, plain
11
+ { {< glyph slug="anubis" >} } same, named form
12
+ { {< glyph slug="anubis" frame="tablet" >} } inside stone tablet
13
+ { {< glyph slug="ra" size="inline" >} } scales with text
14
+ { {< glyph slug="ra" size="block" >} } centered block
15
+ { {< glyph slug="ra" alt="Sun god Ra" >} } override alt text
16
+
17
+ Parameters (all optional except slug):
18
+ slug - Filename (without .svg). Passed positionally or as slug=.
19
+ frame - "tablet" wraps in a limestone rectangular frame (good for
20
+ catalog layouts like the pantheon table where visual
21
+ parallelism across rows matters more than intrinsic width).
22
+ size - "inline" (small, flows with text), "block" (centered
23
+ standalone). Default sits at 1.8em height.
24
+ alt - Screen-reader text. Defaults to humanized slug.
25
+
26
+ SVG source: icons/<slug>.svg, resolved against the consuming site's
27
+ `params.cdnBaseURL` — the theme names no host of its own.
28
+ */ -}}
29
+ {{- $slug := .Get "slug" -}}
30
+ {{- if not $slug }}{{ $slug = .Get 0 }}{{ end -}}
31
+ {{- if not $slug -}}
32
+ {{- errorf "glyph shortcode at %s needs a slug (positional arg 0 or slug=\"…\")" .Position -}}
33
+ {{- end -}}
34
+ {{- $altDefault := humanize (replace $slug "-" " ") | title -}}
35
+ {{- $alt := .Get "alt" | default $altDefault -}}
36
+ {{- $frame := .Get "frame" | default "" -}}
37
+ {{- $size := .Get "size" | default "" -}}
38
+ {{- $src := partial "cdn-url.html" (dict "site" .Site "path" (printf "icons/%s.svg" $slug)) -}}
39
+ {{- $classes := slice "glyph" -}}
40
+ {{- with $frame }}{{ $classes = $classes | append (printf "glyph-%s" .) }}{{ end -}}
41
+ {{- with $size }}{{ $classes = $classes | append (printf "glyph-%s" .) }}{{ end -}}
42
+ {{- $class := delimit $classes " " -}}
43
+ <span class="{{ $class }}"><img src="{{ $src }}" alt="{{ $alt }}" loading="lazy" /></span>
@@ -0,0 +1,63 @@
1
+ {{- /*
2
+ pagelist — the pages carrying a tag, as a plain list.
3
+
4
+ Curated landings are the right front door, but hand-enumerated link lists
5
+ drift: a page gets added and nobody updates the index. This expresses a group
6
+ by INTENT instead. Tag a page and it files itself; untag it and it leaves.
7
+
8
+ Usage:
9
+
10
+ { {< pagelist tag="key-concept" >} } every tagged page
11
+ { {< pagelist tag="key-concept" section="rules" >} } restricted to a section
12
+ { {< pagelist tag="key-concept" sort="weight" >} } title (default) | weight | date
13
+ { {< pagelist tag="key-concept" desc="true" >} } include page descriptions
14
+
15
+ Parameters:
16
+ tag - (required) taxonomy term. Matched case-insensitively.
17
+ section - (optional) restrict to one top-level section, e.g. "rules".
18
+ sort - (optional) "title" (default), "weight", or "date" (newest first).
19
+ desc - (optional) "true" to show each page's description beneath it.
20
+
21
+ An unknown or empty tag warns at build time rather than rendering nothing
22
+ silently — a typo in a tag is exactly the drift this is meant to prevent.
23
+ */ -}}
24
+ {{- $tag := .Get "tag" -}}
25
+ {{- if not $tag }}{{ $tag = .Get 0 }}{{ end -}}
26
+ {{- if not $tag -}}
27
+ {{- errorf "pagelist shortcode at %s needs a tag (tag=\"…\" or positional arg 0)" .Position -}}
28
+ {{- end -}}
29
+
30
+ {{- $term := $tag | urlize -}}
31
+ {{- $pages := slice -}}
32
+ {{- with index .Site.Taxonomies.tags $term -}}
33
+ {{- $pages = .Pages -}}
34
+ {{- end -}}
35
+
36
+ {{- with $.Get "section" -}}
37
+ {{- $sec := . -}}
38
+ {{- $pages = where $pages "Section" $sec -}}
39
+ {{- end -}}
40
+
41
+ {{- $sort := $.Get "sort" | default "title" -}}
42
+ {{- if eq $sort "weight" -}}
43
+ {{- $pages = sort $pages "Weight" -}}
44
+ {{- else if eq $sort "date" -}}
45
+ {{- $pages = sort $pages "Date" "desc" -}}
46
+ {{- else -}}
47
+ {{- $pages = sort $pages "Title" -}}
48
+ {{- end -}}
49
+
50
+ {{- if not $pages -}}
51
+ {{- warnf "pagelist at %s: no pages carry the tag %q — check the spelling, or tag some pages." $.Position $tag -}}
52
+ {{- else -}}
53
+ <ul class="page-list pagelist">
54
+ {{- range $pages }}
55
+ <li class="page-list-item">
56
+ <a class="page-list-title" href="{{ .RelPermalink }}">{{ .Title }}</a>
57
+ {{- if and (eq ($.Get "desc") "true") .Description }}
58
+ <span class="page-list-desc">{{ .Description }}</span>
59
+ {{- end }}
60
+ </li>
61
+ {{- end }}
62
+ </ul>
63
+ {{- end -}}