SEO: render final stat values in SSR (was rendering 0+ to crawlers and OG previewers)

This commit is contained in:
Zac Gaetano 2026-05-04 00:01:59 -04:00
parent a0863a3e20
commit 9cc2513b63

View file

@ -10,12 +10,28 @@ const stats = [
{ value: 5, suffix: "", label: "Industry Verticals", description: "Sports, corporate, financial, aerospace, defense" },
];
function formatCount(count: number, value: number) {
return count >= 1000 && value >= 1000
? `${(count / 1000).toFixed(1).replace(/\.0$/, "")}k`
: count.toString();
}
function AnimatedNumber({ value, suffix }: { value: number; suffix: string }) {
const [count, setCount] = useState(0);
// Start with the final value so SSR/crawlers/OG-previewers see real numbers.
// Animate from 0 -> value only after hydration, when the user can see it.
const [count, setCount] = useState<number>(value);
const [hasMounted, setHasMounted] = useState(false);
const ref = useRef<HTMLSpanElement>(null);
const hasAnimated = useRef(false);
useEffect(() => {
setHasMounted(true);
}, []);
useEffect(() => {
if (!hasMounted) return;
// Reset to 0 only on the client, then count up.
setCount(0);
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !hasAnimated.current) {
@ -39,13 +55,11 @@ function AnimatedNumber({ value, suffix }: { value: number; suffix: string }) {
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, [value]);
}, [value, hasMounted]);
return (
<span ref={ref} className="tabular-nums">
{count >= 1000 && value >= 1000
? `${(count / 1000).toFixed(1).replace(/\.0$/, "")}k`
: count}
{formatCount(count, value)}
{suffix}
</span>
);
@ -53,7 +67,7 @@ function AnimatedNumber({ value, suffix }: { value: number; suffix: string }) {
export default function Stats() {
return (
<section className="py-16 md:py-20 bg-white border-t border-neutral-100">
<section className="py-16 md:py-20 bg-white border-t border-neutral-100" aria-label="Practice statistics">
<div className="max-w-6xl mx-auto px-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-10 md:gap-6">
{stats.map((stat, i) => (