create-brainerce-store 1.66.0 → 1.68.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.
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/nextjs/base/.env.local.ejs +52 -24
- package/templates/nextjs/base/.eslintrc.json +51 -57
- package/templates/nextjs/base/AGENTS.md.ejs +114 -81
- package/templates/nextjs/base/CLAUDE.md.ejs +125 -92
- package/templates/nextjs/base/next.config.ts +103 -86
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +104 -97
- package/templates/nextjs/base/src/app/agents.md/route.ts +4 -3
- package/templates/nextjs/base/src/app/api/auth/me/route.ts +65 -59
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +255 -242
- package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +317 -308
- package/templates/nextjs/base/src/app/blog/page.tsx.ejs +277 -276
- package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +4 -3
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +22 -12
- package/templates/nextjs/base/src/app/error.tsx.ejs +53 -0
- package/templates/nextjs/base/src/app/faq/page.tsx.ejs +46 -46
- package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +25 -26
- package/templates/nextjs/base/src/app/layout.tsx.ejs +273 -257
- package/templates/nextjs/base/src/app/llms.txt/route.ts +4 -3
- package/templates/nextjs/base/src/app/opengraph-image.tsx +1 -1
- package/templates/nextjs/base/src/app/page.tsx +65 -64
- package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +98 -92
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +22 -11
- package/templates/nextjs/base/src/app/robots.ts +3 -2
- package/templates/nextjs/base/src/app/sitemap.ts +4 -3
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +294 -292
- package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +60 -59
- package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +60 -61
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +2 -1
- package/templates/nextjs/base/src/core/lib/brainerce.server.ts +81 -0
- package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +60 -110
- package/templates/nextjs/base/src/core/lib/site-url.ts +230 -0
- package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +301 -285
- package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +97 -96
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +142 -141
- package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +139 -138
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +295 -267
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
- package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +72 -71
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +83 -82
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +120 -119
- package/templates/nextjs/ui-canvas/product/faq-section.tsx.ejs +54 -0
- package/templates/nextjs/ui-canvas/product/review-form.tsx +267 -266
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
2
|
import { notFound } from 'next/navigation';
|
|
3
|
-
import
|
|
4
|
-
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
|
|
3
|
+
import { BrainerceError } from 'brainerce';
|
|
4
|
+
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
|
|
5
5
|
import { buildMetaDescription } from '@/core/lib/seo';
|
|
6
6
|
import { sanitizeHtml } from '@/core/lib/sanitize';
|
|
7
7
|
import { decodeSlug } from '@/core/lib/utils';
|
|
8
8
|
import { CategoryJsonLd } from '@/components/seo/category-json-ld';
|
|
9
|
+
import { getCanonicalSiteUrl } from '@/core/lib/site-url';
|
|
9
10
|
import { Breadcrumbs } from '@/components/seo/breadcrumbs';
|
|
10
11
|
import { ProductGrid } from '@/ui/product/product-grid';
|
|
11
12
|
|
|
@@ -27,7 +28,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
|
27
28
|
const slug = decodeSlug(rawSlug);
|
|
28
29
|
try {
|
|
29
30
|
const [category, storeInfo] = await Promise.all([
|
|
30
|
-
getServerClient(locale).getCategoryBySlug(slug),
|
|
31
|
+
(await getServerClient(locale)).getCategoryBySlug(slug),
|
|
31
32
|
fetchStoreInfo(locale),
|
|
32
33
|
]);
|
|
33
34
|
const description =
|
|
@@ -42,7 +43,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
|
42
43
|
// emits, so the alternates always match real, crawlable URLs.
|
|
43
44
|
const supportedLocales = storeInfo?.i18n?.supportedLocales ?? [];
|
|
44
45
|
const defaultLoc = storeInfo?.i18n?.defaultLocale ?? storeInfo?.language ?? '';
|
|
45
|
-
const baseUrl =
|
|
46
|
+
const baseUrl = await getCanonicalSiteUrl();
|
|
46
47
|
const hreflangLanguages =
|
|
47
48
|
supportedLocales.length > 1
|
|
48
49
|
? Object.fromEntries([
|
|
@@ -76,26 +77,35 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
|
76
77
|
images: category.image ? [category.image] : [],
|
|
77
78
|
},
|
|
78
79
|
};
|
|
79
|
-
} catch {
|
|
80
|
-
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err instanceof BrainerceError && err.statusCode === 404) {
|
|
82
|
+
return { title: 'Category not found' };
|
|
83
|
+
}
|
|
84
|
+
// Anything else (channel origin rejection, network failure) hits the page
|
|
85
|
+
// render below too and surfaces there — don't stamp a misleading title.
|
|
86
|
+
return {};
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
export default async function CategoryPage({ params }: Props) {
|
|
85
91
|
const { slug: rawSlug, locale } = await params;
|
|
86
92
|
const slug = decodeSlug(rawSlug);
|
|
87
|
-
const client = getServerClient(locale);
|
|
93
|
+
const client = await getServerClient(locale);
|
|
88
94
|
|
|
89
|
-
|
|
95
|
+
// Only a genuine 404 becomes a storefront 404; a 403 from the channel's
|
|
96
|
+
// origin check or a network failure must surface as the error it is, not
|
|
97
|
+
// masquerade as a missing category (or an empty one).
|
|
98
|
+
const category = await client.getCategoryBySlug(slug).catch((err: unknown) => {
|
|
99
|
+
if (err instanceof BrainerceError && err.statusCode === 404) return null;
|
|
100
|
+
throw err;
|
|
101
|
+
});
|
|
90
102
|
if (!category) notFound();
|
|
91
103
|
|
|
92
104
|
// The listing endpoint owns pagination/FX/publish gating — reuse it rather
|
|
93
105
|
// than duplicating a product query here.
|
|
94
|
-
const { data: products } = await client
|
|
95
|
-
.getProducts({ categories: [category.id], limit: 48 })
|
|
96
|
-
.catch(() => ({ data: [] as Product[] }));
|
|
106
|
+
const { data: products } = await client.getProducts({ categories: [category.id], limit: 48 });
|
|
97
107
|
|
|
98
|
-
const baseUrl =
|
|
108
|
+
const baseUrl = await getCanonicalSiteUrl();
|
|
99
109
|
const url = `${baseUrl}/category/${category.slug || slug}`;
|
|
100
110
|
const localePrefix = locale ? `/${locale}` : '';
|
|
101
111
|
const breadcrumbItems = [
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error page — rendered when a page render throws. Kept deliberately separate
|
|
8
|
+
* from the 404 page: an API failure (a rejected Origin, a network error, a
|
|
9
|
+
* 5xx) is NOT "product not found", and dressing it up as a 404 sends the
|
|
10
|
+
* merchant hunting for a slug bug that isn't there. The full error is in the
|
|
11
|
+
* server log; this page only needs to say "temporary" and offer a retry.
|
|
12
|
+
*/
|
|
13
|
+
export default function ErrorPage({
|
|
14
|
+
error,
|
|
15
|
+
reset,
|
|
16
|
+
}: {
|
|
17
|
+
error: Error & { digest?: string };
|
|
18
|
+
reset: () => void;
|
|
19
|
+
}) {
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// The server log has the full error; the digest printed here links the two.
|
|
22
|
+
console.error(error);
|
|
23
|
+
}, [error]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="mx-auto flex max-w-2xl flex-col items-center justify-center px-4 py-24 text-center">
|
|
27
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
28
|
+
<%- language === 'he' ? 'שגיאה' : 'Error' %>
|
|
29
|
+
</p>
|
|
30
|
+
<h1 className="mt-2 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
|
31
|
+
<%- language === 'he' ? 'משהו השתבש' : 'Something went wrong' %>
|
|
32
|
+
</h1>
|
|
33
|
+
<p className="mt-4 text-muted-foreground">
|
|
34
|
+
<%- language === 'he' ? 'זו תקלה זמנית בטעינת העמוד — לא עמוד חסר. נסו שוב בעוד רגע.' : 'This is a temporary problem loading the page — not a missing page. Please try again in a moment.' %>
|
|
35
|
+
</p>
|
|
36
|
+
<div className="mt-8 flex gap-4">
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={reset}
|
|
40
|
+
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
|
|
41
|
+
>
|
|
42
|
+
<%- language === 'he' ? 'לנסות שוב' : 'Try again' %>
|
|
43
|
+
</button>
|
|
44
|
+
<Link
|
|
45
|
+
href="/"
|
|
46
|
+
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted"
|
|
47
|
+
>
|
|
48
|
+
<%- language === 'he' ? 'לדף הבית' : 'Go home' %>
|
|
49
|
+
</Link>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FAQ page — fully driven by the merchant's dashboard configuration.
|
|
3
|
-
*
|
|
4
|
-
* The merchant configures FAQs in the Brainerce dashboard under
|
|
5
|
-
* Sell → Content → FAQ
|
|
6
|
-
*
|
|
7
|
-
* This page fetches the merchant's *primary* FAQ (key='main'). For topical
|
|
8
|
-
* FAQs (e.g. /faq/shipping), copy this route to `app/faq/[topic]/page.tsx`
|
|
9
|
-
* and pass `params.topic` to `client.content.faq.get(topic, locale)`.
|
|
10
|
-
*/
|
|
11
|
-
import * as React from 'react';
|
|
12
|
-
import type { Metadata } from 'next';
|
|
13
|
-
|
|
14
|
-
import { getServerClient } from '@/core/lib/brainerce';
|
|
15
|
-
import { FaqSection } from '@/ui/layout/faq-section';
|
|
16
|
-
<% if (i18nEnabled) { %>
|
|
17
|
-
type PageProps = {
|
|
18
|
-
params: Promise<{ locale: string }>;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
22
|
-
const { locale } = await params;
|
|
23
|
-
const faq = await getServerClient(locale).content.faq.get('main', locale).catch(() => null);
|
|
24
|
-
return {
|
|
25
|
-
title: faq?.name ?? 'FAQ',
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export default async function FaqPage({ params }: PageProps) {
|
|
30
|
-
const { locale } = await params;
|
|
31
|
-
const faq = await getServerClient(locale).content.faq.get('main', locale).catch(() => null);
|
|
32
|
-
return <FaqSection faq={faq} />;
|
|
33
|
-
}
|
|
34
|
-
<% } else { %>
|
|
35
|
-
export async function generateMetadata(): Promise<Metadata> {
|
|
36
|
-
const faq = await getServerClient().content.faq.get('main').catch(() => null);
|
|
37
|
-
return {
|
|
38
|
-
title: faq?.name ?? 'FAQ',
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export default async function FaqPage() {
|
|
43
|
-
const faq = await getServerClient().content.faq.get('main').catch(() => null);
|
|
44
|
-
return <FaqSection faq={faq} />;
|
|
45
|
-
}
|
|
46
|
-
<% } %>
|
|
1
|
+
/**
|
|
2
|
+
* FAQ page — fully driven by the merchant's dashboard configuration.
|
|
3
|
+
*
|
|
4
|
+
* The merchant configures FAQs in the Brainerce dashboard under
|
|
5
|
+
* Sell → Content → FAQ
|
|
6
|
+
*
|
|
7
|
+
* This page fetches the merchant's *primary* FAQ (key='main'). For topical
|
|
8
|
+
* FAQs (e.g. /faq/shipping), copy this route to `app/faq/[topic]/page.tsx`
|
|
9
|
+
* and pass `params.topic` to `client.content.faq.get(topic, locale)`.
|
|
10
|
+
*/
|
|
11
|
+
import * as React from 'react';
|
|
12
|
+
import type { Metadata } from 'next';
|
|
13
|
+
|
|
14
|
+
import { getServerClient } from '@/core/lib/brainerce.server';
|
|
15
|
+
import { FaqSection } from '@/ui/layout/faq-section';
|
|
16
|
+
<% if (i18nEnabled) { %>
|
|
17
|
+
type PageProps = {
|
|
18
|
+
params: Promise<{ locale: string }>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
22
|
+
const { locale } = await params;
|
|
23
|
+
const faq = await (await getServerClient(locale)).content.faq.get('main', locale).catch(() => null);
|
|
24
|
+
return {
|
|
25
|
+
title: faq?.name ?? 'FAQ',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default async function FaqPage({ params }: PageProps) {
|
|
30
|
+
const { locale } = await params;
|
|
31
|
+
const faq = await (await getServerClient(locale)).content.faq.get('main', locale).catch(() => null);
|
|
32
|
+
return <FaqSection faq={faq} />;
|
|
33
|
+
}
|
|
34
|
+
<% } else { %>
|
|
35
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
36
|
+
const faq = await (await getServerClient()).content.faq.get('main').catch(() => null);
|
|
37
|
+
return {
|
|
38
|
+
title: faq?.name ?? 'FAQ',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default async function FaqPage() {
|
|
43
|
+
const faq = await (await getServerClient()).content.faq.get('main').catch(() => null);
|
|
44
|
+
return <FaqSection faq={faq} />;
|
|
45
|
+
}
|
|
46
|
+
<% } %>
|
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* IndexNow key file — GET /indexnow-key.txt
|
|
3
|
-
*
|
|
4
|
-
* Brainerce's SEO Autopilot pings IndexNow (instant search-engine indexing)
|
|
5
|
-
* whenever a blog post publishes. The ping includes
|
|
6
|
-
* `keyLocation: https://<your-domain>/indexnow-key.txt`, and search engines
|
|
7
|
-
* verify ownership by fetching this file and matching the key.
|
|
8
|
-
*
|
|
9
|
-
* The key comes from `getStoreInfo().seo.indexNowKey` — it is NOT a secret
|
|
10
|
-
* (the file is public by protocol design). Returns 404 until the platform
|
|
11
|
-
* generates a key for this sales channel, which is harmless.
|
|
12
|
-
*/
|
|
13
|
-
import { getServerClient } from '@/core/lib/brainerce';
|
|
14
|
-
|
|
15
|
-
export const revalidate = 3600;
|
|
16
|
-
|
|
17
|
-
export async function GET() {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* IndexNow key file — GET /indexnow-key.txt
|
|
3
|
+
*
|
|
4
|
+
* Brainerce's SEO Autopilot pings IndexNow (instant search-engine indexing)
|
|
5
|
+
* whenever a blog post publishes. The ping includes
|
|
6
|
+
* `keyLocation: https://<your-domain>/indexnow-key.txt`, and search engines
|
|
7
|
+
* verify ownership by fetching this file and matching the key.
|
|
8
|
+
*
|
|
9
|
+
* The key comes from `getStoreInfo().seo.indexNowKey` — it is NOT a secret
|
|
10
|
+
* (the file is public by protocol design). Returns 404 until the platform
|
|
11
|
+
* generates a key for this sales channel, which is harmless.
|
|
12
|
+
*/
|
|
13
|
+
import { getServerClient } from '@/core/lib/brainerce.server';
|
|
14
|
+
|
|
15
|
+
export const revalidate = 3600;
|
|
16
|
+
|
|
17
|
+
export async function GET() {
|
|
18
|
+
const client = await getServerClient();
|
|
19
|
+
const info = await client.getStoreInfo().catch(() => null);
|
|
20
|
+
const key = info?.seo?.indexNowKey;
|
|
21
|
+
if (!key) return new Response(null, { status: 404 });
|
|
22
|
+
return new Response(key, {
|
|
23
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
24
|
+
});
|
|
25
|
+
}
|