Files
portfolio/docs/HARDCOVER_INTEGRATION.md
denshooter 38a98a9ea2 feat: Add Hardcover currently reading integration with i18n support
- 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
2026-01-15 14:58:34 +01:00

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

  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):

{
  "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):

// 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)

  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:

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