From a2d9b64114983ebb23b7687901f2215daaccb873 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 29 Jul 2025 09:08:37 -0500 Subject: [PATCH 01/22] Add anchor comments to project Cursor instructions --- .cursor/rules/alexandria.mdc | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.cursor/rules/alexandria.mdc b/.cursor/rules/alexandria.mdc index f45fffd..5f6e97e 100644 --- a/.cursor/rules/alexandria.mdc +++ b/.cursor/rules/alexandria.mdc @@ -36,14 +36,30 @@ When responding to prompts, adhere to the following rules: - Avoid proposing code edits unless I specifically tell you to do so. - When giving examples from my codebase, include the file name and line numbers so I can find the relevant code easily. -## Code Style +## AI Anchor Comments Format -Observe the following style guidelines when writing code: +- Use anchor comments prefixed with `AI-NOTE:`, `AI-TODO:`, or `AI-QUESTION:` to share context between AI agents and developers across time. + - Use all-caps prefixes. + - Also _read_ (but do not write) variants of this format that begin with `AI-:` where `` is some date in `MM/DD/YYYY` format. Anchor comments with this format are used by developers to record context. +- **Important:** Before scanning files, ALWAYS search first for `AI-` anchor comments in relevant subdirectories. +- ALWAYS update relevant anchor comments when modifying associated code. +- NEVER remove `AI-` comments unless the developer explicitly instructs it. +- Add new anchor comments as relevant when: + - Code is unusually complex. + - Code is critical to security, performance, or functionality. + - Code is confusing. + - Code could have a bug. + +## Coding Guidelines + +### Prime Directive + +NEVER assume developer intent. If you are unsure about something, ALWAYS stop and ask the developer for clarification before proceeding. ### General Guidance - Use snake_case names for plain TypeScript files. -- Use comments sparingly; code should be self-documenting. +- Use comments sparingly; aim to make code readable and self-documenting. ### JavaScript/TypeScript From 825dee1584521dd00b88349c099b618d62f307a6 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 29 Jul 2025 09:12:39 -0500 Subject: [PATCH 02/22] Specify Deno as the preferred runtime in Cursor rules --- .cursor/rules/alexandria.mdc | 1 + 1 file changed, 1 insertion(+) diff --git a/.cursor/rules/alexandria.mdc b/.cursor/rules/alexandria.mdc index 5f6e97e..c9c9730 100644 --- a/.cursor/rules/alexandria.mdc +++ b/.cursor/rules/alexandria.mdc @@ -58,6 +58,7 @@ NEVER assume developer intent. If you are unsure about something, ALWAYS stop an ### General Guidance +- Prefer to use Deno to manage dependencies, build the project, and run tests. - Use snake_case names for plain TypeScript files. - Use comments sparingly; aim to make code readable and self-documenting. From 8a8bdd18acfe36747ad90a1bed2c62fe88d40bf1 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 29 Jul 2025 10:43:30 -0500 Subject: [PATCH 03/22] TEMP refactor plan for LLMs --- .../components/publications/REFACTOR_PLAN.md | 35 +++++++++++ src/routes/publication/REFACTOR_PLAN.md | 60 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/lib/components/publications/REFACTOR_PLAN.md create mode 100644 src/routes/publication/REFACTOR_PLAN.md diff --git a/src/lib/components/publications/REFACTOR_PLAN.md b/src/lib/components/publications/REFACTOR_PLAN.md new file mode 100644 index 0000000..f65f974 --- /dev/null +++ b/src/lib/components/publications/REFACTOR_PLAN.md @@ -0,0 +1,35 @@ +# Component Refactoring Plan for Path-Based Routing + +This document outlines the necessary changes to Svelte components to support the new path-based routing for publications. + +## 1. `PublicationHeader.svelte` + +This component generates links to publications and needs to be updated to the new URL format. + +### Actions: + +1. **Locate `href` derivation:** Find the `$derived.by` block that computes the `href` constant. +2. **Update URL structure:** Modify the logic to generate URLs in the format `/publication/[type]/[identifier]`. + - If the event has a `d` tag and is a replaceable event (e.g., kind 30040), encode it as an `naddr` and use the URL `/publication/naddr/[naddr]`. + - If the event is not replaceable but has an ID (like a kind 30041), encode it as an `nevent` and use the URL `/publication/nevent/[nevent]`. + - Use the existing `naddrEncode` and `neventEncode` utilities from `src/lib/utils.ts` to encode identifiers. + - If needed, add new `naddrDecode` and `neventDecode` utilities to `src/lib/utils.ts`, leveraging functions from the `nip19` module in the `nostr-tools` package. + +## 2. `Publication.svelte` + +This component is responsible for rendering the publication content. The primary changes will be in how data is passed to it, rather than in its internal logic. + +### Actions: + +1. **Review props:** The component accepts `rootAddress`, `publicationType`, and `indexEvent`. This is good. +2. **Update parent component:** The new `src/routes/publication/[type]/[identifier]/+page.svelte` will be responsible for providing these props from the data loaded on the server. No direct changes to `Publication.svelte` should be needed unless the data shape from the `load` function requires it. It is expected that the `load` function will provide the `indexEvent` directly. +3. **Add identifierType prop:** If the rendering logic needs to know the original identifier type (e.g., `id`, `d`, `naddr`, `nevent`), introduce a new `identifierType` prop to `Publication.svelte`. + +## 3. General Codebase Audit + +Other parts of the application might contain hardcoded links to publications using the old query parameter format. + +### Actions: + +1. **Perform a codebase search:** Search for the strings `"publication?id="` and `"publication?d="` to identify any other places where links are constructed. +2. **Update any found links:** Refactor any discovered instances to use the new `/publication/[type]/[identifier]` format. \ No newline at end of file diff --git a/src/routes/publication/REFACTOR_PLAN.md b/src/routes/publication/REFACTOR_PLAN.md new file mode 100644 index 0000000..a7b9689 --- /dev/null +++ b/src/routes/publication/REFACTOR_PLAN.md @@ -0,0 +1,60 @@ +# Publication Route Refactoring Plan + +This document outlines the plan to refactor the publication routes to improve SSR, add server-side metadata, and switch to path-based routing. + +## 1. New Route Structure + +The current query-based routing (`/publication?id=...`) will be replaced with a path-based structure: `/publication/[type]/[identifier]`. + +### Supported Identifier Types: +- `id`: A raw hex event ID. +- `d`: A `d` tag identifier from a replaceable event. +- `naddr`: A bech32-encoded `naddr` string for a replaceable event. +- `nevent`: A bech32-encoded `nevent` string. + +### Actions: + +1. **Create new route directory:** `src/routes/publication/[type]/[identifier]`. +2. **Move `+page.svelte`:** Relocate the content of the current `src/routes/publication/+page.svelte` to `src/routes/publication/[type]/[identifier]/+page.svelte`. +3. **Preserve old query-based route:** Instead of deleting old files, create `src/routes/publication/+page.server.ts` at the root of `src/routes/publication` to parse `?id=` and `?d=` query parameters and delegate to the new path-based routes. +4. **Review base route:** Ensure `/publication` either renders the main feed (via `PublicationFeed.svelte`) or redirects to `/start`; keep the existing `+page.svelte` in place for backward compatibility. + +## 2. Server-Side Rendering (SSR) and Data Loading + +We will use SvelteKit's `load` functions to fetch data on the server. + +### Actions: + +1. **Create `+page.server.ts`:** Inside `src/routes/publication/[type]/[identifier]/`, create a `+page.server.ts` file. +2. **Implement `load` function:** + - The `load` function will receive `params` containing `type` and `identifier`. + - It will use these params to fetch the publication's root event. The logic will need to handle the different identifier types: + - If `type` is `id`, use the `identifier` as a hex event ID. + - If `type` is `d`, use the `identifier` to search for an event with a matching `d` tag; when multiple events share the same tag, select the event with the latest `created_at` timestamp. // AI-NOTE: choose latest for now; future logic may change. + - If `type` is `naddr` or `nevent`, decode the `identifier` using `nip19.decode()` (from `nostr-tools`) and construct the appropriate filter. Add corresponding `naddrDecode` and `neventDecode` functions to `src/lib/utils.ts` to centralize NIP-19 logic. + - The fetched event will be returned as `data` to the `+page.svelte` component. + - Handle cases where the event is not found by throwing a 404 error using `@sveltejs/kit/error`. + +## 3. Server-Side Metadata + +Publication-specific metadata will be rendered on the server for better link previews. + +### Actions: + +1. **Create `+layout.server.ts`:** Inside `src/routes/publication/[type]/[identifier]/`, create a `+layout.server.ts`. Its `load` function will be very similar to the one in `+page.server.ts`. It will fetch the root event and return the necessary data for metadata (title, summary, image URL). +2. **Create `+layout.svelte`:** Inside `src/routes/publication/[type]/[identifier]/`, create a `+layout.svelte`. +3. **Implement metadata:** + - The layout will receive `data` from its `load` function. + - It will contain a `` block. + - Inside ``, render `` and `<meta>` tags (OpenGraph, Twitter Cards) using properties from the loaded `data`. + - Use `{@render children()}` in `+layout.svelte` to display the page content. + - Refer to https://web.dev/learn/html/metadata/#officially_defined_meta_tags for a compilation of recommended meta tags. + +## 4. Handling Authentication + +For publications requiring authentication, we need to avoid full SSR of content while still providing a good user experience. + +### Actions: + +- Skip authentication/authorization handling in this refactor; it will be addressed separately. +- If the `indexEvent` cannot be fetched, display a user-friendly error message in `+page.svelte` indicating the publication cannot be loaded. \ No newline at end of file From b665e1b019b50591e44e8ef7311d2a5ed8ab9f5d Mon Sep 17 00:00:00 2001 From: buttercat1791 <mjjurkoic@gmail.com> Date: Tue, 29 Jul 2025 10:54:37 -0500 Subject: [PATCH 04/22] AI - refactor for route params Needs developer review --- .../publications/PublicationHeader.svelte | 17 ++- .../components/util/ContainingIndexes.svelte | 4 +- .../util/ViewPublicationLink.svelte | 2 +- .../navigator/EventNetwork/NodeTooltip.svelte | 2 +- src/lib/utils.ts | 36 +++++ src/routes/about/+page.svelte | 4 +- src/routes/publication/+page.server.ts | 25 ++++ .../[type]/[identifier]/+layout.server.ts | 132 ++++++++++++++++++ .../[type]/[identifier]/+layout.svelte | 28 ++++ .../[type]/[identifier]/+page.server.ts | 123 ++++++++++++++++ .../[type]/[identifier]/+page.svelte | 95 +++++++++++++ src/routes/start/+page.svelte | 10 +- 12 files changed, 463 insertions(+), 15 deletions(-) create mode 100644 src/routes/publication/+page.server.ts create mode 100644 src/routes/publication/[type]/[identifier]/+layout.server.ts create mode 100644 src/routes/publication/[type]/[identifier]/+layout.svelte create mode 100644 src/routes/publication/[type]/[identifier]/+page.server.ts create mode 100644 src/routes/publication/[type]/[identifier]/+page.svelte diff --git a/src/lib/components/publications/PublicationHeader.svelte b/src/lib/components/publications/PublicationHeader.svelte index d0ed9b3..20a61a3 100644 --- a/src/lib/components/publications/PublicationHeader.svelte +++ b/src/lib/components/publications/PublicationHeader.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { naddrEncode } from "$lib/utils"; + import { naddrEncode, neventEncode } from "$lib/utils"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { activeInboxRelays } from "$lib/ndk"; import { Card } from "flowbite-svelte"; @@ -20,10 +20,19 @@ const href = $derived.by(() => { const d = event.getMatchingTags("d")[0]?.[1]; - if (d != null) { - return `publication?d=${d}`; + const isReplaceableEvent = event.kind === 30040 || event.kind === 30041; + + if (d != null && isReplaceableEvent) { + // For replaceable events with d tag, use naddr encoding + const naddr = naddrEncode(event, relays); + return `publication/naddr/${naddr}`; + } else if (event.id) { + // For non-replaceable events or events without d tag, use nevent encoding + const nevent = neventEncode(event, relays); + return `publication/nevent/${nevent}`; } else { - return `publication?id=${naddrEncode(event, relays)}`; + // Fallback to d tag if available + return d ? `publication/d/${d}` : null; } }); diff --git a/src/lib/components/util/ContainingIndexes.svelte b/src/lib/components/util/ContainingIndexes.svelte index f2b57f4..be06f3e 100644 --- a/src/lib/components/util/ContainingIndexes.svelte +++ b/src/lib/components/util/ContainingIndexes.svelte @@ -47,12 +47,12 @@ function navigateToIndex(indexEvent: NDKEvent) { const dTag = getMatchingTags(indexEvent, "d")[0]?.[1]; if (dTag) { - goto(`/publication?d=${encodeURIComponent(dTag)}`); + goto(`/publication/d/${encodeURIComponent(dTag)}`); } else { // Fallback to naddr try { const naddr = naddrEncode(indexEvent, $activeInboxRelays); - goto(`/publication?id=${encodeURIComponent(naddr)}`); + goto(`/publication/naddr/${encodeURIComponent(naddr)}`); } catch (err) { console.error("[ContainingIndexes] Error creating naddr:", err); } diff --git a/src/lib/components/util/ViewPublicationLink.svelte b/src/lib/components/util/ViewPublicationLink.svelte index fd7538d..3a6a77f 100644 --- a/src/lib/components/util/ViewPublicationLink.svelte +++ b/src/lib/components/util/ViewPublicationLink.svelte @@ -64,7 +64,7 @@ "ViewPublicationLink: Navigating to publication:", naddrAddress, ); - goto(`/publication?id=${encodeURIComponent(naddrAddress)}`); + goto(`/publication/naddr/${encodeURIComponent(naddrAddress)}`); } else { console.log("ViewPublicationLink: No naddr address found for event"); } diff --git a/src/lib/navigator/EventNetwork/NodeTooltip.svelte b/src/lib/navigator/EventNetwork/NodeTooltip.svelte index ef455bf..8066d4c 100644 --- a/src/lib/navigator/EventNetwork/NodeTooltip.svelte +++ b/src/lib/navigator/EventNetwork/NodeTooltip.svelte @@ -145,7 +145,7 @@ <div class="tooltip-content"> <!-- Title with link --> <div class="tooltip-title"> - <a href="/publication?id={node.id}" class="tooltip-title-link"> + <a href="/publication/id/{node.id}" class="tooltip-title-link"> {node.title || "Untitled"} </a> </div> diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ca992d0..970bdba 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -29,6 +29,42 @@ export function nprofileEncode(pubkey: string, relays: string[]) { return nip19.nprofileEncode({ pubkey, relays }); } +/** + * Decodes an naddr identifier and returns the decoded data + */ +export function naddrDecode(naddr: string) { + try { + if (!naddr.startsWith('naddr')) { + throw new Error('Invalid naddr format'); + } + const decoded = nip19.decode(naddr); + if (decoded.type !== 'naddr') { + throw new Error('Decoded result is not an naddr'); + } + return decoded.data; + } catch (error) { + throw new Error(`Failed to decode naddr: ${error}`); + } +} + +/** + * Decodes an nevent identifier and returns the decoded data + */ +export function neventDecode(nevent: string) { + try { + if (!nevent.startsWith('nevent')) { + throw new Error('Invalid nevent format'); + } + const decoded = nip19.decode(nevent); + if (decoded.type !== 'nevent') { + throw new Error('Decoded result is not an nevent'); + } + return decoded.data; + } catch (error) { + throw new Error(`Failed to decode nevent: ${error}`); + } +} + export function formatDate(unixtimestamp: number) { const months = [ "Jan", diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index 72b4697..715caf7 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -26,11 +26,11 @@ <P class="mb-3"> Alexandria is a reader and writer for <A - href="./publication?d=gitcitadel-project-documentation-curated-publications-specification-7-by-stella-v-1" + href="./publication/d/gitcitadel-project-documentation-curated-publications-specification-7-by-stella-v-1" >curated publications</A > (in Asciidoc), wiki pages (Asciidoc), and will eventually also support long-form articles (markup). It is produced by the <A - href="./publication?d=gitcitadel-project-documentation-by-stella-v-1" + href="./publication/d/gitcitadel-project-documentation-by-stella-v-1" >GitCitadel project team</A >. </P> diff --git a/src/routes/publication/+page.server.ts b/src/routes/publication/+page.server.ts new file mode 100644 index 0000000..c753d79 --- /dev/null +++ b/src/routes/publication/+page.server.ts @@ -0,0 +1,25 @@ +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ url }) => { + const id = url.searchParams.get("id"); + const dTag = url.searchParams.get("d"); + + // Handle backward compatibility for old query-based routes + if (id) { + // Check if id is an naddr or nevent + if (id.startsWith("naddr")) { + throw redirect(301, `/publication/naddr/${id}`); + } else if (id.startsWith("nevent")) { + throw redirect(301, `/publication/nevent/${id}`); + } else { + // Assume it's a hex ID + throw redirect(301, `/publication/id/${id}`); + } + } else if (dTag) { + throw redirect(301, `/publication/d/${dTag}`); + } + + // If no query parameters, redirect to the start page or show publication feed + throw redirect(301, "/start"); +}; \ No newline at end of file diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts new file mode 100644 index 0000000..176b812 --- /dev/null +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -0,0 +1,132 @@ +import { error } from "@sveltejs/kit"; +import type { LayoutServerLoad } from "./$types"; +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import { getActiveRelaySetAsNDKRelaySet } from "../../../../lib/ndk.ts"; +import { getMatchingTags } from "../../../../lib/utils/nostrUtils.ts"; +import { naddrDecode, neventDecode } from "../../../../lib/utils.ts"; +import type NDK from "@nostr-dev-kit/ndk"; + +/** + * Fetches an event by hex ID + */ +async function fetchEventById(ndk: NDK, id: string): Promise<NDKEvent> { + try { + const event = await ndk.fetchEvent(id); + if (!event) { + throw new Error(`Event not found for ID: ${id}`); + } + return event; + } catch (err) { + throw error(404, `Failed to fetch publication root event.\n${err}`); + } +} + +/** + * Fetches an event by d tag + */ +async function fetchEventByDTag(ndk: NDK, dTag: string): Promise<NDKEvent> { + try { + const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); + const events = await ndk.fetchEvents( + { "#d": [dTag] }, + { closeOnEose: false }, + relaySet, + ); + + if (!events || events.size === 0) { + throw new Error(`Event not found for d tag: ${dTag}`); + } + + // Choose the event with the latest created_at timestamp when multiple events share the same d tag + const sortedEvents = Array.from(events).sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); + return sortedEvents[0]; + } catch (err) { + throw error(404, `Failed to fetch publication root event.\n${err}`); + } +} + +/** + * Fetches an event by naddr identifier + */ +async function fetchEventByNaddr(ndk: NDK, naddr: string): Promise<NDKEvent> { + try { + const decoded = naddrDecode(naddr); + const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); + + const filter = { + kinds: [decoded.kind], + authors: [decoded.pubkey], + "#d": [decoded.identifier], + }; + + const event = await ndk.fetchEvent(filter, { closeOnEose: false }, relaySet); + if (!event) { + throw new Error(`Event not found for naddr: ${naddr}`); + } + return event; + } catch (err) { + throw error(404, `Failed to fetch publication root event.\n${err}`); + } +} + +/** + * Fetches an event by nevent identifier + */ +async function fetchEventByNevent(ndk: NDK, nevent: string): Promise<NDKEvent> { + try { + const decoded = neventDecode(nevent); + const event = await ndk.fetchEvent(decoded.id); + if (!event) { + throw new Error(`Event not found for nevent: ${nevent}`); + } + return event; + } catch (err) { + throw error(404, `Failed to fetch publication root event.\n${err}`); + } +} + +export const load: LayoutServerLoad = async ({ params, parent, url }) => { + const { type, identifier } = params; + const { ndk } = await parent(); + + if (!ndk) { + throw error(500, "NDK not available"); + } + + let indexEvent: NDKEvent; + + // Handle different identifier types + switch (type) { + case 'id': + indexEvent = await fetchEventById(ndk, identifier); + break; + case 'd': + indexEvent = await fetchEventByDTag(ndk, identifier); + break; + case 'naddr': + indexEvent = await fetchEventByNaddr(ndk, identifier); + break; + case 'nevent': + indexEvent = await fetchEventByNevent(ndk, identifier); + break; + default: + throw error(400, `Unsupported identifier type: ${type}`); + } + + // Extract metadata for meta tags + const title = getMatchingTags(indexEvent, "title")[0]?.[1] || "Alexandria Publication"; + const summary = getMatchingTags(indexEvent, "summary")[0]?.[1] || + "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; + const image = getMatchingTags(indexEvent, "image")[0]?.[1] || "/screenshots/old_books.jpg"; + const currentUrl = `${url.origin}${url.pathname}`; + + return { + indexEvent, + metadata: { + title, + summary, + image, + currentUrl, + }, + }; +}; \ No newline at end of file diff --git a/src/routes/publication/[type]/[identifier]/+layout.svelte b/src/routes/publication/[type]/[identifier]/+layout.svelte new file mode 100644 index 0000000..84dbc3b --- /dev/null +++ b/src/routes/publication/[type]/[identifier]/+layout.svelte @@ -0,0 +1,28 @@ +<script lang="ts"> + import type { LayoutProps } from "./$types"; + + let { data, children }: LayoutProps = $props(); + const { metadata } = data; +</script> + +<svelte:head> + <!-- Basic meta tags --> + <title>{metadata.title} + + + + + + + + + + + + + + + + + +{@render children()} \ No newline at end of file diff --git a/src/routes/publication/[type]/[identifier]/+page.server.ts b/src/routes/publication/[type]/[identifier]/+page.server.ts new file mode 100644 index 0000000..f58dfe0 --- /dev/null +++ b/src/routes/publication/[type]/[identifier]/+page.server.ts @@ -0,0 +1,123 @@ +import { error } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import { getActiveRelaySetAsNDKRelaySet } from "../../../../lib/ndk.ts"; +import { getMatchingTags } from "../../../../lib/utils/nostrUtils.ts"; +import { naddrDecode, neventDecode } from "../../../../lib/utils.ts"; +import type NDK from "@nostr-dev-kit/ndk"; + +/** + * Fetches an event by hex ID + */ +async function fetchEventById(ndk: NDK, id: string): Promise { + try { + const event = await ndk.fetchEvent(id); + if (!event) { + throw new Error(`Event not found for ID: ${id}`); + } + return event; + } catch (err) { + throw error(404, `Failed to fetch publication root event.\n${err}`); + } +} + +/** + * Fetches an event by d tag + */ +async function fetchEventByDTag(ndk: NDK, dTag: string): Promise { + try { + const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); // true for inbox relays + const events = await ndk.fetchEvents( + { "#d": [dTag] }, + { closeOnEose: false }, + relaySet, + ); + + if (!events || events.size === 0) { + throw new Error(`Event not found for d tag: ${dTag}`); + } + + // AI-NOTE: Choose the event with the latest created_at timestamp when multiple events share the same d tag + const sortedEvents = Array.from(events).sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); + return sortedEvents[0]; + } catch (err) { + throw error(404, `Failed to fetch publication root event.\n${err}`); + } +} + +/** + * Fetches an event by naddr identifier + */ +async function fetchEventByNaddr(ndk: NDK, naddr: string): Promise { + try { + const decoded = naddrDecode(naddr); + const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); + + const filter = { + kinds: [decoded.kind], + authors: [decoded.pubkey], + "#d": [decoded.identifier], + }; + + const event = await ndk.fetchEvent(filter, { closeOnEose: false }, relaySet); + if (!event) { + throw new Error(`Event not found for naddr: ${naddr}`); + } + return event; + } catch (err) { + throw error(404, `Failed to fetch publication root event.\n${err}`); + } +} + +/** + * Fetches an event by nevent identifier + */ +async function fetchEventByNevent(ndk: NDK, nevent: string): Promise { + try { + const decoded = neventDecode(nevent); + const event = await ndk.fetchEvent(decoded.id); + if (!event) { + throw new Error(`Event not found for nevent: ${nevent}`); + } + return event; + } catch (err) { + throw error(404, `Failed to fetch publication root event.\n${err}`); + } +} + +export const load: PageServerLoad = async ({ params, parent }) => { + const { type, identifier } = params; + const { ndk } = await parent(); + + if (!ndk) { + throw error(500, "NDK not available"); + } + + let indexEvent: NDKEvent; + + // Handle different identifier types + switch (type) { + case 'id': + indexEvent = await fetchEventById(ndk, identifier); + break; + case 'd': + indexEvent = await fetchEventByDTag(ndk, identifier); + break; + case 'naddr': + indexEvent = await fetchEventByNaddr(ndk, identifier); + break; + case 'nevent': + indexEvent = await fetchEventByNevent(ndk, identifier); + break; + default: + throw error(400, `Unsupported identifier type: ${type}`); + } + + const publicationType = getMatchingTags(indexEvent, "type")[0]?.[1]; + + return { + publicationType, + indexEvent, + ndk, // Pass ndk to the page for the publication tree + }; +}; \ No newline at end of file diff --git a/src/routes/publication/[type]/[identifier]/+page.svelte b/src/routes/publication/[type]/[identifier]/+page.svelte new file mode 100644 index 0000000..9f37442 --- /dev/null +++ b/src/routes/publication/[type]/[identifier]/+page.svelte @@ -0,0 +1,95 @@ + + + + +
+ +
\ No newline at end of file diff --git a/src/routes/start/+page.svelte b/src/routes/start/+page.svelte index ff617c6..6cb37a3 100644 --- a/src/routes/start/+page.svelte +++ b/src/routes/start/+page.svelte @@ -91,7 +91,7 @@

