feat(theme): multi-section bio + per-link enhancements

Introduces multi-section bio cards and link groups for content organization.
Link buttons now support dict-shape with image, featured, scheduled, rel,
and note attributes. Schema Person flattens sections for SEO. RSS and
single.html templates updated to use new link dict shape.

Enables linktree-style content structuring.
This commit is contained in:
tiennm99 committed 2026-05-15 19:50:49 +07:00
1 parent 740285fa17
commit 4d9107cbf7
6 files changed
+132 -22

No files matched your search

+10 -1
View File
@@ -16,7 +16,16 @@
<language>{{ site.Language.LanguageCode | default site.LanguageCode }}</language>
<lastBuildDate>{{ $now.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>
<atom:link href="{{ "index.xml" | absURL }}" rel="self" type="application/rss+xml" />
{{- range site.Params.links }}
{{- /* Flatten params.links + params.sections.*.links into a single list. */ -}}
{{- $allLinks := slice -}}
{{- if site.Params.sections -}}
{{- range site.Params.sections -}}
{{- range .links -}}{{- $allLinks = $allLinks | append . -}}{{- end -}}
{{- end -}}
{{- else -}}
{{- $allLinks = site.Params.links -}}
{{- end -}}
{{- range $allLinks }}
<item>
<title>{{ .title | transform.XMLEscape }}</title>
<link>{{ .url }}</link>
+36 -10
View File
@@ -2,11 +2,11 @@
{{- $tagline := site.Params.tagline -}}
{{- $bio := site.Params.bio -}}
{{- $links := site.Params.links -}}
{{- $sections := site.Params.sections -}}
{{- $layout := site.Params.layout | default "stack" -}}
{{- $validLayouts := slice "stack" "grid" "inline" -}}
{{- if not (in $validLayouts $layout) -}}
{{- warnf "bonsai: unknown params.layout %q (expected stack|grid|inline), falling back to 'stack'" $layout -}}
{{- $layout = "stack" -}}
{{- $defaultLabel := i18n "nav_links_label" | default "Links" -}}
{{- if and $sections $links -}}
{{- warnf "bonsai: both params.sections and params.links set; sections win, flat links ignored" -}}
{{- end -}}
<article class="bio">
@@ -22,11 +22,37 @@
<p class="bio__text">{{ . | markdownify }}</p>
{{- end }}
{{- if $links }}
<nav class="bio__links bio__links--{{ $layout }}" aria-label="{{ i18n "nav_links_label" | default "Links" }}">
{{- range $links }}
{{- partial "link-button.html" . -}}
{{- end }}
</nav>
{{- if $sections -}}
{{- range $i, $section := $sections -}}
{{- $secLinks := $section.links -}}
{{- if $secLinks -}}
{{- $secLayout := $section.layout | default $layout -}}
{{- $secTitle := $section.title -}}
{{- $slug := "" -}}
{{- with $secTitle }}{{ $slug = . | urlize }}{{ end -}}
{{- if not $slug }}{{ $slug = printf "section-%d" (add $i 1) }}{{ end -}}
<section class="bio__section"{{ with $secTitle }} aria-labelledby="section-{{ $slug }}"{{ else }} aria-label="{{ $defaultLabel }} {{ add $i 1 }}"{{ end }}>
{{- with $secTitle }}
<h2 id="section-{{ $slug }}" class="bio__section-title">{{ . }}</h2>
{{- end }}
{{- with $section.description }}
<p class="bio__section-desc">{{ . | markdownify }}</p>
{{- end }}
{{ partial "link-group.html" (dict "links" $secLinks "layout" $secLayout "label" (or $secTitle $defaultLabel)) }}
</section>
{{- end -}}
{{- end -}}
{{- else if $links -}}
{{ partial "link-group.html" (dict "links" $links "layout" $layout "label" $defaultLabel) }}
{{- end -}}
{{- if site.Params.share }}
{{ partial "share-button.html" . }}
{{- end }}
{{- if site.Params.vcard }}
<a class="bio__vcard" href="{{ `vcard.vcf` | relURL }}" download>{{ i18n "save_contact" | default "Save contact" }}</a>
{{- end }}
{{ partial "qr-block.html" . }}
</article>
@@ -1,12 +1,56 @@
{{- $title := .title -}}
{{- $url := .url -}}
{{- $icon := .icon -}}
{{- /* Render one link button.
Context dict: { "link": {title,url,icon,image,featured,startDate,endDate,rel,note,...}, "analytics": params.analytics }
Reads ONLY from its dict argument (per docs/code-standards.md partial composition rule). */ -}}
{{- $l := .link -}}
{{- $a := .analytics -}}
{{- /* Per-link visibility window: skip rendering when now < startDate or now > endDate.
Build-time evaluation only — re-evaluated at next site rebuild. */ -}}
{{- $now := now -}}
{{- $startOk := true -}}
{{- $endOk := true -}}
{{- with $l.startDate }}{{ $startOk = ge $now (time .) }}{{ end -}}
{{- with $l.endDate }}{{ $endOk = le $now (time .) }}{{ end -}}
{{- if and $startOk $endOk -}}
{{- $title := $l.title -}}
{{- $url := $l.url -}}
{{- $icon := $l.icon -}}
{{- $image := $l.image -}}
{{- $note := $l.note -}}
{{- $external := strings.HasPrefix $url "http" -}}
<a class="link"
{{- /* Class composition: .link + optional .link--featured */ -}}
{{- $classes := "link" -}}
{{- if $l.featured }}{{ $classes = printf "%s link--featured" $classes }}{{ end -}}
{{- /* rel value for external links: always start with safe defaults, then append user rel */ -}}
{{- $rel := "noopener noreferrer" -}}
{{- with $l.rel }}{{ $rel = printf "%s %s" $rel . }}{{ end -}}
{{- /* GA4 click-tracking attr: emit only when analytics enabled (caller passes the resolved dict). */ -}}
{{- $trackClicks := and $a $a.measurementId (ne ($a.trackClicks | default true) false) -}}
{{- /* UTM injection at build time for external links. Splits #fragment first so the
query string lands BEFORE the fragment (preserving link semantics). */ -}}
{{- if and $external $a $a.utmSource -}}
{{- $fragment := "" -}}
{{- if strings.Contains $url "#" -}}
{{- $bits := split $url "#" -}}
{{- $url = index $bits 0 -}}
{{- $fragment = printf "#%s" (index $bits 1) -}}
{{- end -}}
{{- $sep := cond (strings.Contains $url "?") "&" "?" -}}
{{- $utm := printf "utm_source=%s&utm_medium=%s" (urlquery $a.utmSource) (urlquery ($a.utmMedium | default "bio")) -}}
{{- with $a.utmCampaign }}{{ $utm = printf "%s&utm_campaign=%s" $utm (urlquery .) }}{{ end -}}
{{- $url = printf "%s%s%s%s" $url $sep $utm $fragment -}}
{{- end -}}
<a class="{{ $classes }}"
href="{{ $url }}"
{{- if $external }} target="_blank" rel="noopener noreferrer"{{ end }}>
{{- with $icon }}
<span class="link__icon" aria-hidden="true">{{ partial "icon.html" . }}</span>
{{- if $external }} target="_blank" rel="{{ $rel }}"{{ end -}}
{{- if $trackClicks }} data-analytics-event="link:{{ $l.title | urlize }}"{{ end }}>
{{- if $image -}}
<img class="link__thumb" src="{{ $image | strings.TrimPrefix "/" | relURL }}" alt="" width="24" height="24" loading="lazy" decoding="async" />
{{- else if $icon -}}
<span class="link__icon" aria-hidden="true">{{ partial "icon.html" $icon }}</span>
{{- end }}
<span class="link__title">{{ $title }}</span>
<span class="link__title">
{{- $title -}}
{{- with $note }}<span class="link__note">{{ . }}</span>{{- end -}}
</span>
</a>
{{- end -}}
@@ -0,0 +1,21 @@
{{- /* Render a navigation block of links.
Context dict: { "links": [...], "layout": "stack|grid|inline", "label": "..." }
Used both by the flat-list rendering and per-section rendering. */ -}}
{{- $links := .links -}}
{{- $layout := .layout | default "stack" -}}
{{- $label := .label | default (i18n "nav_links_label" | default "Links") -}}
{{- $validLayouts := slice "stack" "grid" "inline" -}}
{{- if not (in $validLayouts $layout) -}}
{{- warnf "bonsai: unknown layout %q (expected stack|grid|inline), falling back to 'stack'" $layout -}}
{{- $layout = "stack" -}}
{{- end -}}
{{- /* Resolve analytics once and pass into each link-button so the partial reads
only its dict argument (per docs/code-standards.md partial composition rule). */ -}}
{{- $analytics := site.Params.analytics -}}
{{- if $links -}}
<nav class="bio__links bio__links--{{ $layout }}" aria-label="{{ $label }}">
{{- range $links }}
{{- partial "link-button.html" (dict "link" . "analytics" $analytics) -}}
{{- end }}
</nav>
{{- end -}}
@@ -2,8 +2,17 @@
Suppressed when site.Params.schema = false. */ -}}
{{- if ne (site.Params.schema | default true) false -}}
{{- $name := site.Params.name | default site.Title -}}
{{- /* Flatten params.links + params.sections.*.links into a single list for sameAs. */ -}}
{{- $allLinks := slice -}}
{{- if site.Params.sections -}}
{{- range site.Params.sections -}}
{{- range .links -}}{{- $allLinks = $allLinks | append . -}}{{- end -}}
{{- end -}}
{{- else -}}
{{- $allLinks = site.Params.links -}}
{{- end -}}
{{- $sameAs := slice -}}
{{- range site.Params.links -}}
{{- range $allLinks -}}
{{- with .url -}}
{{- if not (or (strings.HasPrefix . "mailto:") (strings.HasPrefix . "tel:")) -}}
{{- $sameAs = $sameAs | append . -}}
+3 -2
View File
@@ -1,5 +1,6 @@
{{ define "head_extra" }}
<link rel="stylesheet" href="{{ `css/gallery.css` | relURL }}" />
{{- $g := resources.Get "css/gallery.css" | resources.Minify | resources.Fingerprint "sha384" -}}
<link rel="stylesheet" href="{{ $g.RelPermalink }}" integrity="{{ $g.Data.Integrity }}" crossorigin="anonymous" />
{{ end }}
{{ define "main" }}
@@ -31,7 +32,7 @@
<code class="variants-gallery__code">layout = "{{ .name }}"</code>
<nav class="bio__links bio__links--{{ .name }}" aria-label="{{ .label }} sample">
{{- range $sample }}
{{- partial "link-button.html" . -}}
{{- partial "link-button.html" (dict "link" . "analytics" site.Params.analytics) -}}
{{- end }}
</nav>
</article>