@cimplify/cli 0.6.8 → 0.6.10

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.
Files changed (65) hide show
  1. package/dist/{add-U4S5DBPN.mjs → add-GDHA7MKM.mjs} +1 -1
  2. package/dist/{chunk-AS2SIMRN.mjs → chunk-5L6LJE6I.mjs} +1 -1
  3. package/dist/{chunk-EALN6SAN.mjs → chunk-EKJ6T66O.mjs} +25 -25
  4. package/dist/{chunk-KPEMFKKN.mjs → chunk-ZTKQOLAC.mjs} +1 -1
  5. package/dist/dispatcher.mjs +9 -9
  6. package/dist/{doctor-Y6XJWZVX.mjs → doctor-SSSYBYCL.mjs} +2 -2
  7. package/dist/{explain-WDIQSCUY.mjs → explain-VG7XUP62.mjs} +1 -1
  8. package/dist/{introspect-3EMAYETR.mjs → introspect-HYJ6VI3U.mjs} +2 -2
  9. package/dist/{list-KO2QX42Q.mjs → list-NQP4SU5K.mjs} +1 -1
  10. package/dist/{update-J6ZDMCOB.mjs → update-HHRCPKSU.mjs} +1 -1
  11. package/package.json +1 -1
  12. package/templates/storefront-auto/app/layout.tsx +3 -1
  13. package/templates/storefront-auto/components/product-modal.tsx +104 -0
  14. package/templates/storefront-auto/lib/site-url.ts +3 -26
  15. package/templates/storefront-auto/lib/site.config.ts +7 -0
  16. package/templates/storefront-auto/next.config.ts +7 -0
  17. package/templates/storefront-bakery/app/layout.tsx +3 -1
  18. package/templates/storefront-bakery/lib/site-url.ts +3 -26
  19. package/templates/storefront-bakery/lib/site.config.ts +7 -0
  20. package/templates/storefront-bakery/next.config.ts +7 -0
  21. package/templates/storefront-fashion/app/layout.tsx +3 -1
  22. package/templates/storefront-fashion/components/product-modal.tsx +104 -0
  23. package/templates/storefront-fashion/lib/site-url.ts +3 -26
  24. package/templates/storefront-fashion/lib/site.config.ts +7 -0
  25. package/templates/storefront-fashion/next.config.ts +7 -0
  26. package/templates/storefront-grocery/app/layout.tsx +3 -1
  27. package/templates/storefront-grocery/lib/site-url.ts +3 -26
  28. package/templates/storefront-grocery/lib/site.config.ts +7 -0
  29. package/templates/storefront-grocery/next.config.ts +7 -0
  30. package/templates/storefront-pharmacy/app/layout.tsx +3 -1
  31. package/templates/storefront-pharmacy/components/product-modal.tsx +104 -0
  32. package/templates/storefront-pharmacy/lib/site-url.ts +3 -26
  33. package/templates/storefront-pharmacy/lib/site.config.ts +7 -0
  34. package/templates/storefront-pharmacy/next.config.ts +7 -0
  35. package/templates/storefront-restaurant/app/layout.tsx +3 -1
  36. package/templates/storefront-restaurant/app/reservations/reservations-client.tsx +1 -1
  37. package/templates/storefront-restaurant/lib/site-url.ts +3 -26
  38. package/templates/storefront-restaurant/lib/site.config.ts +7 -0
  39. package/templates/storefront-restaurant/next.config.ts +7 -0
  40. package/templates/storefront-retail/app/layout.tsx +3 -1
  41. package/templates/storefront-retail/components/product-modal.tsx +104 -0
  42. package/templates/storefront-retail/lib/site-url.ts +3 -26
  43. package/templates/storefront-retail/lib/site.config.ts +7 -0
  44. package/templates/storefront-retail/next.config.ts +7 -0
  45. package/templates/storefront-services/app/book/book-client.tsx +2 -1
  46. package/templates/storefront-services/app/layout.tsx +3 -1
  47. package/templates/storefront-services/lib/site-url.ts +3 -26
  48. package/templates/storefront-services/lib/site.config.ts +7 -0
  49. package/templates/storefront-services/next.config.ts +7 -0
  50. package/templates/storefront-auto/app/login/page.tsx +0 -17
  51. package/templates/storefront-auto/app/signup/page.tsx +0 -17
  52. package/templates/storefront-bakery/app/login/page.tsx +0 -17
  53. package/templates/storefront-bakery/app/signup/page.tsx +0 -17
  54. package/templates/storefront-fashion/app/login/page.tsx +0 -17
  55. package/templates/storefront-fashion/app/signup/page.tsx +0 -17
  56. package/templates/storefront-grocery/app/login/page.tsx +0 -17
  57. package/templates/storefront-grocery/app/signup/page.tsx +0 -17
  58. package/templates/storefront-pharmacy/app/login/page.tsx +0 -17
  59. package/templates/storefront-pharmacy/app/signup/page.tsx +0 -17
  60. package/templates/storefront-restaurant/app/login/page.tsx +0 -17
  61. package/templates/storefront-restaurant/app/signup/page.tsx +0 -17
  62. package/templates/storefront-retail/app/login/page.tsx +0 -17
  63. package/templates/storefront-retail/app/signup/page.tsx +0 -17
  64. package/templates/storefront-services/app/login/page.tsx +0 -17
  65. package/templates/storefront-services/app/signup/page.tsx +0 -17
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { gitDetectRoot, gitCurrentBranch, gitCurrentSha, gitStatusPorcelain } from './chunk-K5464A3L.mjs';
3
3
  import { parseEnvFile } from './chunk-DBZ3UOQ2.mjs';
