First-class image gallery block. Two layouts ship today - grid (uniform
tiles, configurable columns 1-6, aspect-ratio presets) and masonry
(CSS-columns Pinterest packing). Click-to-zoom lightbox built in Preact,
portalled to <body> so it escapes the section's `isolation: isolate`
stacking context; keyboard navigation (← → Esc), touch swipe, and body
scroll-lock included.
Two source modes: explicit items[] for curated galleries with per-image
captions/credits/links, or album: <folder> for drop-in folder loading
sorted by filename. Remote URLs (Unsplash/Pexels/CDNs) are passed
through unprocessed; local images get the full responsive variants +
inline base64 LQIP placeholder for instant first-paint over the eventual
sharp image.
Captions, titles, and credits are rendered through Hugo's RenderString
at build time (display: inline) - users get full Markdown (bold/italic/
code/links) in any of those fields with zero JS markdown parser. Alt
text falls back to plainified caption → humanised filename so screen
readers always have something useful.
Caption positions: below (default - accessibility-first), overlay
(magazine gradient), hover (clean grid reveal), none (lightbox-only).
Block-scoped CSS in the co-located style.css is picked up by the
existing block-CSS auto-discovery.
Test site: two demos under home.yaml (curated grid + auto-loaded
masonry) with 9 sample Picsum images stored under
test/assets/media/albums/gallery/ at varied aspect ratios.
Single-location + multi-marker, side-by-side and map-only layouts.
Zero-config — no API key. MapLibre is vendored via vite and loaded
conditionally when a page contains a map block. Provider resolution
is a single switchable function so other tile providers can be added
later without changing the block contract.
Still on node20 - blocked on upstream releases:
peaceiris/actions-hugo@v3 - the maintainer merged a node24 upgrade to main but hasn't cut a new release tag yet; nothing to do until v3.1.0 drops
HugoBlox/gh-action-updater@v2 - needs update
Light-mode `secondary` was `blue` (hue ~260, almost identical to the
`indigo` primary at hue ~277). Result: every primary→secondary
gradient in light mode read as a flat solid colour — Pro pricing tier
border, "Most popular" badge, Pro CTA, hero highlight (when not in
forced-dark sections), stats numbers, comparison-table accents all
lost their gradient direction.
Switch light secondary to `teal` (hue ~182). Dark mode already used
teal, so this unifies the brand pairing across modes. Visible gradient
direction restored everywhere primary→secondary is used.
No block changes needed — the fix lives in the theme palette where
similar brand decisions belong, not as workarounds in individual
blocks.
Two tweaks driven by visual review of the rendered pricing block:
1. Highlighted tier — gradient + glow:
- Bump padding p-px → p-0.5 (1px → 2px) so the gradient border is
thick enough to actually show direction at the edges/corners.
Primary and secondary tokens are close in hue in light mode, so a
1px ring read as a flat indigo line.
- Add shadow-primary-500/20 to the gradient-border wrapper. The
brand-tinted glow carries the visual lift; the gradient itself is
supporting, not load-bearing, at this size.
- Drop the `via-primary-500` middle stop (no perceptible benefit
when the two endpoints are already close).
2. Free price size: text-5xl → text-4xl. The `$XX` rows render as
text-2xl `$` + text-5xl digit, so the digit's visual weight is
what the eye reads. "Free" at text-5xl was wider and dominating
the row vs the other two tiers; text-4xl matches the perceived
weight of `$19`/`$49` better.
The previous section_break implementation used CSS mask-image, which
incidentally established a stacking context on the section. Replacing
it with a gradient overlay div removed that side-effect — and broke
the visual depth on every section with a gradient mesh or radial
background.
Without a stacking context, the section's negative-z-index descendants
(home-section-bg at z:-1 carrying the radial gradient, and the
gradient_mesh wrapper at z:-10 carrying the orbs) escaped the section
and painted behind the page root, where they were occluded by the
page body — flat dark hero, no glow, no orbs.
Add `isolate` (isolation: isolate) to the section element. This is
the no-visual-side-effect way to force a stacking context, restoring
containment of the negative-z layers without affecting siblings or
content.
The mask-image approach for section_break was wrong: making pixels
transparent reveals the body background, not the next sibling section
(which sits below in document flow, not behind in z-order). When the
body bg didn't match the adjacent section, this produced a visible
white/light strip between sections — exactly what the fade was
supposed to prevent.
Replace mask-image with absolutely-positioned gradient overlay divs at
the section's top/bottom edges. The overlay fades from transparent to
a user-specified colour matching the adjacent sibling's background.
Explicit colour declaration is required because Hugo can't reliably
auto-detect the next sibling's bg at parse time.
API change (no backward-compat — feature was added this session, no
sites depend on the legacy string form):
# before
section_break: fade-bottom
# after
section_break:
fade_bottom: "#101828" # CSS colour matching next section's bg
fade_top: "#0a0e27" # optional, independent
size: "6rem" # optional, default 6rem
Update the landing page hero (fade-bottom into dark logos band) and
cta-card (fade-top from white FAQ section above) to the new form.
Switch the logos section to a dark band (bg-gray-900 + force `dark`
mode within the section) and white logo style. Extends the dark zone
from the hero so the hero's section_break: fade-bottom transitions
dark → dark instead of revealing a light strip.
The logos block itself keeps supporting both light and dark — only the
landing page config changes here. Other templates can opt for a light
band by reverting to `bg-gray-50 dark:bg-gray-900` + `logo_style:
grayscale`.
The featured card's quote was centered (inheriting `text-center` from
the section wrapper) while the avatar+name+role row read as left-
aligned because the flex container starts at the card's left edge —
the two halves of the same card disagreed. Multi-card grid was already
left-aligned, so single-vs-grid was inconsistent too.
Move `text-center` from the outer wrapper onto the section header
(title/text) only. The featured card and grid cards now both render
left-aligned, the decorative `"` mark sits naturally as a leading
glyph for the quote, and the awkward forced line breaks from
text-center go away.
- New `content.media` field accepts image (Hugo-pipeline-processed for
responsive srcset) or video (passes through as <video>). Optional
`dark_src` provides a dark-mode image variant.
- New `design.layout` enum: centered (default) | split-left |
split-right | stacked. Split layouts auto-set alignment to 'left'
unless explicitly overridden.
- Bump trust-strip star size from w-4 to w-5 for visual presence.
- Two new shared partials in layouts/_partials/functions/:
- resolve_image_resource: path → {resource | fallback_url}
- process_image_for_props: path → {src, srcset, width, height}
These factor out the resource-resolution + responsive-processing
pipeline so media-bearing blocks don't have to inline it.
- Capabilities registry: media-image, media-video, split-layouts.
- Landing page _index.md: keeps the centered hero (no behavioural
change) but documents the opt-in pattern for split layouts inline.
- Drop the orphan `pt-14` on the outer wrapper (was meant for
transparent floating headers; site headers don't need it)
- New `design.size` enum: compact | default | tall | viewport | none
- default (new): py-24 sm:py-32 lg:py-40 — modern 2026 baseline
(~30% less than the previous py-32 sm:py-48 lg:py-56)
- compact: utility heroes
- tall: the previous default — opt-in for splashy weight
- viewport: min-h-screen with vertical centring for splash heroes
- New `design.alignment` enum: center (default) | left
- Threads through eyebrow/title/subtitle/CTAs/trust/announcement
- 'left' is the prep work for Phase 3 split layouts
- Backward compat: `no_padding: true` still works — maps to size: "none"
when size is unset (legacy field marked deprecated in schema)
- Capabilities registry: size-presets, alignment
Responsive desktop table + mobile stacked-cards competitor comparison.
Supports category headings, boolean check/cross/partial and text cell
values, row striping, highlighted column with gradient border, and a
CTA row. Per-cell zebra striping opts out on the highlighted column to
avoid ring paint-order conflicts.
Replaces block.html with a Preact component featuring smooth CSS grid
0fr→1fr height animation, icon rotation, and multi-paragraph answer
support via renderRich(). Single-open accordion with question/title and
answer/text field aliases for backward compatibility.
Hugo v0.161's css.TailwindCSS reads node_modules/.bin/tailwindcss and
expects either a symlink or a Node-shebang script. pnpm's default
isolated linker creates shell wrappers (#!/bin/sh with NODE_PATH
manipulation), which Hugo rejects with "binary tailwindcss is not a
Node.js script". With node-linker=hoisted, pnpm flattens node_modules
and creates plain symlinks for Node-shebang bins — same shape npm
produces, which Hugo handles via its symlink branch in resolveNodeBin
(common/hexec/exec.go).
- Add .npmrc with node-linker=hoisted to all 9 templates and test/
- Verified end-to-end: academic-cv builds cleanly against published
modules/blox with Hugo v0.161.1
- Kit root and modules/blox keep pnpm's isolated linker for monorepo
The shipped per-template GitHub Actions build workflow still pinned
Node 20 and fell back to Hugo 0.154.5 on missing config - both
incompatible with Hugo v0.161's css.TailwindCSS Node permission sandbox
(Node >= 22, npm @tailwindcss/cli required). Sites upgraded via
`hugoblox upgrade` (which only touches go.mod) hit this immediately.
- Bump NODE_VERSION 20 -> 22 in all 9 templates' .github/workflows/build.yml
- Bump fallback DEFAULT_VERSION 0.154.5 -> 0.161.0 to match the framework's
new module.hugoVersion.min
- Add a "Verify Tailwind CSS dependency" step that fails fast with an
actionable ::error message if @tailwindcss/cli is not resolvable,
replacing Hugo's cryptic "binary tailwindcss is not a Node.js script"
Steps block:
- Three layouts: vertical (stacked with connector line), horizontal
(side-by-side, connector above), timeline (alternating cards on centre
line)
- Three marker styles: number (decimal/padded/roman/alpha), icon, dot
- Layout-aware marker sizing - bare 40px icon for horizontal, tinted
circle for vertical/timeline
- Staggered IntersectionObserver entrance animation
- Code span rendering in step body text
- Added to startup-landing-page template as a horizontal how-it-works
section
Replace legacy static Go HTML logos block with a Preact component. Adds three
layouts (row, grid, marquee), three filter styles (grayscale/color/white),
and three sizes (sm/md/lg). Fixed-width slots normalise visual weight across
SVG aspect ratios; per-item `scale` compensates for viewBox whitespace and
per-item `style` allows individual colour exceptions. Add logos block to startup-landing-page template.
Hugo v0.161 dropped the standalone tailwindcss binary; @tailwindcss/cli
npm package is now the only accepted transformer, invoked via
`node --permission` (Node >= 22 required).
- Bump module.hugoVersion.min to 0.161.0 in modules/blox/hugo.yaml;
add comment explaining why security.node.permissions is left to
Hugo's built-in defaults
- Bump hugo_version / HUGO_VERSION / devcontainer image tag to 0.161.0
across all 9 templates (hugoblox.yaml, netlify.toml, devcontainer.json)
- Bump devcontainer base image Node 20 → 22 and default Hugo version
- Add scripts/check-template-deps.mjs to assert every template declares
tailwindcss + @tailwindcss/cli; wire as pnpm check:template-deps
- Harden CI: new audit-template-deps job gates all build jobs; add
`require.resolve('@tailwindcss/cli/package.json')` check after each
pnpm install to catch the exact "binary is not a Node.js script" failure
Wire .hbb-section to var(--hb-spacing-section) instead of hardcoded 6rem,
enabling hugoblox.layout.spacing (compact/comfortable/spacious) to take effect.
- Default to "spacious" (6rem) to preserve existing visual layouts
- Per-section override via design.spacing.padding still works (inline > class)
- Remove dead design.spacing frontmatter field from all templates
- Set correct layout.spacing token in each template's params.yaml
Closes#3314