Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/app/changelog/feedSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { LoadingArticle } from "@/client/components/article";

// Shared skeleton for the changelog feed. Used both by the route-level
// loading.tsx and as the <Suspense> fallback on the index page so the
// static shell can be prerendered while the dynamic feed streams in.
export function FeedSkeleton() {
return (
<main className="w-full bg-darkPurple min-h-screen">
<div className="max-w-5xl mx-auto px-4 sm:px-6">
<div className="sm:flex sm:gap-10">
{/* Feed skeleton */}
<div className="flex-1 min-w-0 pt-6 pb-10">
{/* Month divider skeleton */}
<div className="flex items-center gap-3 mt-2 mb-4">
<div className="h-3 bg-white/10 animate-pulse rounded w-24" />
<div className="flex-1 h-px bg-white/10" />
</div>
<LoadingArticle />
<LoadingArticle />
<LoadingArticle />
</div>

{/* Sidebar skeleton — desktop only */}
<div className="hidden sm:block w-40 flex-shrink-0">
<div className="pt-6">
<div className="h-8 bg-white/10 animate-pulse rounded-lg w-full mb-6" />
<div className="h-3 bg-white/10 animate-pulse rounded w-14 mb-3" />
<div className="flex flex-col gap-2">
<div className="h-3 bg-white/10 animate-pulse rounded w-24" />
<div className="h-3 bg-white/10 animate-pulse rounded w-20" />
<div className="h-3 bg-white/10 animate-pulse rounded w-24" />
<div className="h-3 bg-white/10 animate-pulse rounded w-20" />
</div>
</div>
</div>
</div>
</div>
</main>
);
}
35 changes: 2 additions & 33 deletions src/app/changelog/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,12 @@
import { Fragment } from "react";

import { LoadingArticle } from "@/client/components/article";
import { FeedSkeleton } from "./feedSkeleton";
import Header from "./header";

export default function Loading() {
return (
<Fragment>
<Header loading />
<main className="w-full bg-darkPurple min-h-screen">
<div className="max-w-5xl mx-auto px-4 sm:px-6">
<div className="sm:flex sm:gap-10">
{/* Feed skeleton */}
<div className="flex-1 min-w-0 pt-6 pb-10">
{/* Month divider skeleton */}
<div className="flex items-center gap-3 mt-2 mb-4">
<div className="h-3 bg-white/10 animate-pulse rounded w-24" />
<div className="flex-1 h-px bg-white/10" />
</div>
<LoadingArticle />
<LoadingArticle />
<LoadingArticle />
</div>

{/* Sidebar skeleton — desktop only */}
<div className="hidden sm:block w-40 flex-shrink-0">
<div className="pt-6">
<div className="h-8 bg-white/10 animate-pulse rounded-lg w-full mb-6" />
<div className="h-3 bg-white/10 animate-pulse rounded w-14 mb-3" />
<div className="flex flex-col gap-2">
<div className="h-3 bg-white/10 animate-pulse rounded w-24" />
<div className="h-3 bg-white/10 animate-pulse rounded w-20" />
<div className="h-3 bg-white/10 animate-pulse rounded w-24" />
<div className="h-3 bg-white/10 animate-pulse rounded w-20" />
</div>
</div>
</div>
</div>
</div>
</main>
<FeedSkeleton />
</Fragment>
);
}
25 changes: 19 additions & 6 deletions src/app/changelog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import type { Metadata } from "next";
import { connection } from "next/server";
import { Fragment } from "react";
import { Fragment, Suspense } from "react";
import { ChangelogList } from "@/client/components/list";
import { getChangelogSummaries } from "../../server/utils";
import { FeedSkeleton } from "./feedSkeleton";
import Header from "./header";

export default async function Page() {
await connection();
const changelogs = await getChangelogSummaries();

export default function Page() {
// Keep the header + document metadata in a statically-prerenderable shell and
// stream the dynamic, search-param-driven feed inside an explicit Suspense
// boundary. Reading URL state (via nuqs/useSearchParams in <ChangelogList />)
// outside of a Suspense boundary makes the prerendered tree disagree with the
// client resume under `cacheComponents`, which surfaced as a hydration
// mismatch ("Cannot read properties of null (reading 'parentNode')").
return (
<Fragment>
<Header />
<ChangelogList changelogs={changelogs} />
<Suspense fallback={<FeedSkeleton />}>
<ChangelogFeed />
</Suspense>
</Fragment>
);
}

async function ChangelogFeed() {
await connection();
const changelogs = await getChangelogSummaries();

return <ChangelogList changelogs={changelogs} />;
}

export function generateMetadata(): Metadata {
return {
description:
Expand Down
Loading