Skip to content

Commit daa1adf

Browse files
odenirdevclaude
andcommitted
feat(web)!: materialized host fetches Site Settings at runtime (buildTime anchors + getSiteConfig)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cb7439c commit daa1adf

3 files changed

Lines changed: 39 additions & 29 deletions

File tree

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
11
import { notFound, permanentRedirect } from 'next/navigation';
2-
import { BlockRenderer, buildMetadata, getPage } from '@ogs-tech/press-web';
2+
import { BlockRenderer, buildMetadata, getPage, getSiteConfig } from '@ogs-tech/press-web';
33
import { customBlocks } from '../../press.blocks';
4-
import { config } from '../../press-config';
4+
import { buildTime } from '../../press-config';
55

66
interface PageProps {
77
params: Promise<{ slug?: string[] }>;
88
}
99

1010
/**
1111
* Catch-all segments → CMS slug. The site root ('/') has no segments and maps to
12-
* the home slug declared in press.config (`config.routes.home`) — so the engine
13-
* no longer hardcodes a magic 'home' string; the adopter owns it.
12+
* the home slug declared in press.config (`buildTime.routes.home`). Routing reads
13+
* the build-time anchor only, so the /home → / redirect stays deterministic and
14+
* independent of CMS availability.
1415
*/
1516
function slugFor(segments?: string[]): string {
16-
return (segments ?? []).join('/') || config.routes.home;
17+
return (segments ?? []).join('/') || buildTime.routes.home;
1718
}
1819

1920
export async function generateMetadata({ params }: PageProps) {
2021
const { slug } = await params;
21-
const page = await getPage(slugFor(slug));
22-
return buildMetadata(config, page ? { title: page.title } : null);
22+
// Next dedupes identical fetches within a request + the ISR Data Cache serves
23+
// getSiteConfig across requests, so this resolves to a single cached round-trip
24+
// even though the layout also calls it.
25+
const [site, page] = await Promise.all([getSiteConfig(buildTime), getPage(slugFor(slug))]);
26+
return buildMetadata(site, page ? { title: page.title } : null);
2327
}
2428

2529
export default async function CatchAllPage({ params }: PageProps) {
@@ -28,9 +32,9 @@ export default async function CatchAllPage({ params }: PageProps) {
2832

2933
// The home page is canonical at the root only. A direct hit on its slug
3034
// (e.g. /home) 308-redirects to '/', so home has no public slug URL.
31-
if (path && path === config.routes.home) permanentRedirect('/');
35+
if (path && path === buildTime.routes.home) permanentRedirect('/');
3236

33-
const page = await getPage(path || config.routes.home);
37+
const page = await getPage(path || buildTime.routes.home);
3438
if (!page) notFound();
3539
return <BlockRenderer blocks={page.body} components={customBlocks} />;
3640
}

‎packages/web/templates/host/app/layout.tsx‎

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,44 @@
11
import { Archivo, Bricolage_Grotesque, IBM_Plex_Mono } from 'next/font/google';
2-
import { buildMetadata, buildThemeStyle } from '@ogs-tech/press-web';
2+
import { buildMetadata, buildThemeStyle, getSiteConfig } from '@ogs-tech/press-web';
33
import '@ogs-tech/press-web/theme.css';
4-
import { config } from '../press-config';
4+
import { buildTime } from '../press-config';
55

6-
// Default-theme fonts, loaded + optimized by next/font at build time (Spec §6).
7-
// Each exposes a CSS variable consumed by theme.css with a fallback:
6+
// Default-theme fonts, loaded + optimized by next/font at build time. Each exposes
7+
// a CSS variable consumed by theme.css with a fallback:
88
// font-family: var(--press-font-body, var(--press-font-body-default))
9-
// so an adopter override (emitted by buildThemeStyle) wins, else the optimized
10-
// default applies. Overriding theme.fonts.* sets the family string only — loading
11-
// that family is the adopter's responsibility.
9+
// so a Site Settings font override (emitted by buildThemeStyle) wins, else the
10+
// optimized default applies. Build-time families are still owned by press.config.
1211
const display = Bricolage_Grotesque({ subsets: ['latin'], display: 'swap', variable: '--press-font-display-default' });
1312
const body = Archivo({ subsets: ['latin'], display: 'swap', variable: '--press-font-body-default' });
1413
const mono = IBM_Plex_Mono({ subsets: ['latin'], weight: ['400', '500'], display: 'swap', variable: '--press-font-mono-default' });
1514

1615
const fontVars = `${display.variable} ${body.variable} ${mono.variable}`;
1716

18-
// Brand defaults, no page: title = seo.defaultTitle, plus the favicon icon.
19-
export const metadata = buildMetadata(config, null);
17+
// Brand defaults, no page: title = seo.defaultTitle + favicon. Fetched at runtime
18+
// from the CMS (ISR ~60s) so editor changes appear without a redeploy.
19+
export async function generateMetadata() {
20+
return buildMetadata(await getSiteConfig(buildTime), null);
21+
}
2022

21-
export default function RootLayout({ children }: { children: React.ReactNode }) {
23+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
24+
const site = await getSiteConfig(buildTime);
2225
return (
23-
<html lang={config.site.locale} data-theme={config.theme.name} className={fontVars}>
26+
<html lang={site.site.locale} data-theme={buildTime.theme.name} className={fontVars}>
2427
<head>
25-
{/* The single injection point for token values (Spec §0). */}
26-
<style dangerouslySetInnerHTML={{ __html: buildThemeStyle(config) }} />
28+
{/* The single injection point for token values (CMS-sourced or DEFAULT_THEME). */}
29+
<style dangerouslySetInnerHTML={{ __html: buildThemeStyle(site) }} />
2730
</head>
2831
<body>
2932
<header>
3033
<a href="/">
31-
{config.brand.logo ? <img src={config.brand.logo} alt="" /> : null}
32-
<span>{config.brand.name}</span>
34+
{site.brand.logo ? <img src={site.brand.logo} alt="" /> : null}
35+
<span>{site.brand.name}</span>
3336
</a>
3437
</header>
3538
<main>{children}</main>
3639
<footer>
3740
<small>
38-
{config.brand.name} · {new Date().getFullYear()}
41+
{site.brand.name} · {new Date().getFullYear()}
3942
</small>
4043
</footer>
4144
</body>
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
// .press/web/press-config.ts (materialized) — resolves the adopter's web-zone
2-
// whitelabel config ONCE into an immutable module constant (deterministic under
3-
// RSC/SSR, the Spec 2 boundary). `../../packages/web/config` is the web zone from
4-
// the materialized .press/web/ depth (two levels up → project root → packages/web/).
2+
// BUILD-TIME anchors ONCE into an immutable module constant: routes, theme.name
3+
// (the <html data-theme> selector + ThemeName guard), and theme.fonts (which
4+
// next/font must know at build time). Identity, SEO, and theme colour/radius
5+
// VALUES are NOT here — they are fetched at runtime from the CMS "Site Settings"
6+
// single type by getSiteConfig (site-settings-cms spec §6). Engine-owned and
7+
// rewritten every run — never hand-edited.
58
import userConfig from '../../packages/web/config';
69
import { resolveConfig } from '@ogs-tech/press-web';
710

8-
export const config = resolveConfig(userConfig);
11+
export const buildTime = resolveConfig(userConfig);

0 commit comments

Comments
 (0)