Plugin Development
On this page 22
This guide covers how to create plugins to extend BunPress functionality.
Plugin Overview
BunPress plugins can:
- Extend or modify configuration
- Validate configuration options
- React to configuration changes
- Hook into build lifecycle events
Plugin Interface
A BunPress plugin implements the ConfigPlugin interface:
import type { ConfigPlugin, BunPressConfig } from '@stacksjs/bunpress'
const myPlugin: ConfigPlugin = {
name: 'my-plugin',
extendConfig(config) {
// Modify and return config
return config
},
validateConfig(config) {
// Return validation result
return { valid: true, errors: [], warnings: [] }
},
onConfigLoad(config) {
// Called when config is loaded
},
onConfigChange(newConfig, oldConfig) {
// Called when config changes
},
}
export default myPlugin
Creating a Plugin
Basic Plugin
A simple plugin that adds default metadata:
// plugins/meta-plugin.ts
import type { ConfigPlugin } from '@stacksjs/bunpress'
export const metaPlugin: ConfigPlugin = {
name: 'meta-defaults',
extendConfig(config) {
return {
...config,
markdown: {
...config.markdown,
meta: {
generator: 'BunPress',
viewport: 'width=device-width, initial-scale=1.0',
...config.markdown?.meta,
},
},
}
},
}
Validation Plugin
A plugin that validates configuration:
// plugins/validation-plugin.ts
import type { ConfigPlugin, ConfigValidationResult } from '@stacksjs/bunpress'
export const validationPlugin: ConfigPlugin = {
name: 'strict-validation',
validateConfig(config): ConfigValidationResult {
const errors: string[] = []
const warnings: string[] = []
// Check for required fields
if (!config.sitemap?.baseUrl) {
errors.push('sitemap.baseUrl is required for SEO')
}
if (!config.markdown?.title) {
warnings.push('No default title set, using fallback')
}
// Check nav structure
if (config.nav) {
for (const item of config.nav) {
if (!item.text) {
errors.push('Navigation items must have text')
}
if (!item.link && !item.items) {
errors.push(`Nav item "${item.text}" needs link or items`)
}
}
}
return {
valid: errors.length === 0,
errors,
warnings,
}
},
}
Lifecycle Plugin
A plugin that hooks into lifecycle events:
// plugins/analytics-plugin.ts
import type { ConfigPlugin } from '@stacksjs/bunpress'
export const analyticsPlugin: ConfigPlugin = {
name: 'analytics-logger',
onConfigLoad(config) {
console.log(`[Analytics] Site loaded: ${config.markdown?.title}`)
console.log(`[Analytics] Docs directory: ${config.docsDir}`)
},
onConfigChange(newConfig, oldConfig) {
if (newConfig.docsDir !== oldConfig.docsDir) {
console.log(`[Analytics] Docs directory changed`)
}
},
}
Registering Plugins
Add plugins to your configuration:
// bunpress.config.ts
import { metaPlugin } from './plugins/meta-plugin'
import { validationPlugin } from './plugins/validation-plugin'
import { analyticsPlugin } from './plugins/analytics-plugin'
export default {
plugins: [
metaPlugin,
validationPlugin,
analyticsPlugin,
],
// ... rest of config
}
Plugin Hooks
extendConfig
Called before configuration is finalized. Use this to add defaults or transform config:
extendConfig(config) {
// Add custom sidebar items
const sidebar = config.markdown?.sidebar || {}
return {
...config,
markdown: {
...config.markdown,
sidebar: {
...sidebar,
'/api/': [
{ text: 'Auto-generated API', link: '/api/' },
...generateApiSidebar(),
],
},
},
}
}
validateConfig
Called after config is loaded. Return validation results:
validateConfig(config): ConfigValidationResult {
const errors: string[] = []
const warnings: string[] = []
// Perform validation checks
if (config.fathom?.enabled && !config.fathom?.siteId) {
errors.push('Fathom siteId required when enabled')
}
return { valid: errors.length === 0, errors, warnings }
}
onConfigLoad
Called once when configuration is initially loaded:
async onConfigLoad(config) {
// Initialize resources
await initializeDatabase()
// Log startup info
console.log(`Starting with ${config.nav?.length || 0} nav items`)
}
onConfigChange
Called when configuration changes during development:
onConfigChange(newConfig, oldConfig) {
// Detect specific changes
if (newConfig.markdown?.toc?.enabled !== oldConfig.markdown?.toc?.enabled) {
console.log('TOC setting changed, rebuilding...')
}
}
Example Plugins
Auto-Sidebar Plugin
Automatically generates sidebar from file structure:
// plugins/auto-sidebar.ts
import { readdirSync, statSync } from 'fs'
import { join, basename, extname } from 'path'
import type { ConfigPlugin, SidebarItem } from '@stacksjs/bunpress'
function generateSidebar(dir: string, basePath: string = ''): SidebarItem[] {
const items: SidebarItem[] = []
const entries = readdirSync(dir)
for (const entry of entries) {
const fullPath = join(dir, entry)
const stat = statSync(fullPath)
if (stat.isDirectory()) {
items.push({
text: formatTitle(entry),
items: generateSidebar(fullPath, `${basePath}/${entry}`),
})
} else if (extname(entry) === '.md') {
const name = basename(entry, '.md')
if (name !== 'index') {
items.push({
text: formatTitle(name),
link: `${basePath}/${name}`,
})
}
}
}
return items
}
function formatTitle(name: string): string {
return name
.replace(/-/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase())
}
export const autoSidebarPlugin: ConfigPlugin = {
name: 'auto-sidebar',
extendConfig(config) {
const docsDir = config.docsDir || './docs'
const guidePath = join(docsDir, 'guide')
return {
...config,
markdown: {
...config.markdown,
sidebar: {
...config.markdown?.sidebar,
'/guide/': generateSidebar(guidePath, '/guide'),
},
},
}
},
}
Git Info Plugin
Adds git commit info to builds:
// plugins/git-info.ts
import { execSync } from 'child_process'
import type { ConfigPlugin } from '@stacksjs/bunpress'
function getGitInfo() {
try {
return {
commit: execSync('git rev-parse --short HEAD').toString().trim(),
branch: execSync('git rev-parse --abbrev-ref HEAD').toString().trim(),
date: execSync('git log -1 --format=%cd --date=short').toString().trim(),
}
} catch {
return null
}
}
export const gitInfoPlugin: ConfigPlugin = {
name: 'git-info',
extendConfig(config) {
const gitInfo = getGitInfo()
if (!gitInfo) {
return config
}
return {
...config,
markdown: {
...config.markdown,
meta: {
...config.markdown?.meta,
'git-commit': gitInfo.commit,
'git-branch': gitInfo.branch,
'build-date': gitInfo.date,
},
},
}
},
}
Social Cards Plugin
Generates Open Graph metadata:
// plugins/social-cards.ts
import type { ConfigPlugin } from '@stacksjs/bunpress'
export const socialCardsPlugin: ConfigPlugin = {
name: 'social-cards',
extendConfig(config) {
const baseUrl = config.sitemap?.baseUrl || ''
const title = config.markdown?.title || 'Documentation'
return {
...config,
markdown: {
...config.markdown,
meta: {
...config.markdown?.meta,
'og:type': 'website',
'og:title': title,
'og:site_name': title,
'og:image': `${baseUrl}/og-image.png`,
'twitter:card': 'summary_large_image',
'twitter:title': title,
'twitter:image': `${baseUrl}/og-image.png`,
},
},
}
},
}
Best Practices
Plugin Naming
Use descriptive, unique names:
const myPlugin: ConfigPlugin = {
name: 'my-company/feature-name', // Namespaced
// ...
}
Error Handling
Handle errors gracefully:
extendConfig(config) {
try {
// Potentially failing operation
const data = loadExternalData()
return { ...config, customData: data }
} catch (error) {
console.warn('[my-plugin] Failed to load data, using defaults')
return config
}
}
Async Operations
Use async hooks for I/O operations:
async onConfigLoad(config) {
// Async operations are supported
const remoteConfig = await fetchRemoteConfig()
console.log('Remote config loaded:', remoteConfig)
}
Type Safety
Use TypeScript for type-safe plugins:
import type {
ConfigPlugin,
BunPressConfig,
ConfigValidationResult,
} from '@stacksjs/bunpress'
const myPlugin: ConfigPlugin = {
name: 'type-safe-plugin',
extendConfig(config: BunPressConfig): BunPressConfig {
return {
...config,
// TypeScript validates this
}
},
validateConfig(config: BunPressConfig): ConfigValidationResult {
return {
valid: true,
errors: [],
warnings: [],
}
},
}