Download

repo-renderer

A static site generator for rendering git repositories

  • vibecoded
Docs 5K wordsMarkdown 4.9KText 83
Binary data 674.7 KiBPNG 674.7 KiB
NameModifiedLinesSize
cmd/ 274 lines1% of this folder's lines 8.9 KiB<1% of this folder's bytes
docs/ 0 lines0% of this folder's lines 674.7 KiB50% of this folder's bytes
examples/ 300 lines1% of this folder's lines 13 KiB<1% of this folder's bytes
hooks/ 36 lines<1% of this folder's lines 1.3 KiB<1% of this folder's bytes
internal/ 17.8K lines88% of this folder's lines: 17.8K lines of code, 6 lines of docs 577.1 KiB42% of this folder's bytes
lexers/ 435 lines2% of this folder's lines 21.2 KiB2% of this folder's bytes
preview/ 209 lines1% of this folder's lines: 198 lines of code, 11 lines of docs 7.3 KiB<1% of this folder's bytes
test/ 107 lines<1% of this folder's lines 4.2 KiB<1% of this folder's bytes
tools/ 270 lines1% of this folder's lines 12.4 KiB<1% of this folder's bytes
.gitignore 6 lines<1% of this folder's lines 86 B<1% of this folder's bytes
Makefile 89 lines<1% of this folder's lines 2.7 KiB<1% of this folder's bytes
README.md 682 lines3% of this folder's lines 33.9 KiB2% of this folder's bytes
bundle.go 12 lines<1% of this folder's lines 438 B<1% of this folder's bytes
go.mod 14 lines<1% of this folder's lines 322 B<1% of this folder's bytes
go.sum 20 lines<1% of this folder's lines 1.7 KiB<1% of this folder's bytes

README.md

Repo Renderer

Repo Renderer takes a git repo and renders it into a purely static website so you can host your code on the web without relying on someone else's git forge (GitHub, Codeberg, Bitbucket, etc.) and without having to host your own code forge (Gitea, Forgejo), which are significantly more resource-intensive than a static site. If you configure Repo Renderer to re-render your repo as a git hook, you can also host your own git backend that keeps the static web view up to date by incrementally re-rendering files in your repo only when they change.

A repository page in the dark theme: the file drawer, language stats, file listing and README

Syntax-highlighted files Search, in the browser The repository index
A Go source file with syntax highlighting Search results for "RenderRepo" The repository index, grouped into sections

Manifesto

The main reason I made this project is to allow me to keep hosting my own code on my own website in an era when GitHub is increasingly enshittified by Microsoft and badly behaved AI company web crawlers are increasingly hammering sites like mine into the ground. In my personal experience, traffic from Microsoft and Amazon has been especially badly behaved on this front.

I do not subscribe to the view that websites are only for humans or that humans should need to prove their humanity through CAPTCHAs in order to use the web. In the words of Michael Bolton:

No way! Why should I [do CAPTCHAs]? He's the one who sucks.

I stand in favor of respectful web users, whether they have Javascript disabled, whether they are using Tor, whether they are CURLing a site to view offline, or whether they are writing a properly rate-limited Python script to crawl a site. With that in mind, this project is an effort to make sure that even a minimum-spec VPS (1 vCPU / 1GB RAM) is capable of hosting all your code without forcing users to jump through hoops and without breaking a sweat as badly-behaved crawlers hammer the site. And hopefully it offers a user experience that is comparable or superior to sites like GitHub or Codeberg for the casual code viewer.

⚠️ SLOP ALERT ⚠️

This project is heavily LLM-authored. I've spent a fair amount of time and effort refining the design and looking over the code, but if LLMs are a dealbreaker for you, this project probably isn't something you'll want to use. I think it's appropriate to use an LLM to mitigate the harmful impact of LLM scrapers on my personal site and to reduce my dependency on GitHub and Microsoft's attempts to enclose the software commons. But that's just my opinion and I can respect people who don't feel the same way.

Overview

This is intended to be used for ultra-lightweight servers that may end up getting absolutely hammered by abusive web crawlers. This project is built with OpenBSD's httpd(8) in mind, which is a high-performance, lightweight, high-security static HTTP server. Each page is rendered at most once per git commit, and served statically to every visitor, with no server rendering taking place on page views.

