├── .npmrc ├── .nuxtrc ├── playground ├── tsconfig.json ├── redirects.csv ├── server │ └── tsconfig.json ├── pages │ ├── some-other.vue │ ├── [generic].vue │ └── index.vue ├── app.vue ├── nuxt.config.ts └── package.json ├── tsconfig.json ├── .eslintrc ├── .eslintignore ├── test ├── fixtures │ └── basic │ │ ├── package.json │ │ ├── pages │ │ ├── some-other.vue │ │ ├── p-[slug].vue │ │ └── p-page-with-query.vue │ │ ├── app.vue │ │ ├── redirects.csv │ │ └── nuxt.config.ts └── basic.test.ts ├── .editorconfig ├── .gitignore ├── LICENSE ├── package.json ├── README.md ├── src ├── module.ts └── runtime │ └── redirectsMiddleware.global └── CHANGELOG.md /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | imports.autoImport=false 2 | typescript.includeWorkspace=true 3 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./playground/.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@nuxt/eslint-config"] 4 | } 5 | -------------------------------------------------------------------------------- /playground/redirects.csv: -------------------------------------------------------------------------------- 1 | code,from,to 2 | 301,/pages?q=some,/page-stoca 3 | 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | src/templates/redirectsMiddleware.global 4 | -------------------------------------------------------------------------------- /playground/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "basic", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /playground/pages/some-other.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/some-other.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /playground/pages/[generic].vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/basic/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/p-[slug].vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/p-page-with-query.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/basic/redirects.csv: -------------------------------------------------------------------------------- 1 | code,from,to 2 | 301,/some,/some-other 3 | 301,/t-page-with-query?q="some",/p-page-with-query 4 | 301,^.*t-(.*)$,/p-$1 5 | -------------------------------------------------------------------------------- /test/fixtures/basic/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import MyModule from '../../../src/module' 2 | 3 | export default defineNuxtConfig({ 4 | modules: [ 5 | MyModule 6 | ] 7 | }) 8 | -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | modules: ["../src/module"], 3 | redirects: { 4 | // alwaysRedirect: false, 5 | // trailingSlash: false, 6 | }, 7 | devtools: { enabled: true }, 8 | }); 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "my-module-playground", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "generate": "nuxi generate" 9 | }, 10 | "devDependencies": { 11 | "nuxt": "latest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode/* 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | !.vscode/extensions.json 42 | !.vscode/*.code-snippets 43 | 44 | # Intellij idea 45 | *.iml 46 | .idea 47 | 48 | # OSX 49 | .DS_Store 50 | .AppleDouble 51 | .LSOverride 52 | .AppleDB 53 | .AppleDesktop 54 | Network Trash Folder 55 | Temporary Items 56 | .apdisk 57 | -------------------------------------------------------------------------------- /test/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { fileURLToPath } from "node:url"; 3 | import { setup, $fetch } from "@nuxt/test-utils"; 4 | 5 | describe("ssr", async () => { 6 | await setup({ 7 | rootDir: fileURLToPath(new URL("./fixtures/basic", import.meta.url)), 8 | }); 9 | 10 | it("redirects to /some-other page when visiting /some", async () => { 11 | // Get response to a server-rendered page with `$fetch`. 12 | const html = await $fetch("/some"); 13 | expect(html).toContain("
some other
"); 14 | }); 15 | 16 | it("redirects to /p-page page when visiting /t-page", async () => { 17 | // Get response to a server-rendered page with `$fetch`. 18 | const html = await $fetch("/t-page"); 19 | expect(html).toContain("p-page"); 20 | }); 21 | 22 | it("redirects to /p-page-with-query when visiting /t-page-with-query?q=some", async () => { 23 | const html = await $fetch("/t-page-with-query?q=some") 24 | expect(html).toContain("p-page-with-query") 25 | }) 26 | }); 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 atoms - a retex brand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@atoms-studio/nuxt-redirects", 3 | "version": "1.2.2", 4 | "description": "Massive redirect rules module handler", 5 | "repository": "@atoms-studio/nuxt-redirects", 6 | "private": false, 7 | "license": "MIT", 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/types.d.ts", 12 | "import": "./dist/module.mjs", 13 | "require": "./dist/module.cjs" 14 | } 15 | }, 16 | "main": "./dist/module.cjs", 17 | "types": "./dist/types.d.ts", 18 | "files": [ 19 | "dist" 20 | ], 21 | "scripts": { 22 | "prepack": "nuxt-module-build", 23 | "dev": "nuxi dev playground", 24 | "dev:analyze": "nuxi analyze playground", 25 | "dev:build": "nuxi build playground", 26 | "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground", 27 | "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags", 28 | "lint": "eslint .", 29 | "test": "vitest run", 30 | "test:watch": "vitest watch" 31 | }, 32 | "dependencies": { 33 | "@nuxt/kit": "^3.6.5", 34 | "consola": "^3.2.3", 35 | "zod": "^3.21.4", 36 | "zod-csv": "^0.0.3" 37 | }, 38 | "devDependencies": { 39 | "@nuxt/devtools": "latest", 40 | "@nuxt/eslint-config": "^0.1.1", 41 | "@nuxt/module-builder": "^0.4.0", 42 | "@nuxt/schema": "^3.6.5", 43 | "@nuxt/test-utils": "^3.6.5", 44 | "@types/node": "^18.17.0", 45 | "changelogen": "^0.5.4", 46 | "eslint": "^8.45.0", 47 | "nuxt": "^3.6.5", 48 | "vitest": "^0.33.0" 49 | } 50 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @atoms-studio/nuxt-redirects 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![License][license-src]][license-href] 6 | [![Nuxt][nuxt-src]][nuxt-href] 7 | 8 | A Nuxt module that allows for redirect management using a simple CSV file, it leverages [zod-csv](https://github.com/bartoszgolebiowski/zod-csv) 9 | under the hood for validation, handling both punctual and regex redirects. An example CSV file can be found [here](/playground/redirects.csv). 10 | 11 | ## Quick Setup 12 | 13 | 1. Add the `@atoms-studio/nuxt-redirects` dependency to your project. 14 | 15 | ```bash 16 | # Using pnpm 17 | pnpm add -D @atoms-studio/nuxt-redirects 18 | 19 | # Using yarn 20 | yarn add --dev @atoms-studio/nuxt-redirects 21 | 22 | # Using npm 23 | npm install --save-dev @atoms-studio/nuxt-redirects 24 | ``` 25 | 26 | 2. Add `@atoms-studio/nuxt-redirects` to the `modules` section of your `nuxt.config.ts` file. 27 | 28 | ```js 29 | export default defineNuxtConfig({ 30 | modules: ["@atoms-studio/nuxt-redirects"], 31 | }); 32 | ``` 33 | 34 | That's it! You can now start using it in your Nuxt app. ✨ 35 | 36 | ## Development 37 | 38 | ```bash 39 | # Install dependencies 40 | npm install 41 | 42 | # Generate type stubs 43 | npm run dev:prepare 44 | 45 | # Develop with the playground 46 | npm run dev 47 | 48 | # Build the playground 49 | npm run dev:build 50 | 51 | # Run ESLint 52 | npm run lint 53 | 54 | # Run Vitest 55 | npm run test 56 | npm run test:watch 57 | 58 | # Release new version 59 | npm run release 60 | ``` 61 | 62 | ## License 63 | 64 | Licensed under the [MIT License](LICENSE). 65 | 66 | [npm-version-src]: https://img.shields.io/npm/v/@atoms-studio/nuxt-redirects/latest.svg?style=flat&colorA=18181B&colorB=28CF8D 67 | [npm-version-href]: https://npmjs.com/package/@atoms-studio/nuxt-redirects 68 | [npm-downloads-src]: https://img.shields.io/npm/dm/@atoms-studio/nuxt-redirects.svg?style=flat&colorA=18181B&colorB=28CF8D 69 | [npm-downloads-href]: https://npmjs.com/package/@atoms-studio/nuxt-redirects 70 | [license-src]: https://img.shields.io/npm/l/@atoms-studio/nuxt-redirects.svg?style=flat&colorA=18181B&colorB=28CF8D 71 | [license-href]: https://npmjs.com/package/@atoms-studio/nuxt-redirects 72 | [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js 73 | [nuxt-href]: https://nuxt.com 74 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineNuxtModule, 3 | createResolver, 4 | addTemplate, 5 | addRouteMiddleware, 6 | } from "@nuxt/kit"; 7 | import { consola } from "consola"; 8 | import { zcsv, parseCSVContent } from "zod-csv"; 9 | import { z } from "zod"; 10 | import { readFile } from "fs/promises"; 11 | 12 | // Module options TypeScript interface definition 13 | export interface ModuleOptions { 14 | csv: string; 15 | trailingSlash: boolean; 16 | alwaysRedirect: boolean; 17 | } 18 | 19 | export default defineNuxtModule({ 20 | meta: { 21 | name: "nuxt-redirects", 22 | configKey: "redirects", 23 | }, 24 | // Default configuration options of the Nuxt module 25 | defaults: { 26 | csv: "redirects.csv", 27 | trailingSlash: false, 28 | alwaysRedirect: false, 29 | }, 30 | async setup(options, nuxt) { 31 | const resolver = createResolver(import.meta.url); 32 | 33 | const redirectsPath = await resolver.resolvePath(options.csv, { 34 | cwd: nuxt.options.srcDir, 35 | }); 36 | 37 | // schema 38 | const redirectsSchema = z.object({ 39 | code: zcsv.number(), 40 | from: zcsv.string(), 41 | to: zcsv.string(), 42 | }); 43 | // reading csv 44 | const csv = await readFile(redirectsPath, { encoding: "utf8" }).catch( 45 | () => { 46 | throw new Error("Error reading redirects csv file"); 47 | }, 48 | ); 49 | const parsedCsv = parseCSVContent(csv, redirectsSchema); 50 | 51 | // get valid rows and write them as a template inside nuxt dir 52 | // you can access it later importing redirects from '#build/nuxt-redirects/redirects' 53 | addTemplate({ 54 | filename: "nuxt-redirects/redirects.ts", 55 | write: true, 56 | getContents: () => { 57 | return ` 58 | export const redirects = ${JSON.stringify(parsedCsv.validRows)} as const 59 | `; 60 | }, 61 | }); 62 | 63 | const { dst } = addTemplate({ 64 | filename: "nuxt-redirects/redirectsMiddleware.ts", 65 | write: true, 66 | options, 67 | src: await resolver.resolvePath("./runtime/redirectsMiddleware.global"), 68 | }); 69 | 70 | addRouteMiddleware({ 71 | name: "redirectsMiddleware", 72 | path: dst!, 73 | global: true, 74 | }); 75 | 76 | // @ts-ignore 77 | consola.info(`Added ${parsedCsv.validRows.length} redirection rules`); 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## v1.2.2 5 | 6 | [compare changes](https://undefined/undefined/compare/v1.2.1...v1.2.2) 7 | 8 | ## v1.2.1 9 | 10 | [compare changes](https://undefined/undefined/compare/v1.2.0...v1.2.1) 11 | 12 | ### 🩹 Fixes 13 | 14 | - Regex redirects corrected (d11df2e) 15 | 16 | ### ❤️ Contributors 17 | 18 | - Matteo Rigoni 19 | 20 | ## v1.2.0 21 | 22 | [compare changes](https://undefined/undefined/compare/v1.1.3...v1.2.0) 23 | 24 | ### 🚀 Enhancements 25 | 26 | - Bring query alongside redirection (91f2b65) 27 | 28 | ### ❤️ Contributors 29 | 30 | - Matteo Rigoni 31 | 32 | ## v1.1.3 33 | 34 | [compare changes](https://undefined/undefined/compare/v1.1.2...v1.1.3) 35 | 36 | ### 🩹 Fixes 37 | 38 | - **redirection:** Fixed excluding query from url (36e12bf) 39 | 40 | ### ❤️ Contributors 41 | 42 | - Matteo Rigoni 43 | 44 | ## v1.1.2 45 | 46 | [compare changes](https://undefined/undefined/compare/v1.1.1...v1.1.2) 47 | 48 | ### 🩹 Fixes 49 | 50 | - Do not include redirects in client bundle if its server only redirection (fe18f60) 51 | 52 | ### ❤️ Contributors 53 | 54 | - Matteo Rigoni 55 | 56 | ## v1.1.1 57 | 58 | [compare changes](https://undefined/undefined/compare/v1.1.0...v1.1.1) 59 | 60 | ### 🏡 Chore 61 | 62 | - Added license (8c25042) 63 | - Updated readme (6ecd4af) 64 | - Update README.md (cedabcd) 65 | - Fix typo in README.md (462f7e6) 66 | 67 | ### ❤️ Contributors 68 | 69 | - Matteo Rigoni ([@Rigo-m](http://github.com/Rigo-m)) 70 | - Gigantino 71 | 72 | ## v1.1.0 73 | 74 | [compare changes](https://undefined/undefined/compare/v1.0.3...v1.1.0) 75 | 76 | ### 🚀 Enhancements 77 | 78 | - **test:** Added basic testing with a punctual and a regex redirection (e9f6a95) 79 | 80 | ### ❤️ Contributors 81 | 82 | - Matteo Rigoni 83 | 84 | ## v1.0.3 85 | 86 | [compare changes](https://undefined/undefined/compare/v1.0.2...v1.0.3) 87 | 88 | ### 🩹 Fixes 89 | 90 | - **example:** Fixed example regex, it should have a forward slash at the beginning (f77fe3c) 91 | - Redirect mixin (0cb8a2f) 92 | 93 | ### ❤️ Contributors 94 | 95 | - Matteo Rigoni 96 | 97 | ## v1.0.2 98 | 99 | [compare changes](https://undefined/undefined/compare/v1.0.1...v1.0.2) 100 | 101 | ### 🩹 Fixes 102 | 103 | - **build:** Build fix and release (903c777) 104 | - **redirection:** Accounted for trailing slash while matching regex (b1cda4a) 105 | 106 | ### ❤️ Contributors 107 | 108 | - Matteo Rigoni 109 | 110 | ## v1.0.1 111 | 112 | 113 | ### 🩹 Fixes 114 | 115 | - **package.json:** Added correct name (b01a479) 116 | 117 | ### 🏡 Chore 118 | 119 | - First commit (aec09d3) 120 | 121 | ### ❤️ Contributors 122 | 123 | - Matteo Rigoni 124 | 125 | -------------------------------------------------------------------------------- /src/runtime/redirectsMiddleware.global: -------------------------------------------------------------------------------- 1 | import { defineNuxtRouteMiddleware, navigateTo } from "nuxt/app"; 2 | 3 | export default defineNuxtRouteMiddleware(async (to, _) => { 4 | if (to.fullPath.includes("robots.txt")) return 5 | const getTypedRedirects = (redirects: any) => 6 | redirects as Array<{ code: number; from: string; to: string }>; 7 | const isRegex = (s: string) => s.startsWith("^") && s.endsWith("$"); 8 | 9 | // if redirection is only server side, skip redirection entirely, else load redirection rules 10 | let redirects: typeof import("./redirects")["redirects"] | null = null 11 | if (!<% print(options.alwaysRedirect) %> && !process.server) { 12 | return 13 | } else { 14 | const { redirects: r } = await import("./redirects") 15 | redirects = r 16 | } 17 | 18 | // divide redirection rules in punctual redirects and regex redirection rules 19 | const typedRedirects = getTypedRedirects(redirects); 20 | 21 | 22 | const punctualRedirects = typedRedirects.filter( 23 | (redirectionRule) => !isRegex(redirectionRule.from) 24 | ); 25 | 26 | const regexRedirects = typedRedirects.filter((redirectionRule) => 27 | isRegex(redirectionRule.from) 28 | ); 29 | 30 | const hasTrailingSlash = (path: string) => path.endsWith("/") 31 | const removeTrailingSlash = (path: string) => hasTrailingSlash(path) ? path.slice(0, -1) : path 32 | const addTrailingSlash = (path: string) => { 33 | if (<% print(options.trailingSlash) %>) { 34 | if (!hasTrailingSlash(path)) { 35 | return path + "/" 36 | } 37 | } 38 | 39 | return path 40 | } 41 | const getQueriesFromPath = (path: string) => { 42 | return path.split("?")?.[1]?.split("&") ?? [] 43 | } 44 | 45 | const joinQueriesAndPath = (path: string, queries: string[]) => { 46 | if (!queries.length) return path 47 | return path + "?" + queries.join('&') 48 | } 49 | 50 | const getUrl = (path: string) => { 51 | return path.split("?")[0] 52 | } 53 | 54 | const getQuery = (path: string) => { 55 | return path.split("?")?.[1] ?? '' 56 | } 57 | 58 | // redirection only happens server side 59 | if (process.server || <% print(options.alwaysRedirect) %>) { 60 | // removing = and trailingSlash, ignore query 61 | let path = removeTrailingSlash(to.fullPath.endsWith("=") 62 | ? to.fullPath.slice(0, -1) 63 | : to.fullPath); 64 | 65 | const query = getQuery(path) 66 | const urlPath = getUrl(path) 67 | 68 | if (to.fullPath !== "/") { 69 | // handling punctual redirects 70 | const punctualRedirection = punctualRedirects.find( 71 | (r) => { 72 | const from = r.from 73 | const fromPath = getUrl(from) 74 | const fromQuery = getQuery(from) 75 | if (fromQuery !== '') { 76 | // check validity of pathQuery 77 | const qArr = query.split("&") 78 | const fromQArr = fromQuery.split("&") 79 | 80 | const queryValidated = fromQArr.every(q => qArr.includes(q)) 81 | 82 | return queryValidated && removeTrailingSlash(fromPath) === urlPath 83 | } 84 | return removeTrailingSlash(fromPath) === urlPath 85 | } 86 | ); 87 | 88 | if (typeof punctualRedirection !== "undefined") { 89 | // join request and redirection queries 90 | const queryParams = [ 91 | ...getQueriesFromPath(to.fullPath), 92 | ...getQueriesFromPath(punctualRedirection.to) 93 | ] 94 | const uri = joinQueriesAndPath(addTrailingSlash(punctualRedirection.to), queryParams) 95 | return navigateTo(uri, { 96 | redirectCode: punctualRedirection.code, 97 | external: true, 98 | }); 99 | } 100 | // punctual redirects had the priority, now we look for any regex redirections 101 | let regexRedirection: { code: number, path: string } | undefined 102 | regexRedirects.forEach((r) => { 103 | const regex = new RegExp(r.from); 104 | if (regex.test(path)) { 105 | const toPath = addTrailingSlash(urlPath.replace(regex, r.to)); 106 | regexRedirection = { code: r.code, path: toPath } 107 | } 108 | }); 109 | 110 | if (typeof regexRedirection !== 'undefined') { 111 | // join request and redirection queries 112 | const queryParams = [ 113 | ...getQueriesFromPath(to.fullPath), 114 | ...getQueriesFromPath(regexRedirection.path) 115 | ] 116 | const uri = joinQueriesAndPath(addTrailingSlash(regexRedirection.path), queryParams) 117 | 118 | return navigateTo(uri, { 119 | redirectCode: regexRedirection.code, 120 | external: true, 121 | }); 122 | } 123 | 124 | 125 | // trailing-slash 126 | if (<% print(options.trailingSlash) %>) { 127 | const originalSplittedPath = to.fullPath.split("?") 128 | const originalPath = originalSplittedPath[0] 129 | 130 | if (!hasTrailingSlash(originalPath)) { 131 | originalSplittedPath[0] = addTrailingSlash(originalPath) 132 | return navigateTo(originalSplittedPath.join("?"), { 133 | redirectCode: 301, 134 | external: true 135 | }) 136 | } 137 | } 138 | } 139 | } 140 | }); 141 | 142 | --------------------------------------------------------------------------------