mirror of
https://github.com/withastro/astro.git
synced 2024-12-30 22:03:56 -05:00
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 <my.burning@gmail.com> --------- Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com> Co-authored-by: bluwy <bjornlu.dev@gmail.com> Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
This commit is contained in:
parent
edc87abd47
commit
24663c9695
4 changed files with 81 additions and 26 deletions
5
.changeset/shy-spoons-sort.md
Normal file
5
.changeset/shy-spoons-sort.md
Normal file
|
@ -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.
|
|
@ -32,9 +32,9 @@ export type RSSOptions = {
|
|||
|
||||
export type RSSFeedItem = {
|
||||
/** Link to item */
|
||||
link: string;
|
||||
link: z.infer<typeof rssSchema>['link'];
|
||||
/** Full content of the item. Should be valid HTML */
|
||||
content?: string | undefined;
|
||||
content?: z.infer<typeof rssSchema>['content'];
|
||||
/** Title of item */
|
||||
title: z.infer<typeof rssSchema>['title'];
|
||||
/** Publication date of item */
|
||||
|
@ -55,11 +55,10 @@ export type RSSFeedItem = {
|
|||
enclosure?: z.infer<typeof rssSchema>['enclosure'];
|
||||
};
|
||||
|
||||
type ValidatedRSSFeedItem = z.infer<typeof rssFeedItemValidator>;
|
||||
type ValidatedRSSFeedItem = z.infer<typeof rssSchema>;
|
||||
type ValidatedRSSOptions = z.infer<typeof rssOptionsValidator>;
|
||||
type GlobResult = z.infer<typeof globResultValidator>;
|
||||
|
||||
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<ValidatedRSSFeed
|
|||
`[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`
|
||||
);
|
||||
}
|
||||
const parsedResult = rssFeedItemValidator.safeParse(
|
||||
{ ...frontmatter, link: url },
|
||||
{ errorMap }
|
||||
);
|
||||
const parsedResult = rssSchema.safeParse({ ...frontmatter, link: url }, { errorMap });
|
||||
|
||||
if (parsedResult.success) {
|
||||
return parsedResult.data;
|
||||
|
@ -210,15 +206,19 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<string> {
|
|||
);
|
||||
// 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<string, unknown> = {};
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue