5 Commits

Author SHA1 Message Date
tyler d34f9f136c 'feat(site): /projects consolidation, homelab copy pass, theme fix' (#6)
Build and Deploy / deploy (push) Successful in 1m0s
Reviewed-on: #6
2026-04-27 06:18:24 +00:00
lerko96 22660bed7a fix(theme): unblock light mode in dev
Hardcoded class="dark" on <html> meant React owned it via JSX. HMR
re-renders and reconciliation kept restoring the class after
classList.toggle removed it, so light toggle never stuck.

ThemeScript already handles initial paint; suppressHydrationWarning
covers the post-script class mismatch.
2026-04-27 00:49:57 -04:00
lerko96 7f614d28b5 feat(projects): consolidate /projects, hide skills, redirect /archive
- /projects: merged page with featured (top) + archive (bottom)
- titles mirror homelab pattern: projects/featured, projects/archive
- nav: archive → projects
- home: drop Skills section and featured grid
- /archive → /projects via meta-refresh + JS redirect stub
2026-04-27 00:49:52 -04:00
lerko96 e9d7a994c7 chore(content): refresh projects + timeline
- featured: swap helm → nib, drop claude-vault, reorder
- helm → archive
- homelab description: 7 VLANs + Wireguard VPN (matches access-tier framing)
- timeline: add Proxmox Backup Server 2025; pfSense entry corrected to Netgate 1100
2026-04-27 00:49:47 -04:00
lerko96 f6118aa7a4 refactor(homelab): rename VLAN col to Segment for VPN row
VPN is an L3 tunnel, not an 802.1Q VLAN. Reframes the table as
network segments / access tiers so the VPN row is consistent with
the others.
2026-04-27 00:49:42 -04:00
8 changed files with 139 additions and 110 deletions
+14 -58
View File
@@ -1,68 +1,24 @@
import type { Metadata } from "next"; "use client";
import Widget from "@/components/Widget";
import { archiveProjects } from "@/data/projects";
export const metadata: Metadata = { import { useEffect } from "react";
title: "Archive | Tyler Koenig",
description: "Earlier projects and experiments — browser extensions, canvas apps, and bootcamp work.", export default function ArchiveRedirect() {
}; useEffect(() => {
window.location.replace("/projects/");
}, []);
export default function ArchivePage() {
return ( return (
<> <>
<div className="mb-4lh"> <meta httpEquiv="refresh" content="0; url=/projects/" />
<p className="font-mono text-sm font-bold text-[var(--color-text)] mb-1lh"> <p className="font-mono text-sm text-[var(--color-text)]">
<span className="text-[var(--color-accent-green)] select-none mr-1ch" aria-hidden="true"></span> This page moved.{" "}
tyler/projects/archive
</p>
<p className="font-mono text-sm text-[var(--color-text)] leading-relaxed max-w-xl opacity-80">
Experiments, browser extensions, and bootcamp projects. Kept here for context not
representative of current work.
</p>
</div>
<Widget title="tyler/projects/archive" badge={archiveProjects.length} as="section">
<div className="flex flex-col gap-px bg-[var(--color-border)]">
{archiveProjects.map((project) => (
<a <a
key={project.slug} href="/projects/"
href={project.githubUrl} className="text-[var(--color-accent-green)] underline"
target="_blank"
rel="noopener noreferrer"
className="bg-[var(--color-surface)] hover:bg-[var(--color-surface-raised)] flex items-start justify-between gap-2ch px-2ch py-1lh group"
> >
<div className="flex flex-col gap-1ch flex-1 min-w-0"> /projects/
<div className="flex items-center gap-1ch">
{project.year && (
<span className="font-mono text-sm text-[var(--color-text-dim)] shrink-0">
{project.year}
</span>
)}
<span className="font-mono text-sm text-[var(--color-text)] group-hover:text-[var(--color-accent-green)] truncate">
{project.title}
</span>
</div>
<p className="font-mono text-sm text-[var(--color-text)] leading-relaxed opacity-75">
{project.description}
</p>
<div className="flex flex-wrap gap-x-1ch gap-y-0.5">
{project.tags.map((tag) => (
<span key={tag} className="font-mono text-sm text-[var(--color-text-dim)]">
{tag}
</span>
))}
</div>
</div>
<span
className="font-mono text-sm text-[var(--color-text-label)] group-hover:text-[var(--color-text)] shrink-0 mt-0.5"
aria-hidden="true"
>
</span>
</a> </a>
))} </p>
</div>
</Widget>
</> </>
); );
} }
+2 -2
View File
@@ -155,7 +155,7 @@ export default function HomelabPage() {
{/* VLAN table */} {/* VLAN table */}
<Widget <Widget
title="homelab/network" title="homelab/network"
meta="8 isolated vlans · default deny inter-vlan" meta="8 network segments · default deny"
as="section" as="section"
> >
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -163,7 +163,7 @@ export default function HomelabPage() {
<thead> <thead>
<tr className="border-b border-[var(--color-border)]"> <tr className="border-b border-[var(--color-border)]">
<th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase"> <th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase">
VLAN Segment
</th> </th>
<th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase"> <th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase">
Name Name
+1 -1
View File
@@ -24,7 +24,7 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en" className="dark"> <html lang="en" suppressHydrationWarning>
<head> <head>
<ThemeScript /> <ThemeScript />
</head> </head>
-12
View File
@@ -1,10 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import Hero from "@/components/Hero"; import Hero from "@/components/Hero";
import Skills from "@/components/Skills";
import Timeline from "@/components/Timeline"; import Timeline from "@/components/Timeline";
import ProjectCard from "@/components/ProjectCard";
import Widget from "@/components/Widget";
import { featuredProjects } from "@/data/projects";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Tyler Koenig", title: "Tyler Koenig",
@@ -17,14 +13,6 @@ export default function Home() {
<> <>
<Hero /> <Hero />
<Timeline /> <Timeline />
<Widget title="tyler/projects" badge={featuredProjects.length}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1ch">
{featuredProjects.map((project) => (
<ProjectCard key={project.slug} project={project} />
))}
</div>
</Widget>
<Skills />
</> </>
); );
} }
+77
View File
@@ -0,0 +1,77 @@
import type { Metadata } from "next";
import Widget from "@/components/Widget";
import ProjectCard from "@/components/ProjectCard";
import { featuredProjects, archiveProjects } from "@/data/projects";
export const metadata: Metadata = {
title: "Projects | Tyler Koenig",
description:
"Featured projects and earlier work — homelab, open-pact, helm, and bootcamp/experiment archive.",
};
export default function ProjectsPage() {
return (
<>
<div className="mb-4lh">
<p className="font-mono text-sm font-bold text-[var(--color-text)] mb-1lh">
<span className="text-[var(--color-accent-green)] select-none mr-1ch" aria-hidden="true"></span>
projects
</p>
<p className="font-mono text-sm text-[var(--color-text)] leading-relaxed max-w-xl opacity-80">
Featured work first. Earlier experiments, browser extensions, and bootcamp projects below kept for context.
</p>
</div>
<Widget title="projects/featured" badge={featuredProjects.length} as="section">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1ch">
{featuredProjects.map((project) => (
<ProjectCard key={project.slug} project={project} />
))}
</div>
</Widget>
<Widget title="projects/archive" badge={archiveProjects.length} as="section">
<div className="flex flex-col gap-px bg-[var(--color-border)]">
{archiveProjects.map((project) => (
<a
key={project.slug}
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="bg-[var(--color-surface)] hover:bg-[var(--color-surface-raised)] flex items-start justify-between gap-2ch px-2ch py-1lh group"
>
<div className="flex flex-col gap-1ch flex-1 min-w-0">
<div className="flex items-center gap-1ch">
{project.year && (
<span className="font-mono text-sm text-[var(--color-text-dim)] shrink-0">
{project.year}
</span>
)}
<span className="font-mono text-sm text-[var(--color-text)] group-hover:text-[var(--color-accent-green)] truncate">
{project.title}
</span>
</div>
<p className="font-mono text-sm text-[var(--color-text)] leading-relaxed opacity-75">
{project.description}
</p>
<div className="flex flex-wrap gap-x-1ch gap-y-0.5">
{project.tags.map((tag) => (
<span key={tag} className="font-mono text-sm text-[var(--color-text-dim)]">
{tag}
</span>
))}
</div>
</div>
<span
className="font-mono text-sm text-[var(--color-text-label)] group-hover:text-[var(--color-text)] shrink-0 mt-0.5"
aria-hidden="true"
>
</span>
</a>
))}
</div>
</Widget>
</>
);
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { useTheme } from "@/context/ThemeContext";
const links = [ const links = [
{ href: "/", label: "tyler" }, { href: "/", label: "tyler" },
{ href: "/homelab/", label: "homelab" }, { href: "/homelab/", label: "homelab" },
{ href: "/archive/", label: "archive" }, { href: "/projects/", label: "projects" },
]; ];
export default function Nav() { export default function Nav() {
+29 -29
View File
@@ -17,22 +17,12 @@ export const projects: Project[] = [
slug: "homelab", slug: "homelab",
title: "homelab", title: "homelab",
description: description:
"8-VLAN segmented network, Proxmox VMs/LXCs, SSO via Authentik, full monitoring stack (VictoriaMetrics + Grafana + Beszel + ntfy).", "7-VLAN segmented network, Wireguard VPN, Proxmox VMs/LXCs, SSO via Authentik, full monitoring stack (VictoriaMetrics + Grafana + Beszel + ntfy).",
tags: ["Markdown", "Mermaid", "Proxmox", "Monitor", "Backup"], tags: ["Markdown", "Mermaid", "Proxmox", "Monitor", "Backup"],
githubUrl: "https://gitea.lerkolabs.com/lerko/homelab", githubUrl: "https://gitea.lerkolabs.com/lerko/homelab",
tier: "featured", tier: "featured",
year: 2026, year: 2026,
}, },
{
slug: "open-pact",
title: "open-pact",
description:
"Open protocol for AI agent identity, delegation, and portable memory. Ed25519 keypair identity, signed delegation warrants, portable signed memory facts. No central registry.",
tags: ["TypeScript", "Ed25519", "DID", "npm", "CC0"],
githubUrl: "https://github.com/lerko96/open-pact",
tier: "featured",
year: 2026,
},
{ {
slug: "portfolio", slug: "portfolio",
title: "portfolio", title: "portfolio",
@@ -43,6 +33,27 @@ export const projects: Project[] = [
tier: "featured", tier: "featured",
year: 2021, year: 2021,
}, },
{
slug: "nib",
title: "nib",
description:
"Capture-first personal journal built with Go + React + SQLite. Currently developing in private when I have spare time.",
tags: ["Go", "React", "SQLite", "Journal", "Stream-of-Thought"],
githubUrl: "https://github.com/lerko96/nib",
tier: "featured",
year: 2026,
},
{
slug: "open-pact",
title: "open-pact",
description:
"Open protocol for AI agent identity, delegation, and portable memory. Ed25519 keypair identity, signed delegation warrants, portable signed memory facts. No central registry.",
tags: ["TypeScript", "Ed25519", "DID", "npm", "CC0"],
githubUrl: "https://github.com/lerko96/open-pact",
tier: "featured",
year: 2026,
},
// --- Archive ---
{ {
slug: "helm", slug: "helm",
title: "helm", title: "helm",
@@ -50,20 +61,19 @@ export const projects: Project[] = [
"Full-stack personal productivity dashboard. Go backend with chi router and SQLite, React + TypeScript frontend. Notes, todos, calendar (CalDAV), clipboard, bookmarks, memos. Self-hosted, single-user, daily use.", "Full-stack personal productivity dashboard. Go backend with chi router and SQLite, React + TypeScript frontend. Notes, todos, calendar (CalDAV), clipboard, bookmarks, memos. Self-hosted, single-user, daily use.",
tags: ["Go", "React", "TypeScript", "SQLite", "CalDAV"], tags: ["Go", "React", "TypeScript", "SQLite", "CalDAV"],
githubUrl: "https://github.com/lerko96/helm", githubUrl: "https://github.com/lerko96/helm",
tier: "featured", tier: "archive",
year: 2026, year: 2026,
}, },
{ {
slug: "claude-vault", slug: "risk-ops",
title: "claude-vault", title: "risk-ops",
description: description:
"A scaffolding system for maintaining a living project knowledge base alongside a code repo, powered by Claude Code skills.", "Browser-based strategy dashboard for Risk: Global Domination (SMG Studio). Open one HTML file — no install needed.",
tags: ["Shell", "Developer-Tools", "Claude", "Knowledge-Management"], tags: ["HTML", "JavaScript"],
githubUrl: "https://github.com/lerko96/claude-vault", githubUrl: "#",
tier: "featured", tier: "archive",
year: 2026, year: 2026,
}, },
// --- Archive ---
{ {
slug: "golf-book-mobile", slug: "golf-book-mobile",
title: "golf-book-mobile", title: "golf-book-mobile",
@@ -76,16 +86,6 @@ export const projects: Project[] = [
statusBadge: "Pending App Store Approval", statusBadge: "Pending App Store Approval",
year: 2025, year: 2025,
}, },
{
slug: "risk-ops",
title: "risk-ops",
description:
"Browser-based strategy dashboard for Risk: Global Domination (SMG Studio). Open one HTML file — no install needed.",
tags: ["HTML", "JavaScript"],
githubUrl: "#",
tier: "archive",
year: 2026,
},
{ {
slug: "twitter-thread-ext", slug: "twitter-thread-ext",
title: "twitter-thread-ext", title: "twitter-thread-ext",
+12 -4
View File
@@ -47,12 +47,19 @@ export const timeline: TimelineEntry[] = [
tags: ["go", "react", "typescript"], tags: ["go", "react", "typescript"],
}, },
{ {
date: "2024-08", date: "2025",
title: "Proxmox Backup Server",
type: "homelab",
description: "Deployed PBS on used desktop hardware for disaster recovery.",
tags: ["backup", "recovery", "retention"],
},
{
date: "2025",
title: "Proxmox Cluster", title: "Proxmox Cluster",
type: "homelab", type: "homelab",
description: description:
"Proxmox VMs/LXCs, SSO via Authentik, full monitoring stack (VictoriaMetrics + Grafana + Beszel + ntfy).", "Proxmox installed on dedicated server and the fun begins. VMs/LXCs, SSO via Authentik, full monitoring stack (VictoriaMetrics + Grafana + Beszel + ntfy).",
tags: ["proxmox", "networking", "monitoring", "sso"], tags: ["proxmox", "containers", "VMs", "linux"],
}, },
{ {
date: "2024-06", date: "2024-06",
@@ -66,7 +73,8 @@ export const timeline: TimelineEntry[] = [
date: "2024-03", date: "2024-03",
title: "pfSense", title: "pfSense",
type: "homelab", type: "homelab",
description: "Netgate pfSense n100 picked up on ebay.", description:
"Netgate 1100 picked up on ebay to experience hands on networking configuration and troubleshooting.",
tags: ["network", "firewall", "vlan", "dhcp"], tags: ["network", "firewall", "vlan", "dhcp"],
}, },
{ {