feat(hero): Phase 3 — media slot and split-layout variants

- 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.
This commit is contained in:
George Cushen committed 2026-05-02 17:52:57 +01:00
1 parent f32f468cf3
commit 80913c028a
6 files changed
+260 -41

No files matched your search

@@ -33,6 +33,24 @@
{{ end }}
{{ end }}
{{/* Resolve hero media (single image, optional dark variant). Videos pass through unprocessed. */}}
{{ with $block.content.media }}
{{ if or (not .type) (eq .type "image") }}
{{ with .src }}
{{ $img_data := partial "functions/process_image_for_props" (dict "path" . "Page" $page) }}
{{ with $img_data.src }}
{{ $props = merge $props (dict "media_image" $img_data) }}
{{ end }}
{{ end }}
{{ with .dark_src }}
{{ $img_data := partial "functions/process_image_for_props" (dict "path" . "Page" $page) }}
{{ with $img_data.src }}
{{ $props = merge $props (dict "media_image_dark" $img_data) }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{/* Resolve icon for single button (cta-card) */}}
{{ with $block.content.button.icon }}
{{ $icon_data := partial "functions/get_icon_data" (dict "name" .) }}
@@ -0,0 +1,45 @@
{{/*
process_image_for_props — One-shot helper: resolves an image path and returns a
{src, srcset?, width?, height?} dict ready to merge into a Preact block's props.
SVGs and GIFs pass through unprocessed (preserves vectors / animation frames).
Inputs:
.path: image path (required)
.Page: current page (recommended for page-resource lookup)
.sizes: slice of widths for srcset generation
(optional, defaults to [600, 900, 1200, 1800])
Returns: image data dict {src, srcset?, width?, height?}, or empty dict if the
path is missing or unresolvable.
*/}}
{{ $result := dict }}
{{ $path := .path | default "" }}
{{ if $path }}
{{ $sizes := .sizes | default (slice 600 900 1200 1800) }}
{{ $resolved := partial "functions/resolve_image_resource" (dict "path" $path "Page" .Page) }}
{{ with $resolved.resource }}
{{ $canProcess := and (reflect.IsImageResourceProcessable .) (ne .MediaType.SubType "gif") }}
{{ if not $canProcess }}
{{ $result = dict "src" .RelPermalink }}
{{ else }}
{{ $responsive := partial "functions/process_responsive_image.html" (dict
"image" .
"mode" "fit"
"sizes" $sizes
) }}
{{ $result = dict
"src" $responsive.fallback.RelPermalink
"srcset" $responsive.srcset
"width" $responsive.fallback.Width
"height" $responsive.fallback.Height
}}
{{ end }}
{{ end }}
{{ with $resolved.fallback_url }}
{{ $result = dict "src" . }}
{{ end }}
{{ end }}
{{ return $result }}
@@ -0,0 +1,54 @@
{{/*
resolve_image_resource — Resolves an image path string to either a Hugo image
resource or a fallback URL. Handles remote URLs, page resources, and assets/media
lookups using the same rules as the inline per-item image resolver.
Inputs:
.path: image path (required) — relative ("dashboard.png", "media/foo.jpg")
or absolute URL ("https://…").
.Page: current page (optional, recommended for page-resource lookup).
Returns dict with one of:
.resource: Hugo image resource (use with .Process, srcset, etc.)
.fallback_url: plain URL string (use as <img src>)
Or an empty dict if path is missing.
*/}}
{{ $result := dict }}
{{ $path := strings.TrimSpace (printf "%v" (.path | default "")) }}
{{ if $path }}
{{ $is_remote := or (strings.HasPrefix $path "http://") (strings.HasPrefix $path "https://") }}
{{ if $is_remote }}
{{ $remote := try (resources.GetRemote $path) }}
{{ if and $remote (not $remote.Err) }}
{{ $result = dict "resource" $remote.Value }}
{{ else }}
{{ $result = dict "fallback_url" $path }}
{{ end }}
{{ else }}
{{ $normalized := $path }}
{{ $normalized = strings.TrimPrefix "/" $normalized }}
{{ $normalized = strings.TrimPrefix "./" $normalized }}
{{ $normalized = strings.TrimPrefix "assets/" $normalized }}
{{ $normalized = strings.TrimPrefix "media/" $normalized }}
{{ $normalized = path.Clean $normalized }}
{{ if and $normalized (ne $normalized ".") }}
{{ $resource := false }}
{{ with .Page }}
{{ $resource = (.Resources.ByType "image").GetMatch $normalized }}
{{ end }}
{{ if not $resource }}
{{ $resource = resources.Get (path.Join "media" $normalized) }}
{{ end }}
{{ if $resource }}
{{ $result = dict "resource" $resource }}
{{ else }}
{{ $result = dict "fallback_url" (printf "/media/%s" $normalized) }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ return $result }}