fix(nuxt): use routeRules to hint pages to prerender (#29172)

This commit is contained in:
Daniel Roe 2024-09-26 17:07:46 +01:00
parent 7f311e7730
commit a28e00c0da
No known key found for this signature in database
GPG Key ID: CBC814C393D93268
6 changed files with 53 additions and 5 deletions

View File

@ -8,6 +8,7 @@ import { hash } from 'ohash'
import { camelCase } from 'scule'
import { filename } from 'pathe/utils'
import type { NuxtTemplate, NuxtTypeTemplate } from 'nuxt/schema'
import type { Nitro } from 'nitropack'
import { annotatePlugins, checkForCircularDependencies } from './app'
@ -512,6 +513,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`,
`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 crawlLinks = ${!!((ctx.nuxt as any)._nitro as Nitro).options.prerender.crawlLinks}`,
].join('\n\n')
},
}

View File

@ -8,6 +8,7 @@ import type { Nuxt, NuxtApp, NuxtPage } from 'nuxt/schema'
import { createRoutesContext } from 'unplugin-vue-router'
import { resolveOptions } from 'unplugin-vue-router/options'
import type { EditableTreeNode, Options as TypedRouterOptions } from 'unplugin-vue-router'
import { createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
import type { NitroRouteConfig } from 'nitropack'
import { defu } from 'defu'
@ -272,7 +273,7 @@ export default defineNuxtModule({
nuxt.hook('app:resolve', (app) => {
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({
src: resolve(runtimeDir, 'plugins/prerender.server'),
mode: 'server',
@ -310,7 +311,20 @@ export default defineNuxtModule({
})
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
// as the rest will be injected at runtime when this is prerendered

View File

@ -1,20 +1,31 @@
import type { RouteRecordRaw } from 'vue-router'
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'
// @ts-expect-error virtual file
import _routes from '#build/routes'
import routerOptions from '#build/router.options'
// @ts-expect-error virtual file
import { crawlLinks } from '#build/nuxt.config.mjs'
let routes: string[]
let _routeRulesMatcher: undefined | ReturnType<typeof toRouteMatcher> = undefined
export default defineNuxtPlugin(async () => {
if (!import.meta.server || !import.meta.prerender || routerOptions.hashMode) {
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))
const batch = routes.splice(0, 10)
prerenderRoutes(batch)
@ -24,10 +35,14 @@ export default defineNuxtPlugin(async () => {
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>()) {
for (const route of routes) {
// 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)
}
// Skip dynamic paths
@ -35,7 +50,9 @@ function processRoutes (routes: RouteRecordRaw[], currentPath = '/', routesToPre
continue
}
const fullPath = joinURL(currentPath, route.path)
routesToPrerender.add(fullPath)
if (shouldPrerender(fullPath)) {
routesToPrerender.add(fullPath)
}
if (route.children) {
processRoutes(route.children, fullPath, routesToPrerender)
}

View File

@ -618,6 +618,11 @@ describe('pages', () => {
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', () => {

View File

@ -67,6 +67,7 @@ export default defineNuxtConfig({
'/route-rules/middleware': { appMiddleware: 'route-rules-middleware' },
'/hydration/spa-redirection/**': { ssr: false },
'/no-scripts': { experimentalNoScripts: true },
'/prerender/**': { prerender: true },
},
output: { dir: process.env.NITRO_OUTPUT_DIR },
prerender: {

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
const wasPrerendered = useState(() => import.meta.prerender)
</script>
<template>
<div>
should be prerendered: {{ wasPrerendered }}
</div>
</template>