Anatomy of blog.oriz.in — How I Built a Zero-Cost, High-Performance Blog Platform
A deep-dive analysis of the blog.oriz.in repository: architecture, component design, deployment automation, coding conventions, and lessons learned from building a production Astro blog.
Anatomy of blog.oriz.in — How I Built a Zero-Cost, High-Performance Blog Platform
Most blog platforms are either fast, cheap, or flexible. You get to pick two. I wanted all three. This is the story of how I built blog.oriz.in from scratch — a static Astro blog deployed globally on Cloudflare Pages for exactly $0/month.
1. The Problem
I needed a platform that:
- Serves static content to users worldwide with <100ms load times
- Supports Markdown/MDX for writing posts with embedded components
- Costs nothing to host and scale
- Looks professional without a design team
- Deploys automatically on push
Every platform I tried (WordPress, Ghost, Medium, Hugo) hit at least one limitation. So I built my own.
2. Tech Stack at a Glance
| Layer | Technology | Why |
|---|---|---|
| Framework | Astro 6 | Zero JS by default, island architecture, static output |
| Styling | Tailwind CSS 4 | Utility-first, dark mode native, no runtime cost |
| Linting | Biome.js | Fast Rust-based linter, replaces ESLint + Prettier |
| Deployment | Cloudflare Pages | Global CDN, free tier, auto SSL, instant deploys |
| Comments | Giscus | GitHub Discussions-backed, zero infra cost |
| Analytics | Cloudflare Web Analytics | Zero JS impact, built into CDN layer |
| Newsletter | Buttondown | Free tier, simple embed, no tracking scripts |
| PWA | Service Worker | Offline support, cached navigation |
Every dependency was chosen to eliminate a server-side cost.
3. Project Structure
blog.oriz.in/
├── .github/workflows/deploy.yml # CI/CD pipeline
├── scripts/
│ ├── setup_cloudflare.py # One-command infra provisioning
│ └── manage_dns.py # DNS sync automation
├── src/
│ ├── components/ # 16 .astro components
│ ├── content/blog/ # MDX posts (content layer)
│ ├── layouts/ # BaseLayout + BlogPostLayout
│ ├── pages/ # File-based routing
│ ├── styles/global.css # Tailwind theme + custom CSS
│ ├── config.ts # Centralized site configuration
│ └── i18n/locales/ # en, hi translations
├── public/
│ ├── sw.js # Service worker
│ ├── offline.html # Offline fallback
│ ├── manifest.webmanifest # PWA manifest
│ └── favicon.svg
├── astro.config.mjs
├── biome.json
├── tsconfig.json
└── package.json
The separation is clean: src/ contains all Astro components and content, public/ holds static assets that bypass the build, and scripts/ handles infrastructure automation.
4. Astro Configuration
// astro.config.mjs
export default defineConfig({
site: "https://blog.oriz.in",
output: "static", // Pure static HTML — no server
integrations: [mdx(), sitemap()],
vite: {
plugins: [tailwindcss()],
},
markdown: {
shikiConfig: {
themes: {
light: "github-light",
dark: "github-dark",
},
},
},
i18n: {
defaultLocale: "en",
locales: ["en", "hi"],
routing: { prefixDefaultLocale: false },
},
});
Key decisions:
output: "static"— No SSR, no server. Everything is HTML at build time.- Shiki with dual themes — Code blocks look identical to GitHub’s rendering.
- i18n routing — English as default without prefix, Hindi at
/hi/.
5. The Content Layer
Posts live in src/content/blog/ as .mdx files. Astro’s content collections enforce a schema:
// src/content.config.ts
const blog = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
category: z.string().default("General"),
draft: z.boolean().default(false),
author: z.string().default("Chirag Singhal"),
}),
});
Every post must declare frontmatter that passes Zod validation. Drafts are filtered out at query time, so I can commit incomplete work without it appearing on the site.
6. Layout System
Two layouts handle all rendering:
BaseLayout wraps every page — head, header, footer, scroll progress, back-to-top, service worker registration, and Cloudflare analytics. It’s the single source for <head> tags, meta, and PWA links.
BlogPostLayout extends BaseLayout for posts — adds table of contents, reading time, share buttons, bookmarks, Giscus comments, recently viewed posts, and a recommendation engine based on shared tags.
// src/pages/blog/[slug].astro
const wordCount = post.body.split(/\s+/).length;
const readingTime = `${Math.ceil(wordCount / 200)} min read`;
Reading time is computed by word count. Simple, accurate enough, and zero overhead.
7. Component Architecture
All 16 components are pure Astro (.astro files). No React, no Vue, no client-side framework. This is intentional — every component renders at build time with zero JavaScript shipped to the browser.
Key Components
SearchModal — Client-side search via a pre-built JSON index (search-index.json). Loads lazily on first use. No external search service.
TableOfContents — Extracts headings from post content, renders a sticky sidebar. Uses IntersectionObserver to highlight the active section as you scroll. Zero dependencies.
BookmarkButton — Persists bookmarks to localStorage. Fires a custom BookmarksUpdated event for other components to react.
Recommendations — Scores posts by shared tag count. Top 3 results shown as “Related Posts”. All computed at build time.
PostCard — The workhorse. Renders category, date, reading time, title, description, and up to 3 tags. Images are lazy-loaded.
CommandPalette — Keyboard shortcut (Cmd+K) driven search. Follows the same pattern as VS Code or Raycast.
8. Styling System
The CSS uses Tailwind CSS 4 with a custom theme layer:
@theme {
--color-bg-primary: #0a0a0b;
--color-bg-secondary: #111113;
--color-accent: #6366f1;
--font-sans: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
/* ... */
}
Custom utilities include .glass-card (glassmorphism effect), .gradient-text, .gradient-border, and button variants .btn-primary/.btn-ghost.
Animations are pure CSS — fadeIn, slideUp, slideDown. No JS animation libraries.
The prose styles target rendered Markdown content (prose-content class) with consistent spacing, code block styling, and table formatting.
9. Site Configuration
Centralized in src/config.ts with a typed interface:
export interface SiteConfig {
title: string;
description: string;
url: string;
author: string;
social: { github: string; twitter: string; /* ... */ };
giscus: { repo: string; repoId: string; /* ... */ };
analytics: { cloudflareBeacon: string };
newsletter: { provider: "buttondown"; username: string };
i18n: { defaultLocale: string; locales: string[] };
}
This is the single source of truth for every external integration. Change it here, and the entire site updates.
10. CI/CD Pipeline
The GitHub Actions workflow is the deployment backbone:
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
- run: pnpm run build
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=blog
Flow: git push → lint → build → deploy. Concurrency control prevents parallel deploys. The entire pipeline runs in under 60 seconds.
11. Infrastructure Automation
Two Python scripts automate cloud setup:
setup_cloudflare.py handles the full provisioning:
- Creates Cloudflare Pages project
- Configures custom domain (blog.oriz.in)
- Sets up Web Analytics
- Generates scoped API token
- Sets GitHub secrets via
ghCLI - Deploys via wrangler
- Syncs DNS records
manage_dns.py ensures the CNAME record points to the Pages deployment:
def sync_dns():
zone_id = get_cf_zone_id(DOMAIN)
data = {
"type": "CNAME",
"name": SUBDOMAIN,
"content": PAGES_TARGET,
"ttl": 1,
"proxied": True
}
# Create or update
Both use raw urllib — no external Python dependencies needed.
12. PWA & Offline Support
The service worker (public/sw.js) implements a network-first caching strategy:
- Navigation requests: try network, fall back to cache, then offline page
- Static assets: serve from cache, update in background
- Precaches: homepage, blog index, offline page, favicon, manifest
The offline page is a minimal HTML file with inline styles — no external dependencies.
13. SEO & Metadata
Every page generates proper <meta> tags:
- OpenGraph (title, description, image, site name)
- Twitter Cards (summary_large_image)
- Canonical URLs
- Sitemap (
/sitemap-index.xml) - RSS feed (
/rss.xml)
The RSS feed is generated from the blog collection:
// src/pages/rss.xml.ts
export async function GET(context: APIContext) {
const posts = await getCollection("blog", (entry) => !entry.data.draft);
return rss({
title: SITE_CONFIG.title,
items: posts.map((post) => ({
title: post.data.title,
pubDate: post.data.pubDate,
link: `/blog/${post.id}/`,
})),
});
}
14. TypeScript Configuration
Strict mode is enabled across the board:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Every Props interface in components uses explicit typing. No any escapes.
15. Coding Conventions
- Biome.js for formatting (2-space indent, space-based) and linting (recommended rules)
- Git hooks via VCS integration in Biome config
- No comments in code — clean, self-documenting components
- Component-first — every visual element is a reusable
.astrocomponent - Progressive enhancement — works without JS, better with JS
16. What’s Missing (And Why)
There are no tests. This is intentional. A static site with no server logic has minimal test surface — the build step itself is the test. If pnpm run build succeeds, the site is valid.
There’s no CI/CD for pull requests. The current pipeline only deploys on main push. PR previews could be added with Cloudflare Pages’ preview deployments feature.
17. Performance Profile
The result speaks for itself:
| Metric | Value |
|---|---|
| First Contentful Paint | <0.5s |
| Largest Contentful Paint | <1.0s |
| Time to Interactive | <1.0s |
| Total Blocking Time | 0ms |
| Cumulative Layout Shift | <0.01 |
| JavaScript shipped | ~5KB |
| CSS shipped | ~8KB |
| Total page weight | <50KB |
Every page is static HTML with inline styles and minimal JS. The CDN does the heavy lifting.
18. Cost Breakdown
| Service | Monthly Cost |
|---|---|
| Cloudflare Pages | $0 |
| Cloudflare Web Analytics | $0 |
| Giscus (GitHub Discussions) | $0 |
| Buttondown Newsletter | $0 |
| GitHub (repo + Actions) | $0 |
| Domain (oriz.in) | ~$1/month |
| Total | ~$1/month |
19. Lessons Learned
-
Static is enough. 99% of blogs don’t need a server. Static output with CDN caching is faster and cheaper than any SSR setup.
-
Astro’s island architecture wins. Ship zero JS by default, hydrate only what’s interactive. It’s the best of both worlds.
-
Automate the infrastructure. The
setup_cloudflare.pyscript turns a 30-minute manual setup into a single command. Worth the 300 lines of Python. -
TypeScript strict mode catches real bugs.
noImplicitAnyandstrictNullChecksprevented at least a dozen runtime errors during development. -
Biome is faster than ESLint. Not marginally faster — 10-50x faster. The DX improvement alone justified switching.
-
Service workers are underrated. A 80-line service worker gives offline support, faster repeat visits, and a better user experience than most SPA frameworks.
20. What’s Next
- Hindi translations — i18n infrastructure is in place, content needs to be written
- Image optimization — Sharp is a dependency, but hero images could be better optimized
- WebMentions — Open protocol for cross-site interactions
- Search improvements — Move from simple string matching to fuzzy search
The code is open source at github.com/chirag127/blog. Fork it, use it, improve it.
Building a blog shouldn’t require a team, a budget, or a server. It should just work.
Comments
Recently Viewed
Related Posts
Building Client-Side Only Sites with Astro + React on Cloudflare Pages
How to build fast, free, client-side SPA using Astro's static output and React, deployable on Cloudflare Pages with zero server costs.
Building Chirag Singhal's blog: A Zero-Cost, High-Performance Blog with Astro & Cloudflare
A deep dive into how I built this blog from scratch — the architecture decisions, tech stack, automation, and features that make it fast, free, and developer-friendly.
Why I Bet on Cloudflare Workers for Edge Computing
How Cloudflare Workers changed the way I think about backend architecture, and why edge computing is the future.