# ridgwaysystems.org Personal homelab site. Landing page, build log / blog, infrastructure overview, service status page, and an admin panel for managing content. ## Why Go? - Compiles to a single static binary. Easy to deploy on FreeBSD with no runtime dependencies. - Standard library handles HTTP, HTML templates, and file serving without pulling in a framework. - Fast startup, low memory footprint. Fits comfortably behind relayd on the SuperMicro 1U. - The tool I know best for this kind of thing. A static site generator (Hugo, etc.) was considered but ruled out because the admin panel requirement means a server-side process is needed anyway. Combining the site server and admin into one binary keeps the deployment simple. ## Project Structure ``` . ├── cmd/server/main.go # Entry point ├── internal/ │ ├── blog/ # Post parsing and store (markdown + YAML frontmatter) │ ├── feed/ # RSS 2.0 feed generation │ ├── handler/ # HTTP handlers (public, admin, auth) │ └── status/ # Service status JSON loading/saving ├── templates/ │ ├── base.html # Shared layout │ ├── index.html # Landing page │ ├── blog.html # Post list │ ├── post.html # Single post │ ├── infrastructure.html # Hardware and network diagram │ ├── status.html # Service status page │ ├── about.html # About / contact │ └── admin/ # Admin panel templates ├── static/css/style.css # Stylesheet (dark mode, responsive) ├── content/posts/ # Blog posts as Markdown files with YAML frontmatter ├── data/status.json # Service status data (updated by cron or manually) ├── .env.example # Configuration reference └── README.md ``` ## Building ```sh go build -o website ./cmd/server ``` Requires Go 1.22+. All dependencies are managed with Go modules. To download dependencies: ```sh go mod tidy ``` ## Running ```sh # Development — reloads templates on each request DEV=1 PORT=8080 go run ./cmd/server # With a .env file (requires a shell that sources it, or use envdir/dotenv) export $(cat .env | grep -v '#' | xargs) ./website ``` ## Configuration All configuration is via environment variables. See `.env.example` for the full list. | Variable | Default | Description | |-----------------------|----------------------|---------------------------------------| | `PORT` | `8080` | Listen port | | `SITE_URL` | `https://ridgwaysystems.org` | Public URL for RSS feed links | | `CONTENT_DIR` | `content` | Path to content directory | | `DATA_DIR` | `data` | Path to data directory (status.json) | | `DEV` | `0` | Set `1` for template hot-reloading | | `ADMIN_PASSWORD_HASH` | — | bcrypt hash of admin password | | `SESSION_SECRET` | — | HMAC signing key for session cookies | ### Generating an admin password hash ```sh # Using htpasswd (from apache2-utils / httpd-tools) htpasswd -bnBC 12 "" yourpassword | tr -d ':\n' # Or write a small Go snippet: # import "golang.org/x/crypto/bcrypt" # hash, _ := bcrypt.GenerateFromPassword([]byte("yourpassword"), 12) # fmt.Println(string(hash)) ``` ## Blog Post Format Posts are Markdown files in `content/posts/`. Filenames become slugs unless overridden in frontmatter. ```markdown --- title: "Post Title" date: 2025-01-15 tags: [openbsd, homelab] slug: optional-custom-slug description: "Short description for meta tags and post listings." draft: false --- Post content here. Standard Markdown with GFM extensions (tables, strikethrough, etc.) Syntax highlighting for fenced code blocks is automatic. ``` ## Status Page JSON Schema `data/status.json` is read on each request to the `/status` page. A cron job or monitoring script can write to this file to update service statuses. ```json { "last_checked": "2025-02-10T12:00:00Z", "services": [ { "name": "Web (httpd)", "description": "ridgwaysystems.org", "url": "https://ridgwaysystems.org", "status": "up", "note": "Optional note shown on status page" } ] } ``` Valid `status` values: `up`, `degraded`, `down`, `unknown`. ### Example cron update script ```sh #!/bin/sh # /usr/local/bin/check-services.sh # Run every 5 minutes via cron. Updates data/status.json. SITE_DIR=/var/www/ridgwaysystems STATUS_FILE=$SITE_DIR/data/status.json TMPFILE=$(mktemp) check() { name="$1" url="$2" if curl -sf --max-time 5 "$url" > /dev/null 2>&1; then echo "up" else echo "down" fi } WEB_STATUS=$(check "web" "https://ridgwaysystems.org") GITEA_STATUS=$(check "gitea" "https://git.ridgwaysystems.org") cat > "$TMPFILE" << EOF { "last_checked": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "services": [ {"name": "Web (httpd)", "description": "ridgwaysystems.org", "status": "$WEB_STATUS"}, {"name": "Gitea", "description": "git.ridgwaysystems.org", "status": "$GITEA_STATUS"} ] } EOF mv "$TMPFILE" "$STATUS_FILE" ``` ## Deploying on FreeBSD ### 1. Build On your control machine (or directly on the server): ```sh GOOS=openbsd GOARCH=amd64 go build -o website ./cmd/server ``` Copy the binary and required files to the server: ```sh scp website srv01:/usr/local/bin/website rsync -av templates/ srv01:/var/www/ridgwaysystems/templates/ rsync -av static/ srv01:/var/www/ridgwaysystems/static/ rsync -av content/ srv01:/var/www/ridgwaysystems/content/ rsync -av data/ srv01:/var/www/ridgwaysystems/data/ ``` ### 2. rc.d service Create `/etc/rc.d/website`: ```sh #!/bin/ksh daemon="/usr/local/bin/website" daemon_user="www" daemon_flags="-f" . /etc/rc.d/rc.subr rc_cmd $1 ``` ```sh chmod +x /etc/rc.d/website rcctl enable website rcctl set website env "PORT=8080 SITE_URL=https://ridgwaysystems.org \ CONTENT_DIR=/var/www/ridgwaysystems/content \ DATA_DIR=/var/www/ridgwaysystems/data \ ADMIN_PASSWORD_HASH= SESSION_SECRET=" rcctl start website ``` ### 3. relayd configuration Add to `/etc/relayd.conf` on fw01: ``` # ridgwaysystems.org http protocol "ridgway-https" { tls { keypair ridgwaysystems.org } match request header set "X-Forwarded-For" value "$REMOTE_ADDR" match request header set "X-Forwarded-Proto" value "https" pass } relay "ridgway-web" { listen on egress port 443 tls protocol "ridgway-https" forward to 10.0.10.10 port 8080 check http "/" code 200 } ``` ### 4. httpd for Let's Encrypt ACME challenges (optional) If using acme-client on fw01 for TLS certificates: ``` # /etc/httpd.conf on fw01 server "ridgwaysystems.org" { listen on * port 80 location "/.well-known/acme-challenge/*" { root "/acme" request strip 2 } location "*" { block return 301 "https://ridgwaysystems.org$REQUEST_URI" } } ``` ### 5. TLS certificates with acme-client ``` # /etc/acme-client.conf authority letsencrypt { api url "https://acme-v02.api.letsencrypt.org/directory" account key "/etc/acme/letsencrypt-privkey.pem" } domain ridgwaysystems.org { domain key "/etc/ssl/private/ridgwaysystems.org.key" domain full chain certificate "/etc/ssl/ridgwaysystems.org.fullchain.pem" sign with letsencrypt } ``` Daily cron renewal in `/etc/daily.local`: ```sh acme-client ridgwaysystems.org && rcctl reload relayd ``` ## Admin Panel The admin panel is available at `/admin`. Log in with the password configured in `ADMIN_PASSWORD_HASH`. Features: - Create, edit, and delete blog posts with Markdown editor and live preview - Edit the service status JSON directly - Session expires after 24 hours The admin panel is intentionally minimal — one user, no roles, no audit log. It's for personal use on a self-hosted site. ## No External Dependencies The site loads nothing from external servers. No CDN fonts, no analytics, no third-party scripts. Everything is self-contained in `static/`. This is intentional.