From 5329c611b1bf8a3b060ea24530bc22e9ff5cbc34 Mon Sep 17 00:00:00 2001 From: dtonon Date: Thu, 20 Aug 2026 14:42:48 +0100 Subject: [PATCH] Warm the server cache from client-side navigations, PUBLIC_SSR_WARM opts out --- .env.example | 2 ++ README.md | 1 + src/lib/config.ts | 5 +++++ src/lib/forum/snapshot.ts | 9 +++++++-- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 8060c38..8bcb2b4 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,8 @@ PUBLIC_GROUP_ID=mygrouprandomid # required in simple mode; the single for PUBLIC_TITLE= # top-bar title; empty falls back to the group name # Render on the server (Node deploy) instead of shipping a static SPA PUBLIC_SSR=no +# With SSR on, warm the server cache from client-side navigations (yes/no) +PUBLIC_SSR_WARM=yes PUBLIC_SEARCH=no # yes | no — enable the search box (requires a relay with NIP-50 support) PUBLIC_LABELS= # comma-separated discussion labels (e.g., bug,feature,question) diff --git a/README.md b/README.md index f47b888..dcf76e9 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Squalk is configured entirely through environment variables (all prefixed `PUBLI | `PUBLIC_TITLE` | no | group name | Title shown in the top bar. When empty it falls back to the group's name. | | `PUBLIC_JOINCODE` | no | `no` | `yes` to show an invite-code field when a join request is rejected (for code-gated relays). | | `PUBLIC_SSR` | no | `no` | `yes` to render pages on the server (crawlable HTML, real 404s). The build then targets Node (`node build`) instead of a static bundle; see [Deploying](#deploying). | +| `PUBLIC_SSR_WARM` | no | `yes` | With `PUBLIC_SSR=yes`, each client-side navigation also asks the server to fetch and cache that page, so a later refresh, shared link or crawler hit is served warm. Costs one extra relay query per navigation on the server; set to `no` to disable. | | `PUBLIC_SEARCH` | no | `no` | `yes` to show a search box at the top of the homepage. Requires a relay with NIP-50 search support. | | `PUBLIC_LABELS` | no | — | Comma-separated discussion labels offered when composing, e.g. `bug,feature,question`. | | `PUBLIC_BLOSSOM_URL` | no | — | Blossom server URL used for media uploads, e.g. `https://blossom.primal.net`. Uploads are disabled when unset. | diff --git a/src/lib/config.ts b/src/lib/config.ts index e736634..c68cb94 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -21,6 +21,11 @@ if (MODE === "simple" && !GROUP_ID) { // Server-side rendering: opt-in, decided at build time (vite.config.ts bakes // PUBLIC_SSR in; svelte.config.js picks the matching adapter). export const SSR_ENABLED = __SQUALK_SSR__; +// With SSR on, a client-side navigation also asks the server for the same +// snapshot (without waiting for it) so the page is already cached for the +// next refresh, shared link or crawler. Costs one server→relay query per +// navigation; opt out with PUBLIC_SSR_WARM=no. +export const SSR_WARM = SSR_ENABLED && env.PUBLIC_SSR_WARM !== "no"; // Server-rendered pages and snapshots are anonymous, so a shared cache may // hold them: fresh for 5 minutes, served stale for an hour while revalidating. // Browsers always revalidate (max-age=0) so a login shows its content at once. diff --git a/src/lib/forum/snapshot.ts b/src/lib/forum/snapshot.ts index 439f058..46f7584 100644 --- a/src/lib/forum/snapshot.ts +++ b/src/lib/forum/snapshot.ts @@ -1,5 +1,6 @@ import { browser } from "$app/environment"; import { error } from "@sveltejs/kit"; +import { SSR_WARM } from "$lib/config"; import type { NostrUser } from "$lib/gadgets"; import type { GroupMetadata, GroupSummary } from "./groups"; import type { Resource } from "./resources"; @@ -37,14 +38,18 @@ type Fetch = typeof fetch; // Read a snapshot endpoint. On the server the response is inlined into the // HTML; in the browser it is consumed only while hydrating that HTML. Client // navigations get null, leaving the stores to fetch live (and authenticated) -// data as they always did — the same path a client-only build takes. +// data as they always did — the same path a client-only build takes — but +// may still poke the endpoint so the server has the page cached. // A 404 from the endpoint becomes a real 404 page on the server, so crawlers // never index an empty shell; the client keeps its own not-found handling. export async function snapshot( fetch: Fetch, url: string, ): Promise { - if (browser && !hasInlinedResponse(url)) return null; + if (browser && !hasInlinedResponse(url)) { + if (SSR_WARM) fetch(url).catch(() => {}); + return null; + } let res: Response; try { res = await fetch(url);