This is a personal site — somewhere to write, and somewhere people can find out who I am and what I work on. The constraint I gave myself was no hosting bill. That turned out to be a useful forcing function: it pushed everything toward clean, minimal architecture, where each component has exactly one job.

The about page has a chatbot on it. That was a deliberate choice, not a novelty — if someone wants to know whether I’ve worked with Kubernetes or led a platform team, they can just ask rather than scanning bullet points. It also gave me something interesting to build.

Everything runs on free tiers. GitHub Pages serves the static site. A Cloudflare Worker handles chatbot requests and proxies them to Groq’s API. Cloudflare KV handles rate limiting. No databases, no servers, no monthly charges.

Hugo with PaperMod handles the writing side. The about page opts out of PaperMod entirely and uses its own layout — the two parts of the site share a domain and a nav bar, but don’t otherwise interfere with each other.


Technical Design

Stack

LayerTool
Static siteHugo (PaperMod theme)
HostingGitHub Pages (custom domain karniks.net)
Chatbot backendCloudflare Worker
LLMGroq — llama-3.1-8b-instant
Rate limitingCloudflare KV
Bot protectionCloudflare Turnstile (invisible)
DiagramsMermaid (rendered client-side)

Hugo Structure

Hugo manages all pages. PaperMod’s profile mode runs the home page — a bio, social links, and a list of recent posts. The /posts/ list and individual post pages use PaperMod’s standard layouts with no customisation needed.

The /about/ page is different. It uses a completely custom layout (layouts/about/single.html) that renders a two-column container. PaperMod’s styles don’t apply here — the page loads its own resume.css independently.

content/
├── _index.md          ← home (profile mode)
├── about/
│   └── index.md       ← about markdown
└── posts/
    └── *.md           ← blog posts

layouts/
├── _default/
│   └── baseof.html    ← PaperMod base (with Mermaid hook)
└── about/
    └── single.html    ← custom two-column layout

Chatbot Architecture

Browser → POST /ask { question, turnstileToken }
  ↓
Cloudflare Worker
  ├─ CORS check (locked to production origin)
  ├─ Turnstile verification
  ├─ Per-IP rate limit: 10 req/hour (KV key: ip:{ip}:hour:{YYYY-MM-DD-HH})
  ├─ Global daily cap: 500 req/day (KV key: global:day:{YYYY-MM-DD})
  ├─ Input validation (non-empty, ≤ 500 chars)
  └─ Groq API call → return { answer }

The Worker is stateless — no conversation history. Each request is independent. The about page text is a hardcoded constant in the Worker; updating it requires redeploying with wrangler deploy.

The LLM prompt instructs the model to answer only from the content and decline questions it can’t answer from that context. Temperature is 0.3, max tokens 400 — enough for a useful answer, not enough to ramble.

Rate limiting uses non-atomic KV reads and writes, which is acceptable for a personal site. A concurrent burst could slightly exceed limits, but Durable Objects would be significant complexity for a marginal improvement.

Mermaid Support

A render-codeblock-mermaid.html partial wraps fenced mermaid code blocks in a <div class="mermaid"> container, and baseof.html conditionally loads the Mermaid ESM bundle when the page has at least one such block. Diagrams render client-side with no build-time dependency.

Free Tier Budget

ServiceFree limitActual usage
GitHub PagesUnlimited static
Cloudflare Workers100K req/day< 500/day (capped by KV)
Cloudflare KV100K reads, 1K writes/day~2 reads + ~1 write per request
Cloudflare TurnstileUnlimited
Groq (free tier)Per-minute token limitsWell within at personal-site traffic