- Static Sites via Docs as Code
- Choosing a Static Site Generator
- The Virtual Environment
- Installing Packages
- Data Isolation
- Project Creation
- Project Configuration
- Building and Serving the Site
- The TOC Tree
- Building the Documentation System
- Version Control
- Publishing Changes
- Automation
- Docs or It Didn’t Happen
One great aspect of some of the most successful and long-lived Free/Libre Open Source (FLOSS) projects is the high quality of their documentation. Well-structured, approachable, and attractive documentation can help you decide whether you have found the right solution, provide an edification opportunity, and serve as an ongoing reference for future issues you may encounter.
We live in an information-dense world, and as we continually learn, it is helpful to keep track of and organize the knowledge we accumulate. Creating a personal documentation system is an excellent way of accomplishing this.
As a GNU/Linux user, you have likely already spent significant time becoming versed in developer-oriented technologies, including:
The same tools provide the base for a powerful and convenient approach to documentation, referred to as Documentation as Code (Docs as Code).
Note: If you are not familiar with the GNU/Linux command line interface, review the Conventions page before proceeding.
Static Sites via Docs as Code
Docs as Code refers to a philosophy of writing documentation with the same tools and methodologies that are used to write code. The basic idea is that content is written in a user-friendly and accessible markup language (e.g., Markdown, reStructuredText), tracked via a version control system (Git), and then automatically tested/published to a serving resource.
Often, a static site generator is used to convert the plain text markup into a final product that can be locally viewed or pushed to a remote web server. There are many FLOSS static site generators to choose from that are based on different technologies, but they all share a few basic characteristics:
- Content is created in a plain text markup language.
- Templates are used to provide the HTML structure for the final site.
- A CLI tool is used to inject the content into the template files and generate the final site.
- How and where you deploy your final site is up to you.
- Extensions/plugins are available to add functionality.
This paradigm differs from many of the traditional content management systems (CMSs) that are database-driven web applications. By utilizing a static site generator and a Docs as Code approach, you can avoid many traditional CMS drawbacks:
-
Improved security posture.
Compared to a database-driven web application, a static website has a much simpler setup with fewer dependencies. This means less surface area for vulnerabilities, less time and effort to get to a secure state, and an easier ongoing experience in maintaining secure content delivery.
-
Improved performance.
While web applications can be configured to serve static content, they often dynamically generate requested HTML pages in real-time. With a static site, your server is simply serving your content, which often leads to better performance for content consumers.
-
Reduced administrative burden.
With a web application, there is likely a complicated stack of software that needs to be continually monitored, updated, and troubleshot. On top of this, you will need to monitor, update, and troubleshoot the CMS itself (along with its themes and plugins).
If you are using a static site, you are basically maintaining a web server, and even that can be handed off to a third-party provider, if you choose to. There is the local stack used to generate your static site to deal with, but that is likely much easier to maintain (and already being taken care of through the administration of your GNU/Linux system).
-
Retention of dynamic elements.
Just because a site is static does not mean that it cannot deliver a dynamic experience. Compared to a database-driven web application, dynamic functionality for a static site is confined to the client side (i.e., the end user's web browser). Through technologies like CSS, JavaScript, and WebAssembly, rich, interactive static sites can be created, without the security, performance, and administrative baggage of traditional CMSs.
-
Data accessibility.
With a static site generator, your content is just a collection of plain text files and assets on your local file system. Since content it not being stored in a database management system (DBMS), it always remains easy to access with whatever tool you desire. Being able to open your favorite editor and simply view your content is a nice alternative to using a What You See Is What You Get (WYSIWYG) editor or running a Structured Query Language (SQL) query.
Static sites via Docs as Code are not always the best solution. If the CMS needs to be used by numerous people that are not comfortable with a developer-oriented toolchain, you are dealing with large amounts of media assets, or you need a well-developed WYSIWYG editor and graphical dashboard, you may be better off with a traditional CMS.
However, for a personal documentation system, adopting Docs as Code and a static site generator is an excellent choice.
Choosing a Static Site Generator
Two very popular static site generators for maintaining a document store are MkDocs and Sphinx. Both are Python-based and use Jinja as their templating engine. Either one of these projects is a fine solution for a personal documentation system, but there are some differences.
MkDocs is based on Markdown and is simpler to get started with. Sphinx is based on reStructuredText (but has excellent Markdown support via MyST), has greater functionality/extensibility, but has a slightly higher learning curve.
Another important difference is related to maintenance. MkDocs is maintained by a relatively small number of people. Sphinx is a slightly older project with a larger maintenance team, perhaps in part because it has been adopted as the documentation generator of choice for numerous FLOSS projects, including:
- Certbot
- coreboot
- Django
- Firefox Source Docs
- Flatpak
- IPython
- JupyterLab
- Linux Vendor Firmware Service
- Matplotlib
- Numpy
- pandas
- Python
- Read the Docs
- scikit-learn
- SciPy
- The Linux Kernel
- Write the Docs
If you decide that MkDocs is a better fit for your documentation needs, these resources may help you get started:
- Getting Started with MkDocs - The project's official getting started tutorial.
- User Guide- MkDocs user documentation.
- Developer Guide - Documentation for MkDocs third party theme and plugin developers.
- MkDocs Themes - A list of third-party MkDocs themes.
- MkDocs Plugins A list of third-party MkDocs plugins.
- Material for MkDocs - The most popular MkDocs theme. If you happen to come across a MkDocs site, it is probably using Material.
- Build Your Python Project Documentation With MkDocs - A nice tutorial that walks through building project documentation using MkDocs from the folks at Real Python.
There are also several good personal and professional examples of MkDocs usage in the wild:
Examining how these sites are set up may give you ideas on how to best customize your MkDocs implementation.
If you are curious about Sphinx, but are not sure if it is worth the slightly higher initial investment time, the following presentation by Paul Everitt does a great job of explaining the power and flexibility of the software:

The following sections will walk through a Sphinx setup, but (outside of project-specific commands) most of it will apply to any Python-based static site generator.
The Virtual Environment
Sphinx is a Python-based static site generator, so after making sure that pip
is set up, our first step (as with any Python-based project) is to create a virtual environment. We create our virtual environment at ${HOME}/venvs/sphinx/
:
$ python3 -m venv --upgrade-deps "${HOME}/venvs/sphinx/" &&
cd "${HOME}/venvs/sphinx/" &&
source 'bin/activate' &&
python3 -m pip install wheel
The above commands do several things:
- Create a virtual environment at
${HOME}/venvs/sphinx/
and update its core dependencies (i.e.,pip
andsetuptools
). - Change to the virtual environment directory.
- Activate the virtual environment.
- Install
wheel
in the virtual environment.
Installing Packages
Next, we need to install the software packages in our virtual environment needed for our Sphinx site. In addition to the Sphinx package, there are several other packages to install:
myst-parser
- MyST is a rich and extensible flavor of Markdown meant for technical documentation and publishing. This package enables us to elegantly use Markdown with our Sphinx site. In addition, it maps new Markdown syntax to Sphinx-specific features, thus enabling us to continue using Markdown, while simultaneously gaining the additional functionality of Sphinx.sphinx-book-theme
- Sphinx comes with a great default theme, Alabaster, but there are many other nice Sphinx themes to choose from. Sphinx Book Theme is one of them.sphinx-autobuild
- This package rebuilds Sphinx documentation on changes with live-reload in the browser. Essentially, this lets you preview the site in the browser as you work on your documentation changes.sphinx-copybutton
- Often, you will see a handy option to copy the contents of a code block as you peruse documentation.sphinx-copybutton
adds this functionality to Sphinx sites.
To install these packages in our virtual environment, we run:
(sphinx) $ python3 -m pip install \
myst-parser \
Sphinx \
sphinx-autobuild \
sphinx-book-theme \
sphinx-copybutton
Data Isolation
Virtual environments are often ephemeral, so we do not want to store our Sphinx content inside of it. So, our next step is to create a docs/
directory in a more persistent location, and then create a symbolic link to it in the virtual environment directory:
(sphinx) $ mkdir -p "${HOME}/venv_projs/docs/" &&
ln -s "${HOME}/venv_projs/docs/" "${HOME}/venvs/sphinx/docs"
Project Creation
A Sphinx project has a specific structure, and Sphinx comes with an interactive script that automatically creates this structure for you, sphinx-quickstart
. We change to the directory where we want to create our Sphinx project and run the script:
cd "${HOME}/venvs/sphinx/docs/" && sphinx-quickstart
After running the above commands, we are asked several questions regarding how to configure the project. We answer like so:
> Separate source and build directories (y/n) [n]: y
> Project name: Docs
> Author name(s): Monty
> Project release []:
> Project language [en]:
Afterwards, the requisite directories/files will be created and you should see a message that starts with a line like:
Finished: An initial directory structure has been created.
Let us quickly look at the created directory tree:
docs
├── build
├── make.bat
├── Makefile
└── source
├── conf.py
├── index.rst
├── _static
└── _templates
build/
- The directory that holds the rendered content.
make.bat
,Makefile
- Convenience scripts to simplify some common Sphinx operations (e.g., rendering the content).
source/conf.py
- The Python script holding the configuration of the project. This file contains the project name and release specified during
sphinx-quickstart
, as well as additional configuration keys. source/index.rst
- The root document of the project.
source/index.rst
serves as the welcome page and contains the root of the table of contents tree (toctree), i.e., a way to connect multiple files into a single hierarchy of documents. source/_static/
- The directory for custom stylesheets and other static files.
source/_templates/
- The directory for custom HTML templates.
Project Configuration
Before we build and serve the site, let us take a look at the default source/conf.py
file:
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'Docs'
copyright = '2023, Monty'
author = 'Monty'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = []
templates_path = ['_templates']
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'alabaster'
html_static_path = ['_static']
Via source/conf.py
, we can use Python constructs like variables, strings, and lists to make configuration changes to our Sphinx site. The Project information
section looks OK, but we need to make some changes to the General configuration
and Options for HTML output
sections.
Afterwards, source/conf.py
should look like this:
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'Docs'
copyright = '2023, Monty'
author = 'Monty'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = ['myst_parser', 'sphinx_copybutton']
templates_path = ['_templates']
exclude_patterns = []
myst_enable_extensions = [
'deflist',
]
myst_heading_anchors = 6
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_book_theme'
html_static_path = ['_static']
html_title = 'Docs'
General Configuration
- In the
extensions
list, we add the module names of the Sphinx extensions we want to use. myst_enable_extensions
is one of many MyST global configuration variables. Specifically, we use it to enable definition lists.myst_heading_anchors
specifies the depth to which Markdown headings should be converted into HTML anchor links.
Options For HTML Output
- We change
html_theme
from the default theme value to our new theme module name,sphinx_book_theme
. - The title of a Sphinx project defaults to
<project> v<revision> documentation
. We usehtml_title
to change it to the simplerDocs
.
Building and Serving the Site
Before we start adding content, let us see what we have so far looks like by building and serving the site with sphinx-autobuild
from the ${HOME}/venvs/sphinx/docs/
directory:
(sphinx) $ make clean &&
sphinx-autobuild \
-b html -j auto \
"${HOME}/venvs/sphinx/docs/source/" \
"${HOME}/venvs/sphinx/docs/build/"
make clean
wipes the build/
directory before sphinx-autobuild
builds the site. This ensures that all of your changes are reflected in the new build.
sphinx-autobuild
's -b
and -j
options are passed as is to the underlying sphinx-build
command, which sphinx-autobuild
utilizes (run sphinx-build --help
for more information):
-b html
is used to specify the builder to use (here, we are buildinghtml
output files).-j auto
builds the site withN
processes, whereN
is equal to your system's central processing unit (CPU) count (this should yield faster build times).
Then, we provide sphinx-autobuild
with the paths of the source/
and build/
directories, respectively. After we run the command, we can view our site at http://127.0.0.1:8000/.
The Sphinx Book Theme is a very nice responsive theme that has helpful features common to many other popular Sphinx themes, including a light/dark theme toggle, built-in search, and dedicated navigation areas (both for the site as a whole and for the currently viewed document).
The TOC Tree
Sphinx offers many of its features through directives, generic blocks of explicit markup that can have:
- Arguments - Given directly after the colon following the directive's name. Each directive decides whether it can have arguments, and how many.
- Options - Given after arguments in the form of a field list.
-
Content - Follows the options or arguments after a blank line. Each directive decides whether to allow content, and what to do with it.
Make sure that the first line of content is indented to the same level as the options.
Sphinx is built around reStructuredText (reST), which cannot interconnect several documents or split documents into multiple output files. To address this, Sphinx uses a custom directive, toctree
, to add relations between the single files the documentation is made of, as well as tables of contents.
We can see what this looks like by examining the default source/index.rst
file that was created for us:
.. Docs documentation master file, created by
sphinx-quickstart on Sat Mar 18 17:40:39 2023.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Docs's documentation!
================================
.. toctree::
:maxdepth: 2
:caption: Contents:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
source/index.rst
starts off with an explicit markup block (i.e., a line that starts with ..
) that is not followed by a valid markup construct. This is how a comment is created in reST:
.. Docs documentation master file, created by
sphinx-quickstart on Sat Mar 18 17:40:39 2023.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
The above example is also a multiline comment, which is created by indenting text after a comment start.
After the comment is the index page's first section header. Section headers are created by underlining (and optionally overlining) the section title with a punctuation character, at least as long as the text:
Welcome to Docs's documentation!
================================
Normally, there are no heading levels assigned to certain characters, as the structure is determined from the succession of headings. However, this convention is used in Python's Style Guide for documenting, which you can adopt:
#
with overline, for parts*
with overline, for chapters=
for sections-
for subsections^
for subsubsections"
for paragraphs
You can use your own marker characters and use a deeper nesting level, but remember that most target formats (e.g., HTML, LaTeX) have a limited supported nesting depth.
Next, we come to the toctree
directive:
.. toctree::
:maxdepth: 2
:caption: Contents:
This toctree
directive block does not have any arguments or any content, which is why the navigation area for the site is empty. However, we do see two options, maxdepth
and caption
.
maxdepth
is used to indicate the depth of the tree. By default, all levels are included.caption
is used to provide atoctree
caption.
The final section of source/index.rst
contains a list of internal subsection links. Internal linking to specific subsections is done via a special reST role provided by Sphinx, :ref:
:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
These internal links use special names and are for the general index, Python module index, and the search page.
Building the Documentation System
Often, Sphinx is used to document code-based projects. Since we aim to create a personal documentation system that includes code (but is not specifically documenting code), let us simplify source/index.rst
to something more appropriate.
First, we create some new directories in ${HOME}/venvs/sphinx/docs/source
. Assume that we intend on creating documents geared towards science and technology, and we want those files to be collected into separate directories. Also, we want them to be available in our documentation site under different navigational sections:
(sphinx) $ mkdir -p \
"${HOME}/venvs/sphinx/docs/source/science/biology/" \
"${HOME}/venvs/sphinx/docs/source/technology/computer-science/"
Next, we create example documents in each directory. Since Sphinx and MyST allow us to mix reST and Markdown content, we create each document as a Markdown document:
(sphinx) $ echo -e '# Science Notes\n\nExample _science_ notes.' > \
"${HOME}/venvs/sphinx/docs/source/science/sci-notes.md" &&
echo -e '# Ecology Notes\n\nExample _ecology_ notes.' > \
"${HOME}/venvs/sphinx/docs/source/science/biology/ecology.md" &&
echo -e '# Technology Notes\n\nExample _technology_ notes.' > \
"${HOME}/venvs/sphinx/docs/source/technology/tech-notes.md" &&
echo -e '# Algorithm Notes\n\nExample _algorithm_ notes.' > \
"${HOME}/venvs/sphinx/docs/source/technology/computer-science/algo-notes.md"
For each directory we create, we also create a separate file that serves as an index page for that directory. Each of these pages will contain its own toctree
.
These pages will be included in source/index.rst
's toctree
, which will enable us to nicely navigate our document structure via our theme. Documents serve as the toctree
directive content and are given as document names, i.e., leave off the filename extensions and use forward slashes (/
) as directory separators.
We can create these files as reST or Markdown files, and how you choose to set up your documentation system is up to you. For example, this is how a toctree
could be set up for our science
directory using MyST:
```{toctree}
:maxdepth: 1
biology/biology
sci-notes
```
Depending on your preference and the construct in question, the syntax for reST or Markdown will seem preferable. For now, we will continue creating toctree
s using reST.
We create the following four files:
-
${HOME}/venvs/sphinx/docs/source/science/science.rst
Science ======= .. toctree:: :maxdepth: 1 biology/biology sci-notes
-
${HOME}/venvs/sphinx/docs/source/science/biology/biology.rst
Biology ======= .. toctree:: :maxdepth: 1 ecology
-
${HOME}/venvs/sphinx/docs/source/technology/technology.rst
Technology ========== .. toctree:: :maxdepth: 1 computer-science/computer-science tech-notes
-
${HOME}/venvs/sphinx/docs/source/technology/computer-science/computer-science.rst
Computer Science ================ .. toctree:: :maxdepth: 1 algo-notes
After these changes, we end up with a source/
directory structure like so:
source
├── conf.py
├── index.rst
├── science
│ ├── biology
│ │ ├── biology.rst
│ │ └── ecology.md
│ ├── science.rst
│ └── sci-notes.md
├── _static
├── technology
│ ├── computer-science
│ │ ├── algo-notes.md
│ │ └── computer-science.rst
│ ├── technology.rst
│ └── tech-notes.md
└── _templates
Finally, we need to update our source/index.rst
file to include science.rst
and tecnology.rst
, and to do some customization (e.g., the hidden
option makes sure that the toctree
is not shown on the source/index.rst
page content area itself, since our theme will take care of displaying it in the navigation area):
.. Docs documentation master file, created by
sphinx-quickstart on Sat Mar 18 17:40:39 2023.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Personal Documentation System Demo
==================================
.. toctree::
:caption: Table of Contents
:hidden:
:maxdepth: 2
science/science
technology/technology
Utilize the **power** of Sphinx and retain the *simplicity* of Markdown by using `MyST <https://myst-parser.readthedocs.io/en/latest/index.html>`_.
If you were already previewing the site, press Ctrl+c to terminate the process and run it again to view the changes:
(sphinx) $ make clean &&
sphinx-autobuild \
-b html -j auto \
"${HOME}/venvs/sphinx/docs/source/" \
"${HOME}/venvs/sphinx/docs/build/"
Version Control
The basic structure of the documentation system is complete. You are the only person working on the documents, but before adding more content, you want to:
- Implement version control (Git)
- Keep everything on your local system
- Keep things simple
- Be able to work and track your changes offline
Also, you do not want to have the administrative burden of maintaining a Git forge, like Gitea or GitLab.
You can accomplish this by initializing a local directory on your system as a bare repository, which is a repository without a working directory. For example, assuming that you have already configured Git and are using main
as the default branch name (git config --global init.defaultBranch main
), we create ${HOME}/sites/docs.git/
and initialize it as a bare directory:
$ mkdir -p "${HOME}/sites/docs.git/" &&
cd "${HOME}/sites/docs.git/" && git init --bare
Then, we can set up ${HOME}/venvs/sphinx/docs/source/
with Git:
$ cd "${HOME}/venvs/sphinx/docs/source/" &&
git init && git add . && git commit -m 'Initial commit' &&
git remote add origin "${HOME}/sites/docs.git/" &&
git push --set-upstream origin main
Now, you can work on your documentation system, preview your work, and version control it in a self-contained, offline fashion.
Publishing Changes
When using a static site, publishing your content is a relatively easy endeavor. Essentially, you need to serve the contents of the build/
directory. Usually, this means syncing the contents of this directory after you create a new build of your site with a web server directory that you have configured to serve content.
GNU/Linux makes available wonderful tools for securely synchronizing changes like this over a network connection, ssh
and rsync
.
An example build/sync workflow could look like this:
-
Add, commit, and push your changes:
cd "${HOME}/venvs/sphinx/docs/source/" && git add . && git commit -v && git push
-
Activate the virtual environment:
source "${HOME}/venvs/sphinx/bin/activate"
-
Change to the
docs/
directory:cd "${HOME}/venvs/sphinx/docs/"
-
Run the build commands:
(sphinx) $ make clean && sphinx-build \ -b html -j auto \ "${HOME}/venvs/sphinx/docs/source/" \ "${HOME}/venvs/sphinx/docs/build/"
-
Sync the changes (note that
rsync
needs to be installed on both the local and remote system):$ rsync \ -aHAXxz \ --delete \ -e "ssh -p ex_port" \ --filter='-x security.selinux' \ --info=progress2 \ --numeric-ids \ "${HOME}/venvs/sphinx/docs/build/" \ ex_user@ex_host:ex_dest/"
Automation
You have walked through the process of creating, version tracking, building, and publishing content using a static site generator. However, an important part of the Docs as Code approach we have not yet incorporated is automation.
The benefits of static site generators over traditional CMSs are manifold, but continually entering these commands is onerous and error prone. Automation is preferable.
As a GNU/Linux user, you have likely already invested the time into learning the foundation of the Docs as Code stack. You are ready to create your own Docs as Code automation solution.
All that you have seen in this post is essentially a series of sequential commands, which means that it is scriptable. In addition, outside of the specific static site generator commands used in the examples, the basic flow is generic and common to other static site generators.
Imagine creating a series of scripts that can be independently used or chained together that cover the entire Docs as Code process. For example, there could be a script to:
- Build and preview the site
- Add, commit, and push site changes to the version control system (Git)
- Build and publish the site
Whichever static site generator you decide to use, if you ever move to a different piece of software, you should be able to update the software-specific build/preview commands and continue using the scripts.
Your scripts can provide the basis for a versatile, stable, long-lived automation solution.
Docs or It Didn’t Happen
Organized, well-maintained documentation can be a boon and help us make sense of the deluge of information we are exposed to. By committing yourself to GNU/Linux and FLOSS, you are empowered with the tools you need to get started with the Docs as Code paradigm.