4 Commits

Author SHA1 Message Date
lerko da61cbba5d feat(content): add uptop, reorder projects, archive open-pact, fix VLAN count
Build and Deploy / deploy (push) Successful in 1m18s
Add uptop (uptime monitoring TUI) as featured project. Reorder nib
above homelab, update nib stack and URL. Move open-pact to archive.
Update portfolio description (Nginx → Caddy). Fix VLAN count 8 → 7.
Trim Hero bio.
2026-05-31 18:17:44 -04:00
lerko 9037461d27 fix(content): consolidate homelab spacing, dynamic service count in hero
Collapse homelab into single Widget to match Projects/Journey spacing.
Hero service count now reads from services data instead of hardcoded 30+.
2026-05-27 17:33:16 -04:00
lerko 4a2ea3c32d Merge pull request 'feat(design): external link colors, section dividers, nav scroll-spy' (#10) from feat/polish-reader-view into staging
Reviewed-on: #10
2026-05-27 18:31:06 +00:00
lerko 6db5b36989 feat(design): external link colors, section dividers, nav scroll-spy
Dusty blue/violet link colors for external links (target="_blank") with
visited state. Both dark and light theme tokens. Mailto links included.

Section dividers via border-top on anchored sections. Scroll-margin-top
clears sticky nav on anchor jumps.

Nav scroll-spy highlights active section using IntersectionObserver +
scroll listener fallback for bottom-of-page edge case.
2026-05-26 20:43:56 -04:00
7 changed files with 124 additions and 44 deletions
+5 -4
View File
@@ -10,7 +10,7 @@ const year = new Date().getFullYear();
href="https://github.com/lerko96" href="https://github.com/lerko96"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="underline hover:text-[var(--color-text)]" class="underline"
> >
GitHub GitHub
</a> </a>
@@ -18,7 +18,7 @@ const year = new Date().getFullYear();
href="https://gitea.lerkolabs.com/lerko" href="https://gitea.lerkolabs.com/lerko"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="underline hover:text-[var(--color-text)]" class="underline"
> >
Gitea Gitea
</a> </a>
@@ -26,13 +26,14 @@ const year = new Date().getFullYear();
href="https://www.linkedin.com/in/tyler-koenig" href="https://www.linkedin.com/in/tyler-koenig"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="underline hover:text-[var(--color-text)]" class="underline"
> >
LinkedIn LinkedIn
</a> </a>
<a <a
href="mailto:tyler@lerkolabs.com" href="mailto:tyler@lerkolabs.com"
class="underline hover:text-[var(--color-text)]" target="_blank"
class="underline"
> >
Email Email
</a> </a>
+11 -6
View File
@@ -1,3 +1,7 @@
---
import { services } from "@/data/services";
---
<section class="mb-3lh"> <section class="mb-3lh">
<h1 class="text-xl font-bold mb-half-lh">Tyler Koenig</h1> <h1 class="text-xl font-bold mb-half-lh">Tyler Koenig</h1>
<p class="text-[var(--color-text-label)] mb-1lh"> <p class="text-[var(--color-text-label)] mb-1lh">
@@ -5,9 +9,9 @@
</p> </p>
<p class="text-[var(--color-text-label)] leading-relaxed mb-1lh"> <p class="text-[var(--color-text-label)] leading-relaxed mb-1lh">
Homelab runs 30+ services across segmented VLANs — pfSense, Authentik SSO, Homelab runs {services.length} services across segmented VLANs — pfSense, Authentik SSO,
full observability stack. Write software too: mobile apps, Go backends, open full observability stack. Write software too: mobile apps, Go backends, open
protocols. Daily drivers, all of it. protocols.
</p> </p>
<nav class="flex flex-wrap items-center gap-x-2ch gap-y-half-lh text-[var(--color-text-label)]"> <nav class="flex flex-wrap items-center gap-x-2ch gap-y-half-lh text-[var(--color-text-label)]">
@@ -15,7 +19,7 @@
href="https://github.com/lerko96" href="https://github.com/lerko96"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="underline hover:text-[var(--color-text)]" class="underline"
> >
GitHub GitHub
</a> </a>
@@ -23,7 +27,7 @@
href="https://gitea.lerkolabs.com/lerko" href="https://gitea.lerkolabs.com/lerko"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="underline hover:text-[var(--color-text)]" class="underline"
> >
Gitea Gitea
</a> </a>
@@ -31,13 +35,14 @@
href="https://www.linkedin.com/in/tyler-koenig" href="https://www.linkedin.com/in/tyler-koenig"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="underline hover:text-[var(--color-text)]" class="underline"
> >
LinkedIn LinkedIn
</a> </a>
<a <a
href="mailto:tyler@lerkolabs.com" href="mailto:tyler@lerkolabs.com"
class="underline hover:text-[var(--color-text)]" target="_blank"
class="underline"
> >
Email Email
</a> </a>
+51 -3
View File
@@ -3,9 +3,9 @@
<a href="/" class="font-semibold">Tyler Koenig</a> <a href="/" class="font-semibold">Tyler Koenig</a>
<div class="flex items-center gap-2ch"> <div class="flex items-center gap-2ch">
<a href="#projects" class="hidden xs:inline text-[var(--color-text-label)] hover:text-[var(--color-text)]">projects</a> <a href="#projects" data-nav-link class="hidden xs:inline text-[var(--color-text-label)] hover:text-[var(--color-text)]">projects</a>
<a href="#journey" class="hidden xs:inline text-[var(--color-text-label)] hover:text-[var(--color-text)]">journey</a> <a href="#journey" data-nav-link class="hidden xs:inline text-[var(--color-text-label)] hover:text-[var(--color-text)]">journey</a>
<a href="#homelab" class="hidden xs:inline text-[var(--color-text-label)] hover:text-[var(--color-text)]">homelab</a> <a href="#homelab" data-nav-link class="hidden xs:inline text-[var(--color-text-label)] hover:text-[var(--color-text)]">homelab</a>
<button <button
data-theme-toggle data-theme-toggle
@@ -38,4 +38,52 @@
}); });
updateTheme(); updateTheme();
const navLinks = document.querySelectorAll<HTMLAnchorElement>("[data-nav-link]");
const sections = Array.from(navLinks).map((link) => ({
link,
section: document.querySelector(link.hash) as HTMLElement,
}));
const atBottom = () =>
window.innerHeight + window.scrollY >= document.body.offsetHeight - 2;
const observer = new IntersectionObserver(
() => {
let active: HTMLAnchorElement | null = null;
if (atBottom()) {
active = sections[sections.length - 1].link;
} else {
for (const { link, section } of sections) {
if (section.getBoundingClientRect().top <= 80) active = link;
}
}
navLinks.forEach((l) => {
l.style.color = l === active ? "var(--color-text)" : "";
});
},
{ rootMargin: "-56px 0px 0px 0px", threshold: 0 },
);
sections.forEach(({ section }) => observer.observe(section));
let ticking = false;
window.addEventListener("scroll", () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
let active: HTMLAnchorElement | null = null;
if (atBottom()) {
active = sections[sections.length - 1].link;
} else {
for (const { link, section } of sections) {
if (section.getBoundingClientRect().top <= 80) active = link;
}
}
navLinks.forEach((l) => {
l.style.color = l === active ? "var(--color-text)" : "";
});
ticking = false;
});
}, { passive: true });
</script> </script>
+1 -1
View File
@@ -15,7 +15,7 @@ const { project } = Astro.props;
href={project.githubUrl} href={project.githubUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="font-semibold underline hover:text-[var(--color-text-label)]" class="font-semibold underline"
> >
{project.title} {project.title}
</a> </a>
+24 -16
View File
@@ -13,6 +13,25 @@ export type Project = {
export const projects: Project[] = [ export const projects: Project[] = [
// --- Featured --- // --- Featured ---
{
slug: "uptop",
title: "uptop",
description: "Live uptime monitoring dashboard for your terminal. SSH-accessible. HTTP, ping, TCP, DNS, push checks with alerts, clustering, and Prometheus metrics.",
tags: ["Go", "Bubbletea", "Monitoring", "Uptime"],
githubUrl: "https://github.com/lerkolabs/uptop",
tier: "featured",
year: 2026,
},
{
slug: "nib",
title: "nib",
description:
"Capture-first personal journal built with Go + SQLite. Currently developing in private when I have spare time.",
tags: ["Go", "JavaScript", "SQLite", "Stream-of-Thought"],
githubUrl: "https://gitea.lerkolabs.com/lerko/nib-v1",
tier: "featured",
year: 2026,
},
{ {
slug: "homelab", slug: "homelab",
title: "homelab", title: "homelab",
@@ -27,33 +46,22 @@ export const projects: Project[] = [
slug: "portfolio", slug: "portfolio",
title: "portfolio", title: "portfolio",
description: description:
"Astro static site, self-hosted in a DMZ LXC behind Nginx, deployed via Gitea Actions CI.", "Astro static site, self-hosted in a DMZ LXC behind Caddy, deployed via Gitea Actions CI.",
tags: ["Astro", "Dockerfile", "Tailwind", "nginx", "Caddy"], tags: ["Astro", "Typescript", "Dockerfile", "Caddy"],
githubUrl: "https://gitea.lerkolabs.com/lerko/portfolio", githubUrl: "https://gitea.lerkolabs.com/lerko/portfolio",
tier: "featured", tier: "featured",
year: 2021, year: 2021,
}, },
{ // --- Archive ---
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", slug: "open-pact",
title: "open-pact", title: "open-pact",
description: description: "Open protocol for AI agent identity, delegation, and portable memory. Ed25519 keypair identity, signed delegation",
"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"], tags: ["TypeScript", "Ed25519", "DID", "npm", "CC0"],
githubUrl: "https://github.com/lerko96/open-pact", githubUrl: "https://github.com/lerko96/open-pact",
tier: "featured", tier: "archive",
year: 2026, year: 2026,
}, },
// --- Archive ---
{ {
slug: "helm", slug: "helm",
title: "helm", title: "helm",
+7 -14
View File
@@ -12,7 +12,7 @@ import { services, categoryOrder, categoryLabels } from "@/data/services";
const glanceStats = [ const glanceStats = [
{ label: "Hypervisor", value: "Proxmox VE" }, { label: "Hypervisor", value: "Proxmox VE" },
{ label: "Firewall", value: "pfSense (Netgate 1100)" }, { label: "Firewall", value: "pfSense (Netgate 1100)" },
{ label: "Network", value: "8 VLANs, default deny, managed switching" }, { label: "Network", value: "7 VLANs, default deny, managed switching" },
{ label: "Services", value: `${services.length} self-hosted across ${categoryOrder.length} categories` }, { label: "Services", value: `${services.length} self-hosted across ${categoryOrder.length} categories` },
]; ];
--- ---
@@ -37,18 +37,15 @@ const glanceStats = [
</section> </section>
<section id="homelab"> <section id="homelab">
<div class="mb-4lh"> <Widget title="Homelab" as="div">
<h2 class="text-xl font-bold mb-half-lh">Homelab</h2> <p class="text-[var(--color-text-label)] leading-relaxed max-w-2xl mb-2lh">
<p class="text-[var(--color-text-label)] leading-relaxed max-w-2xl">
Personal infrastructure environment for learning, self-hosting, and Personal infrastructure environment for learning, self-hosting, and
operational practice. Running 24/7 on production-grade hardware with operational practice. Running 24/7 on production-grade hardware with
real network segmentation, SSO, monitoring, and IaC-style real network segmentation, SSO, monitoring, and IaC-style
documentation. documentation.
</p> </p>
</div>
<Widget title="At a Glance" as="div"> <dl class="flex flex-col mb-2lh">
<dl class="flex flex-col">
{glanceStats.map(({ label, value }) => ( {glanceStats.map(({ label, value }) => (
<div class="flex gap-2ch py-qtr-lh"> <div class="flex gap-2ch py-qtr-lh">
<dt class="text-[var(--color-text-dim)] w-[16ch] shrink-0">{label}</dt> <dt class="text-[var(--color-text-dim)] w-[16ch] shrink-0">{label}</dt>
@@ -56,10 +53,8 @@ const glanceStats = [
</div> </div>
))} ))}
</dl> </dl>
</Widget>
<Widget title="Services" as="div"> <div class="flex flex-wrap gap-x-[3ch] gap-y-qtr-lh mb-2lh">
<div class="flex flex-wrap gap-x-[3ch] gap-y-qtr-lh">
{categoryOrder.map((cat) => { {categoryOrder.map((cat) => {
const count = services.filter((s) => s.category === cat).length; const count = services.filter((s) => s.category === cat).length;
return ( return (
@@ -70,9 +65,7 @@ const glanceStats = [
); );
})} })}
</div> </div>
</Widget>
<div class="mb-4lh">
<p class="text-[var(--color-text-label)] mb-half-lh"> <p class="text-[var(--color-text-label)] mb-half-lh">
Full documentation — network maps, ADRs, runbooks, and service configs. Full documentation — network maps, ADRs, runbooks, and service configs.
</p> </p>
@@ -80,11 +73,11 @@ const glanceStats = [
href="https://gitea.lerkolabs.com/lerko/homelab" href="https://gitea.lerkolabs.com/lerko/homelab"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="underline hover:text-[var(--color-text-label)]" class="underline"
> >
gitea.lerkolabs.com/lerko/homelab → gitea.lerkolabs.com/lerko/homelab →
</a> </a>
</div> </Widget>
</section> </section>
<Footer slot="footer" /> <Footer slot="footer" />
+25
View File
@@ -12,6 +12,8 @@
--color-text: #D4D4D8; --color-text: #D4D4D8;
--color-text-label: #9CA0AA; --color-text-label: #9CA0AA;
--color-text-dim: #888D9B; --color-text-dim: #888D9B;
--color-link: #8CAFC8;
--color-link-visited: #9A94AB;
/* Typography */ /* Typography */
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
@@ -63,6 +65,8 @@ html {
--color-text: #2C2C2C; --color-text: #2C2C2C;
--color-text-label: #6B6560; --color-text-label: #6B6560;
--color-text-dim: #787068; --color-text-dim: #787068;
--color-link: #4A6B8A;
--color-link-visited: #6B6080;
} }
/* Link underlines */ /* Link underlines */
@@ -74,6 +78,16 @@ a {
a:hover { a:hover {
text-decoration-color: currentColor; text-decoration-color: currentColor;
} }
a[target="_blank"] {
color: var(--color-link);
}
a[target="_blank"]:visited {
color: var(--color-link-visited);
}
a[target="_blank"]:hover {
color: var(--color-link);
text-decoration-color: currentColor;
}
/* Focus states */ /* Focus states */
a:focus-visible { a:focus-visible {
@@ -94,6 +108,17 @@ button {
outline-color 120ms linear; outline-color 120ms linear;
} }
/* Section anchors — clear sticky nav, visual rhythm */
section[id] {
scroll-margin-top: 3.5rem;
padding-top: 2lh;
border-top: 1px solid var(--color-border);
}
section[id]:first-of-type {
border-top: none;
padding-top: 0;
}
@media print { @media print {
html { html {
font-family: var(--font-serif); font-family: var(--font-serif);