The renderer only shows the latest commit of each repository's default branch, plus any other branches you put in your config. In order to save on resources, the renderer does not show the full git history of the repository. Users who care about the full git history can clone the repository and use git or their favorite IDE to explore the history.

Features

  • Repository index: A listing of all your repositories, grouped into categories according to your config.
  • Repository overview: the repository overview shows the file listing, README, and stats.
  • Tags: Label repositories with tags like lua or language in the config. They show on the index and on each repository's page, for information only.
  • Lines of code counts: Each repo shows a breakdown of lines of code, words of documentation, and bytes of assets.
  • Source code: each file is viewable as statically rendered, syntax-highlighted pages. No javascript is required.
  • Custom language syntax highlighting: A very wide number of languages are supported out of the box, but it's also possible to extend the renderer using syntax highlighting for custom programming language syntax.
  • Markdown rendering: Markdown files are nicely rendered to make it easy to read your docs, with a separate tab to view the syntax-highlighted Markdown source code. Fenced code blocks get full syntax highlighting in both rendered Markdown and Markdown source code. Images and links also work properly.
  • File tree: Each page shows a collapsible file drawer for quick and easy navigation of the files in the repo.
  • Source archive downloads: Download the whole repo as a .tar.gz file, one archive for each tag and one for the latest code.
  • Super fast client-side search: Seriously, you can just load the code into memory and do a string search to find stuff. It's orders of magnitude faster than you get with GitHub and you can use regexes.
  • Branch browsing. You can list extra branches to render beside the default one (see Browsing other branches).
  • A sitemap: Search engines get every page of each repository's default branch, dated by the last commit that changed it. Other branches, and repositories hidden from the index, are left out.
  • Private repositories: A repository in no section of the index is private by default. With private_htdocs set, it's rendered to a private site at /private/, behind a password; without it, it isn't rendered at all (see Private repositories). Pushes to it are still mirrored either way.
  • A 404 page: A missing address gets a page in the site's own style that links back to the index, not httpd's stock error.
  • Light and dark themes: The site follows your system setting, and a button lets you switch and remembers your choice. You can replace the built-in favicon in the config.
  • Incremental rendering: A push re-renders only the pages that changed. It re-renders everything when the templates or config change, when someone rewrites history, when the set of rendered branches changes, when the repository's description changes, or when the last render failed or was cut short. That last case sweeps away whatever pages a failed render had already written, so reverting a bad commit, or force-pushing back to before it, leaves nothing of it on the site. The file drawer and the Download menu's list are documents of their own that every page shows in a frame, so adding a file rewrites the drawer, not every page, and the list, which names the latest commit, is the only document every push rewrites. Pushing a tag rewrites only that list.
  • A small security surface: httpd serves the pages with a strict Content Security Policy (default-src 'none'), so the only scripts that run are the two the site serves itself, and no other site can show the pages in a frame (frame-ancestors 'self'). Raw files keep their real names and are served with a sandbox policy, which stops them from running as part of the site. The renderer validates every path and tag name before it writes anything, and it restricts its own filesystem access with unveil on OpenBSD.

Every page loads one small script for progressive enhancement: ui.js, which adds the theme toggle, a "Copy" button for the clone command, Escape and click-away handling for the Download menu, and fits the menu's frame to its contents. Without JavaScript, the site is plain HTML: those controls disappear and the download list scrolls in a fixed-height frame.

Code search also uses javascript, but because the site is purely static, there is no fallback for browsers that don't support javascript (no server-side search).

Building

make            # builds ./repo-render for this machine
make openbsd    # builds dist/repo-render-openbsd-amd64, the one file the server needs
make check      # runs go vet, shellcheck, the Go tests, and the node tests
make fuzz       # fuzzes the checks on pushed names and paths, 30s each (FUZZTIME)
make screenshots  # retakes the README's screenshots of the preview site

You need Go 1.26 and git. You only need node and shellcheck for make check, and node and firefox for make screenshots.

How the server is laid out

The files follow OpenBSD's hier(7). Each location is a config setting, named in parentheses below with its default path:

/etc/repo-render/                configuration, owned by root
    repo-render.yaml             the config file (see examples/repo-render.yaml)
    lexers/<name>.xml            your own syntax highlighters (lexer_dirs)
    mirror_key                   optional ssh key for pushing to mirrors (ssh_key)
