8 changed files with 9 additions and 351 deletions
@ -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. |
|
||||||
@ -1,134 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import Publication from "$lib/components/publications/Publication.svelte"; |
|
||||||
import { TextPlaceholder } from "flowbite-svelte"; |
|
||||||
import type { PageProps } from "./$types"; |
|
||||||
import { onDestroy, onMount, setContext } from "svelte"; |
|
||||||
import Processor from "asciidoctor"; |
|
||||||
import ArticleNav from "$components/util/ArticleNav.svelte"; |
|
||||||
import { SveltePublicationTree } from "$lib/components/publications/svelte_publication_tree.svelte"; |
|
||||||
import { TableOfContents } from "$lib/components/publications/table_of_contents.svelte"; |
|
||||||
import { page } from "$app/state"; |
|
||||||
import { goto } from "$app/navigation"; |
|
||||||
|
|
||||||
let { data }: PageProps = $props(); |
|
||||||
|
|
||||||
const publicationTree = new SveltePublicationTree(data.indexEvent, data.ndk); |
|
||||||
const toc = new TableOfContents( |
|
||||||
data.indexEvent.tagAddress(), |
|
||||||
publicationTree, |
|
||||||
page.url.pathname ?? "", |
|
||||||
); |
|
||||||
|
|
||||||
setContext("publicationTree", publicationTree); |
|
||||||
setContext("toc", toc); |
|
||||||
setContext("asciidoctor", Processor()); |
|
||||||
|
|
||||||
// Get publication metadata for OpenGraph tags |
|
||||||
let title = $derived( |
|
||||||
data.indexEvent?.getMatchingTags("title")[0]?.[1] || |
|
||||||
data.parser?.getIndexTitle(data.parser?.getRootIndexId()) || |
|
||||||
"Alexandria Publication", |
|
||||||
); |
|
||||||
let currentUrl = $derived( |
|
||||||
`${page.url.origin}${page.url.pathname}${page.url.search}`, |
|
||||||
); |
|
||||||
|
|
||||||
// Get image and summary from the event tags if available |
|
||||||
// If image unavailable, use the Alexandria default pic. |
|
||||||
let image = $derived( |
|
||||||
data.indexEvent?.getMatchingTags("image")[0]?.[1] || |
|
||||||
"/screenshots/old_books.jpg", |
|
||||||
); |
|
||||||
let summary = $derived( |
|
||||||
data.indexEvent?.getMatchingTags("summary")[0]?.[1] || |
|
||||||
"Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.", |
|
||||||
); |
|
||||||
|
|
||||||
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", |
|
||||||
}); |
|
||||||
}; |
|
||||||
|
|
||||||
db.onsuccess = () => { |
|
||||||
const transaction = db.result.transaction(["bookmarks"], "readwrite"); |
|
||||||
const store = transaction.objectStore("bookmarks"); |
|
||||||
const bookmarkKey = `${data.indexEvent.tagAddress()}`; |
|
||||||
store.put({ key: bookmarkKey, address }); |
|
||||||
}; |
|
||||||
}); |
|
||||||
|
|
||||||
onMount(() => { |
|
||||||
// TODO: Extract IndexedDB interaction to a service layer. |
|
||||||
// Read bookmark from 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"], "readonly"); |
|
||||||
const store = transaction.objectStore("bookmarks"); |
|
||||||
const bookmarkKey = `${data.indexEvent.tagAddress()}`; |
|
||||||
const request = store.get(bookmarkKey); |
|
||||||
|
|
||||||
request.onsuccess = () => { |
|
||||||
if (request.result?.address) { |
|
||||||
// Set the bookmark in the publication tree |
|
||||||
publicationTree.setBookmark(request.result.address); |
|
||||||
|
|
||||||
// Jump to the bookmarked element |
|
||||||
goto(`#${request.result.address}`, { |
|
||||||
replaceState: true, |
|
||||||
}); |
|
||||||
} |
|
||||||
}; |
|
||||||
}; |
|
||||||
}); |
|
||||||
|
|
||||||
onDestroy(() => data.parser.reset()); |
|
||||||
</script> |
|
||||||
|
|
||||||
<svelte:head> |
|
||||||
<!-- Basic meta tags --> |
|
||||||
<title>{title}</title> |
|
||||||
<meta name="description" content={summary} /> |
|
||||||
|
|
||||||
<!-- OpenGraph meta tags --> |
|
||||||
<meta property="og:title" content={title} /> |
|
||||||
<meta property="og:description" content={summary} /> |
|
||||||
<meta property="og:url" content={currentUrl} /> |
|
||||||
<meta property="og:type" content="article" /> |
|
||||||
<meta property="og:site_name" content="Alexandria" /> |
|
||||||
<meta property="og:image" content={image} /> |
|
||||||
|
|
||||||
<!-- Twitter Card meta tags --> |
|
||||||
<meta name="twitter:card" content="summary_large_image" /> |
|
||||||
<meta name="twitter:title" content={title} /> |
|
||||||
<meta name="twitter:description" content={summary} /> |
|
||||||
<meta name="twitter:image" content={image} /> |
|
||||||
</svelte:head> |
|
||||||
|
|
||||||
<ArticleNav |
|
||||||
publicationType={data.publicationType} |
|
||||||
rootId={data.parser.getRootIndexId()} |
|
||||||
indexEvent={data.indexEvent} |
|
||||||
/> |
|
||||||
|
|
||||||
<main class="publication {data.publicationType}"> |
|
||||||
<Publication |
|
||||||
rootAddress={data.indexEvent.tagAddress()} |
|
||||||
publicationType={data.publicationType} |
|
||||||
indexEvent={data.indexEvent} |
|
||||||
/> |
|
||||||
</main> |
|
||||||
@ -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<NDKEvent> { |
|
||||||
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<NDKEvent> { |
|
||||||
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<Partial<Record<string, NDK>>>; |
|
||||||
}) => { |
|
||||||
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, |
|
||||||
}; |
|
||||||
}; |
|
||||||
@ -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 `<svelte:head>` block. |
|
||||||
- Inside `<svelte:head>`, render `<title>` 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. |
|
||||||
Loading…
Reference in new issue