June 6, 2025

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, UML diagrams, and image galleries. Python expressions can be evaluated inline and variables can be defined and then used later in the document. Bash commands can also be executed to add content to the document. 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 typesetting 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 subsequent 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 }
```

Evaluation of Python expressions

Python expressions, enclosed in <[]> brackets, are executed and the value returned by the statement is inserted into the document. Putting a format specifier after the brackets in curly braces applies the Python formatting rules to the output number or string. Including variable_name := value inside of the brackets defines a variable, which can be used later in the document (inside of the evaluation brackets, of course).

For example, <[10 * 4]> becomes 40. <[cos(4.1)]>{:0.3f} becomes -0.575. The cos() works because everything from the math module is already imported. If we define a variable, angle, as in:

 <[angle := pi / 3 ]>

then we could use the variable like this <[tan(angle)]>{:0.3f}, which becomes 1.732.

The <[]> bracket evaluations are performed before any other processing, as a pre-processing step, which means that they can be used within any other environment within the document; everything can be parameterized. Using this feature inside of equations and plotting code can be very handy, particularly if the same complicated string shows up multiple times within the document; you only need to write it once, rather than copying and pasting multiple times. Note that multi-line strings can also be set to a variable; just use Python’s triple-quote syntax for multi-line strings: """ multi-line string """. In fact, this is how the source code of the plots and graphs above was displayed in this document; the variable was used once to create the plot and a second time to display the source code for the plot.

Accessing document metadata

The <[]> brackets can also be used to access the document’s metadata attributes. For example, the title of this post is Introducing nrstatic. It uses the blog.html template. It was originally posted on June 6, 2025, and the summary of the post is:

An introduction to nrstatic—the static webpage creator that I wrote to create this website. It is specifically designed with technical communication in mind.

Check out index.md to see how these were accessed.

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 Sun Jul 6 07:09:17 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. nrstatic can search for the presence of sub-directories named gallery*/, containing images. A gallery is inserted by simply including

 << mkgallery

at the beginning of the line where you want the gallery to appear. Optionally, a directory name containing the gallery images can be specified, as in

 << mkgallery gal2

In this case, the gallery directory does not need to match the pattern gallery*/. There is a helper script, called mkthumbs which must first be called in order to generate the thumbnail images in the gallery directory, but once the thumbnail images exist, everything else about gallery creation is fully automatic. An example gallery is shown below.

nrstatic sub-commands

Currently, there are several sub-commands available. Some of these were already mentioned above. All of these follow the command nrs:

Command Summary
build Build pages that were recently added or modified.
rebuild Delete the file database and rebuild all pages.
publish Publish the website to a remote server (this is configurable).
server Launch an HTTP server for local development / testing.
mksearch Create the search index (this is configurable; I use pagefind for indexing & searching.)
mkgallery Create the HTML for a gallery in the current directory.
mkthumbs Make thumbnail images for the files in the current directory.
shrink-images Resize large images in the current directory because very large files make the galleries very slow.

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