Introducing nrstatic

After years of using WordPress on my websites, I became frustrated with the overhead of maintaining WordPress and its plugins. The process of writing technical articles in WordPress was also a bit annoying. I looked at some of the alternative tools, including static webpage generators that already exist, but none of them were quite what I wanted. So I developed my own, which I call nrstatic. It’s the tool that created all of the pages on the website you are currently viewing.

The pages are written as Markdown files with a bit of metadata added to the header. Mathematical expressions are written in $\rm \LaTeX$ and rendered on your browser using ${\boldsymbol{\mathsf{\color{green}{Math}\color{black}{Ja\mathcal{x}}}}}$. Code syntax highlighting is accomplished with highlight.js. There are also several built-in tools for making plots, graphs, and other diagrams. It is easy to hide elements and then reveal them when the user clicks a link. For example, you can view the source code of the build utility by clicking here.

#! /usr/bin/env python3

import glob

from nrstatic.filedb import FileDB
from nrstatic.rendering import render_page


if __name__ == '__main__':
    files = glob.glob("**/*.md", recursive=True)

    fdb = FileDB()

    for filename in files:
        render_page(filename, fdb)

    fdb.update_templates()

    fdb.close()

If you read through the source above, you may have noticed that the code recursively searches the entire directory tree below the current directory, searching for .md files to render to HTML. Only pages that have changed since the last build are re-generated, which means that the build process is usually quite fast (less than 1 second) on modern hardware. If an HTML template file changes, then all files that use that template are re-generated. If you want to view the Markdown file that was used to generate any page, simply load the index.md file in that directory. The file that generated this page is here.

There’s also a nice little feature which allows you to display only one <div> element at a time, in a particular region of the page. For example, Show Ex1 | Show Ex2 | Show Ex3.

Example 1

This is Example 1, showing the content of the first <div> that shares this region of the page. The example CSS class applied to this.

Example 2

This is Example 2, showing the content of the second <div> that shares this region of the page. This one contains an equation:

$$\nabla \cdot \mathbf{B} = 0$$

The example and input classes are applied to this div.

Example 3

This is the third example, showing the content of the final <div> that shares this region of the page. The example and output classes are applied to this div.

The code isn’t quite ready to release for others to use, but I expect that it will be ready to share before the end of 2025 (a few more features need to be added and detailed documentation needs to be written).

Math example

Here is an un-numbered, un-labeled displaystyle equation:

$$\frac{\partial L}{\partial q_i}(t,\mathbf{q}(t),\dot{\mathbf{q}}(t))-\frac{\d}{\d t}\frac{\partial L}{\partial \dot q_i}(t,\mathbf{q}(t),\dot{\mathbf{q}}(t)) = 0.$$

And this is a labeled equation using the equation environment:

\begin{equation} \int_0^\infty e^{-x^2}\d x = \frac{\sqrt{\pi}}{2} \label{eq:gauss} \end{equation}

This can be referenced as \eqref{eq:gauss} or as Eq. \ref{eq:gauss}. And here are some inline mathematics: $A=\pi r^2$.

Extended Markdown

Standard Markdown and extended Markdown features are implemented using the markdown module for Python1. For example, the the ability to generate tables is enabled, as in:

Column 1 Column 2
row 1 Text for row 1
row 2 Text for row 2
row 3 Text for row 3

The simplot environment

The simplot environment is an environment for making simple plots of functions. The author simply writes the functions that will be plotted directly into the Markdown source file, as demonstrated below. The functions are then plotted as an SVG image and put into an HTML figure.

This is an automatically-generated plot.

To use simplot, simply create a fenced code block and add class="simplot" to the attributes. A caption can optionally be added as well. The source that generated the plot above is here:

```{class="simplot center" caption="This is an automatically-generated plot."}

f = ["2 * x^3 - 4 * x", 
     "3 * x*x + 2 * cos(4*x)", 
     "2 * cos(4*x)"]
linestyle = ['--', '-', ':']
linecolor = ['m', 'b', 'g']
linewidth = [3, 1, 2]
label = ["series 1", 'series 2', '2 * cos(4x)']

grid = True # turn on the grid lines
x = [-2, 2] # lower and upper values of the x-axis
y = [-4, 4] # lower and upper values of the y-axis
title = "Three Functions"
xlabel = "x-axis-label"
ylabel = "y-axis-label"
```

The function (or functions) to be plotted must be entered in the array f = [].

The genplot environment

The genplot environment is the environment for making more general plots by writing the code for the plot directly into the Markdown source file. The code within the block is expected to write an output image with the basename given by the fname variable. It adds a file extension (like .svg or .png) and sets the final file name in the variable filename. The code within the block could be a Python script or you can use the python subprocess module to call literally any other executable code that you have installed on your machine. An example is shown below:

