Skip to content

Commit fda9b5c

Browse files
committed
Added RSS Channel
1 parent c6e0b3d commit fda9b5c

8 files changed

Lines changed: 395 additions & 221 deletions

File tree

app/[rss.xml].tsx

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { LoaderFunction } from 'react-router';
2+
import blogPosts from '@pages/blog/blogposts';
3+
import type { Dayjs } from 'dayjs';
4+
import dayjs from 'dayjs';
5+
import advancedFormat from 'dayjs/plugin/advancedFormat';
6+
import timezone from 'dayjs/plugin/timezone';
7+
import utc from 'dayjs/plugin/utc';
8+
9+
const BASE_URL = 'https://elanora.lol';
10+
11+
dayjs.extend(advancedFormat);
12+
dayjs.extend(timezone);
13+
dayjs.extend(utc);
14+
15+
const rssDateFormatString = 'ddd, DD MMMM YYYY HH:mm:ss z';
16+
17+
export interface RssItem {
18+
title: string;
19+
link: URL | string;
20+
description: string;
21+
pubDate: Dayjs | string;
22+
author: string;
23+
guid: string;
24+
}
25+
26+
export interface RssImage {
27+
url: URL | string;
28+
title: string;
29+
link: URL | string;
30+
width?: number;
31+
height?: number;
32+
description?: string;
33+
}
34+
35+
export interface RssChannelOptionals {
36+
language?: string;
37+
copyright?: string;
38+
managingEditor?: string;
39+
webMaster?: string;
40+
pubDate?: string;
41+
lastBuildDate?: string;
42+
category?: string;
43+
generator?: string;
44+
docs?: URL | string;
45+
cloud?: string;
46+
ttl?: number;
47+
image?: RssImage;
48+
rating?: string;
49+
textInput?: string;
50+
skiphours?: string;
51+
skipDays?: string;
52+
}
53+
54+
export interface RssChannelObj {
55+
title: string;
56+
link: URL | string;
57+
description: string;
58+
atomLink: URL | string;
59+
itemArray?: RssItem[];
60+
metaOptionals?: RssChannelOptionals;
61+
}
62+
63+
export const rssMetaDefaults: RssChannelOptionals = {
64+
language: 'en-us',
65+
generator:
66+
'https://github.com/elanora96/elanora96.github.io/blob/main/app/[rss.xml].tsx',
67+
docs: new URL('https://www.rssboard.org/rss-specification'),
68+
ttl: 60,
69+
};
70+
71+
const rssChannel: RssChannelObj = {
72+
title: 'elanora.lol blog posts',
73+
link: new URL(`${BASE_URL}/blog`),
74+
atomLink: new URL(`${BASE_URL}/blog/rss.xml`),
75+
description: 'The collected writings of elanora96',
76+
itemArray: blogPosts.map((post) => ({
77+
description: post.description ? post.description : '',
78+
author: 'rss@elanora.lol (Elanora Manson)',
79+
pubDate: post.date.format(rssDateFormatString),
80+
title: post.postName,
81+
link: new URL(`${BASE_URL}/blog/${post.postName}`),
82+
guid: `${BASE_URL}/blog/${post.postName}`,
83+
})),
84+
metaOptionals: {
85+
copyright: 'Copyright 2023 - ∞, Elanora Manson',
86+
managingEditor: 'rss@elanora.lol (Elanora Manson)',
87+
webMaster: 'rss@elanora.lol (Elanora Manson)',
88+
pubDate: dayjs().tz('UTC').format(rssDateFormatString),
89+
...rssMetaDefaults,
90+
},
91+
};
92+
93+
export const generateRssChannel = (props: RssChannelObj): string => {
94+
const { title, description, link, atomLink, itemArray, metaOptionals } =
95+
props;
96+
97+
return `<?xml version="1.0" encoding="UTF-8"?>
98+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
99+
<channel>
100+
<title>${title}</title>
101+
<description>${description}</description>
102+
<link>${link}</link>
103+
${
104+
metaOptionals
105+
? Object.entries(metaOptionals)
106+
.map(([key, value]) => `<${key}>${value}</${key}>`)
107+
.join('')
108+
: ''
109+
}
110+
<atom:link href="${atomLink}" rel="self" type="application/rss+xml" />
111+
${
112+
itemArray
113+
? itemArray
114+
.map(
115+
(item) => `
116+
<item>
117+
<title><![CDATA[${item.title}]]></title>
118+
<description><![CDATA[${item.description}]]></description>
119+
<pubDate>${item.pubDate}</pubDate>
120+
<link>${item.link}</link>
121+
${item.guid ? `<guid isPermaLink="false">${item.guid}</guid>` : ''}
122+
</item>`,
123+
)
124+
.join('')
125+
: ''
126+
}
127+
</channel>
128+
</rss>`;
129+
};
130+
131+
export const loader: LoaderFunction = () => {
132+
const feed = generateRssChannel(rssChannel);
133+
134+
const headersObj = {
135+
headers: {
136+
'Content-Type': 'application/xml',
137+
'Cache-Control': 'public, max-age=2419200',
138+
},
139+
};
140+
141+
return new Response(feed, headersObj);
142+
};

