v2.0: Major feature update

New Features:
- API key authentication for external access
- Apple Shortcuts integration endpoint (/api/v1/add)
- Full-text search across all articles
- Folders for organizing articles
- Highlights and notes on articles
- Reading stats with streaks
- Reading goals (daily/weekly/monthly)
- Import from Pocket/Instapaper
- RSS feed output
- PWA support for mobile
- Auto theme scheduling (day/night)
- Settings page with all configuration

API Endpoints:
- /api/v1/add - Add articles via API key
- /api/keys - Manage API keys
- /api/search - Full-text search
- /api/folders - Folder management
- /api/highlights - Highlights/notes
- /api/stats - Reading statistics
- /api/goals - Reading goals
- /api/import - Pocket/Instapaper import
- /api/rss - RSS feed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gemini Agent
2026-01-17 12:19:57 +00:00
parent 27963af055
commit 513576b90e
22 changed files with 2431 additions and 30 deletions

View File

@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db";
import { eq, asc } from "drizzle-orm";
import { v4 as uuidv4 } from "uuid";
// GET /api/folders - List all folders
export async function GET() {
try {
const folders = await db
.select()
.from(schema.folders)
.orderBy(asc(schema.folders.sortOrder));
return NextResponse.json(folders);
} catch (error) {
console.error("Error listing folders:", error);
return NextResponse.json({ error: "Failed to list folders" }, { status: 500 });
}
}
// POST /api/folders - Create folder
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { name, color, icon, parentId } = body;
if (!name) {
return NextResponse.json({ error: "Name is required" }, { status: 400 });
}
const id = uuidv4();
await db.insert(schema.folders).values({
id,
name,
color: color || "#3b82f6",
icon: icon || "folder",
parentId: parentId || null,
});
const folder = await db.select().from(schema.folders).where(eq(schema.folders.id, id)).limit(1);
return NextResponse.json(folder[0], { status: 201 });
} catch (error) {
console.error("Error creating folder:", error);
return NextResponse.json({ error: "Failed to create folder" }, { status: 500 });
}
}
// PATCH /api/folders - Update folder
export async function PATCH(request: NextRequest) {
try {
const body = await request.json();
const { id, name, color, icon, sortOrder } = body;
if (!id) {
return NextResponse.json({ error: "ID is required" }, { status: 400 });
}
const updates: Partial<schema.Folder> = {};
if (name) updates.name = name;
if (color) updates.color = color;
if (icon) updates.icon = icon;
if (sortOrder !== undefined) updates.sortOrder = sortOrder;
await db.update(schema.folders).set(updates).where(eq(schema.folders.id, id));
const folder = await db.select().from(schema.folders).where(eq(schema.folders.id, id)).limit(1);
return NextResponse.json(folder[0]);
} catch (error) {
console.error("Error updating folder:", error);
return NextResponse.json({ error: "Failed to update folder" }, { status: 500 });
}
}
// DELETE /api/folders?id=...
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (!id) {
return NextResponse.json({ error: "ID is required" }, { status: 400 });
}
// Remove folder reference from articles
await db.update(schema.articles).set({ folderId: null }).where(eq(schema.articles.folderId, id));
// Delete folder
await db.delete(schema.folders).where(eq(schema.folders.id, id));
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error deleting folder:", error);
return NextResponse.json({ error: "Failed to delete folder" }, { status: 500 });
}
}

103
src/app/api/goals/route.ts Normal file
View File

