# AdonisJS Documentation Boilerplate
## Introduction
The AdonisJS Documentation Boilerplate is a specialized static site generator designed for creating highly customizable documentation websites. Built on top of the AdonisJS framework, it provides a streamlined workflow for rendering markdown content into static HTML with complete control over the rendering pipeline. Unlike frontend-first tools such as VitePress or Gatsby, this boilerplate takes a backend-first approach using proven markdown rendering libraries while avoiding unnecessary complexity like GraphQL for static sites.
The system leverages DimerApp's markdown processing ecosystem, Shiki for syntax highlighting with VSCode themes and grammars, and Edge templating for HTML generation. It organizes content into collections (such as guides, API references, or config documentation), uses JSON database files for sidebar navigation, and supports automatic URL resolution for both markdown file links and media assets. The boilerplate includes built-in support for Algolia search integration, custom VSCode language grammars, and automated OpenGraph image generation for social media previews.
## Core APIs and Functions
### Collection Configuration
Configure documentation collections with custom renderers and URL prefixes to organize different sections of your documentation site.
```typescript
import { Collection } from '@dimerapp/content'
import { renderer } from './bootstrap.js'
// Create a docs collection
const docs = new Collection()
.db(new URL('../content/docs/db.json', import.meta.url))
.useRenderer(renderer)
.urlPrefix('/guides')
.tap((entry) => {
entry.setMarkdownOptions({ tocDepth: 3 })
entry.rendering((mdFile) => {
mdFile.on('link', (node) => {
if (node.url && !node.url.startsWith('http') && !node.url.startsWith('#')) {
console.log('Internal link detected:', node.url)
}
})
})
})
await docs.boot()
// Create an API reference collection
const apiReference = new Collection()
.db(new URL('../content/api_reference/db.json', import.meta.url))
.useRenderer(renderer)
.urlPrefix('/api')
await apiReference.boot()
export const collections = [docs, apiReference]
```
### Markdown Renderer Configuration
Set up the markdown rendering pipeline with custom themes, code block styling, and HTML element transformations.
```typescript
import edge from 'edge.js'
import { dimer } from '@dimerapp/edge'
import { RenderingPipeline, Renderer } from '@dimerapp/content'
import { docsHook, docsTheme } from '@dimerapp/docs-theme'
// Configure Edge templating engine
edge.use(dimer)
edge.use(docsTheme)
// Create rendering pipeline with custom transformations
const pipeline = new RenderingPipeline()
pipeline.use(docsHook).use((node) => {
if (node.tagName === 'img') {
return pipeline.component('elements/img', { node })
}
})
// Configure renderer with code block theme
export const renderer = new Renderer(edge, pipeline)
.codeBlocksTheme('material-theme-darker')
.useTemplate('docs')
// Register custom VSCode grammars
const grammars = [
{
path: './vscode_grammars/dotenv.tmLanguage.json',
scopeName: 'source.env',
id: 'dotenv',
},
{
path: './vscode_grammars/edge.tmLanguage.json',
scopeName: 'text.html.edge',
id: 'edge',
},
]
grammars.forEach((grammar) => renderer.registerLanguage(grammar))
```
### Edge Global Functions
Register global functions available in all Edge templates for accessing configuration, generating navigation, and formatting dates.
```typescript
import edge from 'edge.js'
import dayjs from 'dayjs'
import collect from 'collect.js'
import { readFile } from 'node:fs/promises'
import relativeTime from 'dayjs/plugin/relativeTime.js'
// Load configuration file
edge.global('getConfig', async () =>
JSON.parse(await readFile(new URL('../content/config.json', import.meta.url), 'utf-8'))
)
// Add date formatting
dayjs.extend(relativeTime)
edge.global('dayjs', dayjs)
// Generate sidebar sections
edge.global('getSections', function (collection, entry) {
const entries = collection.all()
return collect(entries)
.groupBy('meta.category')
.map((items, key) => {
return {
title: key,
isActive: entry.meta.category === key,
items: items
.filter((item) => !item.meta.draft)
.map((item) => ({
href: item.permalink,
title: item.title,
isActive: item.permalink === entry.permalink,
}))
.all(),
}
})
.all()
})
// Generate pagination links
edge.global('getPagination', function (collection, entry) {
const entries = collection.all()
const currentIndex = entries.findIndex((item) => item.permalink === entry.permalink)
return {
previous: {
category: entries[currentIndex - 1]?.meta.category,
title: entries[currentIndex - 1]?.title,
url: entries[currentIndex - 1]?.permalink,
},
next: {
category: entries[currentIndex + 1]?.meta.category,
title: entries[currentIndex + 1]?.title,
url: entries[currentIndex + 1]?.permalink,
},
}
})
```
### Development Server
Start a development server with hot reload for content and template changes, with automatic routing based on collections.
```typescript
import 'reflect-metadata'
import { Ignitor } from '@adonisjs/core'
import { defineConfig } from '@adonisjs/vite'
import { readFile } from 'node:fs/promises'
const APP_ROOT = new URL('../', import.meta.url)
async function defineRoutes(app) {
const { default: server } = await import('@adonisjs/core/services/server')
const { collections } = await import('#src/collections')
const { default: router } = await import('@adonisjs/core/services/router')
server.use([() => import('@adonisjs/static/static_middleware')])
// Load redirects configuration
const redirects = await readFile(app.publicPath('_redirects'), 'utf-8')
const redirectsCollection = redirects.split('\n').reduce((result, line) => {
const [from, to] = line.split(' ')
result[from] = to
return result
}, {})
// Handle all requests
router.get('*', async ({ request, response }) => {
// Check for redirects
if (redirectsCollection[request.url()]) {
return response.redirect(redirectsCollection[request.url()])
}
// Find matching collection entry
for (let collection of collections) {
await collection.refresh()
const entry = collection.findByPermalink(request.url())
if (entry) {
return entry.render({ collection, entry }).catch(console.error)
}
}
return response.notFound('Page not found')
})
}
new Ignitor(APP_ROOT, { importer: (filePath) => import(filePath) })
.tap((app) => {
app.initiating(() => {
app.useConfig({
appUrl: process.env.APP_URL || '',
app: {
appKey: 'zKXHe-Ahdb7aPK1ylAJlRgTefktEaACi',
http: {},
},
static: { enabled: true, etag: true },
vite: defineConfig({}),
})
})
app.starting(defineRoutes)
})
.httpServer()
.start()
```
### Static Site Export
Export all documentation pages to static HTML files with automated OpenGraph image generation.
```typescript
import sharp from 'sharp'
import { Collection } from '@dimerapp/content'
import { mkdir, readFile } from 'node:fs/promises'
import { existsSync, writeFileSync } from 'node:fs'
import string from '@adonisjs/core/helpers/string'
const ogTemplate = await readFile('./assets/og_template.svg', 'utf-8')
async function generateOgImage(entry, htmlOutput) {
const html = await readFile(`dist/${htmlOutput}`, 'utf-8')
const metaDescription = html.match(//)
const description = metaDescription ? metaDescription[1] : ''
// Create output directory
if (!existsSync('public/og')) await mkdir('public/og', { recursive: true })
const filename = entry.contentPath.replace('.md', '').replace(/\//g, '-')
const output = `public/og/${filename}.png`
// Parse description into lines
const lines = description.trim().split(/(.{0,60})(?:\s|$)/g).filter(Boolean)
// Generate SVG with content
const svg = ogTemplate
.replace('{{ title }}', string.encodeSymbols(entry.title.slice(0, 24)))
.replace('{{ line1 }}', lines[0])
.replace('{{ line2 }}', lines[1] || '')
.replace('{{ line3 }}', lines[2] || '')
// Render to PNG
await sharp(Buffer.from(svg))
.resize(1320, 693)
.png()
.toFile(output)
// Insert meta tags
const ogImageUrl = output.replace('public/', 'https://docs.adonisjs.com/')
const tags = `
`
const updatedHtml = html.replace('', `${tags}`)
writeFileSync(`dist/${htmlOutput}`, updatedHtml)
}
async function exportHTML() {
const { collections } = await import('#src/collections')
const { default: ace } = await import('@adonisjs/core/services/ace')
const { default: app } = await import('@adonisjs/core/services/app')
for (let collection of collections) {
for (let entry of collection.all()) {
try {
const output = await entry.writeToDisk(app.makePath('dist'), { collection, entry })
await generateOgImage(entry, output.filePath)
ace.ui.logger.action(`create ${output.filePath}`).succeeded()
} catch (error) {
ace.ui.logger.action(`create ${entry.permalink}`).failed(error)
}
}
}
}
await exportHTML()
```
### Database Entry Structure
Define documentation entries in JSON database files with permalinks, titles, content paths, and categories.
```json
{
"entries": [
{
"permalink": "introduction",
"title": "Introduction",
"contentPath": "./introduction.md",
"category": "Getting Started"
},
{
"permalink": "installation",
"title": "Installation",
"contentPath": "./installation.md",
"category": "Getting Started"
},
{
"permalink": "configuration",
"title": "Configuration",
"contentPath": "./basics/configuration.md",
"category": "Basics"
}
]
}
```
### Site Configuration
Configure site-wide settings including navigation, search, sponsors, and metadata.
```json
{
"menu": [
{
"href": "/guides/introduction",
"title": "Guides"
},
{
"href": "/api/overview",
"title": "API Reference"
}
],
"links": {
"home": {
"title": "Project Name",
"href": "/"
},
"github": {
"title": "GitHub Repository",
"href": "https://github.com/username/project"
}
},
"search": {
"appId": "YOUR_ALGOLIA_APP_ID",
"indexName": "your_index_name",
"apiKey": "your_search_api_key"
},
"fileEditBaseUrl": "https://github.com/username/project/blob/main",
"copyright": "Your Project Name"
}
```
### Frontend Integration
Set up the frontend with Alpine.js components, search functionality, and image zoom features.
```javascript
import 'unpoly'
import Alpine from 'alpinejs'
import mediumZoom from 'medium-zoom'
import docsearch from '@docsearch/js'
import { tabs } from 'edge-uikit/tabs'
import Persist from '@alpinejs/persist'
import collapse from '@alpinejs/collapse'
import {
initZoomComponent,
initBaseComponents,
initSearchComponent,
} from '@dimerapp/docs-theme/scripts'
// Configure Alpine.js plugins
Alpine.plugin(tabs)
Alpine.plugin(Persist)
Alpine.plugin(collapse)
Alpine.plugin(initBaseComponents)
Alpine.plugin(initSearchComponent(docsearch))
Alpine.plugin(initZoomComponent(mediumZoom))
// Start Alpine.js
Alpine.start()
// Handle offline state
up.on('up:fragment:offline', function (event) {
window.location.reload()
})
```
### Vite Build Configuration
Configure Vite for building assets with hot reload support for content and template changes.
```javascript
import { defineConfig } from 'vite'
import adonisjs from '@adonisjs/vite/client'
export default defineConfig({
plugins: [
adonisjs({
entrypoints: ['./assets/app.js', './assets/app.css'],
reload: ['content/**/*', 'templates/**/*.edge'],
}),
],
})
```
### NPM Scripts
Run development server, build static site, and manage the documentation workflow.
```bash
# Start development server with hot reload
npm run dev
# Build static HTML files with Vite assets and download sponsors
npm run export
# Alternative build command
npm run build
# Serve development server without rebuilding assets
npm run serve
# Download sponsor information from configured sources
npm run download:sponsors
```
## Summary and Use Cases
This documentation boilerplate excels at creating professional documentation websites for open-source projects, libraries, frameworks, and API references. It's particularly well-suited for teams that need complete control over the markdown rendering pipeline, want to avoid the complexity of frontend-heavy static site generators, and require features like multi-collection organization, custom VSCode syntax highlighting, and automated social media preview generation. The system handles scenarios from simple single-collection documentation to complex multi-section sites with API references, guides, and configuration documentation all in one place.
The integration patterns center around extending the Collection and Renderer classes, registering Edge global functions for custom template data, and using the tap method to hook into rendering lifecycle events. Projects can customize every aspect from markdown AST transformations to HTML output while maintaining a clean separation between content (markdown files), structure (JSON databases), presentation (Edge templates), and styling (CSS with variables). The boilerplate's backend-first approach using AdonisJS provides a stable foundation that doesn't shift with frontend ecosystem trends, while still delivering modern features like hot module replacement during development and optimized static output for production.