mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-15 02:14:44 +00:00
fix(nuxt): use routeRules
to hint pages to prerender (#29172)
This commit is contained in:
parent
959061fc29
commit
781d8c4174
@ -8,6 +8,7 @@ import { hash } from 'ohash'
|
|||||||
import { camelCase } from 'scule'
|
import { camelCase } from 'scule'
|
||||||
import { filename } from 'pathe/utils'
|
import { filename } from 'pathe/utils'
|
||||||
import type { NuxtTemplate } from 'nuxt/schema'
|
import type { NuxtTemplate } from 'nuxt/schema'
|
||||||
|
import type { Nitro } from 'nitro/types'
|
||||||
|
|
||||||
import { annotatePlugins, checkForCircularDependencies } from './app'
|
import { annotatePlugins, checkForCircularDependencies } from './app'
|
||||||
|
|
||||||
@ -516,6 +517,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
|
|||||||
`export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`,
|
`export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`,
|
||||||
`export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`,
|
`export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`,
|
||||||
`export const chunkErrorEvent = ${ctx.nuxt.options.experimental.emitRouteChunkError ? ctx.nuxt.options.builder === '@nuxt/vite-builder' ? '"vite:preloadError"' : '"nuxt:preloadError"' : 'false'}`,
|
`export const chunkErrorEvent = ${ctx.nuxt.options.experimental.emitRouteChunkError ? ctx.nuxt.options.builder === '@nuxt/vite-builder' ? '"vite:preloadError"' : '"nuxt:preloadError"' : 'false'}`,
|
||||||
|
`export const crawlLinks = ${!!((ctx.nuxt as any)._nitro as Nitro).options.prerender.crawlLinks}`,
|
||||||
].join('\n\n')
|
].join('\n\n')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import type { Nuxt, NuxtApp, NuxtPage } from 'nuxt/schema'
|
|||||||
import { createRoutesContext } from 'unplugin-vue-router'
|
import { createRoutesContext } from 'unplugin-vue-router'
|
||||||
import { resolveOptions } from 'unplugin-vue-router/options'
|
import { resolveOptions } from 'unplugin-vue-router/options'
|
||||||
import type { EditableTreeNode, Options as TypedRouterOptions } from 'unplugin-vue-router'
|
import type { EditableTreeNode, Options as TypedRouterOptions } from 'unplugin-vue-router'
|
||||||
|
import { createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
|
||||||
|
|
||||||
import type { NitroRouteConfig } from 'nitro/types'
|
import type { NitroRouteConfig } from 'nitro/types'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
@ -277,7 +278,7 @@ export default defineNuxtModule({
|
|||||||
|
|
||||||
nuxt.hook('app:resolve', (app) => {
|
nuxt.hook('app:resolve', (app) => {
|
||||||
const nitro = useNitro()
|
const nitro = useNitro()
|
||||||
if (nitro.options.prerender.crawlLinks) {
|
if (nitro.options.prerender.crawlLinks || Object.values(nitro.options.routeRules).some(rule => rule.prerender)) {
|
||||||
app.plugins.push({
|
app.plugins.push({
|
||||||
src: resolve(runtimeDir, 'plugins/prerender.server'),
|
src: resolve(runtimeDir, 'plugins/prerender.server'),
|
||||||
mode: 'server',
|
mode: 'server',
|
||||||
@ -315,7 +316,20 @@ export default defineNuxtModule({
|
|||||||
})
|
})
|
||||||
|
|
||||||
nuxt.hook('nitro:build:before', (nitro) => {
|
nuxt.hook('nitro:build:before', (nitro) => {
|
||||||
if (nuxt.options.dev || !nitro.options.static || nuxt.options.router.options.hashMode || !nitro.options.prerender.crawlLinks) { return }
|
if (nuxt.options.dev || nuxt.options.router.options.hashMode) { return }
|
||||||
|
|
||||||
|
// Inject page patterns that explicitly match `prerender: true` route rule
|
||||||
|
if (!nitro.options.static && !nitro.options.prerender.crawlLinks) {
|
||||||
|
const routeRulesMatcher = toRouteMatcher(createRadixRouter({ routes: nitro.options.routeRules }))
|
||||||
|
for (const route of prerenderRoutes) {
|
||||||
|
const rules = defu({} as Record<string, any>, ...routeRulesMatcher.matchAll(route).reverse())
|
||||||
|
if (rules.prerender) {
|
||||||
|
nitro.options.prerender.routes.push(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nitro.options.static || !nitro.options.prerender.crawlLinks) { return }
|
||||||
|
|
||||||
// Only hint the first route when `ssr: true` and no routes are provided
|
// Only hint the first route when `ssr: true` and no routes are provided
|
||||||
// as the rest will be injected at runtime when this is prerendered
|
// as the rest will be injected at runtime when this is prerendered
|
||||||
|
@ -1,20 +1,31 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
import { joinURL } from 'ufo'
|
import { joinURL } from 'ufo'
|
||||||
|
import { createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
|
||||||
|
import defu from 'defu'
|
||||||
|
|
||||||
import { defineNuxtPlugin } from '#app/nuxt'
|
import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
|
||||||
import { prerenderRoutes } from '#app/composables/ssr'
|
import { prerenderRoutes } from '#app/composables/ssr'
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import _routes from '#build/routes'
|
import _routes from '#build/routes'
|
||||||
import routerOptions from '#build/router.options'
|
import routerOptions from '#build/router.options'
|
||||||
|
// @ts-expect-error virtual file
|
||||||
|
import { crawlLinks } from '#build/nuxt.config.mjs'
|
||||||
|
|
||||||
let routes: string[]
|
let routes: string[]
|
||||||
|
|
||||||
|
let _routeRulesMatcher: undefined | ReturnType<typeof toRouteMatcher> = undefined
|
||||||
|
|
||||||
export default defineNuxtPlugin(async () => {
|
export default defineNuxtPlugin(async () => {
|
||||||
if (!import.meta.server || !import.meta.prerender || routerOptions.hashMode) {
|
if (!import.meta.server || !import.meta.prerender || routerOptions.hashMode) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (routes && !routes.length) { return }
|
if (routes && !routes.length) { return }
|
||||||
|
|
||||||
|
const routeRules = useRuntimeConfig().nitro!.routeRules
|
||||||
|
if (!crawlLinks && routeRules && Object.values(routeRules).some(r => r.prerender)) {
|
||||||
|
_routeRulesMatcher = toRouteMatcher(createRadixRouter({ routes: routeRules }))
|
||||||
|
}
|
||||||
|
|
||||||
routes ||= Array.from(processRoutes(await routerOptions.routes?.(_routes) ?? _routes))
|
routes ||= Array.from(processRoutes(await routerOptions.routes?.(_routes) ?? _routes))
|
||||||
const batch = routes.splice(0, 10)
|
const batch = routes.splice(0, 10)
|
||||||
prerenderRoutes(batch)
|
prerenderRoutes(batch)
|
||||||
@ -24,10 +35,14 @@ export default defineNuxtPlugin(async () => {
|
|||||||
|
|
||||||
const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/
|
const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/
|
||||||
|
|
||||||
|
function shouldPrerender (path: string) {
|
||||||
|
return !_routeRulesMatcher || defu({} as Record<string, any>, ..._routeRulesMatcher.matchAll(path).reverse()).prerender
|
||||||
|
}
|
||||||
|
|
||||||
function processRoutes (routes: RouteRecordRaw[], currentPath = '/', routesToPrerender = new Set<string>()) {
|
function processRoutes (routes: RouteRecordRaw[], currentPath = '/', routesToPrerender = new Set<string>()) {
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
// Add root of optional dynamic paths and catchalls
|
// Add root of optional dynamic paths and catchalls
|
||||||
if (OPTIONAL_PARAM_RE.test(route.path) && !route.children?.length) {
|
if (OPTIONAL_PARAM_RE.test(route.path) && !route.children?.length && shouldPrerender(currentPath)) {
|
||||||
routesToPrerender.add(currentPath)
|
routesToPrerender.add(currentPath)
|
||||||
}
|
}
|
||||||
// Skip dynamic paths
|
// Skip dynamic paths
|
||||||
@ -35,7 +50,9 @@ function processRoutes (routes: RouteRecordRaw[], currentPath = '/', routesToPre
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const fullPath = joinURL(currentPath, route.path)
|
const fullPath = joinURL(currentPath, route.path)
|
||||||
|
if (shouldPrerender(fullPath)) {
|
||||||
routesToPrerender.add(fullPath)
|
routesToPrerender.add(fullPath)
|
||||||
|
}
|
||||||
if (route.children) {
|
if (route.children) {
|
||||||
processRoutes(route.children, fullPath, routesToPrerender)
|
processRoutes(route.children, fullPath, routesToPrerender)
|
||||||
}
|
}
|
||||||
|
@ -620,6 +620,11 @@ describe('pages', () => {
|
|||||||
expect(status).toBe(200)
|
expect(status).toBe(200)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.skipIf(isDev() || isWebpack /* TODO: fix bug with import.meta.prerender being undefined in webpack build */)('prerenders pages hinted with a route rule', async () => {
|
||||||
|
const html = await $fetch('/prerender/test')
|
||||||
|
expect(html).toContain('should be prerendered: true')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('nuxt composables', () => {
|
describe('nuxt composables', () => {
|
||||||
|
1
test/fixtures/basic/nuxt.config.ts
vendored
1
test/fixtures/basic/nuxt.config.ts
vendored
@ -66,6 +66,7 @@ export default defineNuxtConfig({
|
|||||||
'/route-rules/middleware': { appMiddleware: 'route-rules-middleware' },
|
'/route-rules/middleware': { appMiddleware: 'route-rules-middleware' },
|
||||||
'/hydration/spa-redirection/**': { ssr: false },
|
'/hydration/spa-redirection/**': { ssr: false },
|
||||||
'/no-scripts': { experimentalNoScripts: true },
|
'/no-scripts': { experimentalNoScripts: true },
|
||||||
|
'/prerender/**': { prerender: true },
|
||||||
},
|
},
|
||||||
prerender: {
|
prerender: {
|
||||||
routes: [
|
routes: [
|
||||||
|
9
test/fixtures/basic/pages/prerender/test.vue
vendored
Normal file
9
test/fixtures/basic/pages/prerender/test.vue
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const wasPrerendered = useState(() => import.meta.prerender)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
should be prerendered: {{ wasPrerendered }}
|
||||||
|
</div>
|
||||||
|
</template>
|
Loading…
Reference in New Issue
Block a user