From 94347138ef335f375cbae212fbf2dd7428113de2 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 8 Mar 2023 09:53:12 +0000 Subject: [PATCH] feat(rss): add option to remove the trailing slash --- .changeset/popular-rules-divide.md | 17 +++++++++++++++++ packages/astro-rss/README.md | 18 ++++++++++++++++++ packages/astro-rss/src/index.ts | 7 +++++-- packages/astro-rss/src/util.ts | 17 +++++++++++++++-- packages/astro-rss/test/rss.test.js | 15 ++++++++++++++- 5 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 .changeset/popular-rules-divide.md diff --git a/.changeset/popular-rules-divide.md b/.changeset/popular-rules-divide.md new file mode 100644 index 0000000000000..a7e5dc0edde65 --- /dev/null +++ b/.changeset/popular-rules-divide.md @@ -0,0 +1,17 @@ +--- +'@astrojs/rss': minor +--- + +Added `trailingSlash` option, to control whether the emitted URLs should have the trailing slash. + +The new option is optional. Values accepted are: `"always"` and `"never"`. + +```js +import rss from '@astrojs/rss'; + +export const get = () => rss({ + trailingSlash: "never" +}); +``` + +By passing `"never"`, the emitted links won't have the trailing slash. diff --git a/packages/astro-rss/README.md b/packages/astro-rss/README.md index 7036f5f77c201..177d7f7fc8d54 100644 --- a/packages/astro-rss/README.md +++ b/packages/astro-rss/README.md @@ -73,6 +73,8 @@ export function get(context) { customData: 'en-us', // (optional) add arbitrary metadata to opening tag xmlns: { h: 'http://www.w3.org/TR/html4/' }, + // (optional) add or not the trailing slash + trailingSlash: "never" }); } ``` @@ -233,6 +235,22 @@ export async function get(context) { } ``` +### `trailingSlash` + +Type: `"nevery" | "always" (optional)` +Default: "always" + +By default, the library will add trailing slashes to the emitted URLs. To change this behaviour, +you use this option, by passing the value `"never"`. + +```js +import rss from '@astrojs/rss'; + +export const get = () => rss({ + trailingSlash: "never" +}); +``` + --- For more on building with Astro, [visit the Astro docs][astro-rss]. diff --git a/packages/astro-rss/src/index.ts b/packages/astro-rss/src/index.ts index 90c30eb0f735d..95e3e534c3551 100644 --- a/packages/astro-rss/src/index.ts +++ b/packages/astro-rss/src/index.ts @@ -29,6 +29,7 @@ export type RSSOptions = { customData?: z.infer['customData']; /** Whether to include drafts or not */ drafts?: z.infer['drafts']; + trailingSlash?: z.infer['trailingSlash']; }; type RSSFeedItem = { @@ -54,6 +55,7 @@ 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 TRAILING_SLASH = ['always', 'never'] as const; const rssOptionsValidator = z.object({ title: z.string(), description: z.string(), @@ -77,6 +79,7 @@ const rssOptionsValidator = z.object({ drafts: z.boolean().default(false), stylesheet: z.union([z.string(), z.boolean()]).optional(), customData: z.string().optional(), + trailingSlash: z.enum(TRAILING_SLASH).optional(), }); export default async function getRSS(rssOptions: RSSOptions) { @@ -171,7 +174,7 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise { root.rss.channel = { title: rssOptions.title, description: rssOptions.description, - link: createCanonicalURL(site).href, + link: createCanonicalURL(site, rssOptions.trailingSlash, undefined).href, }; if (typeof rssOptions.customData === 'string') Object.assign( @@ -183,7 +186,7 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise { // 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, site).href; + : createCanonicalURL(result.link, rssOptions.trailingSlash, site).href; const item: any = { title: result.title, link: itemLink, diff --git a/packages/astro-rss/src/util.ts b/packages/astro-rss/src/util.ts index ad0e40a68ccf7..0d8ae0549cb4d 100644 --- a/packages/astro-rss/src/util.ts +++ b/packages/astro-rss/src/util.ts @@ -1,10 +1,23 @@ import { z } from 'astro/zod'; +import { RSSOptions } from './index'; /** Normalize URL to its canonical form */ -export function createCanonicalURL(url: string, base?: string): URL { +export function createCanonicalURL( + url: string, + trailingSlash?: RSSOptions['trailingSlash'], + base?: string +): URL { let pathname = url.replace(/\/index.html$/, ''); // index.html is not canonical pathname = pathname.replace(/\/1\/?$/, ''); // neither is a trailing /1/ (impl. detail of collections) - if (!getUrlExtension(url)) pathname = pathname.replace(/(\/+)?$/, '/'); // add trailing slash if there’s no extension + if (trailingSlash !== 'never') { + // add trailing slash if there’s no extension + if (!getUrlExtension(url) || trailingSlash === 'always') + pathname = pathname.replace(/(\/+)?$/, '/'); + } else { + // remove the trailing slash + pathname = pathname.replace(/(\/+)?$/, ''); + } + pathname = pathname.replace(/\/+/g, '/'); // remove duplicate slashes (URL() won’t) return new URL(pathname, base); } diff --git a/packages/astro-rss/test/rss.test.js b/packages/astro-rss/test/rss.test.js index 744471f8cd9ad..54015327daf7d 100644 --- a/packages/astro-rss/test/rss.test.js +++ b/packages/astro-rss/test/rss.test.js @@ -107,7 +107,6 @@ describe('rss', () => { const { body } = await rss({ title, description, - drafts: true, items: [phpFeedItem, { ...web1FeedItem, draft: true }], site, drafts: true, @@ -116,6 +115,20 @@ describe('rss', () => { chai.expect(body).xml.to.equal(validXmlResult); }); + it('should not append trailing slash to URLs with the given option', async () => { + const { body } = await rss({ + title, + description, + items: [phpFeedItem, { ...web1FeedItem, draft: true }], + site, + drafts: true, + trailingSlash: 'never', + }); + + chai.expect(body).xml.to.contain('https://example.com/<'); + chai.expect(body).xml.to.contain('https://example.com/php<'); + }); + it('Deprecated import.meta.glob mapping still works', async () => { const globResult = { './posts/php.md': () =>