mirror of
https://github.com/tiennm99/HugoBlox-kit.git
synced 2026-10-13 04:14:41 +00:00
feat(gallery): sidecar files, EXIF/IPTC, and page-bundle auto-discovery
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[].
This commit is contained in:
1 parent
b67f09b8fd
commit
bdd3820f27
3 files changed
+120
-19
No files matched your search
@@ -1,21 +1,27 @@
|
||||
{{/*
|
||||
resolve_gallery_items — Resolves gallery block items into a list of fully-processed
|
||||
image props ready for the Preact component. Handles two source modes:
|
||||
image props ready for the Preact component. Three source modes:
|
||||
|
||||
1. content.items[] — explicit per-image dicts with optional caption/alt/credit/link
|
||||
2. content.album — folder path relative to assets/, e.g. "media/albums/gallery"
|
||||
1. content.items[] — explicit per-image dicts (curated)
|
||||
2. content.album — folder under assets/, e.g. "media/albums/gallery"
|
||||
3. content.page_resources — auto-discover image resources in the current page bundle
|
||||
|
||||
For each resolved image, produces a dict with:
|
||||
{src, srcset, width, height, aspect_ratio, lqip, alt, caption, credit, link, title}
|
||||
|
||||
Slice 1 caption resolution: frontmatter only (items[].caption). Sidecar + EXIF/IPTC
|
||||
resolution is intentionally deferred to Slice 2 — for now, missing captions stay
|
||||
empty in the UI, with the humanised filename surfaced as the `alt` fallback so
|
||||
screen readers and search engines have something useful.
|
||||
Metadata waterfall — first non-empty value per field wins:
|
||||
1. Frontmatter (items[].caption etc.)
|
||||
2. Sidecar file next to the image (.yaml / .yml / .json)
|
||||
— tried as "<full-name>.yaml" then "<basename-without-ext>.yaml"
|
||||
— looked up in both site assets and the current page bundle
|
||||
3. Embedded IPTC/EXIF via Hugo's .Exif.Tags
|
||||
— caption from Caption-Abstract / ImageDescription / XPSubject / Description
|
||||
— credit from By-line / Artist / Creator
|
||||
4. Humanised filename (alt only — captions stay empty when no metadata exists)
|
||||
|
||||
Inputs:
|
||||
.content: the block's content dict (items / album / sort / limit)
|
||||
.Page: current page (for page-resource image lookup)
|
||||
.content: the block's content dict
|
||||
.Page: current page (used for page-resource lookup + RenderString markdown)
|
||||
|
||||
Returns: dict { items: [...] } or empty if nothing resolves.
|
||||
*/}}
|
||||
@@ -27,7 +33,7 @@
|
||||
{{ $sort_order := lower ($content.sort | default "asc") }}
|
||||
{{ $limit := $content.limit | default 0 }}
|
||||
|
||||
{{/* Build a unified list of {resource?, fallback_url?, meta} entries from either source mode */}}
|
||||
{{/* Build a unified list of {resource?, fallback_url?, meta} entries */}}
|
||||
{{ $candidates := slice }}
|
||||
|
||||
{{/* Source A — explicit items */}}
|
||||
@@ -61,6 +67,23 @@
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Source C — page bundle auto-discovery */}}
|
||||
{{ if $content.page_resources }}
|
||||
{{ with $page }}
|
||||
{{ $imgs := .Resources.ByType "image" }}
|
||||
{{ $sorted := sort $imgs "Name" $sort_order }}
|
||||
{{ range $sorted }}
|
||||
{{ if in $allowed_exts (lower (path.Ext .Name)) }}
|
||||
{{ $candidates = $candidates | append (dict
|
||||
"resource" .
|
||||
"fallback_url" ""
|
||||
"meta" dict
|
||||
) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Apply limit */}}
|
||||
{{ if gt $limit 0 }}
|
||||
{{ $candidates = first $limit $candidates }}
|
||||
@@ -73,17 +96,88 @@
|
||||
{{ $resource := .resource }}
|
||||
{{ $fallback_url := .fallback_url }}
|
||||
|
||||
{{/* Per-item text fields. Caption / title / credit are rendered through
|
||||
Hugo's markdown pipeline at build time (inline mode — no wrapping <p>)
|
||||
so users can put **bold**, *italic*, `code`, and [links](url) in any of
|
||||
them without shipping a JS markdown parser. Alt stays as plain text
|
||||
since it lives inside an HTML attribute. */}}
|
||||
{{/* ---- Metadata waterfall ---- */}}
|
||||
{{ $alt := index $meta "alt" | default "" }}
|
||||
{{ $caption_raw := index $meta "caption" | default "" }}
|
||||
{{ $title_raw := index $meta "title" | default "" }}
|
||||
{{ $credit_raw := index $meta "credit" | default "" }}
|
||||
{{ $link := index $meta "link" | default "" }}
|
||||
|
||||
{{/* Tier 2 — sidecar file lookup. Tries:
|
||||
<name>.yaml/.yml/.json (full filename, Lightroom-style convention)
|
||||
<basename>.yaml/.yml/.json (filename minus extension)
|
||||
in both site assets and the page bundle. */}}
|
||||
{{ $sidecar := false }}
|
||||
{{ if $resource }}
|
||||
{{/* Resource.Name comes back with a leading slash for site assets (e.g.
|
||||
"/media/albums/gallery/01.jpg"); strip it so resources.Get treats the
|
||||
path as relative to assets/. Page-bundle resource names have no
|
||||
leading slash, so TrimPrefix is a no-op for those. */}}
|
||||
{{ $name := strings.TrimPrefix "/" $resource.Name }}
|
||||
{{ $basename := strings.TrimSuffix (path.Ext $name) $name }}
|
||||
{{ $patterns := slice
|
||||
(printf "%s.yaml" $name)
|
||||
(printf "%s.yml" $name)
|
||||
(printf "%s.json" $name)
|
||||
(printf "%s.yaml" $basename)
|
||||
(printf "%s.yml" $basename)
|
||||
(printf "%s.json" $basename)
|
||||
}}
|
||||
{{ range $pattern := $patterns }}
|
||||
{{ if not $sidecar }}
|
||||
{{ $candidate := resources.Get $pattern }}
|
||||
{{ if not $candidate }}{{ $candidate = resources.GetMatch $pattern }}{{ end }}
|
||||
{{ if and (not $candidate) $page }}
|
||||
{{ $candidate = $page.Resources.GetMatch $pattern }}
|
||||
{{ end }}
|
||||
{{ if $candidate }}
|
||||
{{ $sidecar = $candidate }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if $sidecar }}
|
||||
{{ $data := transform.Unmarshal $sidecar }}
|
||||
{{ if reflect.IsMap $data }}
|
||||
{{ if not $caption_raw }}{{ $caption_raw = index $data "caption" | default "" }}{{ end }}
|
||||
{{ if not $title_raw }}{{ $title_raw = index $data "title" | default "" }}{{ end }}
|
||||
{{ if not $credit_raw }}{{ $credit_raw = index $data "credit" | default "" }}{{ end }}
|
||||
{{ if not $alt }}{{ $alt = index $data "alt" | default "" }}{{ end }}
|
||||
{{ if not $link }}{{ $link = index $data "link" | default "" }}{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Tier 3 — embedded IPTC/EXIF. Lightroom/Capture One JPEG exports carry
|
||||
Caption-Abstract + By-line + Copyright; this picks them up automatically. */}}
|
||||
{{ $exif_tags := dict }}
|
||||
{{ if $resource }}
|
||||
{{ $exif_try := try $resource.Exif }}
|
||||
{{ if and (not $exif_try.Err) $exif_try.Value }}
|
||||
{{ $exif_tags = $exif_try.Value.Tags | default dict }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if $exif_tags }}
|
||||
{{ if not $caption_raw }}
|
||||
{{ range $key := (slice "Caption-Abstract" "ImageDescription" "XPSubject" "Description") }}
|
||||
{{ if not $caption_raw }}
|
||||
{{ $v := index $exif_tags $key }}
|
||||
{{ if $v }}{{ $caption_raw = printf "%v" $v }}{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if not $credit_raw }}
|
||||
{{ range $key := (slice "By-line" "Artist" "Creator") }}
|
||||
{{ if not $credit_raw }}
|
||||
{{ $v := index $exif_tags $key }}
|
||||
{{ if $v }}{{ $credit_raw = printf "%v" $v }}{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* ---- Inline-Markdown render of text fields (RenderString display:inline) ---- */}}
|
||||
{{ $caption := $caption_raw }}
|
||||
{{ if and $caption_raw $page }}
|
||||
{{ $caption = $page.RenderString (dict "display" "inline") $caption_raw }}
|
||||
@@ -135,10 +229,8 @@
|
||||
) }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Alt fallback: plainified caption → humanised filename. Caption itself
|
||||
is NOT filled from filename — empty captions stay empty in the UI
|
||||
(Slice 1 behaviour). Plainify strips the HTML produced by RenderString
|
||||
so alt stays attribute-safe. */}}
|
||||
{{/* Tier 4 — alt fallback: plainified caption → humanised filename. Caption
|
||||
itself stays empty when no metadata source has supplied one. */}}
|
||||
{{ if not $entry.alt }}
|
||||
{{ $fallback_alt := "" }}
|
||||
{{ with $caption_raw }}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
title: "The cabin"
|
||||
caption: "*Winter quarters* — where the team writes up over the slow months"
|
||||
credit: "Photo: [Picsum](https://picsum.photos)"
|
||||
alt: "Snow-dusted wooden cabin among pine trees"
|
||||
@@ -0,0 +1,5 @@
|
||||
title: "Summit"
|
||||
caption: "Final ridge of the **Highland traverse**, with the trig point in view"
|
||||
credit: "Photo: [Picsum](https://picsum.photos)"
|
||||
link: "/expeditions/highland-traverse/"
|
||||
keywords: [highland, traverse, summit]
|
||||
Reference in new issue
Block a user