Von Ghost zu Hugo - Ein Schritt-für-Schritt-Leitfaden zur Umwandlung und kostenlosen Bereitstellung Ihrer statischen Website auf Cloudflare Pages KI Generiert

Von Ghost zu Hugo - Ein Schritt-für-Schritt-Leitfaden zur Umwandlung und kostenlosen Bereitstellung Ihrer statischen Website auf Cloudflare Pages

Veröffentlicht:

Inhaltsverzeichnis

Klassische CMS Systeme wie WordPress bieten auch für "Nicht Entwickler" den Vorteil, dass es einfach und ohne Programmierkenntnisse möglich ist Blogbeiträge und Inhalte einfach zu veröffentlichen. Nicht selten übernehmen Marketing Agenturen oder Freelancer das schreiben von Blogbeiträgen. Klassische CMS Systeme haben jedoch Ihre Nachteile, so bieten Sie durch mehr PHP Code und Plugins mehr Angriffsflächen und mögliche Code Fehler. Mehr Abhängigkeiten zu Dritthersteller und oft mehr Aufwand im Bereich Datensicherung.  

Eine gute Lösung für das Problem ist es einfach ein Headless-CMS System im Backend zu verwenden und einen Static Page Generator im Frontend. So können Inhalte einfach im Headless CMS System wie gewohnt eingepflegt werden und über einen Build Prozess dann als Statische Website ausgeliefert werden. Im Frontend läuft dann kein PHP Code und keine Datenbank.

Dabei kann man nicht nur die Vorteile eines modernen CMS nutzen, sondern auch die Geschwindigkeit und Sicherheit statischer Websites genießen. In diesem Blogbeitrag werden wir ein Python-Skript vorstellen, das dies ermöglicht, und erläutern, wie Sie das CMS Ghost sowie Hugo konfigurieren müssen, damit dies funktioniert. Außerdem zeigen wir, wie Sie Ihre statische Seite kostenlos auf Cloudflare Pages hosten und Änderungen in Ghost (neue Blogbeiträge, Änderungen an Blogbeiträgen) automatisch über einen Webhook ausspielen lassen können.

Voraussetzungen

  • Im diesem Blogbeitrag gehen wir davon aus, dass Sie bereits ein CMS haben das eine API für den Zugriff auf Blogbeiträgen ermöglicht. Wir nutzen in diesem Beitrag das CMS "Ghost".
  • Ebenso gehen wir davon aus, dass Sie zumindest Grundkenntnisse in Hugo haben.
  • Sie haben ein Versionskontrollsystem wie GitHub oder Gitlab.

Vorbereitung und Konfiguration von Ghost

Bevor wir das Skript verwenden können, müssen wir sicherstellen, dass Ghost korrekt konfiguriert ist und die erforderlichen API-Schlüssel bereitgestellt werden.

Schritt 1: API-Schlüssel generieren

  1. Melden Sie sich bei Ihrem Ghost-Admin-Panel an.
  2. Navigieren Sie zu Integrationen und wählen Sie Neues benutzerdefiniertes Integrations-Token.
  3. Geben Sie Ihrer Integration einen Namen und erstellen Sie sie.
  4. Kopieren Sie den generierten Content API Key und die API-URL. Diese werden später im Skript benötigt.

Schritt 2: Umgebungsvariablen einrichten

Erstellen Sie eine .env-Datei in Ihrem Projektverzeichnis und fügen Sie die folgenden Zeilen hinzu, wobei Sie YOUR_GHOST_URL und YOUR_GHOST_KEY durch die tatsächlichen Werte ersetzen:

GHOST_URL=https://YOUR_GHOST_URL
GHOST_KEY=YOUR_GHOST_KEY

Diese Umgebungsvariablen werden vom Skript verwendet, um die API-Anfragen an Ghost zu authentifizieren.

Das Python-Skript zur Umwandlung von Ghost zu Hugo

Das Skript, das wir verwenden werden, lädt Blogbeiträge aus Ghost herunter, konvertiert sie in Markdown und speichert die zugehörigen Bilder und Autorinformationen lokal ab. Es erstellt außerdem Thumbnails der Bilder.

Legen Sie das Script in den Hugo Ordner auf oberster Ebene. Die Bilder werden direkt in den Static Ordner gelegt und in den Blogbeiträgen verlinkt.

Hier ist das vollständige Skript:

