# 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 ``` ## 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 ``` ### Since Component Display version badges that link to release notes or changelogs. ```vue ``` ### Cloud Component Display a call-to-action banner linking to Craft Cloud. ```vue ``` ### See Component Create navigational cards that link to internal pages or external URLs with descriptions. ```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 ``` ## 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.