feat: standardize author profiles & add devcontainer

This commit is contained in:
George Cushen committed 2025-12-07 01:50:48 +00:00
1 parent 75234c306b
commit f0eb4a7e9c
80 files changed
+1327 -1019

No files matched your search

@@ -46,7 +46,7 @@
overflow: hidden;
width: 100%;
height: 10px;
border: 1px solid #a8e9e9;
border: 1px solid var(--color-primary-200, #cbd5f5);
border-radius: 10px;
}
@@ -54,5 +54,5 @@
position: absolute;
width: 60%;
height: 10px;
background-color: #3cb5b5;
background-color: var(--color-primary-600, #2563eb);
}
@@ -7,7 +7,7 @@ Showcase your professional accomplishments, certifications, and awards with the
## ✨ Key Features
- **Professional Cards**: Clean, shadowed card design for each award or certification
- **Icon Integration**: Brand icons for certifying organizations and institutions
- **Icon Integration**: Add per-award icons (e.g., trophy, badge) for instant visual cues
- **Certificate Links**: Direct links to verify credentials with proper external link handling
- **Date Formatting**: Customizable date display (month/year format by default)
- **Markdown Support**: Rich text formatting for award titles and descriptions
@@ -55,4 +55,3 @@ Showcase your professional accomplishments, certifications, and awards with the
- **Brand Integration**: Certifying organization icons add visual interest and authenticity
Perfect for professionals, academics, consultants, and anyone who wants to showcase their achievements in a format that builds confidence and demonstrates expertise. Your accomplishments deserve professional presentation.
@@ -5,14 +5,9 @@
{{/* Initialise */}}
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $author := $block.content.username | default "admin" }}
{{ $person_page_path := (printf "/authors/%s" $author) }}
{{ $person_page := site.GetPage $person_page_path }}
{{ if not $person_page }}
{{ errorf "Could not find an author page at `%s`. Please check the value of `author` in your Skill block and create an associated author page if one does not already exist. See https://docs.hugoblox.com/page-builder/#about " $person_page_path }}
{{end}}
{{ $awards := $person_page.Params.awards }}
{{ $author := $block.content.username | default "me" }}
{{ $profile := partial "functions/get_author_profile" $author }}
{{ $awards := $profile.awards }}
<div class="flex flex-col items-center max-w-prose mx-auto gap-3 justify-center">
@@ -24,25 +19,29 @@
<div class="w-full flex flex-col gap-6">
{{ if $awards }}
{{ range $idx, $key := sort $awards ".date_start" "desc" }}
{{ range $idx, $key := sort $awards ".date" "desc" }}
<div class="w-full p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
{{- $icon := .icon }}
{{ with $icon }}
<div class="w-7 h-7 text-gray-500 dark:text-gray-400 mb-3">
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "style=\"height: 100%;\"") }}
</div>
{{ end}}
{{with .url}}<a href={{.}} target="_blank" rel="noopener">{{end}}
<h5 class="mb-2 text-2xl font-semibold tracking-tight text-gray-900 dark:text-white">{{.title | markdownify | emojify}}</h5>
<h5 class="mb-2 text-2xl font-semibold tracking-tight text-gray-900 dark:text-white flex items-center gap-2">
{{ with $icon }}
<span class="inline-flex items-center justify-center w-7 h-7 text-primary-600 dark:text-primary-300">
{{ partial "functions/get_icon" (dict "name" . "attributes" "class='w-6 h-6'") }}
</span>
{{ end }}
<span>{{.title | markdownify | emojify}}</span>
</h5>
{{with .url}}</a>{{end}}
<div class="block mb-3 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">
{{.awarder}} &#8729;
{{ if .date}}
{{ .awarder }} &#8729;
{{ if .date }}
{{ (time .date) | time.Format ($block.design.date_format | default "January 2006") }}
{{else}}
{{ else if .date_start }}
{{ (time .date_start) | time.Format ($block.design.date_format | default "January 2006") }}
{{ else }}
{{ i18n "present" | default "Present" }}
{{end}}
{{ end }}
</div>
{{with .summary}}
@@ -6,6 +6,11 @@ Present a complete professional picture with the Resume Biography 3 block - an e
## 🛠️ Customization
- **Name sizing**: Adjust the profile heading size to accommodate long names using `design.name.size`:
- `compact`: space-friendly for long names
- `balanced` (default): matches the demo style
- `display`: extra-large for short names
- **Section heading overrides**: Customize any of the three main section headings using `content.headings`:
- `about`: Biography section heading (default: "Professional Summary" from i18n `about_me`)
- `education`: Education section heading (default: from i18n `education`)
@@ -20,6 +25,9 @@ content:
about: Research Profile
education: Academic Background
interests: Research Areas
design:
name:
size: compact
```
## ✨ Key Features
@@ -6,20 +6,23 @@
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $author := $block.content.username | default "admin" }}
{{ $person_page_path := (printf "/authors/%s" $author) }}
{{ $person_page := site.GetPage $person_page_path }}
{{ if not $person_page }}
{{ errorf "Could not find an author page at `%s`. Please check the value of `author` in your About widget and create an associated author page if one does not already exist. See https://docs.hugoblox.com/page-builder/#about " $person_page_path }}
{{end}}
{{ $person := $person_page.Params }}
{{ $avatar := ($person_page.Resources.ByType "image").GetMatch "*avatar*" }}
{{ $author := $block.content.username | default "me" }}
{{ $profile := partial "functions/get_author_profile" $author }}
{{ $avatar := $profile.avatar }}
{{/* Avatar customization parameters */}}
{{ $avatar_size := $block.design.avatar.size | default "large" }}
{{ $avatar_shape := $block.design.avatar.shape | default "circle" }}
{{/* Profile name typography */}}
{{ $name_size := $block.design.name.size | default "balanced" }}
{{ $name_class_map := dict
"compact" "text-3xl sm:text-4xl lg:text-5xl"
"balanced" "text-4xl sm:text-5xl lg:text-6xl"
"display" "text-5xl sm:text-6xl lg:text-7xl"
}}
{{ $name_class := index $name_class_map $name_size | default (index $name_class_map "balanced") }}
{{/* Size mappings optimized for 2025 standards: display_size -> [display_px, generation_px] */}}
{{ $size_map := dict "small" (slice "150" "300") "medium" (slice "200" "400") "large" (slice "320" "640") "xl" (slice "400" "800") "xxl" (slice "500" "1000") }}
{{ $size_config := index $size_map $avatar_size | default (index $size_map "large") }}
@@ -68,40 +71,33 @@
{{/* Avatar */}}
{{ if $avatar }}
<div class="avatar-wrapper {{ if $img }}-mt-24{{else}}mb-4{{end}}" style="width: {{$display_size}}px; height: {{$display_size}}px;">
<div class="avatar-wrapper {{ if $img }}-mt-24{{else}}mb-4{{end}}">
{{ $avatar_image := $avatar.Fill (printf "%sx%s Center" $generation_size $generation_size) }}
<img class="avatar {{$shape_class}} border-4 border-white shadow-2xl"
src="{{ $avatar_image.RelPermalink }}"
alt="{{$person_page.Title}}"
alt="{{$profile.title}}"
width="{{$display_size}}"
height="{{$display_size}}"
style="width: {{$display_size}}px; height: {{$display_size}}px; object-fit: cover;">
{{with $person.status.icon}}<span class="avatar-emoji">{{.|emojify}}</span>{{end}}
style="object-fit: cover;">
{{with $profile.status.icon}}<span class="avatar-emoji">{{.|emojify}}</span>{{end}}
</div>
{{ end }}
{{/* Name and Role - Centered */}}
<div class="space-y-3">
<h1 class="text-5xl lg:text-6xl font-black text-gray-900 dark:text-white leading-tight">
{{- if $person.name_pronunciation -}}
<ruby>
<rb>{{ $person_page.Title }}</rb>
<rt class="text-sm text-gray-500 dark:text-gray-400">{{ $person.name_pronunciation }}</rt>
</ruby>
{{- else -}}
{{- $person_page.Title -}}
{{- end -}}
<h1 class="{{ $name_class }} font-black text-gray-900 dark:text-white leading-tight">
{{ $profile.title }}
</h1>
{{- with $person.pronouns -}}
{{- with $profile.pronouns -}}
<p class="text-lg text-gray-600 dark:text-gray-400">({{ . }})</p>
{{- end -}}
{{ with $person.role }}
{{ with $profile.role }}
<p class="text-2xl font-semibold text-primary-600 dark:text-primary-400">{{ . | markdownify | emojify }}</p>
{{ end }}
{{ range $person.organizations }}
{{ range $profile.affiliations }}
<p class="text-lg text-gray-700 dark:text-gray-300">
{{ with .url }}<a href="{{ . }}" target="_blank" rel="noopener" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">{{ end }}
{{ .name }}
@@ -112,12 +108,7 @@
{{/* Social Icons */}}
<div class="flex flex-wrap justify-center gap-4">
{{ range $person.profiles }}
{{ $pack := or .icon_pack "fas" }}
{{ $pack_prefix := $pack }}
{{ if in (slice "fab" "fas" "far" "fal") $pack }}
{{ $pack_prefix = "fa" }}
{{ end }}
{{ range $profile.links }}
{{ $link := .url | default .link }}
{{ $scheme := (urls.Parse $link).Scheme }}
{{ $target := "" }}
@@ -143,7 +134,8 @@
<div class="md:col-span-8">
{{/* Bio Section */}}
{{ with ($block.content.text | emojify | $page.RenderString) | default $person_page.Content }}
{{ $bio_content := ($block.content.text | emojify | $page.RenderString) | default $profile.bio }}
{{ with $bio_content }}
<div class="mb-12">
<div class="flex items-center gap-4 mb-8">
<div class="flex-shrink-0 w-12 h-12 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
@@ -174,7 +166,7 @@
{{ end }}
{{/* Education Section */}}
{{ with $person.education }}
{{ with $profile.education }}
<div class="mb-16">
<div class="flex items-center gap-4 mb-8">
<div class="flex-shrink-0 w-12 h-12 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
@@ -195,9 +187,12 @@
{{end}}
</div>
<div class="flex-1 flex-grow">
{{ $degree := .degree | default .area }}
{{ $year := .year | default (cond (and .start .end) (printf "%s - %s" .start .end) .start) }}
<p class="text-xl font-bold text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
{{ .area }}{{ with .year }}, {{ . }}{{ end }}
{{ $degree }}
</p>
{{ with $year }}<p class="text-gray-600 dark:text-gray-400 font-medium mt-1">{{ . }}</p>{{ end }}
<p class="text-gray-600 dark:text-gray-400 font-medium mt-1">{{ .institution }}</p>
</div>
</div>
@@ -208,7 +203,7 @@
{{ end }}
{{/* Interests Section */}}
{{ with $person.interests }}
{{ with $profile.interests }}
<div>
<div class="flex items-center gap-4 mb-8">
<div class="flex-shrink-0 w-12 h-12 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
@@ -232,4 +227,4 @@
</div>
</div>
</div>
</div>
</div>
@@ -106,6 +106,18 @@
"description": "Custom CSS styles for biography text"
}
}
},
"name": {
"type": "object",
"description": "Profile name typography controls",
"properties": {
"size": {
"type": "string",
"enum": ["compact", "balanced", "display"],
"description": "Heading size for the profile name to suit different name lengths",
"default": "balanced"
}
}
}
}
}
@@ -6,15 +6,9 @@
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $author := $block.content.username | default "admin" }}
{{ $person_page_path := (printf "/authors/%s" $author) }}
{{ $person_page := site.GetPage $person_page_path }}
{{ if not $person_page }}
{{ errorf "Could not find an author page at `%s`. Please check the value of `author` in your About widget and create an associated author page if one does not already exist. See https://docs.hugoblox.com/page-builder/#about " $person_page_path }}
{{end}}
{{ $person := $person_page.Params }}
{{ $avatar := ($person_page.Resources.ByType "image").GetMatch "*avatar*" }}
{{ $author := $block.content.username | default "me" }}
{{ $profile := partial "functions/get_author_profile" $author }}
{{ $avatar := $profile.avatar }}
{{/* Avatar customization parameters */}}
{{ $avatar_size := $block.design.avatar.size | default "medium" }}
@@ -76,30 +70,23 @@
<div class="avatar-wrapper {{ if $img }}-mt-[105px]{{else}}mt-10{{end}}" style="width: {{$display_size}}px; height: {{$display_size}}px;">
{{ $avatar_image := $avatar.Fill (printf "%sx%s Center" $generation_size $generation_size) }}
<img class="avatar {{$shape_class}} bg-white dark:bg-gray-800 p-1" src="{{ $avatar_image.RelPermalink }}" alt="{{$person_page.Title}}"
<img class="avatar {{$shape_class}} bg-white dark:bg-gray-800 p-1" src="{{ $avatar_image.RelPermalink }}" alt="{{$profile.title}}"
width="{{$display_size}}" height="{{$display_size}}"
style="width: {{$display_size}}px; height: {{$display_size}}px; object-fit: cover;">
{{with $person.status.icon}}<span class="avatar-emoji">{{.|emojify}}</span>{{end}}
{{with $profile.status.icon}}<span class="avatar-emoji">{{.|emojify}}</span>{{end}}
</div>
{{ end }}
<div class="text-3xl font-bold mb-2 mt-6">
{{- if $person.name_pronunciation -}}
<ruby>
<rb>{{ $person_page.Title }}</rb>
<rt>{{ $person.name_pronunciation }}</rt>
</ruby>
{{- else -}}
{{- $person_page.Title -}}
{{- end -}}
{{- $profile.title -}}
</div>
{{- with $person.pronouns -}}
{{- with $profile.pronouns -}}
<div class="text-lg font-normal text-gray-600 dark:text-gray-400 -mt-1 mb-2">({{ . }})</div>
{{- end -}}
{{ with $person.role }}<h3 class="font-semibold mb-1">{{ . | markdownify | emojify }}</h3>{{ end }}
{{ with $profile.role }}<h3 class="font-semibold mb-1">{{ . | markdownify | emojify }}</h3>{{ end }}
{{ range $person.organizations }}
{{ range $profile.affiliations }}
<div class="mb-2">
{{ with .url }}<a href="{{ . }}" target="_blank" rel="noopener">{{ end }}
<div>{{ .name }}</div>
@@ -107,13 +94,9 @@
</div>
{{ end }}
{{ if $profile.links }}
<ul class="network-icon dark:text-zinc-100">
{{ range $person.profiles }}
{{ $pack := or .icon_pack "fas" }}
{{ $pack_prefix := $pack }}
{{ if in (slice "fab" "fas" "far" "fal") $pack }}
{{ $pack_prefix = "fa" }}
{{ end }}
{{ range $profile.links }}
{{ $link := .url | default .link }}
{{ $scheme := (urls.Parse $link).Scheme }}
{{ $target := "" }}
@@ -131,8 +114,10 @@
</li>
{{ end }}
</ul>
{{ end }}
{{ with ($block.content.text | emojify | $page.RenderString) | default $person_page.Content }}
{{ $bio_content := ($block.content.text | emojify | $page.RenderString) | default $profile.bio }}
{{ with $bio_content }}
<div class="pt-2 d-flex justify-content-center prose prose-slate lg:prose-xl dark:prose-invert">
<div class="bio-text" {{ with $block.design.biography.style }}{{ (printf "style=\"%s\"" .) | safe.HTMLAttr }}{{end}}>
{{ . }}
@@ -7,13 +7,8 @@
{{ $block := .wcBlock }}
{{ $text := $block.content.text | emojify | $page.RenderString }}
{{ $author := $block.content.username | default "admin" }}
{{ $person_page_path := (printf "/authors/%s" $author) }}
{{ $person_page := site.GetPage $person_page_path }}
{{ if not $person_page }}
{{ errorf "Could not find an author page at `%s`. Please check the value of `author` in your About widget and create an associated author page if one does not already exist. See https://docs.hugoblox.com/page-builder/#about " $person_page_path }}
{{end}}
{{ $person := $person_page.Params }}
{{ $author := $block.content.username | default "me" }}
{{ $profile := partial "functions/get_author_profile" $author }}
{{ $is_education_first := $block.design.is_education_first | default false }}
<div class="flex flex-col items-center mx-auto w-full max-w-prose justify-center">
@@ -26,11 +21,12 @@
<!-- Need Gap >= 3 when flex-row due to negative margin on cols (-start-3) -->
<div class="flex flex-col lg:gap-x-6 w-full px-6 sm:px-0 {{ if $is_education_first }}flex-col-reverse{{end}}">
{{ if $person.work }}
{{ $experience := or $profile.experience $profile.work }}
{{ if $experience }}
<div class="w-full">
<h3 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white text-center">{{ i18n "experience" | default "Experience" }}</h3>
<ol class="relative w-full border-s border-gray-200 dark:border-gray-700">
{{ range $idx, $key := $person.work }}
{{ range $idx, $key := $experience }}
<li class="mb-10 ms-6">
<span class="absolute flex items-center justify-center w-6 h-6 bg-primary-100 rounded-full -start-3 ring-8 ring-white dark:ring-gray-900 dark:bg-primary-900">
{{ if .icon }}
@@ -39,27 +35,31 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="w-5 h-5 text-primary-800 dark:text-primary-300"><path d="M216,56H176V48a24,24,0,0,0-24-24H104A24,24,0,0,0,80,48v8H40A16,16,0,0,0,24,72V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V72A16,16,0,0,0,216,56ZM96,48a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96ZM216,72v41.61A184,184,0,0,1,128,136a184.07,184.07,0,0,1-88-22.38V72Zm0,128H40V131.64A200.19,200.19,0,0,0,128,152a200.25,200.25,0,0,0,88-20.37V200ZM104,112a8,8,0,0,1,8-8h32a8,8,0,0,1,0,16H112A8,8,0,0,1,104,112Z"></path></svg>
{{end}}
</span>
<h3 class="flex items-center mb-1 text-lg font-semibold text-gray-900 dark:text-white">{{.position}}
<h3 class="flex items-center mb-1 text-lg font-semibold text-gray-900 dark:text-white">{{ .role | default .position }}
<!-- <span class="bg-blue-100 text-blue-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300 ms-3">Latest</span>-->
</h3>
<span class="block mb-2 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">{{.company_name}}</span>
<span class="block mb-2 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">{{ .org | default .company_name }}</span>
<time class="block mb-3 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">
{{ if .date_start}}
{{ (time .date_start) | time.Format ($block.design.date_format | default "January 2006") }} –
{{ $start := or .start .date_start }}
{{ $end := or .end .date_end }}
{{ if $start }}
{{ (time $start) | time.Format ($block.design.date_format | default "January 2006") }} –
{{ end }}
{{ if .date_end}}
{{ (time .date_end) | time.Format ($block.design.date_format | default "January 2006") }}
{{else}}
{{ if $end }}
{{ (time $end) | time.Format ($block.design.date_format | default "January 2006") }}
{{ else }}
{{ i18n "present" | default "Present" }}
{{end}}
</time>
<div class="mb-4 text-base font-normal text-gray-500 dark:text-gray-300 prose max-w-none prose-slate dark:prose-invert">{{.summary | $page.RenderString}}</div>
{{ with .button }}
<a href="{{.url}}" target="_blank" rel="noopener" class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg transition-colors hover:bg-gray-100 hover:text-gray-900 focus:z-10 focus:ring-4 focus:outline-none focus:ring-gray-200 focus:text-gray-900 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:text-white dark:focus:ring-gray-700"><svg class="w-3.5 h-3.5 me-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M14.707 7.793a1 1 0 0 0-1.414 0L11 10.086V1.5a1 1 0 0 0-2 0v8.586L6.707 7.793a1 1 0 1 0-1.414 1.414l4 4a1 1 0 0 0 1.416 0l4-4a1 1 0 0 0-.002-1.414Z"/>
<path d="M18 12h-2.55l-2.975 2.975a3.5 3.5 0 0 1-4.95 0L4.55 12H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2Zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/>
</svg> {{.text}}</a>
<a href="{{.url}}" target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg transition-colors hover:bg-gray-100 hover:text-gray-900 focus:z-10 focus:ring-4 focus:outline-none focus:ring-gray-200 focus:text-gray-900 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:text-white dark:focus:ring-gray-700">
{{ with .icon }}
{{ partial "functions/get_icon" (dict "name" . "attributes" "class='w-4 h-4'") }}
{{ end }}
{{ .text | default (i18n "learn_more" | default "Learn more") }}
</a>
{{ end }}
</li>
@@ -68,11 +68,11 @@
</div>
{{ end }}
{{ if $person.education }}
{{ if $profile.education }}
<div class="w-full">
<h3 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white text-center">{{ i18n "education" }}</h3>
<ol class="relative w-full border-s border-gray-200 dark:border-gray-700">
{{ range $idx, $key := $person.education }}
{{ range $idx, $key := $profile.education }}
<li class="mb-10 ms-6">
<span class="absolute flex items-center justify-center w-6 h-6 bg-primary-100 rounded-full -start-3 ring-8 ring-white dark:ring-gray-900 dark:bg-primary-900">
{{ if .icon }}
@@ -81,27 +81,31 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="w-5 h-5 text-primary-800 dark:text-primary-300"><path d="M251.76,88.94l-120-64a8,8,0,0,0-7.52,0l-120,64a8,8,0,0,0,0,14.12L32,117.87v48.42a15.91,15.91,0,0,0,4.06,10.65C49.16,191.53,78.51,216,128,216a130,130,0,0,0,48-8.76V240a8,8,0,0,0,16,0V199.51a115.63,115.63,0,0,0,27.94-22.57A15.91,15.91,0,0,0,224,166.29V117.87l27.76-14.81a8,8,0,0,0,0-14.12ZM128,200c-43.27,0-68.72-21.14-80-33.71V126.4l76.24,40.66a8,8,0,0,0,7.52,0L176,143.47v46.34C163.4,195.69,147.52,200,128,200Zm80-33.75a97.83,97.83,0,0,1-16,14.25V134.93l16-8.53ZM188,118.94l-.22-.13-56-29.87a8,8,0,0,0-7.52,14.12L171,128l-43,22.93L25,96,128,41.07,231,96Z"></path></svg>
{{end}}
</span>
<h3 class="flex items-center mb-1 text-lg font-semibold text-gray-900 dark:text-white">{{.area}}
<h3 class="flex items-center mb-1 text-lg font-semibold text-gray-900 dark:text-white">{{ .degree | default .area }}
<!-- <span class="bg-blue-100 text-blue-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300 ms-3">Latest</span>-->
</h3>
<span class="block mb-2 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">{{.institution}}</span>
<span class="block mb-2 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">{{ .institution }}</span>
<time class="block mb-3 text-sm font-normal leading-none text-gray-500 dark:text-gray-300">
{{ if .date_start }}
{{ (time .date_start) | time.Format ($block.design.date_format | default "January 2006") }} –
{{ $start := or .start .date_start }}
{{ $end := or .end .date_end }}
{{ if $start }}
{{ (time $start) | time.Format ($block.design.date_format | default "January 2006") }} –
{{ end }}
{{ if .date_end}}
{{ (time .date_end) | time.Format ($block.design.date_format | default "January 2006") }}
{{else}}
{{ if $end }}
{{ (time $end) | time.Format ($block.design.date_format | default "January 2006") }}
{{ else }}
{{ i18n "present" | default "Present" }}
{{end}}
</time>
<div class="mb-4 text-base font-normal text-gray-500 dark:text-gray-300 prose max-w-none prose-slate dark:prose-invert">{{.summary | $page.RenderString}}</div>
{{ with .button }}
<a href="{{.url}}" target="_blank" rel="noopener" class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg transition-colors hover:bg-gray-100 hover:text-gray-900 focus:z-10 focus:ring-4 focus:outline-none focus:ring-gray-200 focus:text-gray-900 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:text-white dark:focus:ring-gray-700"><svg class="w-3.5 h-3.5 me-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M14.707 7.793a1 1 0 0 0-1.414 0L11 10.086V1.5a1 1 0 0 0-2 0v8.586L6.707 7.793a1 1 0 1 0-1.414 1.414l4 4a1 1 0 0 0 1.416 0l4-4a1 1 0 0 0-.002-1.414Z"/>
<path d="M18 12h-2.55l-2.975 2.975a3.5 3.5 0 0 1-4.95 0L4.55 12H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2Zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/>
</svg> {{.text}}</a>
<a href="{{.url}}" target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg transition-colors hover:bg-gray-100 hover:text-gray-900 focus:z-10 focus:ring-4 focus:outline-none focus:ring-gray-200 focus:text-gray-900 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:text-white dark:focus:ring-gray-700">
{{ with .icon }}
{{ partial "functions/get_icon" (dict "name" . "attributes" "class='w-4 h-4'") }}
{{ end }}
{{ .text | default (i18n "learn_more" | default "Learn more") }}
</a>
{{ end }}
</li>
@@ -5,14 +5,9 @@
{{/* Initialise */}}
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $author := $block.content.username | default "admin" }}
{{ $person_page_path := (printf "/authors/%s" $author) }}
{{ $person_page := site.GetPage $person_page_path }}
{{ if not $person_page }}
{{ errorf "Could not find an author page at `%s`. Please check the value of `author` in your Skill block and create an associated author page if one does not already exist. See https://docs.hugoblox.com/page-builder/#about " $person_page_path }}
{{end}}
{{ $langs := $person_page.Params.languages }}
{{ $author := $block.content.username | default "me" }}
{{ $profile := partial "functions/get_author_profile" $author }}
{{ $langs := $profile.languages }}
<div class="flex flex-col items-center max-w-prose mx-auto gap-3 justify-center">
@@ -22,8 +17,24 @@
{{ with $block.content.text }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
<div class="flex items-center flex-col lg:flex-row gap-x-8">
<div class="flex items-center flex-col lg:flex-row gap-y-10 lg:gap-y-0 lg:gap-x-8">
{{ range $langs }}
{{ $percent := 100.0 }}
{{ $levelText := .level }}
{{ $label := .label }}
{{ $levelStr := printf "%v" ($levelText | default "") }}
{{ $numericLevel := gt (len (findRE "^[0-9]+(?:\\.[0-9]+)?$" $levelStr)) 0 }}
{{ if $numericLevel }}
{{ $percent = mul (float $levelStr) 20 }}
{{ else if .percent }}
{{ $percent = float .percent }}
{{ end }}
{{ if gt $percent 100 }}{{ $percent = 100 }}{{ end }}
{{ if lt $percent 0 }}{{ $percent = 0 }}{{ end }}
<div class="flex flex-col items-center">
<div class="flex items-center justify-center w-28 h-28">
<svg class="transform -rotate-90" viewBox="0 0 288 288">
@@ -31,11 +42,16 @@
class="text-gray-200 dark:text-gray-700" />
<circle cx="145" cy="145" r="120" stroke="currentColor" stroke-width="30" fill="transparent"
class="text-primary-600 dark:text-primary-300"
style="stroke-dasharray: calc(2 * 22 / 7 * 120); stroke-dashoffset: calc((2 * 22 / 7 * 120) - ((2 * 22 / 7 * 120) * {{.percent}}/100))" />
style="stroke-dasharray: calc(2 * 22 / 7 * 120); stroke-dashoffset: calc((2 * 22 / 7 * 120) - ((2 * 22 / 7 * 120) * {{$percent}}/100))" />
</svg>
<span class="absolute text-3xl">{{.percent}}%</span>
<span class="absolute text-3xl">{{ $percent }}%</span>
</div>
<span class="text-1xl">{{.name}}</span>
{{ if $label }}
<span class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ $label }}</span>
{{ else if and $levelText (not $numericLevel) }}
<span class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ . }}</span>
{{ end }}
</div>
{{ end }}
</div>
@@ -11,7 +11,7 @@ Present your technical and professional competencies with the Resume Skills bloc
- **Icon Integration**: Beautiful icons for each skill that enhance visual recognition
- **Flexible Layout**: Side-by-side categories on larger screens, stacked on mobile
- **Custom Colors**: Configurable progress bar colors to match your brand
- **Toggle Control**: Option to show or hide percentage indicators
- **Automatic Bars**: Bars appear when a skill has a level (1–5)
- **Markdown Support**: Rich text formatting for skill names and descriptions
## 🎯 Perfect For
@@ -64,4 +64,3 @@ Present your technical and professional competencies with the Resume Skills bloc
Perfect for technical professionals, creative experts, and anyone who wants to present their skills in an organized, visually appealing format that helps employers quickly understand their capabilities and expertise levels.
Turn your skill set into a competitive advantage with professional presentation that clearly communicates your value proposition.
@@ -5,15 +5,9 @@
{{/* Initialise */}}
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $author := $block.content.username | default "admin" }}
{{ $person_page_path := (printf "/authors/%s" $author) }}
{{ $person_page := site.GetPage $person_page_path }}
{{ if not $person_page }}
{{ errorf "Could not find an author page at `%s`. Please check the value of `author` in your Skill block and create an associated author page if one does not already exist. See https://docs.hugoblox.com/page-builder/#about " $person_page_path }}
{{end}}
{{ $skills := $person_page.Params.skills }}
{{ $show_show_skill_percentage := $block.design.show_skill_percentage | default true }}
{{ $author := $block.content.username | default "me" }}
{{ $profile := partial "functions/get_author_profile" $author }}
{{ $skills := $profile.skills | default (slice) }}
<div class="flex flex-col items-center max-w-prose mx-auto gap-3 justify-center">
@@ -35,12 +29,7 @@
{{ .name | markdownify | emojify }}
{{ with .description }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
</div>
{{ range .items }}
{{ $pack := or .icon_pack "fas" }}
{{ $pack_prefix := $pack }}
{{ if in (slice "fab" "fas" "far" "fal") $pack }}
{{ $pack_prefix = "fa" }}
{{ end }}
{{ range (.items | default (slice)) }}
<div class="skills-content">
@@ -50,13 +39,26 @@
</span>
{{ end }}
{{ $label := .label | default .name }}
<span class="skills-name text-gray-700 dark:text-gray-300">
{{ .name | markdownify | emojify }}
{{ $label | markdownify | emojify }}
{{ with .description }}<p class="skills-description">{{ . | markdownify | emojify }}</p>{{ end }}
</span>
{{ if .percent | and $show_show_skill_percentage }}
{{/* Numeric level (1-5) only. */}}
{{ $level := 0.0 }}
{{ if isset . "level" }}
{{ $raw := .level }}
{{ $type := printf "%T" $raw }}
{{ if or (eq $type "int") (eq $type "int64") (eq $type "int32") (eq $type "int16") (eq $type "int8") (eq $type "uint") (eq $type "uint64") (eq $type "uint32") (eq $type "uint16") (eq $type "uint8") (eq $type "float64") (eq $type "float32") }}
{{ $level = float $raw }}
{{ end }}
{{ end }}
{{ if gt $level 5 }}{{ $level = 5 }}{{ end }}
{{ if lt $level 0 }}{{ $level = 0 }}{{ end }}
{{ if gt $level 0 }}
{{ $levelPercent := mul $level 20 }}
<div class="skills-wrapper" {{with $color_border}}{{ (printf "style=\"border-color: %s\"" .) | safeHTMLAttr }}{{end}}>
<div class="skills-percent" style="width: {{ (printf "%s" (cast.ToString (.percent | default 100))) | safeCSS }}%; {{with $color}}{{ (printf "background-color: %s" .) | safeCSS }}{{end}}"></div>
<div class="skills-percent" style="width: {{ printf "%.0f" $levelPercent }}%; {{with $color}}{{ (printf "background-color: %s" .) | safeCSS }}{{end}}"></div>
</div>
{{ end }}
</div>
@@ -64,4 +66,3 @@
</div>
{{ end }}
</div>
@@ -39,13 +39,7 @@
},
{
"type": "object",
"properties": {
"show_skill_percentage": {
"type": "boolean",
"description": "Display skill percentage bars",
"default": true
}
}
"properties": {}
}
]
}
@@ -37,119 +37,180 @@
{{ end }}
</div>
{{/* Team Grid */}}
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{{ range $block.content.user_groups }}
{{ $group_name := . }}
{{ $query := where (where site.Pages "Section" "authors") ".Params.user_groups" "intersect" (slice .) }}
{{/* Sort */}}
{{ $sort_by := $block.content.sort_by | default "Params.last_name" }}
{{ $sort_by = partial "functions/get_sort_by_parameter" $sort_by }}
{{ $sort_ascending := $block.content.sort_ascending | default true }}
{{ $sort_order := cond $sort_ascending "asc" "desc" }}
{{ $query = sort $query $sort_by $sort_order }}
{{/* Group Header (if multiple groups) */}}
{{ if and $query (gt (len $block.content.user_groups) 1) }}
<div class="col-span-full">
{{/* Build profiles from data/authors */}}
{{ $allProfiles := slice }}
{{ range $slug, $_ := site.Data.authors }}
{{ $allProfiles = $allProfiles | append (partial "functions/get_author_profile" (dict "slug" $slug)) }}
{{ end }}
{{ $groups := $block.content.user_groups }}
{{ if $groups }}
{{ range $group := $groups }}
{{ $groupProfiles := slice }}
{{ range $allProfiles }}
{{ $u := .user_groups | default (slice) }}
{{ if gt (len (intersect $u (slice $group))) 0 }}
{{ $groupProfiles = $groupProfiles | append . }}
{{ end }}
{{ end }}
{{ $groupProfiles = sort $groupProfiles "title" }}
{{ if and $groupProfiles (gt (len $groups) 1) }}
<div class="col-span-full mb-4">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6 pb-2 border-b border-gray-200 dark:border-gray-700">
{{ $group_name | markdownify }}
{{ $group | markdownify }}
</h3>
</div>
{{ end }}
{{/* Team Members */}}
{{ range $person := $query }}
{{ $avatar := (.Resources.ByType "image").GetMatch "*avatar*" }}
{{ $link := "" }}
{{ with site.GetPage (printf "/authors/%s" (path.Base .File.Dir)) }}
{{ $link = .RelPermalink }}
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 w-full">
{{ range $profile := $groupProfiles }}
{{ $avatar := $profile.avatar }}
{{ $link := printf "/authors/%s/" $profile.slug }}
<div class="group relative">
<div class="relative overflow-hidden rounded-2xl bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
{{ if $avatar }}
<a href="{{ $link }}" class="block relative h-64 overflow-hidden">
{{ $image := $avatar.Fill "400x400 Center webp" }}
<img src="{{ $image.RelPermalink }}"
alt="{{ $profile.title }}"
width="{{ $image.Width }}"
height="{{ $image.Height }}"
loading="lazy"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110">
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</a>
{{ end }}
<div class="p-6">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-1">
<a href="{{ $link }}" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
{{ $profile.title }}
</a>
</h3>
{{ if $show_role }}
<div class="text-gray-600 dark:text-gray-400 mb-3">
{{ with $profile.role }}<p class="font-medium">{{ . }}</p>{{ end }}
{{ if and $show_organizations $profile.affiliations }}
{{ range first 1 $profile.affiliations }}
<p class="text-sm mt-1">{{ .name }}</p>
{{ end }}
{{ end }}
</div>
{{ end }}
{{ with $profile.bio }}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
{{ . }}
</p>
{{ end }}
{{ if and $show_interests $profile.interests }}
<div class="mb-4">
<div class="flex flex-wrap gap-1">
{{ range first 3 $profile.interests }}
<span class="inline-block bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs px-2 py-1 rounded">
{{ . }}
</span>
{{ end }}
</div>
</div>
{{ end }}
{{ if and $show_social $profile.links }}
<div class="flex gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
{{ range $profile.links }}
{{ $l := .url | default .link }}
{{ $scheme := (urls.Parse $l).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ $l = (.url | default .link) | relLangURL }}
{{ if eq (path.Ext $l) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
<a href="{{ $l | safeURL }}"
{{ $target | safeHTMLAttr }}
aria-label="{{ .icon }}"
{{ with .label }} title="{{ . }}"{{ end }}
class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400 transition-colors">
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class=\"w-5 h-5\"") }}
</a>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
{{ end }}
</div>
{{ end }}
{{ else }}
{{ $allProfiles = sort $allProfiles "title" }}
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{{ range $profile := $allProfiles }}
{{ $avatar := $profile.avatar }}
{{ $link := printf "/authors/%s/" $profile.slug }}
<div class="group relative">
<div class="relative overflow-hidden rounded-2xl bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
{{/* Avatar */}}
{{ if $avatar }}
<a href="{{ $link }}" class="block relative h-64 overflow-hidden">
{{ $image := $avatar.Fill "400x400 Center webp" }}
<img src="{{ $image.RelPermalink }}"
alt="{{ .Title }}"
alt="{{ $profile.title }}"
width="{{ $image.Width }}"
height="{{ $image.Height }}"
loading="lazy"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110">
{{/* Gradient Overlay */}}
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</a>
{{ end }}
{{/* Content */}}
<div class="p-6">
{{/* Name */}}
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-1">
<a href="{{ $link }}" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
{{ .Title }}
{{ $profile.title }}
</a>
</h3>
{{/* Role & Organization */}}
{{ if $show_role }}
<div class="text-gray-600 dark:text-gray-400 mb-3">
{{ with .Params.role }}
<p class="font-medium">{{ . }}</p>
{{ end }}
{{ if and $show_organizations .Params.organizations }}
{{ range first 1 .Params.organizations }}
{{ with $profile.role }}<p class="font-medium">{{ . }}</p>{{ end }}
{{ if and $show_organizations $profile.affiliations }}
{{ range first 1 $profile.affiliations }}
<p class="text-sm mt-1">{{ .name }}</p>
{{ end }}
{{ end }}
</div>
{{ end }}
{{/* Bio */}}
{{ with .Params.bio }}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
{{ . }}
</p>
{{ with $profile.bio }}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">{{ . }}</p>
{{ end }}
{{/* Interests */}}
{{ if and $show_interests .Params.interests }}
{{ if and $show_interests $profile.interests }}
<div class="mb-4">
<div class="flex flex-wrap gap-1">
{{ range first 3 .Params.interests }}
<span class="inline-block bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs px-2 py-1 rounded">
{{ . }}
</span>
{{ range first 3 $profile.interests }}
<span class="inline-block bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs px-2 py-1 rounded">{{ . }}</span>
{{ end }}
</div>
</div>
{{ end }}
{{/* Social Links */}}
{{ if $show_social }}
{{ if and $show_social $profile.links }}
<div class="flex gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
{{ range .Params.social }}
{{ $link := .url | default .link }}
{{ $scheme := (urls.Parse $link).Scheme }}
{{ range $profile.links }}
{{ $l := .url | default .link }}
{{ $scheme := (urls.Parse $l).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ $link = (.url | default .link) | relLangURL }}
{{ if eq (path.Ext $link) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ $l = (.url | default .link) | relLangURL }}
{{ if eq (path.Ext $l) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
<a href="{{ $link | safeURL }}"
{{ $target | safeHTMLAttr }}
aria-label="{{ .icon }}"
{{ with .label }} title="{{ . }}"{{ end }}
class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400 transition-colors">
<a href="{{ $l | safeURL }}" {{ $target | safeHTMLAttr }} aria-label="{{ .icon }}" {{ with .label }} title="{{ . }}"{{ end }} class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400 transition-colors">
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class=\"w-5 h-5\"") }}
</a>
{{ end }}
@@ -159,8 +220,8 @@
</div>
</div>
{{ end }}
{{ end }}
</div>
</div>
{{ end }}
{{/* Call to Action */}}
{{ with $block.content.cta }}
@@ -7,19 +7,18 @@
{{ $publisher := $branding.organization }}
{{ $author := "" }}
{{ $author_username := "" }}
{{ $author_slug := "" }}
{{ if and (not $page.Params.authors) ($page.Scratch.Get "superuser_username") }}
{{ $author_username = $page.Scratch.Get "superuser_username" }}
{{ if and (not $page.Params.authors) ($page.Scratch.Get "superuser_slug") }}
{{ $author_slug = $page.Scratch.Get "superuser_slug" }}
{{ else if $page.Params.authors }}
{{ $author = index $page.Params.authors 0 }}
{{ $author_username = urlize $author }}
{{ $author_slug = urlize $author }}
{{ end }}
{{ $taxonomy := "authors" }}
{{ $profile_page := site.GetPage (printf "/%s/%s" $taxonomy $author_username) }}
{{ with $profile_page }}
{{ $author = .Title }}
{{ $profile := partial "functions/get_author_profile" (dict "slug" $author_slug) }}
{{ if $profile.title }}
{{ $author = $profile.title }}
{{ else }}
{{ $author = $author | default $publisher }}
{{ end }}
@@ -0,0 +1,122 @@
{{/*
Return a normalized HugoBlox author/person profile from data.
Input (flexible):
- dict with "slug" key, or a raw string slug.
Output (dict):
- slug: string
- title: string (display name, fallback to humanized slug)
- role: string (optional)
- bio: string (optional)
- status: map (optional)
- affiliations: array (optional)
- ids: map (optional)
- links: array (optional)
- is_owner: bool (optional)
- has_data: bool (true if found in data/authors)
- avatar: resource (optional) – matching assets/media/authors/<slug>.*
*/}}
{{- $input := . -}}
{{- $slug := "" -}}
{{- $inputType := printf "%T" $input -}}
{{- if (reflect.IsMap $input) -}}
{{- $slug = index $input "slug" | default "" -}}
{{- else if eq $inputType "string" -}}
{{- $slug = $input -}}
{{- else -}}
{{- $slug = printf "%v" $input -}}
{{- end -}}
{{- $slug = $slug | urlize -}}
{{- $data := index site.Data.authors $slug -}}
{{- $hasData := $data -}}
{{/* Structured name resolution */}}
{{- $name := $data.name -}}
{{- $display := "" -}}
{{- $given := "" -}}
{{- $family := "" -}}
{{- $middle := "" -}}
{{- $alternate := "" -}}
{{- $namePronunciation := "" -}}
{{- $namePronouns := "" -}}
{{- if $name.display }}{{ $display = $name.display }}{{ end }}
{{- if $name.given }}{{ $given = $name.given }}{{ end }}
{{- if $name.family }}{{ $family = $name.family }}{{ end }}
{{- if $name.middle }}{{ $middle = $name.middle }}{{ end }}
{{- if $name.alternate }}{{ $alternate = $name.alternate }}{{ end }}
{{- if $name.pronunciation }}{{ $namePronunciation = $name.pronunciation }}{{ end }}
{{- if $name.pronouns }}{{ $namePronouns = $name.pronouns }}{{ end }}
{{- $title := "" -}}
{{- if $display -}}
{{- $title = $display -}}
{{- else if $data.title -}}
{{- $title = $data.title -}}
{{- else -}}
{{- $title = $slug | humanize -}}
{{- end -}}
{{- $role := $data.role -}}
{{- $bio := $data.bio -}}
{{- $status := $data.status -}}
{{- $affiliations := $data.affiliations -}}
{{- $ids := $data.ids -}}
{{- $links := $data.links -}}
{{- $isOwner := or ($data.is_owner) ($data.superuser) -}}
{{- $pronouns := $namePronouns | default $data.pronouns -}}
{{- $userGroups := $data.user_groups -}}
{{- $interests := $data.interests -}}
{{- $education := $data.education -}}
{{- $experience := or $data.experience $data.work -}}
{{- $skills := $data.skills -}}
{{- $languages := $data.languages -}}
{{- $awards := $data.awards -}}
{{- $postnominals := slice -}}
{{- if $data.postnominals -}}
{{- if (reflect.IsSlice $data.postnominals) -}}
{{- $postnominals = $data.postnominals -}}
{{- else -}}
{{- $postnominals = slice $data.postnominals -}}
{{- end -}}
{{- end -}}
{{- /* Avatar resolution: look in assets/media/authors/<slug>.* */ -}}
{{- $avatar := resources.GetMatch (printf "media/authors/%s.*" $slug) -}}
{{- $profile := dict
"slug" $slug
"title" $title
"name_display" $display
"name_given" $given
"name_family" $family
"name_middle" $middle
"name_alternate" $alternate
"name_pronunciation" $namePronunciation
"role" $role
"bio" $bio
"status" $status
"affiliations" $affiliations
"ids" $ids
"links" $links
"pronouns" $pronouns
"user_groups" $userGroups
"interests" $interests
"education" $education
"experience" $experience
"skills" $skills
"languages" $languages
"awards" $awards
"postnominals" $postnominals
"is_owner" $isOwner
"has_data" (not (eq $hasData nil))
"avatar" $avatar
-}}
{{- return $profile -}}
@@ -3,16 +3,22 @@
{{/* Don't show author box on normal pages or if author box disabled. */}}
{{ if ne .Type "page" | and (not (or (eq site.Params.profile false) (eq .Params.profile false))) }}
{{/* Display superuser if superuser exists and page authors are not explicitly specified. */}}
{{/* Otherwise, display first author if a profile for them exists. */}}
{{ $authors := .Params.authors }}
{{ if and (not .Params.authors) (.Scratch.Get "superuser_username") }}
{{ $author_page := site.GetPage (printf "/%s/%s" "authors" (.Scratch.Get "superuser_username")) }}
{{ partial "page_author_card" (dict "author_page" $author_page) }}
{{ else if .Params.authors }}
{{ $taxonomy := "authors" }}
{{ range $author_obj := (.GetTerms $taxonomy) }}
{{ partial "page_author_card" (dict "author_page" $author_obj.Page) }}
{{/* If authors not set, fallback to superuser/owner from Scratch */}}
{{ if (not $authors) }}
{{ $superuser := .Scratch.Get "superuser_slug" }}
{{ if $superuser }}
{{ $authors = (slice $superuser) }}
{{ end }}
{{ end }}
{{ if $authors }}
{{ range $idx, $slug := $authors }}
{{ $profile := partial "functions/get_author_profile" $slug }}
{{ if $profile }}
{{ partial "page_author_card" (dict "profile" $profile "page" .) }}
{{ end }}
{{ end }}
{{ end }}
@@ -1,21 +1,19 @@
{{ $profile := .profile }}
{{ $page := .page }}
{{ $author_page := .author_page }}
{{ $site_type := site.Params.hugoblox.identity.type | humanize | default "Person" }}
{{- $has_profile := not (eq nil $author_page.Params.superuser) -}}
{{ $avatar_shape := site.Params.hugoblox.layout.avatar_shape | default "circle" }}
{{/* Check an author profile exists. */}}
{{ if $has_profile }}
{{/* If it's a personal site and primary page author is superuser, link to the homepage rather than their profile page. */}}
{{ $profile_url := $author_page.RelPermalink }}
{{ if and (eq $site_type "Person") (eq $author_page.Params.superuser true) }}
{{ if $profile }}
{{/* If it's a personal site and the profile is owner, link home */}}
{{ $profile_url := printf "/authors/%s/" $profile.slug }}
{{ if and (eq $site_type "Person") ($profile.is_owner) }}
{{ $profile_url = site.BaseURL }}
{{ end }}
{{ $avatar := ($author_page.Resources.ByType "image").GetMatch "*avatar*" }}
{{ $avatar_shape := site.Params.hugoblox.layout.avatar_shape | default "circle" }}
{{ $avatar := $profile.avatar }}
<div class="flex pt-12 pb-4">
{{ if $avatar }}
{{/* Use utility for high-res avatar processing (smart for Retina displays) */}}
{{ $responsive := partial "functions/process_responsive_image.html" (dict
"image" $avatar
"mode" "fill"
@@ -31,7 +29,7 @@
src="{{ $responsive.fallback.RelPermalink }}"
width="96"
height="96"
alt="{{$author_page.Title}}"
alt="{{$profile.title}}"
loading="lazy"
/>
{{ end }}
@@ -42,29 +40,27 @@
<div class="font-semibold leading-6 text-neutral-800 dark:text-neutral-300">
<span class="inline-block">
{{if $profile_url}}<a href="{{$profile_url}}" class="no-underline">{{end}}
{{$author_page.Title}}
{{$profile.title}}
{{if $profile_url}}</a>{{end}}
</span>
{{- with $author_page.Params.pronouns -}}
{{- with $profile.pronouns -}}
<span class="text-sm font-normal text-gray-600 dark:text-gray-400 ml-1 inline-block">({{ . }})</span>
{{- end -}}
</div>
{{ with $author_page.Params.role }}
{{ with $profile.role }}
<div class="text-sm font-bold text-neutral-700 dark:text-neutral-300">
{{. | markdownify | emojify}}
</div>
{{end}}
{{ with $author_page.Params.bio }}
{{ with $profile.bio }}
<div class="text-sm text-neutral-700 dark:text-neutral-300">{{ . | markdownify | emojify }}</div>
{{ end }}
<div class="text-2xl sm:text-lg pt-1">
{{ partial "social_links" $author_page }}
{{ partial "social_links" (dict "Params" (dict "profiles" $profile.links)) }}
</div>
</div>
</div>
@@ -1,17 +1,18 @@
{{/* Display author list. */}}
{{ if .Params.authors }}
{{ range $index, $value := (.GetTerms "authors") }}
{{- /* Highlight the author's name? */ -}}
{{- $highlight_name := .Page.Params.highlight_name | default false -}}
{{ range $index, $term := (.GetTerms "authors") }}
{{- $slug := $term.Data.Term | urlize -}}
{{- $profile := partial "functions/get_author_profile" (dict "slug" $slug) -}}
{{- $display := $profile.title | default $term.LinkTitle -}}
{{- $highlight_name := false -}}
{{- if gt $index 0 }}, {{ end -}}
<span {{ if $highlight_name }}class="font-bold"{{end}}>
{{/* Effectively check the page's `build` option as `build` is not exposed in Hugo's Page object. */}}
{{- if .RelPermalink -}}
<a href="{{.RelPermalink}}">{{.LinkTitle}}</a>
{{- if $term.RelPermalink -}}
<a href="{{ $term.RelPermalink }}">{{ $display }}</a>
{{- else -}}
{{ .LinkTitle }}
{{ $display }}
{{- end -}}
</span>
{{- if isset $.Params "author_notes" -}}
@@ -12,18 +12,30 @@
<meta name="robots" content="noindex" />
{{- end -}}
{{/* Attempt to load superuser. */}}
{{ $superuser_name := "" }}
{{ $superuser_username := "" }}
{{ $superuser_role := "" }}
{{ range first 1 (where (where site.Pages "Section" "authors") "Params.superuser" true) }}
{{ $superuser_name = .Title }}
{{ $superuser_username = path.Base .File.Dir }}
{{ $superuser_role = .Params.role }}
{{ end }}
{{ $scr.Set "superuser_username" $superuser_username }}{{/* Set superuser globally for page_author.html. */}}
{{/* Attempt to load superuser/person from data. */}}
{{ $superuser_name := "" }}
{{ $superuser_slug := "" }}
{{ $superuser_role := "" }}
{{ with $superuser_name }}<meta name="author" content="{{ . }}" />{{ end }}
{{/* Prefer is_owner flag, fallback to slug 'me' */}}
{{ range $slug, $data := site.Data.authors }}
{{ if and (not $superuser_slug) ($data.is_owner) }}
{{ $superuser_slug = $slug }}
{{ end }}
{{ end }}
{{ if and (eq $superuser_slug "") (index site.Data.authors "me") }}
{{ $superuser_slug = "me" }}
{{ end }}
{{ if $superuser_slug }}
{{ $profile := partial "functions/get_author_profile" (dict "slug" $superuser_slug) }}
{{ $superuser_name = $profile.title }}
{{ $superuser_role = $profile.role }}
{{ end }}
{{ $scr.Set "superuser_slug" $superuser_slug }}{{/* Set superuser globally for author rendering. */}}
{{ with $superuser_name }}<meta name="author" content="{{ . }}" />{{ end }}
{{/* Generate page description. */}}
{{ $desc := "" }}
@@ -114,15 +126,22 @@
{{/* Get page image for sharing. */}}
{{ $sharing_image := resources.GetMatch (path.Join "media" "sharing.*") }}
{{ $featured_image := (.Resources.ByType "image").GetMatch "*featured*" }}
{{ $avatar_image := (.Resources.ByType "image").GetMatch "avatar*" }}
{{ $avatar_image := (.Resources.ByType "image").GetMatch "avatar*" }}
{{ $has_logo := fileExists "assets/media/logo.png" | or (fileExists "assets/media/logo.svg") }}
{{ $og_image := "" }}
{{ $twitter_card := "summary_large_image" }}
{{ if (and (eq .Kind "term") $avatar_image) }}
{{/* Match image processing in About widget to prevent generating more images than necessary. */}}
{{ $og_image = ($avatar_image.Fill "540x540 Center").Permalink }}
{{ if and (eq .Kind "term") (eq .Data.Singular "author") }}
{{/* Try data-driven avatar for author term pages */}}
{{ $termSlug := .Data.Term | urlize }}
{{ $profile := partial "functions/get_author_profile" (dict "slug" $termSlug) }}
{{ with $profile.avatar }}
{{ $og_image = (.Fill "540x540 Center").Permalink }}
{{ $twitter_card = "summary" }}
{{ else if $featured_image }}
{{ end }}
{{ else if (and (eq .Kind "term") $avatar_image) }}
{{ $og_image = ($avatar_image.Fill "540x540 Center").Permalink }}
{{ $twitter_card = "summary" }}
{{ else if $featured_image }}
{{ $og_image = $featured_image.Permalink }}
{{ else if $sharing_image }}
{{ $og_image = $sharing_image.Permalink }}
@@ -145,29 +145,20 @@
<div class="flex items-center gap-3 text-xs text-zinc-500 dark:text-zinc-500 flex-wrap mb-3">
{{ if $item.Params.authors }}
<div class="flex items-center gap-2 min-w-0">
{{ $authorTerm := index ($item.GetTerms "authors") 0 }}
{{ if $authorTerm }}
{{/* Author page exists - show avatar or default icon */}}
<div class="relative h-6 w-6 flex-shrink-0">
{{ $avatar := ($authorTerm.Resources.ByType "image").GetMatch "*avatar*" }}
{{ if $avatar }}
{{ $avatar_48 := $avatar.Process "Fill 48x48 Center webp" }}
<img alt="avatar" class="rounded-full object-cover" src="{{$avatar_48.RelPermalink}}" width="24" height="24" loading="lazy" decoding="async" />
{{ else }}
{{/* Default avatar icon when no image */}}
<div class="w-6 h-6 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center">
{{ partial "functions/get_icon" (dict "name" "hero/user" "attributes" "class=\"w-4 h-4 text-gray-500 dark:text-gray-400\"") }}
</div>
{{ end }}
</div>
<span class="truncate max-w-[9rem] text-sm">{{ $authorTerm.Page.LinkTitle }}</span>
{{ else }}
{{/* No author page - just show team icon and first author name */}}
<div class="w-6 h-6 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center flex-shrink-0">
{{ partial "functions/get_icon" (dict "name" "hero/user-group" "attributes" "class=\"w-4 h-4 text-primary-600 dark:text-primary-400\"") }}
</div>
<span class="truncate max-w-[9rem] text-sm">{{ index $item.Params.authors 0 }}</span>
{{ end }}
{{ $slug := index $item.Params.authors 0 | urlize }}
{{ $profile := partial "functions/get_author_profile" $slug }}
{{ $avatar := $profile.avatar }}
<div class="relative h-6 w-6 flex-shrink-0">
{{ if $avatar }}
{{ $avatar_48 := $avatar.Process "Fill 48x48 Center webp" }}
<img alt="avatar" class="rounded-full object-cover" src="{{$avatar_48.RelPermalink}}" width="24" height="24" loading="lazy" decoding="async" />
{{ else }}
<div class="w-6 h-6 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center">
{{ partial "functions/get_icon" (dict "name" "hero/user" "attributes" "class=\"w-4 h-4 text-gray-500 dark:text-gray-400\"") }}
</div>
{{ end }}
</div>
<span class="truncate max-w-[9rem] text-sm">{{ $profile.title }}</span>
</div>
<span class="opacity-40">•</span>
{{ end }}
@@ -0,0 +1,98 @@
{{ define "main" }}
{{ $slug := .Data.Term | urlize }}
{{ $profile := partial "functions/get_author_profile" (dict "slug" $slug) }}
{{ $avatar := $profile.avatar }}
{{ $avatar_shape := site.Params.hugoblox.layout.avatar_shape | default "circle" }}
<section class="max-w-5xl mx-auto px-6 py-10">
<div class="flex flex-col sm:flex-row sm:items-center gap-6">
<div class="flex-shrink-0">
{{ if $avatar }}
{{ $responsive := partial "functions/process_responsive_image.html" (dict
"image" $avatar
"mode" "fill"
"aspect_ratio" "1:1"
"anchor" "Center"
"sizes" (slice 160 240 320 480)
) }}
<img
class="h-32 w-32 sm:h-40 sm:w-40 {{ if ne $avatar_shape "square" }}rounded-full{{ end }}"
srcset="{{ $responsive.srcset }}"
sizes="160px"
src="{{ $responsive.fallback.RelPermalink }}"
width="160"
height="160"
alt="{{ $profile.title }}"
loading="lazy"
/>
{{ else }}
<div class="h-32 w-32 sm:h-40 sm:w-40 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
{{ partial "functions/get_icon" (dict "name" "hero/user" "attributes" "class=\"h-12 w-12 text-gray-400\"") }}
</div>
{{ end }}
</div>
<div class="space-y-2">
<div class="flex items-center gap-2">
<h1 class="text-3xl sm:text-4xl font-bold">{{ $profile.title }}</h1>
{{ with $profile.status }}
{{ with .icon }}
<span class="text-2xl" aria-hidden="true">{{ . }}</span>
{{ end }}
{{ end }}
</div>
{{ with $profile.role }}
<div class="text-lg font-semibold text-neutral-700 dark:text-neutral-200">{{ . | markdownify | emojify }}</div>
{{ end }}
{{ with $profile.affiliations }}
<div class="text-neutral-600 dark:text-neutral-300">
{{ range $index, $org := . }}
{{ if gt $index 0 }} · {{ end }}
{{ if $org.url }}<a href="{{ $org.url }}" class="underline">{{ $org.name }}</a>{{ else }}{{ $org.name }}{{ end }}
{{ end }}
</div>
{{ end }}
{{ with $profile.bio }}
<div class="prose prose-slate dark:prose-invert max-w-none">
{{ . | markdownify | emojify }}
</div>
{{ end }}
{{ if $profile.links }}
<div class="pt-2">
{{ partial "social_links" (dict "Params" (dict "profiles" $profile.links)) }}
</div>
{{ end }}
</div>
</div>
</section>
{{ $view := "card" }}
{{ $block := . }}
{{ $paginator := .Paginate .Pages.ByDate.Reverse }}
{{ $config := dict
"columns" 2
"len" (len $paginator.Pages)
"fill_image" true
"show_date" true
"show_read_time" true
"show_read_more" true
}}
<div class="flex flex-col items-center px-6">
{{ partial "functions/render_view" (dict "fragment" "start" "page" $block "item" . "view" $view "config" $config) }}
{{ range $index, $item := $paginator.Pages }}
{{ partial "functions/render_view" (dict "page" . "item" . "view" $view "index" $index "config" $config) }}
{{ end }}
{{ partial "functions/render_view" (dict "fragment" "end" "page" $block "item" . "view" $view) }}
{{ partial "components/paginator" . }}
</div>
{{ end }}
+7 -15
View File
@@ -78,30 +78,22 @@
<script>console.log('Author notes detected in single.html, Alpine.js should load');</script>
{{ end }}
{{ $taxonomy := "authors" }}
{{ range $i, $author_obj := (.GetTerms $taxonomy) }}
{{ $author_page := $author_obj.Page }}
{{ $avatar := ($author_page.Resources.ByType "image").GetMatch "*avatar*" }}
{{- if and $i (not $avatar) }}<span class="mr-1">,</span>{{ end -}}
{{ with $author_page.Params.website }}
<a href="{{.}}" target="_blank" rel="noreferrer">
{{ end }}
{{ range $i, $slug := .Params.authors }}
{{ $profile := partial "functions/get_author_profile" $slug }}
{{ $avatar := $profile.avatar }}
{{ if and $i (not $avatar) }}<span class="mr-1">,</span>{{ end }}
<div class="group inline-flex items-center text-current gap-x-1.5 mx-1">
{{ with $avatar }}
{{/* Simple 2x retina image for small avatar */}}
{{ $avatar_32 := $avatar.Process "Fill 32x32 Center webp" }}
{{ $avatar_32 := .Process "Fill 32x32 Center webp" }}
<img src="{{ $avatar_32.RelPermalink }}"
width="16"
height="16"
alt="{{ $author_page.Title }}"
alt="{{ $profile.title }}"
class="inline-block h-4 w-4 rounded-full border border-current"
loading="lazy" />
{{ end }}
<div {{if $author_page.Params.website}}class="group-hover:underline"{{end}}>{{ $author_page.Title }}</div>
<div>{{ $profile.title }}</div>
</div>
{{ with $author_page.Params.website }}
</a>
{{ end }}
{{/* Add author note tooltip if available */}}
{{- if isset $.Params "author_notes" -}}
{{- with (index $.Params.author_notes $i) -}}