diff options
Diffstat (limited to 'packages/integrations/netlify')
11 files changed, 212 insertions, 130 deletions
diff --git a/packages/integrations/netlify/README.md b/packages/integrations/netlify/README.md index ec72f2a2c..cee5fa5c2 100644 --- a/packages/integrations/netlify/README.md +++ b/packages/integrations/netlify/README.md @@ -74,6 +74,30 @@ export default defineConfig({ }); ``` +### Static sites + +For static sites you usually don't need an adapter. However, if you use `redirects` configuration (experimental) in your Astro config, the Netlify adapter can be used to translate this to the proper `_redirects` format. + +```js +import { defineConfig } from 'astro/config'; +import netlify from '@astrojs/netlify/static'; + +export default defineConfig({ + adapter: netlify(), + + redirects: { + '/blog/old-post': '/blog/new-post' + }, + experimental: { + redirects: true + } +}); +``` + +Once you run `astro build` there will be a `dist/_redirects` file. Netlify will use that to properly route pages in production. + +> __Note__, you can still include a `public/_redirects` file for manual redirects. Any redirects you specify in the redirects config are appended to the end of your own. + ## Usage [Read the full deployment guide here.](https://docs.astro.build/en/guides/deploy/netlify/) diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json index 26ba3873f..64a106c84 100644 --- a/packages/integrations/netlify/package.json +++ b/packages/integrations/netlify/package.json @@ -37,6 +37,7 @@ "test": "npm run test-fn" }, "dependencies": { + "@astrojs/underscore-redirects": "^0.1.0", "@astrojs/webapi": "^2.2.0", "@netlify/functions": "^1.0.0", "esbuild": "^0.15.18" diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts index fd7fd5fed..510e560f1 100644 --- a/packages/integrations/netlify/src/index.ts +++ b/packages/integrations/netlify/src/index.ts @@ -1,2 +1,3 @@ export { netlifyEdgeFunctions } from './integration-edge-functions.js'; export { netlifyFunctions as default, netlifyFunctions } from './integration-functions.js'; +export { netlifyStatic } from './integration-static.js'; diff --git a/packages/integrations/netlify/src/integration-static.ts b/packages/integrations/netlify/src/integration-static.ts new file mode 100644 index 000000000..8814f9d2a --- /dev/null +++ b/packages/integrations/netlify/src/integration-static.ts @@ -0,0 +1,26 @@ +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:setup': ({ updateConfig }) => { + updateConfig({ + build: { + // Do not output HTML redirects because we are building a `_redirects` file. + redirects: false, + }, + }); + }, + 'astro:config:done': ({ config }) => { + _config = config; + }, + 'astro:build:done': async ({ dir, routes }) => { + await createRedirects(_config, routes, dir, '', 'static'); + } + } + }; +} diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts index 78a61a800..d452ada10 100644 --- a/packages/integrations/netlify/src/shared.ts +++ b/packages/integrations/netlify/src/shared.ts @@ -1,145 +1,25 @@ import type { AstroConfig, RouteData } from 'astro'; -import fs from 'fs'; - -type RedirectDefinition = { - dynamic: boolean; - input: string; - target: string; - weight: 0 | 1; - status: 200 | 404; -}; +import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects'; +import fs from 'node:fs'; export async function createRedirects( config: AstroConfig, routes: RouteData[], dir: URL, entryFile: string, - type: 'functions' | 'edge-functions' | 'builders' + type: 'functions' | 'edge-functions' | 'builders' | 'static' ) { - const _redirectsURL = new URL('./_redirects', dir); const kind = type ?? 'functions'; + const dynamicTarget = `/.netlify/${kind}/${entryFile}`; + const _redirectsURL = new URL('./_redirects', dir); - const definitions: RedirectDefinition[] = []; - - for (const route of routes) { - if (route.pathname) { - if (route.distURL) { - definitions.push({ - dynamic: false, - input: route.pathname, - target: prependForwardSlash(route.distURL.toString().replace(dir.toString(), '')), - status: 200, - weight: 1, - }); - } else { - definitions.push({ - dynamic: false, - input: route.pathname, - target: `/.netlify/${kind}/${entryFile}`, - status: 200, - weight: 1, - }); - - if (route.route === '/404') { - definitions.push({ - dynamic: true, - input: '/*', - target: `/.netlify/${kind}/${entryFile}`, - status: 404, - weight: 0, - }); - } - } - } else { - 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('/'); - - if (route.distURL) { - const target = - `${pattern}` + (config.build.format === 'directory' ? '/index.html' : '.html'); - definitions.push({ - dynamic: true, - input: pattern, - target, - status: 200, - weight: 1, - }); - } else { - definitions.push({ - dynamic: true, - input: pattern, - target: `/.netlify/${kind}/${entryFile}`, - status: 200, - weight: 1, - }); - } - } - } - - let _redirects = prettify(definitions); + const _redirects = createRedirectsFromAstroRoutes({ + config, routes, dir, dynamicTarget + }); + const content = _redirects.print(); // Always use appendFile() because the redirects file could already exist, // e.g. due to a `/public/_redirects` file that got copied to the output dir. // If the file does not exist yet, appendFile() automatically creates it. - await fs.promises.appendFile(_redirectsURL, _redirects, 'utf-8'); -} - -function prettify(definitions: RedirectDefinition[]) { - let minInputLength = 0, - minTargetLength = 0; - definitions.sort((a, b) => { - // Find the longest input, so we can format things nicely - if (a.input.length > minInputLength) { - minInputLength = a.input.length; - } - if (b.input.length > minInputLength) { - minInputLength = b.input.length; - } - - // Same for the target - if (a.target.length > minTargetLength) { - minTargetLength = a.target.length; - } - if (b.target.length > minTargetLength) { - minTargetLength = b.target.length; - } - - // Sort dynamic routes on top - return b.weight - a.weight; - }); - - let _redirects = ''; - // Loop over the definitions - definitions.forEach((defn, i) => { - // Figure out the number of spaces to add. We want at least 4 spaces - // after the input. This ensure that all targets line up together. - let inputSpaces = minInputLength - defn.input.length + 4; - let targetSpaces = minTargetLength - defn.target.length + 4; - _redirects += - (i === 0 ? '' : '\n') + - defn.input + - ' '.repeat(inputSpaces) + - defn.target + - ' '.repeat(Math.abs(targetSpaces)) + - defn.status; - }); - return _redirects; -} - -function prependForwardSlash(str: string) { - return str[0] === '/' ? str : '/' + str; + await fs.promises.appendFile(_redirectsURL, content, 'utf-8'); } diff --git a/packages/integrations/netlify/test/functions/redirects.test.js b/packages/integrations/netlify/test/functions/redirects.test.js new file mode 100644 index 000000000..8a6d36694 --- /dev/null +++ b/packages/integrations/netlify/test/functions/redirects.test.js @@ -0,0 +1,44 @@ +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': '/' + }, + experimental: { + redirects: true, + }, + }); + 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', + // 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', + '/', '/.netlify/functions/entry', '200', + + // A real route + '/team/articles/*', '/.netlify/functions/entry', '200', + ]); + }); +}); diff --git a/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/index.astro b/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/index.astro new file mode 100644 index 000000000..53e029f04 --- /dev/null +++ b/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/index.astro @@ -0,0 +1,6 @@ +<html> +<head><title>Testing</title></head> +<body> + <h1>Testing</h1> +</body> +</html> diff --git a/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/nope.astro b/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/nope.astro new file mode 100644 index 000000000..f48d767ee --- /dev/null +++ b/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/nope.astro @@ -0,0 +1,3 @@ +--- +return Astro.redirect('/'); +--- diff --git a/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/team/articles/[...slug].astro b/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/team/articles/[...slug].astro new file mode 100644 index 000000000..716d3bd5d --- /dev/null +++ b/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/team/articles/[...slug].astro @@ -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> diff --git a/packages/integrations/netlify/test/static/redirects.test.js b/packages/integrations/netlify/test/static/redirects.test.js new file mode 100644 index 000000000..0b153b31c --- /dev/null +++ b/packages/integrations/netlify/test/static/redirects.test.js @@ -0,0 +1,43 @@ +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(), + experimental: { + redirects: true, + }, + 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([ + '/two', '/', '302', + '/other', '/', '301', + '/nope', '/', '301', + + '/blog/*', '/team/articles/*/index.html', '301', + '/team/articles/*', '/team/articles/*/index.html', '200', + ]); + }); +}); diff --git a/packages/integrations/netlify/test/static/test-utils.js b/packages/integrations/netlify/test/static/test-utils.js new file mode 100644 index 000000000..02b5d2ad9 --- /dev/null +++ b/packages/integrations/netlify/test/static/test-utils.js @@ -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) + ), + }, + }, + }, + }); + }, + }, + }; +} |