```

import os
import requests
import yaml
from markdownify import markdownify as md
from PIL import Image
from io import BytesIO
from bs4 import BeautifulSoup
from dotenv import load_dotenv
import re
import mimetypes

# Laden der Umgebungsvariablen
load_dotenv()

# Konfiguration der Ghost API
GHOST_URL = os.getenv('GHOST_URL')
GHOST_KEY = os.getenv('GHOST_KEY')
API_URL = f'{GHOST_URL}/ghost/api/content/posts/?key={GHOST_KEY}&include=tags,authors'

# Verzeichnisse für Bilder
IMAGES_DIR = 'static/images'
THUMBNAILS_DIR = os.path.join(IMAGES_DIR, 'thumbnails')

# Erstellen der Verzeichnisse, falls sie nicht existieren
os.makedirs(IMAGES_DIR, exist_ok=True)
os.makedirs(THUMBNAILS_DIR, exist_ok=True)

# Funktion zum Herunterladen von Bildern
def download_image(url):
    response = requests.get(url)
    response.raise_for_status()
    content_type = response.headers['Content-Type']
    extension = mimetypes.guess_extension(content_type)
    if extension is None:
        extension = '.jpg'  # Standarderweiterung, wenn keine gefunden wird
    return response.content, extension

# Funktion zum Erstellen eines Thumbnails
def create_thumbnail(image_path, thumbnail_path):
    with Image.open(image_path) as img:
        img.thumbnail((150, 150))
        img.save(thumbnail_path)

# Funktion zum Bereinigen des Dateinamens
def clean_filename(filename, extension):
    filename = re.sub(r'[?&].*$', '', filename)
    if not filename.endswith(extension):
        filename += extension
    return filename

# Funktion zum Konvertieren eines Ghost-Beitrags in Markdown
def convert_to_markdown(post):
    content = post['html']
    soup = BeautifulSoup(content, 'html.parser')
    
    # Hinzufügen des Titels als H1-Element im HTML-Inhalt
    title = post['title']
    
    toc = ''
    headers = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
    level = 1
    for header in headers:
        header_id = header.text.strip().replace(' ', '-').lower()
        header['id'] = header_id
        toc += f'<li><a href="#{header_id}">{header.text}</a></li>'
    toc = f'<nav id="TableOfContents"><b>Inhaltsverzeichnis</b><br/><ul>{toc}</ul></nav>'
    
    frontmatter = {
        'title': title if 'title' in post else post['meta_title'],  
        'description': post.get('meta_description', post['excerpt']),
        'pagetitle': title,
        'slug': post['slug'],
        'feature_image': '',
        'thumbnail_image': '',
        'lastmod': post['updated_at'],
        'date': post['published_at'],
        'summary': post['excerpt'],
        'i18nlanguage': 'en',  # Change for your language
        'weight': 1 if post['featured'] else 0,
        'draft': post['visibility'] != 'public',
        'NoTOC': 1,
        'imageAuthorHTML': post.get('feature_image_caption', ''),
    }

    if 'og_title' in post:
        frontmatter['og_title'] = post['og_title']
    if 'og_description' in post:
        frontmatter['og_description'] = post['og_description']

    if post['tags']:
        frontmatter['categories'] = [tag['name'] for tag in post['tags']]

    if post['authors']:
        for author in post['authors']:
            author_image_url = author['profile_image']
            image_filename = clean_filename(os.path.basename(author_image_url), '')
            image_content, extension = download_image(author_image_url)
            image_filename = clean_filename(image_filename, extension)
            image_filename = f"{author['slug']}-{image_filename}"
            image_path = os.path.join(IMAGES_DIR, image_filename)
            thumbnail_path = os.path.join(THUMBNAILS_DIR, image_filename)

            with open(image_path, 'wb') as f:
                f.write(image_content)
            create_thumbnail(image_path, thumbnail_path)

            author['profile_image'] = f'/images/thumbnails/{image_filename}'
        frontmatter['authors'] = post['authors']

    if 'canonical_url' in post:
        frontmatter['canonical'] = post['canonical_url']

    if post['feature_image']:
        feature_image_url = post['feature_image']
        image_filename = clean_filename(os.path.basename(feature_image_url), '')
        image_content, extension = download_image(feature_image_url)
        image_filename = clean_filename(image_filename, extension)
        image_path = os.path.join(IMAGES_DIR, image_filename)
        thumbnail_path = os.path.join(THUMBNAILS_DIR, image_filename)

        with open(image_path, 'wb') as f:
            f.write(image_content)
        create_thumbnail(image_path, thumbnail_path)

        frontmatter['feature_image'] = f'/images/{image_filename}'
        frontmatter['thumbnail_image'] = f'/images/thumbnails/{image_filename}'

    markdown_content = md(str(soup))

    return f"---\n{yaml.dump(frontmatter)}---\n{toc}\n\n{markdown_content}"

# Abrufen und Verarbeiten der Beiträge von der Ghost API
def fetch_and_convert_posts():
    print(f"Fetching posts from {API_URL}")
    response = requests.get(API_URL)
    
    # Debugging: Ausgabe des HTTP-Statuscodes und der URL
    print(f"HTTP Status Code: {response.status_code}")
    print(f"Requested URL: {response.url}")

    # Überprüfen des Statuscodes
    if response.status_code == 404:
        print("Error: The requested URL was not found.")
        return

    response.raise_for_status()
    posts = response.json()['posts']
    for post in posts:
        markdown_content = convert_to_markdown(post)
        markdown_filename = f"content/posts/{post['slug']}.md"
        os.makedirs(os.path.dirname(markdown_filename), exist_ok=True)
        with open(markdown_filename, 'w', encoding='utf-8') as f:
            f.write(markdown_content)

if __name__ == "__main__":
    fetch_and_convert_posts()


```

