Static sites are supposed to be fast, simple, and reliable. But 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
This had a few 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
The core decision was simple:
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)
This one decision simplified everything else.
Fetching and aggregating blog data
Hashnode: canonical content
Hashnode remains the source of truth for:
I fetch all posts using Hashnode’s GraphQL API with pagination handled inside a Node.js script.
Dev.to: distribution and extra reach
Dev.to is where additional readers come from, 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
This is the tricky part. Articles are matched using a layered strategy:
Slug match
Canonical URL match
Title match
Once matched, the final view count becomes:
combinedViews = hashnodeViews + devtoViews
The output for each blog includes:
Writing the static data contract
All processed data is written to the static/blogs.json file.
This file is:
It also includes metadata like the last updated time and the total blog count.
This JSON file effectively replaces 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 all 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
Once all blog data is local, search becomes trivial.
I added:
Because the dataset is small and static:
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
It’s not suitable for real-time analytics
But for a personal blog, the trade-offs are worth it.
The key takeaways from the refactor that made me realize that:
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.