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 2 months ago
parent 20a7cb7a5f
commit fdd68e37a7

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

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

@ -1,4 +1,3 @@
import matter from 'gray-matter'
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { import {
@ -9,78 +8,72 @@ import {
type NewsItem type NewsItem
} from 'sitemap' } from 'sitemap'
import type { SiteConfig } from '../config' import type { SiteConfig } from '../config'
import { slash } from '../shared' import type { PageMeta } from '../plugin'
import { getGitTimestamp } from '../utils/getGitTimestamp'
import { task } from '../utils/task' 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 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 () => { await task('generating sitemap', async () => {
const locales = siteConfig.userConfig.locales || {} const locales = siteConfig.userConfig.locales || {}
const filteredLocales = Object.keys(locales).filter(
(locale) => locales[locale].lang && locale !== 'root'
)
const defaultLang = const defaultLang =
locales?.root?.lang || siteConfig.userConfig.lang || 'en-US' locales.root?.lang || siteConfig.userConfig.lang || 'en-US'
const pages = siteConfig.pages.map( // locale directories whose pages are translations of each other
(page) => siteConfig.rewrites.map[page] || page const localeDirs = Object.keys(locales).filter(
(locale) => locale !== 'root' && locales[locale].lang
) )
const groupedPages: Record<string, { lang: string; url: string }[]> = {} // group each page with its translations under a locale-independent key
pages.forEach((page) => { const pageGroups: Record<
const locale = page.split('/')[0] string,
const lang = locales[locale]?.lang || defaultLang { lang: string; url: string; lastmod?: number }[]
> = {}
let url = page.replace(/(^|\/)index\.md$/, '$1') for (const sourcePage of siteConfig.pages) {
url = url.replace(/\.md$/, siteConfig.cleanUrls ? '' : '.html') const page = siteConfig.rewrites.map[sourcePage] || sourcePage
if (filteredLocales.includes(locale)) page = page.slice(locale.length + 1) const localeDir = page.split('/')[0]
if (!groupedPages[page]) groupedPages[page] = [] const url = page
groupedPages[page].push({ url, lang }) .replace(/(^|\/)index\.md$/, '$1')
}) .replace(/\.md$/, siteConfig.cleanUrls ? '' : '.html')
const _items = await Promise.all( const key = localeDirs.includes(localeDir)
Object.values(groupedPages).map(async (pages) => { ? page.slice(localeDir.length + 1)
if (pages.length < 2) : page
return { url: pages[0].url, lastmod: await getLastmod(pages[0].url) }
return await Promise.all( ;(pageGroups[key] ??= []).push({
pages.map(async ({ url }) => { lang: locales[localeDir]?.lang || defaultLang,
return { url, lastmod: await getLastmod(url), links: pages } 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 items = (await siteConfig.sitemap?.transformItems?.(items)) || items
const sitemapStream = new SitemapStream(siteConfig.sitemap)
const sitemapPath = path.join(siteConfig.outDir, 'sitemap.xml') const sitemapPath = path.join(siteConfig.outDir, 'sitemap.xml')
const sitemapStream = new SitemapStream(siteConfig.sitemap)
const writeStream = fs.createWriteStream(sitemapPath) const writeStream = fs.createWriteStream(sitemapPath)
sitemapStream.pipe(writeStream) sitemapStream.pipe(writeStream)
items.forEach((item) => sitemapStream.write(item)) items.forEach((item) => sitemapStream.write(item))
sitemapStream.end() 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, '') 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( export async function createVitePressPlugin(
siteConfig: SiteConfig, siteConfig: SiteConfig,
ssr = false, ssr = false,
pageToHashMap?: Record<string, string>, pageToHashMap?: Record<string, string>,
clientJSMap?: Record<string, string>, clientJSMap?: Record<string, string>,
pageMetaMap?: Record<string, PageMeta>,
restartServer?: () => Promise<void> restartServer?: () => Promise<void>
) { ) {
const { const {
@ -218,6 +224,11 @@ export async function createVitePressPlugin(
code, code,
id id
) )
if (pageMetaMap) {
pageMetaMap[pageData.relativePath] = {
lastUpdated: pageData.lastUpdated
}
}
allDeadLinks.push(...deadLinks) allDeadLinks.push(...deadLinks)
if (includes.length) { if (includes.length) {
includes.forEach((i) => { includes.forEach((i) => {

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

Loading…
Cancel
Save