diff --git a/docs/components/ModalDemo.vue b/docs/components/ModalDemo.vue new file mode 100644 index 00000000..93dbab8f --- /dev/null +++ b/docs/components/ModalDemo.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/docs/config/app-configs.md b/docs/config/app-configs.md index c4cb66aa..73ee2d41 100644 --- a/docs/config/app-configs.md +++ b/docs/config/app-configs.md @@ -298,10 +298,47 @@ VitePress build hooks allow you to add new functionality and behaviors to your w - Sitemap - Search Indexing - PWA +- Teleports + +### buildEnd + +- Type: `(siteConfig: SiteConfig) => Awaitable` + +`buildEnd` is a build CLI hook, it will run after build (SSG) finish but before VitePress CLI process exits. + +```ts +export default { + async buildEnd(siteConfig) { + // ... + } +} +``` + +### postRender + +- Type: `(context: SSGContext) => Awaitable` + +`postRender` is a build hook, called when SSG rendering is done. It will allow you to handle the teleports content during SSG. + +```ts +export default { + async postRender(context) { + // ... + } +} +``` + +```ts +interface SSGContext { + content: string + teleports?: Record + [key: string]: any +} +``` ### transformHead -- Type: `(ctx: TransformContext) => Awaitable` +- Type: `(context: TransformContext) => Awaitable` `transformHead` is a build hook to transform the head before generating each page. It will allow you to add head entries that cannot be statically added to your VitePress config. You only need to return extra entries, they will be merged automatically with the existing ones. @@ -311,7 +348,7 @@ Don't mutate anything inside the `ctx`. ```ts export default { - async transformHead(ctx) { + async transformHead(context) { // ... } } @@ -367,17 +404,3 @@ export default { } } ``` - -### buildEnd - -- Type: `(siteConfig: SiteConfig) => Awaitable` - -`buildEnd` is a build CLI hook, it will run after build (SSG) finish but before VitePress CLI process exits. - -```ts -export default { - async buildEnd(siteConfig) { - // ... - } -} -``` diff --git a/docs/guide/using-vue.md b/docs/guide/using-vue.md index 43c75245..303e25d4 100644 --- a/docs/guide/using-vue.md +++ b/docs/guide/using-vue.md @@ -260,3 +260,27 @@ export default { **Also see:** - [Vue.js > Dynamic Components](https://vuejs.org/guide/essentials/component-basics.html#dynamic-components) + +## Using Teleports + +Vitepress currently has SSG support for teleports to body only. For other targets, you can wrap them inside the built-in `` component or inject the teleport markup into the correct location in your final page HTML through [`postRender` hook](../config/app-configs#postrender). + + + +::: details +<<< @/components/ModalDemo.vue +::: + +```md + + +
+ // ... +
+
+
+``` + + diff --git a/src/client/app/ssr.ts b/src/client/app/ssr.ts index a5eeb271..a66e6e47 100644 --- a/src/client/app/ssr.ts +++ b/src/client/app/ssr.ts @@ -1,9 +1,12 @@ // entry for SSR import { createApp } from './index.js' import { renderToString } from 'vue/server-renderer' +import type { SSGContext } from '../shared.js' export async function render(path: string) { const { app, router } = await createApp() await router.go(path) - return renderToString(app) + const ctx: SSGContext = { content: '' } + ctx.content = await renderToString(app, ctx) + return ctx } diff --git a/src/node/build/render.ts b/src/node/build/render.ts index a445435d..e695eec6 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -1,23 +1,24 @@ +import escape from 'escape-html' import fs from 'fs-extra' import path from 'path' +import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup' import { pathToFileURL } from 'url' -import escape from 'escape-html' import { normalizePath, transformWithEsbuild } from 'vite' -import type { RollupOutput, OutputChunk, OutputAsset } from 'rollup' +import { resolveSiteDataByRoute, type SiteConfig } from '../config' +import type { SSGContext } from '../shared' import { - type HeadConfig, - type PageData, createTitle, - notFoundPageData, - mergeHead, EXTERNAL_URL_RE, - sanitizeFileName + mergeHead, + notFoundPageData, + sanitizeFileName, + type HeadConfig, + type PageData } from '../shared' import { slash } from '../utils/slash' -import { type SiteConfig, resolveSiteDataByRoute } from '../config' export async function renderPage( - render: (path: string) => Promise, + render: (path: string) => Promise, config: SiteConfig, page: string, // foo.md result: RollupOutput | null, @@ -30,7 +31,8 @@ export async function renderPage( const siteData = resolveSiteDataByRoute(config.site, routePath) // render page - const content = await render(routePath) + const context = await render(routePath) + const { content, teleports } = (await config.postRender?.(context)) ?? context const pageName = sanitizeFileName(page.replace(/\//g, '_')) // server build doesn't need hash @@ -155,7 +157,7 @@ export async function renderPage( ${prefetchLinkString} ${await renderHead(head)} - + ${teleports?.body || ''}
${content}
${ config.mpa diff --git a/src/node/config.ts b/src/node/config.ts index 9c4a816d..bda1406e 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -1,28 +1,29 @@ -import path from 'path' +import type { Options as VuePluginOptions } from '@vitejs/plugin-vue' +import _debug from 'debug' +import fg from 'fast-glob' import fs from 'fs-extra' +import path from 'path' import c from 'picocolors' -import fg from 'fast-glob' import { - normalizePath, - type UserConfig as ViteConfig, + loadConfigFromFile, mergeConfig as mergeViteConfig, - loadConfigFromFile + normalizePath, + type UserConfig as ViteConfig } from 'vite' -import type { Options as VuePluginOptions } from '@vitejs/plugin-vue' +import type { SSGContext } from '../../types/shared' +import { DEFAULT_THEME_PATH } from './alias' +import type { MarkdownOptions } from './markdown/markdown' import { - type SiteData, - type HeadConfig, - type LocaleConfig, - type DefaultTheme, APPEARANCE_KEY, createLangDictionary, + type Awaitable, type CleanUrlsMode, + type DefaultTheme, + type HeadConfig, + type LocaleConfig, type PageData, - type Awaitable + type SiteData } from './shared' -import { DEFAULT_THEME_PATH } from './alias' -import type { MarkdownOptions } from './markdown/markdown' -import _debug from 'debug' export { resolveSiteDataByRoute } from './shared' @@ -104,12 +105,17 @@ export interface UserConfig { */ buildEnd?: (siteConfig: SiteConfig) => Awaitable + /** + * Render end hook: called when SSR rendering is done. + */ + postRender?: (context: SSGContext) => Awaitable + /** * Head transform hook: runs before writing HTML to dist. * * This build hook will allow you to modify the head adding new entries that cannot be statically added. */ - transformHead?: (ctx: TransformContext) => Awaitable + transformHead?: (context: TransformContext) => Awaitable /** * HTML transform hook: runs before writing HTML to dist. @@ -154,6 +160,7 @@ export interface SiteConfig | 'ignoreDeadLinks' | 'cleanUrls' | 'useWebFonts' + | 'postRender' | 'buildEnd' | 'transformHead' | 'transformHtml' @@ -250,6 +257,7 @@ export async function resolveConfig( useWebFonts: userConfig.useWebFonts ?? typeof process.versions.webcontainer === 'string', + postRender: userConfig.postRender, buildEnd: userConfig.buildEnd, transformHead: userConfig.transformHead, transformHtml: userConfig.transformHtml, diff --git a/src/shared/shared.ts b/src/shared/shared.ts index fb014740..7f8b6e1e 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -1,20 +1,21 @@ import type { - SiteData, - PageData, + HeadConfig, LocaleConfig, - HeadConfig + PageData, + SiteData } from '../../types/shared.js' export type { - SiteData, - PageData, + Awaitable, + CleanUrlsMode, + DefaultTheme, HeadConfig, - LocaleConfig, Header, - DefaultTheme, + LocaleConfig, + PageData, PageDataPayload, - CleanUrlsMode, - Awaitable + SiteData, + SSGContext } from '../../types/shared.js' export const EXTERNAL_URL_RE = /^[a-z]+:/i diff --git a/types/shared.d.ts b/types/shared.d.ts index 37435045..7085a1a7 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -1,4 +1,5 @@ // types shared between server and client +import type { SSRContext } from 'vue/server-renderer' export type { DefaultTheme } from './default-theme.js' export type Awaitable = T | PromiseLike @@ -107,3 +108,7 @@ export interface PageDataPayload { path: string pageData: PageData } + +export interface SSGContext extends SSRContext { + content: string +}