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.
@@ -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?q={search_term_string}`,
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