Skip to content

Commit 24663c9

Browse files
florian-lefebvrePrincesseuhbluwyematipico
authored
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 <[email protected]> * Update packages/astro-rss/test/pagesGlobToRssItems.test.js Co-authored-by: Erika <[email protected]> * 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 <[email protected]> --------- Co-authored-by: Erika <[email protected]> Co-authored-by: bluwy <[email protected]> Co-authored-by: Emanuele Stoppa <[email protected]>
1 parent edc87ab commit 24663c9

4 files changed

Lines changed: 81 additions & 26 deletions

File tree

.changeset/shy-spoons-sort.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@astrojs/rss": patch
3+
---
4+
5+
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.

packages/astro-rss/src/index.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ export type RSSOptions = {
3232

3333
export type RSSFeedItem = {
3434
/** Link to item */
35-
link: string;
35+
link: z.infer<typeof rssSchema>['link'];
3636
/** Full content of the item. Should be valid HTML */
37-
content?: string | undefined;
37+
content?: z.infer<typeof rssSchema>['content'];
3838
/** Title of item */
3939
title: z.infer<typeof rssSchema>['title'];
4040
/** Publication date of item */
@@ -55,19 +55,18 @@ export type RSSFeedItem = {
5555
enclosure?: z.infer<typeof rssSchema>['enclosure'];
5656
};
5757

58-
type ValidatedRSSFeedItem = z.infer<typeof rssFeedItemValidator>;
58+
type ValidatedRSSFeedItem = z.infer<typeof rssSchema>;
5959
type ValidatedRSSOptions = z.infer<typeof rssOptionsValidator>;
6060
type GlobResult = z.infer<typeof globResultValidator>;
6161

62-
const rssFeedItemValidator = rssSchema.extend({ link: z.string(), content: z.string().optional() });
6362
const globResultValidator = z.record(z.function().returns(z.promise(z.any())));
6463

6564
const rssOptionsValidator = z.object({
6665
title: z.string(),
6766
description: z.string(),
6867
site: z.preprocess((url) => (url instanceof URL ? url.href : url), z.string().url()),
6968
items: z
70-
.array(rssFeedItemValidator)
69+
.array(rssSchema)
7170
.or(globResultValidator)
7271
.transform((items) => {
7372
if (!Array.isArray(items)) {
@@ -117,7 +116,7 @@ async function validateRssOptions(rssOptions: RSSOptions) {
117116
if (path === 'items' && code === 'invalid_union') {
118117
return [
119118
message,
120-
`The \`items\` property requires properly typed \`title\`, \`pubDate\`, and \`link\` keys.`,
119+
`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.`,
121120
`Check your collection's schema, and visit https://docs.astro.build/en/guides/rss/#generating-items for more info.`,
122121
].join('\n');
123122
}
@@ -138,10 +137,7 @@ export function pagesGlobToRssItems(items: GlobResult): Promise<ValidatedRSSFeed
138137
`[RSS] You can only glob entries within 'src/pages/' when passing import.meta.glob() directly. Consider mapping the result to an array of RSSFeedItems. See the RSS docs for usage examples: https://docs.astro.build/en/guides/rss/#2-list-of-rss-feed-objects`
139138
);
140139
}
141-
const parsedResult = rssFeedItemValidator.safeParse(
142-
{ ...frontmatter, link: url },
143-
{ errorMap }
144-
);
140+
const parsedResult = rssSchema.safeParse({ ...frontmatter, link: url }, { errorMap });
145141

146142
if (parsedResult.success) {
147143
return parsedResult.data;
@@ -210,15 +206,19 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<string> {
210206
);
211207
// items
212208
root.rss.channel.item = items.map((result) => {
213-
// If the item's link is already a valid URL, don't mess with it.
214-
const itemLink = isValidURL(result.link)
215-
? result.link
216-
: createCanonicalURL(result.link, rssOptions.trailingSlash, site).href;
217-
const item: any = {
218-
title: result.title,
219-
link: itemLink,
220-
guid: { '#text': itemLink, '@_isPermaLink': 'true' },
221-
};
209+
const item: Record<string, unknown> = {};
210+
211+
if (result.title) {
212+
item.title = result.title;
213+
}
214+
if (typeof result.link === 'string') {
215+
// If the item's link is already a valid URL, don't mess with it.
216+
const itemLink = isValidURL(result.link)
217+
? result.link
218+
: createCanonicalURL(result.link, rssOptions.trailingSlash, site).href;
219+
item.link = itemLink;
220+
item.guid = { '#text': itemLink, '@_isPermaLink': 'true' };
221+
}
222222
if (result.description) {
223223
item.description = result.description;
224224
}

packages/astro-rss/src/schema.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { z } from 'astro/zod';
22

3-
export const rssSchema = z.object({
4-
title: z.string(),
3+
const sharedSchema = z.object({
54
pubDate: z
65
.union([z.string(), z.number(), z.date()])
7-
.transform((value) => new Date(value))
8-
.refine((value) => !isNaN(value.getTime())),
9-
description: z.string().optional(),
6+
.optional()
7+
.transform((value) => (value === undefined ? value : new Date(value)))
8+
.refine((value) => (value === undefined ? value : !isNaN(value.getTime()))),
109
customData: z.string().optional(),
1110
categories: z.array(z.string()).optional(),
1211
author: z.string().optional(),
@@ -19,4 +18,21 @@ export const rssSchema = z.object({
1918
type: z.string(),
2019
})
2120
.optional(),
21+
link: z.string().optional(),
22+
content: z.string().optional(),
2223
});
24+
25+
export const rssSchema = z.union([
26+
z
27+
.object({
28+
title: z.string(),
29+
description: z.string().optional(),
30+
})
31+
.merge(sharedSchema),
32+
z
33+
.object({
34+
title: z.string().optional(),
35+
description: z.string(),
36+
})
37+
.merge(sharedSchema),
38+
]);

packages/astro-rss/test/pagesGlobToRssItems.test.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('pagesGlobToRssItems', () => {
6666
return chai.expect(pagesGlobToRssItems(globResult)).to.be.rejected;
6767
});
6868

69-
it('should fail on missing "title" key', () => {
69+
it('should fail on missing "title" key and "description"', () => {
7070
const globResult = {
7171
'./posts/php.md': () =>
7272
new Promise((resolve) =>
@@ -75,11 +75,45 @@ describe('pagesGlobToRssItems', () => {
7575
frontmatter: {
7676
title: undefined,
7777
pubDate: phpFeedItem.pubDate,
78-
description: phpFeedItem.description,
78+
description: undefined,
7979
},
8080
})
8181
),
8282
};
8383
return chai.expect(pagesGlobToRssItems(globResult)).to.be.rejected;
8484
});
85+
86+
it('should not fail on missing "title" key if "description" is present', () => {
87+
const globResult = {
88+
'./posts/php.md': () =>
89+
new Promise((resolve) =>
90+
resolve({
91+
url: phpFeedItem.link,
92+
frontmatter: {
93+
title: undefined,
94+
pubDate: phpFeedItem.pubDate,
95+
description: phpFeedItem.description,
96+
},
97+
})
98+
),
99+
};
100+
return chai.expect(pagesGlobToRssItems(globResult)).to.not.be.rejected;
101+
});
102+
103+
it('should fail on missing "description" key if "title" is present', () => {
104+
const globResult = {
105+
'./posts/php.md': () =>
106+
new Promise((resolve) =>
107+
resolve({
108+
url: phpFeedItem.link,
109+
frontmatter: {
110+
title: phpFeedItem.title,
111+
pubDate: phpFeedItem.pubDate,
112+
description: undefined,
113+
},
114+
})
115+
),
116+
};
117+
return chai.expect(pagesGlobToRssItems(globResult)).to.not.be.rejected;
118+
});
85119
});

0 commit comments

Comments
 (0)