Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
  1. Home
  2. Blog
  3. Build it: screens and apps
  4. Changelog Page Template in React: RSS, Email, Permalinks
Auf Deutsch lesen
  1. Blog
  2. Build it: screens and apps
  3. Changelog Page Template in React: RSS, Email, Permalinks
Build it: screens and apps
Build it: screens and apps

Changelog Page Template in React: RSS, Email, Permalinks

SR
Serban Rusu · Founder of Wingo UI
Nov 6, 2026 · 10 min read

On this page

0%
  1. What makes a SaaS changelog worth reading?
  2. How do you install the changelog page template?
  3. How do you store releases so the page, the feed and the emails agree?
  4. How do you render the changelog page in the App Router?
  5. Why does each release need its own page?
  6. How do you add an RSS feed to a Next.js changelog?
  7. How do you wire the email signup?
  8. How do you show what's new inside the app?
  9. What are the common mistakes on a changelog page?
  10. Where do you start?
  11. FAQ
    1. What should a SaaS changelog page include?
    2. What is the difference between a changelog and release notes?
    3. How do I add an RSS feed to a Next.js changelog?
    4. Should each changelog release have its own page?
    5. Is the Wingo UI Changelog Page template free?

TL;DR

A changelog page template should show dated releases newest first, group each release's changes as New, Improved and Fixed, give every release a permalink, and offer RSS and an email signup so people come back on their own. In Next.js, keep the releases in one data file that the page, a static page per release and the RSS route all read. Wingo UI's Changelog Page template renders the list, the filter and the subscribe row; you wire the data, the feed and the signup.

Published Nov 6, 2026

A changelog page is where a buyer checks whether your product is still moving and where a user finds out the thing they asked for shipped. Most of them fail at the second job: nobody remembers to visit a page, so the release that took a month gets read by five people. This tutorial builds a changelog page template in the Next.js App Router that people come back to: dated releases grouped as New, Improved and Fixed, a page per release, an RSS feed and an email signup, all fed from one data file. The UI is our Changelog Page template, so we are not neutral, but the data model, the routes and the feed work the same with any React changelog component or release notes page template.

What makes a SaaS changelog worth reading?

A SaaS changelog gets read when it is easy to scan and easy to follow. The first part is mostly agreed on. Keep a Changelog 1.1.0 (opens in a new tab) (checked November 6, 2026) lists the principles: written for people, an entry for every version, changes of the same type grouped together, versions you can link to, the latest first, and a release date on each, with ISO format (2026-11-06) as the recommended way to write it.

Keep a Changelog groups by six types: Added, Changed, Deprecated, Removed, Fixed and Security. That suits a developer library. For a product changelog read by customers, fewer groups are easier to scan. Gleap's changelog template (opens in a new tab) (updated September 27, 2026, checked November 6, 2026) leads with New, Improved and Fixed, adds Deprecated and Security, and warns that too many labels make a changelog harder to scan. We go one step further and keep three. This is how the six map onto them:

Keep a Changelog typeGroup on a product changelogHow to write it
AddedNewWhat the user can do now that they could not before
ChangedImprovedWhat got faster, clearer or easier, in the user's words
Fixed, SecurityFixedThe symptom that is gone; for security, the impact without exploit details
Deprecated, RemovedSummary plus an Improved lineSay it in the release summary first, with the date and a link to the migration

The second part, following, gets less attention. When we searched "changelog page template" on November 6, 2026, the top results were page builder templates (Framer, Ceptile), a Next.js and MDX starter on GitHub, and copy-paste text formats for a single entry. They focus on how the page looks. Little of it covers the three things that bring readers back: a link per release that keeps working, a feed, and an email when something ships.

How do you install the changelog page template?

One command copies the template and the parts it imports. It is a Pro item, so the account behind the CLI needs Wingo UI Pro.

bash
npx wingo-ui@latest add changelog-page

