diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 0a2593b..931cf9d 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -551,6 +551,11 @@ } function getSkeletonIds(): string[] { + // Only access window on client-side + if (typeof window === 'undefined') { + return ['skeleton-0', 'skeleton-1', 'skeleton-2']; // Default fallback for SSR + } + const skeletonHeight = 192; // The height of the card component in pixels (h-48 = 12rem = 192px). const skeletonCount = Math.floor(window.innerHeight / skeletonHeight) - 2; const skeletonIds = []; diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 92d2430..74ed95a 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -44,6 +44,9 @@ const RELAY_SET_STORAGE_KEY = 'alexandria/relay_set_cache'; * Load persistent relay set from localStorage */ function loadPersistentRelaySet(): { relaySet: { inboxRelays: string[]; outboxRelays: string[] } | null; lastUpdated: number } { + // Only load from localStorage on client-side + if (typeof window === 'undefined') return { relaySet: null, lastUpdated: 0 }; + try { const stored = localStorage.getItem(RELAY_SET_STORAGE_KEY); if (!stored) return { relaySet: null, lastUpdated: 0 }; @@ -69,6 +72,9 @@ function loadPersistentRelaySet(): { relaySet: { inboxRelays: string[]; outboxRe * Save persistent relay set to localStorage */ function savePersistentRelaySet(relaySet: { inboxRelays: string[]; outboxRelays: string[] }): void { + // Only save to localStorage on client-side + if (typeof window === 'undefined') return; + try { const data = { relaySet, @@ -84,6 +90,9 @@ function savePersistentRelaySet(relaySet: { inboxRelays: string[]; outboxRelays: * Clear persistent relay set from localStorage */ function clearPersistentRelaySet(): void { + // Only clear from localStorage on client-side + if (typeof window === 'undefined') return; + try { localStorage.removeItem(RELAY_SET_STORAGE_KEY); } catch (error) { @@ -281,6 +290,9 @@ export function checkWebSocketSupport(): void { * sessions. */ export function getPersistedLogin(): string | null { + // Only access localStorage on client-side + if (typeof window === 'undefined') return null; + const pubkey = localStorage.getItem(loginStorageKey); return pubkey; } @@ -292,6 +304,9 @@ export function getPersistedLogin(): string | null { * time. */ export function persistLogin(user: NDKUser): void { + // Only access localStorage on client-side + if (typeof window === 'undefined') return; + localStorage.setItem(loginStorageKey, user.pubkey); } @@ -300,6 +315,9 @@ export function persistLogin(user: NDKUser): void { * @remarks Use this function when the user logs out. */ export function clearLogin(): void { + // Only access localStorage on client-side + if (typeof window === 'undefined') return; + localStorage.removeItem(loginStorageKey); } @@ -314,6 +332,9 @@ function getRelayStorageKey(user: NDKUser, type: "inbox" | "outbox"): string { } export function clearPersistedRelays(user: NDKUser): void { + // Only access localStorage on client-side + if (typeof window === 'undefined') return; + localStorage.removeItem(getRelayStorageKey(user, "inbox")); localStorage.removeItem(getRelayStorageKey(user, "outbox")); } @@ -529,8 +550,8 @@ export function clearRelaySetCache(): void { console.debug('[NDK.ts] Clearing relay set cache'); persistentRelaySet = null; relaySetLastUpdated = 0; - // Clear from localStorage as well - if (typeof localStorage !== 'undefined') { + // Clear from localStorage as well (client-side only) + if (typeof window !== 'undefined') { localStorage.removeItem('alexandria/relay_set_cache'); } } @@ -613,6 +634,12 @@ export function initNdk(): NDK { const maxRetries = 1; // Reduce to 1 retry const attemptConnection = async () => { + // Only attempt connection on client-side + if (typeof window === 'undefined') { + console.debug("[NDK.ts] Skipping NDK connection during SSR"); + return; + } + try { await ndk.connect(); console.debug("[NDK.ts] NDK connected successfully"); @@ -641,7 +668,10 @@ export function initNdk(): NDK { } }; - attemptConnection(); + // Only attempt connection on client-side + if (typeof window !== 'undefined') { + attemptConnection(); + } // AI-NOTE: Set up userStore subscription after NDK initialization to prevent initialization errors userStore.subscribe(async (userState) => { diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index 6189158..0b96dd0 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -45,6 +45,9 @@ function persistRelays( inboxes: Set, outboxes: Set, ): void { + // Only access localStorage on client-side + if (typeof window === 'undefined') return; + localStorage.setItem( getRelayStorageKey(user, "inbox"), JSON.stringify(Array.from(inboxes).map((relay) => relay.url)), @@ -56,6 +59,11 @@ function persistRelays( } function getPersistedRelays(user: NDKUser): [Set, Set] { + // Only access localStorage on client-side + if (typeof window === 'undefined') { + return [new Set(), new Set()]; + } + const inboxes = new Set( JSON.parse(localStorage.getItem(getRelayStorageKey(user, "inbox")) ?? "[]"), ); @@ -135,6 +143,9 @@ async function getUserPreferredRelays( export const loginMethodStorageKey = "alexandria/login/method"; function persistLogin(user: NDKUser, method: "extension" | "amber" | "npub") { + // Only access localStorage on client-side + if (typeof window === 'undefined') return; + localStorage.setItem(loginStorageKey, user.pubkey); localStorage.setItem(loginMethodStorageKey, method); } @@ -212,7 +223,10 @@ export async function loginWithExtension() { } clearLogin(); - localStorage.removeItem("alexandria/logout/flag"); + // Only access localStorage on client-side + if (typeof window !== 'undefined') { + localStorage.removeItem("alexandria/logout/flag"); + } persistLogin(user, "extension"); } @@ -279,7 +293,10 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { } clearLogin(); - localStorage.removeItem("alexandria/logout/flag"); + // Only access localStorage on client-side + if (typeof window !== 'undefined') { + localStorage.removeItem("alexandria/logout/flag"); + } persistLogin(user, "amber"); } @@ -363,7 +380,10 @@ export async function loginWithNpub(pubkeyOrNpub: string) { userPubkey.set(user.pubkey); clearLogin(); - localStorage.removeItem("alexandria/logout/flag"); + // Only access localStorage on client-side + if (typeof window !== 'undefined') { + localStorage.removeItem("alexandria/logout/flag"); + } persistLogin(user, "npub"); } @@ -373,47 +393,51 @@ export async function loginWithNpub(pubkeyOrNpub: string) { export function logoutUser() { console.log("Logging out user..."); const currentUser = get(userStore); - if (currentUser.ndkUser) { - // Clear persisted relays for the user - localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "inbox")); - localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "outbox")); - } + + // Only access localStorage on client-side + if (typeof window !== 'undefined') { + if (currentUser.ndkUser) { + // Clear persisted relays for the user + localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "inbox")); + localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "outbox")); + } - // Clear all possible login states from localStorage - clearLogin(); + // Clear all possible login states from localStorage + clearLogin(); - // Also clear any other potential login keys that might exist - const keysToRemove = []; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if ( - key && - (key.includes("login") || - key.includes("nostr") || - key.includes("user") || - key.includes("alexandria") || - key === "pubkey") - ) { - keysToRemove.push(key); + // Also clear any other potential login keys that might exist + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if ( + key && + (key.includes("login") || + key.includes("nostr") || + key.includes("user") || + key.includes("alexandria") || + key === "pubkey") + ) { + keysToRemove.push(key); + } } - } - // Specifically target the login storage key - keysToRemove.push("alexandria/login/pubkey"); - keysToRemove.push("alexandria/login/method"); + // Specifically target the login storage key + keysToRemove.push("alexandria/login/pubkey"); + keysToRemove.push("alexandria/login/method"); - keysToRemove.forEach((key) => { - console.log("Removing localStorage key:", key); - localStorage.removeItem(key); - }); + keysToRemove.forEach((key) => { + console.log("Removing localStorage key:", key); + localStorage.removeItem(key); + }); - // Clear Amber-specific flags - localStorage.removeItem("alexandria/amber/fallback"); + // Clear Amber-specific flags + localStorage.removeItem("alexandria/amber/fallback"); - // Set a flag to prevent auto-login on next page load - localStorage.setItem("alexandria/logout/flag", "true"); + // Set a flag to prevent auto-login on next page load + localStorage.setItem("alexandria/logout/flag", "true"); - console.log("Cleared all login data from localStorage"); + console.log("Cleared all login data from localStorage"); + } userStore.set({ pubkey: null, diff --git a/src/lib/utils/markup/asciidoctorPostProcessor.ts b/src/lib/utils/markup/asciidoctorPostProcessor.ts index f2d9318..82a1dc7 100644 --- a/src/lib/utils/markup/asciidoctorPostProcessor.ts +++ b/src/lib/utils/markup/asciidoctorPostProcessor.ts @@ -26,8 +26,8 @@ function replaceWikilinks(html: string): string { const display = (label || target).trim(); const url = `/events?d=${normalized}`; // Output as a clickable with the [[display]] format and matching link colors - // Use onclick to bypass SvelteKit routing and navigate directly - return `${display}`; + // Remove onclick handler to avoid breaking amber session - will be handled by global click handler + return `${display}`; }, ); } @@ -39,8 +39,8 @@ function replaceAsciiDocAnchors(html: string): string { return html.replace(/<\/a>/g, (_match, id) => { const normalized = normalizeDTag(id.trim()); const url = `/events?d=${normalized}`; - // Use onclick to bypass SvelteKit routing and navigate directly - return `${id}`; + // Remove onclick handler to avoid breaking amber session - will be handled by global click handler + return `${id}`; }); } diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index d4b35bd..10ebbe7 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -149,8 +149,8 @@ function replaceWikilinks(text: string): string { const display = (label || target).trim(); const url = `/events?d=${normalized}`; // Output as a clickable with the [[display]] format and matching link colors - // Use onclick to bypass SvelteKit routing and navigate directly - return `${display}`; + // Remove onclick handler to avoid breaking amber session - will be handled by global click handler + return `${display}`; }, ); } diff --git a/src/lib/utils/relay_management.ts b/src/lib/utils/relay_management.ts index c9c10a0..c75a801 100644 --- a/src/lib/utils/relay_management.ts +++ b/src/lib/utils/relay_management.ts @@ -57,6 +57,16 @@ export function testLocalRelayConnection( error?: string; actualUrl?: string; }> { + // Only test connections on client-side + if (typeof window === 'undefined') { + return Promise.resolve({ + connected: false, + requiresAuth: false, + error: "Server-side rendering - connection test skipped", + actualUrl: relayUrl, + }); + } + return new Promise((resolve) => { try { // Ensure the URL is using ws:// protocol for local relays @@ -182,6 +192,16 @@ export function testRemoteRelayConnection( error?: string; actualUrl?: string; }> { + // Only test connections on client-side + if (typeof window === 'undefined') { + return Promise.resolve({ + connected: false, + requiresAuth: false, + error: "Server-side rendering - connection test skipped", + actualUrl: relayUrl, + }); + } + return new Promise((resolve) => { // Ensure the URL is using wss:// protocol for remote relays const secureUrl = relayUrl.replace(/^ws:\/\//, "wss://"); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 3aae73f..dd7f835 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,8 +1,9 @@ - diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index ac50221..a1253a9 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -10,14 +10,18 @@ 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; +// AI-NOTE: SSR enabled for better SEO and OpenGraph support +export const ssr = true; /** * Attempts to restore the user's authentication session from localStorage. * Handles extension, Amber (NIP-46), and npub login methods. + * Only runs on client-side. */ function restoreAuthSession() { + // Only run on client-side + if (!browser) return; + try { const pubkey = getPersistedLogin(); const loginMethod = localStorage.getItem(loginMethodStorageKey); @@ -122,6 +126,7 @@ export const load: LayoutLoad = () => { const ndk = initNdk(); ndkInstance.set(ndk); + // Only restore auth session on client-side if (browser) { restoreAuthSession(); }