create-brainerce-store 1.66.0 → 1.67.0

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 (46) hide show
  1. package/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/nextjs/base/.env.local.ejs +45 -24
  4. package/templates/nextjs/base/.eslintrc.json +51 -57
  5. package/templates/nextjs/base/AGENTS.md.ejs +107 -81
  6. package/templates/nextjs/base/CLAUDE.md.ejs +118 -92
  7. package/templates/nextjs/base/next.config.ts +103 -86
  8. package/templates/nextjs/base/scripts/fetch-store-info.mjs +104 -97
  9. package/templates/nextjs/base/src/app/agents.md/route.ts +4 -3
  10. package/templates/nextjs/base/src/app/api/auth/me/route.ts +65 -59
  11. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +255 -242
  12. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +311 -308
  13. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +277 -276
  14. package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +4 -3
  15. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +6 -5
  16. package/templates/nextjs/base/src/app/faq/page.tsx.ejs +46 -46
  17. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +25 -26
  18. package/templates/nextjs/base/src/app/layout.tsx.ejs +273 -257
  19. package/templates/nextjs/base/src/app/llms.txt/route.ts +4 -3
  20. package/templates/nextjs/base/src/app/opengraph-image.tsx +1 -1
  21. package/templates/nextjs/base/src/app/page.tsx +65 -64
  22. package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +92 -92
  23. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +6 -5
  24. package/templates/nextjs/base/src/app/robots.ts +3 -2
  25. package/templates/nextjs/base/src/app/sitemap.ts +4 -3
  26. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +294 -292
  27. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +60 -59
  28. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +60 -61
  29. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +2 -1
  30. package/templates/nextjs/base/src/core/lib/brainerce.server.ts +81 -0
  31. package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +60 -110
  32. package/templates/nextjs/base/src/core/lib/site-url.ts +197 -0
  33. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  34. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  35. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +301 -285
  36. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +97 -96
  37. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +142 -141
  38. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +139 -138
  39. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +295 -267
  40. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  41. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +72 -71
  42. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +83 -82
  43. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +120 -119
  44. package/templates/nextjs/ui-canvas/product/faq-section.tsx.ejs +54 -0
  45. package/templates/nextjs/ui-canvas/product/review-form.tsx +267 -266
  46. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
