Phase 1 (v0.2.4): Tier 1 silent-failure defects - #62
Merged
Conversation
layouts/partials/head/schema.html emitted `// homepage`, `// schema for posts`, and `//breadcrumb schema` as plain template text inside <script type="application/ld+json"> blocks. JSON has no comment syntax, so every affected block failed to parse and all structured data was silently discarded. Remove the three comments; no other changes (the structural rewrite is a later phase). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRxJsK91SRR9AMxwJvhrqq
layouts/partials/common-partials/opengraph/get-featured-image.html resolved og_image_default with resources.Get, which returns nil for any path not under assets/. The theme's own default value carries a leading slash, and nothing normalized either form, so a miss aborted the build with an opaque "nil pointer evaluating resource.Resource.Resize" that never named the offending param. Normalize the leading slash with strings.TrimPrefix (matching the pattern already used in header.html for headerBackgroundImage), wrap the resources.Get in a with, and errorf naming the param and the resolved value on a miss. The Resize/text-overlay chain now only runs inside the with, so a miss fails with the named error instead of a nil-pointer panic. Also document in the README that og_image_default is assets/-relative only, unlike logo_png which accepts static/ or assets/, and add a Requirements note that extended Hugo is required whenever any OG/ processed image (including og_image_default) is WebP. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRxJsK91SRR9AMxwJvhrqq
layouts/partials/footer.html read .Site.Params.footer.tagCloud
directly at three call sites. The theme's own hugo.toml declares no
[params] at all, and only exampleSite sets [params.footer], so any
site that omits that block got "can't evaluate field tagCloud in
type interface {}".
Replace all three reads with .Param "footer.tagCloud", which resolves
the same page-then-site dotted lookup but returns nil instead of
erroring when the params.footer block is absent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRxJsK91SRR9AMxwJvhrqq
themeBoot.js unconditionally adds `dark` to <html> on OS-dark
preference, and was loaded unconditionally by head.html. Neither
showDarkToggle=false nor baseof.html's hardcoded body dark: classes
gated it, so a site that "turned dark mode off" (by never setting
showDarkToggle, which defaults false) still got OS-dark visitors
rendered dark, with no toggle to escape it.
Add layouts/partials/utils/dark-mode.html, a returning partial that
resolves params.darkMode ("toggle" / "system" / "off"), falling back
to the legacy showDarkToggle param only when darkMode is unset:
- nothing set -> "system" (unchanged behavior)
- showDarkToggle = true -> "toggle" (unchanged behavior)
- darkMode set explicitly -> as written (opt-in)
This mapping matters: a naive derivation straight from showDarkToggle
would resolve every existing site (which never set it) to "off" and
silently strip system-preference dark mode on upgrade.
Wire the resolved value through:
- head.html: skip loading js/themeBoot.js entirely when "off"
- head/js.html: theme-config's showDarkToggle (which gates main.js's
dynamic import of themeSwitcher.js) is now true only for "toggle"
- footer.html: the footer toggle button renders only for "toggle"
- baseof.html: <body> omits the dark: Tailwind variants when "off"
Document params.darkMode as the preferred param (with a new "Dark
Mode" README section) and showDarkToggle as a legacy alias; fix the
README and exampleSite comments that claimed showDarkToggle defaults
to true when head/js.html has always defaulted it to false.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRxJsK91SRR9AMxwJvhrqq
By default default-src 'self' blocks every iframe, including the
theme's own soundcloud and openstreetmap shortcodes — frame-src was
only ever populated from the manual params.csp.frameSrc string, and
exampleSite's hand-written frameSrc list was itself missing the uMap
host its own openstreetmap shortcode demo needs.
Three parts:
1. layouts/shortcodes/soundcloud.html and openstreetmap.html now
register their iframe host on .Page.Store ("cspFrameSrc", keyed and
valued by host so repeats dedupe). layouts/partials/head/csp.html
folds registered hosts into frame-src. Hugo renders <head> (and
this partial) before .Content, so a shortcode's registration would
otherwise arrive too late — force .Content evaluation immediately
before reading the store (`{{- with .Content -}}{{- end -}}`, which
no-ops safely on pages without content instead of erroring).
2. Add params.csp.embeds, a list of preset names (youtube, vimeo,
soundcloud, spotify, umap) mapped to known hosts, for embeds the
theme has no shortcode hook for — Hugo's built-in youtube/vimeo
shortcodes in particular. Folded into frame-src alongside the
auto-detected hosts and the manual frameSrc param, then deduped.
3. Switch exampleSite to the embeds preset (["youtube", "vimeo",
"soundcloud", "umap"]) instead of the hand-written, uMap-missing
frameSrc string, so it now correctly demos its own openstreetmap
shortcode. Document frameSrc and embeds in a new README "Content
Security Policy" section.
The existing "omit frame-src entirely when empty" behavior is
preserved for sites with no embeds configured or used.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRxJsK91SRR9AMxwJvhrqq
… them assets/js/main.js imports @alpinejs/csp, @alpinejs/focus, leaflet, and the Font Awesome packages, but none of them appeared in the theme's own package.json — only in exampleSite's — so a consuming site's js.Build failed until someone reverse-engineered the import list out of main.js. 1. Add a dependencies block to package.json with the same seven packages and version ranges exampleSite/package.json already uses; regenerate package-lock.json (npm install --package-lock-only --ignore-scripts). 2. This is documentation, not an install mechanism: a theme consumed as a git submodule or Hugo Module is never itself npm-installed, and js.Build resolves imports from the project root's node_modules — the consuming site's, not the theme's. The README now says so explicitly, with the exact install command for the site's own project root. 3. layouts/partials/head/js.html now checks os.FileExists "node_modules/@alpinejs/csp/package.json" (project- root relative) before js.Build and warnf's the full install command naming all seven packages when it's missing. This is a warnf, not an errorf: dependency-hoisting setups can have the package resolve from elsewhere and would false-positive on this specific check, so js.Build itself remains the authority on whether the build actually fails — the warning just makes a real failure legible instead of a raw esbuild resolution error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRxJsK91SRR9AMxwJvhrqq
writeStats = true and the three [[build.cachebusters]] rules lived only in exampleSite/config/_default/hugo.toml. The theme's own hugo.toml had no [build] block at all, even though tailwind.config.js globs ./hugo_stats.json for class discovery and the theme's own AGENTS.md notes that exampleSite's copy is intentionally tracked because clean builds depend on it. Every other consumer never got hugo_stats.json written at all (silently falling back to the layouts/**/*.html globs, which misses any class assembled dynamically in a template) and never got cachebusting in `hugo server`. Move both into the theme's hugo.toml — theme config merges into the site's, so consumers inherit them, and a site's own [build] block still wins if it sets one. Remove exampleSite's copy since inheritance now covers it; exampleSite doubles as the reference implementation, so its absence there is deliberate. Add a README note that consumers should add hugo_stats.json to .gitignore or track it deliberately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRxJsK91SRR9AMxwJvhrqq
posthog.html and csp.html read PUBLIC_POSTHOG_KEY, PUBLIC_POSTHOG_HOST,
and PUBLIC_POSTHOG_UI_HOST via getenv. Hugo's default [security.funcs]
getenv allowlist is ^HUGO_ and ^CI$ only, so any PUBLIC_ var needs an
explicit entry — undocumented in the README, and absent from
exampleSite. Without it, on this Hugo version, calling getenv on a
disallowed name doesn't fail quietly: it hard-errors the entire build
("access denied: ... is not whitelisted in policy security.funcs.getenv").
Wrap every PostHog getenv call in `try` so a disallowed var degrades
to "not found" instead of taking the whole build down, and so
posthog.html can tell "blocked by policy" apart from "genuinely
unset." When params.posthog_key is unset, PUBLIC_POSTHOG_KEY resolves
to nothing, and analytics_provider = "posthog" (so PostHog is plausibly
intended), warnf names whichever of the two applies. Sites not using
PostHog get no noise.
Add the required [security] block, commented with an explanation, to
exampleSite/config/_default/hugo.toml. Restructure the README PostHog
section to lead with posthog_key/posthog_host params (no allowlist
needed) and cover the environment-variable path as the secondary,
allowlist-requiring option.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRxJsK91SRR9AMxwJvhrqq
Bump theme.toml to v0.2.4 and the version field in package.json and exampleSite/package.json to 0.2.4, regenerating both lockfiles to match. Add CHANGELOG.md with a v0.2.4 section covering all of Tier 1 (1.1-1.9), including the 2.6 templates.Exists guard and the 1.9 REWRITE.md correction that shipped ahead of this branch. No breaking changes in this release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRxJsK91SRR9AMxwJvhrqq
The 1.7 commit declared the runtime deps at the versions exampleSite pinned at the time (FontAwesome ^6.5.2). Rebasing onto main after the Phase D dependency merges moved exampleSite to 7.3.1, and the conflict resolution updated package.json without regenerating the lockfile, so npm ci refused the mismatch. Regenerated; npm ci now succeeds at both the root and in exampleSite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRxJsK91SRR9AMxwJvhrqq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 of the v0.3 plan (#53) — spec items 1.1–1.8, one commit each, plus release prep. Nothing here is breaking.
Every item is a place Ryder failed without saying so. Several of the spec's acceptance checks fail on
maintoday and pass on this branch.Items
getenvallowlist: documented the required[security.funcs]block, added it (commented) to exampleSite, restructured README to lead with the allowlist-freeposthog_key/posthog_hostparams, and added awarnfthat names whichever failure applies<script type="application/ld+json">that made all structured data unparseablewith-guard theresources.Get,errorfnaming the param instead of a nil-pointer panic. Documentedog_image_defaultasassets/-only.Site.Params.footer.tagCloudreads (all three, not just the one the spec cited)params.darkMode("toggle"/"system"/"off") via a returning partial, with the spec's exact non-regressing default mapping. Fixed the README/code default mismatch.Page.Store;head/csp.htmlfolds them intoframe-src, plus aparams.csp.embedspreset. Fixes exampleSite's own broken uMap demopackage.json, plus a legiblewarnf(noterrorf) naming the required project-root install[build] writeStats+ cachebusters into the theme's ownhugo.tomlso consumers inherit themRelease prep:
theme.toml→v0.2.4, bothpackage.jsonversions →0.2.4, newCHANGELOG.md(### Breaking→ None). No tag — cut that after merge.One deviation from the spec
1.1: the spec assumed a disallowed
getenvsilently returns"". It doesn't — it hard-errors the entire build. Every PostHoggetenvcall is therefore wrapped in Hugo'strybuiltin, so a blocked var degrades to "not found" rather than crashing, and the warning can distinguish blocked by policy from genuinely unset. (tryneeds Hugo 0.141+; the theme already requires 0.146+.)Verification
Independently re-run by the orchestrator with Hugo extended 0.147, beyond the implementing agent's own pass:
exampleSitebuild — exit 0, zero errors/warningsmain)darkMode = "off"→<body class="bg-neutral-100 text-neutral-900 font-titillium">, zero themeBoot references; default build byte-identical to before (dark: classes present, themeBoot loaded)og_image_default = "/nope.png"→ERROR og_image_default "nope.png" not found under assets/ — the path must be assets/-relative, not a nil-pointer panic[params.footer]/[params.csp]builds cleanNote
Rebased onto current
mainafter the Phase D dependency merges; themedependenciesversions match what exampleSite now pins (FontAwesome 7.3.1, Alpine 3.15.12).🤖 Generated with Claude Code
https://claude.ai/code/session_01VRxJsK91SRR9AMxwJvhrqq
Generated by Claude Code