feat(releases) phase C: frontend — Special Edition cards, list, detail#7
feat(releases) phase C: frontend — Special Edition cards, list, detail#7
Conversation
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).
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdds 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. ChangesRelease Feature Implementation
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 8/10 reviews remaining, refill in 8 minutes and 36 seconds. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (21)
site/src/app/page.tsxsite/src/app/releases/[tag]/[slug]/opengraph-image.tsxsite/src/app/releases/[tag]/[slug]/page.tsxsite/src/app/releases/page.tsxsite/src/app/sitemap.tssite/src/components/HomepageFeed.tsxsite/src/components/ReleaseEditionChangelog.tsxsite/src/components/ReleaseEditionHero.tsxsite/src/components/ReleaseEditionStatBar.tsxsite/src/components/ReleaseEditionTopStories.tsxsite/src/components/ReleasesListCompactRow.tsxsite/src/components/ReleasesListHero.tsxsite/src/components/ReleasesListStandardCard.tsxsite/src/components/SpecialEditionCard.tsxsite/src/lib/json-ld.tsxsite/src/lib/releases-loader.tssite/src/lib/releases.test.tssite/src/lib/releases.tssite/src/lib/seo.tssite/src/lib/urls.test.tssite/src/lib/urls.ts
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).
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)
Routes
SEO
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
Test plan
Summary by CodeRabbit