diff --git a/src/pages/index.astro b/src/pages/index.astro
index 56ad64a..91a4a49 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -8,13 +8,15 @@ import ProjectCard from "../components/ProjectCard.astro";
import PostList from "../components/PostList.astro";
import Contact from "../components/Contact.astro";
import { skills } from "../data/skills";
-import { getPosts } from "../lib/blog";
+import { getPosts, allTags } from "../lib/blog";
import { getCollection } from "astro:content";
const featuredProjects = (await getCollection("projects", (p) => p.data.featured)).sort(
(a, b) => a.data.order - b.data.order,
);
-const recentPosts = (await getPosts()).slice(0, 3);
+const allPosts = await getPosts();
+const recentPosts = allPosts.slice(0, 3);
+const postTags = allTags(allPosts);
---
@@ -44,6 +46,14 @@ const recentPosts = (await getPosts()).slice(0, 3);
Lessons from edge Kubernetes, GPUs, and running infrastructure like it matters —
written up as I go.
+ {postTags.length > 0 && (
+
+ )}
Read the blog →
@@ -72,7 +82,18 @@ const recentPosts = (await getPosts()).slice(0, 3);
}
.writing__intro {
max-width: 52ch;
- margin-bottom: var(--space-5);
+ margin-bottom: var(--space-4);
color: var(--text-dim);
}
+ .writing__tags {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: var(--space-6);
+ }
+ .writing__tags-label {
+ font-size: var(--step--1);
+ color: var(--text-faint);
+ }
diff --git a/src/scripts/anim.ts b/src/scripts/anim.ts
index 666e21a..9493fa9 100644
--- a/src/scripts/anim.ts
+++ b/src/scripts/anim.ts
@@ -72,9 +72,15 @@ function run(): void {
* bottom (the progress=1 boundary); the callback maps 0→1 cleanly both ways. */
const bar = document.querySelector(".scroll-progress");
if (bar) {
- scroll((progress: number) => {
- bar.style.transform = `scaleX(${progress})`;
- });
+ // Compute progress straight from scroll position so it reliably spans 0→1
+ // (Motion's default scroll progress topped out ~0.89 at the true bottom).
+ const setBar = (): void => {
+ const max = document.documentElement.scrollHeight - window.innerHeight;
+ const p = max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0;
+ bar.style.transform = `scaleX(${p})`;
+ };
+ scroll(setBar);
+ setBar();
}
/* 5. Nav — animated active-section underline + tactile hover. */
@@ -112,11 +118,18 @@ function initNav(): void {
);
if (sectioned.length) {
const setActive = (): void => {
- const line = window.innerHeight * 0.4; // section crossing the upper area is "active"
+ const vh = window.innerHeight;
+ const ref = vh * 0.45; // the section whose top has passed this reading line is "active"
let current: HTMLAnchorElement | null = null;
for (const a of sectioned) {
const sec = document.getElementById(a.dataset.navId as string);
- if (sec && sec.getBoundingClientRect().top <= line) current = a;
+ if (sec && sec.getBoundingClientRect().top <= ref) current = a;
+ }
+ // At the very bottom the last (often short) section may never push its top
+ // above the ref line — force it, so Contact lights up at the end instead of
+ // the highlight getting stuck on the previous section (Skills).
+ if (window.scrollY + vh >= document.documentElement.scrollHeight - 4) {
+ current = sectioned[sectioned.length - 1];
}
if (current !== active) {
if (active) under(active, false);