@@ -0,0 +1,103 @@
import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db";
import { eq } from "drizzle-orm";
import { v4 as uuidv4 } from "uuid";
// GET /api/goals - List all goals
export async function GET() {
try {
const goals = await db.select().from(schema.readingGoals);
return NextResponse.json(goals);
} catch (error) {
console.error("Error listing goals:", error);
return NextResponse.json({ error: "Failed to list goals" }, { status: 500 });
}
}
// POST /api/goals - Create goal
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { type, metric, target } = body;
if (!type || !metric || !target) {
return NextResponse.json(
{ error: "type, metric, and target are required" },
{ status: 400 }
);
}
// Validate type
if (!["daily", "weekly", "monthly"].includes(type)) {
return NextResponse.json(
{ error: "type must be daily, weekly, or monthly" },
{ status: 400 }
);
}
// Validate metric
if (!["articles", "words", "time"].includes(metric)) {
return NextResponse.json(
{ error: "metric must be articles, words, or time" },
{ status: 400 }
);
}
const id = uuidv4();
await db.insert(schema.readingGoals).values({
id,
type,
metric,
target,
isActive: true,
});
const goal = await db.select().from(schema.readingGoals).where(eq(schema.readingGoals.id, id)).limit(1);
return NextResponse.json(goal[0], { status: 201 });
} catch (error) {
console.error("Error creating goal:", error);
return NextResponse.json({ error: "Failed to create goal" }, { status: 500 });
}
}
// PATCH /api/goals - Update goal
export async function PATCH(request: NextRequest) {
try {
const body = await request.json();
const { id, target, isActive } = body;
if (!id) {
return NextResponse.json({ error: "ID is required" }, { status: 400 });
}
const updates: Partial<schema.ReadingGoal> = {};
if (target !== undefined) updates.target = target;
if (isActive !== undefined) updates.isActive = isActive;
await db.update(schema.readingGoals).set(updates).where(eq(schema.readingGoals.id, id));
const goal = await db.select().from(schema.readingGoals).where(eq(schema.readingGoals.id, id)).limit(1);
return NextResponse.json(goal[0]);
} catch (error) {
console.error("Error updating goal:", error);
return NextResponse.json({ error: "Failed to update goal" }, { status: 500 });
}
}
// DELETE /api/goals?id=...
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (!id) {
return NextResponse.json({ error: "ID is required" }, { status: 400 });
}
await db.delete(schema.readingGoals).where(eq(schema.readingGoals.id, id));
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error deleting goal:", error);
return NextResponse.json({ error: "Failed to delete goal" }, { status: 500 });
}
}

View File

@@ -0,0 +1,95 @@
import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db";
import { eq, desc } from "drizzle-orm";
import { v4 as uuidv4 } from "uuid";
// GET /api/highlights?articleId=... - Get highlights for an article
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const articleId = searchParams.get("articleId");
let query = db.select().from(schema.highlights).orderBy(desc(schema.highlights.createdAt));
if (articleId) {
query = query.where(eq(schema.highlights.articleId, articleId)) as typeof query;
}
const highlights = await query.limit(100);
return NextResponse.json(highlights);
} catch (error) {
console.error("Error listing highlights:", error);
return NextResponse.json({ error: "Failed to list highlights" }, { status: 500 });
}
}
// POST /api/highlights - Create highlight
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { articleId, text, note, color, startOffset, endOffset } = body;
if (!articleId || !text) {
return NextResponse.json({ error: "articleId and text are required" }, { status: 400 });
}
const id = uuidv4();
await db.insert(schema.highlights).values({
id,
articleId,
text,
note: note || null,
color: color || "#fbbf24",
startOffset: startOffset || null,
endOffset: endOffset || null,
});
const highlight = await db.select().from(schema.highlights).where(eq(schema.highlights.id, id)).limit(1);
return NextResponse.json(highlight[0], { status: 201 });
} catch (error) {
console.error("Error creating highlight:", error);
return NextResponse.json({ error: "Failed to create highlight" }, { status: 500 });
}
}
// PATCH /api/highlights - Update highlight (note)
export async function PATCH(request: NextRequest) {
try {
const body = await request.json();
const { id, note, color } = body;
if (!id) {
return NextResponse.json({ error: "ID is required" }, { status: 400 });
}
const updates: Partial<schema.Highlight> = {};
if (note !== undefined) updates.note = note;
if (color) updates.color = color;
await db.update(schema.highlights).set(updates).where(eq(schema.highlights.id, id));
const highlight = await db.select().from(schema.highlights).where(eq(schema.highlights.id, id)).limit(1);
return NextResponse.json(highlight[0]);
} catch (error) {
console.error("Error updating highlight:", error);
return NextResponse.json({ error: "Failed to update highlight" }, { status: 500 });
}
}
// DELETE /api/highlights?id=...
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (!id) {
return NextResponse.json({ error: "ID is required" }, { status: 400 });
}
await db.delete(schema.highlights).where(eq(schema.highlights.id, id));
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error deleting highlight:", error);
return NextResponse.json({ error: "Failed to delete highlight" }, { status: 500 });
}
}

