@rovela-ai/sdk 0.19.4 → 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/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,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the store's shipping / returns / contact facts,
|
|
3
|
+
* derived from real configured data (lib/store-config.ts + store_settings).
|
|
4
|
+
*
|
|
5
|
+
* Both the Organization JSON-LD (app/layout.tsx) and the visible + marked-up
|
|
6
|
+
* FAQ (app/faq/page.tsx) read from here, so the structured data ALWAYS mirrors
|
|
7
|
+
* the visible page content — the invariant search + AI engines require (a
|
|
8
|
+
* mismatch gets the listing suppressed).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { findSettings } from '@rovela-ai/sdk/core/server'
|
|
12
|
+
import type { ReturnPolicyInput } from '@rovela-ai/sdk/core'
|
|
13
|
+
import { storeConfig } from '@/lib/store-config'
|
|
14
|
+
|
|
15
|
+
type Settings = Awaited<ReturnType<typeof findSettings>>
|
|
16
|
+
|
|
17
|
+
/** Parse `storeConfig.orders.returnPolicy` ('30-days' | 'no-returns' | '14-days' …) → days. */
|
|
18
|
+
export function returnWindowDays(): number {
|
|
19
|
+
const p = String(storeConfig.orders?.returnPolicy ?? '30-days')
|
|
20
|
+
if (/no[-\s]?return/i.test(p)) return 0
|
|
21
|
+
const m = p.match(/(\d+)/)
|
|
22
|
+
return m ? parseInt(m[1], 10) : 30
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Conservative default — merchant states the real policy on /faq + legal. */
|
|
26
|
+
export const RETURN_FEES: 'free' | 'customer-paid' = 'customer-paid'
|
|
27
|
+
|
|
28
|
+
export function shipsToCountries(settings: Settings): string[] {
|
|
29
|
+
return settings?.shippingCountries && settings.shippingCountries.length > 0
|
|
30
|
+
? settings.shippingCountries
|
|
31
|
+
: ['US']
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function freeShippingThreshold(settings: Settings): number {
|
|
35
|
+
return settings?.freeShippingThreshold && settings.freeShippingThreshold > 0
|
|
36
|
+
? settings.freeShippingThreshold
|
|
37
|
+
: 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Public support email — merchant's own only. Never the `support@store.com`
|
|
42
|
+
* placeholder or a Rovela address. Falls back to support@<live-domain>.
|
|
43
|
+
*/
|
|
44
|
+
export function supportEmail(settings: Settings): string | null {
|
|
45
|
+
const fromSettings = settings?.storeEmail?.trim()
|
|
46
|
+
if (fromSettings) return fromSettings
|
|
47
|
+
const cfg = storeConfig.supportEmail
|
|
48
|
+
if (cfg && cfg !== 'support@store.com') return cfg
|
|
49
|
+
const host = (process.env.NEXT_PUBLIC_APP_URL || '')
|
|
50
|
+
.replace(/^https?:\/\//, '')
|
|
51
|
+
.replace(/\/.*$/, '')
|
|
52
|
+
.trim()
|
|
53
|
+
const ownDomain =
|
|
54
|
+
!!host &&
|
|
55
|
+
!host.endsWith('rovela.app') &&
|
|
56
|
+
!host.endsWith('vercel.app') &&
|
|
57
|
+
!host.startsWith('localhost')
|
|
58
|
+
return ownDomain ? `support@${host}` : null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Return-policy shape for `buildStoreJsonLdGraph`. Days `0` ⇒ "returns not permitted". */
|
|
62
|
+
export function returnPolicyInput(settings: Settings): ReturnPolicyInput {
|
|
63
|
+
return {
|
|
64
|
+
merchantReturnDays: returnWindowDays(),
|
|
65
|
+
returnFees: RETURN_FEES,
|
|
66
|
+
applicableCountry: shipsToCountries(settings)[0] || 'US',
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* Re-exports utilities from @rovela-ai/sdk/core for convenience.
|
|
5
|
+
* Add any store-specific utilities below the re-exports.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export SDK utilities
|
|
9
|
+
export {
|
|
10
|
+
cn,
|
|
11
|
+
formatPrice,
|
|
12
|
+
parsePrice,
|
|
13
|
+
generateSlug,
|
|
14
|
+
formatDate,
|
|
15
|
+
formatRelativeTime,
|
|
16
|
+
truncate,
|
|
17
|
+
capitalize,
|
|
18
|
+
titleCase,
|
|
19
|
+
isValidEmail,
|
|
20
|
+
isValidPhone,
|
|
21
|
+
} from '@rovela-ai/sdk/core'
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** @type {import('next').NextConfig} */
|
|
2
|
+
const nextConfig = {
|
|
3
|
+
// Enable transpilation of the SDK package
|
|
4
|
+
transpilePackages: ['@rovela-ai/sdk'],
|
|
5
|
+
|
|
6
|
+
// Next.js 16 blocks dev-only requests (HMR WebSocket included) from any
|
|
7
|
+
// origin other than `localhost` by default. Sandbox previews are served
|
|
8
|
+
// cross-origin via Blaxel's proxy on hostnames like
|
|
9
|
+
// <hash>.<region>.preview.bl.run (e.g. abc...88.us-was-1.preview.bl.run)
|
|
10
|
+
// The matcher Next uses (server/app-render/csrf-protection.js → `isCsrfOriginAllowed`)
|
|
11
|
+
// is segment-by-segment: a single `*` matches ONE label only, so
|
|
12
|
+
// `*.preview.bl.run` matches `foo.preview.bl.run` but NOT
|
|
13
|
+
// `foo.us-was-1.preview.bl.run` (two leading labels). Use `**` for
|
|
14
|
+
// cross-label matching — survives Blaxel adding new regions later.
|
|
15
|
+
// See: https://nextjs.org/docs/app/api-reference/config/next-config-js/allowedDevOrigins
|
|
16
|
+
allowedDevOrigins: ['**.preview.bl.run', '**.bl.run'],
|
|
17
|
+
|
|
18
|
+
// Hide Next.js dev badge — merchants see the sandbox preview during edit
|
|
19
|
+
// sessions and the badge looks like a bug to non-technical users. No-op in
|
|
20
|
+
// production builds (badge only renders in `next dev`).
|
|
21
|
+
devIndicators: false,
|
|
22
|
+
|
|
23
|
+
// Skip TypeScript build-time check. The sandbox runs `next dev` continuously
|
|
24
|
+
// during merchant edits — type errors surface immediately in the preview, so
|
|
25
|
+
// by the time we redeploy, the code is already validated. Re-checking on
|
|
26
|
+
// Vercel's build container is wasted time. Belt-and-suspenders only since
|
|
27
|
+
// Turbopack itself doesn't run tsc.
|
|
28
|
+
typescript: { ignoreBuildErrors: true },
|
|
29
|
+
|
|
30
|
+
// THE WIN: Turbopack filesystem cache for production builds. Vercel
|
|
31
|
+
// preserves `.next/cache` between deploys; without this flag, Turbopack
|
|
32
|
+
// ignores it and rebuilds everything from scratch each time (~55s).
|
|
33
|
+
// With the flag, warm rebuilds reuse cached compiler artifacts and drop
|
|
34
|
+
// to ~10-15s.
|
|
35
|
+
//
|
|
36
|
+
// Empirically validated (2026-05-13): sandbox build time goes from 24s
|
|
37
|
+
// warm → 5s warm with this flag enabled. Translation for Vercel:
|
|
38
|
+
// first deploy of a new project stays ~55s (cold), every subsequent
|
|
39
|
+
// deploy drops to ~10-15s. Opt-in experimental flag in Next.js 16,
|
|
40
|
+
// expected to graduate to stable.
|
|
41
|
+
experimental: {
|
|
42
|
+
turbopackFileSystemCacheForBuild: true,
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Ship the blueprint with EVERY serverless function. The email theme
|
|
46
|
+
// derives brand colors/fonts by reading .rovela/blueprint.json at runtime
|
|
47
|
+
// through a bundler-invisible loader (the emails graph is client-reachable,
|
|
48
|
+
// so it cannot statically import fs) -- which also makes the read invisible
|
|
49
|
+
// to Vercel's file tracer. Without this, only routes that statically import
|
|
50
|
+
// the SDK's core/config get the file, and every other function (checkout,
|
|
51
|
+
// webhooks, auth, admin emails) silently falls back to default styling.
|
|
52
|
+
outputFileTracingIncludes: {
|
|
53
|
+
'/**': ['./.rovela/blueprint.json'],
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Image domains for Unsplash, CDNs, and R2 storage
|
|
57
|
+
images: {
|
|
58
|
+
remotePatterns: [
|
|
59
|
+
{
|
|
60
|
+
protocol: 'https',
|
|
61
|
+
hostname: 'images.unsplash.com',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
protocol: 'https',
|
|
65
|
+
hostname: '*.unsplash.com',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
protocol: 'https',
|
|
69
|
+
hostname: '*.supabase.co',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
protocol: 'https',
|
|
73
|
+
hostname: '*.cloudflare.com',
|
|
74
|
+
},
|
|
75
|
+
// R2 storage domains
|
|
76
|
+
{
|
|
77
|
+
protocol: 'https',
|
|
78
|
+
hostname: '*.r2.dev',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
protocol: 'https',
|
|
82
|
+
hostname: '*.r2.cloudflarestorage.com',
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Serve the AI-assistant identity file (public/llms.txt, written during
|
|
88
|
+
// generation) as Markdown, and keep it out of the search index — it's an
|
|
89
|
+
// identity / anti-hallucination signal for ChatGPT / Perplexity / Claude,
|
|
90
|
+
// not a ranking page.
|
|
91
|
+
async headers() {
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
source: '/llms.txt',
|
|
95
|
+
headers: [
|
|
96
|
+
{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' },
|
|
97
|
+
{ key: 'X-Robots-Tag', value: 'noindex' },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = nextConfig
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "store",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "eslint ."
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@rovela-ai/sdk": "0.19.4",
|
|
13
|
+
"@tailwindcss/forms": "0.5.11",
|
|
14
|
+
"@tailwindcss/typography": "0.5.19",
|
|
15
|
+
"autoprefixer": "10.4.24",
|
|
16
|
+
"lucide-react": "0.555.0",
|
|
17
|
+
"next": "16.1.6",
|
|
18
|
+
"next-auth": "4.24.13",
|
|
19
|
+
"postcss": "8.5.6",
|
|
20
|
+
"react": "19.2.4",
|
|
21
|
+
"react-dom": "19.2.4",
|
|
22
|
+
"tailwindcss": "3.4.19"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "24.10.13",
|
|
26
|
+
"@types/react": "19.2.14",
|
|
27
|
+
"@types/react-dom": "19.2.3",
|
|
28
|
+
"eslint": "9.39.2",
|
|
29
|
+
"eslint-config-next": "16.1.6",
|
|
30
|
+
"typescript": "5.9.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
// Pure locale data (no React, edge-safe). SINGLE SOURCE: adding a language
|
|
3
|
+
// to the SDK automatically extends prefix parsing + geo detection here.
|
|
4
|
+
import { SUPPORTED_LOCALES, COUNTRY_TO_LOCALE } from '@rovela-ai/sdk/locales'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Canonical-host redirect (SEO / trust).
|
|
8
|
+
*
|
|
9
|
+
* A deployed store answers on several hosts that all serve identical content:
|
|
10
|
+
* - the Rovela deployment URL ( *.rovela.app )
|
|
11
|
+
* - Vercel's deployment aliases ( *.vercel.app, including per-deploy hashes )
|
|
12
|
+
* - the merchant's own custom domain ( e.g. mystore.com ), once verified
|
|
13
|
+
*
|
|
14
|
+
* With no single canonical host, search engines and AI shopping crawlers index
|
|
15
|
+
* EVERY host as a separate site — duplicate content, diluted ranking, and the
|
|
16
|
+
* platform's *.rovela.app / *.vercel.app URLs surfacing in Google instead of the
|
|
17
|
+
* merchant's brand. (This is exactly what happened to early stores.)
|
|
18
|
+
*
|
|
19
|
+
* Fix: derive the canonical host from NEXT_PUBLIC_APP_URL — the merchant's live
|
|
20
|
+
* URL, which the platform points at the custom domain once it verifies and then
|
|
21
|
+
* triggers a rebuild so this value is re-inlined. When that canonical host is a
|
|
22
|
+
* real custom domain and a request arrives on a DIFFERENT host, 308-redirect to
|
|
23
|
+
* the canonical host (preserving path, query, method, and body).
|
|
24
|
+
*
|
|
25
|
+
* Strictly gated: it only ever fires when the canonical host is a real custom
|
|
26
|
+
* domain. While a store still lives on *.rovela.app (no custom domain yet), or
|
|
27
|
+
* inside the sandbox / preview ( *.bl.run ) or on localhost, the canonical host
|
|
28
|
+
* is NOT a custom domain, so nothing redirects and there are no loops.
|
|
29
|
+
*
|
|
30
|
+
* NEXT_PUBLIC_APP_URL is inlined at build time, so this constant is resolved
|
|
31
|
+
* once per cold start with the value baked into the deployment.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
function resolveCanonicalHost(): string | null {
|
|
35
|
+
const raw = process.env.NEXT_PUBLIC_APP_URL || ''
|
|
36
|
+
if (!raw) return null
|
|
37
|
+
|
|
38
|
+
let host: string
|
|
39
|
+
try {
|
|
40
|
+
host = new URL(raw).host.toLowerCase()
|
|
41
|
+
} catch {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Only a real custom domain is a valid redirect target. Platform + preview +
|
|
46
|
+
// local hosts are never canonical.
|
|
47
|
+
if (
|
|
48
|
+
!host ||
|
|
49
|
+
host.endsWith('.rovela.app') ||
|
|
50
|
+
host.endsWith('.vercel.app') ||
|
|
51
|
+
host.endsWith('.bl.run') ||
|
|
52
|
+
host.startsWith('localhost') ||
|
|
53
|
+
host.startsWith('127.0.0.1')
|
|
54
|
+
) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return host
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const CANONICAL_HOST = resolveCanonicalHost()
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Locale routing (i18n core — SDK `core/i18n`).
|
|
65
|
+
*
|
|
66
|
+
* The proxy stays DB-FREE (edge runtime): it only PARSES the URL and
|
|
67
|
+
* forwards detection hints as request headers; the redirect DECISIONS
|
|
68
|
+
* (which need store settings) live in the root layout via the SDK's
|
|
69
|
+
* `resolveLocaleRedirect`. Contract:
|
|
70
|
+
*
|
|
71
|
+
* /fr/products → rewrite to /products + `x-rovela-locale: fr`
|
|
72
|
+
* any request → `x-rovela-geo-locale` (from x-vercel-ip-country, hint
|
|
73
|
+
* only) + `x-rovela-pathname` (ORIGINAL path, prefix
|
|
74
|
+
* included — the layout builds redirects from it)
|
|
75
|
+
*
|
|
76
|
+
* Locale prefixes are only parsed for page-like paths — /api, /admin,
|
|
77
|
+
* Next internals and file requests pass through untouched.
|
|
78
|
+
*/
|
|
79
|
+
function parseLocalePrefix(pathname: string): { locale: string; rest: string } | null {
|
|
80
|
+
for (const l of SUPPORTED_LOCALES) {
|
|
81
|
+
if (pathname === `/${l}` || pathname.startsWith(`/${l}/`)) {
|
|
82
|
+
const rest = pathname.slice(l.length + 1) || '/'
|
|
83
|
+
// Never treat /fr/api or /fr/admin as localized surfaces.
|
|
84
|
+
if (rest.startsWith('/api') || rest.startsWith('/admin')) return null
|
|
85
|
+
return { locale: l, rest }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isLocaleEligible(pathname: string): boolean {
|
|
92
|
+
if (pathname.startsWith('/api') || pathname.startsWith('/admin')) return false
|
|
93
|
+
if (pathname.startsWith('/_next')) return false
|
|
94
|
+
// File requests (images, sitemap.xml, robots.txt, …) are never localized.
|
|
95
|
+
if (/\.[a-zA-Z0-9]+$/.test(pathname)) return false
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function proxy(request: NextRequest) {
|
|
100
|
+
// ── 1. Canonical-host redirect (must run first: one hop lands the visitor
|
|
101
|
+
// on the canonical host WITH their original path, locale prefix
|
|
102
|
+
// included; the next request there does the locale work). ──
|
|
103
|
+
const requestHost = (request.headers.get('host') || '').toLowerCase().split(':')[0]
|
|
104
|
+
if (
|
|
105
|
+
CANONICAL_HOST &&
|
|
106
|
+
requestHost &&
|
|
107
|
+
requestHost !== CANONICAL_HOST &&
|
|
108
|
+
requestHost !== 'localhost' &&
|
|
109
|
+
requestHost !== '127.0.0.1'
|
|
110
|
+
) {
|
|
111
|
+
const url = new URL(request.url)
|
|
112
|
+
url.protocol = 'https:'
|
|
113
|
+
url.host = CANONICAL_HOST
|
|
114
|
+
url.port = ''
|
|
115
|
+
// 308 (permanent + method/body preserving) so Google consolidates ranking
|
|
116
|
+
// signals onto the canonical host and POSTs to API routes are not broken.
|
|
117
|
+
return NextResponse.redirect(url, 308)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── 2. Locale parse + detection hints. ──
|
|
121
|
+
const url = new URL(request.url)
|
|
122
|
+
if (!isLocaleEligible(url.pathname)) return NextResponse.next()
|
|
123
|
+
|
|
124
|
+
const requestHeaders = new Headers(request.headers)
|
|
125
|
+
requestHeaders.set('x-rovela-pathname', url.pathname)
|
|
126
|
+
|
|
127
|
+
const country = (request.headers.get('x-vercel-ip-country') || '').toUpperCase()
|
|
128
|
+
// Raw IP country for the cookie-banner region rule (consentRegionExempt).
|
|
129
|
+
// Missing/unknown country fails CLOSED (banner shows) — see cookie-consent.
|
|
130
|
+
if (country) requestHeaders.set('x-rovela-country', country)
|
|
131
|
+
const geoLocale = COUNTRY_TO_LOCALE[country]
|
|
132
|
+
if (geoLocale) requestHeaders.set('x-rovela-geo-locale', geoLocale)
|
|
133
|
+
|
|
134
|
+
const parsed = parseLocalePrefix(url.pathname)
|
|
135
|
+
if (parsed) {
|
|
136
|
+
requestHeaders.set('x-rovela-locale', parsed.locale)
|
|
137
|
+
const rewritten = new URL(url)
|
|
138
|
+
rewritten.pathname = parsed.rest
|
|
139
|
+
return NextResponse.rewrite(rewritten, { request: { headers: requestHeaders } })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return NextResponse.next({ request: { headers: requestHeaders } })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const config = {
|
|
146
|
+
// Run on everything EXCEPT Next internals, static assets, and ACME / domain
|
|
147
|
+
// verification paths (served by the platform, not the app). The redirect must
|
|
148
|
+
// still cover pages, API routes, sitemap.xml, robots.txt, and OG images.
|
|
149
|
+
matcher: ['/((?!_next/static|_next/image|_next/data|\\.well-known|favicon\\.ico).*)'],
|
|
150
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind CSS Configuration
|
|
3
|
+
*
|
|
4
|
+
* NOTE: This file is a placeholder. Claude Agent will regenerate it using:
|
|
5
|
+
* import { generateTailwindConfig } from '@rovela-ai/sdk/theme'
|
|
6
|
+
* const config = generateTailwindConfig(blueprint.theme)
|
|
7
|
+
*
|
|
8
|
+
* This default configuration provides basic styling for the initial build.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Config } from 'tailwindcss'
|
|
12
|
+
|
|
13
|
+
const config: Config = {
|
|
14
|
+
content: [
|
|
15
|
+
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
16
|
+
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
17
|
+
'./lib/**/*.{js,ts,jsx,tsx,mdx}',
|
|
18
|
+
// Include SDK components
|
|
19
|
+
'./node_modules/@rovela-ai/sdk/**/*.{js,ts,jsx,tsx}',
|
|
20
|
+
],
|
|
21
|
+
theme: {
|
|
22
|
+
extend: {
|
|
23
|
+
colors: {
|
|
24
|
+
// Semantic colors (CSS variables set in globals.css)
|
|
25
|
+
background: 'hsl(var(--background))',
|
|
26
|
+
foreground: 'hsl(var(--foreground))',
|
|
27
|
+
card: {
|
|
28
|
+
DEFAULT: 'hsl(var(--card))',
|
|
29
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
30
|
+
},
|
|
31
|
+
popover: {
|
|
32
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
33
|
+
foreground: 'hsl(var(--popover-foreground))',
|
|
34
|
+
},
|
|
35
|
+
primary: {
|
|
36
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
37
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
38
|
+
},
|
|
39
|
+
secondary: {
|
|
40
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
41
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
42
|
+
},
|
|
43
|
+
muted: {
|
|
44
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
45
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
46
|
+
},
|
|
47
|
+
accent: {
|
|
48
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
49
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
50
|
+
},
|
|
51
|
+
destructive: {
|
|
52
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
53
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
54
|
+
},
|
|
55
|
+
border: 'hsl(var(--border))',
|
|
56
|
+
input: 'hsl(var(--input))',
|
|
57
|
+
ring: 'hsl(var(--ring))',
|
|
58
|
+
},
|
|
59
|
+
borderRadius: {
|
|
60
|
+
lg: 'var(--radius)',
|
|
61
|
+
md: 'calc(var(--radius) - 2px)',
|
|
62
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
63
|
+
},
|
|
64
|
+
fontFamily: {
|
|
65
|
+
heading: ['var(--font-heading)', 'system-ui', 'sans-serif'],
|
|
66
|
+
body: ['var(--font-body)', 'system-ui', 'sans-serif'],
|
|
67
|
+
mono: ['var(--font-mono)', 'monospace'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
plugins: [],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default config
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"esnext"
|
|
8
|
+
],
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"module": "esnext",
|
|
15
|
+
"moduleResolution": "bundler",
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"isolatedModules": true,
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
"incremental": true,
|
|
20
|
+
"plugins": [
|
|
21
|
+
{
|
|
22
|
+
"name": "next"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"paths": {
|
|
26
|
+
"@/*": [
|
|
27
|
+
"./*"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"include": [
|
|
32
|
+
"next-env.d.ts",
|
|
33
|
+
"**/*.ts",
|
|
34
|
+
"**/*.tsx",
|
|
35
|
+
".next/types/**/*.ts",
|
|
36
|
+
".next/dev/types/**/*.ts"
|
|
37
|
+
],
|
|
38
|
+
"exclude": [
|
|
39
|
+
"node_modules"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import 'next-auth'
|
|
2
|
+
|
|
3
|
+
declare module 'next-auth' {
|
|
4
|
+
interface Session {
|
|
5
|
+
user: {
|
|
6
|
+
id: string
|
|
7
|
+
email: string
|
|
8
|
+
name?: string | null
|
|
9
|
+
image?: string | null
|
|
10
|
+
emailVerified?: boolean
|
|
11
|
+
role?: 'owner' | 'admin'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface User {
|
|
16
|
+
id: string
|
|
17
|
+
email: string
|
|
18
|
+
name?: string | null
|
|
19
|
+
emailVerified?: boolean
|
|
20
|
+
role?: 'owner' | 'admin'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare module 'next-auth/jwt' {
|
|
25
|
+
interface JWT {
|
|
26
|
+
id: string
|
|
27
|
+
email: string
|
|
28
|
+
name?: string | null
|
|
29
|
+
emailVerified?: boolean
|
|
30
|
+
role?: 'owner' | 'admin'
|
|
31
|
+
}
|
|
32
|
+
}
|