Skip to content
Tools & Utilities Programming

Offline Docs Site Builder

By Romi Fadli, S.Kom. 7 min read Tooling & Automation
On this page
A stack of Markdown documents and an orange arrow leading to a browser page with a side menu, a heading, paragraphs, a dark code panel, and a table of contents with its active entry highlighted

Offline Docs Site Builder is a single script that turns a project's Markdown documentation into a small static website: a menu of every page, a table of contents that follows the reader, tables that never cut their cells short, and code blocks with a copy button. The site opens straight from the folder — no web server, no internet connection — and looks like the product it documents.

Documentation in Markdown is pleasant to write and awkward to read: on a server without a browser-friendly viewer, a team gets raw text, long tables that wrap into noise, and links that point at .md files. The builder keeps Markdown as the one source and gives the team pages they actually read.

At a glance

FormA single command-line script, with a page template and a small browser script
LanguagePython 3 with markdown-it-py; Tailwind CSS compiled locally; plain JavaScript in the page
StatusStable, in use
Source codePrivate
What the tool is, what it is built with, and its state.

What it does

A. Builds a site that opens without a server

Every page is assembled at build time from one template: the menu, the table of contents, the content, and the footer. Fonts and styles are stored next to the pages, so opening index.html from a copied folder or a USB stick shows the same site as a web server would. The project's README becomes the home page, and every numbered file such as 01-architecture.md becomes a page, titled by its first heading.

B. Keeps Markdown the only source

The HTML is output, never edited by hand. Every run rebuilds every page, so a page cannot fall behind its Markdown, and a --list option shows the pages it found and the state of each.

C. Makes tables and code usable

Tables sit in a scrolling panel with column widths left to the content and cells that are never truncated, because a cell in operational documentation can be 190 characters long and hiding it costs the reader. Every column except the last can be widened by dragging its edge, down to a minimum width, without selecting text on the way. Code blocks get a panel with the language named and a Copy button that copies the exact text, indentation included, even when the page was opened from a file.

Headings receive the same anchors GitHub would give them, so links already written in Markdown still land in the right place. Links between Markdown files are rewritten to the matching HTML pages, including links from the README and links written with a folder prefix. A link to a page whose source is missing is turned into plain, disabled text rather than a dead link, and the menu shows that page greyed out.

E. Follows the reader through long pages

A table of contents is built from the second- and third-level headings and highlights the section currently on screen as the reader scrolls. Its entries are kept as plain text until the last step, so a heading with an ampersand is not escaped twice.

How I use it

bash
build-docs-html                # rebuild every page from docs/*.md
build-docs-html --list         # pages found and their status
tailwindcss -c docs/html/src/tailwind.config.js \
  -i docs/html/src/tailwind.input.css \
  -o docs/html/assets/tailwind.css --minify     # only when new classes appear
In MarkdownOn the page
A headingan anchor matching GitHub's, and an entry in the table of contents
A tablea scrolling panel with full cells and columns that can be widened
A fenced code blocka panel with its language and a Copy button
A link to another .md filea link to its HTML page, or disabled text when the source is missing
The READMEthe home page
What each Markdown element becomes.

Design decisions

The site has to open from a folder, so it is generated rather than assembled in the browser. Static HTML has no include, and a page opened from a file is not allowed to fetch another file, so a shared header and menu can only exist if they are written into every page at build time. Fonts and the compiled stylesheet live beside the pages for the same reason, and the build uses a Markdown library that was already installed — CommonMark with tables and strikethrough covers every element the documentation uses — so the tool adds no new dependency. When a template change introduces new style classes, the build prints the one command that recompiles the stylesheet.

Markdown stays the only source, and partial builds are gone. The documentation is written and reviewed as Markdown, next to the code; the HTML is a view of it. An earlier version built only some pages while their design was being judged, and that left other pages silently stale, so every run now rebuilds all of them.

Reading comfort comes from the details of the theme, not from a plugin. The interface the documentation belongs to does not use a typography plugin, so the builder gives each element its classes itself, and the pages share the product's colours, fonts, and dark surfaces. Paragraphs are justified with automatic hyphenation tied to the page's Indonesian language, because long technical terms that cannot be broken leave wide gaps between words. Tables never truncate, columns can be widened, code can be copied with its indentation intact, and the table of contents keeps the reader oriented in long pages.

Links must survive the conversion. Anchors follow GitHub's rules exactly, including two details that once broke them: HTML entities are decoded before a heading becomes an anchor, so an ampersand does not turn into the letters of its entity, and punctuation is removed rather than replaced, so a dash between spaces leaves a double hyphen just as GitHub does. Links to Markdown files become links to their pages, and a link that would lead nowhere is disabled instead of producing a 404.

Quiet rendering mistakes are caught in the build, not by readers. The Markdown library writes horizontal rules as self-closing tags and aligned table cells with a style attribute, and both would have slipped past the styling and appeared in the browser's defaults — a bright line across a dark page, a cell without its padding — so both are handled explicitly. A trailing rule that only separated the closing block in Markdown is removed, and the first heading on a page does not carry a top margin inside its card. A missing library or template stops the build with the command that fixes it rather than a traceback.

Specifications

InputsThe project README and numbered Markdown files in the documentation folder
OutputsOne HTML page per source, with a menu, a table of contents, and the shared template; written into the documentation's HTML folder
TemplatePlaceholders for the title, menu, table of contents, content, and year
Works offlineYes: fonts, stylesheet, and script are local, and pages open from the file system
RequirementsPython 3 with markdown-it-py; the Tailwind command-line tool only when styles change
Inputs, outputs, and requirements of the tool.

The script follows the shape I give every small tool: one file, one job, and the same order of sections every time.

Why documentation is written for the next reader, and lives beside the code it explains.

The same idea from a different source: turning text written for tools into pages people read.

Terms

Static site
A set of ready-made HTML pages that need no program running on a server to be shown.
Markdown
A plain-text format whose light markup, such as # for headings, turns into formatted text.
CommonMark
A precise specification of Markdown that different tools can render the same way.
Anchor
An identifier on a heading that a link can point to, written after a # in the address.
Hyphenation
Breaking a long word across two lines at a valid point, so justified text does not leave wide gaps.

Technologies covered

  • Python

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

A purple root circle linked to a blue and an orange intermediate circle, which lead to a signed document with a green seal and to a clock.

Aurograf

DevOps

The digital signature platform of the City Government of South Tangerang: signers prove who they are, sign PDFs to PAdES B-LTA with certificates from the platform's own authorities, and anyone can verify the result — version 2, which I designed, built and operate.

10 min read Read more →

Related Articles

An old layout with a grey banner and a new structure with a green banner, each holding a blue database, a purple document store, and an orange telemetry store; three coloured paths pass through one export package marked with a green check

Four commands that move a stack from its old layout into a new structure across major versions: export without changing the source's data, import each store the way its version allows, verify what really arrived, and rename modules while moving their data along.

11 min read Read more →
Three image layer stacks, each with a version tag of its own colour, blue, orange, and green, each leading to an archive file banded in the same colour

Commands that version every module of a stack on its own, build its image under a tag that means exactly one build, refuse to reuse a version or swallow a failed compile, and archive images by name and tag so versions never overwrite each other.

7 min read Read more →
Four identical grey keys on the left each become a key of its own colour on the right, every new key marked with a green check; below, a purple key stays behind a red bar

One command that rotates an environment's internal credentials key by key: a dry run by default, a backup first, a unique value for every key, a real login to prove each credential that can log in, and rollback for any that fail.

8 min read Read more →