# Blog & Content System

ShipClojure uses [Powerpack](https://github.com/cjohansen/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](https://github.com/magnars/mapdown) format — sections separated by `---` lines with Clojure metadata.

Create a new file in `content/blog-posts/`:

```markdown
--------------------------------------------------------------------------------
: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`:

```clojure
{: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

```clojure
;; 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):

```bash
# 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, 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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://shipclojure.gitbook.io/shipclojure-docs/backend/blog.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
