Personal portfolio and blog. Built with ClojureScript, Replicant, and shadow-cljs.
Static SPA — no server. Blog content is embedded at compile time. Deployed to Netlify.
| Layer | Technology |
|---|---|
| UI | Replicant (hiccup-based, no virtual DOM) |
| Build | shadow-cljs via Babashka tasks |
| Markdown | marked + highlight.js (browser-side) |
| Validation | Malli (compile-time schema checks) |
| RSS | commonmark-java + clj-rss |
| Config | EDN (clj-yaml for frontmatter) |
| Hosting | Netlify with Image CDN |
- Java 21+
- Clojure CLI
- Babashka
- Node.js 18+
bb devThis starts an nREPL with shadow-cljs middleware. Connect your editor, then in the REPL:
(start!) ; starts shadow-cljs server + watches :app build
; dev server at http://localhost:3000Other REPL helpers: (stop!), (cljs-repl!), :cljs/quit.
For a standalone dev server without REPL:
bb watch ; shadow-cljs watch on port 3000Blog posts are markdown files with YAML frontmatter in content/blog/. Media (images) lives in content/media/.
bb import-notes /path/to/vault/ArticlesThe articles directory should contain:
blog/— markdown files with YAML frontmattermedia/(optional) — images referenced by posts
The import normalizes Obsidian-flavored markdown: converts [[wiki links]] to standard web links, rewrites relative media paths, and strips vault-internal sections.
---
date: 2026-02-20
tags:
- clojure
- architecture
rss-feeds:
- clojure
- all
repos:
- [repo-name, "https://github.com/..."]
---The filename becomes the post title and URL slug (e.g. Getting Started with Replicant.md becomes /blog/getting-started-with-replicant).
bb testUses Rich Comment Tests (RCT). All source files are .cljc so tests run on the JVM without a browser.
bb distProduces a dist/ directory with all static files:
index.htmlwith cache-busted JS reference- Optimized JS bundle (
:advancedcompilation) - CSS, media assets, RSS feeds
bb dist runs: bb build (shadow-cljs release + asset copy) -> bb rss (feed generation) -> static file assembly.
Netlify serves the dist/ directory. Config in netlify.toml handles SPA routing (fallback to index.html) and Image CDN optimization for media.
config.edn Site metadata, RSS feed config
build.clj tools.build entry point
bb.edn Babashka task definitions
shadow-cljs.edn shadow-cljs build config
netlify.toml Netlify deploy + image CDN config
content/
├── blog/ Markdown posts (YAML frontmatter)
└── media/ Images referenced by posts
src/loicb/me/
├── config.clj Reads config.edn, provides site-config macro
├── build/
│ ├── md.clj Post loader, posts-data macro, Malli schema
│ ├── rss.clj RSS feed generator
│ └── import.clj Obsidian markdown normalization
└── ui/
├── core.cljc Entry point, dispatch-of, rendering
└── core/
├── db.cljc Pure state updaters (db -> db)
├── history.cljc URL routing (state <-> path)
└── views.cljc Replicant components (defalias)
dev/
└── user.clj REPL helpers
| Task | Description |
|---|---|
bb dev |
Start nREPL with shadow-cljs middleware |
bb watch |
Start shadow-cljs dev server (standalone) |
bb test |
Run RCT tests on JVM |
bb import-notes <dir> |
Import articles from Obsidian vault |
bb copy-assets |
Copy media to public serving directory |
bb build |
Compile optimized JS bundle |
bb rss |
Generate RSS feeds |
bb dist |
Full production build |
bb clean |
Remove all build artifacts |
bb fmt-check |
Check code formatting |
bb fmt-fix |
Fix code formatting |
bb outdated |
Show outdated dependencies |
See LICENSE.