# Stop Hardcoding Testimonials: Model Proof as Data

Most small business sites ship their social proof as copy. Three testimonials in a grid, typed into the layout by hand, each one a string in the markup with a first name and a last initial.

It renders fine and it fails three separate ways. The content goes stale the moment a better review arrives and nobody edits the page. The claims are unverifiable, because an anonymous quote inside your own HTML carries no more weight than a headline you wrote yourself. And none of it is structured, so crawlers and answer engines parsing your page for evidence about this business find prose where they could have found entities.

The fix is to stop treating proof as copy and start treating it as data with a schema, an ingestion path, and a serialization step. Here is the model we use, the code for the ingestion half, and the structured data rules that genuinely apply, including one widely repeated mistake worth avoiding.

Three entity types, not one

Teams usually build a single Testimonial type and push everything into it. That collapses three things with different provenance, different update cadences, and different schema treatment.

Entity

Source of truth

Who vouches

Update cadence

Schema treatment

Review

Third party platform (Google, Yelp, Facebook)

The customer, on a platform you do not control

Continuous, pulled

Review with a real author, attached to the thing reviewed

CaseStudy

You, with client consent

You, with named client corroboration

Per project

Article or CreativeWork, optionally carrying a Review

Credential

Issuing body

A certifying organization

Rarely

hasCredential or award on the business entity

The distinction that matters most is the first column. A Review you pulled from Google is evidence because the platform holds it and the author can be checked. A quote you typed into your own CMS is a claim. Modelling them as the same type invites you to display them identically, which quietly drags the credible one down to the level of the other.

Give each a real content type with required fields. For Review: authorName, rating, text, publishedAt, sourcePlatform, sourceUrl. For CaseStudy: client, service, vehicleOrContext, metricBefore, metricAfter, consentOnFile. Make consentOnFile a required boolean and let it gate rendering, so an unapproved client name cannot reach production by accident.

Ingesting reviews without a stale cache or a quota bill

Reviews have to come from the platform on a schedule. Hitting the Places API on every page render is slow, costs money per request, and is unnecessary for data that changes weekly at best.

Place Details in the Places API (New) returns the rating, the total rating count, and up to five reviews. That five is a hard ceiling, which has one important consequence: the displayed aggregate has to come from userRatingCount and rating, never from counting the array you received.

type ProofReview = { authorName: string rating: number text: string publishedAt: string sourcePlatform: "google" sourceUrl: string }

type ProofSnapshot = { rating: number reviewCount: number reviews: ProofReview\[\] fetchedAt: string }

const FIELDS = \["rating", "userRatingCount", "reviews"\].join(",")

export async function fetchGoogleProof(placeId: string): Promise { const res = await fetch( `https://places.googleapis.com/v1/places/${placeId}`, { headers: { "X-Goog-Api-Key": process.env.PLACES\_API\_KEY!, "X-Goog-FieldMask": FIELDS, }, // revalidate daily; the data moves slower than that next: { revalidate: 86\_400 }, } )

if (!res.ok) throw new Error(`Places ${res.status}`) const place = await res.json()

return { rating: place.rating ?? 0, reviewCount: place.userRatingCount ?? 0, reviews: (place.reviews ?? \[\]).map((r: any) => ({ authorName: r.authorAttribution?.displayName ?? "Google user", rating: r.rating, text: r.originalText?.text ?? r.text?.text ?? "", publishedAt: r.publishTime, sourcePlatform: "google" as const, sourceUrl: r.authorAttribution?.uri ?? "", })), fetchedAt: new Date().toISOString(), } }

Two constraints to build around rather than discover later. Google's Maps Platform terms permit only temporary caching of place content, with place IDs as the documented exception, so this should be a short lived revalidating cache and not a one time copy into your own database (Places policies). And authorAttribution is not decorative. The name and profile link are required attribution when you display the review, which is also why sourceUrl belongs in the model.

On a platform without server-side rendering, the same function belongs in a scheduled job that writes the snapshot into a CMS collection, with the site reading the collection. We build Framer sites and booking systems for automotive shops at Xenon Builds, so this particular shape, a scheduled pull into a CMS collection that the layout binds to, is the one that comes up most often.

The schema rules that actually apply

This is where most implementations go wrong, and confidently.

You cannot mark up your own aggregate rating and expect stars. Google's review snippet documentation is explicit: if the entity being reviewed controls the reviews about itself, pages using LocalBusiness or any other Organization type are ineligible for the star review feature (review snippet docs). Self-collected reviews on your own domain are exactly that case. Putting aggregateRating on your business entity buys you nothing in search and risks a manual action.

AutoDetailing is not a schema.org type. It gets recommended in a lot of local SEO writing, including some of ours, and it does not exist. AutomotiveBusiness has exactly nine subtypes: AutoBodyShop, AutoDealer, AutoPartsStore, AutoRental, AutoRepair, AutoWash, GasStation, MotorcycleDealer, MotorcycleRepair (schema.org). For a detailing or coating shop, AutoWash is the nearest real subtype and AutomotiveBusiness is the honest fallback. An invented type is parsed as an unknown string and gives you a less specific entity than the generic parent would have.

So what is the markup for, if not stars? Entity clarity and extraction. Answer engines reading your page for evidence benefit from a named author attached to a specific service, and that holds whether or not a rich result is on the table. Attach the Review to the Service on the case study page, with a real person as author:

{ "@context": "https://schema.org", "@type": "Service", "name": "Ceramic Coating", "serviceType": "Ceramic coating", "provider": { "@type": "AutoWash", "name": "Example Detail Co", "address": { "@type": "PostalAddress", "addressLocality": "Jacksonville", "addressRegion": "FL", "addressCountry": "US" } }, "areaServed": { "@type": "City", "name": "Jacksonville" }, "review": { "@type": "Review", "author": { "@type": "Person", "name": "Christopher C." }, "datePublished": "2026-04-18", "reviewRating": { "@type": "Rating", "ratingValue": 5, "bestRating": 5 }, "reviewBody": "Hopped on a call and he explained the process clearly and walked me through every step." } }

Serialize this from the same CaseStudy record that renders the visible block. Two sources of truth for one fact is how sites end up shipping a four star rating in the markup and a five star rating on screen.

Order of work

Define the three content types with required fields, and make client consent a gating boolean.

Stand up the scheduled Places pull, storing rating and userRatingCount separately from the review array.

Render attribution with every pulled review, name and profile link included.

Serialize JSON-LD from the same records the layout binds to, never from a second hand written blob.

Keep the proof block out of the critical render path. It is below the fold, the images are avatars, and it should not be what delays your largest contentful paint.

Does it move anything

Here is the honest limit of what we can claim. We do not have a clean measured before and after for this change on a client build, because it has always shipped alongside a wider rebuild, and attributing a conversion delta to the proof block alone would be guesswork.

What exists is industry data rather than our data. Analysis of the vehicle services category indicates businesses carrying more than 50 reviews and a 4.5 star or better average win more conversions and more repeat work (RepuClinic, citing Grand View Research). Treat that as directional, not as a measured effect of an implementation.

The defensible argument for doing it this way is structural rather than statistical. Proof modelled as data stays current without an editor, carries attribution that makes it checkable, and serializes into structured data from a single source. Proof modelled as copy does none of those things and needs a human to remember it exists.

Written by the team at Xenon Builds. We build Framer sites and booking systems for automotive shops across the US, which means we spend a lot of time on conversion, page speed and form UX for a very specific kind of small business.