137
src/app/api/import/route.ts Normal file
View File

@@ -0,0 +1,137 @@
import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db";
import { extractArticle } from "@/lib/utils/extract";
import { eq } from "drizzle-orm";
import { v4 as uuidv4 } from "uuid";
interface ImportItem {
url: string;
title?: string;
tags?: string[];
favorite?: boolean;
archived?: boolean;
addedAt?: string;
}
// POST /api/import - Import from Pocket/Instapaper export
export async function POST(request: NextRequest) {
try {
const contentType = request.headers.get("content-type") || "";
let items: ImportItem[] = [];
if (contentType.includes("application/json")) {
// JSON import (Pocket API format or custom)
const body = await request.json();
items = body.items || body.list || [];
// Handle Pocket export format
if (body.list && typeof body.list === "object" && !Array.isArray(body.list)) {
items = Object.values(body.list).map((item: any) => ({
url: item.given_url || item.resolved_url,
title: item.given_title || item.resolved_title,
tags: item.tags ? Object.keys(item.tags) : [],
favorite: item.favorite === "1",
archived: item.status === "1",
addedAt: item.time_added ? new Date(parseInt(item.time_added) * 1000).toISOString() : undefined,
}));
}
} else if (contentType.includes("text/html")) {
// HTML import (Pocket/Instapaper HTML export)
const html = await request.text();
const urlRegex = /<a[^>]+href="([^"]+)"[^>]*>([^<]*)<\/a>/gi;
let match;
while ((match = urlRegex.exec(html)) !== null) {
const url = match[1];
const title = match[2];
if (url.startsWith("http")) {
items.push({ url, title });
}
}
} else if (contentType.includes("text/csv")) {
// CSV import
const csv = await request.text();
const lines = csv.split("\n").slice(1); // Skip header
for (const line of lines) {
const parts = line.split(",").map((p) => p.trim().replace(/^"|"$/g, ""));
if (parts[0]?.startsWith("http")) {
items.push({
url: parts[0],
title: parts[1] || undefined,
tags: parts[2] ? parts[2].split(";") : undefined,
});
}
}
} else {
return NextResponse.json(
{ error: "Unsupported content type. Use application/json, text/html, or text/csv" },
{ status: 400 }
);
}
if (items.length === 0) {
return NextResponse.json({ error: "No items found to import" }, { status: 400 });
}
const results = {
total: items.length,
imported: 0,
skipped: 0,
failed: 0,
errors: [] as string[],
};
// Process items in batches
for (const item of items) {
try {
// Check if already exists
const existing = await db
.select()
.from(schema.articles)
.where(eq(schema.articles.url, item.url))
.limit(1);
if (existing.length > 0) {
results.skipped++;
continue;
}
// Extract article
const extracted = await extractArticle(item.url);
const id = uuidv4();
await db.insert(schema.articles).values({
id,
url: item.url,
title: item.title || extracted.title,
author: extracted.author,
siteName: extracted.siteName,
excerpt: extracted.excerpt,
content: extracted.content,
textContent: extracted.textContent,
leadImage: extracted.leadImage,
wordCount: extracted.wordCount,
tags: item.tags ? JSON.stringify(item.tags) : "[]",
isFavorite: item.favorite || false,
isArchived: item.archived || false,
createdAt: item.addedAt ? new Date(item.addedAt) : new Date(),
});
results.imported++;
} catch (error) {
results.failed++;
results.errors.push(`${item.url}: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
return NextResponse.json(results);
} catch (error) {
console.error("Import error:", error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Import failed" },
{ status: 500 }
);
}
}

49
src/app/api/keys/route.ts Normal file
View File

@@ -0,0 +1,49 @@
import { NextRequest, NextResponse } from "next/server";
import { createApiKey, listApiKeys, deleteApiKey } from "@/lib/auth";
// GET /api/keys - List all API keys
export async function GET() {
try {
const keys = await listApiKeys();
return NextResponse.json(keys);
} catch (error) {
console.error("Error listing API keys:", error);
return NextResponse.json({ error: "Failed to list API keys" }, { status: 500 });
}
}
// POST /api/keys - Create new API key
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { name } = body;
if (!name) {
return NextResponse.json({ error: "Name is required" }, { status: 400 });
}
const key = await createApiKey(name);
return NextResponse.json(key, { status: 201 });
} catch (error) {
console.error("Error creating API key:", error);
return NextResponse.json({ error: "Failed to create API key" }, { status: 500 });
}
}
// DELETE /api/keys - Delete API key
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (!id) {
return NextResponse.json({ error: "ID is required" }, { status: 400 });
}
await deleteApiKey(id);
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error deleting API key:", error);
return NextResponse.json({ error: "Failed to delete API key" }, { status: 500 });
}
}

63
src/app/api/rss/route.ts Normal file
View File

@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db";
import { desc, eq } from "drizzle-orm";
// GET /api/rss - RSS feed of reading list
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const filter = searchParams.get("filter") || "all"; // all, favorites, archived
let query = db.select().from(schema.articles);
if (filter === "favorites") {
query = query.where(eq(schema.articles.isFavorite, true)) as typeof query;
} else if (filter === "archived") {
query = query.where(eq(schema.articles.isArchived, true)) as typeof query;
} else {
query = query.where(eq(schema.articles.isArchived, false)) as typeof query;
}
const articles = await query.orderBy(desc(schema.articles.createdAt)).limit(50);
// Get base URL from request
const baseUrl = new URL(request.url).origin;
// Generate RSS XML
const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>ReadLater - ${filter === "favorites" ? "Favorites" : filter === "archived" ? "Archived" : "Reading List"}</title>
<link>${baseUrl}</link>
<description>Your saved articles from ReadLater</description>
<language>en-us</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<atom:link href="${baseUrl}/api/rss?filter=${filter}" rel="self" type="application/rss+xml"/>
${articles
.map(
(article) => `
<item>
<title><![CDATA[${article.title}]]></title>
<link>${article.url}</link>
<guid isPermaLink="false">${article.id}</guid>
<pubDate>${new Date(article.createdAt || Date.now()).toUTCString()}</pubDate>
<description><![CDATA[${article.excerpt || ""}]]></description>
${article.author ? `<author>${article.author}</author>` : ""}
${article.siteName ? `<source url="${article.url}">${article.siteName}</source>` : ""}
</item>`
)
.join("")}
</channel>
</rss>`;
return new NextResponse(rss, {
headers: {
"Content-Type": "application/rss+xml; charset=utf-8",
"Cache-Control": "public, max-age=300", // Cache for 5 minutes
},
});
} catch (error) {
console.error("Error generating RSS:", error);
return NextResponse.json({ error: "Failed to generate RSS" }, { status: 500 });
}
}

View File

@@ -0,0 +1,37 @@
import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db";
import { like, or, desc } from "drizzle-orm";
// GET /api/search?q=query
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("q");
if (!query || query.length < 2) {
return NextResponse.json({ error: "Query must be at least 2 characters" }, { status: 400 });
}
const searchTerm = `%${query}%`;
const results = await db
.select()
.from(schema.articles)
.where(
or(
like(schema.articles.title, searchTerm),
like(schema.articles.textContent, searchTerm),
like(schema.articles.author, searchTerm),
like(schema.articles.siteName, searchTerm),
like(schema.articles.tags, searchTerm)
)
)
.orderBy(desc(schema.articles.createdAt))
.limit(50);
return NextResponse.json(results);
} catch (error) {
console.error("Search error:", error);
return NextResponse.json({ error: "Search failed" }, { status: 500 });
}
}

