diff --git a/src/app/changelog/feedSkeleton.tsx b/src/app/changelog/feedSkeleton.tsx
new file mode 100644
index 0000000..10a7ab9
--- /dev/null
+++ b/src/app/changelog/feedSkeleton.tsx
@@ -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 fallback on the index page so the
+// static shell can be prerendered while the dynamic feed streams in.
+export function FeedSkeleton() {
+ return (
+
+
+
+ {/* Feed skeleton */}
+
+ {/* Month divider skeleton */}
+
+
+
+
+
+
+
+
+
+ {/* Sidebar skeleton — desktop only */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/changelog/loading.tsx b/src/app/changelog/loading.tsx
index 5dab378..b47a6fa 100644
--- a/src/app/changelog/loading.tsx
+++ b/src/app/changelog/loading.tsx
@@ -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 (
-
-
-
- {/* Feed skeleton */}
-
- {/* Month divider skeleton */}
-
-
-
-
-
-
-
-
-
- {/* Sidebar skeleton — desktop only */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
);
}
diff --git a/src/app/changelog/page.tsx b/src/app/changelog/page.tsx
index ffef3e5..e86ac50 100644
--- a/src/app/changelog/page.tsx
+++ b/src/app/changelog/page.tsx
@@ -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 )
+ // 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 (
-
+ }>
+
+
);
}
+async function ChangelogFeed() {
+ await connection();
+ const changelogs = await getChangelogSummaries();
+
+ return ;
+}
+
export function generateMetadata(): Metadata {
return {
description: