# 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.