app/pages/blog/blogposts.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
1+
import type { Dayjs } from 'dayjs';
2+
import dayjs from 'dayjs';
3+
import advancedFormat from 'dayjs/plugin/advancedFormat';
4+
import timezone from 'dayjs/plugin/timezone';
5+
import utc from 'dayjs/plugin/utc';
6+
7+
dayjs.extend(advancedFormat);
8+
dayjs.extend(timezone);
9+
dayjs.extend(utc);
10+
dayjs.tz.setDefault('America/Los_Angeles');
11+
112
export interface BlogPost {
213
postName: string;
3-
date: string;
14+
date: Dayjs;
415
blogPostIndexPath: string;
16+
description?: string;
517
}
618

719
export const blogPosts: BlogPost[] = [
820
{
921
postName: 'This is a blog post',
10-
date: '2024-09-02 14:35:00',
22+
date: dayjs('2024-09-02 14:35:00'),
1123
blogPostIndexPath: 'pages/blog/posts/test.mdx',
24+
description: 'This is a test post',
1225
},
1326
{
1427
postName: '28 Years Later',
15-
date: '2024-09-07 18:33:00',
28+
date: dayjs('2024-09-07 18:33:00'),
1629
blogPostIndexPath: 'pages/blog/posts/28yearslater/28yearslater.mdx',
30+
description: 'A once dynamic invite to my 28th Birthday Party',
1731
},
1832
];
1933

app/pages/blog/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ const BlogTableOfContents = () => {
2929
</thead>
3030
<tbody>
3131
{blogPosts.sort(dateAscSort).map(({ date, postName }) => (
32-
<tr key={date}>
33-
<td>{dayjs(date).format('YYYY-MM-DD[ at ]hh:mma')}</td>
32+
<tr key={date.unix()}>
33+
<td>{date.format('YYYY-MM-DD[ at ]hh:mma')}</td>
3434
<td>
3535
<Link to={postName}>{postName}</Link>
3636
</td>
@@ -49,6 +49,11 @@ const Blog = () => {
4949
<Outlet />
5050
</div>
5151
<BlogTableOfContents />
52+
<div className={styles.RSS}>
53+
<Link to={'/blog/rss.xml'}>
54+
<h4>Add to RSS</h4>
55+
</Link>
56+
</div>
5257
</div>
5358
);
5459
};

app/pages/blog/styles.module.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@
1616
}
1717
}
1818

19+
.RSS {
20+
padding: 0.25rem;
21+
border: 0.175rem solid var(--color-orange0-dark);
22+
border-radius: 5%;
23+
background-color: var(--color-orange1-dark);
24+
25+
a {
26+
color: white;
27+
text-decoration: none;
28+
}
29+
}
30+
1931
.BlogTOC {
2032
display: flex;
2133
flex-direction: column;

app/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default [
1111
route('originalHome', 'pages/original-home/index.tsx'),
1212

1313
route('blog', 'pages/blog/index.tsx', blogPostNestedRoutes),
14+
route('blog/rss.xml', '[rss.xml].tsx'),
1415

1516
route('*?', 'pages/catchall/catchall.mdx'),
1617
] satisfies RouteConfig;

flake.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)