@@ -327,11 +388,11 @@
The article you're looking for doesn't exist or couldn't be loaded.
-
+
{/if}
diff --git a/src/routes/blog/[id]/+page.ts b/src/routes/blog/[id]/+page.ts
index d43d0cd..3b07a67 100644
--- a/src/routes/blog/[id]/+page.ts
+++ b/src/routes/blog/[id]/+page.ts
@@ -1 +1,15 @@
-export const prerender = false;
+import { getArticleIdentifier, getSnapshotArticle, seoSnapshot } from '$lib/content/seo-snapshot';
+import type { EntryGenerator, PageLoad } from './$types';
+
+export const prerender = true;
+
+export const entries: EntryGenerator = () =>
+ seoSnapshot.articles.flatMap((article) => {
+ const id = getArticleIdentifier(article);
+ return id ? [{ id }] : [];
+ });
+
+export const load = (({ params }) => ({
+ article: getSnapshotArticle(params.id),
+ generatedAt: seoSnapshot.generatedAt
+})) satisfies PageLoad;
diff --git a/src/routes/chat/+page.svelte b/src/routes/chat/+page.svelte
index a5f7677..382bd95 100644
--- a/src/routes/chat/+page.svelte
+++ b/src/routes/chat/+page.svelte
@@ -56,7 +56,7 @@
diff --git a/src/routes/faqs/+page.svelte b/src/routes/faqs/+page.svelte
index 3f4d57d..80bfee9 100644
--- a/src/routes/faqs/+page.svelte
+++ b/src/routes/faqs/+page.svelte
@@ -1,16 +1,7 @@
-
-
- {#each faqCategories as category, categoryIndex (category.title)}
-
-
- {#if openCategoryIndex === categoryIndex}
-
-
-
-
+ {#each faqCategories as category (category.title)}
+
+
+ {category.title}
+
+
-
-
- {#each category.faqs as faq (faq.question)}
-
-
-
- {faq.question}
-
-
-
-
-
- {#if browser}
-
- {@html DOMPurify.sanitize(faq.answer)}
- {/if}
-
-
-
- {/each}
-
-
- {:else}
-
-
- {/if}
-
+
+ {#each category.faqs as faq (faq.question)}
+
+
+
+ {faq.question}
+
+
+
+
+
+
+ {@html faq.answer}
+
+
+ {/each}
+
+
{/each}
diff --git a/src/routes/robots.txt/+server.ts b/src/routes/robots.txt/+server.ts
new file mode 100644
index 0000000..9fade73
--- /dev/null
+++ b/src/routes/robots.txt/+server.ts
@@ -0,0 +1,9 @@
+import { SITE_ORIGIN } from '$lib/seo';
+
+export const prerender = true;
+
+export function GET() {
+ return new Response(`User-agent: *\nAllow: /\n\nSitemap: ${SITE_ORIGIN}/sitemap.xml\n`, {
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' }
+ });
+}
diff --git a/src/routes/s/[pubkey]/+page.svelte b/src/routes/s/[pubkey]/+page.svelte
index e2d2bcc..7a9f77a 100644
--- a/src/routes/s/[pubkey]/+page.svelte
+++ b/src/routes/s/[pubkey]/+page.svelte
@@ -7,6 +7,8 @@
getAvailableCapabilities,
pubkeyToHexColor,
copyToClipboard,
+ decodeServerIdentifier,
+ encodeServerIdentity,
resolveServerIdentifier
} from '$lib/utils';
import { goto } from '$app/navigation';
@@ -54,11 +56,48 @@
import { createServerNotesFilter } from '$lib/constants';
import { npubEncode } from 'nostr-tools/nip19';
import { parseCapTagsFromEvent, parseCapTagsFromTags } from '$lib/services/payments/cep8-tags';
+ import type { PageData } from './$types';
+
+ let { data }: { data: PageData } = $props();
const requestedIdentifier = page.params.pubkey ?? '';
+ const snapshotAnnouncement = $derived(
+ data.server ? parseServerInitializeMsg(data.server.event) : null
+ );
+ const snapshotIdentifier = $derived.by(() => {
+ if (!data.server) return undefined;
+
+ const decoded = decodeServerIdentifier(requestedIdentifier);
+ if (decoded?.pubkey === data.server.event.pubkey) {
+ return {
+ ...decoded,
+ relayHints: decoded.relayHints.length ? decoded.relayHints : data.server.relays
+ };
+ }
+
+ return {
+ original: requestedIdentifier,
+ pubkey: data.server.event.pubkey,
+ relayHints: data.server.relays,
+ format: 'hex' as const
+ };
+ });
+ const snapshotIdentity = $derived.by(() => {
+ if (!snapshotAnnouncement || !data.server) return undefined;
+
+ const relayHints = snapshotIdentifier?.relayHints ?? data.server.relays;
+ const relaySource: 'nprofile' | 'announcement' =
+ snapshotIdentifier?.format === 'nprofile' ? 'nprofile' : 'announcement';
+ return {
+ ...encodeServerIdentity(snapshotAnnouncement.pubkey, relayHints),
+ relaySource
+ };
+ });
+
const resolvedIdentifierQuery = createQuery({
queryKey: ['server-identifier', requestedIdentifier, page.url.hostname],
- queryFn: () => resolveServerIdentifier(requestedIdentifier, page.url.hostname)
+ queryFn: () => resolveServerIdentifier(requestedIdentifier, page.url.hostname),
+ initialData: () => snapshotIdentifier
});
const resolvedIdentifier = $derived($resolvedIdentifierQuery.data ?? null);
const pubkey = $derived(resolvedIdentifier?.pubkey ?? requestedIdentifier);
@@ -73,18 +112,19 @@
const homeHref = $derived<`/`>('/');
const logoBlackSrc = asset('/logo-black.svg');
- // Dynamic SEO data for server pages
- let seoTitle = $state('Loading server...');
- let seoDescription = $state('Loading server information...');
- let seoImage = $state(logoBlackSrc);
- let seoUrl = $state(`https://contextvm.com/s/${requestedIdentifier}`);
- let seoType = $state('website' as 'website' | 'article');
-
const serverQuery = $derived(
- resolvedIdentifier ? useServerAnnouncement(pubkey, bootstrapRelayHints) : undefined
+ resolvedIdentifier
+ ? useServerAnnouncement(
+ pubkey,
+ bootstrapRelayHints,
+ snapshotAnnouncement ? { server: snapshotAnnouncement, isPublic: true } : undefined
+ )
+ : undefined
);
const serverIdentityQuery = $derived(
- resolvedIdentifier ? useServerIdentity(pubkey, bootstrapRelayHints) : undefined
+ resolvedIdentifier
+ ? useServerIdentity(pubkey, bootstrapRelayHints, snapshotIdentity)
+ : undefined
);
const effectiveRelayHints = $derived(
$serverIdentityQuery?.data?.relayHints?.length
@@ -111,22 +151,23 @@
const hasNotes = $derived($notes.length > 0);
- // Update SEO data when server loads
- $effect(() => {
- if ($serverQuery?.data?.server) {
- const server = $serverQuery.data.server;
- const serverName = server.name || 'Unnamed Server';
- const serverAbout = server.about || 'No description available for this server.';
- const serverPicture = server.picture || logoBlackSrc;
-
- seoTitle = serverName;
- seoDescription =
- serverAbout.length > 160 ? serverAbout.substring(0, 160) + '...' : serverAbout;
- seoImage = serverPicture;
- seoUrl = `https://contextvm.com/s/${requestedIdentifier}`;
- seoType = 'website';
- }
+ const displayedServer = $derived($serverQuery?.data?.server ?? snapshotAnnouncement);
+ const seoTitle = $derived(displayedServer?.name || 'Server');
+ const seoDescription = $derived.by(() => {
+ const about = displayedServer?.about || 'Public ContextVM server on Nostr.';
+ return about.length > 160 ? about.substring(0, 157) + '...' : about;
});
+ const seoImage = $derived(displayedServer?.picture || logoBlackSrc);
+ const canonicalPath = $derived('/s/' + (displayedServer?.pubkey ?? pubkey));
+ const liveStatus = $derived(
+ $serverQuery?.isError
+ ? 'unavailable'
+ : $serverQuery?.isFetching
+ ? 'connecting'
+ : ($serverQuery?.dataUpdatedAt ?? 0) > 0
+ ? 'available'
+ : 'not checked'
+ );
// Only load queries for capabilities that are actually available
let toolsQuery = $derived(
@@ -275,7 +316,7 @@
}
-
+
{#if $resolvedIdentifierQuery.isLoading}
@@ -296,13 +337,13 @@
{:else if $serverQuery?.data?.server}
-
-
+
{#if $serverQuery.data.server.picture}
@@ -327,9 +368,9 @@
-
+
{$serverQuery.data.server.name}
-
+
{#if $serverQuery.data.server.website}
@@ -342,6 +383,15 @@
{$serverQuery.data.server.website}
{/if}
+
+ Snapshot refreshed
+ . Live relay lookup is {liveStatus}.
+
{#if hasPaidCapabilities}
diff --git a/src/routes/s/[pubkey]/+page.ts b/src/routes/s/[pubkey]/+page.ts
index d43d0cd..6761c59 100644
--- a/src/routes/s/[pubkey]/+page.ts
+++ b/src/routes/s/[pubkey]/+page.ts
@@ -1 +1,29 @@
-export const prerender = false;
+import { building } from '$app/environment';
+import { error } from '@sveltejs/kit';
+import { getSnapshotServer, seoSnapshot } from '$lib/content/seo-snapshot';
+import { nprofileEncode, npubEncode } from 'nostr-tools/nip19';
+import type { EntryGenerator, PageLoad } from './$types';
+
+export const prerender = true;
+
+export const entries: EntryGenerator = () =>
+ seoSnapshot.servers.flatMap((server) => [
+ { pubkey: server.event.pubkey },
+ { pubkey: npubEncode(server.event.pubkey) },
+ {
+ pubkey: nprofileEncode({
+ pubkey: server.event.pubkey,
+ relays: server.relays
+ })
+ }
+ ]);
+
+export const load = (({ params }) => {
+ const server = getSnapshotServer(params.pubkey);
+ if (building && !server) error(404, 'Server is not in the prerender snapshot');
+
+ return {
+ server,
+ generatedAt: seoSnapshot.generatedAt
+ };
+}) satisfies PageLoad;
diff --git a/src/routes/servers/+page.svelte b/src/routes/servers/+page.svelte
index 4374112..ff91819 100644
--- a/src/routes/servers/+page.svelte
+++ b/src/routes/servers/+page.svelte
@@ -1,4 +1,5 @@
@@ -91,6 +122,15 @@
Discover and connect with Model Context Protocol servers running on the Nostr network. No
domains, no OAuth, no port forwarding—just cryptographic keys and relays.
+
+ Snapshot refreshed
+ . Live relay updates are {liveStatus}.
+
@@ -175,7 +215,7 @@
{/each}
{/if}
- {:else if !$serverAnnouncements?.length && !loading}
+ {:else if !allServerAnnouncements.length && !loading}
No MCP servers found. Check back later for server announcements.
diff --git a/src/routes/servers/+page.ts b/src/routes/servers/+page.ts
new file mode 100644
index 0000000..9eaa40a
--- /dev/null
+++ b/src/routes/servers/+page.ts
@@ -0,0 +1,7 @@
+import { seoSnapshot } from '$lib/content/seo-snapshot';
+import type { PageLoad } from './$types';
+
+export const load = (() => ({
+ servers: seoSnapshot.servers,
+ generatedAt: seoSnapshot.generatedAt
+})) satisfies PageLoad;
diff --git a/src/routes/servers/i/[hash]/+page.svelte b/src/routes/servers/i/[hash]/+page.svelte
index c28ddb5..57d9cb6 100644
--- a/src/routes/servers/i/[hash]/+page.svelte
+++ b/src/routes/servers/i/[hash]/+page.svelte
@@ -69,8 +69,9 @@
diff --git a/src/routes/servers/t/[cat]/+page.svelte b/src/routes/servers/t/[cat]/+page.svelte
index 58fa69c..1a075cd 100644
--- a/src/routes/servers/t/[cat]/+page.svelte
+++ b/src/routes/servers/t/[cat]/+page.svelte
@@ -69,8 +69,9 @@
diff --git a/src/routes/sitemap.xml/+server.ts b/src/routes/sitemap.xml/+server.ts
new file mode 100644
index 0000000..ad62aac
--- /dev/null
+++ b/src/routes/sitemap.xml/+server.ts
@@ -0,0 +1,26 @@
+import { canonicalUrl } from '$lib/seo';
+import { getArticleIdentifier, seoSnapshot } from '$lib/content/seo-snapshot';
+
+export const prerender = true;
+
+const publicPages = ['/', '/about', '/blog', '/faqs', '/servers', '/slides'];
+
+export function GET() {
+ const articlePages = seoSnapshot.articles.flatMap((article) => {
+ const identifier = getArticleIdentifier(article);
+ return identifier ? [`/blog/${encodeURIComponent(identifier)}`] : [];
+ });
+ const serverPages = seoSnapshot.servers.map((server) => `/s/${server.event.pubkey}`);
+ const urls = [...publicPages, ...articlePages, ...serverPages]
+ .map((path) => ` ${canonicalUrl(path)}`)
+ .join('\n');
+ const sitemap = `
+
+${urls}
+
+`;
+
+ return new Response(sitemap, {
+ headers: { 'Content-Type': 'application/xml; charset=utf-8' }
+ });
+}
diff --git a/src/routes/slides/+page.svelte b/src/routes/slides/+page.svelte
index 9e9b44a..e40989e 100644
--- a/src/routes/slides/+page.svelte
+++ b/src/routes/slides/+page.svelte
@@ -1,6 +1,6 @@