Browse Source

allow for differentiation: ssr versus client rendering

master
silberengel 7 months ago
parent
commit
f199f356a8
  1. 5
      src/lib/components/publications/PublicationFeed.svelte
  2. 34
      src/lib/ndk.ts
  3. 24
      src/lib/stores/userStore.ts
  4. 8
      src/lib/utils/markup/asciidoctorPostProcessor.ts
  5. 4
      src/lib/utils/markup/basicMarkupParser.ts
  6. 20
      src/lib/utils/relay_management.ts
  7. 46
      src/routes/+layout.svelte
  8. 9
      src/routes/+layout.ts

5
src/lib/components/publications/PublicationFeed.svelte

@ -551,6 +551,11 @@
} }
function getSkeletonIds(): string[] { 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 skeletonHeight = 192; // The height of the card component in pixels (h-48 = 12rem = 192px).
const skeletonCount = Math.floor(window.innerHeight / skeletonHeight) - 2; const skeletonCount = Math.floor(window.innerHeight / skeletonHeight) - 2;
const skeletonIds = []; const skeletonIds = [];

34
src/lib/ndk.ts

@ -44,6 +44,9 @@ const RELAY_SET_STORAGE_KEY = 'alexandria/relay_set_cache';
* Load persistent relay set from localStorage * Load persistent relay set from localStorage
*/ */
function loadPersistentRelaySet(): { relaySet: { inboxRelays: string[]; outboxRelays: string[] } | null; lastUpdated: number } { 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 { try {
const stored = localStorage.getItem(RELAY_SET_STORAGE_KEY); const stored = localStorage.getItem(RELAY_SET_STORAGE_KEY);
if (!stored) return { relaySet: null, lastUpdated: 0 }; if (!stored) return { relaySet: null, lastUpdated: 0 };
@ -69,6 +72,9 @@ function loadPersistentRelaySet(): { relaySet: { inboxRelays: string[]; outboxRe
* Save persistent relay set to localStorage * Save persistent relay set to localStorage
*/ */
function savePersistentRelaySet(relaySet: { inboxRelays: string[]; outboxRelays: string[] }): void { function savePersistentRelaySet(relaySet: { inboxRelays: string[]; outboxRelays: string[] }): void {
// Only save to localStorage on client-side
if (typeof window === 'undefined') return;
try { try {
const data = { const data = {
relaySet, relaySet,
@ -84,6 +90,9 @@ function savePersistentRelaySet(relaySet: { inboxRelays: string[]; outboxRelays:
* Clear persistent relay set from localStorage * Clear persistent relay set from localStorage
*/ */
function clearPersistentRelaySet(): void { function clearPersistentRelaySet(): void {
// Only clear from localStorage on client-side
if (typeof window === 'undefined') return;
try { try {
localStorage.removeItem(RELAY_SET_STORAGE_KEY); localStorage.removeItem(RELAY_SET_STORAGE_KEY);
} catch (error) { } catch (error) {
@ -281,6 +290,9 @@ export function checkWebSocketSupport(): void {
* sessions. * sessions.
*/ */
export function getPersistedLogin(): string | null { export function getPersistedLogin(): string | null {
// Only access localStorage on client-side
if (typeof window === 'undefined') return null;
const pubkey = localStorage.getItem(loginStorageKey); const pubkey = localStorage.getItem(loginStorageKey);
return pubkey; return pubkey;
} }
@ -292,6 +304,9 @@ export function getPersistedLogin(): string | null {
* time. * time.
*/ */
export function persistLogin(user: NDKUser): void { export function persistLogin(user: NDKUser): void {
// Only access localStorage on client-side
if (typeof window === 'undefined') return;
localStorage.setItem(loginStorageKey, user.pubkey); localStorage.setItem(loginStorageKey, user.pubkey);
} }
@ -300,6 +315,9 @@ export function persistLogin(user: NDKUser): void {
* @remarks Use this function when the user logs out. * @remarks Use this function when the user logs out.
*/ */
export function clearLogin(): void { export function clearLogin(): void {
// Only access localStorage on client-side
if (typeof window === 'undefined') return;
localStorage.removeItem(loginStorageKey); localStorage.removeItem(loginStorageKey);
} }
@ -314,6 +332,9 @@ function getRelayStorageKey(user: NDKUser, type: "inbox" | "outbox"): string {
} }
export function clearPersistedRelays(user: NDKUser): void { 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, "inbox"));
localStorage.removeItem(getRelayStorageKey(user, "outbox")); localStorage.removeItem(getRelayStorageKey(user, "outbox"));
} }
@ -529,8 +550,8 @@ export function clearRelaySetCache(): void {
console.debug('[NDK.ts] Clearing relay set cache'); console.debug('[NDK.ts] Clearing relay set cache');
persistentRelaySet = null; persistentRelaySet = null;
relaySetLastUpdated = 0; relaySetLastUpdated = 0;
// Clear from localStorage as well // Clear from localStorage as well (client-side only)
if (typeof localStorage !== 'undefined') { if (typeof window !== 'undefined') {
localStorage.removeItem('alexandria/relay_set_cache'); localStorage.removeItem('alexandria/relay_set_cache');
} }
} }
@ -613,6 +634,12 @@ export function initNdk(): NDK {
const maxRetries = 1; // Reduce to 1 retry const maxRetries = 1; // Reduce to 1 retry
const attemptConnection = async () => { const attemptConnection = async () => {
// Only attempt connection on client-side
if (typeof window === 'undefined') {
console.debug("[NDK.ts] Skipping NDK connection during SSR");
return;
}
try { try {
await ndk.connect(); await ndk.connect();
console.debug("[NDK.ts] NDK connected successfully"); console.debug("[NDK.ts] NDK connected successfully");
@ -641,7 +668,10 @@ export function initNdk(): NDK {
} }
}; };
// Only attempt connection on client-side
if (typeof window !== 'undefined') {
attemptConnection(); attemptConnection();
}
// AI-NOTE: Set up userStore subscription after NDK initialization to prevent initialization errors // AI-NOTE: Set up userStore subscription after NDK initialization to prevent initialization errors
userStore.subscribe(async (userState) => { userStore.subscribe(async (userState) => {

24
src/lib/stores/userStore.ts

@ -45,6 +45,9 @@ function persistRelays(
inboxes: Set<NDKRelay>, inboxes: Set<NDKRelay>,
outboxes: Set<NDKRelay>, outboxes: Set<NDKRelay>,
): void { ): void {
// Only access localStorage on client-side
if (typeof window === 'undefined') return;
localStorage.setItem( localStorage.setItem(
getRelayStorageKey(user, "inbox"), getRelayStorageKey(user, "inbox"),
JSON.stringify(Array.from(inboxes).map((relay) => relay.url)), JSON.stringify(Array.from(inboxes).map((relay) => relay.url)),
@ -56,6 +59,11 @@ function persistRelays(
} }
function getPersistedRelays(user: NDKUser): [Set<string>, Set<string>] { function getPersistedRelays(user: NDKUser): [Set<string>, Set<string>] {
// Only access localStorage on client-side
if (typeof window === 'undefined') {
return [new Set<string>(), new Set<string>()];
}
const inboxes = new Set<string>( const inboxes = new Set<string>(
JSON.parse(localStorage.getItem(getRelayStorageKey(user, "inbox")) ?? "[]"), JSON.parse(localStorage.getItem(getRelayStorageKey(user, "inbox")) ?? "[]"),
); );
@ -135,6 +143,9 @@ async function getUserPreferredRelays(
export const loginMethodStorageKey = "alexandria/login/method"; export const loginMethodStorageKey = "alexandria/login/method";
function persistLogin(user: NDKUser, method: "extension" | "amber" | "npub") { 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(loginStorageKey, user.pubkey);
localStorage.setItem(loginMethodStorageKey, method); localStorage.setItem(loginMethodStorageKey, method);
} }
@ -212,7 +223,10 @@ export async function loginWithExtension() {
} }
clearLogin(); clearLogin();
// Only access localStorage on client-side
if (typeof window !== 'undefined') {
localStorage.removeItem("alexandria/logout/flag"); localStorage.removeItem("alexandria/logout/flag");
}
persistLogin(user, "extension"); persistLogin(user, "extension");
} }
@ -279,7 +293,10 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) {
} }
clearLogin(); clearLogin();
// Only access localStorage on client-side
if (typeof window !== 'undefined') {
localStorage.removeItem("alexandria/logout/flag"); localStorage.removeItem("alexandria/logout/flag");
}
persistLogin(user, "amber"); persistLogin(user, "amber");
} }
@ -363,7 +380,10 @@ export async function loginWithNpub(pubkeyOrNpub: string) {
userPubkey.set(user.pubkey); userPubkey.set(user.pubkey);
clearLogin(); clearLogin();
// Only access localStorage on client-side
if (typeof window !== 'undefined') {
localStorage.removeItem("alexandria/logout/flag"); localStorage.removeItem("alexandria/logout/flag");
}
persistLogin(user, "npub"); persistLogin(user, "npub");
} }
@ -373,6 +393,9 @@ export async function loginWithNpub(pubkeyOrNpub: string) {
export function logoutUser() { export function logoutUser() {
console.log("Logging out user..."); console.log("Logging out user...");
const currentUser = get(userStore); const currentUser = get(userStore);
// Only access localStorage on client-side
if (typeof window !== 'undefined') {
if (currentUser.ndkUser) { if (currentUser.ndkUser) {
// Clear persisted relays for the user // Clear persisted relays for the user
localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "inbox")); localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "inbox"));
@ -414,6 +437,7 @@ export function logoutUser() {
localStorage.setItem("alexandria/logout/flag", "true"); localStorage.setItem("alexandria/logout/flag", "true");
console.log("Cleared all login data from localStorage"); console.log("Cleared all login data from localStorage");
}
userStore.set({ userStore.set({
pubkey: null, pubkey: null,

8
src/lib/utils/markup/asciidoctorPostProcessor.ts

@ -26,8 +26,8 @@ function replaceWikilinks(html: string): string {
const display = (label || target).trim(); const display = (label || target).trim();
const url = `/events?d=${normalized}`; const url = `/events?d=${normalized}`;
// Output as a clickable <a> with the [[display]] format and matching link colors // Output as a clickable <a> with the [[display]] format and matching link colors
// Use onclick to bypass SvelteKit routing and navigate directly // Remove onclick handler to avoid breaking amber session - will be handled by global click handler
return `<a class="wikilink text-primary-600 dark:text-primary-500 hover:underline" data-dtag="${normalized}" data-url="${url}" href="${url}" onclick="window.location.href='${url}'; return false;">${display}</a>`; return `<a class="wikilink text-primary-600 dark:text-primary-500 hover:underline" data-dtag="${normalized}" data-url="${url}" href="${url}">${display}</a>`;
}, },
); );
} }
@ -39,8 +39,8 @@ function replaceAsciiDocAnchors(html: string): string {
return html.replace(/<a id="([^"]+)"><\/a>/g, (_match, id) => { return html.replace(/<a id="([^"]+)"><\/a>/g, (_match, id) => {
const normalized = normalizeDTag(id.trim()); const normalized = normalizeDTag(id.trim());
const url = `/events?d=${normalized}`; const url = `/events?d=${normalized}`;
// Use onclick to bypass SvelteKit routing and navigate directly // Remove onclick handler to avoid breaking amber session - will be handled by global click handler
return `<a class="wikilink text-primary-600 dark:text-primary-500 hover:underline" data-dtag="${normalized}" data-url="${url}" href="${url}" onclick="window.location.href='${url}'; return false;">${id}</a>`; return `<a class="wikilink text-primary-600 dark:text-primary-500 hover:underline" data-dtag="${normalized}" data-url="${url}" href="${url}">${id}</a>`;
}); });
} }

4
src/lib/utils/markup/basicMarkupParser.ts

@ -149,8 +149,8 @@ function replaceWikilinks(text: string): string {
const display = (label || target).trim(); const display = (label || target).trim();
const url = `/events?d=${normalized}`; const url = `/events?d=${normalized}`;
// Output as a clickable <a> with the [[display]] format and matching link colors // Output as a clickable <a> with the [[display]] format and matching link colors
// Use onclick to bypass SvelteKit routing and navigate directly // Remove onclick handler to avoid breaking amber session - will be handled by global click handler
return `<a class="wikilink text-primary-600 dark:text-primary-500 hover:underline" data-dtag="${normalized}" data-url="${url}" href="${url}" onclick="window.location.href='${url}'; return false;">${display}</a>`; return `<a class="wikilink text-primary-600 dark:text-primary-500 hover:underline" data-dtag="${normalized}" data-url="${url}" href="${url}">${display}</a>`;
}, },
); );
} }

20
src/lib/utils/relay_management.ts

@ -57,6 +57,16 @@ export function testLocalRelayConnection(
error?: string; error?: string;
actualUrl?: 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) => { return new Promise((resolve) => {
try { try {
// Ensure the URL is using ws:// protocol for local relays // Ensure the URL is using ws:// protocol for local relays
@ -182,6 +192,16 @@ export function testRemoteRelayConnection(
error?: string; error?: string;
actualUrl?: 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) => { return new Promise((resolve) => {
// Ensure the URL is using wss:// protocol for remote relays // Ensure the URL is using wss:// protocol for remote relays
const secureUrl = relayUrl.replace(/^ws:\/\//, "wss://"); const secureUrl = relayUrl.replace(/^ws:\/\//, "wss://");

46
src/routes/+layout.svelte

@ -1,8 +1,9 @@
<script> <script lang="ts">
import "../app.css"; import "../app.css";
import Navigation from "$lib/components/Navigation.svelte"; import Navigation from "$lib/components/Navigation.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { goto } from "$app/navigation";
import { Alert } from "flowbite-svelte"; import { Alert } from "flowbite-svelte";
import { HammerSolid } from "flowbite-svelte-icons"; import { HammerSolid } from "flowbite-svelte-icons";
import { logCurrentRelayConfiguration, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { logCurrentRelayConfiguration, activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
@ -40,6 +41,49 @@
onMount(() => { onMount(() => {
const rect = document.body.getBoundingClientRect(); const rect = document.body.getBoundingClientRect();
// document.body.style.height = `${rect.height}px`; // document.body.style.height = `${rect.height}px`;
// AI-NOTE: Global click handler for wikilinks and hashtags to avoid breaking amber session
function handleInternalLinkClick(event: MouseEvent) {
const target = event.target as HTMLElement;
// Handle wikilinks
if (target.tagName === "A" && target.classList.contains("wikilink")) {
const href = (target as HTMLAnchorElement).getAttribute("href");
if (href && href.startsWith("/")) {
event.preventDefault();
goto(href);
}
}
// Handle hashtag buttons
if (target.tagName === "BUTTON" && target.classList.contains("cursor-pointer")) {
const onclick = target.getAttribute("onclick");
if (onclick && onclick.includes("window.location.href")) {
event.preventDefault();
// Extract the URL from the onclick handler
const match = onclick.match(/window\.location\.href='([^']+)'/);
if (match && match[1]) {
goto(match[1]);
}
}
}
// Handle notification links (divs with onclick handlers)
if (target.tagName === "DIV" && target.classList.contains("cursor-pointer")) {
const onclick = target.getAttribute("onclick");
if (onclick && onclick.includes("window.location.href")) {
event.preventDefault();
// Extract the URL from the onclick handler
const match = onclick.match(/window\.location\.href='([^']+)'/);
if (match && match[1]) {
goto(match[1]);
}
}
}
}
document.addEventListener("click", handleInternalLinkClick);
return () => document.removeEventListener("click", handleInternalLinkClick);
}); });
</script> </script>

9
src/routes/+layout.ts

@ -10,14 +10,18 @@ import type { LayoutLoad } from "./$types";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { browser } from "$app/environment"; import { browser } from "$app/environment";
// AI-NOTE: Leave SSR off until event fetches are implemented server-side. // AI-NOTE: SSR enabled for better SEO and OpenGraph support
export const ssr = false; export const ssr = true;
/** /**
* Attempts to restore the user's authentication session from localStorage. * Attempts to restore the user's authentication session from localStorage.
* Handles extension, Amber (NIP-46), and npub login methods. * Handles extension, Amber (NIP-46), and npub login methods.
* Only runs on client-side.
*/ */
function restoreAuthSession() { function restoreAuthSession() {
// Only run on client-side
if (!browser) return;
try { try {
const pubkey = getPersistedLogin(); const pubkey = getPersistedLogin();
const loginMethod = localStorage.getItem(loginMethodStorageKey); const loginMethod = localStorage.getItem(loginMethodStorageKey);
@ -122,6 +126,7 @@ export const load: LayoutLoad = () => {
const ndk = initNdk(); const ndk = initNdk();
ndkInstance.set(ndk); ndkInstance.set(ndk);
// Only restore auth session on client-side
if (browser) { if (browser) {
restoreAuthSession(); restoreAuthSession();
} }

Loading…
Cancel
Save