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 sim
ple plot
s
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
.
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
gen
eral plot
s 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:

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:
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:
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.
Automatic image gallery creation
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. |