Skip to content

Commit 48b068c

Browse files
committed
Add images to feeds
1 parent 7274f85 commit 48b068c

13 files changed

Lines changed: 125 additions & 23 deletions

File tree

src/lib/schemas/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { default as eoApiErrorSchema } from './eo-api-error-schema';
22
export { default as eoListSchema } from './eo-list-schema';
33
export { default as eoSubscriberSchema } from './eo-subscriber-schema';
4+
export { default as rssItemSchema } from './rssItemSchema';
45
export { default as subscribeFormSchema } from './subscribeFormSchema';
56
export { default as unsubscribeFormSchema } from './unsubscribeFormSchema';

src/lib/schemas/rssItemSchema.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as z from 'zod';
2+
3+
export default z.object({
4+
title: z.string().min(1),
5+
description: z.string().min(1),
6+
link: z.url(),
7+
language: z.literal('en-US'),
8+
pubDate: z.iso.datetime(),
9+
/** Generally, there can be multiple categories. But in our case at most one. */
10+
category: z.enum(['Post', 'Note']).optional(),
11+
enclosure: z
12+
.object({
13+
url: z.url(),
14+
type: z.literal('image/png'),
15+
length: z.int().nonnegative().optional()
16+
})
17+
.optional()
18+
});

src/lib/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { subscribeFormSchema } from '$lib/schemas';
1+
import { subscribeFormSchema, rssItemSchema } from '$lib/schemas';
22
import type { EmbedOptions, Project } from '@stackblitz/sdk';
33
import type { z } from 'zod';
44

55
// Inferred from schemas.
6+
export type RssItem = z.infer<typeof rssItemSchema>;
67
export type SubscribeForm = z.infer<typeof subscribeFormSchema>;
78

89
// Other types.

src/lib/utils/escapeXml.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Escapes special characters for XML content.
3+
* This ensures that content is safe to include in XML documents.
4+
*/
5+
export function escapeXml(text: string): string {
6+
return text
7+
.replace(/&/g, '&amp;')
8+
.replace(/</g, '&lt;')
9+
.replace(/>/g, '&gt;')
10+
.replace(/"/g, '&quot;')
11+
.replace(/'/g, '&apos;');
12+
}

src/routes/api/notes/2025/+server.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { resolveNote } from '$lib/server/resolvers';
12
import noteAmIStillRelevantAsADeveloper from '$notes/(2025)/am-i-still-relevant-as-a-developer/meta';
23
import noteEnvVarsInCode from '$notes/(2025)/env-vars-in-vscode/meta';
34
import noteHostingInEurope from '$notes/(2025)/hosting-in-europe-beyond-big-tech/meta';
@@ -13,13 +14,21 @@ export const prerender = true;
1314

1415
export const GET: RequestHandler = async () => {
1516
// Sort order: latest first.
16-
return json([
17+
const notes = [
1718
notePenpotsMissingSvgTextToPathFeature,
1819
notePrerenderingServerRoutes,
1920
noteHostingInEurope,
2021
noteAmIStillRelevantAsADeveloper,
2122
noteTheUmpteenthInterviewWithRichHarris,
2223
noteHowToWriteExceptionalDocs,
2324
noteEnvVarsInCode
24-
]);
25+
];
26+
27+
const resolvedNotes = await Promise.all(
28+
notes.map((note) => {
29+
return resolveNote(note);
30+
})
31+
);
32+
33+
return json(resolvedNotes);
2534
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { PUBLIC_URL_ORIGIN } from '$env/static/public';
2+
import type { RssItem } from '$lib/types';
3+
import { escapeXml } from '$lib/utils/escapeXml';
4+
import type { NoteMeta } from '@maiertech/sveltekit-helpers';
5+
import { json } from '@sveltejs/kit';
6+
import type { RequestHandler } from './$types';
7+
8+
export const prerender = true;
9+
10+
export const GET: RequestHandler = async ({ fetch }) => {
11+
const response = await fetch('/api/notes/latest');
12+
const notes = (await response.json()) as NoteMeta[];
13+
14+
const rssItems: RssItem[] = notes.map((note) => ({
15+
title: escapeXml(note.title),
16+
description: escapeXml(note.description),
17+
link: `${PUBLIC_URL_ORIGIN}${note.path}`,
18+
language: 'en-US',
19+
pubDate: new Date(note.publishedDate).toUTCString(),
20+
category: 'Note',
21+
enclosure: note.ogImageUrl
22+
? {
23+
url: escapeXml(note.ogImageUrl),
24+
type: 'image/png'
25+
}
26+
: undefined
27+
}));
28+
29+
return json(rssItems);
30+
};

src/routes/api/posts/2021/+server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ export const GET: RequestHandler = async (event) => {
1818
postMoveYourIdeToTheCloud
1919
];
2020

21-
const transformedPosts = await Promise.all(
21+
const resolvedPosts = await Promise.all(
2222
posts.map((post) => {
2323
return resolvePost({ postMeta: post, event });
2424
})
2525
);
2626

27-
return json(transformedPosts);
27+
return json(resolvedPosts);
2828
};

src/routes/api/posts/2022/+server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ export const GET: RequestHandler = async (event) => {
2626
postSvelteKitHmrOnGitpod
2727
];
2828

29-
const transformedPosts = await Promise.all(
29+
const resolvedPosts = await Promise.all(
3030
posts.map((post) => {
3131
return resolvePost({ postMeta: post, event });
3232
})
3333
);
3434

35-
return json(transformedPosts);
35+
return json(resolvedPosts);
3636
};

src/routes/api/posts/2023/+server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ export const GET: RequestHandler = async (event) => {
2424
postStackBlitzCodeflowBeta
2525
];
2626

27-
const transformedPosts = await Promise.all(
27+
const resolvedPosts = await Promise.all(
2828
posts.map((post) => {
2929
return resolvePost({ postMeta: post, event });
3030
})
3131
);
3232

33-
return json(transformedPosts);
33+
return json(resolvedPosts);
3434
};

src/routes/api/posts/2024/+server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ export const GET: RequestHandler = async (event) => {
1010
// Sort order: latest first.
1111
const posts = [postCopilotContext];
1212

13-
const transformedPosts = await Promise.all(
13+
const resolvedPosts = await Promise.all(
1414
posts.map((post) => {
1515
return resolvePost({ postMeta: post, event });
1616
})
1717
);
1818

19-
return json(transformedPosts);
19+
return json(resolvedPosts);
2020
};

0 commit comments

Comments
 (0)