← back to Stayclaim
src/lib/site.ts
127 lines
/**
* Single source of truth for the public-facing brand.
*
* Three-domain ecosystem (one Next.js process, host-routed via middleware.ts):
* - wholivedthere.com → flagship / archive (default)
* - claimmyaddress.com → onboarding / claim flow
* - bubbesblock.com → community / neighborhoods
*
* `SITE_NAME` etc. remain pastdoor as the umbrella brand;
* per-host overrides come from siteForHost(headers().get('host')) in server components.
*/
import { headers } from 'next/headers';
export const SITE_NAME = 'pastdoor';
export const SITE_DOMAIN = 'pastdoor.com';
export const SITE_TAGLINE = 'Nextdoor, but for then.';
export const SITE_DESCRIPTION =
'Every address has a story. Look up the full history of a house — who lived there, what was built and when, what the archival maps show. The opposite of a real-estate listing.';
export type SurfaceKey = 'flagship' | 'claim' | 'community';
export interface SurfaceConfig {
key: SurfaceKey;
domain: string;
brandName: string;
tagline: string;
description: string;
primaryCta: { label: string; href: string };
themeAccent: 'green' | 'red' | 'gold'; // matches Gucci palette decision in PLAN.md
ogTitle: string;
/**
* GA4 measurement ID per surface — populated once the analytics skill creates
* the properties (pending Steve granting the service account GA Admin access).
* Layout.tsx reads NEXT_PUBLIC_GA_<SURFACE_KEY> env vars first; falls back here.
*/
gaMeasurementId?: string;
}
const SURFACES: Record<string, SurfaceConfig> = {
// Flagship — full archive
'wholivedthere.com': {
key: 'flagship',
domain: 'wholivedthere.com',
brandName: 'WhoLivedThere',
tagline: 'Every address has a story.',
description:
'Look up the full history of any house — who lived there, when it was built, what the archives say. Maps, deeds, permits, photos, year-by-year Street View.',
primaryCta: { label: 'Search any address', href: '/' },
themeAccent: 'green',
ogTitle: 'WhoLivedThere — every address has a story',
},
// Onboarding — claim flow
'claimmyaddress.com': {
key: 'claim',
domain: 'claimmyaddress.com',
brandName: 'ClaimMyAddress',
tagline: 'Your home. Your file. Forever.',
description:
'Claim your address. Upload deeds, bills, photos, and the receipts that prove your home is yours. A free, permanent home file backed by public records.',
primaryCta: { label: 'Claim your home', href: '/submit' },
themeAccent: 'red',
ogTitle: 'ClaimMyAddress — your home, your file, forever',
},
// Community — neighborhoods
'bubbesblock.com': {
key: 'community',
domain: 'bubbesblock.com',
brandName: "Bubbe's Block",
tagline: 'The block, then and now.',
description:
"What's happening on your block — and what was happening here a hundred years ago. Neighborhood posts, cup-of-sugar requests, history grounded in the archive.",
primaryCta: { label: 'See your block', href: '/neighborhoods' },
themeAccent: 'gold',
ogTitle: "Bubbe's Block — the block, then and now",
},
};
const DEFAULT_SURFACE: SurfaceConfig = (() => {
const s = SURFACES['wholivedthere.com'];
if (!s) throw new Error('lib/site.ts: default surface (wholivedthere.com) missing from SURFACES');
return s;
})();
/**
* Resolve the surface config for a given host. Strips `www.` and port.
* Falls back to the wholivedthere flagship when host is unknown (dev, preview, etc.)
*/
export function siteForHost(rawHost: string | null | undefined): SurfaceConfig {
if (!rawHost) return DEFAULT_SURFACE;
let host = rawHost.trim().toLowerCase().replace(/^www\./, '');
// IPv6 bracket-aware port strip: `[::1]:3000` → `[::1]`; IPv4/hostname `host:port` → `host`.
if (host.startsWith('[')) {
const end = host.indexOf(']');
if (end !== -1) host = host.slice(0, end + 1);
} else {
host = host.replace(/:\d+$/, '');
}
// Use Object.hasOwn to defeat prototype-pollution lookups (`__proto__`, `constructor`).
return Object.prototype.hasOwnProperty.call(SURFACES, host) ? SURFACES[host] : DEFAULT_SURFACE;
}
/**
* Convenience for server components — reads x-pastdoor-host (set by middleware)
* or the literal Host header. Async because Next 15's `headers()` is async.
*
* `x-pastdoor-host` is validated against known SURFACES keys to prevent client
* spoofing (the header would otherwise be attacker-controlled if a request
* bypasses middleware or the edge fails to strip inbound x-* headers).
*/
export async function currentSurface(): Promise<SurfaceConfig> {
const h = await headers();
const forwarded = h.get('x-pastdoor-host');
const safeForwarded =
forwarded && Object.prototype.hasOwnProperty.call(SURFACES, forwarded.trim().toLowerCase())
? forwarded
: null;
return siteForHost(safeForwarded || h.get('host'));
}
/**
* Sister-domain links shown via the cross-promo CTA component.
* Returns the *other two* surfaces, in the order they should appear.
*/
export function sisterSurfaces(current: SurfaceKey): SurfaceConfig[] {
return Object.values(SURFACES).filter(s => s.key !== current);
}