Internal Blog Implementation Proposal
Overview
A private blog system for authenticated users, with dual access methods:
- CLI tool for programmatic access (nanobot)
- Web UI for human access (user + friends)
Both read/write the same data source — no API needed.
Architecture
┌─────────────────────────────────────────────────────────┐
│ │
│ data/posts.json │
│ (single source of truth) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ │ │ │ │
│ │ CLI Tool │ │ Go Webapp │ │
│ │ internal-blog │ │ /app/internal │ │
│ │ │ │ │ │
│ │ For: nanobot │ │ For: humans │ │
│ │ Access: exec │ │ Access: browser│ │
│ │ │ │ │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ No API. No auth for CLI. Same file. │
│ │
└─────────────────────────────────────────────────────────┘
Data Model
File: ~/agents-app/data/posts.json
{
"posts": [
{
"id": "abc123",
"slug": "weekly-review-feb-21",
"title": "Weekly Review - Feb 21",
"content": "# Weekly Review\n\n...",
"tags": ["review", "weekly"],
"published": false,
"created_at": "2026-02-21T12:00:00Z",
"updated_at": "2026-02-21T12:00:00Z",
"author": "nanobot"
}
]
}
Design decisions:
id: Short unique identifier (nanoid, 8 chars)slug: URL-friendly identifier (auto-generated from title)content: Raw markdowntags: Optional categorizationpublished: Draft/published stateauthor: Who created it (nanobot, openclaw, friend names)
CLI Tool
Location: ~/agents-app/internal-blog
Implementation: Go binary, compiled and installed to ~/.local/bin/internal-blog
Commands
# Create a post
internal-blog create "Title" --file content.md
internal-blog create "Title" --content "# Hello"
internal-blog create "Title" --tag review --tag weekly
# List posts
internal-blog list
internal-blog list --tag review
internal-blog list --author nanobot
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
# Delete post
internal-blog delete <id>
# Search
internal-blog search "keyword"
Web UI
Routes in Go webapp:
| Route | Auth | What |
|---|---|---|
/app/internal | Yes | List all posts |
/app/internal/new | Yes | Create new post (form) |
/app/internal/{id} | Yes | Read post (rendered) |
/app/internal/{id}/edit | Yes | Edit post (form) |
/app/internal/{id}/delete | Yes | Delete post (confirm) |
Implementation Plan
Phase 1: Data Layer + CLI (2-3 hours)
| Task | What |
|---|---|
| Create data schema | data/posts.json structure |
| Build CLI skeleton | Argument parsing, commands |
| Implement CRUD | Create, read, update, delete |
| Add search | Keyword search in title/content |
| Add rendering | Markdown → HTML (render on read) |
Phase 2: Web UI (2-3 hours)
| Task | What |
|---|---|
| Add routes | /app/internal/* in webapp |
| List page | Display all posts |
| Read page | Display single post |
| Create/Edit forms | Web forms for CRUD |
| Delete confirmation | Prevent accidental deletion |
| Auth integration | Require login for all routes |
Phase 3: Polish (1-2 hours)
| Task | What |
|---|---|
| Styling | Match brutal.css aesthetic |
| Error handling | Friendly error messages |
| Validation | Title required, etc. |
| Audit logging | Log all CRUD operations |
Total effort: 5-8 hours
Red Team Review (Codex)
Critical Issues
| Issue | Risk | Mitigation |
|---|---|---|
| Stored XSS via content_html | Persistent malicious HTML | Render on read, sanitize on output, version sanitizer |
| Missing CSRF protection | Cross-site request forgery | Add CSRF tokens to all state-changing forms |
| No authorization model | Anyone can edit/delete anyone’s posts | Add owner field, only owner can edit/delete |
| JSON file concurrency | Lost updates, torn writes, corruption | Use file locking or migrate to SQLite |
High Priority Issues
| Issue | Risk | Mitigation |
|---|---|---|
| Duplicate write paths | Logic drift between CLI and web | Shared domain package for validation, rendering |
| Mutable author field | Spoofed attribution | Author from session/SSH principal, immutable |
| Pre-rendered HTML | Stale content on renderer updates | Render on read, or store renderer version |
Medium Priority Issues
| Issue | Risk | Mitigation |
|---|---|---|
| ID/slug collision | Ambiguous routes, overwrites | Explicit uniqueness checks, collision retries |
| No resource limits | Memory/CPU abuse | Payload size limits, tag count limits |
| Hard delete | No rollback | Tombstones or soft delete |
Missing Test Strategy
| Test Type | Why Needed |
|---|---|
| Sanitizer regression corpus | XSS bypass attempts |
| CSRF tests | Form submission security |
| Concurrent write tests | File locking correctness |
| Authz matrix tests | Who can do what |
| Malformed JSON recovery | Graceful degradation |
Security Design (Updated)
Based on Codex review:
Authorization Model
{
"id": "abc123",
"author": "nanobot",
"author_role": "owner" // owner can edit/delete, others can only read
}
- Only post author can edit/delete their own posts
- Admin user (openclaw) can edit/delete any post
- Friends can create posts, edit/delete their own
CSRF Protection
- All forms include CSRF token (existing gorilla/csrf)
- SameSite=Lax on session cookies
XSS Prevention
- Render markdown on read (no pre-rendered HTML)
- Sanitize HTML output with bluemonday
- Version the sanitizer policy
Concurrency
- Use
flockorgithub.com/gofrs/flockfor file locking - Or migrate to SQLite for MVP (simpler, safer)
Failure Modes
| Failure | Cause | Fix |
|---|---|---|
| File corruption | Concurrent writes | File locking or SQLite |
| CLI not in PATH | Not installed | Add to ~/.local/bin, document |
| Markdown rendering fails | Invalid markdown | Graceful fallback to raw content |
| Post not found | Invalid ID | 404 page in web UI, error in CLI |
| Disk full | No space | Check disk, add monitoring |
| Malformed JSON | Corruption | Validate on load, backup strategy |
Open Questions
- SQLite from start? — Safer concurrency, but adds DB dependency
- Friend trust level? — Same as owner, or per-post ACL?
- Export to Hugo? — Should internal posts be publishable to public blog?
- Soft delete? — Tombstones for accidental deletion recovery
Conclusion
This design provides:
- Simple architecture: One data file (or SQLite), two access methods
- Ergonomic for nanobot: CLI tool, no auth needed
- Ergonomic for humans: Web UI, existing auth
- Low effort: 5-8 hours for MVP
- Security-conscious: CSRF, authz, XSS prevention, file locking
The internal blog becomes a private knowledge base for you and your friends, with nanobot as a contributing author.