feat(site): migrate from Next.js to Astro
Build and Deploy / deploy (push) Successful in 1m42s

Replace Next.js 16 + React 19 with Astro 5. Same visual design,
same deploy pipeline, zero client-side framework.

- All components rewritten as .astro files
- Dark mode via inline scripts (no React context)
- Timeline animation via IntersectionObserver script
- Nav active state computed at build time
- Self-hosted Source Code Pro woff2 fonts
- Drop Font Awesome (icons were never loaded)
- Drop unused headshot PNG (1MB, unreferenced)
- Fix pfSense hardware refs (Netgate 1100, not N100)
- Output: 212KB static HTML vs 2.6MB before
- JS shipped: ~700 bytes inline vs ~130KB React runtime
This commit is contained in:
2026-05-18 20:07:24 -04:00
parent d34f9f136c
commit 0c5d9e03b1
47 changed files with 4898 additions and 6345 deletions
+88
View File
@@ -0,0 +1,88 @@
---
import Widget from "./Widget.astro";
import { timeline, type TimelineType } from "@/data/timeline";
const typeColor: Record<TimelineType, string> = {
career: "var(--color-timeline-career)",
education: "var(--color-timeline-education)",
cert: "var(--color-timeline-cert)",
project: "var(--color-timeline-project)",
homelab: "var(--color-timeline-homelab)",
};
const typeLabel: Record<TimelineType, string> = {
career: "career",
education: "education",
cert: "cert",
project: "project",
homelab: "homelab",
};
---
<Widget title="tyler/journey">
<ol class="relative border-l border-[var(--color-border)] ml-[2px] flex flex-col gap-0">
{timeline.map((entry) => (
<li data-tl-entry class="pl-[3ch] pb-2lh last:pb-0 relative">
<span
class="absolute -left-[7px] top-[3px] w-3 h-3 rounded-full border border-[var(--color-bg)] shrink-0"
style={`background-color: ${typeColor[entry.type]}`}
aria-hidden="true"
/>
<div class="flex items-center gap-1ch mb-half-lh">
<span class="font-mono text-sm text-[var(--color-text-dim)]">{entry.date}</span>
<span
class="font-mono text-sm px-1 border"
style={`color: ${typeColor[entry.type]}; border-color: ${typeColor[entry.type]}; opacity: 0.7;`}
>
{typeLabel[entry.type]}
</span>
</div>
<p class="font-mono text-sm font-semibold text-[var(--color-text)] mb-half-lh">
{entry.title}
</p>
<p class="font-mono text-sm text-[var(--color-text)] opacity-70 leading-relaxed mb-half-lh">
{entry.description}
</p>
{entry.tags && entry.tags.length > 0 && (
<div class="flex flex-wrap gap-x-1ch gap-y-half-lh">
{entry.tags.map((tag) => (
<span class="font-mono text-sm text-[var(--color-text-dim)]">
{tag}
</span>
))}
</div>
)}
</li>
))}
</ol>
</Widget>
<script>
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
const entries = document.querySelectorAll<HTMLElement>("[data-tl-entry]");
const observer = new IntersectionObserver(
(observed) => {
observed.forEach((entry) => {
if (entry.isIntersecting) {
(entry.target as HTMLElement).style.opacity = "1";
(entry.target as HTMLElement).style.transform = "translateY(0)";
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.15 },
);
entries.forEach((el) => {
el.style.opacity = "0";
el.style.transform = "translateY(8px)";
el.style.transition = "opacity 240ms linear, transform 240ms linear";
observer.observe(el);
});
}
</script>