- Python 64.6%
- CSS 17.9%
- HTML 12%
- Shell 5.1%
- Nix 0.4%
|
All checks were successful
Deploy to Forgejo Pages / build-and-deploy (push) Successful in 33s
|
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| content | ||
| data | ||
| layouts | ||
| scripts | ||
| static | ||
| writers | ||
| .gitignore | ||
| auto-continue.sh | ||
| CLAUDE.md | ||
| DNS.md | ||
| hugo.yaml | ||
| LICENSE | ||
| NEXT-STAGE.md | ||
| PLAN.md | ||
| prod-push.sh | ||
| PROMPT.md | ||
| README.md | ||
| shell.nix | ||
| WRITERS.md | ||
CCNN — Conscious Clanker News Network
AI-generated satire news site, fully maintained by AI agents. None of this is real.
Live at https://ccnn.pub.opworks.org.
How it works
CCNN is a Hugo static site with a two-stage AI pipeline driving the content:
┌─────────────────┐ drafts ┌─────────────────┐ polished ┌─────────────┐
│ Ollama drafters │──────────────▶│ Claude editor │───────────────▶│ Hugo build │
│ (local LLM, │ writers/<x>/ │ (selects 3–4, │ content/posts/ │ public/ → │
│ named writers) │ *.md │ polishes, │ <slug>.md │ Forgejo │
│ │ │ deletes rest) │ │ Pages │
└─────────────────┘ └─────────────────┘ └─────────────┘
- Writers are personas defined in
writers/writers.yaml(Neil Robert on tech, Mario Lazaro on food, etc.). Each has a slug, beat, voice, and bio. - Drafter daemon (
scripts/draft_daemon.py) picks a writer, prompts a local Ollama model with that writer's voice, and saves a draft towriters/<slug>/<date>-<slug>.md. Drafts are git-ignored. - Editor pass is a Claude session driven by
PROMPT.md. It reads all drafts, picks the 3–4 strongest, polishes them, publishes tocontent/posts/, and deletes the rest. - Author pages are auto-rendered Hugo taxonomy pages at
/authors/<slug>.html. The roster grid is at/authors.html.
Setup
You need:
- Nix with
nix-shell(provides Hugo + Python). - A local Ollama server with at least one tool-capable model pulled. Defaults to
qwen3.5:9b; override withOLLAMA_MODEL. - The
claudeCLI on PATH (for the editor pass and writer-add helper).
Drop into the dev shell:
nix-shell
This pulls Hugo + Python (with pyyaml) and prints a hint banner.
Local development
Serve the site locally with live-reload:
nix-shell --run "hugo server -D --bind 0.0.0.0"
# or just `deploy` once inside nix-shell
Then open http://localhost:1313/.
One-shot build (output to public/):
nix-shell --run "hugo --quiet"
Managing writers
The roster lives in writers/writers.yaml (source of truth) with one author content page per writer at content/authors/<slug>/_index.md.
Add a writer
python3 scripts/writers.py add
# or with args
python3 scripts/writers.py add "Jane Doe" "32, Seattle, climate beat, dry"
The script collects a name and seed description, then hands off to Claude. Claude picks a beat and voice, polishes the bio in the existing tone, appends an entry to writers/writers.yaml (before the ccnn-staff fallback), creates content/authors/<slug>/_index.md, and runs a Hugo build to verify.
Review the diff and commit when you're happy.
Remove a writer
python3 scripts/writers.py remove charles-geiger
This only edits writers/writers.yaml — the Ollama daemon will stop generating drafts for them. Their author page (content/authors/<slug>/_index.md) and all existing bylined articles stay in place so old links keep working. To fully retire them from the site, delete the author page directory yourself.
Generating articles
Manual one-shot
nix-shell --run "python3 scripts/draft_daemon.py --once"
# specific writer + model
nix-shell --run "python3 scripts/draft_daemon.py --once --writer mary-yan --model qwen3.5:9b"
A draft lands at writers/<slug>/<YYYY-MM-DD>-<slug>.md with status: draft. Drafts are git-ignored — they exist only locally until the editor pass picks them up.
Long-running drafter
nix-shell --run "./scripts/draft-loop.sh"
Defaults: every 3 hours, model qwen3.5:9b. Override with DRAFT_INTERVAL_MIN and OLLAMA_MODEL. Run it under tmux or systemd --user so it survives logouts.
Editor pass (publish)
A single editor pass:
claude --permission-mode bypassPermissions --print "Read PROMPT.md and run one editor pass."
Or on a 12h cadence:
./scripts/editor-loop.sh
# override the cadence
EDITOR_INTERVAL_HOURS=6 ./scripts/editor-loop.sh
The editor reads PROMPT.md, picks 3–4 of the strongest drafts, polishes them (strips duplicate H1s, inline bylines, repetitive tail paragraphs), publishes to content/posts/<slug>.md, deletes the rest, and runs a Hugo build to verify.
Recommended setup
Two long-running processes in separate panes:
# pane 1: drafter (every 3h)
nix-shell --run "./scripts/draft-loop.sh"
# pane 2: editor (every 12h)
./scripts/editor-loop.sh
The drafter accumulates 3–4 candidates between editor passes, the editor picks the best, repeat.
Project layout
.
├── content/
│ ├── posts/ # published articles (Hugo renders these)
│ ├── authors/<slug>/ # one bio page per writer
│ ├── about.md, search.md
├── layouts/ # Hugo templates (single, list, terms, partials)
├── static/css/style.css # styles
├── writers/
│ ├── writers.yaml # roster — source of truth
│ └── <slug>/*.md # generated drafts (git-ignored)
├── scripts/
│ ├── draft_daemon.py # Ollama drafter
│ ├── draft-loop.sh # crash-restart wrapper
│ ├── editor-loop.sh # Claude editor cadence
│ ├── writers.py # add/remove writer CLI
│ └── README.md
├── PROMPT.md # Claude's editor instructions
├── CLAUDE.md # repo guidance for Claude Code
├── hugo.yaml
└── shell.nix
Deployment
The site is hosted via Forgejo Git Pages using the actions/git-pages action. Pushing to main triggers a build and deploy. DNS notes are in DNS.md.
After publishing changes, sanity-check the live site:
- https://ccnn.pub.opworks.org/
- https://ccnn.pub.opworks.org/authors.html
- https://ccnn.pub.opworks.org/index.xml (RSS)
Editorial rules
- All content is satire, written deadpan — never labelled as such on the site.
- Headlines should be specific, absurd, obviously fictional.
- Tags are lowercase-hyphenated (
smart-home, notSmart Home). - One in ten posts may continue an older storyline. Don't force it.
Full editor workflow: see PROMPT.md. Repo guidance for Claude Code: see CLAUDE.md.