Mit Ghost CMS Webseiten für Hugo erstellen und auf Cloudflare veröffentlichen


Vereinfachter Workflow um eine Website zu publizieren:

1. Ghost CMS (lokale VM)
→ Artikel schreiben/bearbeiten
→ http://deinelokaleip:2368/ghost

2. Ghost → Hugo Script
→ python3 ghost_to_hugo.py
→ Artikel werden zu Hugo Markdown Dateien

3. Deploy Script
→ ~/websitename/deploy.sh
→ Hugo baut statische Seite
→ Wrangler pusht zu Cloudflare

4. Live auf Cloudflare Pages ✅
https://webesite.pages.dev

Ghost -> Hugo Python Script

~/deinewebsite-project/ghost-exporter/ghost_to_hugo.py
import os import re import sys import json import yaml import html import shutil import requests import html2text import urllib.request from datetime import datetime from pathlib import Path from urllib.parse import urlparse
============================================================
KONFIGURATION
============================================================
GHOST_URL = "http://deineip" API_KEY = "deinapikey"
HUGO_ROOT = "../deinewebsite-hugo" HUGO_CONTENT_POSTS = f"{HUGO_ROOT}/content/posts" HUGO_CONTENT_PAGES = f"{HUGO_ROOT}/content" HUGO_STATIC_IMAGES = f"{HUGO_ROOT}/static/images"
Bilder herunterladen?
DOWNLOAD_IMAGES = True
Bestehende Posts löschen vor Export?
CLEAN_BEFORE_EXPORT = True
beautifulhugo spezifisch
THEME = "beautifulhugo"
============================================================
LOGGING
============================================================
def log(msg, level="INFO"): timestamp = datetime.now().strftime("%H:%M:%S") icons = {"INFO": "ℹ", "OK": "✅", "WARN": "⚠", "ERROR": "❌", "RUN": "🔄"} print(f"[{timestamp}] {icons.get(level, '•')} {msg}")
============================================================
GHOST API
============================================================
def get_from_ghost(endpoint, extra_params=None): """Holt Daten von der Ghost Content API""" url = f"{GHOST_URL}/ghost/api/content/{endpoint}/" params = { "key": API_KEY, "limit": "all", "formats": "html", "include": "tags,authors" } if extra_params: params.update(extra_params)
try:
    response = requests.get(url, params=params, timeout=30)
    response.raise_for_status()
    return response.json()
except requests.exceptions.ConnectionError:
    log(f"Kann Ghost nicht erreichen: {GHOST_URL}", "ERROR")
    log("Ist die VM gestartet? Ist Ghost aktiv?", "ERROR")
    sys.exit(1)
except requests.exceptions.HTTPError as e:
    log(f"API Fehler: {e}", "ERROR")
    sys.exit(1)
def get_all_posts(): log("Lade Posts von Ghost...", "RUN") data = get_from_ghost("posts") posts = data.get("posts", []) log(f"{len(posts)} Posts gefunden", "OK") return posts
def get_all_pages(): log("Lade Pages von Ghost...", "RUN") data = get_from_ghost("pages") pages = data.get("pages", []) log(f"{len(pages)} Pages gefunden", "OK") return pages
============================================================
BILD-VERARBEITUNG
============================================================
def sanitize_filename(filename): """Bereinigt Dateinamen""" filename = re.sub(r'[^\w\s-.]', '', filename) filename = re.sub(r'\s+', '-', filename) return filename.lower()
def download_image(img_url, save_dir): """Lädt ein Bild herunter und gibt den neuen lokalen Pfad zurück""" if not img_url: return None
# Vollständige URL wenn relativ
if img_url.startswith("/"):
    img_url = f"{GHOST_URL}{img_url}"

try:
    parsed = urlparse(img_url)
    filename = sanitize_filename(os.path.basename(parsed.path))

    if not filename or '.' not in filename:
        filename = "image.jpg"

    os.makedirs(save_dir, exist_ok=True)
    save_path = os.path.join(save_dir, filename)

    if not os.path.exists(save_path):
        headers = {'User-Agent': 'Mozilla/5.0'}
        req = urllib.request.Request(img_url, headers=headers)
        with urllib.request.urlopen(req, timeout=15) as response:
            with open(save_path, 'wb') as f:
                f.write(response.read())
        log(f"  Bild gespeichert: {filename}", "OK")

    # Relativer Pfad für Hugo Static
    relative_to_static = os.path.relpath(
        save_path,
        f"{HUGO_ROOT}/static"
    )
    return f"/{relative_to_static}"

