Last modified: April 10, 2026
·
9 min read

How to Add an RSS Feed to a Next.js Static Site

If you run a blog or content site built with Next.js, you should have an RSS feed. It lets readers subscribe through feed readers like Feedly or Inoreader, it gets picked up by aggregators and newsletters automatically, and search engines use it to discover new content faster. Despite all of this, most Next.js blogs skip it entirely.

The problem is that Next.js doesn't generate RSS feeds out of the box. With a static export (output: 'export'), you also can't use API routes or server-side rendering to generate the feed on the fly. You need to produce the XML file at build time and include it in the output directory.

This guide walks through two approaches: generating the feed with a build script, and generating it with a custom Next.js route. Both produce a valid RSS 2.0 XML file that works with every feed reader.

What Goes Into an RSS Feed

An RSS feed is an XML document with a specific structure. At its core, it contains metadata about your site (title, description, URL) and a list of items, each representing a post or article. Here's a minimal example:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>My Dev Blog</title>
    <link>https://example.com</link>
    <description>Articles about web development</description>
    <language>en</language>
    <atom:link href="https://example.com/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>How to Optimize Your Bundle</title>
      <link>https://example.com/posts/optimize-bundle/</link>
      <description>Learn how to analyze and reduce your JavaScript bundle size.</description>
      <pubDate>Thu, 10 Apr 2026 00:00:00 GMT</pubDate>
      <guid>https://example.com/posts/optimize-bundle/</guid>
    </item>
  </channel>
</rss>

The atom:link with rel="self" is technically optional, but feed validators will warn you if it's missing. It tells readers where the feed itself lives.

Each <item> needs at minimum a <title> and either a <link> or <description>. In practice, you want all three plus <pubDate> and <guid> (a unique identifier, typically the post URL).

Approach 1: Build Script with the feed Library

This is the most straightforward approach. You write a Node.js script that reads your posts data, generates the XML, and writes it to the public/ directory. Since files in public/ are copied as-is to the build output, the feed will be available at /feed.xml.

Install the feed Library

npm install --save-dev feed

The feed library handles all the XML formatting, escaping, and spec compliance. It supports RSS 2.0, Atom 1.0, and JSON Feed, so you get multiple formats from a single data source if you ever need them.

Create the Generation Script

Create a file at scripts/generate-rss.mjs:

import { Feed } from 'feed'
import { writeFileSync } from 'fs'

// Import your posts data.
// Adjust this import path to match where your posts are defined.
// If your posts file is TypeScript, you may need to use a
// compiled version or read the data differently (see notes below).
const SITE_URL = 'https://example.com'
const SITE_TITLE = 'My Dev Blog'
const SITE_DESCRIPTION = 'Articles about web development and software engineering'

// Define your posts here, or import them from a shared data file.
// For a TypeScript project, you might read from a JSON file
// or duplicate the posts array in this script.
const posts = [
  {
    slug: 'optimize-bundle',
    title: 'How to Optimize Your Bundle',
    description: 'Learn how to analyze and reduce your JavaScript bundle size.',
    date: '2026-04-10',
  },
  // ... more posts
]

const feed = new Feed({
  title: SITE_TITLE,
  description: SITE_DESCRIPTION,
  id: SITE_URL,
  link: SITE_URL,
  language: 'en',
  feedLinks: {
    rss2: `${SITE_URL}/feed.xml`,
  },
  copyright: `All rights reserved ${new Date().getFullYear()}, ${SITE_TITLE}`,
})

posts.forEach((post) => {
  feed.addItem({
    title: post.title,
    id: `${SITE_URL}/posts/${post.slug}/`,
    link: `${SITE_URL}/posts/${post.slug}/`,
    description: post.description,
    date: new Date(post.date),
  })
})

writeFileSync('public/feed.xml', feed.rss2())
console.log('RSS feed generated at public/feed.xml')

Handle TypeScript Post Data

If your posts are defined in a TypeScript file (which they probably are in a Next.js project), you have a few options:

  1. Extract posts to a JSON file that both your app and the script can read
  2. Use tsx to run the script so it can import .ts files directly:
npm install --save-dev tsx

Then your script can import TypeScript modules:

// scripts/generate-rss.mts
import { postArr } from '../app/posts/posts.ts'
  1. Duplicate the data, which is simple but means you have two sources of truth. Not ideal for a growing site.

Option 2 is usually the cleanest. You keep a single source of truth for your posts and the script runs against it directly.

Add a Build Script

Update your package.json to run the RSS generation before (or after) the Next.js build:

{
  "scripts": {
    "generate-rss": "tsx scripts/generate-rss.mts",
    "build": "npm run generate-rss && next build"
  }
}

Now every build automatically produces a fresh feed.xml.

