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:

AttributeTypeDescription
:block/titlestringSection heading text
:block/levellongHeading level (1-4)
:block/idstringHTML anchor ID for linking
:block/markdownstringMarkdown body content
:block/codestringCode block content
:block/langkeywordCode language (:clojure, :javascript, etc.)
:block/imagestringImage URL
:block/image-altstringImage alt text
:block/captionstringImage/code caption

Blog post metadata

AttributeRequiredDescription
:page/titlePage title (used in <title> tag)
:page/uriURL path (must start and end with /)
:blog-post/authorReference to author: {:person/id :author-id}
:blog-post/published-atPublication date: #inst "2026-01-15T10:00:00.000-00:00"
:blog-post/tagsKeywords: [:clojure :react]
:blog-post/descriptionCard and meta description
:blog-post/featured?Show as featured post on listing page
:blog-post/imageHero image URL
:open-graph/descriptionSocial 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:

  1. Edit a markdown file in content/
  2. Restart the REPL or re-eval (powerpack.ingest/ingest-all (user/get-powerpack))
  3. 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-frontendrelease-content → uberjar in the correct order.

Key source files

FilePurpose
src/clj/saas/content/config.cljPowerpack configuration, asset targets
src/clj/saas/content/ingest.cljContent ingestion (markdown → Datomic txes)
src/clj/saas/content/pages.cljPage rendering dispatch by :page/kind
src/clj/saas/content/assets.cljOptimus asset optimization middleware
src/cljc/saas/content/ui/layout.cljcHTML document layouts (base, content)
src/cljc/saas/content/ui/components.cljcShared blog components (navbar, cards)
src/cljc/saas/content/ui/blog_article.cljcBlog post page
src/cljc/saas/content/ui/blog_listing.cljcBlog listing page
src/cljc/saas/content/ui/content.cljcMarkdown rendering, TOC extraction
env/dev/clj/saas/content/core.cljDev integrant key (creates Datomic + ingests)
env/dev/clj/saas/content/handlers.cljDev content page handler
env/dev/clj/saas/content/export.cljStatic export entry point
env/prod/clj/saas/content/core.cljProd integrant key (config only)
resources/content-schema.ednDatomic schema for content entities
resources/public/scripts/content.jsTheme switching, TOC highlighting, smooth scroll
content/static-pages.ednStatic page declarations

Portfolio scenes

All blog components have portfolio scenes for visual development:

  • portfolio/src/saas/content/components_scenes.cljs — Cards, navbar, newsletter
  • portfolio/src/saas/content/blog_listing_scenes.cljs — Full listing page
  • portfolio/src/saas/content/blog_article_scenes.cljs — Article with blocks
  • portfolio/src/saas/content/author_scenes.cljs — Author profile page

View them at http://localhost:<portfolio-port>/ under the Blog folder.