Compare commits
16 commits
main
...
redirects-
Author | SHA1 | Date | |
---|---|---|---|
|
4adedbaafe | ||
|
b8ef648f8c | ||
|
11a517b1f1 | ||
|
25d7d208ba | ||
|
4857c7d317 | ||
|
e9e4d72598 | ||
|
83ed3669be | ||
|
ab0539b951 | ||
|
1749ce5d08 | ||
|
eed6a72a2a | ||
|
475294a60e | ||
|
a70820be15 | ||
|
f52116ac03 | ||
|
d6b7104722 | ||
|
ef3ea942cc | ||
|
46e726960f |
35 changed files with 697 additions and 104 deletions
6
.changeset/chatty-actors-stare.md
Normal file
6
.changeset/chatty-actors-stare.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'@astrojs/netlify': minor
|
||||||
|
'astro': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Implements the redirects proposal
|
|
@ -452,6 +452,11 @@ export interface AstroUserConfig {
|
||||||
*/
|
*/
|
||||||
cacheDir?: string;
|
cacheDir?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
*/
|
||||||
|
redirects?: Record<string, string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @docs
|
* @docs
|
||||||
* @name site
|
* @name site
|
||||||
|
@ -1577,6 +1582,8 @@ export interface AstroAdapter {
|
||||||
|
|
||||||
type Body = string;
|
type Body = string;
|
||||||
|
|
||||||
|
export type ValidRedirectStatus = 300 | 301 | 302 | 303 | 304 | 307 | 308;
|
||||||
|
|
||||||
// Shared types between `Astro` global and API context object
|
// Shared types between `Astro` global and API context object
|
||||||
interface AstroSharedContext<Props extends Record<string, any> = Record<string, any>> {
|
interface AstroSharedContext<Props extends Record<string, any> = Record<string, any>> {
|
||||||
/**
|
/**
|
||||||
|
@ -1606,7 +1613,7 @@ interface AstroSharedContext<Props extends Record<string, any> = Record<string,
|
||||||
/**
|
/**
|
||||||
* Redirect to another page (**SSR Only**).
|
* Redirect to another page (**SSR Only**).
|
||||||
*/
|
*/
|
||||||
redirect(path: string, status?: 301 | 302 | 303 | 307 | 308): Response;
|
redirect(path: string, status?: ValidRedirectStatus): Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object accessed via Astro middleware
|
* Object accessed via Astro middleware
|
||||||
|
@ -1821,7 +1828,7 @@ export interface AstroPluginOptions {
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RouteType = 'page' | 'endpoint';
|
export type RouteType = 'page' | 'endpoint' | 'redirect';
|
||||||
|
|
||||||
export interface RoutePart {
|
export interface RoutePart {
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -1829,6 +1836,11 @@ export interface RoutePart {
|
||||||
spread: boolean;
|
spread: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedirectConfig = string | {
|
||||||
|
status: ValidRedirectStatus;
|
||||||
|
destination: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RouteData {
|
export interface RouteData {
|
||||||
route: string;
|
route: string;
|
||||||
component: string;
|
component: string;
|
||||||
|
@ -1841,6 +1853,12 @@ export interface RouteData {
|
||||||
segments: RoutePart[][];
|
segments: RoutePart[][];
|
||||||
type: RouteType;
|
type: RouteType;
|
||||||
prerender: boolean;
|
prerender: boolean;
|
||||||
|
redirect?: RedirectConfig;
|
||||||
|
redirectRoute?: RouteData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RedirectRouteData = RouteData & {
|
||||||
|
redirect: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedRouteData = Omit<RouteData, 'generate' | 'pattern'> & {
|
export type SerializedRouteData = Omit<RouteData, 'generate' | 'pattern'> & {
|
||||||
|
|
|
@ -26,6 +26,7 @@ export function getOutFolder(
|
||||||
case 'endpoint':
|
case 'endpoint':
|
||||||
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
||||||
case 'page':
|
case 'page':
|
||||||
|
case 'redirect':
|
||||||
switch (astroConfig.build.format) {
|
switch (astroConfig.build.format) {
|
||||||
case 'directory': {
|
case 'directory': {
|
||||||
if (STATUS_CODE_PAGES.has(pathname)) {
|
if (STATUS_CODE_PAGES.has(pathname)) {
|
||||||
|
@ -51,6 +52,7 @@ export function getOutFile(
|
||||||
case 'endpoint':
|
case 'endpoint':
|
||||||
return new URL(npath.basename(pathname), outFolder);
|
return new URL(npath.basename(pathname), outFolder);
|
||||||
case 'page':
|
case 'page':
|
||||||
|
case 'redirect':
|
||||||
switch (astroConfig.build.format) {
|
switch (astroConfig.build.format) {
|
||||||
case 'directory': {
|
case 'directory': {
|
||||||
if (STATUS_CODE_PAGES.has(pathname)) {
|
if (STATUS_CODE_PAGES.has(pathname)) {
|
||||||
|
|
|
@ -35,6 +35,7 @@ import { debug, info } from '../logger/core.js';
|
||||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||||
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
|
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
|
||||||
import { callGetStaticPaths } from '../render/route-cache.js';
|
import { callGetStaticPaths } from '../render/route-cache.js';
|
||||||
|
import { getRedirectLocationOrThrow, routeIsRedirect } from '../redirects/index.js';
|
||||||
import {
|
import {
|
||||||
createAssetLink,
|
createAssetLink,
|
||||||
createModuleScriptsSet,
|
createModuleScriptsSet,
|
||||||
|
@ -169,9 +170,16 @@ async function generatePage(
|
||||||
.map(({ sheet }) => sheet)
|
.map(({ sheet }) => sheet)
|
||||||
.reduce(mergeInlineCss, []);
|
.reduce(mergeInlineCss, []);
|
||||||
|
|
||||||
const pageModulePromise = ssrEntry.pageMap?.get(pageData.component);
|
let pageModulePromise = ssrEntry.pageMap?.get(pageData.component);
|
||||||
const middleware = ssrEntry.middleware;
|
const middleware = ssrEntry.middleware;
|
||||||
|
|
||||||
|
if (!pageModulePromise && routeIsRedirect(pageData.route)) {
|
||||||
|
if(pageData.route.redirectRoute) {
|
||||||
|
pageModulePromise = ssrEntry.pageMap?.get(pageData.route.redirectRoute!.component);
|
||||||
|
} else {
|
||||||
|
pageModulePromise = () => Promise.resolve<any>({ default: () => {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!pageModulePromise) {
|
if (!pageModulePromise) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`
|
`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`
|
||||||
|
@ -509,11 +517,26 @@ async function generatePath(
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
throwIfRedirectNotAllowed(response, opts.settings.config);
|
|
||||||
|
switch(true) {
|
||||||
|
case (response.status >= 300 && response.status < 400): {
|
||||||
|
const location = getRedirectLocationOrThrow(response.headers);
|
||||||
|
body = `<!doctype html>
|
||||||
|
<title>Redirecting to: ${location}</title>
|
||||||
|
<meta http-equiv="refresh" content="0;url=${location}" />`;
|
||||||
|
// A dynamic redirect, set the location so that integrations know about it.
|
||||||
|
if(pageData.route.type !== 'redirect') {
|
||||||
|
pageData.route.redirect = location;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
// If there's no body, do nothing
|
// If there's no body, do nothing
|
||||||
if (!response.body) return;
|
if (!response.body) return;
|
||||||
body = await response.text();
|
body = await response.text();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const outFolder = getOutFolder(settings.config, pathname, pageData.route.type);
|
const outFolder = getOutFolder(settings.config, pathname, pageData.route.type);
|
||||||
const outFile = getOutFile(settings.config, outFolder, pathname, pageData.route.type);
|
const outFile = getOutFile(settings.config, outFolder, pathname, pageData.route.type);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { eachPageData, type BuildInternals } from '../internal.js';
|
||||||
import type { AstroBuildPlugin } from '../plugin';
|
import type { AstroBuildPlugin } from '../plugin';
|
||||||
import type { StaticBuildOptions } from '../types';
|
import type { StaticBuildOptions } from '../types';
|
||||||
import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js';
|
import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js';
|
||||||
|
import { routeIsRedirect } from '../../redirects/index.js';
|
||||||
|
|
||||||
function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
|
function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
|
||||||
return {
|
return {
|
||||||
|
@ -28,6 +29,9 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V
|
||||||
let imports = [];
|
let imports = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const pageData of eachPageData(internals)) {
|
for (const pageData of eachPageData(internals)) {
|
||||||
|
if(routeIsRedirect(pageData.route)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const variable = `_page${i}`;
|
const variable = `_page${i}`;
|
||||||
imports.push(
|
imports.push(
|
||||||
`const ${variable} = () => import(${JSON.stringify(pageData.moduleSpecifier)});`
|
`const ${variable} = () => import(${JSON.stringify(pageData.moduleSpecifier)});`
|
||||||
|
|
|
@ -37,6 +37,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
||||||
},
|
},
|
||||||
vite: {},
|
vite: {},
|
||||||
legacy: {},
|
legacy: {},
|
||||||
|
redirects: {},
|
||||||
experimental: {
|
experimental: {
|
||||||
assets: false,
|
assets: false,
|
||||||
hybridOutput: false,
|
hybridOutput: false,
|
||||||
|
@ -137,6 +138,7 @@ export const AstroConfigSchema = z.object({
|
||||||
.optional()
|
.optional()
|
||||||
.default({})
|
.default({})
|
||||||
),
|
),
|
||||||
|
redirects: z.record(z.string(), z.string()).default(ASTRO_CONFIG_DEFAULTS.redirects),
|
||||||
image: z
|
image: z
|
||||||
.object({
|
.object({
|
||||||
service: z.object({
|
service: z.object({
|
||||||
|
|
|
@ -43,6 +43,7 @@ export const AstroErrorData = {
|
||||||
* The `Astro.redirect` function is only available when [Server-side rendering](/en/guides/server-side-rendering/) is enabled.
|
* The `Astro.redirect` function is only available when [Server-side rendering](/en/guides/server-side-rendering/) is enabled.
|
||||||
*
|
*
|
||||||
* 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/)).
|
* 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/)).
|
||||||
|
* @deprecated since version 2.6
|
||||||
*/
|
*/
|
||||||
StaticRedirectNotAvailable: {
|
StaticRedirectNotAvailable: {
|
||||||
title: '`Astro.redirect` is not available in static mode.',
|
title: '`Astro.redirect` is not available in static mode.',
|
||||||
|
@ -719,6 +720,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.`;
|
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.
|
// No headings here, that way Vite errors are merged with Astro ones in the docs, which makes more sense to users.
|
||||||
// Vite Errors - 4xxx
|
// Vite Errors - 4xxx
|
||||||
/**
|
/**
|
||||||
|
|
27
packages/astro/src/core/redirects/helpers.ts
Normal file
27
packages/astro/src/core/redirects/helpers.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import type { RouteData, RedirectRouteData, Params, ValidRedirectStatus } from '../../@types/astro';
|
||||||
|
|
||||||
|
export function routeIsRedirect(route: RouteData | undefined): route is RedirectRouteData {
|
||||||
|
return route?.type === 'redirect';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): string {
|
||||||
|
const routeData = redirectRoute.redirectRoute;
|
||||||
|
const route = redirectRoute.redirect;
|
||||||
|
|
||||||
|
if(typeof routeData !== 'undefined') {
|
||||||
|
return routeData?.generate(data) || routeData?.pathname || '/';
|
||||||
|
} else if(typeof route === 'string') {
|
||||||
|
return route;
|
||||||
|
} else if(typeof route === 'undefined') {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
return route.destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redirectRouteStatus(redirectRoute: RouteData): ValidRedirectStatus {
|
||||||
|
const routeData = redirectRoute.redirectRoute;
|
||||||
|
if(typeof routeData?.redirect === 'object') {
|
||||||
|
return routeData.redirect.status;
|
||||||
|
}
|
||||||
|
return 301;
|
||||||
|
}
|
2
packages/astro/src/core/redirects/index.ts
Normal file
2
packages/astro/src/core/redirects/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { getRedirectLocationOrThrow } from './validate.js';
|
||||||
|
export { routeIsRedirect, redirectRouteGenerate, redirectRouteStatus } from './helpers.js';
|
13
packages/astro/src/core/redirects/validate.ts
Normal file
13
packages/astro/src/core/redirects/validate.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
|
|
||||||
|
export function getRedirectLocationOrThrow(headers: Headers): string {
|
||||||
|
let location = headers.get('location');
|
||||||
|
|
||||||
|
if(!location) {
|
||||||
|
throw new AstroError({
|
||||||
|
...AstroErrorData.RedirectWithNoLocation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return location;
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import type { RenderContext } from './context.js';
|
||||||
import type { Environment } from './environment.js';
|
import type { Environment } from './environment.js';
|
||||||
import { createResult } from './result.js';
|
import { createResult } from './result.js';
|
||||||
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
||||||
|
import { routeIsRedirect, redirectRouteGenerate, redirectRouteStatus } from '../redirects/index.js';
|
||||||
|
|
||||||
interface GetParamsAndPropsOptions {
|
interface GetParamsAndPropsOptions {
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance;
|
||||||
|
@ -111,6 +112,15 @@ export type RenderPage = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) {
|
export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) {
|
||||||
|
if(routeIsRedirect(renderContext.route)) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: redirectRouteStatus(renderContext.route),
|
||||||
|
headers: {
|
||||||
|
location: redirectRouteGenerate(renderContext.route, renderContext.params)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Validate the page component before rendering the page
|
// Validate the page component before rendering the page
|
||||||
const Component = mod.default;
|
const Component = mod.default;
|
||||||
if (!Component)
|
if (!Component)
|
||||||
|
|
|
@ -206,8 +206,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
locals,
|
locals,
|
||||||
request,
|
request,
|
||||||
url,
|
url,
|
||||||
redirect: args.ssr
|
redirect(path, status) {
|
||||||
? (path, status) => {
|
|
||||||
// If the response is already sent, error as we cannot proceed with the redirect.
|
// If the response is already sent, error as we cannot proceed with the redirect.
|
||||||
if ((request as any)[responseSentSymbol]) {
|
if ((request as any)[responseSentSymbol]) {
|
||||||
throw new AstroError({
|
throw new AstroError({
|
||||||
|
@ -221,8 +220,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
Location: path,
|
Location: path,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
: onlyAvailableInSSR('Astro.redirect'),
|
|
||||||
response: response as AstroGlobal['response'],
|
response: response as AstroGlobal['response'],
|
||||||
slots: astroSlots as unknown as AstroGlobal['slots'],
|
slots: astroSlots as unknown as AstroGlobal['slots'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -420,6 +420,47 @@ 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: from,
|
||||||
|
generate,
|
||||||
|
pathname: pathname || void 0,
|
||||||
|
prerender: false,
|
||||||
|
redirect: to,
|
||||||
|
redirectRoute: routes.find(r => r.route === to)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
routes,
|
routes,
|
||||||
};
|
};
|
||||||
|
|
|
@ -57,6 +57,12 @@ export function stringifyChunk(
|
||||||
}
|
}
|
||||||
return renderAllHeadContent(result);
|
return renderAllHeadContent(result);
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
if(chunk instanceof Response) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown chunk type: ${(chunk as any).type}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isSlotString(chunk as string)) {
|
if (isSlotString(chunk as string)) {
|
||||||
|
@ -102,6 +108,7 @@ export function chunkToByteArray(
|
||||||
if (chunk instanceof Uint8Array) {
|
if (chunk instanceof Uint8Array) {
|
||||||
return chunk as Uint8Array;
|
return chunk as Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
// stringify chunk might return a HTMLString
|
// stringify chunk might return a HTMLString
|
||||||
let stringified = stringifyChunk(result, chunk);
|
let stringified = stringifyChunk(result, chunk);
|
||||||
return encoder.encode(stringified.toString());
|
return encoder.encode(stringified.toString());
|
||||||
|
|
25
packages/astro/test/fixtures/ssr-redirect/src/pages/articles/[...slug].astro
vendored
Normal file
25
packages/astro/test/fixtures/ssr-redirect/src/pages/articles/[...slug].astro
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const posts = [
|
||||||
|
{ slug: 'one', data: {draft: false, title: 'One'} },
|
||||||
|
{ slug: 'two', data: {draft: false, title: 'Two'} }
|
||||||
|
];
|
||||||
|
return posts.map((post) => {
|
||||||
|
return {
|
||||||
|
params: { slug: post.slug },
|
||||||
|
props: { draft: post.data.draft, title: post.data.title },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const { slug } = Astro.params;
|
||||||
|
const { title } = Astro.props;
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{ title }</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{ title }</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
packages/astro/test/fixtures/ssr-redirect/src/pages/index.astro
vendored
Normal file
10
packages/astro/test/fixtures/ssr-redirect/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Testing</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,5 +1,6 @@
|
||||||
---
|
---
|
||||||
import Redirect from '../components/redirect.astro';
|
import Redirect from '../components/redirect.astro';
|
||||||
|
const staticMode = import.meta.env.STATIC_MODE;
|
||||||
---
|
---
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
@ -7,6 +8,8 @@ import Redirect from '../components/redirect.astro';
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Testing</h1>
|
<h1>Testing</h1>
|
||||||
|
{ !staticMode ? (
|
||||||
<Redirect />
|
<Redirect />
|
||||||
|
) : <div></div>}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
91
packages/astro/test/redirects.test.js
Normal file
91
packages/astro/test/redirects.test.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
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 {
|
||||||
|
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 () => {
|
||||||
|
process.env.STATIC_MODE = true;
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/ssr-redirect/',
|
||||||
|
output: 'static',
|
||||||
|
redirects: {
|
||||||
|
'/one': '/',
|
||||||
|
'/two': '/',
|
||||||
|
'/blog/[...slug]': '/articles/[...slug]',
|
||||||
|
'/three': {
|
||||||
|
status: 302,
|
||||||
|
destination: '/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Includes the meta refresh tag in Astro.redirect pages', async () => {
|
||||||
|
const html = await fixture.readFile('/secret/index.html');
|
||||||
|
expect(html).to.include('http-equiv="refresh');
|
||||||
|
expect(html).to.include('url=/login');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Includes the meta refresh tag in `redirect` config pages', async () => {
|
||||||
|
let html = await fixture.readFile('/one/index.html');
|
||||||
|
expect(html).to.include('http-equiv="refresh');
|
||||||
|
expect(html).to.include('url=/');
|
||||||
|
|
||||||
|
html = await fixture.readFile('/two/index.html');
|
||||||
|
expect(html).to.include('http-equiv="refresh');
|
||||||
|
expect(html).to.include('url=/');
|
||||||
|
|
||||||
|
html = await fixture.readFile('/three/index.html');
|
||||||
|
expect(html).to.include('http-equiv="refresh');
|
||||||
|
expect(html).to.include('url=/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Generates page for dynamic routes', async () => {
|
||||||
|
let html = await fixture.readFile('/blog/one/index.html');
|
||||||
|
expect(html).to.include('http-equiv="refresh');
|
||||||
|
expect(html).to.include('url=/articles/one');
|
||||||
|
|
||||||
|
html = await fixture.readFile('/blog/two/index.html');
|
||||||
|
expect(html).to.include('http-equiv="refresh');
|
||||||
|
expect(html).to.include('url=/articles/two');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,39 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
||||||
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.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,2 +1,3 @@
|
||||||
export { netlifyEdgeFunctions } from './integration-edge-functions.js';
|
export { netlifyEdgeFunctions } from './integration-edge-functions.js';
|
||||||
export { netlifyFunctions as default, netlifyFunctions } from './integration-functions.js';
|
export { netlifyFunctions as default, netlifyFunctions } from './integration-functions.js';
|
||||||
|
export { netlifyStatic } from './integration-static.js';
|
||||||
|
|
18
packages/integrations/netlify/src/integration-static.ts
Normal file
18
packages/integrations/netlify/src/integration-static.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
|
||||||
|
import type { Args } from './netlify-functions.js';
|
||||||
|
import { createRedirects } from './shared.js';
|
||||||
|
|
||||||
|
export function netlifyStatic(): AstroIntegration {
|
||||||
|
let _config: any;
|
||||||
|
return {
|
||||||
|
name: '@astrojs/netlify',
|
||||||
|
hooks: {
|
||||||
|
'astro:config:done': ({ config }) => {
|
||||||
|
_config = config;
|
||||||
|
},
|
||||||
|
'astro:build:done': async ({ dir, routes }) => {
|
||||||
|
await createRedirects(_config, routes, dir, '', 'static');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,20 +1,27 @@
|
||||||
import type { AstroConfig, RouteData } from 'astro';
|
import type { AstroConfig, RouteData, ValidRedirectStatus } from 'astro';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
type RedirectDefinition = {
|
export type RedirectDefinition = {
|
||||||
dynamic: boolean;
|
dynamic: boolean;
|
||||||
input: string;
|
input: string;
|
||||||
target: string;
|
target: string;
|
||||||
weight: 0 | 1;
|
weight: 0 | 1;
|
||||||
status: 200 | 404;
|
status: 200 | 404 | ValidRedirectStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getRedirectStatus(route: RouteData): ValidRedirectStatus {
|
||||||
|
if(typeof route.redirect === 'object') {
|
||||||
|
return route.redirect.status;
|
||||||
|
}
|
||||||
|
return 301;
|
||||||
|
}
|
||||||
|
|
||||||
export async function createRedirects(
|
export async function createRedirects(
|
||||||
config: AstroConfig,
|
config: AstroConfig,
|
||||||
routes: RouteData[],
|
routes: RouteData[],
|
||||||
dir: URL,
|
dir: URL,
|
||||||
entryFile: string,
|
entryFile: string,
|
||||||
type: 'functions' | 'edge-functions' | 'builders'
|
type: 'functions' | 'edge-functions' | 'builders' | 'static'
|
||||||
) {
|
) {
|
||||||
const _redirectsURL = new URL('./_redirects', dir);
|
const _redirectsURL = new URL('./_redirects', dir);
|
||||||
const kind = type ?? 'functions';
|
const kind = type ?? 'functions';
|
||||||
|
@ -23,7 +30,21 @@ export async function createRedirects(
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
if (route.pathname) {
|
if (route.pathname) {
|
||||||
if (route.distURL) {
|
if(route.redirect) {
|
||||||
|
definitions.push({
|
||||||
|
dynamic: false,
|
||||||
|
input: route.pathname,
|
||||||
|
target: typeof route.redirect === 'object' ? route.redirect.destination : route.redirect,
|
||||||
|
status: getRedirectStatus(route),
|
||||||
|
weight: 1
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(kind === 'static') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (route.distURL) {
|
||||||
definitions.push({
|
definitions.push({
|
||||||
dynamic: false,
|
dynamic: false,
|
||||||
input: route.pathname,
|
input: route.pathname,
|
||||||
|
@ -51,31 +72,18 @@ export async function createRedirects(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const pattern =
|
const pattern = generateDynamicPattern(route);
|
||||||
'/' +
|
|
||||||
route.segments
|
|
||||||
.map(([part]) => {
|
|
||||||
//(part.dynamic ? '*' : part.content)
|
|
||||||
if (part.dynamic) {
|
|
||||||
if (part.spread) {
|
|
||||||
return '*';
|
|
||||||
} else {
|
|
||||||
return ':' + part.content;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return part.content;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.join('/');
|
|
||||||
|
|
||||||
if (route.distURL) {
|
if (route.distURL) {
|
||||||
|
const targetRoute = route.redirectRoute ?? route;
|
||||||
|
const targetPattern = generateDynamicPattern(targetRoute);
|
||||||
const target =
|
const target =
|
||||||
`${pattern}` + (config.build.format === 'directory' ? '/index.html' : '.html');
|
`${targetPattern}` + (config.build.format === 'directory' ? '/index.html' : '.html');
|
||||||
definitions.push({
|
definitions.push({
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
input: pattern,
|
input: pattern,
|
||||||
target,
|
target,
|
||||||
status: 200,
|
status: route.type === 'redirect' ? 301 : 200,
|
||||||
weight: 1,
|
weight: 1,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -98,9 +106,29 @@ export async function createRedirects(
|
||||||
await fs.promises.appendFile(_redirectsURL, _redirects, 'utf-8');
|
await fs.promises.appendFile(_redirectsURL, _redirects, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateDynamicPattern(route: RouteData) {
|
||||||
|
const pattern =
|
||||||
|
'/' +
|
||||||
|
route.segments
|
||||||
|
.map(([part]) => {
|
||||||
|
//(part.dynamic ? '*' : part.content)
|
||||||
|
if (part.dynamic) {
|
||||||
|
if (part.spread) {
|
||||||
|
return '*';
|
||||||
|
} else {
|
||||||
|
return ':' + part.content;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return part.content;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('/');
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
|
||||||
function prettify(definitions: RedirectDefinition[]) {
|
function prettify(definitions: RedirectDefinition[]) {
|
||||||
let minInputLength = 0,
|
let minInputLength = 4,
|
||||||
minTargetLength = 0;
|
minTargetLength = 4;
|
||||||
definitions.sort((a, b) => {
|
definitions.sort((a, b) => {
|
||||||
// Find the longest input, so we can format things nicely
|
// Find the longest input, so we can format things nicely
|
||||||
if (a.input.length > minInputLength) {
|
if (a.input.length > minInputLength) {
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { load as cheerioLoad } from 'cheerio';
|
||||||
|
import { loadFixture, testIntegration } from './test-utils.js';
|
||||||
|
import netlifyAdapter from '../../dist/index.js';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
describe('SSG - Redirects', () => {
|
||||||
|
/** @type {import('../../../astro/test/test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: new URL('../static/fixtures/redirects/', import.meta.url).toString(),
|
||||||
|
output: 'server',
|
||||||
|
adapter: netlifyAdapter({
|
||||||
|
dist: new URL('../static/fixtures/redirects/dist/', import.meta.url),
|
||||||
|
}),
|
||||||
|
site: `http://example.com`,
|
||||||
|
integrations: [testIntegration()],
|
||||||
|
redirects: {
|
||||||
|
'/other': '/'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Creates a redirects file', async () => {
|
||||||
|
let redirects = await fixture.readFile('/_redirects');
|
||||||
|
let parts = redirects.split(/\s+/);
|
||||||
|
expect(parts).to.deep.equal([
|
||||||
|
'/other', '/', '301',
|
||||||
|
'/', '/.netlify/functions/entry', '200',
|
||||||
|
|
||||||
|
// This uses the dynamic Astro.redirect, so we don't know that it's a redirect
|
||||||
|
// until runtime. This is correct!
|
||||||
|
'/nope', '/.netlify/functions/entry', '200',
|
||||||
|
|
||||||
|
// A real route
|
||||||
|
'/team/articles/*', '/.netlify/functions/entry', '200',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
<html>
|
||||||
|
<head><title>Testing</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Testing</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
return Astro.redirect('/');
|
||||||
|
---
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const posts = [
|
||||||
|
{ slug: 'one', data: {draft: false, title: 'One'} },
|
||||||
|
{ slug: 'two', data: {draft: false, title: 'Two'} }
|
||||||
|
];
|
||||||
|
return posts.map((post) => {
|
||||||
|
return {
|
||||||
|
params: { slug: post.slug },
|
||||||
|
props: { draft: post.data.draft, title: post.data.title },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const { slug } = Astro.params;
|
||||||
|
const { title } = Astro.props;
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{ title }</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{ title }</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
39
packages/integrations/netlify/test/static/redirects.test.js
Normal file
39
packages/integrations/netlify/test/static/redirects.test.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture, testIntegration } from './test-utils.js';
|
||||||
|
import { netlifyStatic } from '../../dist/index.js';
|
||||||
|
|
||||||
|
describe('SSG - Redirects', () => {
|
||||||
|
/** @type {import('../../../astro/test/test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: new URL('./fixtures/redirects/', import.meta.url).toString(),
|
||||||
|
output: 'static',
|
||||||
|
adapter: netlifyStatic(),
|
||||||
|
site: `http://example.com`,
|
||||||
|
integrations: [testIntegration()],
|
||||||
|
redirects: {
|
||||||
|
'/other': '/',
|
||||||
|
'/two': {
|
||||||
|
status: 302,
|
||||||
|
destination: '/'
|
||||||
|
},
|
||||||
|
'/blog/[...slug]': '/team/articles/[...slug]'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Creates a redirects file', async () => {
|
||||||
|
let redirects = await fixture.readFile('/_redirects');
|
||||||
|
let parts = redirects.split(/\s+/);
|
||||||
|
expect(parts).to.deep.equal([
|
||||||
|
'/blog/*', '/team/articles/*/index.html', '301',
|
||||||
|
'/two', '/', '302',
|
||||||
|
'/other', '/', '301',
|
||||||
|
'/nope', '/', '301',
|
||||||
|
'/team/articles/*', '/team/articles/*/index.html', '200'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
29
packages/integrations/netlify/test/static/test-utils.js
Normal file
29
packages/integrations/netlify/test/static/test-utils.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// @ts-check
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
export * from '../../../../astro/test/test-utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {import('../../../../astro/dist/types/@types/astro').AstroIntegration}
|
||||||
|
*/
|
||||||
|
export function testIntegration() {
|
||||||
|
return {
|
||||||
|
name: '@astrojs/netlify/test-integration',
|
||||||
|
hooks: {
|
||||||
|
'astro:config:setup': ({ updateConfig }) => {
|
||||||
|
updateConfig({
|
||||||
|
vite: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@astrojs/netlify/netlify-functions.js': fileURLToPath(
|
||||||
|
new URL('../../dist/netlify-functions.js', import.meta.url)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
import type { AstroConfig, RouteData, RoutePart } from 'astro';
|
import type { AstroConfig, RouteData, RoutePart } from 'astro';
|
||||||
|
import nodePath from 'node:path';
|
||||||
|
|
||||||
|
const pathJoin = nodePath.posix.join;
|
||||||
|
|
||||||
// https://vercel.com/docs/project-configuration#legacy/routes
|
// https://vercel.com/docs/project-configuration#legacy/routes
|
||||||
interface VercelRoute {
|
interface VercelRoute {
|
||||||
|
@ -36,6 +39,10 @@ function getMatchPattern(segments: RoutePart[][]) {
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendTrailingSlash(route: string): string {
|
||||||
|
return route.at(-1) === '/' ? route : route + '/';
|
||||||
|
}
|
||||||
|
|
||||||
function getReplacePattern(segments: RoutePart[][]) {
|
function getReplacePattern(segments: RoutePart[][]) {
|
||||||
let n = 0;
|
let n = 0;
|
||||||
let result = '';
|
let result = '';
|
||||||
|
@ -54,23 +61,45 @@ function getReplacePattern(segments: RoutePart[][]) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRedirectLocation(route: RouteData, config: AstroConfig): string {
|
||||||
|
if(route.redirectRoute) {
|
||||||
|
const pattern = getReplacePattern(route.redirectRoute.segments);
|
||||||
|
const path = (config.trailingSlash === 'always' ? appendTrailingSlash(pattern) : pattern);
|
||||||
|
return pathJoin(config.base, path);
|
||||||
|
} else if(typeof route.redirect === 'object') {
|
||||||
|
return pathJoin(config.base, route.redirect.destination);
|
||||||
|
} else {
|
||||||
|
return pathJoin(config.base, route.redirect || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRedirectStatus(route: RouteData): number {
|
||||||
|
if(typeof route.redirect === 'object') {
|
||||||
|
return route.redirect.status;
|
||||||
|
}
|
||||||
|
return 301;
|
||||||
|
}
|
||||||
|
|
||||||
export function getRedirects(routes: RouteData[], config: AstroConfig): VercelRoute[] {
|
export function getRedirects(routes: RouteData[], config: AstroConfig): VercelRoute[] {
|
||||||
let redirects: VercelRoute[] = [];
|
let redirects: VercelRoute[] = [];
|
||||||
|
|
||||||
if (config.trailingSlash === 'always') {
|
|
||||||
for (const route of routes) {
|
|
||||||
if (route.type !== 'page' || route.segments.length === 0) continue;
|
|
||||||
|
|
||||||
|
|
||||||
|
for(const route of routes) {
|
||||||
|
if(route.type === 'redirect') {
|
||||||
|
redirects.push({
|
||||||
|
src: config.base + getMatchPattern(route.segments),
|
||||||
|
headers: { Location: getRedirectLocation(route, config) },
|
||||||
|
status: getRedirectStatus(route)
|
||||||
|
});
|
||||||
|
} else if (route.type === 'page') {
|
||||||
|
if (config.trailingSlash === 'always') {
|
||||||
redirects.push({
|
redirects.push({
|
||||||
src: config.base + getMatchPattern(route.segments),
|
src: config.base + getMatchPattern(route.segments),
|
||||||
headers: { Location: config.base + getReplacePattern(route.segments) + '/' },
|
headers: { Location: config.base + getReplacePattern(route.segments) + '/' },
|
||||||
status: 308,
|
status: 308,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} else if (config.trailingSlash === 'never') {
|
} else if (config.trailingSlash === 'never') {
|
||||||
for (const route of routes) {
|
|
||||||
if (route.type !== 'page' || route.segments.length === 0) continue;
|
|
||||||
|
|
||||||
redirects.push({
|
redirects.push({
|
||||||
src: config.base + getMatchPattern(route.segments) + '/',
|
src: config.base + getMatchPattern(route.segments) + '/',
|
||||||
headers: { Location: config.base + getReplacePattern(route.segments) },
|
headers: { Location: config.base + getReplacePattern(route.segments) },
|
||||||
|
@ -78,6 +107,7 @@ export function getRedirects(routes: RouteData[], config: AstroConfig): VercelRo
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return redirects;
|
return redirects;
|
||||||
}
|
}
|
||||||
|
|
9
packages/integrations/vercel/test/fixtures/redirects/astro.config.mjs
vendored
Normal file
9
packages/integrations/vercel/test/fixtures/redirects/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import vercel from '@astrojs/vercel/static';
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
adapter: vercel({imageService: true}),
|
||||||
|
experimental: {
|
||||||
|
assets: true
|
||||||
|
}
|
||||||
|
});
|
9
packages/integrations/vercel/test/fixtures/redirects/package.json
vendored
Normal file
9
packages/integrations/vercel/test/fixtures/redirects/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "@test/astro-vercel-redirects",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/vercel": "workspace:*",
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
8
packages/integrations/vercel/test/fixtures/redirects/src/pages/index.astro
vendored
Normal file
8
packages/integrations/vercel/test/fixtures/redirects/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Testing</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
25
packages/integrations/vercel/test/fixtures/redirects/src/pages/team/articles/[...slug].astro
vendored
Normal file
25
packages/integrations/vercel/test/fixtures/redirects/src/pages/team/articles/[...slug].astro
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const posts = [
|
||||||
|
{ slug: 'one', data: {draft: false, title: 'One'} },
|
||||||
|
{ slug: 'two', data: {draft: false, title: 'Two'} }
|
||||||
|
];
|
||||||
|
return posts.map((post) => {
|
||||||
|
return {
|
||||||
|
params: { slug: post.slug },
|
||||||
|
props: { draft: post.data.draft, title: post.data.title },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const { slug } = Astro.params;
|
||||||
|
const { title } = Astro.props;
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{ title }</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{ title }</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
56
packages/integrations/vercel/test/redirects.test.js
Normal file
56
packages/integrations/vercel/test/redirects.test.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
|
||||||
|
describe('Redirects', () => {
|
||||||
|
/** @type {import('../../../astro/test/test-utils.js').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/redirects/',
|
||||||
|
redirects: {
|
||||||
|
'/one': '/',
|
||||||
|
'/two': '/',
|
||||||
|
'/three': {
|
||||||
|
status: 302,
|
||||||
|
destination: '/'
|
||||||
|
},
|
||||||
|
'/blog/[...slug]': '/team/articles/[...slug]',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getConfig() {
|
||||||
|
const json = await fixture.readFile('../.vercel/output/config.json');
|
||||||
|
const config = JSON.parse(json);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('define static routes', async () => {
|
||||||
|
const config = await getConfig();
|
||||||
|
|
||||||
|
const oneRoute = config.routes.find(r => r.src === '/\\/one');
|
||||||
|
expect(oneRoute.headers.Location).to.equal('/');
|
||||||
|
expect(oneRoute.status).to.equal(301);
|
||||||
|
|
||||||
|
const twoRoute = config.routes.find(r => r.src === '/\\/two');
|
||||||
|
expect(twoRoute.headers.Location).to.equal('/');
|
||||||
|
expect(twoRoute.status).to.equal(301);
|
||||||
|
|
||||||
|
const threeRoute = config.routes.find(r => r.src === '/\\/three');
|
||||||
|
expect(threeRoute.headers.Location).to.equal('/');
|
||||||
|
expect(threeRoute.status).to.equal(302);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defines dynamic routes', async () => {
|
||||||
|
const config = await getConfig();
|
||||||
|
|
||||||
|
const blogRoute = config.routes.find(r => r.src.startsWith('/\\/blog'));
|
||||||
|
expect(blogRoute).to.not.be.undefined;
|
||||||
|
expect(blogRoute.headers.Location.startsWith('/team/articles')).to.equal(true);
|
||||||
|
expect(blogRoute.status).to.equal(301);
|
||||||
|
});
|
||||||
|
});
|
|
@ -4868,6 +4868,15 @@ importers:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../../../astro
|
version: link:../../../../../astro
|
||||||
|
|
||||||
|
packages/integrations/vercel/test/fixtures/redirects:
|
||||||
|
dependencies:
|
||||||
|
'@astrojs/vercel':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../..
|
||||||
|
astro:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../../../astro
|
||||||
|
|
||||||
packages/integrations/vercel/test/fixtures/serverless-prerender:
|
packages/integrations/vercel/test/fixtures/serverless-prerender:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/vercel':
|
'@astrojs/vercel':
|
||||||
|
|
Loading…
Reference in a new issue