Skip to content

Commit 1f2aa1a

Browse files
HeyItsGilbertclaude
andcommitted
feat(authors): opt-in author profiles with avatars, taglines, bios, links
Authors can now describe themselves via an optional taxonomy term content file at content/authors/<slug>/_index.md: - Avatar resolution avatar -> gravatar_hash -> email -> identicon fallback (author-avatar.html partial; email hashed at build so a raw email is never required in the repo). - Social links partial (website/github/twitter/mastodon/linkedin/bluesky). - preferred_name overrides display only; the authors: byline stays the key, so un-enriched authors render unchanged (no migration of 1,045 files). - List page: enriched profiles sort first, then by count; cards show avatar + tagline + links; search matches name and tagline. - Profile page: avatar + preferred name + links header + Markdown bio. - new-author.ps1 scaffolds a profile at the correct Hugo slug and supports renames (rewrites the byline across content + adds an aliases redirect). - Build-time warning when a profile's slug matches no author (orphan guard). Updated archetypes/authors.md to the full commented schema. See docs/adr/0002-author-profiles.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent de73f81 commit 1f2aa1a

6 files changed

Lines changed: 295 additions & 37 deletions

File tree

archetypes/authors.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,34 @@
11
---
2-
title: '{{ replace .File.ContentBaseName "-" " " | title }}'
3-
description: ""
4-
layout: "authors"
5-
website: ""
6-
twitter: ""
7-
github: ""
2+
# The directory this file lives in MUST equal Hugo's slug of your author name
3+
# exactly as it appears in articles' `authors:` frontmatter
4+
# (lowercase, spaces -> hyphens, punctuation dropped).
5+
# Use `tools/new-author.ps1 "Your Name"` to scaffold this with the correct slug.
6+
7+
# `title` is required by Hugo. Keep it as your author name (the byline key).
8+
title: "{{ replace .File.ContentBaseName "-" " " | title }}"
9+
10+
# Optional: override only how your name is *displayed* (byline + slug are unchanged).
11+
# preferred_name: ""
12+
13+
# One short line shown on your card in the author list.
14+
tagline: ""
15+
16+
# --- Avatar (first match wins) -------------------------------------------------
17+
# 1. avatar: path or URL to an image you control (bypasses Gravatar)
18+
# 2. gravatar_hash: MD5 of your lowercased email (keeps your email out of the repo)
19+
# 3. email: plain email; hashed to a Gravatar at build time (stored publicly)
20+
# If none are set, a stable identicon is generated from your name.
21+
# avatar: ""
22+
# gravatar_hash: ""
23+
# email: ""
24+
25+
# --- Links (full URLs) ---------------------------------------------------------
26+
# website: ""
27+
# github: ""
28+
# twitter: ""
29+
# mastodon: ""
30+
# linkedin: ""
31+
# bluesky: ""
832
---
33+
34+
<!-- Your bio in Markdown. Shown on your profile page. -->

themes/powershell-community/layouts/_default/authors.html

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ <h1 class="text-4xl lg:text-5xl font-bold mb-2">{{ .Title }}</h1>
1818
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
1919
<div class="max-w-md mx-auto">
2020
<div class="relative">
21-
<input type="text" id="author-search"
21+
<input type="text" id="author-search"
2222
placeholder="Search authors..."
2323
class="w-full px-4 py-3 pl-10 pr-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
2424
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
@@ -30,36 +30,58 @@ <h1 class="text-4xl lg:text-5xl font-bold mb-2">{{ .Title }}</h1>
3030
<!-- Authors Grid -->
3131
<section class="py-10 bg-gray-50">
3232
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
33-
{{ $authorTaxonomy := .Site.Taxonomies.authors }}
34-
{{ if $authorTaxonomy }}
33+
{{ $pairs := .Site.Taxonomies.authors.ByCount }}
34+
{{ if $pairs }}
35+
{{/* Enriched profiles (backed by a content file) lead, then bare authors,
36+
preserving ByCount order within each group. */}}
37+
{{ $enriched := slice }}
38+
{{ $bare := slice }}
39+
{{ range $pairs }}
40+
{{ if .Page.File }}
41+
{{ $enriched = $enriched | append . }}
42+
{{ else }}
43+
{{ $bare = $bare | append . }}
44+
{{ end }}
45+
{{ end }}
46+
{{ $ordered := $enriched | append $bare }}
3547
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3" id="authors-container">
36-
{{ range $authorTaxonomy.ByCount }}
37-
<div class="bg-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 overflow-hidden author-card"
38-
data-author="{{ .Term | lower }}">
39-
<div class="p-6">
48+
{{ range $ordered }}
49+
{{ $page := .Page }}
50+
{{ $name := $page.Title }}
51+
{{ $display := $name }}
52+
{{ with $page.Params.preferred_name }}{{ $display = . }}{{ end }}
53+
{{ $tagline := $page.Params.tagline }}
54+
<div class="bg-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 overflow-hidden author-card flex flex-col"
55+
data-author="{{ $display | lower }}"
56+
data-tagline="{{ with $tagline }}{{ . | lower }}{{ end }}">
57+
<div class="p-6 flex flex-col flex-1">
4058
<!-- Author Avatar -->
4159
<div class="flex justify-center mb-4">
42-
<div class="w-16 h-16 bg-gradient-to-br from-blue-400 to-blue-600 rounded-full flex items-center justify-center text-white">
43-
<i class="fas fa-user text-2xl"></i>
44-
</div>
60+
{{ partial "author-avatar.html" (dict "page" $page "name" $name "size" 96 "class" "w-20 h-20 rounded-full object-cover ring-2 ring-blue-100") }}
4561
</div>
4662

