Skip to content

Commit 2e36204

Browse files
feat(rss): add option to remove the trailing slash (#6453)
* feat(rss): add option to remove the trailing slash * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <[email protected]> * suggestions --------- Co-authored-by: Sarah Rainsberger <[email protected]>
1 parent 77a046e commit 2e36204

5 files changed

Lines changed: 65 additions & 5 deletions

File tree

.changeset/popular-rules-divide.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@astrojs/rss': minor
3+
---
4+
5+
Added `trailingSlash` option to control whether or not the emitted URLs should have trailing slashes.
6+
7+
```js
8+
import rss from '@astrojs/rss';
9+
10+
export const get = () => rss({
11+
trailingSlash: false
12+
});
13+
```
14+
15+
By passing `false`, the emitted links won't have trailing slashes.

packages/astro-rss/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export function get(context) {
7373
customData: '<language>en-us</language>',
7474
// (optional) add arbitrary metadata to opening <rss> tag
7575
xmlns: { h: 'http://www.w3.org/TR/html4/' },
76+
// (optional) add trailing slashes to URLs (default: true)
77+
trailingSlash: false
7678
});
7779
}
7880
```
@@ -185,6 +187,21 @@ The `content` key contains the full content of the post as HTML. This allows you
185187
186188
[See our RSS documentation](https://docs.astro.build/en/guides/rss/#including-full-post-content) for examples using content collections and glob imports.
187189
190+
### `trailingSlash`
191+
192+
Type: `boolean (optional)`
193+
Default: `true`
194+
195+
By default, the library will add trailing slashes to the emitted URLs. To prevent this behavior, add `trailingSlash: false` to the `rss` function.
196+
197+
```js
198+
import rss from '@astrojs/rss';
199+
200+
export const get = () => rss({
201+
trailingSlash: false
202+
});
203+
```
204+
188205
## `rssSchema`
189206
190207
When using content collections, you can configure your collection schema to enforce expected [`RSSFeedItem`](#items) properties. Import and apply `rssSchema` to ensure that each collection entry produces a valid RSS feed item:

packages/astro-rss/src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type RSSOptions = {
2929
customData?: z.infer<typeof rssOptionsValidator>['customData'];
3030
/** Whether to include drafts or not */
3131
drafts?: z.infer<typeof rssOptionsValidator>['drafts'];
32+
trailingSlash?: z.infer<typeof rssOptionsValidator>['trailingSlash'];
3233
};
3334

3435
type RSSFeedItem = {
@@ -54,6 +55,7 @@ type GlobResult = z.infer<typeof globResultValidator>;
5455

5556
const rssFeedItemValidator = rssSchema.extend({ link: z.string(), content: z.string().optional() });
5657
const globResultValidator = z.record(z.function().returns(z.promise(z.any())));
58+
5759
const rssOptionsValidator = z.object({
5860
title: z.string(),
5961
description: z.string(),
@@ -77,6 +79,7 @@ const rssOptionsValidator = z.object({
7779
drafts: z.boolean().default(false),
7880
stylesheet: z.union([z.string(), z.boolean()]).optional(),
7981
customData: z.string().optional(),
82+
trailingSlash: z.boolean().default(true),
8083
});
8184

8285
export default async function getRSS(rssOptions: RSSOptions) {
@@ -171,7 +174,7 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<string> {
171174
root.rss.channel = {
172175
title: rssOptions.title,
173176
description: rssOptions.description,
174-
link: createCanonicalURL(site).href,
177+
link: createCanonicalURL(site, rssOptions.trailingSlash, undefined).href,
175178
};
176179
if (typeof rssOptions.customData === 'string')
177180
Object.assign(
@@ -183,7 +186,7 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<string> {
183186
// If the item's link is already a valid URL, don't mess with it.
184187
const itemLink = isValidURL(result.link)
185188
? result.link
186-
: createCanonicalURL(result.link, site).href;
189+
: createCanonicalURL(result.link, rssOptions.trailingSlash, site).href;
187190
const item: any = {
188191
title: result.title,
189192
link: itemLink,

packages/astro-rss/src/util.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import { z } from 'astro/zod';
2+
import { RSSOptions } from './index';
23

34
/** Normalize URL to its canonical form */
4-
export function createCanonicalURL(url: string, base?: string): URL {
5+
export function createCanonicalURL(
6+
url: string,
7+
trailingSlash?: RSSOptions['trailingSlash'],
8+
base?: string
9+
): URL {
510
let pathname = url.replace(/\/index.html$/, ''); // index.html is not canonical
611
pathname = pathname.replace(/\/1\/?$/, ''); // neither is a trailing /1/ (impl. detail of collections)
7-
if (!getUrlExtension(url)) pathname = pathname.replace(/(\/+)?$/, '/'); // add trailing slash if there’s no extension
12+
if (trailingSlash === false) {
13+
// remove the trailing slash
14+
pathname = pathname.replace(/(\/+)?$/, '');
15+
} else if (!getUrlExtension(url)) {
16+
// add trailing slash if there’s no extension or `trailingSlash` is true
17+
pathname = pathname.replace(/(\/+)?$/, '/');
18+
}
19+
820
pathname = pathname.replace(/\/+/g, '/'); // remove duplicate slashes (URL() won’t)
921
return new URL(pathname, base);
1022
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ describe('rss', () => {
107107
const { body } = await rss({
108108
title,
109109
description,
110-
drafts: true,
111110
items: [phpFeedItem, { ...web1FeedItem, draft: true }],
112111
site,
113112
drafts: true,
@@ -116,6 +115,20 @@ describe('rss', () => {
116115
chai.expect(body).xml.to.equal(validXmlResult);
117116
});
118117

118+
it('should not append trailing slash to URLs with the given option', async () => {
119+
const { body } = await rss({
120+
title,
121+
description,
122+
items: [phpFeedItem, { ...web1FeedItem, draft: true }],
123+
site,
124+
drafts: true,
125+
trailingSlash: false,
126+
});
127+
128+
chai.expect(body).xml.to.contain('https://example.com/<');
129+
chai.expect(body).xml.to.contain('https://example.com/php<');
130+
});
131+
119132
it('Deprecated import.meta.glob mapping still works', async () => {
120133
const globResult = {
121134
'./posts/php.md': () =>

0 commit comments

Comments
 (0)