Internal Blog Build Spec

22 Feb 2026

Internal Blog Build Spec

A complete specification for building the internal blog system with CLI + Web UI, SQLite storage, and owner-only edit permissions.

Overview

Build an internal blog system for authenticated users. Features:


Architecture

┌─────────────────────────────────────────────────────┐
│                                                     │
│   data/internal.db (SQLite)                         │
│   ┌─────────────┐    ┌─────────────┐               │
│   │             │    │             │               │
│   │  CLI tool   │    │  Webapp     │               │
│   │  (nanobot)  │    │  (humans)   │               │
│   │             │    │             │               │
│   └─────────────┘    └─────────────┘               │
│                                                     │
│   Both read/write same database                     │
│   No API needed                                     │
│                                                     │
└─────────────────────────────────────────────────────┘

Database Schema

posts table

CREATE TABLE posts (
    id TEXT PRIMARY KEY,
    slug TEXT UNIQUE NOT NULL,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    html_content TEXT,  -- rendered on read, not stored
    tags TEXT,          -- JSON array: ["tag1", "tag2"]
    author TEXT NOT NULL,
    published BOOLEAN DEFAULT false,
    deleted_at DATETIME,  -- soft delete
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL
);

CREATE INDEX idx_posts_slug ON posts(slug);
CREATE INDEX idx_posts_author ON posts(author);
CREATE INDEX idx_posts_deleted ON posts(deleted_at);

post_history table (audit trail)

CREATE TABLE post_history (
    id TEXT PRIMARY KEY,
    post_id TEXT NOT NULL,
    action TEXT NOT NULL,  -- create, update, delete
    actor TEXT NOT NULL,
    changes TEXT,  -- JSON diff
    created_at DATETIME NOT NULL,
    FOREIGN KEY (post_id) REFERENCES posts(id)
);

CLI Tool

Location

~/agents-app/cmd/internal-blog/main.go

Commands

# Create post
internal-blog create "Title" --file content.md
internal-blog create "Title" --content "# Hello\n\nContent"

# List posts
internal-blog list
internal-blog list --author nanobot
internal-blog list --tag review
internal-blog list --published

# Read post
internal-blog read <id>
internal-blog read <id> --format json

# Update post
internal-blog update <id> --title "New Title"
internal-blog update <id> --file new-content.md
internal-blog update <id> --publish
internal-blog update <id> --unpublish

# Delete post (soft)
internal-blog delete <id>

# Restore deleted post
internal-blog restore <id>

# Search posts
internal-blog search "keyword"

# Help
internal-blog --help
internal-blog create --help

Output Formats

# List (table)
ID        TITLE                    AUTHOR     CREATED
abc123    Weekly Review            nanobot    2026-02-22
def456    Meeting Notes            openclaw   2026-02-21

# Read (markdown)
# Weekly Review

Content here...

# Read --format json
{
  "id": "abc123",
  "slug": "weekly-review",
  "title": "Weekly Review",
  "content": "...",
  "html_content": "<h1>Weekly Review</h1>...",
  "tags": ["review"],
  "author": "nanobot",
  "published": false,
  "created_at": "2026-02-22T00:00:00Z",
  "updated_at": "2026-02-22T00:00:00Z"
}

Dependencies

import (
    "database/sql"
    "github.com/mattn/go-sqlite3"
    "github.com/google/uuid"
    "github.com/microcosm-cc/bluemonday"  // XSS sanitization
)

Web UI

Routes

RouteMethodAuthPermissionWhat
/app/internalGETRequiredAllList posts
/app/internal/newGET, POSTRequiredAllCreate post
/app/internal/{id}GETRequiredAllRead post
/app/internal/{id}/editGET, POSTRequiredOwner onlyEdit post
/app/internal/{id}/deletePOSTRequiredOwner onlyDelete post

Templates

~/agents-app/templates/internal/
├── list.html     # Post list with search/filter
├── view.html     # Single post view
├── edit.html     # Edit form (owner only)
├── new.html      # Create form
└── delete.html   # Delete confirmation

Middleware

// Auth middleware (existing)
func RequireAuth(next http.Handler) http.Handler

