Internal Blog Build Spec
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:
- CLI tool for programmatic CRUD (nanobot)
- Web UI for human CRUD (user)
- SQLite storage (single source of truth)
- Owner-only edit/delete, all authenticated users can read
- No Hugo sync — purely internal
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
| Route | Method | Auth | Permission | What |
|---|---|---|---|---|
/app/internal | GET | Required | All | List posts |
/app/internal/new | GET, POST | Required | All | Create post |
/app/internal/{id} | GET | Required | All | Read post |
/app/internal/{id}/edit | GET, POST | Required | Owner only | Edit post |
/app/internal/{id}/delete | POST | Required | Owner only | Delete 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
- List/Read: All authenticated users
- Create: All authenticated users (author = session user)
- Edit/Delete: Owner only (author matches session user)
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)
- Create SQLite database file
- Run schema migrations
- Add database connection to webapp
- Test with manual queries
Files:
data/internal.dbmigrations/001_init.sql
Phase 2: CLI Tool (2-3 hours)
- Create
cmd/internal-blog/main.go - Implement create command
- Implement list command
- Implement read command
- Implement update command
- Implement delete command
- Implement search command
- Build and test
Files:
cmd/internal-blog/main.gointernal/blog/posts.go
Phase 3: Web UI (3-4 hours)
- Add routes to webapp
- Create list template
- Create view template
- Create new template
- Create edit template (owner check)
- Create delete handler (owner check)
- Add navigation link to /app
Files:
main.go(routes)templates/internal/*.html
Phase 4: Security Hardening (1-2 hours)
- Add CSRF tokens to all forms
- Add owner-only middleware
- Add XSS sanitization
- Add audit logging
- Test security scenarios
Phase 5: Testing (2-3 hours)
- Unit tests for CLI commands
- Integration tests for web routes
- Security tests (CSRF, authz, XSS)
- Edge case tests (concurrent edits, etc.)
Testing Checklist
CLI Tests
- Create post with file
- Create post with content flag
- List all posts
- List by author
- List by tag
- Read post (markdown output)
- Read post (JSON output)
- Update title
- Update content
- Publish/unpublish
- Delete post
- Restore post
- Search posts
Web UI Tests
- List posts (authenticated)
- View post (authenticated)
- Create post (authenticated)
- Edit own post
- Cannot edit others’ posts (403)
- Delete own post
- Cannot delete others’ posts (403)
- CSRF token validation
- XSS in content is sanitized
Security Tests
- Unauthenticated user cannot access /app/internal
- User cannot edit another user’s post
- User cannot delete another user’s post
- XSS payload in content is sanitized in HTML output
- CSRF token required for all mutations
- Audit log records all mutations
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
| Error | CLI Output | Web UI |
|---|---|---|
| Post not found | Error: Post not found (exit 1) | 404 page |
| Not owner | N/A (CLI uses –author) | 403 Forbidden |
| Invalid file | Error: File not found (exit 1) | N/A |
| Database error | Error: Database error (exit 1) | 500 page |
| Invalid JSON | Error: 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:
- Remove routes from
main.go - Remove
templates/internal/directory - Delete
data/internal.db - CLI tool is standalone — can keep or remove
No data migration needed (new feature, no existing data).
Success Criteria
- CLI tool can CRUD posts
- Web UI can CRUD posts
- Owner-only edit/delete enforced
- All authenticated users can read
- CSRF protection on all forms
- XSS sanitized in HTML output
- Audit trail for all mutations
- Soft delete with restore capability
- Search works across title/content
- No Hugo sync (purely internal)
Notes for Builder
- Use existing auth system — Sessions, CSRF, rate limiting already work
- Share database logic — CLI and webapp import same
internal/blogpackage - Test owner enforcement — Create posts as different users, verify 403
- Keep it simple — No tags table, just JSON array in posts
- No public sync — This is internal only, no Hugo integration
Estimated Time
| Phase | Time |
|---|---|
| Database setup | 1-2 hours |
| CLI tool | 2-3 hours |
| Web UI | 3-4 hours |
| Security hardening | 1-2 hours |
| Testing | 2-3 hours |
| Total | 10-15 hours |
Codex Review Summary
Reviewed for completeness, security, and clarity. Key findings:
- Critical issues addressed: CSRF, authz, XSS mitigation included
- Risk areas flagged: Concurrent writes (SQLite handles), rollback plan documented
- Verdict: Ready to build
Spec version 1.0 — 2026-02-22