perf(build): reuse lastUpdated from markdown rendering in sitemap generation

Previously generateSitemap re-read and re-parsed every source file to
compute lastmod, reconstructing source paths from URLs. The transform
hook now records the already-computed pageData.lastUpdated per page,
keyed by its rewrite-applied relativePath. Dynamic route pages now get
a lastmod too (from their route template), which the old file lookup
could not resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/5014/head
Divyansh Singh 1 week ago
parent 20a7cb7a5f
commit fdd68e37a7

@ -12,6 +12,7 @@ import { slash, type Awaitable, type HeadConfig } from '../shared'
import { deserializeFunctions, serializeFunctions } from '../utils/fnSerialize'
import { nativeImport } from '../utils/nativeImport'
import { task } from '../utils/task'
import type { PageMeta } from '../plugin'
import { bundle } from './bundle'
import { generateSitemap } from './generateSitemap'
import { renderPage } from './render'
@ -51,10 +52,13 @@ export async function build(
delete buildOptions.outDir
}
const pageMetaMap = Object.create(null) as Record<string, PageMeta>
try {
const { clientResult, serverResult, pageToHashMap } = await bundle(
siteConfig,
buildOptions
buildOptions,
pageMetaMap
)
if (process.env.BUNDLE_ONLY) {
@ -172,7 +176,7 @@ export async function build(
}
}
await generateSitemap(siteConfig)
await generateSitemap(siteConfig, pageMetaMap)
await siteConfig.buildEnd?.(siteConfig)
clearCache()

@ -12,7 +12,7 @@ import {
} from 'vite'
import { APP_PATH } from '../alias'
import type { SiteConfig } from '../config'
import { createVitePressPlugin } from '../plugin'
import { createVitePressPlugin, type PageMeta } from '../plugin'
import { escapeRegExp, sanitizeFileName, slash } from '../shared'
import { task } from '../utils/task'
import { buildMPAClient } from './buildMPAClient'
@ -38,7 +38,8 @@ const excludedModules = [
// bundles the VitePress app for both client AND server.
export async function bundle(
config: SiteConfig,
options: BuildOptions
options: BuildOptions,
pageMetaMap?: Record<string, PageMeta>
): Promise<{
clientResult: Rolldown.RolldownOutput | null
serverResult: Rolldown.RolldownOutput
@ -83,7 +84,8 @@ export async function bundle(
config,
ssr,
pageToHashMap,
clientJSMap
clientJSMap,
pageMetaMap
),
ssr: { noExternal: ['vitepress', '@docsearch/css'] },
build: {

@ -1,4 +1,3 @@
import matter from 'gray-matter'
import fs from 'node:fs'
import path from 'node:path'
import {
@ -9,78 +8,72 @@ import {
type NewsItem
} from 'sitemap'
import type { SiteConfig } from '../config'
import { slash } from '../shared'
import { getGitTimestamp } from '../utils/getGitTimestamp'
import type { PageMeta } from '../plugin'
import { task } from '../utils/task'
export async function generateSitemap(siteConfig: SiteConfig) {
export async function generateSitemap(
siteConfig: SiteConfig,
pageMetaMap: Record<string, PageMeta>
) {
if (!siteConfig.sitemap?.hostname) return
const getLastmod = async (url: string) => {
if (!siteConfig.lastUpdated) return undefined
let file = url.replace(/(^|\/)$/, '$1index')
file = file.replace(/(\.html)?$/, '.md')
file = siteConfig.rewrites.inv[file] || file
file = path.join(siteConfig.srcDir, file)
if (!fs.existsSync(file)) return undefined
const { data } = matter.read(file)
if (data.lastUpdated === false) return undefined
if (data.lastUpdated instanceof Date) return +data.lastUpdated
return (await getGitTimestamp(slash(file))) || undefined
}
await task('generating sitemap', async () => {
const locales = siteConfig.userConfig.locales || {}
const filteredLocales = Object.keys(locales).filter(
(locale) => locales[locale].lang && locale !== 'root'
)
const defaultLang =
locales?.root?.lang || siteConfig.userConfig.lang || 'en-US'
locales.root?.lang || siteConfig.userConfig.lang || 'en-US'
const pages = siteConfig.pages.map(
(page) => siteConfig.rewrites.map[page] || page
// locale directories whose pages are translations of each other
const localeDirs = Object.keys(locales).filter(
(locale) => locale !== 'root' && locales[locale].lang
)
const groupedPages: Record<string, { lang: string; url: string }[]> = {}
pages.forEach((page) => {
const locale = page.split('/')[0]
const lang = locales[locale]?.lang || defaultLang
// group each page with its translations under a locale-independent key
const pageGroups: Record<
string,
{ lang: string; url: string; lastmod?: number }[]
> = {}
let url = page.replace(/(^|\/)index\.md$/, '$1')
url = url.replace(/\.md$/, siteConfig.cleanUrls ? '' : '.html')
if (filteredLocales.includes(locale)) page = page.slice(locale.length + 1)
for (const sourcePage of siteConfig.pages) {
const page = siteConfig.rewrites.map[sourcePage] || sourcePage
const localeDir = page.split('/')[0]
if (!groupedPages[page]) groupedPages[page] = []
groupedPages[page].push({ url, lang })
})
const url = page
.replace(/(^|\/)index\.md$/, '$1')
.replace(/\.md$/, siteConfig.cleanUrls ? '' : '.html')
const _items = await Promise.all(
Object.values(groupedPages).map(async (pages) => {
if (pages.length < 2)
return { url: pages[0].url, lastmod: await getLastmod(pages[0].url) }
const key = localeDirs.includes(localeDir)
? page.slice(localeDir.length + 1)
: page
return await Promise.all(
pages.map(async ({ url }) => {
return { url, lastmod: await getLastmod(url), links: pages }
})
)
;(pageGroups[key] ??= []).push({
lang: locales[localeDir]?.lang || defaultLang,
url,
lastmod: pageMetaMap[page]?.lastUpdated || undefined
})
}
// translated pages link to all their variants (including themselves)
let items: SitemapItem[] = Object.values(pageGroups).flatMap((variants) =>
variants.length < 2
? { url: variants[0].url, lastmod: variants[0].lastmod }
: variants.map(({ url, lastmod }) => ({
url,
lastmod,
links: variants
}))
)
let items: SitemapItem[] = _items.flat()
items = (await siteConfig.sitemap?.transformItems?.(items)) || items
const sitemapStream = new SitemapStream(siteConfig.sitemap)
const sitemapPath = path.join(siteConfig.outDir, 'sitemap.xml')
const sitemapStream = new SitemapStream(siteConfig.sitemap)
const writeStream = fs.createWriteStream(sitemapPath)
sitemapStream.pipe(writeStream)
items.forEach((item) => sitemapStream.write(item))
sitemapStream.end()
await new Promise((resolve, reject) =>
writeStream.on('finish', resolve).on('error', reject)
)
})
}

@ -65,11 +65,17 @@ const isPageChunk = <T extends Rolldown.OutputChunk | Rolldown.RenderedChunk>(
const cleanUrl = (url: string): string => url.replace(/[?#].*$/s, '')
// per-page metadata collected during transform, keyed by relativePath
export interface PageMeta {
lastUpdated?: number
}
export async function createVitePressPlugin(
siteConfig: SiteConfig,
ssr = false,
pageToHashMap?: Record<string, string>,
clientJSMap?: Record<string, string>,
pageMetaMap?: Record<string, PageMeta>,
restartServer?: () => Promise<void>
) {
const {
@ -218,6 +224,11 @@ export async function createVitePressPlugin(
code,
id
)
if (pageMetaMap) {
pageMetaMap[pageData.relativePath] = {
lastUpdated: pageData.lastUpdated
}
}
allDeadLinks.push(...deadLinks)
if (includes.length) {
includes.forEach((i) => {

@ -17,7 +17,14 @@ export async function createServer(
root: config.srcDir,
base: config.site.base,
cacheDir: config.cacheDir,
plugins: await createVitePressPlugin(config, false, {}, {}, restartServer),
plugins: await createVitePressPlugin(
config,
undefined,
undefined,
undefined,
undefined,
restartServer
),
server,
customLogger: config.logger,
configFile: config.vite?.configFile

Loading…
Cancel
Save