Skip to content

Commit a5d11e4

Browse files
authored
Add JSONLD for the FAQs; nod to new project (#19)
* WIP * WIP * WIP
1 parent 31a43ce commit a5d11e4

File tree

6 files changed

+139
-62
lines changed

6 files changed

+139
-62
lines changed

src/app/layout.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Analytics } from "@vercel/analytics/next";
22
import type { Metadata } from "next";
33
import { Geist, Geist_Mono } from "next/font/google";
4+
import { getFaqStructuredData } from "@/lib/faqs";
45
import "./globals.css";
56

67
const geistSans = Geist({
@@ -102,6 +103,8 @@ export default function RootLayout({
102103
],
103104
};
104105

106+
const faqStructuredData = getFaqStructuredData();
107+
105108
return (
106109
<html lang="en">
107110
<head>
@@ -113,6 +116,12 @@ export default function RootLayout({
113116
__html: JSON.stringify(structuredData),
114117
}}
115118
/>
119+
<script
120+
type="application/ld+json"
121+
dangerouslySetInnerHTML={{
122+
__html: JSON.stringify(faqStructuredData),
123+
}}
124+
/>
116125
</head>
117126
<body
118127
className={`${geistSans.variable} ${geistMono.variable} antialiased`}

src/app/page.tsx

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import LZString from "lz-string";
44
import { useEffect, useState } from "react";
5+
import { FAQS } from "@/lib/faqs";
56

67
type FeedItem = {
78
title?: string;
@@ -229,53 +230,14 @@ export default function Home() {
229230
</div>
230231
</div>
231232

232-
<div className="">
233-
<h2 className="font-semibold text-gray-800">How do I use this?</h2>
234-
<p className="text-gray-600">
235-
Put the URLs of RSS feeds you want to combine in the box above;
236-
idly (or passionately) browse the preview to make sure it's what
237-
you want; hit the button to get a permalink (that's a base-64
238-
encoded URL of the feeds, so no real worry about bitrot).
239-
</p>
240-
</div>
241-
242-
<div className="">
243-
<h2 className="font-semibold text-gray-800">
244-
Why would I want to do this?
245-
</h2>
246-
<p className="text-gray-600">
247-
Lots of things take RSS. Relatively few things do a great job of
248-
interleaving multiple RSS feeds. This is a simple tool to do that.
249-
</p>
250-
</div>
251-
252-
<div className="">
253-
<h2 className="font-semibold text-gray-800">
254-
May I refer to it as rss<sup>4</sup>?
255-
</h2>
256-
<p className="text-gray-600">If you insist.</p>
257-
</div>
258-
259-
<div className="">
260-
<h2 className="font-semibold text-gray-800">Who built this?</h2>
261-
<p className="text-gray-600">
262-
Your friends at{" "}
263-
<a
264-
href="https://buttondown.com?utm_source=rss4"
265-
className="text-blue-600 hover:text-blue-800"
266-
>
267-
Buttondown
268-
</a>
269-
, and they even made it{" "}
270-
<a
271-
href="https://github.com/buttondown/rssrssrssrss"
272-
className="text-blue-600 hover:text-blue-800"
273-
>
274-
open source
275-
</a>
276-
.
277-
</p>
278-
</div>
233+
{FAQS.map((faq, index) => (
234+
<div key={index} className="">
235+
<h2 className="font-semibold text-gray-800">
236+
{faq.questionJsx ?? faq.question}
237+
</h2>
238+
<p className="text-gray-600">{faq.answerJsx}</p>
239+
</div>
240+
))}
279241

280242
{errorMessage && (
281243
<div className="mt-4 p-3 border border-red-300 rounded-md bg-red-50 text-red-700">

src/lib/__snapshots__/rss.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ exports[`rss - XML to final output should handle failed feeds in merge 1`] = `
7676
<item>
7777
<title>Valid Article</title>
7878
<guid></guid>
79-
<pubDate>Mon, 28 Oct 2025 10:00:00 GMT</pubDate>
79+
<pubDate>NORMALIZED_DATE</pubDate>
8080
<source url="http://example.com/valid">Valid Feed</source>
8181
</item>
8282
</channel>

src/lib/faqs.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { ReactNode } from "react";
2+
3+
type FAQ = {
4+
question: string;
5+
questionJsx?: ReactNode;
6+
answer: string;
7+
answerJsx: ReactNode;
8+
};
9+
10+
export const FAQS: FAQ[] = [
11+
{
12+
question: "How do I use this?",
13+
answer:
14+
"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).",
15+
answerJsx: (
16+
<>
17+
Put the URLs of RSS feeds you want to combine in the box above; idly (or
18+
passionately) browse the preview to make sure it's what you want; hit
19+
the button to get a permalink (that's a base-64 encoded URL of the
20+
feeds, so no real worry about bitrot).
21+
</>
22+
),
23+
},
24+
{
25+
question: "Why would I want to do this?",
26+
answer:
27+
"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.",
28+
answerJsx: (
29+
<>
30+
Lots of things take RSS. Relatively few things do a great job of
31+
interleaving multiple RSS feeds. This is a simple tool to do that.
32+
</>
33+
),
34+
},
35+
{
36+
question: "May I refer to it as rss4?",
37+
questionJsx: (
38+
<>
39+
May I refer to it as rss<sup>4</sup>?
40+
</>
41+
),
42+
answer: "If you insist.",
43+
answerJsx: <>If you insist.</>,
44+
},
45+
{
46+
question: "What if I have a sitemap but not an RSS feed?",
47+
answer:
48+
"Check out Sitemap to RSS (https://www.sitemaptorss.com/), which does exactly what it says on the tin.",
49+
answerJsx: (
50+
<>
51+
Check out{" "}
52+
<a
53+
href="https://www.sitemaptorss.com/"
54+
className="text-blue-600 hover:text-blue-800"
55+
>
56+
Sitemap to RSS
57+
</a>
58+
, which does exactly what it says on the tin.
59+
</>
60+
),
61+
},
62+
{
63+
question: "Who built this?",
64+
answer:
65+
"Your friends at Buttondown (https://buttondown.com), and they even made it open source (https://github.com/buttondown/rssrssrssrss).",
66+
answerJsx: (
67+
<>
68+
Your friends at{" "}
69+
<a
70+
href="https://buttondown.com?utm_source=rss4"
71+
className="text-blue-600 hover:text-blue-800"
72+
>
73+
Buttondown
74+
</a>
75+
, and they even made it{" "}
76+
<a
77+
href="https://github.com/buttondown/rssrssrssrss"
78+
className="text-blue-600 hover:text-blue-800"
79+
>
80+
open source
81+
</a>
82+
.
83+
</>
84+
),
85+
},
86+
];
87+
88+
export const getFaqStructuredData = () => ({
89+
"@context": "https://schema.org",
90+
"@type": "FAQPage",
91+
mainEntity: FAQS.map((faq) => ({
92+
"@type": "Question",
93+
name: faq.question,
94+
acceptedAnswer: {
95+
"@type": "Answer",
96+
text: faq.answer,
97+
},
98+
})),
99+
});

src/lib/rss.test.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ describe("rss - XML to final output", () => {
4747
// Parse XML feeds directly
4848
const result1 = await parseFeedFromXml(
4949
feed1Xml,
50-
"http://example.com/feed1"
50+
"http://example.com/feed1",
5151
);
5252
const result2 = await parseFeedFromXml(
5353
feed2Xml,
54-
"http://example2.com/feed2"
54+
"http://example2.com/feed2",
5555
);
5656

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

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

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

108108
// Generate JSON Feed output
109109
const jsonOutput = generateJSONFeed(
110110
mergedFeed,
111-
"http://example.com/merged"
111+
"http://example.com/merged",
112112
);
113113

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

129129
const validResult = await parseFeedFromXml(
130130
feedXml,
131-
"http://example.com/valid"
131+
"http://example.com/valid",
132132
);
133133
const failedResult = {
134134
feed: null,
@@ -139,7 +139,7 @@ describe("rss - XML to final output", () => {
139139
// Merge feeds including failed one
140140
const mergedFeed = mergeFeeds(
141141
[{ ...validResult, url: "http://example.com/valid" }, failedResult],
142-
"http://example.com/merged"
142+
"http://example.com/merged",
143143
);
144144

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

155161
expect(normalizedRss).toMatchSnapshot();
156162
});
@@ -187,15 +193,15 @@ describe("rss - XML to final output", () => {
187193
// Parse XML feed directly
188194
const result = await parseFeedFromXml(
189195
feedXml,
190-
"https://www.evalapply.org/index.xml"
196+
"https://www.evalapply.org/index.xml",
191197
);
192198

193199
expect(result.error).toBeNull();
194200

195201
// Merge feeds
196202
const mergedFeed = mergeFeeds(
197203
[{ ...result, url: "https://www.evalapply.org/index.xml" }],
198-
"http://example.com/merged"
204+
"http://example.com/merged",
199205
);
200206

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

210216
expect(normalizedRss).toMatchSnapshot();
211217

212218
// Also test JSON Feed generation
213219
const jsonOutput = generateJSONFeed(
214220
mergedFeed,
215-
"http://example.com/merged"
221+
"http://example.com/merged",
216222
);
217223

218224
expect(jsonOutput).toMatchSnapshot();

tsconfig.tsbuildinfo

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)