# Active Site Cookie + RSC Initial Fetch — Design

- **Date**: 2026-06-08
- **Status**: approved (design)
- **Section**: cross-cutting (perf) — foundation + Backlink Profil pilot
- **Depends on**: `(app)` layout (already an async RSC passing `initialSites`), `site-context`, the per-page client containers.

## Goal

Kill the client-fetch waterfall (27 client containers fetch via `useEffect` after hydration because the active site lives in `localStorage`, invisible to the server). Move the active site to a **cookie** so Server Components can read it and fetch the initial data on the server — data is in the first paint, no post-hydration round-trip. `loading.tsx` becomes meaningful (pages now suspend).

**Scope = decomposed**: this slice ships the **foundation** (cookie everywhere, backward-compatible) **+ one pilot page** (Backlink → Profil) converted to RSC initial-fetch + a section `loading.tsx`. Other sections (keywords, content, …) roll out in follow-up slices using the proven pattern. The foundation alone breaks nothing — existing client containers keep working off the context value (now cookie-seeded).

## Foundation

### Cookie
Active site id stored in cookie **`active-site-id`** (path `/`, `sameSite=lax`, 1-year max-age, not httpOnly — the client switcher writes it).

### Server helper — `src/lib/sites/active-site.ts`
```ts
import "server-only";
import { cookies } from "next/headers";
export const ACTIVE_SITE_COOKIE = "active-site-id";
/** Validated active site id for this request: cookie value if it's a real site, else the first. */
export function pickActiveSiteId(siteIds: string[], cookieVal: string | undefined): string {
  if (cookieVal && siteIds.includes(cookieVal)) return cookieVal;
  return siteIds[0] ?? "";
}
export async function getActiveSiteId(siteIds: string[]): Promise<string> {
  const c = (await cookies()).get(ACTIVE_SITE_COOKIE)?.value;
  return pickActiveSiteId(siteIds, c);
}
```
`pickActiveSiteId` is pure + unit-tested.

### Layout (`src/app/(app)/layout.tsx`)
Reads the cookie, computes `initialActiveSiteId = pickActiveSiteId(sites.map(s=>s.id), cookie)`, passes it to `SiteProvider`.

### `SiteProvider` (`src/lib/keywords/site-context.tsx`)
- New prop `initialActiveSiteId: string`; initial state = it (drop the `localStorage` restore `useEffect` → no hydration flicker, no client read).
- `setActiveSiteId(id)`: set state **+ write the cookie** (`document.cookie = ...`) **+ `router.refresh()`** (re-run Server Components with the new cookie). Keep `addSite`/`removeSite` writing the cookie the same way.
- `useActiveSite()` API unchanged → all 27 containers keep working.

### Switcher
`team-switcher.tsx` is unchanged (still calls `setActiveSiteId`); the behavior change lives in the provider.

## Pilot — Backlink → Profil (RSC initial fetch)

### Page (`src/app/(app)/backlink/profile/page.tsx`) → async RSC
```tsx
const sites = await getSitesForOrg(await getActiveOrgId());
const siteId = await getActiveSiteId(sites.map((s) => s.id));
const [stats, page0] = siteId
  ? await Promise.all([getDomainProfileStats(siteId), getDomainBacklinksPage(siteId, { page: 0, pageSize: 50 })])
  : [null, { rows: [], total: 0 }];
// render header + <BacklinkProfileContainer initialStats={stats} initialPage={page0} />
```
Uses the existing query functions directly (server). Parallel `Promise.all`.

### Container (`domain-profile-container.tsx`)
- Accept `initialStats: DomainProfileStats | null` + `initialPage: DomainBacklinksPage` props.
- Seed `stats`/`rows`/`total` from them (no fetch on first render). The existing `load()` effect only runs on **interaction** (filter/page change) or after import — guard the first run so the server-provided page isn't immediately refetched (e.g. track a `firstLoad` ref, or only refetch when `page`/`filter` differ from the initial 0/"all").
- `activeSiteId` from `useActiveSite()` stays (for the import `fetch` + refetch).

### `loading.tsx` (`src/app/(app)/backlink/loading.tsx`)
Section-level skeleton (DashboardHeader bar + KPI/table skeletons) — now fires because the page awaits.

## Data flow (after)

`switch site → setActiveSiteId writes cookie + router.refresh() → layout + page re-run on the server with the new cookie → page fetches new site's data → streams in (loading.tsx during) → container receives fresh initial props`. Filter/pagination inside a page stay client-side (server actions) for snappy interaction without a full refresh.

## Components / files

```
src/lib/sites/active-site.ts          — cookie name + pickActiveSiteId (pure) + getActiveSiteId
src/lib/sites/active-site.test.ts     — pickActiveSiteId unit tests
src/app/(app)/layout.tsx              — read cookie → initialActiveSiteId
src/lib/keywords/site-context.tsx     — initialActiveSiteId prop, cookie write + router.refresh, drop localStorage
src/app/(app)/backlink/profile/page.tsx       — async RSC initial fetch
src/components/backlink/domain-profile-container.tsx — accept initial props, skip first client fetch
src/app/(app)/backlink/loading.tsx    — section skeleton
```

## Error / empty states

- No sites → `siteId = ""` → page renders empty container (existing empty states).
- Cookie points at a deleted/foreign site → `pickActiveSiteId` falls back to the first site.
- `router.refresh()` failure is non-fatal (client state already updated optimistically).

## Testing

- **Unit** (`active-site.test.ts`): `pickActiveSiteId` — valid cookie kept, invalid/missing → first, empty list → "".
- **Manual (d3k)**: load `/backlink/profile` → KPIs + first table page present **on first paint** (check the server-rendered HTML / no empty-then-fill flash); the `getDomainBacklinksPage` action is NOT called on initial load (only on filter/page/import); switch site in the header → page refreshes to the new site's data; `loading.tsx` skeleton flashes on navigation; existing pages (keywords/content) still work unchanged off the cookie-seeded context; `d3k errors` clean. (Also confirm active-site still persists across full reloads via the cookie.)

## Non-goals (this slice)

- Converting keywords / content / other sections to RSC fetch — follow-up slices (mechanical, same pattern).
- httpOnly/secure cookie hardening, multi-tab cookie sync.
- Removing the client refetch on filter/pagination (kept intentionally for snappy in-page interaction).
- React Query / SWR adoption.
