From 24663c9695385fed9ece57bf4aecdca3a8581e70 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Sat, 6 Jan 2024 08:47:29 +0100 Subject: [PATCH] fix(rss): make title optional if description is provided (#9610) * fix(rss): make title optional if description is provided * feat(rss): simplify schema * fix(rss): update tests to match new behavior * Update packages/astro-rss/test/pagesGlobToRssItems.test.js Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com> * Update packages/astro-rss/test/pagesGlobToRssItems.test.js Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com> * feat: make link and pubDate optional * feat: improve item normalization * Update shy-spoons-sort.md * Fix test fail * Update .changeset/shy-spoons-sort.md Co-authored-by: Emanuele Stoppa --------- Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com> Co-authored-by: bluwy Co-authored-by: Emanuele Stoppa --- .changeset/shy-spoons-sort.md | 5 +++ packages/astro-rss/src/index.ts | 38 +++++++++---------- packages/astro-rss/src/schema.ts | 26 ++++++++++--- .../test/pagesGlobToRssItems.test.js | 38 ++++++++++++++++++- 4 files changed, 81 insertions(+), 26 deletions(-) create mode 100644 .changeset/shy-spoons-sort.md diff --git a/.changeset/shy-spoons-sort.md b/.changeset/shy-spoons-sort.md new file mode 100644 index 0000000000..bbeb4aca06 --- /dev/null +++ b/.changeset/shy-spoons-sort.md @@ -0,0 +1,5 @@ +--- +"@astrojs/rss": patch +--- + +Fixes the RSS schema to make the `title` optional if the description is already provided. It also makes `pubDate` and `link` optional, as specified in the RSS specification. diff --git a/packages/astro-rss/src/index.ts b/packages/astro-rss/src/index.ts index c8cf19d602..b866365ab3 100644 --- a/packages/astro-rss/src/index.ts +++ b/packages/astro-rss/src/index.ts @@ -32,9 +32,9 @@ export type RSSOptions = { export type RSSFeedItem = { /** Link to item */ - link: string; + link: z.infer['link']; /** Full content of the item. Should be valid HTML */ - content?: string | undefined; + content?: z.infer['content']; /** Title of item */ title: z.infer['title']; /** Publication date of item */ @@ -55,11 +55,10 @@ export type RSSFeedItem = { enclosure?: z.infer['enclosure']; }; -type ValidatedRSSFeedItem = z.infer; +type ValidatedRSSFeedItem = z.infer; type ValidatedRSSOptions = z.infer; type GlobResult = z.infer; -const rssFeedItemValidator = rssSchema.extend({ link: z.string(), content: z.string().optional() }); const globResultValidator = z.record(z.function().returns(z.promise(z.any()))); const rssOptionsValidator = z.object({ @@ -67,7 +66,7 @@ const rssOptionsValidator = z.object({ description: z.string(), site: z.preprocess((url) => (url instanceof URL ? url.href : url), z.string().url()), items: z - .array(rssFeedItemValidator) + .array(rssSchema) .or(globResultValidator) .transform((items) => { if (!Array.isArray(items)) { @@ -117,7 +116,7 @@ async function validateRssOptions(rssOptions: RSSOptions) { if (path === 'items' && code === 'invalid_union') { return [ message, - `The \`items\` property requires properly typed \`title\`, \`pubDate\`, and \`link\` keys.`, + `The \`items\` property requires at least the \`title\` or \`description\` key. They must be properly typed, as well as \`pubDate\` and \`link\` keys if provided.`, `Check your collection's schema, and visit https://docs.astro.build/en/guides/rss/#generating-items for more info.`, ].join('\n'); } @@ -138,10 +137,7 @@ export function pagesGlobToRssItems(items: GlobResult): Promise { ); // items root.rss.channel.item = items.map((result) => { - // If the item's link is already a valid URL, don't mess with it. - const itemLink = isValidURL(result.link) - ? result.link - : createCanonicalURL(result.link, rssOptions.trailingSlash, site).href; - const item: any = { - title: result.title, - link: itemLink, - guid: { '#text': itemLink, '@_isPermaLink': 'true' }, - }; + const item: Record = {}; + + if (result.title) { + item.title = result.title; + } + if (typeof result.link === 'string') { + // If the item's link is already a valid URL, don't mess with it. + const itemLink = isValidURL(result.link) + ? result.link + : createCanonicalURL(result.link, rssOptions.trailingSlash, site).href; + item.link = itemLink; + item.guid = { '#text': itemLink, '@_isPermaLink': 'true' }; + } if (result.description) { item.description = result.description; } diff --git a/packages/astro-rss/src/schema.ts b/packages/astro-rss/src/schema.ts index 98aa35f812..788fe86fb3 100644 --- a/packages/astro-rss/src/schema.ts +++ b/packages/astro-rss/src/schema.ts @@ -1,12 +1,11 @@ import { z } from 'astro/zod'; -export const rssSchema = z.object({ - title: z.string(), +const sharedSchema = z.object({ pubDate: z .union([z.string(), z.number(), z.date()]) - .transform((value) => new Date(value)) - .refine((value) => !isNaN(value.getTime())), - description: z.string().optional(), + .optional() + .transform((value) => (value === undefined ? value : new Date(value))) + .refine((value) => (value === undefined ? value : !isNaN(value.getTime()))), customData: z.string().optional(), categories: z.array(z.string()).optional(), author: z.string().optional(), @@ -19,4 +18,21 @@ export const rssSchema = z.object({ type: z.string(), }) .optional(), + link: z.string().optional(), + content: z.string().optional(), }); + +export const rssSchema = z.union([ + z + .object({ + title: z.string(), + description: z.string().optional(), + }) + .merge(sharedSchema), + z + .object({ + title: z.string().optional(), + description: z.string(), + }) + .merge(sharedSchema), +]); diff --git a/packages/astro-rss/test/pagesGlobToRssItems.test.js b/packages/astro-rss/test/pagesGlobToRssItems.test.js index 82af5ba125..e72f6d3b3c 100644 --- a/packages/astro-rss/test/pagesGlobToRssItems.test.js +++ b/packages/astro-rss/test/pagesGlobToRssItems.test.js @@ -66,7 +66,24 @@ describe('pagesGlobToRssItems', () => { return chai.expect(pagesGlobToRssItems(globResult)).to.be.rejected; }); - it('should fail on missing "title" key', () => { + it('should fail on missing "title" key and "description"', () => { + const globResult = { + './posts/php.md': () => + new Promise((resolve) => + resolve({ + url: phpFeedItem.link, + frontmatter: { + title: undefined, + pubDate: phpFeedItem.pubDate, + description: undefined, + }, + }) + ), + }; + return chai.expect(pagesGlobToRssItems(globResult)).to.be.rejected; + }); + + it('should not fail on missing "title" key if "description" is present', () => { const globResult = { './posts/php.md': () => new Promise((resolve) => @@ -80,6 +97,23 @@ describe('pagesGlobToRssItems', () => { }) ), }; - return chai.expect(pagesGlobToRssItems(globResult)).to.be.rejected; + return chai.expect(pagesGlobToRssItems(globResult)).to.not.be.rejected; + }); + + it('should fail on missing "description" key if "title" is present', () => { + const globResult = { + './posts/php.md': () => + new Promise((resolve) => + resolve({ + url: phpFeedItem.link, + frontmatter: { + title: phpFeedItem.title, + pubDate: phpFeedItem.pubDate, + description: undefined, + }, + }) + ), + }; + return chai.expect(pagesGlobToRssItems(globResult)).to.not.be.rejected; }); });