A Fresh Coat of Green
So. New site. Or, more accurately, the same site but actually working properly and with a couple of bits I’ve been meaning to add for ages.
I’m not going to pretend this was some massive ground-up rebuild since the skeleton of the site has been in place for a while now, and honestly I still love the cyberpunk aesthetic. Matrix rain, green-on-black, win. That’s not going anywhere for a while, What I did want to fix was the stuff that had been annoying me every time I opened the page.
Here’s what changed…
The GitHub activity graph was always hidden
The header has had a GitHub contributions graph in it for a while, pulled live from ghchart.rshah.org. The problem? It was sitting inside a <details> element, collapsed by default. So unless you actually clicked on “GitHub Activity”, you’d never see it.
<!-- old: hidden behind a click -->
<details class="terminal">
<summary>GitHub Activity</summary>
<div class="gh-graph">...</div>
</details>
The fix was straightforward. Pull it out of the <details> entirely and just render it as a plain div. Now when you land on the page, the contribution graph is right there in the header where it belongs.
<!-- new: always visible -->
<div class="terminal gh-open">
<div class="gh-graph">...</div>
</div>
Small change. Should have done it months ago.
The Projects section had a structural bug
This one was embarrassing. The third project card, the one linking to the post about how I built this site, was sitting completely outside its parent container in the HTML. Like, structurally adrift. It was rendering okay-ish in most browsers because browsers are forgiving like that, but it was misaligned and technically broken.
<!-- old: third card floating outside the list -->
</div>
</div>
<div class="item"> <!-- ← orphaned -->
...
</div>
</div>
</div>
Fixed it by making sure all three project cards live inside the same .list and .bd wrappers. Obvious in hindsight. The HTML is now actually valid, which feels good.
The biggest change: tabbed feeds
This is the one I’m most pleased with. Previously the left column had a single “Blog” card that pulled the four most recent posts from my blog. Functional, but a bit flat.
What I wanted was a way to separate content into distinct feeds without adding more cards and making the layout feel cluttered. Tabs felt like the right call.
The card now has two tabs: Home and Blog.
- Home pulls from the
Frontpagetag on the blog. This is going to be where I put stuff I actually want front and centre when people visit, rather than just chronologically whatever I posted last. - Blog merges two tag feeds,
BlogandJournal, deduplicates them, and sorts everything by date descending. So it’s one coherent stream regardless of which tag a post has.
The tab UI is purely vanilla JS, no libraries needed:
document.querySelectorAll(".tab-btn").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".tab-btn").forEach(t => {
t.classList.remove("active");
t.setAttribute("aria-selected", "false");
});
document.querySelectorAll(".tab-panel").forEach(p => {
p.classList.add("is-hidden");
});
btn.classList.add("active");
btn.setAttribute("aria-selected", "true");
const target = document.getElementById(btn.getAttribute("aria-controls"));
if (target) target.classList.remove("is-hidden");
});
});
The feeds themselves use a two-step fetch with a CORS proxy fallback, so they work even when direct requests get blocked:
const fetchText = async (url) => {
try {
const r = await fetch(url, { cache: "no-store" });
if (r.ok) return await r.text();
} catch {}
try {
const r = await fetch(
"https://api.allorigins.win/raw?url=" + encodeURIComponent(url),
{ cache: "no-store" }
);
if (r.ok) return await r.text();
} catch {}
throw new Error("Fetch failed");
};
Both feeds load in parallel on page load, so switching tabs is instant. No waiting around.
The merge and dedup logic for the Blog tab is worth calling out too. Pulling from two tag pages means you might get the same post appearing in both, so everything goes through a Set to strip duplicates before sorting:
const allPosts = [], seen = new Set();
for (const url of urls) {
for (const p of extractPosts(await fetchText(url), url)) {
if (!seen.has(p.url)) {
seen.add(p.url);
allPosts.push(p);
}
}
}
allPosts.sort((a, b) => parseDate(b.date) - parseDate(a.date));
Clean enough. Does the job.
Honestly, this is still a pretty simple site. One page, no framework, and it is still just HTML, CSS, with a tiny a bit of vanilla JS. It loads fast and it’s easy to maintain.
No more heavy CMS’ ever.