Skip to content
Lingows
Faceted iceberg over a wireframe foundation, for writing schema markup that validates.

Tutorial

Write schema markup that validates

Most schema markup on small business pages is broken in ways Google Search Console never flags. This walks through building a single connected JSON-LD graph for Organization, LocalBusiness, Service, FAQPage, and BreadcrumbList, and testing it properly.

  • Intermediate
  • 12 minute read
  • Last reviewed 2026-08-10

Schema markup does not directly move rankings, but it removes ambiguity for machines reading your page. Search engines and answer engines both use it to confirm what an entity is, who it belongs to, and how pages relate to each other. Bad schema is worse than no schema, because it asserts wrong facts with confidence.

The most common failure is not a syntax error. It is scattered, disconnected blocks: one script tag for Organization, another for FAQPage, neither referencing the other. Each block gets parsed as an island. The fix is a single graph per page where every node links to every other node through @id.

Before you start

  • Access to edit the head or body of your page templates
  • A stable canonical URL structure (schema keyed to URLs that change breaks silently)
  • Real business details: legal name, address, phone, service list, and FAQ content that already exists on the page

Steps

Work through it in order

  1. Step 1

    Understand why one @graph beats scattered blocks

    JSON-LD supports a top-level @graph array holding multiple typed nodes in one script tag. Each node gets an explicit @id, usually the page URL plus a fragment like #organization or #localbusiness. Other nodes reference that @id instead of repeating the data.

    This matters because search engines resolve entities by @id. If your Organization node on the homepage and your LocalBusiness node on the contact page both claim to be 'the publisher' without a shared @id, you have described two different things instead of one business viewed two ways.

  2. Step 2

    Build the Organization node

    This is the anchor node most other types will reference through 'publisher' or 'provider'. Keep sameAs limited to profiles you actually control and update, not every directory listing that ever existed.

    Organization node
    {
      "@context": "https://schema.org",
      "@id": "https://www.example.com/#organization",
      "@type": "Organization",
      "name": "Lingows",
      "url": "https://www.example.com/",
      "logo": "https://www.example.com/logo.png",
      "telephone": "720-378-8970",
      "email": "help@lingows.com",
      "sameAs": [
        "https://www.linkedin.com/company/lingows",
        "https://www.google.com/maps/place/?q=place_id:REPLACE_WITH_REAL_PLACE_ID"
      ]
    }
  3. Step 3

    Add LocalBusiness with a real address block

    LocalBusiness should reference the Organization node with @id rather than duplicating name and logo. PostalAddress is its own typed object, not a flat string. Errors here usually come from putting the full address in a single 'address' string field instead of the structured object.

    LocalBusiness node
    {
      "@id": "https://www.example.com/#localbusiness",
      "@type": "LocalBusiness",
      "name": "Lingows",
      "parentOrganization": { "@id": "https://www.example.com/#organization" },
      "address": {
        "@type": "PostalAddress",
        "streetAddress": "400 E Simpson St",
        "addressLocality": "Lafayette",
        "addressRegion": "CO",
        "postalCode": "80026",
        "addressCountry": "US"
      },
      "telephone": "720-378-8970",
      "priceRange": "$$"
    }
  4. Step 4

    Describe a Service and link it to the provider

    Service nodes should point 'provider' at the Organization @id, not restate the whole business object. If you serve a metro area rather than a single storefront, use areaServed with a City or AdministrativeArea type instead of a plain string.

    Service node
    {
      "@id": "https://www.example.com/seo/technical-seo#service",
      "@type": "Service",
      "serviceType": "Technical SEO Audit",
      "provider": { "@id": "https://www.example.com/#organization" },
      "areaServed": {
        "@type": "AdministrativeArea",
        "name": "Denver Metro Area"
      },
      "url": "https://www.example.com/seo/technical-seo"
    }
  5. Step 5

    Write FAQPage from real on-page questions only

    Google has restricted FAQPage rich results mostly to government and health sites, but the markup still helps answer engines extract question and answer pairs cleanly. Every Question and acceptedAnswer must match visible text on the page. Do not invent questions for markup that do not appear as content.

    FAQPage node
    {
      "@id": "https://www.example.com/seo/technical-seo#faq",
      "@type": "FAQPage",
      "mainEntity": [
        {
          "@type": "Question",
          "name": "How long does a technical SEO audit take?",
          "acceptedAnswer": {
            "@type": "Answer",
            "text": "A standard audit for a site under 500 pages takes one to two weeks, covering crawlability, indexation, site speed, and structured data."
          }
        }
      ]
    }
  6. Step 6

    Add BreadcrumbList that matches the visible path

    BreadcrumbList position values must be sequential integers starting at 1, and each item's name should match the visible breadcrumb trail. A frequent error is listing the homepage twice or skipping a position number, which fails validation silently in some parsers and loudly in others.

    BreadcrumbList node
    {
      "@id": "https://www.example.com/seo/technical-seo#breadcrumb",
      "@type": "BreadcrumbList",
      "itemListElement": [
        { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://www.example.com/" },
        { "@type": "ListItem", "position": 2, "name": "SEO", "item": "https://www.example.com/seo" },
        { "@type": "ListItem", "position": 3, "name": "Technical SEO", "item": "https://www.example.com/seo/technical-seo" }
      ]
    }
  7. Step 7

    Assemble the full graph and test it

    Combine all nodes under one @graph array in a single script tag. Run it through Google's Rich Results Test and Schema.org's validator, not just one of them. They catch different classes of errors: Google flags eligibility for rich results, Schema.org's validator flags actual spec violations.

    Common failures at this stage: missing required properties for the type you declared, a @type value that does not exist (typing 'Buisness' instead of 'Business' is more common than it should be), and dates that are not in ISO 8601 format.

    Combined graph
    <script type="application/ld+json">
    {
      "@context": "https://schema.org",
      "@graph": [
        { "@id": "https://www.example.com/#organization", "@type": "Organization", "name": "Lingows" },
        { "@id": "https://www.example.com/#localbusiness", "@type": "LocalBusiness", "parentOrganization": { "@id": "https://www.example.com/#organization" } },
        { "@id": "https://www.example.com/seo/technical-seo#service", "@type": "Service", "provider": { "@id": "https://www.example.com/#organization" } },
        { "@id": "https://www.example.com/seo/technical-seo#faq", "@type": "FAQPage", "mainEntity": [] },
        { "@id": "https://www.example.com/seo/technical-seo#breadcrumb", "@type": "BreadcrumbList", "itemListElement": [] }
      ]
    }
    </script>

Pitfalls

What goes wrong in practice

  • Copying a competitor's schema template and forgetting to change the @id URLs, which silently attaches your content to their entity graph in some parsers.
  • Marking up FAQ content that is not visible on the page. This is treated as spam markup, not just a warning.
  • Using the same @id across multiple pages for a Service node that actually differs per page, which merges distinct services into one entity.
  • Skipping re-validation after a CMS migration. Template changes routinely strip or duplicate JSON-LD blocks without anyone noticing until a client asks why rich results disappeared.

Do I need FAQPage, LocalBusiness, and Service schema on every page?

No. Put Organization and LocalBusiness on the homepage and contact page, Service schema on each service page it actually describes, FAQPage only where there is real visible FAQ content, and BreadcrumbList wherever a breadcrumb trail is rendered. Markup should describe what already exists on the page, not pad it.

  • One @graph per page, not one script tag per type
  • Every node needs an explicit, stable @id
  • Validate with both Google's Rich Results Test and Schema.org's validator

Rather have this done for you

We run these steps as part of a program, with the checks automated where they can be.