Overview

Why I Rebuilt My Website (Again)

January 17, 2026 | 8 min read

I already had a perfectly functioning website.

It loaded fast. It displayed my posts. It had dark mode. It did everything a personal site is supposed to do. So naturally, I spent the last few weeks rebuilding it from scratch.

If you’re reading this, you’re on v4. The previous version will live forever at v3.screenager.dev — a monument to “good enough.” This post is about why good enough stopped being good enough.

The Trigger

I stumbled upon astro-erudite by enscribe while browsing GitHub at 2 AM. I came across his beautiful portfolio a few years ago, so been following him ever since. His accompanying post, “The State of Static Blogs in 2024,” hit me like a truck:

There should not be a single reason why you would need a command palette search bar to find a blog post on your own site.

That line broke something in my brain. I looked at my existing site — the command palette I’d spent hours implementing, the analytics dashboard I checked twice a year, the share buttons nobody clicked — and realized I’d been building features for an imaginary audience instead of building something I actually wanted to use.

enscribe’s philosophy was refreshing: kill the slop. No ESLint pre-commit hooks enforcing arbitrary rules on your own blog posts. No Umami analytics unless you’re actually making decisions based on the data. No Giscus comments unless you want to moderate spam. No .env files in a static blog.

If you have literally anything involving an .env file in a blogging site, maybe think about what you are doing for a moment.

I forked the template. Then I spent the next few weeks making it mine.

What Changed

The Loader That Sets the Tone

When you first land on the site, you don’t see the homepage. You see this:

╔═╗╔═╗╦═╗╔═╗╔═╗╔╗╔╔═╗╔═╗╔═╗╦═╗
╚═╗║ ╠╦╝║╣ ║╣ ║║║╠═╣║ ╦║╣ ╠╦╝
╚═╝╚═╝╩╚═╚═╝╚═╝╝╚╝╩ ╩╚═╝╚═╝╩╚═

ASCII art, typewritten character by character, cursor blinking. Then it smoothly translates to its final position as the page fades in.

Is this necessary? Absolutely not. But it tells you something about the site before you read a single word. It says: this is a place where someone cares about the small things.

Hover over it after it loads. The characters light up in a wave, glowing in the primary color. I wrote that animation at 3 AM and refuse to remove it.

The Dock

At the bottom of the screen sits a floating navigation dock. Not a navbar — a dock. Liquid glass aesthetic with layered transparency, lensing effects, and that satisfying macOS-style blur.

backdrop-filter:
blur(var(--glass-blur))
saturate(var(--glass-saturate))
contrast(1.05)
brightness(1.03);

The active page indicator slides smoothly between items on navigation. The theme toggle triggers a circular reveal animation that expands from the button itself. Scroll down on a blog post and a “back to top” button animates in from nothing.

Every interaction has been considered. Not because users demanded it, but because I wanted it.

Live Competitive Programming Stats

Here’s where it gets interesting.

The homepage has a stats grid that pulls live data from three APIs:

  • Codeforces: Current rating, rank (with color), contest history chart, delta from last contest
  • LeetCode: Rating, problems solved, streak, activity heatmap
  • GitHub: Repos, stars, followers, contribution heatmap

Click on any card and they swap positions with a GSAP Flip animation. The main card expands to show detailed stats; the others compress to thumbnails.

The Codeforces chart draws itself with a progressive reveal animation — each data point appears slightly after the last, creating that satisfying “drawing” effect. But here’s the thing: it only does that animation once per session. Refresh the page, navigate away and back — the chart appears instantly. Because the first impression matters, but subsequent loads shouldn’t waste your time.

const shouldAnimate = !sessionStorage.getItem(chartAnimatedKey)
drawRatingChart(history, grid, shouldAnimate)
if (shouldAnimate) {
sessionStorage.setItem(chartAnimatedKey, 'true')
}

The Tech Stack Marquee

Below the stats, there’s an infinite horizontal scroll of tech icons. Standard stuff, right?

Wrong.

Hover over the marquee and it smoothly decelerates to a stop. Move your cursor around — the icons are magnetically attracted to it. They scale up, glow in their brand colors, and show labels. Click one and it explodes into particles: tiny copies of the icon flying outward, dot particles scattering, then the icon reassembles with an elastic bounce.

// Icon explosion effect (for click)
function explodeIcon(particleContainer, source, color) {
// Hide original icon temporarily
gsap.to(iconEl, {
scale: 0,
duration: 0.15,
ease: 'power2.in'
})
// Create icon particles exploding outward
for (let i = 0; i < particleCount; i++) {
// ... spawn mini-icons flying in all directions
}
// Restore icon with bounce
gsap.to(iconEl, {
scale: 1,
duration: 0.5,
delay: 0.3,
ease: 'elastic.out(1, 0.4)'
})
}

