From 040cd5448a25a0c1aefe13600b4dd4ab4717e3c1 Mon Sep 17 00:00:00 2001 From: Jasper Smet Date: Sun, 15 Mar 2026 16:11:39 +0100 Subject: [PATCH 1/3] initial vitepress redistributable theme + asset syncing configuration --- .vitepress/config.js | 97 ++-------------------- README.md | 188 ++++++++++++++++++++++--------------------- bin/cakedocs.js | 68 ++++++++++++++++ config/index.js | 165 +++++++++++++++++++++++++++++++++++++ package.json | 22 ++++- 5 files changed, 355 insertions(+), 185 deletions(-) create mode 100755 bin/cakedocs.js create mode 100644 config/index.js diff --git a/.vitepress/config.js b/.vitepress/config.js index 0799e1a..6fb1c0c 100644 --- a/.vitepress/config.js +++ b/.vitepress/config.js @@ -1,94 +1,7 @@ -import { defineConfig } from 'vitepress' -import { substitutionsReplacer } from './plugins/substitutions-replacer.js' -import { deepMerge, loadConfigOverrides, applyBaseToHeadTags } from './utils.js' +// This config is used for developing/previewing the skeleton itself. +// Consumer projects use: import baseConfig from '@cakephp/docs-skeleton/config' +import baseConfig from '../config/index.js' -const defaultConfig = { - srcDir: 'docs', - title: 'CakePHP', - description: 'CakePHP Documentation - The rapid development PHP framework', - ignoreDeadLinks: true, - substitutions: { - '|phpversion|': { value: '8.4', format: 'bold' }, - '|minphpversion|': { value: '8.1', format: 'italic' }, - // Add more substitutions here as needed - // '|cakeversion|': { value: '5.1', format: 'bold' }, - // '|projectname|': 'CakePHP', // Simple string without formatting - }, - head: [ - ['link', { rel: 'icon', type: 'image/png', href: '/favicon/favicon-96x96.png', sizes: '96x96' }], - ['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon/favicon.svg' }], - ['link', { rel: 'shortcut icon', href: '/favicon/favicon.ico' }], - ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/favicon/apple-touch-icon.png' }], - ['meta', { name: 'apple-mobile-web-app-title', content: 'CakePHP' }], - ['link', { rel: 'manifest', href: '/favicon/site.webmanifest' }], - [ - 'script', - { - 'data-collect-dnt': 'true', - async: 'true', - src: 'https://scripts.simpleanalyticscdn.com/latest.js' - } - ], - [ - 'script', - { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-MD3J6G9QVX' } - ], - [ - 'script', - {}, - `window.dataLayer = window.dataLayer || []; - function gtag(){dataLayer.push(arguments);} - gtag('js', new Date()); - gtag('config', 'G-MD3J6G9QVX', { 'anonymize_ip': true });` - ] - ], - themeConfig: { - logo: '/logo.svg', - outline: { - level: [2, 3], - }, - search: { - provider: 'local', - }, - footer: { - message: 'Released under the MIT License.', - copyright: 'Copyright © Cake Software Foundation, Inc. All rights reserved.' - }, - lastUpdated: { - text: 'Updated at', - formatOptions: { - dateStyle: 'full', - timeStyle: 'medium' - } - }, - versionBanner: false - }, - build: { - rollupOptions: { - output: { - manualChunks: { - 'framework': ['vue'] - } - } - } - }, - markdown: { - lineNumbers: true, - }, - locales: {} +export default { + extends: baseConfig, } - -const overrides = await loadConfigOverrides(import.meta.url) -const mergedConfig = deepMerge(defaultConfig, overrides) - -// Configure markdown plugins after mergedConfig is available -mergedConfig.markdown.config = (md) => { - md.use(substitutionsReplacer, { substitutions: mergedConfig.substitutions || {} }) -} - -// Apply base path to head tags if base is specified -if (overrides.base) { - applyBaseToHeadTags(mergedConfig, overrides.base) -} - -export default defineConfig(mergedConfig) diff --git a/README.md b/README.md index 429bbd7..c14c2c4 100644 --- a/README.md +++ b/README.md @@ -1,154 +1,158 @@ -# CakePHP Documentation Skeleton +# CakePHP Documentation Theme -A [VitePress](https://vitepress.dev/) based documentation skeleton for creating CakePHP branded documentation sites. +A distributable [VitePress](https://vitepress.dev/) theme for CakePHP documentation sites. Install it as an npm dependency — it provides the CakePHP-branded theme, default config, and shared assets. ## Features -- 🎨 CakePHP branded theme and styling -- ⚙️ Easy configuration overrides +- CakePHP branded theme extending VitePress default (custom fonts, colors, components) +- Base config with sensible defaults (favicon, analytics, search, footer) +- Text substitutions for version placeholders (`|phpversion|`, etc.) +- Version banner component for outdated docs +- Public asset syncing (favicon, fonts, icons, logo) -## Getting Started +## Using in a docs project -### Installation +### 1. Install ```bash -npm install +npm install @cakephp/docs-skeleton vitepress ``` -### Development - -Start the development server: +### 2. Scaffold boilerplate ```bash -npm run docs:dev +npx cakedocs init ``` -The documentation will be available at `http://localhost:5173` +This creates two files: -### Building for Production +| File | Purpose | +|---|---| +| `.vitepress/config.js` | Extends the skeleton's base config | +| `.vitepress/theme/index.js` | Re-exports the CakePHP theme | -Build the static site: +### 3. Configure -```bash -npm run docs:build -``` +Edit `.vitepress/config.js` — your overrides are merged with the base config via VitePress `extends`: -Preview the production build: +```javascript +import baseConfig from '@cakephp/docs-skeleton/config' -```bash -npm run docs:preview +export default { + extends: baseConfig, + base: '/5.x/', + themeConfig: { + sidebar: [], + nav: [], + socialLinks: [ + { icon: 'github', link: 'https://github.com/cakephp/cakephp' }, + ], + }, + substitutions: { + '|phpversion|': { value: '8.4', format: 'bold' }, + '|minphpversion|': { value: '8.1', format: 'italic' }, + }, +} ``` -## Configuration +### 4. Add scripts to `package.json` + +```json +{ + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + } +} +``` -### Default Configuration +### 5. Run -The default VitePress configuration is located in `.vitepress/config.js`. This file contains all the base settings for your documentation site. +```bash +npm run docs:dev +``` -For detailed information about VitePress configuration options, please refer to the [VitePress Configuration Reference](https://vitepress.dev/reference/site-config). +### Project structure -### Custom Overrides +``` +your-project/ +├── .vitepress/ +│ ├── config.js ← extends base config +│ └── theme/ +│ └── index.js ← re-exports theme +├── docs/ +│ └── index.md +└── package.json +``` -To customize the configuration without modifying the core files: +--- -1. Copy `config.default.js` to `config.js` in the project root -2. Add your configuration overrides to the exported object -3. Your overrides will be deep merged with the default configuration +## Extending the theme -**Example** (`config.js`): +The theme re-export in `.vitepress/theme/index.js` can be customized: ```javascript +import CakephpTheme from '@cakephp/docs-skeleton' +import { h } from 'vue' +import MyBanner from './components/MyBanner.vue' + export default { - title: 'My Plugin Documentation', - themeConfig: { - sidebar: { - '/': [ - { text: 'Home', link: '/' }, - { text: 'Guide', link: '/guide' } - ] - } + extends: CakephpTheme, + Layout() { + return h(CakephpTheme.Layout, null, { + 'layout-top': () => h(MyBanner), + }) } } ``` -### Version Banner +## Text Substitutions -You can display an e.g. "Outdated Version" banner on your documentation site by adding the following configuration: +Define placeholders replaced automatically in all Markdown: ```javascript export default { - themeConfig: { - versionBanner: { - message: 'You are viewing an older version of this documentation.', - link: '/latest/', - linkText: 'Go to latest docs.' - } + extends: baseConfig, + substitutions: { + '|phpversion|': { value: '8.4', format: 'bold' }, + '|minphpversion|': { value: '8.1', format: 'italic' }, + '|cakeversion|': '5.2', } } ``` -## Writing Documentation - -### Content Location +In Markdown: `Requires PHP |phpversion| or higher.` renders as: Requires PHP **8.4** or higher. -All markdown documentation files should be placed in the `docs/` directory. - -### Text Substitutions - -You can use placeholders in your markdown files that will be automatically replaced with configured values. This is useful for version numbers or other values that need to be updated across multiple files. - -**Configuration** (`config.js`): +## Version Banner ```javascript export default { - substitutions: { - '|phpversion|': { value: '8.4', format: 'bold' }, - '|minphpversion|': { value: '8.1', format: 'italic' }, - '|myversion|': '1.0.0' // Simple string without formatting + extends: baseConfig, + themeConfig: { + versionBanner: { + message: 'You are viewing docs for an older version.', + link: '/latest/', + linkText: 'Go to latest docs.' + } } } ``` -**Usage in Markdown**: +--- -```markdown -This plugin requires PHP |phpversion| or higher (minimum |minphpversion|). -``` - -**Result**: This plugin requires PHP **8.4** or higher (minimum *8.1*). - -## Project Structure - -``` -. -├── .vitepress/ # VitePress configuration -│ ├── config.js # Main VitePress config (defaults) -│ ├── utils.js # Utility functions -│ ├── theme/ # Custom theme components -│ └── plugins/ # Markdown-it plugins -├── docs/ # Documentation content (markdown files) -├── config.js # Your configuration overrides (create from config.default.js) -└── config.default.js # Template for configuration overrides -``` +## Skeleton development -## Linting - -Lint your configuration and scripts: +To work on the skeleton theme itself: ```bash +npm install +npm run docs:dev # preview at http://localhost:5173 +npm run docs:build npm run lint -``` - -Auto-fix linting issues: - -```bash npm run lint:fix ``` ## License Licensed under The MIT License. For full copyright and license information, please see the [LICENSE](LICENSE) file. - -## About CakePHP - -CakePHP is a rapid development framework for PHP which uses commonly known design patterns like Associative Data Mapping, Front Controller, and MVC. Learn more at [https://cakephp.org](https://cakephp.org) diff --git a/bin/cakedocs.js b/bin/cakedocs.js new file mode 100755 index 0000000..26e159f --- /dev/null +++ b/bin/cakedocs.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node +import { existsSync, mkdirSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' + +const [, , command] = process.argv + +const VITEPRESS_CONFIG_TEMPLATE = `import baseConfig from '@cakephp/docs-skeleton/config' + +export default { + extends: baseConfig, + + // Your overrides here + themeConfig: { + socialLinks: [ + { icon: 'github', link: 'https://github.com/cakephp/cakephp' }, + ], + editLink: { + pattern: 'https://github.com/cakephp/docs/edit/main/docs/:path', + text: 'Edit this page on GitHub', + }, + sidebar: [], + nav: [], + }, +} +` + +const THEME_INDEX_TEMPLATE = `export { default } from '@cakephp/docs-skeleton' +` + +function writeIfMissing(filePath, content, label) { + if (existsSync(filePath)) { + console.log(` skip ${label} (already exists)`) + return + } + mkdirSync(dirname(filePath), { recursive: true }) + writeFileSync(filePath, content, 'utf8') + console.log(` create ${label}`) +} + +function runInit() { + const cwd = process.cwd() + console.log(`Scaffolding @cakephp/docs-skeleton in ${cwd}\n`) + + writeIfMissing(join(cwd, '.vitepress', 'config.js'), VITEPRESS_CONFIG_TEMPLATE, '.vitepress/config.js') + writeIfMissing(join(cwd, '.vitepress', 'theme', 'index.js'), THEME_INDEX_TEMPLATE, '.vitepress/theme/index.js') + + console.log(` +Done! Next steps: + + 1. Add your docs to docs/ + 2. Edit .vitepress/config.js to set your sidebar, nav, and other options + 3. Run: npx vitepress dev +`) +} + +switch (command) { + case 'init': + runInit() + break + default: + console.log(` +Usage: cakedocs + +Commands: + init Scaffold .vitepress/config.js and .vitepress/theme/index.js +`) + process.exit(command ? 1 : 0) +} diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..c7b38f0 --- /dev/null +++ b/config/index.js @@ -0,0 +1,165 @@ +import { substitutionsReplacer } from '../.vitepress/plugins/substitutions-replacer.js' +import { applyBaseToHeadTags } from '../.vitepress/utils.js' +import { fileURLToPath } from 'node:url' +import path from 'node:path' +import fs from 'node:fs' + +const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const packagePublicDir = path.join(packageRoot, 'docs', 'public') +const packageThemeDir = path.join(packageRoot, '.vitepress', 'theme') +const skeletonPublicDirectories = ['favicon', 'fonts', 'icons'] +const skeletonPublicFiles = ['logo.svg'] + +function copyDirectoryIfMissing(sourceDir, destinationDir) { + if (!fs.existsSync(sourceDir)) { + return + } + + fs.mkdirSync(destinationDir, { recursive: true }) + + for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { + const sourcePath = path.join(sourceDir, entry.name) + const destinationPath = path.join(destinationDir, entry.name) + + if (entry.isDirectory()) { + copyDirectoryIfMissing(sourcePath, destinationPath) + continue + } + + if (fs.existsSync(destinationPath)) { + continue + } + + fs.mkdirSync(path.dirname(destinationPath), { recursive: true }) + fs.copyFileSync(sourcePath, destinationPath) + } +} + +function copyFileIfMissing(sourcePath, destinationPath) { + if (!fs.existsSync(sourcePath) || fs.existsSync(destinationPath)) { + return + } + + fs.mkdirSync(path.dirname(destinationPath), { recursive: true }) + fs.copyFileSync(sourcePath, destinationPath) +} + +function syncSkeletonPublicAssets(consumerPublicDir) { + for (const directoryName of skeletonPublicDirectories) { + copyDirectoryIfMissing( + path.join(packagePublicDir, directoryName), + path.join(consumerPublicDir, directoryName) + ) + } + + for (const fileName of skeletonPublicFiles) { + copyFileIfMissing( + path.join(packagePublicDir, fileName), + path.join(consumerPublicDir, fileName) + ) + } +} + +function createSkeletonPublicAssetsPlugin() { + return { + name: 'cakephp-docs-skeleton-public-assets', + configResolved(config) { + const publicDir = config.publicDir + if (publicDir) { + syncSkeletonPublicAssets(publicDir) + } + } + } +} + +/** + * Base VitePress config for CakePHP documentation. + * + * Use via `extends` in your .vitepress/config.js: + * + * import baseConfig from '@cakephp/docs-skeleton/config' + * export default { extends: baseConfig, ... } + */ +const baseConfig = { + srcDir: 'docs', + title: 'CakePHP', + description: 'CakePHP Documentation - The rapid development PHP framework', + ignoreDeadLinks: true, + substitutions: { + '|phpversion|': { value: '8.4', format: 'bold' }, + '|minphpversion|': { value: '8.1', format: 'italic' }, + }, + head: [ + ['link', { rel: 'icon', type: 'image/png', href: '/favicon/favicon-96x96.png', sizes: '96x96' }], + ['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon/favicon.svg' }], + ['link', { rel: 'shortcut icon', href: '/favicon/favicon.ico' }], + ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/favicon/apple-touch-icon.png' }], + ['meta', { name: 'apple-mobile-web-app-title', content: 'CakePHP' }], + ['link', { rel: 'manifest', href: '/favicon/site.webmanifest' }], + [ + 'script', + { + 'data-collect-dnt': 'true', + async: 'true', + src: 'https://scripts.simpleanalyticscdn.com/latest.js' + } + ], + [ + 'script', + { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-MD3J6G9QVX' } + ], + [ + 'script', + {}, + `window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', 'G-MD3J6G9QVX', { 'anonymize_ip': true });` + ] + ], + themeConfig: { + logo: '/logo.svg', + outline: { + level: [2, 3], + }, + search: { + provider: 'local', + }, + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © Cake Software Foundation, Inc. All rights reserved.' + }, + lastUpdated: { + text: 'Updated at', + formatOptions: { + dateStyle: 'full', + timeStyle: 'medium' + } + }, + versionBanner: false + }, + build: { + rollupOptions: { + output: { + manualChunks: { + 'framework': ['vue'] + } + } + } + }, + markdown: { + lineNumbers: true, + config: (md) => { + md.use(substitutionsReplacer, { substitutions: baseConfig.substitutions || {} }) + } + }, + locales: {}, + themeDir: packageThemeDir, + vite: { + plugins: [ + createSkeletonPublicAssetsPlugin() + ] + } +} + +export default baseConfig diff --git a/package.json b/package.json index 5cf9d1e..a64c82d 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,25 @@ { + "name": "@cakephp/docs-skeleton", + "version": "1.0.0", + "type": "module", + "exports": { + ".": "./.vitepress/theme/index.js", + "./config": "./config/index.js" + }, + "bin": { + "cakedocs": "./bin/cakedocs.js" + }, + "files": [ + ".vitepress/theme/", + ".vitepress/plugins/", + ".vitepress/utils.js", + "config/", + "bin/", + "docs/public/" + ], + "peerDependencies": { + "vitepress": "^2.0.0-alpha.15" + }, "devDependencies": { "@eslint/js": "^9.15.0", "eslint": "^9.39.2", @@ -8,7 +29,6 @@ "vitepress-plugin-llms": "^1.7.5", "vue": "^3.5.21" }, - "type": "module", "scripts": { "docs:dev": "vitepress dev . --base /", "docs:build": "vitepress build", From 9373ee209c7578ada3f0952b501b1b5db7449115 Mon Sep 17 00:00:00 2001 From: Jasper Smet Date: Sun, 15 Mar 2026 17:09:33 +0100 Subject: [PATCH 2/3] fix linting errors --- .vitepress/theme/components/VersionBanner.vue | 6 +++++- bin/cakedocs.js | 18 +++++++++++++----- config/index.js | 1 - 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.vitepress/theme/components/VersionBanner.vue b/.vitepress/theme/components/VersionBanner.vue index 729b2e6..676789c 100644 --- a/.vitepress/theme/components/VersionBanner.vue +++ b/.vitepress/theme/components/VersionBanner.vue @@ -105,7 +105,11 @@ onUnmounted(() => { aria-live="polite" > {{ message }} - {{ linkText }} + {{ linkText }} diff --git a/bin/cakedocs.js b/bin/cakedocs.js index 26e159f..d4373e5 100755 --- a/bin/cakedocs.js +++ b/bin/cakedocs.js @@ -27,24 +27,32 @@ export default { const THEME_INDEX_TEMPLATE = `export { default } from '@cakephp/docs-skeleton' ` +function out(message) { + process.stdout.write(message.endsWith('\n') ? message : `${message}\n`) +} + +function err(message) { + process.stderr.write(message.endsWith('\n') ? message : `${message}\n`) +} + function writeIfMissing(filePath, content, label) { if (existsSync(filePath)) { - console.log(` skip ${label} (already exists)`) + out(` skip ${label} (already exists)`) return } mkdirSync(dirname(filePath), { recursive: true }) writeFileSync(filePath, content, 'utf8') - console.log(` create ${label}`) + out(` create ${label}`) } function runInit() { const cwd = process.cwd() - console.log(`Scaffolding @cakephp/docs-skeleton in ${cwd}\n`) + out(`Scaffolding @cakephp/docs-skeleton in ${cwd}\n`) writeIfMissing(join(cwd, '.vitepress', 'config.js'), VITEPRESS_CONFIG_TEMPLATE, '.vitepress/config.js') writeIfMissing(join(cwd, '.vitepress', 'theme', 'index.js'), THEME_INDEX_TEMPLATE, '.vitepress/theme/index.js') - console.log(` + out(` Done! Next steps: 1. Add your docs to docs/ @@ -58,7 +66,7 @@ switch (command) { runInit() break default: - console.log(` + err(` Usage: cakedocs Commands: diff --git a/config/index.js b/config/index.js index c7b38f0..4ad4813 100644 --- a/config/index.js +++ b/config/index.js @@ -1,5 +1,4 @@ import { substitutionsReplacer } from '../.vitepress/plugins/substitutions-replacer.js' -import { applyBaseToHeadTags } from '../.vitepress/utils.js' import { fileURLToPath } from 'node:url' import path from 'node:path' import fs from 'node:fs' From beb4b50177ce8fd81d5113d2e3fc49b107ddf2f2 Mon Sep 17 00:00:00 2001 From: Jasper Smet Date: Sun, 15 Mar 2026 17:18:28 +0100 Subject: [PATCH 3/3] add srr rules --- config/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/index.js b/config/index.js index 4ad4813..924e8a6 100644 --- a/config/index.js +++ b/config/index.js @@ -155,6 +155,9 @@ const baseConfig = { locales: {}, themeDir: packageThemeDir, vite: { + ssr: { + noExternal: ['@cakephp/docs-skeleton', 'vitepress'] + }, plugins: [ createSkeletonPublicAssetsPlugin() ]