feat(web): pricing — by-the-numbers stat strip, verifiable facts only

Adds a four-stat band below the hero CTAs — 27 brand catalogs, 1M+ OEM/alt
parts, 30-day free trial, 7-day no-questions refund. Big tabular-nums numerals
with small labels; divides on sm+. Every number is product or policy fact we
can defend — no fabricated customer counts, no testimonial copy.

Side effect: FAQPage JSON-LD now injected into <head> via useEffect (was an
inline <script dangerouslySetInnerHTML>). Same SEO payload, less attack
surface, tagged with data-sase-faq so it's diff-friendly.
This commit is contained in:
2026-05-31 23:18:14 +03:00
parent b8bbdbda67
commit 931c0cfd85
3 changed files with 74 additions and 19 deletions

View File

@@ -545,6 +545,12 @@
"yearly": "Yearly"
},
"yearlyBadge": "2 months free",
"stats": {
"brands": { "value": "27", "label": "brand catalogs" },
"parts": { "value": "1M+", "label": "OEM & alternative parts" },
"trial": { "value": "30 days", "label": "full access, free" },
"refund": { "value": "7 days", "label": "no-questions refund" }
},
"pageTitle": "Pricing — Sase.tr | Chassis Search Plans",
"how": {
"title": "How it works",

View File

@@ -545,6 +545,12 @@
"yearly": "Yıllık"
},
"yearlyBadge": "2 ay bedava",
"stats": {
"brands": { "value": "27", "label": "marka kataloğu" },
"parts": { "value": "1M+", "label": "OEM ve alternatif parça" },
"trial": { "value": "30 gün", "label": "tüm özellikler ücretsiz" },
"refund": { "value": "7 gün", "label": "koşulsuz iade" }
},
"pageTitle": "Fiyatlandırma — Sase.tr | Şase Sorgulama Planları",
"how": {
"title": "Nasıl çalışır",

View File

@@ -17,7 +17,7 @@ import {
Info,
RotateCcw,
} from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
export const Route = createFileRoute("/pricing")({
component: PricingPage,
@@ -134,6 +134,8 @@ function PricingPage() {
</div>
</div>
<PricingStatsStrip />
<div className="mx-auto mt-10 flex max-w-2xl items-start justify-center gap-2 rounded-lg border border-border bg-muted/30 px-4 py-2.5 text-sm text-muted-foreground">
<Info className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
<span>{t("subscription.planHelper")}</span>
@@ -162,6 +164,37 @@ function PricingPage() {
);
}
// Stat row — verifiable product/policy facts only (catalog size, trial length,
// refund window). No customer-count claims, no testimonials.
function PricingStatsStrip() {
const { t } = useTranslation();
const items = [
{ value: "pricing.stats.brands.value", label: "pricing.stats.brands.label" },
{ value: "pricing.stats.parts.value", label: "pricing.stats.parts.label" },
{ value: "pricing.stats.trial.value", label: "pricing.stats.trial.label" },
{ value: "pricing.stats.refund.value", label: "pricing.stats.refund.label" },
];
return (
<div className="mx-auto mt-12 max-w-5xl">
<dl className="grid grid-cols-2 gap-y-6 rounded-2xl border border-border bg-muted/20 p-6 sm:grid-cols-4 sm:gap-y-0 sm:divide-x sm:divide-border">
{items.map((it) => (
<div
key={it.value}
className="flex flex-col items-center justify-center px-4 text-center"
>
<dt className="text-3xl font-bold leading-none tabular-nums text-foreground sm:text-4xl">
{t(it.value)}
</dt>
<dd className="mt-2 text-xs uppercase tracking-wider text-muted-foreground sm:text-sm sm:normal-case sm:tracking-normal">
{t(it.label)}
</dd>
</div>
))}
</dl>
</div>
);
}
function BillingPeriodToggle({
value,
onChange,
@@ -354,25 +387,40 @@ export function PricingHowItWorks() {
);
}
const FAQ_IDS = [1, 2, 3, 4, 5, 6] as const;
export function PricingFaq() {
const { t } = useTranslation();
const ids = [1, 2, 3, 4, 5, 6] as const;
// Emit FAQPage JSON-LD so Google can render rich-result Q/A blocks in SERP
// — pure SEO upside, no UI cost. Renders inert <script type=application/ld+json>.
const jsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: ids.map((i) => ({
"@type": "Question",
name: t(`pricing.faq.q${i}`),
acceptedAnswer: { "@type": "Answer", text: t(`pricing.faq.a${i}`) },
})),
};
// Inject FAQPage JSON-LD into <head> so Google can render rich-result Q/A
// blocks in SERP. Imperative head injection keeps us off React's `script`
// path and avoids dangerouslySetInnerHTML — content is i18n-sourced anyway,
// but this is also the cleanest way to render structured data.
useEffect(() => {
const payload = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: FAQ_IDS.map((i) => ({
"@type": "Question",
name: t(`pricing.faq.q${i}`),
acceptedAnswer: { "@type": "Answer", text: t(`pricing.faq.a${i}`) },
})),
};
const script = document.createElement("script");
script.type = "application/ld+json";
script.setAttribute("data-sase-faq", "1");
script.text = JSON.stringify(payload);
document.head.appendChild(script);
return () => {
script.remove();
};
}, [t]);
return (
<section className="mx-auto mt-20 max-w-3xl">
<h2 className="text-center text-2xl font-bold">{t("pricing.faq.title")}</h2>
<div className="mt-8 divide-y divide-border overflow-hidden rounded-2xl border border-border">
{ids.map((i) => (
{FAQ_IDS.map((i) => (
<details key={i} className="group">
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 p-5 font-medium">
{t(`pricing.faq.q${i}`)}
@@ -384,11 +432,6 @@ export function PricingFaq() {
</details>
))}
</div>
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: structured-data payload, content is from our own i18n bundle. */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</section>
);
}