mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
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 <sarah@rainsberger.ca> * suggestions --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
77a046e886
commit
2e362042c2
5 changed files with 65 additions and 5 deletions
15
.changeset/popular-rules-divide.md
Normal file
15
.changeset/popular-rules-divide.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
'@astrojs/rss': minor
|
||||
---
|
||||
|
||||
Added `trailingSlash` option to control whether or not the emitted URLs should have trailing slashes.
|
||||
|
||||
```js
|
||||
import rss from '@astrojs/rss';
|
||||
|
||||
export const get = () => rss({
|
||||
trailingSlash: false
|
||||
});
|
||||
```
|
||||
|
||||
By passing `false`, the emitted links won't have trailing slashes.
|
|
@ -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 trailing slashes to URLs (default: true)
|
||||
trailingSlash: false
|
||||
});
|
||||
}
|
||||
```
|
||||
|
@ -185,6 +187,21 @@ The `content` key contains the full content of the post as HTML. This allows you
|
|||
|
||||
[See our RSS documentation](https://docs.astro.build/en/guides/rss/#including-full-post-content) for examples using content collections and glob imports.
|
||||
|
||||
### `trailingSlash`
|
||||
|
||||
Type: `boolean (optional)`
|
||||
Default: `true`
|
||||
|
||||
By default, the library will add trailing slashes to the emitted URLs. To prevent this behavior, add `trailingSlash: false` to the `rss` function.
|
||||
|
||||
```js
|
||||
import rss from '@astrojs/rss';
|
||||
|
||||
export const get = () => rss({
|
||||
trailingSlash: false
|
||||
});
|
||||
```
|
||||
|
||||
## `rssSchema`
|
||||
|
||||
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:
|
||||
|
|
|
@ -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 = {
|
||||
|
@ -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 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.boolean().default(true),
|
||||
});
|
||||
|
||||
export default async function getRSS(rssOptions: RSSOptions) {
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
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 === false) {
|
||||
// remove the trailing slash
|
||||
pathname = pathname.replace(/(\/+)?$/, '');
|
||||
} else if (!getUrlExtension(url)) {
|
||||
// add trailing slash if there’s no extension or `trailingSlash` is true
|
||||
pathname = pathname.replace(/(\/+)?$/, '/');
|
||||
}
|
||||
|
||||
pathname = pathname.replace(/\/+/g, '/'); // remove duplicate slashes (URL() won’t)
|
||||
return new URL(pathname, base);
|
||||
}
|
||||
|
|
|
@ -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: false,
|
||||
});
|
||||
|
||||
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': () =>
|
||||
|
|
Loading…
Reference in a new issue