Moving Off Hashnode: Self-Hosting Everything on Astro + Cloudflare

Moving Off Hashnode: Self-Hosting Everything on Astro + Cloudflare

#self-hosting#astro#cloudflare#blogging#seo#jamstack

I moved this blog off Hashnode. It now lives in a Git repo as a pile of Markdown files, builds with Astro, and ships from Cloudflare Pages — profile and blog on one domain, faizahmed.in. No platform, no CDN I don’t control, no cookie banner.

Hashnode is a genuinely good product and it served me well for years. This isn’t a “Hashnode bad” post. It’s a “I wanted to own the whole stack” post — and a practical log of how to do the move without nuking your SEO, which was my one real fear.

Why I left

A few itches that finally added up:

  • Ownership. My posts are now plain Markdown in Git. The presentation, the URLs, the build — all mine. No lock-in, no export-and-pray.
  • Performance. No UI framework. Astro renders static HTML; a little vanilla JS powers the interactive bits (theme toggle, ⌘K search, newsletter, code-copy), and Astro’s View Transitions give instant, app-like navigation.
  • Privacy. I dropped Google Analytics for cookieless analytics. No cookies means no consent banner and a much smaller privacy footprint.
  • No external dependencies. Every image and the font are self-hosted. The page doesn’t phone home to a third-party CDN to render.
  • Customisation. A sticky table of contents, terminal-style code blocks, a magazine feed, series and tag pages — all things I can change in an afternoon instead of filing a feature request.

”Self-host everything” — what that actually meant

This was the fun part, and the part most people skip.

Images. Hashnode serves post images from cdn.hashnode.com. If I left them hot-linked and later deleted my Hashnode account, every image would 404. So the importer downloads every image — covers and in-body, including the Unsplash ones — into each post’s folder, then a script downscales them. The covers used to be 1–2 MB PNGs; now they’re ≤1600px and optimised to WebP at build time.

Fonts. Ubuntu Mono is self-hosted (bundled into the build), not pulled from Google Fonts. That kills a render-blocking request and a third-party connection.

Analytics. Umami — cookieless and consent-exempt — proxied first-party through a Cloudflare Pages Function, so it loads from my own domain (ad-blockers can’t drop it) and no data goes to a third-party beacon. A short privacy page covers it.

Newsletter. I run my own instead of renting an audience: a Cloudflare Pages Function takes the signup and Resend delivers the emails, with a proper one-click unsubscribe. No third-party embed, no tracking pixels — I own the list.

The only things still loaded from outside are the YouTube embeds (click-to-load — no request until you actually press play) and the Mermaid runtime on the few posts with diagrams. Everything else comes from my own origin.

The scary part: not breaking SEO

My posts ranked at blog.faizahmed.in. Throwing that away would have been dumb. The trick is to treat it as a site move, not a delete-and-recreate:

  1. Keep the slugs identical. Each imported post uses the exact slug it had on Hashnode, so blog.faizahmed.in/some-post maps 1:1 to faizahmed.in/some-post.

  2. 301-redirect, path-preserved. A single Cloudflare rule sends every old URL to the new one, permanently:

    blog.faizahmed.in/*  →  301  →  https://faizahmed.in/${1}
  3. Flip the canonical. Every page’s <link rel="canonical"> now points at the apex. (Leaving it on the old host while that host redirects away is a classic self-inflicted wound — don’t.)

  4. Tell Google. Verify the new property, submit the sitemap, and run Change of Address in Search Console.

A 301 passes essentially all ranking signal to the new URL. Expect a small wobble for a week or two while the engines reprocess — then it settles.

The stack

  • Astro with the Content Layer API — Markdown posts, no client framework, static output.
  • Markdown in Git — frontmatter for title, date, slug, tags, series. Writing a post is creating a folder and a file.
  • Cloudflare Pages — free static hosting, deploys on every push to main.
  • Cloudflare Pages Functions — a first-party Umami analytics proxy and the newsletter (Resend) signup, both served from my own domain.
  • Self-hosted assets + a _headers file with a real Content-Security-Policy and HSTS for good measure.

A new post is just:

---
title: "My Next Post"
datePublished: 2026-06-20
slug: my-next-post
tags: nodejs, aws
series: null
---

Write here.

What I gave up (being honest)

Self-hosting isn’t free of trade-offs:

  • The editor. Hashnode’s writing experience is nice. I now write in my editor and push to Git. I prefer it, but it’s a different workflow.
  • The built-in audience. Hashnode surfaces you to its community and feed; a standalone site starts cold and has to earn its traffic. (Email subscriptions I rebuilt myself — see the newsletter above.)
  • Comments. Gone for now. Adding them back means a third-party widget — which fights the “no external scripts” goal — so I’m leaving them off.

For me the trade was worth it: a faster, private, fully-owned site I can shape however I want, with the back catalogue’s SEO intact. If you’ve been thinking about the same move, the only part worth sweating is the redirects — get those right and the rest is just a build step.

Get new posts by email

Backend, auth, and shipping compliant systems. No spam, unsubscribe anytime.