From 307ff4912bac1095ebf382d70241f19409b2f8b8 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sun, 14 Jun 2026 16:30:47 +0300 Subject: add: templating --- .gitignore | 1 + _shared/build.py | 94 ++++++++++++++++++++++ _shared/footer.html | 12 +++ _shared/style.css | 22 +++++ build.sh | 2 + demo.gumx.cc/demos.json | 9 +++ demo.gumx.cc/meta | 2 + files.gumx.cc/body.html | 6 ++ files.gumx.cc/meta | 2 + hooks/post-receive | 19 +++++ irc.gumx.cc/body.html | 9 +++ irc.gumx.cc/index.html | 16 +++- irc.gumx.cc/meta | 2 + .../.well-known/autoconfig/mail/config-v1.1.xml | 22 +++++ mail.gumx.cc/body.html | 12 +++ mail.gumx.cc/index.html | 12 ++- mail.gumx.cc/meta | 2 + pgp.gumx.cc/body.html | 7 ++ pgp.gumx.cc/index.html | 14 +++- pgp.gumx.cc/meta | 2 + twt.gumx.cc/body.html | 49 +++++++++++ twt.gumx.cc/extra.css | 4 + twt.gumx.cc/meta | 2 + vpn.gumx.cc/body.html | 19 +++++ vpn.gumx.cc/index.html | 15 +++- vpn.gumx.cc/meta | 2 + wk.fo/body.html | 19 +++++ wk.fo/index.html | 16 +++- wk.fo/meta | 2 + 29 files changed, 383 insertions(+), 12 deletions(-) create mode 100644 .gitignore create mode 100644 _shared/build.py create mode 100644 _shared/footer.html create mode 100644 _shared/style.css create mode 100644 build.sh create mode 100644 demo.gumx.cc/demos.json create mode 100644 demo.gumx.cc/meta create mode 100644 files.gumx.cc/body.html create mode 100644 files.gumx.cc/meta create mode 100644 hooks/post-receive create mode 100644 irc.gumx.cc/body.html create mode 100644 irc.gumx.cc/meta create mode 100644 mail.gumx.cc/.well-known/autoconfig/mail/config-v1.1.xml create mode 100644 mail.gumx.cc/body.html create mode 100644 mail.gumx.cc/meta create mode 100644 pgp.gumx.cc/body.html create mode 100644 pgp.gumx.cc/meta create mode 100644 twt.gumx.cc/body.html create mode 100644 twt.gumx.cc/extra.css create mode 100644 twt.gumx.cc/meta create mode 100644 vpn.gumx.cc/body.html create mode 100644 vpn.gumx.cc/meta create mode 100644 wk.fo/body.html create mode 100644 wk.fo/meta diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52cdfdb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*/index.html diff --git a/_shared/build.py b/_shared/build.py new file mode 100644 index 0000000..935bf5c --- /dev/null +++ b/_shared/build.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Build sites from body.html + meta into index.html using shared templates.""" +import json +import os +import sys + +FAVICON = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1IDUiPjxyZWN0IHg9IjEiIHk9IjAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIxIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMCIgeT0iMiIgd2lkdGg9IjEiIGhlaWdodD0iMSIvPjxyZWN0IHg9IjEiIHk9IjIiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PC9zdmc+" + + +def render(title, breadcrumb, style, extra_css, body, footer): + css = style + ("\n" + extra_css if extra_css.strip() else "") + return f""" + + + + + +{title} + + + +
+

gumx / {breadcrumb}

