123456dist/rsc/assets/ ├── holocron-stable-{hash}.js # framework + components (shared) ├── holocron-data.js # config, navigation, MDX loaders (per tenant) ├── holocron-page-index-{hash}.js # page content (per tenant) ├── holocron-page-getting-started-{hash}.js └── ...fonts, icons, runtime
| Layer | Changes per tenant? | Naming | Size |
holocron-stable-{hash}.js | No | Content-hashed | ~2-6 MB |
holocron-data.js | Yes | Deterministic, no hash | ~5-50 KB |
holocron-page-{slug}-{hash}.js | Yes | Deterministic per slug | ~1-10 KB each |
| Client JS/CSS | No | Content-hashed | ~3-10 MB |
123456789101112131415161718192021┌─────────────────────────────────────────────────────────────────────────┐ │ 1. Build once │ │ npx vite build │ │ ► produces dist/ with stable chunks + template data │ └────────────────────────────────────┬────────────────────────────────────┘ │ ┌───────────────────────────┼───────────────────────────┐ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Tenant A │ │ Tenant B │ │ Tenant C │ │ │ │ │ │ │ │ generateData() │ │ generateData() │ │ generateData() │ │ ► data.js │ │ ► data.js │ │ ► data.js │ │ ► page chunks │ │ ► page chunks │ │ ► page chunks │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ Deploy (content-addressable) │ │ Shared stable chunk uploaded once. Only data.js + pages per tenant. │ └─────────────────────────────────────────────────────────────────────────┘
generateHolocronData to produce the data chunk and page chunks for a tenant without running a full Vite build.12345678910111213141516171819202122232425262728293031import { normalizeConfig, generateHolocronData, } from '@holocron.so/vite' import fs from 'node:fs' import path from 'node:path' // 1. Load and normalize the tenant's docs.json const raw = JSON.parse(fs.readFileSync('./tenant-a/docs.json', 'utf-8')) const config = normalizeConfig(raw) // 2. Collect all page slugs from the config navigation const slugs = collectSlugsFromConfig(config) // 3. Generate data chunk + page chunks const result = await generateHolocronData({ config, getMdxSource: async (slug) => { return fs.readFileSync(`./tenant-a/pages/${slug}.mdx`, 'utf-8') }, slugs, base: '/', }) // 4. Write to the deploy directory (copy dist/ first, then overwrite) const assetsDir = './deploy/tenant-a/rsc/assets' fs.writeFileSync(path.join(assetsDir, 'holocron-data.js'), result.dataChunkSource) for (const [slug, chunk] of result.pageChunks) { fs.writeFileSync(path.join(assetsDir, chunk.filename), chunk.source) }
getMdxSource callback can load content from anywhere. For a CMS-backed platform:12345678const result = await generateHolocronData({ config, getMdxSource: async (slug) => { const row = await db.query('SELECT content FROM pages WHERE slug = ?', [slug]) return row.content }, slugs, })
12345678910111213141516171819function collectSlugsFromConfig(config) { const slugs = [] for (const tab of config.navigation.tabs) { for (const group of tab.groups) { walkGroup(group, slugs) } } return slugs } function walkGroup(group, slugs) { for (const entry of group.pages) { if (typeof entry === 'string') { slugs.push(entry) } else if ('pages' in entry) { walkGroup(entry, slugs) } } }
holocron-data.js and holocron-page-*.js files are new. A typical tenant deploy uploads 50-100 KB instead of 10+ MB.1234567891011121314151617181920import { createHash } from 'node:crypto' // Collect all files from the deploy directory const files = collectFiles('./deploy/tenant-a') // Hash each file const manifest = files.map(f => ({ path: f.relativePath, hash: createHash('sha256').update(f.content).digest('hex'), })) // POST to create deployment — server returns which hashes already exist const { deploymentId, existingHashes } = await api.createDeployment({ files: manifest }) // Upload only new files const newFiles = files.filter(f => !existingHashes.includes(f.hash)) await api.uploadFiles(deploymentId, newFiles) // Finalize — site goes live instantly await api.finalizeDeployment(deploymentId)
dist/ from the template build, then overwrite just the data files.1234567891011121314151617181920import { cpSync, writeFileSync } from 'node:fs' // Copy the template build cpSync('./dist', `./deploys/${tenantId}`, { recursive: true }) // Overwrite data layer with tenant-specific content const assetsDir = `./deploys/${tenantId}/rsc/assets` // Remove the template data files for (const f of readdirSync(assetsDir)) { if (f === 'holocron-data.js' || f.startsWith('holocron-page-')) { unlinkSync(path.join(assetsDir, f)) } } // Write tenant data files writeFileSync(path.join(assetsDir, 'holocron-data.js'), result.dataChunkSource) for (const [, chunk] of result.pageChunks) { writeFileSync(path.join(assetsDir, chunk.filename), chunk.source) }
generateOpenAPIPages function takes a raw spec (YAML, JSON, or parsed object) and produces MDX pages with the <OpenAPIEndpoint> component, which is already bundled in the stable shell chunk.12345678910111213141516171819202122232425262728293031323334import { normalizeConfig, generateHolocronData, generateOpenAPIPages, } from '@holocron.so/vite' // 1. Generate OpenAPI pages from a spec const { pages: apiPages, navigation: apiNavGroups } = await generateOpenAPIPages({ spec: openApiYamlString, // YAML, JSON string, or parsed object slugPrefix: 'api', // pages land at api/get-users, api/post-users, etc. }) // 2. Merge OpenAPI pages into the tenant's page map const allPages = { ...tenantPages, ...apiPages } const allSlugs = Object.keys(allPages) // 3. Merge OpenAPI navigation into the config const config = normalizeConfig({ ...tenantDocsJson, navigation: { ...tenantDocsJson.navigation, tabs: [ ...tenantDocsJson.navigation.tabs, { tab: 'API Reference', groups: apiNavGroups }, ], }, }) // 4. Generate data as usual const result = await generateHolocronData({ config, getMdxSource: async (slug) => allPages[slug] ?? '', slugs: allSlugs, })
generateOpenAPIPages processes the spec through the same pipeline as the Vite plugin: @scalar bundling, 3.1 upgrade, full dereferencing. Each operation becomes one MDX page with curl examples, request/response examples, and the interactive <OpenAPIEndpoint> component.navigation output groups endpoints by their first OpenAPI tag, ready to drop into a tab's groups array.customCss in the tenant's docs.json to inject arbitrary CSS at runtime. This is useful for multi-tenant sites where users cannot provide CSS files at build time.1234{ "name": "My Docs", "customCss": ".editorial-heading { font-weight: 800; } .slot-navbar { border-bottom: 2px solid var(--primary); }" }
<style> tag alongside the theme's color and font styles.fonts in docs.json to use Google Fonts or custom web fonts. The shell loads them at runtime via Google Fonts API.1234567891011{ "fonts": { "heading": { "family": "Space Grotesk", "weight": 700 }, "body": { "family": "Inter" } } }
--font-sans, --font-heading).holocron-data.js contains all the metadata Orama needs.| Feature | Multi-tenant support |
| MDX pages (all content) | Works |
| Navigation (tabs, groups, nested) | Works |
| Code blocks (Shiki highlighting) | Works (runtime) |
| Mermaid diagrams | Works (client-side render) |
| Math / KaTeX | Works (client-side render) |
| Colors / theming | Works via colors config |
| Custom fonts | Works via fonts config |
| Custom CSS | Works via customCss config |
| Icons (lucide, emoji, URL) | Works |
| Redirects | Works |
| Sidebar search | Works (Orama, client-side) |
| AI chat widget | Works (built into shell) |
| Anchors | Works |
| OpenAPI tabs | Works via generateOpenAPIPages |
| Custom MDX components | Not supported |
| Changelog tabs | Not yet supported |
| Export | Purpose |
normalizeConfig(raw) | Parse a raw docs.json object into a HolocronConfig |
parseConfigSource(jsonString) | Parse a JSON/JSONC string into a HolocronConfig |
generateHolocronData(opts) | Produce holocron-data.js + page chunk sources |
generateOpenAPIPages(opts) | Generate MDX pages + navigation from an OpenAPI spec |
buildNavigationData(opts) | Build the enriched navigation tree (used internally by generateHolocronData) |
processMdx(content, library) | Process a single MDX string into normalized content + metadata |
collectIconRefs(opts) | Collect all icon refs needed for the icon atlas |
@holocron.so/vite.generateHolocronData is pure computation. It doesn't touch the filesystem, Vite, or git. You provide the config object and an async MDX loader; it returns JS source strings.