rss
Setup RSS, Atom, and JSON Feed generation for Next.js with Payload CMS integration. Use this skill when the user says "setup rss", "add rss feed", "setup atom feed", "add feed", "rss integration", or "syndication feeds".
What this skill does
# RSS Feed Integration
Sets up RSS 2.0, Atom, and JSON Feed generation for Next.js App Router with Payload CMS integration, SEO-optimized feed discovery, and content team-friendly debug tools.
## Quick Start
```bash
# 1. Install feed generation package
bun add feedsmith
# 2. Start dev server
bun dev
# 3. Test feeds
open http://localhost:3000/feed.xml
open http://localhost:3000/feed.atom
open http://localhost:3000/feed.json
# 4. Debug page
open http://localhost:3000/debug/rss
```
## What Gets Created
### Route Handlers
1. **`src/app/feed.xml/route.ts`** - RSS 2.0 feed (most compatible)
2. **`src/app/feed.atom/route.ts`** - Atom feed (better semantics)
3. **`src/app/feed.json/route.ts`** - JSON Feed (modern, easy parsing)
### Library Files
1. **`src/lib/rss/generate.ts`** - Feed generation utilities
2. **`src/lib/rss/types.ts`** - TypeScript types for feed items
3. **`src/lib/rss/fetch-content.ts`** - CMS content fetching
### Debug & Testing
1. **`src/app/debug/rss/page.tsx`** - Feed preview and validation
2. **`e2e/specs/rss.spec.ts`** - Playwright validation tests
## Installation
```bash
bun add feedsmith
```
## Environment Variables
Add to `.env.local`:
```env
# Site Configuration (required for absolute URLs)
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Feed Metadata
NEXT_PUBLIC_SITE_NAME="Your Site Name"
NEXT_PUBLIC_SITE_DESCRIPTION="Your site description"
```
Add to `src/env.ts`:
```typescript
client: {
NEXT_PUBLIC_APP_URL: z.string().url(),
NEXT_PUBLIC_SITE_NAME: z.string().optional(),
NEXT_PUBLIC_SITE_DESCRIPTION: z.string().optional(),
}
```
## Prerequisites
### TypeScript Path Alias
Add `@payload-config` to `tsconfig.json` paths (required for Payload CMS imports):
```json
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@payload-config": ["./src/payload.config.ts"]
}
},
"exclude": ["node_modules", "e2e"]
}
```
## Setup Steps
### 1. Create Feed Types (`src/lib/rss/types.ts`)
```typescript
export type FeedItem = {
id: string;
title: string;
slug: string;
description: string;
content?: string;
publishedAt: Date;
updatedAt?: Date;
author?: {
name: string;
email?: string;
};
categories?: string[];
featuredImage?: {
url: string;
alt?: string;
};
};
export type FeedConfig = {
title: string;
description: string;
siteUrl: string;
feedUrl: string;
language?: string;
copyright?: string;
managingEditor?: string;
webMaster?: string;
ttl?: number;
image?: {
url: string;
title: string;
link: string;
};
};
```
### 2. Create Content Fetcher (`src/lib/rss/fetch-content.ts`)
```typescript
import { getPayload } from "payload";
import config from "@payload-config";
import type { FeedItem } from "./types";
/**
* Fetch published posts from Payload CMS
* Falls back to empty array if Payload is not configured
*/
export async function fetchFeedItems(limit = 50): Promise<FeedItem[]> {
try {
const payload = await getPayload({ config });
const posts = await payload.find({
collection: "posts",
where: {
status: { equals: "published" },
},
sort: "-publishedAt",
limit,
depth: 2,
});
return posts.docs.map((post) => ({
id: String(post.id),
title: post.title,
slug: post.slug,
description: post.excerpt ?? "",
content: extractTextFromLexical(post.content),
publishedAt: new Date(post.publishedAt ?? post.createdAt),
updatedAt: new Date(post.updatedAt),
author:
typeof post.author !== "string" && typeof post.author !== "number" && post.author
? {
name: post.author.name ?? post.author.email,
email: post.author.email,
}
: undefined,
categories:
post.categories?.map((cat: string | number | { name: string }) =>
typeof cat === "string" || typeof cat === "number" ? String(cat) : cat.name
) ?? [],
featuredImage:
typeof post.featuredImage !== "string" && typeof post.featuredImage !== "number" && post.featuredImage
? {
url: post.featuredImage.url ?? "",
alt: post.featuredImage.alt,
}
: undefined,
}));
} catch {
// Payload not configured or collection doesn't exist
console.warn("RSS: Could not fetch from Payload CMS, returning empty feed");
return [];
}
}
/**
* Extract plain text from Lexical rich text content
*/
function extractTextFromLexical(content: unknown): string {
if (!content || typeof content !== "object") return "";
const root = content as { root?: { children?: unknown[] } };
if (!root.root?.children) return "";
function extractText(nodes: unknown[]): string {
return nodes
.map((node) => {
const n = node as { text?: string; children?: unknown[] };
if (n.text) return n.text;
if (n.children) return extractText(n.children);
return "";
})
.join(" ");
}
return extractText(root.root.children).trim();
}
```
### 3. Create Feed Generator (`src/lib/rss/generate.ts`)
```typescript
import {
generateRssFeed,
generateAtomFeed,
generateJsonFeed,
} from "feedsmith";
import type { FeedItem, FeedConfig } from "./types";
import { env } from "@/env";
const DEFAULT_CONFIG: FeedConfig = {
title: env.NEXT_PUBLIC_SITE_NAME ?? "My Site",
description: env.NEXT_PUBLIC_SITE_DESCRIPTION ?? "Latest updates from our site",
siteUrl: env.NEXT_PUBLIC_APP_URL,
feedUrl: `${env.NEXT_PUBLIC_APP_URL}/feed.xml`,
language: "en-US",
copyright: `Copyright ${new Date().getFullYear()}`,
ttl: 60,
};
/**
* Generate RSS 2.0 feed
*/
export function generateRss(items: FeedItem[], config?: Partial<FeedConfig>): string {
const feedConfig = { ...DEFAULT_CONFIG, ...config };
const siteUrl = feedConfig.siteUrl;
return generateRssFeed({
title: feedConfig.title,
link: siteUrl,
description: feedConfig.description,
language: feedConfig.language,
copyright: feedConfig.copyright,
managingEditor: feedConfig.managingEditor,
webMaster: feedConfig.webMaster,
pubDate: new Date(),
lastBuildDate: new Date(),
ttl: feedConfig.ttl,
image: feedConfig.image,
items: items.map((item) => ({
title: item.title,
link: `${siteUrl}/blog/${item.slug}`,
description: item.description,
author: item.author?.email
? `${item.author.email} (${item.author.name})`
: item.author?.name,
guid: `${siteUrl}/blog/${item.slug}`,
pubDate: item.publishedAt,
categories: item.categories?.map((name) => ({ name })),
enclosure: item.featuredImage?.url
? {
url: item.featuredImage.url.startsWith("http")
? item.featuredImage.url
: `${siteUrl}${item.featuredImage.url}`,
type: "image/jpeg",
length: 0,
}
: undefined,
})),
});
}
/**
* Generate Atom feed
*/
export function generateAtom(items: FeedItem[], config?: Partial<FeedConfig>): string {
const feedConfig = { ...DEFAULT_CONFIG, ...config };
const siteUrl = feedConfig.siteUrl;
return generateAtomFeed({
id: `${siteUrl}/feed.atom`,
title: feedConfig.title,
subtitle: feedConfig.description,
updated: new Date(),
links: [
{ href: siteUrl },
{ href: `${siteUrl}/feed.atom`, rel: "self", type: "application/atom+xml" },
],
rights: feedConfig.copyright,
entries: items.map((item) => ({
id: `${siteUrl}/blog/${item.slug}`,
title: item.title,
updated: item.updatedAt ?? item.publishedAt,
published: item.publishedAt,
summary: item.description,
content: item.content,
links: [{ href: `${siteUrl}/blog/${item.slug}` }],
authors: item.author
? [{ name: item.author.name, email: item.author.email }]
: [],
categories: item.categories?.map((term) => ({ term })),
})),
});
}
/**
* GenRelated in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.