You get components/templates/changelog-page.tsx and changelog-page-data.ts, plus what they use: the free Marketing Section (for the page backdrop, the heading and the email form), the Marketing Header and Footer, the Segmented Control for the New / Improved / Fixed filter, the Badge for group labels and the Latest tag, and the Pagination for Load older releases. With no releases prop it renders eight sample releases of a made-up product called Acme.

Pick Fixed in the filter and watch both the list and the release count change, then scroll: from 768px the version rail sticks beside the release you are reading
$ npx wingo-ui@latest add changelog-page
ProChangelog Page docs

How do you store releases so the page, the feed and the emails agree?

Put them in one plain TypeScript file. The page, the per-release pages, the RSS route and anything that sends email all import it, so a release cannot appear in one and be missing from another.

ts
// lib/changelog.ts
import type { ChangelogRelease } from "@/components/templates/changelog-page-data"
// newest first; date is an ISO day, yyyy-mm-dd
export const releases: ChangelogRelease[] = [
{
version: "1.4.0",
date: "2026-11-03",
title: "Recurring tasks",
summary: "Repeat any task daily, weekly or on a custom schedule.",
glyph: "repeat",
tone: "success",
changes: [
{ type: "new", text: "Recurring tasks with daily, weekly and custom schedules.", href: "/docs/recurring-tasks" },
{ type: "improved", text: "Boards with 500 cards open faster." },
{ type: "fixed", text: "Due dates no longer shift by a day across time zones." },
],
},
// older releases below
]
// the template's changelogAnchor, copied: its module is "use client", so server code cannot call it
export const releaseSlug = (version: string) => `v${version.replace(/[^a-z0-9]+/gi, "-")}`
export const findRelease = (slug: string) => releases.find((release) => releaseSlug(release.version) === slug)

The type import costs nothing on the server: types are erased at build time, and changelog-page-data.ts has no "use client" directive anyway, so you could import CHANGELOG_RELEASES from it too. The slug helper is different: changelog-page.tsx is a client module, and a server component that calls a function exported from one gets an error instead of a string. Copying the one-line helper keeps your slugs identical to the template's anchors, so 1.4.0 becomes v1-4-0 everywhere.

glyph and tone pick the drawn cover (the glyph keys are in CHANGELOG_GLYPHS: timeline, repeat, workflow, doc, chart, plug, cloud, shield, sparkles). Pass cover with an image URL to use a screenshot instead.

How do you render the changelog page in the App Router?

Split it in two, as with any client template: a server page.tsx for metadata and a small client wrapper for everything that is a function (linkComponent, onSubscribe).

tsx
// app/(marketing)/changelog/page.tsx
import type { Metadata } from "next"
import { ChangelogView } from "./changelog-view"
export const metadata: Metadata = {
title: "Changelog",
description: "New features, improvements and fixes in Northwind, release by release.",
alternates: {
canonical: "/changelog",
// adds <link rel="alternate" type="application/rss+xml"> so feed readers find it
types: { "application/rss+xml": "/changelog/rss.xml" },
},
}
export default function ChangelogRoute() {
return <ChangelogView />
}
tsx
// app/(marketing)/changelog/changelog-view.tsx
"use client"
import type { CSSProperties } from "react"
import Link from "next/link"
import { ChangelogPage } from "@/components/templates/changelog-page"
import { releases } from "@/lib/changelog"
import { subscribeToChangelog } from "./actions"
export function ChangelogView() {
return (
<ChangelogPage
header={false}
footer={false}
// your layout's sticky header is 65px tall; the version rail sticks below it
style={{ "--topbar-offset": "65px" } as CSSProperties}
releases={releases}
linkComponent={Link}
rssHref="/changelog/rss.xml"
onSubscribe={subscribeToChangelog}
// every release is already loaded, so skip the 700 ms placeholder wait
onLoadMore={() => {}}
labels={{
title: "What's new in Northwind",
description: "New features, improvements and fixes, newest first.",
}}
/>
)
}

