Lightweight static HTML alternative to hgweb UI
hg clone https://code.matthewwild.co.uk/hgstatic
tip: 09179ec0040f (2026-09-25)
Serve a directory of Mercurial repositories with hg clone support but a
fully static web rendering.
This became necessary for me due to the volume of requests my hgweb UI was receiving from AI scrapers. The static HTML is much faster to serve, though doesn't have all the features of hgweb. Currently it generates a repo index with dates and descriptions, a rendered README for each project, and source viewer for the latest (tip) revision, with line numbering and syntax highlighting.
While hg technically supports pulling a plain static repository served over
HTTP, this is less efficient than the Mercurial wire protocol, which e.g.
allows a hg client to discover and fetch only changesets that it doesn't
already have locally.
The core "trick" used in this project is that a normal hg clone/hg pull
over HTTP always request to repo root with a ?cmd= query parameter (i.e. the
"smart" protocol). The web server can be configured to route those requests to
a minimal hgweb instance, but serves everything else as static files
regenerated by a push hook.
hgstatic.py: the static site generator. Requires only hg; uses
pygments (highlighting), markdown-it-py or markdown, and docutils
(README rendering) when installed (falling back to <pre> without them).
Settings are read from the command line or from a hgstatic.ini (see below).examples/hgweb_wsgi.py + examples/hgweb.config: wire-protocol-only
hgweb, run under gunicorn (or any WSGI server). The config path comes
from $HGWEB_CONFIG. examples/hgweb@.service is a per-domain systemd
template for it.examples/nginx.conf / examples/apache.conf: the routing: ?cmd= →
hgweb, everything else → static files.Assumes repos in /srv/hg (nested directories are fine), static output in
/var/www/hg, site at https://code.example.org.
Install the generator and its optional deps:
install -m 755 hgstatic.py /usr/local/bin/hgstatic
apt install python3-pygments python3-markdown-it python3-docutils
Initial full generation (rerun any time to rebuild everything):
hgstatic --root /srv/hg --out /var/www/hg \
--clone-base https://code.example.org --title "my repositories"
Add a hook to regenerate on push. Hooks run with the repo as cwd, so one
global snippet covers all repos, e.g. in /etc/mercurial/hgrc.d/hgstatic.rc
(or per-repo .hg/hgrc):
[hooks]
changegroup.hgstatic = hgstatic --only "$PWD"
--only regenerates just the pushed repo plus the top-level index.
Given only --only, hgstatic locates an hgstatic.ini by walking up
from the repo (also checking config/ subdirectories), and exits
quietly if there is none. The ini holds the settings from step 2:
[hgstatic]
root = /srv/hg
out = /var/www/hg
clone-base = https://code.example.org
title = my repositories
With the ini in place, step 2's full run is just
hgstatic -c /path/to/hgstatic.ini. Command-line options can be used to
override ini values if necessary.
hgweb for the wire protocol: copy examples/hgweb.config to
/etc/hgweb.config and examples/hgweb_wsgi.py to /opt/hgweb/, then
run it as the user owning the repos:
# /etc/systemd/system/hgweb.service
[Unit]
Description=hgweb (Mercurial wire protocol)
After=network.target
[Service]
User=hg
RuntimeDirectory=hgweb
ExecStart=/usr/bin/gunicorn --workers 2 \
--bind unix:/run/hgweb/hgweb.sock --chdir /opt/hgweb \
hgweb_wsgi:application
[Install]
WantedBy=multi-user.target
The app reads its config path from $HGWEB_CONFIG (default
/etc/hgweb.config). For several independent instances on one
machine, use the examples/hgweb@.service template instead.
Web server, either:
examples/nginx.conf (server name, TLS, paths).examples/apache.conf; needs
a2enmod rewrite proxy proxy_http, and gunicorn bound to
127.0.0.1:8420 instead of the unix socket.For a complete from-scratch walk-through on a Sympl host, including multiple domains on one machine, see docs/sympl.md.
Pushes happen over ssh. HTTP push is disabled in hgweb.config.
web.description
in each repo's .hg/hgrc.README/README.md/README.rst/README.txt at the repo root is
rendered on the repo page.| .hgignore | 32 B | |
| README.md | 4.7 KiB | |
| docs/sympl.md | 5.5 KiB | |
| examples/apache.conf | 797 B | |
| examples/hgweb.config | 388 B | |
| examples/hgweb@.service | 638 B | |
| examples/hgweb_wsgi.py | 636 B | |
| examples/nginx.conf | 842 B | |
| hgstatic.py | 15.0 KiB |