Skip to content

Commit 498e09b

Browse files
committed
feat: implement RSS and sitemap generation with global posts metadata
1 parent aef1faf commit 498e09b

File tree

9 files changed

+186
-25
lines changed

9 files changed

+186
-25
lines changed

src/lib/components/seo-head/seo-head.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,9 @@
4343
<meta name="twitter:title" content={metadata.title} />
4444
<meta name="twitter:description" content={metadata.description} />
4545
<meta name="twitter:image" content={metadata.image} />
46+
47+
<!-- sitemap.xml and rss.xml -->
48+
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
49+
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml" />
50+
<link rel="alternate" type="application/rss+xml" title="Atom" href="/atom.xml" />
4651
</svelte:head>

src/lib/server/posts/services/get-posts.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import { z } from 'zod';
66
import { generateOgURL } from '../../../utils/og';
77
import { detectLanguage } from '../lib/detect-language';
88
import { rawPostSchema } from '../schemas/raw-post-schema';
9-
import type { PostMetadata } from '../types';
9+
import type { PostBloatedMetadata, PostMetadata } from '../types';
1010

1111
/**
1212
* Fetches all the posts metadata. It also adds the slug and the SEO metadata
1313
* @returns all the posts metadata
1414
*/
15-
export async function getPosts(): Promise<PostMetadata[]> {
15+
async function getPosts(): Promise<PostBloatedMetadata[]> {
1616
const postFiles = import.meta.glob('/content/posts/**/*.md');
1717

18-
const postMetadataPromises: Promise<PostMetadata>[] = Object.entries(postFiles).map(
18+
const postMetadataPromises: Promise<PostBloatedMetadata>[] = Object.entries(postFiles).map(
1919
async ([filepath, resolver]) => {
2020
const resolverData = await resolver();
2121

@@ -62,8 +62,10 @@ export async function getPosts(): Promise<PostMetadata[]> {
6262
}
6363
};
6464

65-
const data: PostMetadata = {
65+
const data: PostBloatedMetadata = {
6666
cover,
67+
filepath,
68+
renderedHtml,
6769
language,
6870
slug,
6971
readingTime: expectedReadingTime.minutes,
@@ -90,3 +92,12 @@ export async function getPosts(): Promise<PostMetadata[]> {
9092
return bDate.getTime() - aDate.getTime();
9193
});
9294
}
95+
96+
export const GLOBAL_POSTS: PostBloatedMetadata[] = await getPosts();
97+
export const GLOBAL_POSTS_SLIM: PostMetadata[] = GLOBAL_POSTS.map((post) => {
98+
return {
99+
...post,
100+
renderedHtml: undefined,
101+
filepath: undefined,
102+
}
103+
});

src/lib/server/posts/types/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ export type PostMetadata = Required<
88
}
99
>;
1010

11+
export type PostBloatedMetadata = Required<
12+
RawPostSchema & {
13+
slug: string;
14+
language: Language;
15+
readingTime: number;
16+
filepath: string;
17+
renderedHtml: string;
18+
}
19+
>;
20+
1121
export type Language =
1222
| { niceName: 'Português'; code: 'pt-br' }
1323
| { niceName: 'English'; code: 'en' };

src/routes/+page.server.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import { postMessage } from '$/lib/server/contact/services/post-message';
2-
import { getPosts } from '$/lib/server/posts/services/get-posts';
2+
import { GLOBAL_POSTS_SLIM } from '$/lib/server/posts/services/get-posts';
33
import { getDemoProjects } from '$/lib/server/projects/services/get-demo-projects';
44
import { getFeaturedProjects } from '$/lib/server/projects/services/get-feature-projects';
55
import { getProjects } from '$/lib/server/projects/services/get-projects';
66
import { fail, redirect } from '@sveltejs/kit';
77
import type { Actions, PageServerLoad } from './$types';
88

99
export const load: PageServerLoad = async () => {
10-
const posts = await getPosts();
10+
const featuredPost = GLOBAL_POSTS_SLIM[0];
1111

12-
const featuredPost = posts[0];
13-
14-
const recentPosts = posts.splice(1, 3);
12+
const recentPosts = GLOBAL_POSTS_SLIM.splice(1, 3);
1513

1614
const featuredProjects = getFeaturedProjects();
1715

src/routes/blog/+page.server.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import { extractCategories } from '$/lib/server/posts/lib/categories';
2-
import { getPosts } from '$/lib/server/posts/services/get-posts';
2+
import { GLOBAL_POSTS_SLIM } from '$/lib/server/posts/services/get-posts';
33
import Fuse from 'fuse.js';
44
import type { PageServerLoad } from './$types';
55

66
export const load: PageServerLoad = async ({ url }) => {
77
const search = url.searchParams.get('search');
88

9-
const posts = await getPosts();
10-
11-
const featuredPost = posts.shift();
9+
const featuredPost = GLOBAL_POSTS_SLIM.shift();
1210

1311
if (search) {
14-
const fuse = new Fuse(posts, {
12+
const fuse = new Fuse(GLOBAL_POSTS_SLIM, {
1513
keys: ['title', { name: 'tags', weight: 0.5 }, { name: 'excerpt', weight: 0.5 }]
1614
});
1715

@@ -20,7 +18,7 @@ export const load: PageServerLoad = async ({ url }) => {
2018
return { posts: filteredPosts, featuredPost: null, search };
2119
}
2220

23-
const categories = extractCategories(posts);
21+
const categories = extractCategories(GLOBAL_POSTS_SLIM);
2422

25-
return { posts, featuredPost, search, categories };
23+
return { posts: GLOBAL_POSTS_SLIM, featuredPost, search, categories };
2624
};
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1+
import { getPostRecommendations } from '$/lib/server/posts/lib/get-post-recommendations';
2+
import { GLOBAL_POSTS_SLIM } from '$/lib/server/posts/services/get-posts';
13
import { error } from '@sveltejs/kit';
24
import type { PageServerLoad } from './$types';
3-
import { getPosts } from '$/lib/server/posts/services/get-posts';
4-
import { getPostRecommendations } from '$/lib/server/posts/lib/get-post-recommendations';
55

66
export const load: PageServerLoad = async ({ params }) => {
77
const { post: slug } = params;
88

9-
const posts = await getPosts();
109

11-
const currentPostIndex = posts.findIndex((post) => slug.toLowerCase() === post.slug);
10+
const currentPostIndex = GLOBAL_POSTS_SLIM
11+
.findIndex((post) => slug.toLowerCase() === post.slug);
1212

1313
if (currentPostIndex === -1) {
1414
error(404, 'Post not found');
1515
}
1616

17-
const currentPost = posts[currentPostIndex];
17+
const currentPost = GLOBAL_POSTS_SLIM[currentPostIndex];
1818

1919
return {
2020
post: currentPost,
21-
recommendations: getPostRecommendations(posts, currentPostIndex)
21+
recommendations: getPostRecommendations(GLOBAL_POSTS_SLIM, currentPostIndex)
2222
};
2323
};

src/routes/blog/categories/[category]/+page.server.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { filterPostsByCategory } from '$/lib/server/posts/lib/categories';
2-
import { getPosts } from '$/lib/server/posts/services/get-posts';
2+
import { GLOBAL_POSTS_SLIM } from '$/lib/server/posts/services/get-posts';
33
import { error } from '@sveltejs/kit';
44
import type { PageServerLoad } from './$types';
55

66
export const load: PageServerLoad = async ({ params }) => {
77
const category = params.category;
8-
const posts = await getPosts();
98

10-
const filteredPosts = filterPostsByCategory(posts, category);
9+
const filteredPosts = filterPostsByCategory(GLOBAL_POSTS_SLIM, category);
1110

1211
if (filteredPosts.length === 0) {
1312
error(404, `Category "${category}" not found`);

src/routes/rss.xml/+server.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { website } from '$/lib/assets/config/website';
2+
import { GLOBAL_POSTS } from '$/lib/server/posts/services/get-posts';
3+
4+
export const prerender = true;
5+
6+
type FeedItem = {
7+
title: string;
8+
slug: string;
9+
publishDate: string;
10+
content: string;
11+
};
12+
13+
const feedUpdated = new Date();
14+
15+
const xml = (posts: FeedItem[]) => `<?xml version="1.0" encoding="utf-8"?>
16+
<feed xmlns="http://www.w3.org/2005/Atom">
17+
<title>${website.title} - Blog</title>
18+
<link href="${website.url}/rss.xml" rel="self"/>
19+
<link href="${website.url}"/>
20+
<id>${website.url}/</id>
21+
<updated>${feedUpdated.toISOString()}</updated>
22+
<author>
23+
<name>Adam Sanderson</name>
24+
</author>
25+
<subtitle>${website.description}</subtitle>
26+
<generator>SvelteKit</generator>
27+
${posts
28+
.map(
29+
(post) => ` <entry>
30+
<title>${post.title}</title>
31+
<link href="${website.url}/blog/${post.slug}/"/>
32+
<id>${website.url}/blog/${post.slug}/</id>
33+
<updated>${new Date(post.publishDate).toISOString()}</updated>
34+
<published>${new Date(post.publishDate).toISOString()}</published>
35+
<content type="html"><![CDATA[${post.content}]]></content>
36+
</entry>`
37+
)
38+
.join('\n')}
39+
</feed>`;
40+
41+
42+
export async function GET() {
43+
const feedItems: FeedItem[] = GLOBAL_POSTS.map((post) => ({
44+
title: post.title,
45+
slug: post.slug,
46+
publishDate: post.date,
47+
content: post.renderedHtml,
48+
}));
49+
50+
const headers = {
51+
'Cache-Control': 'max-age=0, s-maxage=3600',
52+
'Content-Type': 'application/xml'
53+
};
54+
55+
const body = xml(feedItems);
56+
57+
return new Response(body, { headers });
58+
}

src/routes/sitemap.xml/+server.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { website } from '$/lib/assets/config/website';
2+
import { extractCategories } from '$/lib/server/posts/lib/categories';
3+
import { GLOBAL_POSTS_SLIM } from '$/lib/server/posts/services/get-posts';
4+
import fs from 'node:fs';
5+
import path from 'node:path';
6+
7+
export const prerender = true;
8+
9+
export async function GET() {
10+
const categories = extractCategories(GLOBAL_POSTS_SLIM);
11+
12+
// read everything in the src/routes folder, then only considers the ones with a +page.svelte file
13+
const routes = fs
14+
.readdirSync(path.join('src', 'routes'), { recursive: true })
15+
.filter((file) => typeof file === 'string' && file.endsWith('+page.svelte'))
16+
.map((file) => {
17+
if (typeof file !== 'string') {
18+
throw new Error('File is not a string');
19+
}
20+
21+
const pathname = file.replace(/\/\+page\.svelte|\+page\.svelte/, '');
22+
23+
const route = website.url + '/' + pathname;
24+
25+
if (route.includes('[post]')) {
26+
return GLOBAL_POSTS_SLIM.map(({ slug, date }) => ({
27+
url: route.replace('[post]', slug),
28+
date: new Date(date).toISOString()
29+
}));
30+
}
31+
32+
if (route.includes('[category]')) {
33+
return categories.map((slug) => ({
34+
url: route.replace('[category]', slug),
35+
date: new Date().toISOString()
36+
}));
37+
}
38+
39+
return {
40+
url: route,
41+
date: new Date().toISOString()
42+
};
43+
})
44+
.flat();
45+
46+
const urls = routes.map((route) => {
47+
return `
48+
<url>
49+
<loc>${route.url}</loc>
50+
<lastmod>${route.date}</lastmod>
51+
<changefreq>weekly</changefreq>
52+
<priority>0.5</priority>
53+
</url>
54+
`;
55+
});
56+
57+
return new Response(
58+
`
59+
<?xml version="1.0" encoding="UTF-8" ?>
60+
<urlset
61+
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
62+
xmlns:xhtml="https://www.w3.org/1999/xhtml"
63+
xmlns:mobile="https://www.google.com/schemas/sitemap-mobile/1.0"
64+
xmlns:news="https://www.google.com/schemas/sitemap-news/0.9"
65+
xmlns:image="https://www.google.com/schemas/sitemap-image/1.1"
66+
xmlns:video="https://www.google.com/schemas/sitemap-video/1.1"
67+
>
68+
<url>
69+
<loc>${website.url}</loc>
70+
<lastmod>${new Date().toISOString()}</lastmod>
71+
<changefreq>weekly</changefreq>
72+
<priority>1.0</priority>
73+
</url>
74+
${urls.join('')}
75+
</urlset>`.trim(),
76+
{
77+
headers: {
78+
'Content-Type': 'application/xml'
79+
}
80+
}
81+
);
82+
}

0 commit comments

Comments
 (0)