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.
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
- 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
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.
All other display toggles use positive naming (show_*, toc, share, etc.).
Flipping to show_date (default: true) aligns the API and reduces cognitive load.
BREAKING CHANGE
Add a second resolution tier to the typography system: before falling
back to Google Fonts, check assets/dist/font/ for a matching local
file. When found, emit @font-face with local() fallback instead of a
CDN <link>. Variable fonts detected from ".var." filename convention.
Bundle JetBrains Mono alongside Inter so the default "modern" pack
loads entirely from local files — zero external font requests.
SVG resources don't support .Width, .Height, or .Process in Hugo,
causing crashes in process_responsive_image.html and all callers
that pass SVGs without guards.
Add SVG checks to process_responsive_image.html, preact-wrapper.html,
resume-biography, resume-biography-3, authors/term, and
page_author_card.
Fixes#3305
Hugo doesn't natively merge language-specific data directories with the default data. Add a `get_authors_data` partial that manually
looks up `data/<lang>/authors/` and deep-merges translated fields
on top of the default data/authors/ profiles.
Centralizes all `site.Data.authors` access through this new partial
so author names, bios, roles, and other fields can be translated
per language while inheriting non-translated fields from the default.
migrate deprecated `includeFiles` to `files` glob slice (Hugo >= 0.153.0)
Replace `module.mounts.includeFiles` with the new `files` setting
introduced in Hugo v0.153.0. Update minimum Hugo version accordingly.
Affected files:
- modules/blox/hugo.yaml
- All template module.yaml configs (7 templates)
BREAKING CHANGE
See: https://gohugo.io/configuration/module/#files
Add explicit type checking and proper trimming for status icon extraction
to match the more robust pattern used in resume-biography-3.
Fixes issue where users couldn't omit the emoji status badge.
Previous commits broke the 2-column grid layout by prematurely closing
the grid container after the image, leaving the text content outside
the grid. Now both image and text are properly wrapped within the same
md:grid md:grid-cols-2 container.
Fixes regression where image and paragraph appeared stacked vertically
instead of side-by-side on medium+ screens.
Add visual pronunciation guides (pinyin, furigana, IPA) displayed as ruby text annotations above the profile name in biography blocks.
- Add conditional ruby rendering in resume-biography and resume-biography-3
- Include browser fallback for non-ruby-supporting browsers
Usage: Set `name.pronunciation` in author YAML (e.g., "Wáng Wěi")
- Refactor build into reusable workflow (DRY)
- Add deploy.host config to control deployment target
- Upgrade to latest action versions (Node 22, Go 1.23, Python 3.13)
- Add timeout-minutes and concurrency groups for safety
- Use github.run_id for unique PR branch names
- Fix package_json_path for standalone repo compatibility
- Scope GITHUB_TOKEN to specific steps for security