Splux Linux

source-based Linux using SPS

splux-site · log · files · refs · tags · releases · people · languages · GitHub · commit

8e8042c


Ship a static git browser and drop the Forgejo backend.

Date
Commit8e8042cff8a01bfd32fbb000aba0058164eae64c
GitHubmirror

Parents

Stat

 README.md                                        |  26 +-
 forgejo/README.md                                |  38 --
 forgejo/app.ini.example                          |  80 ----
 forgejo/import-repos.sh                          |  62 ---
 forgejo/install.sh                               | 153 -------
 forgejo/templates/custom/extra_links.tmpl        |   4 -
 forgejo/templates/custom/extra_links_footer.tmpl |   3 -
 forgejo/theme-splux.css                          | 306 -------------
 site/assets/git.js                               |  99 +---
 site/assets/live.js                              |  14 +-
 site/assets/style.css                            |  85 ++--
 site/docs/first-boot/index.html                  |   1 +
 site/docs/images/index.html                      |   2 +-
 site/docs/index.html                             |  12 +-
 site/docs/sps/index.html                         |  12 +-
 site/download/index.html                         |   2 +-
 site/git/index.html                              |  21 +-
 site/index.html                                  |  20 +-
 site/install/index.html                          |  10 +-
 site/packages/index.html                         |   2 +-
 site/source/index.html                           |  18 +-
 tools/build-git.sh                               | 559 +++++++++++++++++++++++
 tools/build-site.sh                              |  65 ++-
 tools/collect-news.sh                            |  14 +-
 tools/git.awk                                    | 132 ++++++
 tools/packages.awk                               |   8 +-
 tools/subst.awk                                  |   2 +-
 27 files changed, 890 insertions(+), 860 deletions(-)

Diff

diff --git a/README.md b/README.md
index c9a0a6b..2d42303 100644
--- a/README.md
+++ b/README.md
@@ -77,18 +77,22 @@ repository there. `SITE_PREFIX` stays empty so links are rooted at `/`.
 
 ## Git
 
-`/git/` is the public forge page. Canonical clone, accounts, and pull
-requests belong on Splux Git (`https://git.splux.robertflexx.dev`).
-GitHub stays a mirror of git history. Live ISO files stay on GitHub
-Releases. The Forgejo theme and installer live in `forgejo/`.
-
-Point a DNS A or AAAA record for `git.splux` at the host that runs
-Forgejo, then put TLS in front of port 3000 (or a tunnel) and set
-`ROOT_URL` to `https://git.splux.robertflexx.dev/`. Until that name
-answers, the handbook page still lists GitHub clone URLs.
+`/git/` is a static browse of SPS, sps-core, sps-extra, and this
+repository. The Pages workflow clones those trees and writes commit,
+file, and language pages. There is no git server, no accounts, and no
+pull requests. Clone from GitHub:
 
 ```sh
-sh forgejo/install.sh
-sh forgejo/import-repos.sh
+git clone https://github.com/RobertFlexx/SPS.git
+git clone https://github.com/RobertFlexx/sps-core.git
+git clone https://github.com/RobertFlexx/sps-extra.git
+git clone https://github.com/RobertFlexx/splux-site.git
 ```
 
