- Add CurrentlyReading component with beautiful design - Integrate into About section - Add German and English translations - Add n8n API route for Hardcover integration - Add comprehensive documentation for n8n setup
12 KiB
📚 Hardcover Integration Guide
Übersicht
Diese Anleitung zeigt dir, wie du die Hardcover API in n8n integrierst, um deine aktuell gelesenen Bücher auf deiner Portfolio-Website anzuzeigen.
🎯 Was wird angezeigt?
Die Integration zeigt:
- Titel des aktuell gelesenen Buches
- Bild des Buchcovers
- Autor(en) des Buches
- Lesefortschritt (Prozent)
📋 Voraussetzungen
- Hardcover Account mit API-Zugriff
- n8n Installation (lokal oder Cloud)
- GraphQL Endpoint von Hardcover
- API Credentials (Token/Key) für Hardcover
🔧 n8n Workflow Setup
Schritt 1: Webhook Node erstellen
- Öffne n8n und erstelle einen neuen Workflow
- Füge einen Webhook Node hinzu
- Konfiguriere den Webhook:
- HTTP Method:
GET - Path:
/webhook/hardcover/currently-reading - Response Mode:
Last Node(wenn du einen separaten Respond Node verwendest) oderRespond to Webhook(wenn der Webhook automatisch antworten soll) - Response Code:
200
- HTTP Method:
Wichtig: Wenn du Response Mode: Last Node verwendest, musst du einen separaten "Respond to Webhook" Node am Ende hinzufügen. Wenn du Response Mode: Respond to Webhook verwendest, entferne den separaten "Respond to Webhook" Node.
Schritt 2: HTTP Request Node für Hardcover API
- Füge einen HTTP Request Node nach dem Webhook hinzu
- Konfiguriere den Node:
Settings:
- Method:
POST - URL:
https://api.hardcover.app/graphql(oder deine Hardcover GraphQL URL) - Authentication:
Header AuthoderGeneric Credential Type- Name:
Authorization - Value:
Bearer YOUR_HARDCOVER_TOKEN
- Name:
Headers:
Content-Type: application/json
Body (JSON):
{
"query": "query GetCurrentlyReading { me { user_books(where: {status_id: {_eq: 2}}) { user_book_reads(limit: 1, order_by: {started_at: desc}) { progress } edition { title image { url } book { contributions { author { name } } } } } } }"
}
Schritt 3: Daten transformieren
- Füge einen Code Node oder Set Node hinzu
- Transformiere die Hardcover-Antwort in das erwartete Format:
Beispiel Transformation (Code Node - JavaScript):
// Hardcover API Response kommt als GraphQL Response
// Die Response ist ein Array: [{ data: { me: [{ user_books: [...] }] } }]
const graphqlResponse = $input.all()[0].json;
// Extrahiere die Daten - Response-Struktur: [{ data: { me: [{ user_books: [...] }] } }]
const responseData = Array.isArray(graphqlResponse) ? graphqlResponse[0] : graphqlResponse;
const meData = responseData?.data?.me;
const userBooks = (Array.isArray(meData) && meData[0]?.user_books) || meData?.user_books || [];
if (!userBooks || userBooks.length === 0) {
return {
json: {
currentlyReading: null
}
};
}
// Sortiere nach Fortschritt, falls mehrere Bücher vorhanden sind
const sortedBooks = userBooks.sort((a, b) => {
const progressA = a.user_book_reads?.[0]?.progress || 0;
const progressB = b.user_book_reads?.[0]?.progress || 0;
return progressB - progressA; // Höchster zuerst
});
// Formatiere alle Bücher
const formattedBooks = sortedBooks.map(book => {
const edition = book.edition || {};
const bookData = edition.book || {};
const contributions = bookData.contributions || [];
const authors = contributions
.filter(c => c.author && c.author.name)
.map(c => c.author.name);
const readData = book.user_book_reads?.[0] || {};
const progress = readData.progress || 0;
const image = edition.image?.url || null;
return {
title: edition.title || 'Unknown Title',
authors: authors.length > 0 ? authors : ['Unknown Author'],
image: image,
progress: Math.round(progress) || 0, // Progress ist bereits in Prozent (z.B. 65.75)
startedAt: readData.started_at || null,
};
});
// Gib alle Bücher zurück
return {
json: {
currentlyReading: formattedBooks.length > 0 ? formattedBooks : null
}
};
### Schritt 4: Response Node
**Option A: Automatische Response (Empfohlen)**
1. Setze den Webhook Node auf **Response Mode**: `Respond to Webhook`
2. **Entferne** den separaten "Respond to Webhook" Node
3. Der Webhook antwortet automatisch mit der Ausgabe des Code Nodes
**Option B: Manueller Respond Node**
1. Setze den Webhook Node auf **Response Mode**: `Last Node`
2. Füge einen **Respond to Webhook** Node nach dem Code Node hinzu
3. Verbinde den Code Node mit dem Respond to Webhook Node
4. Stelle sicher, dass die Antwort als JSON zurückgegeben wird
**Response Format (mit allen Büchern):**
```json
{
"currentlyReading": [
{
"title": "Ready Player Two",
"authors": ["Ernest Cline"],
"image": "https://assets.hardcover.app/...",
"progress": 66,
"startedAt": null
},
{
"title": "Die Mitternachtsbibliothek",
"authors": ["Matt Haig"],
"image": "https://assets.hardcover.app/...",
"progress": 57,
"startedAt": null
}
]
}
Oder wenn kein Buch gelesen wird:
{
"currentlyReading": null
}
🔐 Environment Variables
Stelle sicher, dass folgende Umgebungsvariablen in deiner .env Datei gesetzt sind:
# n8n Configuration
N8N_WEBHOOK_URL=https://n8n.dk0.dev
N8N_SECRET_TOKEN=your-n8n-secret-token
N8N_API_KEY=your-n8n-api-key
# Hardcover API (optional, falls du es direkt verwenden willst)
HARDCOVER_API_URL=https://api.hardcover.app/graphql
HARDCOVER_API_TOKEN=your-hardcover-token
📡 API Endpoint
Die Portfolio-Website stellt folgenden Endpoint bereit:
GET /api/n8n/hardcover/currently-reading
Response Format
Erfolgreich:
{
"currentlyReading": {
"title": "Der Herr der Ringe",
"authors": ["J.R.R. Tolkien"],
"image": "https://example.com/book-cover.jpg",
"progress": 45,
"startedAt": "2024-01-15T10:00:00Z"
}
}
Kein Buch:
{
"currentlyReading": null
}
Fehler:
{
"error": "Rate limit exceeded. Please try again later."
}
Rate Limiting
- Development: 60 Requests pro Minute
- Production: 10 Requests pro Minute
- Cache: 5 Minuten (300 Sekunden)
🎨 Frontend Integration
Die API sollte nur einmal beim initialen Laden der Seite aufgerufen werden.
Beispiel React Component:
"use client";
import { useEffect, useState } from "react";
interface CurrentlyReading {
title: string;
authors: string[];
image: string | null;
progress: number;
startedAt: string | null;
}
export default function CurrentlyReadingWidget() {
const [book, setBook] = useState<CurrentlyReading | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Nur einmal beim Laden der Seite
const fetchCurrentlyReading = async () => {
try {
const res = await fetch("/api/n8n/hardcover/currently-reading", {
cache: "default",
});
if (!res.ok) {
throw new Error("Failed to fetch");
}
const data = await res.json();
setBook(data.currentlyReading);
} catch (error) {
console.error("Error fetching currently reading:", error);
setBook(null);
} finally {
setLoading(false);
}
};
fetchCurrentlyReading();
}, []); // Leeres Array = nur einmal beim Mount
if (loading) {
return <div>Loading...</div>;
}
if (!book) {
return null; // Kein Buch = nichts anzeigen
}
return (
<div className="currently-reading-widget">
<img src={book.image || "/placeholder-book.png"} alt={book.title} />
<div>
<h3>{book.title}</h3>
<p>{book.authors.join(", ")}</p>
<div className="progress-bar">
<div style={{ width: `${book.progress}%` }} />
</div>
<p>{book.progress}% gelesen</p>
</div>
</div>
);
}
🔍 Troubleshooting
Problem: "Unused Respond to Webhook node found in the workflow"
Fehler:
n8n hardcover webhook failed: 500 {"code":0,"message":"Unused Respond to Webhook node found in the workflow"}
Lösung: Dieser Fehler tritt auf, wenn du einen separaten "Respond to Webhook" Node hast, der nicht korrekt mit dem Workflow verbunden ist.
Option 1: Automatische Response verwenden (Empfohlen)
- Öffne den Webhook Node
- Stelle sicher, dass Response Mode auf
Respond to Webhookgesetzt ist - Entferne den separaten "Respond to Webhook" Node (falls vorhanden)
- Der Webhook Node antwortet automatisch mit der letzten Node-Ausgabe
Option 2: Manueller Respond Node
- Falls du einen separaten "Respond to Webhook" Node verwenden möchtest:
- Stelle sicher, dass dieser Node direkt nach dem Code/Set Node verbunden ist
- Der Webhook Node sollte auf Response Mode:
Last Nodegesetzt sein - Der "Respond to Webhook" Node muss die Daten vom Code Node erhalten
Workflow-Struktur (Option 1 - Empfohlen):
Webhook (Response Mode: Respond to Webhook)
↓
HTTP Request (Hardcover API)
↓
Code Node (Transformation)
↓
(Webhook antwortet automatisch mit Code Node Output)
Workflow-Struktur (Option 2):
Webhook (Response Mode: Last Node)
↓
HTTP Request (Hardcover API)
↓
Code Node (Transformation)
↓
Respond to Webhook Node
Problem: n8n Webhook gibt leere Antwort zurück
Lösung:
- Prüfe, ob der Hardcover API Token korrekt ist
- Stelle sicher, dass der GraphQL Query korrekt formatiert ist
- Prüfe die n8n Logs für Fehlerdetails
- Stelle sicher, dass der Code Node die Daten korrekt zurückgibt (
return { json: {...} })
Problem: API gibt null zurück, obwohl ein Buch gelesen wird
Lösung:
- Prüfe, ob
status_id: 2der korrekte Status für "Currently Reading" ist - Stelle sicher, dass der GraphQL Query die richtigen Felder abfragt
- Prüfe die Hardcover API direkt mit einem GraphQL Client
- Debug: Füge einen
console.logim Code Node hinzu, um die rohe Response zu sehen
Problem: Rate Limit Fehler
Lösung:
- Die API cached Daten für 5 Minuten
- Reduziere die Anzahl der API-Aufrufe im Frontend
- Stelle sicher, dass die API nur einmal beim Laden der Seite aufgerufen wird
Problem: CORS Fehler
Lösung:
- n8n sollte die CORS-Header korrekt setzen
- Prüfe die n8n Webhook-Konfiguration
- Stelle sicher, dass die Portfolio-Website-URL in n8n erlaubt ist
📚 GraphQL Query Details
Der verwendete GraphQL Query:
query GetCurrentlyReading {
me {
user_books(where: {status_id: {_eq: 2}}) {
user_book_reads(limit: 1, order_by: {started_at: desc}) {
progress
}
edition {
title
image {
url
}
book {
contributions {
author {
name
}
}
}
}
}
}
}
Erklärung:
status_id: {_eq: 2}= Filtert nach Büchern mit Status "Currently Reading" (ID 2)user_book_reads(limit: 1, order_by: {started_at: desc})= Holt den neuesten Lesefortschrittprogress= Lesefortschritt als Dezimalzahl (0.0 - 1.0)edition.title= Titel des Buchesedition.image.url= URL zum Buchcoverbook.contributions[].author.name= Liste der Autorennamen
🚀 Deployment
-
n8n Workflow aktivieren
- Stelle sicher, dass der Workflow aktiviert ist
- Teste den Webhook mit einem GET Request
-
Environment Variables setzen
- Füge
N8N_WEBHOOK_URLzur.envhinzu - Füge
N8N_SECRET_TOKENhinzu (optional, für Auth)
- Füge
-
Frontend Integration
- Füge die
CurrentlyReadingWidgetKomponente zur Homepage hinzu - Stelle sicher, dass die API nur einmal aufgerufen wird
- Füge die
-
Testen
- Lade die Homepage neu
- Prüfe die Browser-Konsole für Fehler
- Prüfe die Network-Tab für API-Aufrufe
📝 Notizen
- Die API cached Daten für 5 Minuten, um n8n nicht zu überlasten
- Die API sollte nur einmal beim initialen Laden der Seite aufgerufen werden
- Falls kein Buch gelesen wird, gibt die API
nullzurück - Die API verwendet Rate Limiting, um Missbrauch zu verhindern