@@ -30,9 +28,7 @@ const heroText = computed(() => frontmatter.value.heroText || site.value.title)
{{ heroText }}
-
- {{ frontmatter.tagline }}
-
+ {{ tagline }}
frontmatter.value.heroText || site.value.title)
}
}
-.description {
+.tagline {
margin: 0;
margin-top: 0.25rem;
line-height: 1.3;
@@ -108,7 +104,7 @@ const heroText = computed(() => frontmatter.value.heroText || site.value.title)
}
@media (min-width: 420px) {
- .description {
+ .tagline {
line-height: 1.2;
font-size: 1.6rem;
}
diff --git a/src/client/theme-default/components/NavLinks.vue b/src/client/theme-default/components/NavLinks.vue
index aaa622a1..bc221596 100644
--- a/src/client/theme-default/components/NavLinks.vue
+++ b/src/client/theme-default/components/NavLinks.vue
@@ -1,13 +1,13 @@
diff --git a/src/client/theme-default/composables/nav.ts b/src/client/theme-default/composables/nav.ts
index 06f8bae6..7c935721 100644
--- a/src/client/theme-default/composables/nav.ts
+++ b/src/client/theme-default/composables/nav.ts
@@ -1,57 +1,30 @@
import { computed } from 'vue'
-import { useRoute, useData, inBrowser } from 'vitepress'
+import { useData, useRoute } from 'vitepress'
import type { DefaultTheme } from '../config'
-export function useLocaleLinks() {
- const route = useRoute()
- const { site } = useData()
+export function useLanguageLinks() {
+ const { site, localePath, theme } = useData()
return computed(() => {
- const theme = site.value.themeConfig as DefaultTheme.Config
- const locales = theme.locales
+ const langs = site.value.langs
+ const localePaths = Object.keys(langs)
- if (!locales) {
+ // one language
+ if (localePaths.length < 2) {
return null
}
- const localeKeys = Object.keys(locales)
+ const route = useRoute()
- if (localeKeys.length <= 1) {
- return null
- }
-
- // handle site base
- const siteBase = inBrowser ? site.value.base : '/'
-
- const siteBaseWithoutSuffix = siteBase.endsWith('/')
- ? siteBase.slice(0, -1)
- : siteBase
-
- // remove site base in browser env
- const routerPath = route.path.slice(siteBaseWithoutSuffix.length)
-
- const currentLangBase = localeKeys.find((key) => {
- return key === '/' ? false : routerPath.startsWith(key)
- })
-
- const currentContentPath = currentLangBase
- ? routerPath.substring(currentLangBase.length - 1)
- : routerPath
-
- const candidates = localeKeys.map((v) => {
- const localePath = v.endsWith('/') ? v.slice(0, -1) : v
-
- return {
- text: locales[v].label,
- link: `${localePath}${currentContentPath}`
- }
- })
+ // intentionally remove the leading slash because each locale has one
+ const currentPath = route.path.replace(localePath.value, '')
- const currentLangKey = currentLangBase ? currentLangBase : '/'
+ const candidates = localePaths.map((localePath) => ({
+ text: langs[localePath].label,
+ link: `${localePath}${currentPath}`
+ }))
- const selectText = locales[currentLangKey].selectText
- ? locales[currentLangKey].selectText
- : 'Languages'
+ const selectText = theme.value.selectText || 'Languages'
return {
text: selectText,
diff --git a/src/client/theme-default/styles/code.css b/src/client/theme-default/styles/code.css
index 1e821af9..680d34cf 100644
--- a/src/client/theme-default/styles/code.css
+++ b/src/client/theme-default/styles/code.css
@@ -26,6 +26,7 @@ div[class*='language-'] {
li > div[class*='language-'] {
border-radius: 6px 0 0 6px;
margin: 1rem -1.5rem 1rem -1.25rem;
+ line-height: initial;
}
@media (min-width: 420px) {
diff --git a/src/node/alias.ts b/src/node/alias.ts
index 22d8d80f..f98ba565 100644
--- a/src/node/alias.ts
+++ b/src/node/alias.ts
@@ -2,12 +2,10 @@ import path from 'path'
import { Alias, AliasOptions } from 'vite'
const PKG_ROOT = path.join(__dirname, '../../')
-export const APP_PATH = path.join(__dirname, '../client/app')
-export const SHARED_PATH = path.join(__dirname, '../client/shared')
-export const DEFAULT_THEME_PATH = path.join(
- __dirname,
- '../client/theme-default'
-)
+export const DIST_CLIENT_PATH = path.join(__dirname, '../client')
+export const APP_PATH = path.join(DIST_CLIENT_PATH, 'app')
+export const SHARED_PATH = path.join(DIST_CLIENT_PATH, 'shared')
+export const DEFAULT_THEME_PATH = path.join(DIST_CLIENT_PATH, 'theme-default')
// special virtual file
// we can't directly import '/@siteData' because
diff --git a/src/node/build/build.ts b/src/node/build/build.ts
index 29fa53b2..a8934b1e 100644
--- a/src/node/build/build.ts
+++ b/src/node/build/build.ts
@@ -6,14 +6,22 @@ import { renderPage } from './render'
import { OutputChunk, OutputAsset } from 'rollup'
import ora from 'ora'
-export async function build(root: string, buildOptions: BuildOptions = {}) {
+export async function build(
+ root: string,
+ buildOptions: BuildOptions & { mpa?: string } = {}
+) {
const start = Date.now()
process.env.NODE_ENV = 'production'
const siteConfig = await resolveConfig(root)
+ if (buildOptions.mpa) {
+ siteConfig.mpa = true
+ delete buildOptions.mpa
+ }
+
try {
- const [clientResult, , pageToHashMap] = await bundle(
+ const { clientResult, serverResult, pageToHashMap } = await bundle(
siteConfig,
buildOptions
)
@@ -22,11 +30,15 @@ export async function build(root: string, buildOptions: BuildOptions = {}) {
spinner.start('rendering pages...')
try {
- const appChunk = clientResult.output.find(
- (chunk) => chunk.type === 'chunk' && chunk.isEntry
- ) as OutputChunk
+ const appChunk =
+ clientResult &&
+ (clientResult.output.find(
+ (chunk) => chunk.type === 'chunk' && chunk.isEntry
+ ) as OutputChunk)
- const cssChunk = clientResult.output.find(
+ const cssChunk = (
+ siteConfig.mpa ? serverResult : clientResult
+ ).output.find(
(chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css')
) as OutputAsset
diff --git a/src/node/build/buildMPAClient.ts b/src/node/build/buildMPAClient.ts
new file mode 100644
index 00000000..6d89d6cd
--- /dev/null
+++ b/src/node/build/buildMPAClient.ts
@@ -0,0 +1,46 @@
+import { build } from 'vite'
+import { SiteConfig } from '..'
+import { RollupOutput } from 'rollup'
+
+const virtualEntry = 'client.js'
+
+export async function buildMPAClient(
+ js: Record,
+ config: SiteConfig
+): Promise {
+ const files = Object.keys(js)
+ const themeFiles = files.filter((f) => !f.endsWith('.md'))
+ const pages = files.filter((f) => f.endsWith('.md'))
+
+ return build({
+ root: config.srcDir,
+ base: config.site.base,
+ logLevel: 'warn',
+ build: {
+ emptyOutDir: false,
+ outDir: config.outDir,
+ rollupOptions: {
+ input: [virtualEntry, ...pages]
+ }
+ },
+ plugins: [
+ {
+ name: 'vitepress-mpa-client',
+ resolveId(id) {
+ if (id === virtualEntry) {
+ return id
+ }
+ },
+ load(id) {
+ if (id === virtualEntry) {
+ return themeFiles
+ .map((file) => `import ${JSON.stringify(file)}`)
+ .join('\n')
+ } else if (id in js) {
+ return js[id]
+ }
+ }
+ }
+ ]
+ }) as Promise
+}
diff --git a/src/node/build/bundle.ts b/src/node/build/bundle.ts
index ec2e228d..bb736847 100644
--- a/src/node/build/bundle.ts
+++ b/src/node/build/bundle.ts
@@ -1,11 +1,13 @@
import ora from 'ora'
import path from 'path'
+import fs from 'fs-extra'
import { slash } from '../utils/slash'
import { APP_PATH } from '../alias'
import { SiteConfig } from '../config'
import { RollupOutput } from 'rollup'
import { build, BuildOptions, UserConfig as ViteUserConfig } from 'vite'
import { createVitePressPlugin } from '../plugin'
+import { buildMPAClient } from './buildMPAClient'
export const okMark = '\x1b[32m✓\x1b[0m'
export const failMark = '\x1b[31m✖\x1b[0m'
@@ -14,9 +16,14 @@ export const failMark = '\x1b[31m✖\x1b[0m'
export async function bundle(
config: SiteConfig,
options: BuildOptions
-): Promise<[RollupOutput, RollupOutput, Record]> {
+): Promise<{
+ clientResult: RollupOutput
+ serverResult: RollupOutput
+ pageToHashMap: Record
+}> {
const { root, srcDir } = config
const pageToHashMap = Object.create(null)
+ const clientJSMap = Object.create(null)
// define custom rollup input
// this is a multi-entry build - every page is considered an entry chunk
@@ -38,7 +45,13 @@ export async function bundle(
root: srcDir,
base: config.site.base,
logLevel: 'warn',
- plugins: createVitePressPlugin(root, config, ssr, pageToHashMap),
+ plugins: createVitePressPlugin(
+ root,
+ config,
+ ssr,
+ pageToHashMap,
+ clientJSMap
+ ),
// @ts-ignore
ssr: {
noExternal: ['vitepress']
@@ -64,12 +77,15 @@ export async function bundle(
if (!chunk.isEntry && /runtime/.test(chunk.name)) {
return `assets/framework.[hash].js`
}
- return `assets/[name].[hash].js`
+ return adComponentRE.test(chunk.name)
+ ? `assets/ui-custom.[hash].js`
+ : `assets/[name].[hash].js`
}
})
}
},
- minify: ssr ? false : !process.env.DEBUG
+ // minify with esbuild in MPA mode (for CSS)
+ minify: ssr ? (config.mpa ? 'esbuild' : false) : !process.env.DEBUG
}
})
@@ -80,7 +96,7 @@ export async function bundle(
spinner.start('building client + server bundles...')
try {
;[clientResult, serverResult] = await (Promise.all([
- build(resolveViteConfig(false)),
+ config.mpa ? null : build(resolveViteConfig(false)),
build(resolveViteConfig(true))
]) as Promise<[RollupOutput, RollupOutput]>)
} catch (e) {
@@ -93,5 +109,28 @@ export async function bundle(
symbol: okMark
})
- return [clientResult, serverResult, pageToHashMap]
+ if (config.mpa) {
+ // in MPA mode, we need to copy over the non-js asset files from the
+ // server build since there is no client-side build.
+ for (const chunk of serverResult.output) {
+ if (!chunk.fileName.endsWith('.js')) {
+ const tempPath = path.resolve(config.tempDir, chunk.fileName)
+ const outPath = path.resolve(config.outDir, chunk.fileName)
+ await fs.copy(tempPath, outPath)
+ }
+ }
+ // also copy over public dir
+ const publicDir = path.resolve(config.srcDir, 'public')
+ if (fs.existsSync(publicDir)) {
+ await fs.copy(publicDir, config.outDir)
+ }
+ // build `
+ fs.removeSync(path.resolve(config.outDir, matchingChunk.fileName))
+ } else {
+ inlinedScript = ``
+ }
+ }
+ }
+
const html = `
@@ -83,10 +108,17 @@ export async function renderPage(
${content}
-
-
+ ${
+ config.mpa
+ ? ''
+ : ``
+ }
+ ${
+ appChunk
+ ? ``
+ : ``
+ }
+ ${inlinedScript}