Logochanges.page

Next.js App Router

Using the React SDK with Next.js App Router.

Installation

npm install @changespage/react react-markdown

Server Component with Client Hydration

app/changelog/page.tsx
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} />;
}
app/changelog/changelog-list.tsx
"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

app/changelog/page.tsx
"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

app/changelog/page.tsx
"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>
  );
}

On this page