create-brainerce-store 1.53.0 → 1.54.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/src/app/blog/[slug]/page.tsx.ejs +17 -31
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +92 -0
- package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -0
- package/templates/nextjs/base/src/app/llms.txt/route.ts +44 -0
- package/templates/nextjs/base/src/app/sitemap.ts +18 -1
- package/templates/nextjs/base/src/components/brainerce-bot.tsx +7 -0
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +564 -551
- package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -0
- package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -0
- package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +4 -1
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +7 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { BlogPost } from 'brainerce';
|
|
2
|
+
import { buildArticleJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
|
|
3
|
+
import { getNonce } from '@/core/lib/nonce';
|
|
4
|
+
|
|
5
|
+
interface ArticleJsonLdProps {
|
|
6
|
+
post: BlogPost;
|
|
7
|
+
url: string;
|
|
8
|
+
/** Store display name — feeds Article.publisher (and the author fallback). */
|
|
9
|
+
organizationName?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Article + BreadcrumbList structured data for a blog post page.
|
|
14
|
+
*
|
|
15
|
+
* Uses the SDK builders for the schema shape, but renders the <script> here so
|
|
16
|
+
* we can attach the CSP nonce (the SDK's jsonLdScriptProps can't).
|
|
17
|
+
*/
|
|
18
|
+
export async function ArticleJsonLd({ post, url, organizationName }: ArticleJsonLdProps) {
|
|
19
|
+
const nonce = await getNonce();
|
|
20
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
|
|
21
|
+
|
|
22
|
+
const articleJsonLd = buildArticleJsonLd(post, {
|
|
23
|
+
siteUrl: baseUrl,
|
|
24
|
+
path: url,
|
|
25
|
+
organizationName,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const breadcrumbJsonLd = buildBreadcrumbJsonLd([
|
|
29
|
+
{ name: 'Home', url: baseUrl || '/' },
|
|
30
|
+
{ name: 'Blog', url: `${baseUrl}/blog` },
|
|
31
|
+
{ name: post.title, url },
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<script
|
|
37
|
+
type="application/ld+json"
|
|
38
|
+
nonce={nonce}
|
|
39
|
+
suppressHydrationWarning
|
|
40
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(articleJsonLd) }}
|
|
41
|
+
/>
|
|
42
|
+
<script
|
|
43
|
+
type="application/ld+json"
|
|
44
|
+
nonce={nonce}
|
|
45
|
+
suppressHydrationWarning
|
|
46
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
|
|
47
|
+
/>
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Escape `<`, `>`, `&` so AI-generated article copy can't break out of the
|
|
53
|
+
// <script> element or inject HTML.
|
|
54
|
+
function serializeJsonLd(value: unknown): string {
|
|
55
|
+
return JSON.stringify(value)
|
|
56
|
+
.replace(/</g, '\\u003c')
|
|
57
|
+
.replace(/>/g, '\\u003e')
|
|
58
|
+
.replace(/&/g, '\\u0026');
|
|
59
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CategoryDetail } from 'brainerce';
|
|
2
|
+
import { buildCollectionPageJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
|
|
3
|
+
import { getNonce } from '@/core/lib/nonce';
|
|
4
|
+
|
|
5
|
+
interface CategoryJsonLdProps {
|
|
6
|
+
category: CategoryDetail;
|
|
7
|
+
url: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* CollectionPage + BreadcrumbList structured data for a category (collection)
|
|
12
|
+
* landing page. NEVER emit Product markup on a listing page — Google's Product
|
|
13
|
+
* rich results are single-product only (see ProductJsonLd for PDPs).
|
|
14
|
+
*
|
|
15
|
+
* Uses the SDK builders for the schema shape, but renders the <script> here so
|
|
16
|
+
* we can attach the CSP nonce (the SDK's jsonLdScriptProps can't).
|
|
17
|
+
*/
|
|
18
|
+
export async function CategoryJsonLd({ category, url }: CategoryJsonLdProps) {
|
|
19
|
+
const nonce = await getNonce();
|
|
20
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
|
|
21
|
+
|
|
22
|
+
const collectionJsonLd = buildCollectionPageJsonLd(category, { siteUrl: baseUrl, path: url });
|
|
23
|
+
|
|
24
|
+
// Breadcrumb: Home → ancestor categories → this category.
|
|
25
|
+
const crumbs: Array<{ name: string; url: string }> = [
|
|
26
|
+
{ name: 'Home', url: baseUrl || '/' },
|
|
27
|
+
];
|
|
28
|
+
for (const ancestor of category.breadcrumb) {
|
|
29
|
+
if (ancestor.slug) {
|
|
30
|
+
crumbs.push({ name: ancestor.name, url: `${baseUrl}/category/${ancestor.slug}` });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
crumbs.push({ name: category.name, url });
|
|
34
|
+
const breadcrumbJsonLd = buildBreadcrumbJsonLd(crumbs);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<script
|
|
39
|
+
type="application/ld+json"
|
|
40
|
+
nonce={nonce}
|
|
41
|
+
suppressHydrationWarning
|
|
42
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(collectionJsonLd) }}
|
|
43
|
+
/>
|
|
44
|
+
<script
|
|
45
|
+
type="application/ld+json"
|
|
46
|
+
nonce={nonce}
|
|
47
|
+
suppressHydrationWarning
|
|
48
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
|
|
49
|
+
/>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Escape `<`, `>`, `&` so merchant-authored category copy can't break out of
|
|
55
|
+
// the <script> element or inject HTML.
|
|
56
|
+
function serializeJsonLd(value: unknown): string {
|
|
57
|
+
return JSON.stringify(value)
|
|
58
|
+
.replace(/</g, '\\u003c')
|
|
59
|
+
.replace(/>/g, '\\u003e')
|
|
60
|
+
.replace(/&/g, '\\u0026');
|
|
61
|
+
}
|
|
@@ -58,8 +58,11 @@ export async function OrganizationJsonLd({ storeInfo, baseUrl }: OrganizationJso
|
|
|
58
58
|
potentialAction: {
|
|
59
59
|
'@type': 'SearchAction',
|
|
60
60
|
target: {
|
|
61
|
+
// Must match the storefront's real search param — the product listing
|
|
62
|
+
// reads `?search=` (see use-product-listing.ts), not `?q=`. A wrong
|
|
63
|
+
// param makes the sitelinks search box land on an empty listing.
|
|
61
64
|
'@type': 'EntryPoint',
|
|
62
|
-
urlTemplate: `${baseUrl}/products?
|
|
65
|
+
urlTemplate: `${baseUrl}/products?search={search_term_string}`,
|
|
63
66
|
},
|
|
64
67
|
'query-input': 'required name=search_term_string',
|
|
65
68
|
},
|
|
@@ -48,6 +48,7 @@ export async function ProductJsonLd({ product, url, currency }: ProductJsonLdPro
|
|
|
48
48
|
// and AI Overviews ingest this field directly and reject markup. Strip HTML
|
|
49
49
|
// first; if the description is empty after stripping, fall back to the name.
|
|
50
50
|
const cleanDescription = stripHtmlForSeo(product.description) || product.name;
|
|
51
|
+
const brandName = product.brands?.[0]?.name;
|
|
51
52
|
const productJsonLd: Record<string, unknown> = {
|
|
52
53
|
'@context': 'https://schema.org',
|
|
53
54
|
'@type': 'Product',
|
|
@@ -56,6 +57,12 @@ export async function ProductJsonLd({ product, url, currency }: ProductJsonLdPro
|
|
|
56
57
|
image: imageUrl,
|
|
57
58
|
url,
|
|
58
59
|
sku: product.sku || product.id,
|
|
60
|
+
// Brand + identifiers qualify the PDP for Google's free merchant listings
|
|
61
|
+
// (no Merchant Center feed needed). Emitted only when present — Google
|
|
62
|
+
// penalizes fabricated identifiers.
|
|
63
|
+
...(brandName ? { brand: { '@type': 'Brand', name: brandName } } : {}),
|
|
64
|
+
...(product.gtin ? { gtin: product.gtin } : {}),
|
|
65
|
+
...(product.mpn ? { mpn: product.mpn } : {}),
|
|
59
66
|
offers,
|
|
60
67
|
};
|
|
61
68
|
|