This serves zero practical purpose. I love it.

Scramble Text

Certain keywords in the intro paragraph have a hover effect. Mouse over “compilers” or “algorithms” and the text scrambles through random characters before resolving back:

algorithms → @#$%^&*! → a#g%r!t^s → algorithms

GSAP’s ScrambleTextPlugin handles this. It’s subtle, it’s unnecessary, and it makes me smile every time.

The Blog Post Page

This is where the actual content lives, so I restrained myself. Mostly.

Table of Contents: On desktop, a sticky TOC lives in the left margin. It uses IntersectionObserver to highlight headings as you scroll — including parent headings when their children are visible. Scroll the page and the TOC scrolls to keep the active item centered. On mobile, it collapses into a header dropdown.

Subposts: For longer series, posts can have child posts. The navigation sidebar shows all parts, tracks your progress, and lets you jump between them. Reading time shows both the current part and the total for the series.

Cover Images: Each post can have a featured image that appears in the header and gets used for Open Graph cards. The Image component from Astro handles optimization automatically.

Breadcrumbs: Every page except the homepage has a breadcrumb trail. Sounds boring. Is boring. But it’s SEO-friendly and helps users understand where they are.

KaTeX: Full LaTeX\LaTeX support for when I want to write about algorithms:

T(n)=aT(nb)+f(n)T(n) = aT\left(\frac{n}{b}\right) + f(n)

Expressive Code: Code blocks with syntax highlighting, line numbers, diffs, collapsible sections, and file titles. The works.

The RSS Feed

Most RSS feeds look like raw XML. Mine looks like this:

/* Design system colors matching global.css */
:root {
--background: oklch(0.94 0.00011 271.152);
--foreground: oklch(0.12 0 0);
--primary: oklch(0.60 0.14 155);
/* ... */
}

An XSL stylesheet transforms the RSS XML into a styled HTML page that matches the site’s design system. Same fonts, same colors, same spacing. If someone opens the feed URL in a browser, they see a beautiful page explaining what RSS is and listing all posts — not angle brackets and namespace declarations.

The subscribe page (/subscribe) explains RSS to newcomers, provides copy-to-clipboard functionality, and links to popular readers like Feedly and Inoreader. Because RSS deserves better than it’s been treated.

The CRT Effect

Look closely at the background. There’s a subtle scanline overlay — barely visible horizontal lines that create a slight CRT monitor effect:

body::after {
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.015) 0px,
transparent 1px,
transparent 2px,
rgba(0, 0, 0, 0.015) 3px
);
}

It’s 1.5% opacity. Most people won’t consciously notice it. But they’ll feel something slightly different about the site. That’s the idea.

Theme Transitions

Click the theme toggle. The new theme reveals itself in an expanding circle from the button, like ink spreading through water:

[data-theme-transition="to-light"]::view-transition-new(root) {
clip-path: circle(0% at var(--theme-toggle-x) var(--theme-toggle-y));
animation: theme-expand 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

View Transitions API makes this surprisingly easy. The position is calculated from the button’s center coordinates.

What I Kept From the Original (old version)

The foundation is solid:

  • shadcn/ui for the theming convention. bg-background text-foreground instead of bg-stone-50 dark:bg-stone-900.
  • Tailwind for styling. No surprises here.
  • MDX for blog posts. Components inside markdown.
  • View Transitions for smooth page navigation.

What I Removed

Per enscribe’s philosophy:

  • Command palette search
  • Analytics
  • Comment system
  • Share buttons
  • Newsletter signup
  • Everything requiring an .env file

The site is static. Builds once, deploys to edge, loads instantly. No database. No server-side anything. No tracking. No cookies.

The Font

JetBrains Mono Nerd Font everywhere. Monospace for code, monospace for prose, monospace for navigation. The site is a terminal. The terminal is the site.

Why This Matters (Or Doesn’t)

I don’t have thousands of readers. I don’t monetize this. I don’t need analytics to optimize conversion funnels because there are no funnels.

I built this because building it was fun. Because I wanted to see if I could make an ASCII art loader feel right. Because I wanted my RSS feed to be beautiful. Because I wanted a dock that felt like it belonged on a high-end laptop’s UI.

This is the website I wanted to visit. Now I get to.

If you’re thinking about rebuilding your own site: don’t do it for engagement metrics or SEO optimization or because your current stack is “outdated.” Do it because you want to. That’s reason enough.


Credits

This site is built on astro-erudite by enscribe, whose template and philosophy shaped everything here. If you want to build something similar, start there.

$ git clone https://github.com/jktrn/astro-erudite

Then make it yours.