feat: add option to customize title template

close #303
pull/639/head
Kia King Ishii 3 years ago
parent 86b49dde6b
commit 67e77f7871

@ -45,7 +45,7 @@ export default {
- Type: `string` - Type: `string`
- Default: `VitePress` - Default: `VitePress`
Title for the site. This will be the suffix for all page titles, and displayed in the nav bar. Title for the site. This will be displayed in the nav bar. Also used as the suffix for all page titles unless `titleTemplate` is defined.
```ts ```ts
export default { export default {
@ -53,6 +53,21 @@ export default {
} }
``` ```
## titleTemplate
- Type: `string | boolean`
The suffix for the title. For example, if you set `title` as `VitePress` and set `titleTemplate` as `My Site`, the html title becomes `VitePress | My Site`.
Set `false` to disable the feature. If the option is `undefined`, then the value of `title` option will be used.
```ts
export default {
title: 'VitePress',
titleTemplate: 'Vite & Vue powered static site generator.'
}
```
## description ## description
- Type: `string` - Type: `string`

@ -27,6 +27,19 @@ title: VitePress
--- ---
``` ```
## titleTemplate
- Type: `string | boolean`
The suffix for the title. It's same as [config.titleTemplate](../config/app-configs#titleTemplate), and it overrides the app config.
```yaml
---
title: VitePress,
titleTemplate: Vite & Vue powered static site generator.
---
```
## description ## description
- Type: `string` - Type: `string`

@ -1,6 +1,9 @@
--- ---
layout: home layout: home
title: VitePress
titleTemplate: Vite & Vue powered static site generator.
hero: hero:
name: VitePress name: VitePress
text: Vite & Vue powered static site generator. text: Vite & Vue powered static site generator.

@ -1,5 +1,5 @@
import { watchEffect, Ref } from 'vue' import { watchEffect, Ref } from 'vue'
import { HeadConfig, SiteData } from '../../shared' import { HeadConfig, SiteData, createTitle } from '../../shared'
import { Route } from '../router' import { Route } from '../router'
export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) { export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
@ -56,12 +56,12 @@ export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
watchEffect(() => { watchEffect(() => {
const pageData = route.data const pageData = route.data
const siteData = siteDataByRouteRef.value const siteData = siteDataByRouteRef.value
const pageTitle = pageData && pageData.title
const pageDescription = pageData && pageData.description const pageDescription = pageData && pageData.description
const frontmatterHead = pageData && pageData.frontmatter.head const frontmatterHead = pageData && pageData.frontmatter.head
// update title and description // update title and description
document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title document.title = createTitle(siteData, pageData)
document document
.querySelector(`meta[name=description]`)! .querySelector(`meta[name=description]`)!
.setAttribute('content', pageDescription || siteData.description) .setAttribute('content', pageDescription || siteData.description)

@ -1,7 +1,12 @@
import { InjectionKey, Ref, shallowRef, readonly, computed, inject } from 'vue' import { InjectionKey, Ref, shallowRef, readonly, computed, inject } from 'vue'
import { Route } from './router' import { Route } from './router'
import serializedSiteData from '@siteData' import serializedSiteData from '@siteData'
import { resolveSiteDataByRoute, PageData, SiteData } from '../shared' import {
PageData,
SiteData,
resolveSiteDataByRoute,
createTitle
} from '../shared'
import { withBase } from './utils' import { withBase } from './utils'
export const dataSymbol: InjectionKey<VitePressData> = Symbol() export const dataSymbol: InjectionKey<VitePressData> = Symbol()
@ -54,9 +59,7 @@ export function initData(route: Route): VitePressData {
return withBase(path || '/') return withBase(path || '/')
}), }),
title: computed(() => { title: computed(() => {
return route.data.title return createTitle(site.value, route.data)
? route.data.title + ' | ' + site.value.title
: site.value.title
}), }),
description: computed(() => { description: computed(() => {
return route.data.description || site.value.description return route.data.description || site.value.description

@ -1,7 +1,7 @@
import path from 'path' import path from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
import { SiteConfig, resolveSiteDataByRoute } from '../config' import { SiteConfig, resolveSiteDataByRoute } from '../config'
import { HeadConfig } from '../shared' import { HeadConfig, createTitle } from '../shared'
import { normalizePath, transformWithEsbuild } from 'vite' import { normalizePath, transformWithEsbuild } from 'vite'
import { RollupOutput, OutputChunk, OutputAsset } from 'rollup' import { RollupOutput, OutputChunk, OutputAsset } from 'rollup'
import { slash } from '../utils/slash' import { slash } from '../utils/slash'
@ -92,11 +92,7 @@ export async function renderPage(
? `<link rel="stylesheet" href="${siteData.base}${cssChunk.fileName}">` ? `<link rel="stylesheet" href="${siteData.base}${cssChunk.fileName}">`
: '' : ''
const title: string = const title: string = createTitle(siteData, pageData)
pageData.title && pageData.title !== 'Home'
? `${pageData.title} | ${siteData.title}`
: siteData.title
const description: string = pageData.description || siteData.description const description: string = pageData.description || siteData.description
const head = addSocialTags( const head = addSocialTags(

@ -32,6 +32,7 @@ export interface UserConfig<ThemeConfig = any> {
base?: string base?: string
lang?: string lang?: string
title?: string title?: string
titleTemplate?: string | boolean
description?: string description?: string
head?: HeadConfig[] head?: HeadConfig[]
appearance?: boolean appearance?: boolean
@ -248,6 +249,7 @@ export async function resolveSiteData(
return { return {
lang: userConfig.lang || 'en-US', lang: userConfig.lang || 'en-US',
title: userConfig.title || 'VitePress', title: userConfig.title || 'VitePress',
titleTemplate: userConfig.titleTemplate,
description: userConfig.description || 'A VitePress site', description: userConfig.description || 'A VitePress site',
base: userConfig.base ? userConfig.base.replace(/([^/])$/, '$1/') : '/', base: userConfig.base ? userConfig.base.replace(/([^/])$/, '$1/') : '/',
head: resolveSiteDataHead(userConfig), head: resolveSiteDataHead(userConfig),

@ -134,6 +134,7 @@ export function createMarkdownToVueRenderFn(
const pageData: PageData = { const pageData: PageData = {
title: inferTitle(frontmatter, content), title: inferTitle(frontmatter, content),
titleTemplate: frontmatter.titleTemplate,
description: inferDescription(frontmatter), description: inferDescription(frontmatter),
frontmatter, frontmatter,
headers: data.headers || [], headers: data.headers || [],
@ -197,17 +198,17 @@ function genPageDataCode(tags: string[], data: PageData) {
return tags return tags
} }
const inferTitle = (frontmatter: any, content: string) => { const inferTitle = (frontmatter: Record<string, any>, content: string) => {
if (frontmatter.title) { if (frontmatter.title) {
return deeplyParseHeader(frontmatter.title) return deeplyParseHeader(frontmatter.title)
} }
if (frontmatter.home) {
return 'Home'
}
const match = content.match(/^\s*#+\s+(.*)/m) const match = content.match(/^\s*#+\s+(.*)/m)
if (match) { if (match) {
return deeplyParseHeader(match[1].trim()) return deeplyParseHeader(match[1].trim())
} }
return '' return ''
} }

@ -1,4 +1,4 @@
import { LocaleConfig, SiteData } from '../../types/shared' import { SiteData, PageData, LocaleConfig } from '../../types/shared'
export type { export type {
SiteData, SiteData,
@ -83,6 +83,36 @@ export function resolveSiteDataByRoute(
}) })
} }
/**
* Create the page title string based on configs.
*/
export function createTitle(siteData: SiteData, pageData: PageData): string {
const title = pageData.title || siteData.title
const template = pageData.titleTemplate ?? siteData.titleTemplate
const templateString = createTitleTemplate(siteData.title, template)
return `${title}${templateString}`
}
function createTitleTemplate(
siteTitle: string,
template?: string | boolean
): string {
if (template === false) {
return ''
}
if (template === true || template === undefined) {
return ` | ${siteTitle}`
}
if (siteTitle === template) {
return ''
}
return ` | ${template}`
}
/** /**
* Clean up the route by removing the `base` path if it's set in config. * Clean up the route by removing the `base` path if it's set in config.
*/ */

3
types/shared.d.ts vendored

@ -5,6 +5,7 @@ export { DefaultTheme } from './default-theme'
export interface PageData { export interface PageData {
relativePath: string relativePath: string
title: string title: string
titleTemplate?: string | boolean
description: string description: string
headers: Header[] headers: Header[]
frontmatter: Record<string, any> frontmatter: Record<string, any>
@ -28,6 +29,7 @@ export interface SiteData<ThemeConfig = any> {
lang: string lang: string
title: string title: string
titleTemplate?: string | boolean
description: string description: string
head: HeadConfig[] head: HeadConfig[]
appearance: boolean appearance: boolean
@ -64,6 +66,7 @@ export type HeadConfig =
export interface LocaleConfig { export interface LocaleConfig {
lang: string lang: string
title?: string title?: string
titleTemplate?: string | boolean
description?: string description?: string
head?: HeadConfig[] head?: HeadConfig[]
label?: string label?: string

Loading…
Cancel
Save