# 📚 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 1. **Hardcover Account** mit API-Zugriff 2. **n8n Installation** (lokal oder Cloud) 3. **GraphQL Endpoint** von Hardcover 4. **API Credentials** (Token/Key) für Hardcover --- ## 🔧 n8n Workflow Setup ### Schritt 1: Webhook Node erstellen 1. Öffne n8n und erstelle einen neuen Workflow 2. Füge einen **Webhook** Node hinzu 3. Konfiguriere den Webhook: - **HTTP Method**: `GET` - **Path**: `/webhook/hardcover/currently-reading` - **Response Mode**: `Last Node` (wenn du einen separaten Respond Node verwendest) oder `Respond to Webhook` (wenn der Webhook automatisch antworten soll) - **Response Code**: `200` **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 1. Füge einen **HTTP Request** Node nach dem Webhook hinzu 2. Konfiguriere den Node: **Settings:** - **Method**: `POST` - **URL**: `https://api.hardcover.app/graphql` (oder deine Hardcover GraphQL URL) - **Authentication**: `Header Auth` oder `Generic Credential Type` - **Name**: `Authorization` - **Value**: `Bearer YOUR_HARDCOVER_TOKEN` **Headers:** ``` Content-Type: application/json ``` **Body (JSON):** ```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 1. Füge einen **Code** Node oder **Set** Node hinzu 2. Transformiere die Hardcover-Antwort in das erwartete Format: **Beispiel Transformation (Code Node - JavaScript):** ```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:** ```json { "currentlyReading": null } ``` --- ## 🔐 Environment Variables Stelle sicher, dass folgende Umgebungsvariablen in deiner `.env` Datei gesetzt sind: ```bash # 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:** ```json { "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:** ```json { "currentlyReading": null } ``` **Fehler:** ```json { "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:** ```typescript "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(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
Loading...
; } if (!book) { return null; // Kein Buch = nichts anzeigen } return (
{book.title}

{book.title}

{book.authors.join(", ")}

{book.progress}% gelesen

); } ``` --- ## 🔍 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)** 1. Öffne den **Webhook** Node 2. Stelle sicher, dass **Response Mode** auf `Respond to Webhook` gesetzt ist 3. Entferne den separaten "Respond to Webhook" Node (falls vorhanden) 4. Der Webhook Node antwortet automatisch mit der letzten Node-Ausgabe **Option 2: Manueller Respond Node** 1. 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 Node`** gesetzt 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: 2` der 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.log` im 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: ```graphql 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 Lesefortschritt - `progress` = Lesefortschritt als Dezimalzahl (0.0 - 1.0) - `edition.title` = Titel des Buches - `edition.image.url` = URL zum Buchcover - `book.contributions[].author.name` = Liste der Autorennamen --- ## 🚀 Deployment 1. **n8n Workflow aktivieren** - Stelle sicher, dass der Workflow aktiviert ist - Teste den Webhook mit einem GET Request 2. **Environment Variables setzen** - Füge `N8N_WEBHOOK_URL` zur `.env` hinzu - Füge `N8N_SECRET_TOKEN` hinzu (optional, für Auth) 3. **Frontend Integration** - Füge die `CurrentlyReadingWidget` Komponente zur Homepage hinzu - Stelle sicher, dass die API nur einmal aufgerufen wird 4. **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 `null` zurück - Die API verwendet Rate Limiting, um Missbrauch zu verhindern --- ## 🔗 Weitere Ressourcen - [Hardcover API Dokumentation](https://hardcover.app) - [n8n Dokumentation](https://docs.n8n.io) - [GraphQL Best Practices](https://graphql.org/learn/best-practices/)