diff --git a/__tests__/e2e/.vitepress/config.ts b/__tests__/e2e/.vitepress/config.ts
index 84fc64f2..e4e503a6 100644
--- a/__tests__/e2e/.vitepress/config.ts
+++ b/__tests__/e2e/.vitepress/config.ts
@@ -86,6 +86,11 @@ const sidebar: DefaultTheme.Config['sidebar'] = {
export default defineConfig({
title: 'Example',
description: 'An example app using VitePress.',
+ markdown: {
+ image: {
+ lazyLoading: true
+ }
+ },
themeConfig: {
sidebar,
search: {
diff --git a/__tests__/e2e/markdown-extensions/index.md b/__tests__/e2e/markdown-extensions/index.md
index 36d6c028..47246c18 100644
--- a/__tests__/e2e/markdown-extensions/index.md
+++ b/__tests__/e2e/markdown-extensions/index.md
@@ -196,3 +196,7 @@ export default config
## Markdown File Inclusion with Range without End
+
+## Image Lazy Loading
+
+
\ No newline at end of file
diff --git a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts
index 42a89891..23b8b1a0 100644
--- a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts
+++ b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts
@@ -65,7 +65,7 @@ describe('Table of Contents', () => {
test('render toc', async () => {
const items = page.locator('#table-of-contents + nav ul li')
const count = await items.count()
- expect(count).toBe(35)
+ expect(count).toBe(36)
})
})
@@ -280,3 +280,10 @@ describe('Markdown File Inclusion', () => {
expect(await p.textContent()).not.toContain('title')
})
})
+
+describe('Image Lazy Loading', () => {
+ test('render loading="lazy" in the
tag', async () => {
+ const img = page.locator('#image-lazy-loading + p img')
+ expect(await img.getAttribute('loading')).toBe('lazy')
+ })
+})
diff --git a/docs/guide/markdown.md b/docs/guide/markdown.md
index 8cd9d4ed..ef3cc0e8 100644
--- a/docs/guide/markdown.md
+++ b/docs/guide/markdown.md
@@ -847,6 +847,21 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
| $\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} = \vec{\mathbf{0}}$ | curl of $\vec{\mathbf{E}}$ is proportional to the rate of change of $\vec{\mathbf{B}}$ |
| $\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} = 4 \pi \rho$ | _wha?_ |
+## Image Lazy Loading
+
+You can enable lazy loading for each image added via markdown by setting `lazyLoading` to `true` in your config file:
+
+```js
+export default {
+ markdown: {
+ image: {
+ // image lazy loading is disabled by default
+ lazyLoading: true
+ }
+ }
+}
+```
+
## Advanced Configuration
VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the Markdown renderer. A lot of the extensions above are implemented via custom plugins. You can further customize the `markdown-it` instance using the `markdown` option in `.vitepress/config.js`:
diff --git a/docs/reference/default-theme-config.md b/docs/reference/default-theme-config.md
index 8402dc08..fb6d30b5 100644
--- a/docs/reference/default-theme-config.md
+++ b/docs/reference/default-theme-config.md
@@ -406,6 +406,20 @@ export interface DocFooter {
Can be used to customize the dark mode switch label. This label is only displayed in the mobile view.
+## lightModeSwitchTitle
+
+- Type: `string`
+- Default: `Switch to light theme`
+
+Can be used to customize the light mode switch title that appears on hovering.
+
+## darkModeSwitchTitle
+
+- Type: `string`
+- Default: `Switch to dark theme`
+
+Can be used to customize the dark mode switch title that appears on hovering.
+
## sidebarMenuLabel
- Type: `string`
diff --git a/docs/reference/site-config.md b/docs/reference/site-config.md
index b5e124d2..1cf9d726 100644
--- a/docs/reference/site-config.md
+++ b/docs/reference/site-config.md
@@ -24,6 +24,62 @@ export default {
}
```
+:::details Dynamic (Async) Config
+
+If you need to dynamically generate the config, you can also default export a function. For example:
+
+```ts
+import { defineConfig } from 'vitepress'
+
+export default async () => defineConfig({
+ const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
+
+ return {
+ // app level config options
+ lang: 'en-US',
+ title: 'VitePress',
+ description: 'Vite & Vue powered static site generator.',
+
+ // theme level config options
+ themeConfig: {
+ sidebar: [
+ ...posts.map((post) => ({
+ text: post.name,
+ link: `/posts/${post.name}`
+ }))
+ ]
+ }
+ }
+})
+```
+
+You can also use top-level `await`. For example:
+
+```ts
+import { defineConfig } from 'vitepress'
+
+const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
+
+export default defineConfig({
+ // app level config options
+ lang: 'en-US',
+ title: 'VitePress',
+ description: 'Vite & Vue powered static site generator.',
+
+ // theme level config options
+ themeConfig: {
+ sidebar: [
+ ...posts.map((post) => ({
+ text: post.name,
+ link: `/posts/${post.name}`
+ }))
+ ]
+ }
+})
+```
+
+:::
+
### Config Intellisense
Using the `defineConfig` helper will provide TypeScript-powered intellisense for config options. Assuming your IDE supports it, this should work in both JavaScript and TypeScript.
diff --git a/src/client/app/data.ts b/src/client/app/data.ts
index 77cc80d3..ccca8123 100644
--- a/src/client/app/data.ts
+++ b/src/client/app/data.ts
@@ -89,7 +89,7 @@ export function initData(route: Route): VitePressData {
frontmatter: computed(() => route.data.frontmatter),
params: computed(() => route.data.params),
lang: computed(() => site.value.lang),
- dir: computed(() => site.value.dir),
+ dir: computed(() => route.data.frontmatter.dir || site.value.dir || 'ltr'),
localeIndex: computed(() => site.value.localeIndex || 'root'),
title: computed(() => {
return createTitle(site.value, route.data)
diff --git a/src/client/theme-default/components/VPSwitchAppearance.vue b/src/client/theme-default/components/VPSwitchAppearance.vue
index 16828e7e..f63d1ce4 100644
--- a/src/client/theme-default/components/VPSwitchAppearance.vue
+++ b/src/client/theme-default/components/VPSwitchAppearance.vue
@@ -5,14 +5,16 @@ import VPSwitch from './VPSwitch.vue'
import VPIconMoon from './icons/VPIconMoon.vue'
import VPIconSun from './icons/VPIconSun.vue'
-const { isDark } = useData()
+const { isDark, theme } = useData()
const toggleAppearance = inject('toggle-appearance', () => {
isDark.value = !isDark.value
})
const switchTitle = computed(() => {
- return isDark.value ? 'Switch to light theme' : 'Switch to dark theme'
+ return isDark.value
+ ? theme.value.lightModeSwitchTitle || 'Switch to light theme'
+ : theme.value.darkModeSwitchTitle || 'Switch to dark theme'
})
diff --git a/src/client/theme-default/styles/base.css b/src/client/theme-default/styles/base.css
index 5fb3a3cc..af2ed98a 100644
--- a/src/client/theme-default/styles/base.css
+++ b/src/client/theme-default/styles/base.css
@@ -39,7 +39,6 @@ body {
font-weight: 400;
color: var(--vp-c-text-1);
background-color: var(--vp-c-bg);
- direction: ltr;
font-synthesis: style;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
diff --git a/src/node/config.ts b/src/node/config.ts
index ee0e2e94..d4a76de6 100644
--- a/src/node/config.ts
+++ b/src/node/config.ts
@@ -104,7 +104,8 @@ export async function resolveConfig(
const { pages, dynamicRoutes, rewrites } = await resolvePages(
srcDir,
- userConfig
+ userConfig,
+ logger
)
const config: SiteConfig = {
diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts
index f93fd837..e64a1bde 100644
--- a/src/node/markdown/markdown.ts
+++ b/src/node/markdown/markdown.ts
@@ -23,7 +23,7 @@ import type { Logger } from 'vite'
import { containerPlugin, type ContainerOptions } from './plugins/containers'
import { highlight } from './plugins/highlight'
import { highlightLinePlugin } from './plugins/highlightLines'
-import { imagePlugin } from './plugins/image'
+import { imagePlugin, type Options as ImageOptions } from './plugins/image'
import { lineNumberPlugin } from './plugins/lineNumbers'
import { linkPlugin } from './plugins/link'
import { preWrapperPlugin } from './plugins/preWrapper'
@@ -166,6 +166,7 @@ export interface MarkdownOptions extends MarkdownIt.Options {
* @see https://vitepress.dev/guide/markdown#math-equations
*/
math?: boolean | any
+ image?: ImageOptions
}
export type MarkdownRenderer = MarkdownIt
@@ -198,7 +199,7 @@ export const createMarkdownRenderer = async (
.use(preWrapperPlugin, { hasSingleTheme })
.use(snippetPlugin, srcDir)
.use(containerPlugin, { hasSingleTheme }, options.container)
- .use(imagePlugin)
+ .use(imagePlugin, options.image)
.use(
linkPlugin,
{ target: '_blank', rel: 'noreferrer', ...options.externalLinks },
diff --git a/src/node/markdown/plugins/image.ts b/src/node/markdown/plugins/image.ts
index 06d1a360..d6f7eb5f 100644
--- a/src/node/markdown/plugins/image.ts
+++ b/src/node/markdown/plugins/image.ts
@@ -3,7 +3,15 @@
import type MarkdownIt from 'markdown-it'
import { EXTERNAL_URL_RE } from '../../shared'
-export const imagePlugin = (md: MarkdownIt) => {
+export interface Options {
+ /**
+ * Support native lazy loading for the `
` tag.
+ * @default false
+ */
+ lazyLoading?: boolean
+}
+
+export const imagePlugin = (md: MarkdownIt, { lazyLoading }: Options = {}) => {
const imageRule = md.renderer.rules.image!
md.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx]
@@ -12,6 +20,9 @@ export const imagePlugin = (md: MarkdownIt) => {
if (!/^\.?\//.test(url)) url = './' + url
token.attrSet('src', decodeURIComponent(url))
}
+ if (lazyLoading) {
+ token.attrSet('loading', 'lazy')
+ }
return imageRule(tokens, idx, options, env, self)
}
}
diff --git a/src/node/plugin.ts b/src/node/plugin.ts
index c079dad6..0c3daddc 100644
--- a/src/node/plugin.ts
+++ b/src/node/plugin.ts
@@ -268,7 +268,11 @@ export async function createVitePressPlugin(
if (file.endsWith('.md')) {
Object.assign(
siteConfig,
- await resolvePages(siteConfig.srcDir, siteConfig.userConfig)
+ await resolvePages(
+ siteConfig.srcDir,
+ siteConfig.userConfig,
+ siteConfig.logger
+ )
)
}
diff --git a/src/node/plugins/dynamicRoutesPlugin.ts b/src/node/plugins/dynamicRoutesPlugin.ts
index e42e8c13..cf9576ae 100644
--- a/src/node/plugins/dynamicRoutesPlugin.ts
+++ b/src/node/plugins/dynamicRoutesPlugin.ts
@@ -1,6 +1,7 @@
import {
loadConfigFromFile,
normalizePath,
+ type Logger,
type Plugin,
type ViteDevServer
} from 'vite'
@@ -13,7 +14,11 @@ import { resolveRewrites } from './rewritesPlugin'
export const dynamicRouteRE = /\[(\w+?)\]/g
-export async function resolvePages(srcDir: string, userConfig: UserConfig) {
+export async function resolvePages(
+ srcDir: string,
+ userConfig: UserConfig,
+ logger: Logger
+) {
// Important: fast-glob doesn't guarantee order of the returned files.
// We must sort the pages so the input list to rollup is stable across
// builds - otherwise different input order could result in different exports
@@ -39,7 +44,11 @@ export async function resolvePages(srcDir: string, userConfig: UserConfig) {
;(dynamicRouteRE.test(file) ? dynamicRouteFiles : pages).push(file)
})
- const dynamicRoutes = await resolveDynamicRoutes(srcDir, dynamicRouteFiles)
+ const dynamicRoutes = await resolveDynamicRoutes(
+ srcDir,
+ dynamicRouteFiles,
+ logger
+ )
pages.push(...dynamicRoutes.routes.map((r) => r.path))
const rewrites = resolveRewrites(pages, userConfig.rewrites)
@@ -141,7 +150,7 @@ export const dynamicRoutesPlugin = async (
if (!/\.md$/.test(ctx.file)) {
Object.assign(
config,
- await resolvePages(config.srcDir, config.userConfig)
+ await resolvePages(config.srcDir, config.userConfig, config.logger)
)
}
for (const id of mods) {
@@ -154,7 +163,8 @@ export const dynamicRoutesPlugin = async (
export async function resolveDynamicRoutes(
srcDir: string,
- routes: string[]
+ routes: string[],
+ logger: Logger
): Promise {
const pendingResolveRoutes: Promise[] = []
const routeFileToModulesMap: Record> = {}
@@ -170,7 +180,7 @@ export async function resolveDynamicRoutes(
const pathsFile = paths.find((p) => fs.existsSync(p))
if (pathsFile == null) {
- console.warn(
+ logger.warn(
c.yellow(
`Missing paths file for dynamic route ${route}: ` +
`a corresponding ${paths[0]} (or .ts/.mjs/.mts) file is needed.`
@@ -183,15 +193,15 @@ export async function resolveDynamicRoutes(
let mod = routeModuleCache.get(pathsFile)
if (!mod) {
try {
- mod = (await loadConfigFromFile({} as any, pathsFile)) as RouteModule
+ mod = (await loadConfigFromFile(
+ {} as any,
+ pathsFile,
+ undefined,
+ 'silent'
+ )) as RouteModule
routeModuleCache.set(pathsFile, mod)
- } catch (e) {
- console.warn(
- c.yellow(
- `Invalid paths file export in ${pathsFile}. ` +
- `Expects default export of an object with a "paths" property.`
- )
- )
+ } catch (e: any) {
+ logger.warn(`${c.yellow(`Failed to load ${pathsFile}:`)}\n${e.stack}`)
continue
}
}
@@ -210,7 +220,7 @@ export async function resolveDynamicRoutes(
const loader = mod!.config.paths
if (!loader) {
- console.warn(
+ logger.warn(
c.yellow(
`Invalid paths file export in ${pathsFile}. ` +
`Missing "paths" property from default export.`
diff --git a/types/default-theme.d.ts b/types/default-theme.d.ts
index c89551b0..157faa00 100644
--- a/types/default-theme.d.ts
+++ b/types/default-theme.d.ts
@@ -96,6 +96,16 @@ export namespace DefaultTheme {
*/
darkModeSwitchLabel?: string
+ /**
+ * @default 'Switch to light theme'
+ */
+ lightModeSwitchTitle?: string
+
+ /**
+ * @default 'Switch to dark theme'
+ */
+ darkModeSwitchTitle?: string
+
/**
* @default 'Menu'
*/