Das Script verwendet diverse Abhängigkeiten. Diese hinterlegen wir in einer Datei (gleiches Verzeichnis) mit dem Namen "requirements.txt"

requests
pyyaml
pillow
markdownify
beautifulsoup4
python-dotenv

Das Script können Sie nun ausführen mit

pip install -r requirements.txt
python ./build.py

Deployment auf Cloudflare Pages

Nachdem das Script die Markdown-Dateien generiert hat, werden diese von Hugo als Blogbeiträge verwendet. Sie können nun Ihre Hugo Website wie verwohnt veröffentlichen. Wenn Sie mit Cloudflare Pages Ihre Website hosten möchten. Können Sie dort die Seite kostenlos anlegen und den Build Process auch darüber automatisieren sowie Änderungen im Ghost direkt über einen Webhook ausspielen.

Schritt 1: Cloudflare Pages einrichten

  1. Erstellen Sie ein kostenloses Konto bei Cloudflare.
  2. Navigieren Sie zu Pages und erstellen Sie ein neues Projekt.
  3. Verbinden Sie Ihr Git-Repository mit dem Hugo-Projekt.
  4. Konfigurieren Sie die Build-Einstellungen: Damit sämtliche Abhängigkeiten im Python Build verwendet werden können, nutzen wir ein Bash Script um Cloudflare zu konfigurieren.

Auch diese Datei legen wir direkt ins Hugo Verzeichnis mit dem Namen "deploy.sh"

npm install @tryghost/content-api js-yaml fs-extra dotenv jsdom flowbite axios sharp
npm install
echo $PWD
python -m pip install -r requirements.txt
python ./build.py

Ebenso konfigurieren wir unter "Umgebungsvariablen" folgende Versionen.

Variable NameWert
HUGO_VERSIONv0.105.0
NODE_VERSIONv19.9.0
PYTHON_VERSIONv3.11.5

Als Build Command verwenden wir:

/bin/sh ./deploy.sh && hugo
Build Einstellungen für Hugo
Build Einstellungen für Hugo

Schritt 2: Website erstellen und CNAME-Record anlegen

Sofern der Build erfolgreich war, bekommen Sie eine Webadresse von Cloudflare Pages. Sie können nun eine Domain auf die Webadresse legen indem Sie auf "Custom Domain" klicken und den Anweisungen folge leisten.

Schritt 3: Webhook einrichten

Um automatische Deployments bei Änderungen in Ghost zu ermöglichen, können Sie einen Webhook einrichten:

  1. Gehen Sie im Cloudflare Pages Dashboard zu den Deployments-Einstellungen.
  2. Erstellen Sie einen neuen Webhook.
  3. Kopieren Sie die Webhook-URL.
  4. Gehen Sie zurück zu Ihrem Ghost-Admin-Panel und navigieren Sie zu Integrationen.
  5. Fügen Sie eine neue Integration hinzu und geben Sie die Webhook-URL ein.

Für folgende Aktionen habe ich ein Webhook angelegt:

  • Neue Blogartikel
  • Änderung an Blogartikel
  • Löschen von Blogartikel

Nun wird jedes Mal, wenn Sie einen neuen Beitrag in Ghost veröffentlichen oder aktualisieren, ein Deployment auf Cloudflare Pages ausgelöst und Ihre statische Website wird automatisch aktualisiert.

Fazit

Mit diesem Ansatz können Sie die leistungsstarke Content-Management-Funktionalität von Ghost nutzen und gleichzeitig die Geschwindigkeit und Sicherheit statischer Websites, die mit Hugo und Cloudflare Pages bereitgestellt werden, genießen. Indem Sie die hier vorgestellten Schritte befolgen, können Sie Ihre Inhalte effizient verwalten und bereitstellen, ohne auf Flexibilität und Benutzerfreundlichkeit zu verzichten. Viel Erfolg bei Ihrem Projekt!