Skip to content

Phase 1 (v0.2.4): Tier 1 silent-failure defects - #62

Merged
benstraw merged 10 commits into
mainfrom
claude/phase-1-tier1-defects
Jul 26, 2026
Merged

Phase 1 (v0.2.4): Tier 1 silent-failure defects#62
benstraw merged 10 commits into
mainfrom
claude/phase-1-tier1-defects

Conversation

@benstraw

Copy link
Copy Markdown
Member

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 main today and pass on this branch.

Items

# Fix
1.1 PostHog getenv allowlist: documented the required [security.funcs] block, added it (commented) to exampleSite, restructured README to lead with the allowlist-free posthog_key/posthog_host params, and added a warnf that names whichever failure applies
1.2 Removed the three JS comments inside <script type="application/ld+json"> that made all structured data unparseable
1.3 OG resolver: trim leading slash, with-guard the resources.Get, errorf naming the param instead of a nil-pointer panic. Documented og_image_default as assets/-only
1.4 Guarded the unguarded .Site.Params.footer.tagCloud reads (all three, not just the one the spec cited)
1.5 New params.darkMode ("toggle"/"system"/"off") via a returning partial, with the spec's exact non-regressing default mapping. Fixed the README/code default mismatch
1.6 Embed shortcodes register their iframe host on .Page.Store; head/csp.html folds them into frame-src, plus a params.csp.embeds preset. Fixes exampleSite's own broken uMap demo
1.7 Declared the seven runtime JS deps in the theme package.json, plus a legible warnf (not errorf) naming the required project-root install
1.8 Moved [build] writeStats + cachebusters into the theme's own hugo.toml so consumers inherit them

Release prep: theme.tomlv0.2.4, both package.json versions → 0.2.4, new CHANGELOG.md (### Breaking → None). No tag — cut that after merge.

One deviation from the spec

1.1: the spec assumed a disallowed getenv silently returns "". It doesn't — it hard-errors the entire build. Every PostHog getenv call is therefore wrapped in Hugo's try builtin, so a blocked var degrades to "not found" rather than crashing, and the warning can distinguish blocked by policy from genuinely unset. (try needs 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:

  • Clean exampleSite build — exit 0, zero errors/warnings
  • Spec check 1 — all 64 JSON-LD blocks across the built site parse as JSON (fails on main)
  • Spec check 5darkMode = "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)
  • Spec check 7og_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
  • Spec check 3 — getenv warning fires when PostHog is selected but the var is blocked; silent once the security block is added
  • Spec check 6 — fixture site with no [params.footer] / [params.csp] builds clean
  • Unit tests: 14/14 pass

Note

Rebased onto current main after the Phase D dependency merges; theme dependencies versions 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

claude added 10 commits July 26, 2026 07:27
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
@benstraw
benstraw merged commit 0de61a3 into main Jul 26, 2026
2 checks passed
@benstraw
benstraw deleted the claude/phase-1-tier1-defects branch July 26, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants