|
|
|
@ -39,14 +39,13 @@ export async function renderPage(
|
|
|
|
usedIcons: Set<string>
|
|
|
|
usedIcons: Set<string>
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
const routePath = `/${page.replace(/\.md$/, '')}`
|
|
|
|
const routePath = `/${page.replace(/\.md$/, '')}`
|
|
|
|
|
|
|
|
|
|
|
|
const relativeBase = isRelativeBase(config.site.base)
|
|
|
|
const relativeBase = isRelativeBase(config.site.base)
|
|
|
|
const pageBase = relativeBase ? relativePathToRoot(page) : config.site.base
|
|
|
|
const pageBase = relativeBase ? relativePathToRoot(page) : config.site.base
|
|
|
|
|
|
|
|
|
|
|
|
// user hooks must never see the build sentinel
|
|
|
|
// user hooks must never see the build sentinel
|
|
|
|
const desentinel = (value: string) =>
|
|
|
|
const desentinel = (value: string) =>
|
|
|
|
relativeBase ? value.replaceAll(RELATIVE_BASE_SENTINEL, pageBase) : value
|
|
|
|
relativeBase ? value.replaceAll(RELATIVE_BASE_SENTINEL, pageBase) : value
|
|
|
|
|
|
|
|
|
|
|
|
// render page
|
|
|
|
|
|
|
|
const context = await render(routePath)
|
|
|
|
const context = await render(routePath)
|
|
|
|
if (relativeBase) {
|
|
|
|
if (relativeBase) {
|
|
|
|
context.content = desentinel(context.content)
|
|
|
|
context.content = desentinel(context.content)
|
|
|
|
@ -56,9 +55,9 @@ export async function renderPage(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// SSR filled the original context's icon set — drain it before postRender,
|
|
|
|
|
|
|
|
// which may return a fresh object without it; a hook can still contribute
|
|
|
|
// collect the icons rendered during SSR; postRender may replace the
|
|
|
|
// additional icons through the object it returns
|
|
|
|
// context and contribute more
|
|
|
|
context.vpIcons?.forEach((icon) => usedIcons.add(icon))
|
|
|
|
context.vpIcons?.forEach((icon) => usedIcons.add(icon))
|
|
|
|
|
|
|
|
|
|
|
|
const rendered = (await config.postRender?.(context)) ?? context
|
|
|
|
const rendered = (await config.postRender?.(context)) ?? context
|
|
|
|
@ -70,10 +69,6 @@ export async function renderPage(
|
|
|
|
const pageName = sanitizeFileName(page.replace(/\//g, '_'))
|
|
|
|
const pageName = sanitizeFileName(page.replace(/\//g, '_'))
|
|
|
|
// server build doesn't need hash
|
|
|
|
// server build doesn't need hash
|
|
|
|
const pageServerJsFileName = pageName + '.js'
|
|
|
|
const pageServerJsFileName = pageName + '.js'
|
|
|
|
// for any initial page load, we only need the lean version of the page js
|
|
|
|
|
|
|
|
// since the static content is already on the page!
|
|
|
|
|
|
|
|
const pageHash = pageToHashMap[pageName.toLowerCase()]
|
|
|
|
|
|
|
|
const pageClientJsFileName = `${config.assetsDir}/${pageName}.${pageHash}.lean.js`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let pageData: PageData
|
|
|
|
let pageData: PageData
|
|
|
|
let hasCustom404 = true
|
|
|
|
let hasCustom404 = true
|
|
|
|
@ -102,26 +97,27 @@ export async function renderPage(
|
|
|
|
: ''
|
|
|
|
: ''
|
|
|
|
const pageAssets = relativeBase ? assets.map(desentinel) : assets
|
|
|
|
const pageAssets = relativeBase ? assets.map(desentinel) : assets
|
|
|
|
|
|
|
|
|
|
|
|
const title: string = createTitle(siteData, pageData)
|
|
|
|
const title = createTitle(siteData, pageData)
|
|
|
|
const description: string = pageData.description || siteData.description
|
|
|
|
const description = pageData.description || siteData.description
|
|
|
|
const stylesheetLink = cssChunk
|
|
|
|
const dir = pageData.frontmatter.dir || siteData.dir || 'ltr'
|
|
|
|
? `<link rel="preload stylesheet" href="${assetUrl(cssChunk.fileName)}" as="style">`
|
|
|
|
const isDefault404 = page === '404.md' && !hasCustom404
|
|
|
|
: ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let preloadLinks =
|
|
|
|
// the initial load only needs the lean page js — the static content is
|
|
|
|
config.mpa || (!hasCustom404 && page === '404.md')
|
|
|
|
// already in the HTML
|
|
|
|
? []
|
|
|
|
const pageHash = pageToHashMap[pageName.toLowerCase()]
|
|
|
|
: result && appChunk
|
|
|
|
const pageClientJsFileName = `${config.assetsDir}/${pageName}.${pageHash}.lean.js`
|
|
|
|
? [
|
|
|
|
|
|
|
|
...new Set([
|
|
|
|
let preloadLinks: string[] = []
|
|
|
|
// resolve imports for index.js + page.md.js and inject script tags
|
|
|
|
if (result && appChunk && !config.mpa && !isDefault404) {
|
|
|
|
// for them as well so we fetch everything as early as possible
|
|
|
|
preloadLinks = [
|
|
|
|
// without having to wait for entry chunks to parse
|
|
|
|
...new Set([
|
|
|
|
...(await resolvePageImports(config, page, result, appChunk)),
|
|
|
|
// the imports of index.js + page.md.js as well, so everything
|
|
|
|
pageClientJsFileName
|
|
|
|
// fetches without waiting for the entry chunks to parse
|
|
|
|
])
|
|
|
|
...(await resolvePageImports(config, page, result, appChunk)),
|
|
|
|
]
|
|
|
|
pageClientJsFileName
|
|
|
|
: []
|
|
|
|
])
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let prefetchLinks: string[] = []
|
|
|
|
let prefetchLinks: string[] = []
|
|
|
|
|
|
|
|
|
|
|
|
@ -163,21 +159,27 @@ export async function renderPage(
|
|
|
|
)
|
|
|
|
)
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const transformContext = (head: HeadConfig[]) => ({
|
|
|
|
|
|
|
|
page,
|
|
|
|
|
|
|
|
siteConfig: config,
|
|
|
|
|
|
|
|
siteData,
|
|
|
|
|
|
|
|
pageData,
|
|
|
|
|
|
|
|
title,
|
|
|
|
|
|
|
|
description,
|
|
|
|
|
|
|
|
head,
|
|
|
|
|
|
|
|
content,
|
|
|
|
|
|
|
|
assets: pageAssets
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const head = mergeHead(
|
|
|
|
const head = mergeHead(
|
|
|
|
headBeforeTransform,
|
|
|
|
headBeforeTransform,
|
|
|
|
(await config.transformHead?.({
|
|
|
|
(await config.transformHead?.(transformContext(headBeforeTransform))) || []
|
|
|
|
page,
|
|
|
|
|
|
|
|
siteConfig: config,
|
|
|
|
|
|
|
|
siteData,
|
|
|
|
|
|
|
|
pageData,
|
|
|
|
|
|
|
|
title,
|
|
|
|
|
|
|
|
description,
|
|
|
|
|
|
|
|
head: headBeforeTransform,
|
|
|
|
|
|
|
|
content,
|
|
|
|
|
|
|
|
assets: pageAssets
|
|
|
|
|
|
|
|
})) || []
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const stylesheetLink = cssChunk
|
|
|
|
|
|
|
|
? `<link rel="preload stylesheet" href="${assetUrl(cssChunk.fileName)}" as="style">`
|
|
|
|
|
|
|
|
: ''
|
|
|
|
|
|
|
|
|
|
|
|
let inlinedScript = ''
|
|
|
|
let inlinedScript = ''
|
|
|
|
if (config.mpa && result) {
|
|
|
|
if (config.mpa && result) {
|
|
|
|
const matchingChunk = result.output.find(
|
|
|
|
const matchingChunk = result.output.find(
|
|
|
|
@ -197,20 +199,18 @@ export async function renderPage(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const dir = pageData.frontmatter.dir || siteData.dir || 'ltr'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const html = `<!DOCTYPE html>
|
|
|
|
const html = `<!DOCTYPE html>
|
|
|
|
<html lang="${siteData.lang}" dir="${dir}">
|
|
|
|
<html lang="${siteData.lang}" dir="${dir}">
|
|
|
|
<head>
|
|
|
|
<head>
|
|
|
|
<meta charset="utf-8">
|
|
|
|
<meta charset="utf-8">
|
|
|
|
${
|
|
|
|
${
|
|
|
|
isMetaViewportOverridden(head)
|
|
|
|
hasNamedMeta(head, 'viewport')
|
|
|
|
? ''
|
|
|
|
? ''
|
|
|
|
: '<meta name="viewport" content="width=device-width,initial-scale=1">'
|
|
|
|
: '<meta name="viewport" content="width=device-width,initial-scale=1">'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
<title>${escapeHtml(title)}</title>
|
|
|
|
<title>${escapeHtml(title)}</title>
|
|
|
|
${
|
|
|
|
${
|
|
|
|
isDescriptionOverridden(head)
|
|
|
|
hasNamedMeta(head, 'description')
|
|
|
|
? ''
|
|
|
|
? ''
|
|
|
|
: `<meta name="description" content="${escapeHtml(description)}">`
|
|
|
|
: `<meta name="description" content="${escapeHtml(description)}">`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -245,17 +245,7 @@ export async function renderPage(
|
|
|
|
const transformedHtml = await config.transformHtml?.(
|
|
|
|
const transformedHtml = await config.transformHtml?.(
|
|
|
|
finalHtml,
|
|
|
|
finalHtml,
|
|
|
|
htmlFileName,
|
|
|
|
htmlFileName,
|
|
|
|
{
|
|
|
|
transformContext(head)
|
|
|
|
page,
|
|
|
|
|
|
|
|
siteConfig: config,
|
|
|
|
|
|
|
|
siteData,
|
|
|
|
|
|
|
|
pageData,
|
|
|
|
|
|
|
|
title,
|
|
|
|
|
|
|
|
description,
|
|
|
|
|
|
|
|
head,
|
|
|
|
|
|
|
|
content,
|
|
|
|
|
|
|
|
assets: pageAssets
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
await writeFile(htmlFileName, transformedHtml || finalHtml)
|
|
|
|
await writeFile(htmlFileName, transformedHtml || finalHtml)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -267,45 +257,35 @@ async function resolvePageImports(
|
|
|
|
appChunk: Rolldown.OutputChunk
|
|
|
|
appChunk: Rolldown.OutputChunk
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
page = config.rewrites.inv[page] || page
|
|
|
|
page = config.rewrites.inv[page] || page
|
|
|
|
// find the page's js chunk and inject script tags for its imports so that
|
|
|
|
|
|
|
|
// they start fetching as early as possible
|
|
|
|
|
|
|
|
let srcPath = path.resolve(config.srcDir, page)
|
|
|
|
let srcPath = path.resolve(config.srcDir, page)
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
if (!config.vite?.resolve?.preserveSymlinks) {
|
|
|
|
if (!config.vite?.resolve?.preserveSymlinks) {
|
|
|
|
srcPath = await realpath(srcPath)
|
|
|
|
srcPath = await realpath(srcPath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
} catch {
|
|
|
|
// if the page is a virtual page generated by a dynamic route this would
|
|
|
|
// virtual pages generated by dynamic routes have no file on disk
|
|
|
|
// fail, which is expected
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
srcPath = normalizePath(srcPath)
|
|
|
|
srcPath = normalizePath(srcPath)
|
|
|
|
const pageChunk = result.output.find(
|
|
|
|
const pageChunk = result.output.find(
|
|
|
|
(chunk): chunk is Rolldown.OutputChunk =>
|
|
|
|
(chunk): chunk is Rolldown.OutputChunk =>
|
|
|
|
chunk.type === 'chunk' && chunk.facadeModuleId === srcPath
|
|
|
|
chunk.type === 'chunk' && chunk.facadeModuleId === srcPath
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return [
|
|
|
|
// dynamic imports are intentionally not preloaded
|
|
|
|
...appChunk.imports,
|
|
|
|
return [...appChunk.imports, ...(pageChunk?.imports || [])]
|
|
|
|
// ...appChunk.dynamicImports,
|
|
|
|
|
|
|
|
...(pageChunk?.imports || [])
|
|
|
|
|
|
|
|
// ...pageChunk.dynamicImports
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function renderHead(head: HeadConfig[]): Promise<string> {
|
|
|
|
async function renderHead(head: HeadConfig[]): Promise<string> {
|
|
|
|
const tags = await Promise.all(
|
|
|
|
const tags = await Promise.all(
|
|
|
|
head.map(async ([tag, attrs = {}, innerHTML = '']) => {
|
|
|
|
head.map(async ([tag, attrs = {}, innerHTML = '']) => {
|
|
|
|
const openTag = `<${tag}${renderAttrs(attrs)}>`
|
|
|
|
const openTag = `<${tag}${renderAttrs(attrs)}>`
|
|
|
|
if (tag !== 'link' && tag !== 'meta') {
|
|
|
|
if (tag === 'link' || tag === 'meta') return openTag
|
|
|
|
if (
|
|
|
|
if (
|
|
|
|
tag === 'script' &&
|
|
|
|
tag === 'script' &&
|
|
|
|
(attrs.type === undefined || attrs.type.includes('javascript'))
|
|
|
|
(attrs.type === undefined || attrs.type.includes('javascript'))
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
innerHTML = (await minify('inline-script.js', innerHTML)).code
|
|
|
|
innerHTML = (await minify('inline-script.js', innerHTML)).code
|
|
|
|
}
|
|
|
|
|
|
|
|
return `${openTag}${innerHTML}</${tag}>`
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
return openTag
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return `${openTag}${innerHTML}</${tag}>`
|
|
|
|
})
|
|
|
|
})
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return tags.join('\n ')
|
|
|
|
return tags.join('\n ')
|
|
|
|
@ -313,27 +293,18 @@ async function renderHead(head: HeadConfig[]): Promise<string> {
|
|
|
|
|
|
|
|
|
|
|
|
function renderAttrs(attrs: Record<string, string>): string {
|
|
|
|
function renderAttrs(attrs: Record<string, string>): string {
|
|
|
|
return Object.keys(attrs)
|
|
|
|
return Object.keys(attrs)
|
|
|
|
.map((key) => {
|
|
|
|
.map((key) =>
|
|
|
|
if (isBooleanAttr(key)) return ` ${key}`
|
|
|
|
isBooleanAttr(key) ? ` ${key}` : ` ${key}="${escapeHtml(attrs[key])}"`
|
|
|
|
return ` ${key}="${escapeHtml(attrs[key] as string)}"`
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
.join('')
|
|
|
|
.join('')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function filterOutHeadDescription(head: HeadConfig[] = []) {
|
|
|
|
function filterOutHeadDescription(head: HeadConfig[] = []) {
|
|
|
|
return head.filter(([type, attrs]) => {
|
|
|
|
return head.filter(
|
|
|
|
return !(type === 'meta' && attrs?.name === 'description')
|
|
|
|
([type, attrs]) => !(type === 'meta' && attrs?.name === 'description')
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isDescriptionOverridden(head: HeadConfig[] = []) {
|
|
|
|
|
|
|
|
return head.some(([type, attrs]) => {
|
|
|
|
|
|
|
|
return type === 'meta' && attrs?.name === 'description'
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isMetaViewportOverridden(head: HeadConfig[] = []) {
|
|
|
|
function hasNamedMeta(head: HeadConfig[], name: string) {
|
|
|
|
return head.some(([type, attrs]) => {
|
|
|
|
return head.some(([type, attrs]) => type === 'meta' && attrs?.name === name)
|
|
|
|
return type === 'meta' && attrs?.name === 'viewport'
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|