A non-trivial plot made within the genplot environment.

To use genplot, simply create a fenced code block and add class="genplot" to the attributes. A caption can optionally be added as well. The source that generated the plot is here: click to show

```{class="genplot center" caption='A non-trivial plot made within the genplot environment.'}
import numpy as np
import matplotlib.pylab as plt
from scipy.stats.kde import gaussian_kde

n_points = 10000
xvals = np.random.randn(n_points)
yvals = np.random.randn(n_points)

pdf = gaussian_kde([xvals, yvals], bw_method=0.14)
grid_x, grid_y = np.mgrid[-5:5:0.075, -5:5:0.075]
density = pdf(np.vstack([grid_x.flatten(), grid_y.flatten()])).reshape(grid_x.shape)

v = plt.pcolormesh(grid_x, grid_y, density, shading='gouraud', cmap=plt.cm.YlOrBr)

levels = [0.05, 0.1, 0.15]
colors = ['k', 'w', 'w']

contours = plt.contour(grid_x, grid_y, density, levels, 
                       linestyles=':', 
                       linewidths=0.6,
                       colors=colors)

plt.clabel(contours,
           inline=True,
           inline_spacing=0, 
           fontsize=8,
           fmt='%1.2f')

plt.colorbar(v, pad=0.015)
plt.xlim((-4, 4))
plt.ylim((-4, 4))
plt.xlabel('x')
plt.ylabel('y')
plt.title('2D Probability Distribution', fontsize=14, fontname='Liberation Serif');

# the plotting code must define filename:
filename = f"{fname}.png" # here's where you use `fname` and define `filename`
plt.savefig(filename)
```

The first time that a plot is generated, the processing may take a while if the plotting code is computationally expensive. On future builds, the plot will not need to be generated again unless the code that was used to create it changes. Each output image file name contains a hash of the code that created the image. A side effect of this is that the directory containing the Markdown file will accumulate many different versions of the same plot as the plot evolves over time, unless the images are manually deleted.

A Graphviz graph example

Graphviz graphs can automatically be generated by putting the code for the graph in a code block and adding the attribute class="graph". Optionally, a caption can be added. Here’s an example:

This is a tree.

View source

```{class="graph center" caption="This is a tree."}
digraph G {

bgcolor="#ffffff00"

node [shape=circle, width=0.5, fixedsize=true];

0 -> 1
0 -> 2

1 -> 3
1 -> 5

2 -> 4
2 -> 6

3 -> 7
3 -> 9

5 -> 11
5 -> 13

4 -> 8
4 -> 10

6 -> 12
6 -> 14

}
```

A Mermaid UML example

Similarly, Mermaid UML syntax is also enabled. Simply add class="mermaid" to a fenced code block containing Mermaid UML syntax and it will be rendered. For example:

flowchart TB A e1@--> C A e2@--> D B --> C B --> D e1@{ animate: true } e2@{ animate: true }

View source

```{class="mermaid center"}

flowchart TB
    A e1@--> C
    A e2@--> D
    B --> C
    B --> D
e1@{ animate: true }
e2@{ animate: true }
```

Bash command output inclusion

The standard output stream from any Bash command string can be inserted into the page by simply putting << command at the beginning of a line (with no whitespace before it). This allows for essentially endless possibilities. To include the contents of another file on the local system:

 << cat /path/to/file

To include something downloaded from a web server:

 << wget -q -O - URL

To run a command on a remote system on which you have set up passwordless SSH access and include the output of that command:

 << ssh username@remote "command"

A concrete example:

 << echo "This was last built $(date -u) on a system named '$(hostname).'"

becomes:

This was last built Wed Jun 25 09:37:20 AM UTC 2025 on a system named ‘Gauss.’

Automatic index creation

Setting the metadata parameter MAKE_INDEX = True causes nrstatic to create a summary page containing links and summaries of all of the pages in the directory tree under that page. For example, the blog page is created using this feature.

An example of an automatically-generated index page.

Image galleries are displayed using nanogallery2. The nrstatic module searches for the presence of sub-directories named gallery*/, containing images. If a Markdown page contains the macro instruction to insert a gallery, then the images in the directory will automatically be displayed in a gallery.

There is a helper script, called mkthumbs which must first be called in order to generate the thumbnail images, but once the thumbnail images exist, everything else is fully automatic. An example gallery is shown below. Refer to the end of the index.md file to see the macro that triggered the insertion of this gallery.


  1. This is an example of a footnote. The Markdown module that I mentioned is: here