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/dist/index.js +2 -2
- package/messages/en.json +505 -502
- package/messages/he.json +505 -502
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +4 -0
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +14 -1
- package/templates/nextjs/base/src/ui/product/faq-section.tsx.ejs +46 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|