4763
<!-- Author Info -->
48-
<h2 class="text-2xl font-bold text-center text-gray-900 mb-2">
49-
<a href="{{ .Page.RelPermalink }}" class="hover:text-blue-600 transition-colors duration-200">
50-
{{ .Page.Title }}
64+
<h2 class="text-2xl font-bold text-center text-gray-900 mb-1">
65+
<a href="{{ $page.RelPermalink }}" class="hover:text-blue-600 transition-colors duration-200">
66+
{{ $display }}
5167
</a>
5268
</h2>
5369

70+
{{ with $tagline }}
71+
<p class="text-center text-gray-600 text-sm mb-3">{{ . }}</p>
72+
{{ end }}
73+
5474
<!-- Article Count -->
5575
<p class="text-center text-gray-600 mb-4">
5676
<span class="text-3xl font-bold text-blue-600">{{ .Count }}</span><br>
57-
<span class="text-sm">article{{ if gt .Count 1 }}s{{ end }} published</span>
77+
<span class="text-sm">article{{ if ne .Count 1 }}s{{ end }} published</span>
5878
</p>
5979

80+
{{ partial "author-links.html" (dict "page" $page "class" "flex justify-center gap-4 text-lg mb-4") }}
81+
6082
<!-- View Profile Button -->
61-
<a href="{{ .Page.RelPermalink }}"
62-
class="block w-full bg-blue-600 text-white text-center px-4 py-2 rounded-lg font-medium hover:bg-blue-700 transition-colors duration-200">
83+
<a href="{{ $page.RelPermalink }}"
84+
class="mt-auto block w-full bg-blue-600 text-white text-center px-4 py-2 rounded-lg font-medium hover:bg-blue-700 transition-colors duration-200">
6385
<i class="fas fa-arrow-right mr-2"></i>View Profile
6486
</a>
6587
</div>
@@ -78,16 +100,16 @@ <h2 class="text-2xl font-bold text-center text-gray-900 mb-2">
78100

