From c03dcfdbbc5a99abdb7517a03482bccf875d1953 Mon Sep 17 00:00:00 2001
From: Yuiki Saito <yuikisaito@ysaito.win>
Date: Mon, 12 May 2025 00:11:07 +0900
Subject: feat: Add NETSCAPE-Bookmark-file-1 export format support (#1374)

* Add function to export bookmarks in NETSCAPE-Bookmark-file-1 format

* Update export endpoint to support NETSCAPE format

* Add format selection to export UI

* include tags in the export

---------

Co-authored-by: Mohamed Bassem <me@mbassem.com>
---
 apps/web/app/api/bookmarks/export/route.tsx | 56 +++++++++++++++++++++--------
 1 file changed, 41 insertions(+), 15 deletions(-)

(limited to 'apps/web/app/api/bookmarks/export/route.tsx')

diff --git a/apps/web/app/api/bookmarks/export/route.tsx b/apps/web/app/api/bookmarks/export/route.tsx
index e550fcb5..f568b9f7 100644
--- a/apps/web/app/api/bookmarks/export/route.tsx
+++ b/apps/web/app/api/bookmarks/export/route.tsx
@@ -1,15 +1,23 @@
-import { toExportFormat, zExportSchema } from "@/lib/exportBookmarks";
+import { NextRequest } from "next/server";
+import {
+  toExportFormat,
+  toNetscapeFormat,
+  zExportSchema,
+} from "@/lib/exportBookmarks";
 import { api, createContextFromRequest } from "@/server/api/client";
 import { z } from "zod";
 
 import { MAX_NUM_BOOKMARKS_PER_PAGE } from "@karakeep/shared/types/bookmarks";
 
 export const dynamic = "force-dynamic";
-export async function GET(request: Request) {
+export async function GET(request: NextRequest) {
   const ctx = await createContextFromRequest(request);
   if (!ctx.user) {
     return Response.json({ error: "Unauthorized" }, { status: 401 });
   }
+
+  const format = request.nextUrl.searchParams.get("format") ?? "json";
+
   const req = {
     limit: MAX_NUM_BOOKMARKS_PER_PAGE,
     useCursorV2: true,
@@ -17,25 +25,43 @@ export async function GET(request: Request) {
   };
 
   let resp = await api.bookmarks.getBookmarks(req);
-  let results = resp.bookmarks.map(toExportFormat);
+  let bookmarks = resp.bookmarks;
 
   while (resp.nextCursor) {
     resp = await api.bookmarks.getBookmarks({
-      ...request,
+      ...req,
       cursor: resp.nextCursor,
     });
-    results = [...results, ...resp.bookmarks.map(toExportFormat)];
+    bookmarks = [...bookmarks, ...resp.bookmarks];
   }
 
-  const exportData: z.infer<typeof zExportSchema> = {
-    bookmarks: results.filter((b) => b.content !== null),
-  };
+  if (format === "json") {
+    // Default JSON format
+    const exportData: z.infer<typeof zExportSchema> = {
+      bookmarks: bookmarks
+        .map(toExportFormat)
+        .filter((b) => b.content !== null),
+    };
 
-  return new Response(JSON.stringify(exportData), {
-    status: 200,
-    headers: {
-      "Content-type": "application/json",
-      "Content-disposition": `attachment; filename="karakeep-export-${new Date().toISOString()}.json"`,
-    },
-  });
+    return new Response(JSON.stringify(exportData), {
+      status: 200,
+      headers: {
+        "Content-type": "application/json",
+        "Content-disposition": `attachment; filename="hoarder-export-${new Date().toISOString()}.json"`,
+      },
+    });
+  } else if (format === "netscape") {
+    // Netscape format
+    const netscapeContent = toNetscapeFormat(bookmarks);
+
+    return new Response(netscapeContent, {
+      status: 200,
+      headers: {
+        "Content-type": "text/html",
+        "Content-disposition": `attachment; filename="bookmarks-${new Date().toISOString()}.html"`,
+      },
+    });
+  } else {
+    return Response.json({ error: "Invalid format" }, { status: 400 });
+  }
 }
-- 
cgit v1.2.3-70-g09d2

