diff --git a/src/app.tsx b/src/app.tsx
index 34fd7b4..06e1210 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -15,6 +15,7 @@ import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback";
import { BarsProvider, useBars } from "./context/bars";
import { DarkModeProvider } from "./context/darkMode";
import { AuthProvider } from "./context/auth";
+import { SiteProvider } from "./context/SiteContext";
import { createWindowWidth, isMobile } from "~/lib/resize-utils";
import { MOBILE_CONFIG } from "./config";
import CustomScrollbar from "./components/CustomScrollbar";
@@ -203,17 +204,19 @@ export default function App() {
)}
>
-
- (
-
- {props.children}
-
- )}
- >
-
-
-
+
+
+ (
+
+ {props.children}
+
+ )}
+ >
+
+
+
+
diff --git a/src/context/SiteContext.tsx b/src/context/SiteContext.tsx
new file mode 100644
index 0000000..d6082aa
--- /dev/null
+++ b/src/context/SiteContext.tsx
@@ -0,0 +1,104 @@
+/**
+ * SiteContext — SolidJS provider exposing the active `Site` to the component
+ * tree (task 01 keystone).
+ *
+ * Resolution strategy:
+ * - Server (SSR): reads the module-level value bound by `setServerSite()`,
+ * which `entry-server.tsx` calls per-request via `getSiteFromEvent(event)`.
+ * - Client (hydration): reads the SSR-injected `window.__SITE__` id (written
+ * into the document shell by `entry-server.tsx`) so the post-hydration
+ * value matches the `data-site` attribute on ``. Falls back to
+ * `resolveSiteFromHost(window.location.hostname)` if the injected id is
+ * missing (e.g. client-side navigation / hard refresh quirks).
+ *
+ * NOTE on race-safety: SSR of a personal site is single-render-per-request in
+ * practice; the module-level holder is adequate here. Server functions that
+ * need authoritative per-request site resolution MUST use
+ * `getSiteFromEvent` / `getSiteFromRequest` directly rather than reading
+ * the provider — do not rely on `useSite()` for authorization decisions.
+ */
+import {
+ createContext,
+ useContext,
+ onMount,
+ createSignal,
+ type Accessor,
+ type ParentComponent
+} from "solid-js";
+import { isServer } from "solid-js/web";
+import {
+ resolveSiteFromHost,
+ resolveSiteFromLocation,
+ SITE_CONFIG,
+ MAIN_SITE,
+ type Site,
+ type SiteId
+} from "~/lib/site-context";
+
+// ── SSR binding ──────────────────────────────────────────────────────────
+let serverSite: Site = MAIN_SITE;
+
+/**
+ * SSR-only. Called by `entry-server.tsx` immediately before rendering so the
+ * component tree reads the correct site during the initial SSR pass.
+ */
+export function setServerSite(site: Site): void {
+ if (!isServer) return;
+ serverSite = site;
+}
+
+// ── Client hydration data ────────────────────────────────────────────────
+declare global {
+ interface Window {
+ __SITE__?: SiteId;
+ }
+}
+
+/** Resolve the client-side active site, preferring the SSR-injected id. */
+export function resolveClientSite(): Site {
+ if (typeof window === "undefined") return MAIN_SITE;
+ const injected = window.__SITE__;
+ if (injected && SITE_CONFIG[injected]) return SITE_CONFIG[injected];
+ return resolveSiteFromLocation(window.location.hostname);
+}
+
+// ── Context ──────────────────────────────────────────────────────────────
+const SiteContext = createContext>(() => MAIN_SITE);
+
+export const SiteProvider: ParentComponent = (props) => {
+ const initial: Site = isServer ? serverSite : resolveClientSite();
+ const [site, setSite] = createSignal(initial);
+
+ // Reconcile after mount: covers the rare case where the injected id was
+ // unavailable during the synchronous init or the host changed via
+ // client-side navigation.
+ onMount(() => {
+ const resolved = resolveClientSite();
+ if (resolved.id !== site().id) setSite(resolved);
+ });
+
+ return (
+ {props.children}
+ );
+};
+
+/**
+ * Access the active `Site` config anywhere in the tree.
+ *
+ * Returns an accessor (`() => Site`) consistent with the rest of the app's
+ * context / signal conventions. Use it for branding (PageHead titleSuffix,
+ * brand color, OG image, favicon), per-site navigation, and canonical URLs.
+ *
+ * @example
+ * ```tsx
+ * const site = useSite();
+ * return {`Blog${site().titleSuffix}`};
+ * ```
+ */
+export function useSite(): Accessor {
+ return useContext(SiteContext);
+}
+
+export { SITE_CONFIG, MAIN_SITE };
+export type { Site, SiteId };
+export { resolveSiteFromHost, resolveSiteFromLocation };
diff --git a/src/entry-server.tsx b/src/entry-server.tsx
index c46d032..99d7640 100644
--- a/src/entry-server.tsx
+++ b/src/entry-server.tsx
@@ -1,19 +1,29 @@
// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server";
+import { getSiteFromEvent } from "~/server/site-context-server";
+import { setServerSite } from "~/context/SiteContext";
-export default createHandler(() => (
- (
-
-
-
-
-
-
- {assets}
-
-
- {children}
- {scripts}
-
-
- )}
- />
-));
+
+ {/* Hydration data for SiteContext — must run before the app bundle. */}
+
+ {assets}
+
+
+ {children}
+ {scripts}
+
+