From 141d66d7bb822973161b72033e118afda0786763 Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Sun, 24 May 2026 19:36:19 -0400 Subject: [PATCH 1/2] feat(design): single-page consolidation, drop typeface picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge projects + homelab + timeline into one scrollable page. Remove typeface picker (sans/serif/mono), keep theme toggle. Nav simplified to name + toggle. Hero gains anchor links for in-page navigation (#projects, #journey, #homelab). Old pages become meta-refresh redirects. Timeline redesigned as two-column grid layout — date left, content right — cutting vertical space ~50%. Focus states added for keyboard nav. Tags dropped from timeline entries. --- src/components/Hero.astro | 6 + src/components/Nav.astro | 82 ++----------- src/components/Timeline.astro | 29 ++--- src/layouts/Base.astro | 6 - src/pages/archive.astro | 6 +- src/pages/homelab.astro | 214 ++-------------------------------- src/pages/index.astro | 209 ++++++++++++++++++++++++++++++++- src/pages/projects.astro | 76 ++---------- src/styles/globals.css | 23 ++-- 9 files changed, 275 insertions(+), 376 deletions(-) diff --git a/src/components/Hero.astro b/src/components/Hero.astro index 6d057b4c..717d9d9c 100644 --- a/src/components/Hero.astro +++ b/src/components/Hero.astro @@ -42,4 +42,10 @@ Email + + diff --git a/src/components/Nav.astro b/src/components/Nav.astro index b6b5399b..8f77cfac 100644 --- a/src/components/Nav.astro +++ b/src/components/Nav.astro @@ -1,53 +1,14 @@ ---- -const pathname = Astro.url.pathname; - -const links = [ - { href: "/", label: "tyler" }, - { href: "/homelab/", label: "homelab" }, - { href: "/projects/", label: "projects" }, -]; ---- -
@@ -71,31 +32,4 @@ const links = [ }); updateTheme(); - - const tfBtns = document.querySelectorAll("[data-typeface-btn]"); - - function updateTypeface() { - const current = document.documentElement.dataset.typeface || "sans"; - tfBtns.forEach((b) => { - const val = b.getAttribute("data-typeface-btn"); - if (val === current) { - b.classList.add("font-bold", "text-[var(--color-text)]"); - b.classList.remove("text-[var(--color-text-dim)]"); - } else { - b.classList.remove("font-bold", "text-[var(--color-text)]"); - b.classList.add("text-[var(--color-text-dim)]"); - } - }); - } - - tfBtns.forEach((b) => { - b.addEventListener("click", () => { - const val = b.getAttribute("data-typeface-btn")!; - document.documentElement.dataset.typeface = val; - localStorage.setItem("lerko96-typeface", val); - updateTypeface(); - }); - }); - - updateTypeface(); diff --git a/src/components/Timeline.astro b/src/components/Timeline.astro index a04f5f47..ba663309 100644 --- a/src/components/Timeline.astro +++ b/src/components/Timeline.astro @@ -14,29 +14,24 @@ const typeLabel: Record = { --- -
    +
      {timeline.map((entry) => ( -
    1. -

      +

    2. +
      {isDate(entry.date) ? : {entry.date} } - · - {typeLabel[entry.type]} -

      - -

      {entry.title}

      - -

      - {entry.description} -

      - - {entry.tags && entry.tags.length > 0 && ( -

      - {entry.tags.join(" · ")} +

      +
      +

      + {entry.title} + · {typeLabel[entry.type]} +

      +

      + {entry.description}

      - )} +
    3. ))}
    diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index c615eb4d..f06ffc04 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -25,12 +25,6 @@ const { if (dark) document.documentElement.classList.add("dark"); })(); - - - + + Redirecting... -

    This page moved. /projects/

    +

    This page moved. /#projects

    diff --git a/src/pages/homelab.astro b/src/pages/homelab.astro index d28aef6c..8dcb1bc5 100644 --- a/src/pages/homelab.astro +++ b/src/pages/homelab.astro @@ -1,202 +1,12 @@ ---- -import Base from "@/layouts/Base.astro"; -import Nav from "@/components/Nav.astro"; -import Footer from "@/components/Footer.astro"; -import Widget from "@/components/Widget.astro"; -import { services, categoryOrder, categoryLabels } from "@/data/services"; - -const glanceStats = [ - { label: "Hypervisor", value: "Proxmox VE" }, - { label: "Firewall", value: "pfSense (Netgate 1100)" }, - { label: "Switching", value: "TP-Link Omada (managed)" }, - { label: "ISP", value: "AT&T Fiber 1 Gbps" }, - { label: "VPN", value: "WireGuard (pfSense)" }, - { label: "Reverse Proxy", value: "Caddy + Cloudflare DNS-01" }, - { label: "Auth", value: "Authentik SSO" }, - { label: "DNS", value: "Pi-hole → Unbound → Cloudflare" }, - { label: "Containers", value: "9 LXC + 2 VMs" }, -]; - -const vlans = [ - { id: "MGMT", name: "MGMT", purpose: "Network equipment only" }, - { id: "LAN", name: "LAN", purpose: "Trusted personal devices" }, - { id: "Lab", name: "Homelab", purpose: "All self-hosted services" }, - { id: "Guest", name: "Guests", purpose: "Internet only, RFC1918 blocked" }, - { id: "IoT", name: "IoT", purpose: "Smart home, isolated" }, - { id: "WFH", name: "WFH", purpose: "Work devices, no personal access" }, - { id: "DMZ", name: "DMZ", purpose: "Public-facing, hard-blocked internally" }, - { id: "VPN", name: "VPN", purpose: "WireGuard clients, LAN-equivalent access" }, -]; - -const adrs = [ - { - title: "ISP gateway: passthrough mode", - decision: - "ISP gateway stays in-line in passthrough mode, pfSense gets the public IP directly. Gateway WiFi disabled.", - why: "Carrier locks 802.1X auth to their own gateway hardware, and bypassing it is brittle — breaks on firmware updates and only saves a millisecond or two. True bridge mode isn't supported. Passthrough is the cleanest option that keeps pfSense as the actual perimeter.", - }, - { - title: "Caddy over NGINX Proxy Manager", - decision: - "Caddy with DNS-01 challenge via Cloudflare API. All subdomains resolve to Caddy internally via Pi-hole. Caddy terminates TLS and proxies to backends.", - why: "Single Caddyfile, automatic certs without ever needing to expose internal services to the internet for an HTTP-01 challenge. NPM has more UI overhead for the same outcome. Traefik is more complex for no benefit at this scale.", - }, - { - title: "WireGuard over OpenVPN", - decision: - "WireGuard on pfSense as the only remote-access path. Clients get the access tier documented in the access model — same as LAN, plus the admin surfaces that aren't reachable any other way.", - why: "Faster, simpler config, better battery life on mobile. Throughput on the firewall hardware comfortably exceeds the WAN link. OpenVPN has no advantage here. Tailscale would add an external relay dependency for a problem WireGuard already solves.", - }, - { - title: "Pi-hole in Homelab VLAN, not MGMT", - decision: - "Pi-hole runs in the Homelab VLAN. Firewall allows port 53 inbound from VLANs that need local resolution. MGMT uses pfSense Unbound as its primary resolver instead.", - why: "Putting Pi-hole in MGMT would mean opening MGMT to all the VLANs that need DNS — much bigger attack surface for the most sensitive tier. DNS traffic crossing into the Homelab VLAN is the lesser risk, and Homelab is already where service traffic terminates anyway.", - }, - { - title: "Netgate 1100 for pfSense", - decision: - "Netgate 1100 (Marvell ARMADA 3720, dual-core ARM) as the firewall appliance. ~6W idle, line-rate NAT at 1 Gbps, WireGuard at ~100–150 Mbps.", - why: "Purpose-built for pfSense. Right-sized for 1 Gbps fiber — NAT saturates the link, WireGuard is fast enough for remote access. A full rack server wastes power for this role. Configs and version tracked in private repo.", - }, - { - title: "Shared Postgres + Redis in apps LXC", - decision: - "One Postgres instance hosting multiple databases. One Redis instance. A single init script provisions schemas on first run.", - why: "Avoids ~15 separate DB containers. Big RAM savings. Productivity apps colocate in one LXC anyway, so a shared backing store there is the natural shape.", - }, - { - title: "Gitea CI/CD: self-hosted runner, internal pipeline, static deploy", - decision: - "Self-hosted Gitea Actions runner builds the portfolio on push, then deploys pre-built static files to the public-facing host. Build runs in an isolated container so the runner host stays clean. Public host serves static files only — no build toolchain on it.", - why: "Keeps the whole pipeline internal. No external runners, no GitHub Actions. The build/serve split means the public-facing host has the smallest possible footprint — static file server, nothing more.", - }, - { - title: "Authentik over Authelia", - decision: "Authentik as the SSO provider across all self-hosted services.", - why: "Full OIDC provider + forward auth in one. Lets services like Outline, Gitea, and Vikunja use real SSO rather than just a login gate. Authelia is forward-auth only — no OIDC provider capability.", - }, -]; ---- - - -