Three props are easy to miss. With header={false} the template stops reserving room for its own sticky bar, so pass your header's height through --topbar-offset or the rail slides under it. Without onLoadMore, Load older releases waits 700 ms to imitate a request; a no-op makes it instant when the data is local. And every visible string lives in labels: the title says Acme and the description promises a weekly release until you change them.

Why does each release need its own page?

Because the anchor link breaks once the list paginates, and because one release at one URL is the easiest thing to share. Every release on the page has an id (v1-4-0), and the version pill links to it. But the page renders only pageSize releases at first (4 by default), so a link to /changelog#v1-0-0 from an old email lands at the top of a page where that release does not exist yet.

A static page per release fixes that, and it gives RSS items, emails and posts a link that always works. ChangelogReleaseEntry, the part the template uses for each release, renders one on its own:

tsx
// app/(marketing)/changelog/[slug]/page.tsx
import type { Metadata } from "next"
import { notFound } from "next/navigation"
import { findRelease, releaseSlug, releases } from "@/lib/changelog"
import { ReleaseView } from "./release-view"
export const dynamicParams = false
export function generateStaticParams() {
return releases.map((release) => ({ slug: releaseSlug(release.version) }))
}
type Props = { params: Promise<{ slug: string }> }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const release = findRelease((await params).slug)
if (!release) return {}
return {
title: `${release.version}: ${release.title}`,
description: release.summary,
alternates: { canonical: `/changelog/${releaseSlug(release.version)}` },
}
}
export default async function ReleaseRoute({ params }: Props) {
const release = findRelease((await params).slug)
if (!release) notFound()
return <ReleaseView release={release} latest={release === releases[0]} />
}
tsx
// app/(marketing)/changelog/[slug]/release-view.tsx
"use client"
import Link from "next/link"
import { ChangelogReleaseEntry } from "@/components/templates/changelog-page"
import type { ChangelogRelease } from "@/components/templates/changelog-page-data"
import { NewsletterSection } from "@/components/blocks/newsletter-section"
import { subscribeToChangelog } from "../actions"
export function ReleaseView({ release, latest }: { release: ChangelogRelease; latest: boolean }) {
return (
<main className="px-4 pt-6 sm:px-6">
{/* the entry's title is an h2, so the page still needs an h1 */}
<h1 className="sr-only">{`${release.version}: ${release.title}`}</h1>
<ChangelogReleaseEntry release={release} latest={latest} layout="stacked" linkComponent={Link} />
<NewsletterSection
layout="inline"
title="Get the next release by email"
description="One short email when we ship. No other mail."
// the default shows a sample reader count; hide it until you have a real one
subscribers={0}
onSubscribe={subscribeToChangelog}
linkComponent={Link}
/>
</main>
)
}

The release is plain JSON, so it crosses from the server page to the client wrapper without trouble. Note the subscribers={0}: the Newsletter Section defaults to a sample count of 12,400 readers, which is fine in a demo and a fabricated number on your site. Zero hides the proof line.

Type an email and press Subscribe: an invalid address shows an error, a valid one spins, then the form turns into a success line. On a phone the field and button stack at full width
$ npx wingo-ui@latest add newsletter-section
ProNewsletter Section docs

How do you add an RSS feed to a Next.js changelog?

A route handler that reads the same releases and returns RSS 2.0. Link each item to its release page, and put the grouped changes in the description so readers see the whole release without leaving their feed reader.

