From Ghost to WriteFreely: My Migration to the Fediverse

Beschreibung Why I migrated my blog from Ghost to WriteFreely and how you can do it too

Why WriteFreely?

After years with various blogging platforms, I was looking for something simpler. Ghost was great, but:

Setup: WriteFreely on LXC (Proxmox)

Why Native Installation Instead of Docker?

After several frustrating hours with Docker mount problems, I decided on native installation. Best decision ever!

# Download WriteFreely binary
cd /opt
wget https://github.com/writefreely/writefreely/releases/download/v0.15.0/writefreely_0.15.0_linux_arm64.tar.gz
tar -xzf writefreely_0.15.0_linux_arm64.tar.gz
mv writefreely /opt/writefreely
cd /opt/writefreely

# Interactive configuration
./writefreely --config

Configuration (config.ini)

[server]
bind = 0.0.0.0  # Important for reverse proxy
port = 8080
https = false   # Reverse proxy handles HTTPS

[app]
site_name = My Blog
host = https://blog.example.com  # Your real domain!
single_user = true
federation = true  # This is the key!
public_registration = false

[database]
type = sqlite3  # Simpler than MySQL for single user
filename = writefreely.db

As systemd Service

# Create service file
sudo tee /etc/systemd/system/writefreely.service << 'EOF'
[Unit]
Description=WriteFreely
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/writefreely
ExecStart=/opt/writefreely/writefreely
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable writefreely
sudo systemctl start writefreely

Reverse Proxy (Traefik/Nginx)

Example configuration: – Frontend: blog.example.comBackend: http://192.168.1.100:8080SSL: Automatic

Migration from Ghost

Export from Ghost

Ghost Admin → Settings → Labs → “Export content”

Migration Script (Python)

Since WriteFreely's import features are limited, I migrated most posts manually. For automatic migration:

#!/usr/bin/env python3
import json
import requests
from datetime import datetime

def migrate_ghost_to_writefreely(ghost_export, writefreely_url, token):
    with open(ghost_export, 'r') as f:
        data = json.load(f)
    
    posts = data['db'][0]['data']['posts']
    
    for post in posts:
        if post['status'] == 'published':
            # WriteFreely API Call
            payload = {
                'body': convert_html_to_markdown(post['html']),
                'title': post['title']
            }
            # ... API integration

My tip: For small blogs (< 20 posts), manual copying is often faster and cleaner.

Custom CSS: The Perfect Dark Theme

After many iterations, here's my final WriteFreely dark theme:

/* WriteFreely Custom Dark Theme */

/* === SANS-SERIF FONT === */
body, html, * {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif !important;
}

/* === ANTHRACITE DESIGN === */
body, html {
    background: #2e2e2e !important;
    color: #f0f0f0 !important;
}

/* === COLORED HEADING HIERARCHY === */
body h1 { color: #ffffff !important; font-size: 1.8rem !important; }
body h2 { color: #7db3f3 !important; font-size: 1.5rem !important; }
body h3 { color: #98d982 !important; font-size: 1.3rem !important; }
body h4 { color: #f9cc81 !important; font-size: 1.2rem !important; }

/* === LINKS === */
body a { color: #7db3f3 !important; }
body a:hover { color: #98d982 !important; }

/* === CODE BOXES (This was a battle!) === */
body pre {
    background: #1e1e1e !important;
    color: #f0f0f0 !important;
    border: 1px solid #444 !important;
    border-radius: 8px !important;
    padding: 1rem !important;
}

body code {
    background: #1e1e1e !important;
    color: #f0f0f0 !important;
    border: 1px solid #444 !important;
    border-radius: 8px !important;
    padding: 0.2rem 0.4rem !important;
}

/* === IMAGES WITH ROUNDED CORNERS === */
body img {
    border-radius: 12px !important;
    max-width: 100% !important;
}

/* === REMOVE FOOTER === */
body footer { display: none !important; }

HTML Shortcuts for Beautiful Images

For consistent image presentation, I use these HTML shortcuts:

<!-- Standard image with shadow -->
<img src="https://cdn.example.com/images/image.png" alt="Description" 
     style="border-radius: 12px !important; 
            box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; 
            margin: 2rem auto !important; 
            display: block !important;">

<!-- Image with caption -->
<figure style="text-align: center; margin: 2rem auto;">
    <img src="https://cdn.example.com/images/image.png" alt="Description"
         style="border-radius: 12px !important; 
                box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important;">
    <figcaption style="color: #8b949e; font-style: italic; margin-top: 1rem;">
        Caption here
    </figcaption>
</figure>

ActivityPub: Automatically in the Fediverse

The best thing about WriteFreely: It just works!

Testing Federation

# Test WebFinger
curl https://blog.example.com/.well-known/webfinger?resource=acct:username@blog.example.com

# ActivityPub profile
curl -H "Accept: application/activity+json" https://blog.example.com/@username

Troubleshooting: Common Problems

Problem 1: Code boxes stay white

Solution: Edit WriteFreely's write.css directly:

cd /opt/writefreely/static/css
sudo nano write.css

# Search for: background:#f8f8f8
# Replace with: background:#1e1e1e

Problem 2: CSS not applied

Solution: Aggressively clear browser cache, test different browsers.

Problem 3: ActivityPub doesn't work

Solution: – federation = true in config.ini – host = https://your-real-domain.com (not localhost!) – Reverse proxy configured correctly

Webspace for Images

Separate container for static assets:

# docker-compose.yml for webspace
services:
  webspace:
    image: httpd:latest
    volumes:
      - ./webspace:/usr/local/apache2/htdocs
    labels:
      - "traefik.http.routers.webspace.rule=(Host(`cdn.example.com`))"
      # ... Additional labels

Conclusion: Is the Switch Worth It?

Definitely yes! After migration:

Much faster – Page loads lightning fast ✅ Less maintenance – One binary, no updates, no Node.js ✅ Fediverse integration – My posts automatically reach more people ✅ Focus on writing – No distractions from complex themes/plugins ✅ Resource efficient – Runs smoothly on small LXC

Resources

Credits

Migration completed: ✅ Self-hosted, ✅ Fediverse-ready, ✅ Minimal & fast