@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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -0
- package/docs/authoring/assets.md +15 -0
- package/docs/commands.md +18 -19
- package/docs/configuration.md +2 -6
- package/docs/guides/module-project.md +2 -2
- package/docs/guides/site.md +41 -0
- package/docs/project-setup.md +4 -4
- package/docs/reference/theme-pages.md +496 -0
- package/engine/art-slots.mjs +2 -1
- package/engine/foreign-catalog.mjs +2 -1
- package/engine/helpers.mjs +5 -3
- package/engine/site-config.mjs +19 -24
- package/hugo-theme/data/banners.yaml +69 -0
- package/hugo-theme/layouts/404.html +85 -0
- package/hugo-theme/layouts/_default/baseof.html +64 -0
- package/hugo-theme/layouts/_default/list.html +139 -0
- package/hugo-theme/layouts/_default/single.html +4 -0
- package/hugo-theme/layouts/blog/list.html +54 -0
- package/hugo-theme/layouts/blog-post/single.html +36 -0
- package/hugo-theme/layouts/index.html +75 -0
- package/hugo-theme/layouts/partials/banner-url.html +117 -0
- package/hugo-theme/layouts/partials/cdn-url.html +36 -0
- package/hugo-theme/layouts/partials/draft-notice.html +41 -0
- package/hugo-theme/layouts/partials/draft-state.html +26 -0
- package/hugo-theme/layouts/partials/footer.html +29 -0
- package/hugo-theme/layouts/partials/header.html +35 -0
- package/hugo-theme/layouts/partials/hero-banner.html +86 -0
- package/hugo-theme/layouts/partials/holdings/entry.html +21 -0
- package/hugo-theme/layouts/partials/holdings/kind-groups.html +71 -0
- package/hugo-theme/layouts/partials/holdings.html +61 -0
- package/hugo-theme/layouts/partials/infobox/entry.html +15 -0
- package/hugo-theme/layouts/partials/infobox/items.html +26 -0
- package/hugo-theme/layouts/partials/infobox/section.html +56 -0
- package/hugo-theme/layouts/partials/infobox/value.html +43 -0
- package/hugo-theme/layouts/partials/infobox.html +57 -0
- package/hugo-theme/layouts/partials/page-body.html +154 -0
- package/hugo-theme/layouts/partials/place-map.html +9 -0
- package/hugo-theme/layouts/partials/related.html +121 -0
- package/hugo-theme/layouts/partials/search.html +84 -0
- package/hugo-theme/layouts/partials/site-url.html +32 -0
- package/hugo-theme/layouts/partials/tags.html +30 -0
- package/hugo-theme/layouts/partials/toc.html +28 -0
- package/hugo-theme/layouts/partials/warn-missing-banner.html +3 -0
- package/hugo-theme/layouts/shortcodes/cartouche.html +64 -0
- package/hugo-theme/layouts/shortcodes/exturl.html +12 -0
- package/hugo-theme/layouts/shortcodes/glyph.html +43 -0
- package/hugo-theme/layouts/shortcodes/pagelist.html +63 -0
- package/hugo-theme/static/css/sohl-icons.css +57 -0
- package/hugo-theme/static/css/style.css +2953 -0
- package/hugo-theme/static/fonts/game-icons-kb.woff2 +0 -0
- package/hugo-theme/static/img/banners/takheperu-hieroglyphs.svg +1 -0
- package/hugo-theme/static/img/cartouches/takheperu.svg +1 -0
- package/hugo-theme/theme.toml +11 -0
- package/package.json +2 -1
- package/types/engine/art-slots.d.mts +2 -1
- package/types/engine/helpers.d.mts +5 -3
- package/types/engine/site-config.d.mts +9 -9
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Which banner images exist in the shared artwork set.
|
|
2
|
+
#
|
|
3
|
+
# `partials/hero-banner.html` resolves a page's hero image to a path under
|
|
4
|
+
# `images/banners/`, and that path is emitted into a `background-image` with no
|
|
5
|
+
# way to test it: the artwork is served from the consuming site's
|
|
6
|
+
# `params.cdnBaseURL`, and Hugo cannot ask a remote host whether a URL exists.
|
|
7
|
+
# So a banner that was never drawn used to 404 in silence — the band rendered
|
|
8
|
+
# with its title, palette and gradient intact and a dead image URL behind it,
|
|
9
|
+
# and nothing failed: not the build, not Hugo, not the deploy guard
|
|
10
|
+
# (issue #36).
|
|
11
|
+
#
|
|
12
|
+
# This file is the declaration that replaces that missing test. A name listed
|
|
13
|
+
# here is asserted to exist; a resolved banner whose name is absent falls back
|
|
14
|
+
# to `default.webp` and Hugo warns once for it during the consumer's build.
|
|
15
|
+
#
|
|
16
|
+
# It speaks for the names this theme resolves itself — a hand-authored
|
|
17
|
+
# `banner:` and the subtype defaults below. A note's own `data.banner` arrives
|
|
18
|
+
# already resolved from an asset Address, against a real asset record, so it is
|
|
19
|
+
# used as given and never looked for here.
|
|
20
|
+
#
|
|
21
|
+
# Keeping it in step is not left to hope: `npm run lint:banners`
|
|
22
|
+
# (`utils/check-banners.mjs`, part of `npm run lint`) fetches every name below
|
|
23
|
+
# from a real asset host and fails on any non-200. Add a name here only after
|
|
24
|
+
# the file is published, or the check will say so.
|
|
25
|
+
#
|
|
26
|
+
# A consumer publishing its own artwork set overrides the whole list by
|
|
27
|
+
# shipping its own `data/banners.yaml` — Hugo reads the site's data directory
|
|
28
|
+
# in preference to the theme's.
|
|
29
|
+
|
|
30
|
+
# The asset root every name below is resolved under, and the name used when a
|
|
31
|
+
# resolved banner is not in `available`. Both are relative to the consuming
|
|
32
|
+
# site's `params.cdnBaseURL`, like every other image path in this theme.
|
|
33
|
+
dir: images/banners
|
|
34
|
+
fallback: default.webp
|
|
35
|
+
|
|
36
|
+
# Verified against https://cdn.heroiclands.org on 2026-08-29.
|
|
37
|
+
available:
|
|
38
|
+
- affliction
|
|
39
|
+
- armor-clothing
|
|
40
|
+
- character
|
|
41
|
+
- company
|
|
42
|
+
- containers
|
|
43
|
+
- continent
|
|
44
|
+
- creature
|
|
45
|
+
- default
|
|
46
|
+
- dev-docs
|
|
47
|
+
- faith
|
|
48
|
+
- fvtt-add-on-modules
|
|
49
|
+
- fvtt-cc-anvil
|
|
50
|
+
- misc-gear
|
|
51
|
+
- module-dnload
|
|
52
|
+
- mystical-abilities
|
|
53
|
+
- orators-chance
|
|
54
|
+
- organization
|
|
55
|
+
- pantheon
|
|
56
|
+
- people
|
|
57
|
+
- projectiles
|
|
58
|
+
- region
|
|
59
|
+
- rules
|
|
60
|
+
- settlement
|
|
61
|
+
- site
|
|
62
|
+
- skills-attributes
|
|
63
|
+
- sohl-icon-3d-1920x1080
|
|
64
|
+
- ta-kheperu-banner
|
|
65
|
+
- tamzir-banner
|
|
66
|
+
- tapestry-of-dreams
|
|
67
|
+
- trauma
|
|
68
|
+
- user-guide
|
|
69
|
+
- weapons
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
"Page not found" page.
|
|
3
|
+
|
|
4
|
+
Hugo renders this template to `public/404.html`, and a static host such as
|
|
5
|
+
Cloudflare Pages serves that file — with a genuine HTTP 404 — for any path
|
|
6
|
+
the deployment does not publish. Without it, Pages falls back to the site
|
|
7
|
+
root, so an unknown path answers 200 with the landing page: a soft-404 that
|
|
8
|
+
makes every "does this URL resolve?" check report success, and lets search
|
|
9
|
+
engines index retired content as live (issue #1416).
|
|
10
|
+
|
|
11
|
+
Every consumer needs exactly this, so it lives here rather than being copied
|
|
12
|
+
into each one (issue #1454). The theme carries layout, not addresses, so the
|
|
13
|
+
wording and the routes back are supplied by the consuming site:
|
|
14
|
+
|
|
15
|
+
[params.notfound]
|
|
16
|
+
heroimage = "images/banner.webp" # CDN-relative or absolute
|
|
17
|
+
tagline = "This site has no page at"
|
|
18
|
+
sitenoun = "site" # used in the body prose
|
|
19
|
+
[[params.notfound.links]]
|
|
20
|
+
title = "Home"
|
|
21
|
+
url = "/" # relURL'd unless already absolute
|
|
22
|
+
text = "Everything, from the top."
|
|
23
|
+
|
|
24
|
+
Each is guarded: a consumer supplying none still builds, and simply gets the
|
|
25
|
+
generic wording with no list of routes back.
|
|
26
|
+
*/ -}}
|
|
27
|
+
{{ define "main" }}
|
|
28
|
+
{{- $nf := .Site.Params.notfound | default dict -}}
|
|
29
|
+
{{- $img := $nf.heroimage | default "images/banners/default.webp" -}}
|
|
30
|
+
{{- $heroURL := partial "cdn-url.html" (dict "site" .Site "path" $img) -}}
|
|
31
|
+
{{- $tagline := $nf.tagline | default "This site has no page at" -}}
|
|
32
|
+
{{- $noun := $nf.sitenoun | default "site" -}}
|
|
33
|
+
<section class="hero hero-with-image" style="background-image: url('{{ $heroURL }}');">
|
|
34
|
+
<div class="hero-inner" data-pagefind-ignore>
|
|
35
|
+
<h1>Page not found</h1>
|
|
36
|
+
<p class="hero-tagline">{{ $tagline }} <code id="missing-path">the address you followed</code>.</p>
|
|
37
|
+
</div>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
<article class="single">
|
|
41
|
+
<div class="single-content">
|
|
42
|
+
<div class="single-body">
|
|
43
|
+
<p>
|
|
44
|
+
The page may have been renamed, moved to another part of the
|
|
45
|
+
{{ $noun }}, or retired along with the concept it described. Links
|
|
46
|
+
from older discussions, issues, and search results can outlive the
|
|
47
|
+
pages they point at.
|
|
48
|
+
</p>
|
|
49
|
+
{{- with $nf.links }}
|
|
50
|
+
<p>Start again from one of these:</p>
|
|
51
|
+
<ul class="page-list">
|
|
52
|
+
{{- range . }}
|
|
53
|
+
{{- $url := .url -}}
|
|
54
|
+
{{- if not (or (hasPrefix $url "http://") (hasPrefix $url "https://") (hasPrefix $url "//")) -}}
|
|
55
|
+
{{- /* relURL only mounts a path under the site's base when it is
|
|
56
|
+
given relative: a leading slash means "from the origin", and
|
|
57
|
+
the base path is left off. Trim it, so a consumer writing the
|
|
58
|
+
obvious "/section/" gets a link inside its own site even when
|
|
59
|
+
that site is served under a prefix. */ -}}
|
|
60
|
+
{{- $url = strings.TrimPrefix "/" $url | relURL -}}
|
|
61
|
+
{{- end }}
|
|
62
|
+
<li class="page-list-item">
|
|
63
|
+
<a class="page-list-title" href="{{ $url }}">{{ .title }}</a>
|
|
64
|
+
{{- with .text }}
|
|
65
|
+
<span class="page-list-desc">{{ . }}</span>
|
|
66
|
+
{{- end }}
|
|
67
|
+
</li>
|
|
68
|
+
{{- end }}
|
|
69
|
+
</ul>
|
|
70
|
+
{{- end }}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</article>
|
|
74
|
+
|
|
75
|
+
{{/* Name the address that failed, so a reader can see what they followed and
|
|
76
|
+
quote it in a bug report. Static hosting cannot template it — Pages serves
|
|
77
|
+
this one file for every unmatched path — so it is filled in client-side,
|
|
78
|
+
and the markup above reads sensibly without scripting. */}}
|
|
79
|
+
<script>
|
|
80
|
+
(function () {
|
|
81
|
+
var el = document.getElementById("missing-path");
|
|
82
|
+
if (el) el.textContent = window.location.pathname;
|
|
83
|
+
})();
|
|
84
|
+
</script>
|
|
85
|
+
{{ end }}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{{ if not .IsHome }}{{ .Title }} — {{ end }}{{ .Site.Title }}</title>
|
|
7
|
+
<meta name="description" content="{{ with .Params.description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&display=swap" rel="stylesheet">
|
|
11
|
+
<link rel="stylesheet" href="{{ "css/style.css" | relURL }}">
|
|
12
|
+
{{/* Icon glyphs. SoHL documentation writes icons as <i class="fa-solid …">
|
|
13
|
+
or <i class="ginf-…">, the same markup the Foundry client renders, so a
|
|
14
|
+
page reads identically in a journal and here. Font Awesome's FREE set is
|
|
15
|
+
enough — the system deliberately uses no Pro-only icons — and the
|
|
16
|
+
game-icons webfont is subset to just the glyphs the docs reference
|
|
17
|
+
(~3 KB), generated by the SoHL repo. */}}
|
|
18
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
|
|
19
|
+
<link rel="stylesheet" href="{{ "css/sohl-icons.css" | relURL }}">
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
{{ partial "header.html" . }}
|
|
23
|
+
<main>
|
|
24
|
+
{{ block "main" . }}{{ end }}
|
|
25
|
+
</main>
|
|
26
|
+
{{ partial "footer.html" . }}
|
|
27
|
+
<script>
|
|
28
|
+
// Auto-enhance <span data-copy="URL"> elements with a clipboard copy button
|
|
29
|
+
document.querySelectorAll('[data-copy]').forEach(function(el) {
|
|
30
|
+
var url = el.getAttribute('data-copy');
|
|
31
|
+
el.classList.add('exturl');
|
|
32
|
+
|
|
33
|
+
// Wrap existing text in a code element if not already
|
|
34
|
+
if (!el.querySelector('.exturl-text')) {
|
|
35
|
+
var code = document.createElement('code');
|
|
36
|
+
code.className = 'exturl-text';
|
|
37
|
+
code.textContent = el.textContent;
|
|
38
|
+
el.textContent = '';
|
|
39
|
+
el.appendChild(code);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var btn = document.createElement('button');
|
|
43
|
+
btn.className = 'exturl-copy';
|
|
44
|
+
btn.title = 'Copy URL to clipboard';
|
|
45
|
+
btn.setAttribute('aria-label', 'Copy URL to clipboard');
|
|
46
|
+
btn.innerHTML =
|
|
47
|
+
'<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">' +
|
|
48
|
+
'<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>' +
|
|
49
|
+
'<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>' +
|
|
50
|
+
'</svg>' +
|
|
51
|
+
'<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">' +
|
|
52
|
+
'<polyline points="20 6 9 17 4 12"/>' +
|
|
53
|
+
'</svg>';
|
|
54
|
+
btn.addEventListener('click', function() {
|
|
55
|
+
navigator.clipboard.writeText(url).then(function() {
|
|
56
|
+
btn.classList.add('copied');
|
|
57
|
+
setTimeout(function() { btn.classList.remove('copied'); }, 1500);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
el.appendChild(btn);
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
</body>
|
|
64
|
+
</html>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
Default list layout: hero, the authored body when there is one,
|
|
3
|
+
and an auto-list of the section's members.
|
|
4
|
+
|
|
5
|
+
Minimal by design: a package-build consumer disables Hugo's `section`,
|
|
6
|
+
`taxonomy` and `term` kinds, so this layout never runs for one — the home
|
|
7
|
+
page and its pages are all it renders through `index.html` and
|
|
8
|
+
`_default/single.html`. A site that keeps those kinds enabled — one that
|
|
9
|
+
is not a package-build consumer, publishing a hand-curated section of its
|
|
10
|
+
own — still needs a landing for them, which is what this file is for.
|
|
11
|
+
|
|
12
|
+
Content rows render "Name + description", inline rather than through a
|
|
13
|
+
shared partial: this is the only caller left, and a description is emitted
|
|
14
|
+
only when present, so a page carrying none renders a plain title row.
|
|
15
|
+
*/ -}}
|
|
16
|
+
{{ define "main" }}
|
|
17
|
+
{{ partial "hero-banner.html" . }}
|
|
18
|
+
<article class="single">
|
|
19
|
+
<div class="single-content">
|
|
20
|
+
{{ if .Content }}
|
|
21
|
+
<div class="single-body" data-pagefind-body>
|
|
22
|
+
{{ .Content }}
|
|
23
|
+
</div>
|
|
24
|
+
{{ end }}
|
|
25
|
+
|
|
26
|
+
{{- /*
|
|
27
|
+
A curated landing is the best navigation — a rules section groups its
|
|
28
|
+
pages under editorial headings like "Key Concepts", which no hierarchy
|
|
29
|
+
can infer. But gating the auto-list on "has no body" meant authoring an
|
|
30
|
+
intro silently replaced the section's ONLY navigation, and hand-curated
|
|
31
|
+
lists drift: one guide reached 41 pages linking 1, and another had
|
|
32
|
+
quietly orphaned a page.
|
|
33
|
+
|
|
34
|
+
So the auto-list is a GAP-FILLER: a section with a body lists only the
|
|
35
|
+
pages that are not reachable from that body by following links. A landing
|
|
36
|
+
whose hierarchy covers everything shows nothing extra, and a page can
|
|
37
|
+
never become unreachable — a newly added page appears under "Orphaned
|
|
38
|
+
Pages" until someone files it into the curated index.
|
|
39
|
+
|
|
40
|
+
Taxonomy and term pages keep their own listing shapes below.
|
|
41
|
+
*/ -}}
|
|
42
|
+
{{- $showAutoList :=
|
|
43
|
+
or (eq .Kind "taxonomy")
|
|
44
|
+
(eq .Kind "term")
|
|
45
|
+
(eq .Kind "section")
|
|
46
|
+
-}}
|
|
47
|
+
{{- $members := .Pages -}}
|
|
48
|
+
{{- /*
|
|
49
|
+
Pages reachable from the authored body, matched on RelPermalink so the
|
|
50
|
+
comparison survives aliases and wikilink resolution.
|
|
51
|
+
|
|
52
|
+
The needle deliberately stops after the URL rather than closing the quote,
|
|
53
|
+
so a link that carries a fragment counts. A rules landing reaches its
|
|
54
|
+
Characters chapter as `…/characters-introduction/#body-structure`, and
|
|
55
|
+
demanding the closing quote made that chapter read as unlinked. A trailing
|
|
56
|
+
slash ends every RelPermalink, so the open-ended match cannot bleed into a
|
|
57
|
+
sibling (`/rules/fate/` never prefixes `/rules/fate-points/`).
|
|
58
|
+
|
|
59
|
+
Reachability is TRANSITIVE, because a curated corpus is a hierarchy rather
|
|
60
|
+
than a flat index: a rules landing links ten chapter introductions, and
|
|
61
|
+
each of those links its own pages — up to four hops deep (root → Trauma →
|
|
62
|
+
Body → Injury → Bleeding). Testing only what the landing links DIRECTLY
|
|
63
|
+
counted every page below the first hop as a leftover. So a page counts as
|
|
64
|
+
listed when the body links it, or when any page already counted as listed
|
|
65
|
+
links it.
|
|
66
|
+
|
|
67
|
+
The walk runs to a fixed point: each pass appends the newly reached pages'
|
|
68
|
+
content to the haystack, and the first pass that reaches nothing new stops
|
|
69
|
+
it. Twelve passes is far past the depth of any corpus here (the rules need
|
|
70
|
+
four), and the `$growing` guard makes the surplus passes free.
|
|
71
|
+
*/ -}}
|
|
72
|
+
{{- $unlisted := $members -}}
|
|
73
|
+
{{- if and (eq .Kind "section") .Content -}}
|
|
74
|
+
{{- $haystack := .Content -}}
|
|
75
|
+
{{- $reached := slice -}}
|
|
76
|
+
{{- $growing := true -}}
|
|
77
|
+
{{- range seq 12 -}}
|
|
78
|
+
{{- if $growing -}}
|
|
79
|
+
{{- $growing = false -}}
|
|
80
|
+
{{- range $members -}}
|
|
81
|
+
{{- if not (in $reached .RelPermalink) -}}
|
|
82
|
+
{{- if in $haystack (printf "href=\"%s" .RelPermalink) -}}
|
|
83
|
+
{{- $reached = $reached | append .RelPermalink -}}
|
|
84
|
+
{{- $haystack = print $haystack .Content -}}
|
|
85
|
+
{{- $growing = true -}}
|
|
86
|
+
{{- end -}}
|
|
87
|
+
{{- end -}}
|
|
88
|
+
{{- end -}}
|
|
89
|
+
{{- end -}}
|
|
90
|
+
{{- end -}}
|
|
91
|
+
{{- $unlisted = where $members "RelPermalink" "not in" $reached -}}
|
|
92
|
+
{{- end -}}
|
|
93
|
+
{{- /*
|
|
94
|
+
Label the leftovers only when a curated body precedes them. With
|
|
95
|
+
reachability transitive, anything still here is genuinely unreachable
|
|
96
|
+
by reading — an orphan — so the heading says so rather than claiming to be
|
|
97
|
+
the section's full contents.
|
|
98
|
+
*/ -}}
|
|
99
|
+
{{- $labelList := and (eq .Kind "section") .Content -}}
|
|
100
|
+
{{- if $showAutoList -}}
|
|
101
|
+
{{- with $unlisted -}}
|
|
102
|
+
{{- if eq $.Kind "taxonomy" -}}
|
|
103
|
+
<ul class="page-list">
|
|
104
|
+
{{- range . }}
|
|
105
|
+
<li class="page-list-item">
|
|
106
|
+
<a class="page-list-title" href="{{ .RelPermalink }}">{{ .Title }}</a>
|
|
107
|
+
<span class="page-list-count">{{ len .Pages }}</span>
|
|
108
|
+
</li>
|
|
109
|
+
{{- end }}
|
|
110
|
+
</ul>
|
|
111
|
+
{{- else -}}
|
|
112
|
+
{{- if $labelList }}
|
|
113
|
+
<h2 class="page-list-heading">Orphaned Pages</h2>
|
|
114
|
+
{{- end }}
|
|
115
|
+
<ul class="page-list">
|
|
116
|
+
{{- range . }}
|
|
117
|
+
<li class="page-list-item">
|
|
118
|
+
<a class="page-list-title" href="{{ .RelPermalink }}">{{ .Title }}</a>
|
|
119
|
+
{{- with .Description }}
|
|
120
|
+
<span class="page-list-desc">{{ . }}</span>
|
|
121
|
+
{{- end }}
|
|
122
|
+
</li>
|
|
123
|
+
{{- end }}
|
|
124
|
+
</ul>
|
|
125
|
+
{{- end -}}
|
|
126
|
+
{{- else -}}
|
|
127
|
+
{{- /*
|
|
128
|
+
"Nothing here yet" is the right answer for an empty auto-listed section,
|
|
129
|
+
and the wrong one for a curated landing — there a leftover list of zero
|
|
130
|
+
means the hierarchy covers everything, so nothing should be said at all.
|
|
131
|
+
*/ -}}
|
|
132
|
+
{{- if not $labelList -}}
|
|
133
|
+
<p class="page-list-empty">Nothing here yet.</p>
|
|
134
|
+
{{- end -}}
|
|
135
|
+
{{- end -}}
|
|
136
|
+
{{- end }}
|
|
137
|
+
</div>
|
|
138
|
+
</article>
|
|
139
|
+
{{ end }}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{{ define "main" }}
|
|
2
|
+
{{ partial "hero-banner.html" . }}
|
|
3
|
+
<article class="single">
|
|
4
|
+
<div class="single-content">
|
|
5
|
+
{{ if .Content }}
|
|
6
|
+
<div class="single-body">
|
|
7
|
+
{{ .Content }}
|
|
8
|
+
</div>
|
|
9
|
+
{{ end }}
|
|
10
|
+
|
|
11
|
+
{{- /* Blog list: walk every descendant page in any /blog/YYYY/MM/
|
|
12
|
+
date bucket, filter to actual blog posts, sort newest-first,
|
|
13
|
+
and emit a list of entries with date + series + title +
|
|
14
|
+
description. The default list.html skips auto-listing for
|
|
15
|
+
hand-crafted section indexes (any _index.md), which is
|
|
16
|
+
correct for most sections but wrong for a blog landing. */ -}}
|
|
17
|
+
{{- $posts := where .Site.RegularPages "Section" "blog" -}}
|
|
18
|
+
{{- $posts = where $posts "Type" "blog-post" -}}
|
|
19
|
+
{{- $posts = $posts.ByDate.Reverse -}}
|
|
20
|
+
|
|
21
|
+
{{- with $posts }}
|
|
22
|
+
<ul class="blog-list">
|
|
23
|
+
{{- range . }}
|
|
24
|
+
<li class="blog-list-item">
|
|
25
|
+
{{ if .Date }}
|
|
26
|
+
<time class="blog-list-date" datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
|
|
27
|
+
{{ end }}
|
|
28
|
+
<a class="blog-list-title" href="{{ .RelPermalink }}">{{ .Title }}</a>
|
|
29
|
+
{{- /* Auto-generate a ~100-word preview from the rendered body.
|
|
30
|
+
`.Plain` strips HTML tags. `htmlUnescape` then decodes
|
|
31
|
+
entities so smart quotes Goldmark emitted as `’`
|
|
32
|
+
etc. become real characters (otherwise the browser
|
|
33
|
+
displays them literally after Hugo re-escapes the `&`).
|
|
34
|
+
Collapse paragraph-break whitespace into single spaces,
|
|
35
|
+
and append an ellipsis only if we actually truncated. */ -}}
|
|
36
|
+
{{- $plain := .Plain | htmlUnescape -}}
|
|
37
|
+
{{- $collapsed := trim (replaceRE `\s+` " " $plain) " " -}}
|
|
38
|
+
{{- $words := split $collapsed " " -}}
|
|
39
|
+
{{- $preview := $collapsed -}}
|
|
40
|
+
{{- if gt (len $words) 100 -}}
|
|
41
|
+
{{- $preview = printf "%s…" (delimit (first 100 $words) " ") -}}
|
|
42
|
+
{{- end -}}
|
|
43
|
+
{{- with $preview }}
|
|
44
|
+
<p class="blog-list-desc">{{ . }}</p>
|
|
45
|
+
{{- end }}
|
|
46
|
+
</li>
|
|
47
|
+
{{- end }}
|
|
48
|
+
</ul>
|
|
49
|
+
{{- else }}
|
|
50
|
+
<p class="page-list-empty">No posts yet.</p>
|
|
51
|
+
{{- end }}
|
|
52
|
+
</div>
|
|
53
|
+
</article>
|
|
54
|
+
{{ end }}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{{ define "main" }}
|
|
2
|
+
{{ partial "hero-banner.html" . }}
|
|
3
|
+
|
|
4
|
+
{{- /* Blog posts use the same content chrome as other pages but with
|
|
5
|
+
a blog-specific meta line (series + date) and no right-rail
|
|
6
|
+
sidebar or auto-generated TOC. If a post grows long enough to
|
|
7
|
+
warrant a TOC, we can opt it in via a frontmatter flag later. */ -}}
|
|
8
|
+
|
|
9
|
+
<article class="single">
|
|
10
|
+
<div class="single-content">
|
|
11
|
+
<div class="single-meta blog-meta">
|
|
12
|
+
{{ with .Params.series }}
|
|
13
|
+
<div class="blog-series">
|
|
14
|
+
<span class="blog-series-label">Series</span>
|
|
15
|
+
<span class="blog-series-name">{{ . }}</span>
|
|
16
|
+
</div>
|
|
17
|
+
{{ end }}
|
|
18
|
+
{{ if .Params.date }}
|
|
19
|
+
<time class="single-date" datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
|
|
20
|
+
{{ end }}
|
|
21
|
+
{{ partial "tags.html" . }}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="single-body" data-pagefind-body>
|
|
25
|
+
{{ .Content }}
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{{ partial "related.html" . }}
|
|
29
|
+
|
|
30
|
+
<nav class="single-nav">
|
|
31
|
+
{{ with .PrevInSection }}<a href="{{ .Permalink }}" class="nav-prev">← {{ .Title }}</a>{{ end }}
|
|
32
|
+
{{ with .NextInSection }}<a href="{{ .Permalink }}" class="nav-next">{{ .Title }} →</a>{{ end }}
|
|
33
|
+
</nav>
|
|
34
|
+
</div>
|
|
35
|
+
</article>
|
|
36
|
+
{{ end }}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
Home layout. Two shapes share it, chosen by what the home page carries.
|
|
3
|
+
|
|
4
|
+
**A package homepage** — the entry point to one published package, at its
|
|
5
|
+
own prefix (/sohl/, /thalorna/, /kethira/, …), mounted as the package's
|
|
6
|
+
`_index.md` and selected when it declares `type: homepage`. It renders
|
|
7
|
+
through the same structure `_default/single.html` gives a note — the hero
|
|
8
|
+
from its `banner`, then its body — through the shared `page-body.html`
|
|
9
|
+
partial, so a homepage and a page look like one family. A package supplies
|
|
10
|
+
only its own title, description, banner and body; the theme owns the
|
|
11
|
+
arrangement.
|
|
12
|
+
|
|
13
|
+
**The featured grid** — a site whose home page is a set of entry points into
|
|
14
|
+
everything it publishes. This is the older shape and stays the default, so a
|
|
15
|
+
consumer that has not adopted the homepage note keeps rendering exactly as
|
|
16
|
+
before.
|
|
17
|
+
|
|
18
|
+
The theme owns the arrangement; the consuming site owns every address and
|
|
19
|
+
every word. The grid reads them from `params.home`:
|
|
20
|
+
|
|
21
|
+
heroimage Banner behind the hero band. Resolved through cdn-url.html,
|
|
22
|
+
so it may be absolute or relative to `params.cdnBaseURL`.
|
|
23
|
+
herotitle Hero heading.
|
|
24
|
+
herotagline Hero standfirst.
|
|
25
|
+
headings Column headings across the top row of the featured grid.
|
|
26
|
+
Each: `title`, and an optional `class`, appended as
|
|
27
|
+
`home-section-title--<class>` for grid placement.
|
|
28
|
+
cards The entry-point cards. Each: `title`, `url`, `text`, and
|
|
29
|
+
optional `image`, `alt` (image alt text, defaulting to
|
|
30
|
+
`title`) and `key` (appended as `home-card--<key>` for
|
|
31
|
+
grid placement).
|
|
32
|
+
|
|
33
|
+
Headings and cards render flat, as direct children of `.home-featured`,
|
|
34
|
+
because they are the grid's items — the CSS places each one by its
|
|
35
|
+
modifier class.
|
|
36
|
+
*/ -}}
|
|
37
|
+
{{ define "main" }}
|
|
38
|
+
{{- if eq (.Params.type | default "") "homepage" -}}
|
|
39
|
+
{{- partial "hero-banner.html" . -}}
|
|
40
|
+
{{- partial "page-body.html" . -}}
|
|
41
|
+
{{- else -}}
|
|
42
|
+
{{- with .Site.Params.home }}
|
|
43
|
+
<section class="hero hero-with-image"{{ with .heroimage }} style="background-image: url('{{ partial "cdn-url.html" (dict "site" $.Site "path" .) }}');"{{ end }}>
|
|
44
|
+
<div class="hero-inner" data-pagefind-ignore>
|
|
45
|
+
<h1>{{ .herotitle }}</h1>
|
|
46
|
+
{{- with .herotagline }}
|
|
47
|
+
<p class="hero-tagline">{{ . }}</p>
|
|
48
|
+
{{- end }}
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
<section class="home-sections">
|
|
53
|
+
<div class="home-featured">
|
|
54
|
+
{{- range .headings }}
|
|
55
|
+
<h2 class="home-section-title{{ with .class }} home-section-title--{{ . }}{{ end }}">{{ .title }}</h2>
|
|
56
|
+
{{- end }}
|
|
57
|
+
{{- range .cards }}
|
|
58
|
+
|
|
59
|
+
<div class="home-card{{ with .key }} home-card--{{ . }}{{ end }}">
|
|
60
|
+
{{- $url := .url }}{{ $title := .title }}{{ $alt := .alt | default .title }}
|
|
61
|
+
{{- with .image }}
|
|
62
|
+
<a href="{{ $url }}">
|
|
63
|
+
<img src="{{ partial "cdn-url.html" (dict "site" $.Site "path" .) }}" alt="{{ $alt }}" class="card-image">
|
|
64
|
+
</a>
|
|
65
|
+
{{- end }}
|
|
66
|
+
<h2><a href="{{ $url }}">{{ $title }}</a></h2>
|
|
67
|
+
<p>{{ .text }}</p>
|
|
68
|
+
<a href="{{ $url }}" class="card-button">More Info →</a>
|
|
69
|
+
</div>
|
|
70
|
+
{{- end }}
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
{{- end }}
|
|
74
|
+
{{- end }}
|
|
75
|
+
{{ end }}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
Resolve a `banner:` value to a URL, or to "" when there is none.
|
|
3
|
+
|
|
4
|
+
Split out of `hero-banner.html` (issue #41): a single implementation is what
|
|
5
|
+
keeps a second caller from drifting the way issue #36 already failed once —
|
|
6
|
+
silently, behind a dead URL.
|
|
7
|
+
|
|
8
|
+
Resolution, in order:
|
|
9
|
+
1. `data.banner` — a note's own hero image, which the content build has
|
|
10
|
+
already resolved from an asset Address into the address its asset host
|
|
11
|
+
serves. It is used exactly as given. A blank one is a note naming no
|
|
12
|
+
hero image on purpose and declines an image.
|
|
13
|
+
2. `banner` — a hand-authored page's value. `none` (or `false`) declines an
|
|
14
|
+
image and returns "". A full URL is used as-is; anything else is a
|
|
15
|
+
fragment under `images/`.
|
|
16
|
+
3. `images/banners/{subtype}.webp`, when neither is set and a `subtype` is
|
|
17
|
+
supplied. A caller that passes no subtype gets "" instead, which is
|
|
18
|
+
what a card wants: a section with no banner of its own renders as the
|
|
19
|
+
text card it rendered before.
|
|
20
|
+
4. `images/banners/{fallback}` — whenever the name picked by 2 or 3 is not
|
|
21
|
+
in `data/banners.yaml`'s declared inventory. Hugo cannot ask a remote
|
|
22
|
+
host whether a URL exists, so the declaration stands in for the test;
|
|
23
|
+
see `hero-banner.html` for why, and `data/banners.yaml` for how the
|
|
24
|
+
list is kept honest.
|
|
25
|
+
|
|
26
|
+
The check applies only to a relative path resolving directly inside the
|
|
27
|
+
declared banner directory. An absolute URL, and a fragment pointing anywhere
|
|
28
|
+
else under `images/`, pass through untouched: the theme has no inventory for
|
|
29
|
+
either and must not second-guess an address it cannot know about. A step 1
|
|
30
|
+
address is never checked either — the build resolved it against a real asset
|
|
31
|
+
record and dropped one nothing answered, so there is no question left to
|
|
32
|
+
ask.
|
|
33
|
+
|
|
34
|
+
Whatever survives resolves through `cdn-url.html`, so the theme names a path
|
|
35
|
+
and never a host.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
{{ partial "banner-url.html" (dict "site" .Site "banner" .Params.banner
|
|
39
|
+
"subtype" $subtype "context" .RelPermalink) }}
|
|
40
|
+
|
|
41
|
+
Params:
|
|
42
|
+
site — the site context (.Site), for `params.cdnBaseURL`.
|
|
43
|
+
data — the page's `data` map (`.Params.data`), read for the `banner`
|
|
44
|
+
the content build resolved. Omit for a page that carries none.
|
|
45
|
+
banner — the raw `banner:` value; may be unset, `none`/`false`, a
|
|
46
|
+
fragment or an absolute URL.
|
|
47
|
+
subtype — optional. The name to default to when `banner` is unset. Omit
|
|
48
|
+
for "no banner means no image".
|
|
49
|
+
context — optional. A page address, named in the missing-banner warning so
|
|
50
|
+
the report says where the want came from.
|
|
51
|
+
*/ -}}
|
|
52
|
+
{{- $site := .site -}}
|
|
53
|
+
{{- $subtype := .subtype | default "" -}}
|
|
54
|
+
{{- $context := .context | default "" -}}
|
|
55
|
+
{{- $data := .data | default dict -}}
|
|
56
|
+
|
|
57
|
+
{{- $banners := hugo.Data.banners | default dict -}}
|
|
58
|
+
{{- $dir := $banners.dir | default "images/banners" -}}
|
|
59
|
+
{{- $fallback := $banners.fallback | default "default.webp" -}}
|
|
60
|
+
{{- $available := $banners.available | default slice -}}
|
|
61
|
+
|
|
62
|
+
{{- /* `none`/`false` is a decision, not an absence, so it is read before the
|
|
63
|
+
resolution order rather than as a step in it. Formatted rather than
|
|
64
|
+
tested for truth, and deliberately not passed through `default`: both
|
|
65
|
+
`false` and an unset banner are falsy, and only formatting tells them
|
|
66
|
+
apart. */ -}}
|
|
67
|
+
{{- $raw := printf "%v" .banner -}}
|
|
68
|
+
{{- $declined := or (eq (lower $raw) "none") (eq $raw "false") -}}
|
|
69
|
+
|
|
70
|
+
{{- /* A note's resolved banner, formatted for the same reason the decline
|
|
71
|
+
above is: an unset key and a deliberate blank are both falsy, and only
|
|
72
|
+
formatting tells them apart. `<nil>` covers both an absent key and an
|
|
73
|
+
explicit `null`, which the note format reads as *unset* — so the
|
|
74
|
+
resolution carries on to the subtype default. `""` is the format's
|
|
75
|
+
deliberate blank: a note naming no hero image, which declines one. */ -}}
|
|
76
|
+
{{- $resolved := printf "%v" (index $data "banner") -}}
|
|
77
|
+
{{- $fromBuild := ne $resolved "<nil>" -}}
|
|
78
|
+
|
|
79
|
+
{{- $banner := "" -}}
|
|
80
|
+
{{- $absolute := false -}}
|
|
81
|
+
{{- if $fromBuild -}}
|
|
82
|
+
{{- $banner = $resolved -}}
|
|
83
|
+
{{- $absolute = true -}}
|
|
84
|
+
{{- else if not $declined -}}
|
|
85
|
+
{{- with .banner -}}
|
|
86
|
+
{{- $banner = printf "images/%s" (strings.TrimPrefix "/" .) -}}
|
|
87
|
+
{{- if or (hasPrefix . "http://") (hasPrefix . "https://") (hasPrefix . "//") -}}
|
|
88
|
+
{{- $banner = . -}}
|
|
89
|
+
{{- $absolute = true -}}
|
|
90
|
+
{{- end -}}
|
|
91
|
+
{{- else -}}
|
|
92
|
+
{{- with $subtype -}}
|
|
93
|
+
{{- $banner = printf "%s/%s.webp" $dir . -}}
|
|
94
|
+
{{- end -}}
|
|
95
|
+
{{- end -}}
|
|
96
|
+
|
|
97
|
+
{{- /* The declared-existence check. Deduplicated by name, so a subtype with
|
|
98
|
+
no artwork is reported once per build rather than once per page. */ -}}
|
|
99
|
+
{{- if and $banner (not $absolute) (eq (path.Dir $banner) $dir) -}}
|
|
100
|
+
{{- $name := strings.TrimSuffix (path.Ext $banner) (path.Base $banner) -}}
|
|
101
|
+
{{- if not (in $available $name) -}}
|
|
102
|
+
{{- partialCached "warn-missing-banner.html" (dict
|
|
103
|
+
"name" $name
|
|
104
|
+
"dir" $dir
|
|
105
|
+
"fallback" $fallback
|
|
106
|
+
"context" $context
|
|
107
|
+
) $name -}}
|
|
108
|
+
{{- $banner = printf "%s/%s" $dir $fallback -}}
|
|
109
|
+
{{- end -}}
|
|
110
|
+
{{- end -}}
|
|
111
|
+
{{- end -}}
|
|
112
|
+
|
|
113
|
+
{{- $url := "" -}}
|
|
114
|
+
{{- if $banner -}}
|
|
115
|
+
{{- $url = partial "cdn-url.html" (dict "site" $site "path" $banner) -}}
|
|
116
|
+
{{- end -}}
|
|
117
|
+
{{- return $url -}}
|