TL;DR: Place a docinfo.html in your adoc Studio project’s document folder and set :docinfo: shared in an attributes file at the top of the project sidebar. The docinfo.html content is injected into the <head> of every exported HTML page – use JavaScript to dynamically inject navbar, footer, and other elements into the body.
When you export AsciiDoc documentation to HTML for your website, you often want it to feel like a native part of your site – complete with navigation, footer, and consistent styling.
The docinfo.html mechanism in AsciiDoc provides exactly this capability. In this article, we’ll show you how we integrated the adoc Studio Manual into the main website using this technique.
Basics
Before diving into docinfo, let’s understand how adoc Studio’s website export is structured by default. This knowledge is essential for successful integration.
The Default HTML Structure
When you export documentation to HTML for website use, adoc Studio generates pages with a specific structure:
<body data-is-website>
<nav class="home">...</nav> <!-- Breadcrumb/Home navigation -->
<nav class="search">...</nav> <!-- Search results panel -->
<nav class="search-toggle">...</nav> <!-- Search button -->
<nav class="language">...</nav> <!-- Language switcher -->
<main>...</main> <!-- Document content -->
<nav class="toc">...</nav> <!-- Table of contents -->
</body>The body element uses CSS Grid for layout and includes the data-is-website attribute that activates website-specific styling.
Built-in Navigation Elements
adoc Studio provides several navigation elements out of the box:
| Element | Class | Purpose |
|---|---|---|
| Home Navigation | nav.home |
Breadcrumb trail back to documentation root |
| Search Panel | nav.search |
Displays search results |
| Search Toggle | nav.search-toggle |
Button to open/close search |
| Language Switcher | nav.language |
Switch between language versions |
| Table of Contents | nav.toc |
Document navigation sidebar |
These elements are positioned at the top of the page by default, serving as the primary navigation for the documentation.
Why Reorganize for Website Integration?
When integrating documentation into a website, you typically want to add a main site navigation bar at the very top – the same navbar used across your entire website. This creates a problem:
- The default nav elements occupy the top position
- Your site navbar also needs to be at the top
- Visual hierarchy requires the main navbar first, then documentation-specific navigation
The solution is to create a SubNav – a secondary navigation bar below your main navbar that contains the reorganized documentation navigation elements.
┌─────────────────────────────────────────┐
│ Main Site Navbar (injected) │ ← Your website navigation
├─────────────────────────────────────────┤
│ SubNav: Home | Search | Language │ ← Reorganized doc elements
├─────────────────────────────────────────┤
│ Main Content │ TOC Sidebar │
│ │ │
└─────────────────────────────────────────┘This approach preserves all built-in functionality (search, language switching, breadcrumbs) while integrating seamlessly with your website’s design.
But how do we add this custom navigation? The solution is a docinfo file.
What is docinfo?
The docinfo mechanism in AsciiDoc allows you to inject custom content into your exported HTML documents. A docinfo.html file is automatically inserted at the end of the <head> section of every exported page – ideal for meta tags, scripts, and stylesheets. As we’ll show in this article, you can use JavaScript in the head to inject content anywhere in the document.
Private vs. Shared Docinfo
AsciiDoc distinguishes between two types of docinfo files:
Private docinfo – Named after your document:
my-document.adoc
my-document-docinfo.html ← Only for this documentShared docinfo – Used by all documents in the folder:
docinfo.html ← Shared by all .adoc files
chapter1.adoc
chapter2.adocFor documentation projects, shared docinfo is typically the better choice, as you want consistent navigation and styling across all pages.
Enabling Docinfo in Your Document
To activate docinfo processing, add the :docinfo: attribute to your document header:
= My Documentation
:docinfo: shared
This is my document content...The :docinfo: attribute accepts these values:
| Value | Behavior |
|---|---|
shared |
Uses docinfo.html (shared file) |
private |
Uses {docname}-docinfo.html (document-specific) |
shared-head |
Only docinfo.html for head |
private-head |
Only {docname}-docinfo.html for head |
shared,private |
Both shared and private files |
For website integration, use:
:docinfo: sharedAdditional Docinfo Attributes
Besides :docinfo:, there are two more attributes for fine-tuning:
| Attribute | Values | Description |
|---|---|---|
:docinfodir: |
Directory path | Location of docinfo files. Defaults to the source file directory. |
:docinfosubs: |
Comma-separated substitutions | AsciiDoc substitutions applied to docinfo content. Default: attributes |
With :docinfosubs: you can control which AsciiDoc substitutions are performed on docinfo content – e.g., attributes for attribute replacement or specialchars for special characters.
Integrating docinfo in adoc Studio
In adoc Studio, place the docinfo.html file in your project’s document folder alongside your .adoc files:
Manual.ads/
├── Manual/
│ ├── docinfo.html ← Shared docinfo file
│ ├── attributes.adoc ← Has :docinfo: shared (must be at the top!)
│ ├── chapter1.adoc
│ ├── chapter2.adoc
│ └── ...Important: The :docinfo: shared attribute must be placed above the H1 title line (=) so it’s defined in the document header and applies to all documents.
Recommended structure:
Learn.ads/
├── Learn/
│ ├── start.adoc ← Attributes file (at the top of the sidebar!)
│ ├── docinfo.html
│ ├── intro.en.adoc
│ ├── chapter1.en.adoc
│ └── ...The start.adoc contains only attributes:
// Shared attributes for all Learn documents
:docinfo: shared
:icons: fontWhen you export to HTML, adoc Studio will automatically include the docinfo content in every generated page.
The Basic Structure of a docinfo.html
A docinfo.html for website integration typically contains three parts:
1. CSS Links
Loading external stylesheets for consistent styling:
<link rel="stylesheet" href="/assets/css/site.css">2. HTML Templates
Defining the elements to inject as JavaScript strings:
const navbarHTML = '<nav class="nav">...</nav>';
const footerHTML = '<footer class="footer">...</footer>';3. JavaScript Logic
Injecting and initializing the elements when the page loads:
document.addEventListener('DOMContentLoaded', function() {
document.body.insertAdjacentHTML('afterbegin', navbarHTML);
document.body.insertAdjacentHTML('beforeend', footerHTML);
});Setting Up Multilingual Support
Language Detection
For multilingual documentation, you need to detect the current language to show the correct translations. With adoc Studio’s website export, the language is embedded in the URL path:
- German:
/help/Manual/de/page.html - English:
/help/Manual/en/page.html
The structure is: /{target-folder}/{project-name}/{language}/page.html – where help is the folder on the web server where the adoc Studio project Manual was exported to.
Here’s how to detect it:
const path = window.location.pathname;
const manualLangMatch = path.match(/\/Manual\/(de|en)\//);
let lang = 'en'; // default
if (manualLangMatch) {
lang = manualLangMatch[1]; // 'de' or 'en'
}Using the Same docinfo.html for Multiple Projects
If you have multiple AsciiDoc projects (e.g., a manual and a learning path), you can use the same docinfo.html for all of them. Simply copy the file to each project folder.
For language detection and the language switcher to work across all projects, you need to extend the path detection:
var path = window.location.pathname;
// Detection for Manual AND Learn
var isManualPage = path.includes('/Manual/');
var isLearnPage = path.includes('/learn/');
var manualLangMatch = path.match(/\/Manual\/(de|en)\//);
var learnLangMatch = path.match(/\/learn\/(de|en)\//);
// Determine language
var lang;
if (isManualPage && manualLangMatch) {
lang = manualLangMatch[1];
} else if (isLearnPage && learnLangMatch) {
lang = learnLangMatch[1];
} else {
lang = (path.startsWith('/de/') || path === '/de') ? 'de' : 'en';
}For the language switcher, you also need to handle both path types:
var enUrl, deUrl;
if (isManualPage && manualLangMatch) {
var basePath = path.match(/^(.*\/Manual\/)/);
if (basePath) {
enUrl = basePath[1] + 'en/index.html';
deUrl = basePath[1] + 'de/index.html';
}
} else if (isLearnPage && learnLangMatch) {
var basePath = path.match(/^(.*\/learn\/)/);
if (basePath) {
enUrl = basePath[1] + 'en/index.html';
deUrl = basePath[1] + 'de/index.html';
}
}Tip: With localized folder names (e.g., German folders named differently than English ones), it’s often easier to switch to the index page of the respective language rather than translating the exact page.
Managing Translations
For localization, there are two approaches: manual (embedded translations) or automated (API-based). Both have their pros and cons.
Approach 1: Embedded Translations (Manual)
The simplest approach is to store all translations directly in your docinfo.html:
var translations = {
de: {
'nav.features': 'Funktionen',
'nav.pricing': 'Preise',
'footer.newsletter': 'Newsletter',
'footer.subscribe': 'Abonnieren'
},
en: {
'nav.features': 'Features',
'nav.pricing': 'Pricing',
'footer.newsletter': 'Newsletter',
'footer.subscribe': 'Subscribe'
}
};
// Helper function
var t = function(key) {
return translations[lang][key] || key;
};| Advantages | Disadvantages |
|---|---|
| Works offline | Translations must be manually kept in sync |
| No server dependency | Duplicate maintenance if website already has a translation system |
| Easy to understand |
Approach 2: Load Translations via API (Automated)
If your website already has a translation system (e.g., Kirby CMS language files), you can share them via API:
1. Create an API endpoint (/api/translations.php):
<?php
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$lang = $_GET['lang'] ?? 'en';
$allowedLangs = ['de', 'en'];
if (!in_array($lang, $allowedLangs)) {
http_response_code(400);
exit;
}
$langFile = __DIR__ . '/../site/languages/' . $lang . '.php';
$langData = require $langFile;
echo json_encode($langData['translations'] ?? []);2. Load translations in docinfo.html:
async function loadTranslations() {
try {
var response = await fetch('/api/translations.php?lang=' + lang);
if (!response.ok) throw new Error('API error');
return await response.json();
} catch (err) {
console.error('Falling back to embedded translations');
// Return fallback translations
return {
'nav.features': lang === 'de' ? 'Funktionen' : 'Features',
'nav.pricing': lang === 'de' ? 'Preise' : 'Pricing'
};
}
}
async function renderUI() {
var translations = await loadTranslations();
var t = function(key) { return translations[key] || key; };
// Build HTML templates with translations...
}| Advantages | Disadvantages |
|---|---|
| Single source of truth for translations | Requires network access |
| No duplicate maintenance | API must be deployed |
| Changes automatically in sync |
Our Recommendation: For smaller projects, Approach 1 (embedded) is sufficient. For larger projects with an existing translation system, we recommend Approach 2 (API) with fallback translations for offline scenarios.
Template Strings in docinfo.html
Important: AsciiDoc interprets {variableName} as attributes. Therefore, use string concatenation instead of JavaScript template literals:
// WRONG - AsciiDoc removes {lang}:
var url = `/api?lang=${lang}`;
// CORRECT - String concatenation:
var url = '/api?lang=' + lang;Then use translations in your templates:
var navbarHTML = '' +
'<nav class="nav">' +
' <a href="#features">' + t('nav.features') + '</a>' +
' <a href="#pricing">' + t('nav.pricing') + '</a>' +
'</nav>';Integrating Website Elements
Injecting a Navigation Bar
The navbar should be inserted at the very beginning of the body:
const navbarHTML = `
<nav class="nav" id="site-nav">
<div class="container">
<a href="/" class="nav__logo">
<img src="/assets/images/logo.png" alt="Logo">
</a>
<ul class="nav__links">
<li><a href="/#features">${t('nav.features')}</a></li>
<li><a href="/#pricing">${t('nav.pricing')}</a></li>
</ul>
</div>
</nav>`;
document.body.insertAdjacentHTML('afterbegin', navbarHTML);Reorganizing Built-in Elements into a SubNav
Now comes the crucial step: moving the built-in navigation elements into a SubNav that sits below your main navbar. This preserves all functionality while establishing the correct visual hierarchy.
Why This Step is Necessary
When you inject a navbar at the beginning of the body, the built-in elements (nav.home, nav.search-toggle, nav.language) remain in their original positions – which is now below your navbar but above the main content. Without reorganization, you’d have:
- Your navbar at the top
- Random navigation elements floating in between
- The main content
By moving these elements into a structured SubNav container, you create a clean, organized interface.
Creating the SubNav Container
First, create a container div and position it directly after your main navbar:
// Create SubNav container
const docSubnav = document.createElement('div');
docSubnav.id = 'doc-subnav';
document.getElementById('site-nav').insertAdjacentElement('afterend', docSubnav);Moving Built-in Elements
Now move each built-in element into the SubNav. The order matters – arrange them logically:
// Move breadcrumb/home navigation
const navHome = document.querySelector('body > nav.home');
if (navHome) {
docSubnav.appendChild(navHome);
}
// Move search toggle button
const navSearchToggle = document.querySelector('body > nav.search-toggle');
if (navSearchToggle) {
docSubnav.appendChild(navSearchToggle);
}
// Move language switcher
const navLanguage = document.querySelector('body > nav.language');
if (navLanguage) {
docSubnav.appendChild(navLanguage);
}Preserving Search Functionality
The search toggle button needs a click handler to show/hide the search panel:
if (navSearchToggle) {
navSearchToggle.addEventListener('click', function() {
const navSearch = document.querySelector('nav.search');
if (navSearch) {
navSearch.classList.toggle('is-active');
}
});
}Adding a Footer
The footer should be appended to the end of the body:
const footerHTML = `
<footer class="footer">
<div class="container">
<!-- Your footer content -->
</div>
</footer>`;
document.body.insertAdjacentHTML('beforeend', footerHTML);Implementing a Language Switcher
For the language switcher to work correctly within the Manual, you need to swap the language segment in the URL:
let enUrl, deUrl;
if (path.includes('/Manual/')) {
// Swap /Manual/de/ ↔ /Manual/en/
enUrl = path.replace(/\/Manual\/de\//, '/Manual/en/');
deUrl = path.replace(/\/Manual\/en\//, '/Manual/de/');
}
const langSwitcherHTML = `
<div class="lang-switcher">
<a href="${enUrl}" class="${lang === 'en' ? 'active' : ''}">EN</a>
<a href="${deUrl}" class="${lang === 'de' ? 'active' : ''}">DE</a>
</div>`;See it in action: Explore our Learning Path or the adoc Studio Manual to see how we integrated navbar and footer using docinfo.
Conclusion
The docinfo.html mechanism provides a powerful way to integrate AsciiDoc documentation seamlessly into your website. Depending on your project size, you can choose between:
- Manual: Self-contained docinfo.html with embedded translations – ideal for smaller projects
- Automated: API-based translations and shared JavaScript – ideal for larger projects with an existing CMS
Remember: AsciiDoc interprets {...} as attributes. Therefore, use string concatenation (' + variable + ') instead of template literals in your JavaScript strings.
The key is to think of docinfo.html as a bridge between your static documentation and your dynamic website – allowing them to work together as a cohesive whole.