Skip to content

Commit

Permalink
feat: sitemap generation (#2691)
Browse files Browse the repository at this point in the history
  • Loading branch information
brc-dd authored Jul 27, 2023
1 parent b61f36d commit 5563695
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 49 deletions.
8 changes: 8 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export default defineConfig({
lastUpdated: true,
cleanUrls: true,

sitemap: {
hostname: 'https://vitepress.dev'
},

head: [
['meta', { name: 'theme-color', content: '#3c8772' }],
[
Expand Down Expand Up @@ -131,6 +135,10 @@ function sidebarGuide() {
{
text: 'MPA Mode',
link: '/guide/mpa-mode'
},
{
text: 'Sitemap Generation',
link: '/guide/sitemap-generation'
}
]
},
Expand Down
53 changes: 53 additions & 0 deletions docs/guide/sitemap-generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Sitemap Generation

VitePress comes with out-of-the-box support for generating a `sitemap.xml` file for your site. To enable it, add the following to your `.vitepress/config.js`:

```ts
import { defineConfig } from 'vitepress'

export default defineConfig({
sitemap: {
hostname: 'https://example.com'
}
})
```

To have `<lastmod>` tags in your `sitemap.xml`, you can enable the [`lastUpdated`](../reference/default-theme-last-updated) option.

## Options

Sitemap support is powered by the [`sitemap`](https://www.npmjs.com/package/sitemap) module. You can pass any options supported by it to the `sitemap` option in your config file. These will be passed directly to the `SitemapStream` constructor. Refer to the [`sitemap` documentation](https://www.npmjs.com/package/sitemap#options-you-can-pass) for more details. Example:

```ts
import { defineConfig } from 'vitepress'

export default defineConfig({
sitemap: {
hostname: 'https://example.com',
lastmodDateOnly: false
}
})
```

## `transformItems` Hook

You can use the `sitemap.transformItems` hook to modify the sitemap items before they are written to the `sitemap.xml` file. This hook is called with an array of sitemap items and expects an array of sitemap items to be returned. Example:

```ts
import { defineConfig } from 'vitepress'

export default defineConfig({
sitemap: {
hostname: 'https://example.com',
transformItems: (items) => {
// add new items or modify/filter existing items
items.push({
url: '/extra-page',
changefreq: 'monthly',
priority: 0.8
})
return items
}
}
})
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
"shiki-processor": "^0.1.3",
"simple-git-hooks": "^2.9.0",
"sirv": "^2.0.3",
"sitemap": "^7.1.1",
"supports-color": "^9.4.0",
"typescript": "^5.1.6",
"vitest": "^0.33.0",
Expand Down
32 changes: 32 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 5 additions & 14 deletions src/node/build/build.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createHash } from 'crypto'
import fs from 'fs-extra'
import { createRequire } from 'module'
import ora from 'ora'
import path from 'path'
import { packageDirectorySync } from 'pkg-dir'
import { rimraf } from 'rimraf'
Expand All @@ -11,7 +10,9 @@ import type { BuildOptions } from 'vite'
import { resolveConfig, type SiteConfig } from '../config'
import { slash, type HeadConfig } from '../shared'
import { deserializeFunctions, serializeFunctions } from '../utils/fnSerialize'
import { bundle, failMark, okMark } from './bundle'
import { task } from '../utils/task'
import { bundle } from './bundle'
import { generateSitemap } from './generateSitemap'
import { renderPage } from './render'

export async function build(
Expand Down Expand Up @@ -43,10 +44,7 @@ export async function build(
const entryPath = path.join(siteConfig.tempDir, 'app.js')
const { render } = await import(pathToFileURL(entryPath).toString())

const spinner = ora({ discardStdin: false })
spinner.start('rendering pages...')

try {
await task('rendering pages', async () => {
const appChunk =
clientResult &&
(clientResult.output.find(
Expand Down Expand Up @@ -118,14 +116,6 @@ export async function build(
)
)
)
} catch (e) {
spinner.stopAndPersist({
symbol: failMark
})
throw e
}
spinner.stopAndPersist({
symbol: okMark
})

// emit page hash map for the case where a user session is open
Expand All @@ -139,6 +129,7 @@ export async function build(
if (!process.env.DEBUG) await rimraf(siteConfig.tempDir)
}

await generateSitemap(siteConfig)
await siteConfig.buildEnd?.(siteConfig)

siteConfig.logger.info(
Expand Down
31 changes: 9 additions & 22 deletions src/node/build/bundle.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import ora from 'ora'
import path from 'path'
import fs from 'fs-extra'
import path from 'path'
import type { GetModuleInfo, RollupOutput } from 'rollup'
import { fileURLToPath } from 'url'
import {
build,
normalizePath,
type BuildOptions,
type UserConfig as ViteUserConfig
} from 'vite'
import type { GetModuleInfo, RollupOutput } from 'rollup'
import type { SiteConfig } from '../config'
import { APP_PATH } from '../alias'
import type { SiteConfig } from '../config'
import { createVitePressPlugin } from '../plugin'
import { sanitizeFileName, slash } from '../shared'
import { task } from '../utils/task'
import { buildMPAClient } from './buildMPAClient'
import { fileURLToPath } from 'url'
import { normalizePath } from 'vite'

export const okMark = '\x1b[32m✓\x1b[0m'
export const failMark = '\x1b[31m✖\x1b[0m'

// A list of default theme components that should only be loaded on demand.
const lazyDefaultThemeComponentsRE =
Expand Down Expand Up @@ -142,24 +139,14 @@ export async function bundle(
}
})

let clientResult: RollupOutput | null
let serverResult: RollupOutput
let clientResult!: RollupOutput | null
let serverResult!: RollupOutput

const spinner = ora({ discardStdin: false })
spinner.start('building client + server bundles...')
try {
await task('building client + server bundles', async () => {
clientResult = config.mpa
? null
: ((await build(await resolveViteConfig(false))) as RollupOutput)
serverResult = (await build(await resolveViteConfig(true))) as RollupOutput
} catch (e) {
spinner.stopAndPersist({
symbol: failMark
})
throw e
}
spinner.stopAndPersist({
symbol: okMark
})

if (config.mpa) {
Expand Down
60 changes: 60 additions & 0 deletions src/node/build/generateSitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import fs from 'fs-extra'
import path from 'path'
import {
SitemapStream,
type EnumChangefreq,
type Img,
type LinkItem,
type NewsItem
} from 'sitemap'
import type { SiteConfig } from '../config'
import { getGitTimestamp } from '../utils/getGitTimestamp'
import { task } from '../utils/task'

export async function generateSitemap(siteConfig: SiteConfig) {
if (!siteConfig.sitemap?.hostname) return

await task('generating sitemap', async () => {
let items: SitemapItem[] = await Promise.all(
siteConfig.pages.map(async (page) => {
//
let url = siteConfig.rewrites.map[page] || page
url = url.replace(/(^|\/)?index.md$/, '$1')
url = url.replace(/\.md$/, siteConfig.cleanUrls ? '' : '.html')

const lastmod = siteConfig.lastUpdated && (await getGitTimestamp(page))
return lastmod ? { url, lastmod } : { url }
})
)
items = items.sort((a, b) => a.url.localeCompare(b.url))
items = (await siteConfig.sitemap?.transformItems?.(items)) || items

const sitemapStream = new SitemapStream(siteConfig.sitemap)
const sitemapPath = path.join(siteConfig.outDir, 'sitemap.xml')
const writeStream = fs.createWriteStream(sitemapPath)

sitemapStream.pipe(writeStream)
items.forEach((item) => sitemapStream.write(item))
sitemapStream.end()
})
}

// ============================== Patched Types ===============================

export interface SitemapItem {
lastmod?: string | number | Date
changefreq?: `${EnumChangefreq}`
fullPrecisionPriority?: boolean
priority?: number
news?: NewsItem
expires?: string
androidLink?: string
ampLink?: string
url: string
video?: any
img?: string | Img | (string | Img)[]
links?: LinkItem[]
lastmodfile?: string | Buffer | URL
lastmodISO?: string
lastmodrealtime?: boolean
}
3 changes: 2 additions & 1 deletion src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ export async function resolveConfig(
transformHtml: userConfig.transformHtml,
transformPageData: userConfig.transformPageData,
rewrites,
userConfig
userConfig,
sitemap: userConfig.sitemap
}

// to be shared with content loaders
Expand Down
33 changes: 22 additions & 11 deletions src/node/siteConfig.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import {
type Awaitable,
type HeadConfig,
type LocaleConfig,
type LocaleSpecificConfig,
type PageData,
type SiteData,
type SSGContext
} from './shared'
import type { MarkdownOptions } from './markdown'
import type { Options as VuePluginOptions } from '@vitejs/plugin-vue'
import { type Logger, type UserConfig as ViteConfig } from 'vite'
import type { SitemapStreamOptions } from 'sitemap'
import type { Logger, UserConfig as ViteConfig } from 'vite'
import type { SitemapItem } from './build/generateSitemap'
import type { MarkdownOptions } from './markdown'
import type {
Awaitable,
HeadConfig,
LocaleConfig,
LocaleSpecificConfig,
PageData,
SSGContext,
SiteData
} from './shared'

export type RawConfigExports<ThemeConfig = any> =
| Awaitable<UserConfig<ThemeConfig>>
Expand Down Expand Up @@ -138,6 +140,14 @@ export interface UserConfig<ThemeConfig = any>
*/
rewrites?: Record<string, string>

/**
* @experimental
*/
sitemap?: SitemapStreamOptions & {
hostname: string
transformItems?: (items: SitemapItem[]) => Awaitable<SitemapItem[]>
}

/**
* Build end hook: called when SSG finish.
* @param siteConfig The resolved configuration.
Expand Down Expand Up @@ -192,6 +202,7 @@ export interface SiteConfig<ThemeConfig = any>
| 'transformHead'
| 'transformHtml'
| 'transformPageData'
| 'sitemap'
> {
root: string
srcDir: string
Expand Down
Loading

0 comments on commit 5563695

Please sign in to comment.