Skip to content

feat(releases) phase C: frontend — Special Edition cards, list, detail#7

Merged
znat merged 3 commits intomainfrom
feat/releases-phase-c-frontend
May 2, 2026
Merged

feat(releases) phase C: frontend — Special Edition cards, list, detail#7
znat merged 3 commits intomainfrom
feat/releases-phase-c-frontend

Conversation

@znat
Copy link
Copy Markdown
Owner

@znat znat commented May 2, 2026

Builds on Phase A (#5) and Phase B (#6, both merged). Renders the release data the analyzer writes. All components are direct ports from gitsky — only adapted for single-repo + the data shape (no images, no highlights).

Components (lifted, then adapted)

Component Source Adaptation
`SpecialEditionCard` `apps/web/app/o/[slug]/components/SpecialEditionCard.tsx` drop imageUrl, highlightCount, slug; reduce stats 4→3
`ReleasesListHero` (re-export of SpecialEditionCard for single-repo)
`ReleasesListStandardCard` `apps/web/app/[owner]/[repo]/releases/components/ReleasesListStandardCard.tsx` drop owner/repoName chips
`ReleasesListCompactRow` `apps/web/app/[owner]/[repo]/releases/components/ReleasesListCompactRow.tsx` drop repoName, add Pre-release badge
`ReleaseEditionHero` `apps/web/app/[owner]/[repo]/releases/[tag]/components/ReleaseEditionHero.tsx` drop imageUrl block
`ReleaseEditionStatBar` `apps/web/app/o/[slug]/releases/.../ReleaseEditionStatBar.tsx` drop Highlights & Coverage stats; 6→4 cells
`ReleaseEditionTopStories` `apps/web/app/o/[slug]/releases/.../ReleaseEditionTopStories.tsx` drop 'use client' + usePRPanel — `` to `/stories///` instead; drop highlightKeys
`ReleaseEditionChangelog` `apps/web/app/o/[slug]/releases/.../ReleaseEditionChangelog.tsx` drop 'use client' + usePRPanel — `` to story; takes resolved `Story[]` (we look up changelog IDs against the on-disk story set)

Routes

  • `/releases/` → list page (hero + standard + compact split)
  • `/releases///` → detail page (hero + stat bar + top stories + changelog)
  • `/releases///opengraph-image.png` → 1200×630 ImageResponse

SEO

  • `buildReleaseMetadata`, `buildReleasesListMetadata` in `lib/seo.ts`
  • `buildReleaseJsonLd` (NewsArticle) + `buildReleasesListJsonLd` (CollectionPage + ItemList) in `lib/json-ld.tsx`
  • `sitemap.ts` extended with `/releases/` + per-release entries
  • Existing `rename-og-png` postbuild script handles the new OG paths automatically (it walks the entire `out/` tree)

Homepage feed integration

`HomepageFeed` now accepts `releasesByDay`; `SpecialEditionCard` renders at the top of each day, above PR features (gitsky pattern).

Static-export workaround

znat/gitpulse currently has zero GitHub releases. Next 16 with `output: 'export'` errors out on an empty `generateStaticParams[]`. Workaround: the dynamic route returns one sentinel entry (`no_releases_yet`) that 404s when no real releases exist. Once the first release is tagged, the sentinel is replaced by real entries. Drops itself naturally.

Tests

14 new — `urls.test` for `releasePath`/`releaseSlug`/`releaseOgImagePath` (incl. tag-with-slash encoding); `releases.test` for `formatLines`/`formatReleaseDate`/`groupReleasesByDay`. 38 site tests pass total (was 25).

What's next

  • After merge: tag `v1.0.0` so znat/gitpulse has its first release → first Special Edition appears.
  • Phase D follow-ups: any cross-cutting tests not covered by per-phase suites.

Test plan

  • `yarn typecheck` clean
  • `yarn test` — 54/54 action + 38/38 site green (92 total)
  • CI green
  • After merge: self-deploy renders `/releases/` (empty for now — list page shows "No editions yet" until first tag)
  • After v1.0.0 tag: real Special Edition appears on homepage + at /releases/

Summary by CodeRabbit

  • New Features
    • Added a full Releases area (index + per-release pages) with hero, top stories, full changelogs, and integrated release editions into the homepage feed and timeline
    • New release UI: preview cards, compact rows, stat bar, and full‑screen release hero
    • Autogenerated Open Graph images and structured JSON‑LD for releases
  • Bug Fixes / Improvements
    • Sitemap now includes releases and improved freshness logic
    • Stable UTC date formatting and humanized line counts for releases
  • Tests
    • Unit tests for release date/line formatting and grouping helpers

Renders the release data Phase B writes. Components, routes, and SEO
are ported as directly as possible from gitsky:
- SpecialEditionCard (homepage feed): lifted from gitsky's
  apps/web/app/o/[slug]/components/SpecialEditionCard.tsx; drops
  imageUrl, highlightCount, slug; reduces stats strip 4 → 3 cells
- ReleasesListHero/Standard/Compact (list page): lifted from
  apps/web/app/[owner]/[repo]/releases/components/*.tsx; drops
  owner/repoName chips
- ReleaseEditionHero / StatBar / TopStories / Changelog (detail
  page): lifted from
  apps/web/app/[owner]/[repo]/releases/[tag]/components/* and
  apps/web/app/o/[slug]/releases/[owner]/[repo]/[tag]/components/*;
  drops 'use client' (we navigate via <Link>, not openPanel),
  drops imageUrl, drops highlightKeys, drops Coverage stat,
  changelog takes resolved Story[] (we look up changelog IDs
  against the on-disk story set rather than denormalizing again)

Routes:
- /releases/                          → list page (hero + standard + compact split)
- /releases/<tag>/<slug>/             → detail page (hero + stat bar + top stories + changelog)
- /releases/<tag>/<slug>/opengraph-image.png → 1200x630 ImageResponse

SEO:
- buildReleaseMetadata, buildReleasesListMetadata in lib/seo.ts
- buildReleaseJsonLd (NewsArticle), buildReleasesListJsonLd
  (CollectionPage + ItemList) in lib/json-ld.tsx
- sitemap.ts extended with /releases/ + per-release entries
- Existing rename-og-png postbuild script handles the new OG paths
  automatically (it walks the entire out/ tree)

Homepage feed integration:
- HomepageFeed now accepts releasesByDay; SpecialEditionCard renders
  at the top of each day, above PR features (gitsky pattern)
- app/page.tsx loads releases + groups them by publishedAt date

Static-export workaround: when there are no releases on disk yet,
generateStaticParams returns a single sentinel entry that 404s. Next
16 with output: 'export' errors out on an empty static-params array;
this lets the route file stay valid until the first real release.

Tests: 14 new (urls.test releasePath/releaseSlug/releaseOgImagePath,
releases.test formatLines/formatReleaseDate/groupReleasesByDay).
38 site tests pass total (was 25).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds a Releases feature: types, loaders, URL/SEO/JSON‑LD builders, index/detail pages including a forced‑static OG image generator, multiple release UI components, homepage and sitemap integration, and unit tests for release helpers.

Changes

Release Feature Implementation

Layer / File(s) Summary
Data Types & Structures
site/src/lib/releases.ts
Adds Release, ReleaseTopStory, ReleaseManifest* interfaces, ReleasesByDay type, formatReleaseDate, formatLines, and groupReleasesByDay.
Data Loading
site/src/lib/releases-loader.ts
Adds loadReleases() (reads/parses JSON from public/data/releases) and loadRelease(tag) (lookup by tag).
Utility Tests
site/src/lib/releases.test.ts
Vitest tests for formatLines, formatReleaseDate, and groupReleasesByDay with a makeRelease helper.
URL Builders & Tests
site/src/lib/urls.ts, site/src/lib/urls.test.ts
Adds releasesIndexPath, releaseSlug, releasePath, releaseOgImagePath and tests validating slug selection, path encoding, and OG image path.
SEO & JSON‑LD
site/src/lib/seo.ts, site/src/lib/json-ld.tsx
Adds buildReleaseMetadata, buildReleasesListMetadata, buildReleaseJsonLd, and buildReleasesListJsonLd.
Sitemap Integration
site/src/app/sitemap.ts
Loads releases, includes /releases/ index and per-release sitemap entries, and factors release publish dates into newest.
Homepage Integration
site/src/app/page.tsx, site/src/components/HomepageFeed.tsx
Homepage loads releases via loadReleases() and groupReleasesByDay() and passes releasesByDay into HomepageFeed; HomepageFeed merges story days and releases into a unified date-sorted feed and renders SpecialEditionCard entries.
Pages: Releases Index
site/src/app/releases/page.tsx
Adds /releases/ index page with generateMetadata(), JSON‑LD, empty-state handling, hero/standard/compact sections, and presentational dividers.
Pages: Release Detail
site/src/app/releases/[tag]/[slug]/page.tsx
Adds release detail page with generateStaticParams() (including sentinel fallback), generateMetadata(), canonical/OG URL handling, JSON‑LD, resolveChangelog(), and rendering of hero, conditional stat bar, top stories, and changelog.
OG Image Generator
site/src/app/releases/[tag]/[slug]/opengraph-image.tsx
Adds forced-static OG image route (dynamic = 'force-static', size, contentType, alt), generateStaticParams() (per-release or sentinel), and async handler that renders a release OG image or Not Found image.
Edition UI Components
site/src/components/ReleaseEditionHero.tsx, ReleaseEditionTopStories.tsx, ReleaseEditionStatBar.tsx, ReleaseEditionChangelog.tsx
New components for release pages: hero (label/badge/quip/date), ranked top stories with metadata, stat bar (PRs/contributors/adds/dels), and changelog grouped by category.
List UI Components
site/src/components/SpecialEditionCard.tsx, ReleasesListStandardCard.tsx, ReleasesListCompactRow.tsx, site/src/components/ReleasesListHero.tsx
New list/cards for releases; SpecialEditionCard used as ReleasesListHero; semver-derived styling helpers and badges added.
Integration & Wiring
site/src/app/page.tsx, site/src/app/releases/*, site/src/components/*
Wires loaders, URL helpers, SEO/JSON‑LD, and components across pages and homepage to render releases end-to-end.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant NextServer as Next.js Server
    participant Loader as Releases Loader
    participant FS as Filesystem
    participant OG as OG Image Generator

    Browser->>NextServer: GET /releases/ or /releases/:tag/:slug/
    NextServer->>Loader: loadReleases() / loadRelease(tag)
    Loader->>FS: read release JSON files
    FS-->>Loader: release JSON
    Loader-->>NextServer: Release objects
    NextServer->>NextServer: build metadata / JSON-LD / render components
    NextServer-->>Browser: HTML response

    Browser->>OG: GET /releases/:tag/:slug/opengraph-image.png
    OG->>Loader: loadRelease(tag)
    Loader->>FS: read JSON
    FS-->>Loader: release JSON
    Loader-->>OG: Release
    OG-->>Browser: PNG image
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Hopping through JSON and dates so fine,

Editions lined up by publish time;
Heroes, changelogs, and OG art bright,
Cards and feeds now show release-night;
A tiny rabbit cheers the rollout light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main addition: frontend release components and pages (Special Edition cards, list, and detail pages).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/releases-phase-c-frontend

Review rate limit: 8/10 reviews remaining, refill in 8 minutes and 36 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@site/src/app/releases/`[tag]/[slug]/opengraph-image.tsx:
- Around line 18-21: generateStaticParams currently returns encoded tag values
and the request handler decodes params.tag later; remove the manual
URI-encoding/decoding so Next.js can manage encoding for you: in
generateStaticParams (the function mapping releases to { tag, slug }) remove
encodeURIComponent around r.tag, and remove the corresponding decodeURIComponent
usage where params.tag is read (the code that reads/uses params.tag in this
module), returning/using the raw tag string instead.

In `@site/src/app/releases/`[tag]/[slug]/page.tsx:
- Around line 48-63: Both generateMetadata and ReleaseDetailPage accept params
but only validate tag; you must also validate the params.slug against the
resolved release's canonical slug to enforce canonical URLs. After decoding tag
and loading the release via loadRelease(decodeURIComponent(tag)) (in both
generateMetadata and ReleaseDetailPage), compute the canonical slug from the
release (e.g., release.slug or build from release data) and compare it to
params.slug (decoded); if they differ, call notFound() (or issue a redirect to
the canonical URL) instead of rendering. Update generateMetadata,
ReleaseDetailPage, and any helper used to buildReleaseMetadata to perform this
slug check to ensure alternate slugs do not serve identical content.

In `@site/src/app/sitemap.ts`:
- Around line 39-55: The sitemap currently only adds the releases index when
releases.length > 0, causing /releases/ to disappear if there are zero releases;
move the entries.push for the index (the call using
canonicalUrl(releasesIndexPath()) and newestRelease) out of the if block so the
index entry is always added, while keeping the loop that pushes individual
release entries (using releasePath(release) and release.publishedAt) conditional
on releases.length > 0.

In `@site/src/components/ReleasesListStandardCard.tsx`:
- Around line 35-37: The quoted quip heading in ReleasesListStandardCard
currently renders even when release.quip is empty, producing “”; update the JSX
to conditionally render the <h3> block only when release.quip contains
non-whitespace text (e.g., check release.quip?.trim().length > 0) so the quoted
heading is omitted for empty quips; locate the h3 that uses release.quip in
ReleasesListStandardCard and wrap it with that truthy/trim check or an inline
conditional render to prevent showing empty quotes.

In `@site/src/components/SpecialEditionCard.tsx`:
- Around line 173-179: The group-hover transform on the arrow never fires
because the Link element with className "font-feed-mono ...
group-hover:translate-x-[3px]" is not a "group" container; update the Link
(component symbol: Link) to include the "group" utility class on its className
(or add a parent element with className="group") so the child span's
"group-hover:translate-x-[3px]" can trigger; ensure className keeps existing
classes and only adds "group".

In `@site/src/lib/releases-loader.ts`:
- Around line 18-23: Wrap the per-file read/parse inside a try/catch so a single
malformed file doesn't abort the whole load: for each entry in files (the map
that calls JSON.parse(readFileSync(join(RELEASES_DIR, f), 'utf8')) as Release)
catch read/parse errors, log or collect the error (include filename f) and skip
that entry, then continue and finally sort only the successfully parsed Release
objects by publishedAt as currently done; ensure the returned array excludes
failed parses instead of throwing.

In `@site/src/lib/releases.test.ts`:
- Around line 48-52: Add a UTC-boundary test for formatReleaseDate to catch
day-shift regressions around midnight UTC: inside the existing
describe('formatReleaseDate') block add an it case that passes a timestamp at
the UTC boundary (e.g. '2026-05-02T00:00:00Z' and/or '2026-05-01T23:59:59Z') to
formatReleaseDate and assert the expected formatted string ('May 2, 2026' or
'May 1, 2026' as appropriate); reference the formatReleaseDate function and the
existing test suite to place the new it() alongside the current ISO test so the
edge-case is covered.

In `@site/src/lib/releases.ts`:
- Around line 49-57: The Intl.DateTimeFormat used by RELEASES_DATE_FMT and
consumed by formatReleaseDate lacks an explicit timeZone, causing different
rendered dates for UTC ISO timestamps across environments; update the
RELEASES_DATE_FMT options to include timeZone: 'UTC' so dates are formatted
consistently regardless of host timezone (modify the RELEASES_DATE_FMT
declaration).

In `@site/src/lib/urls.test.ts`:
- Around line 102-129: Add tests covering the case where a release has name: ''
(empty string) so the slug and path logic fall back to the tag the same way as
when name is null: add a test within the releaseSlug suite using makeRelease({
name: '', tag: 'v1.0.0' }) and expect releaseSlug(release) toBe 'v1-0-0', and
add a corresponding test in the releasePath suite using makeRelease({ tag:
'v1.0.0', name: '' }) expecting releasePath(release) toBe
'/releases/v1.0.0/v1-0-0/'; reference releaseSlug, releasePath, and makeRelease
to locate where to insert these cases.

In `@site/src/lib/urls.ts`:
- Around line 36-53: The releaseSlug function should treat empty strings as
missing names instead of using the empty value; change its logic to check that
release.name is non-empty (e.g., trim/length check or truthiness) and only
slugify release.name when it contains characters, otherwise slugify release.tag;
keep releasePath and releaseOgImagePath using releaseSlug(release) and
tagSegment = encodeURIComponent(release.tag) as before so the fallback slug is
correct for both normal and OG routes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d6b699a5-7f2e-486e-925e-94d075f14914

📥 Commits

Reviewing files that changed from the base of the PR and between fd92c99 and 0afe8a5.

📒 Files selected for processing (21)
  • site/src/app/page.tsx
  • site/src/app/releases/[tag]/[slug]/opengraph-image.tsx
  • site/src/app/releases/[tag]/[slug]/page.tsx
  • site/src/app/releases/page.tsx
  • site/src/app/sitemap.ts
  • site/src/components/HomepageFeed.tsx
  • site/src/components/ReleaseEditionChangelog.tsx
  • site/src/components/ReleaseEditionHero.tsx
  • site/src/components/ReleaseEditionStatBar.tsx
  • site/src/components/ReleaseEditionTopStories.tsx
  • site/src/components/ReleasesListCompactRow.tsx
  • site/src/components/ReleasesListHero.tsx
  • site/src/components/ReleasesListStandardCard.tsx
  • site/src/components/SpecialEditionCard.tsx
  • site/src/lib/json-ld.tsx
  • site/src/lib/releases-loader.ts
  • site/src/lib/releases.test.ts
  • site/src/lib/releases.ts
  • site/src/lib/seo.ts
  • site/src/lib/urls.test.ts
  • site/src/lib/urls.ts

Comment thread site/src/app/releases/[tag]/[slug]/opengraph-image.tsx
Comment thread site/src/app/releases/[tag]/[slug]/page.tsx
Comment thread site/src/app/sitemap.ts Outdated
Comment thread site/src/components/ReleasesListStandardCard.tsx Outdated
Comment thread site/src/components/SpecialEditionCard.tsx
Comment thread site/src/lib/releases-loader.ts Outdated
Comment thread site/src/lib/releases.test.ts
Comment thread site/src/lib/releases.ts
Comment thread site/src/lib/urls.test.ts
Comment thread site/src/lib/urls.ts
znat added 2 commits May 2, 2026 11:51
CodeRabbit flagged that pre-encoding the tag in generateStaticParams
double-encodes tags containing special characters (e.g.
'release/v1.0.0' would become 'release%252Fv1.0.0' once Next re-encodes
the segment for the filesystem path). Per the Next.js App Router docs,
generateStaticParams should return raw segment values — Next handles
encoding on the way out and decoding on the way in.

For plain tags like v1.0.0 the prior code happened to work because there
was nothing to re-encode, but it was a latent bug.

Layered rules now consistent across the route:
- generateStaticParams: raw segment values (this fix)
- params.tag in handler: already decoded by Next, used as-is (this fix)
- releasePath() URL builder for <Link href>: still encodes (we own the
  outgoing href and it must match the encoded static path Next emitted)

Refs #7 (comment)
8 fixes from CodeRabbit:

- urls.ts: releaseSlug now uses '||' instead of '??' so empty-string
  names fall back to the tag (was: empty slug → /releases/<tag>/)
- releases.ts: formatReleaseDate pinned to UTC so host-tz machines
  don't shift dates by a day near midnight UTC
- releases-loader.ts: per-file try/catch so one corrupt JSON doesn't
  fail the whole release listing
- sitemap.ts: /releases/ index entry always included (previously
  gated on non-empty list)
- ReleasesListStandardCard.tsx: guard quip <h3> when release.quip is ''
- SpecialEditionCard.tsx: scope CTA arrow's group-hover to a named
  /cta group (the parent <Link> wasn't a group, so the animation
  never fired)
- page.tsx (release detail): validate slug against releaseSlug(release)
  to enforce canonical URLs — alternate slug paths now 404 instead of
  serving identical content under multiple URLs
- urls.test.ts: regression test for empty-string name slug fallback
- releases.test.ts: UTC day-boundary cases for formatReleaseDate

92 → 94 tests pass total (54 action + 40 site).
@znat znat merged commit 61a6717 into main May 2, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant