Restructure `publication` frontmatter from a flat Markdown string into a
structured object (name, short_name, volume, issue, pages, publisher),
resolved through a new `functions/resolve_publication.html` partial that
also emits a per-page warning pointing legacy sites at `hugoblox migrate
publications`. APA/MLA citation rendering and the publication metadata
row both consume the resolver - no parsing of conventional Markdown
strings at render time.
Add work-level metadata fields surfaced via a new reusable
`components/award_badge.html` partial (variants: pill, card-corner,
inline, row; level-driven icon + colour tokens; pagefind filter):
- `awards[]` - name, level (winner/runner_up/honorable_mention/nominee/
selected/featured), issuer, date, note, url; skim-row pills above the
featured image, full row in the metadata grid, corner badges on card
and slides-gallery views, inline marker on citation views, pill on
date-title-summary
- `funding[]` - funder + grant + url, rendered as a metadata grid row
- `license` - SPDX-friendly string, metadata grid row
- `peer_reviewed`, `open_access` - status pills, decoupled from
the publication block so preprints can surface them
Demo content updated in academic-cv (and the same shape in research-lab).
Route all .Params.image access through a single normalizer partial that
returns the params as a map and emits a warnf with the expected shape
when frontmatter is set but not an object.
Three new design.layout values, all driven by the existing items/resolver
infrastructure (no Hugo-side changes).
- justified: Flickr-style row-balanced layout. Each tile's flex-basis is
derived from its precomputed aspect ratio at the configured row_height,
flex-grow proportional to ratio so wider images claim more leftover
row space. Pure CSS, no JS layout math. New design.row_height field
(clamped 120-640, default 240). Last item capped at max-width: 50% so
a lonely last-row image doesn't stretch grotesquely.
- carousel: CSS scroll-snap-type: x mandatory horizontal strip. Touch
swipe is native; optional Preact prev/next arrows render on md+ only.
Per-breakpoint flex-basis driven by design.columns (mobile always
shows ~1 + peeking next). Full-bleed scroll via negative margins
matched by inner padding so the strip feels native at any width.
- slideshow: Single-image presentation with crossfade transitions, dots,
arrows, optional autoplay with pause-on-hover, touch swipe, and a
click-through to the lightbox at full resolution. New design.autoplay
(default false) and design.autoplay_interval (ms, min 1000, default
5000) fields. Aspect defaults to "wide" for presentational feel.
Test site adds three new demos pointing at the same album folder so
users can compare layouts side-by-side. Block bundle grows from 39KB
to 66KB; pages without a gallery still ship 0KB (init.html gating).
Adds the metadata pipeline that makes the gallery block self-curating.
Per-image metadata now resolves through a four-tier waterfall, first
non-empty source wins:
1. Frontmatter (items[].caption etc.)
2. Sidecar file next to the image - tries six patterns:
<name>.yaml / .yml / .json and <basename>.yaml / .yml / .json,
looked up in both site assets and the page bundle (covers album
mode and page-resource mode alike).
3. Embedded IPTC/EXIF via Hugo's .Exif.Tags - caption from
Caption-Abstract / ImageDescription / XPSubject / Description,
credit from By-line / Artist / Creator. Wrapped in `try` so
non-EXIF formats and bad metadata never break the build.
4. Humanised filename - alt fallback only; captions stay empty
rather than fabricated from filenames.
Also adds a third source mode - content.page_resources: true -
auto-discovers image resources attached to the current page bundle.
Drop-in for blog posts and project pages where photos live next to
index.md; sidecar lookup works inside the bundle too.
Fixes a leading-slash quirk: Resource.Name returns "/media/..." for
site assets but a bare path for page-bundle resources; the path is
now normalised so sidecar lookups land in either layout.
Test site adds two sidecar YAML files (04-cabin.jpg.yaml and
06-mountain.jpg.yaml) - the masonry album demo now picks up titles,
markdown captions, and links from those sidecars without listing any
items[].
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.
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.
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.
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
- Use design.spacing.padding to remove top padding on biography section
with banner image (previously caused a large 96px gap above the banner)
- Add proper vertical spacing (mt-4) between bio text and Download Résumé button
- Implement design.show_status option in both biography and biography-3 blocks
(config was accepted but never checked — status emoji always rendered)
Removes x-transition:leave directives from portfolio cards so hidden items
exit the grid flow immediately. This fixes two visual glitches when clicking
filter buttons: "ghost" cards briefly appearing during the overlap of enter
and leave animations, and cards snapping from their original grid position
to the start after a delayed reflow.
Fixes#3309
site_head.html, jsonld/article.html, jsonld/qapage.html, and
jsonld/event.html hardcode `(.Resources.ByType "image").GetMatch
"*featured*"` to find featured images. This ignores custom filenames
set via `.Params.image.filename` in front matter, causing og:image
and JSON-LD image to fall back to the site icon.
Replace the hardcoded pattern with the `get_featured_image` partial
(already used by jsonld/webpage.html), which properly searches for:
1. Files matching `*featured*` in the page directory
2. Custom filename from `.Params.image.filename` in the page directory
3. Custom filename in `assets/media/` directory
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add `actions/configure-pages` step to all template build workflows and
pass the detected base_url to `hugo --baseURL`, so sites deployed to
subdirectory paths (e.g. user.github.io/repo-name/) generate correct
asset URLs. Also fix leading-slash relURL calls in slides layout that
would ignore the baseURL path component.
Replace manual format detection (printf "%T", MediaType.SubType whitelists,
duck-typing) with Hugo's new reflect.IsResource, reflect.IsImageResource, and
reflect.IsImageResourceProcessable functions across 19 template files.
Key changes:
- process_responsive_image: collapse 15-line format whitelist into single
reflect.IsImageResourceProcessable call
- Unify three-way SVG/processable/GIF branches into two-way $canProcess guard
- Remove duplicate GIF passthrough branches in single.html, events/page.html
- Add missing SVG guard in research-areas block (was calling .Fill unprotected)
- Replace nil checks with reflect.IsResource in get_cover_image/get_featured_image
Design decision: GIFs are intentionally excluded from processing despite Hugo
supporting it — processing strips animation frames, so we preserve user-optimized
animated GIFs by checking (IsImageResourceProcessable AND not GIF).
Move block-specific CSS from centralized assets/css/blox/ to co-located
style.css files within each block directory. The css.html partial now
auto-discovers style.css files from bundled and community blocks and
concatenates them into the Tailwind entry via resources.Concat, enabling
full @apply support while avoiding VFS path resolution issues.
Shared biography styles (avatar, ruby) are extracted to blox/shared/css/
and auto-included before per-block styles. Navbar CSS moves to
components/ as it's a framework-level concern. The separate community
CSS pipeline in site_head.html is removed — all block CSS now flows
through a single Tailwind entry point.
Replace site.LanguageCode → site.Language.Locale and
.Language.LanguageCode → .Language.Locale in module layouts,
and languageCode → locale in all template language configs.
Enabled by new Hugo v0.153 release.
Replace the brittle `urlquery` + blanket `+` → `%20` workaround with
Hugo's `urls.PathEscape` for mailto URIs, which natively percent-encodes
spaces as `%20`. The old approach could corrupt titles containing
literal `+` characters.