diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index 479df8b67c..6c6d4c9364 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -503,11 +503,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { for (const route of ['/200.html', '/404.html']) { routes.add(route) } - if (nuxt.options.ssr) { - if (nitro.options.prerender.crawlLinks) { - routes.add('/') - } - } else { + if (!nuxt.options.ssr) { routes.add('/index.html') } }) diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index 735b13703b..2a14be7ec9 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -3,6 +3,7 @@ import { mkdir, readFile } from 'node:fs/promises' import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, logger, resolvePath, updateTemplates, useNitro } from '@nuxt/kit' import { dirname, join, relative, resolve } from 'pathe' import { genImport, genObjectFromRawEntries, genString } from 'knitwork' +import { joinURL } from 'ufo' import type { Nuxt, NuxtApp, NuxtPage } from 'nuxt/schema' import { createRoutesContext } from 'unplugin-vue-router' import { resolveOptions } from 'unplugin-vue-router/options' @@ -18,6 +19,8 @@ import type { PageMetaPluginOptions } from './plugins/page-meta' import { PageMetaPlugin } from './plugins/page-meta' import { RouteInjectionPlugin } from './plugins/route-injection' +const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/ + export default defineNuxtModule({ meta: { name: 'pages', @@ -264,6 +267,61 @@ export default defineNuxtModule({ } }) + // Record all pages for use in prerendering + const prerenderRoutes = new Set() + + function processPages (pages: NuxtPage[], currentPath = '/') { + for (const page of pages) { + // Add root of optional dynamic paths and catchalls + if (OPTIONAL_PARAM_RE.test(page.path) && !page.children?.length) { + prerenderRoutes.add(currentPath) + } + + // Skip dynamic paths + if (page.path.includes(':')) { continue } + + const route = joinURL(currentPath, page.path) + prerenderRoutes.add(route) + + if (page.children) { + processPages(page.children, route) + } + } + } + + nuxt.hook('pages:extend', (pages) => { + if (nuxt.options.dev) { return } + + prerenderRoutes.clear() + processPages(pages) + }) + + // For static sites with ssr: false with crawl, prerender all routes + nuxt.hook('nitro:init', (nitro) => { + if (nuxt.options.dev || !nitro.options.static || nuxt.options.router.options.hashMode || !nitro.options.prerender.crawlLinks) { return } + + // Only hint the first route when `ssr: true` and no routes are provided + if (nuxt.options.ssr) { + nitro.hooks.hook('prerender:routes', (routes) => { + if ([...routes].every(r => r.endsWith('.html'))) { + const [firstPage] = [...prerenderRoutes].sort() + if (firstPage) { + routes.add(firstPage) + } + } + }) + return + } + + // Prerender all non-dynamic page routes when generating `ssr: false` app + nuxt.hook('nitro:build:before', (nitro) => { + for (const route of nitro.options.prerender.routes || []) { + prerenderRoutes.add(route) + } + nitro.options.prerender.routes = Array.from(prerenderRoutes) + }) + }) + nuxt.hook('imports:extend', (imports) => { imports.push( { name: 'definePageMeta', as: 'definePageMeta', from: resolve(runtimeDir, 'composables') },