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
This commit is contained in:
131
app/api/n8n/hardcover/currently-reading/route.ts
Normal file
131
app/api/n8n/hardcover/currently-reading/route.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// app/api/n8n/hardcover/currently-reading/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
// Cache für 5 Minuten, damit wir n8n nicht zuspammen
|
||||
// Hardcover-Daten ändern sich nicht so häufig
|
||||
export const revalidate = 300;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
// Rate limiting for n8n hardcover endpoint
|
||||
const ip =
|
||||
request.headers.get("x-forwarded-for") ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"unknown";
|
||||
const ua = request.headers.get("user-agent") || "unknown";
|
||||
const { checkRateLimit } = await import('@/lib/auth');
|
||||
|
||||
// In dev, many requests can share ip=unknown; use UA to avoid a shared bucket.
|
||||
const rateKey =
|
||||
process.env.NODE_ENV === "development" && ip === "unknown"
|
||||
? `ua:${ua.slice(0, 120)}`
|
||||
: ip;
|
||||
const maxPerMinute = process.env.NODE_ENV === "development" ? 60 : 10;
|
||||
|
||||
if (!checkRateLimit(rateKey, maxPerMinute, 60000)) { // requests per minute
|
||||
return NextResponse.json(
|
||||
{ error: 'Rate limit exceeded. Please try again later.' },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if n8n webhook URL is configured
|
||||
const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL;
|
||||
|
||||
if (!n8nWebhookUrl) {
|
||||
console.warn("N8N_WEBHOOK_URL not configured for hardcover endpoint");
|
||||
// Return fallback if n8n is not configured
|
||||
return NextResponse.json({
|
||||
currentlyReading: null,
|
||||
});
|
||||
}
|
||||
|
||||
// Rufe den n8n Webhook auf
|
||||
// Add timestamp to query to bypass Cloudflare cache
|
||||
const webhookUrl = `${n8nWebhookUrl}/webhook/hardcover/currently-reading?t=${Date.now()}`;
|
||||
console.log(`Fetching currently reading from: ${webhookUrl}`);
|
||||
|
||||
// Add timeout to prevent hanging requests
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||||
|
||||
try {
|
||||
const res = await fetch(webhookUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...(process.env.N8N_SECRET_TOKEN && {
|
||||
Authorization: `Bearer ${process.env.N8N_SECRET_TOKEN}`,
|
||||
}),
|
||||
...(process.env.N8N_API_KEY && {
|
||||
"X-API-Key": process.env.N8N_API_KEY,
|
||||
}),
|
||||
},
|
||||
next: { revalidate: 300 },
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => 'Unknown error');
|
||||
console.error(`n8n hardcover webhook failed: ${res.status}`, errorText);
|
||||
throw new Error(`n8n error: ${res.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const raw = await res.text().catch(() => "");
|
||||
if (!raw || !raw.trim()) {
|
||||
throw new Error("Empty response body received from n8n");
|
||||
}
|
||||
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch (parseError) {
|
||||
// Sometimes upstream sends HTML or a partial response; include a snippet for debugging.
|
||||
const snippet = raw.slice(0, 240);
|
||||
throw new Error(
|
||||
`Invalid JSON from n8n (${res.status}): ${snippet}${raw.length > 240 ? "…" : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
// n8n gibt oft ein Array zurück: [{...}]. Wir wollen nur das Objekt.
|
||||
const readingData = Array.isArray(data) ? data[0] : data;
|
||||
|
||||
// Safety check: if readingData is still undefined/null (e.g. empty array), use fallback
|
||||
if (!readingData) {
|
||||
throw new Error("Empty data received from n8n");
|
||||
}
|
||||
|
||||
// Ensure currentlyReading has proper structure
|
||||
if (readingData.currentlyReading && typeof readingData.currentlyReading === "object") {
|
||||
// Already properly formatted from n8n
|
||||
} else if (readingData.currentlyReading === null || readingData.currentlyReading === undefined) {
|
||||
// No reading data - keep as null
|
||||
readingData.currentlyReading = null;
|
||||
}
|
||||
|
||||
return NextResponse.json(readingData);
|
||||
} catch (fetchError: unknown) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
||||
console.error("n8n hardcover webhook request timed out");
|
||||
} else {
|
||||
console.error("n8n hardcover webhook fetch error:", fetchError);
|
||||
}
|
||||
throw fetchError;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Error fetching n8n hardcover data:", error);
|
||||
console.error("Error details:", {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
n8nUrl: process.env.N8N_WEBHOOK_URL ? 'configured' : 'missing',
|
||||
});
|
||||
// Leeres Fallback-Objekt, damit die Seite nicht abstürzt
|
||||
return NextResponse.json({
|
||||
currentlyReading: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user