@@ -0,0 +1,197 @@
1
+ import { headers } from 'next/headers';
2
+
3
+ /**
4
+ * The storefront's public origin — the value that ends up in canonical tags,
5
+ * sitemap.xml, robots.txt, JSON-LD, and the `Origin` header the backend
6
+ * compares against the sales channel's configured domain.
7
+ *
8
+ * ⛔ NEVER hardcode this, and never reintroduce a literal default. The
9
+ * scaffolder cannot know the answer: `npx create-brainerce-store` is very
10
+ * often run by an AI builder (ChatGPT, Lovable, Bolt) that deploys the result
11
+ * to a sandbox domain *it* owns, and the merchant moves it to their own domain
12
+ * later. The old `.env.local` shipped `NEXT_PUBLIC_SITE_URL=http://localhost:3000`
13
+ * as a confident wrong answer, which is worse than no answer — it silently
14
+ * wrote `http://localhost:3000` into every sitemap and canonical tag of every
15
+ * storefront not run from a laptop, and `https://example.com` into the ones
16
+ * where that env file never travelled at all (it is gitignored).
17
+ *
18
+ * Two resolvers, split by what the caller actually needs:
19
+ *
20
+ * {@link getCanonicalSiteUrl} — the address the store should be KNOWN by.
21
+ * Env first, because a canonical URL must be stable across preview deploys
22
+ * and must not be forgeable by a request header.
23
+ *
24
+ * {@link getRequestOrigin} — the address a request actually ARRIVED on.
25
+ * Request headers first, because this is what the backend's origin check
26
+ * has to match; claiming the production domain from a preview deployment
27
+ * would pass a check that ought to fail.
28
+ */
29
+
30
+ const DEV_FALLBACK = 'http://localhost:3000';
31
+
32
+ function isLoopback(host: string): boolean {
33
+ const name = host.split(':')[0].toLowerCase();
34
+ return name === 'localhost' || name === '127.0.0.1' || name === '[::1]' || name === '::1';
35
+ }
36
+
37
+ /**
38
+ * Coerce a host (`shop.example.com`) or a full URL (`https://shop.example.com/`)
39
+ * into a bare origin. Returns null for anything unparseable, so a malformed env
40
+ * var degrades to the next source instead of throwing.
41
+ */
42
+ function toOrigin(raw: string | undefined): string | null {
43
+ const trimmed = raw?.trim();
44
+ if (!trimmed) return null;
45
+ const hasProtocol = /^https?:\/\//i.test(trimmed);
46
+ const bare = trimmed.replace(/^\/\//, '');
47
+ const candidate = hasProtocol ? trimmed : `${isLoopback(bare) ? 'http' : 'https'}://${bare}`;
48
+ try {
49
+ return new URL(candidate).origin;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Explicitly configured origin. `SITE_URL` is preferred; `NEXT_PUBLIC_SITE_URL`
57
+ * is accepted as a backwards-compatible alias for storefronts scaffolded before
58
+ * this resolver existed.
59
+ *
60
+ * The `NEXT_PUBLIC_` prefix was always a misnomer here — nothing client-side
61
+ * reads this value, every consumer is server-side — and the prefix implies a
62
+ * build-time inline that does not apply to server reads. Prefer `SITE_URL`.
63
+ */
64
+ function fromExplicitEnv(): string | null {
65
+ return toOrigin(process.env.SITE_URL) ?? toOrigin(process.env.NEXT_PUBLIC_SITE_URL);
66
+ }
67
+
68
+ /**
69
+ * Origins injected by the hosting platform. These are present during
70
+ * `next build` as well as at runtime, so they answer correctly with no request
71
+ * in hand.
72
+ *
73
+ * Production domains are listed before per-deployment domains: a preview build
74
+ * must not stamp its throwaway URL into canonical tags.
75
+ *
76
+ * Netlify's `URL` is guarded behind `NETLIFY` — the bare name is too generic to
77
+ * trust on its own, and a merchant's own `URL` variable must not be mistaken
78
+ * for a site address.
79
+ */
80
+ function fromPlatformEnv(): string | null {
81
+ const env = process.env;
82
+ const candidates = [
83
+ env.VERCEL_PROJECT_PRODUCTION_URL,
84
+ env.VERCEL_URL,
85
+ env.NETLIFY ? env.URL : undefined,
86
+ env.NETLIFY ? env.DEPLOY_PRIME_URL : undefined,
87
+ env.RENDER_EXTERNAL_URL,
88
+ env.RAILWAY_PUBLIC_DOMAIN,
89
+ env.CF_PAGES_URL,
90
+ ];
91
+ for (const candidate of candidates) {
92
+ const origin = toOrigin(candidate);
93
+ if (origin) return origin;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * Origin derived from the incoming request.
100
+ *
101
+ * ⚠️ Caller-controlled. `curl -H 'Host: evil.example'` reaches this, so a
102
+ * storefront relying on it can have a canonical tag poisoned by whoever asks.
103
+ * That is an accepted trade for the AI-builder case — a sandbox injecting none
104
+ * of the platform vars above would otherwise have no correct source at all —
105
+ * and it disappears the moment `SITE_URL` is set, which is why every caller
106
+ * prefers the env.
107
+ */
108
+ function fromForwardedHeaders(h: Headers): string | null {
109
+ // `x-forwarded-host` carries a comma-separated chain when several proxies
110
+ // append to it; the first entry is the original client-facing host.
111
+ const forwardedHost = h.get('x-forwarded-host')?.split(',')[0]?.trim();
112
+ const host = forwardedHost || h.get('host')?.trim();
113
+ if (!host) return null;
114
+ const proto =
115
+ h.get('x-forwarded-proto')?.split(',')[0]?.trim() || (isLoopback(host) ? 'http' : 'https');
116
+ return toOrigin(`${proto}://${host}`);
117
+ }
118
+
119
+ let warnedMissingSiteUrl = false;
120
+
121
+ /**
122
+ * Alarm at the point of failure rather than fail open. Reaching the dev
123
+ * fallback in production means every absolute URL this process emits is wrong,
124
+ * and no downstream layer can detect that — a canonical tag pointing at
125
+ * localhost looks perfectly well-formed to a crawler.
126
+ */
127
+ function warnUnresolved(): void {
128
+ if (warnedMissingSiteUrl || process.env.NODE_ENV !== 'production') return;
129
+ warnedMissingSiteUrl = true;
130
+ console.warn(
131
+ [
132
+ `[brainerce] Could not determine this storefront's public URL; falling back to ${DEV_FALLBACK}.`,
133
+ 'Sitemap, robots.txt, canonical tags and JSON-LD will all be wrong.',
134
+ ' Fix: set SITE_URL to your public origin (e.g. SITE_URL=https://shop.example.com)',
135
+ " in your hosting provider's environment variables.",
136
+ ' Also check the channel: a LIVE sales channel only accepts requests whose Origin',
137
+ ' matches the Domain configured on it. Add this host under Sales Channels -> your',
138
+ ' channel -> Domain in the Brainerce dashboard, or storefront API calls are rejected',
139
+ ' with 403 no matter what SITE_URL says.',
140
+ ].join('\n')
141
+ );
142
+ }
143
+
144
+ /**
145
+ * Env-only resolution, safe to call outside a request (module scope, build
146
+ * scripts, `next.config.ts`). Returns null rather than guessing.
147
+ */
148
+ export function getSiteUrlFromEnv(): string | null {
149
+ return fromExplicitEnv() ?? fromPlatformEnv();
150
+ }
151
+
152
+ /**
153
+ * The origin this store should be known by — canonical tags, sitemap entries,
154
+ * JSON-LD `url` fields, OpenGraph. Stable across deploys when `SITE_URL` is set.
155
+ */
156
+ export async function getCanonicalSiteUrl(): Promise<string> {
157
+ const configured = getSiteUrlFromEnv();
158
+ if (configured) return configured;
159
+
160
+ // Only reached when nothing is configured, so the `headers()` call — and the
161
+ // dynamic rendering it forces — is only paid in that case.
162
+ //
163
+ // ⛔ Deliberately NOT wrapped in try/catch. During static generation
164
+ // `headers()` throws Next's DynamicServerError, which is a *signal*, not a
165
+ // failure: Next catches it upstream and re-renders the route dynamically.
166
+ // Catching it here returns DEV_FALLBACK instead, and Next then bakes
167
+ // `http://localhost:3000` into the prerendered sitemap.xml and robots.txt —
168
+ // reproducing the exact bug this module exists to prevent. Verified: with
169
+ // the catch in place the build emitted localhost into `sitemap.xml.body`.
170
+ const fromRequest = fromForwardedHeaders(await headers());
171
+ if (fromRequest) return fromRequest;
172
+
173
+ warnUnresolved();
174
+ return DEV_FALLBACK;
175
+ }
176
+
177
+ /**
178
+ * The origin a request actually arrived on — used for the `Origin` header sent
179
+ * to the Brainerce backend, which must match the channel's configured domain.
180
+ *
181
+ * Pass the live request headers where you have them (route handlers, the BFF
182
+ * proxy); omit to read the ambient request context.
183
+ */
184
+ export async function getRequestOrigin(requestHeaders?: Headers): Promise<string> {
185
+ // Same rule as above: no try/catch around the ambient `headers()` read, so
186
+ // Next's DynamicServerError propagates and the route is re-rendered
187
+ // dynamically rather than prerendered against a fallback origin.
188
+ const h = requestHeaders ?? (await headers());
189
+ const fromRequest = fromForwardedHeaders(h);
190
+ if (fromRequest) return fromRequest;
191
+
192
+ const configured = getSiteUrlFromEnv();
193
+ if (configured) return configured;
194
+
195
+ warnUnresolved();
196
+ return DEV_FALLBACK;
197
+ }