129
src/app/api/stats/route.ts Normal file
View File

@@ -0,0 +1,129 @@
import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db";
import { eq, desc, gte, sql } from "drizzle-orm";
import { v4 as uuidv4 } from "uuid";
// GET /api/stats - Get reading stats
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const days = parseInt(searchParams.get("days") || "30");
// Get date range
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const startDateStr = startDate.toISOString().split("T")[0];
// Get daily stats
const dailyStats = await db
.select()
.from(schema.readingStats)
.where(gte(schema.readingStats.date, startDateStr))
.orderBy(desc(schema.readingStats.date));
// Calculate totals
const totals = dailyStats.reduce(
(acc, day) => ({
articlesRead: acc.articlesRead + (day.articlesRead || 0),
articlesAdded: acc.articlesAdded + (day.articlesAdded || 0),
wordsRead: acc.wordsRead + (day.wordsRead || 0),
timeSpentSeconds: acc.timeSpentSeconds + (day.timeSpentSeconds || 0),
}),
{ articlesRead: 0, articlesAdded: 0, wordsRead: 0, timeSpentSeconds: 0 }
);
// Calculate current streak
let streak = 0;
const today = new Date().toISOString().split("T")[0];
const sortedDays = [...dailyStats].sort((a, b) => b.date.localeCompare(a.date));
for (const day of sortedDays) {
if (day.articlesRead && day.articlesRead > 0) {
streak++;
} else if (day.date !== today) {
break;
}
}
// Get article counts
const articleCounts = await db
.select({
total: sql<number>`count(*)`,
favorites: sql<number>`sum(case when is_favorite = 1 then 1 else 0 end)`,
archived: sql<number>`sum(case when is_archived = 1 then 1 else 0 end)`,
unread: sql<number>`sum(case when reading_progress < 100 and is_archived = 0 then 1 else 0 end)`,
})
.from(schema.articles);
return NextResponse.json({
period: { days, startDate: startDateStr },
totals,
streak,
daily: dailyStats,
library: articleCounts[0],
});
} catch (error) {
console.error("Error getting stats:", error);
return NextResponse.json({ error: "Failed to get stats" }, { status: 500 });
}
}
// POST /api/stats - Record reading activity
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { articleId, wordsRead, timeSpentSeconds, completed } = body;
const today = new Date().toISOString().split("T")[0];
// Get or create today's stats
const existing = await db
.select()
.from(schema.readingStats)
.where(eq(schema.readingStats.date, today))
.limit(1);
if (existing.length > 0) {
await db
.update(schema.readingStats)
.set({
articlesRead: (existing[0].articlesRead || 0) + (completed ? 1 : 0),
wordsRead: (existing[0].wordsRead || 0) + (wordsRead || 0),
timeSpentSeconds: (existing[0].timeSpentSeconds || 0) + (timeSpentSeconds || 0),
})
.where(eq(schema.readingStats.date, today));
} else {
await db.insert(schema.readingStats).values({
id: uuidv4(),
date: today,
articlesRead: completed ? 1 : 0,
wordsRead: wordsRead || 0,
timeSpentSeconds: timeSpentSeconds || 0,
});
}
// Update article reading time if articleId provided
if (articleId && timeSpentSeconds) {
const article = await db
.select()
.from(schema.articles)
.where(eq(schema.articles.id, articleId))
.limit(1);
if (article.length > 0) {
await db
.update(schema.articles)
.set({
readingTimeSeconds: (article[0].readingTimeSeconds || 0) + timeSpentSeconds,
finishedAt: completed ? new Date() : article[0].finishedAt,
})
.where(eq(schema.articles.id, articleId));
}
}
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error recording stats:", error);
return NextResponse.json({ error: "Failed to record stats" }, { status: 500 });
}
}