79101
{{ define "scripts" }}
80102
<script>
81-
// Author search functionality
103+
// Author search: match on name or tagline
82104
document.getElementById('author-search').addEventListener('input', function(e) {
83105
const searchTerm = e.target.value.toLowerCase();
84106
const authors = document.querySelectorAll('.author-card');
85-
107+
86108
authors.forEach(author => {
87-
const authorName = author.dataset.author;
88-
89-
if (authorName.includes(searchTerm)) {
90-
author.style.display = 'block';
109+
const haystack = (author.dataset.author + ' ' + (author.dataset.tagline || ''));
110+
111+
if (haystack.includes(searchTerm)) {
112+
author.style.display = '';
91113
} else {
92114
author.style.display = 'none';
93115
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{{- /*
2+
Resolve and render an author avatar.
3+
Params: page (the author/term page, may be nil), name (string), size (px),
4+
class (CSS classes for the <img>).
5+
Precedence: avatar -> gravatar_hash -> email -> identicon fallback.
6+
*/ -}}
7+
{{- $page := .page -}}
8+
{{- $name := .name -}}
9+
{{- $size := .size | default 160 -}}
10+
{{- $class := .class | default "" -}}
11+
{{- $src := "" -}}
12+
{{- with $page -}}
13+
{{- $p := .Params -}}
14+
{{- if $p.avatar -}}
15+
{{- $src = $p.avatar -}}
16+
{{- else if $p.gravatar_hash -}}
17+
{{- $src = printf "https://www.gravatar.com/avatar/%s?s=%d&d=identicon" $p.gravatar_hash $size -}}
18+
{{- else if $p.email -}}
19+
{{- $hash := md5 (lower (strings.TrimSpace $p.email)) -}}
20+
{{- $src = printf "https://www.gravatar.com/avatar/%s?s=%d&d=identicon" $hash $size -}}
21+
{{- end -}}
22+
{{- end -}}
23+
{{- if not $src -}}
24+
{{- $seed := md5 (lower $name) -}}
25+
{{- $src = printf "https://www.gravatar.com/avatar/%s?s=%d&d=identicon&f=y" $seed $size -}}
26+
{{- end -}}
27+
<img src="{{ $src }}" alt="{{ $name }} avatar" class="{{ $class }}" width="{{ $size }}" height="{{ $size }}" loading="lazy">
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{{- /*
2+
Render an author's social links as icon anchors.
3+
Params: page (the author/term page), class (CSS classes for the wrapper).
4+
Each link value is a full URL. Unset links are skipped.
5+
*/ -}}
6+
{{- $page := .page -}}
7+
{{- $class := .class | default "" -}}
8+
{{- with $page -}}
9+
{{- $p := .Params -}}
10+
{{- $links := slice
11+
(dict "key" "website" "icon" "fas fa-globe" "label" "Website")
12+
(dict "key" "github" "icon" "fab fa-github" "label" "GitHub")
13+
(dict "key" "twitter" "icon" "fab fa-twitter" "label" "Twitter")
14+
(dict "key" "mastodon" "icon" "fab fa-mastodon" "label" "Mastodon")
15+
(dict "key" "linkedin" "icon" "fab fa-linkedin" "label" "LinkedIn")
16+
(dict "key" "bluesky" "icon" "fas fa-cloud" "label" "Bluesky")
17+
-}}
18+
{{- $any := false -}}
19+
{{- range $links }}{{ if index $p .key }}{{ $any = true }}{{ end }}{{ end -}}
20+
{{- if $any -}}
21+
<div class="{{ $class }}">
22+
{{- range $links -}}
23+
{{- $icon := .icon -}}
24+
{{- $label := .label -}}
25+
{{- with index $p .key }}
26+
<a href="{{ . }}" target="_blank" rel="noopener noreferrer me"
27+
class="text-gray-400 hover:text-blue-600 transition-colors duration-200"
28+
title="{{ $label }}" aria-label="{{ $label }}">
29+
<i class="{{ $icon }}"></i>
30+
</a>
31+
{{- end -}}
32+
{{- end }}
33+
</div>
34+
{{- end -}}
35+
{{- end -}}

themes/powershell-community/layouts/taxonomy/author.html

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
{{ define "main" }}
22

3+
{{ $display := .Title }}
4+
{{ with .Params.preferred_name }}{{ $display = . }}{{ end }}
5+
6+
{{/* An enriched profile with no tagged content almost always means the directory
7+
slug does not match any author name in frontmatter — flag it at build time. */}}
8+
{{ if and .File (eq (len .Pages) 0) }}
9+
{{ warnf "author profile %q has no published content — its directory slug likely does not match any author name in `authors:` frontmatter" .RelPermalink }}
10+
{{ end }}
11+
312
<!-- Authors Header -->
413
<section class="bg-gradient-to-r from-blue-600 to-blue-800 py-10">
514
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
615
<div class="text-center text-white">
7-
<div class="w-16 h-16 mx-auto mb-4 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
8-
<i class="fas fa-user-edit text-3xl"></i>
16+
<div class="flex justify-center mb-4">
17+
{{ partial "author-avatar.html" (dict "page" . "name" .Title "size" 128 "class" "w-24 h-24 rounded-full object-cover ring-4 ring-white/30") }}
918
</div>
10-
<h1 class="text-4xl lg:text-5xl font-bold mb-2">{{ .Title }}</h1>
19+
<h1 class="text-4xl lg:text-5xl font-bold mb-2">{{ $display }}</h1>
20+
{{ with .Params.tagline }}
21+
<p class="text-xl opacity-90">{{ . }}</p>
22+
{{ else }}
1123
<p class="text-xl opacity-90">Explore articles and content from this author</p>
24+
{{ end }}
1225
</div>
1326
</div>
1427
</section>
@@ -26,20 +39,22 @@ <h1 class="text-4xl lg:text-5xl font-bold mb-2">{{ .Title }}</h1>
2639

2740
<!-- Author Summary -->
2841
<div class="bg-white rounded-xl shadow-lg p-6 mb-8">
29-
<div class="flex items-center mb-4">
30-
<div class="w-16 h-16 bg-gradient-to-br from-blue-400 to-blue-600 rounded-full flex items-center justify-center text-white mr-5">
31-
<i class="fas fa-user text-2xl"></i>
32-
</div>
33-
<div>
34-
<h2 class="text-3xl font-bold text-gray-900">{{ .Title }}</h2>
42+
<div class="flex flex-col sm:flex-row sm:items-center gap-5">
43+
{{ partial "author-avatar.html" (dict "page" . "name" .Title "size" 112 "class" "w-20 h-20 rounded-full object-cover ring-2 ring-blue-100 shrink-0") }}
44+
<div class="flex-1">
45+
<h2 class="text-3xl font-bold text-gray-900">{{ $display }}</h2>
3546
<p class="text-gray-600">
3647
{{ len $articles }} article{{ if ne (len $articles) 1 }}s{{ end }}
3748
{{ if $podcasts }}
3849
&nbsp;•&nbsp; {{ len $podcasts }} podcast episode{{ if ne (len $podcasts) 1 }}s{{ end }}
3950
{{ end }}
4051
</p>
52+
{{ partial "author-links.html" (dict "page" . "class" "flex gap-4 text-lg mt-3") }}
4153
</div>
4254
</div>
55+
{{ with .Content }}
56+
<div class="prose max-w-none mt-6 pt-6 border-t border-gray-100">{{ . }}</div>
57+
{{ end }}
4358
</div>
4459

4560
<!-- Content Grid -->

0 commit comments

Comments
 (0)