Skip to content

Commit cb5fe69

Browse files
committed
feat: archetype-driven generation with dual layout variants
- Expand archetype selector to produce feature-forward (A) and canonical (B) block orderings per page, leveraging new `typicalOrder`, `excludes`, and `blockVocabulary` catalog fields. - Refactor multi-source pipeline: remove `tenant`/`pages`/`seo` from input; introduce `makeVariantField` to produce `a`/`b` content pairs for long strings across all block builders, feeding the dual-variant renderer. - Extend schemas with `LayoutVariant`, `PageBundle.variant`, and richer `ArchetypeCatalog`. - Update renderer/CMS routes to consume variant-aware bundles; drop explicit tenant arg in favor of `readSiteProfile()`. - Add demo-mode infrastructure: `ClientThemeSync`, `DemoModePopup`, `lib/demo/demo-state.ts`, plus refine/rebuild archetype catalog scripts. - Refresh `website-builder` SKILL.md and add Phase 9B archetype docs.
1 parent 8ed4de7 commit cb5fe69

26 files changed

Lines changed: 1849 additions & 833 deletions

app/[tenant]/[slug]/page.tsx

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
import { notFound } from "next/navigation"
22
import { CmsBlockRenderer } from "@/components/cms/CmsRenderer"
3-
import { readCmsPages, getActiveTenant } from "@/lib/cms"
3+
import { readCmsPageVariants } from "@/lib/cms"
4+
import { parseDemoState, resolvePageBlocks, resolveBlockData } from "@/lib/demo/demo-state"
45

