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:

  1. The default nav elements occupy the top position
  2. Your site navbar also needs to be at the top
  3. 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 document

Shared docinfo – Used by all documents in the folder:

docinfo.html                  ← Shared by all .adoc files
chapter1.adoc
chapter2.adoc

For 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: shared

Additional 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: font

When 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>`;

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.

Additional Resources