except Exception as e:
    log(f"  Bild-Download fehlgeschlagen ({img_url}): {e}", "WARN")
    return img_url  # Original-URL als Fallback
def process_html_images(html_content, post_slug): """Findet alle Bilder im HTML und lädt sie herunter""" if not DOWNLOAD_IMAGES: return html_content
img_dir = os.path.join(HUGO_STATIC_IMAGES, post_slug)
img_pattern = re.compile(r'<img([^>]*?)src=["\']([^"\']+)["\']([^>]*?)>', re.IGNORECASE)

def replace_img(match):
    before = match.group(1)
    url = match.group(2)
    after = match.group(3)
    new_url = download_image(url, img_dir)
    return f'<img{before}src="{new_url}"{after}>'

return img_pattern.sub(replace_img, html_content)
============================================================
HTML → MARKDOWN
============================================================
def html_to_markdown(html_content): """Konvertiert Ghost HTML zu Markdown""" converter = html2text.HTML2Text() converter.ignore_links = False converter.ignore_images = False converter.ignore_emphasis = False converter.body_width = 0 converter.protect_links = True converter.unicode_snob = True return converter.handle(html_content).strip()
============================================================
FRONT MATTER - beautifulhugo Format
============================================================
def build_front_matter(post, is_page=False): """Erstellt beautifulhugo-kompatibles Front Matter"""
pub_date  = post.get("published_at") or post.get("created_at", "")
updated   = post.get("updated_at", "")

# Tags und Kategorien trennen
# Ghost interne Tags beginnen mit # → werden Kategorien
tags = []
categories = []
if post.get("tags"):
    for tag in post["tags"]:
        name = tag.get("name", "")
        if name.startswith("#"):
            categories.append(name[1:].strip())
        else:
            tags.append(name)

# Autoren
# beautifulhugo erwartet "author" als String (erster Autor)
author = ""
if post.get("authors"):
    author = post["authors"][0].get("name", "")

# Basis Front Matter
front_matter = {
    "title":   post.get("title", "Kein Titel"),
    "date":    pub_date,
    "lastmod": updated,
    "draft":   post.get("status") != "published",
    "slug":    post.get("slug", ""),
    "author":  author,          # beautifulhugo: nur String, kein Array
}

# Description / Excerpt
if post.get("custom_excerpt"):
    front_matter["description"] = post["custom_excerpt"]
elif post.get("meta_description"):
    front_matter["description"] = post["meta_description"]

# Tags & Kategorien nur wenn vorhanden
if tags:
    front_matter["tags"] = tags
if categories:
    front_matter["categories"] = categories

# -------------------------------------------------------
# Cover / Feature Image
# beautifulhugo nutzt "image" direkt (kein verschachteltes cover-Objekt)
# -------------------------------------------------------
if post.get("feature_image"):
    img_url = post["feature_image"]
    if DOWNLOAD_IMAGES:
        img_dir   = os.path.join(HUGO_STATIC_IMAGES, post.get("slug", "unknown"))
        local_url = download_image(img_url, img_dir)
        front_matter["image"] = local_url or img_url
    else:
        front_matter["image"] = img_url

    # Thumbnail = gleiche Bild (beautifulhugo zeigt thumbnail in Listen)
    front_matter["thumbnail"] = front_matter["image"]

# SEO
if post.get("meta_title"):
    front_matter["meta_title"] = post["meta_title"]

if post.get("canonical_url"):
    front_matter["canonicalURL"] = post["canonical_url"]

return front_matter
============================================================
POST KONVERTIEREN
============================================================
def convert_post(post, output_dir, is_page=False): """Konvertiert einen Ghost Post zu Hugo Markdown"""
slug  = post.get("slug", "unknown")
title = post.get("title", "Kein Titel")

log(f"Konvertiere: {title}", "RUN")

# HTML Content
html_content = post.get("html", "") or ""

