# Craft CMS Documentation Project
This project serves as the complete documentation system for Craft CMS and its ecosystem, hosted at craftcms.com/docs. Built on VuePress with a heavily customized theme, it provides versioned documentation for multiple products including Craft CMS (versions 2.x through 5.x), Craft Commerce, Craft Cloud, Craft Nitro, and a Getting Started Tutorial. The system uses markdown content organized into "docsets" - self-contained documentation products that can have multiple versions, each with independent navigation and search functionality.
The architecture combines VuePress's markdown processing capabilities with a custom Vue.js theme that integrates Tailwind CSS for styling. Key features include FlexSearch-powered search across all docsets, custom markdown extensions for Craft-specific content (like class references and config settings), and a sophisticated anchor prefix system that automatically expands shorthand links like `craft5:` into full API documentation URLs. The project supports multi-version documentation with primary and EOL (end-of-life) version indicators, making it easy for users to navigate between different product versions.
## Documentation Configuration
### DocSet Configuration
A docset defines a documentation product with versioning, search, and navigation settings.
```javascript
// docs/.vuepress/sets/craft-cms.js
module.exports = {
title: "Craft CMS Documentation | %v",
setTitle: "Craft CMS",
handle: "craft",
icon: "/docs/icons/craft.svg",
baseDir: "",
versions: [
["5.x", { label: "5.x" }],
["4.x", { label: "4.x" }],
["3.x", { label: "3.x", isEol: true }],
["2.x", { label: "2.x", isEol: true }],
],
defaultVersion: "5.x",
abandoned: false,
searchPlaceholder: "Search the Craft docs (Press "/" to focus)",
primarySet: true,
sidebar: {
"5.x": {
"/reference/": [
{
title: "Reference",
collapsable: false,
children: [
["", "Index"],
],
},
{
title: "Element Types",
collapsable: false,
children: [
"element-types/addresses",
"element-types/assets",
"element-types/entries",
],
},
],
},
},
};
```
### Main VuePress Configuration
Configure the entire documentation site with plugins, docsets, and markdown extensions.
```javascript
// docs/.vuepress/config.js
const markdownHelpers = require('./theme/util/markdown');
module.exports = {
theme: "craftdocs",
base: "/docs/",
plugins: [
["@vuepress/google-analytics", { ga: "UA-39036834-9" }],
[
"vuepress-plugin-medium-zoom",
{
selector: ".theme-default-content img:not(.no-zoom)",
delay: 1000,
options: {
margin: 24,
background: "var(--medium-zoom-overlay-color)",
scrollOffset: 0
}
}
],
["vuepress-plugin-container", { type: "tip", defaultTitle: "" }],
["vuepress-plugin-container", { type: "warning", defaultTitle: "" }],
["vuepress-plugin-container", { type: "danger", defaultTitle: "" }],
[
"vuepress-plugin-container",
{
type: "details",
before: info =>
`${
info ? `${info}` : ""
}\n`,
after: () => "\n"
}
],
[require("./plugins/craft.js")],
],
shouldPrefetch: () => false,
head: require("./head"),
themeConfig: {
title: "Craft Documentation",
docSets: [
require("./sets/craft-cms"),
require("./sets/craft-commerce"),
require("./sets/craft-cloud"),
require("./sets/craft-nitro"),
require("./sets/getting-started-tutorial")
],
docsRepo: "craftcms/docs",
docsDir: "docs",
docsBranch: "main",
baseUrl: "https://craftcms.com/docs",
searchPlaceholder: "Search all documentation (Press "/" to focus)",
editLinks: true,
nextLinks: true,
prevLinks: true,
searchMaxSuggestions: 10,
nav: [
{ text: "Knowledge Base", link: "https://craftcms.com/knowledge-base" }
],
codeLanguages: {
twig: "Twig",
php: "PHP",
graphql: "GraphQL",
js: "JavaScript",
json: "JSON",
xml: "XML",
treeview: "Folder",
graphql: "GraphQL", // Note: duplicate key (last one wins)
csv: "CSV"
},
feedback: {
helpful: "Was this page helpful?",
thanks: "Thanks for your feedback!",
more: "Report an Issue →"
}
},
markdown: {
extractHeaders: ['h2', 'h3', 'h4'],
anchor: {
level: [2, 3, 4],
permalinkSymbol: '#',
renderPermalink: markdownHelpers.renderPermalink,
},
toc: {
format(content) {
return content.replace(/[_`]/g, "");
}
},
extendMarkdown(md) {
// provide our own highlight.js to customize Prism setup
md.options.highlight = require("./theme/highlight");
// add markdown extensions
md
.use(require("./theme/markup"))
.use(require("markdown-it-deflist"))
.use(require("markdown-it-imsize"))
.use(require("markdown-it-include"));
}
},
postcss: {
plugins: require("../../postcss.config.js").plugins,
}
};
```
## Anchor Prefix System
### Anchor Prefix Configuration
Define shorthand prefixes that expand into full URLs for API documentation and external resources.
```javascript
// docs/.vuepress/anchor-prefixes.js
/**
* Each key, when used as an anchor prefix, will be expanded into a full link
* based on the rules of `format`.
*
* Format can be...
* - `internal` for Craft+Commerce class docs
* - `yii2` and `yii1` for Yii docs
* - `config` for Craft config settings
* - `generic` for replacement of the supplied `base` only and no additional formatting
* - `source` for GitHub-hosted file references.
*/
module.exports = {
'craft5': { base: 'https://docs.craftcms.com/api/v5/', format: 'internal' },
'craft4': { base: 'https://docs.craftcms.com/api/v4/', format: 'internal' },
'craft3': { base: 'https://docs.craftcms.com/api/v3/', format: 'internal' },
'craft2': { base: 'https://docs.craftcms.com/api/v2/', format: 'internal' },
'commerce5': { base: 'https://docs.craftcms.com/commerce/api/v5/', format: 'internal' },
'commerce4': { base: 'https://docs.craftcms.com/commerce/api/v4/', format: 'internal' },
'commerce3': { base: 'https://docs.craftcms.com/commerce/api/v3/', format: 'internal' },
'commerce2': { base: 'https://docs.craftcms.com/commerce/api/v2/', format: 'internal' },
'commerce1': { base: 'https://docs.craftcms.com/commerce/api/v1/', format: 'internal' },
'yii2': { base: 'https://www.yiiframework.com/doc/api/2.0/', format: 'yii' },
'yii1': { base: 'https://www.yiiframework.com/doc/api/1.1/', format: 'yii' },
'guide': { base: 'https://www.yiiframework.com/doc/guide/2.0/en/', format: 'generic' },
'config5': { base: '/5.x/reference/config/general.md#', format: 'config' },
'config4': { base: '/4.x/config/general.md#', format: 'config' },
'config3': { base: '/3.x/config/config-settings.md#', format: 'config' },
'config2': { base: '/2.x/config-settings.md#', format: 'config' },
'kb': { base: 'https://craftcms.com/knowledge-base/', format: 'generic' },
'repo': { base: 'https://github.com/', format: 'generic' },
'plugin': { base: 'https://plugins.craftcms.com/', format: 'generic', },
'craftcom' : { base: 'https://craftcms.com/', format: 'generic' },
// This doesn't do anything, but I'd hoped we could implement a context-aware format:
'@': { base: '/', format: 'set-local' },
};
```
### Prefix Replacement in Markdown
Automatically expand anchor prefixes in markdown links during parsing.
```javascript
// docs/.vuepress/theme/util/replace-anchor-prefixes.js (excerpt)
const dictionary = require("../../anchor-prefixes");
function replacePrefixes(md, ctx) {
// Expand custom prefix into full URL
md.normalizeLink = url => {
return replacePrefix(url, ctx);
}
// Remove custom prefix from link text
md.normalizeLinkText = linkText => {
if (usesCustomPrefix(linkText)) {
return removePrefix(linkText);
}
return linkText;
}
}
function replacePrefix(link, ctx) {
link = decodeURIComponent(link);
if (!usesCustomPrefix(link)) {
return link;
}
const prefix = getPrefix(link);
const prefixSettings = dictionary[prefix];
if (prefixSettings.format === "internal") {
// Parse class reference like "craft5:craft\\elements\\Entry::find()"
const ref = parseReference(link);
let url = `${prefixSettings.base}${slugifyClassName(ref.className)}.html`;
if (ref.isMethod) {
url += `#method-${ref.subject.replace('()', '').toLowerCase()}`;
} else if (ref.isProperty) {
url += `#property-${ref.subject.replace('$', '').toLowerCase()}`;
}
return url;
}
return prefixSettings.base + removePrefix(link);
}
// Usage in markdown:
// [Entry Element](craft5:craft\elements\Entry)
// Expands to: https://docs.craftcms.com/api/v5/craft-elements-entry.html
```
## Search System
### FlexSearch Index Builder
Build searchable indexes for all docsets with version and language support.
```javascript
// docs/.vuepress/theme/util/flexsearch-service.js (excerpt)
import Flexsearch from "flexsearch";
let indexes = [];
const defaultLang = "en-US";
export default {
buildIndex(pages) {
const indexSettings = {
async: true,
doc: {
id: "key",
field: ["title", "keywords", "headersStr", "content"]
}
};
// Global index for primary sets
const globalIndex = new Flexsearch(indexSettings);
globalIndex.add(
pages.filter(page => {
return page.lang === defaultLang && page.isPrimary;
})
);
indexes["global"] = globalIndex;
// Create version-specific indexes: setHandle|version|lang
let docSets = pages
.map(page => page.docSetHandle)
.filter((handle, index, self) => handle && self.indexOf(handle) === index);
for (let docSet of docSets) {
const docSetPages = pages.filter(page => page.docSetHandle === docSet);
let versions = [...new Set(docSetPages.map(page => page.version))];
let languages = [...new Set(docSetPages.map(page => page.lang))];
for (let version of versions) {
for (let language of languages) {
const setIndex = new FlexSearch(indexSettings);
const setKey = `${docSet}|${version}|${language}`;
const setPages = pages.filter(page =>
page.docSetHandle === docSet &&
page.lang === language &&
page.version === version
);
setIndex.add(setPages);
indexes[setKey] = setIndex;
}
}
}
},
search(queryString, docSetHandle, version, lang = defaultLang) {
if (!queryString) return [];
const indexKey = docSetHandle
? `${docSetHandle}|${version}|${lang}`
: "global";
return indexes[indexKey].search(queryString, { limit: 10 });
}
};
```
### Search Box Component
Vue component for real-time search with keyboard navigation.
```vue
{{ s.docSetTitle }}
```
## Local Storage Utilities
### Storage Helper Functions
Manage browser localStorage with namespaced keys based on URL paths.
```javascript
// docs/.vuepress/theme/Storage.js
const storagePrefix = function(base) {
let p = base
.replace(/^\//, "")
.replace(/\/$/, "")
.replace(/\//g, ".");
return p ? p + "." : "";
};
const setStorage = function(name, value, base) {
if (typeof localStorage === "undefined") {
return;
}
localStorage[storagePrefix(base) + name] = value;
};
const getStorage = function(name, base) {
if (typeof localStorage === "undefined") {
return;
}
name = storagePrefix(base) + name;
if (typeof localStorage[name] === "undefined") {
return;
}
return localStorage[name];
};
const unsetStorage = function(name, base) {
if (typeof localStorage === "undefined") {
return;
}
name = storagePrefix(base) + name;
if (typeof localStorage[name] === "undefined") {
return;
}
delete localStorage[name];
};
export { storagePrefix, getStorage, setStorage, unsetStorage };
// Usage example:
// import { getStorage, setStorage } from './Storage';
// setStorage('theme', 'dark', '/docs/craft/5.x/');
// // Stores as: "docs.craft.5.x.theme" = "dark"
// const theme = getStorage('theme', '/docs/craft/5.x/');
```
## Custom Vue Components
### Block Component
Reusable component for creating labeled content blocks in documentation.
```vue
{{ label }}
```
### Since Component
Display version badges that link to release notes or changelogs.
```vue
{{ ver }}+
```
### Cloud Component
Display a call-to-action banner linking to Craft Cloud.
```vue
```
### Todo Component
Development utility component that logs TODOs to the browser console without rendering visible content.
```vue
```
### StatusLabel Component
Display status badges with color-coded indicators.
```vue
{{ label }}
```
## HTML Head Configuration
### Head.js Setup
Configure HTML head elements including theme handling, favicons, and meta tags.
```javascript
// docs/.vuepress/head.js
module.exports = [
[
"script",
{},
`let htmlElement = document.getElementsByTagName("html")[0];
if (localStorage && localStorage['docs.theme']) {
htmlElement.className += (htmlElement.className ? ' ' : '') + 'theme-' + localStorage['docs.theme'];
}`
],
[
"link",
{
rel: "icon",
href: "https://docs.craftcms.com/siteicons/favicon-16x16.png"
}
],
[
"link",
{
rel: "apple-touch-icon",
sizes: "180x180",
href: "https://docs.craftcms.com/siteicons/apple-touch-icon.png"
}
],
[
"link",
{
rel: "icon",
type: "image/png",
sizes: "32x32",
href: "https://docs.craftcms.com/siteicons/favicon-32x32.png"
}
],
[
"link",
{
rel: "icon",
type: "image/png",
sizes: "16x16",
href: "https://docs.craftcms.com/siteicons/favicon-16x16.png"
}
],
[
"link",
{
rel: "mask-icon",
href: "https://docs.craftcms.com/siteicons/safari-pinned-tab.svg",
color: "#e5422b"
}
],
["meta", { name: "msapplication-TileColor", content: "#f1f5fd" }],
[
"meta",
{
name: "msapplication-config",
content: "https://docs.craftcms.com/browserconfig.xml"
}
],
["meta", { name: "theme-color", content: "#1a202c", media: "(prefers-color-scheme: dark)" }],
["meta", { name: "theme-color", content: "#f1f5fd" }]
];
// The script restores the user's theme preference from localStorage on page load
// before the page renders, preventing a flash of incorrect theme.
```
## Build Plugin
### Craft Documentation Plugin
Custom VuePress plugin for anchor replacement and sitemap generation.
```javascript
// docs/.vuepress/plugins/craft.js
const path = require('path');
const fs = require('fs');
const { replacePrefixes } = require("../theme/util/replace-anchor-prefixes");
/**
* This plugin attempts to provide the "AppContext" instance to anchor replacements.
*/
module.exports = function(options, ctx) {
return {
name: 'craft',
extendMarkdown: function(md) {
md.use(function(md) {
return replacePrefixes(md, ctx);
});
},
async generated () {
const manifest = [];
ctx.pages.forEach(function(p) {
manifest.push({
title: p.title,
path: p.path,
summary: p.frontmatter.description || p.excerpt || null,
keywords: (p.frontmatter.keywords || '').split(' '),
});
});
await fs.writeFile(
path.resolve(ctx.outDir, 'sitemap.json'),
JSON.stringify(manifest, null, 4),
function(err) {
if (err) {
throw err;
}
console.log("Wrote sitemap.json!");
},
);
}
};
};
```
## Development Workflow
### NPM Scripts
Common development and build commands for the documentation project.
```bash
# Start development server with hot reload
npm run docs:dev
# Start with debugger attached
npm run docs:devdebug
# Start without cache (useful after config changes)
npm run docs:devnocache
# Build production site (includes sitemap and redirects)
npm run docs:build
# Generate sitemap
npm run docs:sitemap
# Copy redirects file
npm run docs:redirects
# Serve built site locally
npm run serve-static
# Run textlint for prose quality
npm run textlint
# Auto-fix textlint issues
npm run textlint:fix
# Run Playwright tests
npm run test:playwright
# Run Jest tests
npm run test:jest
```
## Summary
The Craft CMS Documentation system is designed as a scalable, multi-product documentation platform that handles versioning elegantly while maintaining excellent user experience. It serves documentation for Craft CMS across major versions (2.x through 5.x) alongside related products like Commerce, Cloud, Nitro, and a Getting Started Tutorial. The system's key strength lies in its docset architecture, which treats each product-version combination as an independent documentation unit with its own navigation, search index, and configuration while sharing a common theme and component library.
The project demonstrates advanced VuePress customization through its comprehensive anchor prefix system (supporting prefixes for Craft 2-5, Commerce 1-5, Yii 1-2, and config settings across versions), FlexSearch integration for fast client-side search across versioned content, and a rich set of custom Vue components including Block, Since, StatusLabel, and specialized components for documentation features. The system includes custom markdown container types (tip, warning, danger, details), user feedback mechanisms, and a sophisticated theme system that persists preferences via localStorage. The build pipeline includes automated sitemap generation, redirects handling, accessibility testing with axe-core via Playwright, and prose quality checks with textlint. Developers can extend the system by adding new docsets in `docs/.vuepress/sets/`, creating custom Vue components in `docs/.vuepress/components/`, or enhancing the markdown parser with additional plugins. The entire theme is styled with Tailwind CSS and supports dark mode with proper theme-color meta tags, making it both maintainable and user-friendly.