Say you run a six-crew roofing company. Call it Ridgeline Roofing, working three counties outside Columbus. A homeowner with hail damage asks Google's AI mode "who does storm damage roof repair near me" and gets three names back. Ridgeline isn't one of them, despite being closer, cheaper, and better reviewed than at least one company that made the list.
The company that got named didn't outrank Ridgeline. It got read. Its site has structured data telling crawlers exactly what it does, where, and for how much — machine-readable facts instead of a homepage full of adjectives. Ridgeline's site has neither, so the AI has nothing to quote and moves to a business it can verify.
This is the practical half of GEO: showing up in an AI-generated answer instead of a ranked list of links. That post covered the concept. This one is the setup — the actual JSON-LD block a roofing company can drop into its site today.
Why roofing specifically
Roofing sits on top of a pile of facts an assistant needs and usually can't find on the page: what services (repair vs. full replacement vs. inspection vs. storm/insurance claims), what materials (asphalt shingle, metal, tile, flat/commercial membrane), what towns, and whether the company handles insurance claims directly. A roofing site that says "Quality You Can Trust Since 1998" gives an AI none of that. A roofing site with RoofingContractor schema gives it all of it, in a format built to be parsed rather than read.
Schema.org has a purpose-built type for this — RoofingContractor, a subtype of HomeAndConstructionBusiness — and almost nobody in the trade uses it. That gap is the opportunity.
The base block: who you are and where you work
This goes in a <script type="application/ld+json"> tag, typically in the <head> of the homepage. Swap every value for Ridgeline's with your own.
{
"@context": "https://schema.org",
"@type": "RoofingContractor",
"name": "Ridgeline Roofing",
"image": "https://example.com/assets/ridgeline-crew.jpg",
"telephone": "+1-614-555-0119",
"url": "https://example.com",
"priceRange": "$$",
"address": {
"@type": "PostalAddress",
"streetAddress": "418 Industrial Pkwy",
"addressLocality": "Columbus",
"addressRegion": "OH",
"postalCode": "43215"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 39.9612,
"longitude": -82.9988
},
"areaServed": [
{ "@type": "City", "name": "Columbus" },
{ "@type": "City", "name": "Westerville" },
{ "@type": "City", "name": "Dublin" },
{ "@type": "City", "name": "Delaware" }
],
"openingHoursSpecification": {
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "07:00",
"closes": "18:00"
}
}
Two fields do more work than the rest. areaServed as a list of named cities beats a vague "Central Ohio" — an assistant answering "roofer in Westerville" can match a named city directly, and can't confidently match a region it has to guess the boundaries of. geo matters for the same reason: it lets an assistant compute distance instead of trusting a claim.
The services block: what you actually do
A roofing company that only lists "roofing services" reads as generic. One that lists distinct, named services reads as a business with capabilities an assistant can match against a specific question. Add a makesOffer array to the same block, or as a separate script tag:
{
"@context": "https://schema.org",
"@type": "RoofingContractor",
"name": "Ridgeline Roofing",
"makesOffer": [
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Roof repair",
"areaServed": "Columbus, OH",
"description": "Leak repair, storm damage patching, and shingle replacement for residential roofs."
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Full roof replacement",
"areaServed": "Columbus, OH",
"description": "Tear-off and replacement, asphalt shingle and standing-seam metal."
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Insurance claim roof inspection",
"areaServed": "Columbus, OH",
"description": "Documented inspection and estimate for hail or wind damage claims."
}
}
]
}
That third service — insurance claim inspection — is worth naming explicitly if it's part of the business. "Does anyone handle the insurance paperwork" is a real question homeowners ask an assistant after a storm, and a company whose schema names that service directly is the one that gets surfaced for it.
A block worth stealing outright: storm response
Roofing has a seasonal, event-driven demand pattern that most other trades don't: a hailstorm hits a zip code and a thousand homeowners search "roof damage" in the same 48 hours. A Service entry naming storm response as its own offering, with areaServed matching the affected counties, is the single highest-value schema addition a roofing company can make going into storm season — it's the moment an assistant is fielding the most queries and most competitors' sites say nothing specific about it.
What to skip
Don't add aggregateRating or review markup unless the numbers are real and current — Google has penalized fabricated review schema before, and an assistant that catches an inflated or stale rating against the actual Google Business Profile loses trust in the rest of the page's data, not just that one field. Same logic as asking customers for specific language instead of star counts: the schema should describe what's true, not what looks good.
Where this actually goes
All of it belongs on the homepage, plus the Service entries repeated (with matching areaServed) on any dedicated service pages the site already has — a roof-replacement landing page should carry its own Service schema, not just live inside the homepage's array. Validate with Google's Rich Results Test before it ships; a malformed block is worse than no block, because it can get the whole page's structured data ignored.
Same principle as the after-hours phone call for HVAC and plumbing: the business that's easiest to verify wins the moment the customer is ready to act, and for roofing that moment is disproportionately a storm, not a slow Tuesday afternoon.
FAQ
Do I need a developer to add this?
No. It's a static block of JSON pasted into the page's HTML — most site builders (WordPress, Webflow, Squarespace with a code injection field) have a spot for a custom <head> script. The work is filling in accurate values, not writing code.
Will this actually move the needle, or is it a checkbox?
On its own it's necessary, not sufficient — it makes the site legible to a crawler, it doesn't make the crawler prefer you over a competitor with a stronger review history or more consistent citations. Pair it with the review and consistency fixes from the GEO post; schema without evidence behind it is a data sheet nobody's checking against anything.
How often does this need updating?
Whenever the facts change — new service area, new service line, new hours. Stale schema (a service area that no longer matches where the crews actually work, hours that changed after a staffing shift) is worse than none, because an assistant that catches the mismatch against the Google Business Profile or a recent review starts discounting the rest of the page.