Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Analytics } from "@vercel/analytics/next";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { getFaqStructuredData } from "@/lib/faqs";
import "./globals.css";

const geistSans = Geist({
Expand Down Expand Up @@ -102,6 +103,8 @@ export default function RootLayout({
],
};

const faqStructuredData = getFaqStructuredData();

return (
<html lang="en">
<head>
Expand All @@ -113,6 +116,12 @@ export default function RootLayout({
__html: JSON.stringify(structuredData),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(faqStructuredData),
}}
/>
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
Expand Down
56 changes: 9 additions & 47 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import LZString from "lz-string";
import { useEffect, useState } from "react";
import { FAQS } from "@/lib/faqs";

type FeedItem = {
title?: string;
Expand Down Expand Up @@ -229,53 +230,14 @@ export default function Home() {
</div>
</div>

<div className="">
<h2 className="font-semibold text-gray-800">How do I use this?</h2>
<p className="text-gray-600">
Put the URLs of RSS feeds you want to combine in the box above;
idly (or passionately) browse the preview to make sure it's what
you want; hit the button to get a permalink (that's a base-64
encoded URL of the feeds, so no real worry about bitrot).
</p>
</div>

<div className="">
<h2 className="font-semibold text-gray-800">
Why would I want to do this?
</h2>
<p className="text-gray-600">
Lots of things take RSS. Relatively few things do a great job of
interleaving multiple RSS feeds. This is a simple tool to do that.
</p>
</div>

<div className="">
<h2 className="font-semibold text-gray-800">
May I refer to it as rss<sup>4</sup>?
</h2>
<p className="text-gray-600">If you insist.</p>
</div>

<div className="">
<h2 className="font-semibold text-gray-800">Who built this?</h2>
<p className="text-gray-600">
Your friends at{" "}
<a
href="https://buttondown.com?utm_source=rss4"
className="text-blue-600 hover:text-blue-800"
>
Buttondown
</a>
, and they even made it{" "}
<a
href="https://github.com/buttondown/rssrssrssrss"
className="text-blue-600 hover:text-blue-800"
>
open source
</a>
.
</p>
</div>
{FAQS.map((faq, index) => (
<div key={index} className="">
<h2 className="font-semibold text-gray-800">
{faq.questionJsx ?? faq.question}
</h2>
<p className="text-gray-600">{faq.answerJsx}</p>
</div>
))}

{errorMessage && (
<div className="mt-4 p-3 border border-red-300 rounded-md bg-red-50 text-red-700">
Expand Down
2 changes: 1 addition & 1 deletion src/lib/__snapshots__/rss.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ exports[`rss - XML to final output should handle failed feeds in merge 1`] = `
<item>
<title>Valid Article</title>
<guid></guid>
<pubDate>Mon, 28 Oct 2025 10:00:00 GMT</pubDate>
<pubDate>NORMALIZED_DATE</pubDate>
<source url="http://example.com/valid">Valid Feed</source>
</item>
</channel>
Expand Down
99 changes: 99 additions & 0 deletions src/lib/faqs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { ReactNode } from "react";

type FAQ = {
question: string;
questionJsx?: ReactNode;
answer: string;
answerJsx: ReactNode;
};

export const FAQS: FAQ[] = [
{
question: "How do I use this?",
answer:
"Put the URLs of RSS feeds you want to combine in the box; browse the preview to make sure it's what you want; hit the button to get a permalink (that's a base-64 encoded URL of the feeds, so no real worry about bitrot).",
answerJsx: (
<>
Put the URLs of RSS feeds you want to combine in the box above; idly (or
passionately) browse the preview to make sure it's what you want; hit
the button to get a permalink (that's a base-64 encoded URL of the
feeds, so no real worry about bitrot).
</>
),
},
{
question: "Why would I want to do this?",
answer:
"Lots of things take RSS. Relatively few things do a great job of interleaving multiple RSS feeds. This is a simple tool to do that.",
answerJsx: (
<>
Lots of things take RSS. Relatively few things do a great job of
interleaving multiple RSS feeds. This is a simple tool to do that.
</>
),
},
{
question: "May I refer to it as rss4?",
questionJsx: (
<>
May I refer to it as rss<sup>4</sup>?
</>
),
answer: "If you insist.",
answerJsx: <>If you insist.</>,
},
{
question: "What if I have a sitemap but not an RSS feed?",
answer:
"Check out Sitemap to RSS (https://www.sitemaptorss.com/), which does exactly what it says on the tin.",
answerJsx: (
<>
Check out{" "}
<a
href="https://www.sitemaptorss.com/"
className="text-blue-600 hover:text-blue-800"
>
Sitemap to RSS
</a>
, which does exactly what it says on the tin.
</>
),
},
{
question: "Who built this?",
answer:
"Your friends at Buttondown (https://buttondown.com), and they even made it open source (https://github.com/buttondown/rssrssrssrss).",
answerJsx: (
<>
Your friends at{" "}
<a
href="https://buttondown.com?utm_source=rss4"
className="text-blue-600 hover:text-blue-800"
>
Buttondown
</a>
, and they even made it{" "}
<a
href="https://github.com/buttondown/rssrssrssrss"
className="text-blue-600 hover:text-blue-800"
>
open source
</a>
.
</>
),
},
];

export const getFaqStructuredData = () => ({
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: FAQS.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: {
"@type": "Answer",
text: faq.answer,
},
})),
});
34 changes: 20 additions & 14 deletions src/lib/rss.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ describe("rss - XML to final output", () => {
// Parse XML feeds directly
const result1 = await parseFeedFromXml(
feed1Xml,
"http://example.com/feed1"
"http://example.com/feed1",
);
const result2 = await parseFeedFromXml(
feed2Xml,
"http://example2.com/feed2"
"http://example2.com/feed2",
);

expect(result1.error).toBeNull();
Expand All @@ -63,7 +63,7 @@ describe("rss - XML to final output", () => {
{ ...result1, url: "http://example.com/feed1" },
{ ...result2, url: "http://example2.com/feed2" },
],
"http://example.com/merged"
"http://example.com/merged",
);

// Generate RSS output
Expand All @@ -72,7 +72,7 @@ describe("rss - XML to final output", () => {
// Normalize dates in RSS output for snapshot comparison
const normalizedRss = rssOutput.replace(
/<lastBuildDate>.*?<\/lastBuildDate>/,
"<lastBuildDate>NORMALIZED_DATE</lastBuildDate>"
"<lastBuildDate>NORMALIZED_DATE</lastBuildDate>",
);

expect(normalizedRss).toMatchSnapshot();
Expand Down Expand Up @@ -102,13 +102,13 @@ describe("rss - XML to final output", () => {
// Merge feeds
const mergedFeed = mergeFeeds(
[{ ...result, url: "http://example.com/feed" }],
"http://example.com/merged"
"http://example.com/merged",
);

// Generate JSON Feed output
const jsonOutput = generateJSONFeed(
mergedFeed,
"http://example.com/merged"
"http://example.com/merged",
);

expect(jsonOutput).toMatchSnapshot();
Expand All @@ -128,7 +128,7 @@ describe("rss - XML to final output", () => {

const validResult = await parseFeedFromXml(
feedXml,
"http://example.com/valid"
"http://example.com/valid",
);
const failedResult = {
feed: null,
Expand All @@ -139,7 +139,7 @@ describe("rss - XML to final output", () => {
// Merge feeds including failed one
const mergedFeed = mergeFeeds(
[{ ...validResult, url: "http://example.com/valid" }, failedResult],
"http://example.com/merged"
"http://example.com/merged",
);

// Generate RSS output
Expand All @@ -148,9 +148,15 @@ describe("rss - XML to final output", () => {
// Normalize dates and timestamps in RSS output for snapshot comparison
// Error items have dynamically generated timestamps, so normalize them
const normalizedRss = rssOutput
.replace(/<lastBuildDate>.*?<\/lastBuildDate>/, "<lastBuildDate>NORMALIZED_DATE</lastBuildDate>")
.replace(
/<lastBuildDate>.*?<\/lastBuildDate>/,
"<lastBuildDate>NORMALIZED_DATE</lastBuildDate>",
)
.replace(/guid>error-.*?-\d+</g, "guid>error-NORMALIZED_TIMESTAMP<")
.replace(/<pubDate>Sat, \d+ Dec \d{4} \d{2}:\d{2}:\d{2} GMT<\/pubDate>/g, "<pubDate>NORMALIZED_DATE</pubDate>");
.replace(
/<pubDate>\w{3}, \d+ \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT<\/pubDate>/g,
"<pubDate>NORMALIZED_DATE</pubDate>",
);

expect(normalizedRss).toMatchSnapshot();
});
Expand Down Expand Up @@ -187,15 +193,15 @@ describe("rss - XML to final output", () => {
// Parse XML feed directly
const result = await parseFeedFromXml(
feedXml,
"https://www.evalapply.org/index.xml"
"https://www.evalapply.org/index.xml",
);

expect(result.error).toBeNull();

// Merge feeds
const mergedFeed = mergeFeeds(
[{ ...result, url: "https://www.evalapply.org/index.xml" }],
"http://example.com/merged"
"http://example.com/merged",
);

// Generate RSS output
Expand All @@ -204,15 +210,15 @@ describe("rss - XML to final output", () => {
// Normalize dates in RSS output for snapshot comparison
const normalizedRss = rssOutput.replace(
/<lastBuildDate>.*?<\/lastBuildDate>/,
"<lastBuildDate>NORMALIZED_DATE</lastBuildDate>"
"<lastBuildDate>NORMALIZED_DATE</lastBuildDate>",
);

expect(normalizedRss).toMatchSnapshot();

// Also test JSON Feed generation
const jsonOutput = generateJSONFeed(
mergedFeed,
"http://example.com/merged"
"http://example.com/merged",
);

expect(jsonOutput).toMatchSnapshot();
Expand Down
1 change: 1 addition & 0 deletions tsconfig.tsbuildinfo

Large diffs are not rendered by default.