diff --git a/cypress/e2e/theme-yun/redirect.spec.ts b/cypress/e2e/theme-yun/redirect.spec.ts new file mode 100644 index 000000000..79371d64d --- /dev/null +++ b/cypress/e2e/theme-yun/redirect.spec.ts @@ -0,0 +1,25 @@ +context('Client Redirect', { + baseUrl: Cypress.env('theme-yun'), +}, () => { + beforeEach(() => { + cy.visit('/') + }) + + it('/redirect/old1', () => { + cy.visit('/redirect/old1') + + cy.url().should('eq', `${Cypress.env('theme-yun')}posts/redirect`) + }) + + it('/redirect/old2', () => { + cy.visit('/redirect/old2') + + cy.url().should('eq', `${Cypress.env('theme-yun')}posts/redirect`) + }) + + it('/foo', () => { + cy.visit('/foo') + + cy.url().should('eq', `${Cypress.env('theme-yun')}about`) + }) +}) diff --git a/demo/yun/.valaxy/typed-router.d.ts b/demo/yun/.valaxy/typed-router.d.ts index 2c530aaa5..258994b55 100644 --- a/demo/yun/.valaxy/typed-router.d.ts +++ b/demo/yun/.valaxy/typed-router.d.ts @@ -87,6 +87,7 @@ declare module 'vue-router/auto/routes' { '/posts/lots-of-images': RouteRecordInfo<'/posts/lots-of-images', '/posts/lots-of-images', Record, Record>, '/posts/markdown': RouteRecordInfo<'/posts/markdown', '/posts/markdown', Record, Record>, '/posts/post-updated': RouteRecordInfo<'/posts/post-updated', '/posts/post-updated', Record, Record>, + '/posts/redirect': RouteRecordInfo<'/posts/redirect', '/posts/redirect', Record, Record>, '/posts/test': RouteRecordInfo<'/posts/test', '/posts/test', Record, Record>, '/posts/test-images': RouteRecordInfo<'/posts/test-images', '/posts/test-images', Record, Record>, '/posts/test-tags': RouteRecordInfo<'/posts/test-tags', '/posts/test-tags', Record, Record>, diff --git a/demo/yun/pages/posts/redirect.md b/demo/yun/pages/posts/redirect.md new file mode 100644 index 000000000..006831c38 --- /dev/null +++ b/demo/yun/pages/posts/redirect.md @@ -0,0 +1,6 @@ +--- +title: 重定向 +from: + - /redirect/old1 + - /redirect/old2 +--- diff --git a/demo/yun/site.config.ts b/demo/yun/site.config.ts index 446ed8d1e..352fc961f 100644 --- a/demo/yun/site.config.ts +++ b/demo/yun/site.config.ts @@ -145,11 +145,14 @@ export default defineSiteConfig({ enable: true, }, - redirects: [ - { - from: '/foo', - to: '/about', - }, - ], + redirects: { + useVueRouter: true, + rules: [ + { + from: '/foo', + to: '/about', + }, + ], + }, }) diff --git a/docs/pages/guide/config/index.md b/docs/pages/guide/config/index.md index 2e9936c15..864353ac3 100644 --- a/docs/pages/guide/config/index.md +++ b/docs/pages/guide/config/index.md @@ -575,56 +575,98 @@ Examples can be found in [Partial Content Encryption](/examples/partial-content- ### Client Redirects {lang="en"} +```ts +interface Redirects { + // https://router.vuejs.org/guide/essentials/redirect-and-alias.html + // Whether to use VueRouter, default is true + useVueRouter?: boolean + rules?: RedirectRule[] +} +interface RedirectRule { + // Redirect original route + from: string | string[] + // Redirect target route + to: string +} +``` + ::: zh-CN -这会生成额外的 HTML 页面,用与跳转到 valaxy 中已有的页面。 +示例: ::: ::: en -This will generate additional HTML pages, used to jump to the valaxy's existing pages. +For example: ::: -::: tip - -
-客户端重定向只在 SSG build 时启用 -
+```ts +// site.config.ts +export default defineSiteConfig({ + redirects: { + useVueRouter: true, + rules: [ + { + from: ['/foo', '/bar'], + to: '/about', + }, + { + from: '/v1/about', + to: '/about', + }, + ] + }, +}) +``` -
-Client redirects will only be enabled in SSG build -
+::: zh-CN +`/foo`, `/bar`, `/v1/about` 这些路由会被重定向到 `/about`。 +::: +::: en +`/foo`, `/bar`, `/v1/about` these routes will be redirected to `/about`。 ::: ::: zh-CN -例如: +你也可以在 Front Matter 中配置: ::: ::: en -For example: +You can also set it in the Front Matter: ::: -```ts -// site.config.ts -export default defineSiteConfig({ - redirects: [ - { - from: ['/foo', '/bar'], - to: '/about', - }, - { - from: '/v1/about', - to: '/about', - }, - ], -}) +```md + +--- +from: + - /redirect/old1 + - /redirect/old2 +--- +``` + +```md + +--- +from: /v1/redirect +--- ``` ::: zh-CN -`/foo`, `/bar`, `/v1/about` 这些路由会被重定向到 `/about`。 +`/redirect/old1`, `/redirect/old2`, `/v1/redirect` 这些路由会被重定向到 `/posts/redirect`。 ::: ::: en -`/foo`, `/bar`, `/v1/about` these routes will be redirected to `/about`。 +`/redirect/old1`, `/redirect/old2`, `/v1/redirect` these routes will be redirected to `/posts/redirect`。 +::: + +::: tip + +
+在 SSG 构建时,如果 useVueRouter 为 false,则会为每一个源路由生成一个 html 文件 +
+ +
+When building SSG, if useVueRouter is false, an html file will be generated for each original route +
+ ::: ### 图片预览(Medium Zoom) {lang="zh-CN"} diff --git a/packages/valaxy/client/main.ts b/packages/valaxy/client/main.ts index 0db5af050..904694c51 100644 --- a/packages/valaxy/client/main.ts +++ b/packages/valaxy/client/main.ts @@ -21,6 +21,8 @@ import 'uno.css' import setupMain from './setup/main' +const valaxyConfig = initValaxyConfig() + /** * register global components * @param ctx @@ -29,9 +31,13 @@ export function registerComponents(ctx: ViteSSGContext) { ctx.app.component('AppLink', AppLink) } +const { redirectRoutes, useVueRouter } = valaxyConfig.value.runtimeConfig.redirects +if (useVueRouter) + routes.push(...redirectRoutes) + // fix chinese path routes.forEach((i) => { - i.children?.forEach((j) => { + i?.children?.forEach((j) => { j.path = encodeURI(j.path) }) }) @@ -66,10 +72,10 @@ export const createApp = ViteSSG( (ctx) => { // app-level provide const { app } = ctx - const config = initValaxyConfig() - app.provide(valaxyConfigSymbol, config) + + app.provide(valaxyConfigSymbol, valaxyConfig) registerComponents(ctx) - setupMain(ctx, config) + setupMain(ctx, valaxyConfig) }, ) diff --git a/packages/valaxy/node/build.ts b/packages/valaxy/node/build.ts index 583f8984c..f39ad07d0 100644 --- a/packages/valaxy/node/build.ts +++ b/packages/valaxy/node/build.ts @@ -76,12 +76,14 @@ export async function postProcessForSSG(options: ResolvedValaxyOptions) { } } - await generateClientRedirects(options) + if (!options.config.siteConfig.redirects?.useVueRouter) + await generateClientRedirects(options) } export async function generateClientRedirects(options: ResolvedValaxyOptions) { + consola.info('generate client redirects...') const outputPath = resolve(options.userRoot, 'dist') - const redirectRules = collectRedirects(options.config.siteConfig?.redirects ?? []) + const redirectRules = collectRedirects(options.redirects) const task = redirectRules.map(async (rule) => { const fromPath = join(outputPath, `${rule.from}.html`) diff --git a/packages/valaxy/node/config/site.ts b/packages/valaxy/node/config/site.ts index 933512139..eca1ddc9e 100644 --- a/packages/valaxy/node/config/site.ts +++ b/packages/valaxy/node/config/site.ts @@ -97,6 +97,11 @@ export const defaultSiteConfig: SiteConfig = { salt: webcrypto.getRandomValues(new Uint8Array(16)), iv: webcrypto.getRandomValues(new Uint8Array(16)), }, + + redirects: { + useVueRouter: true, + rules: [], + }, } /** diff --git a/packages/valaxy/node/config/valaxy.ts b/packages/valaxy/node/config/valaxy.ts index bfc4bab27..caa260441 100644 --- a/packages/valaxy/node/config/valaxy.ts +++ b/packages/valaxy/node/config/valaxy.ts @@ -26,7 +26,13 @@ export const defaultValaxyConfig: ValaxyNodeConfig = { // markdown: { // excerpt: '', // }, - runtimeConfig: { addons: {} }, + runtimeConfig: { + addons: {}, + redirects: { + useVueRouter: true, + redirectRoutes: [], + }, + }, modules: { rss: { diff --git a/packages/valaxy/node/options.ts b/packages/valaxy/node/options.ts index 5fbf8bb23..8fa23e26f 100644 --- a/packages/valaxy/node/options.ts +++ b/packages/valaxy/node/options.ts @@ -8,7 +8,7 @@ import { ensureSuffix, uniq } from '@antfu/utils' import defu from 'defu' import { blue, cyan, magenta, yellow } from 'picocolors' import consola from 'consola' -import type { DefaultTheme, RuntimeConfig } from 'valaxy/types' +import type { DefaultTheme, RedirectItem, RuntimeConfig } from 'valaxy/types' import { resolveImportPath } from './utils' import { defaultValaxyConfig, @@ -23,6 +23,7 @@ import { parseAddons } from './utils/addons' import { getThemeRoot } from './utils/theme' import { resolveSiteConfig } from './config/site' import { countPerformanceTime } from './utils/performance' +import { collectRedirects } from './utils/clientRedirects' // for cli entry export interface ValaxyEntryOptions { @@ -88,6 +89,10 @@ export interface ResolvedValaxyOptions { * Record */ addons: ValaxyAddonResolver[] + /** + * Collect redirect rule + */ + redirects: RedirectItem[] } export interface ValaxyServerOptions { @@ -137,6 +142,10 @@ export async function processValaxyOptions(valaxyOptions: ResolvedValaxyOptions, ...config, runtimeConfig: { addons: {}, + redirects: { + useVueRouter: true, + redirectRoutes: [], + }, }, } valaxyOptions.addons = addons @@ -190,6 +199,8 @@ export async function resolveOptions( const { config: themeConfig, configFile: themeConfigFile } = resolvedTheme + const redirects = collectRedirects(siteConfig.redirects?.rules) + // merge with valaxy userValaxyConfig = defu({ siteConfig }, { themeConfig }, userValaxyConfig) @@ -213,13 +224,20 @@ export async function resolveOptions( theme, config: { ...userValaxyConfig, - runtimeConfig: { addons: {} }, + runtimeConfig: { + addons: {}, + redirects: { + useVueRouter: true, + redirectRoutes: [], + }, + }, }, configFile: configFile || '', siteConfigFile: siteConfigFile || '', themeConfigFile: themeConfigFile || '', pages: pages.sort(), addons: [], + redirects, } debug(valaxyOptions) diff --git a/packages/valaxy/node/plugins/valaxy.ts b/packages/valaxy/node/plugins/valaxy.ts index bf5379f1b..b3ee7a177 100644 --- a/packages/valaxy/node/plugins/valaxy.ts +++ b/packages/valaxy/node/plugins/valaxy.ts @@ -10,6 +10,7 @@ import { defu } from 'defu' import pascalCase from 'pascalcase' import type { DefaultTheme, PageDataPayload, Pkg, SiteConfig } from 'valaxy/types' import { dim, yellow } from 'picocolors' +import type { RouteRecordRaw } from 'vue-router' import { defaultSiteConfig, mergeValaxyConfig, resolveSiteConfig, resolveUserThemeConfig } from '../config' import type { ResolvedValaxyOptions, ValaxyServerOptions } from '../options' import { processValaxyOptions, resolveOptions, resolveThemeValaxyConfig } from '../options' @@ -19,6 +20,22 @@ import type { ValaxyNodeConfig } from '../types' import { checkMd } from '../markdown/check' import { vLogger } from '../logger' import { countPerformanceTime } from '../utils/performance' +import { isProd } from '../utils/env' + +function generateConfig(options: ResolvedValaxyOptions) { + const routes = options.redirects.map((redirect) => { + return { + path: redirect.from, + redirect: redirect.to, + } + }) + options.config.runtimeConfig.redirects = { + useVueRouter: isProd() ? options.config.siteConfig.redirects!.useVueRouter! : true, + redirectRoutes: routes, + } + + return `export default ${JSON.stringify(JSON.stringify(options.config))}` +} /** * for /@valaxyjs/styles @@ -164,7 +181,7 @@ export function createValaxyPlugin(options: ResolvedValaxyOptions, serverOptions load(id) { if (id === '/@valaxyjs/config') // stringify twice for \" - return `export default ${JSON.stringify(JSON.stringify(valaxyConfig))}` + return generateConfig(options) if (id === '/@valaxyjs/context') { return `export default ${JSON.stringify(JSON.stringify({ diff --git a/packages/valaxy/node/plugins/vueRouter.ts b/packages/valaxy/node/plugins/vueRouter.ts index 5df81319c..667bf62c0 100644 --- a/packages/valaxy/node/plugins/vueRouter.ts +++ b/packages/valaxy/node/plugins/vueRouter.ts @@ -114,6 +114,23 @@ export function createRouterPlugin(options: ResolvedValaxyOptions) { if (!isDate(mdFm.updated)) mdFm.updated = new Date(mdFm.updated!) + if (mdFm.from) { + if (Array.isArray(mdFm.from)) { + mdFm.from.forEach((from) => { + options.redirects.push({ + from, + to: route.fullPath, + }) + }) + } + else { + options.redirects.push({ + from: mdFm.from, + to: route.fullPath, + }) + } + } + // set route meta route.addToMeta({ frontmatter: mdFm, diff --git a/packages/valaxy/node/utils/clientRedirects.ts b/packages/valaxy/node/utils/clientRedirects.ts index 3128090cd..f3520761e 100644 --- a/packages/valaxy/node/utils/clientRedirects.ts +++ b/packages/valaxy/node/utils/clientRedirects.ts @@ -1,6 +1,6 @@ import { writeFile } from 'node:fs/promises' import { ensureFile } from 'fs-extra' -import type { RedirectRule } from 'valaxy/types' +import type { RedirectItem, RedirectRule } from 'valaxy/types' function handleRoute(route: string) { if (route === '/') @@ -10,14 +10,12 @@ function handleRoute(route: string) { return route } -interface RedirectItem { - from: string - to: string -} +export function collectRedirects(redirectRules?: RedirectRule[]): RedirectItem[] { + if (!redirectRules) + return [] -export function collectRedirects(redirectRule: RedirectRule[]) { const redirects: RedirectItem[] = [] - for (const rule of redirectRule) { + for (const rule of redirectRules) { if (Array.isArray(rule.from)) { for (const from of rule.from) { redirects.push({ diff --git a/packages/valaxy/shims.d.ts b/packages/valaxy/shims.d.ts index f5b78c653..2025df897 100644 --- a/packages/valaxy/shims.d.ts +++ b/packages/valaxy/shims.d.ts @@ -60,3 +60,11 @@ declare module 'vue-router' { frontmatter: Post } } + +declare module '/@valaxyjs/redirects' { + import type { RouteRecordRaw } from 'vue-router' + + export const redirectRoutes: RouteRecordRaw[] + + export const useVueRouter: boolean +} diff --git a/packages/valaxy/types/config.ts b/packages/valaxy/types/config.ts index f59ed3cc2..712b8b75f 100644 --- a/packages/valaxy/types/config.ts +++ b/packages/valaxy/types/config.ts @@ -1,6 +1,7 @@ import type { ZoomOptions } from 'medium-zoom' import type { FuseOptions } from '@vueuse/integrations/useFuse' import type { ILazyLoadOptions } from 'vanilla-lazyload' +import type { RouteRecordRaw } from 'vue-router' import type { ValaxyAddon } from '../types' import type { DefaultTheme } from './default-theme' import type { PostFrontMatter } from './posts' @@ -27,6 +28,11 @@ export interface RedirectRule { from: string | string[] } +export interface RedirectItem { + from: string + to: string +} + // shared with valaxy node and client export interface SiteConfig { /** @@ -323,7 +329,10 @@ export interface SiteConfig { * @description:en-US client redirect rules * @description:zh-CN 客户端重定向规则 */ - redirects?: RedirectRule[] + redirects?: { + useVueRouter?: boolean + rules?: RedirectRule[] + } } export type PartialDeep = { @@ -335,6 +344,10 @@ export type PartialDeep = { */ export interface RuntimeConfig { addons: Record + redirects: { + useVueRouter: boolean + redirectRoutes: RouteRecordRaw[] + } } export interface Pkg { diff --git a/packages/valaxy/types/posts.ts b/packages/valaxy/types/posts.ts index 356531b1a..b3c73d953 100644 --- a/packages/valaxy/types/posts.ts +++ b/packages/valaxy/types/posts.ts @@ -176,6 +176,11 @@ export interface PageFrontMatter extends Record { * @description:zh-CN 限制代码块的高度,单位是 px */ codeHeightLimit?: number + /** + * @description:en-US Source path for client redirection + * @description:zh-CN 客户端重定向的源路径 + */ + from?: string | string[] } export interface PostFrontMatter extends PageFrontMatter {