create-brainerce-store 1.65.0 → 1.66.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.65.0",
3
+ "version": "1.66.0",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
5
  "bin": {
6
6
  "create-brainerce-store": "dist/index.js"
@@ -8,6 +8,7 @@ import { decodeSlug } from '@/core/lib/utils';
8
8
  import { ProductJsonLd } from '@/components/seo/product-json-ld';
9
9
  import { Breadcrumbs } from '@/components/seo/breadcrumbs';
10
10
  import { ReviewsSection } from '@/ui/product/reviews-section';
11
+ import { ProductFaqSection } from '@/ui/product/faq-section';
11
12
  import { ProductClientSection } from '@/ui/product/product-client-section';
12
13
 
13
14
  type Props = {
@@ -181,6 +182,9 @@ export default async function ProductDetailPage({ params }: Props) {
181
182
  <Breadcrumbs items={breadcrumbItems} />
182
183
  </div>
183
184
  <ProductClientSection product={product} />
185
+ {/* Visible Q&A + the FAQPage JSON-LD above read the same product.faq —
186
+ the extractable format AI answer engines cite. */}
187
+ <ProductFaqSection product={product} locale={locale} />
184
188
  <ReviewsSection productId={product.id} />
185
189
  </>
186
190
  );
@@ -1,5 +1,5 @@
1
1
  import type { Product, ShippingSummaryEntry } from 'brainerce';
2
- import { buildProductJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
2
+ import { buildProductJsonLd, buildBreadcrumbJsonLd, buildProductFaqJsonLd } from 'brainerce';
3
3
  import { getNonce } from '@/core/lib/nonce';
4
4
  import { resolveCurrency } from '@/core/lib/resolve-currency';
5
5
 
@@ -42,6 +42,11 @@ export async function ProductJsonLd({ product, url, currency, shipping }: Produc
42
42
  { name: product.name, url },
43
43
  ]);
44
44
 
45
+ // FAQPage from the product's Q&A — null (no script) when the product has
46
+ // none. The visible FAQ section on the PDP renders the SAME pairs; this is
47
+ // corroboration for engines, never a substitute for visible text.
48
+ const faqJsonLd = buildProductFaqJsonLd(product);
49
+
45
50
  return (
46
51
  <>
47
52
  <script
@@ -56,6 +61,14 @@ export async function ProductJsonLd({ product, url, currency, shipping }: Produc
56
61
  suppressHydrationWarning
57
62
  dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
58
63
  />
64
+ {faqJsonLd ? (
65
+ <script
66
+ type="application/ld+json"
67
+ nonce={nonce}
68
+ suppressHydrationWarning
69
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(faqJsonLd) }}
70
+ />
71
+ ) : null}
59
72
  </>
60
73
  );
61
74
  }
@@ -0,0 +1,46 @@
1
+ import type { Product } from 'brainerce';
2
+ <% if (i18nEnabled) { %>import { getMessages, defaultLocale } from '@/i18n';<% } else { %>import { defaultLocale, messages } from '@/i18n';<% } %>
3
+
4
+ type FaqStrings = Record<string, string>;
5
+
6
+ <% if (i18nEnabled) { %>async function getFaqStrings(locale: string): Promise<FaqStrings> {
7
+ return ((await getMessages(locale)).faq ?? {}) as FaqStrings;
8
+ }<% } else { %>async function getFaqStrings(_locale: string): Promise<FaqStrings> {
9
+ return ((messages as Record<string, unknown>).faq ?? {}) as FaqStrings;
10
+ }<% } %>
11
+
12
+ interface ProductFaqSectionProps {
13
+ product: Product;
14
+ locale?: string;
15
+ }
16
+
17
+ /**
18
+ * Visible product Q&A — the extractable question/answer format AI answer
19
+ * engines cite. The SAME pairs are emitted as FAQPage JSON-LD by
20
+ * <ProductJsonLd> (both read product.faq, so they can never drift). Server
21
+ * component on purpose: the pairs must be in the initial HTML payload — AI
22
+ * crawlers don't run JavaScript. Content inside a closed <details> is still
23
+ * present in the HTML, so the accordion costs nothing for extraction.
24
+ */
25
+ export async function ProductFaqSection({
26
+ product,
27
+ locale = defaultLocale,
28
+ }: ProductFaqSectionProps) {
29
+ const pairs = (product.faq ?? []).filter((item) => item?.q && item?.a);
30
+ if (pairs.length === 0) return null;
31
+ const t = await getFaqStrings(locale);
32
+
33
+ return (
34
+ <section className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8">
35
+ <h2 className="mb-6 text-2xl font-semibold">{t.title ?? 'Frequently asked questions'}</h2>
36
+ <div className="divide-y rounded-xl border">
37
+ {pairs.map((item) => (
38
+ <details key={item.q} className="px-5 py-4">
39
+ <summary className="cursor-pointer text-base font-medium">{item.q}</summary>
40
+ <p className="mt-2 text-sm leading-relaxed opacity-80">{item.a}</p>
41
+ </details>
42
+ ))}
43
+ </div>
44
+ </section>
45
+ );
46
+ }