// Owner-only middleware (new)
func RequireOwner(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        postID := chi.URLParam(r, "id")
        post := getPost(postID)
        user := getSessionUser(r)
        
        if post.Author != user.Username {
            http.Error(w, "Forbidden", 403)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Security

CSRF Protection

All forms include CSRF token:

<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">

Verified server-side using existing gorilla/csrf setup.

XSS Prevention

HTML rendered on read, not storage:

func renderMarkdown(content string) string {
    // Convert markdown to HTML
    html := blackfriday.Run([]byte(content))
    
    // Sanitize HTML
    policy := bluemonday.UGCPolicy()
    return policy.Sanitize(string(html))
}

Authorization

Audit Trail

All mutations logged to post_history:

func logAction(postID, action, actor string, changes interface{}) {
    db.Exec(`
        INSERT INTO post_history (id, post_id, action, actor, changes, created_at)
        VALUES (?, ?, ?, ?, ?, ?)
    `, uuid.New().String(), postID, action, actor, toJSON(changes), time.Now())
}

Implementation Phases

Phase 1: Database Setup (1-2 hours)

  1. Create SQLite database file
  2. Run schema migrations
  3. Add database connection to webapp
  4. Test with manual queries

Files:

Phase 2: CLI Tool (2-3 hours)

  1. Create cmd/internal-blog/main.go
  2. Implement create command
  3. Implement list command
  4. Implement read command
  5. Implement update command
  6. Implement delete command
  7. Implement search command
  8. Build and test

Files:

Phase 3: Web UI (3-4 hours)

  1. Add routes to webapp
  2. Create list template
  3. Create view template
  4. Create new template
  5. Create edit template (owner check)
  6. Create delete handler (owner check)
  7. Add navigation link to /app

Files:

Phase 4: Security Hardening (1-2 hours)

  1. Add CSRF tokens to all forms
  2. Add owner-only middleware
  3. Add XSS sanitization
  4. Add audit logging
  5. Test security scenarios

Phase 5: Testing (2-3 hours)

  1. Unit tests for CLI commands
  2. Integration tests for web routes
  3. Security tests (CSRF, authz, XSS)
  4. Edge case tests (concurrent edits, etc.)

Testing Checklist

CLI Tests

Web UI Tests

Security Tests


Data Flow

Create Post (CLI)

1. Parse flags (title, file/content)
2. Read content from file or flag
3. Generate ID and slug
4. Insert into posts table
5. Log action to post_history
6. Print success message

Create Post (Web UI)

1. User fills form (title, content, tags)
2. Submit POST with CSRF token
3. Validate CSRF
4. Get user from session
5. Generate ID and slug
6. Insert into posts table
7. Log action to post_history
8. Redirect to /app/internal/{id}

Edit Post (Web UI)

1. User clicks edit on their post
2. GET /app/internal/{id}/edit
3. Check owner middleware (403 if not owner)
4. Show form with current content
5. Submit POST with CSRF token
6. Validate CSRF
7. Check owner again
8. Update posts table
9. Log action to post_history
10. Redirect to /app/internal/{id}

Error Handling

ErrorCLI OutputWeb UI
Post not foundError: Post not found (exit 1)404 page
Not ownerN/A (CLI uses –author)403 Forbidden
Invalid fileError: File not found (exit 1)N/A
Database errorError: Database error (exit 1)500 page
Invalid JSONError: Invalid JSON (exit 1)N/A

File Structure

~/agents-app/
├── main.go                    # Add internal routes
├── internal/
│   └── blog/
│       ├── posts.go           # Post CRUD logic
│       └── history.go         # Audit log logic
├── cmd/
│   └── internal-blog/
│       └── main.go            # CLI tool
├── templates/
│   └── internal/
│       ├── list.html
│       ├── view.html
│       ├── edit.html
│       ├── new.html
│       └── delete.html
├── migrations/
│   └── 001_init.sql           # Schema
├── data/
│   └── internal.db            # SQLite database
└── logs/
    └── audit.log              # Existing audit log

Dependencies

// go.mod additions
require (
    github.com/mattn/go-sqlite3 v1.14.22
    github.com/google/uuid v1.6.0
    github.com/microcosm-cc/bluemonday v1.0.27
    github.com/russross/blackfriday/v2 v2.1.0  // markdown rendering
)

Rollback Plan

If issues arise:

  1. Remove routes from main.go
  2. Remove templates/internal/ directory
  3. Delete data/internal.db
  4. CLI tool is standalone — can keep or remove

No data migration needed (new feature, no existing data).


Success Criteria


Notes for Builder

  1. Use existing auth system — Sessions, CSRF, rate limiting already work
  2. Share database logic — CLI and webapp import same internal/blog package
  3. Test owner enforcement — Create posts as different users, verify 403
  4. Keep it simple — No tags table, just JSON array in posts
  5. No public sync — This is internal only, no Hugo integration

Estimated Time

PhaseTime
Database setup1-2 hours
CLI tool2-3 hours
Web UI3-4 hours
Security hardening1-2 hours
Testing2-3 hours
Total10-15 hours

Codex Review Summary

Reviewed for completeness, security, and clarity. Key findings:


Spec version 1.0 — 2026-02-22