Service Schema Tells Search Engines What You Do and Where
The /ai page had a FAQPage schema for a while. It answers the questions a prospect might type into a search bar: "who does AI automation in Carmel" or "AI workflow consulting Hamilton County." That's one job. But there was no machine-readable signal identifying the actual service, who provides it, or what geography it covers. We added a Service schema to fill that gap.
Two Schemas, Two Jobs
FAQPage is Q&A. Search engines and AI answer engines use it to pull a quoted answer from your page. It doesn't describe your business or your offer. It describes a list of question-answer pairs.
Service describes the offer itself: the service type, a description, the provider, and the geographic area covered. When both schemas are on the same page, a crawler can connect the dots. The FAQ answers questions about the service; the Service node tells the engine what that service is and where it applies.
On /ai we render them in sequence:
<JsonLd data={serviceSchema({ ... })} />
<JsonLd data={faqPageSchema(faqs)} />
Two separate <script type="application/ld+json"> blocks, both server-rendered.
What the Service Schema Actually Contains
serviceSchema({
name: "Fractional AI Operations Partner",
serviceType: "AI workflow automation and operations consulting",
description:
"We install AI operations engines into service businesses — quote drafting, scheduling, follow-ups, and job-history memory — and maintain them on an optional monthly retainer. Starts with a free 30-minute operations audit.",
url: `${localBusiness.url}/ai`,
provider: {
name: localBusiness.name,
url: localBusiness.url,
telephone: localBusiness.telephone,
},
areaServed: cityServiceArea,
})
cityServiceArea is cities.map((c) => c.name), which gives you six strings: Kokomo, Carmel, Fishers, Westfield, Noblesville, Indianapolis. The helper maps each to { "@type": "City", name } in the output JSON-LD. Schema.org's Service type accepts an array for areaServed, so all six land in one block.
The Phrase Has to Match
The name field in the schema is "Fractional AI Operations Partner." That same phrase now appears in:
- The
/aipage meta description - The city-specific FAQ answers in
src/content/local.ts - The city landing page metadata generated by
generateMetadata()
That repetition is deliberate. When an AI search engine sees the same phrase in the <meta name="description">, in FAQ answer text, and in a Service schema node on the same domain, it has strong signal that the phrase describes a real, stable offering. A one-off mention in a heading is weaker.
The FAQ answers in local.ts were updated at the same time as the schema addition. They went from "Twenty One Media provides AI consulting..." to "Twenty One Media is a fractional AI operations partner that..." The schema and the prose now reinforce each other.
The Helper Function
We keep all JSON-LD constructors in src/lib/jsonLd.ts. The serviceSchema function takes a typed input object and returns plain JSON. No library, no runtime dependency:
export function serviceSchema(input: ServiceSchemaInput) {
return {
"@context": "https://schema.org",
"@type": "Service",
name: input.name,
serviceType: input.serviceType,
description: input.description,
url: input.url,
provider: {
"@type": "LocalBusiness",
name: input.provider.name,
url: input.provider.url,
...(input.provider.telephone
? { telephone: input.provider.telephone }
: {}),
},
areaServed: input.areaServed.map((name) => ({ "@type": "City", name })),
};
}
The telephone is optional because not every service schema needs it. The provider type is LocalBusiness rather than Organization because we're a geographic service business. That distinction matters for local search ranking signals.
What This Covers That FAQPage Doesn't
A FAQPage schema by itself doesn't tell a search engine your service has a specific provider with a phone number and a footprint across six Indiana cities. It just tells the engine "these are Q&A pairs." The Service schema is the entity record. Both belong on the page.