Rebuilding My Static Blog with Build-Time Data and Instant Search

Published: (February 4, 2026 at 12:51 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

Cover image for Rebuilding My Static Blog with Build-Time Data and Instant Search

Static sites are supposed to be fast, simple, and reliable. Over time, my personal blog started behaving like a dynamic app—runtime API calls, pagination logic everywhere, and fragmented view counts spread across platforms.

Last week, I rebuilt the blog section of ravgeet.in (Nuxt.js) to fix this properly. The end result is still a static site, but now it feels alive: aggregated view counts, instant search and sorting, and zero runtime dependencies on external APIs.

This post walks through the thinking, architecture, and trade‑offs behind that rebuild.

The problem with my old setup

Originally, my blog worked like this:

  • Blog content lived on Hashnode (canonical source)
  • Some posts were also cross‑posted to Dev.to
  • Pages fetched blog data at runtime using Hashnode’s GraphQL API
  • Pagination logic (hasNextPage, cursors) lived inside the UI

Downsides

  • A static site depending on live APIs felt wrong
  • Local development and builds were slower and flaky
  • Adding features like search or sorting would require more APIs

I wanted the blog to stay static—but smarter.

Build‑time data as a contract

Move all external data fetching to build time, and treat the result as immutable static data.

Instead of fetching blogs at runtime, I introduced a build step that:

  • Fetches blogs from Hashnode
  • Fetches articles from Dev.to
  • Matches the same article across platforms
  • Aggregates view counts
  • Writes everything into a single JSON file

At runtime, the site only reads from that JSON.

Hashnode + Dev.to

Build‑time fetch & normalize

static/blogs.json

Nuxt UI (search, sort, views)

Fetching and aggregating blog data

Hashnode: canonical content

Hashnode remains the source of truth for:

  • Title, slug, content, tags
  • Publish date
  • Cover image
  • Base view count

All posts are fetched using Hashnode’s GraphQL API with pagination handled inside a Node.js script.

Dev.to: distribution and extra reach

Dev.to brings additional readers, so ignoring those views felt wrong. Using the Dev.to API (with a personal access token), I fetch all my articles and extract:

  • slug
  • canonical_url
  • page_views_count

Matching articles across platforms

Articles are matched using a layered strategy:

  1. Slug match
  2. Canonical URL match
  3. Title match

Once matched, the final view count becomes:

combinedViews = hashnodeViews + devtoViews;

The output for each blog includes:

  • Combined views
  • Platform‑specific views (for debugging)
  • Dev.to URL (if matched)

Writing the static data contract

All processed data is written to static/blogs.json. This file is:

  • Generated at build time
  • Git‑ignored
  • Treated as read‑only by the app

It also includes metadata like the last updated time and the total blog count, effectively replacing my entire blog API.

Replacing runtime APIs with static services

Previously, services/blogs.js made live GraphQL calls. After the refactor:

  • The service dynamically imports blogs.json
  • find, findOne, and search operate locally
  • No Axios, no pagination state, no network failures

From the UI’s perspective, nothing changed—but under the hood, everything became predictable.

Instant search and sorting

With all blog data local, search becomes trivial. I added:

  • Client‑side text search (title, brief, tags)

Sorting options

  • Published date (recent / oldest)
  • View count (most / least)

Because the dataset is small and static:

  • Search results are instant
  • No debouncing hacks
  • No loading states
  • Sorting is deterministic

This dramatically improves discoverability without introducing a search service.

Trade‑offs and lessons learned

This approach isn’t perfect:

  • Build time increases slightly
  • The JSON file grows over time
  • Not suitable for real‑time analytics

But for a personal blog, the trade‑offs are worth it. Key takeaways

  • Static doesn’t mean lifeless
  • Build‑time data pipelines are underrated
  • One clean data contract simplifies UI, UX, and performance

If you’re curious, the full implementation lives in the ravgeet.in repository.

Back to Blog

Related posts

Read more »

Replace Turbo confirm with native dialog

Rails, when using turbo‑links, ships with a built‑in confirmation dialog for destructive actions. You've probably used it countless times: erb The default turbo...