145
src/app/api/v1/add/route.ts Normal file
View File

@@ -0,0 +1,145 @@
import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db";
import { extractArticle } from "@/lib/utils/extract";
import { validateApiKey, extractApiKey } from "@/lib/auth";
import { v4 as uuidv4 } from "uuid";
import { eq } from "drizzle-orm";
// POST /api/v1/add - Add article (for Apple Shortcuts)
// Accepts: { url: string, tags?: string[], folderId?: string }
// Auth: Bearer token or X-API-Key header
export async function POST(request: NextRequest) {
try {
// Validate API key
const apiKey = extractApiKey(request.headers);
if (!apiKey) {
return NextResponse.json(
{ success: false, error: "Missing API key. Use Authorization: Bearer <key> or X-API-Key header." },
{ status: 401 }
);
}
const keyRecord = await validateApiKey(apiKey);
if (!keyRecord) {
return NextResponse.json(
{ success: false, error: "Invalid API key" },
{ status: 401 }
);
}
// Parse request body
const body = await request.json();
const { url, tags, folderId } = body;
if (!url) {
return NextResponse.json(
{ success: false, error: "URL is required" },
{ status: 400 }
);
}
// Check if article already exists
const existing = await db
.select()
.from(schema.articles)
.where(eq(schema.articles.url, url))
.limit(1);
if (existing.length > 0) {
return NextResponse.json({
success: true,
message: "Article already saved",
article: {
id: existing[0].id,
title: existing[0].title,
url: existing[0].url,
},
});
}
// Extract article content
const extracted = await extractArticle(url);
const id = uuidv4();
const newArticle: schema.NewArticle = {
id,
url,
title: extracted.title,
author: extracted.author,
siteName: extracted.siteName,
excerpt: extracted.excerpt,
content: extracted.content,
textContent: extracted.textContent,
leadImage: extracted.leadImage,
wordCount: extracted.wordCount,
tags: tags ? JSON.stringify(tags) : "[]",
folderId: folderId || null,
};
await db.insert(schema.articles).values(newArticle);
// Update daily stats
const today = new Date().toISOString().split("T")[0];
const existingStats = await db
.select()
.from(schema.readingStats)
.where(eq(schema.readingStats.date, today))
.limit(1);
if (existingStats.length > 0) {
await db
.update(schema.readingStats)
.set({ articlesAdded: (existingStats[0].articlesAdded || 0) + 1 })
.where(eq(schema.readingStats.date, today));
} else {
await db.insert(schema.readingStats).values({
id: uuidv4(),
date: today,
articlesAdded: 1,
});
}
return NextResponse.json({
success: true,
message: "Article saved",
article: {
id,
title: extracted.title,
url,
wordCount: extracted.wordCount,
readingTime: Math.ceil(extracted.wordCount / 200),
},
});
} catch (error) {
console.error("Error saving article:", error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "Failed to save article",
},
{ status: 500 }
);
}
}
// GET /api/v1/add?url=... - Alternative for simpler shortcuts
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const url = searchParams.get("url");
if (!url) {
return NextResponse.json(
{ success: false, error: "URL parameter required" },
{ status: 400 }
);
}
// Create a mock request with the URL in the body
const mockRequest = new NextRequest(request.url, {
method: "POST",
headers: request.headers,
body: JSON.stringify({ url }),
});
return POST(mockRequest);
}