diff --git a/.gitignore b/.gitignore index a51fd35..ec462c8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,8 @@ __pycache__/ # Real credentials must never be committed — use the .example files archinstall/user_credentials.json iso/out/ + +# Hugo website +website/public/ +website/resources/ +website/.hugo_build.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a79e96..9343502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r - **Forgejo Actions runner** live on Proxmox VM (`forge-runner-01`, Ubuntu 24.04) with DinD sidecar — CI green end-to-end. Setup scripts in `ops/forgejo-runner/`. - **Walking-skeleton live ISO** (`iso/build.sh`). Overlays an Arch `releng` profile with Flask + the webinstaller, bakes a systemd unit that auto-starts the wizard on boot, produces a hybrid BIOS/UEFI ISO via `mkarchiso` in a privileged `archlinux:latest` container. Tested booting under OVMF in Proxmox — wizard screens 1–3 respond at `http://:5000`. +- **Public website at [furtka.org](https://furtka.org)** (`website/`). Hugo static site, English + German, served from `/var/www/furtka.org` on `forge-runner-01` via nginx. Upstream openresty proxy handles TLS. Intentionally minimal single-page copy while the project is pre-alpha. Deploy is `./website/deploy.sh` (rsync + remote Hugo build); one-time VM setup in `ops/nginx/setup-vm.sh`. ## [26.0-alpha] - 2026-04-13 diff --git a/ops/nginx/furtka.org.conf b/ops/nginx/furtka.org.conf new file mode 100644 index 0000000..19aa68b --- /dev/null +++ b/ops/nginx/furtka.org.conf @@ -0,0 +1,28 @@ +server { + listen 80; + listen [::]:80; + server_name furtka.org www.furtka.org; + + root /var/www/furtka.org; + index index.html; + + charset utf-8; + + location / { + try_files $uri $uri/ $uri.html =404; + } + + location = /favicon.svg { + access_log off; + log_not_found off; + expires 7d; + } + + location ~* \.(css|js|svg|woff2?|png|jpg|jpeg|webp|avif)$ { + access_log off; + expires 30d; + add_header Cache-Control "public, immutable"; + } + + error_page 404 /404.html; +} diff --git a/ops/nginx/setup-vm.sh b/ops/nginx/setup-vm.sh new file mode 100755 index 0000000..7c0e19a --- /dev/null +++ b/ops/nginx/setup-vm.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# One-time setup on forge-runner-01 for furtka.org. +# Idempotent — safe to re-run. +# +# Usage (on the VM, with sudo): +# sudo ops/nginx/setup-vm.sh +set -euo pipefail + +OWNER="${SUDO_USER:-daniel}" +WEBROOT="/var/www/furtka.org" +SRCROOT="/srv/furtka-site" +SITE_CONF="/etc/nginx/sites-available/furtka.org" +SITE_LINK="/etc/nginx/sites-enabled/furtka.org" + +install -d -o "$OWNER" -g "$OWNER" -m 0755 "$WEBROOT" +install -d -o "$OWNER" -g "$OWNER" -m 0755 "$SRCROOT" + +cp "$(dirname "$0")/furtka.org.conf" "$SITE_CONF" +ln -sfn "$SITE_CONF" "$SITE_LINK" + +# Drop the Ubuntu default site so it doesn't shadow us on :80. +rm -f /etc/nginx/sites-enabled/default + +nginx -t +systemctl reload nginx + +echo "OK: furtka.org ready at $WEBROOT (owner $OWNER)" diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..b998af4 --- /dev/null +++ b/website/README.md @@ -0,0 +1,61 @@ +# website/ — furtka.org + +Hugo source for [furtka.org](https://furtka.org). Intentionally minimal while +the project is pre-alpha: a single idea page in English and German, nothing more. +More pages will come back when there's something real to show. + +## Local build + +```sh +cd website +hugo server # http://localhost:1313 +``` + +Requires Hugo **extended** ≥ 0.140. + +## Deploy + +Hosted on `forge-runner-01` (Proxmox VM, Ubuntu 24.04). Hugo runs on the VM; +nginx serves the built output from `/var/www/furtka.org`. TLS is terminated by +an upstream openresty reverse proxy — the VM itself only speaks plain HTTP. + +First time only, on the VM: + +```sh +ssh forge-runner +sudo /srv/furtka-site/ops/nginx/setup-vm.sh # or copy the script over first +``` + +From then on, deploy from your dev machine: + +```sh +./website/deploy.sh +``` + +The script rsyncs `website/` to `/srv/furtka-site/` on the VM and runs +`hugo --minify` into `/var/www/furtka.org`. + +## Structure + +``` +hugo.toml Hugo config (multilingual: en default, de) +content/ Markdown pages + _index.md Home (EN) + _index.de.md Home (DE) +layouts/ Custom inline theme — no external theme or framework + _default/ baseof, single, list + partials/ head, header, footer, gate SVG, lang switcher + index.html Home-only layout with editorial hero +assets/css/main.css Stylesheet (fingerprinted + minified on build) +static/favicon.svg Gate mark in crimson +deploy.sh Rsync + remote Hugo build +``` + +## Design + +Modern-minimal on paper-white light / near-black dark. System-UI sans +(no webfonts — zero external requests, matches the self-hosting ethos). +Deep crimson accent, `prefers-color-scheme` switch. + +The gate SVG is the one brand mark — a small wicket-gate glyph repeated in the +header, footer, and favicon. diff --git a/website/assets/css/main.css b/website/assets/css/main.css new file mode 100644 index 0000000..d1ea01a --- /dev/null +++ b/website/assets/css/main.css @@ -0,0 +1,260 @@ +:root { + --bg: #f7f6f3; + --bg-subtle: #efeee8; + --fg: #0e0e0f; + --fg-muted: #6b6b6f; + --accent: #c03a28; + --accent-hover: #a0301f; + --border: #e4e3dc; + --font-sans: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", + Arial, "Noto Sans", sans-serif; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; + --measure: 32rem; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0d0d0f; + --bg-subtle: #17171a; + --fg: #ececee; + --fg-muted: #8a8a90; + --accent: #ff6b56; + --accent-hover: #ff8b78; + --border: #232326; + } +} + +* { box-sizing: border-box; } +html { -webkit-text-size-adjust: 100%; } + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font-family: var(--font-sans); + font-size: 1.0625rem; + line-height: 1.65; + font-feature-settings: "kern", "cv11", "ss01"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + display: flex; + flex-direction: column; + min-height: 100vh; + text-rendering: optimizeLegibility; +} + +.container { + max-width: 52rem; + margin-inline: auto; + padding-inline: 1.5rem; +} + +main.container { + width: 100%; + flex: 1; + padding-block: 3rem 4.5rem; +} + +.gate-mark { + color: var(--accent); + vertical-align: -0.2em; +} + +/* ── Kicker (shared small-caps style) ────────────────────────── */ + +.kicker { + font-family: var(--font-sans); + font-weight: 500; + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--fg-muted); +} + +/* ── Site header ─────────────────────────────────────────────── */ + +.site-header { + border-bottom: 1px solid var(--border); + padding-block: 1rem; +} +.site-header .container { + display: flex; + align-items: center; + gap: 1.25rem; +} +.site-title { + margin-right: auto; + display: inline-flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + color: inherit; +} +.site-title .gate-mark { + width: 1.15em; + height: 1.15em; + vertical-align: -0.15em; +} +.site-title .wordmark { + font-weight: 600; + letter-spacing: 0.14em; + color: var(--fg); + text-transform: uppercase; + font-size: 0.78rem; +} +.site-title:hover .wordmark { color: var(--accent); } + +/* ── Language switcher ───────────────────────────────────────── */ + +.lang-switcher { + display: flex; + list-style: none; + padding: 0; + margin: 0; + gap: 0.5rem; + align-items: center; +} +.lang-switcher li { display: flex; } +.lang-switcher a { + font-family: var(--font-sans); + font-weight: 500; + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--fg-muted); + text-decoration: none; + padding: 0.1rem 0.15rem; + border-bottom: 1.5px solid transparent; +} +.lang-switcher a:hover { color: var(--accent); } +.lang-switcher .is-active a { + color: var(--fg); + border-bottom-color: var(--accent); +} + +/* ── Hero (home) ─────────────────────────────────────────────── */ + +.hero { + padding-block: 3.5rem 2.5rem; +} + +.status-chip { + display: inline-flex; + align-items: center; + gap: 0.45rem; + font-family: var(--font-sans); + font-weight: 600; + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--accent); + margin: 0 0 1.75rem; +} +.status-chip::before { + content: ""; + display: inline-block; + width: 0.5rem; + height: 0.5rem; + border-radius: 999px; + background: var(--accent); +} +.status-chip .mono { + font-family: var(--font-mono); + font-weight: 500; + letter-spacing: 0.02em; + text-transform: none; + font-size: 0.78rem; +} + +.home h1 { + font-family: var(--font-sans); + font-weight: 800; + font-size: clamp(3.25rem, 10vw, 6.5rem); + line-height: 0.95; + letter-spacing: -0.035em; + margin: 0 0 1.5rem; + color: var(--fg); +} + +.home .lede { + font-family: var(--font-sans); + font-weight: 400; + font-size: clamp(1.15rem, 1.8vw, 1.375rem); + line-height: 1.4; + letter-spacing: -0.005em; + color: var(--fg-muted); + margin: 0; + max-width: 36rem; +} + +/* ── Body prose ──────────────────────────────────────────────── */ + +.prose { + max-width: var(--measure); +} +.prose p { + margin: 0 0 1.2rem; + letter-spacing: -0.003em; +} +.prose p:last-child { margin-bottom: 0; } + +.prose strong { + font-weight: 600; + color: var(--fg); +} + +.prose a { + color: var(--accent); + text-decoration: underline; + text-decoration-color: color-mix(in srgb, var(--accent) 35%, transparent); + text-decoration-thickness: 1px; + text-underline-offset: 3px; + transition: color 120ms, text-decoration-color 120ms; +} +.prose a:hover { + color: var(--accent-hover); + text-decoration-color: var(--accent); +} + +.prose em, .prose i { font-style: italic; } + +/* ── Footer ──────────────────────────────────────────────────── */ + +.site-footer { + margin-top: auto; + border-top: 1px solid var(--border); + padding-block: 1.25rem; +} +.site-footer .container { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} +.site-footer .kicker { margin: 0; } +.site-footer a { + color: var(--fg-muted); + text-decoration: none; + border-bottom: 1px solid transparent; + padding-bottom: 1px; + transition: color 120ms, border-color 120ms; +} +.site-footer a:hover { + color: var(--accent); + border-bottom-color: var(--accent); +} + +/* ── Selection + focus ───────────────────────────────────────── */ + +::selection { + background: var(--accent); + color: var(--bg); +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; + border-radius: 2px; +} diff --git a/website/content/_index.de.md b/website/content/_index.de.md new file mode 100644 index 0000000..ccf6dc6 --- /dev/null +++ b/website/content/_index.de.md @@ -0,0 +1,18 @@ +--- +title: "Furtka" +description: "Offenes Heimserver-Betriebssystem — einfach genug für alle." +status: "26.0-alpha — in Arbeit" +--- + +**Furtka** ist ein offenes Heimserver-Betriebssystem. +USB-Stick einstecken, durch einen Assistenten klicken, und aus jedem +alten x86-PC wird eine private Cloud für den Haushalt — mit eigenen +Apps, eigener Domain, eigenen Daten. + +Das Ziel ist einfach: **dein Vater soll das einrichten können.** + +Wir sind zu zweit und bauen das öffentlich, abends und am Wochenende. +Es ist früh. Außer uns selbst sollte das noch niemand benutzen. +Sobald es etwas Echtes zu zeigen gibt, steht es hier. + +Mitlesen? Schreib an hallo@furtka.org. diff --git a/website/content/_index.md b/website/content/_index.md new file mode 100644 index 0000000..a061823 --- /dev/null +++ b/website/content/_index.md @@ -0,0 +1,18 @@ +--- +title: "Furtka" +description: "Open-source home server OS — simple enough for everyone." +status: "26.0-alpha — work in progress" +--- + +**Furtka** is an open-source home server OS. +Boot from USB, click through a wizard, and any old x86 PC +turns into a private cloud for your household — with your own apps, +your own domain, your own data. + +The goal is simple: **your dad should be able to set this up.** + +We're two people building it in public on evenings and weekends, +and it's early. Nothing here is ready for other people yet. +When there's something real to show, this page will say so. + +Want to follow along? Write to hallo@furtka.org. diff --git a/website/deploy.sh b/website/deploy.sh new file mode 100755 index 0000000..04a5cea --- /dev/null +++ b/website/deploy.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Deploy furtka.org to forge-runner-01. +# Rsyncs the Hugo source up to the VM and builds it in-place. +set -euo pipefail + +HOST="${FURTKA_HOST:-forge-runner}" +SRCROOT="/srv/furtka-site" +WEBROOT="/var/www/furtka.org" + +HERE="$(cd "$(dirname "$0")" && pwd)" + +echo "→ rsync website/ to $HOST:$SRCROOT" +rsync -az --delete \ + --exclude='.hugo_build.lock' \ + --exclude='public/' \ + --exclude='resources/' \ + "$HERE/" "$HOST:$SRCROOT/" + +echo "→ build on $HOST" +ssh "$HOST" "cd $SRCROOT && hugo --minify --cleanDestinationDir -d $WEBROOT" + +echo "OK: deployed to https://furtka.org/" diff --git a/website/hugo.toml b/website/hugo.toml new file mode 100644 index 0000000..18214bd --- /dev/null +++ b/website/hugo.toml @@ -0,0 +1,36 @@ +baseURL = "https://furtka.org/" +title = "Furtka" +defaultContentLanguage = "en" +defaultContentLanguageInSubdir = false +enableRobotsTXT = true + +[params] + description = "Open-source home server OS — simple enough for everyone." + version = "26.0-alpha" + contactEmail = "hallo@furtka.org" + +[markup.goldmark.renderer] + unsafe = true + +[languages] + [languages.en] + languageCode = "en-us" + languageName = "English" + title = "Furtka" + weight = 1 + [languages.en.params] + description = "Open-source home server OS — simple enough for everyone." + + [languages.de] + languageCode = "de-de" + languageName = "Deutsch" + title = "Furtka" + weight = 2 + [languages.de.params] + description = "Offenes Heimserver-Betriebssystem — einfach genug für alle." + +[build] + writeStats = true + +[minify] + minifyOutput = true diff --git a/website/layouts/_default/baseof.html b/website/layouts/_default/baseof.html new file mode 100644 index 0000000..67bd51a --- /dev/null +++ b/website/layouts/_default/baseof.html @@ -0,0 +1,13 @@ + + + + {{ partial "head.html" . }} + + + {{ partial "header.html" . }} +
+ {{ block "main" . }}{{ end }} +
+ {{ partial "footer.html" . }} + + diff --git a/website/layouts/_default/list.html b/website/layouts/_default/list.html new file mode 100644 index 0000000..8aaf7e2 --- /dev/null +++ b/website/layouts/_default/list.html @@ -0,0 +1,21 @@ +{{ define "main" }} +
+ +
+ {{ .Content }} +
+ {{ with .Pages }} + + {{ end }} +
+{{ end }} diff --git a/website/layouts/_default/single.html b/website/layouts/_default/single.html new file mode 100644 index 0000000..14902c5 --- /dev/null +++ b/website/layouts/_default/single.html @@ -0,0 +1,11 @@ +{{ define "main" }} +
+ +
+ {{ .Content }} +
+
+{{ end }} diff --git a/website/layouts/index.html b/website/layouts/index.html new file mode 100644 index 0000000..1cf88fe --- /dev/null +++ b/website/layouts/index.html @@ -0,0 +1,14 @@ +{{ define "main" }} +
+
+ {{ with .Params.status }} +

{{ . | safeHTML }}

+ {{ end }} +

{{ .Title }}

+ {{ with site.Params.description }}

{{ . }}

{{ end }} +
+
+ {{ .Content }} +
+
+{{ end }} diff --git a/website/layouts/partials/footer.html b/website/layouts/partials/footer.html new file mode 100644 index 0000000..48af58b --- /dev/null +++ b/website/layouts/partials/footer.html @@ -0,0 +1,9 @@ + diff --git a/website/layouts/partials/gate.html b/website/layouts/partials/gate.html new file mode 100644 index 0000000..2a5036b --- /dev/null +++ b/website/layouts/partials/gate.html @@ -0,0 +1,6 @@ + diff --git a/website/layouts/partials/head.html b/website/layouts/partials/head.html new file mode 100644 index 0000000..a83ab54 --- /dev/null +++ b/website/layouts/partials/head.html @@ -0,0 +1,16 @@ + + +{{ if .IsHome }}{{ site.Title }} — {{ site.Params.description }}{{ else }}{{ .Title }} · {{ site.Title }}{{ end }} + + + + + + + +{{ $parts := split .Site.Language.LanguageCode "-" }} +{{ range .AllTranslations }} + +{{ end }} +{{ $css := resources.Get "css/main.css" | minify | fingerprint }} + diff --git a/website/layouts/partials/header.html b/website/layouts/partials/header.html new file mode 100644 index 0000000..96cd9b5 --- /dev/null +++ b/website/layouts/partials/header.html @@ -0,0 +1,9 @@ + diff --git a/website/layouts/partials/lang-switcher.html b/website/layouts/partials/lang-switcher.html new file mode 100644 index 0000000..70ce30e --- /dev/null +++ b/website/layouts/partials/lang-switcher.html @@ -0,0 +1,23 @@ + diff --git a/website/static/favicon.svg b/website/static/favicon.svg new file mode 100644 index 0000000..9898a02 --- /dev/null +++ b/website/static/favicon.svg @@ -0,0 +1,6 @@ + + + + + +