From Ghost to WriteFreely: My Migration to the Fediverse
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:
- ✅ Native ActivityPub Integration – My blog is automatically available in the Fediverse
- ✅ Minimal Resource Usage – Single binary, no Node.js, no complex infrastructure
- ✅ Focus on Writing – No distractions, just writing
- ✅ Self-hosted – Full control over my data
- ✅ Markdown-native – No formatting battles
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.com
– Backend: http://192.168.1.100:8080
– SSL: 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!
- My blog:
@username@blog.example.com
- Followers: Automatically via Mastodon, Pleroma, etc.
- Posts: Automatically federated to the Fediverse
- Interaction: Comments via ActivityPub possible
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
- WriteFreely: https://writefreely.org
- ActivityPub Plugin (WordPress): https://wordpress.org/plugins/activitypub/
- Fediverse Test: https://fediverse.party
Credits
- Header Image by Daniel Thomas on Unsplash
Migration completed: ✅ Self-hosted, ✅ Fediverse-ready, ✅ Minimal & fast