feat(design): Phase 4 article layout (#196)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Juan Manuel Servera committed May 25, 2026 at 22:23 UTC 13435537d19d461355f1a43b1893a5310047b478
16 files changed +471 -36
.squad/agents/amy/history.md
+2
@@ -16,3 +16,5 @@
16 - 2026-05-25 PaperMod gotcha: adding a skip-to-content link requires overriding `layouts/_default/baseof.html`; keep the theme submodule untouched and place site-level chrome overrides in root `layouts/partials/`.
17 - 2026-05-25 Phase 3 homepage: use `layouts/index.html` for editorial entry-point structure; keep home-specific styling in `assets/css/extended/squadscope.css` and reuse token variables only.
18 - 2026-05-25 Home visual verification runs through `node scripts/design/verify-visual.mjs --base http://localhost:1313/SquadScope/` after starting `/tmp/hugo server -D --bind 0.0.0.0 --port 1313 --baseURL http://localhost:1313/SquadScope/`.
19 +- 2026-05-25 Phase 4 article layout: keep analyzer Markdown clean and derive article components from Hugo partials/render hooks (`article-content`, `repo-card`, `render-link`) instead of raw HTML in content.
20 +- 2026-05-25 Weekly press context uses canonical `## Where Industry Meets Code`; wrapping it in a Hugo partial preserves clean heading flow while giving it complementary landmark styling.
assets/css/extended/squadscope.css
+299 -1
@@ -464,8 +464,306 @@ summary:focus-visible,
464 grid-column: span 1;
465 }
466
467 - .search-shortcuts {
467 + .search-shortcuts,
468 + .repo-card__footer,
469 + .article-week-nav,
470 + .article-footer__prompt {
471 flex-direction: column;
472 align-items: stretch;
473 }
474 +
475 + .article-week-nav__link--next {
476 + text-align: start;
477 + }
478 +}
479 +
480 +.squad-article {
481 + max-width: calc(var(--prose-measure) + var(--space-10));
482 +}
483 +
484 +.article-header {
485 + display: grid;
486 + gap: var(--space-3);
487 + padding-block: var(--space-5);
488 + border-bottom: var(--size-border-thin) solid var(--color-border);
489 +}
490 +
491 +.article-header .breadcrumbs,
492 +.article-header .section-topline,
493 +.article-header .post-title,
494 +.article-header .post-description,
495 +.article-meta {
496 + margin: 0;
497 +}
498 +
499 +.article-header .post-title {
500 + max-width: var(--prose-measure);
501 +}
502 +
503 +.article-subtitle {
504 + max-width: var(--prose-measure);
505 + color: var(--color-text-muted);
506 + font-size: var(--text-lg);
507 + line-height: var(--leading-relaxed);
508 +}
509 +
510 +.article-meta,
511 +.article-footer__prompt,
512 +.article-footer__updated,
513 +.repo-card__stats {
514 + color: var(--color-text-muted);
515 + font-size: var(--text-sm);
516 + line-height: var(--leading-normal);
517 +}
518 +
519 +.squad-article .report-metrics,
520 +.article-jump-nav,
521 +.article-featured-repos,
522 +.squad-report .report-shortcuts {
523 + margin-top: var(--space-4);
524 +}
525 +
526 +.squad-article .report-metrics {
527 + grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--space-10)), 1fr));
528 +}
529 +
530 +.squad-article .metric-card,
531 +.repo-card,
532 +.article-jump-nav,
533 +.press-context-callout,
534 +.article-week-nav__link {
535 + border: var(--size-border-thin) solid var(--color-border);
536 + border-radius: var(--radius-md);
537 + background: var(--color-surface);
538 +}
539 +
540 +.squad-article .metric-card {
541 + min-width: var(--space-10);
542 +}
543 +
544 +.squad-article .metric-value {
545 + font-size: var(--text-xl);
546 + font-weight: 700;
547 +}
548 +
549 +.article-jump-nav {
550 + display: flex;
551 + flex-wrap: wrap;
552 + gap: var(--space-2) var(--space-3);
553 + align-items: center;
554 + padding: var(--space-2);
555 +}
556 +
557 +.article-jump-nav__label,
558 +.press-context-callout__kicker {
559 + color: var(--color-text-muted);
560 + font-size: var(--text-xs);
561 + font-weight: 700;
562 + letter-spacing: var(--tracking-nav);
563 + text-transform: uppercase;
564 +}
565 +
566 +.article-jump-nav ul,
567 +.article-week-nav,
568 +.article-footer__prompt,
569 +.repo-card__heading,
570 +.repo-card__footer {
571 + display: flex;
572 + flex-wrap: wrap;
573 + gap: var(--space-2);
574 + align-items: center;
575 +}
576 +
577 +.article-jump-nav ul {
578 + margin: 0;
579 + padding: 0;
580 + list-style: none;
581 +}
582 +
583 +.article-jump-nav a,
584 +.repo-card__button {
585 + display: inline-flex;
586 + align-items: center;
587 + justify-content: center;
588 + min-height: calc(var(--space-4) + var(--space-2));
589 + padding-inline: var(--space-3);
590 + border: var(--size-border-thin) solid var(--color-border);
591 + border-radius: var(--radius-full);
592 + text-decoration: none;
593 +}
594 +
595 +.article-featured-repos {
596 + display: grid;
597 +}
598 +
599 +.repo-card {
600 + display: grid;
601 + gap: var(--space-3);
602 + padding: var(--space-4);
603 +}
604 +
605 +.repo-card__main {
606 + display: grid;
607 + gap: var(--space-2);
608 +}
609 +
610 +.repo-card__heading {
611 + justify-content: space-between;
612 +}
613 +
614 +.repo-card__title,
615 +.repo-inline-link,
616 +.article-content a:not(.anchor),
617 +.article-footer a {
618 + color: var(--color-accent);
619 + text-decoration-thickness: var(--size-border-thin);
620 + text-underline-offset: var(--space-1);
621 +}
622 +
623 +.repo-card__title {
624 + overflow-wrap: anywhere;
625 + font-size: var(--text-lg);
626 + font-weight: 700;
627 }
628 +
629 +.repo-card__language {
630 + padding: var(--space-1) var(--space-2);
631 + border-radius: var(--radius-sm);
632 + background: var(--color-accent-muted);
633 + color: var(--color-accent);
634 + font-size: var(--text-xs);
635 + font-weight: 700;
636 +}
637 +
638 +.repo-card__description {
639 + margin: 0;
640 + color: var(--color-text-muted);
641 + line-height: var(--leading-relaxed);
642 +}
643 +
644 +.repo-card__footer {
645 + justify-content: space-between;
646 +}
647 +
648 +.repo-card__stats {
649 + display: flex;
650 + flex-wrap: wrap;
651 + gap: var(--space-2);
652 + margin: 0;
653 +}
654 +
655 +.repo-card__button {
656 + color: var(--color-text);
657 + font-size: var(--text-sm);
658 + font-weight: 700;
659 +}
660 +
661 +.article-content {
662 + max-width: var(--prose-measure);
663 + margin-inline: auto;
664 + font-size: var(--text-base);
665 + line-height: var(--leading-relaxed);
666 +}
667 +
668 +.article-content h2 {
669 + margin-top: var(--space-7);
670 + padding-top: var(--space-5);
671 + border-top: var(--size-border-thin) solid var(--color-border);
672 +}
673 +
674 +.article-content h3 {
675 + margin-top: var(--space-5);
676 +}
677 +
678 +.article-content code,
679 +.article-content pre code {
680 + font-family: var(--font-mono);
681 +}
682 +
683 +.article-content code {
684 + background: var(--color-code-bg);
685 + color: var(--color-text);
686 + border-radius: var(--radius-sm);
687 +}
688 +
689 +.article-content blockquote {
690 + margin: var(--space-5) 0;
691 + padding: var(--space-3) var(--space-4);
692 + border-inline-start: calc(var(--size-border-thin) * 4) solid var(--color-accent);
693 + border-radius: var(--radius-md);
694 + background: var(--color-accent-muted);
695 + color: var(--color-text);
696 + font-size: var(--text-lg);
697 + font-style: italic;
698 +}
699 +
700 +.press-context-callout {
701 + margin: var(--space-6) 0;
702 + padding: var(--space-4);
703 + border-color: var(--color-accent);
704 + border-inline-start-width: calc(var(--size-border-thin) * 4);
705 + background: var(--color-accent-muted);
706 +}
707 +
708 +.press-context-callout__kicker,
709 +.press-context-callout__title,
710 +.press-context-callout__body p {
711 + margin: 0;
712 +}
713 +
714 +.press-context-callout__title {
715 + padding: 0;
716 + border: 0;
717 + color: var(--color-text);
718 + font-size: var(--text-xl);
719 +}
720 +
721 +.press-context-callout__body {
722 + display: grid;
723 + gap: var(--space-3);
724 + margin-top: var(--space-3);
725 + color: var(--color-text-muted);
726 +}
727 +
728 +.press-context-callout__body p:first-child {
729 + color: var(--color-text);
730 + font-style: italic;
731 +}
732 +
733 +.article-footer {
734 + display: grid;
735 + gap: var(--space-4);
736 + margin-top: var(--space-7);
737 + padding-top: var(--space-5);
738 + border-top: var(--size-border-thin) solid var(--color-border);
739 +}
740 +
741 +.article-footer>* {
742 + margin: 0;
743 +}
744 +
745 +.article-week-nav {
746 + align-items: stretch;
747 +}
748 +
749 +.article-week-nav__link {
750 + flex: 1 1 var(--space-16);
751 + display: grid;
752 + gap: var(--space-1);
753 + padding: var(--space-3);
754 + color: var(--color-text);
755 + text-decoration: none;
756 +}
757 +
758 +.article-week-nav__link--next {
759 + text-align: end;
760 +}
761 +
762 +.article-week-nav__link span {
763 + color: var(--color-text-muted);
764 + font-size: var(--text-xs);
765 + font-weight: 700;
766 + letter-spacing: var(--tracking-nav);
767 + text-transform: uppercase;
768 +}
769 +
layouts/_markup/render-link.html new
+8
@@ -0,0 +1,8 @@
1 +{{- $destination := .Destination -}}
2 +{{- $isWeeklyRepo := and (eq .Page.Type "weekly") (hasPrefix $destination "https://github.com/") -}}
3 +{{- $repoName := "" -}}
4 +{{- if $isWeeklyRepo -}}
5 + {{- $repoName = replaceRE `^https://github\.com/([^/#?]+/[^/#?]+).*$` "$1" $destination -}}
6 + {{- if eq $repoName $destination -}}{{- $isWeeklyRepo = false -}}{{- end -}}
7 +{{- end -}}
8 +<a href="{{ $destination | safeURL }}"{{ with .Title }} title="{{ . }}"{{ end }}{{ if $isWeeklyRepo }} class="repo-inline-link" aria-label="Open GitHub repository {{ $repoName }}"{{ end }}>{{ .Text | safeHTML }}</a>
layouts/partials/article-content.html new
+29
@@ -0,0 +1,29 @@
1 +{{- $content := .Content -}}
2 +{{- $pressHeadings := slice `<h2 id="where-industry-meets-code">Where Industry Meets Code</h2>` `<h2 id=where-industry-meets-code>Where Industry Meets Code</h2>` -}}
3 +{{- $pressHeading := "" -}}
4 +{{- range $pressHeadings -}}
5 + {{- if and (not $pressHeading) (in $content .) -}}
6 + {{- $pressHeading = . -}}
7 + {{- end -}}
8 +{{- end -}}
9 +{{- if $pressHeading -}}
10 + {{- $parts := split $content $pressHeading -}}
11 + {{- index $parts 0 | safeHTML -}}
12 + {{- $afterHeading := index $parts 1 -}}
13 + {{- $sectionParts := split $afterHeading "<h2" -}}
14 + {{- $pressBody := index $sectionParts 0 -}}
15 + {{- $remaining := "" -}}
16 + {{- if gt (len $sectionParts) 1 -}}
17 + {{- $remaining = print "<h2" (delimit (after 1 $sectionParts) "<h2") -}}
18 + {{- end -}}
19 + <aside class="press-context-callout" role="complementary" aria-labelledby="where-industry-meets-code">
20 + <p class="press-context-callout__kicker">Industry &amp; Press</p>
21 + <h2 id="where-industry-meets-code" class="press-context-callout__title">Where Industry Meets Code</h2>
22 + <div class="press-context-callout__body">
23 + {{- $pressBody | safeHTML -}}
24 + </div>
25 + </aside>
26 + {{- $remaining | safeHTML -}}
27 +{{- else -}}
28 + {{- $content | safeHTML -}}
29 +{{- end -}}
layouts/partials/article-featured-repos.html new
+10
@@ -0,0 +1,10 @@
1 +{{- $page := . -}}
2 +{{- with $page.Params.top_repo -}}
3 +<section class="article-featured-repos" aria-label="Featured repository">
4 + {{ partial "repo-card.html" (dict
5 + "repo" .
6 + "description" (printf "Top repository anchoring %s's GitHub signal." ($page.Params.week | default "this report"))
7 + "variant" "hero"
8 + ) }}
9 +</section>
10 +{{- end -}}
layouts/partials/article-footer.html new
+38
@@ -0,0 +1,38 @@
1 +{{- $page := . -}}
2 +<footer class="post-footer article-footer">
3 + {{- $tags := $page.Language.Params.Taxonomies.tag | default "tags" -}}
4 + {{- $categories := $page.Language.Params.Taxonomies.category | default "categories" -}}
5 + <div class="article-footer__taxonomies" aria-label="Report taxonomy">
6 + <ul class="post-tags">
7 + {{- range ($page.GetTerms $categories) }}
8 + <li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
9 + {{- end }}
10 + {{- range ($page.GetTerms $tags) }}
11 + <li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
12 + {{- end }}
13 + </ul>
14 + </div>
15 +
16 + <nav class="article-week-nav" aria-label="Weekly report navigation">
17 + {{- with $page.PrevInSection }}
18 + <a class="article-week-nav__link" href="{{ .RelPermalink }}">
19 + <span>Read prior week</span>
20 + <strong>{{ .Title }}</strong>
21 + </a>
22 + {{- end }}
23 + {{- with $page.NextInSection }}
24 + <a class="article-week-nav__link article-week-nav__link--next" href="{{ .RelPermalink }}">
25 + <span>Read next week</span>
26 + <strong>{{ .Title }}</strong>
27 + </a>
28 + {{- end }}
29 + </nav>
30 +
31 + <div class="article-footer__prompt">
32 + <a href="{{ "index.xml" | absURL }}">Subscribe via RSS</a>
33 + <span aria-hidden="true">·</span>
34 + <a href="mailto:?subject={{ $page.Title | urlquery }}&amp;body={{ $page.Permalink | urlquery }}">Share this report</a>
35 + </div>
36 +
37 + <p class="article-footer__updated">Last updated <time datetime="{{ $page.Lastmod.Format "2006-01-02T15:04:05Z07:00" }}">{{ $page.Lastmod.Format "Jan 2, 2006" }}</time></p>
38 +</footer>
layouts/partials/article-jump-nav.html new
+27
@@ -0,0 +1,27 @@
1 +{{- $page := . -}}
2 +{{- $sections := slice
3 + (dict "id" "this-weeks-trends" "label" "Trends")
4 + (dict "id" "where-industry-meets-code" "label" "Industry")
5 + (dict "id" "signal-noise" "label" "Signal")
6 + (dict "id" "blind-spots" "label" "Blind Spots")
7 + (dict "id" "the-week-ahead" "label" "Week Ahead")
8 + (dict "id" "key-references" "label" "References")
9 +-}}
10 +{{- $links := slice -}}
11 +{{- range $sections -}}
12 + {{- $quotedID := printf "id=\"%s\"" .id -}}
13 + {{- $bareID := printf "id=%s" .id -}}
14 + {{- if or (in $page.Content $quotedID) (in $page.Content $bareID) -}}
15 + {{- $links = $links | append . -}}
16 + {{- end -}}
17 +{{- end -}}
18 +{{- if gt (len $links) 0 -}}
19 +<nav class="article-jump-nav" aria-label="Jump to article sections">
20 + <span class="article-jump-nav__label">Jump to</span>
21 + <ul>
22 + {{- range $links }}
23 + <li><a href="#{{ .id }}">{{ .label }}</a></li>
24 + {{- end }}
25 + </ul>
26 +</nav>
27 +{{- end -}}
layouts/partials/article-meta.html new
+20
@@ -0,0 +1,20 @@
1 +{{- $page := . -}}
2 +{{- $meta := slice -}}
3 +{{- if not $page.Date.IsZero -}}
4 + {{- $meta = $meta | append (printf "<time datetime=%q>%s</time>" ($page.Date.Format "2006-01-02T15:04:05Z07:00") ($page.Date.Format "Jan 2, 2006")) -}}
5 +{{- end -}}
6 +{{- if $page.ReadingTime -}}
7 + {{- $meta = $meta | append (printf "%d min read" $page.ReadingTime) -}}
8 +{{- end -}}
9 +{{- with $page.Params.week -}}
10 + {{- $meta = $meta | append . -}}
11 +{{- end -}}
12 +{{- with $page.Params.repos_featured -}}
13 + {{- $meta = $meta | append (printf "%v repos featured" .) -}}
14 +{{- end -}}
15 +{{- with $page.Params.top_repo -}}
16 + {{- $meta = $meta | append (printf "Top repo: %s" .) -}}
17 +{{- end -}}
18 +{{- if gt (len $meta) 0 -}}
19 +<p class="article-meta" aria-label="Report metadata">{{ delimit $meta " <span aria-hidden=\"true\">·</span> " | safeHTML }}</p>
20 +{{- end -}}
layouts/partials/repo-card.html new
+26
@@ -0,0 +1,26 @@
1 +{{- $repo := .repo -}}
2 +{{- $description := .description | default "Featured GitHub repository from this report." -}}
3 +{{- $language := .language -}}
4 +{{- $stars := .stars -}}
5 +{{- $forks := .forks -}}
6 +{{- $variant := .variant | default "featured" -}}
7 +{{- if $repo -}}
8 +<article class="repo-card repo-card--{{ $variant }}" aria-label="Featured repository {{ $repo }}">
9 + <div class="repo-card__main">
10 + <div class="repo-card__heading">
11 + <a class="repo-card__title" href="https://github.com/{{ $repo }}" aria-label="Open GitHub repository {{ $repo }}">{{ $repo }}</a>
12 + {{- with $language }}<span class="repo-card__language">{{ . }}</span>{{- end }}
13 + </div>
14 + <p class="repo-card__description">{{ $description }}</p>
15 + </div>
16 + <div class="repo-card__footer">
17 + <p class="repo-card__stats">
18 + {{- if $stars }}<span>{{ $stars }} stars</span>{{- end -}}
19 + {{- if and $stars $forks }}<span aria-hidden="true">·</span>{{- end -}}
20 + {{- if $forks }}<span>{{ $forks }} forks</span>{{- end -}}
21 + {{- if not (or $stars $forks) }}<span>GitHub repository</span>{{- end -}}
22 + </p>
23 + <a class="repo-card__button" href="https://github.com/{{ $repo }}" aria-label="View {{ $repo }} on GitHub">GitHub →</a>
24 + </div>
25 +</article>
26 +{{- end -}}
layouts/partials/report-metrics.html
+3 -3
@@ -4,7 +4,7 @@
4 {{- $hasStars := isset $page.Params "stars_tracked" -}}
5 {{- $hasTopRepo := and (isset $page.Params "top_repo") (ne (index $page.Params "top_repo") "") -}}
6 {{- if or $hasWeek $hasRepos $hasStars $hasTopRepo -}}
7 -<div class="report-metrics">
7 +<div class="report-metrics" aria-label="Report metrics">
8 {{- if $hasWeek -}}
9 <div class="metric-card">
10 <span class="metric-label">Week</span>
@@ -14,13 +14,13 @@
14 {{- if $hasRepos -}}
15 <div class="metric-card">
16 <span class="metric-label">Repos featured</span>
17 - <strong class="metric-value">{{ index $page.Params "repos_featured" }}</strong>
17 + <strong class="metric-value">{{ lang.FormatNumber 0 (index $page.Params "repos_featured") }}</strong>
18 </div>
19 {{- end -}}
20 {{- if $hasStars -}}
21 <div class="metric-card">
22 <span class="metric-label">Stars tracked</span>
23 - <strong class="metric-value">{{ index $page.Params "stars_tracked" }}</strong>
23 + <strong class="metric-value">{{ lang.FormatNumber 0 (index $page.Params "stars_tracked") }}</strong>
24 </div>
25 {{- end -}}
26 {{- if $hasTopRepo -}}
layouts/weekly/single.html
+9 -32
@@ -1,50 +1,27 @@
1 {{- define "main" }}
2 -<article class="post-single squad-report" data-pagefind-body>
2 +<article class="post-single squad-report squad-article" data-pagefind-body>
3 {{ partial "pagefind-metadata.html" . }}
4
5 - <header class="post-header">
5 + <header class="post-header article-header">
6 {{ partial "breadcrumbs.html" . }}
7 <p class="section-topline">Weekly summary</p>
8 <h1 class="post-title entry-hint-parent">{{ .Title }}</h1>
9 {{- with (.Description | default .Params.summary) }}
10 - <div class="post-description">{{ . }}</div>
10 + <p class="post-description article-subtitle">{{ . }}</p>
11 {{- end }}
12 - <div class="post-meta">
13 - {{- partial "post_meta.html" . -}}
14 - {{- partial "translation_list.html" . -}}
15 - {{- partial "edit_post.html" . -}}
16 - {{- partial "post_canonical.html" . -}}
17 - </div>
12 + {{ partial "article-meta.html" . }}
13 </header>
14
15 {{ partial "report-metrics.html" . }}
21 -
22 - {{- with .Params.correlation }}
23 - <div class="correlation-badges">
24 - {{ partial "correlation-badge.html" . }}
25 - </div>
26 - {{- end }}
16 + {{ partial "article-jump-nav.html" . }}
17 + {{ partial "article-featured-repos.html" . }}
18
19 {{ partial "report-shortcuts.html" . }}
20
30 - <div class="post-content md-content">
31 - {{ .Content }}
21 + <div class="post-content md-content article-content">
22 + {{ partial "article-content.html" . }}
23 </div>
24
34 - <footer class="post-footer">
35 - {{- $tags := .Language.Params.Taxonomies.tag | default "tags" }}
36 - {{- $categories := .Language.Params.Taxonomies.category | default "categories" }}
37 - <ul class="post-tags">
38 - {{- range ($.GetTerms $categories) }}
39 - <li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
40 - {{- end }}
41 - {{- range ($.GetTerms $tags) }}
42 - <li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
43 - {{- end }}
44 - </ul>
45 - {{- if (.Param "ShowPostNavLinks") }}
46 - {{- partial "post_nav_links.html" . }}
47 - {{- end }}
48 - </footer>
25 + {{ partial "article-footer.html" . }}
26 </article>
27 {{- end }}
screenshots/design-verification/2026-05-25/weekly-w21-mobile-dark.png
Binary files /dev/null and b/screenshots/design-verification/2026-05-25/weekly-w21-mobile-dark.png differ
screenshots/design-verification/2026-05-25/weekly-w21-wide-light.png
Binary files /dev/null and b/screenshots/design-verification/2026-05-25/weekly-w21-wide-light.png differ
screenshots/design-verification/2026-05-25/weekly-w22-desktop-dark.png
Binary files a/screenshots/design-verification/2026-05-25/weekly-w22-desktop-dark.png and b/screenshots/design-verification/2026-05-25/weekly-w22-desktop-dark.png differ
screenshots/design-verification/2026-05-25/weekly-w22-mobile-light.png
Binary files /dev/null and b/screenshots/design-verification/2026-05-25/weekly-w22-mobile-light.png differ
screenshots/design-verification/2026-05-25/weekly-w22-press-callout-dark.png
Binary files /dev/null and b/screenshots/design-verification/2026-05-25/weekly-w22-press-callout-dark.png differ