4
- import { package_default } from './chunk-AS2SIMRN.mjs';
4
+ import { package_default } from './chunk-5L6LJE6I.mjs';
5
5
  import { parseArgs } from './chunk-C4M3DXKC.mjs';
6
6
  import { readAuthOrNull, readProjectLinkOrNull, readProjectState } from './chunk-UBAI443T.mjs';
7
7
  import { bold, dim, yellow, green, info, result, red } from './chunk-E2T2SBP5.mjs';
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { TEMPLATES } from './chunk-EALN6SAN.mjs';
3
- import { package_default } from './chunk-AS2SIMRN.mjs';
2
+ import { TEMPLATES } from './chunk-EKJ6T66O.mjs';
3
+ import { package_default } from './chunk-5L6LJE6I.mjs';
4
4
 
5
5
  // src/dispatcher.ts
6
6
  var VERSION = package_default.version ?? "unknown";
@@ -137,15 +137,15 @@ var COMMANDS = {
137
137
  logs: () => import('./logs-YNN2PQ24.mjs'),
138
138
  status: () => import('./status-JSYXM5RT.mjs'),
139
139
  dev: () => import('./dev-ONW2S77K.mjs'),
140
- introspect: () => import('./introspect-3EMAYETR.mjs'),
141
- doctor: () => import('./doctor-Y6XJWZVX.mjs'),
142
- explain: () => import('./explain-WDIQSCUY.mjs'),
140
+ introspect: () => import('./introspect-HYJ6VI3U.mjs'),
141
+ doctor: () => import('./doctor-SSSYBYCL.mjs'),
142
+ explain: () => import('./explain-VG7XUP62.mjs'),
143
143
  assets: () => import('./assets-EBEMMENZ.mjs'),
144
144
  repo: () => import('./repo-WOBWKEAO.mjs'),
145
- list: () => import('./list-KO2QX42Q.mjs'),
146
- add: () => import('./add-U4S5DBPN.mjs'),
147
- update: () => import('./update-J6ZDMCOB.mjs'),
148
- upgrade: () => import('./update-J6ZDMCOB.mjs'),
145
+ list: () => import('./list-NQP4SU5K.mjs'),
146
+ add: () => import('./add-GDHA7MKM.mjs'),
147
+ update: () => import('./update-HHRCPKSU.mjs'),
148
+ upgrade: () => import('./update-HHRCPKSU.mjs'),
149
149
  "auth-step-up": () => import('./auth-step-up-BIUYQJP6.mjs')
150
150
  };