# Bilder im Content ersetzen
if html_content and DOWNLOAD_IMAGES:
    log(f"  Verarbeite Bilder...")
    html_content = process_html_images(html_content, slug)

# HTML → Markdown
markdown_content = html_to_markdown(html_content) if html_content else ""

# Front Matter
front_matter = build_front_matter(post, is_page)

yaml_str = yaml.dump(
    front_matter,
    allow_unicode=True,
    default_flow_style=False,
    sort_keys=False
)

file_content = f"---\n{yaml_str}---\n\n{markdown_content}\n"

# Speichern
os.makedirs(output_dir, exist_ok=True)
filepath = os.path.join(output_dir, f"{slug}.md")

with open(filepath, "w", encoding="utf-8") as f:
    f.write(file_content)

log(f"  Gespeichert: {slug}.md", "OK")
return filepath
============================================================
STATISCHE SEITEN
beautifulhugo hat KEINE eigene /search Seite
Dafür: About, Impressum etc. aus Ghost Pages
============================================================
def create_about_page(): """Erstellt About-Seite falls nicht aus Ghost importiert""" about_dir = os.path.join(HUGO_CONTENT_PAGES, "about") about_file = os.path.join(about_dir, "index.md")
if os.path.exists(about_file):
    log("About-Seite bereits vorhanden (übersprungen)", "INFO")
    return

os.makedirs(about_dir, exist_ok=True)
content = """\

title: "Über deinewebsite" date: 2024-01-01 draft: false
Über deinewebsite
Willkommen bei deinewebsite – hier findest du spannende Artikel zu verschiedenen Themen.
Kontakt
Schreib uns: kontakt@deinewebsite.de """ with open(about_file, "w", encoding="utf-8") as f: f.write(content) log("About-Seite erstellt", "OK")
def create_impressum_page(): """Erstellt leere Impressum-Seite als Platzhalter""" imp_dir = os.path.join(HUGO_CONTENT_PAGES, "impressum") imp_file = os.path.join(imp_dir, "index.md")
if os.path.exists(imp_file):
    return

os.makedirs(imp_dir, exist_ok=True)
content = """\

title: "Impressum" date: 2024-01-01 draft: false
Impressum
Angaben gemäß § 5 TMG
Betreiber: deinewebsite
E-Mail: kontakt@deinewebsite.de
""" with open(imp_file, "w", encoding="utf-8") as f: f.write(content) log("Impressum-Seite erstellt", "OK")
============================================================
AUFRÄUMEN
============================================================
def clean_hugo_content(): """Löscht alte generierte Content-Dateien""" if os.path.exists(HUGO_CONTENT_POSTS): shutil.rmtree(HUGO_CONTENT_POSTS) log("Alte Posts gelöscht", "OK")
if DOWNLOAD_IMAGES and os.path.exists(HUGO_STATIC_IMAGES):
    shutil.rmtree(HUGO_STATIC_IMAGES)
    log("Alte Bilder gelöscht", "OK")
============================================================
STATISTIK
============================================================
def print_stats(posts, pages, start_time): duration = (datetime.now() - start_time).seconds published_posts = [p for p in posts if p.get("status") == "published"] published_pages = [p for p in pages if p.get("status") == "published"]
print("\n" + "="*50)
print("📊 EXPORT ZUSAMMENFASSUNG")
print("="*50)
print(f"  Posts exportiert:      {len(posts)} ({len(published_posts)} veröffentlicht)")
print(f"  Pages exportiert:      {len(pages)} ({len(published_pages)} veröffentlicht)")
print(f"  Theme:                 {THEME}")
print(f"  Dauer:                 {duration} Sekunden")
print(f"  Hugo Content Ordner:   {os.path.abspath(HUGO_ROOT)}")
print("="*50)
print()
============================================================
MAIN
============================================================
def main(): start_time = datetime.now()
print("\n" + "="*50)
print(f"🚀 deinewebsite Ghost → Hugo Exporter ({THEME})")
print("="*50 + "\n")

# Aufräumen
if CLEAN_BEFORE_EXPORT:
    log("Bereinige alte Dateien...", "RUN")
    clean_hugo_content()

