kuh-VEE-tha. Sanskrit for poetry.
I built kavitha because most Ghost themes ask you to pick a lane: blog or magazine, writing or portfolio. I wanted both — writing on one side, projects on the other — running on one Ghost install, with no fork and no second domain.
It's MIT, gscan-clean, native Handlebars + vanilla CSS + ~80 lines of vanilla JS. Repo: github.com/snehithkumar-d/kavitha.
Why another Ghost theme?
Ghost has a strong default theme (Source) and a healthy marketplace, so the bar for another theme is high. Two things kept pushing me to build my own.
One Ghost site, two collections. Most themes give you one feed: posts. If you want a separate Projects section, the common advice is "use a tag" — but tag pages don't get their own permalink semantics, their own RSS feed, or their own card layout. Some people run two Ghost sites behind a reverse proxy. That's a lot of operational tax for what should be a content split.
Writing posts and project posts have different layout needs. A blog post wants a generous reading column, a feature image, an author byline, comments. A project post wants metadata in a sidebar — status, stack, links — a different header treatment, often no byline. Themes that try to render both with one template end up mediocre at both.
kavitha solves both: one Ghost install, two collections — writing and projects — each with its own template (post.hbs vs post-project.hbs), each with its own RSS feed. The split is invisible to the writer (you just toggle one internal tag) but it's structural for visitors and for search engines.
What you get as a visitor
The home page is an asymmetric two-column grid. The left column is the hero — a terminal-style breadcrumb (~/home/snehith), the site title, and a short bio. The right column is a Status sidebar with Now, Based, Writing, Reading rows. Each row hides if you leave it empty in admin, so the sidebar gracefully collapses for sites that don't want a now-page-style block.
Below the hero: a terminal-style list of recent writing, a 2-up project grid, and a members-CTA block which hides itself if Ghost members are disabled.
The visible features:
- Light, dark, and accent. Default follows prefers-color-scheme, with a manual toggle in the nav. Accent is whatever the admin picks in Ghost Design → Brand. A small bootstrap script computes WCAG-aware foreground luminance so text on accent backgrounds is always readable.
- Native Ghost everything. Search, comments, members, paid tiers, magic-link auth via Portal — all wired through Ghost's official APIs. No third-party widgets.
- Three font families, self-hosted. Fraunces (serif), Geist (sans), Geist Mono — all woff2 with font-display: swap. Admin picks the body font.
- Reading polish. Skip-to-content link, top-of-page scroll-progress bar on posts, copy-to-clipboard buttons on every code block, X / LinkedIn / copy-link share buttons, related-posts block at the foot of every post. If a visitor has JavaScript disabled, everything except the theme toggle still works — layout, fonts, dark/light from prefers-color-scheme, all of it.
What you get as an admin
Ghost caps custom theme settings at 20. kavitha uses 19, leaving headroom. Grouped:
- Appearance — color scheme default (Auto/Light/Dark), body font (Serif/Sans/Mono), post image style (Full-width/Wide/Inline), show/hide author byline, show/hide post feature image.
- Section copy — blog section title, projects section title, members CTA title, members CTA body. Anything that would otherwise be a hardcoded string lives in admin.
- Hero & status sidebar — terminal handle (the ~/home/handle text), show-hero-card toggle (wraps the hero in an accent card vs plain terminal text), and four optional status rows: Now, Based, Writing, Reading.
- Footer — GitHub, Twitter, LinkedIn URLs, show-theme-credit toggle.
A few things I deliberately didn't expose as settings even though I could have: reading time (always shown), section subtitles (just use the title — most sites don't need both), and a footer signature (rare, easier to fork). Trimming to 19 was a forcing function — every setting has to earn its slot.
The projects collection: the trick that makes the theme work
This is the part that makes kavitha specifically a writer-and-builder theme, and it's the part most worth understanding if you want to fork it.
Ghost ships with one collection: posts. To get two, you do two things:
1. routes.yaml. Ghost lets you upload a routes.yaml that defines collections, routes, and templates. The shipped routes.yaml declares two collections: one at /writing/ (everything not tagged #project), one at /projects/ (only posts tagged #project). Each gets its own permalink pattern and its own RSS feed.
2. The #project internal tag. Ghost has two kinds of tags — public ones (visible in /tag/foo/ pages, in post metadata, in feeds) and internal ones (prefixed with #, hidden from public taxonomy). #project is internal, so it never leaks into the visitor's view of the site — but routes.yaml filters on it.
flowchart LR
A[Ghost post] --> B{routes.yaml<br/>filter}
B -- has #project tag --> C["/projects/{slug}/<br/>post-project.hbs"]
B -- no #project tag --> D["/writing/{slug}/<br/>post.hbs"]
C --> E["/projects/<br/>2-up grid index"]
D --> F["/writing/<br/>terminal-style list"]
C --> G["/projects/rss/"]
D --> H["/writing/rss/"]The workflow as a writer: open Ghost, write the post like normal, add the #project internal tag if it's a project, save. The split is automatic. Same admin, same Lexical editor, same cards — but the post lands in a different URL space and renders with a different template.
One gotcha worth flagging: don't create a Ghost Page with the slug projects, writing, tag, or author. Those are claimed by routes.yaml and a page with those slugs becomes orphaned.
Setup: two install paths
Path A — upload the zip (no GitHub needed):
- Download the latest kavitha.zip from the Releases page.
- Ghost admin → Settings → Design → Change theme → Upload theme → pick the zip → Activate.
- Ghost admin → Settings → Labs → Routes → upload routes.yaml (rename from routes.yaml.example first). This is what unlocks /writing/ and /projects/.
- Ghost admin → Settings → Design → Customize → set Terminal handle, social URLs, and any status sidebar text you want.
- Ghost admin → Settings → Navigation → add Home /, Writing /writing/, Projects /projects/, About /about/.
That's it. Refresh and the site is live.
Path B — auto-deploy via GitHub Actions:
This is the path I run on snehithkumar.com. Push to main, the theme rebuilds and uploads itself. Setup is two short steps:
flowchart TD
A[git push main<br/>or tag] --> B[GitHub Actions]
B --> C[npm ci]
C --> D[npm run fonts<br/>fetch woff2 files]
D --> E[npm run build<br/>postcss + esbuild]
E --> F[gscan validate]
F -- 0 errors --> G[zip → kavitha.zip]
F -- errors --> X[fail · don't deploy]
G --> H[POST to Ghost<br/>Admin API]
H --> I[Ghost activates<br/>new version]- In Ghost admin → Settings → Integrations → Add custom integration (name it "GitHub Actions") → copy the Admin API URL and Admin API Key.
- In GitHub repo → Settings → Secrets and variables → Actions → add GHOST_ADMIN_API_URL and GHOST_ADMIN_API_KEY from step 1.
Push to main or push a tag and the deploy workflow fires. It runs gscan first — if Ghost's official validator reports any errors, the deploy fails before it reaches your live site. That's the safety I wanted: I can't push a broken theme even if I try.
Tech notes: the boring choices that matter
- No framework. Handlebars, vanilla CSS, 80 lines of vanilla JS. No React, no Vue, no Tailwind, no build-time JS framework. The theme ships zero JS runtime tax to visitors.
- Self-hosted fonts. No Google Fonts request, no third-party connection on first paint. npm run fonts downloads Fraunces / Geist / Geist Mono into assets/fonts at build time.
- No FOUC. A single inline head script reads the saved theme + accent before first paint and sets the right CSS custom properties on html. It's the only inline script in the theme.
- gscan-clean. Ghost's official gscan reports zero errors and zero warnings. CI gates deploy on this.
- Every Lexical card styled. Image, gallery, bookmark, callout (all 9 color variants), toggle, button, product, file, audio, video, header, embed, blockquote-alt — including the wide and full-width modifiers. If Ghost ships it, kavitha styles it.
What's next
v0.1.2 (April 2026) added the polish pass: related posts, share buttons, code-copy, scroll progress, skip-to-content, font preloads.
v0.2 backlog (in CHANGELOG.md) is mostly developer-facing: restoring the live build pipeline in CI, adding syntax highlighting (Prism or Shiki), auto-generated table of contents on long posts, and OG image generation for posts without a feature image. None of that changes how the theme looks today.
Try it
- License: MIT
If you fork it for your own site or a client, the "built with kavitha" link in the footer is on by default but can be toggled off in admin. The MIT license only requires the LICENSE file ships with redistribution.
This site runs on kavitha. The deploy you're reading right now went out via the GitHub Actions flow above.