# Journal CMS

**Perfectly Paced Journeys · Content Authoring Tool**

A standalone HTML/CSS/JS content management system for writing, managing, and publishing self-contained journal posts directly to a GitHub repository. No server, no framework, no build step — open the file in any browser and connect your repo.

---

## Contents

- [Purpose](#purpose)
- [Architecture](#architecture)
  - [Core Engine](#core-engine)
  - [Niche Packs](#niche-packs)
  - [Themes](#themes)
- [Installation](#installation)
  - [Step 1 — Create a GitHub Personal Access Token](#step-1--create-a-github-personal-access-token)
  - [Step 2 — Open the CMS and Connect](#step-2--open-the-cms-and-connect)
- [Running the CMS](#running-the-cms)
- [Using the Editor](#using-the-editor)
  - [Creating a Post](#creating-a-post)
  - [Module System](#module-system)
  - [Template Picker](#template-picker)
  - [Publishing](#publishing)
  - [Images](#images)
  - [Site Settings](#site-settings)
  - [Branding](#branding)
- [Post Format](#post-format)
- [Post Index and build.js](#post-index-and-buildjs)
- [Deploying to Cloudflare Pages](#deploying-to-cloudflare-pages)
- [Module Reference — Travel Niche](#module-reference--travel-niche)
- [Module Reference — Finance Niche](#module-reference--finance-niche)
- [Module Reference — Real-Estate Niche](#module-reference--real-estate-niche)
- [Module Reference — Legal Niche](#module-reference--legal-niche)
- [Troubleshooting](#troubleshooting)

---

## Purpose

The Journal CMS is an authoring tool that produces self-contained HTML post files deployable to any static host (Cloudflare Pages, GitHub Pages, Netlify) with no server-side processing required.

Each post is a single `.html` file that:
- Renders fully without JavaScript
- Contains all styles inline (no external CSS dependencies)
- Embeds its own metadata as a JSON `<script>` block for round-trip editing
- Carries a hidden DRM fingerprint (`<meta name="ppj-content-id">`) for license verification

---

## Architecture

The CMS is split into a **Core Engine** and a **Niche Pack**. The engine handles every UI concern (sidebar, editor, modules, images, branding, publishing). The niche pack supplies what is content-specific: module definitions, form renderers, post renderers, and the post stylesheet.

```
journal-cms.html              ← Internal PPJ version (dark gold theme, travel niche)
journal-cms-travel.html     ← Commercial distribution (light theme, travel niche)
journal-cms-finance.html      ← Commercial distribution (navy theme, finance niche)

journal-cms/
  cms.css                ← Structural styles (layout, components — no colors or fonts)
  themes/
    inkwell.css          ← PPJ internal editor skin (warm gold, dark — Cormorant/Jost)
    parchment.css        ← Travel buyer editor skin (cream, light — Lora/Inter)
    obsidian.css         ← Finance buyer editor skin (near-black, dark — IBM Plex)
  niches/
    travel/              ← Travel niche pack
    finance/             ← Finance niche pack
  js/                    ← Core engine (see below)
```

---

### Core Engine

`journal-cms/js/` — 12 JavaScript modules that handle all CMS functionality. These are niche-agnostic: they never contain hard-coded module types, templates, or category names.

| Module | Responsibility |
|---|---|
| `main.js` | DOMContentLoaded init. Reads `data-niche` from `<html>`, dynamically imports the niche pack, sets `state.niche`, calls `pack.exposeGlobals()`. |
| `state.js` | Shared mutable app state. `state.niche` holds the loaded niche pack. |
| `constants.js` | Post HTML markers (`_cm`/`_ce`), font lists, `generateContentId()`. Not niche-specific. |
| `utils.js` | `escHtml`, `toast`, `setStatus`, `formatDate`, `toSlug` |
| `api.js` | GitHub Contents API: read/write/delete posts, images, config, and `post-index.json` |
| `config.js` | GitHub auth setup, localStorage persistence, Site Settings modal |
| `sidebar.js` | Post list sidebar (reads `state.niche` for category labels) |
| `editor.js` | Field population, preview sync, slug/position controls |
| `builder.js` | Assembles post HTML: calls `state.niche.collectModuleData()`, embeds `post.css` from `state.niche.postCssPath`, applies branding overrides |
| `modules.js` | Module card UI: add, remove, reorder, source cycling |
| `posts.js` | Post lifecycle: open, new, save, publish, delete, duplicate. Template picker (reads `state.niche.TEMPLATES`). |
| `images.js` | Image manager: upload to GitHub, Unsplash search, image library |
| `styles.js` | Branding modal: accent color, heading font, body font, live preview |

---

### Niche Packs

`journal-cms/niches/<niche>/` — each niche is four files:

| File | What it defines |
|---|---|
| `config.js` | `CATEGORIES`, `MODULE_DEFS`, `TEMPLATES`, scorecard dimensions, source cycle labels |
| `form-renderer.js` | Form field HTML for each module type. `exposeGlobals()` puts add-block handlers on `window`. |
| `post-renderer.js` | `collectModuleData()` reads form field values. `renderModuleHtml()` generates published HTML. |
| `pack.js` | Re-exports everything from the other three files. Exports `postCssPath` (path to the niche's post stylesheet). |

`main.js` loads the correct pack by reading `data-niche` from `<html>`:

```html
<!-- PPJ internal niche -->
<html lang="en" data-niche="ppj">

<!-- Commercial travel niche -->
<html lang="en" data-niche="travel">

<!-- Commercial finance niche -->
<html lang="en" data-niche="finance">

<!-- Commercial real-estate niche -->
<html lang="en" data-niche="real-estate">

<!-- Commercial legal niche -->
<html lang="en" data-niche="legal">
```

**Available niches:**

| Niche | Shell | Modules | Templates | Post aesthetic |
|---|---|---|---|---|
| `ppj` | `journal-cms.html` | 15 modules | 9 templates | Warm paper/ink — full intelligence layer |
| `travel` | `journal-cms-travel.html` | 15 modules | 9 templates | Warm paper/ink — generic buyer framing |
| `finance` | `journal-cms-finance.html` | 16 modules | 7 templates | Dark navy, IBM Plex Mono, signal colors |
| `real-estate` | `journal-cms-realestate.html` | 16 modules | 9 templates | Forest green, warm gold — boutique property magazine |
| `legal` | `journal-cms-legal.html` | 15 modules | 7 templates | Parchment, dark ink — law-review authority |

---

### Themes

The CMS has a two-layer CSS architecture:

- **`cms.css`** — purely structural (layout, spacing, component shapes). No colors or font names.
- **`themes/*.css`** — colors, fonts, and visual identity. Each theme is self-contained.

The HTML shell links one theme:

```html
<!-- PPJ internal: inkwell (warm gold, dark) -->
<link rel="stylesheet" href="./journal-cms/themes/inkwell.css">

<!-- Travel buyer: parchment (cream, light) -->
<link rel="stylesheet" href="./journal-cms/themes/parchment.css">

<!-- Finance buyer: obsidian (near-black, dark) -->
<link rel="stylesheet" href="./journal-cms/themes/obsidian.css">
```

**Theme tokens (same variable names across all themes):**

| Variable | Purpose |
|---|---|
| `--deep` | Body background |
| `--panel` | Card / sidebar / panel background |
| `--ink` | Topbar background |
| `--paper` | Primary text color |
| `--stone` | Secondary / muted text |
| `--warm` | Accent color — buttons, highlights, links |
| `--border` | Border / divider color |
| `--font-serif` | Heading / brand font |
| `--font-sans` | UI / body font |

**Post stylesheets** (separate from editor themes):
- Travel → `journal-cms/niches/travel/post.css` (embedded inline in every travel post)
- Finance → `journal-cms/niches/finance/post.css` (embedded inline in every finance post)

---

## Repository Setup

Before installing, decide how you want to organise your GitHub repository. There are two valid approaches. The right choice depends on how much you value separation of concerns over simplicity.

---

### Option A — Single repository (journal + CMS together)

The journal (public posts) and the CMS files live in the same GitHub repository. This is the simplest setup and the one described by default throughout this guide.

```
your-repo/
├── journal-cms-travel.html
├── journal-cms/          ← CMS engine + themes + niche
└── journal/
    └── posts/            ← published HTML posts
```

**Cloudflare setup:** Two Pages projects, both pointing at the same repository.

| Project | Build command | Publish dir | Purpose |
|---|---|---|---|
| `my-journal` | `node journal/build.js` | `journal` | Public blog readers see |
| `my-cms` | *(none)* | `/` | Where you write |

When you publish a post, the CMS commits the post file to `journal/posts/`. This triggers a redeploy of the Journal project. The CMS project serves static files and does not redeploy on content changes — only on pushes that change CMS source files.

| | |
|---|---|
| **Pros** | One repo, one history, one set of branches. Simpler to set up and manage. Preview URLs for every branch cover both the journal and the CMS automatically. |
| **Cons** | CMS source code and published posts share the same repository. Anyone with read access to your posts also sees the CMS internals. No separation between content and tooling. |

**Best for:** Solo writers, buyers who want the simplest possible setup.

---

### Option B — Separate repositories (journal in one repo, CMS in another)

The published posts live in a dedicated content repository. The CMS files live in a separate tool repository. The CMS connects to the content repository via your Personal Access Token — there is no requirement for both to be in the same repo.

```
content-repo/              ← journal posts, images, build output
    journal/
        posts/
        images/
        build.js
        ...

cms-repo/                  ← CMS engine, themes, niche pack
    journal-cms-travel.html
    journal-cms/
```

**Cloudflare setup:** Two Pages projects from two different repositories.

| Project | Repository | Build command | Publish dir | Purpose |
|---|---|---|---|---|
| `my-journal` | `content-repo` | `node journal/build.js` | `journal` | Public blog |
| `my-cms` | `cms-repo` | *(none)* | `/` | Writing tool |

When you connect the CMS, set the **Repository** field to `yourusername/content-repo` — not to the CMS repo. The CMS will read and write posts there, even though the CMS itself is hosted from a different repository.

| | |
|---|---|
| **Pros** | Clean separation: content history is independent of tool updates. Updating the CMS (bug fix, new module) does not touch the content repo. The content repo stays minimal — just posts, images, and the build script. |
| **Cons** | Two repositories to manage. Preview branches must be coordinated manually if you want to test a CMS change alongside a journal change. Slightly more initial setup. |

**Best for:** Writers who update their CMS regularly and want a clean content history; anyone who treats their post files as a long-term archive.

---

### Cloudflare Access — Restricting CMS access

When the CMS is hosted on Cloudflare Pages, anyone who knows the URL can reach the setup screen. In practice this is low risk (without a valid GitHub token the CMS cannot do anything), but for a production deployment it is good practice to put a login gate in front of it.

Cloudflare Access (part of Cloudflare Zero Trust, free tier available) adds email verification to any Pages project in a few minutes.

**Setup:**

1. Go to [one.dash.cloudflare.com](https://one.dash.cloudflare.com) → **Access** → **Applications** → **Add an application**
2. Choose **Self-hosted**
3. Fill in the application details:

| Field | Value |
|---|---|
| Application name | `Journal CMS` (or any name) |
| Session duration | `24 hours` (reasonable for daily use) |
| Application domain | Your CMS Pages URL (e.g. `my-cms.pages.dev` or `write.yourdomain.com`) |

4. Click **Next** → **Add a policy**
5. Configure the policy:

| Field | Value |
|---|---|
| Policy name | `Allowed writers` |
| Action | `Allow` |
| Include | `Emails` → add your email address (and any collaborators) |

6. Click **Save policy** → **Save application**

After this, visiting the CMS URL will redirect to a Cloudflare login page asking for your email. Cloudflare sends a one-time code to that address. Once verified, you are signed in for the session duration.

> **No account required for visitors.** Access uses a one-time email code — there is no username/password to manage. Add or remove allowed emails at any time in the Access dashboard.

> **Cloudflare Access free tier** includes up to 50 users. For a single-writer or small-team setup, this is always free.

---

## Installation

### Step 1 — Create a GitHub Personal Access Token

1. Go to [github.com](https://github.com) → **Settings** → **Developer settings** → **Personal access tokens** → **Tokens (classic)**.
2. Click **Generate new token (classic)**.
3. Give it a descriptive name (e.g. `Journal CMS`).
4. Select the **`repo`** scope (full control of private repositories).
5. Click **Generate token** and copy it immediately — it will not be shown again.

> Keep this token secret. It has write access to your repository. Store it in a password manager.

### Step 2 — Open the CMS and Connect

1. Open the CMS HTML file in your browser (see [Running the CMS](#running-the-cms)).
2. The setup screen appears on first launch.
3. Optionally expand **Site Identity** to enter your Gumroad Order ID, production domain, and journal title. These can be set later via Site Settings.
4. Enter your **GitHub Personal Access Token** in the token field.
5. Enter your **Repository** in the format `yourusername/repo-name`.
6. Set **Branch** (default: `main`) and **Posts folder** (default: `journal/posts`).
7. Click **Connect Repository →**.

Settings are stored in `localStorage` under `ppj_cms_cfg`. They persist between sessions on the same browser.

---

## Running the CMS

**The CMS must be served over HTTP — do not open via `file://`** (ES modules are blocked on the file:// protocol).

**Cloudflare Pages (recommended — no software required):**
1. Push the project to a GitHub repository.
2. In Cloudflare Pages, create a new project connected to that repository.
3. Set **Build command** to *(empty)* and **Build output directory** to `/`.
4. Deploy. Access the CMS at `https://your-project.pages.dev/journal-cms-travel.html`.

This is identical to the buyer experience and works from any browser on any device. See `ReadMe_Overview.md` for the full step-by-step setup.

**VS Code with Live Server (local fallback):**
1. Open the project folder in VS Code.
2. Install the [Live Server extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer).
3. Right-click the CMS HTML file → **Open with Live Server**.
4. The server opens at `http://127.0.0.1:5500/journal-cms-travel.html`.

**Python static server (local fallback):**
```bash
python3 -m http.server 5500
# then open http://localhost:5500/journal-cms-travel.html
```

> **Do not open the CMS via `file://`** — ES modules are blocked on the file:// protocol. Always serve over HTTP or HTTPS.

---

## Using the Editor

### Creating a Post

1. Click **+ New Post** in the sidebar.
2. The template picker opens — see [Template Picker](#template-picker).
3. The metadata module appears — fill in title, date, location, excerpt, and other core fields. The slug is auto-generated from the title but can be edited manually.
4. Add content modules using **+ Add Module** at the bottom of the module stack.
5. Click **Publish to GitHub →** to commit the post.

### Module System

Posts are composed of typed modules arranged in a vertical stack. Each module has a distinct purpose and set of fields. Modules can be:
- Reordered by dragging the handle
- Collapsed to save space while keeping their data
- Removed individually

The available modules depend on which niche is active (see Module Reference sections below).

### Template Picker

When creating a new post, the template picker shows all available templates for the active niche. Each template card displays:
- Template name and description
- A row of **module stack pills** showing which modules the template adds, in order
- Required modules (metadata) are highlighted in the accent color

Choose the template that best matches the type of post you are writing — the module stack is added automatically, ready to fill in.

### Publishing

Clicking **Publish to GitHub** does the following:
1. Collects all module field data via the niche's `collectModuleData()`
2. Fetches the niche's `post.css` and embeds it inline (cached after first load)
3. Injects a `<style>` block with the buyer's branding overrides (accent color, fonts)
4. Injects a `<meta name="ppj-content-id">` DRM fingerprint
5. Generates a self-contained HTML file
6. Commits it to `journal/posts/<slug>.html` in the repository via the GitHub Contents API
7. Updates `journal/post-index.json` with the post's metadata

If Cloudflare Pages is connected to the repository, the deployment triggers automatically.

### Share Captions and Newsletter Snippets

After a post is created, the right sidebar shows two panels for distributing the post:

**Share Captions** — three copy buttons generate platform-specific text ready to paste:

| Button | Output |
|---|---|
| Copy Twitter / X | Title + excerpt (trimmed to 280 chars) + first 2 hashtags + post URL |
| Copy LinkedIn | Title + full excerpt + post URL + all hashtags |
| Copy Substack Note | Title + excerpt + post URL (no hashtags — Substack uses its own topic tagging) |

Hashtags come from the **Social Hashtags** field at the bottom of the Post Metadata module. Each niche pre-populates this with relevant defaults. Edit the field on any post before copying a caption if the post needs different tags. The value is saved with the post so it persists across sessions.

Default hashtags per niche:

| Niche | Default |
|---|---|
| Travel | `#travelblog #slowtravel #traveltips` |
| Finance | `#investing #stockmarket #finance` |
| Real Estate | `#realestate #property #housing` |
| Legal | `#legaladvice #law #legalinsights` |

Hashtags are not embedded in the published post HTML — they exist only in the form field for caption generation. Open Graph and Twitter Card meta tags are embedded in every saved post automatically and are separate from these.

**Newsletter Snippet** — a Copy Markdown button generates a formatted Markdown card for pasting into Substack, Beehiiv, or any email editor. Includes title, category/date byline, excerpt, optional hero image, and link.

### Images

The **Image Manager** (camera icon in the toolbar) provides three tabs:
- **Upload** — select a local file and commit it to `journal/images/` in your repository. The image URL uses your production domain if set.
- **Unsplash** — search Unsplash and embed a photo. Requires an Unsplash Access Key set in Site Settings.
- **Library** — browse images already committed to `journal/images/`.

### Site Settings

Accessible via the gear icon in the toolbar.

| Field | Description |
|---|---|
| **Gumroad Order ID** | Used to generate the DRM fingerprint. Stored locally only. |
| **Production Domain** | Used to construct image URLs and the sitemap. |
| **Journal Title** | Shown on the public index and library pages. |
| **Max Posts on Index** | How many posts to show on the public index page. |
| **Google Service Account JSON** | For Google Indexing API notifications. Stored locally only. |
| **Unsplash Access Key** | For the Unsplash image search tab. Stored locally only. |

Changes to title, domain, and max posts are pushed to `journal/config.json` in the repository.

### Branding

The **Branding** modal (palette icon in the toolbar) controls:

| Setting | Options |
|---|---|
| **Accent Color** | Color picker — sets `--accent` in every saved post |
| **Heading Font** | 8 curated Google Fonts (Cormorant Garamond, Playfair Display, Lora, Libre Baskerville, etc.) |
| **Body Font** | 8 curated Google Fonts (Jost, Inter, DM Sans, Raleway, etc.) |

Changes preview live in the preview panel. On save, all future posts embed a `<style>` block that overrides the niche's `post.css` defaults with the chosen branding. The `post.css` file is never modified.

> **Applying branding changes to existing posts:** Because each post is a self-contained HTML file with its styles embedded at publish time, branding changes only affect posts published after the change is saved. To update the look of older posts, open each one in the CMS and click Publish again — the post is re-saved with the current branding applied.

---

## Post Format

Each published post is a self-contained HTML file:

```html
<!-- DRM fingerprint -->
<meta name="ppj-content-id" content="ppj-a3f72c91">

<!-- All post styles: niche post.css + branding overrides, embedded inline -->
<style>/* ... */</style>

<!-- Post metadata (read back by CMS on edit) -->
<script type="application/json" id="post-meta">
{
  "title": "My Post Title",
  "slug": "my-post-title",
  "date": "2025-03-15",
  "niche": "travel",
  ...
}
</script>

<!-- Module markers (used for round-trip editing) -->
<!-- MODULE:hero -->
...hero content...
<!-- /MODULE:hero -->
```

**Round-trip editing:**  
Opening an existing post in the CMS reads the post file, parses the `post-meta` JSON to restore all metadata, reads the `<!-- MODULE:type -->` markers to reconstruct the module stack, and extracts content from each HTML section back into the form fields. Posts can always be reopened, edited, and re-saved without information loss.

Posts are fully self-contained — they render correctly even without the rest of the journal directory.

---

## Post Index and build.js

`journal/post-index.json` caches the metadata of every post. It is updated automatically by the CMS on every publish and delete — keeping the sidebar fast without fetching every file from GitHub.

If you add, rename, or delete posts outside the CMS (e.g. directly on GitHub), regenerate it by running the build script:

```bash
node journal/build.js
```

This also regenerates `journal/index.html` (the public post listing) and `journal/library.html` (the Field Library archive).

**Dry run (shows what would be built without writing files):**
```bash
node journal/build.js --dry-run
```

---

## Deploying to Cloudflare Pages

1. Push the project to a GitHub repository.
2. In Cloudflare Pages, create a new project and connect the repository.
3. Set the **build command** to `node journal/build.js`.
4. Set the **publish directory** to `journal`.
5. Deploy.

Every time you publish a post via the CMS, a GitHub commit is created, which triggers a Cloudflare Pages deployment automatically.

> **If Cloudflare is not redeploying:** check that the CMS branch (set in the Connect screen) matches the production branch in Cloudflare Pages → Settings → Builds & deployments.

---

## Module Reference — Travel Niche

| Module | Purpose |
|---|---|
| `metadata` | Core post fields: title, slug, date, location, excerpt, hero image, category, status, social hashtags |
| `hero` | Full-width hero image with overlay text |
| `body` | Rich text content block (paragraphs, headings, lists) |
| `pullquote` | Large decorative quote with optional attribution |
| `midheading` | Section heading with optional subtitle |
| `scorecard` | Destination rating grid across 8 dimensions |
| `seasons` | Best-times-to-visit grid with month indicators |
| `inlineimages` | 1–3 column image gallery with captions |
| `personas` | Traveller persona cards (Solo, Couple, Family, Group) |
| `companion` | "Read next" companion post link |
| `timeline` | Day-by-day itinerary with icons |
| `transport` | Transport options table (type, route, cost, duration) |
| `eventmetrics` | Key event metrics (dates, venue, attendance, cost range) |
| `closing` | Closing paragraph with recap and CTA |
| `newslettercta` | Email signup call-to-action |

---

## Module Reference — Finance Niche

| Module | Purpose |
|---|---|
| `metadata` | Core post fields: title, slug, date, excerpt, hero image, category, status, social hashtags |
| `hero` | Full-width hero image with overlay text |
| `riskrating` | 6-dimension signal grid (Fundamentals, Technicals, Sentiment, Valuation, Macro, Entry Timing) — Bullish / Neutral / Bearish per axis |
| `body` | Rich text content block |
| `pullquote` | Large decorative quote |
| `midheading` | Section heading with optional subtitle |
| `tickercard` | Ticker symbol, price, % change, 52W high/low, market cap, sector |
| `keymetrics` | Repeatable rows: metric name, value, context/note |
| `thesisblock` | Bull case vs Bear case with conviction level and bias tag (Bullish/Neutral/Bearish) |
| `tradesetup` | Direction, entry zone, target, stop-loss, thesis summary |
| `economevent` | Repeatable economic events: name, date, expected/actual values, market impact badge |
| `timeline` | Event or milestone timeline |
| `inlineimages` | 1–3 column image gallery with captions |
| `closing` | Closing paragraph with recap |
| `newslettercta` | Email signup call-to-action |
| `disclaimer` | Pre-filled legal not-financial-advice disclaimer block |

---

## Module Reference — Real-Estate Niche

| Module | Purpose |
|---|---|
| `metadata` | Core post fields: title, slug, date, location, content type, author, excerpt, hero image, category, status, social hashtags |
| `hero` | Full-width hero image with overlay text |
| `market-scorecard` | 8-dimension market rating — Affordability, Inventory, Price Trend, Days on Market, School Quality, Walkability, Investment Potential, Overall Rating (1–5 stars + note per dimension) — singleton |
| `property-card` | Address, list/sale price, beds, baths, sq ft, lot size, days on market, MLS number, status badge (Active / Under Contract / Sold / Off Market) — singleton |
| `market-metrics` | Repeatable metric rows — Median Sale Price, Days on Market, Price-to-List Ratio, Inventory, and any custom metric |
| `price-history` | 1-year, 3-year, 5-year median price milestones rendered as a vertical dot-timeline with % change badges — singleton |
| `neighborhood-grid` | Walk Score, Transit Score, Bike Score, Noise Level, Safety Rating, Green Space, Dining & Nightlife, School Quality — each with score bar and note — singleton |
| `comparable-sales` | Table of recent comparable sales: address, sale price, sq ft, price/sqft, DOM, date sold |
| `investment-thesis` | Cap Rate, Gross Rent Multiplier, Cash-on-Cash Return, Expected Appreciation, Investment Grade — singleton |
| `seasonal-timing` | Month-by-month grid: best time to buy, sell, or invest with notes — singleton |
| `body` | Lead paragraph and supporting body text with optional inline pull quote |
| `pullquote` | Standalone blockquote — a key market insight |
| `midheading` | Section heading with optional subtitle |
| `timeline` | Chronological event list — neighborhood history or market cycle narrative |
| `closing` | Summary and call to action (schedule a showing, book a consultation, download a report) |
| `newslettercta` | Email capture block for market report subscribers |

---

## Module Reference — Legal Niche

| Module | Purpose |
|---|---|
| `metadata` | Core post fields: title, slug, date, practice area, content type, author credentials, bar admission, publish date, last updated, social hashtags |
| `hero` | Full-width hero image with overlay text |
| `regulatory-context` | Governing statute, CFR/USC citation, issuing agency, effective date, jurisdiction scope, amendment history — singleton |
| `risk-indicator` | Risk level badge (Critical / High / Medium / Low), risk category (Regulatory / Litigation / Reputational / Operational / Financial), affected parties, timeline to exposure — singleton |
| `key-holdings` | Repeatable case blocks: case name, citation, court, date, holding summary, optional dissent |
| `compliance-checklist` | Repeatable action items: description, responsible party, deadline (Immediate / 30 days / 90 days / Ongoing), status (Required / Recommended / Optional) — singleton |
| `practice-implications` | Structured guidance: practice advice, client counseling, contract updates, filing/disclosure requirements — singleton |
| `jurisdiction-matrix` | Table of jurisdictions: jurisdiction name, specific rule, effective date, compliance status (In Effect / Pending / Proposed) — singleton |
| `body` | Opening lead and supporting narrative legal analysis |
| `pullquote` | Highlighted statutory quote, key holding, or key doctrinal statement |
| `midheading` | Section heading — especially important for long compliance briefings |
| `timeline` | Chronological sequence: regulatory developments, case history, or compliance deadlines |
| `related-authorities` | Repeatable list: related statutes, regulations, or cases with citation and relevance note |
| `closing` | Summary of key points and recommended next steps |
| `disclaimer` | Attorney advertising compliance disclaimer — **locked**: always present, cannot be removed |
| `cle-info` | Accreditation info, credit hours, provider name, course number — for CLE-qualifying content — singleton |

> **Locked module:** The `disclaimer` module has `locked: true` in the niche config. The module card renders without a remove button and the text is pre-filled. Bar advertising rules require this to appear on all published legal content.

---

## Troubleshooting

### "GitHub API error 401 — Bad credentials"

Your token is invalid, expired, or missing `repo` scope.

1. Go to GitHub → Settings → Developer settings → Personal access tokens.
2. Check if your token has expired.
3. Generate a new token with `repo` scope, then use Disconnect → reconnect.

---

### "GitHub API error 409 — Conflict"

The post was modified on GitHub since you last loaded it. This happens with simultaneous sessions.

Click the post in the sidebar to reload it fresh, then re-apply changes and publish again.

---

### Sidebar is empty but posts exist in the repo

1. Verify the PAT is valid and has `repo` scope.
2. Verify the Repository field matches `username/repo-name` exactly.
3. Verify the Branch and Posts folder are correct.

Open the browser console (F12) to see the specific GitHub API error.

---

### Post doesn't appear on the live site after publishing

1. Go to Cloudflare Pages → your project → Deployments tab.
2. Check if a new deployment was triggered. If not, check the branch setting.
3. If a deployment triggered but failed, check the build logs in Cloudflare.

---

### Modules are missing after re-opening a post

Posts edited manually in a text editor may have had the `<!-- MODULE:type -->` markers removed. The CMS requires these markers to reconstruct the module stack. Posts created and saved by the CMS always include them — do not remove them if manually editing a post file.

---

