Skip to content
Supra Builds

I Rewrote Hashnode's Next.js Starter Kit in Astro — From 150 kB to ~15 kB of Client JS

Same CMS, same features, 90% less JavaScript. Here's the story, the technical decisions, and how you can deploy your own in under 2 minutes.

黃小黃

黃小黃

· 7 min read

I Rewrote Hashnode's Next.js Starter Kit in Astro — From 150 kB to ~15 kB of Client JS

Your blog doesn't need 150 kB of JavaScript.

I discovered this when I started using Hashnode. Their official Next.js starter kit worked fine out of the box — but something felt off. A blog that publishes a few articles a week was loading an entire React runtime, multiple JavaScript bundles, and a full client-side router. For what? Rendering text and images.

So I rewrote the entire thing in Astro. The result? A fully-featured blog frontend that ships ~15 kB of client-side JavaScript — a 90% reduction. Same features. Same CMS. Dramatically less code sent to your readers' browsers.

Here's the story, the technical decisions, and how you can deploy your own in under 2 minutes.

The Problem: A React Runtime for a Blog

Don't get me wrong — Hashnode's starter kit is well-built, and the team has done solid work with it. But there's a fundamental mismatch: a blog is mostly static content, yet the starter kit ships an entire React runtime to the browser.

When I first deployed it, I opened DevTools and looked at what was being loaded. For a page that's essentially an article with some images, the browser was downloading:

  • React + ReactDOM
  • The Next.js client-side router
  • Hydration logic
  • Various runtime utilities

All together, 150 kB+ of JavaScript — before any of my actual content loads.

Then I thought about what a blog post page actually needs to do on the client side:

  • Render text (HTML does this natively)
  • Display images (HTML does this natively)
  • Apply syntax highlighting (CSS can handle most of this)
  • Toggle dark mode (a few lines of vanilla JS)

There's also operational complexity. The starter kit uses SSR (Server-Side Rendering) or ISR (Incremental Static Regeneration), which means you need a Node.js server or a platform that supports edge functions. For a blog that publishes a few posts a week, this felt like overkill.

There had to be a lighter way to do this.

Why Astro?

Astro is built around a philosophy that aligns perfectly with content-heavy sites: ship zero JavaScript by default. Every page is pre-rendered to static HTML at build time. No framework runtime. No hydration. Just HTML, CSS, and your content.

The key concept is Islands Architecture. Instead of hydrating the entire page with a JavaScript framework, Astro lets you create small "islands" of interactivity — only the components that genuinely need JavaScript get it. Everything else stays as static HTML.

For a blog, this means:

  • Article content? Static HTML. Zero JS.
  • Navigation and layout? Static HTML. Zero JS.
  • Dark mode toggle? A tiny island with a few lines of vanilla JS.
  • Search modal? An island that loads only when triggered.

This isn't a trade-off. It's the right architecture for the job.

Astro is also framework-agnostic. If I ever need a React or Svelte component for something complex, I can drop it in as an island. But for this project, vanilla JS in Astro components was more than enough.

Key Architecture Decisions

GraphQL Client: Lightweight by Design

Hashnode's API is GraphQL-based. The Next.js starter kit typically pairs this with heavier clients. I chose graphql-request — a minimal GraphQL client with zero unnecessary dependencies. Since it only runs at build time in a static Astro site, it adds zero bytes to the client bundle.

The entire client setup is 16 lines:

// src/lib/client.ts
import { GraphQLClient } from 'graphql-request';

const GQL_ENDPOINT =
  import.meta.env.PUBLIC_HASHNODE_GQL_ENDPOINT || 'https://gql.hashnode.com';

export const gqlClient = new GraphQLClient(GQL_ENDPOINT, {
  headers: {
    'hn-trace-app': 'astro-starter-hashnode',
  },
});

export const PUBLICATION_HOST =
  import.meta.env.PUBLIC_HASHNODE_PUBLICATION_HOST || 'engineering.hashnode.com';

All GraphQL queries are organized in 11 dedicated files (845 lines total), covering everything from homepage posts to RSS feeds to search.

Static Output + Smart Prefetching

The Astro config is intentionally minimal:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  site: siteUrl,
  output: 'static',
  prefetch: {
    prefetchAll: false,
    defaultStrategy: 'hover',
  },
  vite: {
    plugins: [tailwindcss()],
  },
});

Two things to note:

  1. output: 'static' — Every page is pre-built as an HTML file. No server needed.
  2. defaultStrategy: 'hover' — When a user hovers over a link, Astro prefetches that page in the background. By the time they click, the page is already cached. This gives the feel of a SPA without any client-side router.

Tailwind CSS v4

Styling uses Tailwind CSS v4 with the @tailwindcss/typography plugin for beautiful article rendering. The entire CSS output compiles to a single 55.6 kB file — and that's CSS, not JavaScript. It doesn't block interactivity.

Building Features Without a Framework

Here's where it gets interesting. The Hashnode Next.js starter kit uses React for features like dark mode, search, and comments. I rebuilt all of them without any framework.

Dark Mode: CSS + localStorage

Dark mode doesn't need React state management. It needs a class toggle and a localStorage call:

// Inside Header.astro <script> tag
const themeToggle = document.getElementById('theme-toggle');
themeToggle?.addEventListener('click', () => {
  const isDark = document.documentElement.classList.toggle('dark');
  localStorage.setItem('theme', isDark ? 'dark' : 'light');
});

That's it. The initial theme is set by an inline script in the <head> (to prevent flash of wrong theme), and the toggle is this 4-line event listener. Tailwind's dark: variant handles all the styling.

Search: Vanilla JS + Hashnode's GraphQL API

The search modal was the most complex piece. In the Next.js version, this would be a React component with state management, effects, and possibly a state library. In Astro, it's a single .astro file with a <script> tag.

The implementation uses:

  • Debounced input (300ms) to avoid hammering the API
  • AbortController to cancel in-flight requests when the user types again
  • Keyboard shortcuts (Cmd/Ctrl + K to open, Escape to close)
  • Hashnode's searchPostsOfPublication GraphQL query for server-side search
// Search with debounce and abort control
input?.addEventListener('input', (e) => {
  clearTimeout(debounceTimer);
  const query = e.target.value.trim();
  debounceTimer = setTimeout(() => performSearch(query), 300);
});

async function performSearch(query) {
  if (searchAbort) searchAbort.abort();
  searchAbort = new AbortController();

  const res = await fetch(gqlEndpoint, {
    method: 'POST',
    signal: searchAbort.signal,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: searchQuery,
      variables: { first: 10, filter: { query, publicationId } },
    }),
  });
  // ... render results
}

No React. No state library. Just the DOM APIs that browsers have shipped for years.

Full Feature List

Every feature from the Next.js starter kit has been rebuilt — plus a few extras:

  • Dark Mode — System preference detection + manual toggle
  • SearchCmd/Ctrl + K shortcut, live GraphQL search
  • View Transitions — SPA-like page transitions with morph animations, no full-page reloads
  • Comments — Hashnode's native comment threads + optional Giscus integration
  • Newsletter — Built-in subscription form via Hashnode API
  • SEO — Open Graph tags, Twitter cards, canonical URLs, structured data
  • RSS Feed — Full-content RSS with content:encoded
  • Sitemap — Auto-generated XML sitemap
  • Analytics — Supports GA4, GTM, Fathom, Plausible, Umami, and more
  • Table of Contents — Auto-generated from post headings
  • Pagination — Cursor-based with numbered pages
  • Series & Tags — Dedicated archive pages
  • Responsive — Mobile-first with Tailwind CSS
  • Accessibility — Semantic HTML, ARIA labels, keyboard navigation

The Results

Here's the comparison with real data from the built output:

MetricNext.js Starter KitAstro Starter Hashnode
Client JS~150 kB+~15 kB
JS FilesMultiple React bundlesA few small scripts
Build OutputSSR / ISRFully static HTML
Framework RuntimeReact + ReactDOMNone
Server RequiredYes (Node.js)No (static hosting)
CSSCSS-in-JS + Tailwind1 file, 55.6 kB (Tailwind)

That ~15 kB includes Astro's View Transitions (for smooth, SPA-like page navigation with morph animations) and the prefetch script. It's not React. It's not a full client-side router. It's the minimal JS needed to make the experience feel polished — and it's still 10x less than what the Next.js version ships.

Performance comparison chart

Get Started in 2 Minutes

Want to try it? You can deploy your own Hashnode blog frontend right now.

Option 1: One-Click Deploy

Click one of these buttons to deploy instantly:

You'll be asked for one environment variable: your Hashnode publication host (e.g., yourblog.hashnode.dev). That's it.

Option 2: Run Locally

git clone https://github.com/supra126/astro-starter-hashnode.git
cd astro-starter-hashnode
npm install
npm run dev

Open http://localhost:4321. If you don't set a PUBLIC_HASHNODE_PUBLICATION_HOST, it defaults to Hashnode's engineering blog as demo content.

Multi-Site Support

Since this is a statically generated frontend, you can deploy multiple instances with different PUBLIC_HASHNODE_PUBLICATION_HOST values. Same codebase, different blogs.

If you find this useful, I'd appreciate a star on GitHub. Found a bug or have a feature request? Open an issue. Pull requests are welcome.


The web doesn't have a performance problem. It has a complexity problem. Most blogs don't need a JavaScript framework runtime. They need HTML, CSS, and a handful of interactive islands. Astro makes this the default, and the results speak for themselves: ~15 kB of JavaScript for a fully-featured blog with smooth page transitions — 10x less than the React equivalent.

Your readers — and their browsers — will thank you.

黃小黃

黃小黃

Full-stack product engineer and open source contributor based in Taiwan. I specialize in building practical solutions that solve real-world problems with focus on stability and user experience. Passionate about Product Engineering, Solutions Architecture, and Open Source collaboration.

More Posts

Your API Wasn't Built for AI Agents — Here's How to Fix It

Your API Wasn't Built for AI Agents — Here's How to Fix It

By 2026, over 30% of API traffic will come from AI agents rather than human-driven applications. That number will keep climbing. Here's the uncomfortable truth: most APIs were designed for human developers who read documentation, interpret ambiguous ...

黃小黃 黃小黃 · · 16 min