/var/git/                        git's home (git_dir)
    <repo>.git                   bare repositories, owner and group git
    .ssh/authorized_keys         the keys that may push
/var/www/htdocs/<host>/          the website, inside httpd's chroot (htdocs)
    index.html                   repository index
    err/404.html                 the page for a missing address (httpd's errdocs)
    robots.txt                   keeps crawlers to the pages, off tarballs, raw files
                                 and the clone export, asks them to wait 10 seconds
                                 between fetches, and names the sitemap
    sitemap.xml                  sitemap index: one sitemap per repository on the index
    sitemap/<repo>.xml           every page of the default branch, dated by its last change
    ads.txt                      the spec's placeholder record: no one may sell ads
                                 on the site
    code/<repo>.html             a repository's root page
    code/<repo>/<path>.html      its files and directories
    code/<repo>@<branch>/…       extra branches (more below)
    raw/<repo>/<path>            raw files, byte for byte
    svg/<repo>/<path>.svg        SVGs prepared for embedding, with the theme
                                 colour applied
    snapshot/<repo>/<repo>@<commit>.tar.gz
                                 tarball of the latest commit (the Download menu's
                                 "latest"), named by the first 12 digits of its hash
    archive/<repo>/<repo>-<tag>.tar.gz    tarball of each tag
    iframes/<repo>/filetree.html the file drawer, one document framed by every page
    iframes/<repo>/downloads.html
                                 the download list: latest commit and tags, framed by
                                 the Download menu
    types/<repo>/<row>/<type>.html
                                 the files of one type, linked from the root
                                 page (row: code, docs or binary)
    search/<repo>/index.html     search page, at /search/<repo>/
    search/<repo>/corpus.json    the text the search page searches; the page adds
                                 ?v=<hash of it>, so httpd can cache it for good
    static/                      style.css, chroma.css, ui.js, search.js, favicon;
                                 pages (and style.css's icons) add ?v=<hash> to
                                 these, so an upgrade never pairs a page with a
                                 cached old copy, and httpd can cache them for good
    git/<repo>.git/              read-only clone export, served at /git/
/var/www/private/<host>/         optional private site, git:www 0750 (private_htdocs)
    private/…                    the same layout for private repositories, served
                                 at /private/ behind a password; no sitemap, robots.txt,
                                 error pages or static/ of its own
/var/www/conf/repo-render.htpasswd
                                 the private site's logins, root:www 0640 (htpasswd)
/var/spool/repo-render/queue     job queue: the hook adds to it, the renderer drains it (spool_dir)
/var/db/repo-render/             render state and lock (state_dir)
/var/log/daemon                  the renderer's log, through syslog (log)
/usr/local/share/repo-render/    the post-receive hook, bundled lexers and examples
                                 (repo-render share-files, or make install)

A directory has a page of its own, just like a file does: /code/foo/src is the file code/foo/src.html. That way, a file named index can live anywhere in a repository without conflicting with anything.

Configuration

The config file is /etc/repo-render/repo-render.yaml. To use another file, pass --config FILE or set REPO_RENDER_CONFIG. Only host is required. This example shows the settings you are most likely to change:

host: code.example.com

site:
  title: Bruce's code
  tagline: Things I made
  footer: >-                   # optional HTML at the bottom of every page
    Built with <a href="/code/repo-renderer">Repo Renderer</a>.
  clone_url: "https://code.example.com/git/{repo}.git"
  unlisted: show               # repositories in no section go in a trailing section (default: private)
  unlisted_title: Other projects

sections:
  - title: Command line programs
    repos: [bp, bb, btui]
  - title: "[Tomo](https://tomo.example.com)"   # title and description are markdown
    description: The [Tomo](https://tomo.example.com) programming language and related projects.
    repos: [tomo, tomo-json]

lexers:
  tomo:                        # uses tomo.xml from lexer_dirs, or a chroma builtin
    globs: ["*.tm"]
    aliases: [tomo, tm]        # names you can use in ```fences

repos:
  bp:
    mirrors: [git@github.com:me/bp.git, git@codeberg.org:me/bp.git]
  tomo:
    branches: [dev]            # also render the dev branch
    tags: [language, tomo]     # shown on the index and the repository's page

The config is YAML. Write prose and HTML as a folded block (>-, then indented lines) rather than in quotes; the lines are joined with spaces and nothing inside needs escaping. Quote short values that start with *, { or [, like the globs and clone_url above.

A few more settings are worth knowing about:

  • style and style_light choose the syntax highlighting colours for dark and light mode.
  • site.favicon replaces the built-in icon. Point it at an .svg, .png, or .ico file.
  • git_dir, htdocs, spool_dir, state_dir, log, ssh_key and lexer_dirs move the files listed in the layout above. A relative path in the config is relative to the config file.
  • site.unlisted decides what happens to a repository that is in no section. private, the default, keeps it off the public site: it goes on the private site, or nowhere when there is none (see Private repositories), and whatever an earlier render left on the public site is removed on its next push or render. show lists it in a trailing section titled site.unlisted_title. hide renders it but leaves it off the index, so anyone with the URL can still read it. repo-render check lists the private repositories.
  • Rendered markdown drops Pandoc title blocks (the leading % lines).
  • repo_max_bytes (default 1 GiB, 0 for no cap) caps the total size of a commit's files. A branch whose tip is over it is not rendered: the site keeps its last render and the log says why. A tag over it gets no tarball. The other size limits apply per file, so without this a commit of many files just under them could fill the disk the whole site is on. The output takes a few times the cap (raw copies, pages and a tarball), and the cap applies to each branch and each tag separately.
  • repo_max_files (default 25000, 0 for no cap) does the same for the number of files, symlinks and submodules in a commit. Each file becomes several files of output, so many tiny ones could use up the disk's inodes while staying far under repo_max_bytes.
  • archive_retention (default 1h) is how long a latest-commit tarball stays after a push replaces it. Its name never changes, so httpd can tell browsers to cache it for good. Keeping it a while means a page or menu loaded before the push still links to a file that exists. The renderer removes old ones when it next renders the repository. A tarball of a commit that a force push took off the branch goes at once. Tag tarballs follow their tags: each render remakes the tarball of a tag that has moved and removes that of a tag that is gone, even when the tag was changed in the bare repository, where no hook runs.

examples/repo-render.yaml documents every other setting, including size limits and worker count. The renderer rejects unknown keys. Run repo-render check after you edit the config.

The repository includes syntax highlighters for Tomo, BP, and Nomsu in lexers/. make install copies them to /usr/local/share/repo-render/lexers, which lexer_dirs searches by default, so you only need to add their entries under lexers: to the config. Put your own lexers in /etc/repo-render/lexers/. That directory is searched first, so a file there also overrides a bundled lexer of the same name.

Private repositories

A repository in no section is private (unless site.unlisted says otherwise). Without private_htdocs, private repositories aren't rendered at all. With it, they're rendered as a site of their own at /private/, with its own index, and httpd asks for a password before it serves anything there:

private_htdocs: /var/www/private/code.example.com
htpasswd: /var/www/conf/repo-render.htpasswd   # the default
  • private_htdocs must be inside httpd's chroot but outside htdocs, so only the password-protected locations of the generated httpd.conf ever serve it. An httpd.conf from before you set it doesn't serve it at all. The renderer writes into its private/ directory. It should be git:www, mode 0750, so no other local user can read it.

  • The logins are in the htpasswd file, which you make with htpasswd(1). httpd reads it on every request, so a new password needs no reload:

    htpasswd /var/www/conf/repo-render.htpasswd bruce
    chown root:www /var/www/conf/repo-render.htpasswd
    chmod 640 /var/www/conf/repo-render.htpasswd
    
  • After setting private_htdocs, write the httpd.conf again (repo-render httpd-conf > /etc/repo-render/httpd.conf), reload httpd, and run repo-render render --all. repo-render setup-check checks the directory and the htpasswd file, and that httpd.conf is up to date.

  • Private pages link only within /private/, carry noindex, are left out of the sitemap, and are cached by the browser alone. Clone one with the login in the URL, or let git ask for it: git clone https://bruce@code.example.com/private/git/REPO.git.

  • A repository that moves between the sites, because you added it to a section or took it out, is removed from the one it left on its next push or render.

  • Basic authentication sends the password with every request, so the site must only be reached over TLS, as relayd serves it.

Browsing other branches

By default, the site shows each repository's default branch. To show others, list them in the repository's config:

repos:
  tomo:
    branches: [dev, my-name/feature-with-kebabs]

Write exact branch names. Patterns like feature/* are not supported, so only branches you name appear on the site. Branch names can contain letters, digits, ., _, and -, with / between parts. The same goes for branch, which sets the default branch in place of the bare repository's HEAD.

The default branch keeps the same URLs it always had. Each listed branch gets its own copy of the site under <repo>@<branch>, with every / in the branch name written as ::

/code/tomo/src/main.c                            the default branch
/code/tomo@dev/src/main.c                        the dev branch
/code/tomo@my-name:feature-with-kebabs/src/main.c

Raw files, images, search, and tarballs follow the same pattern (/raw/tomo@dev/…, /search/tomo@dev, and so on). Each branch renders incrementally, like a separate repository. A branch picker next to the Download button lets visitors switch between them, and it links to the root of the other branch. Search engines are asked not to index the non-default branches. The clone export includes the listed branches too, so git clone -b dev works. Each branch's Download menu offers that branch's latest commit. Tags belong to the repository, so every branch lists the same tags.

If you list a branch that does not exist, the renderer skips it until it appears. If you remove a branch from the list, or delete it from the repository, the renderer deletes its pages on the next run.

Commands

repo-render [--config FILE] <command>
Command What it does
kick Process the job queue while holding the render lock. The post-receive hook runs this.
render [--full] [--no-mirror] REPO… Render the named repositories, and push the ones that changed to their mirrors. --full ignores saved state and re-renders everything; --no-mirror skips the mirrors.
render --all [--full] [--no-mirror] Render every repository in git_dir, and delete the output of any that is no longer there. To take a repository off the site, remove it from git_dir and run this, or make it private.
index Rebuild only the index page and the static files.
enqueue REPO REF OLD NEW Add a job to the queue. The post-receive hook runs this.
add-repo [--branch B] REPO [DESCRIPTION] Create a bare repository ready for pushes: branch and tag deletions refused, and the hook attached through core.hooksPath. Run it as git. The default branch is main. DESCRIPTION goes in the repository's description file.
check Check the config, lexers, and templates. Exits with an error if anything is wrong.
serve [--listen ADDR] Run a development server that answers as examples/httpd.conf does: the same rewrites, security headers and clone export.
setup-check [--host HOST] Check the install on OpenBSD and print the commands that fix what is missing or wrong, changing nothing. Run it as root. Works before there is a config.
httpd-conf Print the site's httpd server block, for /etc/repo-render/httpd.conf.
share-files Write the hook, lexers and examples as a tar archive, for tar -xpf - -C /usr/local/share/repo-render.

Trying it locally

The quickest way is make preview. It clones (or fetches) the repositories listed in preview/repos.txt, renders them with preview/repo-render.yaml into .preview/, and serves the site at http://127.0.0.1:8080/. Edit those two files to change what gets rendered; make preview PREVIEW_LISTEN=:9000 picks another address, and make preview-clean starts over from scratch.

To try it on a project of your own by hand, make a scratch directory with a config whose paths are relative to it, clone a project into it, render it, and start the development server:

mkdir -p /tmp/repo-render/git
cat > /tmp/repo-render/repo-render.yaml <<'EOF'
host: localhost
git_dir: git
htdocs: htdocs
spool_dir: spool
state_dir: db
log: ""
EOF
export REPO_RENDER_CONFIG=/tmp/repo-render/repo-render.yaml
git clone --bare ~/some/project /tmp/repo-render/git/project.git
./repo-render render --all
./repo-render serve        # http://127.0.0.1:8080/

To test the push path, install the hook in the scratch repository. The hook passes REPO_RENDER_CONFIG on to repo-render:

cp hooks/post-receive /tmp/repo-render/git/project.git/hooks/
REPO_RENDER="$PWD/repo-render" git push /tmp/repo-render/git/project.git

Deploying on OpenBSD

Checking the install

make openbsd builds dist/repo-render-openbsd-amd64 (OPENBSD_ARCH=arm64 for an arm64 machine). It is the only file to copy to the server: it carries the hook, lexers and examples. Then, on the server:

doas ./repo-render-openbsd-amd64 setup-check --host code.example.com

setup-check changes nothing. It checks each part of the install, in the order the steps under By hand take, and prints ✓ for each that is in place. Under each ✗ it prints the commands, to run as root, that fix it on this system: the useradd for a missing git account, the chown -R for a site directory someone else owns, the line /etc/httpd.conf lacks. A · waits on a fix above it, and a ? is something it cannot see without root. Run the commands you agree with, then check again, until everything is ✓. --host names the site for the config it tells you to write; once there is a config, it is not needed.

The last checks run httpd -n, run repo-render check as git, and ask httpd for the site's front page by its name, on the config's listen address, expecting the site's security headers. So when everything is ✓, the site is being served, and a push will render.

To upgrade, copy the new binary over and run its setup-check: it says to install it, to rewrite the hook and lexers if they changed (with repo-render share-files), and to rewrite the site's httpd configuration if the rules changed (with repo-render httpd-conf).

httpd serves the site as plain HTTP on the config's listen address (*:8080 unless you set it), and relayd terminates TLS and forwards to it, as in:

relay "https" {
        listen on $external_address port 443 tls
        protocol "https"
        forward to <httpd> port 8080 check tcp interval 30
}

relayd and certificates are yours to set up. The site's server block logs relayd's forwarded client address and still answers acme-client's challenges, for certificates fetched through it. If httpd.conf already has a server for the host, take it out: put its other names in the config's aliases.

setup-check confines itself with pledge(2) and unveil(2): it can read the filesystem, run httpd, rcctl and repo-render by absolute path, and connect to httpd, and nothing else. Those commands start unconfined, as exec(2) clears both.

By hand

The git user owns the repositories, takes the pushes over SSH, and runs the renderer: a push triggers a hook that adds a job to a queue and starts repo-render kick in the background. The steps below assume the site is code.example.com. Run them as root.

  1. Install git and create the user:

    pkg_add git
    echo /usr/local/bin/git-shell >> /etc/shells
    useradd -d /var/git -g =uid -s /usr/local/bin/git-shell git
    install -d -o git -g git -m 755 /var/git
    install -d -o git -g git -m 700 /var/git/.ssh
    

    Its home is the repositories, so a push names a repository by its name alone. Put the SSH keys of the people who may push in /var/git/.ssh/authorized_keys (owner git, mode 600). git-shell lets them push but not log in.

  2. Install the program. On a machine with Go, run make openbsd and copy dist/repo-render-openbsd-amd64 to the server. Then:

    install -o root -g bin -m 755 repo-render-openbsd-amd64 /usr/local/bin/repo-render
    install -d -m 755 /usr/local/share/repo-render
    repo-render share-files | tar -xpf - -C /usr/local/share/repo-render
    

    share-files writes the hook, lexers and examples it carries. If Go is installed on the server, make install does the same.

  3. Create the config and the directories:

    install -d /etc/repo-render
    echo 'host: code.example.com' > /etc/repo-render/repo-render.yaml
    install -d -o git -g git -m 755 /var/www/htdocs/code.example.com \
        /var/db/repo-render /var/spool/repo-render
    su -s /bin/sh git -c "repo-render check"
    

    check prints every path it uses, runs git under the same sandbox as the renderer, and reports anything missing. To use the bundled syntax highlighters for Tomo, BP and Nomsu, add their lexers: entries from examples/repo-render.yaml.

    The renderer logs through syslog, to /var/log/daemon, which newsyslog already rotates. The config belongs to root, so a person who can push cannot change it. They cannot change the renderer's state or the site either, but only because git-shell lets them run nothing but git: the renderer runs as git too.

  4. Add each repository:

    su -s /bin/sh git -c 'repo-render add-repo NAME "One-line description"'
    

    Then push to git@code.example.com:NAME.git. The site shows the main branch. Use add-repo --branch master if you push master.

    The description is the repository's description file, the one gitweb and cgit read, and the only place the site takes it from. To change it, rewrite the file and render the repository, which redoes every page, since each one carries the description:

    su -s /bin/sh git -c "echo 'New description' > /var/git/NAME.git/description && repo-render render NAME"
    

    Older versions also took a description under repos: in the config, which overrode the file. The renderer now rejects it and names the file to move it to.

    To wire up a repository you created some other way, give it the hook:

    su -s /bin/sh git -c "git -C /var/git/NAME.git config core.hooksPath /usr/local/share/repo-render/hooks"
    
  5. Configure httpd. repo-render httpd-conf prints a complete server block, headers and all, for the config's host, htdocs and listen, serving plain HTTP for relayd to forward to (examples/httpd.conf is one for code.example.com). Put it in its own file and include that:

    repo-render httpd-conf > /etc/repo-render/httpd.conf
    echo 'include "/etc/repo-render/httpd.conf"' >> /etc/httpd.conf
    httpd -n && rcctl enable httpd && rcctl start httpd
    

    After an upgrade, write it again: the rules can change. Its errdocs line makes httpd answer a missing address with the site's own 404 page. The site needs OpenBSD 8.0 or later, whose httpd can set response headers. Keep the headers: the pages carry no Content Security Policy of their own, and raw files keep their real names (.html, .svg and all), so the sandbox policy on /raw and /svg is what stops them from running as part of the site. httpd uses only the first location that matches a request, so a rule you add goes inside the existing block for its path, not in a new block after it. The rules themselves live in internal/httpd/rules.go, which the development server serves by too: each is a GlobLocation (location "…", where only * is allowed) or a PatternLocation (location match "…", a Lua pattern), written exactly as httpd.conf has it.

  6. Optional: mirror to other hosts. List git URLs under mirrors in a repository's config, and every push is pushed on to each of them, in turn: GitHub, Codeberg, sourcehut, another server of yours, anything git push can reach. One that fails is logged and retried on the next push, and does not hold up the others. The renderer uses /etc/repo-render/mirror_key as its ssh key (the ssh_key setting) and ssh's system-wide host keys, so nothing needs to go in a home directory:

    ssh-keygen -t ed25519 -N '' -f /etc/repo-render/mirror_key   # add the .pub to each host as a key with write access
    chown git /etc/repo-render/mirror_key
    ssh-keyscan github.com codeberg.org >> /etc/ssh/ssh_known_hosts   # each mirror's host
    

    Mirroring only ever sends. Your server is the repository; each mirror holds a copy, and the renderer never fetches from one, so a change made on a mirror (a merged pull request, a web edit, a compromised account) never reaches your site, and the next push overwrites the branch there. Take pull requests by fetching them yourself and pushing to your server, and turn off merging on the mirrors (or protect the branch) so no one merges there by mistake.

    Older versions mirrored to one URL, set as github, with the key at /etc/repo-render/github_key. The renderer now rejects github: write it as mirrors: [URL], and rename the key or set ssh_key to it.

The renderer only ever reads the repositories and writes to the website, /var/db/repo-render and /var/spool/repo-render. The hook is a fixed script that people who push cannot change. Before the renderer writes anything, it checks every path and tag name from the repositories.

Tests

go test ./... creates temporary bare repositories and pushes to them for real, then checks the rendered output. It covers:

  • the first render, and incremental renders for modified, added, and deleted files
  • force pushes, config changes, and tags (including tag names with slashes), and that a tag push rewrites only the download list
  • latest-commit tarballs: named by commit, linked only from the download list, and removed once replaced for longer than archive_retention, or at once when a force push drops their commit
  • recovery from a hostile commit: after a revert or a force push, nothing it put on the site remains, even when rendering it failed partway or died, and tag tarballs follow tags moved or deleted on the server
  • README selection and listing truncation
  • markdown link rewriting and HTML escaping
  • raw copies and themed SVGs
  • symlinks and submodules
  • the file drawer, and which pushes rewrite it
  • index sections and branch views
  • the clone export, which the tests clone back with git
  • the development server's rewrites and its 404 page, and that it answers every URL of a rendered site as examples/httpd.conf would, by a model of httpd serving it (first match wins, a rewrite is looked up again) that matches locations with internal/httpd, and that examples/httpd.conf is what the generator writes (go test ./internal/httpd -run TestHTTPDConfExample -update regenerates it)
  • the guarantee that a file page and a markdown fence highlight the same code identically
  • falling back to plain text for highlighters that loop, crash, or run too long

node --test test/*_test.js tests the search matching logic and the "2 days ago" wording.