Skip to content

Commit ee7dbf9

Browse files
committed
feat(nx-dev): add rss and atom feeds
1 parent 6fd5530 commit ee7dbf9

File tree

4 files changed

+144
-1
lines changed

4 files changed

+144
-1
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { blogApi } from '../../../lib/blog.api';
2+
3+
function escapeHtml(text: string): string {
4+
return text
5+
.replace(/&/g, '&')
6+
.replace(/</g, '&lt;')
7+
.replace(/>/g, '&gt;')
8+
.replace(/"/g, '&quot;')
9+
.replace(/'/g, '&#39;');
10+
}
11+
12+
function getExcerpt(content: string, description?: string): string {
13+
if (description && description.trim().length > 0) {
14+
return description;
15+
}
16+
const paragraphs = content
17+
.split(/\n\n+/)
18+
.map((p) => p.trim())
19+
.filter((p) => p.length > 0);
20+
return paragraphs.slice(0, 2).join('\n\n');
21+
}
22+
23+
export async function GET() {
24+
const posts = await blogApi.getBlogs((p) => !!p.published);
25+
const items = posts
26+
.map((post) => {
27+
const link = `https://nx.dev/blog/${post.slug}`;
28+
const excerpt = getExcerpt(post.content, post.description);
29+
const authors =
30+
post.authors && post.authors.length > 0
31+
? post.authors
32+
: [{ name: 'Nx Team', image: '', twitter: '', github: '' }];
33+
const authorElements = authors
34+
.map(
35+
(author) =>
36+
`\n <author>\n <name>${escapeHtml(
37+
author.name
38+
)}</name>\n </author>`
39+
)
40+
.join('');
41+
return `\n <entry>\n <title>${escapeHtml(
42+
post.title
43+
)}</title>\n <link href="${link}"/>\n <id>${link}</id>\n <updated>${new Date(
44+
post.date
45+
).toISOString()}</updated>${authorElements}\n <summary><![CDATA[${excerpt}]]></summary>\n </entry>`;
46+
})
47+
.join('');
48+
49+
const atom = `<?xml version="1.0" encoding="utf-8"?>\n<feed xmlns="http://www.w3.org/2005/Atom">\n <title>Nx Blog</title>\n <link href="https://nx.dev/blog"/>\n <link rel="self" href="https://nx.dev/blog/atom.xml"/>\n <id>https://nx.dev/blog</id>\n <updated>${new Date().toISOString()}</updated>\n <author>\n <name>Nx Team</name>\n <email>[email protected]</email>\n </author>${items}\n</feed>`;
50+
51+
return new Response(atom, {
52+
headers: {
53+
'Content-Type': 'application/atom+xml',
54+
},
55+
});
56+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { blogApi } from '../../../lib/blog.api';
2+
3+
function escapeHtml(text: string): string {
4+
return text
5+
.replace(/&/g, '&amp;')
6+
.replace(/</g, '&lt;')
7+
.replace(/>/g, '&gt;')
8+
.replace(/"/g, '&quot;')
9+
.replace(/'/g, '&#39;');
10+
}
11+
12+
function getExcerpt(content: string, description?: string): string {
13+
if (description && description.trim().length > 0) {
14+
return description;
15+
}
16+
const paragraphs = content
17+
.split(/\n\n+/)
18+
.map((p) => p.trim())
19+
.filter((p) => p.length > 0);
20+
return paragraphs.slice(0, 2).join('\n\n');
21+
}
22+
23+
export async function GET() {
24+
const posts = await blogApi.getBlogs((p) => !!p.published);
25+
const items = posts
26+
.map((post) => {
27+
const link = `https://nx.dev/blog/${post.slug}`;
28+
const excerpt = getExcerpt(post.content, post.description);
29+
const authorString =
30+
post.authors && post.authors.length > 0
31+
? post.authors.map((author) => author.name).join(', ')
32+
: 'Nx Team';
33+
return `\n <item>\n <title>${escapeHtml(
34+
post.title
35+
)}</title>\n <description><![CDATA[${excerpt}]]></description>\n <link>${link}</link>\n <guid>${link}</guid>\n <author>${escapeHtml(
36+
authorString
37+
)}</author>\n <pubDate>${new Date(
38+
post.date
39+
).toUTCString()}</pubDate>\n </item>`;
40+
})
41+
.join('');
42+
43+
const rss = `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0">\n <channel>\n <title>Nx Blog</title>\n <link>https://nx.dev/blog</link>\n <description>Updates from the Nx team</description>\n <managingEditor>[email protected] (Nx Team)</managingEditor>\n <webMaster>[email protected] (Nx Team)</webMaster>${items}\n </channel>\n</rss>`;
44+
45+
return new Response(rss, {
46+
headers: {
47+
'Content-Type': 'application/rss+xml',
48+
},
49+
});
50+
}

nx-dev/nx-dev/app/layout.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export const metadata: Metadata = {
3838
rel: 'mask-icon',
3939
},
4040
],
41+
alternates: {
42+
types: {
43+
'application/rss+xml': '/blog/rss.xml',
44+
'application/atom+xml': '/blog/atom.xml',
45+
},
46+
},
4147
};
4248

4349
// Viewport settings for the entire site
@@ -62,6 +68,18 @@ export default function RootLayout({ children }: { children: ReactNode }) {
6268
content="#DA532C"
6369
key="windows-tile-color"
6470
/>
71+
<link
72+
rel="alternate"
73+
type="application/rss+xml"
74+
title="Nx Blog RSS Feed"
75+
href="/blog/rss.xml"
76+
/>
77+
<link
78+
rel="alternate"
79+
type="application/atom+xml"
80+
title="Nx Blog Atom Feed"
81+
href="/blog/atom.xml"
82+
/>
6583
<script
6684
type="text/javascript"
6785
dangerouslySetInnerHTML={{

nx-dev/ui-blog/src/lib/blog-container.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FeaturedBlogs } from './featured-blogs';
55
import { useEffect, useMemo, useState } from 'react';
66
import { Filters } from './filters';
77
import { useSearchParams } from 'next/navigation';
8+
import Link from 'next/link';
89
import { ALL_TOPICS } from './topics';
910
import {
1011
ComputerDesktopIcon,
@@ -15,6 +16,8 @@ import {
1516
ChatBubbleOvalLeftEllipsisIcon,
1617
ListBulletIcon,
1718
VideoCameraIcon,
19+
RssIcon,
20+
AtSymbolIcon,
1821
} from '@heroicons/react/24/outline';
1922

2023
export interface BlogContainerProps {
@@ -94,8 +97,24 @@ export function BlogContainer({ blogPosts, tags }: BlogContainerProps) {
9497
<FeaturedBlogs blogs={firstFiveBlogs} />
9598
{!!remainingBlogs.length && (
9699
<>
97-
<div className="mx-auto mb-8 mt-20 border-b-2 border-slate-300 pb-3 text-sm dark:border-slate-700">
100+
<div className="mx-auto mb-8 mt-20 flex items-center justify-between border-b-2 border-slate-300 pb-3 text-sm dark:border-slate-700">
98101
<h2 className="font-semibold">More blogs</h2>
102+
<div className="flex gap-2">
103+
<Link
104+
href="/blog/rss.xml"
105+
aria-label="RSS feed"
106+
prefetch={false}
107+
>
108+
<RssIcon className="h-5 w-5 text-slate-600 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200" />
109+
</Link>
110+
<Link
111+
href="/blog/atom.xml"
112+
aria-label="Atom feed"
113+
prefetch={false}
114+
>
115+
<AtSymbolIcon className="h-5 w-5 text-slate-600 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200" />
116+
</Link>
117+
</div>
99118
</div>
100119
<MoreBlogs blogs={remainingBlogs} />
101120
</>

0 commit comments

Comments
 (0)