Skip to content

Commit

Permalink
feat(rss): add option to remove the trailing slash
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico committed Mar 8, 2023
1 parent fbab73c commit 9434713
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 5 deletions.
17 changes: 17 additions & 0 deletions .changeset/popular-rules-divide.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions packages/astro-rss/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export function get(context) {
customData: '<language>en-us</language>',
// (optional) add arbitrary metadata to opening <rss> tag
xmlns: { h: 'http://www.w3.org/TR/html4/' },
// (optional) add or not the trailing slash
trailingSlash: "never"
});
}
```
Expand Down Expand Up @@ -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].
Expand Down
7 changes: 5 additions & 2 deletions packages/astro-rss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type RSSOptions = {
customData?: z.infer<typeof rssOptionsValidator>['customData'];
/** Whether to include drafts or not */
drafts?: z.infer<typeof rssOptionsValidator>['drafts'];
trailingSlash?: z.infer<typeof rssOptionsValidator>['trailingSlash'];
};

type RSSFeedItem = {
Expand All @@ -54,6 +55,7 @@ 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 TRAILING_SLASH = ['always', 'never'] as const;
const rssOptionsValidator = z.object({
title: z.string(),
description: z.string(),
Expand All @@ -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) {
Expand Down Expand Up @@ -171,7 +174,7 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<string> {
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(
Expand All @@ -183,7 +186,7 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<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, site).href;
: createCanonicalURL(result.link, rssOptions.trailingSlash, site).href;
const item: any = {
title: result.title,
link: itemLink,
Expand Down
17 changes: 15 additions & 2 deletions packages/astro-rss/src/util.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Expand Down
15 changes: 14 additions & 1 deletion packages/astro-rss/test/rss.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ describe('rss', () => {
const { body } = await rss({
title,
description,
drafts: true,
items: [phpFeedItem, { ...web1FeedItem, draft: true }],
site,
drafts: true,
Expand All @@ -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': () =>
Expand Down

0 comments on commit 9434713

Please sign in to comment.