@rovela-ai/sdk 0.19.3 → 0.19.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/server/admin-session.d.ts.map +1 -1
- package/dist/admin/server/admin-session.js +7 -0
- package/dist/admin/server/admin-session.js.map +1 -1
- package/dist/auth/server/customer-session.d.ts.map +1 -1
- package/dist/auth/server/customer-session.js +5 -0
- package/dist/auth/server/customer-session.js.map +1 -1
- package/dist/checkout/server/checkout-customer.d.ts.map +1 -1
- package/dist/checkout/server/checkout-customer.js +5 -0
- package/dist/checkout/server/checkout-customer.js.map +1 -1
- package/docs/changelog/README.md +53 -0
- package/docs/changelog/epoch-01-chrome-consent.md +63 -0
- package/docs/changelog/epoch-02-platform-commerce.md +108 -0
- package/docs/changelog/epoch-03-i18n-files.md +72 -0
- package/docs/changelog/epoch-04-emails.md +65 -0
- package/docs/changelog/epoch-05-admin-experience.md +98 -0
- package/docs/changelog/epoch-06-mobile-integrity.md +72 -0
- package/docs/design/admin-ledger-visual-universe.html +1961 -0
- package/docs/design/admin-sidebar-and-band.html +542 -0
- package/package.json +3 -1
- package/templates/store-template/.claude/skills/add-section/SKILL.md +438 -0
- package/templates/store-template/.claude/skills/build-feature/SKILL.md +201 -0
- package/templates/store-template/.claude/skills/customize-theme/SKILL.md +167 -0
- package/templates/store-template/.claude/skills/debug-errors/SKILL.md +68 -0
- package/templates/store-template/.claude/skills/frontend-design/SKILL.md +176 -0
- package/templates/store-template/.claude/skills/generate-images/SKILL.md +115 -0
- package/templates/store-template/.claude/skills/manage-products/SKILL.md +187 -0
- package/templates/store-template/.claude/skills/override-sdk-component/SKILL.md +154 -0
- package/templates/store-template/.env.example +50 -0
- package/templates/store-template/.mcp.json +8 -0
- package/templates/store-template/.rovela/blueprint.example.json +190 -0
- package/templates/store-template/CLAUDE.md +14 -0
- package/templates/store-template/app/LayoutContent.tsx +106 -0
- package/templates/store-template/app/[...path]/page.tsx +79 -0
- package/templates/store-template/app/account/addresses/page.tsx +45 -0
- package/templates/store-template/app/account/layout.tsx +17 -0
- package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +277 -0
- package/templates/store-template/app/account/orders/[id]/page.tsx +725 -0
- package/templates/store-template/app/account/orders/page.tsx +153 -0
- package/templates/store-template/app/account/page.tsx +184 -0
- package/templates/store-template/app/account/profile/page.tsx +434 -0
- package/templates/store-template/app/admin/(dashboard)/account/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/events/page.tsx +14 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/layout.tsx +48 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/visitors/page.tsx +14 -0
- package/templates/store-template/app/admin/(dashboard)/categories/[id]/page.tsx +38 -0
- package/templates/store-template/app/admin/(dashboard)/categories/new/page.tsx +35 -0
- package/templates/store-template/app/admin/(dashboard)/categories/page.tsx +223 -0
- package/templates/store-template/app/admin/(dashboard)/customers/[id]/page.tsx +26 -0
- package/templates/store-template/app/admin/(dashboard)/customers/page.tsx +33 -0
- package/templates/store-template/app/admin/(dashboard)/discounts/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/emails/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/files/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/layout.tsx +44 -0
- package/templates/store-template/app/admin/(dashboard)/loading.tsx +88 -0
- package/templates/store-template/app/admin/(dashboard)/locations/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/orders/[id]/page.tsx +66 -0
- package/templates/store-template/app/admin/(dashboard)/orders/page.tsx +26 -0
- package/templates/store-template/app/admin/(dashboard)/page.tsx +146 -0
- package/templates/store-template/app/admin/(dashboard)/payments/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/products/[id]/page.tsx +38 -0
- package/templates/store-template/app/admin/(dashboard)/products/new/page.tsx +35 -0
- package/templates/store-template/app/admin/(dashboard)/products/page.tsx +31 -0
- package/templates/store-template/app/admin/(dashboard)/settings/page.tsx +46 -0
- package/templates/store-template/app/admin/(dashboard)/shipping/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/tax/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/users/[id]/page.tsx +114 -0
- package/templates/store-template/app/admin/(dashboard)/users/page.tsx +16 -0
- package/templates/store-template/app/admin/accept-invite/page.tsx +36 -0
- package/templates/store-template/app/admin/forgot-password/page.tsx +13 -0
- package/templates/store-template/app/admin/layout.tsx +47 -0
- package/templates/store-template/app/admin/login/page.tsx +528 -0
- package/templates/store-template/app/admin/reset-password/page.tsx +36 -0
- package/templates/store-template/app/api/account/addresses/[id]/route.ts +10 -0
- package/templates/store-template/app/api/account/addresses/route.ts +10 -0
- package/templates/store-template/app/api/account/change-password/route.ts +107 -0
- package/templates/store-template/app/api/account/orders/[id]/refund/route.ts +11 -0
- package/templates/store-template/app/api/account/orders/[id]/return/route.ts +14 -0
- package/templates/store-template/app/api/account/orders/[id]/route.ts +42 -0
- package/templates/store-template/app/api/account/orders/route.ts +29 -0
- package/templates/store-template/app/api/account/profile/route.ts +165 -0
- package/templates/store-template/app/api/admin/accept-invite/route.ts +11 -0
- package/templates/store-template/app/api/admin/analytics/events/route.ts +9 -0
- package/templates/store-template/app/api/admin/analytics/route.ts +10 -0
- package/templates/store-template/app/api/admin/analytics/visitors/route.ts +9 -0
- package/templates/store-template/app/api/admin/branding/favicon/route.ts +9 -0
- package/templates/store-template/app/api/admin/branding/logo/route.ts +9 -0
- package/templates/store-template/app/api/admin/categories/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/categories/route.ts +9 -0
- package/templates/store-template/app/api/admin/check/route.ts +10 -0
- package/templates/store-template/app/api/admin/customer-groups/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/customer-groups/route.ts +10 -0
- package/templates/store-template/app/api/admin/customers/[id]/addresses/[addressId]/route.ts +15 -0
- package/templates/store-template/app/api/admin/customers/[id]/addresses/route.ts +15 -0
- package/templates/store-template/app/api/admin/customers/[id]/route.ts +11 -0
- package/templates/store-template/app/api/admin/customers/bulk/route.ts +8 -0
- package/templates/store-template/app/api/admin/customers/route.ts +9 -0
- package/templates/store-template/app/api/admin/discounts/[id]/route.ts +11 -0
- package/templates/store-template/app/api/admin/discounts/route.ts +10 -0
- package/templates/store-template/app/api/admin/emails/preview/route.ts +4 -0
- package/templates/store-template/app/api/admin/emails/route.ts +5 -0
- package/templates/store-template/app/api/admin/emails/test/route.ts +4 -0
- package/templates/store-template/app/api/admin/files/route.ts +8 -0
- package/templates/store-template/app/api/admin/forgot-password/route.ts +7 -0
- package/templates/store-template/app/api/admin/linked-customer/route.ts +11 -0
- package/templates/store-template/app/api/admin/locations/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/locations/route.ts +10 -0
- package/templates/store-template/app/api/admin/me/password/route.ts +13 -0
- package/templates/store-template/app/api/admin/me/route.ts +11 -0
- package/templates/store-template/app/api/admin/orders/[id]/refund/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/return/approve/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/return/reject/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/orders/[id]/ship-with-shippo/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/[id]/shippo-rates/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/bulk/route.ts +8 -0
- package/templates/store-template/app/api/admin/orders/route.ts +9 -0
- package/templates/store-template/app/api/admin/payment-methods/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/payment-methods/route.ts +10 -0
- package/templates/store-template/app/api/admin/products/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/bulk/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/stats/route.ts +10 -0
- package/templates/store-template/app/api/admin/reset-password/route.ts +10 -0
- package/templates/store-template/app/api/admin/settings/route.ts +11 -0
- package/templates/store-template/app/api/admin/setup/route.ts +10 -0
- package/templates/store-template/app/api/admin/setup-guide/manual-mark/route.ts +13 -0
- package/templates/store-template/app/api/admin/setup-guide/route.ts +10 -0
- package/templates/store-template/app/api/admin/shipping/carriers/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/carriers/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/rates/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/rates/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/shippo/api-key/route.ts +1 -0
- package/templates/store-template/app/api/admin/shipping/shippo/validate/route.ts +1 -0
- package/templates/store-template/app/api/admin/shipping/zones/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/zones/route.ts +9 -0
- package/templates/store-template/app/api/admin/stats/route.ts +9 -0
- package/templates/store-template/app/api/admin/stripe-status/route.ts +1 -0
- package/templates/store-template/app/api/admin/tax-zones/[id]/route.ts +7 -0
- package/templates/store-template/app/api/admin/tax-zones/route.ts +7 -0
- package/templates/store-template/app/api/admin/users/[id]/route.ts +14 -0
- package/templates/store-template/app/api/admin/users/route.ts +12 -0
- package/templates/store-template/app/api/admin-auth/[...nextauth]/route.ts +17 -0
- package/templates/store-template/app/api/analytics/track/route.ts +12 -0
- package/templates/store-template/app/api/auth/[...nextauth]/route.ts +19 -0
- package/templates/store-template/app/api/auth/forgot-password/route.ts +9 -0
- package/templates/store-template/app/api/auth/register/route.ts +9 -0
- package/templates/store-template/app/api/auth/resend-verification/route.ts +9 -0
- package/templates/store-template/app/api/auth/reset-password/route.ts +10 -0
- package/templates/store-template/app/api/auth/verify-email/route.ts +10 -0
- package/templates/store-template/app/api/categories/route.ts +10 -0
- package/templates/store-template/app/api/checkout/countries/route.ts +7 -0
- package/templates/store-template/app/api/checkout/discount/route.ts +9 -0
- package/templates/store-template/app/api/checkout/manual/route.ts +10 -0
- package/templates/store-template/app/api/checkout/payment-methods/route.ts +8 -0
- package/templates/store-template/app/api/checkout/pickup-locations/route.ts +8 -0
- package/templates/store-template/app/api/checkout/route.ts +10 -0
- package/templates/store-template/app/api/checkout/shipping/route.ts +21 -0
- package/templates/store-template/app/api/checkout/status/route.ts +10 -0
- package/templates/store-template/app/api/contact/route.ts +164 -0
- package/templates/store-template/app/api/media/presign/route.ts +7 -0
- package/templates/store-template/app/api/media/route.ts +7 -0
- package/templates/store-template/app/api/products/[slug]/route.ts +10 -0
- package/templates/store-template/app/api/products/route.ts +10 -0
- package/templates/store-template/app/api/store/settings/route.ts +12 -0
- package/templates/store-template/app/api/webhooks/stripe/route.ts +63 -0
- package/templates/store-template/app/auth/forgot-password/page.tsx +38 -0
- package/templates/store-template/app/auth/layout.tsx +17 -0
- package/templates/store-template/app/auth/reset-password/page.tsx +81 -0
- package/templates/store-template/app/auth/signin/page.tsx +62 -0
- package/templates/store-template/app/auth/signup/page.tsx +38 -0
- package/templates/store-template/app/auth/verify-email/page.tsx +185 -0
- package/templates/store-template/app/cart/page.tsx +137 -0
- package/templates/store-template/app/checkout/page.tsx +88 -0
- package/templates/store-template/app/checkout/success/page.tsx +50 -0
- package/templates/store-template/app/contact/layout.tsx +24 -0
- package/templates/store-template/app/contact/page.tsx +321 -0
- package/templates/store-template/app/faq/page.tsx +153 -0
- package/templates/store-template/app/globals.css +99 -0
- package/templates/store-template/app/layout.tsx +269 -0
- package/templates/store-template/app/not-found.tsx +35 -0
- package/templates/store-template/app/page.tsx +23 -0
- package/templates/store-template/app/privacy/page.tsx +281 -0
- package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +179 -0
- package/templates/store-template/app/products/[slug]/page.tsx +149 -0
- package/templates/store-template/app/products/layout.tsx +26 -0
- package/templates/store-template/app/products/page.tsx +102 -0
- package/templates/store-template/app/robots.ts +25 -0
- package/templates/store-template/app/sitemap.ts +76 -0
- package/templates/store-template/app/terms/page.tsx +313 -0
- package/templates/store-template/components/PathSyncBridge.tsx +61 -0
- package/templates/store-template/components/Providers.tsx +37 -0
- package/templates/store-template/components/layout/Footer.tsx +150 -0
- package/templates/store-template/components/layout/Header.tsx +162 -0
- package/templates/store-template/components/sections/.gitkeep +7 -0
- package/templates/store-template/components/sections/CTA.tsx +51 -0
- package/templates/store-template/components/sections/Features.tsx +91 -0
- package/templates/store-template/components/sections/Hero.tsx +85 -0
- package/templates/store-template/eslint.config.mjs +18 -0
- package/templates/store-template/lib/store-config.ts +77 -0
- package/templates/store-template/lib/store-policy.ts +68 -0
- package/templates/store-template/lib/utils.ts +21 -0
- package/templates/store-template/next-env.d.ts +6 -0
- package/templates/store-template/next.config.js +105 -0
- package/templates/store-template/package.json +32 -0
- package/templates/store-template/postcss.config.js +6 -0
- package/templates/store-template/proxy.ts +150 -0
- package/templates/store-template/public/rovela-logo.png +0 -0
- package/templates/store-template/tailwind.config.ts +74 -0
- package/templates/store-template/tsconfig.json +41 -0
- package/templates/store-template/types/next-auth.d.ts +32 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global CSS Styles
|
|
3
|
+
*
|
|
4
|
+
* NOTE: This file is a placeholder. Claude Agent will regenerate it using:
|
|
5
|
+
* import { generateGlobalsCss } from '@rovela-ai/sdk/theme'
|
|
6
|
+
* const css = generateGlobalsCss(blueprint.theme)
|
|
7
|
+
*
|
|
8
|
+
* This default configuration provides basic styling for the initial build.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
@tailwind base;
|
|
12
|
+
@tailwind components;
|
|
13
|
+
@tailwind utilities;
|
|
14
|
+
|
|
15
|
+
@layer base {
|
|
16
|
+
:root {
|
|
17
|
+
/* Default color palette (neutral gray) */
|
|
18
|
+
--background: 0 0% 100%;
|
|
19
|
+
--foreground: 0 0% 3.9%;
|
|
20
|
+
|
|
21
|
+
--card: 0 0% 100%;
|
|
22
|
+
--card-foreground: 0 0% 3.9%;
|
|
23
|
+
|
|
24
|
+
--popover: 0 0% 100%;
|
|
25
|
+
--popover-foreground: 0 0% 3.9%;
|
|
26
|
+
|
|
27
|
+
--primary: 0 0% 9%;
|
|
28
|
+
--primary-foreground: 0 0% 98%;
|
|
29
|
+
|
|
30
|
+
--secondary: 0 0% 96.1%;
|
|
31
|
+
--secondary-foreground: 0 0% 9%;
|
|
32
|
+
|
|
33
|
+
--muted: 0 0% 96.1%;
|
|
34
|
+
--muted-foreground: 0 0% 45.1%;
|
|
35
|
+
|
|
36
|
+
--accent: 0 0% 96.1%;
|
|
37
|
+
--accent-foreground: 0 0% 9%;
|
|
38
|
+
|
|
39
|
+
--destructive: 0 84.2% 60.2%;
|
|
40
|
+
--destructive-foreground: 0 0% 98%;
|
|
41
|
+
|
|
42
|
+
--border: 0 0% 89.8%;
|
|
43
|
+
--input: 0 0% 89.8%;
|
|
44
|
+
--ring: 0 0% 3.9%;
|
|
45
|
+
|
|
46
|
+
--radius: 0.5rem;
|
|
47
|
+
|
|
48
|
+
/* Font variables */
|
|
49
|
+
--font-heading: 'Inter', system-ui, sans-serif;
|
|
50
|
+
--font-body: 'Inter', system-ui, sans-serif;
|
|
51
|
+
--font-mono: 'JetBrains Mono', monospace;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.dark {
|
|
55
|
+
--background: 0 0% 3.9%;
|
|
56
|
+
--foreground: 0 0% 98%;
|
|
57
|
+
|
|
58
|
+
--card: 0 0% 3.9%;
|
|
59
|
+
--card-foreground: 0 0% 98%;
|
|
60
|
+
|
|
61
|
+
--popover: 0 0% 3.9%;
|
|
62
|
+
--popover-foreground: 0 0% 98%;
|
|
63
|
+
|
|
64
|
+
--primary: 0 0% 98%;
|
|
65
|
+
--primary-foreground: 0 0% 9%;
|
|
66
|
+
|
|
67
|
+
--secondary: 0 0% 14.9%;
|
|
68
|
+
--secondary-foreground: 0 0% 98%;
|
|
69
|
+
|
|
70
|
+
--muted: 0 0% 14.9%;
|
|
71
|
+
--muted-foreground: 0 0% 63.9%;
|
|
72
|
+
|
|
73
|
+
--accent: 0 0% 14.9%;
|
|
74
|
+
--accent-foreground: 0 0% 98%;
|
|
75
|
+
|
|
76
|
+
--destructive: 0 62.8% 30.6%;
|
|
77
|
+
--destructive-foreground: 0 0% 98%;
|
|
78
|
+
|
|
79
|
+
--border: 0 0% 14.9%;
|
|
80
|
+
--input: 0 0% 14.9%;
|
|
81
|
+
--ring: 0 0% 83.1%;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@layer base {
|
|
86
|
+
* {
|
|
87
|
+
@apply border-border;
|
|
88
|
+
}
|
|
89
|
+
body {
|
|
90
|
+
@apply bg-background text-foreground;
|
|
91
|
+
font-family: var(--font-body);
|
|
92
|
+
}
|
|
93
|
+
h1, h2, h3, h4, h5, h6 {
|
|
94
|
+
font-family: var(--font-heading);
|
|
95
|
+
}
|
|
96
|
+
code, pre {
|
|
97
|
+
font-family: var(--font-mono);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { Inter } from 'next/font/google'
|
|
3
|
+
import './globals.css'
|
|
4
|
+
|
|
5
|
+
import { headers, cookies } from 'next/headers'
|
|
6
|
+
import { redirect, permanentRedirect } from 'next/navigation'
|
|
7
|
+
import { Providers } from '@/components/Providers'
|
|
8
|
+
import { LayoutContent } from './LayoutContent'
|
|
9
|
+
import { getBlueprint, findSettings } from '@rovela-ai/sdk/core/server'
|
|
10
|
+
import {
|
|
11
|
+
resolveStoreLocaleConfig,
|
|
12
|
+
resolveRequestLocale,
|
|
13
|
+
resolveLocaleRedirect,
|
|
14
|
+
stripLocalePrefix,
|
|
15
|
+
LOCALE_HEADER,
|
|
16
|
+
GEO_LOCALE_HEADER,
|
|
17
|
+
PATHNAME_HEADER,
|
|
18
|
+
LOCALE_COOKIE,
|
|
19
|
+
} from '@rovela-ai/sdk/core'
|
|
20
|
+
import { localizedAlternates } from '@rovela-ai/sdk/core/server'
|
|
21
|
+
import { buildStoreJsonLdGraph, serializeJsonLd } from '@rovela-ai/sdk/core'
|
|
22
|
+
import { supportEmail, returnPolicyInput, shipsToCountries } from '@/lib/store-policy'
|
|
23
|
+
import { sanitizeCookieConsentConfig, consentRegionExempt } from '@rovela-ai/sdk/core/cookie-consent'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Font Configuration
|
|
27
|
+
*
|
|
28
|
+
* NOTE: Claude Agent will modify this based on blueprint.theme.typography
|
|
29
|
+
* The fonts below are defaults that work for most stores.
|
|
30
|
+
*/
|
|
31
|
+
const fontBody = Inter({
|
|
32
|
+
subsets: ['latin'],
|
|
33
|
+
variable: '--font-body',
|
|
34
|
+
display: 'swap',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const fontHeading = Inter({
|
|
38
|
+
subsets: ['latin'],
|
|
39
|
+
variable: '--font-heading',
|
|
40
|
+
weight: ['500', '600', '700'],
|
|
41
|
+
display: 'swap',
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Dynamic Metadata Configuration
|
|
46
|
+
*
|
|
47
|
+
* Reads store name from STORE_NAME env var (updated by branding editor)
|
|
48
|
+
* Favicon: prefers NEXT_PUBLIC_STORE_FAVICON_URL (custom upload),
|
|
49
|
+
* falls back to NEXT_PUBLIC_STORE_LOGO_URL (logo doubles as favicon).
|
|
50
|
+
* Merchants who don't upload a separate favicon get their logo as favicon
|
|
51
|
+
* by default.
|
|
52
|
+
* Falls back to blueprint data for SEO description and keywords.
|
|
53
|
+
*/
|
|
54
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
55
|
+
// Try to load blueprint for rich SEO data
|
|
56
|
+
let blueprint = null
|
|
57
|
+
try {
|
|
58
|
+
blueprint = getBlueprint()
|
|
59
|
+
} catch {
|
|
60
|
+
// Blueprint might not exist during initial setup
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Live store settings — settings-first identity (name, favicon, logo).
|
|
64
|
+
// What the merchant edits in /admin must reach metadata without a redeploy.
|
|
65
|
+
// Fail-open: a DB hiccup falls back to env/blueprint.
|
|
66
|
+
const brandingSettings = await findSettings().catch(() => null)
|
|
67
|
+
|
|
68
|
+
// Store name: LIVE settings first, then the build-time env var, then blueprint.
|
|
69
|
+
const storeName =
|
|
70
|
+
brandingSettings?.storeName || process.env.STORE_NAME || blueprint?.store?.name || 'Store'
|
|
71
|
+
|
|
72
|
+
// SEO data from blueprint with fallbacks
|
|
73
|
+
const seoTitle = blueprint?.seo?.title || storeName
|
|
74
|
+
const seoDescription =
|
|
75
|
+
blueprint?.seo?.description ||
|
|
76
|
+
blueprint?.store?.description ||
|
|
77
|
+
`Shop at ${storeName}`
|
|
78
|
+
const seoKeywords = blueprint?.seo?.keywords || []
|
|
79
|
+
|
|
80
|
+
// Favicon: live store settings first (favicon_url / logo_url — updated
|
|
81
|
+
// instantly by the /admin branding page AND mirrored by the platform
|
|
82
|
+
// branding routes), then the build-time env vars for legacy stores whose
|
|
83
|
+
// settings row predates the mirror. Custom favicon wins; logo is the
|
|
84
|
+
// fallback mark. Fail-open — a DB hiccup falls back to env.
|
|
85
|
+
const faviconUrl =
|
|
86
|
+
brandingSettings?.faviconUrl ||
|
|
87
|
+
process.env.NEXT_PUBLIC_STORE_FAVICON_URL ||
|
|
88
|
+
brandingSettings?.logoUrl ||
|
|
89
|
+
process.env.NEXT_PUBLIC_STORE_LOGO_URL ||
|
|
90
|
+
null
|
|
91
|
+
|
|
92
|
+
// OpenGraph / Twitter share image: prefer a blueprint-supplied OG image,
|
|
93
|
+
// fall back to the store logo. Closes the "no OG image on storefronts" gap.
|
|
94
|
+
const storeUrl = process.env.NEXT_PUBLIC_APP_URL || ''
|
|
95
|
+
const ogImage =
|
|
96
|
+
blueprint?.seo?.ogImage ||
|
|
97
|
+
brandingSettings?.logoUrl ||
|
|
98
|
+
process.env.NEXT_PUBLIC_STORE_LOGO_URL ||
|
|
99
|
+
faviconUrl ||
|
|
100
|
+
null
|
|
101
|
+
let metadataBase: URL | undefined
|
|
102
|
+
try {
|
|
103
|
+
metadataBase = storeUrl ? new URL(storeUrl) : undefined
|
|
104
|
+
} catch {
|
|
105
|
+
metadataBase = undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Locale alternates (self-canonical + hreflang) for every page that does
|
|
109
|
+
// not set its own — pages exporting `alternates` (PDP, listing, contact,
|
|
110
|
+
// legal, FAQ) override this field. Covers the homepage and any future page
|
|
111
|
+
// with zero per-page work. The header carries the ORIGINAL request path
|
|
112
|
+
// (locale prefix included); strip it to get the bare path.
|
|
113
|
+
let alternates: Metadata['alternates'] | undefined
|
|
114
|
+
try {
|
|
115
|
+
const hdrs = await headers()
|
|
116
|
+
const originalPath = hdrs.get(PATHNAME_HEADER) || '/'
|
|
117
|
+
alternates = await localizedAlternates(stripLocalePrefix(originalPath))
|
|
118
|
+
} catch {
|
|
119
|
+
// headers() unavailable (build-time) — omit alternates.
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
...(metadataBase && { metadataBase }),
|
|
124
|
+
...(alternates && { alternates }),
|
|
125
|
+
title: {
|
|
126
|
+
default: seoTitle,
|
|
127
|
+
template: `%s | ${storeName}`,
|
|
128
|
+
},
|
|
129
|
+
description: seoDescription,
|
|
130
|
+
keywords: seoKeywords,
|
|
131
|
+
// Maximum rich-result + AI Overview eligibility: allow full snippets and
|
|
132
|
+
// large image previews for every search + AI shopping crawler.
|
|
133
|
+
robots: {
|
|
134
|
+
index: true,
|
|
135
|
+
follow: true,
|
|
136
|
+
googleBot: {
|
|
137
|
+
index: true,
|
|
138
|
+
follow: true,
|
|
139
|
+
'max-image-preview': 'large',
|
|
140
|
+
'max-snippet': -1,
|
|
141
|
+
'max-video-preview': -1,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
...(faviconUrl && {
|
|
145
|
+
icons: {
|
|
146
|
+
icon: faviconUrl,
|
|
147
|
+
shortcut: faviconUrl,
|
|
148
|
+
apple: faviconUrl,
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
openGraph: {
|
|
152
|
+
siteName: storeName,
|
|
153
|
+
type: 'website',
|
|
154
|
+
title: seoTitle,
|
|
155
|
+
description: seoDescription,
|
|
156
|
+
...(storeUrl && { url: storeUrl }),
|
|
157
|
+
...(ogImage && { images: [{ url: ogImage }] }),
|
|
158
|
+
},
|
|
159
|
+
twitter: {
|
|
160
|
+
card: 'summary_large_image',
|
|
161
|
+
title: seoTitle,
|
|
162
|
+
description: seoDescription,
|
|
163
|
+
...(ogImage && { images: [ogImage] }),
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Root Layout
|
|
170
|
+
*
|
|
171
|
+
* This layout wraps all pages and provides:
|
|
172
|
+
* - Providers for SessionProvider (NextAuth)
|
|
173
|
+
* - LayoutContent for route-aware rendering (separates admin vs customer)
|
|
174
|
+
*
|
|
175
|
+
* Admin routes (/admin/*) get minimal wrapper - AdminLayout handles its own chrome.
|
|
176
|
+
* Customer routes get full store experience with Header, Footer, Cart.
|
|
177
|
+
*
|
|
178
|
+
* This separation ensures clear disconnection between admin panel and store.
|
|
179
|
+
*/
|
|
180
|
+
export default async function RootLayout({
|
|
181
|
+
children,
|
|
182
|
+
}: {
|
|
183
|
+
children: React.ReactNode
|
|
184
|
+
}) {
|
|
185
|
+
// R021: read the merchant's cookie-banner master toggle once per
|
|
186
|
+
// request. Defaults to `true` for safety — if settings aren't seeded
|
|
187
|
+
// yet (very first render on a brand-new store) the banner shows and
|
|
188
|
+
// the merchant can turn it off in admin Settings.
|
|
189
|
+
let cookieBannerEnabled = true
|
|
190
|
+
let cookieConsentConfig: ReturnType<typeof sanitizeCookieConsentConfig> = {}
|
|
191
|
+
let settings: Awaited<ReturnType<typeof findSettings>> = null
|
|
192
|
+
try {
|
|
193
|
+
settings = await findSettings()
|
|
194
|
+
if (settings) {
|
|
195
|
+
cookieBannerEnabled = !!settings.cookieBannerEnabled
|
|
196
|
+
cookieConsentConfig = sanitizeCookieConsentConfig(settings.cookieConsent)
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// Settings table not reachable — fail open (banner shows). Safer
|
|
200
|
+
// than failing closed in EU jurisdictions.
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Locale (i18n core) ────────────────────────────────────────────────────
|
|
204
|
+
// The proxy parsed any /fr prefix into headers; here (where settings are
|
|
205
|
+
// available) we pick the render locale and apply locale-correctness
|
|
206
|
+
// redirects: strip prefixes the store doesn't serve, and route a visitor
|
|
207
|
+
// preference (explicit cookie, else IP-country hint) from a bare URL to
|
|
208
|
+
// its prefixed equivalent. See SDK core/i18n/resolve for the contract.
|
|
209
|
+
const hdrs = await headers()
|
|
210
|
+
const cookieStore = await cookies()
|
|
211
|
+
const localeConfig = resolveStoreLocaleConfig(settings)
|
|
212
|
+
const localeInput = {
|
|
213
|
+
prefixLocale: hdrs.get(LOCALE_HEADER),
|
|
214
|
+
cookieLocale: cookieStore.get(LOCALE_COOKIE)?.value,
|
|
215
|
+
geoLocale: hdrs.get(GEO_LOCALE_HEADER),
|
|
216
|
+
config: localeConfig,
|
|
217
|
+
}
|
|
218
|
+
const localeRedirect = resolveLocaleRedirect({
|
|
219
|
+
...localeInput,
|
|
220
|
+
pathname: hdrs.get(PATHNAME_HEADER),
|
|
221
|
+
})
|
|
222
|
+
if (localeRedirect) {
|
|
223
|
+
// 308 for structural strips (URL shapes the store never serves), 307 for
|
|
224
|
+
// visitor-preference hops (cookie/geo dependent — never cacheable).
|
|
225
|
+
if (localeRedirect.permanent) permanentRedirect(localeRedirect.path)
|
|
226
|
+
redirect(localeRedirect.path)
|
|
227
|
+
}
|
|
228
|
+
const locale = resolveRequestLocale(localeInput)
|
|
229
|
+
|
|
230
|
+
// Site-wide Organization + WebSite JSON-LD (brand identity for AI assistants).
|
|
231
|
+
// Server-rendered so non-JS crawlers (GPTBot / OAI-SearchBot) can read it.
|
|
232
|
+
const storeJsonLd = buildStoreJsonLdGraph({
|
|
233
|
+
storeUrl: process.env.NEXT_PUBLIC_APP_URL || '',
|
|
234
|
+
storeName: settings?.storeName || process.env.STORE_NAME || 'Store',
|
|
235
|
+
logoUrl: settings?.logoUrl || process.env.NEXT_PUBLIC_STORE_LOGO_URL || null,
|
|
236
|
+
// Enriched entity signals (contactPoint + return policy + areaServed) from
|
|
237
|
+
// real configured data — see lib/store-policy. All optional in the builder,
|
|
238
|
+
// so a brand-new store with no settings still emits a valid base graph.
|
|
239
|
+
contactEmail: supportEmail(settings),
|
|
240
|
+
returnPolicy: returnPolicyInput(settings),
|
|
241
|
+
shipsToCountries: shipsToCountries(settings),
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<html lang={locale} className={`${fontBody.variable} ${fontHeading.variable}`}>
|
|
246
|
+
<body className="min-h-screen flex flex-col antialiased">
|
|
247
|
+
<script
|
|
248
|
+
type="application/ld+json"
|
|
249
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(storeJsonLd) }}
|
|
250
|
+
/>
|
|
251
|
+
<Providers>
|
|
252
|
+
<LayoutContent
|
|
253
|
+
cookieBannerEnabled={cookieBannerEnabled}
|
|
254
|
+
cookieConsentConfig={cookieConsentConfig}
|
|
255
|
+
consentRegionExempt={consentRegionExempt(
|
|
256
|
+
cookieConsentConfig,
|
|
257
|
+
hdrs.get('x-rovela-country')
|
|
258
|
+
)}
|
|
259
|
+
locale={locale}
|
|
260
|
+
defaultLocale={localeConfig.defaultLocale}
|
|
261
|
+
enabledLocales={localeConfig.enabledLocales}
|
|
262
|
+
>
|
|
263
|
+
{children}
|
|
264
|
+
</LayoutContent>
|
|
265
|
+
</Providers>
|
|
266
|
+
</body>
|
|
267
|
+
</html>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Localized 404 page — server component. Without this file Next.js serves
|
|
3
|
+
* its default English, unbranded 404. Locale comes from the proxy header via
|
|
4
|
+
* getRequestLocale (fail-open to the store default), so /fr/unknown-path
|
|
5
|
+
* renders French chrome.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { LocaleLink as Link, getT } from '@rovela-ai/sdk/core'
|
|
9
|
+
import { getRequestLocale } from '@rovela-ai/sdk/core/server'
|
|
10
|
+
|
|
11
|
+
export default async function NotFound() {
|
|
12
|
+
const { locale } = await getRequestLocale().catch(() => ({ locale: 'en' as const }))
|
|
13
|
+
const t = getT(locale)
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
|
17
|
+
<div className="mx-auto max-w-md text-center">
|
|
18
|
+
<p className="text-sm font-semibold text-primary">404</p>
|
|
19
|
+
<h1 className="mt-2 text-3xl font-bold font-heading">{t('notFound.title')}</h1>
|
|
20
|
+
<p className="mt-4 text-muted-foreground">{t('notFound.body')}</p>
|
|
21
|
+
<div className="mt-8 flex items-center justify-center gap-4">
|
|
22
|
+
<Link
|
|
23
|
+
href="/"
|
|
24
|
+
className="rounded-md bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
|
25
|
+
>
|
|
26
|
+
{t('notFound.backHome')}
|
|
27
|
+
</Link>
|
|
28
|
+
<Link href="/products" className="text-sm font-medium text-primary hover:underline">
|
|
29
|
+
{t('footer.allProducts')}
|
|
30
|
+
</Link>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Homepage
|
|
5
|
+
*
|
|
6
|
+
* Pre-baked with theme-aware sections for instant branded preview.
|
|
7
|
+
* Claude Code will replace these section components with custom versions
|
|
8
|
+
* that include business-specific content from the questionnaire and brand identity.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import Hero from '@/components/sections/Hero'
|
|
12
|
+
import Features from '@/components/sections/Features'
|
|
13
|
+
import CTA from '@/components/sections/CTA'
|
|
14
|
+
|
|
15
|
+
export default function HomePage() {
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<Hero />
|
|
19
|
+
<Features />
|
|
20
|
+
<CTA />
|
|
21
|
+
</>
|
|
22
|
+
)
|
|
23
|
+
}
|