diff --git a/.env.example b/.env.example index b01a221..f4feeba 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,12 @@ CMS_INTERNAL_ORIGIN=http://127.0.0.1:5099 # Production uses https://media.sgf.dev/ without a /media/ prefix. MEDIA_PUBLIC_URL=http://127.0.0.1:5099/media/ +# Private runtime flags. Leave disabled outside production. +# SEARCH_INDEXING_ENABLED controls robots.txt and the noindex metadata. +SEARCH_INDEXING_ENABLED=false +# ANALYTICS_ENABLED loads the legacy Plausible tracker only when true. +ANALYTICS_ENABLED=false + # Adapter-node request cap: 8 MiB avatar plus multipart overhead. BODY_SIZE_LIMIT=9M diff --git a/README.md b/README.md index e735511..9c86ca8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,12 @@ privately, never in Git, client code or Docker build arguments: `https://media.sgf.dev/`. Use `http://127.0.0.1:5099/media/` locally. Native CMS media paths map to that base; existing public media URLs keep their crop and sizing parameters. See `docs/media.md`. +- `SEARCH_INDEXING_ENABLED` is a private runtime flag, defaulting to `false`. + Set it to `true` only on production sites that should be indexed. It controls + both `/robots.txt` and the page's `noindex, nofollow` metadata. +- `ANALYTICS_ENABLED` is a private runtime flag, defaulting to `false`. + Set it to `true` to load the existing Plausible tracker from `plausible.sgf.dev`. + Neither flag requires a rebuild. Only the exact value `true` enables them. - Backend `SGFDevs__MemberBridge__FrontendOrigin` must be the frontend origin, for example `http://127.0.0.1:3000`, so reset emails link to Kit. @@ -93,6 +99,10 @@ checks. `PUBLIC_SITE_ORIGIN` controls canonical metadata, not request trust. Set `BODY_SIZE_LIMIT=9M` for the built server to allow the existing 8 MiB avatar limit plus multipart overhead. Application validation still limits accepted files. +`GET /healthz` returns plain text `ok\n` with `Cache-Control: no-store`. +It does not call the CMS or look up a member session, even with a member cookie. +Use it for frontend liveness probes, not CMS readiness checks. + ## Container and CI `Dockerfile` builds Node output with the existing lockfile, then installs only diff --git a/src/env.ts b/src/env.ts index dfb06b9..7a4acac 100644 --- a/src/env.ts +++ b/src/env.ts @@ -36,9 +36,12 @@ export const variables = defineEnvVars({ return url.origin; } }, - PUBLIC_SEARCH_INDEXING_ENABLED: { - description: 'Set to true only for production builds that should be indexed. Missing or false emits noindex.', - public: true, + ANALYTICS_ENABLED: { + description: 'Private runtime flag. Set to true to load the Plausible tracker. Defaults to false.', + schema: (value) => value === 'true' + }, + SEARCH_INDEXING_ENABLED: { + description: 'Private runtime flag. Set to true only on production sites that should be indexed. Defaults to false.', schema: (value) => value === 'true' } }); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 8866f30..b2a19e4 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -4,6 +4,8 @@ import { memberCookieHeader } from './lib/server/auth/cookies'; export const handle: Handle = async ({ event, resolve }) => { event.locals.member = null; + // Health probes must not depend on CMS sessions, even with a member cookie. + if (event.url.pathname === '/healthz') return resolve(event); const hasCookie = Boolean(memberCookieHeader(event.cookies, memberCookieName)); if (hasCookie) { try { diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 165c834..cc5d729 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,3 +1,8 @@ +import { ANALYTICS_ENABLED, SEARCH_INDEXING_ENABLED } from '$app/env/private'; import type { LayoutServerLoad } from './$types'; -export const load: LayoutServerLoad = ({ locals }) => ({ member: locals.member }); +export const load: LayoutServerLoad = ({ locals }) => ({ + member: locals.member, + analyticsEnabled: ANALYTICS_ENABLED, + searchIndexingEnabled: SEARCH_INDEXING_ENABLED +}); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 481f8e1..77a29e3 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -11,6 +11,9 @@ + {#if data.analyticsEnabled} + {@html ``} + {/if} {#if !contentPage || contentPage.description} {/if} diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 92b7554..ba1d018 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,4 +1,4 @@ -import { PUBLIC_SEARCH_INDEXING_ENABLED, PUBLIC_SITE_ORIGIN } from '$app/env/public'; +import { PUBLIC_SITE_ORIGIN } from '$app/env/public'; import { canonicalUrlForPathname, siteOrigin } from '../lib/shell/canonical-url'; import type { LayoutLoad } from './$types'; @@ -9,6 +9,7 @@ export const load: LayoutLoad = ({ url, data }) => { canonicalUrl: canonicalUrlForPathname(origin, url.pathname), ogImageUrl: new URL('/images/og.jpg', origin).href, pageTitle: 'Springfield Devs', - searchIndexingEnabled: PUBLIC_SEARCH_INDEXING_ENABLED === true + searchIndexingEnabled: data.searchIndexingEnabled, + analyticsEnabled: data.analyticsEnabled }; }; diff --git a/src/routes/healthz/+server.ts b/src/routes/healthz/+server.ts new file mode 100644 index 0000000..189fce8 --- /dev/null +++ b/src/routes/healthz/+server.ts @@ -0,0 +1,6 @@ +import { text } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = () => text('ok\n', { + headers: { 'Cache-Control': 'no-store' } +}); diff --git a/src/routes/robots.txt/+server.ts b/src/routes/robots.txt/+server.ts new file mode 100644 index 0000000..0aea54b --- /dev/null +++ b/src/routes/robots.txt/+server.ts @@ -0,0 +1,8 @@ +import { SEARCH_INDEXING_ENABLED } from '$app/env/private'; +import { text } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = () => text( + `User-agent: *\n${SEARCH_INDEXING_ENABLED ? 'Allow: /' : 'Disallow: /'}\n`, + { headers: { 'Cache-Control': 'no-store' } } +);