Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thin-memes-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/underscore-redirects': patch
---

Fixes generated redirect files to respect Astro’s `trailingSlash` configuration, so redirect routes work with the expected URL format in built output instead of returning a 404 when accessed with a trailing slash.
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ describe(
it('Creates a redirects file', async () => {
const redirects = await fixture.readFile('./_redirects');
const parts = redirects.split(/\s+/);
assert.deepEqual(parts, ['', '/other', '/', '301', '']);
// Snapshots are not supported in Node.js test yet (https://github.com/nodejs/node/issues/48260)
assert.equal(redirects, '\n/other / 301\n');
// based on https://github.com/withastro/astro/issues/16030 for the default option `trailingSlash: 'ignore'` both variants should be generated
assert.deepEqual(parts, ['', '/other/', '/', '301', '/other', '/', '301', '']);
});

it('Does not create .html files', async () => {
Expand Down
9 changes: 9 additions & 0 deletions packages/integrations/netlify/test/static/redirects.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,22 @@ describe('SSG - Redirects', () => {
it('Creates a redirects file', async () => {
const redirects = await fixture.readFile('./_redirects');
const parts = redirects.split(/\s+/);
// based on https://github.com/withastro/astro/issues/16030 for the default option `trailingSlash: 'ignore'` both variants should be generated
assert.deepEqual(parts, [
'',

'/two/',
'/',
'302',

'/two',
'/',
'302',

'/other/',
'/',
'301',

'/other',
'/',
'301',
Expand Down
52 changes: 44 additions & 8 deletions packages/underscore-redirects/src/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function getRedirectStatus(route: IntegrationResolvedRoute): ValidRedirectStatus
}

interface CreateRedirectsFromAstroRoutesParams {
config: Pick<AstroConfig, 'build' | 'output' | 'base'>;
config: Pick<AstroConfig, 'build' | 'output' | 'base' | 'trailingSlash'>;
/**
* Maps a `RouteData` to a dynamic target
*/
Expand All @@ -27,6 +27,35 @@ interface CreateRedirectsFromAstroRoutesParams {
assets: HookParameters<'astro:build:done'>['assets'];
}

/**
* Returns the path(s) to use for a redirect entry based on the trailingSlash config.
* - 'always': ensures the path ends with '/'
* - 'never': ensures the path does not end with '/'
* - 'ignore'(default): returns both with and without trailing slash variants
*/
export function getTrailingSlashPaths(
inputPath: string,
trailingSlash: 'always' | 'never' | 'ignore',
): string[] {
if (inputPath === '/') {
return ['/'];
}

const hasTrailingSlash = inputPath.endsWith('/');
const withoutSlash = hasTrailingSlash ? inputPath.slice(0, -1) : inputPath;
const withSlash = hasTrailingSlash ? inputPath : inputPath + '/';

switch (trailingSlash) {
case 'always':
return [withSlash];
case 'never':
return [withoutSlash];
case 'ignore':
default:
return [withoutSlash, withSlash];
}
}

/**
* Takes a set of routes and creates a Redirects object from them.
*/
Expand Down Expand Up @@ -57,13 +86,20 @@ export function createRedirectsFromAstroRoutes({
// Use `entrypoint` when available to keep trailing slashes in _redirects.
const inputPath =
route.type === 'redirect' && route.entrypoint ? route.entrypoint : route.pathname;
redirects.add({
dynamic: false,
input: `${base}${inputPath}`,
target: typeof route.redirect === 'object' ? route.redirect.destination : route.redirect,
status: getRedirectStatus(route),
weight: 2,
});

// Generate redirect entries based on trailingSlash config.
const trailingSlash = config.trailingSlash ?? 'ignore';
const paths = getTrailingSlashPaths(inputPath, trailingSlash);
for (const path of paths) {
redirects.add({
dynamic: false,
input: `${base}${path}`,
target:
typeof route.redirect === 'object' ? route.redirect.destination : route.redirect,
status: getRedirectStatus(route),
weight: 2,
});
}
continue;
}

Expand Down
1 change: 1 addition & 0 deletions packages/underscore-redirects/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {
createHostedRouteDefinition,
createRedirectsFromAstroRoutes,
getTrailingSlashPaths
} from './astro.js';
export { HostRoutes } from './host-route.js';
export { printAsRedirects } from './print.js';
23 changes: 22 additions & 1 deletion packages/underscore-redirects/test/astro.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { createRedirectsFromAstroRoutes } from '../dist/index.js';
import { createRedirectsFromAstroRoutes, getTrailingSlashPaths } from '../dist/index.js';

describe('Astro', () => {
it('Creates a Redirects object from routes', () => {
Expand All @@ -25,4 +25,25 @@ describe('Astro', () => {

assert.equal(_redirects.definitions.length, 2);
});

it('Generates correct paths for root', () => {
assert.deepEqual(getTrailingSlashPaths('/', 'ignore'), ['/']);
assert.deepEqual(getTrailingSlashPaths('/', 'always'), ['/']);
assert.deepEqual(getTrailingSlashPaths('/', 'never'), ['/']);
});

it('Generates correct paths for trailingslash ignore', () => {
assert.deepEqual(getTrailingSlashPaths('/path', 'ignore'), ['/path', '/path/']);
assert.deepEqual(getTrailingSlashPaths('/path/', 'ignore'), ['/path', '/path/']);
});

it('Generates correct paths for trailingslash always', () => {
assert.deepEqual(getTrailingSlashPaths('/path', 'always'), ['/path/']);
assert.deepEqual(getTrailingSlashPaths('/path/', 'always'), ['/path/']);
});

it('Generates correct paths for trailingslash never', () => {
assert.deepEqual(getTrailingSlashPaths('/path', 'never'), ['/path']);
assert.deepEqual(getTrailingSlashPaths('/path/', 'never'), ['/path']);
});
});
Loading