php-framework/docs/.vitepress/sidebar.js
Snider 632bca9111 feat(docs): restructure with PHP/Go framework sections
- Add auto-discovery sidebar with nested directory support
- Create packages index with search and grid layout
- Move framework docs to packages/php/
- Update nav: Guide | PHP | Go | Packages | Security

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 10:47:50 +00:00

178 lines
5.1 KiB
JavaScript

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
// Auto-discover packages from docs/packages/
// Each package folder should have an index.md
//
// Frontmatter options:
// title: "Page Title" - Used in sidebar
// sidebarTitle: "Short Title" - Override for sidebar (optional)
// order: 10 - Sort order (lower = first)
// collapsed: true - Start group collapsed (for directories)
export function getPackagesSidebar(docsDir) {
const packagesDir = path.join(docsDir, 'packages')
if (!fs.existsSync(packagesDir)) {
return {}
}
const sidebar = {}
const packages = fs.readdirSync(packagesDir, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => d.name)
.sort()
for (const pkg of packages) {
const pkgDir = path.join(packagesDir, pkg)
// Build sidebar tree recursively
const items = buildSidebarItems(pkgDir, `/packages/${pkg}`)
if (items.length === 0) continue
// Get package title from index.md
let packageTitle = formatTitle(pkg)
const indexPath = path.join(pkgDir, 'index.md')
if (fs.existsSync(indexPath)) {
const content = fs.readFileSync(indexPath, 'utf-8')
const { data } = matter(content)
if (data.title) {
packageTitle = data.title
} else {
const h1Match = content.match(/^#\s+(.+)$/m)
if (h1Match) packageTitle = h1Match[1]
}
}
sidebar[`/packages/${pkg}/`] = [
{
text: packageTitle,
items: items
}
]
}
return sidebar
}
// Recursively build sidebar items for a directory
function buildSidebarItems(dir, urlBase) {
const entries = fs.readdirSync(dir, { withFileTypes: true })
const items = []
// Process files first, then directories
const files = entries.filter(e => !e.isDirectory() && e.name.endsWith('.md'))
const dirs = entries.filter(e => e.isDirectory())
// Add markdown files
for (const file of files) {
const filePath = path.join(dir, file.name)
const content = fs.readFileSync(filePath, 'utf-8')
const { data } = matter(content)
let title = data.sidebarTitle || data.title
if (!title) {
const h1Match = content.match(/^#\s+(.+)$/m)
title = h1Match ? h1Match[1] : formatTitle(file.name.replace('.md', ''))
}
const isIndex = file.name === 'index.md'
items.push({
file: file.name,
text: isIndex ? 'Overview' : title,
link: isIndex ? `${urlBase}/` : `${urlBase}/${file.name.replace('.md', '')}`,
order: data.order ?? (isIndex ? -1 : 100)
})
}
// Add subdirectories as collapsed groups
for (const subdir of dirs) {
const subdirPath = path.join(dir, subdir.name)
const subdirUrl = `${urlBase}/${subdir.name}`
const subItems = buildSidebarItems(subdirPath, subdirUrl)
if (subItems.length === 0) continue
// Check for index.md in subdir for title/order
let groupTitle = formatTitle(subdir.name)
let groupOrder = 200
let collapsed = true
const indexPath = path.join(subdirPath, 'index.md')
if (fs.existsSync(indexPath)) {
const content = fs.readFileSync(indexPath, 'utf-8')
const { data } = matter(content)
if (data.sidebarTitle || data.title) {
groupTitle = data.sidebarTitle || data.title
} else {
const h1Match = content.match(/^#\s+(.+)$/m)
if (h1Match) groupTitle = h1Match[1]
}
if (data.order !== undefined) groupOrder = data.order
if (data.collapsed !== undefined) collapsed = data.collapsed
}
items.push({
text: groupTitle,
collapsed: collapsed,
items: subItems,
order: groupOrder
})
}
// Sort by order, then alphabetically
items.sort((a, b) => {
const orderA = a.order ?? 100
const orderB = b.order ?? 100
if (orderA !== orderB) return orderA - orderB
return a.text.localeCompare(b.text)
})
// Remove order from final output
return items.map(({ order, file, ...item }) => item)
}
// Get nav items for packages dropdown
export function getPackagesNav(docsDir) {
const packagesDir = path.join(docsDir, 'packages')
if (!fs.existsSync(packagesDir)) {
return []
}
return fs.readdirSync(packagesDir, { withFileTypes: true })
.filter(d => d.isDirectory())
.filter(d => fs.existsSync(path.join(packagesDir, d.name, 'index.md')))
.map(d => {
const indexPath = path.join(packagesDir, d.name, 'index.md')
const content = fs.readFileSync(indexPath, 'utf-8')
const { data } = matter(content)
let title = data.navTitle || data.title
if (!title) {
const h1Match = content.match(/^#\s+(.+)$/m)
title = h1Match ? h1Match[1] : formatTitle(d.name)
}
return {
text: title,
link: `/packages/${d.name}/`,
order: data.navOrder ?? 100
}
})
.sort((a, b) => {
if (a.order !== b.order) return a.order - b.order
return a.text.localeCompare(b.text)
})
.map(({ text, link }) => ({ text, link }))
}
// Convert kebab-case to Title Case
function formatTitle(str) {
return str
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}