Strategy Guide // 05

Schema Markup for Roofers: The Copy-Paste Setup AI Search Can Read

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 on the list, even though it's closer, cheaper, and better reviewed than at least one company that is.

The company that got named has structured data on its site. That markup spells out its services and service area for a crawler, down to a price range, so nothing has to be guessed. Ridgeline's homepage is mostly adjectives. The AI finds nothing to quote there, so it picks a business it can verify.

This post is the hands-on side of GEO, which means showing up inside an AI-generated answer instead of in a ranked list of links. The earlier post covered the idea. This one gives you the JSON-LD a roofing company can add to its site today.

Why roofing is a good fit

An assistant answering a roofing question needs facts most roofing sites never state. It wants to know which services you offer (repair, full replacement, inspection, storm and insurance work) and which materials you install, from asphalt shingle and metal to tile and flat commercial membrane. It also needs the towns you cover and whether you handle insurance claims directly. A homepage that says "Quality You Can Trust Since 1998" answers none of that. RoofingContractor schema can answer all of it, in a format built for parsing.

Schema.org already has a type made for the trade. RoofingContractor is a subtype of HomeAndConstructionBusiness, and almost nobody in roofing uses it. That gap is your opening.

The base block

Put this inside a <script type="application/ld+json"> tag, usually in the <head> of the homepage. Replace every Ridgeline value 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 carry most of the weight. The first is areaServed. A list of named cities beats "Central Ohio," because an assistant answering "roofer in Westerville" can match a named city directly. It can't confidently match a region whose edges it would have to guess. The second is geo, which lets the assistant work out distance from coordinates instead of taking your word for it.

The services block

A company that lists only "roofing services" looks generic. Name each service separately and an assistant has something to match against a specific question. Add a makesOffer array, either to the same block or in 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."
      }
    }
  ]
}

If insurance claim inspections are part of the business, name that service outright. After a storm, homeowners ask assistants whether anyone will handle the insurance paperwork. The company whose schema names that service is the one that comes up.

Storm response, the block to copy first

Roofing demand follows the weather in a way most trades don't see. A hailstorm hits a zip code and a thousand homeowners search "roof damage" within the same 48 hours. Give storm response its own Service entry, with areaServed set to the affected counties. Going into storm season, nothing else you add to your schema is worth as much. Assistants field the most queries in those days, and most competitors' sites say nothing specific about storm work.

What to leave out

Skip aggregateRating and review markup unless the numbers are real and current. Google has penalized fabricated review schema before. An assistant that catches an inflated or stale rating against your Google Business Profile also stops trusting everything else the page claims. The same reasoning sits behind asking customers for specific language instead of star counts. Schema should describe what's true, even when a better-looking number is tempting.

Where the markup goes

All of it goes on the homepage. Then repeat the Service entries, with matching areaServed, on any service pages the site already has. A roof replacement landing page should carry its own Service schema instead of leaning on the homepage's array.

Run each block through Google's Rich Results Test before it ships. A malformed block can get all of a page's structured data ignored, which leaves you worse off than having none.

The after-hours phone call for HVAC and plumbing works on the same principle. When a customer is ready to act, the business that's easiest to verify wins. For roofers, that moment comes after a storm far more often than on a slow Tuesday afternoon.

FAQ

Do I need a developer to add this?

No. It's a fixed block of JSON pasted into the page's HTML. Most site builders have a place for a custom <head> script, including WordPress, Webflow, and Squarespace through its code injection field. The work is in getting the values right. You aren't writing code.

Is schema enough on its own?

You need it, but it won't win by itself. Schema makes the site readable to a crawler. A competitor with a stronger review history or more consistent citations can still come out ahead. Pair it with the review and consistency fixes from the GEO post, because schema with no evidence behind it is a data sheet nobody checks.

How often does this need updating?

Whenever the facts change, whether that's a new service area, a new service line, or new hours. Stale schema is worse than none. Say your service area no longer matches where the crews work, or your hours changed after a staffing shift. An assistant that spots the mismatch against your Google Business Profile or a recent review starts discounting the rest of the page.

Reading about it is step one. Running on it is step two.

Level 99 builds the CRM, automations, and AI agents that answer, book, and follow up with every lead, so the traffic your visibility earns becomes booked jobs.

Book a Call

Prefer proof first? Read the case studies. The math is shown so you can check it.