ts
// app/(marketing)/changelog/rss.xml/route.ts
import { releases, releaseSlug } from "@/lib/changelog"
export const dynamic = "force-static"
const SITE = "https://northwind.dev"
const GROUPS = [["new", "New"], ["improved", "Improved"], ["fixed", "Fixed"]] as const
const escape = (text: string) =>
text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;")
export function GET() {
const items = releases
.slice(0, 50)
.map((release) => {
const url = `${SITE}/changelog/${releaseSlug(release.version)}`
const body = GROUPS.map(([type, label]) => {
const changes = release.changes.filter((change) => change.type === type)
if (!changes.length) return ""
return `<h3>${label}</h3><ul>${changes.map((change) => `<li>${escape(change.text)}</li>`).join("")}</ul>`
}).join("")
return `<item>
<title>${escape(`${release.version}: ${release.title}`)}</title>
<link>${url}</link>
<guid isPermaLink="true">${url}</guid>
<pubDate>${new Date(`${release.date}T12:00:00Z`).toUTCString()}</pubDate>
<description>${escape(`<p>${escape(release.summary)}</p>${body}`)}</description>
</item>`
})
.join("\n")
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel>
<title>Northwind changelog</title>
<link>${SITE}/changelog</link>
<description>New features, improvements and fixes in Northwind.</description>
${items}
</channel></rss>`
return new Response(xml, { headers: { "Content-Type": "application/rss+xml; charset=utf-8" } })
}

The description is HTML escaped twice on purpose: once for the text inside the HTML, once so the HTML itself is valid inside XML. Since Next.js 15, GET route handlers are not cached by default, so force-static is what turns the feed into a file built once per deploy. With Cache Components turned on, drop that line: a handler that reads no request data is prerendered anyway. Noon UTC (T12:00:00Z) keeps the release on its calendar day for readers from UTC-12 to UTC+11; the template goes further and formats its dates in UTC, so every visitor sees the same day. For a live changelog page example, our own changelog works this way, with a page per version and an RSS feed. If you also run a blog, the Next.js blog template post covers a JSON Feed next to RSS.

How do you wire the email signup?

onSubscribe gets the trimmed, already validated email. The form spins while the promise runs, shows the success line when it resolves and the error line when it rejects. That last part matters: a Server Action that returns { ok: false } still resolves, so the visitor sees "You're in". Throw instead.

ts
// app/(marketing)/changelog/actions.ts
"use server"
import { addSubscriber, sendConfirmation } from "@/lib/subscribers" // your database and mailer
export async function subscribeToChangelog(email: string) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) throw new Error("Invalid email")
const { token, alreadyConfirmed } = await addSubscriber(email, { list: "changelog" })
// double opt-in: nothing is sent to the list until they click the confirmation link
if (!alreadyConfirmed) await sendConfirmation(email, token)
}

Use double opt-in and answer the same way for a new, pending or existing address, so the form cannot be used to check who is on your list. The default success line already says "Check your inbox to confirm", which matches that flow. When you publish a release, send one short email per release with the title, the summary and a link to its page. That makes the page per release the target of every email you send.

How do you show what's new inside the app?

The public page reaches people who look for it. Users who are already signed in are easier to reach: put the last few releases inside the app, in a "What's new" panel or the help menu. A full changelog page is too heavy there; the Timeline fits a side panel and reads the same data.

tsx
// components/whats-new.tsx
"use client"
import { Timeline, type TimelineItemData } from "@/components/ui/timeline"
import { releases, releaseSlug } from "@/lib/changelog"
const items: TimelineItemData[] = releases.slice(0, 5).map((release) => ({
id: releaseSlug(release.version),
title: `${release.version}: ${release.title}`,
description: release.summary,
// noon UTC, so a release dated Nov 3 never shows as Nov 2 in New York
time: `${release.date}T12:00:00Z`,
tone: release.tone,
href: `/changelog/${releaseSlug(release.version)}`,
}))
export function WhatsNew() {
return <Timeline items={items} timeFormat="date" timePosition="side" locale="en-US" />
}

timePosition="side" puts the dates in a column on the start side once the timeline is 512px wide and folds them under the title in a narrow panel or on a phone. It is a container query, so it reacts to the panel's width. timeFormat="date" prints the day; the default relative ("3 days ago") suits activity feeds better than releases. Each title links to the release page.

What are the common mistakes on a changelog page?

  • Commit messages as entries. "fix tz offset in date util" means nothing to a customer; "Due dates no longer shift by a day across time zones" does. Write the symptom the user saw.
  • Bundling. One change per line. A new integration and a billing fix are two entries, in two groups.
  • Long gaps. A changelog whose newest entry is five months old tells a buyer the opposite of what you want. Ship small releases and write them up as you go.
  • Dead follow buttons. The template's RSS button points at /changelog/rss.xml by default. Build the route or pass rssHref="" to hide it.
  • Sample content in production. Search the rendered page for Acme before launch: the default title, the description, the newsletter count and, if you keep them, the template's own header and footer.

For the rest of the marketing site (pricing, contact, legal, about), the SaaS website template guide covers each page and how they share one layout, and more step-by-step builds sit in the Build it category. The support side of the same site, with search, topics and articles, is in our help center template guide.

Where do you start?

Copy the data file and the routes above, then install the template with npx wingo-ui@latest add changelog-page and replace the sample releases with your last three. The template, the Timeline and the Newsletter Section are part of Wingo UI Pro, $8 a month, $80 a year or $150 once (pricing); you can click through the Changelog Page template in all three designs (classic, minimal and editorial) before you decide.

Components in this post

  • Changelog Page

    A React changelog page template: email and RSS subscribe, releases on a sticky version rail with drawn covers and New, Improved and Fixed groups.

    Pro
  • Timeline

    A React timeline component for activity feeds, audit trails, changelogs and order tracking, with icons, relative times and day groups.

    Pro
  • Newsletter Section

    A React newsletter signup section for marketing pages: centered, split with the latest issue, a raised card or one compact row.

    Pro

FAQ

What should a SaaS changelog page include?

Releases newest first, each with a version, an ISO date, a short title, a one-sentence summary and its changes grouped by type (new, improved, fixed). Add a permalink per release, an RSS feed and an email signup so readers can follow it without remembering to check.

What is the difference between a changelog and release notes?

A changelog is the running record of every release, in one place and one format. Release notes usually describe a single release in more depth, often as an email or a blog post that links back to its changelog entry.

How do I add an RSS feed to a Next.js changelog?

Add a route handler at app/changelog/rss.xml/route.ts that reads the same releases array as the page and returns RSS 2.0 XML with the content type application/rss+xml. Mark it force-static and list it in the page metadata under alternates.types so feed readers find it.

Should each changelog release have its own page?

Yes, once you have more releases than fit on the first screen. A static page per release gives RSS items, emails and social posts a link that always works, and each page can carry its own title and description for search.

Is the Wingo UI Changelog Page template free?

No. The Changelog Page template, the Timeline and the Newsletter Section are Pro items, installed with npx wingo-ui@latest add once your account has Pro. The Marketing Section they build on is free.

Try it in your project

Changelog Page

Pro

A React changelog page template: email and RSS subscribe, releases on a sticky version rail with drawn covers and New, Improved and Fixed groups.

75 components free, no account needed. Pro from $8 a month.

Open Changelog Page liveSee pricing
  • Changelog
  • Next.js
  • Templates
  • RSS

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Build it: screens and apps

SaaS Website Template: The 7 Pages Beyond the Home Page

A SaaS website template needs more than a home page. What pricing, about, careers, changelog, contact, legal and compare pages must hold, built in Next.js.

SRSerban Rusu·Nov 5, 2026·20 min read
Component guides

Audit Log UI in React: Filters, Diffs and CSV Export

Build an audit log UI in React that people can use: who did what to which record, day groups, filters, before and after diffs, CSV export and the event shape.

SRSerban Rusu·Nov 6, 2026·10 min read
Component guides

React Gantt Chart: Drag, Progress and Dependencies

Build a React Gantt chart with draggable bars, progress handles and dependency arrows, save changes to a server, and know when a paid Gantt library pays off.

SRSerban Rusu·Oct 19, 2026·10 min read
Newsletter

Get new posts by email

New posts, product news and occasional offers from Wingo UI. We count clicks to keep it relevant.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • For teams
  • Theme generator

Resources

  • Docs
  • Contact
  • Programs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Compare

  • Wingo UI vs paid kits
  • Wingo UI vs shadcn/ui
  • shadcn/ui alternatives
  • MUI alternatives
  • Tailwind Plus alternatives
  • Magic UI alternatives
  • Free components

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI