Compare commits

..

2 commits

Author SHA1 Message Date
91aa33b891 feat(brand): Mark M is the Furtka logo — website, installer, box UI, console, boot splash
All checks were successful
CI / lint (pull_request) Successful in 30s
CI / validate-json (pull_request) Successful in 24s
CI / markdown-links (pull_request) Successful in 16s
CI / test (pull_request) Successful in 1m47s
CI / lint (push) Successful in 28s
CI / test (push) Successful in 1m56s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 21s
Deploy site / deploy (push) Successful in 5s
Build ISO / build-iso (push) Successful in 20m27s
The logo vote on furtka.org closed with M 29 · R 13 (42 votes).

- website/tools/brand-assets.py generates every brand file from the
  logo-assets.py geometry: favicons (small-size tier, accent with a
  dark-mode variant) for website / webinstaller / on-box UI, 180 px
  apple-touch-icon, 1200x630 default share card, 640x480 syslinux splash,
  and docs/brand/ (mark, small mark, lockups, 20-column console art)
- website: header mark, favicon, touch icon, default og:image; /logo/ now
  shows the final result (static counts, winner highlighted, no voting
  UI or poll.js); homepage link reads 'meet our new logo'
- webinstaller: M in the header + tab icon
- on-box UI: .brand mark via CSS mask in brand red (not the UI's green),
  favicon on every page, brand line on the login page
- console: M art above the text of /etc/issue on the installed box
  (furtka-welcome) and the live ISO (furtka-update-issue)
- ISO: build.sh swaps releng's Arch splash for the Furtka one
- README shows the lockup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wq1hzUp4ZmCKHDuLKCnu73
2026-09-28 15:08:49 +02:00
0cad2e05c8 feat(website): download page for the ISO, homepage with download CTA and 3-step how-it-works
- /download/ and /de/download/: big download button, file name, size and
  release date, requirements (64-bit PC, UEFI or BIOS, >=16 GB disk that
  gets erased, USB stick, network cable with internet, second device),
  five install steps (Etcher / Rufus DD mode / dd, boot menu, Secure Boot
  off, proksi.local:5000, wizard, pull stick), checksum + verify commands
  when the release has one, alpha notice, link to all releases
- the release data comes from the Forgejo API at build time
  (partials/release.html): newest release with an .iso asset, falling back
  to params.version if the lookup fails; getresource cache disabled so a
  build never pins an old release
- homepage: status chip follows the latest release, 'Download ISO' primary
  CTA + 'Source code' ghost CTA, numbered how-it-works cards, download band
  before the closer
- releases now upload furtka-<ver>.iso.sha256, and release.yml rebuilds the
  site after publishing so the download page follows each release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wq1hzUp4ZmCKHDuLKCnu73
2026-09-28 14:52:59 +02:00
41 changed files with 753 additions and 48 deletions

View file

@ -57,3 +57,13 @@ jobs:
env: env:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_RELEASE_TOKEN }} FORGEJO_TOKEN: ${{ secrets.FORGEJO_RELEASE_TOKEN }}
run: ./scripts/publish-release.sh "${GITHUB_REF_NAME}" run: ./scripts/publish-release.sh "${GITHUB_REF_NAME}"
- name: Refresh furtka.org download page
# The download page reads the latest release (ISO, size, date,
# checksum) from the Forgejo API at build time, so rebuild the site
# now that the release exists. Same runner + bind mounts as
# deploy-site.yml. A failure here doesn't touch the release.
continue-on-error: true
run: |
apk add --no-cache hugo rsync
./website/deploy-ci.sh

View file

@ -7,6 +7,18 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r
## [Unreleased] ## [Unreleased]
### Added
- **New logo: Mark M** (a lowercase *f* cut out of an arched gate), winner of
the furtka.org vote (29 of 42 votes). It is now in the website header,
favicon and share card, the installer header and tab icon, the on-box UI
(every page and the login), the console welcome screens of the live ISO
and the installed box, and the BIOS boot-menu splash. Brand files in
`docs/brand/`, generated by `website/tools/brand-assets.py`.
- **furtka.org/download/** (and `/de/download/`): ISO download with size,
date and checksum, requirements and step-by-step install guide. Releases
now publish `furtka-<ver>.iso.sha256` and refresh the site.
### Security ### Security
- **App names are validated everywhere.** `POST /api/apps/remove` with - **App names are validated everywhere.** `POST /api/apps/remove` with

View file

@ -1,3 +1,5 @@
<p><img src="docs/brand/furtka-lockup.svg" alt="Furtka" height="56"></p>
# Furtka # Furtka
**Open-source home server OS — simple enough for everyone.** · [furtka.org](https://furtka.org) **Open-source home server OS — simple enough for everyone.** · [furtka.org](https://furtka.org)

View file

@ -45,7 +45,7 @@ Tag per meaningful milestone, not on a calendar. A milestone is: ISO boots, a wi
git push origin 26.1-alpha git push origin 26.1-alpha
``` ```
5. **The release workflow does the rest.** `.forgejo/workflows/release.yml` fires on the tag push and runs on the self-hosted runner: `scripts/build-release-tarball.sh` builds the self-update payload (tarball + sha256 + release.json under `dist/`), `iso/build.sh` builds the live-installer ISO, `scripts/publish-release.sh` uploads tarball + sha256 + release.json + ISO to the Forgejo release page. Pre-release is flagged automatically based on the suffix (`-alpha`/`-beta`/`-rc`). ISO build is `continue-on-error`: a flaky ISO step doesn't block the core tarball (the thing boxes need for self-update). 5. **The release workflow does the rest.** `.forgejo/workflows/release.yml` fires on the tag push and runs on the self-hosted runner: `scripts/build-release-tarball.sh` builds the self-update payload (tarball + sha256 + release.json under `dist/`), `iso/build.sh` builds the live-installer ISO, `scripts/publish-release.sh` uploads tarball + sha256 + release.json + ISO to the Forgejo release page. Pre-release is flagged automatically based on the suffix (`-alpha`/`-beta`/`-rc`). The ISO gets a `furtka-<ver>.iso.sha256` sidecar, and a last step rebuilds furtka.org so the download page (`/download/`) picks up the new ISO, size, date and checksum from the Forgejo API. ISO build is `continue-on-error`: a flaky ISO step doesn't block the core tarball (the thing boxes need for self-update).
The release workflow needs one secret set at repo **Settings → Secrets → Actions**: The release workflow needs one secret set at repo **Settings → Secrets → Actions**:
- `FORGEJO_RELEASE_TOKEN` — a PAT with `write:repository` scope. - `FORGEJO_RELEASE_TOKEN` — a PAT with `write:repository` scope.

View file

@ -13,6 +13,17 @@ hostname=$(cat /etc/hostname 2>/dev/null || hostname)
mapfile -t ips < <(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1) mapfile -t ips < <(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
new=$( new=$(
# Furtka mark (docs/brand/console-banner.txt, from brand-assets.py)
echo ' ▄▄▄▄▄▄▄▄'
echo ' ▄██████████▄'
echo ' █████▀▀▀▀▀▀███'
echo ' ████▀ ████'
echo ' ████ █████████'
echo ' ████ ████'
echo ' ████ ▄▄▄▄▄████'
echo ' ████ █████████'
echo ' ████ █████████'
echo ' ▀▀▀▀ ▀▀▀▀▀▀▀▀▀'
echo echo
echo " Furtka is ready." echo " Furtka is ready."
echo echo

1
assets/www/favicon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><style>svg{color:#c03a28}@media (prefers-color-scheme:dark){svg{color:#ff6b56}}</style><path fill="currentColor" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/></svg>

After

Width:  |  Height:  |  Size: 316 B

View file

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Install local HTTPS · Furtka</title> <title>Install local HTTPS · Furtka</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">

View file

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Furtka</title> <title>Furtka</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">

View file

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Settings · Furtka</title> <title>Settings · Furtka</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">

View file

@ -73,13 +73,17 @@ body {
align-items: center; align-items: center;
gap: 0.55rem; gap: 0.55rem;
} }
/* Furtka mark (M), drawn as a mask so it takes the accent colour in both
themes. Generated geometry: website/tools/brand-assets.py. */
.brand::before { .brand::before {
content: ""; content: "";
width: 0.7rem; width: 1.35rem;
height: 0.7rem; height: 1.35rem;
background: var(--accent); /* Brand red, not the UI's green accent — the logo keeps its colour.
border-radius: 2px; This stylesheet is dark-first; the light theme gets the deeper red. */
transform: rotate(45deg); background: #ff6b56;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cpath fill-rule='evenodd' d='M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cpath fill-rule='evenodd' d='M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z'/%3E%3C/svg%3E") center / contain no-repeat;
} }
.nav-links { display: flex; gap: 0.25rem; } .nav-links { display: flex; gap: 0.25rem; }
.nav-links a { .nav-links a {
@ -490,3 +494,8 @@ details.log-details[open] > summary { color: var(--fg); }
color: var(--accent); color: var(--accent);
} }
.app-icon svg { width: 36px; height: 36px; } .app-icon svg { width: 36px; height: 36px; }
.login-brand { margin: 0 0 1.5rem; font-size: 1.2rem; }
@media (prefers-color-scheme: light) {
.brand::before { background: #c03a28; }
}

19
docs/brand/README.md Normal file
View file

@ -0,0 +1,19 @@
# Furtka brand files
The Furtka logo is **Mark M** — a lowercase *f* cut out of an arched gate. It won the
public logo vote on furtka.org (closed 2026-09-28: M 29, R 13 of 42 votes).
| File | Use |
|---|---|
| `furtka-mark.svg` | The mark, 32 px and up |
| `furtka-mark-small.svg` | Simplified drawing for 16–24 px (favicons, tab icons) |
| `furtka-lockup.svg` | Mark + wordmark, accent colour on light grounds |
| `furtka-lockup-white.svg` | Mark + wordmark in white, for dark or accent grounds |
| `console-banner.txt` | Text-mode art for the console welcome screens |
Colours: accent `#c03a28` (dark UI: `#ff6b56`), ink `#0d0d0f`.
Everything here and the copies in `website/static/`, `webinstaller/static/`,
`assets/www/` and `iso/overlay/splash.png` are generated by
`website/tools/brand-assets.py` from the geometry in `website/tools/logo-assets.py`.
Change the geometry there and re-run the script — don't edit the outputs by hand.

View file

@ -0,0 +1,10 @@
▄▄▄▄▄▄▄▄
▄██████████▄
█████▀▀▀▀▀▀███
████▀ ████
████ █████████
████ ████
████ ▄▄▄▄▄████
████ █████████
████ █████████
▀▀▀▀ ▀▀▀▀▀▀▀▀▀

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -4 232.44 64.00" role="img" aria-label="Furtka"><g transform="scale(1.0000) translate(-6 -2)"><path fill="#ffffff" fill-rule="evenodd" d="M6 60V28a26 26 0 0 1 52 0v32Z M20.0 60V26.5A10.5 10.5 0 0 1 30.5 16H45.0v8.6H32.5a3.5 3.5 0 0 0-3.5 3.5V32h16v8.6H29.0V60Z"/></g><g fill="#ffffff"><path transform="translate(68.24 58.00) scale(0.05354 -0.05354)" d="M115 0V494H30V600H115V637Q115 687 132.5 724.0Q150 761 192.0 781.5Q234 802 305 802Q333 802 358 799V695Q344 698 324 698Q287 698 273.5 682.0Q260 666 260 632V600H358V494H260V0Z"/><path transform="translate(87.21 58.00) scale(0.05354 -0.05354)" d="M271 -15Q169 -15 119.0 48.5Q69 112 69 218V600H218V244Q218 179 241.5 143.0Q265 107 316 107Q379 107 413.0 156.0Q447 205 447 276V600H596V0H462V90H438Q423 44 382.5 14.5Q342 -15 271 -15Z"/><path transform="translate(121.45 58.00) scale(0.05354 -0.05354)" d="M76 0V600H211V492H235Q241 521 257.0 547.0Q273 573 307.0 589.5Q341 606 399 606H428V471H387Q301 471 263.0 427.0Q225 383 225 294V0Z"/><path transform="translate(144.17 58.00) scale(0.05354 -0.05354)" d="M162 0Q139 27 125.5 65.0Q112 103 112 179V494H30V600H114V750H259V600H358V494H260V196Q260 116 278.0 74.5Q296 33 321 6V0Z"/><path transform="translate(163.14 58.00) scale(0.05354 -0.05354)" d="M78 0V800H226V366H274L449 600H608V594L397 320L632 6V0H457L274 254H226V0Z"/><path transform="translate(195.88 58.00) scale(0.05354 -0.05354)" d="M280 -13Q198 -13 144.0 27.5Q90 68 63.5 138.5Q37 209 37 301Q37 395 64.0 465.0Q91 535 145.0 574.0Q199 613 280 613Q344 613 384.5 587.5Q425 562 445 522H469V600H606V0H469V78H445Q425 41 385.0 14.0Q345 -13 280 -13ZM317 95Q381 95 419.5 151.0Q458 207 458 300Q458 394 419.5 449.5Q381 505 317 505Q257 505 223.5 453.0Q190 401 190 300Q190 199 223.5 147.0Q257 95 317 95Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -4 232.44 64.00" role="img" aria-label="Furtka"><g transform="scale(1.0000) translate(-6 -2)"><path fill="#c03a28" fill-rule="evenodd" d="M6 60V28a26 26 0 0 1 52 0v32Z M20.0 60V26.5A10.5 10.5 0 0 1 30.5 16H45.0v8.6H32.5a3.5 3.5 0 0 0-3.5 3.5V32h16v8.6H29.0V60Z"/></g><g fill="#c03a28"><path transform="translate(68.24 58.00) scale(0.05354 -0.05354)" d="M115 0V494H30V600H115V637Q115 687 132.5 724.0Q150 761 192.0 781.5Q234 802 305 802Q333 802 358 799V695Q344 698 324 698Q287 698 273.5 682.0Q260 666 260 632V600H358V494H260V0Z"/><path transform="translate(87.21 58.00) scale(0.05354 -0.05354)" d="M271 -15Q169 -15 119.0 48.5Q69 112 69 218V600H218V244Q218 179 241.5 143.0Q265 107 316 107Q379 107 413.0 156.0Q447 205 447 276V600H596V0H462V90H438Q423 44 382.5 14.5Q342 -15 271 -15Z"/><path transform="translate(121.45 58.00) scale(0.05354 -0.05354)" d="M76 0V600H211V492H235Q241 521 257.0 547.0Q273 573 307.0 589.5Q341 606 399 606H428V471H387Q301 471 263.0 427.0Q225 383 225 294V0Z"/><path transform="translate(144.17 58.00) scale(0.05354 -0.05354)" d="M162 0Q139 27 125.5 65.0Q112 103 112 179V494H30V600H114V750H259V600H358V494H260V196Q260 116 278.0 74.5Q296 33 321 6V0Z"/><path transform="translate(163.14 58.00) scale(0.05354 -0.05354)" d="M78 0V800H226V366H274L449 600H608V594L397 320L632 6V0H457L274 254H226V0Z"/><path transform="translate(195.88 58.00) scale(0.05354 -0.05354)" d="M280 -13Q198 -13 144.0 27.5Q90 68 63.5 138.5Q37 209 37 301Q37 395 64.0 465.0Q91 535 145.0 574.0Q199 613 280 613Q344 613 384.5 587.5Q425 562 445 522H469V600H606V0H469V78H445Q425 41 385.0 14.0Q345 -13 280 -13ZM317 95Q381 95 419.5 151.0Q458 207 458 300Q458 394 419.5 449.5Q381 505 317 505Q257 505 223.5 453.0Q190 401 190 300Q190 199 223.5 147.0Q257 95 317 95Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#c03a28" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/></svg>

After

Width:  |  Height:  |  Size: 224 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#c03a28" fill-rule="evenodd" d="M6 60V28a26 26 0 0 1 52 0v32Z M20.0 60V26.5A10.5 10.5 0 0 1 30.5 16H45.0v8.6H32.5a3.5 3.5 0 0 0-3.5 3.5V32h16v8.6H29.0V60Z"/></svg>

After

Width:  |  Height:  |  Size: 236 B

View file

@ -73,6 +73,7 @@ _HTML = """<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Furtka Apps</title> <title>Furtka Apps</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
@ -557,12 +558,14 @@ _HTML_LOGIN = """<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Furtka · {{ TITLE }}</title> <title>Furtka · {{ TITLE }}</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
</head> </head>
<body> <body>
<main class="wrap login-wrap"> <main class="wrap login-wrap">
<p class="brand login-brand">Furtka</p>
<h1>{{ HEADING }}</h1> <h1>{{ HEADING }}</h1>
<p class="lede">{{ LEDE }}</p> <p class="lede">{{ LEDE }}</p>
<form id="login-form" onsubmit="return doLogin(event)"> <form id="login-form" onsubmit="return doLogin(event)">

View file

@ -55,6 +55,9 @@ cp -a "$SCRIPT_DIR/overlay/airootfs/." "$PROFILE_WORK/airootfs/"
rm -rf "$PROFILE_WORK/airootfs/etc/systemd/system/getty@tty1.service.d" rm -rf "$PROFILE_WORK/airootfs/etc/systemd/system/getty@tty1.service.d"
echo "==> Rebranding boot menu (GRUB + syslinux + systemd-boot)" echo "==> Rebranding boot menu (GRUB + syslinux + systemd-boot)"
# BIOS boot menu background: Furtka lockup instead of the Arch logo
# (generated by website/tools/brand-assets.py, 640x480 as syslinux wants).
cp "$SCRIPT_DIR/overlay/splash.png" "$PROFILE_WORK/syslinux/splash.png"
# releng ships menu entries labelled "Arch Linux install medium" across three # releng ships menu entries labelled "Arch Linux install medium" across three
# bootloader configs (BIOS syslinux, GRUB, systemd-boot for UEFI). Rewrite to # bootloader configs (BIOS syslinux, GRUB, systemd-boot for UEFI). Rewrite to
# our brand. Done with sed (not a static overlay) so upstream archiso file # our brand. Done with sed (not a static overlay) so upstream archiso file

View file

@ -16,6 +16,17 @@ issue=/etc/issue
mapfile -t ips < <(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1) mapfile -t ips < <(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
new=$( new=$(
# Furtka mark (docs/brand/console-banner.txt, from brand-assets.py)
echo ' ▄▄▄▄▄▄▄▄'
echo ' ▄██████████▄'
echo ' █████▀▀▀▀▀▀███'
echo ' ████▀ ████'
echo ' ████ █████████'
echo ' ████ ████'
echo ' ████ ▄▄▄▄▄████'
echo ' ████ █████████'
echo ' ████ █████████'
echo ' ▀▀▀▀ ▀▀▀▀▀▀▀▀▀'
echo echo
echo " Open Furtka in a browser on another device on your network:" echo " Open Furtka in a browser on another device on your network:"
echo echo

BIN
iso/overlay/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -113,6 +113,15 @@ if [ -f "$ISO" ]; then
if ! upload_asset "$ISO"; then if ! upload_asset "$ISO"; then
echo "warning: ISO upload failed — release published without ISO asset" >&2 echo "warning: ISO upload failed — release published without ISO asset" >&2
fi fi
# Checksum sidecar for the download page on furtka.org (and anyone
# verifying by hand). Uploaded even when the ISO upload reported a
# failure — a 504 there often still lands the file. Written from inside
# dist/ so the line carries the bare filename `sha256sum -c` expects.
ISO_SHA="$ISO.sha256"
(cd "$DIST_DIR" && sha256sum "$(basename "$ISO")" > "$(basename "$ISO_SHA")")
if ! upload_asset "$ISO_SHA"; then
echo "warning: ISO checksum upload failed" >&2
fi
fi fi
echo "Release $VERSION published: https://$HOST/$REPO/releases/tag/$VERSION" echo "Release $VERSION published: https://$HOST/$REPO/releases/tag/$VERSION"

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><style>svg{color:#c03a28}@media (prefers-color-scheme:dark){svg{color:#ff6b56}}</style><path fill="currentColor" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/></svg>

After

Width:  |  Height:  |  Size: 316 B

View file

@ -4,6 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Furtka Installer{% endblock %}</title> <title>{% block title %}Furtka Installer{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{% block head_extra %}{% endblock %} {% block head_extra %}{% endblock %}
</head> </head>
@ -11,12 +12,8 @@
<header class="site-header"> <header class="site-header">
<div class="container"> <div class="container">
<a href="{{ url_for('install_step_1') }}" class="site-title" aria-label="Furtka Installer — restart"> <a href="{{ url_for('install_step_1') }}" class="site-title" aria-label="Furtka Installer — restart">
<svg class="gate-mark" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg class="gate-mark" width="24" height="24" viewBox="0 0 64 64" aria-hidden="true">
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path fill="currentColor" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/>
<path d="M4 20 V12 A9 9 0 0 1 20 12 V20"/>
<line x1="12" y1="5" x2="12" y2="20"/>
<line x1="3" y1="20" x2="21" y2="20"/>
<line x1="15" y1="12" x2="15" y2="14.5"/>
</svg> </svg>
<span class="wordmark">Furtka<span class="sep">·</span>Installer</span> <span class="wordmark">Furtka<span class="sep">·</span>Installer</span>
</a> </a>

View file

@ -356,6 +356,214 @@ main.container {
.cta--primary { animation: none; } .cta--primary { animation: none; }
} }
.cta-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.9rem 1.1rem;
}
.cta--ghost {
color: var(--fg);
border: 1px solid var(--border);
background: var(--card-bg);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.cta--ghost:hover {
border-color: var(--accent);
color: var(--accent);
transform: translateY(-2px);
}
/* ── How it works (home) ─────────────────────────────────── */
.how-steps {
list-style: none;
counter-reset: how;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: 1rem;
}
.how-step {
counter-increment: how;
position: relative;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 1rem;
padding: 1.4rem 1.5rem 1.5rem;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.how-step::before {
content: counter(how, decimal-leading-zero);
display: block;
font-family: var(--font-mono);
font-weight: 600;
font-size: 2.4rem;
line-height: 1;
letter-spacing: -0.04em;
margin-bottom: 0.9rem;
background-image: linear-gradient(135deg, var(--accent), var(--fg-muted));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.how-step h3 { margin: 0 0 0.35rem; font-size: 1.15rem; letter-spacing: -0.01em; }
.how-step p { margin: 0; color: var(--fg-muted); font-size: 0.98rem; line-height: 1.5; }
.how-step strong { color: var(--fg); font-weight: 600; }
/* ── Download band (home, before the closer) ─────────────── */
.dl-band {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1.25rem 2rem;
margin-block: 4.5rem 0;
padding: 2rem;
border-radius: 1.25rem;
border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
background:
radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 60%),
var(--card-bg);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.dl-band h2 { margin: 0 0 0.3rem; font-size: 1.5rem; letter-spacing: -0.02em; }
.dl-band p { margin: 0; color: var(--fg-muted); max-width: 26rem; }
/* ── Download page ───────────────────────────────────────── */
.dl-hero { padding-block: 1rem 1rem; }
.dl-hero h1 {
font-size: clamp(2.4rem, 7vw, 4rem);
line-height: 1;
letter-spacing: -0.035em;
margin: 0 0 1rem;
}
.dl-hero .lede {
font-size: clamp(1.05rem, 1.6vw, 1.2rem);
color: var(--fg-muted);
max-width: 36rem;
margin: 0 0 2rem;
}
.dl-card {
padding: 1.75rem;
border-radius: 1.25rem;
border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
background:
radial-gradient(120% 140% at 100% 0%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 60%),
var(--card-bg);
}
.dl-card .cta svg { flex: none; }
.dl-meta {
display: flex;
flex-wrap: wrap;
gap: 0.75rem 2rem;
margin: 1.5rem 0 0;
}
.dl-meta div { min-width: 0; }
.dl-meta dt {
font-size: 0.7rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--fg-muted);
}
.dl-meta dd { margin: 0.15rem 0 0; overflow-wrap: anywhere; }
.mono { font-family: var(--font-mono); font-size: 0.92em; }
.dl-host { margin: 1.25rem 0 0; font-size: 0.85rem; color: var(--fg-muted); }
.dl-alpha {
margin: 1.5rem 0 0;
padding: 0.9rem 1.1rem;
border-left: 3px solid var(--accent);
background: var(--bg-subtle);
border-radius: 0 0.6rem 0.6rem 0;
font-size: 0.95rem;
max-width: 40rem;
}
.dl-section { margin-top: 3.5rem; }
.dl-needs { margin: 0; padding-left: 1.2rem; max-width: 40rem; }
.dl-needs li { margin-bottom: 0.55rem; }
.dl-needs li::marker { color: var(--accent); }
.dl-steps {
list-style: none;
counter-reset: dl;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 1rem;
}
.step-card {
counter-increment: dl;
display: grid;
grid-template-columns: 2.6rem 1fr;
gap: 0 1rem;
padding: 1.3rem 1.4rem;
border: 1px solid var(--card-border);
border-radius: 1rem;
background: var(--card-bg);
}
.step-card::before {
content: counter(dl);
grid-row: span 2;
display: grid;
place-items: center;
width: 2.6rem;
height: 2.6rem;
border-radius: 999px;
background: linear-gradient(135deg, var(--accent), var(--accent-hover));
color: #fff;
font-weight: 700;
}
.step-card h2 { margin: 0.35rem 0 0.4rem; font-size: 1.15rem; letter-spacing: -0.01em; }
.step-body { min-width: 0; color: var(--fg-muted); }
.step-body p { margin: 0 0 0.7rem; }
.step-body p:last-child { margin: 0; }
.step-body strong { color: var(--fg); font-weight: 600; }
.step-body a, .dl-older a, .dl-alpha a { color: var(--accent); }
kbd {
font-family: var(--font-mono);
font-size: 0.85em;
padding: 0.05rem 0.4rem;
border: 1px solid var(--border);
border-bottom-width: 2px;
border-radius: 0.3rem;
background: var(--bg);
color: var(--fg);
}
.download pre {
margin: 0.4rem 0 0;
padding: 0.85rem 1rem;
border-radius: 0.6rem;
background: var(--bg-subtle);
border: 1px solid var(--border);
overflow-x: auto;
font-size: 0.85rem;
line-height: 1.55;
color: var(--fg);
}
.download code { font-family: var(--font-mono); }
.download :not(pre) > code {
padding: 0.05rem 0.3rem;
border-radius: 0.3rem;
background: var(--bg-subtle);
font-size: 0.9em;
}
.dl-hash code { overflow-wrap: anywhere; white-space: pre-wrap; }
.dl-cmds .c { color: var(--fg-muted); }
.dl-muted { color: var(--fg-muted); }
.dl-older { margin-top: 3rem; }
@media (max-width: 30rem) {
.dl-card, .dl-band { padding: 1.35rem; }
.step-card { grid-template-columns: 1fr; }
.step-card::before { grid-row: auto; margin-bottom: 0.6rem; }
}
/* ── Intro paragraph (home, between hero and feature grids) ─ */ /* ── Intro paragraph (home, between hero and feature grids) ─ */
.intro { .intro {
@ -485,6 +693,16 @@ main.container {
.poll-bar { height: 0.45rem; border-radius: 999px; background: var(--bg-subtle); overflow: hidden; display: none; } .poll-bar { height: 0.45rem; border-radius: 999px; background: var(--bg-subtle); overflow: hidden; display: none; }
.poll-bar i { display: block; height: 100%; width: 0; background: var(--accent); transition: width 500ms ease; } .poll-bar i { display: block; height: 100%; width: 0; background: var(--accent); transition: width 500ms ease; }
.poll--voted .poll-bar { display: block; } .poll--voted .poll-bar { display: block; }
.poll-bar--final { display: block; }
.poll-result {
margin: 1.5rem 0 0;
padding: 1rem 1.2rem;
border-left: 3px solid var(--accent);
background: var(--bg-subtle);
border-radius: 0 0.6rem 0.6rem 0;
max-width: 40rem;
}
.poll-result strong { color: var(--accent); }
.poll-count { margin: 0; min-height: 1.4em; font-size: 0.85rem; color: var(--fg-muted); font-variant-numeric: tabular-nums; } .poll-count { margin: 0; min-height: 1.4em; font-size: 0.85rem; color: var(--fg-muted); font-variant-numeric: tabular-nums; }
.poll-status { margin: 1.25rem 0 0; min-height: 1.5em; color: var(--fg-muted); } .poll-status { margin: 1.25rem 0 0; min-height: 1.5em; color: var(--fg-muted); }
.poll-note { margin-top: 1rem; max-width: var(--measure); font-size: 0.85rem; color: var(--fg-muted); } .poll-note { margin-top: 1rem; max-width: var(--measure); font-size: 0.85rem; color: var(--fg-muted); }

View file

@ -1,9 +1,11 @@
--- ---
title: "Furtka" title: "Furtka"
description: "Offenes Heimserver-Betriebssystem — einfach genug für alle." description: "Offenes Heimserver-Betriebssystem — einfach genug für alle."
status: "<span class=\"mono\">26.20-alpha</span> — in Arbeit" status_text: "in Arbeit"
cta_download: "ISO herunterladen"
cta_source: "Quellcode"
poll_link: "/de/logo/" poll_link: "/de/logo/"
poll_text: "Wir suchen ein Logo — hilf uns beim Aussuchen" poll_text: "Abgestimmt — das ist unser neues Logo"
# features_today / features_next müssen index-parallel zu content/_index.md bleiben. # features_today / features_next müssen index-parallel zu content/_index.md bleiben.
intro: | intro: |
**Furtka** ist ein offenes Heimserver-Betriebssystem. **Furtka** ist ein offenes Heimserver-Betriebssystem.
@ -12,6 +14,16 @@ intro: |
Apps, eigenem Namen im Netz, eigenen Daten. Apps, eigenem Namen im Netz, eigenen Daten.
Das Ziel ist einfach: **dein Vater soll das einrichten können.** Das Ziel ist einfach: **dein Vater soll das einrichten können.**
steps_label: "So funktioniert's"
steps:
- title: "Herunterladen"
body: "Installer holen und auf einen USB-Stick schreiben."
- title: "Installieren"
body: "Computer vom Stick starten und dem Assistenten folgen — im Browser auf Handy oder Laptop."
- title: "Loslegen"
body: "**http://furtka.local** öffnen und Apps mit einem Klick installieren."
band_title: "Lust, es auszuprobieren?"
band_text: "Kostenlos und quelloffen (AGPL-3.0). Läuft auf 64-Bit-PCs, auch auf alten."
features_today_label: "Was heute schon geht" features_today_label: "Was heute schon geht"
features_today: features_today:
- "Vom USB-Stick booten und Furtka auf die Festplatte einrichten" - "Vom USB-Stick booten und Furtka auf die Festplatte einrichten"

View file

@ -1,9 +1,11 @@
--- ---
title: "Furtka" title: "Furtka"
description: "Open-source home server OS — simple enough for everyone." description: "Open-source home server OS — simple enough for everyone."
status: "<span class=\"mono\">26.20-alpha</span> — work in progress" status_text: "work in progress"
cta_download: "Download ISO"
cta_source: "Source code"
poll_link: "/logo/" poll_link: "/logo/"
poll_text: "We're picking a logo — help us choose" poll_text: "The vote is in — meet our new logo"
# Keep features_today / features_next index-aligned with content/_index.de.md. # Keep features_today / features_next index-aligned with content/_index.de.md.
intro: | intro: |
**Furtka** is an open-source home server OS. **Furtka** is an open-source home server OS.
@ -12,6 +14,16 @@ intro: |
your own name on the network, your own data. your own name on the network, your own data.
The goal is simple: **your dad should be able to set this up.** The goal is simple: **your dad should be able to set this up.**
steps_label: "How it works"
steps:
- title: "Download"
body: "Get the installer and write it to a USB stick."
- title: "Install"
body: "Start the computer from the stick and follow the wizard — in the browser on your phone or laptop."
- title: "Use"
body: "Open **http://furtka.local** and install apps with one click."
band_title: "Ready to try it?"
band_text: "Free and open source (AGPL-3.0). Runs on 64-bit PCs, old ones included."
features_today_label: "What works today" features_today_label: "What works today"
features_today: features_today:
- "Boot from USB stick and install Furtka onto the hard drive" - "Boot from USB stick and install Furtka onto the hard drive"

View file

@ -0,0 +1,40 @@
---
title: "Furtka herunterladen"
description: "Der Furtka-Installer als ISO-Datei: auf einen USB-Stick schreiben, den Computer davon starten und den Heimserver im Browser einrichten."
translationKey: "download"
url: /de/download/
type: download
button: "ISO herunterladen"
released: "Veröffentlicht"
size_label: "Größe"
alpha: "Furtka ist **Alpha-Software**: Dinge ändern sich und manches geht kaputt. Installiere es auf einem Computer, dessen Festplatte du nicht brauchst, und sichere alles Wichtige."
host_note: "Die Datei kommt von unserem Forgejo-Server (forgejo.sourcegate.online)."
need_label: "Was du brauchst"
needs:
- "Einen **64-Bit-PC** (x86-64) — ein alter Laptop oder Desktop reicht. UEFI- und ältere BIOS-Rechner funktionieren beide."
- "Eine Festplatte mit **mindestens 16 GB** für Furtka. **Sie wird komplett gelöscht.**"
- "Einen **USB-Stick** mit mindestens 2 GB (wird ebenfalls gelöscht)."
- "Ein **Netzwerkkabel** mit Internetzugang — der Installer lädt Pakete herunter, und WLAN wird nicht automatisch eingerichtet."
- "Ein **zweites Gerät mit Browser** (Handy oder Laptop) im selben Netzwerk."
steps_label: "So installierst du Furtka"
steps:
- title: "ISO auf einen USB-Stick schreiben"
body: |
Nimm [balenaEtcher](https://etcher.balena.io/) (Windows, macOS, Linux) oder [Rufus](https://rufus.ie/) unter Windows — wenn Rufus fragt, wähle den **DD-Abbild-Modus**. Unter Linux geht auch `dd` (ersetze `/dev/sdX` durch deinen Stick — lieber zweimal prüfen):
```
sudo dd if={iso} of=/dev/sdX bs=4M status=progress oflag=sync
```
- title: "Computer vom Stick starten"
body: "Stick und Netzwerkkabel einstecken, Computer einschalten und die Taste fürs Bootmenü drücken (oft <kbd>F11</kbd>, <kbd>F12</kbd> oder <kbd>Esc</kbd> — sie wird kurz am Bildschirm angezeigt). Den USB-Stick auswählen. Startet der Computer nicht davon, schalte **Secure Boot** in den Firmware-Einstellungen aus."
- title: "Installer von einem anderen Gerät öffnen"
body: "Nach kurzer Zeit zeigt der Bildschirm eine Adresse an — meist **http://proksi.local:5000**, dazu die IP-Adresse als Ausweichweg. Öffne sie im Browser auf Handy oder Laptop."
- title: "Durch den Assistenten klicken"
body: "Drei Seiten: ein Name für deinen Server mit Benutzername und Passwort, die Festplatte und eine letzte Kontrolle. Danach lädt der Installer alles herunter und richtet es ein."
- title: "Stick raus, neu starten, fertig"
body: "Wenn der Assistent es sagt, den USB-Stick abziehen und neu starten. Dein Server ist dann unter **http://&lt;name&gt;.local** erreichbar — zum Beispiel http://furtka.local."
verify_label: "Download prüfen (optional)"
verify_intro: "Vergleiche die Prüfsumme der heruntergeladenen Datei mit dieser:"
verify_missing: "Für die ISO dieses Releases ist noch keine Prüfsumme veröffentlicht — künftige Releases bringen eine mit."
older: "Alle Releases und Änderungen"
---

View file

@ -0,0 +1,40 @@
---
title: "Download Furtka"
description: "The Furtka installer as an ISO file: write it to a USB stick, start your computer from it, and set up your home server in the browser."
translationKey: "download"
url: /download/
type: download
button: "Download ISO"
released: "Released"
size_label: "Size"
alpha: "Furtka is **alpha software**: things change and some will break. Install it on a computer whose disk you don't need, and keep a backup of anything important."
host_note: "The file is served from our Forgejo server (forgejo.sourcegate.online)."
need_label: "What you need"
needs:
- "A **64-bit PC** (x86-64) — an old laptop or desktop is fine. Both UEFI and older BIOS computers work."
- "A disk of **at least 16 GB** for Furtka. **It will be erased completely.**"
- "A **USB stick** with at least 2 GB (also erased)."
- "A **network cable** with internet access — the installer downloads packages, and Wi-Fi isn't set up automatically."
- "A **second device with a browser** (phone or laptop) on the same network."
steps_label: "How to install"
steps:
- title: "Write the ISO to a USB stick"
body: |
Use [balenaEtcher](https://etcher.balena.io/) (Windows, macOS, Linux) or [Rufus](https://rufus.ie/) on Windows — if Rufus asks, pick **DD image mode**. On Linux you can also use `dd` (replace `/dev/sdX` with your stick — double-check it):
```
sudo dd if={iso} of=/dev/sdX bs=4M status=progress oflag=sync
```
- title: "Start the computer from the stick"
body: "Plug in the stick and the network cable, switch the computer on and press the one-time boot menu key (often <kbd>F11</kbd>, <kbd>F12</kbd> or <kbd>Esc</kbd> — it flashes briefly on screen). Pick the USB stick. If the computer refuses to start from it, switch off **Secure Boot** in the firmware settings."
- title: "Open the installer from another device"
body: "After a short while the screen shows an address — usually **http://proksi.local:5000**, plus the IP address as a fallback. Open it in a browser on your phone or laptop."
- title: "Click through the wizard"
body: "Three pages: a name for your server with your username and password, the disk to install on, and a last check. Then the installer downloads and sets everything up."
- title: "Pull the stick, restart, done"
body: "When the wizard says so, remove the USB stick and restart. Your server comes up at **http://&lt;name&gt;.local** — for example http://furtka.local."
verify_label: "Check the download (optional)"
verify_intro: "Compare the checksum of the file you downloaded with this one:"
verify_missing: "This release has no published checksum for the ISO yet — future releases include one."
older: "All releases and changelogs"
---

View file

@ -1,6 +1,11 @@
--- ---
title: "Welches Logo?" title: "Unser neues Logo"
description: "Zwei Vorschläge für das Furtka-Zeichen. Wähl das, das dir besser gefällt — ein Klick genügt." description: "Zwei Vorschläge für das Furtka-Zeichen, und ihr habt gewählt. So ist die Abstimmung ausgegangen — und so sähe jedes Zeichen überall aus, wo es funktionieren müsste."
closed: true
result: "Die Abstimmung ist beendet — **M hat mit 29 von 42 Stimmen gewonnen** (R: 13). Danke an alle, die abgestimmt haben! M ist jetzt das Furtka-Logo."
final:
m: 29
r: 13
translationKey: "logo" translationKey: "logo"
layout: single layout: single
type: poll type: poll
@ -17,7 +22,7 @@ r:
title: "R — Fenster, Rad und F" title: "R — Fenster, Rad und F"
text: "Ein Bogenfenster mit einem Speichenrad dahinter; in der oberen rechten Scheibe wird die Speiche zum F." text: "Ein Bogenfenster mit einem Speichenrad dahinter; in der oberen rechten Scheibe wird die Speiche zum F."
show: show:
intro: "Noch unentschieden? Hier ist jedes Zeichen überall dort, wo es funktionieren muss." intro: "Beide Zeichen überall dort, wo sie hätten funktionieren müssen."
sizes: "Groß und klein" sizes: "Groß und klein"
sizes_text: "Vom Poster bis zum 16-Pixel-Symbol im Browser-Tab. Unter 32 Pixeln wechseln beide auf eine vereinfachte Zeichnung — M verliert nichts, R behält Fenster, Kreuz und Nabe." sizes_text: "Vom Poster bis zum 16-Pixel-Symbol im Browser-Tab. Unter 32 Pixeln wechseln beide auf eine vereinfachte Zeichnung — M verliert nichts, R behält Fenster, Kreuz und Nabe."
name: "Mit dem Namen" name: "Mit dem Namen"
@ -48,6 +53,6 @@ labels:
*Furtka* ist polnisch für die kleine Gartenpforte — die Tür im Zaun, durch *Furtka* ist polnisch für die kleine Gartenpforte — die Tür im Zaun, durch
die man hineinkommt, ohne das große Tor aufzumachen. Beide Zeichen spielen die man hineinkommt, ohne das große Tor aufzumachen. Beide Zeichen spielen
damit: das eine zeichnet den Buchstaben ins Tor, das andere schaut durchs damit: das eine zeichnet den Buchstaben ins Tor, das andere schaut durchs
Tor auf ein Rad. Stimm direkt hier ab — oder scroll erst weiter und sieh Tor auf ein Rad. Unten stehen beide mit dem Endergebnis — und jedes Zeichen
beide Zeichen überall, wo sie funktionieren müssen, vom Poster bis zum überall dort, wo es hätte funktionieren müssen, vom Poster bis zum winzigen
winzigen Symbol im Browser-Tab. Symbol im Browser-Tab.

View file

@ -1,6 +1,11 @@
--- ---
title: "Which logo?" title: "Our new logo"
description: "Two proposals for the Furtka mark. Pick the one you like better — it takes one click." description: "Two proposals for the Furtka mark, and you picked one. Here is how the vote ended — and each mark everywhere it would have had to work."
closed: true
result: "The vote is closed — **M won with 29 of 42 votes** (R: 13). Thank you to everyone who voted! M is now the Furtka logo."
final:
m: 29
r: 13
translationKey: "logo" translationKey: "logo"
url: /logo/ url: /logo/
layout: single layout: single
@ -18,7 +23,7 @@ r:
title: "R — window, wheel and F" title: "R — window, wheel and F"
text: "An arched window with a wheel of spokes behind it; in the top-right pane the spoke becomes an F." text: "An arched window with a wheel of spokes behind it; in the top-right pane the spoke becomes an F."
show: show:
intro: "Not sure yet? Here is each mark everywhere it would have to work." intro: "Both marks everywhere they would have had to work."
sizes: "Big and small" sizes: "Big and small"
sizes_text: "From a poster to the 16-pixel icon in a browser tab. Below 32 pixels both marks switch to a simplified drawing — M drops nothing, R keeps the window, the cross and the hub." sizes_text: "From a poster to the 16-pixel icon in a browser tab. Below 32 pixels both marks switch to a simplified drawing — M drops nothing, R keeps the window, the cross and the hub."
name: "With the name" name: "With the name"
@ -49,5 +54,5 @@ labels:
*Furtka* is Polish for a small garden gate — the little door in the fence *Furtka* is Polish for a small garden gate — the little door in the fence
that lets you in without opening the whole thing. Both marks play with that: that lets you in without opening the whole thing. Both marks play with that:
one draws the letter into the gate, the other looks through the gate at a one draws the letter into the gate, the other looks through the gate at a
wheel. Vote right here — or scroll on first and see both marks everywhere wheel. Below are both, with the final count — and each mark everywhere it
they would have to work, from a poster to the tiny icon in a browser tab. would have had to work, from a poster to the tiny icon in a browser tab.

View file

@ -8,6 +8,11 @@ enableRobotsTXT = true
description = "Open-source home server OS — simple enough for everyone." description = "Open-source home server OS — simple enough for everyone."
version = "26.20-alpha" version = "26.20-alpha"
contactEmail = "hallo@furtka.org" contactEmail = "hallo@furtka.org"
# Forgejo repo the download page reads its latest release (ISO asset,
# size, date, checksum) from at build time. `version` above is only the
# fallback when that lookup fails.
forgejoURL = "https://forgejo.sourcegate.online"
repoPath = "daniel/furtka"
[markup.goldmark.renderer] [markup.goldmark.renderer]
unsafe = true unsafe = true
@ -32,5 +37,12 @@ enableRobotsTXT = true
[build] [build]
writeStats = true writeStats = true
# The release lookup must be fresh on every build — Hugo's default keeps
# remote resources cached forever, which would pin the download page to
# whatever release existed on the first build.
[caches]
[caches.getresource]
maxAge = 0
[minify] [minify]
minifyOutput = true minifyOutput = true

View file

@ -0,0 +1,63 @@
{{ define "main" }}
{{- $r := partialCached "release.html" . "release" -}}
{{- $file := path.Base $r.url -}}
<article class="download">
<header class="dl-hero">
<p class="status-chip"><span class="mono">{{ $r.version }}</span></p>
<h1>{{ .Title }}</h1>
<p class="lede">{{ .Description }}</p>
<div class="dl-card">
<a class="cta cta--primary" href="{{ $r.url }}" download>
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/></svg>
{{ .Params.button }}
</a>
<dl class="dl-meta">
<div><dt>ISO</dt><dd class="mono">{{ $file }}</dd></div>
{{ with $r.size }}<div><dt>{{ $.Params.size_label }}</dt><dd>{{ lang.FormatNumber 1 (div (float .) 1e9) }} GB</dd></div>{{ end }}
{{ with $r.date }}<div><dt>{{ $.Params.released }}</dt><dd>{{ time.Format ":date_long" . }}</dd></div>{{ end }}
</dl>
<p class="dl-host">{{ .Params.host_note }}</p>
</div>
<p class="dl-alpha">{{ .Params.alpha | markdownify }}</p>
</header>
<section class="dl-section">
<p class="section-eyebrow">{{ .Params.need_label }}</p>
<ul class="dl-needs">
{{ range .Params.needs }}<li>{{ . | markdownify }}</li>{{ end }}
</ul>
</section>
<section class="dl-section">
<p class="section-eyebrow">{{ .Params.steps_label }}</p>
<ol class="dl-steps">
{{ range .Params.steps }}
<li class="step-card">
<h2>{{ .title }}</h2>
<div class="step-body">{{ replace .body "{iso}" $file | markdownify }}</div>
</li>
{{ end }}
</ol>
</section>
<section class="dl-section">
<p class="section-eyebrow">{{ .Params.verify_label }}</p>
{{ with $r.sha256 }}
<p>{{ $.Params.verify_intro }}</p>
<pre class="dl-hash"><code>{{ . }}</code></pre>
<pre class="dl-cmds"><code><span class="c"># Linux</span>
sha256sum {{ $file }}
<span class="c"># macOS</span>
shasum -a 256 {{ $file }}
<span class="c"># Windows (PowerShell)</span>
Get-FileHash {{ $file }}</code></pre>
{{ else }}
<p class="dl-muted">{{ $.Params.verify_missing }}</p>
{{ end }}
</section>
<p class="dl-older"><a href="{{ $r.all }}">{{ .Params.older }} →</a></p>
</article>
{{ end }}

View file

@ -1,19 +1,20 @@
{{ define "main" }} {{ define "main" }}
{{- $r := partialCached "release.html" . "release" -}}
{{- $dl := "download/" | relLangURL -}}
<article class="home"> <article class="home">
<header class="hero"> <header class="hero">
{{ with .Params.status }} <p class="status-chip reveal"><span class="mono">{{ $r.version }}</span> — {{ .Params.status_text }}</p>
<p class="status-chip reveal">{{ . | safeHTML }}</p>
{{ end }}
<h1 class="reveal">{{ .Title }}</h1> <h1 class="reveal">{{ .Title }}</h1>
{{ with site.Params.description }}<p class="lede reveal">{{ . }}</p>{{ end }} {{ with site.Params.description }}<p class="lede reveal">{{ . }}</p>{{ end }}
<p class="cta-row reveal"> <p class="cta-row reveal">
<a class="cta cta--primary" href="https://forgejo.sourcegate.online/daniel/furtka/releases"> <a class="cta cta--primary" href="{{ $dl }}">
{{ if eq site.Language.Lang "de" }}Neuestes Release{{ else }}Latest release{{ end }} {{ .Params.cta_download }}
<span aria-hidden="true">→</span> <span aria-hidden="true">→</span>
</a> </a>
<a class="cta cta--ghost" href="{{ site.Params.forgejoURL }}/{{ site.Params.repoPath }}">{{ .Params.cta_source }}</a>
</p> </p>
{{ with .Params.poll_link }} {{ with .Params.poll_link }}
<p class="reveal"><a class="home-poll" href="{{ . }}">{{ partial "mark-m.html" (dict "class" "" "id" "hm") }}{{ partial "mark-r.html" (dict "class" "" "id" "hr") }}<span>{{ $.Params.poll_text }} →</span></a></p> <p class="reveal"><a class="home-poll" href="{{ . }}">{{ partial "mark-m.html" (dict "class" "" "id" "hm") }}<span>{{ $.Params.poll_text }} →</span></a></p>
{{ end }} {{ end }}
</header> </header>
@ -21,6 +22,20 @@
<section class="intro">{{ . | markdownify }}</section> <section class="intro">{{ . | markdownify }}</section>
{{ end }} {{ end }}
{{ with .Params.steps }}
<section class="feature-section">
{{ with $.Params.steps_label }}<p class="section-eyebrow">{{ . }}</p>{{ end }}
<ol class="how-steps">
{{ range . }}
<li class="how-step" data-gsap="card">
<h3>{{ .title }}</h3>
<p>{{ .body | markdownify }}</p>
</li>
{{ end }}
</ol>
</section>
{{ end }}
{{ if .Params.features_today }} {{ if .Params.features_today }}
<section class="feature-section"> <section class="feature-section">
{{ with .Params.features_today_label }}<p class="section-eyebrow">{{ . }}</p>{{ end }} {{ with .Params.features_today_label }}<p class="section-eyebrow">{{ . }}</p>{{ end }}
@ -43,6 +58,16 @@
</section> </section>
{{ end }} {{ end }}
{{ with .Params.band_title }}
<section class="dl-band" data-gsap="card">
<div>
<h2>{{ . }}</h2>
<p>{{ $.Params.band_text }}</p>
</div>
<a class="cta cta--primary" href="{{ $dl }}">{{ $.Params.cta_download }} <span aria-hidden="true">→</span></a>
</section>
{{ end }}
{{ with .Content }} {{ with .Content }}
<section class="prose closer">{{ . }}</section> <section class="prose closer">{{ . }}</section>
{{ end }} {{ end }}

View file

@ -1,6 +1,2 @@
<svg class="gate-mark" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> {{- /* Furtka mark (M, small-size tier) — generated by tools/brand-assets.py from logo-assets.py geometry. */ -}}
<path d="M4 20 V12 A9 9 0 0 1 20 12 V20"/> <svg class="gate-mark" width="1em" height="1em" viewBox="0 0 64 64" aria-hidden="true" focusable="false"><path fill="currentColor" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/></svg>
<line x1="12" y1="5" x2="12" y2="20"/>
<line x1="3" y1="20" x2="21" y2="20"/>
<line x1="15" y1="12" x2="15" y2="14.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 384 B

After

Width:  |  Height:  |  Size: 390 B

View file

@ -10,12 +10,13 @@
<meta name="theme-color" content="#f7f6f3" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#f7f6f3" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0d0d0f" media="(prefers-color-scheme: dark)"> <meta name="theme-color" content="#0d0d0f" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/svg+xml" href="/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<meta property="og:site_name" content="{{ site.Title }}"> <meta property="og:site_name" content="{{ site.Title }}">
<meta property="og:title" content="{{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} · {{ site.Title }}{{ end }}"> <meta property="og:title" content="{{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} · {{ site.Title }}{{ end }}">
<meta property="og:description" content="{{ with .Params.description }}{{ . }}{{ else }}{{ site.Params.description }}{{ end }}"> <meta property="og:description" content="{{ with .Params.description }}{{ . }}{{ else }}{{ site.Params.description }}{{ end }}">
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}"> <meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
<meta property="og:url" content="{{ .Permalink }}"> <meta property="og:url" content="{{ .Permalink }}">
{{ with .Params.image }}<meta property="og:image" content="{{ . | absURL }}"> {{ with .Params.image | default "/img/brand/social.png" }}<meta property="og:image" content="{{ . | absURL }}">
<meta property="og:image:width" content="1200"> <meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630"> <meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">{{ end }} <meta name="twitter:card" content="summary_large_image">{{ end }}

View file

@ -0,0 +1,57 @@
{{- /*
Latest Furtka release that has an ISO attached, looked up on Forgejo at
build time. Returns a dict: version, url (ISO), size (bytes, 0 = unknown),
date (time or ""), sha256 ("" when the release has no .iso.sha256), page
(release page URL), live (false when the lookup failed and we fell back
to site.Params.version). Use via partialCached "release.html" . "release".
*/ -}}
{{- $repo := printf "%s/%s" site.Params.forgejoURL site.Params.repoPath -}}
{{- $v := site.Params.version -}}
{{- $out := dict
"version" $v
"url" (printf "%s/releases/download/%s/furtka-%s.iso" $repo $v $v)
"size" 0
"date" ""
"sha256" ""
"page" (printf "%s/releases/tag/%s" $repo $v)
"all" (printf "%s/releases" $repo)
"live" false
-}}
{{- $api := printf "%s/api/v1/repos/%s/releases?limit=10" site.Params.forgejoURL site.Params.repoPath -}}
{{- with try (resources.GetRemote $api) -}}
{{- with .Err -}}
{{- warnf "release.html: release lookup failed, using site.Params.version: %s" . -}}
{{- else with .Value -}}
{{- $done := false -}}
{{- range (. | transform.Unmarshal) -}}
{{- if and (not $done) (not .draft) -}}
{{- $rel := . -}}
{{- $iso := "" -}}
{{- $sum := "" -}}
{{- range .assets -}}
{{- if strings.HasSuffix .name ".iso" }}{{ $iso = . }}{{ end -}}
{{- if strings.HasSuffix .name ".iso.sha256" }}{{ $sum = . }}{{ end -}}
{{- end -}}
{{- with $iso -}}
{{- $done = true -}}
{{- $out = merge $out (dict
"version" $rel.tag_name
"url" .browser_download_url
"size" .size
"date" (time.AsTime $rel.published_at)
"page" $rel.html_url
"live" true) -}}
{{- with $sum -}}
{{- with try (resources.GetRemote .browser_download_url) -}}
{{- with .Value -}}
{{- $hash := index (split (trim .Content " \n") " ") 0 -}}
{{- if eq (len $hash) 64 }}{{ $out = merge $out (dict "sha256" $hash) }}{{ end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- return $out -}}

View file

@ -4,32 +4,43 @@
side gets the "first thing on the page" advantage for the whole poll. */}} side gets the "first thing on the page" advantage for the whole poll. */}}
{{- $order := $p.order | default (slice "m" "r") -}} {{- $order := $p.order | default (slice "m" "r") -}}
{{- $s := $p.show -}} {{- $s := $p.show -}}
<article class="poll" data-poll="{{ $p.poll }}" data-api="/api/poll/{{ $p.poll }}"> <article class="{{ if $p.closed }}poll-closed{{ else }}poll{{ end }}" data-poll="{{ $p.poll }}" data-api="/api/poll/{{ $p.poll }}">
<header class="page-header"> <header class="page-header">
<h1>{{ .Title }}</h1> <h1>{{ .Title }}</h1>
{{ with $p.description }}<p class="lede">{{ . }}</p>{{ end }} {{ with $p.description }}<p class="lede">{{ . }}</p>{{ end }}
</header> </header>
{{ with $p.result }}<p class="poll-result" role="status">{{ . | markdownify }}</p>{{ end }}
<div class="prose">{{ .Content }}</div> <div class="prose">{{ .Content }}</div>
{{/* ── Quick vote ─────────────────────────────────────────── */}} {{/* ── Quick vote ─────────────────────────────────────────── */}}
<div class="poll-grid" id="vote"> <div class="poll-grid" id="vote">
{{ range $k := $order }}{{ $d := index site.Data.logo $k }} {{ range $k := $order }}{{ $d := index site.Data.logo $k }}
{{ $c := index $p $k }} {{ $c := index $p $k }}
<section class="poll-card" data-choice="{{ $k }}" aria-labelledby="poll-{{ $k }}-title"> {{- $total := 0 }}{{ range $v := $p.final }}{{ $total = add $total $v }}{{ end }}
{{- $won := and $p.closed (eq $k (cond (gt (index $p.final "m") (index $p.final "r")) "m" "r")) }}
<section class="poll-card{{ if $won }} is-yours{{ end }}" data-choice="{{ $k }}" aria-labelledby="poll-{{ $k }}-title">
<div class="poll-art poll-art--{{ $k }}">{{ $d.display | safeHTML }}</div> <div class="poll-art poll-art--{{ $k }}">{{ $d.display | safeHTML }}</div>
<h2 id="poll-{{ $k }}-title">{{ $c.title }}</h2> <h2 id="poll-{{ $k }}-title">{{ $c.title }}</h2>
<p class="poll-desc">{{ $c.text }}</p> <p class="poll-desc">{{ $c.text }}</p>
{{ if $p.closed }}
{{ $n := index $p.final $k }}
<div class="poll-bar poll-bar--final" aria-hidden="true"><i style="width: {{ div (mul $n 100) $total }}%"></i></div>
<p class="poll-count">{{ $n }} / {{ $total }}</p>
{{ else }}
<button type="button" class="poll-btn" data-vote="{{ $k }}">{{ $p.labels.vote }}</button> <button type="button" class="poll-btn" data-vote="{{ $k }}">{{ $p.labels.vote }}</button>
<div class="poll-bar" aria-hidden="true"><i></i></div> <div class="poll-bar" aria-hidden="true"><i></i></div>
<p class="poll-count" data-count="{{ $k }}"></p> <p class="poll-count" data-count="{{ $k }}"></p>
{{ end }}
</section> </section>
{{ end }} {{ end }}
</div> </div>
{{ if not $p.closed }}
<p class="poll-status" role="status" aria-live="polite" <p class="poll-status" role="status" aria-live="polite"
data-t-voted="{{ $p.labels.voted }}" data-t-voted="{{ $p.labels.voted }}"
data-t-total="{{ $p.labels.total }}" data-t-total="{{ $p.labels.total }}"
data-t-error="{{ $p.labels.error }}" data-t-error="{{ $p.labels.error }}"
data-t-change="{{ $p.labels.change }}">{{ $p.labels.nojs }}</p> data-t-change="{{ $p.labels.change }}">{{ $p.labels.nojs }}</p>
{{ end }}
{{/* ── Showcase ───────────────────────────────────────────── */}} {{/* ── Showcase ───────────────────────────────────────────── */}}
<div class="show"> <div class="show">
@ -148,6 +159,7 @@
</div> </div>
{{/* ── Vote again at the end ─────────────────────────────── */}} {{/* ── Vote again at the end ─────────────────────────────── */}}
{{ if not $p.closed }}
<section class="poll-final"> <section class="poll-final">
<h2 class="show-h">{{ $s.final }}</h2> <h2 class="show-h">{{ $s.final }}</h2>
<div class="poll-final-row"> <div class="poll-final-row">
@ -162,7 +174,10 @@
</div> </div>
</section> </section>
<p class="poll-note">{{ $p.labels.note | safeHTML }}</p> <p class="poll-note">{{ $p.labels.note | safeHTML }}</p>
{{ end }}
</article> </article>
{{ if not $p.closed }}
{{ $js := resources.Get "js/poll.js" | minify | fingerprint }} {{ $js := resources.Get "js/poll.js" | minify | fingerprint }}
<script defer src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}"></script> <script defer src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}"></script>
{{ end }} {{ end }}
{{ end }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,6 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#a33e2e" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><style>svg{color:#c03a28}@media (prefers-color-scheme:dark){svg{color:#ff6b56}}</style><path fill="currentColor" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/></svg>
<path d="M4 20 V12 A9 9 0 0 1 20 12 V20"/>
<line x1="12" y1="5" x2="12" y2="20"/>
<line x1="3" y1="20" x2="21" y2="20"/>
<line x1="15" y1="12" x2="15" y2="14.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 334 B

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Generate the Furtka brand files from the winning logo (Mark M) everywhere
it ships: website, live installer, on-box UI, ISO boot splash, repo docs.
python3 website/tools/brand-assets.py
Reuses the geometry and rasteriser of logo-assets.py (the poll showcase), so
the mark is drawn from one source. Needs the same build-only deps: cairosvg,
Pillow, fontTools. Output files are committed; re-run after a geometry change.
"""
import importlib.util
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.abspath(os.path.join(HERE, "..", ".."))
_spec = importlib.util.spec_from_file_location("logo_assets", os.path.join(HERE, "logo-assets.py"))
la = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(la)
K = "m"
COLOR = la.MARKS[K]["color"] # light-theme accent
COLOR_DARK = "#ff6b56" # dark-theme accent used by the site and the box UI
INK = la.MARKS[K]["ink"]
def out(*parts):
path = os.path.join(REPO, *parts)
os.makedirs(os.path.dirname(path), exist_ok=True)
return path
def favicon_svg():
"""Small-size tier, accent colour, lighter accent on dark browser chrome."""
inner = la.body(K, "min", "currentColor")
x, y, w, h = la.MARKS[K]["vb"]
return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x} {y} {w} {h}">'
f"<style>svg{{color:{COLOR}}}"
f"@media (prefers-color-scheme:dark){{svg{{color:{COLOR_DARK}}}}}</style>"
f"{inner}</svg>\n"
)
def mark_svg(tier, color):
return (
la.svg(K, tier, color, standalone=True)
.replace(' class="pm"', "")
.replace(' aria-hidden="true" focusable="false"', "")
+ "\n"
)
def lockup_svg(color):
s = la.lockup(K).replace("currentColor", color)
return (
s.replace('<svg class="pm"', f"<svg {la.XMLNS}").replace(
' aria-hidden="true" focusable="false"', ' role="img" aria-label="Furtka"'
)
+ "\n"
)
def write(path, text):
with open(path, "w") as f:
f.write(text)
def main():
fav = favicon_svg()
for p in (
("website", "static", "favicon.svg"),
("webinstaller", "static", "favicon.svg"),
("assets", "www", "favicon.svg"),
):
write(out(*p), fav)
la.tile(K, 180, pad=0.2).convert("RGB").save(
out("website", "static", "apple-touch-icon.png"), optimize=True
)
# Default share card for pages without their own og:image.
la.lockup_png(K, 1200, 630, False, 230).convert("RGB").save(
out("website", "static", "img", "brand", "social.png"), optimize=True
)
# syslinux (BIOS boot menu) wants a 640x480 PNG and draws its menu over
# the middle of it, so the lockup sits small in the top band — same
# layout as releng's Arch splash, which build.sh replaces with this.
splash_band = la.lockup_png(K, 640, 120, False, 44)
splash = la.Image.new("RGBA", (640, 480), INK)
splash.alpha_composite(splash_band, (0, 8))
splash.convert("RGB").save(out("iso", "overlay", "splash.png"), optimize=True)
write(out("docs", "brand", "furtka-mark.svg"), mark_svg("flat", COLOR))
write(out("docs", "brand", "furtka-mark-small.svg"), mark_svg("min", COLOR))
write(out("docs", "brand", "furtka-lockup.svg"), lockup_svg(COLOR))
write(out("docs", "brand", "furtka-lockup-white.svg"), lockup_svg("#ffffff"))
write(out("docs", "brand", "console-banner.txt"), la.banner(K, cols=20) + "\n")
print("brand assets ok")
if __name__ == "__main__":
sys.exit(main())