# Posts exportieren
posts = get_all_posts()
failed_posts = 0
for post in posts:
    try:
        convert_post(post, HUGO_CONTENT_POSTS)
    except Exception as e:
        log(f"Fehler bei Post '{post.get('title')}': {e}", "ERROR")
        failed_posts += 1

# Pages exportieren
pages = get_all_pages()
failed_pages = 0
for page in pages:
    try:
        convert_post(page, HUGO_CONTENT_PAGES, is_page=True)
    except Exception as e:
        log(f"Fehler bei Page '{page.get('title')}': {e}", "ERROR")
        failed_pages += 1

# Statische Hilfseiten
create_about_page()
create_impressum_page()

# Fehler-Zusammenfassung
if failed_posts or failed_pages:
    log(f"{failed_posts + failed_pages} Fehler beim Export aufgetreten", "WARN")

print_stats(posts, pages, start_time)
log("Export abgeschlossen!", "OK")
log("Teste lokal mit: cd ../deinewebsite-hugo && hugo server", "INFO")
if name == "main": main()

deploy script - deploy.sh

~/deinewebsite-project/deploy.sh
#!/bin/bash
set -e # Bei Fehler stoppen
Farben für Output
RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color
Konfiguration
PROJECT_DIR="$HOME/deinewebsite-project" EXPORTER_DIR="$PROJECT_DIR/ghost-exporter" HUGO_DIR="$PROJECT_DIR/deinewebsite-hugo" GHOST_VM_IP="deineip"
Timestamp
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "" echo -e "${BLUE}================================================${NC}" echo -e "${BLUE} 🎉 deinewebsite Deploy Pipeline${NC}" echo -e "${BLUE} $TIMESTAMP${NC}" echo -e "${BLUE}================================================${NC}" echo ""
----- SCHRITT 1: VM Erreichbarkeit prüfen -----
echo -e "${YELLOW}[1/6] Prüfe Ghost VM...${NC}" if ! ping -c 1 $GHOST_VM_IP &> /dev/null; then echo -e "${RED}❌ VM nicht erreichbar: $GHOST_VM_IP${NC}" echo "Bitte VirtualBox und Ghost starten" exit 1 fi
Ghost API prüfen
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" 
"http://$GHOST_VM_IP/ghost/api/content/posts/?key=test&limit=1" 
--max-time 5)
if [ "$HTTP_STATUS" = "000" ]; then echo -e "${RED}❌ Ghost nicht erreichbar${NC}" echo "SSH in VM und 'ghost status' prüfen" exit 1 fi
echo -e "${GREEN}✅ Ghost VM erreichbar${NC}"
----- SCHRITT 2: Theme prüfen -----
echo "" echo -e "${YELLOW}[2/6] Prüfe beautifulhugo Theme...${NC}"
THEME_DIR="$HUGO_DIR/themes/beautifulhugo"
if [ ! -d "$THEME_DIR" ]; then echo -e "${YELLOW}⚠️ Theme nicht gefunden - klone beautifulhugo...${NC}" cd $HUGO_DIR git submodule add https://github.com/halogenica/beautifulhugo.git themes/beautifulhugo echo -e "${GREEN}✅ Theme installiert${NC}" else # Theme aktualisieren echo " Theme gefunden, prüfe auf Updates..." cd $THEME_DIR git pull origin master --quiet 2>/dev/null && 
echo -e "${GREEN}✅ Theme aktuell${NC}" || 
echo -e "${YELLOW}⚠️ Theme Update übersprungen (kein Git oder offline)${NC}" fi
beautifulhugo benötigt keine JSON Output für Suche
Prüfen ob search-Layout vorhanden (beautifulhugo hat eigenes)
if [ ! -f "$HUGO_DIR/content/search/index.md" ]; then echo -e "${YELLOW}⚠️ Kein search/index.md - beautifulhugo nutzt eigene Suche${NC}" fi
----- SCHRITT 3: Ghost → Hugo Export -----
echo "" echo -e "${YELLOW}[3/6] Exportiere Ghost Content...${NC}" cd $EXPORTER_DIR source venv/bin/activate python3 ghost_to_hugo.py
if [ $? -ne 0 ]; then echo -e "${RED}❌ Export fehlgeschlagen${NC}" exit 1 fi
Exportierte Posts zählen
POST_COUNT=$(ls $HUGO_DIR/content/posts/*.md 2>/dev/null | wc -l) echo -e "${GREEN}✅ Export erfolgreich (${POST_COUNT} Posts)${NC}"
----- SCHRITT 4: Hugo Build -----
echo "" echo -e "${YELLOW}[4/6] Baue Hugo Site mit beautifulhugo...${NC}" cd $HUGO_DIR
Alten Build löschen
rm -rf public/
Config-Syntax prüfen
echo " Prüfe config.yaml..." if ! hugo config --quiet > /dev/null 2>&1; then echo -e "${RED}❌ config.yaml hat Fehler:${NC}" hugo config 2>&1 | grep -i error exit 1 fi
Hugo bauen
hugo --minify --gc
if [ $? -ne 0 ]; then echo -e "${RED}❌ Hugo Build fehlgeschlagen${NC}" echo "" echo -e "${YELLOW}Häufige beautifulhugo Fehler:${NC}" echo " → author muss Objekt sein (name/email), nicht String" echo " → Prüfe config.yaml Einrückung bei 'params:'" exit 1 fi
Build-Größe anzeigen
BUILD_SIZE=$(du -sh public/ | cut -f1) PAGE_COUNT=$(find public/ -name "*.html" | wc -l) echo -e "${GREEN}✅ Hugo Build erfolgreich${NC}" echo -e " 📦 Größe: ${BUILD_SIZE} | 📄 Seiten: ${PAGE_COUNT}"
----- SCHRITT 5: Lokale Vorschau (optional) -----
echo "" read -p "$(echo -e ${YELLOW}'Lokale Vorschau ansehen? (j/N): '${NC})" PREVIEW if [[ $PREVIEW =~ ^[Jj]$ ]]; then echo "" echo -e "${BLUE} beautifulhugo Vorschau läuft...${NC}" echo -e " Öffne: ${GREEN}http://localhost:1313${NC}" echo " STRG+C oder Enter zum Beenden" echo "" hugo server 
--disableFastRender 
--bind 0.0.0.0 
--port 1313 & SERVER_PID=$!
read -p "Enter drücken um weiter zu deployen..."
kill $SERVER_PID 2>/dev/null
wait $SERVER_PID 2>/dev/null
echo ""
fi
----- SCHRITT 6: Cloudflare Deploy -----
echo "" echo -e "${YELLOW}[6/6] Deploye zu Cloudflare Pages...${NC}"
Wrangler verfügbar?
if ! command -v wrangler &> /dev/null; then echo -e "${RED}❌ wrangler nicht gefunden${NC}" echo " Installieren: npm install -g wrangler" exit 1 fi
wrangler pages deploy public 
--project-name=deinewebsite 
--commit-message="deinewebsite Update: $TIMESTAMP"
if [ $? -ne 0 ]; then echo -e "${RED}❌ Cloudflare Deploy fehlgeschlagen${NC}" echo " Prüfe: wrangler login" exit 1 fi
----- FERTIG -----
echo "" echo -e "${GREEN}================================================${NC}" echo -e "${GREEN} ✅ deinewebsite erfolgreich deployed!${NC}" echo -e "${GREEN} 🌐 https://deinewebsite.pages.dev${NC}" echo -e "${GREEN} 📝 ${POST_COUNT} Posts live${NC}" echo -e "${GREEN} ⏱ $(date '+%H:%M:%S')${NC}" echo -e "${GREEN}================================================${NC}" echo ""

Deploy auf Cloudflare Pages (kostenlos)

1. Wrangler installieren

npm install -g wrangler

2. Bei Cloudflare anmelden

wrangler login

→ Öffnet Browser → Cloudflare Account einloggen → Erlauben klicken


3. Projekt bauen & deployen

cd ~/deinewebsite-project/deinewebsite-hugo

# Site bauen
hugo --minify

# Pagefind Index erstellen
pagefind --site public

# Zu Cloudflare deployen
wrangler pages deploy public --project-name=deinewebsite-blog

4. Fertig! 🎉

✨ Deployment complete!
🌍 https://deinewebsite.pages.dev

Du bekommst eine kostenlose URL wie deinewebsite-blog.pages.dev

News  coding