+
+
+{body} +
+{footer} + + +""" + + +def build_demo_body(demos_file): + demos = json.load(open(demos_file)) + parts = [] + for d in demos: + url = d.get("url", "#") + title = d.get("title", d.get("name", "")) + desc = d.get("description", "") + src = d.get("source", "") + src_link = f' / source' if src else "" + parts.append(f'

{title}{src_link}

\n

{desc}

') + return "\n".join(parts) + + +def build(sites_dir): + shared = os.path.join(sites_dir, "_shared") + style = open(os.path.join(shared, "style.css")).read() + footer = open(os.path.join(shared, "footer.html")).read() + + for site in sorted(os.listdir(sites_dir)): + if site.startswith("_") or site == "fonts" or site == "hooks": + continue + site_dir = os.path.join(sites_dir, site) + if not os.path.isdir(site_dir): + continue + + body_file = os.path.join(site_dir, "body.html") + demos_file = os.path.join(site_dir, "demos.json") + + if site == "demo.gumx.cc" and os.path.exists(demos_file): + body = build_demo_body(demos_file) + elif os.path.exists(body_file): + body = open(body_file).read() + else: + continue + + title = site + breadcrumb = site + meta_file = os.path.join(site_dir, "meta") + if os.path.exists(meta_file): + for line in open(meta_file): + k, _, v = line.strip().partition("=") + if k == "TITLE": + title = v.strip('"') + elif k == "BREADCRUMB": + breadcrumb = v.strip('"') + + extra_css = "" + extra_file = os.path.join(site_dir, "extra.css") + if os.path.exists(extra_file): + extra_css = open(extra_file).read() + + out = render(title, breadcrumb, style, extra_css, body, footer) + with open(os.path.join(site_dir, "index.html"), "w") as f: + f.write(out) + print(f"built: {site}") + + +if __name__ == "__main__": + build(sys.argv[1] if len(sys.argv) > 1 else ".") diff --git a/_shared/footer.html b/_shared/footer.html new file mode 100644 index 0000000..3aedfc6 --- /dev/null +++ b/_shared/footer.html @@ -0,0 +1,12 @@ + diff --git a/_shared/style.css b/_shared/style.css new file mode 100644 index 0000000..0f7e7cf --- /dev/null +++ b/_shared/style.css @@ -0,0 +1,22 @@ +@font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Regular.woff2); font-weight: normal; } +@font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Bold.woff2); font-weight: bold; } +* { unicode-bidi: plaintext; box-sizing: border-box; } +html { color: black; background-color: white; } +body { font-family: "Kawkab Mono"; font-size: 16px; line-height: 1.4; margin: 0; padding: 4rem 0; min-height: 100%; overflow-wrap: break-word; } +main, header, footer { max-width: 800px; margin-inline: auto; padding: 0 2rem; } +h1, header, footer { text-align: center; } +main { text-align: justify; } +p, h2, h3, h4 { margin: 1em 0 0 0; } +ol { margin: 0.5em 0 0 0; } +table { margin: auto; border-collapse: collapse; } +th, td { border: 1px solid; padding: 0.3em 0.8em; } +pre { margin: 1em 0; } +pre code { border: thin solid; padding: 1em; display: block; text-align: start; overflow-x: scroll; } +code { font-size: 85%; } +hr { border: none; border-top: thin solid; margin: 1.25rem 0; } +header { margin-bottom: 1em; } +footer { margin-top: 3em; } +a { color: inherit; } +@media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } } +@media (max-width: 400px) { body { font-size: 0.8em; } h1 { font-size: 1.6em; } } +@media (prefers-color-scheme: dark) { html { filter: invert(1); } img { filter: invert(1); } } diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..6de6968 --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +python3 "$(dirname "$0")/_shared/build.py" "$(dirname "$0")" diff --git a/demo.gumx.cc/demos.json b/demo.gumx.cc/demos.json new file mode 100644 index 0000000..ae6d7df --- /dev/null +++ b/demo.gumx.cc/demos.json @@ -0,0 +1,9 @@ +[ + { + "name": "no-style-please", + "title": "zola-no-style-please", + "url": "/no-style-please/", + "description": "a Zola theme with no style, or rather, just a little style.", + "source": "https://git.gumx.cc/zola-no-style-please" + } +] diff --git a/demo.gumx.cc/meta b/demo.gumx.cc/meta new file mode 100644 index 0000000..496fc7e --- /dev/null +++ b/demo.gumx.cc/meta @@ -0,0 +1,2 @@ +TITLE="demo.gumx.cc" +BREADCRUMB="demo" diff --git a/files.gumx.cc/body.html b/files.gumx.cc/body.html new file mode 100644 index 0000000..3c34353 --- /dev/null +++ b/files.gumx.cc/body.html @@ -0,0 +1,6 @@ +

File hosting via 0x0. Uploads require a token.

+

upload

+
curl -F "file=@photo.jpg" -H "Authorization: YOUR_TOKEN" https://files.gumx.cc/
+

Files expire after 24 hours by default. Max 256 MB.

+

access

+

Contact hi@gumx.cc to request a token.

diff --git a/files.gumx.cc/meta b/files.gumx.cc/meta new file mode 100644 index 0000000..2c19518 --- /dev/null +++ b/files.gumx.cc/meta @@ -0,0 +1,2 @@ +TITLE="files.gumx.cc" +BREADCRUMB="files" diff --git a/hooks/post-receive b/hooks/post-receive new file mode 100644 index 0000000..aa142ed --- /dev/null +++ b/hooks/post-receive @@ -0,0 +1,19 @@ +#!/bin/sh +set -e + +WORK=/home/git/build/sites + +cd "$WORK" +python3 _shared/build.py . + +for SITE in irc.gumx.cc vpn.gumx.cc mail.gumx.cc pgp.gumx.cc wk.fo twt.gumx.cc files.gumx.cc demo.gumx.cc; do + if [ -d "$SITE" ]; then + WEBROOT="/var/www/$SITE" + mkdir -p "$WEBROOT" + rsync -rlptD --delete --exclude="/fonts" "$SITE/" "$WEBROOT/" + mkdir -p "$WEBROOT/fonts" + rsync -rlptD fonts/ "$WEBROOT/fonts/" + fi +done + +echo "sites deployed" diff --git a/irc.gumx.cc/body.html b/irc.gumx.cc/body.html new file mode 100644 index 0000000..ee66fec --- /dev/null +++ b/irc.gumx.cc/body.html @@ -0,0 +1,9 @@ +

Personal IRC server running ngircd, fronted by a Soju bouncer. Access is by invitation.

+

connect

+ + +
serverirc.gumx.cc:6697 (TLS)
+

request access

+

Send a message to hi@gumx.cc with your nick and preferred IRC client.

+

bots

+

irc bots

diff --git a/irc.gumx.cc/index.html b/irc.gumx.cc/index.html index a18a309..efb2b69 100644 --- a/irc.gumx.cc/index.html +++ b/irc.gumx.cc/index.html @@ -2,6 +2,7 @@ + irc.gumx.cc @@ -33,20 +39,26 @@ a { color: inherit; }

Personal IRC server running ngircd, fronted by a Soju bouncer. Access is by invitation.

connect

- +
serverwk.fo:6697 (TLS)
serverirc.gumx.cc:6697 (TLS)

request access

Send a message to hi@gumx.cc with your nick and preferred IRC client.

+

bots

+

irc bots

+ + diff --git a/irc.gumx.cc/meta b/irc.gumx.cc/meta new file mode 100644 index 0000000..fdf1428 --- /dev/null +++ b/irc.gumx.cc/meta @@ -0,0 +1,2 @@ +TITLE="irc.gumx.cc" +BREADCRUMB="irc" diff --git a/mail.gumx.cc/.well-known/autoconfig/mail/config-v1.1.xml b/mail.gumx.cc/.well-known/autoconfig/mail/config-v1.1.xml new file mode 100644 index 0000000..8098148 --- /dev/null +++ b/mail.gumx.cc/.well-known/autoconfig/mail/config-v1.1.xml @@ -0,0 +1,22 @@ + + + + gumx.cc + gumx mail + gumx + + mail.gumx.cc + 993 + SSL + password-cleartext + %EMAILADDRESS% + + + mail.gumx.cc + 587 + STARTTLS + password-cleartext + %EMAILADDRESS% + + + diff --git a/mail.gumx.cc/body.html b/mail.gumx.cc/body.html new file mode 100644 index 0000000..7a3e9fe --- /dev/null +++ b/mail.gumx.cc/body.html @@ -0,0 +1,12 @@ +

Self-hosted mail running Postfix and Dovecot.

+

contact

+

hi@gumx.cc

+

IMAP / SMTP

+ + + +
IMAPmail.gumx.cc:993 (TLS)
SMTPmail.gumx.cc:587 (STARTTLS)
+

CalDAV / CardDAV

+

Available at /dav/ via Xandikos. Credentials on request.

+

mailing list

+

Site updates and discussion: gumx.cc mailing list.

diff --git a/mail.gumx.cc/index.html b/mail.gumx.cc/index.html index 9e5597c..e22b67a 100644 --- a/mail.gumx.cc/index.html +++ b/mail.gumx.cc/index.html @@ -2,6 +2,7 @@ + mail.gumx.cc @@ -42,16 +48,20 @@ a { color: inherit; }

Available at /dav/ via Xandikos. Credentials on request.

mailing list

Site updates and discussion: gumx.cc mailing list.

+ + diff --git a/mail.gumx.cc/meta b/mail.gumx.cc/meta new file mode 100644 index 0000000..50375b1 --- /dev/null +++ b/mail.gumx.cc/meta @@ -0,0 +1,2 @@ +TITLE="mail.gumx.cc" +BREADCRUMB="mail" diff --git a/pgp.gumx.cc/body.html b/pgp.gumx.cc/body.html new file mode 100644 index 0000000..df5b04d --- /dev/null +++ b/pgp.gumx.cc/body.html @@ -0,0 +1,7 @@ +

Curated HKP keyserver. Keys are added after in-person verification only.

+

lookup

+
gpg --keyserver hkps://pgp.gumx.cc --recv-keys <fingerprint>
+

Or via HTTPS:

+
curl "https://pgp.gumx.cc/pks/lookup?op=get&search=0x<fingerprint>"
+

submit

+

Keys are not accepted without prior arrangement. Contact hi@gumx.cc.

diff --git a/pgp.gumx.cc/index.html b/pgp.gumx.cc/index.html index 7c3db3b..96e7623 100644 --- a/pgp.gumx.cc/index.html +++ b/pgp.gumx.cc/index.html @@ -2,6 +2,7 @@ + pgp.gumx.cc @@ -34,19 +40,23 @@ a { color: inherit; }

lookup

gpg --keyserver hkps://pgp.gumx.cc --recv-keys <fingerprint>

Or via HTTPS:

-
curl "https://pgp.gumx.cc/pks/lookup?op=get&search=0x<fingerprint>"
+
curl "https://pgp.gumx.cc/pks/lookup?op=get&search=0x<fingerprint>"

submit

Keys are not accepted without prior arrangement. Contact hi@gumx.cc.

+ + diff --git a/pgp.gumx.cc/meta b/pgp.gumx.cc/meta new file mode 100644 index 0000000..6cbe97d --- /dev/null +++ b/pgp.gumx.cc/meta @@ -0,0 +1,2 @@ +TITLE="pgp.gumx.cc" +BREADCRUMB="pgp" diff --git a/twt.gumx.cc/body.html b/twt.gumx.cc/body.html new file mode 100644 index 0000000..7f3adc3 --- /dev/null +++ b/twt.gumx.cc/body.html @@ -0,0 +1,49 @@ +

+twtxt.txt / +follow: twtxt follow gumx https://twt.gumx.cc/twtxt.txt +

+
+ + diff --git a/twt.gumx.cc/extra.css b/twt.gumx.cc/extra.css new file mode 100644 index 0000000..39c27d7 --- /dev/null +++ b/twt.gumx.cc/extra.css @@ -0,0 +1,4 @@ +.twt { margin: 1.5em 0 0 0; } +.twt-time { font-size: 0.85em; opacity: 0.6; display: block; } +.twt-text { display: block; margin-top: 0.25em; } +#error { opacity: 0.5; } diff --git a/twt.gumx.cc/meta b/twt.gumx.cc/meta new file mode 100644 index 0000000..1721f95 --- /dev/null +++ b/twt.gumx.cc/meta @@ -0,0 +1,2 @@ +TITLE="twt.gumx.cc" +BREADCRUMB="twt" diff --git a/vpn.gumx.cc/body.html b/vpn.gumx.cc/body.html new file mode 100644 index 0000000..49fb589 --- /dev/null +++ b/vpn.gumx.cc/body.html @@ -0,0 +1,19 @@ +

WireGuard VPN. Endpoint: vpn.gumx.cc:51820. Access is by invitation.

+

setup

+
    +
  1. Generate a keypair: wg genkey | tee private.key | wg pubkey > public.key
  2. +
  3. Send your public key to hi@gumx.cc
  4. +
  5. Receive your assigned IP (10.0.0.x/32) and the server public key
  6. +
  7. Create /etc/wireguard/wg0.conf and bring it up with wg-quick up wg0
  8. +
+

example client config

+
[Interface]
+PrivateKey = <your private key>
+Address    = 10.0.0.x/32
+DNS        = 1.1.1.1
+
+[Peer]
+PublicKey  = <server public key>
+Endpoint   = vpn.gumx.cc:51820
+AllowedIPs = 0.0.0.0/0
+PersistentKeepalive = 25
diff --git a/vpn.gumx.cc/index.html b/vpn.gumx.cc/index.html index c2a3ce3..0d33729 100644 --- a/vpn.gumx.cc/index.html +++ b/vpn.gumx.cc/index.html @@ -2,6 +2,7 @@ + vpn.gumx.cc @@ -31,7 +36,7 @@ a { color: inherit; }

gumx / vpn

-

WireGuard VPN. Endpoint: wk.fo:51820. Access is by invitation.

+

WireGuard VPN. Endpoint: vpn.gumx.cc:51820. Access is by invitation.

setup

  1. Generate a keypair: wg genkey | tee private.key | wg pubkey > public.key
  2. @@ -47,19 +52,23 @@ DNS = 1.1.1.1 [Peer] PublicKey = <server public key> -Endpoint = wk.fo:51820 +Endpoint = vpn.gumx.cc:51820 AllowedIPs = 0.0.0.0/0 PersistentKeepalive = 25 +
+ diff --git a/vpn.gumx.cc/meta b/vpn.gumx.cc/meta new file mode 100644 index 0000000..3c1ebbc --- /dev/null +++ b/vpn.gumx.cc/meta @@ -0,0 +1,2 @@ +TITLE="vpn.gumx.cc" +BREADCRUMB="vpn" diff --git a/wk.fo/body.html b/wk.fo/body.html new file mode 100644 index 0000000..40ac3f3 --- /dev/null +++ b/wk.fo/body.html @@ -0,0 +1,19 @@ +

File sharing, IRC bouncer, and VPN for friends. All services require an invitation.

+

file sharing

+

Upload via HTTPS (token required):

+
curl -F "file=@photo.jpg" -H "Authorization: YOUR_TOKEN" https://wk.fo/
+

Files expire after 24 hours by default (max 48h with expires param). Max 256 MB.

+

irc

+ + + +
serverwk.fo:6697 (TLS)
typeSoju bouncer
+

Use the credentials provided to you on invite. Works with any IRC client that supports SASL PLAIN.

+

vpn

+ + + +
serverwk.fo:51820 (UDP)
typeWireGuard
+

A config file is provided to you on invite. Import it into any WireGuard client.

+

access

+

Contact hi@gumx.cc.

diff --git a/wk.fo/index.html b/wk.fo/index.html index c79d706..fbb74cb 100644 --- a/wk.fo/index.html +++ b/wk.fo/index.html @@ -2,6 +2,7 @@ + wk.fo
-

wk.fo

+

gumx / wk.fo

File sharing, IRC bouncer, and VPN for friends. All services require an invitation.

@@ -51,16 +55,20 @@ a { color: inherit; }

A config file is provided to you on invite. Import it into any WireGuard client.

access

Contact hi@gumx.cc.

+
+ diff --git a/wk.fo/meta b/wk.fo/meta new file mode 100644 index 0000000..e068cc2 --- /dev/null +++ b/wk.fo/meta @@ -0,0 +1,2 @@ +TITLE="wk.fo" +BREADCRUMB="wk.fo" -- cgit v1.2.3