Approach 2: Route-Based Generation with Static Export

If you prefer to keep everything inside the Next.js routing system, you can generate the feed as a static route. This works with output: 'export' as long as you return a Response object from a GET function in a route.ts file.

Create app/feed.xml/route.ts:

import { postArr } from '@/app/posts/posts'

const SITE_URL = 'https://example.com'
const SITE_TITLE = 'My Dev Blog'
const SITE_DESCRIPTION = 'Articles about web development'

function escapeXml(str: string): string {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;')
}

function generateRssFeed(): string {
  const items = postArr
    .map((post) => {
      const pubDate = post.lastModifiedDate
        ? new Date(post.lastModifiedDate).toUTCString()
        : new Date().toUTCString()

      return `    <item>
      <title>${escapeXml(post.title)}</title>
      <link>${SITE_URL}/posts/${post.slug}/</link>
      <description>${escapeXml(post.description)}</description>
      <pubDate>${pubDate}</pubDate>
      <guid>${SITE_URL}/posts/${post.slug}/</guid>
    </item>`
    })
    .join('\n')

  return `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>${escapeXml(SITE_TITLE)}</title>
    <link>${SITE_URL}</link>
    <description>${escapeXml(SITE_DESCRIPTION)}</description>
    <language>en</language>
    <atom:link href="${SITE_URL}/feed.xml" rel="self" type="application/rss+xml"/>
${items}
  </channel>
</rss>`
}

export function GET() {
  const feed = generateRssFeed()
  return new Response(feed, {
    headers: {
      'Content-Type': 'application/xml',
    },
  })
}

With output: 'export', Next.js pre-renders this route at build time and writes the output to out/feed.xml/index.xml (or out/feed.xml depending on your trailingSlash config).

The advantage: no extra build step, no separate script, and the feed data stays in sync with your posts automatically because it imports from the same source.

The downside: you're hand-writing XML instead of using a library that handles edge cases for you. The escapeXml function above covers the basics, but the feed library handles things like CDATA sections, multiple authors, and categories more robustly. For a simple blog with titles and descriptions, hand-written XML is fine.

Linking the Feed in Your HTML

For feed readers to auto-discover your RSS feed, add a <link> tag to the <head> of your site. In Next.js App Router, you do this through the metadata export in your root layout:

// app/layout.tsx
export const metadata = {
  // ... your existing metadata
  alternates: {
    types: {
      'application/rss+xml': '/feed.xml',
    },
  },
}

This produces:

<link rel="alternate" type="application/rss+xml" href="/feed.xml" />

Feed readers and browser extensions look for this tag to offer subscription options. Without it, users would need to know the exact feed URL.

Validating Your Feed

Before you ship, run your feed through a validator. The most widely used one is the W3C Feed Validation Service. Paste your feed URL (or the raw XML) and it will flag any issues: missing required fields, malformed dates, encoding problems, etc.

Common issues and fixes:

  • "Missing atom:link with rel=self": Add the <atom:link> element shown in the examples above, and include the xmlns:atom namespace on the <rss> tag
  • "pubDate is not in RFC 822 format": Use new Date(dateString).toUTCString() which outputs the correct format (e.g., Thu, 10 Apr 2026 00:00:00 GMT)
  • "Entity not defined": You have unescaped &, <, or > in your titles or descriptions. Make sure to escape XML entities

Keeping the Feed Up to Date

Since this is a static site, your feed only updates when you build and deploy. This is perfectly fine. RSS readers poll your feed periodically (usually every few hours) and pick up new items when they appear.

A few things to keep in mind:

  • Item order matters: Put the most recent posts first. Most feed readers sort by pubDate, but some display items in document order
  • Don't change GUIDs: The <guid> is how readers track which items they've already shown. If you change a post's slug (and therefore its GUID), readers will treat it as a new item
  • Keep a reasonable number of items: You don't need every post you've ever written in the feed. The 20-30 most recent posts is standard. Feed readers only care about new items since they last checked

Adding the Feed to Your Sitemap and Robots.txt

While not strictly required, referencing your feed in robots.txt helps some crawlers discover it:

User-agent: *
Allow: /

Sitemap: https://example.com/sitemap.xml

Google and other search engines already check sitemaps and RSS feeds independently, but there's no cost to being explicit.

Conclusion

Adding an RSS feed to a static Next.js site takes about 30 minutes of work and gives your content a distribution channel that works without any platform dependency. Readers get updates in their tool of choice, aggregators pick up your content automatically, and search engines have one more signal for discovering new pages.

The build script approach with the feed library is the most robust option for production. The route-based approach is simpler if you want zero external dependencies. Either way, the result is the same: a valid XML file that makes your content accessible to anyone who prefers RSS over visiting websites directly.