151
151
  var COMMAND_PREFIXES = {
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { gatherIntrospection } from './chunk-KPEMFKKN.mjs';
2
+ import { gatherIntrospection } from './chunk-ZTKQOLAC.mjs';
3
3
  import './chunk-K5464A3L.mjs';
4
4
  import './chunk-DBZ3UOQ2.mjs';
5
- import './chunk-AS2SIMRN.mjs';
5
+ import './chunk-5L6LJE6I.mjs';
6
6
  import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
7
7
  import { ApiClient } from './chunk-MAOO6ZZ5.mjs';
8
8
  import { readAuthOrNull } from './chunk-UBAI443T.mjs';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { package_default } from './chunk-AS2SIMRN.mjs';
2
+ import { package_default } from './chunk-5L6LJE6I.mjs';
3
3
  import { parseArgs } from './chunk-C4M3DXKC.mjs';
4
4
  import { bold, dim, info, result, CliError, CLI_ERROR_CODE } from './chunk-E2T2SBP5.mjs';
5
5
 
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-KPEMFKKN.mjs';
2
+ export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-ZTKQOLAC.mjs';
3
3
  import './chunk-K5464A3L.mjs';
4
4
  import './chunk-DBZ3UOQ2.mjs';
5
- import './chunk-AS2SIMRN.mjs';
5
+ import './chunk-5L6LJE6I.mjs';
6
6
  import './chunk-C4M3DXKC.mjs';
7
7
  import './chunk-UBAI443T.mjs';
8
8
  import './chunk-E2T2SBP5.mjs';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { REGISTRY_INDEX } from './chunk-EALN6SAN.mjs';
2
+ import { REGISTRY_INDEX } from './chunk-EKJ6T66O.mjs';
3
3
  import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
4
4
  import { CliError, CLI_ERROR_CODE, info, bold, dim, green, result } from './chunk-E2T2SBP5.mjs';
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { package_default } from './chunk-AS2SIMRN.mjs';
2
+ import { package_default } from './chunk-5L6LJE6I.mjs';
3
3
  import { promptYesNo } from './chunk-ITAFAORS.mjs';
4
4
  import { parseArgs, flagBool, flagString } from './chunk-C4M3DXKC.mjs';
5
5
  import { success, bold, info, dim, result, failure, CliError, CLI_ERROR_CODE, step, isJsonMode } from './chunk-E2T2SBP5.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/cli",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "Cimplify CLI — deploy, manage env vars, link projects, and scaffold storefronts",
5
5
  "keywords": [
6
6
  "cimplify",
@@ -53,7 +53,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
53
53
  </Suspense>
54
54
  <Providers>
55
55
  <Header />
56
- <main className="flex-1 pb-12 w-full">{children}</main>
56
+ <main className="flex-1 pb-12 w-full">
57
+ <Suspense fallback={null}>{children}</Suspense>
58
+ </main>
57
59
  <Footer />
58
60
  <Suspense fallback={null}>
59
61
  <ProductModal />
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import Image from "next/image";
5
+ import { useRouter, useSearchParams, usePathname } from "next/navigation";
6
+ import { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";
7
+
8
+ /**
9
+ * URL-driven product modal. Reads `?product=<slug>` and renders the SDK's
10
+ * `<ProductSheet/>` — vertical layout with image-on-top, then header, then
11
+ * the variant/add-on/composite/bundle customizer. Closing the modal clears
12
+ * the search param. Deep-linkable and survives reloads.
13
+ */
14
+ export function ProductModal() {
15
+ const router = useRouter();
16
+ const pathname = usePathname();
17
+ const searchParams = useSearchParams();
18
+ const slug = searchParams?.get("product") ?? null;
19
+
20
+ const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });
21
+ const { addItem } = useCart();
22
+
23
+ useEffect(() => {
24
+ if (!slug) return;
25
+ const original = document.body.style.overflow;
26
+ document.body.style.overflow = "hidden";
27
+ return () => {
28
+ document.body.style.overflow = original;
29
+ };
30
+ }, [slug]);
31
+
32
+ useEffect(() => {
33
+ if (!slug) return;
34
+ const onKey = (e: KeyboardEvent) => {
35
+ if (e.key === "Escape") close();
36
+ };
37
+ window.addEventListener("keydown", onKey);
38
+ return () => window.removeEventListener("keydown", onKey);
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps
40
+ }, [slug]);
41
+
42
+ if (!slug) return null;
43
+
44
+ function close() {
45
+ const next = new URLSearchParams(searchParams?.toString() ?? "");
46
+ next.delete("product");
47
+ const qs = next.toString();
48
+ router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
49
+ }
50
+
51
+ return (
52
+ <div
53
+ role="dialog"
54
+ aria-modal="true"
55
+ onClick={close}
56
+ className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"
57
+ >
58
+ <div
59
+ onClick={(e) => e.stopPropagation()}
60
+ className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"
61
+ >
62
+ <button
63
+ onClick={close}
64
+ aria-label="Close product details"
65
+ className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"
66
+ >
67
+
68
+ </button>
69
+ {product ? (
70
+ <ProductSheet
71
+ product={product}
72
+ onClose={close}
73
+ onAddToCart={async (p, qty, options) => {
74
+ await addItem(p, qty, options);
75
+ close();
76
+ }}
77
+ renderImage={({ src, alt, className }) => (
78
+ <Image
79
+ src={src}
80
+ alt={alt}
81
+ width={1200}
82
+ height={900}
83
+ className={className}
84
+ style={{ width: "100%", height: "auto", objectFit: "cover" }}
85
+ priority
86
+ />
87
+ )}
88
+ classNames={{
89
+ root: "p-6 sm:p-8 gap-4",
90
+ image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",
91
+ header: "flex items-baseline justify-between gap-4",
92
+ name: "font-serif text-2xl font-semibold m-0",
93
+ price: "text-lg font-semibold text-primary",
94
+ description: "text-sm text-muted-foreground leading-relaxed",
95
+ customizer: "pt-2",
96
+ }}
97
+ />
98
+ ) : (
99
+ <div className="p-8 text-center text-muted-foreground">Loading…</div>
100
+ )}
101
+ </div>
102
+ </div>
103
+ );
104
+ }
@@ -1,29 +1,6 @@
1
- import { headers } from "next/headers";
1
+ import { SITE_URL } from "./site.config";
2
2
 
3
- /**
4
- * Canonical absolute URL for this storefront, derived at request time.
5
- *
6
- * Resolution order:
7
- * 1. `NEXT_PUBLIC_SITE_URL` — set when you need a fixed canonical
8
- * (multiple domains, www-vs-apex preference, etc.).
9
- * 2. The request `Host` header — works automatically on every deploy,
10
- * including preview URLs and custom domains.
11
- * 3. `https://example.com` — only ever returned during prerender of
12
- * a page that has no live request (rare in App Router).
13
- */
3
+ /** Canonical absolute URL for this storefront. */
14
4
  export async function getSiteUrl(): Promise<string> {
15
- const explicit = process.env.NEXT_PUBLIC_SITE_URL?.trim();
16
- if (explicit) return explicit;
17
-
18
- try {
19
- const h = await headers();
20
- const host = h.get("host");
21
- if (host) {
22
- const isLocal = host.startsWith("localhost") || host.startsWith("127.");
23
- return `${isLocal ? "http" : "https"}://${host}`;
24
- }
25
- } catch {
26
- // `headers()` is unavailable in some build contexts — fall through.
27
- }
28
- return "https://example.com";
5
+ return SITE_URL;
29
6
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Canonical absolute URL. The platform overwrites this at build time with the
3
+ * storefront's primary domain; the default only applies to local builds. It's
4
+ * per-deploy config, not per-request, so it stays a static constant — keeping
5
+ * the root layout prerenderable under `cacheComponents`.
6
+ */
7
+ export const SITE_URL = "https://example.com";
@@ -24,6 +24,13 @@ const nextConfig: NextConfig = {
24
24
  // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,
25
25
  // dynamic Suspense boundaries fill in when ready.
26
26
  cacheComponents: true,
27
+ async redirects() {
28
+ // Config-level so cacheComponents needn't prerender a redirect()-only page.
29
+ return [
30
+ { source: "/login", destination: "/account", permanent: false },
31
+ { source: "/signup", destination: "/account", permanent: false },
32
+ ];
33
+ },
27
34
  async rewrites() {
28
35
  return [
29
36
  { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },
@@ -53,7 +53,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
53
53
  </Suspense>
54
54
  <Providers>
55
55
  <Header />
56
- <main className="flex-1 pb-12 w-full">{children}</main>
56
+ <main className="flex-1 pb-12 w-full">
57
+ <Suspense fallback={null}>{children}</Suspense>
58
+ </main>
57
59
  <Footer />
58
60
  <Suspense fallback={null}>
59
61
  <ProductModal />
@@ -1,29 +1,6 @@
1
- import { headers } from "next/headers";
1
+ import { SITE_URL } from "./site.config";
2
2
 
3
- /**
4
- * Canonical absolute URL for this storefront, derived at request time.
5
- *
6
- * Resolution order:
7
- * 1. `NEXT_PUBLIC_SITE_URL` — set when you need a fixed canonical
8
- * (multiple domains, www-vs-apex preference, etc.).
9
- * 2. The request `Host` header — works automatically on every deploy,
10
- * including preview URLs and custom domains.
11
- * 3. `https://example.com` — only ever returned during prerender of
12
- * a page that has no live request (rare in App Router).
13
- */
3
+ /** Canonical absolute URL for this storefront. */
14
4
  export async function getSiteUrl(): Promise<string> {
15
- const explicit = process.env.NEXT_PUBLIC_SITE_URL?.trim();
16
- if (explicit) return explicit;
17
-
18
- try {
19
- const h = await headers();
20
- const host = h.get("host");
21
- if (host) {
22
- const isLocal = host.startsWith("localhost") || host.startsWith("127.");
23
- return `${isLocal ? "http" : "https"}://${host}`;
24
- }
25
- } catch {
26
- // `headers()` is unavailable in some build contexts — fall through.
27
- }
28
- return "https://example.com";
5
+ return SITE_URL;
29
6
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Canonical absolute URL. The platform overwrites this at build time with the
3
+ * storefront's primary domain; the default only applies to local builds. It's
4
+ * per-deploy config, not per-request, so it stays a static constant — keeping
5
+ * the root layout prerenderable under `cacheComponents`.
6
+ */
7
+ export const SITE_URL = "https://example.com";
@@ -24,6 +24,13 @@ const nextConfig: NextConfig = {
24
24
  // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,
25
25
  // dynamic Suspense boundaries fill in when ready.
26
26
  cacheComponents: true,
27
+ async redirects() {
28
+ // Config-level so cacheComponents needn't prerender a redirect()-only page.
29
+ return [
30
+ { source: "/login", destination: "/account", permanent: false },
31
+ { source: "/signup", destination: "/account", permanent: false },
32
+ ];
33
+ },
27
34
  async rewrites() {
28
35
  return [
29
36
  { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },
@@ -54,7 +54,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
54
54
  </Suspense>
55
55
  <Providers>
56
56
  <Header />
57
- <main className="flex-1 pb-12 w-full">{children}</main>
57
+ <main className="flex-1 pb-12 w-full">
58
+ <Suspense fallback={null}>{children}</Suspense>
59
+ </main>
58
60
  <Footer />
59
61
  <Suspense fallback={null}>
60
62
  <ProductModal />
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import Image from "next/image";
5
+ import { useRouter, useSearchParams, usePathname } from "next/navigation";
6
+ import { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";
7
+
8
+ /**
9
+ * URL-driven product modal. Reads `?product=<slug>` and renders the SDK's
10
+ * `<ProductSheet/>` — vertical layout with image-on-top, then header, then
11
+ * the variant/add-on/composite/bundle customizer. Closing the modal clears
12
+ * the search param. Deep-linkable and survives reloads.
13
+ */
14
+ export function ProductModal() {
15
+ const router = useRouter();
16
+ const pathname = usePathname();
17
+ const searchParams = useSearchParams();
18
+ const slug = searchParams?.get("product") ?? null;
19
+
20
+ const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });
21
+ const { addItem } = useCart();
22
+
23
+ useEffect(() => {
24
+ if (!slug) return;
25
+ const original = document.body.style.overflow;
26
+ document.body.style.overflow = "hidden";
27
+ return () => {
28
+ document.body.style.overflow = original;
29
+ };
30
+ }, [slug]);
31
+
32
+ useEffect(() => {
33
+ if (!slug) return;
34
+ const onKey = (e: KeyboardEvent) => {
35
+ if (e.key === "Escape") close();
36
+ };
37
+ window.addEventListener("keydown", onKey);
38
+ return () => window.removeEventListener("keydown", onKey);
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps
40
+ }, [slug]);
41
+
42
+ if (!slug) return null;
43
+
44
+ function close() {
45
+ const next = new URLSearchParams(searchParams?.toString() ?? "");
46
+ next.delete("product");
47
+ const qs = next.toString();
48
+ router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
49
+ }
50
+
51
+ return (
52
+ <div
53
+ role="dialog"
54
+ aria-modal="true"
55
+ onClick={close}
56
+ className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"
57
+ >
58
+ <div
59
+ onClick={(e) => e.stopPropagation()}
60
+ className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"
61
+ >
62
+ <button
63
+ onClick={close}
64
+ aria-label="Close product details"
65
+ className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"
66
+ >
67
+
68
+ </button>
69
+ {product ? (
70
+ <ProductSheet
71
+ product={product}
72
+ onClose={close}
73
+ onAddToCart={async (p, qty, options) => {
74
+ await addItem(p, qty, options);
75
+ close();
76
+ }}
77
+ renderImage={({ src, alt, className }) => (
78
+ <Image
79
+ src={src}
80
+ alt={alt}
81
+ width={1200}
82
+ height={900}
83
+ className={className}
84
+ style={{ width: "100%", height: "auto", objectFit: "cover" }}
85
+ priority
86
+ />
87
+ )}
88
+ classNames={{
89
+ root: "p-6 sm:p-8 gap-4",
90
+ image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",
91
+ header: "flex items-baseline justify-between gap-4",
92
+ name: "font-serif text-2xl font-semibold m-0",
93
+ price: "text-lg font-semibold text-primary",
94
+ description: "text-sm text-muted-foreground leading-relaxed",
95
+ customizer: "pt-2",
96
+ }}
97
+ />
98
+ ) : (
99
+ <div className="p-8 text-center text-muted-foreground">Loading…</div>
100
+ )}
101
+ </div>
102
+ </div>
103
+ );
104
+ }
@@ -1,29 +1,6 @@
1
- import { headers } from "next/headers";
1
+ import { SITE_URL } from "./site.config";
2
2
 
3
- /**
4
- * Canonical absolute URL for this storefront, derived at request time.
5
- *
6
- * Resolution order:
7
- * 1. `NEXT_PUBLIC_SITE_URL` — set when you need a fixed canonical
8
- * (multiple domains, www-vs-apex preference, etc.).
9
- * 2. The request `Host` header — works automatically on every deploy,
10
- * including preview URLs and custom domains.
11
- * 3. `https://example.com` — only ever returned during prerender of
12
- * a page that has no live request (rare in App Router).
13
- */
3
+ /** Canonical absolute URL for this storefront. */
14
4
  export async function getSiteUrl(): Promise<string> {
15
- const explicit = process.env.NEXT_PUBLIC_SITE_URL?.trim();
16
- if (explicit) return explicit;
17
-
18
- try {
19
- const h = await headers();
20
- const host = h.get("host");
21
- if (host) {
22
- const isLocal = host.startsWith("localhost") || host.startsWith("127.");
23
- return `${isLocal ? "http" : "https"}://${host}`;
24
- }
25
- } catch {
26
- // `headers()` is unavailable in some build contexts — fall through.
27
- }
28
- return "https://example.com";
5
+ return SITE_URL;
29
6
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Canonical absolute URL. The platform overwrites this at build time with the
3
+ * storefront's primary domain; the default only applies to local builds. It's
4
+ * per-deploy config, not per-request, so it stays a static constant — keeping
5
+ * the root layout prerenderable under `cacheComponents`.
6
+ */
7
+ export const SITE_URL = "https://example.com";
@@ -24,6 +24,13 @@ const nextConfig: NextConfig = {
24
24
  // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,
25
25
  // dynamic Suspense boundaries fill in when ready.
26
26
  cacheComponents: true,
27
+ async redirects() {
28
+ // Config-level so cacheComponents needn't prerender a redirect()-only page.
29
+ return [
30
+ { source: "/login", destination: "/account", permanent: false },
31
+ { source: "/signup", destination: "/account", permanent: false },
32
+ ];
33
+ },
27
34
  async rewrites() {
28
35
  return [
29
36
  { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },
@@ -47,7 +47,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
47
47
  </Suspense>
48
48
  <Providers>
49
49
  <Header />
50
- <main className="flex-1 pb-12 w-full">{children}</main>
50
+ <main className="flex-1 pb-12 w-full">
51
+ <Suspense fallback={null}>{children}</Suspense>
52
+ </main>
51
53
  <Footer />
52
54
  <Suspense fallback={null}>
53
55
  <ProductModal />
@@ -1,29 +1,6 @@
1
- import { headers } from "next/headers";
1
+ import { SITE_URL } from "./site.config";
2
2
 
3
- /**
4
- * Canonical absolute URL for this storefront, derived at request time.
5
- *
6
- * Resolution order:
7
- * 1. `NEXT_PUBLIC_SITE_URL` — set when you need a fixed canonical
8
- * (multiple domains, www-vs-apex preference, etc.).
9
- * 2. The request `Host` header — works automatically on every deploy,
10
- * including preview URLs and custom domains.
11
- * 3. `https://example.com` — only ever returned during prerender of
12
- * a page that has no live request (rare in App Router).
13
- */
3
+ /** Canonical absolute URL for this storefront. */
14
4
  export async function getSiteUrl(): Promise<string> {
15
- const explicit = process.env.NEXT_PUBLIC_SITE_URL?.trim();
16
- if (explicit) return explicit;
17
-
18
- try {
19
- const h = await headers();
20
- const host = h.get("host");
21
- if (host) {
22
- const isLocal = host.startsWith("localhost") || host.startsWith("127.");
23
- return `${isLocal ? "http" : "https"}://${host}`;
24
- }
25
- } catch {
26
- // `headers()` is unavailable in some build contexts — fall through.
27
- }
28
- return "https://example.com";
5
+ return SITE_URL;
29
6
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Canonical absolute URL. The platform overwrites this at build time with the
3
+ * storefront's primary domain; the default only applies to local builds. It's
4
+ * per-deploy config, not per-request, so it stays a static constant — keeping
5
+ * the root layout prerenderable under `cacheComponents`.
6
+ */
7
+ export const SITE_URL = "https://example.com";
@@ -24,6 +24,13 @@ const nextConfig: NextConfig = {
24
24
  // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,
25
25
  // dynamic Suspense boundaries fill in when ready.
26
26
  cacheComponents: true,
27
+ async redirects() {
28
+ // Config-level so cacheComponents needn't prerender a redirect()-only page.
29
+ return [
30
+ { source: "/login", destination: "/account", permanent: false },
31
+ { source: "/signup", destination: "/account", permanent: false },
32
+ ];
33
+ },
27
34
  async rewrites() {
28
35
  return [
29
36
  { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },
@@ -53,7 +53,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
53
53
  </Suspense>
54
54
  <Providers>
55
55
  <Header />
56
- <main className="flex-1 pb-12 w-full">{children}</main>
56
+ <main className="flex-1 pb-12 w-full">
57
+ <Suspense fallback={null}>{children}</Suspense>
58
+ </main>
57
59
  <Footer />
58
60
  <Suspense fallback={null}>
59
61
  <ProductModal />