[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Changelog RSS Feed

Build a Changelog RSS Feed

Build a changelog RSS feed generator with Atom and RSS 2.0 output, category filtering, webhook triggers, email digest integration, and JSON feed support for developer product updates.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Max leads DevRel at a 20-person API company. Developers want to know when the API changes — new endpoints, deprecations, breaking changes. The changelog page exists but developers don't visit it proactively. Email announcements have 10% open rate. Some developers want RSS to integrate with their existing feed readers. Others want webhooks to trigger CI pipeline updates. Slack integration requests keep coming. They need a universal changelog feed: RSS 2.0, Atom, JSON Feed, webhook notifications, email digests, and category filtering.

Step 1: Build the Feed Generator

typescript
import { pool } from "../db";
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);

interface ChangelogEntry {
  id: string;
  title: string;
  content: string;
  contentHtml: string;
  type: "feature" | "improvement" | "fix" | "deprecation" | "breaking";
  version: string | null;
  tags: string[];
  publishedAt: string;
  author: string;
}

interface FeedOptions {
  format: "rss" | "atom" | "json";
  types?: string[];
  tags?: string[];
  limit?: number;
}

const FEED_META = {
  title: process.env.PRODUCT_NAME || "API Changelog",
  description: "Latest updates, features, and changes",
  link: process.env.CHANGELOG_URL || "https://example.com/changelog",
  feedUrl: process.env.FEED_URL || "https://example.com/changelog/feed",
};

export async function generateFeed(options: FeedOptions): Promise<string> {
  const entries = await getEntries(options);

  switch (options.format) {
    case "rss": return generateRSS(entries);
    case "atom": return generateAtom(entries);
    case "json": return generateJSON(entries);
    default: throw new Error(`Unknown format: ${options.format}`);
  }
}

async function getEntries(options: FeedOptions): Promise<ChangelogEntry[]> {
  const cacheKey = `feed:${JSON.stringify(options)}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  let sql = "SELECT * FROM changelog_entries WHERE published_at IS NOT NULL";
  const params: any[] = [];
  let idx = 1;
  if (options.types?.length) { sql += ` AND type = ANY($${idx})`; params.push(options.types); idx++; }
  if (options.tags?.length) { sql += ` AND tags::jsonb ?| $${idx}`; params.push(options.tags); idx++; }
  sql += ` ORDER BY published_at DESC LIMIT $${idx}`;
  params.push(options.limit || 50);

  const { rows } = await pool.query(sql, params);
  const entries = rows.map((r: any) => ({ ...r, tags: JSON.parse(r.tags) }));
  await redis.setex(cacheKey, 300, JSON.stringify(entries));
  return entries;
}

function generateRSS(entries: ChangelogEntry[]): string {
  const items = entries.map((e) => `    <item>
      <title>${escXml(e.title)}</title>
      <link>${FEED_META.link}/${e.id}</link>
      <guid isPermaLink="false">${e.id}</guid>
      <pubDate>${new Date(e.publishedAt).toUTCString()}</pubDate>
      <category>${e.type}</category>
      <description>${escXml(e.content)}</description>
      <content:encoded><![CDATA[${e.contentHtml}]]></content:encoded>
    </item>`).join("\n");

  return `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>${escXml(FEED_META.title)}</title>
    <link>${FEED_META.link}</link>
    <description>${escXml(FEED_META.description)}</description>
    <atom:link href="${FEED_META.feedUrl}" rel="self" type="application/rss+xml" />
    <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
${items}
  </channel>
</rss>`;
}

function generateAtom(entries: ChangelogEntry[]): string {
  const items = entries.map((e) => `  <entry>
    <title>${escXml(e.title)}</title>
    <link href="${FEED_META.link}/${e.id}" />
    <id>urn:changelog:${e.id}</id>
    <updated>${new Date(e.publishedAt).toISOString()}</updated>
    <category term="${e.type}" />
    <summary>${escXml(e.content.slice(0, 300))}</summary>
    <content type="html">${escXml(e.contentHtml)}</content>
    <author><name>${escXml(e.author)}</name></author>
  </entry>`).join("\n");

  return `<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>${escXml(FEED_META.title)}</title>
  <link href="${FEED_META.link}" />
  <link href="${FEED_META.feedUrl}" rel="self" />
  <updated>${new Date().toISOString()}</updated>
  <id>${FEED_META.link}</id>
${items}
</feed>`;
}

function generateJSON(entries: ChangelogEntry[]): string {
  return JSON.stringify({
    version: "https://jsonfeed.org/version/1.1",
    title: FEED_META.title,
    home_page_url: FEED_META.link,
    feed_url: FEED_META.feedUrl + "?format=json",
    items: entries.map((e) => ({
      id: e.id, title: e.title, url: `${FEED_META.link}/${e.id}`,
      content_html: e.contentHtml, content_text: e.content,
      date_published: new Date(e.publishedAt).toISOString(),
      tags: [e.type, ...e.tags], authors: [{ name: e.author }],
    })),
  }, null, 2);
}

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

// Publish new changelog entry + notify subscribers
export async function publishEntry(entry: Omit<ChangelogEntry, "id">): Promise<string> {
  const id = `cl-${Date.now().toString(36)}`;
  await pool.query(
    `INSERT INTO changelog_entries (id, title, content, content_html, type, version, tags, author, published_at, created_at)
     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())`,
    [id, entry.title, entry.content, entry.contentHtml, entry.type, entry.version, JSON.stringify(entry.tags), entry.author, entry.publishedAt]
  );

  // Invalidate feed cache
  const keys = await redis.keys("feed:*");
  if (keys.length) await redis.del(...keys);

  // Notify webhook subscribers
  const { rows: webhooks } = await pool.query("SELECT url FROM changelog_webhooks WHERE active = true");
  for (const wh of webhooks) {
    await redis.rpush("webhook:delivery:queue", JSON.stringify({ url: wh.url, payload: { event: "changelog.published", entry: { ...entry, id } } }));
  }

  return id;
}

Results

  • 3 feed formats — RSS 2.0 for traditional readers, Atom for standards compliance, JSON Feed for modern apps; all from same data; generated in <10ms
  • Filtered feeds/feed?types=breaking,deprecation for teams that only care about breaking changes; no noise from minor fixes
  • Webhook integration — new changelog entry → webhooks fire → CI pipeline re-reads API spec → SDK regenerated; automated response to API changes
  • Developer reach: 10% → 45% — RSS in feed readers + webhook in Slack + email digest weekly; developers get updates in their preferred channel
  • Cache with instant invalidation — feed cached for 5 minutes; new entry invalidates immediately; subscribers see updates within seconds