$ cd ../projects

~/projects/ghost-blog/README.md

LIVEPUBLICIN PROGRESS# AI Automation# Infrastructure

Self-Hosted Blog & AI Publishing

My long-running blog moved from Blogger to a self-hosted Ghost instance on the homelab. After the migration I ran a measured SEO campaign that restructured 15 years of content into a nine-topic taxonomy and pruned thin pages from the sitemap. On top of that sits AI-assisted publishing: a Claude Code skill that drafts and publishes posts, and a larger topic-to-article pipeline that is still being built.

Ghost 6DockerCloudflare TunnelPythonPlaywrightGhost Admin APIClaude Coden8n
posts migrated
1,286
tags in sitemap
1,303 → 9
posts in sitemap
1,297 → 632
failed bulk changes
0

cat architecture.flow

System Architecture

architecture.flow

Migration

Blogger feed.atom
convert_blogger.py
Ghost import JSON
Universal Import (browser)
Ghost 6 + MySQL

SEO campaign

Search Console + audit sheet
Topic taxonomy
Snapshot + export
Admin API batch (JSONL log)
Re-verify live

Publishing

Claude Code session
blog-post skill
SVG feature image
Admin API (draft)
Publish on approval

cat problem.md

Business Problem

Fifteen years of writing lived on Blogger, a platform I didn't control. A first migration attempt never went live.

Once the blog was migrated, Search Console told the real story: 242 pages indexed, over 3,400 not indexed, and zero clicks in three months. The archive had 1,302 tags, and 1,117 of them were used by a single post. Each one was a thin page competing for crawl budget.

Writing new posts was also slow. Turning a finished piece of engineering work into a published article took as long as the work itself.

cat solution.md

Solution

I self-hosted Ghost 6 on the homelab behind Cloudflare Tunnel and wrote a converter from Blogger's Atom export to Ghost's import format. The import itself ran through Ghost's own Universal Import, driven by a signed-in browser session. That keeps every post on Ghost's supported HTML-to-editor path.

The SEO work ran as a tracked, 24-step campaign with an audit workbook as the source of truth. The tags were collapsed into a nine-topic taxonomy, keeping existing URLs where possible. Thin or duplicated posts were moved to draft rather than deleted, so every change is reversible.

Every bulk change was preceded by a full content snapshot and Ghost export, executed through the Admin API, logged step by step to a JSONL change log, and re-verified against the live site.

For new writing, a Claude Code skill turns a session into a post in my voice, generates a feature image, and publishes through the Admin API. It saves a draft by default and only publishes or schedules when explicitly told to. A fuller pipeline (topic discovery, scoring, research, generation, QC and human approval) is in progress, with its first phase complete.

ls ./components

Architecture Decisions

Ghost 6 on Docker

Ghost and its database run as containers on the homelab, exposed only through Cloudflare Tunnel, with no open inbound ports.

Converter, not raw SQL

The Blogger export is converted to Ghost's import JSON and imported through Ghost itself, so content goes through the supported HTML-to-editor conversion instead of being written straight into the database.

Least-privilege Admin API

Day-to-day automation uses a custom integration key that can manage posts, tags and images but can't reach the database import and export endpoints.

Audit workbook as source of truth

Every URL, its topic, its decision and its status are tracked in one workbook, so the campaign is resumable and every change can be traced.

Reversible by design

Snapshots before every batch, a JSONL change log per step, and draft instead of delete. Any decision can be rolled back.

AI publishing pipeline (in progress)

n8n, PostgreSQL and Redis/RQ workers for topic scoring, research, generation, QC and media, with mandatory human approval before anything is published.

./run --trace

SEO Batch Change Procedure

  1. 01

    Decide in the workbook

    Each URL gets a topic and a decision (keep, retag, rewrite meta, or move to draft) in the audit workbook.

  2. 02

    Snapshot

    A full Ghost export and JSON snapshot are taken before any live change.

  3. 03

    Apply through the Admin API

    Changes run in batches, and each step is appended to a JSONL change log.

  4. 04

    Verify live

    Updated pages, the sitemap and meta descriptions are re-fetched and checked against the intended state.

tail -f incidents.log

Engineering Challenges

Import rejected as "Invalid lexical structure"

Symptom
The first migration attempt produced content Ghost refused to load.
Root cause
Posts were written straight into the database, bypassing Ghost's HTML-to-lexical conversion.
Fix
Re-ran the migration through Ghost's Universal Import. Two edge cases followed: posts with empty HTML, and posts that were only a Flash embed. Each got a placeholder line so no post was dropped.

The API key couldn't import

Symptom
Integration keys are refused on the database import endpoint.
Fix
Ran the one-off import through the owner's signed-in browser session with Playwright, and kept the API key for routine, least-privilege publishing.

Homepage burying new posts

Symptom
Fresh articles didn't appear where readers would see them.
Root cause
The old theme listed content by tag, alphabetically.
Fix
Switched to Casper for a chronological feed. Theme choice turned out to be a discoverability decision, not just a cosmetic one.

Tag bloat at archive scale

Symptom
1,302 tags, 1,117 of them single-use, each one a thin sitemap page.
Fix
Designed a nine-topic taxonomy, renamed existing tags where URLs could be kept, re-tagged 612 posts, and removed the old single-use tags in logged batches with zero failures.

cat package.json | jq .stack

Technology Stack

Platform

Ghost 6Casper themeMySQLDocker ComposeCloudflare Tunnel

Migration & SEO

PythonPlaywrightSearch ConsoleBing Webmaster ToolsExcel / openpyxl

AI publishing

Claude Code skillGhost Admin API (JWT)n8nPostgreSQLRedis / RQ

echo $EXIT_STATUS

Project Outcome

outcome.log

The whole archive (1,286 Blogger posts) now lives on infrastructure I control. The sitemap is down from 1,297 posts and 1,303 tags to 632 posts and 9 topics, and 30 key posts have hand-written meta descriptions, all verified live.

Traffic results are still being measured, and a final report is due at the end of the campaign. The AI publishing pipeline is honestly marked as in progress. The skill-based publishing path is live today.