From e9a94f9c71eaa881c9e0ea36bc2fd0401e5d23f6 Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 4 Aug 2026 21:18:49 +0200 Subject: [PATCH 1/3] perf(home): count bookmarks with one page, not a full paginated load The homepage "in the chamber" counter fetched /api/bookmarks, which paginates every bookmark (up to 20 sequential daily.dev calls of 50) just to read items.length. Add a lightweight GET /api/bookmarks/count backed by countBookmarks(), which hits daily.dev once: it returns an exact total when the API exposes one or the pile fits in a single page, otherwise a first-page count the UI renders as "N+". The counter keeps working; the cost drops from up to 20 calls plus the full payload to a single call. Co-Authored-By: Claude Fable 5 --- src/lib/daily.ts | 31 ++++++++++++++++++++++++++++++- src/pages/api/bookmarks/count.ts | 22 ++++++++++++++++++++++ src/pages/index.astro | 8 ++++++-- 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 src/pages/api/bookmarks/count.ts diff --git a/src/lib/daily.ts b/src/lib/daily.ts index d88e2b6..6dbbf24 100644 --- a/src/lib/daily.ts +++ b/src/lib/daily.ts @@ -47,11 +47,40 @@ export async function validateToken(token: string): Promise { } // Response shape per OpenAPI: { data: BookmarkedPost[], pagination: { cursor, hasNextPage } } +// `total` is read defensively: the current API does not document it, but if a +// future version returns it we can report an exact count from a single page. type BookmarksPage = { data: Bookmark[]; - pagination?: { cursor?: string | null; hasNextPage?: boolean }; + pagination?: { cursor?: string | null; hasNextPage?: boolean; total?: number }; + total?: number; }; +/** + * Cheap chamber tally for the homepage. Fetches a single page instead of + * paginating the whole pile (which `listAllBookmarks` would do just to count). + * Returns an exact count when the API exposes a total or the pile fits in one + * page; otherwise `exact` is false and the count is the first-page size, which + * the UI renders as "N+". + */ +export async function countBookmarks( + token: string, + opts: { unreadOnly?: boolean } = {}, +): Promise<{ count: number; exact: boolean }> { + const url = new URL(`${BASE}/bookmarks/`); + url.searchParams.set("limit", "50"); + if (opts.unreadOnly) url.searchParams.set("unreadOnly", "true"); + + const res = await fetch(url, { headers: authHeaders(token) }); + if (!res.ok) throw new Error(`countBookmarks failed: ${res.status}`); + + const body = (await res.json()) as BookmarksPage; + const total = body.pagination?.total ?? body.total; + if (typeof total === "number") return { count: total, exact: true }; + + const items = body.data ?? []; + return { count: items.length, exact: !(body.pagination?.hasNextPage ?? false) }; +} + /** Fetches one page (max 50). `unreadOnly` targets the dead-weight pile we want to cull. */ export async function listBookmarks( token: string, diff --git a/src/pages/api/bookmarks/count.ts b/src/pages/api/bookmarks/count.ts new file mode 100644 index 0000000..5597171 --- /dev/null +++ b/src/pages/api/bookmarks/count.ts @@ -0,0 +1,22 @@ +import type { APIRoute } from "astro"; +import { countBookmarks } from "../../../lib/daily"; +import { getToken } from "../../../lib/session"; + +// GET /api/bookmarks/count?unreadOnly=true โ€” a lightweight tally for the +// homepage "in the chamber" counter. Unlike /api/bookmarks, this hits daily.dev +// once (a single page) instead of paginating the whole pile just to count. +export const GET: APIRoute = async ({ url, cookies }) => { + const token = getToken(cookies); + if (!token) return new Response(JSON.stringify({ error: "Not signed in" }), { status: 401 }); + + const unreadOnly = url.searchParams.get("unreadOnly") === "true"; + try { + const { count, exact } = await countBookmarks(token, { unreadOnly }); + return new Response(JSON.stringify({ count, exact }), { + headers: { "Content-Type": "application/json" }, + }); + } catch (err) { + console.error("[bookmarks:count] ", err); + return new Response(JSON.stringify({ error: "Failed to count bookmarks" }), { status: 502 }); + } +}; diff --git a/src/pages/index.astro b/src/pages/index.astro index bf974c1..cc5e7f7 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -82,9 +82,13 @@ const profile = await currentUser(Astro.cookies); const countEl = document.getElementById("bm-count"); if (countEl) { - fetch("/api/bookmarks") + // Lightweight count endpoint: one daily.dev call, not a full paginated load. + fetch("/api/bookmarks/count") .then((r) => r.json()) - .then((d) => { countEl.textContent = String(d.items?.length ?? 0); }) + .then((d) => { + const n = d.count ?? 0; + countEl.textContent = d.exact ? String(n) : `${n}+`; + }) .catch(() => { countEl.textContent = "?"; }); } From bceaea5f2c0eed5b1476e1021859cd736147b3da Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 4 Aug 2026 21:18:49 +0200 Subject: [PATCH 2/3] chore: remove dead spike/shots.mjs Throwaway visual-diff helper for the already-merged Tailwind migration branch. Nothing references it (README points at spike/spike.mjs; package.json has no hook), so it is safe to delete. Co-Authored-By: Claude Fable 5 --- spike/shots.mjs | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 spike/shots.mjs diff --git a/spike/shots.mjs b/spike/shots.mjs deleted file mode 100644 index 7c07005..0000000 --- a/spike/shots.mjs +++ /dev/null @@ -1,25 +0,0 @@ -// Throwaway visual-diff helper for the Tailwind migration branch. -// Usage: node spike/shots.mjs