Ghost als Headless CMSGhost + Hugo: Die perfekte Kombination aus Headless CMS und Static Site Generator
Veröffentlicht:Nach vielen Experimenten mit verschiedenen Content-Management-Systemen und Static Site Generators habe ich mich für eine Kombination entschieden, die für meine Anforderungen perfekt funktioniert: Ghost als Headless CMS und Hugo als Static Site Generator. In diesem Beitrag erkläre ich, warum diese Kombination für mich die beste Lösung ist.
Warum klassische CMS wie WordPress nicht mehr ausreichen
Klassische CMS wie WordPress oder Joomla haben mich immer wieder vor Herausforderungen gestellt. Zwar beschleunigen die WYSIWYG Editoren von CMS System extrem die Entwicklungszeit von neues Seiten und Inhalten. Aber diese Zeitersparnis ging oft für Systempflege und Optimierung wieder drauf. Ein großes Problem waren die ständigen Updates – sowohl für das CMS selbst als auch für die zahlreichen Erweiterungen. Jedes Update brachte das Risiko mit sich, dass Plugins oder Themes nicht mehr kompatibel waren. Ein weiteres Ärgernis war der Quellcode der Erweiterungen, der oft unminifiziertes und schlecht optimiertes CSS und JavaScript mitbrachte, was die Ladezeiten negativ beeinflusste. Auch viele Themes waren überladen mit unnötigem HTML und CSS, was nicht nur die Performance verschlechterte, sondern auch das individuelle Anpassen erschwerte. Zusätzlich verursachte der Overhead durch PHP und Datenbankabfragen unnötige Verzögerungen bei der HTML-Generierung – selbst einfache Seiten mussten erst aus verschiedenen Quellen zusammengesetzt werden, anstatt direkt statisch ausgeliefert zu werden. Diese Faktoren haben mich dazu gebracht, eine schlankere, performantere Lösung zu suchen – und mit Ghost + Hugo habe ich sie gefunden.
Vorteile von Ghost als Headless CMS
Ghost ist ursprünglich als leichtgewichtiges Blogging-Framework bekannt, doch es bietet auch eine leistungsstarke API, mit der sich Inhalte als Headless CMS verwalten lassen. Hier sind die Hauptgründe, warum ich Ghost nutze:
- Markdown-basierte Inhalte: Schreiben in Markdown ist effizient, und Ghost bietet eine tolle Editor-Erfahrung.
- Saubere und intuitive UI: Die Admin-Oberfläche von Ghost ist minimalistisch, schnell und einfach zu bedienen.
- Starke API: Die REST- und GraphQL-API von Ghost ermöglicht eine einfache Anbindung an externe Frontends.
- Integrierte SEO-Funktionen: Meta-Tags, Canonical URLs, AMP-Unterstützung – alles direkt mit dabei.
- Self-hosted : Ich kann Ghost auf meinem eigenen Server hosten oder den offiziellen Ghost(Pro)-Service nutzen.
Da Ghost als Headless CMS genutzt werden kann, trenne ich die Inhalte von der Darstellung – genau hier kommt Hugo ins Spiel. Ghost bietet mit den Komfort den ich von WordPress kenne, jedoch werden die Inhalte beim Ausspielen von Blogartikel statisch erzeugt und somit unabhängig von Ghost verfügbar.
Warum ich Hugo als Static Site Generator gewählt habe?
Hugo ist einer der schnellsten Static Site Generatoren auf dem Markt. Er generiert komplette Websites in Millisekunden und ist besonders für Blogs oder Content-lastige Websites geeignet. Meine Hauptgründe für Hugo:
- Unglaubliche Geschwindigkeit: Selbst große Websites mit vielen Seiten werden in Sekunden generiert.
- Markdown-First Ansatz: Perfekte Integration mit den Markdown-Inhalten aus Ghost.
- Kein Overhead durch Datenbanken: Statische Seiten sind sicher und extrem schnell.
- SEO-freundlich: Automatische Generierung von sitemaps, strukturierte Daten und schnelle Ladezeiten.
- Flexibel mit Themes: Es gibt viele anpassbare Themes und eine aktive Community. Ich selbst hab mein Theme mit Flowbite entwickelt und habe damit eine sehr hohe Pagespeed. Um optimierung muss ich nicht überhaupt nicht mehr kümmern.
Durch die Kombination von Ghost und Hugo habe ich also eine saubere Trennung von Content-Management und Frontend-Rendering, was mir maximale Performance und Flexibilität bietet.
So funktioniert die Ghost + Hugo Integration?
Inhalte in Ghost erstellen
- Ich schreibe meine Artikel direkt in Ghost und nutze die API, um die Inhalte abzurufen.
Beiträge per API abrufen
- Über die Ghost Content API hole ich mir die Beiträge als JSON und speichere sie als Markdown-Dateien in Hugo. Dieser Schritt übernimmt ein Python Script für mich. Dieses Script wird direkt über Cloudflare Pages getriggert und spielt mir alle Webinhalte neu aus.
Markdown mit Hugo verarbeiten
- Mit
hugowerden die Markdown-Dateien in eine ultraschnelle, statische Website umgewandelt.
Automatisiertes Deployment mit Cloudflare
- Ich pushe die generierten Dateien auf GitLab und Cloudflare deployed die Seite automatisch. Vor dem ausliefern wird noch das Python Script zum erzeugen der Blogbeiträge getriggert.
Das Python-Skript im Detail
Hier das Python Script das ich nutze, falls Ihr es gebrauchen könnt
import os
import requests
import yaml
from PIL import Image
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&limit=15&page='
# 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 Bereinigen des Dateinamens
def clean_filename(filename, extension):
filename = re.sub(r'[?&].*$', '', filename)
if not filename.endswith(extension):
filename += extension
return filename
# Funktion zum Zuschneiden und Anpassen der Bildgröße
def resize_and_crop(image_path, output_path, size=(1024, 1024)):
with Image.open(image_path) as img:
img_width, img_height = img.size
target_width, target_height = size
if img_width > target_width or img_height > target_height:
crop_width = min(img_width, target_width)
crop_height = min(img_height, target_height)
left = (img_width - crop_width) / 2
top = (img_height - crop_height) / 2
right = (img_width + crop_width) / 2
bottom = (img_height + crop_height) / 2
img = img.crop((left, top, right, bottom))
img = img.resize(size, Image.LANCZOS)
img.save(output_path)
# Funktion zum Erstellen eines Thumbnails mit Zuschneiden und Anpassen der Bildgröße
def create_thumbnail(image_path, thumbnail_path, size=(356, 356)):
with Image.open(image_path) as img:
img_width, img_height = img.size
if img_width != img_height:
crop_size = min(img_width, img_height)
left = (img_width - crop_size) / 2
top = (img_height - crop_size) / 2
right = (img_width + crop_size) / 2
bottom = (img_height + crop_size) / 2
img = img.crop((left, top, right, bottom))
img.thumbnail(size, Image.LANCZOS)
img.save(thumbnail_path)
# Funktion zum Konvertieren eines Ghost-Beitrags
def convert_to_html(post):
content = post['html']
soup = BeautifulSoup(content, 'html.parser')
# Hinzufügen des Titels als H1-Element im HTML-Inhalt
title = post['title']
frontmatter = {
'title': title if 'title' in post else post['meta_title'],
# Verwende den Titel aus den Metadaten, falls er im Frontmatter fehlt
'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)
# Resize and crop thumbnail to 356x356
create_thumbnail(image_path, thumbnail_path, size=(356, 356))
frontmatter['feature_image'] = f'/images/{image_filename}'
frontmatter['thumbnail_image'] = f'/images/thumbnails/{image_filename}'
return f"---\n{yaml.dump(frontmatter)}---\n\n{content}"
# Abrufen und Verarbeiten der Beiträge von der Ghost API
def fetch_and_convert_posts():
page = 1
all_posts = []
while True:
print(f"Fetching posts from {API_URL}{page}")
response = requests.get(f"{API_URL}{page}")
# 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']
if not posts:
break
all_posts.extend(posts)
page += 1
print(f"Converting {len(all_posts)} posts to HTML")
for post in all_posts:
html_content = convert_to_html(post)
html_filename = f"content/posts/{post['slug']}.html"
os.makedirs(os.path.dirname(html_filename), exist_ok=True)
with open(html_filename, 'w', encoding='utf-8') as f:
f.write(html_content)
if __name__ == "__main__":
fetch_and_convert_posts()
Fazit: Warum Ghost + Hugo mein Setup bleibt
Die Kombination aus Ghost als Headless CMS und Hugo als Static Site Generator bietet für mich die besten Vorteile:
Maximale Performance durch statische Seiten
Einfache Content-Pflege dank Ghosts schöner UI
Flexibilität & Kontrolle durch die API-gesteuerte Struktur
SEO-Optimierung von Haus aus
Leichte Wartung & Deployment mit GitLab & Cloudflare
Code Kontrolle und Versionierung
Falls du also ein leistungsstarkes CMS suchst, das ohne großen Overhead auskommt, und gleichzeitig von den Vorteilen statischer Seiten profitieren möchtest, kann ich diese Kombination nur empfehlen! 🎯
Hast du Fragen oder nutzt du eine ähnliche Lösung? Schreib es in die Kommentare!