Skip to content
Merged
19 changes: 19 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,22 @@ and used as a site section, "Module" means specifically these org-stewarded proj
— not the generic sense of "any PowerShell module you `Install-Module`", which is the
subject of the site as a whole.
_Avoid (for this concept)_: project, tool, package

**Author**:
A person credited as a contributor to site content (articles, podcast episodes) via
the `authors:` frontmatter. Each distinct Author is surfaced as a taxonomy term with
its own profile page, and may optionally describe themselves (avatar, tagline, bio,
links). An Author exists the moment they are credited on a piece of content; the
self-description is optional enrichment, not what makes someone an Author.
_Avoid (for this concept)_: contributor, writer, user, account

The **author name** stored in `authors:` frontmatter is both the display byline and
the source of the Profile URL slug — it is the stable key. An Author may additionally
set a **preferred name** on their Profile, which overrides only how their name is
*displayed*; it never changes the slug or the byline key. Authors without a Profile
display their author name unchanged.

**Profile**:
The per-Author page at `/authors/<name>/` listing that Author's content and, when
provided, their self-description. Distinct from the **Author list** — the index page
at `/authors/` showing every Author as a card.
62 changes: 62 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,68 @@ If you're not comfortable with Git, you can pitch or submit your article through
- Include a brief intro that tells readers what they'll learn
- Test any code examples before submitting

## Adding Your Author Profile

Once you've been credited as an author, you can give yourself a richer profile page at
`/authors/<your-name>/` — an avatar, a tagline, social links, and a bio. Profiles are
**opt-in**: if you don't add one, your name still works as a byline exactly as before.

Your profile lives in a single file at `content/authors/<slug>/_index.md`, where `<slug>`
**must** match your name as it appears in articles' `authors:` front matter (lowercased,
spaces become hyphens, punctuation dropped — e.g. `Jane Doe` → `jane-doe`). Getting the
slug wrong creates a page that attaches to nothing, so let the helper script do it:

```powershell
./tools/new-author.ps1 "Jane Doe"
```

This scaffolds `content/authors/jane-doe/_index.md` with every field commented. Fill in
what you want and delete the rest:

```yaml
---
title: "Jane Doe" # required — keep this as your byline name
preferred_name: "Jane" # optional — changes only how your name is displayed
tagline: "Cloud automation, mostly."
# --- Avatar (first one set wins) ---
# avatar: "/images/authors/jane.jpg" # an image you host in this repo
gravatar_hash: "..." # MD5 of your lowercased email — keeps your email private
# email: "jane@example.com" # convenient, but stored publicly in this repo
# --- Links (full URLs) ---
github: "https://github.com/janedoe"
website: "https://janedoe.dev"
# twitter / mastodon / linkedin / bluesky also supported
---

Your bio in Markdown goes here. It shows on your profile page.
```

### Choosing an avatar

