fix(design): restore journalistic nav shell (#213)
Restore the publication-front shell that PR #205 over-pruned: dense navigation, topic shortcuts, Pagefind search, and a useful all-weeks archive while keeping the explainer on About.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Juan Manuel Servera committed
May 25, 2026 at 23:40 UTC
357bfde70aa8f064abf3c87e00149dcd3eca653b
10 files changed
+449
-25
.squad/decisions/inbox/calculon-journalistic-shell-baseline.md
new
+19
@@ -0,0 +1,19 @@
1
+# Decision: Journalistic shell baseline
2
+
3
+**Date:** 2026-05-25T23:31:03+02:00
4
+**Owner:** Calculon
5
+**Status:** Proposed
6
+
7
+## Decision
8
+
9
+The journalistic shell is a non-negotiable baseline for SquadScope. Navigation density, search, weekly archive access, and topic shortcuts must remain present in future home-page cleanups.
10
+
11
+## Rationale
12
+
13
+jmservera rejected the PR #205 revision because it over-pruned the publication shell. Future cleanups may relocate explanatory body content, but they must not remove the publication affordances that make the site feel like an editorial front page.
14
+
15
+## Implications
16
+
17
+- Keep top-level access to all weeks, topics, and search.
18
+- Keep a home-page rail or equivalent surfacing active topics and recent issues.
19
+- Preserve `/about/` as the home for the explainer and transparency dashboard.
assets/css/common/header.css
+51
-1
@@ -176,7 +176,7 @@
176
display: block;
177
}
178
179
-@media (max-width: 47.9375rem) {
179
+@media (max-width: 56rem) {
180
.header-nav {
181
align-items: center;
182
}
@@ -238,3 +238,53 @@
238
background: var(--color-accent-muted);
239
}
240
}
241
+
242
+.topic-shortcuts {
243
+ display: flex;
244
+ flex-wrap: wrap;
245
+ align-items: center;
246
+ gap: var(--space-2);
247
+ max-width: calc(var(--nav-width) + var(--gap) * 2);
248
+ margin: 0 auto;
249
+ padding: var(--space-2) var(--gap);
250
+ border-top: var(--size-border-thin) solid var(--color-border);
251
+ color: var(--color-text-muted);
252
+ font-size: var(--text-sm);
253
+}
254
+
255
+.topic-shortcuts__label {
256
+ letter-spacing: 0.04em;
257
+ text-transform: uppercase;
258
+}
259
+
260
+.topic-shortcuts a {
261
+ display: inline-flex;
262
+ align-items: center;
263
+ min-height: calc(var(--space-4) + var(--space-1));
264
+ padding: 0 var(--space-2);
265
+ border: var(--size-border-thin) solid var(--color-border);
266
+ border-radius: var(--radius-full);
267
+ background: var(--color-bg);
268
+ color: var(--color-text);
269
+ font-weight: 700;
270
+ text-decoration: none;
271
+}
272
+
273
+.topic-shortcuts a:hover,
274
+.topic-shortcuts a:focus-visible {
275
+ border-color: var(--color-accent);
276
+ color: var(--color-accent);
277
+}
278
+
279
+.topic-shortcuts__utility {
280
+ background: var(--color-accent-muted) !important;
281
+ color: var(--color-accent) !important;
282
+}
283
+
284
+@media (max-width: 56rem) {
285
+ .topic-shortcuts {
286
+ overflow-x: auto;
287
+ flex-wrap: nowrap;
288
+ padding-block: var(--space-2);
289
+ }
290
+}
assets/css/extended/squadscope.css
+157
@@ -765,3 +765,160 @@ summary:focus-visible,
765
text-transform: uppercase;
766
}
767
768
+
769
+.section-heading-row {
770
+ display: flex;
771
+ align-items: flex-end;
772
+ justify-content: space-between;
773
+ gap: var(--space-3);
774
+}
775
+
776
+.section-heading-row h2 {
777
+ margin: var(--space-1) 0 0;
778
+ font-size: var(--text-xl);
779
+ line-height: var(--leading-tight);
780
+}
781
+
782
+.section-heading-row > a {
783
+ color: var(--color-accent);
784
+ font-size: var(--text-sm);
785
+ font-weight: 700;
786
+ white-space: nowrap;
787
+}
788
+
789
+.home-content-grid,
790
+.home-rail,
791
+.home-quick-links,
792
+.home-topic-rail,
793
+.home-topic-list,
794
+.weekly-archive,
795
+.weekly-archive-list {
796
+ display: grid;
797
+ gap: var(--space-3);
798
+}
799
+
800
+.home-tag-strip {
801
+ display: flex;
802
+ flex-wrap: wrap;
803
+ align-items: center;
804
+ gap: var(--space-2);
805
+ padding: var(--space-2) var(--space-3);
806
+ border-block: 1px solid var(--color-border);
807
+}
808
+
809
+.home-tag-strip a,
810
+.home-topic-card,
811
+.home-quick-links a {
812
+ color: var(--color-text);
813
+ font-weight: 700;
814
+ text-decoration-thickness: 1px;
815
+ text-underline-offset: var(--space-1);
816
+}
817
+
818
+.home-tag-strip a {
819
+ padding: var(--space-1) var(--space-2);
820
+ border-radius: var(--radius-full);
821
+ background: var(--color-accent-muted);
822
+ color: var(--color-accent);
823
+ text-decoration: none;
824
+}
825
+
826
+.home-recent,
827
+.home-topic-rail,
828
+.home-quick-links,
829
+.weekly-archive-entry {
830
+ border: 1px solid var(--color-border);
831
+ border-radius: var(--radius-md);
832
+ background: var(--color-surface);
833
+}
834
+
835
+.home-recent,
836
+.home-topic-rail,
837
+.home-quick-links {
838
+ padding: var(--space-4);
839
+}
840
+
841
+.home-topic-list {
842
+ gap: var(--space-2);
843
+}
844
+
845
+.home-topic-card {
846
+ display: flex;
847
+ justify-content: space-between;
848
+ gap: var(--space-2);
849
+ padding: var(--space-2) 0;
850
+ border-bottom: 1px solid var(--color-border);
851
+ text-decoration: none;
852
+}
853
+
854
+.home-topic-card:last-child {
855
+ border-bottom: 0;
856
+}
857
+
858
+.home-topic-card small {
859
+ color: var(--color-text-muted);
860
+ font-size: var(--text-sm);
861
+ font-weight: 600;
862
+ white-space: nowrap;
863
+}
864
+
865
+.home-quick-links {
866
+ align-content: start;
867
+}
868
+
869
+.home-quick-links a {
870
+ display: flex;
871
+ justify-content: space-between;
872
+ gap: var(--space-3);
873
+ padding: var(--space-2) 0;
874
+ border-bottom: 1px solid var(--color-border);
875
+}
876
+
877
+.home-quick-links a::after {
878
+ content: "→";
879
+ color: var(--color-accent);
880
+}
881
+
882
+.home-quick-links a:last-child {
883
+ border-bottom: 0;
884
+}
885
+
886
+.weekly-archive-entry {
887
+ display: grid;
888
+ gap: var(--space-2);
889
+ padding: var(--space-4);
890
+}
891
+
892
+.weekly-archive-entry h3 {
893
+ margin: 0;
894
+ font-size: var(--text-xl);
895
+ line-height: var(--leading-tight);
896
+}
897
+
898
+.weekly-archive-entry p {
899
+ margin: 0;
900
+ color: var(--color-text-muted);
901
+}
902
+
903
+.weekly-archive-entry__meta {
904
+ display: flex;
905
+ flex-wrap: wrap;
906
+ gap: var(--space-2);
907
+ color: var(--color-text-muted);
908
+ font-size: var(--text-sm);
909
+ letter-spacing: 0.04em;
910
+ text-transform: uppercase;
911
+}
912
+
913
+@media (min-width: 769px) {
914
+ .home-content-grid {
915
+ grid-template-columns: minmax(0, 2fr) minmax(calc(var(--space-16) + var(--space-10)), 1fr);
916
+ }
917
+}
918
+
919
+@media (max-width: 640px) {
920
+ .section-heading-row {
921
+ align-items: flex-start;
922
+ flex-direction: column;
923
+ }
924
+}
content/search/_index.md
+1
-1
@@ -2,7 +2,7 @@
2
title = 'Search'
3
date = '2026-05-18T10:27:35.339+02:00'
4
draft = false
5
-layout = 'pagefind-search'
5
+layout = 'search'
6
pagefind = true
7
summary = 'Search weekly, monthly, and yearly SquadScope reports with Pagefind filters.'
8
+++
content/weekly/_index.md
+1
@@ -3,6 +3,7 @@ title = 'Weekly summaries'
3
date = '2026-05-18T10:27:35.339+02:00'
4
draft = false
5
summary = 'Browse each weekly SquadScope report.'
6
+aliases = ['/weeks/']
7
+++
8
9
Weekly editions collect the most notable new repositories, star surges, and critical analysis for each publishing cycle.
hugo.toml
+17
-5
@@ -49,33 +49,45 @@ rssLimit = 20
49
[menu]
50
[[menu.main]]
51
identifier = 'weekly'
52
- name = 'Weekly'
52
+ name = 'All weeks'
53
url = '/weekly/'
54
weight = 10
55
56
+ [[menu.main]]
57
+ identifier = 'topics'
58
+ name = 'Topics'
59
+ url = '/topics/'
60
+ weight = 20
61
+
62
[[menu.main]]
63
identifier = 'monthly'
64
name = 'Monthly'
65
url = '/monthly/'
60
- weight = 20
66
+ weight = 30
67
68
[[menu.main]]
69
identifier = 'yearly'
70
name = 'Yearly'
71
url = '/yearly/'
66
- weight = 30
72
+ weight = 40
73
+
74
+ [[menu.main]]
75
+ identifier = 'search'
76
+ name = 'Search'
77
+ url = '/search/'
78
+ weight = 50
79
80
[[menu.main]]
81
identifier = 'methodology'
82
name = 'Methodology'
83
url = '/methodology/'
72
- weight = 40
84
+ weight = 60
85
86
[[menu.main]]
87
identifier = 'about'
88
name = 'About'
89
url = '/about/'
78
- weight = 50
90
+ weight = 70
91
92
ignoreFiles = ['data/analyzed/.*\\.md$', 'data/metrics/.*\\.md$', 'data/metrics/.*\\.jsonl$']
93
layouts/index.html
+66
-18
@@ -1,7 +1,7 @@
1
{{- define "main" -}}
2
{{- $weeklyPages := (where site.RegularPages "Type" "weekly").ByDate.Reverse -}}
3
{{- $latest := cond (gt (len $weeklyPages) 0) (index $weeklyPages 0) nil -}}
4
-{{- $recent := after 1 $weeklyPages -}}
4
+{{- $topTags := first 6 site.Taxonomies.tags.ByCount -}}
5
6
<div class="home-shell">
7
<section class="home-hero home-hero--minimal" aria-labelledby="home-title">
@@ -21,6 +21,7 @@
21
{{ partial "report-metrics.html" $latest }}
22
<div class="home-actions">
23
<a class="home-action-primary" href="{{ $latest.RelPermalink }}">Read full analysis</a>
24
+ <a class="home-action-secondary" href="{{ `weekly/` | absLangURL }}">View all weeks</a>
25
</div>
26
</article>
27
{{- else -}}
@@ -31,24 +32,71 @@
32
</section>
33
{{- end -}}
34
34
- {{- if gt (len $recent) 0 -}}
35
- <section class="home-recent home-recent--compact" aria-labelledby="recent-reports-heading">
36
- <div class="section-topline">Recent archive</div>
37
- <h2 id="recent-reports-heading">Previous weekly analyses</h2>
38
- <div class="home-report-list home-report-list--compact">
39
- {{- range first 6 $recent -}}
40
- <article class="home-report-card home-report-card--compact">
41
- <a class="home-report-title" href="{{ .RelPermalink }}">
42
- <span>{{ .Title }}</span>
43
- </a>
44
- <div class="home-report-meta">
45
- {{- with .Params.week }}<span>{{ . }}</span>{{- end }}
46
- <time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "Jan 2, 2006" }}</time>
35
+ {{- if $topTags -}}
36
+ <nav class="home-tag-strip" aria-label="Active topic shortcuts">
37
+ <span class="home-tag-label">Active topics</span>
38
+ {{- range $topTags }}
39
+ <a href="{{ printf "tags/%s/" (.Name | urlize) | absLangURL }}">{{ .Name }}</a>
40
+ {{- end }}
41
+ <a href="{{ `topics/` | absLangURL }}">All topics</a>
42
+ </nav>
43
+ {{- end -}}
44
+
45
+ <div class="home-content-grid">
46
+ {{- if gt (len $weeklyPages) 0 -}}
47
+ <section class="home-recent" aria-labelledby="recent-reports-heading">
48
+ <div class="section-heading-row">
49
+ <div>
50
+ <div class="section-topline">Recent issues</div>
51
+ <h2 id="recent-reports-heading">Weekly archive</h2>
52
+ </div>
53
+ <a href="{{ `weekly/` | absLangURL }}">View all weeks</a>
54
+ </div>
55
+ <div class="home-report-list">
56
+ {{- range first 6 $weeklyPages -}}
57
+ <article class="home-report-card">
58
+ <a class="home-report-title" href="{{ .RelPermalink }}">
59
+ <span class="home-report-week">{{ .Params.week | default (.Date.Format "2006-01-02") }}</span>
60
+ <span>{{ .Title }}</span>
61
+ </a>
62
+ <p>{{ .Params.summary | default .Summary }}</p>
63
+ <div class="home-report-meta">
64
+ <time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "Jan 2, 2006" }}</time>
65
+ {{- with .Params.repos_featured }}<span>{{ . }} repos featured</span>{{- end }}
66
+ </div>
67
+ </article>
68
+ {{- end -}}
69
+ </div>
70
+ </section>
71
+ {{- end -}}
72
+
73
+ <aside class="home-rail" aria-labelledby="home-rail-heading">
74
+ {{- if $topTags -}}
75
+ <section class="home-topic-rail" aria-labelledby="home-rail-heading">
76
+ <div class="section-topline">Topics to follow</div>
77
+ <h2 id="home-rail-heading">Active storylines</h2>
78
+ <div class="home-topic-list">
79
+ {{- range $topTags -}}
80
+ <a class="home-topic-card" href="{{ printf "tags/%s/" (.Name | urlize) | absLangURL }}">
81
+ <span>{{ .Name }}</span>
82
+ <small>{{ .Count }} issue{{ if ne .Count 1 }}s{{ end }}</small>
83
+ </a>
84
+ {{- end -}}
85
</div>
48
- </article>
86
+ </section>
87
{{- end -}}
50
- </div>
51
- </section>
52
- {{- end -}}
88
+
89
+ <nav class="home-quick-links" aria-labelledby="quick-links-heading">
90
+ <div class="section-topline">Quick links</div>
91
+ <h2 id="quick-links-heading">Move through the report</h2>
92
+ <a href="{{ `weekly/` | absLangURL }}">All weeks</a>
93
+ <a href="{{ `topics/` | absLangURL }}">Topic index</a>
94
+ <a href="{{ `monthly/` | absLangURL }}">Monthly rollups</a>
95
+ <a href="{{ `yearly/` | absLangURL }}">Yearly rollups</a>
96
+ <a href="{{ `search/` | absLangURL }}">Search</a>
97
+ <a href="{{ `index.xml` | absLangURL }}">RSS feed</a>
98
+ </nav>
99
+ </aside>
100
+ </div>
101
</div>
102
{{- end -}}
layouts/partials/header.html
+12
@@ -80,4 +80,16 @@
80
</div>
81
</div>
82
</nav>
83
+
84
+ {{- $featuredTags := first 5 site.Taxonomies.tags.ByCount }}
85
+ {{- if $featuredTags }}
86
+ <nav class="topic-shortcuts" aria-label="Topic shortcuts">
87
+ <span class="topic-shortcuts__label">Topics</span>
88
+ {{- range $featuredTags }}
89
+ <a href="{{ printf "tags/%s/" (.Name | urlize) | absLangURL }}">{{ .Name }}</a>
90
+ {{- end }}
91
+ <a class="topic-shortcuts__utility" href="{{ `weekly/` | absLangURL }}">All weeks</a>
92
+ <a class="topic-shortcuts__utility" href="{{ `search/` | absLangURL }}">Search</a>
93
+ </nav>
94
+ {{- end }}
95
</header>
layouts/search/search.html
new
+85
@@ -0,0 +1,85 @@
1
+{{- define "main" -}}
2
+<header class="page-header">
3
+ {{- partial "breadcrumbs.html" . }}
4
+ <h1>{{ .Title }}</h1>
5
+ {{- with (.Description | default .Params.summary) }}
6
+ <div class="post-description">{{ . }}</div>
7
+ {{- end }}
8
+</header>
9
+
10
+{{- if .Content }}
11
+<div class="post-content md-content">
12
+ {{ .Content }}
13
+</div>
14
+{{- end }}
15
+
16
+<section class="search-shell" aria-labelledby="search-interface-title">
17
+ <div class="search-intro-card">
18
+ <span class="section-topline">Static search</span>
19
+ <h2 id="search-interface-title">Find trends by keyword or filter</h2>
20
+ <p>Search every published report, then narrow the results by category, tag, year, or featured repository.</p>
21
+ <div class="search-shortcuts" id="search-shortcuts" aria-label="Search keyboard shortcuts">
22
+ <span class="search-shortcuts-title">Keyboard</span>
23
+ <span class="search-shortcut"><kbd>/</kbd><span>Focus search</span></span>
24
+ <span class="search-shortcut"><kbd>Tab</kbd><span>Move through filters and results</span></span>
25
+ <span class="search-shortcut"><kbd>Enter</kbd><span>Open a result</span></span>
26
+ </div>
27
+ <p id="search-help" class="visually-hidden">Search every SquadScope report by keyword or filter. Use slash to focus the search field, Tab to move through filters and results, and Enter to open a result.</p>
28
+ </div>
29
+ <div id="search" class="search-panel" role="search" aria-labelledby="search-interface-title" aria-describedby="search-help search-shortcuts"></div>
30
+</section>
31
+
32
+<script>
33
+ window.addEventListener("DOMContentLoaded", function () {
34
+ const searchElement = document.getElementById("search");
35
+ if (!searchElement || !window.PagefindUI) return;
36
+
37
+ const applyA11y = function () {
38
+ const input = searchElement.querySelector(".pagefind-ui__search-input");
39
+ if (input) {
40
+ input.setAttribute("aria-label", "Search SquadScope reports");
41
+ input.setAttribute("aria-describedby", "search-help search-shortcuts");
42
+ }
43
+
44
+ const results = searchElement.querySelector(".pagefind-ui__results");
45
+ if (results) {
46
+ results.setAttribute("aria-live", "polite");
47
+ results.setAttribute("aria-label", "Search results");
48
+ }
49
+
50
+ const clearButton = searchElement.querySelector(".pagefind-ui__search-clear");
51
+ if (clearButton && !clearButton.getAttribute("aria-label")) {
52
+ clearButton.setAttribute("aria-label", "Clear search query");
53
+ }
54
+
55
+ searchElement.querySelectorAll("button").forEach(function (button) {
56
+ if (!button.getAttribute("aria-label")) {
57
+ const label = button.textContent.trim();
58
+ if (label) {
59
+ button.setAttribute("aria-label", label);
60
+ }
61
+ }
62
+ });
63
+ };
64
+
65
+ const observer = new MutationObserver(applyA11y);
66
+ observer.observe(searchElement, { childList: true, subtree: true });
67
+
68
+ new PagefindUI({
69
+ element: "#search",
70
+ bundlePath: "{{ `pagefind/` | absURL }}",
71
+ showSubResults: true,
72
+ showImages: false,
73
+ excerptLength: 24,
74
+ focusOnSlash: true,
75
+ sort: { date: "desc" },
76
+ openFilters: ["Categories", "Tags", "Repository", "Year"],
77
+ translations: {
78
+ placeholder: "Search reports, tags, categories, or repos"
79
+ }
80
+ });
81
+
82
+ requestAnimationFrame(applyA11y);
83
+ });
84
+</script>
85
+{{- end -}}
layouts/weekly/list.html
new
+40
@@ -0,0 +1,40 @@
1
+{{- define "main" -}}
2
+{{- $weeklyPages := (where site.RegularPages "Type" "weekly").ByDate.Reverse -}}
3
+<header class="page-header">
4
+ {{- partial "breadcrumbs.html" . }}
5
+ <p class="section-topline">Weekly archive</p>
6
+ <h1>{{ .Title }}</h1>
7
+ {{- with (.Description | default .Params.summary) }}
8
+ <div class="post-description">{{ . }}</div>
9
+ {{- end }}
10
+</header>
11
+
12
+{{- if .Content }}
13
+<div class="post-content md-content">
14
+ {{ .Content }}
15
+</div>
16
+{{- end }}
17
+
18
+<section class="weekly-archive" aria-labelledby="weekly-archive-heading">
19
+ <div class="section-heading-row">
20
+ <div>
21
+ <div class="section-topline">All editions</div>
22
+ <h2 id="weekly-archive-heading">{{ len $weeklyPages }} weekly issue{{ if ne (len $weeklyPages) 1 }}s{{ end }}</h2>
23
+ </div>
24
+ <a href="{{ `archive/` | absLangURL }}">Full site archive</a>
25
+ </div>
26
+
27
+ <div class="weekly-archive-list">
28
+ {{- range $weeklyPages -}}
29
+ <article class="weekly-archive-entry">
30
+ <div class="weekly-archive-entry__meta">
31
+ {{- with .Params.week }}<span>{{ . }}</span>{{- end }}
32
+ <time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "Jan 2, 2006" }}</time>
33
+ </div>
34
+ <h3><a href="{{ .RelPermalink }}">{{ .Title }}</a></h3>
35
+ <p>{{ .Params.summary | default .Summary }}</p>
36
+ </article>
37
+ {{- end -}}
38
+ </div>
39
+</section>
40
+{{- end -}}