Next.js App Router
Using the React SDK with Next.js App Router.
Installation
npm install @changespage/react react-markdownServer Component with Client Hydration
import { createChangesPageClient } from "@changespage/react";
import { ChangelogList } from "./changelog-list";
const client = createChangesPageClient({
baseUrl: "https://your-page.changes.page",
});
export default async function ChangelogPage() {
const initialData = await client.getPosts({ limit: 10 });
return <ChangelogList initialData={initialData} />;
}"use client";
import { createChangesPageClient, usePosts, getTagLabel } from "@changespage/react";
import Markdown from "react-markdown";
const client = createChangesPageClient({
baseUrl: "https://your-page.changes.page",
});
export function ChangelogList({ initialData }: { initialData: Awaited<ReturnType<typeof client.getPosts>> }) {
const { posts, hasMore, loading, loadMore } = usePosts({
client,
initialData,
});
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<div>
{post.tags.map((tag) => (
<span key={tag}>{getTagLabel(tag)}</span>
))}
</div>
<h2>{post.title}</h2>
<Markdown>{post.plain_text_content}</Markdown>
</article>
))}
{hasMore && (
<button type="button" onClick={loadMore} disabled={loading}>
{loading ? "Loading..." : "Load More"}
</button>
)}
</div>
);
}Client-Only
"use client";
import { createChangesPageClient, usePosts, getTagLabel } from "@changespage/react";
import Markdown from "react-markdown";
const client = createChangesPageClient({
baseUrl: "https://your-page.changes.page",
});
export default function ChangelogPage() {
const { posts, hasMore, loading, loadMore } = usePosts({ client });
if (loading && posts.length === 0) {
return <div>Loading...</div>;
}
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<div>
{post.tags.map((tag) => (
<span key={tag}>{getTagLabel(tag)}</span>
))}
</div>
<h2>{post.title}</h2>
<Markdown>{post.plain_text_content}</Markdown>
</article>
))}
{hasMore && (
<button type="button" onClick={loadMore} disabled={loading}>
{loading ? "Loading..." : "Load More"}
</button>
)}
</div>
);
}With Tailwind CSS
"use client";
import { createChangesPageClient, usePosts, getTagLabel, type PostTag } from "@changespage/react";
import Markdown from "react-markdown";
const client = createChangesPageClient({
baseUrl: "https://your-page.changes.page",
});
const tagColors: Record<PostTag, string> = {
new: "bg-green-100 text-green-800",
fix: "bg-red-100 text-red-800",
improvement: "bg-blue-100 text-blue-800",
announcement: "bg-purple-100 text-purple-800",
alert: "bg-yellow-100 text-yellow-800",
};
export default function ChangelogPage() {
const { posts, hasMore, loading, loadMore } = usePosts({ client });
if (loading && posts.length === 0) {
return (
<div className="flex justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-gray-800" />
</div>
);
}
return (
<div className="mx-auto max-w-2xl space-y-8 px-4 py-8">
{posts.map((post) => (
<article key={post.id} className="rounded-lg border border-gray-200 p-6">
<div className="mb-3 flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${tagColors[tag]}`}
>
{getTagLabel(tag)}
</span>
))}
</div>
<h2 className="mb-2 text-xl font-semibold text-gray-900">{post.title}</h2>
<div className="prose prose-sm text-gray-600">
<Markdown>{post.plain_text_content}</Markdown>
</div>
</article>
))}
{hasMore && (
<button
type="button"
onClick={loadMore}
disabled={loading}
className="w-full rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
>
{loading ? "Loading..." : "Load More"}
</button>
)}
</div>
);
}