The avatar is resolved in this order: `avatar` → `gravatar_hash` → `email` → an
auto-generated identicon. To use [Gravatar](https://gravatar.com/) without putting your
email in the repo, store the **MD5 hash** of your lowercased email instead:

```powershell
$email = "jane@example.com"
[System.BitConverter]::ToString(
[System.Security.Cryptography.MD5]::Create().ComputeHash(
[System.Text.Encoding]::UTF8.GetBytes($email.Trim().ToLowerInvariant())
)
).Replace("-", "").ToLowerInvariant()
```

### Changed your name?

If your byline ever needs to change across all your content (and your profile URL), use:

```powershell
./tools/new-author.ps1 "Old Name" -To "New Name"
```

This rewrites the byline everywhere it appears and adds a redirect so your old profile
URL keeps working. Open a PR with the result.

## Submitting a Community Event

Add your PowerShell-related event to our community calendar:
Expand Down
38 changes: 32 additions & 6 deletions archetypes/authors.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
---
title: '{{ replace .File.ContentBaseName "-" " " | title }}'
description: ""
layout: "authors"
website: ""
twitter: ""
github: ""
# The directory this file lives in MUST equal Hugo's slug of your author name
# exactly as it appears in articles' `authors:` frontmatter
# (lowercase, spaces -> hyphens, punctuation dropped).
# Use `tools/new-author.ps1 "Your Name"` to scaffold this with the correct slug.

# `title` is required by Hugo. Keep it as your author name (the byline key).
title: "{{ replace .File.ContentBaseName "-" " " | title }}"

# Optional: override only how your name is *displayed* (byline + slug are unchanged).
# preferred_name: ""

# One short line shown on your card in the author list.
tagline: ""

# --- Avatar (first match wins) -------------------------------------------------
# 1. avatar: path or URL to an image you control (bypasses Gravatar)
# 2. gravatar_hash: MD5 of your lowercased email (keeps your email out of the repo)
# 3. email: plain email; hashed to a Gravatar at build time (stored publicly)
# If none are set, a stable identicon is generated from your name.
# avatar: ""
# gravatar_hash: ""
# email: ""

# --- Links (full URLs) ---------------------------------------------------------
# website: ""
# github: ""
# twitter: ""
# mastodon: ""
# linkedin: ""
# bluesky: ""
---

<!-- Your bio in Markdown. Shown on your profile page. -->
16 changes: 16 additions & 0 deletions content/authors/gilbert-sanchez/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: "Gilbert Sanchez"
tagline: "PowerShell nerd, open source contributor, and accidental homelab architect"
gravatar_hash: "8a5f80a8824ba8c72c0295b000476718"
website: "https://gilbertsanchez.com"
github: "https://github.com/HeyItsGilbert"
mastodon: "https://fosstodon.org/@HeyItsGilbert"
linkedin: "https://www.linkedin.com/in/gilbertsanchez"
bluesky: "https://bsky.app/profile/gilbertsanchez.com"
---

I maintain projects like psake, lead a team of engineers by day, and automate
everything in sight by night — the house runs on Home Assistant and I have
no regrets. I also care a lot about making tech more accessible and welcoming,
especially for neurodiverse folks. If it can be scripted or made more human,
it probably already has been. Find me at [gilbertsanchez.com](https://gilbertsanchez.com/).
45 changes: 45 additions & 0 deletions docs/adr/0002-author-profiles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Author profiles: enrich taxonomy term pages, keep the display name as the key

Authors are a Hugo taxonomy derived from the `authors:` frontmatter on ~1,045 articles
and podcast episodes (see [[Author]] in `CONTEXT.md`). The term pages at `/authors/<name>/`
are auto-generated and show only a name and a content list. To let an Author describe
themselves (avatar, tagline, bio, links), we enrich each term page with an **optional**
content file at `content/authors/<slug>/_index.md`. Profiles are **opt-in via PR** — not
pre-scaffolded — so most Authors have no file and must keep rendering correctly.

The load-bearing decision is what gets stored in every article's `authors:` frontmatter.
We keep the **display name** there (`authors: [James Petty]`), exactly as today. The name
is both the byline and the source of the Profile slug — the stable key. A Profile may set
a **preferred name** that overrides only how the name is *displayed*; it never changes the
slug or the byline. Authors without a Profile render their author name unchanged.

## Considered options

- **Slug as the key** (`authors: [james-petty]`, display name rendered from the Profile) —
rejected. It decouples slug from name, but the display name would then live *only* in the
Profile. With ~70 of 80 Authors un-enriched at launch, their bylines and cards would fall
back to a humanized slug — `darren-mar-elia` → "Darren Mar Elia" (wrong hyphen),
`jasonmorgan` → "Jasonmorgan". It also forces a repo-wide migration of all 1,045 files.
- **Display name as the key, with an optional preferred-name override** — chosen. Zero
migration, correct bylines for every Author for free, and the changeable-display-name
benefit is preserved for those who opt in. The only thing it gives up is a slug decoupled
from the original name — but the slug is a URL we want stable anyway, and renaming it is a
redirect-and-find/replace job in *either* model.

A central `data/authors.yaml` was also rejected in favor of taxonomy term content files:
term files bind to the existing `/authors/<name>/` page automatically and carry a Markdown
bio, which a data file cannot do naturally.

## Consequences

- The profile content file's directory **must** equal Hugo's slug of the exact author
string; a mismatch yields an orphan page that enriches nothing. A `new-author.ps1`
scaffolder generates the file with the right slug, and a build-time check flags author
content files whose slug matches no taxonomy term.
- Renaming a slug is the rare, deliberate case: `new-author.ps1` rewrites the term across
content and adds an `aliases` redirect on the Profile to preserve the old URL.
- Avatars resolve `avatar` → `gravatar_hash` → `email` → `identicon` fallback. Raw `email`
is convenient but lands in the public repo; `gravatar_hash` lets the privacy-conscious
avoid that, and an explicit `avatar` image bypasses Gravatar entirely.
- The Author list orders enriched profiles first, then by count, so a partially-filled grid
reads as intentional rather than sparse.
66 changes: 44 additions & 22 deletions themes/powershell-community/layouts/_default/authors.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h1 class="text-4xl lg:text-5xl font-bold mb-2">{{ .Title }}</h1>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-md mx-auto">
<div class="relative">
<input type="text" id="author-search"
<input type="text" id="author-search"
placeholder="Search authors..."
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">
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
Expand All @@ -30,36 +30,58 @@ <h1 class="text-4xl lg:text-5xl font-bold mb-2">{{ .Title }}</h1>
<!-- Authors Grid -->
<section class="py-10 bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{{ $authorTaxonomy := .Site.Taxonomies.authors }}
{{ if $authorTaxonomy }}
{{ $pairs := .Site.Taxonomies.authors.ByCount }}
{{ if $pairs }}
{{/* Enriched profiles (backed by a content file) lead, then bare authors,
preserving ByCount order within each group. */}}
{{ $enriched := slice }}
{{ $bare := slice }}
{{ range $pairs }}
{{ if .Page.File }}
{{ $enriched = $enriched | append . }}
{{ else }}
{{ $bare = $bare | append . }}
{{ end }}
{{ end }}
{{ $ordered := $enriched | append $bare }}
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3" id="authors-container">
{{ range $authorTaxonomy.ByCount }}
<div class="bg-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 overflow-hidden author-card"
data-author="{{ .Term | lower }}">
<div class="p-6">
{{ range $ordered }}
{{ $page := .Page }}
{{ $name := $page.Title }}
{{ $display := $name }}
{{ with $page.Params.preferred_name }}{{ $display = . }}{{ end }}
{{ $tagline := $page.Params.tagline }}
<div class="bg-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 overflow-hidden author-card flex flex-col"
data-author="{{ $display | lower }}"
data-tagline="{{ with $tagline }}{{ . | lower }}{{ end }}">
<div class="p-6 flex flex-col flex-1">
<!-- Author Avatar -->
<div class="flex justify-center mb-4">
<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">
<i class="fas fa-user text-2xl"></i>
</div>
{{ 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") }}
</div>

<!-- Author Info -->
<h2 class="text-2xl font-bold text-center text-gray-900 mb-2">
<a href="{{ .Page.RelPermalink }}" class="hover:text-blue-600 transition-colors duration-200">
{{ .Page.Title }}
<h2 class="text-2xl font-bold text-center text-gray-900 mb-1">
<a href="{{ $page.RelPermalink }}" class="hover:text-blue-600 transition-colors duration-200">
{{ $display }}
</a>
</h2>

{{ with $tagline }}
<p class="text-center text-gray-600 text-sm mb-3 line-clamp-2">{{ . }}</p>
{{ end }}

<!-- Article Count -->
<p class="text-center text-gray-600 mb-4">
<span class="text-3xl font-bold text-blue-600">{{ .Count }}</span><br>
<span class="text-sm">article{{ if gt .Count 1 }}s{{ end }} published</span>
<span class="text-sm">article{{ if ne .Count 1 }}s{{ end }} published</span>
</p>

{{ partial "author-links.html" (dict "page" $page "class" "flex justify-center gap-4 text-lg mb-4") }}

<!-- View Profile Button -->
<a href="{{ .Page.RelPermalink }}"
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">
<a href="{{ $page.RelPermalink }}"
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">
<i class="fas fa-arrow-right mr-2"></i>View Profile
</a>
</div>
Expand All @@ -78,16 +100,16 @@ <h2 class="text-2xl font-bold text-center text-gray-900 mb-2">

{{ define "scripts" }}
<script>
// Author search functionality
// Author search: match on name or tagline
document.getElementById('author-search').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const authors = document.querySelectorAll('.author-card');

authors.forEach(author => {
const authorName = author.dataset.author;
if (authorName.includes(searchTerm)) {
author.style.display = 'block';
const haystack = (author.dataset.author + ' ' + (author.dataset.tagline || ''));

if (haystack.includes(searchTerm)) {
author.style.display = '';
} else {
author.style.display = 'none';
}
Expand Down
18 changes: 2 additions & 16 deletions themes/powershell-community/layouts/_default/single.html
Original file line number Diff line number Diff line change
Expand Up @@ -118,22 +118,8 @@ <h3 class="text-lg font-semibold text-gray-900 mb-3">Tags</h3>
</div>
{{ end }}

<!-- Author Bio (if available) -->
{{ with .Params.author_bio }}
<div class="mt-6 pt-6 border-t border-gray-200">
<div class="bg-gray-50 rounded-xl p-6">
<div class="flex items-start space-x-4">
<div class="w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center">
<i class="fas fa-user text-white text-xl"></i>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-gray-900 mb-2">About <a href="{{ "/authors/" | relURL }}{{ $.Params.author | urlize }}/" class="hover:text-blue-600 transition-colors duration-200">{{ $.Params.author }}</a></h3>
<p class="text-gray-600">{{ . }}</p>
</div>
</div>
</div>
</div>
{{ end }}
<!-- About the Author (sourced from author profiles) -->
{{ partial "article-author-about.html" . }}

<!-- Related Posts -->
{{ $related := .Site.RegularPages.Related . | first 3 }}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{{- /*
"About the Author" section for article/single pages, sourced from each credited
author's profile (taxonomy term content file). Renders only for authors who have
opted into a profile, so articles by un-enriched authors are unchanged.
*/ -}}
{{- $enriched := slice -}}
{{- with .GetTerms "authors" -}}
{{- range . -}}
{{- if .File -}}{{- $enriched = $enriched | append . -}}{{- end -}}
{{- end -}}
{{- end -}}
{{- with $enriched -}}
<div class="mt-8 pt-6 border-t border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">About the Author{{ if gt (len .) 1 }}s{{ end }}</h3>
<div class="space-y-4">
{{- range . -}}
{{- $display := .Title -}}{{- with .Params.preferred_name -}}{{- $display = . -}}{{- end -}}
<div class="bg-gray-50 rounded-xl p-6">
<div class="flex items-start gap-4">
{{ partial "author-avatar.html" (dict "page" . "name" .Title "size" 96 "class" "w-16 h-16 rounded-full object-cover ring-2 ring-blue-100 shrink-0") }}
<div class="flex-1 min-w-0">
<h4 class="text-lg font-semibold text-gray-900">
<a href="{{ .RelPermalink }}" class="hover:text-blue-600 transition-colors duration-200">{{ $display }}</a>
</h4>
{{ with .Params.tagline }}<p class="text-sm text-gray-500 mb-2">{{ . }}</p>{{ end }}
{{ with .Content }}<div class="prose prose-sm max-w-none text-gray-600">{{ . }}</div>{{ end }}
{{ partial "author-links.html" (dict "page" . "class" "flex gap-4 text-lg mt-3") }}
<a href="{{ .RelPermalink }}" class="inline-flex items-center text-sm font-medium text-blue-600 hover:text-blue-700 mt-3">
View profile<i class="fas fa-arrow-right ml-2"></i>
</a>
</div>
</div>
</div>
{{- end -}}
</div>
</div>
{{- end -}}
27 changes: 27 additions & 0 deletions themes/powershell-community/layouts/partials/author-avatar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{{- /*
Resolve and render an author avatar.
Params: page (the author/term page, may be nil), name (string), size (px),
class (CSS classes for the <img>).
Precedence: avatar -> gravatar_hash -> email -> identicon fallback.
*/ -}}
{{- $page := .page -}}
{{- $name := .name -}}
{{- $size := .size | default 160 -}}
{{- $class := .class | default "" -}}
{{- $src := "" -}}
{{- with $page -}}
{{- $p := .Params -}}
{{- if $p.avatar -}}
{{- $src = $p.avatar -}}
{{- else if $p.gravatar_hash -}}
{{- $src = printf "https://www.gravatar.com/avatar/%s?s=%d&d=identicon" $p.gravatar_hash $size -}}
{{- else if $p.email -}}
{{- $hash := md5 (lower (strings.TrimSpace $p.email)) -}}
{{- $src = printf "https://www.gravatar.com/avatar/%s?s=%d&d=identicon" $hash $size -}}
{{- end -}}
{{- end -}}
{{- if not $src -}}
{{- $seed := md5 (lower $name) -}}
{{- $src = printf "https://www.gravatar.com/avatar/%s?s=%d&d=identicon&f=y" $seed $size -}}
{{- end -}}
<img src="{{ $src }}" alt="{{ $name }} avatar" class="{{ $class }}" width="{{ $size }}" height="{{ $size }}" loading="lazy">
Loading
Loading