| 1 | {{- /* |
| 2 | Article visual orchestrator — guarantees every weekly article gets either a |
| 3 | meaningful generated visual or an intentional fallback. Lead approach is |
| 4 | locally-generated SVG; a safe local-image path is wired as a documented hook |
| 5 | for the image policy (#329). |
| 6 | |
| 7 | Selection order: |
| 8 | 1. visual = "none" -> intentional fallback card |
| 9 | 2. compliant LOCAL cover image -> processed, locally-hosted <img> (#329 hook) |
| 10 | 3. default -> generated SVG cover card |
| 11 | |
| 12 | SAFE COVER POLICY (#329): an image is only rendered when `cover.image` resolves |
| 13 | to a Hugo resource (page bundle or global asset) — i.e. it is downloaded and |
| 14 | locally hosted. External/hotlinked URLs are intentionally ignored here; do NOT |
| 15 | rely on fair use or reuse og:image. Attribution + registry handling land in #329. |
| 16 | */ -}} |
| 17 | {{- $page := . -}} |
| 18 | {{- $visual := $page.Params.visual -}} |
| 19 | {{- if eq (printf "%v" $visual) "none" -}} |
| 20 | {{ partial "visuals/fallback-card.html" (dict "page" $page) }} |
| 21 | {{- else -}} |
| 22 | {{- $img := "" -}} |
| 23 | {{- with $page.Params.cover -}} |
| 24 | {{- with .image -}} |
| 25 | {{- /* Only accept locally-hosted resources, matched EXACTLY (not as a |
| 26 | substring glob) so selection is deterministic and policy-safe. |
| 27 | External/hotlinked URLs never resolve to a resource and are ignored. */ -}} |
| 28 | {{- /* Escape glob metacharacters (*?[]\) so a cover.image value containing |
| 29 | them is matched literally by GetMatch rather than as a glob pattern. */ -}} |
| 30 | {{- $pattern := replaceRE `([*?\[\]\\])` `\${1}` . -}} |
| 31 | {{- $bundle := $page.Resources.GetMatch $pattern -}} |
| 32 | {{- $global := resources.Get . -}} |
| 33 | {{- $candidate := or $bundle $global -}} |
| 34 | {{- if and $candidate (eq $candidate.ResourceType "image") -}} |
| 35 | {{- $img = $candidate -}} |
| 36 | {{- end -}} |
| 37 | {{- end -}} |
| 38 | {{- end -}} |
| 39 | {{- if $img -}} |
| 40 | {{- $alt := ($page.Params.cover.alt | default $page.Title | plainify) -}} |
| 41 | {{- $attribution := $page.Params.cover.attribution -}} |
| 42 | <figure class="article-cover article-cover--image"> |
| 43 | {{- $card := $img.Fill "800x400 webp q82" -}} |
| 44 | {{- $card2x := $img.Fill "1600x800 webp q82" -}} |
| 45 | <img class="article-cover__img" |
| 46 | src="{{ $card.RelPermalink }}" |
| 47 | srcset="{{ $card.RelPermalink }} 800w, {{ $card2x.RelPermalink }} 1600w" |
| 48 | sizes="(min-width: 768px) 800px, 100vw" |
| 49 | width="{{ $card.Width }}" height="{{ $card.Height }}" |
| 50 | loading="eager" decoding="async" alt="{{ $alt }}"> |
| 51 | {{- /* Render attribution as a safe inline subset (#329): allow simple |
| 52 | emphasis/links from Markdown, but strip any <img> the markdown |
| 53 | image syntax would emit so hotlinked imagery cannot appear here. */ -}} |
| 54 | {{- with $attribution }}<figcaption class="article-cover__attribution">{{ . | markdownify | replaceRE `(?i)<img[^>]*>` "" | safeHTML }}</figcaption>{{ end }} |
| 55 | </figure> |
| 56 | {{- else -}} |
| 57 | {{ partial "visuals/cover-card.html" (dict "page" $page) }} |
| 58 | {{- end -}} |
| 59 | {{- end -}} |