Blog & Content System
ShipClojure uses Powerpack to manage blog posts, static pages (Privacy Policy, Terms of Service), and other content. Content is written in markdown, indexed in an in-memory Datomic database, and rendered to static HTML using UIX components.
How it works
content/ ← Markdown & EDN source files
↓ powerpack.ingest
In-memory Datomic DB ← Pages, blocks, authors, tags
↓ saas.content.pages/render-page
UIX components (SSR) ← DaisyUI-styled blog UI
↓ uix.dom.server/render-to-static-markup
Static HTML ← Served by Ring or exported to files
In development, the middleware wrap-serve-powerpack-pages dynamically renders content pages from the Datomic DB on each request.
In production, pages are pre-exported as static HTML files and served from the classpath.
Content directory structure
content/
├── authors/
│ └── ovistoica.edn ← Author profiles
├── blog-posts/
│ └── my-post.md ← Blog posts in mapdown format
└── static-pages.edn ← Page declarations (blog listing, tags, ToS, etc.)
Writing a blog post
Blog posts use mapdown format — sections separated by --- lines with Clojure metadata.
Create a new file in content/blog-posts/:
--------------------------------------------------------------------------------
:page/title My Blog Post Title
:page/uri /blog/my-blog-post/
:blog-post/author {:person/id :ovistoica}
:blog-post/published-at #inst "2026-01-15T10:00:00.000-00:00"
:blog-post/tags [:clojure :tutorial]
:blog-post/featured? true
:blog-post/description A short description for cards and SEO.
:open-graph/description A short description for social sharing.
--------------------------------------------------------------------------------
:block/title Introduction
:block/level 2
:block/id introduction
:block/markdown
Your markdown content here. Supports **bold**, *italic*, [links](https://example.com),
code blocks, lists, images, and everything else markdown supports.
--------------------------------------------------------------------------------
:block/title Next Section
:block/level 2
:block/id next-section
:block/markdown
More content for the next section...
Block attributes
Each block (separated by --- lines) can have:
| Attribute | Type | Description |
|---|---|---|
:block/title | string | Section heading text |
:block/level | long | Heading level (1-4) |
:block/id | string | HTML anchor ID for linking |
:block/markdown | string | Markdown body content |
:block/code | string | Code block content |
:block/lang | keyword | Code language (:clojure, :javascript, etc.) |
:block/image | string | Image URL |
:block/image-alt | string | Image alt text |
:block/caption | string | Image/code caption |
Blog post metadata
| Attribute | Required | Description |
|---|---|---|
:page/title | ✅ | Page title (used in <title> tag) |
:page/uri | ✅ | URL path (must start and end with /) |
:blog-post/author | ✅ | Reference to author: {:person/id :author-id} |
:blog-post/published-at | ✅ | Publication date: #inst "2026-01-15T10:00:00.000-00:00" |
:blog-post/tags | Keywords: [:clojure :react] | |
:blog-post/description | Card and meta description | |
:blog-post/featured? | Show as featured post on listing page | |
:blog-post/image | Hero image URL | |
:open-graph/description | Social sharing description |
Adding an author
Create a file in content/authors/yourname.edn:
{:person/id :yourname
:person/given-name "Your"
:person/family-name "Name"
:person/full-name "Your Name"
:person/email "you@example.com"
:person/bio "A short bio about yourself."
:person/photo "/assets/images/avatars/yourname.png"}
Place your avatar image at resources/public/assets/images/avatars/yourname.png.
Auto-generated pages
The following pages are created automatically from your content:
/blog/— Blog listing with featured post and grid/blog/tags/— Tag cloud with post counts/blog/tag/<tag>/— Posts filtered by tag/blog/author/<author-id>/— Author profile with their posts/sitemap.xml— XML sitemap for search engines/blog/feed.xml— RSS feed
Static pages (Privacy Policy, ToS)
Static pages are declared in content/static-pages.edn. The Privacy Policy and Terms of Service entries include AI prompt comments — copy the prompt into ChatGPT/Claude, replace the placeholders with your business details, and paste the result back.
Development workflow
In dev mode (bb dev), content pages are served dynamically:
- Edit a markdown file in
content/ - Restart the REPL or re-eval
(powerpack.ingest/ingest-all (user/get-powerpack)) - Refresh the page in the browser
Useful REPL commands
;; Access the powerpack app
(user/get-powerpack)
;; Re-ingest all content after changes
(powerpack.ingest/ingest-all (user/get-powerpack))
;; Query the content DB
(require '[datomic.api :as d])
(d/q '[:find ?title ?uri
:where
[?e :page/title ?title]
[?e :page/uri ?uri]]
(d/db (:datomic/conn (user/get-powerpack))))
Building for production
Content export requires frontend assets to be built first (CSS/JS must exist for Optimus fingerprinting):
# Full release (handles ordering automatically):
bb release
# Or step by step:
bb release-frontend # Build CSS + JS
bb build:content # Export static HTML via Powerpack
bb copy:content # Copy HTML/XML to resources/public/
The Dockerfile handles this automatically — bb release runs release-frontend → release-content → uberjar in the correct order.
Key source files
| File | Purpose |
|---|---|
src/clj/saas/content/config.clj | Powerpack configuration, asset targets |
src/clj/saas/content/ingest.clj | Content ingestion (markdown → Datomic txes) |
src/clj/saas/content/pages.clj | Page rendering dispatch by :page/kind |
src/clj/saas/content/assets.clj | Optimus asset optimization middleware |
src/cljc/saas/content/ui/layout.cljc | HTML document layouts (base, content) |
src/cljc/saas/content/ui/components.cljc | Shared blog components (navbar, cards) |
src/cljc/saas/content/ui/blog_article.cljc | Blog post page |
src/cljc/saas/content/ui/blog_listing.cljc | Blog listing page |
src/cljc/saas/content/ui/content.cljc | Markdown rendering, TOC extraction |
env/dev/clj/saas/content/core.clj | Dev integrant key (creates Datomic + ingests) |
env/dev/clj/saas/content/handlers.clj | Dev content page handler |
env/dev/clj/saas/content/export.clj | Static export entry point |
env/prod/clj/saas/content/core.clj | Prod integrant key (config only) |
resources/content-schema.edn | Datomic schema for content entities |
resources/public/scripts/content.js | Theme switching, TOC highlighting, smooth scroll |
content/static-pages.edn | Static page declarations |
Portfolio scenes
All blog components have portfolio scenes for visual development:
portfolio/src/saas/content/components_scenes.cljs— Cards, navbar, newsletterportfolio/src/saas/content/blog_listing_scenes.cljs— Full listing pageportfolio/src/saas/content/blog_article_scenes.cljs— Article with blocksportfolio/src/saas/content/author_scenes.cljs— Author profile page
View them at http://localhost:<portfolio-port>/ under the Blog folder.