+News commit links point at `/git/<repo>/commit/<sha>/`. Recipe pages
+link into `/git/sps-core/` and `/git/sps-extra/`. Live ISO files stay
+on GitHub Releases.
+
+`git.splux.robertflexx.dev` is not a separate service. GitHub Pages
+allows one custom domain on this repository (`splux.robertflexx.dev`).
+The git UI is `https://splux.robertflexx.dev/git/`.
diff --git a/forgejo/README.md b/forgejo/README.md
deleted file mode 100644
index 7ad5031..0000000
--- a/forgejo/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Splux Git
-
-Forgejo themed to match the Splux handbook. The live process lives in
-`/home/robert/splux-forge`. This directory is the theme, example config,
-and installer that the static site ships so the forge and the handbook
-stay in the same style.
-
-```sh
-sh forgejo/install.sh
-```
-
-That copies the theme, writes `app.ini`, and starts port 3000. Create
-the admin account yourself (the installer does not set a password):
-
-```sh
-/home/robert/splux-forge/bin/forgejo -w /home/robert/splux-forge/data \
-  admin user create --username RobertFlexx --email ohf9ck@gmail.com \
-  --admin --password 'your-password'
-```
-
-Then import SPS, sps-core, sps-extra, and splux-site from GitHub:
-
-```sh
-sh forgejo/import-repos.sh
-```
-
-The public handbook page is `/git/`. That page is official on the
-site. Clone URLs on the internet still use GitHub until
-`git.splux.robertflexx.dev` has DNS and TLS in front of this process.
-LAN:
-
-```sh
-git clone http://10.0.0.139:3000/RobertFlexx/SPS.git
-git clone ssh://robert@10.0.0.139:2222/RobertFlexx/SPS.git
-```
-
-Push mirrors to GitHub are a per-repo setting in the forge UI. They
-need a GitHub PAT. Issues and pull requests stay on Splux Git.
diff --git a/forgejo/app.ini.example b/forgejo/app.ini.example
deleted file mode 100644
index 1b1e694..0000000
--- a/forgejo/app.ini.example
+++ /dev/null
@@ -1,80 +0,0 @@
-; Splux Git. Copy to $FORGEJO_WORK_DIR/custom/conf/app.ini
-; forgejo/install.sh writes the live file with secrets filled in.
-
-APP_NAME = Splux Git
-APP_SLOGAN = official repositories for Splux and SPS
-RUN_USER = robert
-WORK_PATH = /home/robert/splux-forge/data
-
-[server]
-APP_DATA_PATH = /home/robert/splux-forge/data
-PROTOCOL = http
-DOMAIN = 10.0.0.139
-HTTP_ADDR = 0.0.0.0
-HTTP_PORT = 3000
-; Change ROOT_URL to https://git.splux.robertflexx.dev/ after DNS and TLS exist.
-ROOT_URL = http://10.0.0.139:3000/
-DISABLE_SSH = false
-START_SSH_SERVER = true
-SSH_DOMAIN = 10.0.0.139
-SSH_PORT = 2222
-SSH_LISTEN_PORT = 2222
-LANDING_PAGE = explore
-OFFLINE_MODE = false
-
-[database]
-DB_TYPE = sqlite3
-PATH = /home/robert/splux-forge/data/forgejo.db
-
-[security]
-INSTALL_LOCK = true
-; SECRET_KEY and INTERNAL_TOKEN are generated by install.sh
-
-[oauth2]
-; JWT_SECRET is generated by install.sh
-
-[service]
-DISABLE_REGISTRATION = true
-REQUIRE_SIGNIN_VIEW = false
-ENABLE_NOTIFY_MAIL = false
-DEFAULT_KEEP_EMAIL_PRIVATE = true
-NO_REPLY_ADDRESS = noreply.splux.robertflexx.dev
-
-[ui]
-DEFAULT_THEME = splux
-THEMES = splux,forgejo-dark,forgejo-auto,forgejo-light
-SHOW_USER_EMAIL = false
-DEFAULT_SHOW_FULL_NAME = false
-
-[repository]
-DEFAULT_BRANCH = main
-DEFAULT_PRIVATE = last
-ENABLE_PUSH_CREATE_USER = false
-ENABLE_PUSH_CREATE_ORG = false
-
-[picture]
-DISABLE_GRAVATAR = true
-ENABLE_FEDERATED_AVATAR = false
-
-[cors]
-ENABLED = true
-SCHEME = https
-ALLOW_DOMAIN = splux.robertflexx.dev
-ALLOW_SUBDOMAIN = false
-METHODS = GET,HEAD,OPTIONS
-HEADERS = Content-Type,Authorization
-MAX_AGE = 10m
-ALLOW_CREDENTIALS = false
-
-[other]
-SHOW_FOOTER_VERSION = false
-SHOW_FOOTER_TEMPLATE_LOAD_TIME = false
-SHOW_FOOTER_POWERED_BY = false
-
-[session]
-PROVIDER = file
-
-[log]
-MODE = file
-LEVEL = info
-ROOT_PATH = /home/robert/splux-forge/log
diff --git a/forgejo/import-repos.sh b/forgejo/import-repos.sh
deleted file mode 100755
index 70cb588..0000000
--- a/forgejo/import-repos.sh
+++ /dev/null
@@ -1,62 +0,0 @@
-#!/bin/sh
-# Import official GitHub trees into Splux Git. No tokens.
-# Usage: sh forgejo/import-repos.sh
-set -eu
-
-FORGE=${FORGE:-/home/robert/splux-forge}
-BIN=$FORGE/bin/forgejo
-DATA=$FORGE/data
-DUMP=${DUMP:-/tmp/fj-dump}
-FJ() {
-	"$BIN" -w "$DATA" "$@"
-}
-
-already() {
-	curl -sf -o /dev/null "http://127.0.0.1:3000/api/v1/repos/RobertFlexx/$1"
-}
-
-import_one() {
-	name=$1
-	url=$2
-	if already "$name"
-	then
-		printf '%s\n' "already in forge: $name" >&2
-		return 0
-	fi
-	unitdir=$DUMP/RobertFlexx/$name
-	if [ ! -f "$unitdir/repo.yml" ]; then
-		workdir=$(mktemp -d "$DUMP/work.XXXXXX")
-		printf '%s\n' "dumping $name" >&2
-		FJ dump-repo \
-			--git_service github \
-			--clone_addr "$url" \
-			--owner_name RobertFlexx \
-			--repo_name "$name" \
-			--repo_dir "$workdir" \
-			--units labels,releases,milestones
-		mkdir -p "$DUMP/RobertFlexx"
-		if [ -d "$workdir/RobertFlexx/$name" ]; then
-			rm -rf "$unitdir"
-			mv "$workdir/RobertFlexx/$name" "$unitdir"
-		fi
-		rm -rf "$workdir"
-	fi
-	[ -f "$unitdir/repo.yml" ] || {
-		printf '%s\n' "dump missing $unitdir/repo.yml" >&2
-		return 1
-	}
-	printf '%s\n' "restoring $name" >&2
-	FJ restore-repo \
-		--owner_name RobertFlexx \
-		--repo_name "$name" \
-		--repo_dir "$unitdir" \
-		--units labels,releases,milestones
-	printf '%s\n' "imported $name" >&2
-}
-
-mkdir -p "$DUMP"
-import_one SPS https://github.com/RobertFlexx/SPS.git
-import_one sps-core https://github.com/RobertFlexx/sps-core.git
-import_one sps-extra https://github.com/RobertFlexx/sps-extra.git
-import_one splux-site https://github.com/RobertFlexx/splux-site.git
-printf '%s\n' "import done" >&2
diff --git a/forgejo/install.sh b/forgejo/install.sh
deleted file mode 100755
index 0dae969..0000000
--- a/forgejo/install.sh
+++ /dev/null
@@ -1,153 +0,0 @@
-#!/bin/sh
-# Install Splux Git (Forgejo) on this host and copy the Splux theme.
-# Does not create an admin user or access token.
-# Usage: sh forgejo/install.sh
-set -eu
-
-SITE=$(CDPATH= cd -P "$(dirname "$0")/.." && pwd) || exit 1
-FORGE=${FORGE:-/home/robert/splux-forge}
-THEME=$SITE/forgejo
-DATA=$FORGE/data
-CONF=$DATA/custom/conf/app.ini
-BIN=$FORGE/bin/forgejo
-FJ() {
-	"$BIN" -w "$DATA" -c "$CONF" "$@"
-}
-
-[ -x "$BIN" ] || {
-	printf '%s\n' "missing $BIN" >&2
-	exit 1
-}
-
-mkdir -p "$DATA/custom/conf" "$DATA/custom/public/assets/css" \
-	"$DATA/custom/public/assets/img" "$DATA/custom/templates/custom" \
-	"$FORGE/log"
-
-install_theme() {
-	cp "$THEME/theme-splux.css" "$DATA/custom/public/assets/css/theme-splux.css"
-	cp "$THEME/templates/custom/extra_links.tmpl" \
-		"$DATA/custom/templates/custom/extra_links.tmpl"
-	cp "$THEME/templates/custom/extra_links_footer.tmpl" \
-		"$DATA/custom/templates/custom/extra_links_footer.tmpl"
-	if [ -f "$SITE/site/assets/sps.png" ]; then
-		cp "$SITE/site/assets/sps.png" "$DATA/custom/public/assets/img/logo.png"
-		cp "$SITE/site/assets/sps.png" \
-			"$DATA/custom/public/assets/img/apple-touch-icon.png"
-	fi
-	if [ -f "$SITE/site/assets/favicon.png" ]; then
-		cp "$SITE/site/assets/favicon.png" "$DATA/custom/public/assets/img/favicon.png"
-	fi
-}
-
-write_app_ini() {
-	sk=$("$BIN" generate secret SECRET_KEY)
-	it=$("$BIN" generate secret INTERNAL_TOKEN)
-	jw=$("$BIN" generate secret JWT_SECRET)
-	umask 077
-	cat >"$CONF" <<EOF
-APP_NAME = Splux Git
-APP_SLOGAN = official repositories for Splux and SPS
-RUN_USER = robert
-WORK_PATH = $DATA
-
-[server]
-APP_DATA_PATH = $DATA
-PROTOCOL = http
-DOMAIN = 10.0.0.139
-HTTP_ADDR = 0.0.0.0
-HTTP_PORT = 3000
-ROOT_URL = http://10.0.0.139:3000/
-DISABLE_SSH = false
-START_SSH_SERVER = true
-SSH_DOMAIN = 10.0.0.139
-SSH_PORT = 2222
-SSH_LISTEN_PORT = 2222
-LANDING_PAGE = explore
-OFFLINE_MODE = false
-
-[database]
-DB_TYPE = sqlite3
-PATH = $DATA/forgejo.db
-
-[security]
-INSTALL_LOCK = true
-SECRET_KEY = $sk
-INTERNAL_TOKEN = $it
-
-[oauth2]
-JWT_SECRET = $jw
-
-[service]
-DISABLE_REGISTRATION = true
-REQUIRE_SIGNIN_VIEW = false
-ENABLE_NOTIFY_MAIL = false
-DEFAULT_KEEP_EMAIL_PRIVATE = true
-NO_REPLY_ADDRESS = noreply.splux.robertflexx.dev
-
-[ui]
-DEFAULT_THEME = splux
-THEMES = splux,forgejo-dark,forgejo-auto,forgejo-light
-SHOW_USER_EMAIL = false
-
-[repository]
-DEFAULT_BRANCH = main
-DEFAULT_PRIVATE = last
-ENABLE_PUSH_CREATE_USER = false
-ENABLE_PUSH_CREATE_ORG = false
-
-[picture]
-DISABLE_GRAVATAR = true
-ENABLE_FEDERATED_AVATAR = false
-
-[cors]
-ENABLED = true
-SCHEME = https
-ALLOW_DOMAIN = splux.robertflexx.dev
-METHODS = GET,HEAD,OPTIONS
-HEADERS = Content-Type,Authorization
-MAX_AGE = 10m
-ALLOW_CREDENTIALS = false
-
-[other]
-SHOW_FOOTER_VERSION = false
-SHOW_FOOTER_TEMPLATE_LOAD_TIME = false
-SHOW_FOOTER_POWERED_BY = false
-
-[session]
-PROVIDER = file
-
-[log]
-MODE = file
-LEVEL = info
-ROOT_PATH = $FORGE/log
-EOF
-}
-
-install_theme
-
-if [ ! -f "$CONF" ] || ! grep -q '^INSTALL_LOCK = true' "$CONF" 2>/dev/null
-then
-	write_app_ini
-fi
-
-FJ migrate
-sh "$FORGE/start.sh"
-
-i=0
-while [ "$i" -lt 40 ]
-do
-	if curl -fsS http://127.0.0.1:3000/api/v1/version >/dev/null 2>&1
-	then
-		break
-	fi
-	i=$((i + 1))
-	sleep 1
-done
-curl -fsS http://127.0.0.1:3000/api/v1/version >/dev/null || {
-	printf '%s\n' "forgejo did not start on :3000" >&2
-	exit 1
-}
-
-printf '%s\n' "Splux Git: http://10.0.0.139:3000/" >&2
-printf '%s\n' "Create the admin account yourself, then migrate the four GitHub trees from the web UI:" >&2
-printf '%s\n' "  $BIN -w $DATA -c $CONF admin user create --username RobertFlexx --email ohf9ck@gmail.com --admin --password 'your-password'" >&2
diff --git a/forgejo/templates/custom/extra_links.tmpl b/forgejo/templates/custom/extra_links.tmpl
deleted file mode 100644
index 37ed272..0000000
--- a/forgejo/templates/custom/extra_links.tmpl
+++ /dev/null
@@ -1,4 +0,0 @@
-<a class="item" href="https://splux.robertflexx.dev/">Handbook</a>
-<a class="item" href="https://splux.robertflexx.dev/git/">Git</a>
-<a class="item" href="https://splux.robertflexx.dev/packages/">Packages</a>
-<a class="item" href="https://splux.robertflexx.dev/download/">Download</a>
diff --git a/forgejo/templates/custom/extra_links_footer.tmpl b/forgejo/templates/custom/extra_links_footer.tmpl
deleted file mode 100644
index 004d307..0000000
--- a/forgejo/templates/custom/extra_links_footer.tmpl
+++ /dev/null
@@ -1,3 +0,0 @@
-<a href="https://splux.robertflexx.dev/">splux.robertflexx.dev</a>
-·
-<a href="https://github.com/RobertFlexx">GitHub mirror</a>
diff --git a/forgejo/theme-splux.css b/forgejo/theme-splux.css
deleted file mode 100644
index 12e6bc0..0000000
--- a/forgejo/theme-splux.css
+++ /dev/null
@@ -1,306 +0,0 @@
-/*
- * Splux Git. Same black, gray, yellow, monospace as splux.robertflexx.dev.
- * Drop at $FORGEJO_CUSTOM/public/assets/css/theme-splux.css
- */
-
-@import url("./theme-forgejo-dark.css");
-
-:root {
-	color-scheme: dark;
-
-	--fonts-proportional: ui-monospace, "SFMono-Regular", "Cascadia Mono",
-		"Liberation Mono", "DejaVu Sans Mono", "Adwaita Mono", monospace !important;
-	--fonts-monospace: ui-monospace, "SFMono-Regular", "Cascadia Mono",
-		"Liberation Mono", "DejaVu Sans Mono", "Adwaita Mono", monospace !important;
-
-	--color-primary: #ffff00 !important;
-	--color-primary-hover: #ffff66 !important;
-	--color-primary-active: #cccc00 !important;
-	--color-primary-light-1: #ffff33 !important;
-	--color-primary-light-2: #ffff66 !important;
-	--color-primary-light-3: #ffff99 !important;
-	--color-primary-dark-1: #cccc00 !important;
-	--color-primary-dark-2: #999900 !important;
-	--color-primary-alpha-10: rgba(255, 255, 0, 0.10) !important;
-	--color-primary-alpha-20: rgba(255, 255, 0, 0.20) !important;
-	--color-primary-alpha-40: rgba(255, 255, 0, 0.40) !important;
-	--color-accent: #ffff00 !important;
-	--color-secondary-button: #8a8a8a !important;
-	--color-secondary-button-hover: #d6d6d6 !important;
-
-	--color-body: #0a0a0a !important;
-	--color-box-body: #111111 !important;
-	--color-box-body-highlight: #1a1a1a !important;
-	--color-secondary: #2a2a2a !important;
-	--color-secondary-dark-1: #1a1a1a !important;
-	--color-secondary-dark-2: #111111 !important;
-	--color-secondary-dark-3: #0a0a0a !important;
-	--color-secondary-dark-4: #000000 !important;
-	--color-secondary-dark-5: #000000 !important;
-	--color-secondary-dark-6: #000000 !important;
-	--color-secondary-dark-7: #000000 !important;
-	--color-secondary-dark-8: #000000 !important;
-	--color-secondary-dark-9: #000000 !important;
-	--color-secondary-dark-10: #000000 !important;
-	--color-secondary-dark-11: #000000 !important;
-	--color-secondary-dark-12: #000000 !important;
-	--color-secondary-dark-13: #000000 !important;
-	--color-secondary-light-1: #333333 !important;
-	--color-secondary-alpha-20: rgba(255, 255, 255, 0.04) !important;
-	--color-secondary-alpha-40: rgba(255, 255, 255, 0.07) !important;
-	--color-secondary-alpha-60: rgba(255, 255, 255, 0.10) !important;
-	--color-secondary-alpha-80: rgba(255, 255, 255, 0.14) !important;
-
-	--color-border: #2a2a2a !important;
-	--color-light-border: #2a2a2a !important;
-
-	--color-text: #d6d6d6 !important;
-	--color-text-dark: #d6d6d6 !important;
-	--color-text-light: #8a8a8a !important;
-	--color-text-light-1: #8a8a8a !important;
-	--color-text-light-2: #6e6e6e !important;
-	--color-text-light-3: #555555 !important;
-	--color-text-light-4: #444444 !important;
-	--color-text-light-5: #333333 !important;
-	--color-text-light-6: #2a2a2a !important;
-
-	--color-nav-bg: #0a0a0a !important;
-	--color-nav-hover-bg: #1a1a1a !important;
-	--color-nav-text: #d6d6d6 !important;
-	--color-header-wrapper: #0a0a0a !important;
-	--color-header-bar: #0a0a0a !important;
-	--color-footer: #0a0a0a !important;
-	--color-menu: #111111 !important;
-
-	--color-markup-code-block: #111111 !important;
-	--color-code-bg: #111111 !important;
-	--color-code-sidebar-bg: #0a0a0a !important;
-	--color-input-background: #111111 !important;
-	--color-input-border: #2a2a2a !important;
-	--color-input-text: #d6d6d6 !important;
-	--color-hover: #1a1a1a !important;
-	--color-active: #1a1a1a !important;
-	--color-active-line: rgba(255, 255, 0, 0.08) !important;
-
-	--color-button: #111111 !important;
-	--color-button-hover: #1a1a1a !important;
-
-	--color-label-text: #d6d6d6 !important;
-	--color-label-hover-bg: #1a1a1a !important;
-	--color-label-active-bg: rgba(255, 255, 0, 0.12) !important;
-	--color-label-active-text: #ffff00 !important;
-
-	--color-red: #ff6666 !important;
-	--color-orange: #ffcc00 !important;
-	--color-yellow: #ffff00 !important;
-	--color-olive: #cccc00 !important;
-	--color-green: #88cc44 !important;
-	--color-teal: #88aaaa !important;
-	--color-blue: #88aacc !important;
-	--color-violet: #aaaaee !important;
-	--color-purple: #cc88cc !important;
-	--color-pink: #eeaaaa !important;
-	--color-brown: #aa8866 !important;
-	--color-grey: #8a8a8a !important;
-	--color-black: #0a0a0a !important;
-	--color-gold: #ffff00 !important;
-	--color-white: #d6d6d6 !important;
-
-	--color-diff-removed-word-bg: rgba(255, 102, 102, 0.28) !important;
-	--color-diff-added-word-bg: rgba(255, 255, 0, 0.22) !important;
-	--color-diff-removed-row-bg: rgba(255, 102, 102, 0.08) !important;
-	--color-diff-added-row-bg: rgba(255, 255, 0, 0.07) !important;
-	--color-diff-removed-row-border: rgba(255, 102, 102, 0.25) !important;
-	--color-diff-added-row-border: rgba(255, 255, 0, 0.25) !important;
-	--color-diff-inactive: #111111 !important;
-
-	--color-console-fg: #d6d6d6 !important;
-	--color-console-bg: #0a0a0a !important;
-	--color-console-border: #2a2a2a !important;
-
-	--shadow-xs: none !important;
-	--shadow-sm: none !important;
-	--shadow-md: none !important;
-	--shadow-lg: none !important;
-	--shadow-xl: none !important;
-
-	--border-radius: 0 !important;
-	--border-radius-circle: 0 !important;
-}
-
-html,
-body {
-	background: #0a0a0a !important;
-	color: #d6d6d6 !important;
-	font-family: ui-monospace, "SFMono-Regular", "Cascadia Mono",
-		"Liberation Mono", "DejaVu Sans Mono", monospace !important;
-	font-size: 15px !important;
-	line-height: 1.5 !important;
-}
-
-*,
-.ui.segment,
-.ui.card,
-.ui.menu,
-.ui.dropdown .menu,
-.ui.modal,
-.ui.popup {
-	box-shadow: none !important;
-	border-radius: 0 !important;
-}
-
-.ui.segment,
-.ui.attached.segment,
-.repository .header-wrapper,
-.page-content {
-	border-color: #2a2a2a !important;
-	background: #0a0a0a !important;
-}
-
-.ui.card,
-.ui.cards > .card {
-	border: 1px solid #2a2a2a !important;
-	background: #111111 !important;
-}
-
-.flex-item {
-	border-bottom: 1px solid #1a1a1a !important;
-}
-
-a:not(.button):not(.ui.label) {
-	color: #ffff00 !important;
-	text-decoration: none !important;
-}
-
-a:not(.button):hover {
-	text-decoration: underline !important;
-	color: #ffff66 !important;
-}
-
-.ui.button,
-.ui.basic.button {
-	border-radius: 0 !important;
-	font-family: inherit !important;
-	text-transform: none !important;
-	font-weight: 400 !important;
-}
-
-.ui.primary.button,
-a.button.primary {
-	background: #ffff00 !important;
-	color: #0a0a0a !important;
-	border: 1px solid #ffff00 !important;
-	font-weight: 700 !important;
-}
-
-.ui.primary.button:hover,
-a.button.primary:hover {
-	background: #0a0a0a !important;
-	color: #ffff00 !important;
-	border: 1px solid #ffff00 !important;
-}
-
-.ui.basic.button {
-	background: transparent !important;
-	color: #8a8a8a !important;
-	border: 1px solid #2a2a2a !important;
-}
-
-.ui.basic.button:hover {
-	color: #ffff00 !important;
-	border-color: #ffff00 !important;
-	background: transparent !important;
-}
-
-.ui.tabular.menu {
-	border-bottom: 1px solid #2a2a2a !important;
-}
-
-.ui.tabular.menu .item {
-	border-radius: 0 !important;
-	color: #8a8a8a !important;
-}
-
-.ui.tabular.menu .active.item {
-	background: #111111 !important;
-	color: #ffff00 !important;
-	border-color: #2a2a2a !important;
-	border-bottom-color: #0a0a0a !important;
-}
-
-.ui.secondary.menu .active.item {
-	color: #ffff00 !important;
-}
-
-#navbar,
-.ui.secondary.menu {
-	background: #0a0a0a !important;
-	border-bottom: 1px solid #2a2a2a !important;
-}
-
-.page-header,
-.repo-header {
-	border-bottom: 1px solid #2a2a2a !important;
-}
-
-.ui.dropdown .menu {
-	background: #111111 !important;
-	border: 1px solid #2a2a2a !important;
-}
-
-.ui.dropdown .menu > .item {
-	color: #d6d6d6 !important;
-}
-
-.ui.dropdown .menu > .item:hover {
-	background: #1a1a1a !important;
-	color: #ffff00 !important;
-}
-
-input,
-textarea,
-.ui.form input,
-.ui.input input,
-.ui.selection.dropdown {
-	background: #111111 !important;
-	color: #d6d6d6 !important;
-	border: 1px solid #2a2a2a !important;
-	border-radius: 0 !important;
-	font-family: inherit !important;
-}
-
-footer {
-	background: #0a0a0a !important;
-	border-top: 1px solid #2a2a2a !important;
-	color: #8a8a8a !important;
-}
-
-.code-view .lines-code,
-.code-view .lines-num,
-.chroma,
-pre,
-code {
-	font-family: inherit !important;
-}
-
-::-webkit-scrollbar {
-	width: 8px;
-	height: 8px;
-}
-
-::-webkit-scrollbar-track {
-	background: #0a0a0a;
-}
-
-::-webkit-scrollbar-thumb {
-	background: #2a2a2a;
-}
-
-::-webkit-scrollbar-thumb:hover {
-	background: #ffff00;
-}
-
-::selection {
-	background: rgba(255, 255, 0, 0.28);
-	color: #0a0a0a;
-}
diff --git a/site/assets/git.js b/site/assets/git.js
index bee9d2c..10b43b5 100644
--- a/site/assets/git.js
+++ b/site/assets/git.js
@@ -1,81 +1,32 @@
-// Fill /git/ with GitHub metadata and whether Splux Git answers.
+// Filter the static git index. Pages are built with the handbook;
+// GitHub is only a clone mirror, not a runtime backend.
 (function () {
-	var API = "https://api.github.com/repos/RobertFlexx/";
-	var ACCEPT = { Accept: "application/vnd.github+json" };
-	var thisScript = document.currentScript ||
-		document.querySelector("script[src*=\"git.js\"]");
-	var gitHost = (thisScript && thisScript.getAttribute("data-git")) ||
-		"https://git.splux.robertflexx.dev";
-	gitHost = String(gitHost).replace(/\/+$/, "");
-
-	function esc(s) {
-		return String(s)
-			.replace(/&/g, "&amp;")
-			.replace(/</g, "&lt;")
-			.replace(/>/g, "&gt;")
-			.replace(/"/g, "&quot;");
-	}
-
-	function json(url) {
-		return fetch(url, { headers: ACCEPT }).then(function (r) {
-			if (!r.ok)
-				throw new Error(String(r.status));
-			return r.json();
-		});
-	}
-
-	function when(iso) {
-		if (!iso)
-			return "";
-		var d = new Date(iso);
-		if (isNaN(d.getTime()))
-			return iso;
-		try {
-			return d.toLocaleString(undefined, {
-				year: "numeric",
-				month: "short",
-				day: "numeric"
-			});
-		} catch (e) {
-			return iso.slice(0, 10);
-		}
+	function ready() {
+		var input = document.getElementById("git-filter");
+		var table = document.getElementById("git-repos");
+		if (input && table)
+			bindFilter(input, table);
+		var tree = document.querySelector("table.git-tree");
+		if (input && tree)
+			bindFilter(input, tree);
 	}
 
-	var status = document.getElementById("git-status");
-	if (status) {
-		fetch(gitHost + "/api/v1/version", { mode: "cors" }).then(function (r) {
-			if (!r.ok)
-				throw new Error(String(r.status));
-			return r.json();
-		}).then(function (v) {
-			var ver = (v && (v.version || v.forgejo)) || "up";
-			status.innerHTML = "reachable (" + esc(String(ver)) + ")";
-		}).catch(function () {
-			status.textContent =
-				"not reachable from this browser yet; clone GitHub until DNS and TLS exist";
-			var rows = document.querySelectorAll("#git-repos tr[data-repo]");
-			for (var j = 0; j < rows.length; j++) {
-				var name = rows[j].getAttribute("data-repo");
-				var a = rows[j].querySelector("a.name");
-				if (a && name)
-					a.setAttribute("href",
-						"https://github.com/RobertFlexx/" + name);
+	function bindFilter(input, table) {
+		function apply() {
+			var q = (input.value || "").toLowerCase();
+			var rows = table.querySelectorAll("tbody tr");
+			for (var i = 0; i < rows.length; i++) {
+				var hay = rows[i].getAttribute("data-search") ||
+					rows[i].textContent || "";
+				rows[i].hidden = q !== "" &&
+					hay.toLowerCase().indexOf(q) === -1;
 			}
-		});
+		}
+		input.addEventListener("input", apply);
 	}
 
-	var rows = document.querySelectorAll("#git-repos tr[data-repo]");
-	for (var i = 0; i < rows.length; i++) {
-		(function (row) {
-			var name = row.getAttribute("data-repo");
-			json(API + encodeURIComponent(name)).then(function (repo) {
-				var lang = row.querySelector(".lang");
-				var pushed = row.querySelector(".pushed");
-				if (lang && repo.language)
-					lang.textContent = repo.language;
-				if (pushed && repo.pushed_at)
-					pushed.textContent = when(repo.pushed_at);
-			}).catch(function () {});
-		})(rows[i]);
-	}
+	if (document.readyState === "loading")
+		document.addEventListener("DOMContentLoaded", ready);
+	else
+		ready();
 })();
diff --git a/site/assets/live.js b/site/assets/live.js
index ce633ae..a1af22b 100644
--- a/site/assets/live.js
+++ b/site/assets/live.js
@@ -21,7 +21,7 @@
 		document.querySelector("script[src*=\"live.js\"]");
 	var dataRoot = (thisScript && thisScript.getAttribute("data-root")) || "./";
 	var gitHost = (thisScript && thisScript.getAttribute("data-git")) ||
-		"https://git.splux.robertflexx.dev";
+		"https://splux.robertflexx.dev/git";
 	gitHost = String(gitHost).replace(/\/+$/, "");
 	if (!feed && !brief && !isoEl && !tagEl)
 		return;
@@ -321,15 +321,19 @@
 
 	function commitHref(repoPath, sha, htmlUrl) {
 		if (gitHost && repoPath && sha)
-			return gitHost + "/RobertFlexx/" + repoPath + "/commit/" + sha;
+			return gitHost + "/" + repoPath + "/commit/" + sha + "/";
 		return htmlUrl || "";
 	}
 
 	function forgeCommitHref(url) {
-		var m = /^https:\/\/github\.com\/RobertFlexx\/([^/]+)\/commit\/([0-9a-fA-F]+)/.exec(String(url || ""));
+		var u = String(url || "");
+		var m = /^https:\/\/github\.com\/RobertFlexx\/([^/]+)\/commit\/([0-9a-fA-F]+)/.exec(u);
 		if (m && gitHost)
-			return gitHost + "/RobertFlexx/" + m[1] + "/commit/" + m[2];
-		return url;
+			return gitHost + "/" + m[1] + "/commit/" + m[2] + "/";
+		m = /^https:\/\/git\.splux\.robertflexx\.dev\/RobertFlexx\/([^/]+)\/commit\/([0-9a-fA-F]+)/.exec(u);
+		if (m && gitHost)
+			return gitHost + "/" + m[1] + "/commit/" + m[2] + "/";
+		return u;
 	}
 
 	function setSha(id, repoPath, sha) {
diff --git a/site/assets/style.css b/site/assets/style.css
index 6a68315..228449f 100644
--- a/site/assets/style.css
+++ b/site/assets/style.css
@@ -627,65 +627,56 @@ table.docs th {
 	}
 }
 
-.wip {
-	margin: 1.25rem 0 2rem;
-	border: 1px solid var(--rule);
-	background: #111;
-}
-
-.wip-tape {
-	height: 12px;
-	background: repeating-linear-gradient(
-		-45deg,
-		var(--accent),
-		var(--accent) 10px,
-		#111 10px,
-		#111 20px
-	);
+pre.block.git-blob {
+	max-height: 70vh;
+	font-size: 0.82rem;
+	line-height: 1.4;
+	white-space: pre;
 }
 
-.wip-board {
-	display: flex;
-	flex-direction: column;
-	align-items: center;
-	justify-content: center;
-	gap: 1.25rem;
-	padding: 3rem 1.5rem 2.5rem;
-	text-align: center;
+.git-nav {
 	color: var(--muted);
 }
 
-.wip-board p {
-	margin: 0;
-	max-width: none;
+.lang-bar {
+	display: flex;
+	height: 10px;
+	border: 1px solid var(--rule);
+	margin: 0.5rem 0 0.75rem;
+	overflow: hidden;
+	background: #111;
 }
 
-.wip-mark,
-.wip-prompt {
-	color: var(--fg);
+.lang-seg {
+	display: block;
+	height: 100%;
+	background: var(--accent);
+	opacity: 0.95;
+	min-width: 1px;
 }
 
-.wip-copy {
-	white-space: pre-line;
-	line-height: 1.65;
-}
+.lang-seg:nth-child(2) { opacity: 0.7; }
+.lang-seg:nth-child(3) { opacity: 0.5; }
+.lang-seg:nth-child(n+4) { opacity: 0.35; background: #888; }
 
-.wip-cursor {
-	display: inline-block;
-	width: 0.7em;
-	height: 1.05em;
-	margin-left: 0.2em;
-	vertical-align: -0.12em;
-	background: var(--accent);
-	animation: wip-blink 1.05s steps(1, end) infinite;
+.lang-list {
+	list-style: none;
+	padding: 0;
+	margin: 0 0 1.25rem;
+	color: var(--muted);
+	font-size: 0.92rem;
 }
 
-@keyframes wip-blink {
-	50% { opacity: 0; }
+.lang-list li {
+	display: inline;
+	margin-right: 1rem;
 }
 
-@media (prefers-reduced-motion: reduce) {
-	.wip-cursor {
-		animation: none;
-	}
+#git-filter {
+	background: #111;
+	color: var(--fg);
+	border: 1px solid var(--rule);
+	font: inherit;
+	padding: 0.25rem 0.5rem;
+	min-width: 12rem;
 }
diff --git a/site/docs/first-boot/index.html b/site/docs/first-boot/index.html
index 0ba11a0..1662947 100644
--- a/site/docs/first-boot/index.html
+++ b/site/docs/first-boot/index.html
@@ -56,6 +56,7 @@ dhcpcd eth0</pre>
 <tr><td>runit</td><td>symlink under <code>/etc/runit/runsvdir/default</code></td></tr>
 <tr><td>dinit</td><td>symlink under <code>/etc/dinit.d/boot.d</code></td></tr>
 <tr><td>Shepherd</td><td>Scheme files under <code>/etc/shepherd.d</code></td></tr>
+<tr><td>SysV</td><td>scripts in <code>/etc/rc.d/init.d</code>; enable with <code>S*</code> links in <code>rc2.d</code> through <code>rc5.d</code></td></tr>
 </tbody>
 </table>
 
diff --git a/site/docs/images/index.html b/site/docs/images/index.html
index 2048e52..24e98a4 100644
--- a/site/docs/images/index.html
+++ b/site/docs/images/index.html
@@ -17,7 +17,7 @@
 <main id="main">
 <h2>Live images</h2>
 <hr class="rule">
-<p>Images are hybrid ISOs from <a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS">RobertFlexx/SPS</a>. Filenames stay the same; the current tag is <a href="https://github.com/RobertFlexx/SPS/releases/latest">live-2026-09-06-1</a> (2026-09-06). Verify against <a href="https://github.com/RobertFlexx/SPS/releases/latest/download/SHA256SUMS">SHA256SUMS</a>.</p>
+<p>Images are hybrid ISOs from <a href="../../../../git/SPS/">SPS</a>. Filenames stay the same; the current tag is <a href="https://github.com/RobertFlexx/SPS/releases/latest">live-2026-09-06-1</a> (2026-09-06). Verify against <a href="https://github.com/RobertFlexx/SPS/releases/latest/download/SHA256SUMS">SHA256SUMS</a>.</p>
 <p>Every image includes SPS (<code>src</code>, <code>sget</code>, <code>mkpkg</code>, <code>pkin</code>, <code>setup</code>, <code>mkiso</code>, and the rest), <code>dialog</code>, disk tools, <code>dhcpcd</code>, <code>grub-install</code>, and <code>grub-mkconfig</code>. Live PID 1 is busybox init on published snapshots. <code>/bin/sh</code> is busybox. Login is <code>root</code> with an empty password.</p>
 
 <table class="docs">
diff --git a/site/docs/index.html b/site/docs/index.html
index d37ac0e..5d0da81 100644
--- a/site/docs/index.html
+++ b/site/docs/index.html
@@ -55,12 +55,12 @@
 <h3>Reference trees</h3>
 <ul class="plain">
 <li><a href="../../../../packages/">Package browser</a> (1474 packages)</li>
-<li><a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS/src/branch/main/README.md">SPS README</a></li>
-<li><a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS/src/branch/main/DESIGN.md">SPS 1.0 formats and behavior</a></li>
-<li><a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS/src/branch/main/man/setup.1">setup(1)</a></li>
-<li><a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS/src/branch/main/PACKAGE-MAINTAINER.md">Package maintainer guide</a></li>
-<li><a href="https://splux.robertflexx.dev/git/RobertFlexx/sps-core/src/branch/main/README.md">core collection</a></li>
-<li><a href="https://splux.robertflexx.dev/git/RobertFlexx/sps-extra/src/branch/main/README.md">extra collection</a></li>
+<li><a href="../../../../git/SPS/blob/README.md/">SPS README</a></li>
+<li><a href="../../../../git/SPS/blob/DESIGN.md/">SPS 1.0 formats and behavior</a></li>
+<li><a href="../../../../git/SPS/blob/man/setup.1/">setup(1)</a></li>
+<li><a href="../../../../git/SPS/blob/PACKAGE-MAINTAINER.md/">Package maintainer guide</a></li>
+<li><a href="../../../../git/sps-core/">core collection</a></li>
+<li><a href="../../../../git/sps-extra/">extra collection</a></li>
 <li><a href="../../../../git/">Splux Git</a></li>
 <li><a href="../../../../source/">Source repositories</a></li>
 </ul>
diff --git a/site/docs/sps/index.html b/site/docs/sps/index.html
index 674527a..3e2eea2 100644
--- a/site/docs/sps/index.html
+++ b/site/docs/sps/index.html
@@ -56,13 +56,10 @@ unset SPS_DB SPS_CACHE SPS_BUILD SPS_CONFIG SPS_REPOS_CONFIG SPS_REPO_ROOT</pre>
 <p>Leaving those unset is required so they resolve under the target, not under the live root.</p>
 
 <h3>Repositories</h3>
-<p><code>/etc/sps/repos.conf</code> lists trees. Higher priority wins. Splux Git is official. Until that name answers, use the GitHub mirrors, not both at once.</p>
+<p><code>/etc/sps/repos.conf</code> lists trees. Higher priority wins. Clone from GitHub. The handbook git pages are a static browse of the same trees, not a clone URL.</p>
 <pre class="block">dir core /usr/src/sps/core 100
 dir extra /usr/src/sps/extra 80
 
-git core https://splux.robertflexx.dev/git/RobertFlexx/sps-core.git 100
-git extra https://splux.robertflexx.dev/git/RobertFlexx/sps-extra.git 80
-
 git core https://github.com/RobertFlexx/sps-core.git 100
 git extra https://github.com/RobertFlexx/sps-extra.git 80</pre>
 <p>On tty and Plasma images the live <code>repos.conf</code> uses <code>dir</code> entries pointing at the bundled trees. Slim images have no recipes, so they use the git lines. <code>src update</code> for a git repo needs <code>git</code>, which slim does not ship.</p>
@@ -86,6 +83,7 @@ sget install curl</pre>
 <tr><td><code>runit</code></td><td><code>init-runit</code></td><td>runit, eudev, elogind</td></tr>
 <tr><td><code>dinit</code></td><td><code>init-dinit</code></td><td>dinit, eudev, elogind</td></tr>
 <tr><td><code>shepherd</code></td><td><code>init-shepherd</code></td><td>GNU Shepherd, eudev, elogind, Guile</td></tr>
+<tr><td><code>sysvinit</code></td><td><code>init-sysvinit</code></td><td>SysV init, eudev, elogind</td></tr>
 <tr><td><code>none</code></td><td>(no init package)</td><td>the installed tree has no PID 1 from SPS</td></tr>
 </tbody>
 </table>
@@ -96,9 +94,9 @@ sget install curl</pre>
 
 <h3>Further reading</h3>
 <ul class="plain">
-<li><a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS/src/branch/main/README.md">SPS README</a></li>
-<li><a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS/src/branch/main/DESIGN.md">DESIGN.md</a></li>
-<li><a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS/src/branch/main/PACKAGE-MAINTAINER.md">Recipe format</a></li>
+<li><a href="../../../../git/SPS/blob/README.md/">SPS README</a></li>
+<li><a href="../../../../git/SPS/blob/DESIGN.md/">DESIGN.md</a></li>
+<li><a href="../../../../git/SPS/blob/PACKAGE-MAINTAINER.md/">Recipe format</a></li>
 <li><a href="../../../../packages/">Package browser</a></li>
 </ul>
 </main>
diff --git a/site/download/index.html b/site/download/index.html
index 94ae8c2..e4fbeeb 100644
--- a/site/download/index.html
+++ b/site/download/index.html
@@ -15,7 +15,7 @@
 <main id="main">
 <h2>Download</h2>
 <hr class="rule">
-<p>Images are hybrid ISOs from <a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS">RobertFlexx/SPS</a>. The filenames below are stable. This page names the current <code>live-*</code> ISO tag and links to that GitHub release.</p>
+<p>Images are hybrid ISOs from <a href="../../../../git/SPS/">SPS</a>. The filenames below are stable. This page names the current <code>live-*</code> ISO tag and links to that GitHub release.</p>
 <div class="dl-row"><span class="muted">Latest tag</span><span id="live-tag">live-2026-09-06-1</span></div>
 <div class="dl-row"><span class="muted">Released</span><span id="live-date">2026-09-06</span></div>
 <div class="dl-row"><span class="muted">Architecture</span><span>x86_64</span></div>
diff --git a/site/git/index.html b/site/git/index.html
index bd11ee4..6fcfa3c 100644
--- a/site/git/index.html
+++ b/site/git/index.html
@@ -3,8 +3,8 @@
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Git (WIP) - Splux Linux</title>
-<meta name="description" content="Splux Git. Under construction.">
+<title>Git - Splux Linux</title>
+<meta name="description" content="Static browse of the official Splux git trees.">
 <link rel="icon" href="../../../../assets/favicon.png" type="image/png">
 <link rel="stylesheet" href="../../../../assets/style.css">
 </head>
@@ -13,22 +13,9 @@
 <div class="wrap">
 @@HEADER@@
 <main id="main">
-<h2>Git <span class="num">(WIP)</span></h2>
+<h2>Git</h2>
 <hr class="rule">
-<div class="wip" role="status">
-<div class="wip-tape" aria-hidden="true"></div>
-<div class="wip-board">
-<p class="wip-mark">[ under construction ]</p>
-<p class="wip-copy">the floor is open
-the roof is not
-
-no trees
-no clones
-no pull requests</p>
-<p class="wip-prompt">$ <span class="wip-cursor"></span></p>
-</div>
-<div class="wip-tape" aria-hidden="true"></div>
-</div>
+<p>This page is generated at site build from the official clones. If you are reading it in the source tree, run <code>tools/build-site.sh</code>.</p>
 </main>
 @@FOOTER@@
 </div>
diff --git a/site/index.html b/site/index.html
index 10fd030..f2af9ae 100644
--- a/site/index.html
+++ b/site/index.html
@@ -69,9 +69,9 @@ pkin: installed curl 8.21.0-2</pre>
 
 <h2 id="repos"><span class="num">[004]</span> Repositories</h2>
 <hr class="rule">
-<p><a href="https://splux.robertflexx.dev/git/RobertFlexx/sps-core">sps-core</a><br>
+<p><a href="../../../../git/sps-core/">sps-core</a><br>
 Base system, toolchain, libraries, boot, filesystems, and networking. 119 packages. GitHub mirror: <a href="https://github.com/RobertFlexx/sps-core">RobertFlexx/sps-core</a>.</p>
-<p><a href="https://splux.robertflexx.dev/git/RobertFlexx/sps-extra">sps-extra</a><br>
+<p><a href="../../../../git/sps-extra/">sps-extra</a><br>
 Desktop software, graphics, multimedia, development tools, and other packages. 1355 packages. GitHub mirror: <a href="https://github.com/RobertFlexx/sps-extra">RobertFlexx/sps-extra</a>.</p>
 <p>SPS also supports administrator-maintained local repositories in addition to the official trees. Browse recipes from <a href="../../../../git/">Splux Git</a>.</p>
 
@@ -80,24 +80,24 @@ Desktop software, graphics, multimedia, development tools, and other packages. @
 <p><a href="../../../../docs/">Handbook</a><br>
 <a href="../../../../install/">Install</a> (setup, manual tty, slim)<br>
 <a href="../../../../docs/images/">Live images</a><br>
-<a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS/src/branch/main/README.md">SPS README</a><br>
-<a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS/src/branch/main/DESIGN.md">SPS 1.0 formats</a></p>
+<a href="../../../../git/SPS/blob/README.md/">SPS README</a><br>
+<a href="../../../../git/SPS/blob/DESIGN.md/">SPS 1.0 formats</a></p>
 
 <h2 id="source"><span class="num">[006]</span> Source</h2>
 <hr class="rule">
 <p>
 <a href="../../../../git/">Splux Git</a>
-canonical forge<br>
-<a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS">SPS</a>
+static browse of the official trees<br>
+<a href="../../../../git/SPS/">SPS</a>
 the package system<br>
-<a href="https://splux.robertflexx.dev/git/RobertFlexx/sps-core">sps-core</a>
+<a href="../../../../git/sps-core/">sps-core</a>
 official base recipes<br>
-<a href="https://splux.robertflexx.dev/git/RobertFlexx/sps-extra">sps-extra</a>
+<a href="../../../../git/sps-extra/">sps-extra</a>
 official extra recipes<br>
-<a href="https://splux.robertflexx.dev/git/RobertFlexx/splux-site">splux-site</a>
+<a href="../../../../git/splux-site/">splux-site</a>
 this website
 </p>
-<p class="note">GitHub holds a read-only mirror of the same history. Live ISO files still come from GitHub Releases.</p>
+<p class="note">GitHub holds the clone URLs and live ISO files. This site rebuilds HTML from those trees; it does not run a git server.</p>
 
 <h2 id="status"><span class="num">[007]</span> Project status</h2>
 <hr class="rule">
diff --git a/site/install/index.html b/site/install/index.html
index 300abba..0b060a9 100644
--- a/site/install/index.html
+++ b/site/install/index.html
@@ -54,7 +54,7 @@
 
 <h4>4. Run setup</h4>
 <pre class="block">setup</pre>
-<p>Profiles: <code>minimal</code>, <code>server</code>, <code>plasma-desktop</code>, <code>plasma-full</code>. Init: systemd, OpenRC, s6, runit, dinit, or Shepherd. Those packages conflict. Guided partitioning waits until the last confirmation. Kernel <code>none</code> is the interactive default for a console profile; a disk that must boot itself needs <code>linux-desktop</code> or <code>linux</code>.</p>
+<p>Profiles: <code>minimal</code>, <code>server</code>, <code>plasma-desktop</code>, <code>plasma-full</code>. Init: systemd, OpenRC, s6, runit, dinit, Shepherd, or SysV. Those packages conflict. Guided partitioning waits until the last confirmation. Kernel <code>none</code> is the interactive default for a console profile; a disk that must boot itself needs <code>linux-desktop</code> or <code>linux</code>.</p>
 <p>Non-interactive examples:</p>
 <pre class="block">setup --plan --profile plasma-desktop --target /mnt --user alex
 
@@ -106,7 +106,7 @@ setup --non-interactive \
 
 <h4>What setup does</h4>
 <p>It writes hostname, timezone, keymap, locale, and <code>/etc/sps/init</code>, copies bundled recipe trees when they exist, writes <code>sps.conf</code>, runs <code>src update</code>, then <code>sget install</code>. Users land after packages so <code>filesystem</code>'s passwd file is already there. If you passed <code>--install-bootloader</code>, it runs live <code>grub-install</code> (EFI removable <code>EFI/BOOT/BOOTX64.EFI</code>, then NVRAM via <code>efibootmgr</code> when that binary exists; BIOS uses <code>i386-pc</code>). It does not run <code>grub-mkconfig</code>. See <code>/root/sps-bootloader.txt</code> on the target.</p>
-<p>A Plasma install compiles Qt and KDE and takes a long time. See <a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS/src/branch/main/man/setup.1">setup(1)</a> for every flag.</p>
+<p>A Plasma install compiles Qt and KDE and takes a long time. See <a href="../../../../git/SPS/blob/man/setup.1/">setup(1)</a> for every flag.</p>
 </section>
 
 <section class="tabpanel" role="tabpanel" id="panel-manual" aria-labelledby="tab-manual">
@@ -222,6 +222,7 @@ unset SPS_DB SPS_CACHE SPS_BUILD SPS_CONFIG SPS_REPOS_CONFIG SPS_REPO_ROOT</pre>
 <button type="button" role="tab" id="tab-runit" aria-controls="panel-init-runit" aria-selected="false" tabindex="-1" data-hash="runit">runit</button>
 <button type="button" role="tab" id="tab-dinit" aria-controls="panel-init-dinit" aria-selected="false" tabindex="-1" data-hash="dinit">dinit</button>
 <button type="button" role="tab" id="tab-shepherd" aria-controls="panel-init-shepherd" aria-selected="false" tabindex="-1" data-hash="shepherd">Shepherd</button>
+<button type="button" role="tab" id="tab-sysvinit" aria-controls="panel-init-sysvinit" aria-selected="false" tabindex="-1" data-hash="sysvinit">SysV</button>
 </div>
 <section class="tabpanel" role="tabpanel" id="panel-init-systemd" aria-labelledby="tab-systemd">
 <pre class="block">printf '%s\n' systemd &gt; /mnt/etc/sps/init
@@ -253,6 +254,11 @@ unset SPS_DB SPS_CACHE SPS_BUILD SPS_CONFIG SPS_REPOS_CONFIG SPS_REPO_ROOT</pre>
 # later: sget install init-shepherd</pre>
 <p>GNU Shepherd plus eudev, elogind, and Guile. Services are Scheme files under <code>/etc/shepherd.d</code>.</p>
 </section>
+<section class="tabpanel" role="tabpanel" id="panel-init-sysvinit" aria-labelledby="tab-sysvinit">
+<pre class="block">printf '%s\n' sysvinit &gt; /mnt/etc/sps/init
+# later: sget install init-sysvinit</pre>
+<p>SysV init plus eudev and elogind. Service scripts land in <code>/etc/rc.d/init.d</code>. Enable with <code>S*</code> links in <code>rc2.d</code> through <code>rc5.d</code>.</p>
+</section>
 </div>
 </li>
 <li>
diff --git a/site/packages/index.html b/site/packages/index.html
index 20f7989..e48f0cf 100644
--- a/site/packages/index.html
+++ b/site/packages/index.html
@@ -15,7 +15,7 @@
 <main id="main">
 <h2>Packages</h2>
 <hr class="rule">
-<p>Generated from the official <a href="https://splux.robertflexx.dev/git/RobertFlexx/sps-core">sps-core</a> and <a href="https://splux.robertflexx.dev/git/RobertFlexx/sps-extra">sps-extra</a> recipe trees. 1474 packages (119 core, 1355 extra). Updated 2026-10-19 13:01 UTC. Browse them from <a href="../../../../git/">Splux Git</a>.</p>
+<p>Generated from the official <a href="../../../../git/sps-core/">sps-core</a> and <a href="../../../../git/sps-extra/">sps-extra</a> recipe trees. 1474 packages (119 core, 1355 extra). Updated 2026-10-19 13:01 UTC. Browse them from <a href="../../../../git/">Splux Git</a>.</p>
 <form id="pkg-search" role="search">
 <label class="search" for="q">Search
 <input id="q" name="q" type="search" autocomplete="off" spellcheck="false">
diff --git a/site/source/index.html b/site/source/index.html
index 6ecffd2..553c77a 100644
--- a/site/source/index.html
+++ b/site/source/index.html
@@ -15,15 +15,15 @@
 <main id="main">
 <h2>Source</h2>
 <hr class="rule">
-<p>Canonical trees belong on <a href="../../../../git/">Splux Git</a>. GitHub is a read-only mirror of the same history. License for SPS and the package collections is ISC. See each repository.</p>
-<p><a href="https://splux.robertflexx.dev/git/RobertFlexx/SPS">SPS</a><br>
-Source Package System. POSIX shell and AWK. Installer and live ISO builder live here too. GitHub: <a href="https://github.com/RobertFlexx/SPS">RobertFlexx/SPS</a>.</p>
-<p><a href="https://splux.robertflexx.dev/git/RobertFlexx/sps-core">sps-core</a><br>
-Official base recipe collection. GitHub: <a href="https://github.com/RobertFlexx/sps-core">RobertFlexx/sps-core</a>.</p>
-<p><a href="https://splux.robertflexx.dev/git/RobertFlexx/sps-extra">sps-extra</a><br>
-Official extra recipe collection. GitHub: <a href="https://github.com/RobertFlexx/sps-extra">RobertFlexx/sps-extra</a>.</p>
-<p><a href="https://splux.robertflexx.dev/git/RobertFlexx/splux-site">splux-site</a><br>
-This website. Static HTML generated from the recipe trees. GitHub: <a href="https://github.com/RobertFlexx/splux-site">RobertFlexx/splux-site</a>.</p>
+<p>Canonical trees are browsed on <a href="../../../../git/">Splux Git</a>, a static HTML view rebuilt with this site. Clone from GitHub. License for SPS and the package collections is ISC. See each repository.</p>
+<p><a href="../../../../git/SPS/">SPS</a><br>
+Source Package System. POSIX shell and AWK. Installer and live ISO builder live here too. Clone: <a href="https://github.com/RobertFlexx/SPS.git">github.com/RobertFlexx/SPS.git</a>.</p>
+<p><a href="../../../../git/sps-core/">sps-core</a><br>
+Official base recipe collection. Clone: <a href="https://github.com/RobertFlexx/sps-core.git">github.com/RobertFlexx/sps-core.git</a>.</p>
+<p><a href="../../../../git/sps-extra/">sps-extra</a><br>
+Official extra recipe collection. Clone: <a href="https://github.com/RobertFlexx/sps-extra.git">github.com/RobertFlexx/sps-extra.git</a>.</p>
+<p><a href="../../../../git/splux-site/">splux-site</a><br>
+This website. Static HTML generated from the recipe trees. Clone: <a href="https://github.com/RobertFlexx/splux-site.git">github.com/RobertFlexx/splux-site.git</a>.</p>
 </main>
 @@FOOTER@@
 </div>
diff --git a/tools/build-git.sh b/tools/build-git.sh
new file mode 100755
index 0000000..cd6a9d8
--- /dev/null
+++ b/tools/build-git.sh
@@ -0,0 +1,559 @@
+#!/bin/sh
+# Build a static git browser under $OUT/git from local clones.
+# No forge and no git-http-backend. Clone URLs are GitHub.
+# Env: OUT CORE EXTRA SPS SITE GIT_HOST SITE_URL
+
+set -eu
+
+cd "$(CDPATH= cd -P "$(dirname "$0")/.." && pwd)" || exit 1
+
+OUT=${OUT:-_site}
+CORE=${CORE:-vendor/core}
+EXTRA=${EXTRA:-vendor/extra}
+SPS=${SPS:-vendor/sps}
+SITE=${SITE:-.}
+SITE_URL=${SITE_URL:-https://splux.robertflexx.dev}
+GIT_HOST=${GIT_HOST:-$SITE_URL/git}
+GIT_HOST=${GIT_HOST%/}
+AWK=tools/git.awk
+MAXBLOB=262144
+
+esc() {
+	awk -f "$AWK" -v mode=one -v text="$1"
+}
+
+is_git() {
+	git -C "$1" rev-parse --is-inside-work-tree >/dev/null 2>&1
+}
+
+lang_of() {
+	path=$1
+	base=${path##*/}
+	case $path in
+		lib/setup/sv/*)
+			printf '%s\n' Shell
+			return
+			;;
+	esac
+	case $base in
+		recipe) printf '%s\n' Recipe; return ;;
+		run) printf '%s\n' Shell; return ;;
+		Makefile|makefile|GNUmakefile) printf '%s\n' Make; return ;;
+		LICENSE|COPYING|COPYRIGHT) printf '%s\n' Text; return ;;
+		.gitignore|.gitattributes|.gitmodules) printf '%s\n' Git; return ;;
+		CNAME) printf '%s\n' Text; return ;;
+	esac
+	case $path in
+		*.sh) printf '%s\n' Shell ;;
+		*.awk) printf '%s\n' AWK ;;
+		*.c|*.h|*.in) printf '%s\n' C ;;
+		*.md) printf '%s\n' Markdown ;;
+		*.html) printf '%s\n' HTML ;;
+		*.css) printf '%s\n' CSS ;;
+		*.js) printf '%s\n' JavaScript ;;
+		*.json) printf '%s\n' JSON ;;
+		*.yml|*.yaml) printf '%s\n' YAML ;;
+		*.py) printf '%s\n' Python ;;
+		*.scm) printf '%s\n' Scheme ;;
+		*.service) printf '%s\n' systemd ;;
+		*.1|*.5|*.8) printf '%s\n' Manual ;;
+		*.svg) printf '%s\n' SVG ;;
+		*.xml) printf '%s\n' XML ;;
+		*.txt|*.conf|*.ini|*.cfg) printf '%s\n' Text ;;
+		*.png|*.jpg|*.jpeg|*.gif|*.webp|*.ico|*.woff|*.woff2|*.ttf)
+			printf '%s\n' Binary ;;
+		*.gz|*.xz|*.zst|*.bz2|*.zip|*.tar)
+			printf '%s\n' Binary ;;
+		*) printf '%s\n' Other ;;
+	esac
+}
+
+is_binary_name() {
+	case $1 in
+		*.png|*.jpg|*.jpeg|*.gif|*.webp|*.ico|*.woff|*.woff2|*.ttf| \
+		*.gz|*.xz|*.zst|*.bz2|*.zip|*.tar|*.bin)
+			return 0 ;;
+	esac
+	return 1
+}
+
+page_begin() {
+	_dest=$1
+	_title=$2
+	_desc=$3
+	mkdir -p "$(dirname "$_dest")"
+	{
+		printf '%s\n' '<!DOCTYPE html>'
+		printf '%s\n' '<html lang="en">'
+		printf '%s\n' '<head>'
+		printf '%s\n' '<meta charset="utf-8">'
+		printf '%s\n' '<meta name="viewport" content="width=device-width, initial-scale=1">'
+		printf '<title>%s</title>\n' "$(esc "$_title")"
+		printf '<meta name="description" content="%s">\n' "$(esc "$_desc")"
+		printf '%s\n' '<link rel="icon" href="../../../../assets/favicon.png" type="image/png">'
+		printf '%s\n' '<link rel="stylesheet" href="../../../../assets/style.css">'
+		printf '%s\n' '</head>'
+		printf '%s\n' '<body>'
+		printf '%s\n' '<a class="skip" href="#main">Skip to content</a>'
+		printf '%s\n' '<div class="wrap">'
+		printf '%s\n' '@@HEADER@@'
+		printf '%s\n' '<main id="main">'
+	} >"$_dest"
+}
+
+page_end() {
+	_dest=$1
+	{
+		printf '%s\n' '</main>'
+		printf '%s\n' '@@FOOTER@@'
+		printf '%s\n' '</div>'
+		if [ "${2-}" = "gitjs" ]; then
+			printf '%s\n' '<script src="../../../../assets/git.js?v=4c4f12c-20261019130111"></script>'
+		fi
+		printf '%s\n' '</body></html>'
+	} >>"$_dest"
+}
+
+write_redirect() {
+	dest=$1
+	target=$2
+	mkdir -p "$(dirname "$dest")"
+	{
+		printf '%s\n' '<!DOCTYPE html>'
+		printf '%s\n' '<html lang="en"><head>'
+		printf '%s\n' '<meta charset="utf-8">'
+		printf '<meta http-equiv="refresh" content="0;url=%s">\n' "$(esc "$target")"
+		printf '<link rel="canonical" href="%s">\n' "$(esc "$target")"
+		printf '<title>Redirect</title></head><body>\n'
+		printf '<p><a href="%s">Continue</a></p>\n' "$(esc "$target")"
+		printf '%s\n' '</body></html>'
+	} >"$dest"
+}
+
+github_of() {
+	case $1 in
+		SPS) printf '%s\n' https://github.com/RobertFlexx/SPS ;;
+		sps-core) printf '%s\n' https://github.com/RobertFlexx/sps-core ;;
+		sps-extra) printf '%s\n' https://github.com/RobertFlexx/sps-extra ;;
+		splux-site) printf '%s\n' https://github.com/RobertFlexx/splux-site ;;
+		*) printf '%s\n' "https://github.com/RobertFlexx/$1" ;;
+	esac
+}
+
+desc_of() {
+	case $1 in
+		SPS) printf '%s\n' 'Source Package System. Installer and live ISO builder.' ;;
+		sps-core) printf '%s\n' 'Official base recipe collection.' ;;
+		sps-extra) printf '%s\n' 'Official extra recipe collection.' ;;
+		splux-site) printf '%s\n' 'This website. Static HTML from the recipe trees.' ;;
+		*) printf '%s\n' "$1" ;;
+	esac
+}
+
+mkdir -p "$OUT/git" "$OUT/data"
+
+index_rows=$OUT/data/git-index.tsv
+: >"$index_rows"
+
+build_repo() {
+	name=$1
+	dir=$2
+	is_git "$dir" || return 0
+
+	gh=$(github_of "$name")
+	repo_desc=$(desc_of "$name")
+	head=$(git -C "$dir" rev-parse HEAD)
+	short=$(git -C "$dir" rev-parse --short HEAD)
+	branch=$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || printf '%s\n' HEAD)
+	lastiso=$(git -C "$dir" log -1 --format='%cI')
+	ncommits=$(git -C "$dir" rev-list --count HEAD)
+	nfiles=$(git -C "$dir" ls-tree -r --name-only HEAD | wc -l)
+	nfiles=$(printf '%s' "$nfiles" | tr -d ' ')
+
+	base=$OUT/git/$name
+	mkdir -p "$base/log" "$base/refs" "$base/tree" "$base/commit" "$base/blob"
+
+	# Language counts by blob size at HEAD.
+	langtmp=$base/.langs.tsv
+	: >"$langtmp"
+	git -C "$dir" ls-tree -r -l HEAD | while IFS= read -r line
+	do
+		# 100644 blob HASH SIZE PATH
+		set -- $line
+		mode=$1
+		kind=$2
+		obj=$3
+		sz=$4
+		shift 4
+		path=$*
+		[ "$kind" = blob ] || continue
+		[ -n "$path" ] || continue
+		lang=$(lang_of "$path")
+		printf '%s\t%s\n' "$lang" "$sz"
+	done | awk -F '\t' '
+		{ b[$1] += $2 + 0 }
+		END {
+			for (k in b)
+				print k "\t" b[k]
+		}
+	' | LC_ALL=C sort -t "$(printf '\t')" -k2,2nr >"$langtmp"
+
+	# Commits
+	clog=$base/.commits.tsv
+	git -C "$dir" log --format='%H%x09%h%x09%ct%x09%cI%x09%an%x09%s' |
+		awk -F '\t' 'BEGIN { OFS="\t" } {
+			subj=$6
+			for (i = 7; i <= NF; i++) subj = subj " " $i
+			gsub(/\t/, " ", subj)
+			print $1, $2, $3, $4, subj, $5
+		}' >"$clog"
+
+	# Repo home
+	page_begin "$base/index.html" "$name - Splux Git" "$repo_desc"
+	{
+		printf '<h2>%s</h2>\n' "$(esc "$name")"
+		printf '%s\n' '<hr class="rule">'
+		printf '<p>%s</p>\n' "$(esc "$repo_desc")"
+		printf '%s\n' '<p class="git-nav">'
+		printf '<a href="log/">log</a> · <a href="tree/">files</a> · <a href="refs/">refs</a> · '
+		printf '<a href="%s">GitHub mirror</a>\n' "$(esc "$gh")"
+		printf '%s\n' '</p>'
+		printf '%s\n' '<div class="info">'
+		printf '<div class="dl-row"><span class="muted">HEAD</span><span><a href="commit/%s/"><code>%s</code></a> (%s)</span></div>\n' \
+			"$(esc "$head")" "$(esc "$short")" "$(esc "$branch")"
+		printf '<div class="dl-row"><span class="muted">Commits</span><span>%s</span></div>\n' \
+			"$(esc "$ncommits")"
+		printf '<div class="dl-row"><span class="muted">Files</span><span>%s</span></div>\n' \
+			"$(esc "$nfiles")"
+		printf '<div class="dl-row"><span class="muted">Clone</span><span><code>git clone %s.git</code></span></div>\n' \
+			"$(esc "$gh")"
+		printf '%s\n' '</div>'
+		if [ -s "$langtmp" ]; then
+			printf '%s\n' '<h3>Languages</h3>'
+			awk -f "$AWK" -v mode=langs "$langtmp"
+		fi
+		printf '%s\n' '<h3>Recent commits</h3>'
+		awk -f "$AWK" -v mode=commits -v commitpfx="commit/" -v limit=20 "$clog"
+		printf '%s\n' '<p><a href="log/">Full log</a></p>'
+		if git -C "$dir" cat-file -e HEAD:README.md 2>/dev/null; then
+			printf '%s\n' '<h3>README.md</h3>'
+			git -C "$dir" show HEAD:README.md | awk -f "$AWK" -v mode=pre
+		elif git -C "$dir" cat-file -e HEAD:README 2>/dev/null; then
+			printf '%s\n' '<h3>README</h3>'
+			git -C "$dir" show HEAD:README | awk -f "$AWK" -v mode=pre
+		fi
+	} >>"$base/index.html"
+	page_end "$base/index.html"
+
+	# Full log
+	page_begin "$base/log/index.html" "Log - $name - Splux Git" "Commit log for $name"
+	{
+		printf '<p class="git-nav"><a href="../">%s</a> · log</p>\n' "$(esc "$name")"
+		printf '<h2>Log</h2>\n'
+		printf '%s\n' '<hr class="rule">'
+		awk -f "$AWK" -v mode=commits -v commitpfx="../commit/" "$clog"
+	} >>"$base/log/index.html"
+	page_end "$base/log/index.html"
+
+	# Refs
+	page_begin "$base/refs/index.html" "Refs - $name - Splux Git" "Branches and tags for $name"
+	{
+		printf '<p class="git-nav"><a href="../">%s</a> · refs</p>\n' "$(esc "$name")"
+		printf '<h2>Refs</h2>\n'
+		printf '%s\n' '<hr class="rule">'
+		printf '%s\n' '<h3>Branches</h3>'
+		printf '%s\n' '<ul class="plain">'
+		git -C "$dir" for-each-ref --format='%(objectname) %(refname:short)' refs/heads |
+		while IFS= read -r line
+		do
+			[ -n "$line" ] || continue
+			h=${line%% *}
+			r=${line#* }
+			printf '<li><a href="../commit/%s/">%s</a> <code>%s</code></li>\n' \
+				"$(esc "$h")" "$(esc "$r")" "$(esc "${h%"${h#???????}"}")"
+		done
+		printf '%s\n' '</ul>'
+		printf '%s\n' '<h3>Tags</h3>'
+		printf '%s\n' '<ul class="plain">'
+		ntags=0
+		git -C "$dir" for-each-ref --format='%(objectname) %(refname:short)' refs/tags |
+		while IFS= read -r line
+		do
+			[ -n "$line" ] || continue
+			h=${line%% *}
+			r=${line#* }
+			# Peel annotated tags to the commit when possible.
+			c=$(git -C "$dir" rev-parse "$r^{commit}" 2>/dev/null || printf '%s\n' "$h")
+			printf '<li><a href="../commit/%s/">%s</a></li>\n' \
+				"$(esc "$c")" "$(esc "$r")"
+			ntags=1
+		done
+		if [ "$ntags" = 0 ]; then
+			:
+		fi
+		printf '%s\n' '</ul>'
+	} >>"$base/refs/index.html"
+	page_end "$base/refs/index.html"
+
+	# Commits
+	while IFS="$(printf '\t')" read -r hash shortc epoch iso subject author || [ -n "${hash-}" ]
+	do
+		[ -n "${hash-}" ] || continue
+		cdir=$base/commit/$hash
+		mkdir -p "$cdir"
+		page_begin "$cdir/index.html" "$shortc - $name - Splux Git" "$subject"
+		{
+			printf '<p class="git-nav"><a href="../../">%s</a> · <a href="../../log/">log</a> · commit</p>\n' \
+				"$(esc "$name")"
+			printf '<h2><code>%s</code></h2>\n' "$(esc "$shortc")"
+			printf '%s\n' '<hr class="rule">'
+			printf '<p>%s</p>\n' "$(esc "$subject")"
+			printf '%s\n' '<div class="info">'
+			printf '<div class="dl-row"><span class="muted">Author</span><span>%s</span></div>\n' \
+				"$(esc "$author")"
+			printf '<div class="dl-row"><span class="muted">Date</span><span><time datetime="%s">%s</time></span></div>\n' \
+				"$(esc "$iso")" "$(esc "$iso")"
+			printf '<div class="dl-row"><span class="muted">Commit</span><span><code>%s</code></span></div>\n' \
+				"$(esc "$hash")"
+			printf '<div class="dl-row"><span class="muted">GitHub</span><span><a href="%s/commit/%s">mirror</a></span></div>\n' \
+				"$(esc "$gh")" "$(esc "$hash")"
+			printf '%s\n' '</div>'
+			printf '%s\n' '<h3>Parents</h3>'
+			printf '%s\n' '<ul class="plain">'
+			git -C "$dir" rev-list --parents -n 1 "$hash" | {
+				read -r self rest || rest=
+				for p in $rest
+				do
+					ps=$(git -C "$dir" rev-parse --short "$p")
+					printf '<li><a href="../%s/"><code>%s</code></a></li>\n' \
+						"$(esc "$p")" "$(esc "$ps")"
+				done
+			}
+			printf '%s\n' '</ul>'
+			printf '%s\n' '<h3>Stat</h3>'
+			git -C "$dir" show --stat --format= "$hash" | awk -f "$AWK" -v mode=pre
+			printf '%s\n' '<h3>Diff</h3>'
+			# Truncate huge diffs so the Pages artifact stays small.
+			git -C "$dir" show --format= --color=never "$hash" |
+				dd bs=1024 count=300 2>/dev/null |
+				awk -f "$AWK" -v mode=pre
+		} >>"$cdir/index.html"
+		page_end "$cdir/index.html"
+		if [ -n "$shortc" ] && [ "$shortc" != "$hash" ]; then
+			write_redirect "$base/commit/$shortc/index.html" "../$hash/"
+		fi
+	done <"$clog"
+
+	# Trees and blobs at HEAD
+	write_tree() {
+		treepath=${1-}
+		if [ -n "$treepath" ]; then
+			tdest=$base/tree/$treepath/index.html
+			title="$treepath - $name - Splux Git"
+		else
+			tdest=$base/tree/index.html
+			title="Files - $name - Splux Git"
+		fi
+		if [ -z "$treepath" ]; then
+			up_repo="../"
+			blobpfx="../blob/"
+			treepfx="./"
+		else
+			depth=1
+			rest=$treepath
+			while [ "$rest" != "${rest#*/}" ]
+			do
+				depth=$((depth + 1))
+				rest=${rest#*/}
+			done
+			up=""
+			j=0
+			while [ "$j" -le "$depth" ]
+			do
+				up="../$up"
+				j=$((j + 1))
+			done
+			up_repo=$up
+			blobpfx=$up"blob/"
+			treepfx="./"
+		fi
+		page_begin "$tdest" "$title" "HEAD tree for $name"
+		{
+			printf '<p class="git-nav"><a href="%s">%s</a> · files</p>\n' \
+				"$(esc "$up_repo")" "$(esc "$name")"
+			if [ -n "$treepath" ]; then
+				printf '<h2>%s</h2>\n' "$(esc "$treepath")"
+			else
+				printf '%s\n' '<h2>Files</h2>'
+			fi
+			printf '%s\n' '<hr class="rule">'
+			if [ -n "$treepath" ]; then
+				printf '<p><a href="../">../</a></p>\n'
+			fi
+			printf '%s\n' '<p><label for="git-filter">Filter</label> <input id="git-filter" type="search" placeholder="file"></p>'
+			ent=$base/.entries.tsv
+			: >"$ent"
+			if [ -n "$treepath" ]; then
+				git -C "$dir" ls-tree -l "HEAD:$treepath"
+			else
+				git -C "$dir" ls-tree -l HEAD
+			fi | while IFS= read -r line
+			do
+				set -- $line
+				[ $# -ge 5 ] || continue
+				kind=$2
+				sz=$4
+				shift 4
+				path=$*
+				basepath=${path##*/}
+				[ -n "$basepath" ] || continue
+				if [ "$kind" = tree ]; then
+					printf 'dir\t\t%s\t%s\n' "$basepath/" "$treepfx$basepath/"
+				elif [ "$kind" = blob ]; then
+					if [ -n "$treepath" ]; then
+						fullpath=$treepath/$basepath
+					else
+						fullpath=$basepath
+					fi
+					printf 'file\t%s\t%s\t%s\n' "$sz" "$basepath" "$blobpfx$fullpath/"
+				fi
+			done >"$ent"
+			if [ -s "$ent" ]; then
+				awk -f "$AWK" -v mode=entries "$ent"
+			else
+				printf '%s\n' '<p class="muted">empty directory</p>'
+			fi
+		} >>"$tdest"
+		page_end "$tdest" gitjs
+	}
+
+	write_tree ""
+	# Every directory that contains a blob.
+	git -C "$dir" ls-tree -r -d --name-only HEAD | while IFS= read -r d
+	do
+		[ -n "$d" ] || continue
+		write_tree "$d"
+	done
+
+	git -C "$dir" ls-tree -r -l HEAD | while IFS= read -r line
+	do
+		set -- $line
+		[ "$2" = blob ] || continue
+		sz=$4
+		obj=$3
+		shift 4
+		path=$*
+		[ -n "$path" ] || continue
+		bdest=$base/blob/$path/index.html
+		page_begin "$bdest" "$path - $name - Splux Git" "$path at HEAD"
+		parent=${path%/*}
+		# depth of blob/path/... 
+		depth=1
+		rest=$path
+		while [ "$rest" != "${rest#*/}" ]
+		do
+			depth=$((depth + 1))
+			rest=${rest#*/}
+		done
+		up=""
+		j=0
+		while [ "$j" -le "$depth" ]
+		do
+			up="../$up"
+			j=$((j + 1))
+		done
+		# blob/a/b -> up is ../../../ (blob + a + b) = depth+1, plus we started le depth so depth+1. Good.
+		{
+			if [ "$parent" = "$path" ]; then
+				dirhref="${up}tree/"
+			else
+				dirhref="${up}tree/${parent}/"
+			fi
+			printf '<p class="git-nav"><a href="%s">%s</a> · <a href="%s">directory</a></p>\n' \
+				"$(esc "$up")" "$(esc "$name")" "$(esc "$dirhref")"
+			printf '<h2>%s</h2>\n' "$(esc "$path")"
+			printf '%s\n' '<hr class="rule">'
+			printf '<p class="muted">%s bytes at <a href="%scommit/%s/">%s</a> · <a href="%s/blob/main/%s">GitHub</a></p>\n' \
+				"$(esc "$sz")" "$(esc "$up")" "$(esc "$head")" "$(esc "$short")" \
+				"$(esc "$gh")" "$(esc "$path")"
+			szn=$(printf '%s' "$sz" | tr -d ' ')
+			case $szn in
+				''|*[!0-9]*) szn=0 ;;
+			esac
+			if is_binary_name "$path"; then
+				printf '%s\n' '<p>Binary file. Clone the repository or open the GitHub mirror.</p>'
+			elif [ "$szn" -gt "$MAXBLOB" ]; then
+				printf '%s\n' '<p>File is too large to embed here. Clone the repository or open the GitHub mirror.</p>'
+			else
+				git -C "$dir" cat-file blob "$obj" | awk -f "$AWK" -v mode=pre
+			fi
+		} >>"$bdest"
+		page_end "$bdest"
+	done
+
+	day=${lastiso%%T*}
+	printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
+		"$name" "$repo_desc" "$short" "$head" "$ncommits" "$nfiles" "$day" "$gh" \
+		>>"$index_rows"
+
+	rm -f "$langtmp" "$clog" "$base/.entries.tsv"
+	printf '%s\n' "git: $name ($ncommits commits, $nfiles files)" >&2
+}
+
+build_repo SPS "$SPS"
+build_repo sps-core "$CORE"
+build_repo sps-extra "$EXTRA"
+build_repo splux-site "$SITE"
+
+# Index
+page_begin "$OUT/git/index.html" "Git - Splux Linux" "Static browse of the official Splux git trees."
+{
+	printf '%s\n' '<h2>Git</h2>'
+	printf '%s\n' '<hr class="rule">'
+	printf '%s\n' '<p>This is a static browse of the official trees, rebuilt with the handbook. There is no git server here: no accounts, no pull requests, and no <code>git clone</code> from this host. Clone the GitHub mirrors. The commits, files, and language counts on these pages are the same history.</p>'
+	printf '%s\n' '<p><label for="git-filter">Filter</label> <input id="git-filter" type="search" placeholder="repository"></p>'
+	printf '%s\n' '<table class="pkgs" id="git-repos">'
+	printf '%s\n' '<thead><tr><th>Repository</th><th>Description</th><th>HEAD</th><th>Commits</th><th>Updated</th></tr></thead>'
+	printf '%s\n' '<tbody>'
+	if [ -s "$index_rows" ]; then
+		while IFS="$(printf '\t')" read -r name desc short head ncommits nfiles day gh || [ -n "${name-}" ]
+		do
+			[ -n "${name-}" ] || continue
+			printf '<tr data-repo="%s" data-search="%s %s">' \
+				"$(esc "$name")" "$(esc "$name")" "$(esc "$desc")"
+			printf '<td><a class="name" href="%s/">%s</a></td>' "$(esc "$name")" "$(esc "$name")"
+			printf '<td>%s</td>' "$(esc "$desc")"
+			printf '<td><a href="%s/commit/%s/"><code>%s</code></a></td>' \
+				"$(esc "$name")" "$(esc "$head")" "$(esc "$short")"
+			printf '<td>%s</td>' "$(esc "$ncommits")"
+			printf '<td>%s</td>' "$(esc "$day")"
+			printf '</tr>\n'
+		done <"$index_rows"
+	fi
+	printf '%s\n' '</tbody></table>'
+	printf '%s\n' '<h3>Clone</h3>'
+	printf '%s\n' '<pre class="block">git clone https://github.com/RobertFlexx/SPS.git'
+	printf '%s\n' 'git clone https://github.com/RobertFlexx/sps-core.git'
+	printf '%s\n' 'git clone https://github.com/RobertFlexx/sps-extra.git'
+	printf '%s\n' 'git clone https://github.com/RobertFlexx/splux-site.git</pre>'
+	printf '%s\n' '<p class="note">Live ISO files stay on GitHub Releases. Recipe browse also lives under <a href="../../../../packages/">packages</a>.</p>'
+} >>"$OUT/git/index.html"
+page_end "$OUT/git/index.html" gitjs
+
+# JSON summary
+awk -F '\t' '
+function jesc(s) {
+	gsub(/\\/, "\\\\", s)
+	gsub(/"/, "\\\"", s)
+	return s
+}
+BEGIN { print "[" }
+{
+	if (NR > 1) print ","
+	printf "{\"name\":\"%s\",\"description\":\"%s\",\"head\":\"%s\",\"full\":\"%s\",\"commits\":%s,\"files\":%s,\"updated\":\"%s\",\"github\":\"%s\"}", \
+		jesc($1), jesc($2), jesc($3), jesc($4), $5 + 0, $6 + 0, jesc($7), jesc($8)
+}
+END { print "\n]" }
+' "$index_rows" >"$OUT/data/git.json"
+
+printf '%s\n' "git: wrote $OUT/git" >&2
diff --git a/tools/build-site.sh b/tools/build-site.sh
index 7f8c5cb..2cb7de2 100755
--- a/tools/build-site.sh
+++ b/tools/build-site.sh
@@ -17,7 +17,7 @@ CORE_GIT=${CORE_GIT:-https://github.com/RobertFlexx/sps-core}
 EXTRA_GIT=${EXTRA_GIT:-https://github.com/RobertFlexx/sps-extra}
 SPS_GIT=${SPS_GIT:-https://github.com/RobertFlexx/SPS}
 SITE_URL=${SITE_URL:-https://splux.robertflexx.dev}
-GIT_HOST=${GIT_HOST:-https://git.splux.robertflexx.dev}
+GIT_HOST=${GIT_HOST:-https://splux.robertflexx.dev/git}
 
 clone_tree() {
 	url=$1
@@ -69,7 +69,7 @@ write_header() {
 <a href="${root}packages/">packages</a>
 <a href="${root}install/">install</a>
 <a href="${root}docs/">docs</a>
-<a href="${root}git/">git (WIP)</a>
+<a href="${root}git/">git</a>
 <a href="${root}source/">source</a>
 <a href="${root}news/">news</a>
 </nav>
@@ -81,8 +81,8 @@ write_footer() {
 	cat <<EOF
 <footer>
 <p>Splux Linux</p>
-<p><a href="${root}">home</a> · <a href="${root}packages/">packages</a> · <a href="${root}git/">git (WIP)</a> · <a href="${root}docs/">documentation</a></p>
-<p>This site was fully handmade by <a href="${GIT_HOST}/RobertFlexx">RobertFlexx</a>.</p>
+<p><a href="${root}">home</a> · <a href="${root}packages/">packages</a> · <a href="${root}git/">git</a> · <a href="${root}docs/">documentation</a></p>
+<p>This site was fully handmade by <a href="https://github.com/RobertFlexx">RobertFlexx</a>.</p>
 </footer>
 EOF
 }
@@ -99,7 +99,7 @@ write_docs_nav() {
 		install "${root}install/" "Install" \
 		sps "${root}docs/sps/" "SPS tools" \
 		firstboot "${root}docs/first-boot/" "After first boot" \
-		git "${root}git/" "Git (WIP)"
+		git "${root}git/" "Git"
 	while [ "$#" -ge 3 ]
 	do
 		if [ "$cur" = "$1" ]; then
@@ -149,8 +149,8 @@ scan_tree() {
 }
 
 printf '%s\n' "reading recipes" >&2
-scan_tree core "$CORE" "$GIT_HOST/RobertFlexx/sps-core" "https://github.com/RobertFlexx/sps-core"
-scan_tree extra "$EXTRA" "$GIT_HOST/RobertFlexx/sps-extra" "https://github.com/RobertFlexx/sps-extra"
+scan_tree core "$CORE" "$GIT_HOST/sps-core" "https://github.com/RobertFlexx/sps-core"
+scan_tree extra "$EXTRA" "$GIT_HOST/sps-extra" "https://github.com/RobertFlexx/sps-extra"
 
 LC_ALL=C sort -t "$(printf '\t')" -k1,1 -k5,5 "$tsv" >"$tmp/sorted.tsv"
 mv "$tmp/sorted.tsv" "$tsv"
@@ -282,9 +282,9 @@ livesig=${site_sha}-$(date -u '+%Y%m%d%H%M%S' 2>/dev/null || date +%Y%m%d%H%M%S)
 cat >"$tmp/news-info.html" <<EOF
 <div class="info">
 <div class="dl-row"><span class="muted">Latest ISO</span><span id="live-iso"><a href="$notes_url">$tag</a> ($date)</span></div>
-<div class="dl-row"><span class="muted">SPS</span><span id="live-sps"><a href="$GIT_HOST/RobertFlexx/SPS/commit/$sps_sha">$sps_sha</a></span></div>
-<div class="dl-row"><span class="muted">sps-core</span><span id="live-core"><a href="$GIT_HOST/RobertFlexx/sps-core/commit/$core_sha">$core_sha</a></span></div>
-<div class="dl-row"><span class="muted">sps-extra</span><span id="live-extra"><a href="$GIT_HOST/RobertFlexx/sps-extra/commit/$extra_sha">$extra_sha</a></span></div>
+<div class="dl-row"><span class="muted">SPS</span><span id="live-sps"><a href="$GIT_HOST/SPS/commit/$sps_sha/">$sps_sha</a></span></div>
+<div class="dl-row"><span class="muted">sps-core</span><span id="live-core"><a href="$GIT_HOST/sps-core/commit/$core_sha/">$core_sha</a></span></div>
+<div class="dl-row"><span class="muted">sps-extra</span><span id="live-extra"><a href="$GIT_HOST/sps-extra/commit/$extra_sha/">$extra_sha</a></span></div>
 <div class="dl-row"><span class="muted">Packages</span><span>$npkgs ($ncore core, $nextra extra)</span></div>
 <div class="dl-row"><span class="muted">Site build</span><span>$generated ($site_sha)</span></div>
 </div>
@@ -332,7 +332,50 @@ subst site/docs/images/index.html "$OUT/docs/images/index.html" "../../" images
 subst site/docs/sps/index.html "$OUT/docs/sps/index.html" "../../" sps
 subst site/docs/first-boot/index.html "$OUT/docs/first-boot/index.html" "../../" firstboot
 subst site/source/index.html "$OUT/source/index.html" "../"
-subst site/git/index.html "$OUT/git/index.html" "../"
+
+export OUT CORE EXTRA SPS SITE GIT_HOST SITE_URL
+sh tools/build-git.sh
+find "$OUT/git" -name index.html >"$tmp/gitpages"
+while IFS= read -r page
+do
+	[ -n "$page" ] || continue
+	rel=${page#"$OUT"/}
+	depth=0
+	rest=$rel
+	while [ "$rest" != "${rest#*/}" ]
+	do
+		depth=$((depth + 1))
+		rest=${rest#*/}
+	done
+	root=""
+	i=0
+	while [ "$i" -lt "$depth" ]
+	do
+		root="../$root"
+		i=$((i + 1))
+	done
+	write_header "$root" >"$tmp/hgit.html"
+	write_footer "$root" >"$tmp/fgit.html"
+	awk -f tools/subst.awk \
+		-v headerfile="$tmp/hgit.html" \
+		-v footerfile="$tmp/fgit.html" \
+		-v rowsfile="" \
+		-v newsfile="" \
+		-v brieffile="" \
+		-v infofile="" \
+		-v root="$root" \
+		-v tag="$tag" \
+		-v date="$date" \
+		-v npkgs="$npkgs" \
+		-v ncore="$ncore" \
+		-v nextra="$nextra" \
+		-v generated="$generated" \
+		-v livesig="$livesig" \
+		-v curlver="$curlver" \
+		-v githost="$GIT_HOST" \
+		"$page" >"$tmp/gpage.html"
+	mv "$tmp/gpage.html" "$page"
+done <"$tmp/gitpages"
 
 awk -f tools/news.awk -v mode=html -v page=1 -v per="$news_per" \
 	"$tmp/news.tsv" >"$tmp/news.html"
diff --git a/tools/collect-news.sh b/tools/collect-news.sh
index a42c44f..0b6dfd7 100755
--- a/tools/collect-news.sh
+++ b/tools/collect-news.sh
@@ -13,7 +13,7 @@ CORE=${CORE:-vendor/core}
 EXTRA=${EXTRA:-vendor/extra}
 SPS=${SPS:-vendor/sps}
 SITE=${SITE:-.}
-GIT_HOST=${GIT_HOST:-https://git.splux.robertflexx.dev}
+GIT_HOST=${GIT_HOST:-https://splux.robertflexx.dev/git}
 GIT_HOST=${GIT_HOST%/}
 
 tab=$(printf '\t')
@@ -84,7 +84,7 @@ git_commits() {
 		[ -n "${iso-}" ] || continue
 		[ -n "${hash-}" ] || continue
 		emit "$epoch" "$iso" commit "$repo" \
-			"$baseurl/commit/$hash" "$subject" "" "$author" "" \
+			"$baseurl/commit/$hash/" "$subject" "" "$author" "" \
 			"$(github_avatar "$author")"
 	done
 }
@@ -103,10 +103,10 @@ read_commit_tsv() {
 	done <"$file"
 }
 
-git_commits "$SPS" SPS "$GIT_HOST/RobertFlexx/SPS"
-git_commits "$CORE" sps-core "$GIT_HOST/RobertFlexx/sps-core"
-git_commits "$EXTRA" sps-extra "$GIT_HOST/RobertFlexx/sps-extra"
-git_commits "$SITE" splux-site "$GIT_HOST/RobertFlexx/splux-site"
+git_commits "$SPS" SPS "$GIT_HOST/SPS"
+git_commits "$CORE" sps-core "$GIT_HOST/sps-core"
+git_commits "$EXTRA" sps-extra "$GIT_HOST/sps-extra"
+git_commits "$SITE" splux-site "$GIT_HOST/splux-site"
 
 if command -v gh >/dev/null 2>&1; then
 	if gh api --paginate "repos/RobertFlexx/SPS/releases?per_page=100" --jq \
@@ -137,7 +137,7 @@ if command -v gh >/dev/null 2>&1; then
 		if gh api --paginate "repos/${path}/commits?per_page=100" --jq \
 			'.[] | [
 				.commit.committer.date,
-				("'"$GIT_HOST"'/'"$path"'/commit/" + .sha),
+				("'"$GIT_HOST"'/'"$label"'/commit/" + .sha + "/"),
 				((.commit.message // "") | split("\n")[0] | gsub("[\r\t]"; " ")),
 				((.author.login // .commit.author.name // "") | gsub("[\r\t]"; " ")),
 				(if .commit.verification.verified == true then "yes" else "no" end),
diff --git a/tools/git.awk b/tools/git.awk
new file mode 100644
index 0000000..388597e
--- /dev/null
+++ b/tools/git.awk
@@ -0,0 +1,132 @@
+# HTML fragments for the static Splux Git browser.
+# mode=one       escape $0 (or -v text=) as one HTML string
+# mode=pre       escape stdin as a <pre class="block git-blob">
+# mode=langs     TSV lang<TAB>bytes -> bar plus list
+# mode=commits   TSV hash short epoch iso subject author
+#                -v commitpfx= relative prefix ending in commit/
+# mode=entries   TSV kind size name href  (kind=dir|file)
+
+function esc(s) {
+	gsub(/&/, "\\&amp;", s)
+	gsub(/</, "\\&lt;", s)
+	gsub(/>/, "\\&gt;", s)
+	gsub(/"/, "\\&quot;", s)
+	return s
+}
+
+function pct(n, d) {
+	if (d <= 0)
+		return 0
+	return int((n * 1000) / d) / 10
+}
+
+BEGIN {
+	FS = "\t"
+	limit = limit + 0
+	if (mode == "one") {
+		if (text != "")
+			printf "%s", esc(text)
+		else {
+			while ((getline line) > 0)
+				printf "%s", esc(line)
+		}
+		exit
+	}
+}
+
+mode == "pre" {
+	if (!opened) {
+		print "<pre class=\"block git-blob\">"
+		opened = 1
+	}
+	print esc($0)
+}
+
+mode == "langs" {
+	n++
+	lang[n] = $1
+	bytes[n] = $2 + 0
+	total += bytes[n]
+}
+
+mode == "commits" {
+	if (limit > 0 && n >= limit)
+		next
+	n++
+	hash[n] = $1
+	short[n] = $2
+	iso[n] = $4
+	subject[n] = $5
+	author[n] = $6
+}
+
+mode == "entries" {
+	n++
+	kind[n] = $1
+	size[n] = $2
+	name[n] = $3
+	href[n] = $4
+}
+
+END {
+	if (mode == "pre") {
+		if (!opened)
+			print "<pre class=\"block git-blob\"></pre>"
+		else
+			print "</pre>"
+		exit
+	}
+	if (mode == "langs") {
+		if (n < 1)
+			exit
+		print "<div class=\"lang-bar\" role=\"img\" aria-label=\"Languages\">"
+		for (i = 1; i <= n; i++) {
+			w = pct(bytes[i], total)
+			if (w <= 0 && bytes[i] > 0)
+				w = 0.1
+			printf "<span class=\"lang-seg\" style=\"width:%s%%\" title=\"%s %s%%\"></span>\n", \
+				w, esc(lang[i]), w
+		}
+		print "</div>"
+		print "<ul class=\"lang-list\">"
+		for (i = 1; i <= n; i++)
+			printf "<li>%s <span class=\"muted\">%s%%</span></li>\n", \
+				esc(lang[i]), pct(bytes[i], total)
+		print "</ul>"
+		exit
+	}
+	if (mode == "commits") {
+		print "<table class=\"pkgs git-log\">"
+		print "<thead><tr><th>Commit</th><th>Subject</th><th>Author</th><th>When</th></tr></thead>"
+		print "<tbody>"
+		for (i = 1; i <= n; i++) {
+			href = commitpfx esc(hash[i]) "/"
+			day = iso[i]
+			sub(/T.*/, "", day)
+			printf "<tr>"
+			printf "<td><a href=\"%s\"><code>%s</code></a></td>", href, esc(short[i])
+			printf "<td><a href=\"%s\">%s</a></td>", href, esc(subject[i])
+			printf "<td>%s</td>", esc(author[i])
+			printf "<td><time datetime=\"%s\">%s</time></td>", esc(iso[i]), esc(day)
+			print "</tr>"
+		}
+		print "</tbody></table>"
+		exit
+	}
+	if (mode == "entries") {
+		print "<table class=\"pkgs git-tree\">"
+		print "<thead><tr><th></th><th>Name</th><th>Size</th></tr></thead>"
+		print "<tbody>"
+		for (i = 1; i <= n; i++) {
+			label = kind[i] == "dir" ? "dir" : "file"
+			sz = kind[i] == "dir" ? "" : size[i]
+			printf "<tr data-search=\"%s\">", esc(tolower(name[i]))
+			printf "<td class=\"muted\">%s</td>", label
+			printf "<td><a href=\"%s\">%s</a></td>", esc(href[i]), esc(name[i])
+			printf "<td class=\"muted\">%s</td>", esc(sz)
+			print "</tr>"
+		}
+		print "</tbody></table>"
+		exit
+	}
+}
diff --git a/tools/packages.awk b/tools/packages.awk
index 56d92df..84215e9 100644
--- a/tools/packages.awk
+++ b/tools/packages.awk
@@ -145,9 +145,9 @@ function write_page(    dir, ncomp, i, prefix, parts, cat, page, gh_dir) {
 		print "<a href=\"" github "/tree/main/" gh_dir "\">repository directory</a><br>" > page
 		print "<a href=\"" github "/commits/main/" gh_dir "\">history</a>" > page
 	} else {
-		print "<a href=\"" github "/src/branch/main/" gh_dir "/recipe\">view recipe</a><br>" > page
-		print "<a href=\"" github "/src/branch/main/" gh_dir "\">repository directory</a><br>" > page
-		print "<a href=\"" github "/commits/branch/main/" gh_dir "\">history</a>" > page
+		print "<a href=\"" github "/blob/" gh_dir "/recipe/\">view recipe</a><br>" > page
+		print "<a href=\"" github "/tree/" gh_dir "/\">repository directory</a><br>" > page
+		print "<a href=\"" github "/log/\">history</a>" > page
 	}
 	if (mirror != "") {
 		print "<br>" > page
@@ -162,7 +162,7 @@ function write_page(    dir, ncomp, i, prefix, parts, cat, page, gh_dir) {
 
 BEGIN {
 	if (repo == "") repo = "unknown"
-	if (github == "") github = "https://git.splux.robertflexx.dev/RobertFlexx/sps-core"
+	if (github == "") github = "https://splux.robertflexx.dev/git/sps-core"
 	if (pkgpath == "") pkgpath = "unknown"
 }
 
diff --git a/tools/subst.awk b/tools/subst.awk
index eb37dc2..03c2b0e 100644
--- a/tools/subst.awk
+++ b/tools/subst.awk
@@ -21,7 +21,7 @@ BEGIN {
 		close(footerfile)
 	}
 	if (githost == "")
-		githost = "https://git.splux.robertflexx.dev"
+		githost = "https://splux.robertflexx.dev/git"
 }
 
 $0 == "@@HEADER@@" {