5-
interface TenantSlugPageProps {
6-
params: Promise<{ tenant: string; slug: string }>
6+
interface SlugPageProps {
7+
params: Promise<{ slug: string }>
8+
searchParams: Promise<{ theme?: string; layout?: string; text?: string }>
79
}
810

9-
export default async function TenantSlugPage({ params }: TenantSlugPageProps) {
10-
const { tenant, slug } = await params
11-
const activeTenant = getActiveTenant()
12-
const pages = readCmsPages(activeTenant)
13-
const page = pages.find((p) => p.slug === slug)
11+
export default async function SlugPage({ params, searchParams }: SlugPageProps) {
12+
const { slug } = await params
13+
const sp = await searchParams
14+
const state = parseDemoState(new URLSearchParams(
15+
Object.entries(sp).filter(([, v]) => v != null).map(([k, v]) => `${k}=${v}`).join("&")
16+
))
17+
const page = readCmsPageVariants(slug)
1418

15-
if (!page || page.blocks.length === 0) {
19+
if (!page || page.variants.length === 0) {
1620
notFound()
1721
}
1822

19-
return (
20-
<>
21-
{page.blocks.map((block, idx) => (
22-
<CmsBlockRenderer key={`${block.type}-${idx}`} block={block} />
23-
))}
24-
</>
25-
)
23+
const blocks = resolvePageBlocks(page, state.layout)
24+
const resolvedBlocks = blocks.map((block, idx) => {
25+
const resolved = state.text ? resolveBlockData(block, state.text) : block
26+
return <CmsBlockRenderer key={`${resolved.type}-${idx}`} block={resolved} />
27+
})
28+
29+
return <>{resolvedBlocks}</>
2630
}

app/[tenant]/layout.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
import { ThemeProvider } from "@/components/cms/ThemeProvider"
2-
import { readTheme, toCssVars, getActiveTenant, getActiveThemeVariant } from "@/lib/cms"
2+
import { readTheme, toCssVars, ACTIVE_THEME_VARIANT } from "@/lib/cms"
33

4-
interface TenantLayoutProps {
5-
children: React.ReactNode
6-
params: Promise<{ tenant: string }>
7-
}
8-
9-
export default async function TenantLayout({ children, params }: TenantLayoutProps) {
10-
const { tenant } = await params
11-
const activeTenant = getActiveTenant()
12-
const themeVariant = getActiveThemeVariant()
13-
const theme = readTheme(activeTenant, themeVariant)
14-
const cssVars = theme ? toCssVars(theme, themeVariant) : undefined
4+
export default function TenantLayout({ children }: { children: React.ReactNode }) {
5+
const theme = readTheme(ACTIVE_THEME_VARIANT)
6+
const cssVars = theme ? toCssVars(theme, ACTIVE_THEME_VARIANT) : undefined
157

168
const cssVarsStyle = cssVars
179
? Object.entries(cssVars)
@@ -20,7 +12,7 @@ export default async function TenantLayout({ children, params }: TenantLayoutPro
2012
: ""
2113

2214
return (
23-
<ThemeProvider tenant={activeTenant} cssVars={cssVars}>
15+
<ThemeProvider cssVars={cssVars}>
2416
<style
2517
dangerouslySetInnerHTML={{
2618
__html: `:root{${cssVarsStyle}}`,
@@ -29,4 +21,4 @@ export default async function TenantLayout({ children, params }: TenantLayoutPro
2921
{children}
3022
</ThemeProvider>
3123
)
32-
}
24+
}

app/[tenant]/page.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
import { notFound } from "next/navigation"
22
import { CmsBlockRenderer } from "@/components/cms/CmsRenderer"
3-
import { readCmsPages } from "@/lib/cms"
3+
import { readCmsPageVariants, readArchetypeCatalog } from "@/lib/cms"
4+
import { parseDemoState, resolvePageBlocks, resolveBlockData } from "@/lib/demo/demo-state"
45

5-
interface TenantPageProps {
6-
params: Promise<{ tenant: string }>
6+
interface PageProps {
7+
searchParams: Promise<{ theme?: string; layout?: string; text?: string }>
78
}
89

9-
export default async function TenantPage({ params }: TenantPageProps) {
10-
const { tenant } = await params
11-
const pages = readCmsPages(tenant)
12-
const home = pages.find((p) => p.slug === "home")
10+
export default async function HomePage({ searchParams }: PageProps) {
11+
const sp = await searchParams
12+
const state = parseDemoState(new URLSearchParams(
13+
Object.entries(sp).filter(([, v]) => v != null).map(([k, v]) => `${k}=${v}`).join("&")
14+
))
15+
const page = readCmsPageVariants("home")
1316

14-
if (!home) {
17+
if (!page || page.variants.length === 0) {
1518
notFound()
1619
}
1720

18-
return (
19-
<>
20-
{home.blocks.map((block, idx) => (
21-
<CmsBlockRenderer key={`${block.type}-${idx}`} block={block} />
22-
))}
23-
</>
24-
)
21+
const blocks = resolvePageBlocks(page, state.layout)
22+
const resolvedBlocks = blocks.map((block, idx) => {
23+
const resolved = state.text ? resolveBlockData(block, state.text) : block
24+
return <CmsBlockRenderer key={`${resolved.type}-${idx}`} block={resolved} />
25+
})
26+
27+
return <>{resolvedBlocks}</>
2528
}

app/about/page.tsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,20 @@
1-
import { notFound } from "next/navigation"
21
import { CmsBlockRenderer } from "@/components/cms/CmsRenderer"
3-
import { readCmsPages, getActiveTenant } from "@/lib/cms"
4-
import type { Metadata } from "next"
5-
6-
const tenant = getActiveTenant()
7-
const aboutCms = readCmsPages(tenant).find((p) => p.slug === "about")
8-
9-
export const metadata: Metadata = {
10-
title: "About",
11-
description: "Learn about our story, our values, and what makes our cafe special.",
12-
}
2+
import { notFound } from "next/navigation"
3+
import { readCmsPages } from "@/lib/cms"
134

145
export default function AboutPage() {
15-
if (!aboutCms || aboutCms.blocks.length === 0) {
16-
return notFound()
6+
const pages = readCmsPages()
7+
const about = pages.find((p) => p.slug === "about")
8+
9+
if (!about) {
10+
notFound()
1711
}
1812

1913
return (
20-
<div>
21-
{aboutCms.blocks.map((block, idx) => (
14+
<>
15+
{about.blocks.map((block, idx) => (
2216
<CmsBlockRenderer key={`${block.type}-${idx}`} block={block} />
2317
))}
24-
</div>
18+
</>
2519
)
26-
}
20+
}

app/api/cms/theme/route.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { NextRequest, NextResponse } from "next/server"
2-
import { readTheme, getActiveTenant, getActiveThemeVariant } from "@/lib/cms"
2+
import { readTheme, ACTIVE_THEME_VARIANT, listThemeVariants, toCssVars } from "@/lib/cms"
33

44
export async function GET(request: NextRequest) {
5-
const tenant = request.nextUrl.searchParams.get("tenant") || getActiveTenant()
6-
const variant = request.nextUrl.searchParams.get("variant") || getActiveThemeVariant()
5+
const searchParams = request.nextUrl.searchParams
6+
const variant = searchParams.get("variant") || ACTIVE_THEME_VARIANT
7+
const theme = readTheme(variant)
78

8-
const theme = readTheme(tenant, variant)
99
if (!theme) {
1010
return NextResponse.json({ error: "Theme not found" }, { status: 404 })
1111
}
1212

13-
return NextResponse.json(theme)
14-
}
13+
return NextResponse.json({
14+
variant,
15+
theme,
16+
cssVars: toCssVars(theme, variant),
17+
availableVariants: listThemeVariants(),
18+
})
19+
}

app/contact/page.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
import type { Metadata } from "next"
2+
import { notFound } from "next/navigation"
23
import { CmsBlockRenderer } from "@/components/cms/CmsRenderer"
3-
import { readCmsPages, getActiveTenant } from "@/lib/cms"
4-
5-
const tenant = getActiveTenant()
6-
const contactCms = readCmsPages(tenant).find((p) => p.slug === "contact")
4+
import { readCmsPages } from "@/lib/cms"
75

86
export const metadata: Metadata = {
97
title: "Contact",
108
description: "Get in touch with us. Find our location, hours, and contact information.",
119
}
1210

1311
export default function ContactPage() {
14-
if (!contactCms || contactCms.blocks.length === 0) {
15-
return notFound()
12+
const pages = readCmsPages()
13+
const contact = pages.find((p) => p.slug === "contact")
14+
15+
if (!contact) {
16+
notFound()
1617
}
1718

1819
return (
1920
<div>
20-
{contactCms.blocks.map((block, idx) => (
21+
{contact.blocks.map((block, idx) => (
2122
<CmsBlockRenderer key={`${block.type}-${idx}`} block={block} />
2223
))}
2324
</div>
2425
)
25-
}
26+
}

app/layout.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ import { Footer } from "@/components/layout/footer"
66
import { CartDrawer } from "@/components/cart/CartDrawer"
77
import { ToastProvider } from "@/components/ui/ToastProvider"
88
import { DemoBadge } from "@/components/demo/DemoBadge"
9+
import { DemoModePopup } from "@/components/demo/DemoModePopup"
10+
import { ClientThemeSync } from "@/components/demo/ClientThemeSync"
911
import { ToastContainer } from "@/components/ui/toast"
1012
import { requireEnv } from "@/lib/env"
11-
import { getActiveTenant, readSiteProfile } from "@/lib/cms"
13+
import { readSiteProfile } from "@/lib/cms"
1214

1315
const inter = Inter({ subsets: ["latin"] })
1416

1517
export async function generateMetadata(): Promise<Metadata> {
16-
const tenant = getActiveTenant()
17-
const profile = readSiteProfile(tenant)
18+
const profile = readSiteProfile()
1819
const title = profile?.seo?.title || profile?.siteName || "Cafe Template"
1920
const description = profile?.seo?.description || profile?.description || ""
2021

@@ -38,8 +39,7 @@ export default function RootLayout({
3839
}: Readonly<{
3940
children: React.ReactNode
4041
}>) {
41-
const tenant = getActiveTenant()
42-
const siteProfile = readSiteProfile(tenant)
42+
const siteProfile = readSiteProfile()
4343

4444
return (
4545
<html lang="en">
@@ -51,6 +51,8 @@ export default function RootLayout({
5151
</ToastProvider>
5252
<ToastContainer />
5353
<DemoBadge />
54+
<ClientThemeSync />
55+
<DemoModePopup />
5456
<CartDrawer />
5557
</body>
5658
</html>

app/manifest.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import type { MetadataRoute } from "next"
2-
import { getActiveTenant, readSiteProfile } from "@/lib/cms"
2+
import { readSiteProfile } from "@/lib/cms"
33

44
export default function manifest(): MetadataRoute.Manifest {
5-
const tenant = getActiveTenant()
6-
const profile = readSiteProfile(tenant)
5+
const profile = readSiteProfile()
76

87
return {
98
name: profile?.siteName || "Cafe Template",
@@ -14,4 +13,4 @@ export default function manifest(): MetadataRoute.Manifest {
1413
background_color: "#ffffff",
1514
theme_color: "#d97706",
1615
}
17-
}
16+
}

app/page.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1-
import { redirect } from "next/navigation"
2-
import { getActiveTenant } from "@/lib/cms"
1+
import { notFound } from "next/navigation"
2+
import { CmsBlockRenderer } from "@/components/cms/CmsRenderer"
3+
import { readCmsPageVariants } from "@/lib/cms"
4+
import { parseDemoState, resolvePageBlocks, resolveBlockData } from "@/lib/demo/demo-state"
35

4-
export const dynamic = "force-dynamic"
6+
interface PageProps {
7+
searchParams: Promise<{ theme?: string; layout?: string; text?: string }>
8+
}
59

6-
export default function RootPage() {
7-
const tenant = getActiveTenant()
8-
redirect(`/${tenant}`)
9-
}
10+
export default async function RootHomePage({ searchParams }: PageProps) {
11+
const sp = await searchParams
12+
const state = parseDemoState(new URLSearchParams(
13+
Object.entries(sp).filter(([, v]) => v != null).map(([k, v]) => `${k}=${v}`).join("&")
14+
))
15+
const page = readCmsPageVariants("home")
16+
17+
if (!page || page.variants.length === 0) {
18+
notFound()
19+
}
20+
21+
const blocks = resolvePageBlocks(page, state.layout)
22+
const resolvedBlocks = blocks.map((block, idx) => {
23+
const resolved = state.text ? resolveBlockData(block, state.text) : block
24+
return <CmsBlockRenderer key={`${resolved.type}-${idx}`} block={resolved} />
25+
})
26+
27+
return <>{resolvedBlocks}</>
28+
}

components/cms/ThemeProvider.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@ const MOTION_SPEED: Record<string, string> = {
1010
}
1111

1212
export function ThemeProvider({
13-
tenant,
1413
cssVars,
1514
children,
1615
}: {
17-
tenant: string
1816
cssVars?: Record<string, string>
1917
children: React.ReactNode
2018
}) {

0 commit comments

Comments
 (0)