Skip to content

Commit cffbdca

Browse files
Link calendar entries back to their user group (#526)
## Summary ```diff themes/powershell-community/layouts/ ├── _default/calendar.html # upcoming card: <a> → <div>, title/arrow become their own links, │ # badge slotted into the meta row on both upcoming + past cards └── partials/ + ├── calendar-matched-user-group.html # match externalUrl's Meetup host → user-group page + └── calendar-user-group-badge.html # render logo/icon + name link to /user-groups/<slug>/ content/calendar/ohio-powershell-user-group.md # drop now-redundant hand-written group link docs/adr/0006-calendar-user-group-linking.md # why: runtime URL match, not a stored field ``` ```mermaid flowchart LR A[calendar entry<br/>Params.externalUrl] -->|meetup.com host| B[calendar-matched-user-group.html] B -->|scans| C[/user-groups/*.md<br/>Params.links/] C -->|slug match| D[matched Page] D --> E[calendar-user-group-badge.html] E -->|renders| F[logo/icon + name<br/>→ /user-groups/slug/] ``` Matching happens at render time against the Meetup slug embedded in both the calendar entry's `externalUrl` and the user group's `links[].url` — no new frontmatter field, no resync of existing entries. Rationale and rejected alternatives (explicit `userGroup` field, hybrid) are in [ADR 0006](docs/adr/0006-calendar-user-group-linking.md). ## Evidence - **Before:** calendar cards were visually identical regardless of source group; the whole card was one `<a>` to the external event, with no path back to the group's page (Ohio's one manual entry had a hand-written prose link as the only exception). - **After:** built the site with `hugo --gc` and inspected `public/calendar/index.html`: - 15/15 Meetup-linked entries (13 synced + 2 hand-authored, including one I'd initially miscategorized as a plain conference until its `externalUrl` turned out to be a Meetup link) render a badge to the correct `/user-groups/<slug>/` page. - Groups with a `logo` (Ohio, Swiss, Inn-Salzach) render their image; groups without one (Hamburg, Research Triangle) fall back to the same `fa-people-group` icon the user-groups list page already uses. - Genuine non-group conferences (PSConfEU, Summit, SQL Saturday, Day of Data) confirmed to render with **no** badge — unchanged. ## Merge Danger **Door:** Two-way — template/partial change only, trivially revertable. **Blast Radius:** `/calendar/` page only. Upcoming-event cards change structure (no longer one whole-card link; title and the arrow icon are now separate click targets), which is a layout/interaction change worth a visual check but not a breaking one — no other templates or content schemas are touched.
1 parent c744462 commit cffbdca

8 files changed

Lines changed: 125 additions & 10 deletions

File tree

‎assets/css/tailwind.css‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎content/calendar/ohio-powershell-user-group.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,4 @@ virtual: true
88
---
99
The Ohio PowerShell User Group puts common PowerShell wisdom to the test —
1010
benchmarking competing approaches and exploring performance, the pipeline,
11-
parallel execution, and output behavior. See the
12-
[group page](/user-groups/ohio-powershell/) for more.
11+
parallel execution, and output behavior.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Calendar entries link back to their user group by matching the Meetup URL host at render time, not an explicit stored field
2+
3+
Calendar entries (`content/calendar/meetup-*.md`) are generated by
4+
`.github/scripts/sync-meetup-events.mjs` from each user group's
5+
`links[].url` on `meetup.com`; manual entries like
6+
`ohio-powershell-user-group.md` also happen to carry a `meetup.com`
7+
`externalUrl`. To show a group's logo/name badge on the calendar page and
8+
link it back to `/user-groups/<slug>/`, the template
9+
(`themes/powershell-community/layouts/_default/calendar.html`) matches each
10+
entry's `externalUrl` host against every user group's `links[].url` host at
11+
render time, rather than the sync script stamping a `userGroup: <slug>`
12+
field into generated frontmatter.
13+
14+
## Considered options
15+
16+
- **Explicit `userGroup` field, written by the sync script** — rejected (for
17+
now). More "obviously correct" in the abstract, but the script currently
18+
discards the per-group URL once it flattens all groups' Meetup slugs into
19+
one set (`groups()` in `sync-meetup-events.mjs`), so this would mean
20+
threading that mapping through, resyncing/backfilling the 13 existing
21+
generated entries, and still leaving manual entries unmatched unless an
22+
author remembers to set the field by hand.
23+
- **Runtime match on Meetup URL host** — chosen. The join key (the Meetup
24+
group slug embedded in both the calendar entry's `externalUrl` and the
25+
user group's `links[].url`) is already 100% reliable across every existing
26+
entry, synced or manual, with no script change and no backfill. It also
27+
means Ohio's hand-authored entry matches for free, since its
28+
`externalUrl` happens to be a `meetup.com` link too.
29+
30+
## Consequences
31+
32+
- Linking is **implicitly scoped to Meetup-sourced events.** A future
33+
non-Meetup user-group event (e.g. Eventbrite) will render with no group
34+
badge unless this matching logic is extended to other hosts, or unless we
35+
revisit this decision in favor of an explicit field.
36+
- The match is **deliberately on URL host, not on a link's declared `icon` or
37+
`name`.** A user group that mislabels its Meetup link's icon still matches
38+
correctly, because the template never looks at it.
39+
- The calendar template now has to enumerate `/user-groups/` pages per
40+
rendered event to find a match. This is a cheap, build-time cost (list
41+
sizes are small), not a runtime one — Hugo is a static site generator.

‎purgecss.config.cjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ module.exports = {
2222
// These never appear in committed markup, so pin the whole status palette.
2323
'bg-blue-500', 'bg-green-500', 'bg-purple-500',
2424
'bg-orange-500', 'bg-red-500', 'bg-yellow-500',
25+
// Only rendered inside the calendar's upcoming-events loop, so a CI
26+
// build with zero upcoming events would otherwise purge it.
27+
'group-hover:text-purple-600',
2528
],
2629
},
2730
output: 'assets/css/tailwind.css',

‎themes/powershell-community/layouts/_default/calendar.html‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ <h2 class="text-3xl font-bold text-gray-900">Upcoming Events</h2>
5252
{{ if $upcoming }}
5353
<div class="space-y-5 mb-10">
5454
{{ range $upcoming }}
55-
<a href="{{ .Params.externalUrl }}" target="_blank" rel="noopener"
56-
class="block bg-gray-50 rounded-xl border border-gray-200 hover:border-purple-400 hover:shadow-md transition-all duration-200 overflow-hidden">
55+
{{ $matchedGroup := partial "calendar-matched-user-group.html" (dict "externalUrl" .Params.externalUrl "site" $.Site) }}
56+
<div class="bg-gray-50 rounded-xl border border-gray-200 hover:border-purple-400 hover:shadow-md transition-all duration-200 overflow-hidden">
5757
<div class="md:flex">
5858
<!-- Date Block -->
5959
<div class="md:w-36 bg-purple-600 text-white p-6 flex flex-col items-center justify-center text-center flex-shrink-0">
@@ -64,8 +64,10 @@ <h2 class="text-3xl font-bold text-gray-900">Upcoming Events</h2>
6464
</div>
6565
<!-- Event Details -->
6666
<div class="p-6 flex-1">
67-
<h3 class="text-xl font-bold text-gray-900 mb-2">{{ .Title }}</h3>
68-
<p class="text-gray-600 mb-3">{{ .Summary | plainify | truncate 240 }}</p>
67+
<a href="{{ .Params.externalUrl }}" target="_blank" rel="noopener" class="group block">
68+
<h3 class="text-xl font-bold text-gray-900 mb-2 underline group-hover:text-purple-600">{{ .Title }}</h3>
69+
<p class="text-gray-600 mb-3">{{ .Summary | plainify | truncate 240 }}</p>
70+
</a>
6971
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500">
7072
<div class="flex items-center">
7173
<i class="fas fa-map-marker-alt text-purple-600 mr-2"></i>
@@ -87,14 +89,21 @@ <h3 class="text-xl font-bold text-gray-900 mb-2">{{ .Title }}</h3>
8789
Virtual
8890
</div>
8991
{{ end }}
92+
{{ with $matchedGroup }}
93+
<div class="flex items-center">
94+
{{ partial "calendar-user-group-badge.html" . }}
95+
</div>
96+
{{ end }}
9097
</div>
9198
</div>
9299
<!-- Arrow -->
93100
<div class="hidden md:flex items-center pr-6">
94-
<i class="fas fa-external-link-alt text-gray-400"></i>
101+
<a href="{{ .Params.externalUrl }}" target="_blank" rel="noopener" aria-label="View details for {{ .Title }}">
102+
<i class="fas fa-external-link-alt text-gray-400"></i>
103+
</a>
95104
</div>
96105
</div>
97-
</a>
106+
</div>
98107
{{ end }}
99108
</div>
100109
{{ else }}
@@ -115,7 +124,8 @@ <h2 class="text-3xl font-bold text-gray-900">Past Events</h2>
115124
</div>
116125
<div class="space-y-4 mb-10">
117126
{{ range (sort $past "Params.startDate" "desc") }}
118-
<div class="bg-gray-50 rounded-lg border border-gray-200 p-4 opacity-75">
127+
{{ $matchedGroup := partial "calendar-matched-user-group.html" (dict "externalUrl" .Params.externalUrl "site" $.Site) }}
128+
<div class="bg-gray-50 rounded-lg border border-gray-200 p-4">
119129
<div class="flex items-center justify-between">
120130
<div>
121131
<h3 class="font-semibold text-gray-700">{{ .Title }}</h3>
@@ -125,6 +135,9 @@ <h3 class="font-semibold text-gray-700">{{ .Title }}</h3>
125135
{{ $startDate := time .Params.startDate }}
126136
{{ $startDate.Format "January 2, 2006" }}
127137
</span>
138+
{{ with $matchedGroup }}
139+
<span>{{ partial "calendar-user-group-badge.html" . }}</span>
140+
{{ end }}
128141
</div>
129142
</div>
130143
{{ if .Params.externalUrl }}

‎themes/powershell-community/layouts/_default/single.html‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444

4545
<!-- Title -->
4646
<h1 class="text-3xl lg:text-4xl font-bold text-gray-900 mb-4 leading-tight">{{ .Title }}</h1>
47+
{{ if eq .Section "calendar" }}
48+
{{ $matchedGroup := partial "calendar-matched-user-group.html" (dict "externalUrl" .Params.externalUrl "site" .Site) }}
49+
{{ with $matchedGroup }}
50+
<div class="mb-4">{{ partial "calendar-user-group-badge.html" . }}</div>
51+
{{ end }}
52+
{{ end }}
4753

4854
<!-- Author and Reading Time -->
4955
<div class="flex items-center justify-between">
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{{/*
2+
Resolves a calendar entry's Meetup externalUrl to the user group that owns
3+
it, by matching the Meetup group slug embedded in the calendar entry's URL
4+
against the slug embedded in each user group's `links[].url`. See
5+
docs/adr/0006-calendar-user-group-linking.md for why this is a runtime
6+
match rather than a stored field.
7+
8+
@param .externalUrl string: the calendar entry's `externalUrl` param
9+
@param .site Site: the site context, for GetPage
10+
@returns Page|string: the matched user-group page, or "" if none
11+
*/}}
12+
{{- $url := .externalUrl -}}
13+
{{- $site := .site -}}
14+
{{- /* Case-insensitive on scheme+host only: a locale prefix is only
15+
recognized in Meetup's own convention (lowercase language, UPPERCASE
16+
region, e.g. "de-DE") so it isn't confused with a lowercase two-letter-
17+
pair group slug like "ps-ug". */ -}}
18+
{{- $slugPattern := `^(?i:https?://(?:www\.)?meetup\.com)/(?:[a-z]{2}-[A-Z]{2}/)?([^/?#]+).*$` -}}
19+
{{- $match := "" -}}
20+
{{- if $url -}}
21+
{{- $eventSlug := replaceRE $slugPattern "$1" $url -}}
22+
{{- if ne $eventSlug $url -}}
23+
{{- $groupsPage := $site.GetPage "/user-groups" -}}
24+
{{- if $groupsPage -}}
25+
{{- range $groupsPage.Pages -}}
26+
{{- $group := . -}}
27+
{{- range .Params.links -}}
28+
{{- $linkSlug := replaceRE $slugPattern "$1" .url -}}
29+
{{- if and (ne $linkSlug .url) (eq (lower $linkSlug) (lower $eventSlug)) -}}
30+
{{- $match = $group -}}
31+
{{- end -}}
32+
{{- end -}}
33+
{{- end -}}
34+
{{- end -}}
35+
{{- end -}}
36+
{{- end -}}
37+
{{- return $match -}}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{{/*
2+
Renders a small logo/icon + name badge linking to a user group's page.
3+
4+
@param . Page: the user-group page to render a badge for
5+
*/}}
6+
{{- $group := . -}}
7+
<a href="{{ $group.RelPermalink }}" class="inline-flex items-center gap-1.5 text-blue-600 hover:text-blue-700 underline">
8+
<span class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-blue-100 overflow-hidden flex-shrink-0">
9+
{{ if $group.Params.logo }}
10+
<img src="{{ $group.Params.logo }}" alt="" width="20" height="20" class="w-full h-full object-contain">
11+
{{ else }}
12+
<i class="fas fa-people-group text-xs" aria-hidden="true"></i>
13+
{{ end }}
14+
</span>
15+
{{ $group.Title }}
16+
</a>

0 commit comments

Comments
 (0)