Back to Docs

Releases & Changelog

Display your changelog and release notes in your app. Public releases require no authentication.

The pattern

  • Newest page first, then "Load more". Fetch limit=10, then pass meta.nextCursor as cursor for older releases. A button is one choice; infinite scroll or older/newer links work the same way, with the same cursor.
  • Server-render and cache it. In Next.js, export const revalidate = 60: visitors get a cached page instantly, and new releases appear within about a minute. No setup needed.
  • If the fetch fails, throw. Don't render "no releases", or your cache may store the empty page.
  • Content is markdown. Render it with a markdown library; never inject it as raw HTML.

Using a coding agent? Ask it to add a Simple Product changelog. It gets these same examples from the MCP setup guide.

// useReleases.ts: newest 10, then "Load more" pages with the cursor
import { useCallback, useEffect, useState } from 'react';

export interface Release {
  id: string;
  title: string;
  content: string; // markdown
  publishedAt: number;
}

export function useReleases(workspace: string, pageSize = 10) {
  const [releases, setReleases] = useState<Release[]>([]);
  const [cursor, setCursor] = useState<string | null>(null);
  const [hasMore, setHasMore] = useState(false);
  const [isLoading, setIsLoading] = useState(true);

  const fetchPage = useCallback(async (after: string | null) => {
    setIsLoading(true);
    const params = new URLSearchParams({ workspace, limit: String(pageSize) });
    if (after) params.set('cursor', after);
    const { data, meta } = await fetch(`https://simpleproduct.dev/api/v1/releases/public?${params}`).then((r) => r.json());
    setReleases((prev) => (after ? [...prev, ...data] : data));
    setCursor(meta.nextCursor);
    setHasMore(meta.hasMore);
    setIsLoading(false);
  }, [workspace, pageSize]);

  useEffect(() => { fetchPage(null); }, [fetchPage]);

  return { releases, isLoading, hasMore, loadMore: () => fetchPage(cursor) };
}

// Changelog.tsx
import ReactMarkdown from 'react-markdown';
import { useReleases } from './useReleases';

export function Changelog({ workspace }: { workspace: string }) {
  const { releases, isLoading, hasMore, loadMore } = useReleases(workspace);

  return (
    <div>
      <h2>What's New</h2>
      {releases.map((release) => (
        <article key={release.id}>
          <h3>{release.title}</h3>
          <time>{new Date(release.publishedAt).toLocaleDateString()}</time>
          {/* content is markdown */}
          <ReactMarkdown>{release.content}</ReactMarkdown>
        </article>
      ))}
      {isLoading && <p>Loading…</p>}
      {hasMore && !isLoading && <button onClick={loadMore}>Load more</button>}
    </div>
  );
}

RSS Feed

Subscribe to releases via RSS. Great for letting users follow updates in their feed reader.

https://simpleproduct.dev/api/v1/releases/rss?workspace=YOUR_WORKSPACE_SLUG

API Reference

GET /api/v1/releases/public

Get published releases, newest first, one page at a time. No authentication required.

Query Parameters

ParameterRequiredDescription
workspaceYesYour workspace slug
limitNoReleases per page, 1–100. Default 20.
cursorNometa.nextCursor from the previous page, to get the next one. Omit it for the newest.

Response

{ "data": [ { "id": "...", "title": "v2.1.0", "content": "## What's New...", "publishedAt": 1790000000000, "slug": "v2-1-0" } ], "meta": { "count": 20, "nextCursor": "…", "hasMore": true } }

Keep calling with cursor until hasMore is false. Responses can be cached: they send Cache-Control: s-maxage=60, stale-while-revalidate=300 and an ETag.

POST /api/v1/releases

Create a new release. Requires authentication.

Request Body

{ "title": "v2.1.0", "content": "## What's New\n- Dark mode\n- Bug fixes", "status": "published", "publishedAt": 1234567890 }

Fields

FieldTypeDescription
titlestringRelease title (e.g., "v2.1.0")
contentstringRelease notes content (markdown)
statusstring"draft" or "published"
publishedAtnumberUnix timestamp (milliseconds)

Tips

  • •Cache responses: The API sends Cache-Control: s-maxage=60 and an ETag, so CDNs and browsers can reuse responses. In Next.js, use revalidate.
  • •Offer RSS: Link to the RSS feed so users can follow updates in their preferred reader