diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts new file mode 100644 index 0000000..cec774b --- /dev/null +++ b/src/pages/rss.xml.ts @@ -0,0 +1,24 @@ +import rss from '@astrojs/rss'; +import { getCollection } from 'astro:content'; +import type { APIContext } from 'astro'; + +export async function GET(context: APIContext) { + const posts = await getCollection('posts'); + const base = import.meta.env.BASE_URL; + + const sortedPosts = posts.sort( + (a, b) => b.data.date.getTime() - a.data.date.getTime() + ); + + return rss({ + title: 'Crash Test Dev', + description: 'Technical publishing by Ken Chau', + site: context.site!, + items: sortedPosts.map((post) => ({ + title: post.data.title, + pubDate: post.data.date, + description: post.data.summary ?? '', + link: `${base}posts/${post.id}/`, + })), + }); +} \ No newline at end of file diff --git a/tests/rss-feed.test.mjs b/tests/rss-feed.test.mjs new file mode 100644 index 0000000..d39ef65 --- /dev/null +++ b/tests/rss-feed.test.mjs @@ -0,0 +1,94 @@ +import { describe, it } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { readFileSync, existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const filePath = resolve('src/pages/rss.xml.ts'); + +function readSource() { + return readFileSync(filePath, 'utf-8'); +} + +describe('RSS feed endpoint', () => { + it('should exist at src/pages/rss.xml.ts', () => { + assert.ok(existsSync(filePath), 'RSS feed file should exist'); + }); + + describe('Imports', () => { + it('should import rss from @astrojs/rss', () => { + const content = readSource(); + assert.match(content, /import\s+rss\s+from\s+['"]@astrojs\/rss['"]/, 'should import rss'); + }); + + it('should import getCollection from astro:content', () => { + const content = readSource(); + assert.match(content, /import\s+.*getCollection.*from\s+['"]astro:content['"]/, 'should import getCollection'); + }); + + it('should import APIContext type', () => { + const content = readSource(); + assert.match(content, /import\s+.*APIContext.*from\s+['"]astro['"]/, 'should import APIContext from astro'); + }); + }); + + describe('GET function', () => { + it('should export an async GET function with APIContext parameter', () => { + const content = readSource(); + assert.match(content, /export\s+async\s+function\s+GET\s*\(\s*context\s*:\s*APIContext\s*\)/, 'should export async GET(context: APIContext)'); + }); + }); + + describe('RSS configuration', () => { + it('should set title to Crash Test Dev', () => { + const content = readSource(); + assert.match(content, /title\s*:\s*['"]Crash Test Dev['"]/, 'should have correct title'); + }); + + it('should set description to Technical publishing by Ken Chau', () => { + const content = readSource(); + assert.match(content, /description\s*:\s*['"]Technical publishing by Ken Chau['"]/, 'should have correct description'); + }); + + it('should set site to context.site!', () => { + const content = readSource(); + assert.match(content, /site\s*:\s*context\.site!/, 'should use context.site!'); + }); + }); + + describe('Items mapping', () => { + it('should fetch posts via getCollection', () => { + const content = readSource(); + assert.match(content, /getCollection\s*\(\s*['"]posts['"]/, 'should call getCollection("posts")'); + }); + + it('should sort posts descending by date', () => { + const content = readSource(); + assert.match(content, /sort\s*\(/, 'should call sort'); + }); + + it('should read BASE_URL', () => { + const content = readSource(); + assert.match(content, /import\.meta\.env\.BASE_URL/, 'should read BASE_URL from import.meta.env'); + }); + + it('should map items with title', () => { + const content = readSource(); + assert.match(content, /title\s*:\s*.*\.data\.title/, 'should map title from post data'); + }); + + it('should map items with pubDate from date', () => { + const content = readSource(); + assert.match(content, /pubDate\s*:\s*.*\.data\.date/, 'should map pubDate from post date'); + }); + + it('should map items with description from summary with fallback', () => { + const content = readSource(); + assert.match(content, /description\s*:\s*.*\.data\.summary\s*\?\?\s*['"]['"]/, 'should map description with summary ?? empty string fallback'); + }); + + it('should map items with link using base and post id', () => { + const content = readSource(); + assert.match(content, /link\s*:.*posts\/.*\.id/, 'should map link with base + posts/ + post.id'); + }); + }); +}); \ No newline at end of file