Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redirects #7067

Merged
merged 49 commits into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
46e7269
Redirects spike
matthewp May 10, 2023
ef3ea94
Allow redirects in static mode
matthewp May 11, 2023
d6b7104
Support in Netlify as well
matthewp May 11, 2023
f52116a
Adding a changeset
matthewp May 11, 2023
a70820b
Rename file
matthewp May 11, 2023
475294a
Merge branch 'main' into redirects-ssg
matthewp May 19, 2023
eed6a72
Fix build problem
matthewp May 19, 2023
1749ce5
Refactor to be more modular
matthewp May 19, 2023
ab0539b
Fix location ref
matthewp May 19, 2023
83ed366
Late test should only run in SSR
matthewp May 19, 2023
e9e4d72
Merge branch 'main' into redirects-ssg
matthewp May 22, 2023
4857c7d
Support redirects in Netlify SSR configuration (#7167)
matthewp May 23, 2023
25d7d20
Implement support for dynamic routes in redirects (#7173)
matthewp May 23, 2023
11a517b
Merge branch 'main' into redirects-ssg
matthewp May 23, 2023
ffc771e
Implement support for redirects config in the Vercel adapter (#7182)
matthewp May 23, 2023
f55e422
Add support for the object notation in redirects
matthewp May 23, 2023
2904ced
Merge branch 'main' into redirects-ssg
matthewp May 23, 2023
c2f889b
Use status 308 for non-GET redirects (#7186)
matthewp May 24, 2023
af2ceea
Merge branch 'main' into redirects-ssg
matthewp May 24, 2023
8b4d248
Implement redirects in Cloudflare (#7198)
matthewp May 25, 2023
ef9a456
Test that redirects can come from middleware (#7213)
matthewp May 26, 2023
fa03a41
Implement priority (#7210)
matthewp May 30, 2023
02a8506
Merge branch 'main' into redirects-ssg
matthewp May 30, 2023
eb7617d
Refactor
matthewp May 30, 2023
d7d0b22
Fix netlify test ordering
matthewp May 30, 2023
fd52bd6
Merge branch 'main' into redirects-ssg
matthewp May 30, 2023
a39eb51
Fix ordering again
matthewp May 30, 2023
d3895a2
Redirects: Allow preventing the output of the static HTML file (#7245)
matthewp May 30, 2023
2700c12
Do a simple push for priority
matthewp May 30, 2023
60dcfb6
Adding changesets
matthewp May 30, 2023
c040279
Put the implementation behind a flag.
matthewp May 31, 2023
79263e3
Self review
matthewp May 31, 2023
5198529
Update .changeset/chatty-actors-stare.md
matthewp May 31, 2023
63b5cfa
Update packages/astro/src/@types/astro.ts
matthewp May 31, 2023
17d0538
Update packages/astro/src/@types/astro.ts
matthewp May 31, 2023
7b64d65
Update packages/astro/src/@types/astro.ts
matthewp May 31, 2023
cd8e703
Update packages/astro/src/@types/astro.ts
matthewp May 31, 2023
7fd2a0a
Update docs on dynamic restrictions.
matthewp May 31, 2023
42bfa65
Update packages/astro/src/@types/astro.ts
matthewp Jun 1, 2023
4da0c7b
Update packages/astro/src/@types/astro.ts
matthewp Jun 1, 2023
3eaf936
Code review changes
matthewp Jun 1, 2023
7c0905b
Document netlify static adapter
matthewp Jun 1, 2023
63210fe
Update packages/astro/src/@types/astro.ts
matthewp Jun 1, 2023
57e8105
Slight reword
matthewp Jun 1, 2023
131f9d2
Update .changeset/twenty-suns-vanish.md
matthewp Jun 1, 2023
afcc209
Add a note about public/_redirects file
matthewp Jun 1, 2023
4afaa37
Merge branch 'main' into redirects-ssg
natemoo-re Jun 1, 2023
303b79a
Update packages/astro/src/@types/astro.ts
matthewp Jun 1, 2023
33a9d5f
Merge branch 'main' into redirects-ssg
matthewp Jun 5, 2023
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
6 changes: 6 additions & 0 deletions .changeset/chatty-actors-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@astrojs/netlify': minor
'astro': minor
---

Implements the redirects proposal
8 changes: 7 additions & 1 deletion packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,11 @@ export interface AstroUserConfig {
*/
cacheDir?: string;

/**
* TODO
*/
redirects?: Record<string, string>;

/**
* @docs
* @name site
Expand Down Expand Up @@ -1704,7 +1709,7 @@ export interface AstroPluginOptions {
logging: LogOptions;
}

export type RouteType = 'page' | 'endpoint';
export type RouteType = 'page' | 'endpoint' | 'redirect';

export interface RoutePart {
content: string;
Expand All @@ -1724,6 +1729,7 @@ export interface RouteData {
segments: RoutePart[][];
type: RouteType;
prerender: boolean;
redirect?: string;
}

export type SerializedRouteData = Omit<RouteData, 'generate' | 'pattern'> & {
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/build/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function getOutFolder(
case 'endpoint':
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
case 'page':
case 'redirect':
switch (astroConfig.build.format) {
case 'directory': {
if (STATUS_CODE_PAGES.has(pathname)) {
Expand All @@ -51,6 +52,7 @@ export function getOutFile(
case 'endpoint':
return new URL(npath.basename(pathname), outFolder);
case 'page':
case 'redirect':
switch (astroConfig.build.format) {
case 'directory': {
if (STATUS_CODE_PAGES.has(pathname)) {
Expand Down
42 changes: 33 additions & 9 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
createAPIContext,
throwIfRedirectNotAllowed,
} from '../endpoint/index.js';
import { AstroError } from '../errors/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { debug, info } from '../logger/core.js';
import { callMiddleware } from '../middleware/callMiddleware.js';
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
Expand Down Expand Up @@ -72,6 +72,12 @@ export function rootRelativeFacadeId(facadeId: string, settings: AstroSettings):
return facadeId.slice(fileURLToPath(settings.config.root).length);
}

function redirectWithNoLocation() {
throw new AstroError({
...AstroErrorData.RedirectWithNoLocation
});
}

// Determines of a Rollup chunk is an entrypoint page.
export function chunkIsPage(
settings: AstroSettings,
Expand Down Expand Up @@ -172,13 +178,17 @@ async function generatePage(
.map(({ sheet }) => sheet)
.reduce(mergeInlineCss, []);

const pageModule = ssrEntry.pageMap?.get(pageData.component);
let pageModule = ssrEntry.pageMap?.get(pageData.component);
const middleware = ssrEntry.middleware;

if (!pageModule) {
throw new Error(
`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`
);
if(pageData.route.type === 'redirect') {
pageModule = { 'default': Function.prototype as any };
} else {
throw new Error(
`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`
);
}
}

if (shouldSkipDraft(pageModule, opts.settings)) {
Expand Down Expand Up @@ -510,10 +520,24 @@ async function generatePath(
}
throw err;
}
throwIfRedirectNotAllowed(response, opts.settings.config);
// If there's no body, do nothing
if (!response.body) return;
body = await response.text();

switch(response.status) {
case 301:
case 302: {
const location = response.headers.get("location");
if(!location) {
return void redirectWithNoLocation();
}
body = `<!doctype html><meta http-equiv="refresh" content="0;url=${location}" />`;
pageData.route.redirect = location;
break;
}
default: {
// If there's no body, do nothing
if (!response.body) return;
body = await response.text();
}
}
}

const outFolder = getOutFolder(settings.config, pathname, pageData.route.type);
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/core/build/plugins/plugin-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
let imports = [];
let i = 0;
for (const pageData of eachPageData(internals)) {
if(pageData.route.type === 'redirect') {
continue;
}
const variable = `_page${i}`;
imports.push(`import * as ${variable} from ${JSON.stringify(pageData.moduleSpecifier)};`);
importMap += `[${JSON.stringify(pageData.component)}, ${variable}],`;
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
},
vite: {},
legacy: {},
redirects: {},
experimental: {
assets: false,
inlineStylesheets: 'never',
Expand Down Expand Up @@ -133,6 +134,7 @@ export const AstroConfigSchema = z.object({
.optional()
.default({})
),
redirects: z.record(z.string(), z.string()).default(ASTRO_CONFIG_DEFAULTS.redirects),
image: z
.object({
service: z.object({
Expand Down
13 changes: 13 additions & 0 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const AstroErrorData = {
* To redirect on a static website, the [meta refresh attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) can be used. Certain hosts also provide config-based redirects (ex: [Netlify redirects](https://docs.netlify.com/routing/redirects/)).
*/
StaticRedirectNotAvailable: {
// TODO remove
matthewp marked this conversation as resolved.
Show resolved Hide resolved
title: '`Astro.redirect` is not available in static mode.',
code: 3001,
message:
Expand Down Expand Up @@ -717,6 +718,18 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
return `The information stored in \`Astro.locals\` for the path "${href}" is not serializable.\nMake sure you store only serializable data.`;
},
},
/**
* @docs
* @see
* - [Astro.redirect](https://docs.astro.build/en/guides/server-side-rendering/#astroredirect)
* @description
* A redirect must be given a location with the `Location` header.
*/
RedirectWithNoLocation: {
// TODO remove
title: 'A redirect must be given a location with the `Location` header.',
code: 3035,
},
// No headings here, that way Vite errors are merged with Astro ones in the docs, which makes more sense to users.
// Vite Errors - 4xxx
/**
Expand Down
9 changes: 9 additions & 0 deletions packages/astro/src/core/render/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ export type RenderPage = {
};

export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) {
if(renderContext.route?.type === 'redirect') {
return new Response(null, {
status: 301,
headers: {
'location': renderContext.route.redirect!
}
});
}

// Validate the page component before rendering the page
const Component = mod.default;
if (!Component)
Expand Down
30 changes: 14 additions & 16 deletions packages/astro/src/core/render/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,23 +204,21 @@ export function createResult(args: CreateResultArgs): SSRResult {
locals,
request,
url,
redirect: args.ssr
? (path, status) => {
// If the response is already sent, error as we cannot proceed with the redirect.
if ((request as any)[responseSentSymbol]) {
throw new AstroError({
...AstroErrorData.ResponseSentError,
});
}
redirect(path, status) {
// If the response is already sent, error as we cannot proceed with the redirect.
if ((request as any)[responseSentSymbol]) {
throw new AstroError({
...AstroErrorData.ResponseSentError,
});
}

return new Response(null, {
status: status || 302,
headers: {
Location: path,
},
});
}
: onlyAvailableInSSR('Astro.redirect'),
return new Response(null, {
status: status || 302,
headers: {
Location: path,
},
});
},
response: response as AstroGlobal['response'],
slots: astroSlots as unknown as AstroGlobal['slots'],
};
Expand Down
39 changes: 39 additions & 0 deletions packages/astro/src/core/routing/manifest/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,45 @@ export function createRouteManifest(
});
});

Object.entries(settings.config.redirects).forEach(([from, to]) => {
const trailingSlash = config.trailingSlash;

const segments = removeLeadingForwardSlash(from)
.split(path.posix.sep)
.filter(Boolean)
.map((s: string) => {
validateSegment(s);
return getParts(s, from);
});

const pattern = getPattern(segments, settings.config.base, trailingSlash);
const generate = getRouteGenerator(segments, trailingSlash);
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
? `/${segments.map((segment) => segment[0].content).join('/')}`
: null;
const params = segments
.flat()
.filter((p) => p.dynamic)
.map((p) => p.content);
const route = `/${segments
.map(([{ dynamic, content }]) => (dynamic ? `[${content}]` : content))
.join('/')}`.toLowerCase();


routes.unshift({
type: 'redirect',
route,
pattern,
segments,
params,
component: '',
generate,
pathname: pathname || void 0,
prerender: false,
redirect: to
});
});

return {
routes,
};
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/src/runtime/server/render/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export function stringifyChunk(
}
return renderAllHeadContent(result);
}
default: {
if(chunk instanceof Response) {
return '';
}
throw new Error(`Unknown chunk type: ${(chunk as any).type}`);
}
}
} else {
if (isSlotString(chunk as string)) {
Expand Down Expand Up @@ -102,6 +108,7 @@ export function chunkToByteArray(
if (chunk instanceof Uint8Array) {
return chunk as Uint8Array;
}

// stringify chunk might return a HTMLString
let stringified = stringifyChunk(result, chunk);
return encoder.encode(stringified.toString());
Expand Down
57 changes: 57 additions & 0 deletions packages/astro/test/redirects.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect } from 'chai';
import { loadFixture } from './test-utils.js';
import testAdapter from './test-adapter.js';

describe('Astro.redirect', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;

describe('output: "server"', () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-redirect/',
output: 'server',
adapter: testAdapter(),
});
await fixture.build();
});

it('Returns a 302 status', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/secret');
const response = await app.render(request);
expect(response.status).to.equal(302);
expect(response.headers.get('location')).to.equal('/login');
});

it('Warns when used inside a component', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/late');
const response = await app.render(request);
try {
const text = await response.text();
expect(false).to.equal(true);
} catch (e) {
expect(e.message).to.equal(
'The response has already been sent to the browser and cannot be altered.'
);
}
});
});

describe('output: "static"', () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-redirect/',
output: 'static',
});
await fixture.build();
});

it('Includes the meta refresh tag.', async () => {
const html = await fixture.readFile('/secret/index.html');
expect(html).to.include('http-equiv="refresh');
expect(html).to.include('url=/login');
});
});
});
39 changes: 0 additions & 39 deletions packages/astro/test/ssr-redirect.test.js

This file was deleted.

Loading