From 12df5de215c7bcfc520e6ffb1a204c50096ee984 Mon Sep 17 00:00:00 2001 From: Oscar Otero Date: Thu, 21 Dec 2023 21:06:35 +0100 Subject: [PATCH 1/3] redirects plugin --- core/file.ts | 6 + core/utils/lume_config.ts | 1 + middlewares/redirects.ts | 2 +- plugins/redirects.ts | 142 +++++ tests/__snapshots__/redirects.test.ts.snap | 615 +++++++++++++++++++++ tests/assets/redirects/page1.vto | 6 + tests/assets/redirects/page2.vto | 7 + tests/plugins.test.ts | 1 + tests/redirects.test.ts | 39 ++ 9 files changed, 818 insertions(+), 1 deletion(-) create mode 100644 plugins/redirects.ts create mode 100644 tests/__snapshots__/redirects.test.ts.snap create mode 100644 tests/assets/redirects/page1.vto create mode 100644 tests/assets/redirects/page2.vto create mode 100644 tests/redirects.test.ts diff --git a/core/file.ts b/core/file.ts index 384ce20be..bc59a409a 100644 --- a/core/file.ts +++ b/core/file.ts @@ -214,6 +214,12 @@ export interface Data extends RawData { /** The url of a page */ url: string; + /** + * The old url(s) of a page + * @see https://lume.land/plugins/redirects/ + */ + oldUrl?: string | string[]; + /** The basename of the page */ basename: string; diff --git a/core/utils/lume_config.ts b/core/utils/lume_config.ts index 6e5a8fd10..c203335ba 100644 --- a/core/utils/lume_config.ts +++ b/core/utils/lume_config.ts @@ -30,6 +30,7 @@ export const pluginNames = [ "prism", "pug", "reading_info", + "redirects", "relations", "relative_urls", "remark", diff --git a/middlewares/redirects.ts b/middlewares/redirects.ts index 34cedc59a..2fa46fc93 100644 --- a/middlewares/redirects.ts +++ b/middlewares/redirects.ts @@ -6,7 +6,7 @@ export interface Options { export interface Redirect { to: string; - code: 301 | 302 | 307 | 308 | 200; + code: 301 | 302 | 303 | 307 | 308 | 200; } /** Implements redirects */ diff --git a/plugins/redirects.ts b/plugins/redirects.ts new file mode 100644 index 000000000..9d6c7a12a --- /dev/null +++ b/plugins/redirects.ts @@ -0,0 +1,142 @@ +import { merge } from "../core/utils/object.ts"; +import { Page } from "../core/file.ts"; +import { log } from "../core/utils/log.ts"; + +import type Site from "../core/site.ts"; + +export interface Options { + /** The redirects output format */ + output?: "json" | "netlify" | "vercel" | "html" | OutputStrategy; + + /** The default status code to use */ + defaultStatus?: Status; +} + +type Status = 301 | 302 | 307 | 308; +type Redirect = [string, string, Status]; +type OutputStrategy = ( + redirects: Redirect[], + site: Site, +) => Promise | void; + +export const defaults: Options = { + output: "json", + defaultStatus: 301, +}; + +/** Export strategies */ + +const outputs: Record = { + async netlify(redirects: Redirect[], site: Site) { + const content = redirects.map(([from, to, code]) => `${from} ${to} ${code}`) + .join("\n"); + const page = await site.getOrCreatePage("_redirects"); + page.content = content; + }, + + async vercel(redirects: Redirect[], site: Site) { + const config = { + redirects: redirects.map(([source, destination, statusCode]) => ({ + source, + destination, + statusCode, + })), + }; + + const page = await site.getOrCreatePage("vercel.json"); + const content = JSON.parse(page.content as string | undefined || "{}"); + Object.assign(content, config); + page.content = JSON.stringify(content, null, 2); + }, + html(redirects: Redirect[], site: Site) { + for (const [url, to, statusCode] of redirects) { + const timeout = (statusCode === 301 || statusCode === 308) ? 0 : 1; + const content = ` + + + + Redirecting… + + + +

Redirecting…

+ Click here if you are not redirected. + +`; + const page = Page.create({ url, content }); + site.pages.push(page); + } + }, + json(redirects: Redirect[], site: Site) { + const obj = Object.fromEntries( + redirects.map(( + [from, to, code], + ) => [from, code === 301 ? to : { to, code }]), + ); + const page = Page.create({ + url: "_redirects.json", + content: JSON.stringify(obj, null, 2), + }); + site.pages.push(page); + }, +}; + +export default function (userOptions?: Options) { + const options = merge(defaults, userOptions); + + return (site: Site) => { + site.process("*", (pages) => { + const redirects: Redirect[] = []; + + pages.forEach((page) => { + const { url, oldUrl } = page.data; + + if (url && oldUrl) { + const oldUrls = Array.isArray(oldUrl) ? oldUrl : [oldUrl]; + + for (const old of oldUrls) { + const redirect = parseRedirection(url, old, options.defaultStatus); + if (redirect) { + redirects.push(redirect); + } + } + } + }); + + if (!redirects.length) { + return; + } + + const outputFn = typeof options.output === "string" + ? outputs[options.output] + : options.output; + + if (!outputFn) { + log.error(`[redirects] Invalid output format: ${options.output}`); + throw new Error(`Invalid output format: ${options.output}`); + } + + return outputFn(redirects, site); + }); + }; +} + +const validStatusCodes = [301, 302, 303, 307, 308]; + +function parseRedirection( + newUrl: string, + oldUrl: string, + defaultCode: Status, +): [string, string, Status] | undefined { + const [from, code] = oldUrl.split(/\s+/); + const parsedCode = code ? parseInt(code) : defaultCode; + + if (!validStatusCodes.includes(parsedCode)) { + log.error( + `Invalid status code for redirection from ${from} to ${newUrl} (${code}).`, + ); + return; + } + + return [from, newUrl, parsedCode as Status]; +} diff --git a/tests/__snapshots__/redirects.test.ts.snap b/tests/__snapshots__/redirects.test.ts.snap new file mode 100644 index 000000000..705c82042 --- /dev/null +++ b/tests/__snapshots__/redirects.test.ts.snap @@ -0,0 +1,615 @@ +export const snapshot = {}; + +snapshot[`redirects plugin 1`] = ` +{ + formats: [ + { + engines: 0, + ext: ".page.toml", + loader: [AsyncFunction: toml], + pageType: "page", + }, + { + engines: 1, + ext: ".page.ts", + loader: [AsyncFunction: module], + pageType: "page", + }, + { + engines: 1, + ext: ".page.js", + loader: [AsyncFunction: module], + pageType: "page", + }, + { + engines: 0, + ext: ".page.jsonc", + loader: [AsyncFunction: json], + pageType: "page", + }, + { + engines: 0, + ext: ".page.json", + loader: [AsyncFunction: json], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: json], + engines: 0, + ext: ".json", + loader: [AsyncFunction: json], + }, + { + dataLoader: [AsyncFunction: json], + engines: 0, + ext: ".jsonc", + loader: [AsyncFunction: json], + }, + { + engines: 1, + ext: ".md", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + engines: 1, + ext: ".markdown", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: module], + engines: 1, + ext: ".js", + loader: [AsyncFunction: module], + }, + { + dataLoader: [AsyncFunction: module], + engines: 1, + ext: ".ts", + loader: [AsyncFunction: module], + }, + { + engines: 1, + ext: ".vento", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + engines: 1, + ext: ".vto", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: toml], + engines: 0, + ext: ".toml", + loader: [AsyncFunction: toml], + }, + { + dataLoader: [AsyncFunction: yaml], + engines: 0, + ext: ".yaml", + loader: [AsyncFunction: yaml], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: yaml], + engines: 0, + ext: ".yml", + loader: [AsyncFunction: yaml], + pageType: "page", + }, + ], + src: [ + "/", + "/page1.vto", + "/page2.vto", + ], +} +`; + +snapshot[`redirects plugin 2`] = `[]`; + +snapshot[`redirects plugin 3`] = ` +[ + { + content: '{ + "/old-page2/": "/page2/", + "/other-old-page2/": { + "to": "/page2/", + "code": 307 + }, + "/old-page1/": "/page1/" +}', + data: { + basename: "_redirects", + content: '{ + "/old-page2/": "/page2/", + "/other-old-page2/": { + "to": "/page2/", + "code": 307 + }, + "/old-page1/": "/page1/" +}', + page: [ + "src", + "data", + ], + url: "_redirects.json", + }, + src: { + asset: true, + ext: "", + path: "", + remote: undefined, + }, + }, + { + content: " +Page 1", + data: { + basename: "page1", + children: "Page 1", + content: "Page 1", + date: [], + mergedKeys: [ + "tags", + ], + oldUrl: "/old-page1/", + page: [ + "src", + "data", + ], + paginate: "paginate", + search: [], + tags: "Array(0)", + url: "/page1/", + }, + src: { + asset: false, + ext: ".vto", + path: "/page1", + remote: undefined, + }, + }, + { + content: " +Page 2 +", + data: { + basename: "page2", + children: "Page 2 +", + content: "Page 2 +", + date: [], + mergedKeys: [ + "tags", + ], + oldUrl: "Array(2)", + page: [ + "src", + "data", + ], + paginate: "paginate", + search: [], + tags: "Array(0)", + url: "/page2/", + }, + src: { + asset: false, + ext: ".vto", + path: "/page2", + remote: undefined, + }, + }, +] +`; + +snapshot[`redirects plugin for netlify 1`] = ` +{ + formats: [ + { + engines: 0, + ext: ".page.toml", + loader: [AsyncFunction: toml], + pageType: "page", + }, + { + engines: 1, + ext: ".page.ts", + loader: [AsyncFunction: module], + pageType: "page", + }, + { + engines: 1, + ext: ".page.js", + loader: [AsyncFunction: module], + pageType: "page", + }, + { + engines: 0, + ext: ".page.jsonc", + loader: [AsyncFunction: json], + pageType: "page", + }, + { + engines: 0, + ext: ".page.json", + loader: [AsyncFunction: json], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: json], + engines: 0, + ext: ".json", + loader: [AsyncFunction: json], + }, + { + dataLoader: [AsyncFunction: json], + engines: 0, + ext: ".jsonc", + loader: [AsyncFunction: json], + }, + { + engines: 1, + ext: ".md", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + engines: 1, + ext: ".markdown", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: module], + engines: 1, + ext: ".js", + loader: [AsyncFunction: module], + }, + { + dataLoader: [AsyncFunction: module], + engines: 1, + ext: ".ts", + loader: [AsyncFunction: module], + }, + { + engines: 1, + ext: ".vento", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + engines: 1, + ext: ".vto", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: toml], + engines: 0, + ext: ".toml", + loader: [AsyncFunction: toml], + }, + { + dataLoader: [AsyncFunction: yaml], + engines: 0, + ext: ".yaml", + loader: [AsyncFunction: yaml], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: yaml], + engines: 0, + ext: ".yml", + loader: [AsyncFunction: yaml], + pageType: "page", + }, + ], + src: [ + "/", + "/page1.vto", + "/page2.vto", + ], +} +`; + +snapshot[`redirects plugin for netlify 2`] = `[]`; + +snapshot[`redirects plugin for netlify 3`] = ` +[ + { + content: "/old-page2/ /page2/ 301 +/other-old-page2/ /page2/ 307 +/old-page1/ /page1/ 301", + data: { + basename: "_redirects", + page: [ + "src", + "data", + ], + url: "/_redirects", + }, + src: { + asset: true, + ext: "", + path: "", + remote: undefined, + }, + }, + { + content: " +Page 1", + data: { + basename: "page1", + children: "Page 1", + content: "Page 1", + date: [], + mergedKeys: [ + "tags", + ], + oldUrl: "/old-page1/", + page: [ + "src", + "data", + ], + paginate: "paginate", + search: [], + tags: "Array(0)", + url: "/page1/", + }, + src: { + asset: false, + ext: ".vto", + path: "/page1", + remote: undefined, + }, + }, + { + content: " +Page 2 +", + data: { + basename: "page2", + children: "Page 2 +", + content: "Page 2 +", + date: [], + mergedKeys: [ + "tags", + ], + oldUrl: "Array(2)", + page: [ + "src", + "data", + ], + paginate: "paginate", + search: [], + tags: "Array(0)", + url: "/page2/", + }, + src: { + asset: false, + ext: ".vto", + path: "/page2", + remote: undefined, + }, + }, +] +`; + +snapshot[`redirects plugin for vercel 1`] = ` +{ + formats: [ + { + engines: 0, + ext: ".page.toml", + loader: [AsyncFunction: toml], + pageType: "page", + }, + { + engines: 1, + ext: ".page.ts", + loader: [AsyncFunction: module], + pageType: "page", + }, + { + engines: 1, + ext: ".page.js", + loader: [AsyncFunction: module], + pageType: "page", + }, + { + engines: 0, + ext: ".page.jsonc", + loader: [AsyncFunction: json], + pageType: "page", + }, + { + engines: 0, + ext: ".page.json", + loader: [AsyncFunction: json], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: json], + engines: 0, + ext: ".json", + loader: [AsyncFunction: json], + }, + { + dataLoader: [AsyncFunction: json], + engines: 0, + ext: ".jsonc", + loader: [AsyncFunction: json], + }, + { + engines: 1, + ext: ".md", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + engines: 1, + ext: ".markdown", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: module], + engines: 1, + ext: ".js", + loader: [AsyncFunction: module], + }, + { + dataLoader: [AsyncFunction: module], + engines: 1, + ext: ".ts", + loader: [AsyncFunction: module], + }, + { + engines: 1, + ext: ".vento", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + engines: 1, + ext: ".vto", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: toml], + engines: 0, + ext: ".toml", + loader: [AsyncFunction: toml], + }, + { + dataLoader: [AsyncFunction: yaml], + engines: 0, + ext: ".yaml", + loader: [AsyncFunction: yaml], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: yaml], + engines: 0, + ext: ".yml", + loader: [AsyncFunction: yaml], + pageType: "page", + }, + ], + src: [ + "/", + "/page1.vto", + "/page2.vto", + ], +} +`; + +snapshot[`redirects plugin for vercel 2`] = `[]`; + +snapshot[`redirects plugin for vercel 3`] = ` +[ + { + content: '{ + "redirects": [ + { + "source": "/old-page2/", + "destination": "/page2/", + "statusCode": 301 + }, + { + "source": "/other-old-page2/", + "destination": "/page2/", + "statusCode": 307 + }, + { + "source": "/old-page1/", + "destination": "/page1/", + "statusCode": 301 + } + ] +}', + data: { + basename: "vercel", + page: [ + "src", + "data", + ], + url: "/vercel.json", + }, + src: { + asset: true, + ext: "", + path: "", + remote: undefined, + }, + }, + { + content: " +Page 1", + data: { + basename: "page1", + children: "Page 1", + content: "Page 1", + date: [], + mergedKeys: [ + "tags", + ], + oldUrl: "/old-page1/", + page: [ + "src", + "data", + ], + paginate: "paginate", + search: [], + tags: "Array(0)", + url: "/page1/", + }, + src: { + asset: false, + ext: ".vto", + path: "/page1", + remote: undefined, + }, + }, + { + content: " +Page 2 +", + data: { + basename: "page2", + children: "Page 2 +", + content: "Page 2 +", + date: [], + mergedKeys: [ + "tags", + ], + oldUrl: "Array(2)", + page: [ + "src", + "data", + ], + paginate: "paginate", + search: [], + tags: "Array(0)", + url: "/page2/", + }, + src: { + asset: false, + ext: ".vto", + path: "/page2", + remote: undefined, + }, + }, +] +`; diff --git a/tests/assets/redirects/page1.vto b/tests/assets/redirects/page1.vto new file mode 100644 index 000000000..d97bcae77 --- /dev/null +++ b/tests/assets/redirects/page1.vto @@ -0,0 +1,6 @@ +--- +url: /page1/ +oldUrl: /old-page1/ +--- + +Page 1 \ No newline at end of file diff --git a/tests/assets/redirects/page2.vto b/tests/assets/redirects/page2.vto new file mode 100644 index 000000000..c86e3a0e8 --- /dev/null +++ b/tests/assets/redirects/page2.vto @@ -0,0 +1,7 @@ +--- +oldUrl: + - /old-page2/ + - /other-old-page2/ 307 +--- + +Page 2 diff --git a/tests/plugins.test.ts b/tests/plugins.test.ts index f34fb758f..7e9a21022 100644 --- a/tests/plugins.test.ts +++ b/tests/plugins.test.ts @@ -37,6 +37,7 @@ Deno.test("Plugins list in init", () => { "prism", "pug", "reading_info", + "redirects", "relations", "relative_urls", "remark", diff --git a/tests/redirects.test.ts b/tests/redirects.test.ts new file mode 100644 index 000000000..a68fea8f6 --- /dev/null +++ b/tests/redirects.test.ts @@ -0,0 +1,39 @@ +import { assertSiteSnapshot, build, getSite } from "./utils.ts"; +import redirects from "../plugins/redirects.ts"; + +Deno.test("redirects plugin", async (t) => { + const site = getSite({ + src: "redirects", + }); + + site.use(redirects()); + + await build(site); + await assertSiteSnapshot(t, site); +}); + +Deno.test("redirects plugin for netlify", async (t) => { + const site = getSite({ + src: "redirects", + }); + + site.use(redirects({ + output: "netlify", + })); + + await build(site); + await assertSiteSnapshot(t, site); +}); + +Deno.test("redirects plugin for vercel", async (t) => { + const site = getSite({ + src: "redirects", + }); + + site.use(redirects({ + output: "vercel", + })); + + await build(site); + await assertSiteSnapshot(t, site); +}); From d702ebfc94f5e72802db2d43b28d3eefcc337210 Mon Sep 17 00:00:00 2001 From: Oscar Otero Date: Thu, 21 Dec 2023 21:14:48 +0100 Subject: [PATCH 2/3] sort redirects, change default to html --- plugins/redirects.ts | 7 +- tests/__snapshots__/redirects.test.ts.snap | 344 +++++++++++++++++++-- tests/redirects.test.ts | 13 + 3 files changed, 337 insertions(+), 27 deletions(-) diff --git a/plugins/redirects.ts b/plugins/redirects.ts index 9d6c7a12a..97c075af3 100644 --- a/plugins/redirects.ts +++ b/plugins/redirects.ts @@ -20,12 +20,11 @@ type OutputStrategy = ( ) => Promise | void; export const defaults: Options = { - output: "json", + output: "html", defaultStatus: 301, }; -/** Export strategies */ - +/** Output strategies */ const outputs: Record = { async netlify(redirects: Redirect[], site: Site) { const content = redirects.map(([from, to, code]) => `${from} ${to} ${code}`) @@ -116,6 +115,8 @@ export default function (userOptions?: Options) { throw new Error(`Invalid output format: ${options.output}`); } + redirects.sort((a, b) => a[0].localeCompare(b[0])); + return outputFn(redirects, site); }); }; diff --git a/tests/__snapshots__/redirects.test.ts.snap b/tests/__snapshots__/redirects.test.ts.snap index 705c82042..48c64916c 100644 --- a/tests/__snapshots__/redirects.test.ts.snap +++ b/tests/__snapshots__/redirects.test.ts.snap @@ -115,29 +115,117 @@ snapshot[`redirects plugin 2`] = `[]`; snapshot[`redirects plugin 3`] = ` [ { - content: '{ - "/old-page2/": "/page2/", - "/other-old-page2/": { - "to": "/page2/", - "code": 307 + content: ' + + + + Redirecting… + + + +

Redirecting…

+ Click here if you are not redirected. + +', + data: { + basename: "old-page1", + content: ' + + + + Redirecting… + + + +

Redirecting…

+ Click here if you are not redirected. + +', + page: [ + "src", + "data", + ], + url: "/old-page1/", + }, + src: { + asset: true, + ext: "", + path: "", + remote: undefined, + }, }, - "/old-page1/": "/page1/" -}', + { + content: ' + + + + Redirecting… + + + +

Redirecting…

+ Click here if you are not redirected. + +', data: { - basename: "_redirects", - content: '{ - "/old-page2/": "/page2/", - "/other-old-page2/": { - "to": "/page2/", - "code": 307 + basename: "old-page2", + content: ' + + + + Redirecting… + + + +

Redirecting…

+ Click here if you are not redirected. + +', + page: [ + "src", + "data", + ], + url: "/old-page2/", + }, + src: { + asset: true, + ext: "", + path: "", + remote: undefined, + }, }, - "/old-page1/": "/page1/" -}', + { + content: ' + + + + Redirecting… + + + +

Redirecting…

+ Click here if you are not redirected. + +', + data: { + basename: "other-old-page2", + content: ' + + + + Redirecting… + + + +

Redirecting…

+ Click here if you are not redirected. + +', page: [ "src", "data", ], - url: "_redirects.json", + url: "/other-old-page2/", }, src: { asset: true, @@ -323,9 +411,9 @@ snapshot[`redirects plugin for netlify 2`] = `[]`; snapshot[`redirects plugin for netlify 3`] = ` [ { - content: "/old-page2/ /page2/ 301 -/other-old-page2/ /page2/ 307 -/old-page1/ /page1/ 301", + content: "/old-page1/ /page1/ 301 +/old-page2/ /page2/ 301 +/other-old-page2/ /page2/ 307", data: { basename: "_redirects", page: [ @@ -520,6 +608,11 @@ snapshot[`redirects plugin for vercel 3`] = ` { content: '{ "redirects": [ + { + "source": "/old-page1/", + "destination": "/page1/", + "statusCode": 301 + }, { "source": "/old-page2/", "destination": "/page2/", @@ -529,11 +622,6 @@ snapshot[`redirects plugin for vercel 3`] = ` "source": "/other-old-page2/", "destination": "/page2/", "statusCode": 307 - }, - { - "source": "/old-page1/", - "destination": "/page1/", - "statusCode": 301 } ] }', @@ -613,3 +701,211 @@ Page 2 }, ] `; + +snapshot[`redirects plugin for json 1`] = ` +{ + formats: [ + { + engines: 0, + ext: ".page.toml", + loader: [AsyncFunction: toml], + pageType: "page", + }, + { + engines: 1, + ext: ".page.ts", + loader: [AsyncFunction: module], + pageType: "page", + }, + { + engines: 1, + ext: ".page.js", + loader: [AsyncFunction: module], + pageType: "page", + }, + { + engines: 0, + ext: ".page.jsonc", + loader: [AsyncFunction: json], + pageType: "page", + }, + { + engines: 0, + ext: ".page.json", + loader: [AsyncFunction: json], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: json], + engines: 0, + ext: ".json", + loader: [AsyncFunction: json], + }, + { + dataLoader: [AsyncFunction: json], + engines: 0, + ext: ".jsonc", + loader: [AsyncFunction: json], + }, + { + engines: 1, + ext: ".md", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + engines: 1, + ext: ".markdown", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: module], + engines: 1, + ext: ".js", + loader: [AsyncFunction: module], + }, + { + dataLoader: [AsyncFunction: module], + engines: 1, + ext: ".ts", + loader: [AsyncFunction: module], + }, + { + engines: 1, + ext: ".vento", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + engines: 1, + ext: ".vto", + loader: [AsyncFunction: text], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: toml], + engines: 0, + ext: ".toml", + loader: [AsyncFunction: toml], + }, + { + dataLoader: [AsyncFunction: yaml], + engines: 0, + ext: ".yaml", + loader: [AsyncFunction: yaml], + pageType: "page", + }, + { + dataLoader: [AsyncFunction: yaml], + engines: 0, + ext: ".yml", + loader: [AsyncFunction: yaml], + pageType: "page", + }, + ], + src: [ + "/", + "/page1.vto", + "/page2.vto", + ], +} +`; + +snapshot[`redirects plugin for json 2`] = `[]`; + +snapshot[`redirects plugin for json 3`] = ` +[ + { + content: '{ + "/old-page1/": "/page1/", + "/old-page2/": "/page2/", + "/other-old-page2/": { + "to": "/page2/", + "code": 307 + } +}', + data: { + basename: "_redirects", + content: '{ + "/old-page1/": "/page1/", + "/old-page2/": "/page2/", + "/other-old-page2/": { + "to": "/page2/", + "code": 307 + } +}', + page: [ + "src", + "data", + ], + url: "_redirects.json", + }, + src: { + asset: true, + ext: "", + path: "", + remote: undefined, + }, + }, + { + content: " +Page 1", + data: { + basename: "page1", + children: "Page 1", + content: "Page 1", + date: [], + mergedKeys: [ + "tags", + ], + oldUrl: "/old-page1/", + page: [ + "src", + "data", + ], + paginate: "paginate", + search: [], + tags: "Array(0)", + url: "/page1/", + }, + src: { + asset: false, + ext: ".vto", + path: "/page1", + remote: undefined, + }, + }, + { + content: " +Page 2 +", + data: { + basename: "page2", + children: "Page 2 +", + content: "Page 2 +", + date: [], + mergedKeys: [ + "tags", + ], + oldUrl: "Array(2)", + page: [ + "src", + "data", + ], + paginate: "paginate", + search: [], + tags: "Array(0)", + url: "/page2/", + }, + src: { + asset: false, + ext: ".vto", + path: "/page2", + remote: undefined, + }, + }, +] +`; diff --git a/tests/redirects.test.ts b/tests/redirects.test.ts index a68fea8f6..74a2b797f 100644 --- a/tests/redirects.test.ts +++ b/tests/redirects.test.ts @@ -37,3 +37,16 @@ Deno.test("redirects plugin for vercel", async (t) => { await build(site); await assertSiteSnapshot(t, site); }); + +Deno.test("redirects plugin for json", async (t) => { + const site = getSite({ + src: "redirects", + }); + + site.use(redirects({ + output: "json", + })); + + await build(site); + await assertSiteSnapshot(t, site); +}); From ac8932324df4ca626ff51c24d5ad3fd89aa619a5 Mon Sep 17 00:00:00 2001 From: Oscar Otero Date: Fri, 26 Jan 2024 18:47:35 +0100 Subject: [PATCH 3/3] code style --- core/file.ts | 6 -- plugins/redirects.ts | 132 +++++++++++++++++++++++++------------------ 2 files changed, 78 insertions(+), 60 deletions(-) diff --git a/core/file.ts b/core/file.ts index bc59a409a..384ce20be 100644 --- a/core/file.ts +++ b/core/file.ts @@ -214,12 +214,6 @@ export interface Data extends RawData { /** The url of a page */ url: string; - /** - * The old url(s) of a page - * @see https://lume.land/plugins/redirects/ - */ - oldUrl?: string | string[]; - /** The basename of the page */ basename: string; diff --git a/plugins/redirects.ts b/plugins/redirects.ts index 97c075af3..ae74951d8 100644 --- a/plugins/redirects.ts +++ b/plugins/redirects.ts @@ -6,7 +6,7 @@ import type Site from "../core/site.ts"; export interface Options { /** The redirects output format */ - output?: "json" | "netlify" | "vercel" | "html" | OutputStrategy; + output?: "html" | "json" | "netlify" | "vercel" | OutputStrategy; /** The default status code to use */ defaultStatus?: Status; @@ -24,60 +24,12 @@ export const defaults: Options = { defaultStatus: 301, }; -/** Output strategies */ +/** Predefined output strategies */ const outputs: Record = { - async netlify(redirects: Redirect[], site: Site) { - const content = redirects.map(([from, to, code]) => `${from} ${to} ${code}`) - .join("\n"); - const page = await site.getOrCreatePage("_redirects"); - page.content = content; - }, - - async vercel(redirects: Redirect[], site: Site) { - const config = { - redirects: redirects.map(([source, destination, statusCode]) => ({ - source, - destination, - statusCode, - })), - }; - - const page = await site.getOrCreatePage("vercel.json"); - const content = JSON.parse(page.content as string | undefined || "{}"); - Object.assign(content, config); - page.content = JSON.stringify(content, null, 2); - }, - html(redirects: Redirect[], site: Site) { - for (const [url, to, statusCode] of redirects) { - const timeout = (statusCode === 301 || statusCode === 308) ? 0 : 1; - const content = ` - - - - Redirecting… - - - -

Redirecting…

- Click here if you are not redirected. - -`; - const page = Page.create({ url, content }); - site.pages.push(page); - } - }, - json(redirects: Redirect[], site: Site) { - const obj = Object.fromEntries( - redirects.map(( - [from, to, code], - ) => [from, code === 301 ? to : { to, code }]), - ); - const page = Page.create({ - url: "_redirects.json", - content: JSON.stringify(obj, null, 2), - }); - site.pages.push(page); - }, + html, + json, + netlify, + vercel, }; export default function (userOptions?: Options) { @@ -141,3 +93,75 @@ function parseRedirection( return [from, newUrl, parsedCode as Status]; } + +/** HTML redirect */ +function html(redirects: Redirect[], site: Site): void { + for (const [url, to, statusCode] of redirects) { + const timeout = (statusCode === 301 || statusCode === 308) ? 0 : 1; + const content = ` + + + + Redirecting… + + + +

Redirecting…

+ Click here if you are not redirected. + +`; + const page = Page.create({ url, content }); + site.pages.push(page); + } +} + +/** JSON redirect (to use with redirect middleware) */ +function json(redirects: Redirect[], site: Site): void { + const obj = Object.fromEntries( + redirects.map(( + [from, to, code], + ) => [from, code === 301 ? to : { to, code }]), + ); + const page = Page.create({ + url: "_redirects.json", + content: JSON.stringify(obj, null, 2), + }); + site.pages.push(page); +} + +/** Netlify redirect */ +async function netlify(redirects: Redirect[], site: Site): Promise { + const content = redirects.map(([from, to, code]) => `${from} ${to} ${code}`) + .join("\n"); + const page = await site.getOrCreatePage("_redirects"); + page.content = content; +} + +/** Vercel redirect */ +async function vercel(redirects: Redirect[], site: Site): Promise { + const config = { + redirects: redirects.map(([source, destination, statusCode]) => ({ + source, + destination, + statusCode, + })), + }; + + const page = await site.getOrCreatePage("vercel.json"); + const content = JSON.parse(page.content as string | undefined || "{}"); + Object.assign(content, config); + page.content = JSON.stringify(content, null, 2); +} + +/** Extends Data interface */ +declare global { + namespace Lume { + export interface Data { + /** + * The old url(s) of a page + * @see https://lume.land/plugins/redirects/ + */ + oldUrl?: string | string[]; + } + } +}