An example of a book is Jane Eyre

@@ -127,7 +127,7 @@

An example of a research paper is Less Partnering, Less Children, or Both?

@@ -145,9 +145,9 @@

Our own team uses Alexandria to document the app, to display our blog entriesblog entries, as well as to store copies of our most interesting - technical specifications.

@@ -168,7 +168,7 @@ collaborative knowledge bases and documentation. Wiki pages, such as this one about the goto("/publication/d/sybil")}>Sybil utility use the same Asciidoc format as other publications but are specifically designed for interconnected, evolving content.

From 2ce83b75cad1ac305bab32976b2a7ad274983c4b Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 29 Jul 2025 14:09:06 -0500 Subject: [PATCH 05/22] Set return types for NIP-19 decoding utils --- src/lib/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 970bdba..bc2a2ab 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,7 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; import { getMatchingTags } from "./utils/nostrUtils.ts"; +import { AddressPointer, EventPointer } from "nostr-tools/nip19"; export function neventEncode(event: NDKEvent, relays: string[]) { return nip19.neventEncode({ @@ -32,7 +33,7 @@ export function nprofileEncode(pubkey: string, relays: string[]) { /** * Decodes an naddr identifier and returns the decoded data */ -export function naddrDecode(naddr: string) { +export function naddrDecode(naddr: string): AddressPointer { try { if (!naddr.startsWith('naddr')) { throw new Error('Invalid naddr format'); @@ -50,7 +51,7 @@ export function naddrDecode(naddr: string) { /** * Decodes an nevent identifier and returns the decoded data */ -export function neventDecode(nevent: string) { +export function neventDecode(nevent: string): EventPointer { try { if (!nevent.startsWith('nevent')) { throw new Error('Invalid nevent format'); From 114b3a035b1e8b001743a45e4b541990db68dfae Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 29 Jul 2025 14:14:05 -0500 Subject: [PATCH 06/22] Simplify link generation in `PublicationHeader` component The component only needs to work with kind 30040 index events. --- .../publications/PublicationHeader.svelte | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/lib/components/publications/PublicationHeader.svelte b/src/lib/components/publications/PublicationHeader.svelte index 20a61a3..c293fc9 100644 --- a/src/lib/components/publications/PublicationHeader.svelte +++ b/src/lib/components/publications/PublicationHeader.svelte @@ -7,6 +7,7 @@ import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import LazyImage from "$components/util/LazyImage.svelte"; import { generateDarkPastelColor } from "$lib/utils/image_utils"; + import { indexKind } from "$lib/consts"; const { event } = $props<{ event: NDKEvent }>(); @@ -19,20 +20,16 @@ }); const href = $derived.by(() => { - const d = event.getMatchingTags("d")[0]?.[1]; - const isReplaceableEvent = event.kind === 30040 || event.kind === 30041; + const dTag = event.getMatchingTags("d")[0]?.[1]; + const isIndexEvent = event.kind === indexKind; - if (d != null && isReplaceableEvent) { - // For replaceable events with d tag, use naddr encoding + if (dTag != null && isIndexEvent) { + // For index events with d tag, use naddr encoding const naddr = naddrEncode(event, relays); return `publication/naddr/${naddr}`; - } else if (event.id) { - // For non-replaceable events or events without d tag, use nevent encoding - const nevent = neventEncode(event, relays); - return `publication/nevent/${nevent}`; } else { // Fallback to d tag if available - return d ? `publication/d/${d}` : null; + return dTag ? `publication/d/${dTag}` : null; } }); From 2150a311ecfb5575ac96e2f79e1ac2fa94e9af8c Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 29 Jul 2025 14:24:14 -0500 Subject: [PATCH 07/22] Revise general project instructions --- .cursor/rules/alexandria.mdc | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.cursor/rules/alexandria.mdc b/.cursor/rules/alexandria.mdc index c9c9730..61e8b28 100644 --- a/.cursor/rules/alexandria.mdc +++ b/.cursor/rules/alexandria.mdc @@ -9,11 +9,7 @@ You are senior full-stack software engineer with 20 years of experience writing ## Project Overview -Alexandria is a Nostr project written in Svelte 5 and SvelteKit 2. It is a web app for reading, commenting on, and publishing books, blogs, and other long-form content stored on Nostr relays. It revolves around breaking long AsciiDoc documents into Nostr events, with each event containing a paragraph or so of text from the document. These individual content events are organized by index events into publications. An index contains an ordered list of references to other index events or content events, forming a tree. - -### Reader Features - -In reader mode, Alexandria loads a document tree from a root publication index event. The AsciiDoc text content of the various content events, along with headers specified by tags in the index events, is composed and rendered as a single document from the user's point of view. +Alexandria is a Nostr project written in Svelte 5 and SvelteKit 2. It is a web app for reading, commenting on, and publishing books, blogs, and other long-form content stored on Nostr relays. ### Tech Stack @@ -36,7 +32,13 @@ When responding to prompts, adhere to the following rules: - Avoid proposing code edits unless I specifically tell you to do so. - When giving examples from my codebase, include the file name and line numbers so I can find the relevant code easily. -## AI Anchor Comments Format +## Coding Guidelines + +### Prime Directive + +NEVER assume developer intent. If you are unsure about something, ALWAYS stop and ask the developer for clarification before proceeding. + +### AI Anchor Comments - Use anchor comments prefixed with `AI-NOTE:`, `AI-TODO:`, or `AI-QUESTION:` to share context between AI agents and developers across time. - Use all-caps prefixes. @@ -50,12 +52,6 @@ When responding to prompts, adhere to the following rules: - Code is confusing. - Code could have a bug. -## Coding Guidelines - -### Prime Directive - -NEVER assume developer intent. If you are unsure about something, ALWAYS stop and ask the developer for clarification before proceeding. - ### General Guidance - Prefer to use Deno to manage dependencies, build the project, and run tests. From 1bd29506798a9420123330c3a5242f47b00611ec Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 29 Jul 2025 14:26:42 -0500 Subject: [PATCH 08/22] Clean up redirects on query param based routes --- src/routes/publication/+page.server.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/publication/+page.server.ts b/src/routes/publication/+page.server.ts index c753d79..29fc5a6 100644 --- a/src/routes/publication/+page.server.ts +++ b/src/routes/publication/+page.server.ts @@ -1,7 +1,7 @@ import { redirect } from "@sveltejs/kit"; import type { PageServerLoad } from "./$types"; -export const load: PageServerLoad = async ({ url }) => { +export const load: PageServerLoad = ({ url }) => { const id = url.searchParams.get("id"); const dTag = url.searchParams.get("d"); @@ -20,6 +20,7 @@ export const load: PageServerLoad = async ({ url }) => { throw redirect(301, `/publication/d/${dTag}`); } - // If no query parameters, redirect to the start page or show publication feed + // If no query parameters, redirect to the start page or show publication feed\ + // AI-TODO: Redirect to a "not found" page. throw redirect(301, "/start"); }; \ No newline at end of file From e093ee599d20b5ca110d9d5f9bbb90fdc5a9d9f6 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 29 Jul 2025 14:28:44 -0500 Subject: [PATCH 09/22] Give explicit instructions on reading and updating anchor comemnts --- .cursor/rules/alexandria.mdc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.cursor/rules/alexandria.mdc b/.cursor/rules/alexandria.mdc index 61e8b28..15ee6d0 100644 --- a/.cursor/rules/alexandria.mdc +++ b/.cursor/rules/alexandria.mdc @@ -54,6 +54,8 @@ NEVER assume developer intent. If you are unsure about something, ALWAYS stop an ### General Guidance +- Before writing any code, ALWAYS search the codebase for relevant anchor comments. +- Whenever updating code, ALWAYS update relevant anchor comments. - Prefer to use Deno to manage dependencies, build the project, and run tests. - Use snake_case names for plain TypeScript files. - Use comments sparingly; aim to make code readable and self-documenting. From 4a4b29f46c4a0aacf1113db59d54eb7c9c1b839d Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 29 Jul 2025 15:16:11 -0500 Subject: [PATCH 10/22] Clean up old pages and add TODOs to new ones --- .../components/publications/REFACTOR_PLAN.md | 35 ----- src/routes/+layout.ts | 2 - src/routes/publication/+page.svelte | 134 ------------------ src/routes/publication/+page.ts | 115 --------------- src/routes/publication/REFACTOR_PLAN.md | 60 -------- .../[type]/[identifier]/+layout.server.ts | 2 + .../[type]/[identifier]/+page.server.ts | 2 + .../[type]/[identifier]/+page.svelte | 10 +- 8 files changed, 9 insertions(+), 351 deletions(-) delete mode 100644 src/lib/components/publications/REFACTOR_PLAN.md delete mode 100644 src/routes/publication/+page.svelte delete mode 100644 src/routes/publication/+page.ts delete mode 100644 src/routes/publication/REFACTOR_PLAN.md diff --git a/src/lib/components/publications/REFACTOR_PLAN.md b/src/lib/components/publications/REFACTOR_PLAN.md deleted file mode 100644 index f65f974..0000000 --- a/src/lib/components/publications/REFACTOR_PLAN.md +++ /dev/null @@ -1,35 +0,0 @@ -# Component Refactoring Plan for Path-Based Routing - -This document outlines the necessary changes to Svelte components to support the new path-based routing for publications. - -## 1. `PublicationHeader.svelte` - -This component generates links to publications and needs to be updated to the new URL format. - -### Actions: - -1. **Locate `href` derivation:** Find the `$derived.by` block that computes the `href` constant. -2. **Update URL structure:** Modify the logic to generate URLs in the format `/publication/[type]/[identifier]`. - - If the event has a `d` tag and is a replaceable event (e.g., kind 30040), encode it as an `naddr` and use the URL `/publication/naddr/[naddr]`. - - If the event is not replaceable but has an ID (like a kind 30041), encode it as an `nevent` and use the URL `/publication/nevent/[nevent]`. - - Use the existing `naddrEncode` and `neventEncode` utilities from `src/lib/utils.ts` to encode identifiers. - - If needed, add new `naddrDecode` and `neventDecode` utilities to `src/lib/utils.ts`, leveraging functions from the `nip19` module in the `nostr-tools` package. - -## 2. `Publication.svelte` - -This component is responsible for rendering the publication content. The primary changes will be in how data is passed to it, rather than in its internal logic. - -### Actions: - -1. **Review props:** The component accepts `rootAddress`, `publicationType`, and `indexEvent`. This is good. -2. **Update parent component:** The new `src/routes/publication/[type]/[identifier]/+page.svelte` will be responsible for providing these props from the data loaded on the server. No direct changes to `Publication.svelte` should be needed unless the data shape from the `load` function requires it. It is expected that the `load` function will provide the `indexEvent` directly. -3. **Add identifierType prop:** If the rendering logic needs to know the original identifier type (e.g., `id`, `d`, `naddr`, `nevent`), introduce a new `identifierType` prop to `Publication.svelte`. - -## 3. General Codebase Audit - -Other parts of the application might contain hardcoded links to publications using the old query parameter format. - -### Actions: - -1. **Perform a codebase search:** Search for the strings `"publication?id="` and `"publication?d="` to identify any other places where links are constructed. -2. **Update any found links:** Refactor any discovered instances to use the new `/publication/[type]/[identifier]` format. \ No newline at end of file diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 80555ad..7ac4f69 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -9,8 +9,6 @@ import Pharos, { pharosInstance } from "../lib/parser.ts"; import type { LayoutLoad } from "./$types"; import { get } from "svelte/store"; -export const ssr = false; - export const load: LayoutLoad = () => { // Initialize NDK with new relay management system const ndk = initNdk(); diff --git a/src/routes/publication/+page.svelte b/src/routes/publication/+page.svelte deleted file mode 100644 index eacc71b..0000000 --- a/src/routes/publication/+page.svelte +++ /dev/null @@ -1,134 +0,0 @@ - - - - - {title} - - - - - - - - - - - - - - - - - - - -
- -
diff --git a/src/routes/publication/+page.ts b/src/routes/publication/+page.ts deleted file mode 100644 index a79423f..0000000 --- a/src/routes/publication/+page.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { error } from "@sveltejs/kit"; -import type { Load } from "@sveltejs/kit"; -import type { NDKEvent } from "@nostr-dev-kit/ndk"; -import { nip19 } from "nostr-tools"; -import { getActiveRelaySetAsNDKRelaySet } from "../../lib/ndk.ts"; -import { getMatchingTags } from "../../lib/utils/nostrUtils.ts"; -import type NDK from "@nostr-dev-kit/ndk"; - -/** - * Decodes an naddr identifier and returns a filter object - */ -function decodeNaddr(id: string) { - try { - if (!id.startsWith("naddr")) return {}; - - const decoded = nip19.decode(id); - if (decoded.type !== "naddr") return {}; - - const data = decoded.data; - return { - kinds: [data.kind], - authors: [data.pubkey], - "#d": [data.identifier], - }; - } catch (e) { - console.error("Failed to decode naddr:", e); - return null; - } -} - -/** - * Fetches an event by ID or filter - */ -async function fetchEventById(ndk: NDK, id: string): Promise { - const filter = decodeNaddr(id); - - // Handle the case where filter is null (decoding error) - if (filter === null) { - // If we can't decode the naddr, try using the raw ID - try { - const event = await ndk.fetchEvent(id); - if (!event) { - throw new Error(`Event not found for ID: ${id}`); - } - return event; - } catch (err) { - throw error(404, `Failed to fetch publication root event.\n${err}`); - } - } - - const hasFilter = Object.keys(filter).length > 0; - - try { - const event = await (hasFilter - ? ndk.fetchEvent(filter) - : ndk.fetchEvent(id)); - - if (!event) { - throw new Error(`Event not found for ID: ${id}`); - } - return event; - } catch (err) { - throw error(404, `Failed to fetch publication root event.\n${err}`); - } -} - -/** - * Fetches an event by d tag - */ -async function fetchEventByDTag(ndk: NDK, dTag: string): Promise { - try { - const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); // true for inbox relays - const event = await ndk.fetchEvent( - { "#d": [dTag] }, - { closeOnEose: false }, - relaySet, - ); - - if (!event) { - throw new Error(`Event not found for d tag: ${dTag}`); - } - return event; - } catch (err) { - throw error(404, `Failed to fetch publication root event.\n${err}`); - } -} - -// TODO: Use path params instead of query params. -export const load: Load = async ({ - url, - parent, -}: { - url: URL; - parent: () => Promise>>; -}) => { - const id = url.searchParams.get("id"); - const dTag = url.searchParams.get("d"); - const { ndk } = await parent(); - - if (!id && !dTag) { - throw error(400, "No publication root event ID or d tag provided."); - } - - // Fetch the event based on available parameters - const indexEvent = id - ? await fetchEventById(ndk!, id) - : await fetchEventByDTag(ndk!, dTag!); - - const publicationType = getMatchingTags(indexEvent, "type")[0]?.[1]; - - return { - publicationType, - indexEvent, - }; -}; diff --git a/src/routes/publication/REFACTOR_PLAN.md b/src/routes/publication/REFACTOR_PLAN.md deleted file mode 100644 index a7b9689..0000000 --- a/src/routes/publication/REFACTOR_PLAN.md +++ /dev/null @@ -1,60 +0,0 @@ -# Publication Route Refactoring Plan - -This document outlines the plan to refactor the publication routes to improve SSR, add server-side metadata, and switch to path-based routing. - -## 1. New Route Structure - -The current query-based routing (`/publication?id=...`) will be replaced with a path-based structure: `/publication/[type]/[identifier]`. - -### Supported Identifier Types: -- `id`: A raw hex event ID. -- `d`: A `d` tag identifier from a replaceable event. -- `naddr`: A bech32-encoded `naddr` string for a replaceable event. -- `nevent`: A bech32-encoded `nevent` string. - -### Actions: - -1. **Create new route directory:** `src/routes/publication/[type]/[identifier]`. -2. **Move `+page.svelte`:** Relocate the content of the current `src/routes/publication/+page.svelte` to `src/routes/publication/[type]/[identifier]/+page.svelte`. -3. **Preserve old query-based route:** Instead of deleting old files, create `src/routes/publication/+page.server.ts` at the root of `src/routes/publication` to parse `?id=` and `?d=` query parameters and delegate to the new path-based routes. -4. **Review base route:** Ensure `/publication` either renders the main feed (via `PublicationFeed.svelte`) or redirects to `/start`; keep the existing `+page.svelte` in place for backward compatibility. - -## 2. Server-Side Rendering (SSR) and Data Loading - -We will use SvelteKit's `load` functions to fetch data on the server. - -### Actions: - -1. **Create `+page.server.ts`:** Inside `src/routes/publication/[type]/[identifier]/`, create a `+page.server.ts` file. -2. **Implement `load` function:** - - The `load` function will receive `params` containing `type` and `identifier`. - - It will use these params to fetch the publication's root event. The logic will need to handle the different identifier types: - - If `type` is `id`, use the `identifier` as a hex event ID. - - If `type` is `d`, use the `identifier` to search for an event with a matching `d` tag; when multiple events share the same tag, select the event with the latest `created_at` timestamp. // AI-NOTE: choose latest for now; future logic may change. - - If `type` is `naddr` or `nevent`, decode the `identifier` using `nip19.decode()` (from `nostr-tools`) and construct the appropriate filter. Add corresponding `naddrDecode` and `neventDecode` functions to `src/lib/utils.ts` to centralize NIP-19 logic. - - The fetched event will be returned as `data` to the `+page.svelte` component. - - Handle cases where the event is not found by throwing a 404 error using `@sveltejs/kit/error`. - -## 3. Server-Side Metadata - -Publication-specific metadata will be rendered on the server for better link previews. - -### Actions: - -1. **Create `+layout.server.ts`:** Inside `src/routes/publication/[type]/[identifier]/`, create a `+layout.server.ts`. Its `load` function will be very similar to the one in `+page.server.ts`. It will fetch the root event and return the necessary data for metadata (title, summary, image URL). -2. **Create `+layout.svelte`:** Inside `src/routes/publication/[type]/[identifier]/`, create a `+layout.svelte`. -3. **Implement metadata:** - - The layout will receive `data` from its `load` function. - - It will contain a `` block. - - Inside ``, render `` and `<meta>` tags (OpenGraph, Twitter Cards) using properties from the loaded `data`. - - Use `{@render children()}` in `+layout.svelte` to display the page content. - - Refer to https://web.dev/learn/html/metadata/#officially_defined_meta_tags for a compilation of recommended meta tags. - -## 4. Handling Authentication - -For publications requiring authentication, we need to avoid full SSR of content while still providing a good user experience. - -### Actions: - -- Skip authentication/authorization handling in this refactor; it will be addressed separately. -- If the `indexEvent` cannot be fetched, display a user-friendly error message in `+page.svelte` indicating the publication cannot be loaded. \ No newline at end of file diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts index 176b812..a9ddd3c 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.server.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -6,6 +6,8 @@ import { getMatchingTags } from "../../../../lib/utils/nostrUtils.ts"; import { naddrDecode, neventDecode } from "../../../../lib/utils.ts"; import type NDK from "@nostr-dev-kit/ndk"; +// AI-TODO: Use `fetchEventWithFallback` from `nostrUtils.ts` to retrieve events in this file. + /** * Fetches an event by hex ID */ diff --git a/src/routes/publication/[type]/[identifier]/+page.server.ts b/src/routes/publication/[type]/[identifier]/+page.server.ts index f58dfe0..3c033c7 100644 --- a/src/routes/publication/[type]/[identifier]/+page.server.ts +++ b/src/routes/publication/[type]/[identifier]/+page.server.ts @@ -6,6 +6,8 @@ import { getMatchingTags } from "../../../../lib/utils/nostrUtils.ts"; import { naddrDecode, neventDecode } from "../../../../lib/utils.ts"; import type NDK from "@nostr-dev-kit/ndk"; +// AI-TODO: Use `fetchEventWithFallback` from `nostrUtils.ts` to retrieve events in this file. + /** * Fetches an event by hex ID */ diff --git a/src/routes/publication/[type]/[identifier]/+page.svelte b/src/routes/publication/[type]/[identifier]/+page.svelte index 9f37442..07cf547 100644 --- a/src/routes/publication/[type]/[identifier]/+page.svelte +++ b/src/routes/publication/[type]/[identifier]/+page.svelte @@ -80,11 +80,11 @@ }); </script> - <ArticleNav - publicationType={data.publicationType} - rootId={data.indexEvent.id} - indexEvent={data.indexEvent} - /> +<ArticleNav + publicationType={data.publicationType} + rootId={data.indexEvent.id} + indexEvent={data.indexEvent} +/> <main class="publication {data.publicationType}"> <Publication From 2bb42d3ec4c58274b1e8bfdca243bf190dbc3aa3 Mon Sep 17 00:00:00 2001 From: buttercat1791 <mjjurkoic@gmail.com> Date: Tue, 29 Jul 2025 16:26:14 -0500 Subject: [PATCH 11/22] Clean up and refactor based on AI code review --- .../publications/PublicationFeed.svelte | 8 +- .../navigator/EventNetwork/NodeTooltip.svelte | 2 +- src/lib/utils.ts | 63 ++++++++----- src/lib/utils/event_search.ts | 3 +- .../advancedAsciidoctorPostProcessor.ts | 6 +- src/lib/utils/network_detection.ts | 5 +- src/lib/utils/nostrUtils.ts | 83 +++++++++++++++++ src/lib/utils/search_types.ts | 2 +- src/routes/+layout.ts | 23 +++-- src/routes/publication/+page.server.ts | 33 +++++-- .../[type]/[identifier]/+layout.server.ts | 89 +------------------ .../[type]/[identifier]/+page.server.ts | 89 +------------------ 12 files changed, 183 insertions(+), 223 deletions(-) diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 674eb5a..8156cfe 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -290,9 +290,9 @@ }; // Debounced search function - const debouncedSearch = debounce(async (query: string) => { + const debouncedSearch = debounce((query: string | undefined) => { console.debug("[PublicationFeed] Search query changed:", query); - if (query.trim()) { + if (query && query.trim()) { const filtered = filterEventsBySearch(allIndexEvents); eventsInView = filtered.slice(0, 30); endOfFeed = filtered.length <= 30; @@ -303,10 +303,6 @@ }, 300); $effect(() => { - console.debug( - "[PublicationFeed] Search query effect triggered:", - props.searchQuery, - ); debouncedSearch(props.searchQuery); }); diff --git a/src/lib/navigator/EventNetwork/NodeTooltip.svelte b/src/lib/navigator/EventNetwork/NodeTooltip.svelte index 8066d4c..8e95b6e 100644 --- a/src/lib/navigator/EventNetwork/NodeTooltip.svelte +++ b/src/lib/navigator/EventNetwork/NodeTooltip.svelte @@ -145,7 +145,7 @@ <div class="tooltip-content"> <!-- Title with link --> <div class="tooltip-title"> - <a href="/publication/id/{node.id}" class="tooltip-title-link"> + <a href={`/publication/id/${node.id}`} class="tooltip-title-link"> {node.title || "Untitled"} </a> </div> diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bc2a2ab..00576d5 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,7 +1,21 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; import { getMatchingTags } from "./utils/nostrUtils.ts"; -import { AddressPointer, EventPointer } from "nostr-tools/nip19"; +import type { AddressPointer, EventPointer } from "nostr-tools/nip19"; + +export class DecodeError extends Error { + constructor(message: string) { + super(message); + this.name = "DecodeError"; + } +} + +export class InvalidKindError extends DecodeError { + constructor(message: string) { + super(message); + this.name = "InvalidKindError"; + } +} export function neventEncode(event: NDKEvent, relays: string[]) { return nip19.neventEncode({ @@ -31,39 +45,41 @@ export function nprofileEncode(pubkey: string, relays: string[]) { } /** - * Decodes an naddr identifier and returns the decoded data + * Decodes a nostr identifier (naddr, nevent) and returns the decoded data. + * @param identifier The nostr identifier to decode. + * @param expectedType The expected type of the decoded data ('naddr' or 'nevent'). + * @returns The decoded data. */ -export function naddrDecode(naddr: string): AddressPointer { +function decodeNostrIdentifier<T extends AddressPointer | EventPointer>( + identifier: string, + expectedType: "naddr" | "nevent", +): T { try { - if (!naddr.startsWith('naddr')) { - throw new Error('Invalid naddr format'); + if (!identifier.startsWith(expectedType)) { + throw new InvalidKindError(`Invalid ${expectedType} format`); } - const decoded = nip19.decode(naddr); - if (decoded.type !== 'naddr') { - throw new Error('Decoded result is not an naddr'); + const decoded = nip19.decode(identifier); + if (decoded.type !== expectedType) { + throw new InvalidKindError(`Decoded result is not an ${expectedType}`); } - return decoded.data; + return decoded.data as T; } catch (error) { - throw new Error(`Failed to decode naddr: ${error}`); + throw new DecodeError(`Failed to decode ${expectedType}: ${error}`); } } +/** + * Decodes an naddr identifier and returns the decoded data + */ +export function naddrDecode(naddr: string): AddressPointer { + return decodeNostrIdentifier<AddressPointer>(naddr, "naddr"); +} + /** * Decodes an nevent identifier and returns the decoded data */ export function neventDecode(nevent: string): EventPointer { - try { - if (!nevent.startsWith('nevent')) { - throw new Error('Invalid nevent format'); - } - const decoded = nip19.decode(nevent); - if (decoded.type !== 'nevent') { - throw new Error('Decoded result is not an nevent'); - } - return decoded.data; - } catch (error) { - throw new Error(`Failed to decode nevent: ${error}`); - } + return decodeNostrIdentifier<EventPointer>(nevent, "nevent"); } export function formatDate(unixtimestamp: number) { @@ -206,7 +222,8 @@ Array.prototype.findIndexAsync = function <T>( * @param wait The number of milliseconds to delay * @returns A debounced version of the function */ -export function debounce<T extends (...args: unknown[]) => unknown>( +// deno-lint-ignore no-explicit-any +export function debounce<T extends (...args: any[]) => any>( func: T, wait: number, ): (...args: Parameters<T>) => void { diff --git a/src/lib/utils/event_search.ts b/src/lib/utils/event_search.ts index 25319c0..5330ebb 100644 --- a/src/lib/utils/event_search.ts +++ b/src/lib/utils/event_search.ts @@ -1,7 +1,8 @@ import { ndkInstance } from "../ndk.ts"; import { fetchEventWithFallback } from "./nostrUtils.ts"; import { nip19 } from "nostr-tools"; -import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; +import type { NDKFilter } from "@nostr-dev-kit/ndk"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; import { get } from "svelte/store"; import { wellKnownUrl, isValidNip05Address } from "./search_utils.ts"; import { TIMEOUTS, VALIDATION } from "./search_constants.ts"; diff --git a/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts b/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts index 10ec1a7..41e4df9 100644 --- a/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts +++ b/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts @@ -32,9 +32,11 @@ export async function postProcessAdvancedAsciidoctorHtml( } if ( typeof globalThis !== "undefined" && - typeof globalThis.MathJax?.typesetPromise === "function" + // deno-lint-ignore no-explicit-any + typeof (globalThis as any).MathJax?.typesetPromise === "function" ) { - setTimeout(() => globalThis.MathJax.typesetPromise(), 0); + // deno-lint-ignore no-explicit-any + setTimeout(() => (globalThis as any).MathJax.typesetPromise(), 0); } return processedHtml; } catch (error) { diff --git a/src/lib/utils/network_detection.ts b/src/lib/utils/network_detection.ts index 40bb568..e69543a 100644 --- a/src/lib/utils/network_detection.ts +++ b/src/lib/utils/network_detection.ts @@ -153,10 +153,11 @@ export function getRelaySetForNetworkCondition( */ export function startNetworkMonitoring( onNetworkChange: (condition: NetworkCondition) => void, - checkInterval: number = 60000 // Increased to 60 seconds to reduce spam + checkInterval: number = 60000, // Increased to 60 seconds to reduce spam ): () => void { let lastCondition: NetworkCondition | null = null; - let intervalId: number | null = null; + // deno-lint-ignore no-explicit-any + let intervalId: any = null; const checkNetwork = async () => { try { diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 91d3309..813f1e5 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -12,6 +12,8 @@ import { schnorr } from "@noble/curves/secp256k1"; import { bytesToHex } from "@noble/hashes/utils"; import { wellKnownUrl } from "./search_utility.ts"; import { VALIDATION } from "./search_constants.ts"; +import { error } from "@sveltejs/kit"; +import { naddrDecode, neventDecode } from "../utils.ts"; const badgeCheckSvg = '<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2c-.791 0-1.55.314-2.11.874l-.893.893a.985.985 0 0 1-.696.288H7.04A2.984 2.984 0 0 0 4.055 7.04v1.262a.986.986 0 0 1-.288.696l-.893.893a2.984 2.984 0 0 0 0 4.22l.893.893a.985.985 0 0 1 .288.696v1.262a2.984 2.984 0 0 0 2.984 2.984h1.262c.261 0 .512.104.696.288l.893.893a2.984 2.984 0 0 0 4.22 0l.893-.893a.985.985 0 0 1 .696-.288h1.262a2.984 2.984 0 0 0 2.984-2.984V15.7c0-.261.104-.512.288-.696l.893-.893a2.984 2.984 0 0 0 0-4.22l-.893-.893a.985.985 0 0 1-.288-.696V7.04a2.984 2.984 0 0 0-2.984-2.984h-1.262a.985.985 0 0 1-.696-.288l-.893-.893A2.984 2.984 0 0 0 12 2Zm3.683 7.73a1 1 0 1 0-1.414-1.413l-4.253 4.253-1.277-1.277a1 1 0 0 0-1.415 1.414l1.985 1.984a1 1 0 0 0 1.414 0l4.96-4.96Z" clip-rule="evenodd"/></svg>'; @@ -668,3 +670,84 @@ export function prefixNostrAddresses(content: string): string { return `nostr:${match}`; }); } + +// Added functions for fetching events by various identifiers + +/** + * Fetches an event by hex ID, throwing a SvelteKit 404 error if not found. + */ +export async function fetchEventById(ndk: NDK, id: string): Promise<NDKEvent> { + try { + const event = await fetchEventWithFallback(ndk, id); + if (!event) { + throw error(404, `Event not found for ID: ${id}`); + } + return event; + } catch (err) { + if (err && typeof err === "object" && "status" in err) { + throw err; + } + throw error(404, `Failed to fetch event by ID: ${err}`); + } +} + +/** + * Fetches an event by d tag, throwing a 404 if not found. + */ +export async function fetchEventByDTag(ndk: NDK, dTag: string): Promise<NDKEvent> { + try { + const event = await fetchEventWithFallback(ndk, { "#d": [dTag], limit: 1 }); + if (!event) { + throw error(404, `Event not found for d-tag: ${dTag}`); + } + return event; + } catch (err) { + if (err && typeof err === "object" && "status" in err) { + throw err; + } + throw error(404, `Failed to fetch event by d-tag: ${err}`); + } +} + +/** + * Fetches an event by naddr identifier. + */ +export async function fetchEventByNaddr(ndk: NDK, naddr: string): Promise<NDKEvent> { + try { + const decoded = naddrDecode(naddr); + const filter = { + kinds: [decoded.kind], + authors: [decoded.pubkey], + "#d": [decoded.identifier], + }; + const event = await fetchEventWithFallback(ndk, filter); + if (!event) { + throw error(404, `Event not found for naddr: ${naddr}`); + } + return event; + } catch (err) { + if (err && typeof err === "object" && "status" in err) { + throw err; + } + throw error(404, `Failed to fetch event by naddr: ${err}`); + } +} + +/** + * Fetches an event by nevent identifier. + */ +export async function fetchEventByNevent(ndk: NDK, nevent: string): Promise<NDKEvent> { + try { + const decoded = neventDecode(nevent); + const event = await fetchEventWithFallback(ndk, decoded.id); + if (!event) { + throw error(404, `Event not found for nevent: ${nevent}`); + } + return event; + } catch (err) { + if (err && typeof err === "object" && "status" in err) { + throw err; + } + throw error(404, `Failed to fetch event by nevent: ${err}`); + } +} diff --git a/src/lib/utils/search_types.ts b/src/lib/utils/search_types.ts index 134ceff..a537edb 100644 --- a/src/lib/utils/search_types.ts +++ b/src/lib/utils/search_types.ts @@ -1,4 +1,4 @@ -import { NDKEvent, NDKFilter, NDKSubscription } from "@nostr-dev-kit/ndk"; +import type { NDKEvent, NDKFilter, NDKSubscription } from "@nostr-dev-kit/ndk"; /** * Extended NostrProfile interface for search results diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 7ac4f69..59bc393 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -8,12 +8,13 @@ import { loginMethodStorageKey } from "../lib/stores/userStore.ts"; import Pharos, { pharosInstance } from "../lib/parser.ts"; import type { LayoutLoad } from "./$types"; import { get } from "svelte/store"; +import { browser } from "$app/environment"; -export const load: LayoutLoad = () => { - // Initialize NDK with new relay management system - const ndk = initNdk(); - ndkInstance.set(ndk); - +/** + * Attempts to restore the user's authentication session from localStorage. + * Handles extension, Amber (NIP-46), and npub login methods. + */ +async function restoreAuthSession() { try { const pubkey = getPersistedLogin(); const loginMethod = localStorage.getItem(loginMethodStorageKey); @@ -111,9 +112,19 @@ export const load: LayoutLoad = () => { `Failed to restore login: ${e}\n\nContinuing with anonymous session.`, ); } +} + +export const load: LayoutLoad = () => { + // Initialize NDK with new relay management system + const ndk = initNdk(); + ndkInstance.set(ndk); + + if (browser) { + restoreAuthSession(); + } const parser = new Pharos(ndk); - pharosInstance.set(parser); + pharosInstance.set(parser); return { ndk, diff --git a/src/routes/publication/+page.server.ts b/src/routes/publication/+page.server.ts index 29fc5a6..1b66af2 100644 --- a/src/routes/publication/+page.server.ts +++ b/src/routes/publication/+page.server.ts @@ -1,6 +1,22 @@ import { redirect } from "@sveltejs/kit"; import type { PageServerLoad } from "./$types"; +// Route pattern constants +const ROUTES = { + PUBLICATION_BASE: "/publication", + NADDR: "/publication/naddr", + NEVENT: "/publication/nevent", + ID: "/publication/id", + D_TAG: "/publication/d", + START: "/start", +} as const; + +// Identifier prefixes +const IDENTIFIER_PREFIXES = { + NADDR: "naddr", + NEVENT: "nevent", +} as const; + export const load: PageServerLoad = ({ url }) => { const id = url.searchParams.get("id"); const dTag = url.searchParams.get("d"); @@ -8,19 +24,18 @@ export const load: PageServerLoad = ({ url }) => { // Handle backward compatibility for old query-based routes if (id) { // Check if id is an naddr or nevent - if (id.startsWith("naddr")) { - throw redirect(301, `/publication/naddr/${id}`); - } else if (id.startsWith("nevent")) { - throw redirect(301, `/publication/nevent/${id}`); + if (id.startsWith(IDENTIFIER_PREFIXES.NADDR)) { + throw redirect(301, `${ROUTES.NADDR}/${id}`); + } else if (id.startsWith(IDENTIFIER_PREFIXES.NEVENT)) { + throw redirect(301, `${ROUTES.NEVENT}/${id}`); } else { // Assume it's a hex ID - throw redirect(301, `/publication/id/${id}`); + throw redirect(301, `${ROUTES.ID}/${id}`); } } else if (dTag) { - throw redirect(301, `/publication/d/${dTag}`); + throw redirect(301, `${ROUTES.D_TAG}/${dTag}`); } - // If no query parameters, redirect to the start page or show publication feed\ - // AI-TODO: Redirect to a "not found" page. - throw redirect(301, "/start"); + // If no query parameters, redirect to the start page + throw redirect(301, ROUTES.START); }; \ No newline at end of file diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts index a9ddd3c..1209f7b 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.server.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -1,95 +1,12 @@ import { error } from "@sveltejs/kit"; import type { LayoutServerLoad } from "./$types"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; -import { getActiveRelaySetAsNDKRelaySet } from "../../../../lib/ndk.ts"; -import { getMatchingTags } from "../../../../lib/utils/nostrUtils.ts"; -import { naddrDecode, neventDecode } from "../../../../lib/utils.ts"; -import type NDK from "@nostr-dev-kit/ndk"; - -// AI-TODO: Use `fetchEventWithFallback` from `nostrUtils.ts` to retrieve events in this file. - -/** - * Fetches an event by hex ID - */ -async function fetchEventById(ndk: NDK, id: string): Promise<NDKEvent> { - try { - const event = await ndk.fetchEvent(id); - if (!event) { - throw new Error(`Event not found for ID: ${id}`); - } - return event; - } catch (err) { - throw error(404, `Failed to fetch publication root event.\n${err}`); - } -} - -/** - * Fetches an event by d tag - */ -async function fetchEventByDTag(ndk: NDK, dTag: string): Promise<NDKEvent> { - try { - const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); - const events = await ndk.fetchEvents( - { "#d": [dTag] }, - { closeOnEose: false }, - relaySet, - ); - - if (!events || events.size === 0) { - throw new Error(`Event not found for d tag: ${dTag}`); - } - - // Choose the event with the latest created_at timestamp when multiple events share the same d tag - const sortedEvents = Array.from(events).sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); - return sortedEvents[0]; - } catch (err) { - throw error(404, `Failed to fetch publication root event.\n${err}`); - } -} - -/** - * Fetches an event by naddr identifier - */ -async function fetchEventByNaddr(ndk: NDK, naddr: string): Promise<NDKEvent> { - try { - const decoded = naddrDecode(naddr); - const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); - - const filter = { - kinds: [decoded.kind], - authors: [decoded.pubkey], - "#d": [decoded.identifier], - }; - - const event = await ndk.fetchEvent(filter, { closeOnEose: false }, relaySet); - if (!event) { - throw new Error(`Event not found for naddr: ${naddr}`); - } - return event; - } catch (err) { - throw error(404, `Failed to fetch publication root event.\n${err}`); - } -} - -/** - * Fetches an event by nevent identifier - */ -async function fetchEventByNevent(ndk: NDK, nevent: string): Promise<NDKEvent> { - try { - const decoded = neventDecode(nevent); - const event = await ndk.fetchEvent(decoded.id); - if (!event) { - throw new Error(`Event not found for nevent: ${nevent}`); - } - return event; - } catch (err) { - throw error(404, `Failed to fetch publication root event.\n${err}`); - } -} +import { getMatchingTags, fetchEventById, fetchEventByDTag, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/nostrUtils.ts"; export const load: LayoutServerLoad = async ({ params, parent, url }) => { const { type, identifier } = params; - const { ndk } = await parent(); + // deno-lint-ignore no-explicit-any + const { ndk } = (await parent()) as any; if (!ndk) { throw error(500, "NDK not available"); diff --git a/src/routes/publication/[type]/[identifier]/+page.server.ts b/src/routes/publication/[type]/[identifier]/+page.server.ts index 3c033c7..95b58fe 100644 --- a/src/routes/publication/[type]/[identifier]/+page.server.ts +++ b/src/routes/publication/[type]/[identifier]/+page.server.ts @@ -1,95 +1,12 @@ import { error } from "@sveltejs/kit"; import type { PageServerLoad } from "./$types"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; -import { getActiveRelaySetAsNDKRelaySet } from "../../../../lib/ndk.ts"; -import { getMatchingTags } from "../../../../lib/utils/nostrUtils.ts"; -import { naddrDecode, neventDecode } from "../../../../lib/utils.ts"; -import type NDK from "@nostr-dev-kit/ndk"; - -// AI-TODO: Use `fetchEventWithFallback` from `nostrUtils.ts` to retrieve events in this file. - -/** - * Fetches an event by hex ID - */ -async function fetchEventById(ndk: NDK, id: string): Promise<NDKEvent> { - try { - const event = await ndk.fetchEvent(id); - if (!event) { - throw new Error(`Event not found for ID: ${id}`); - } - return event; - } catch (err) { - throw error(404, `Failed to fetch publication root event.\n${err}`); - } -} - -/** - * Fetches an event by d tag - */ -async function fetchEventByDTag(ndk: NDK, dTag: string): Promise<NDKEvent> { - try { - const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); // true for inbox relays - const events = await ndk.fetchEvents( - { "#d": [dTag] }, - { closeOnEose: false }, - relaySet, - ); - - if (!events || events.size === 0) { - throw new Error(`Event not found for d tag: ${dTag}`); - } - - // AI-NOTE: Choose the event with the latest created_at timestamp when multiple events share the same d tag - const sortedEvents = Array.from(events).sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); - return sortedEvents[0]; - } catch (err) { - throw error(404, `Failed to fetch publication root event.\n${err}`); - } -} - -/** - * Fetches an event by naddr identifier - */ -async function fetchEventByNaddr(ndk: NDK, naddr: string): Promise<NDKEvent> { - try { - const decoded = naddrDecode(naddr); - const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); - - const filter = { - kinds: [decoded.kind], - authors: [decoded.pubkey], - "#d": [decoded.identifier], - }; - - const event = await ndk.fetchEvent(filter, { closeOnEose: false }, relaySet); - if (!event) { - throw new Error(`Event not found for naddr: ${naddr}`); - } - return event; - } catch (err) { - throw error(404, `Failed to fetch publication root event.\n${err}`); - } -} - -/** - * Fetches an event by nevent identifier - */ -async function fetchEventByNevent(ndk: NDK, nevent: string): Promise<NDKEvent> { - try { - const decoded = neventDecode(nevent); - const event = await ndk.fetchEvent(decoded.id); - if (!event) { - throw new Error(`Event not found for nevent: ${nevent}`); - } - return event; - } catch (err) { - throw error(404, `Failed to fetch publication root event.\n${err}`); - } -} +import { getMatchingTags, fetchEventById, fetchEventByDTag, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/nostrUtils.ts"; export const load: PageServerLoad = async ({ params, parent }) => { const { type, identifier } = params; - const { ndk } = await parent(); + // deno-lint-ignore no-explicit-any + const { ndk } = (await parent()) as any; if (!ndk) { throw error(500, "NDK not available"); From cccbb01e3ad758dc851379f1f2529b82f686c01e Mon Sep 17 00:00:00 2001 From: buttercat1791 <mjjurkoic@gmail.com> Date: Tue, 29 Jul 2025 16:36:25 -0500 Subject: [PATCH 12/22] Add TODOs for next steps --- src/lib/utils/nostrUtils.ts | 3 +++ src/routes/publication/[type]/[identifier]/+layout.server.ts | 2 ++ src/routes/publication/[type]/[identifier]/+layout.svelte | 1 + 3 files changed, 6 insertions(+) diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 813f1e5..5faa1bb 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -430,6 +430,9 @@ Promise.prototype.withTimeout = function <T>( return withTimeout(timeoutMs, this); }; +// TODO: Implement fetch for no-auth relays using the WebSocketPool and raw WebSockets. +// This fetch function will be used for server-side loading. + /** * Fetches an event using a two-step relay strategy: * 1. First tries standard relays with timeout diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts index 1209f7b..2c6bebe 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.server.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -5,6 +5,8 @@ import { getMatchingTags, fetchEventById, fetchEventByDTag, fetchEventByNaddr, f export const load: LayoutServerLoad = async ({ params, parent, url }) => { const { type, identifier } = params; + + // TODO: Remove the need for NDK in nostrUtils dependencies, since NDK is not available on the server. // deno-lint-ignore no-explicit-any const { ndk } = (await parent()) as any; diff --git a/src/routes/publication/[type]/[identifier]/+layout.svelte b/src/routes/publication/[type]/[identifier]/+layout.svelte index 84dbc3b..ce533f6 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.svelte +++ b/src/routes/publication/[type]/[identifier]/+layout.svelte @@ -5,6 +5,7 @@ const { metadata } = data; </script> +<!-- TODO: Provide fallback metadata values to use if the publication is on an auth-to-read relay. --> <svelte:head> <!-- Basic meta tags --> <title>{metadata.title} From bc22b21c3312859ebbd5a61ca852e2a007079648 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 30 Jul 2025 00:29:00 -0500 Subject: [PATCH 13/22] Turn SSR back on --- src/routes/+layout.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 59bc393..ac50221 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -10,11 +10,14 @@ import type { LayoutLoad } from "./$types"; import { get } from "svelte/store"; import { browser } from "$app/environment"; +// AI-NOTE: Leave SSR off until event fetches are implemented server-side. +export const ssr = false; + /** * Attempts to restore the user's authentication session from localStorage. * Handles extension, Amber (NIP-46), and npub login methods. */ -async function restoreAuthSession() { +function restoreAuthSession() { try { const pubkey = getPersistedLogin(); const loginMethod = localStorage.getItem(loginMethodStorageKey); From aa76910bc40fdd5012262d285a94c85260fa6a11 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 30 Jul 2025 01:25:39 -0500 Subject: [PATCH 14/22] Use utils based on raw WebSockets for SSR --- src/lib/utils/nostrUtils.ts | 83 ---------- src/lib/utils/websocket_utils.ts | 143 ++++++++++++++++++ .../[type]/[identifier]/+layout.server.ts | 29 ++-- .../[type]/[identifier]/+page.server.ts | 28 ++-- .../[type]/[identifier]/+page.svelte | 17 ++- 5 files changed, 174 insertions(+), 126 deletions(-) create mode 100644 src/lib/utils/websocket_utils.ts diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 5faa1bb..3d67c57 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -12,8 +12,6 @@ import { schnorr } from "@noble/curves/secp256k1"; import { bytesToHex } from "@noble/hashes/utils"; import { wellKnownUrl } from "./search_utility.ts"; import { VALIDATION } from "./search_constants.ts"; -import { error } from "@sveltejs/kit"; -import { naddrDecode, neventDecode } from "../utils.ts"; const badgeCheckSvg = ''; @@ -673,84 +671,3 @@ export function prefixNostrAddresses(content: string): string { return `nostr:${match}`; }); } - -// Added functions for fetching events by various identifiers - -/** - * Fetches an event by hex ID, throwing a SvelteKit 404 error if not found. - */ -export async function fetchEventById(ndk: NDK, id: string): Promise { - try { - const event = await fetchEventWithFallback(ndk, id); - if (!event) { - throw error(404, `Event not found for ID: ${id}`); - } - return event; - } catch (err) { - if (err && typeof err === "object" && "status" in err) { - throw err; - } - throw error(404, `Failed to fetch event by ID: ${err}`); - } -} - -/** - * Fetches an event by d tag, throwing a 404 if not found. - */ -export async function fetchEventByDTag(ndk: NDK, dTag: string): Promise { - try { - const event = await fetchEventWithFallback(ndk, { "#d": [dTag], limit: 1 }); - if (!event) { - throw error(404, `Event not found for d-tag: ${dTag}`); - } - return event; - } catch (err) { - if (err && typeof err === "object" && "status" in err) { - throw err; - } - throw error(404, `Failed to fetch event by d-tag: ${err}`); - } -} - -/** - * Fetches an event by naddr identifier. - */ -export async function fetchEventByNaddr(ndk: NDK, naddr: string): Promise { - try { - const decoded = naddrDecode(naddr); - const filter = { - kinds: [decoded.kind], - authors: [decoded.pubkey], - "#d": [decoded.identifier], - }; - const event = await fetchEventWithFallback(ndk, filter); - if (!event) { - throw error(404, `Event not found for naddr: ${naddr}`); - } - return event; - } catch (err) { - if (err && typeof err === "object" && "status" in err) { - throw err; - } - throw error(404, `Failed to fetch event by naddr: ${err}`); - } -} - -/** - * Fetches an event by nevent identifier. - */ -export async function fetchEventByNevent(ndk: NDK, nevent: string): Promise { - try { - const decoded = neventDecode(nevent); - const event = await fetchEventWithFallback(ndk, decoded.id); - if (!event) { - throw error(404, `Event not found for nevent: ${nevent}`); - } - return event; - } catch (err) { - if (err && typeof err === "object" && "status" in err) { - throw err; - } - throw error(404, `Failed to fetch event by nevent: ${err}`); - } -} diff --git a/src/lib/utils/websocket_utils.ts b/src/lib/utils/websocket_utils.ts new file mode 100644 index 0000000..9d0d382 --- /dev/null +++ b/src/lib/utils/websocket_utils.ts @@ -0,0 +1,143 @@ +import { WebSocketPool } from "../data_structures/websocket_pool.ts"; +import { error } from "@sveltejs/kit"; +import { naddrDecode, neventDecode } from "../utils.ts"; + +export interface NostrEvent { + id: string; + pubkey: string; + created_at: number; + kind: number; + tags: string[][]; + content: string; + sig: string; +} + +export interface NostrFilter { + ids?: string[]; + authors?: string[]; + kinds?: number[]; + [tag: `#${string}`]: string[] | undefined; + since?: number; + until?: number; + limit?: number; +} + +export async function fetchNostrEvent(filter: NostrFilter): Promise { + // TODO: Improve relay selection when relay management is implemented. + const ws = await WebSocketPool.instance.acquire("wss://thecitadel.nostr1.com"); + const subId = crypto.randomUUID(); + + const res = new Promise((resolve, reject) => { + ws.addEventListener("message", (ev) => { + const data = JSON.parse(ev.data); + + if (data[1] !== subId) { + return; + } + + switch (data[0]) { + case "EVENT": + break; + case "CLOSED": + reject(new Error(`[WebSocket Utils]: Subscription ${subId} closed`)); + break; + case "EOSE": + resolve(null); + break; + } + + const event = data[2] as NostrEvent; + if (!event) { + return; + } + + resolve(event); + }); + + ws.addEventListener("error", (ev) => { + reject(ev); + }); + }).withTimeout(2000); + + ws.send(JSON.stringify(["REQ", subId, filter])); + return res; +} + +/** + * Fetches an event by hex ID, throwing a SvelteKit 404 error if not found. + */ +export async function fetchEventById(id: string): Promise { + try { + const event = await fetchNostrEvent({ ids: [id], limit: 1 }); + if (!event) { + throw error(404, `Event not found for ID: ${id}`); + } + return event; + } catch (err) { + if (err && typeof err === "object" && "status" in err) { + throw err; + } + throw error(404, `Failed to fetch event by ID: ${err}`); + } +} + +/** + * Fetches an event by d tag, throwing a 404 if not found. + */ +export async function fetchEventByDTag(dTag: string): Promise { + try { + const event = await fetchNostrEvent({ "#d": [dTag], limit: 1 }); + if (!event) { + throw error(404, `Event not found for d-tag: ${dTag}`); + } + return event; + } catch (err) { + if (err && typeof err === "object" && "status" in err) { + throw err; + } + throw error(404, `Failed to fetch event by d-tag: ${err}`); + } +} + +/** + * Fetches an event by naddr identifier. + */ +export async function fetchEventByNaddr(naddr: string): Promise { + try { + const decoded = naddrDecode(naddr); + const filter = { + kinds: [decoded.kind], + authors: [decoded.pubkey], + "#d": [decoded.identifier], + }; + const event = await fetchNostrEvent(filter); + if (!event) { + throw error(404, `Event not found for naddr: ${naddr}`); + } + return event; + } catch (err) { + if (err && typeof err === "object" && "status" in err) { + throw err; + } + throw error(404, `Failed to fetch event by naddr: ${err}`); + } +} + +/** + * Fetches an event by nevent identifier. + */ +export async function fetchEventByNevent(nevent: string): Promise { + try { + const decoded = neventDecode(nevent); + const event = await fetchNostrEvent({ ids: [decoded.id], limit: 1 }); + if (!event) { + throw error(404, `Event not found for nevent: ${nevent}`); + } + return event; + } catch (err) { + if (err && typeof err === "object" && "status" in err) { + throw err; + } + throw error(404, `Failed to fetch event by nevent: ${err}`); + } +} diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts index 2c6bebe..b89da64 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.server.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -1,44 +1,35 @@ import { error } from "@sveltejs/kit"; import type { LayoutServerLoad } from "./$types"; -import type { NDKEvent } from "@nostr-dev-kit/ndk"; -import { getMatchingTags, fetchEventById, fetchEventByDTag, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/nostrUtils.ts"; +import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent, NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; -export const load: LayoutServerLoad = async ({ params, parent, url }) => { +export const load: LayoutServerLoad = async ({ params, url }) => { const { type, identifier } = params; - // TODO: Remove the need for NDK in nostrUtils dependencies, since NDK is not available on the server. - // deno-lint-ignore no-explicit-any - const { ndk } = (await parent()) as any; - - if (!ndk) { - throw error(500, "NDK not available"); - } - - let indexEvent: NDKEvent; + let indexEvent: NostrEvent; // Handle different identifier types switch (type) { case 'id': - indexEvent = await fetchEventById(ndk, identifier); + indexEvent = await fetchEventById(identifier); break; case 'd': - indexEvent = await fetchEventByDTag(ndk, identifier); + indexEvent = await fetchEventByDTag(identifier); break; case 'naddr': - indexEvent = await fetchEventByNaddr(ndk, identifier); + indexEvent = await fetchEventByNaddr(identifier); break; case 'nevent': - indexEvent = await fetchEventByNevent(ndk, identifier); + indexEvent = await fetchEventByNevent(identifier); break; default: throw error(400, `Unsupported identifier type: ${type}`); } // Extract metadata for meta tags - const title = getMatchingTags(indexEvent, "title")[0]?.[1] || "Alexandria Publication"; - const summary = getMatchingTags(indexEvent, "summary")[0]?.[1] || + const title = indexEvent.tags.find((tag) => tag[0] === "title")?.[1] || "Alexandria Publication"; + const summary = indexEvent.tags.find((tag) => tag[0] === "summary")?.[1] || "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; - const image = getMatchingTags(indexEvent, "image")[0]?.[1] || "/screenshots/old_books.jpg"; + const image = indexEvent.tags.find((tag) => tag[0] === "image")?.[1] || "/screenshots/old_books.jpg"; const currentUrl = `${url.origin}${url.pathname}`; return { diff --git a/src/routes/publication/[type]/[identifier]/+page.server.ts b/src/routes/publication/[type]/[identifier]/+page.server.ts index 95b58fe..18a5e41 100644 --- a/src/routes/publication/[type]/[identifier]/+page.server.ts +++ b/src/routes/publication/[type]/[identifier]/+page.server.ts @@ -1,42 +1,38 @@ import { error } from "@sveltejs/kit"; import type { PageServerLoad } from "./$types"; -import type { NDKEvent } from "@nostr-dev-kit/ndk"; -import { getMatchingTags, fetchEventById, fetchEventByDTag, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/nostrUtils.ts"; +import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent, NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; -export const load: PageServerLoad = async ({ params, parent }) => { +export const load: PageServerLoad = async ({ params }) => { const { type, identifier } = params; - // deno-lint-ignore no-explicit-any - const { ndk } = (await parent()) as any; - if (!ndk) { - throw error(500, "NDK not available"); - } - - let indexEvent: NDKEvent; + let indexEvent: NostrEvent | null; // Handle different identifier types switch (type) { case 'id': - indexEvent = await fetchEventById(ndk, identifier); + indexEvent = await fetchEventById(identifier); break; case 'd': - indexEvent = await fetchEventByDTag(ndk, identifier); + indexEvent = await fetchEventByDTag(identifier); break; case 'naddr': - indexEvent = await fetchEventByNaddr(ndk, identifier); + indexEvent = await fetchEventByNaddr(identifier); break; case 'nevent': - indexEvent = await fetchEventByNevent(ndk, identifier); + indexEvent = await fetchEventByNevent(identifier); break; default: throw error(400, `Unsupported identifier type: ${type}`); } - const publicationType = getMatchingTags(indexEvent, "type")[0]?.[1]; + if (!indexEvent) { + throw error(404, `Event not found for ${type}: ${identifier}`); + } + + const publicationType = indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; return { publicationType, indexEvent, - ndk, // Pass ndk to the page for the publication tree }; }; \ No newline at end of file diff --git a/src/routes/publication/[type]/[identifier]/+page.svelte b/src/routes/publication/[type]/[identifier]/+page.svelte index 07cf547..9b786a8 100644 --- a/src/routes/publication/[type]/[identifier]/+page.svelte +++ b/src/routes/publication/[type]/[identifier]/+page.svelte @@ -1,6 +1,5 @@
-
+
+ {#if expanded} -
+
-
+
+ {#if nodeTypesExpanded} -
    - - {#each Object.entries(eventCounts).sort(([a], [b]) => Number(a) - Number(b)) as [kindStr, count]} - {@const kind = Number(kindStr)} - {@const countNum = count as number} - {@const color = getEventKindColor(kind)} - {@const name = getEventKindName(kind)} - {#if countNum > 0} -
  • -
    - +
    +
      + + {#each Object.entries(eventCounts).sort(([a], [b]) => Number(a) - Number(b)) as [kindStr, count]} + {@const kind = Number(kindStr)} + {@const countNum = count as number} + {@const color = getEventKindColor(kind)} + {@const name = getEventKindName(kind)} + {#if countNum > 0} +
    • +
      + + +
      + + {kind} - {name} ({countNum}) -
    - - {kind} - {name} ({countNum}) - -
  • - {/if} - {/each} - - -
  • - - - - - {#if starMode} - Radial connections from centers to related events - {:else} - Arrows indicate relationships and sequence +
  • {/if} - - - - - {#if showPersonNodes && personAnchors.length > 0} -
  • - - - - - Authored by person - -
  • + {/each} + +
  • - - References person + + {#if starMode} + Radial connections from centers to related events + {:else} + Arrows indicate relationships and sequence + {/if}
  • - {/if} -
+ + + {#if showPersonNodes && personAnchors.length > 0} +
  • + + + + + Authored by person + +
  • +
  • + + + + + References person + +
  • + {/if} + +
    {/if}
    -
    tagControlsExpanded = !tagControlsExpanded}> +
    + {#if tagControlsExpanded} -
    +
    @@ -231,8 +253,9 @@ {#if showTagAnchors}
    - + - Count - - +
    + {#if autoDisabledTags} +
    + Note: All {tagAnchors.length} tags were auto-disabled to prevent graph overload. Click individual tags below to enable them. +
    + {/if} + + +
    +
    + Sort by: + + +
    - -
    - -
    - {#each sortedAnchors as anchor} - {@const tagId = `${anchor.type}-${anchor.label}`} - {@const isDisabled = disabledTags.has(tagId)} - - {/each} + {/each} +
    {/if}
    @@ -352,7 +362,13 @@
    -
    personVisualizerExpanded = !personVisualizerExpanded}> +
    + {#if personVisualizerExpanded} -
    +
    @@ -373,7 +389,9 @@ showPersonNodes = !showPersonNodes; onPersonSettingsChange(); }} + onkeydown={(e) => e.key === 'Enter' || e.key === ' ' ? (showPersonNodes = !showPersonNodes, onPersonSettingsChange()) : null} class="px-2 py-1 border border-gray-300 dark:border-gray-700 rounded text-xs font-medium cursor-pointer transition min-w-[3rem] hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500 {showPersonNodes ? 'bg-blue-600 text-white border-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:text-white dark:border-blue-600 dark:hover:bg-blue-700' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'}" + aria-pressed={showPersonNodes} > {showPersonNodes ? 'ON' : 'OFF'} @@ -430,37 +448,30 @@ > {#each personAnchors as person} {@const isDisabled = disabledPersons.has(person.pubkey)} - + {/each}
    - {:else if showPersonNodes} -

    - No people found in the current events. -

    {/if}
    {/if} diff --git a/src/lib/navigator/EventNetwork/Settings.svelte b/src/lib/navigator/EventNetwork/Settings.svelte index 584834b..cd4e1e8 100644 --- a/src/lib/navigator/EventNetwork/Settings.svelte +++ b/src/lib/navigator/EventNetwork/Settings.svelte @@ -42,7 +42,13 @@
    -
    +
    + {#if expanded} -
    +
    Showing {count} of {totalCount} events @@ -63,9 +69,12 @@
    -
    e.key === 'Enter' || e.key === ' ' ? toggleEventTypes() : null} + aria-expanded={eventTypesExpanded} + aria-controls="event-types-content" >

    Event Configuration @@ -77,21 +86,24 @@ {/if}

    -
    + {#if eventTypesExpanded} - +
    + +
    {/if}
    - -
    -
    e.key === 'Enter' || e.key === ' ' ? toggleVisualSettings() : null} + aria-expanded={visualSettingsExpanded} + aria-controls="visual-settings-content" >

    Visual Settings @@ -103,32 +115,31 @@ {/if}

    -
    + {#if visualSettingsExpanded} - -
    -
    - -

    - Toggle between star clusters (on) and linear sequence (off) - visualization -

    +
    +
    +
    + +

    + Toggle between star clusters (on) and linear sequence (off) + visualization +

    +
    +
    - -
    - {/if}
    diff --git a/src/lib/navigator/EventNetwork/TagTable.svelte b/src/lib/navigator/EventNetwork/TagTable.svelte index fa02295..55e603d 100644 --- a/src/lib/navigator/EventNetwork/TagTable.svelte +++ b/src/lib/navigator/EventNetwork/TagTable.svelte @@ -12,12 +12,12 @@ }>(); // Computed property for unique tags - let uniqueTags = $derived(() => { - const tagMap = new Map(); + let uniqueTags = $derived.by(() => { + const tagMap = new Map(); - events.forEach(event => { + events.forEach((event: NDKEvent) => { const tags = event.tags || []; - tags.forEach(tag => { + tags.forEach((tag: string[]) => { if (tag[0] === selectedTagType) { const tagValue = tag[1]; const count = tagMap.get(tagValue)?.count || 0; diff --git a/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts b/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts index b998703..aaafa00 100644 --- a/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts @@ -293,13 +293,15 @@ export function createPersonLinks( connectionType = 'referenced'; } - return { + const link: PersonLink = { source: anchor, target: node, isSequential: false, connectionType, }; - }).filter(Boolean); // Remove undefineds + + return link; + }).filter((link): link is PersonLink => link !== undefined); // Remove undefineds and type guard }); debug("Created person links", { linkCount: links.length }); diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts deleted file mode 100644 index 467f6e7..0000000 --- a/src/lib/stores/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './relayStore'; -export * from './displayLimits'; \ No newline at end of file diff --git a/src/lib/utils/nostr_identifiers.ts b/src/lib/utils/nostr_identifiers.ts index 246fc9b..8e789d7 100644 --- a/src/lib/utils/nostr_identifiers.ts +++ b/src/lib/utils/nostr_identifiers.ts @@ -1,9 +1,9 @@ import { VALIDATION } from './search_constants'; -import type { NostrEventId } from './nostr_identifiers'; /** * Nostr identifier types */ +export type NostrEventId = string; // 64-character hex string export type NostrCoordinate = string; // kind:pubkey:d-tag format export type NostrIdentifier = NostrEventId | NostrCoordinate; diff --git a/src/lib/utils/websocket_utils.ts b/src/lib/utils/websocket_utils.ts index 9d0d382..bad0818 100644 --- a/src/lib/utils/websocket_utils.ts +++ b/src/lib/utils/websocket_utils.ts @@ -27,7 +27,7 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise const ws = await WebSocketPool.instance.acquire("wss://thecitadel.nostr1.com"); const subId = crypto.randomUUID(); - const res = new Promise((resolve, reject) => { + const res = new Promise((resolve, reject) => { ws.addEventListener("message", (ev) => { const data = JSON.parse(ev.data); @@ -42,7 +42,7 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise reject(new Error(`[WebSocket Utils]: Subscription ${subId} closed`)); break; case "EOSE": - resolve(null); + reject(new Error(`[WebSocket Utils]: Event not found`)); break; } diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts index b89da64..f2c64dc 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.server.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -1,6 +1,7 @@ import { error } from "@sveltejs/kit"; import type { LayoutServerLoad } from "./$types"; -import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent, NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; +import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts"; +import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; export const load: LayoutServerLoad = async ({ params, url }) => { const { type, identifier } = params; diff --git a/src/routes/publication/[type]/[identifier]/+page.server.ts b/src/routes/publication/[type]/[identifier]/+page.server.ts index 18a5e41..b23adcf 100644 --- a/src/routes/publication/[type]/[identifier]/+page.server.ts +++ b/src/routes/publication/[type]/[identifier]/+page.server.ts @@ -1,6 +1,7 @@ import { error } from "@sveltejs/kit"; import type { PageServerLoad } from "./$types"; -import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent, NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; +import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts"; +import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; export const load: PageServerLoad = async ({ params }) => { const { type, identifier } = params; From 08d146b8f7aacb51be1c87b4557ace6b41dff39b Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 2 Aug 2025 00:31:35 +0200 Subject: [PATCH 18/22] fix problem with trying to call websockets from the browser --- .../[type]/[identifier]/+layout.server.ts | 35 +++------- .../[type]/[identifier]/+layout.ts | 66 ++++++++++++++++++- .../[type]/[identifier]/+page.server.ts | 35 ++-------- .../publication/[type]/[identifier]/+page.ts | 54 +++++++++++++++ 4 files changed, 135 insertions(+), 55 deletions(-) create mode 100644 src/routes/publication/[type]/[identifier]/+page.ts diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts index f2c64dc..f97639a 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.server.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -1,40 +1,23 @@ import { error } from "@sveltejs/kit"; import type { LayoutServerLoad } from "./$types"; -import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts"; -import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; export const load: LayoutServerLoad = async ({ params, url }) => { const { type, identifier } = params; - let indexEvent: NostrEvent; - - // Handle different identifier types - switch (type) { - case 'id': - indexEvent = await fetchEventById(identifier); - break; - case 'd': - indexEvent = await fetchEventByDTag(identifier); - break; - case 'naddr': - indexEvent = await fetchEventByNaddr(identifier); - break; - case 'nevent': - indexEvent = await fetchEventByNevent(identifier); - break; - default: - throw error(400, `Unsupported identifier type: ${type}`); + // Validate the identifier type for SSR + const validTypes = ['id', 'd', 'naddr', 'nevent']; + if (!validTypes.includes(type)) { + throw error(400, `Unsupported identifier type: ${type}`); } - // Extract metadata for meta tags - const title = indexEvent.tags.find((tag) => tag[0] === "title")?.[1] || "Alexandria Publication"; - const summary = indexEvent.tags.find((tag) => tag[0] === "summary")?.[1] || - "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; - const image = indexEvent.tags.find((tag) => tag[0] === "image")?.[1] || "/screenshots/old_books.jpg"; + // Provide basic metadata for SSR - actual fetching will happen on client + const title = "Alexandria Publication"; + const summary = "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; + const image = "/screenshots/old_books.jpg"; const currentUrl = `${url.origin}${url.pathname}`; return { - indexEvent, + indexEvent: null, // Will be fetched on client side metadata: { title, summary, diff --git a/src/routes/publication/[type]/[identifier]/+layout.ts b/src/routes/publication/[type]/[identifier]/+layout.ts index 77ab0a0..0830e1a 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.ts @@ -1 +1,65 @@ -export const ssr = true; +import { error } from "@sveltejs/kit"; +import type { LayoutLoad } from "./$types"; +import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts"; +import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; +import { browser } from "$app/environment"; + +export const load: LayoutLoad = async ({ params, url }) => { + const { type, identifier } = params; + + // Only fetch on the client side where WebSocket is available + if (!browser) { + // Return basic metadata for SSR + return { + indexEvent: null, + metadata: { + title: "Alexandria Publication", + summary: "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.", + image: "/screenshots/old_books.jpg", + currentUrl: `${url.origin}${url.pathname}`, + }, + }; + } + + let indexEvent: NostrEvent; + + try { + // Handle different identifier types + switch (type) { + case 'id': + indexEvent = await fetchEventById(identifier); + break; + case 'd': + indexEvent = await fetchEventByDTag(identifier); + break; + case 'naddr': + indexEvent = await fetchEventByNaddr(identifier); + break; + case 'nevent': + indexEvent = await fetchEventByNevent(identifier); + break; + default: + throw error(400, `Unsupported identifier type: ${type}`); + } + + // Extract metadata for meta tags + const title = indexEvent.tags.find((tag) => tag[0] === "title")?.[1] || "Alexandria Publication"; + const summary = indexEvent.tags.find((tag) => tag[0] === "summary")?.[1] || + "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; + const image = indexEvent.tags.find((tag) => tag[0] === "image")?.[1] || "/screenshots/old_books.jpg"; + const currentUrl = `${url.origin}${url.pathname}`; + + return { + indexEvent, + metadata: { + title, + summary, + image, + currentUrl, + }, + }; + } catch (err) { + console.error('Failed to fetch publication:', err); + throw error(404, `Failed to load publication: ${err}`); + } +}; diff --git a/src/routes/publication/[type]/[identifier]/+page.server.ts b/src/routes/publication/[type]/[identifier]/+page.server.ts index b23adcf..5695e77 100644 --- a/src/routes/publication/[type]/[identifier]/+page.server.ts +++ b/src/routes/publication/[type]/[identifier]/+page.server.ts @@ -1,39 +1,18 @@ import { error } from "@sveltejs/kit"; import type { PageServerLoad } from "./$types"; -import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts"; -import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; export const load: PageServerLoad = async ({ params }) => { const { type, identifier } = params; - let indexEvent: NostrEvent | null; - - // Handle different identifier types - switch (type) { - case 'id': - indexEvent = await fetchEventById(identifier); - break; - case 'd': - indexEvent = await fetchEventByDTag(identifier); - break; - case 'naddr': - indexEvent = await fetchEventByNaddr(identifier); - break; - case 'nevent': - indexEvent = await fetchEventByNevent(identifier); - break; - default: - throw error(400, `Unsupported identifier type: ${type}`); - } - - if (!indexEvent) { - throw error(404, `Event not found for ${type}: ${identifier}`); + // Validate the identifier type for SSR + const validTypes = ['id', 'd', 'naddr', 'nevent']; + if (!validTypes.includes(type)) { + throw error(400, `Unsupported identifier type: ${type}`); } - const publicationType = indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; - + // Provide basic data for SSR - actual fetching will happen on client return { - publicationType, - indexEvent, + publicationType: "", // Will be determined on client side + indexEvent: null, // Will be fetched on client side }; }; \ No newline at end of file diff --git a/src/routes/publication/[type]/[identifier]/+page.ts b/src/routes/publication/[type]/[identifier]/+page.ts new file mode 100644 index 0000000..6de9d27 --- /dev/null +++ b/src/routes/publication/[type]/[identifier]/+page.ts @@ -0,0 +1,54 @@ +import { error } from "@sveltejs/kit"; +import type { PageLoad } from "./$types"; +import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts"; +import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; +import { browser } from "$app/environment"; + +export const load: PageLoad = async ({ params }) => { + const { type, identifier } = params; + + // Only fetch on the client side where WebSocket is available + if (!browser) { + // Return basic data for SSR + return { + publicationType: "", + indexEvent: null, + }; + } + + let indexEvent: NostrEvent; + + try { + // Handle different identifier types + switch (type) { + case 'id': + indexEvent = await fetchEventById(identifier); + break; + case 'd': + indexEvent = await fetchEventByDTag(identifier); + break; + case 'naddr': + indexEvent = await fetchEventByNaddr(identifier); + break; + case 'nevent': + indexEvent = await fetchEventByNevent(identifier); + break; + default: + throw error(400, `Unsupported identifier type: ${type}`); + } + + if (!indexEvent) { + throw error(404, `Event not found for ${type}: ${identifier}`); + } + + const publicationType = indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; + + return { + publicationType, + indexEvent, + }; + } catch (err) { + console.error('Failed to fetch publication:', err); + throw error(404, `Failed to load publication: ${err}`); + } +}; \ No newline at end of file From 2c0c98190f7d39daac5c93ec68b85ab8e18e8a34 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 2 Aug 2025 01:02:13 +0200 Subject: [PATCH 19/22] fixed the prox issue --- src/routes/proxy+layout.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/routes/proxy+layout.ts diff --git a/src/routes/proxy+layout.ts b/src/routes/proxy+layout.ts new file mode 100644 index 0000000..8a97a72 --- /dev/null +++ b/src/routes/proxy+layout.ts @@ -0,0 +1,5 @@ +import type { LayoutLoad } from "./$types"; + +export const load: LayoutLoad = async () => { + return {}; +}; \ No newline at end of file From 8b6db819dcebceecdc0d9b0f7f14b324f943af74 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 2 Aug 2025 01:15:32 +0200 Subject: [PATCH 20/22] Fixed relay display on About page --- src/lib/ndk.ts | 7 +- .../[type]/[identifier]/+page.svelte | 86 +++++++++++-------- 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 17dbf69..70592ba 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -6,7 +6,7 @@ import NDK, { NDKUser, NDKEvent, } from "@nostr-dev-kit/ndk"; -import { get, writable, type Writable } from "svelte/store"; +import { writable, get, type Writable } from "svelte/store"; import { loginStorageKey, } from "./consts.ts"; @@ -33,6 +33,11 @@ export const outboxRelays = writable([]); export const activeInboxRelays = writable([]); export const activeOutboxRelays = writable([]); +// Subscribe to userStore changes and update ndkSignedIn accordingly +userStore.subscribe((userState) => { + ndkSignedIn.set(userState.signedIn); +}); + /** * Custom authentication policy that handles NIP-42 authentication manually * when the default NDK authentication fails diff --git a/src/routes/publication/[type]/[identifier]/+page.svelte b/src/routes/publication/[type]/[identifier]/+page.svelte index 9b786a8..11fd1f8 100644 --- a/src/routes/publication/[type]/[identifier]/+page.svelte +++ b/src/routes/publication/[type]/[identifier]/+page.svelte @@ -12,41 +12,51 @@ let { data }: PageProps = $props(); - const indexEvent = createNDKEvent(data.ndk, data.indexEvent); - const publicationTree = new SveltePublicationTree(indexEvent, data.ndk); - const toc = new TableOfContents( + // data.indexEvent can be null from server-side rendering + // We need to handle this case properly + const indexEvent = data.indexEvent ? createNDKEvent(data.ndk, data.indexEvent) : null; + + // Only create publication tree if we have a valid index event + const publicationTree = indexEvent ? new SveltePublicationTree(indexEvent, data.ndk) : null; + const toc = indexEvent ? new TableOfContents( indexEvent.tagAddress(), - publicationTree, + publicationTree!, page.url.pathname ?? "", - ); + ) : null; setContext("publicationTree", publicationTree); setContext("toc", toc); setContext("asciidoctor", Processor()); - publicationTree.onBookmarkMoved((address) => { - goto(`#${address}`, { - replaceState: true, - }); - - // TODO: Extract IndexedDB interaction to a service layer. - // Store bookmark in IndexedDB - const db = indexedDB.open("alexandria", 1); - db.onupgradeneeded = () => { - const objectStore = db.result.createObjectStore("bookmarks", { - keyPath: "key", + // Only set up bookmark handling if we have a valid publication tree + if (publicationTree && indexEvent) { + publicationTree.onBookmarkMoved((address) => { + goto(`#${address}`, { + replaceState: true, }); - }; - db.onsuccess = () => { - const transaction = db.result.transaction(["bookmarks"], "readwrite"); - const store = transaction.objectStore("bookmarks"); - const bookmarkKey = `${indexEvent.tagAddress()}`; - store.put({ key: bookmarkKey, address }); - }; - }); + // TODO: Extract IndexedDB interaction to a service layer. + // Store bookmark in IndexedDB + const db = indexedDB.open("alexandria", 1); + db.onupgradeneeded = () => { + const objectStore = db.result.createObjectStore("bookmarks", { + keyPath: "key", + }); + }; + + db.onsuccess = () => { + const transaction = db.result.transaction(["bookmarks"], "readwrite"); + const store = transaction.objectStore("bookmarks"); + const bookmarkKey = `${indexEvent.tagAddress()}`; + store.put({ key: bookmarkKey, address }); + }; + }); + } onMount(() => { + // Only handle bookmarks if we have valid components + if (!publicationTree || !indexEvent) return; + // TODO: Extract IndexedDB interaction to a service layer. // Read bookmark from IndexedDB const db = indexedDB.open("alexandria", 1); @@ -81,16 +91,24 @@ }); - - -
    - -
    \ No newline at end of file + +
    + +
    +{:else} +
    +
    +

    Loading publication...

    +
    +
    +{/if} \ No newline at end of file From c00d9b8fec464bf4fabc11612149419761b1fcb2 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 2 Aug 2025 01:21:02 +0200 Subject: [PATCH 21/22] fixed events page width on narrow screens --- src/routes/events/+page.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index ae93f34..15c469c 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -354,9 +354,9 @@
    -
    +
    -
    +
    Events @@ -775,7 +775,7 @@ {#if showSidePanel && event} -
    +
    Event Details