Bluewich Get Quote
// SCHEMA · 2025-12-22 · 17 min read

Schema.org for SaaS Products: Beyond the Basics

Most SaaS sites ship Organization, WebSite, and call it done. Then they wonder why competitors get rich results and they don't. This is the schema graph we ship for SaaS clients — across 14 production deploys — and the silent-failure pitfalls that cause Google to ignore your markup entirely.

By Yunmin Shin · Published 2025-12-22 · Updated 2026-02-14

Why basic schema isn't enough for SaaS

SaaS products live in a competitive SERP. "Best [category] software" queries are dominated by review aggregators (G2, Capterra, TrustRadius). To compete, your own site needs to emit the same structured signals those aggregators do — pricing, ratings, supported platforms, features. Without it, Google can't tell whether your page describes a product or just talks about one.

Across 14 SaaS deploys we've measured, sites with the full graph outperform sites with basic schema by 2.3x in rich-result impressions over 90 days. Same content, same authority, different markup. That's the difference between rendering as a blue link and rendering with stars + price + platform badges.

For the underlying SEO surface principles, see our Next.js 15 RSC patterns. For AEO/LLM citation signals, our SitPlay AEO research covers the parallel disciplines.

The graph we ship

Every SaaS landing page emits a connected graph, not isolated objects. Use @id references so Google understands the relationships:

{
  "@context": "https://schema.org",
  "@graph": [
    { "@type": "Organization", "@id": "https://example.com/#org", ... },
    { "@type": "WebSite",     "@id": "https://example.com/#website",
      "publisher": { "@id": "https://example.com/#org" } },
    { "@type": "SoftwareApplication", "@id": "https://example.com/#app", ... },
    { "@type": "Offer", "@id": "https://example.com/#offer-pro", ... },
    { "@type": "FAQPage", "@id": "https://example.com/#faq", ... }
  ]
}

This single graph block goes inside one <script type="application/ld+json"> tag in the document head, server-rendered (per Pattern 5 of our Next.js article). Don't split into multiple script tags — Google merges them, but it's noisier to debug.

SoftwareApplication: the centerpiece

This is where most SaaS sites either skip the type or fill it incorrectly. The minimum we ship:

{
  "@type": "SoftwareApplication",
  "@id": "https://example.com/#app",
  "name": "Acme Project Manager",
  "url": "https://example.com",
  "applicationCategory": "BusinessApplication",
  "operatingSystem": "Web, iOS 16+, Android 10+",
  "description": "Project management for distributed teams. Real-time sync, ฿0/user starter.",
  "screenshot": [
    "https://example.com/screenshot-dashboard.png",
    "https://example.com/screenshot-mobile.png"
  ],
  "softwareVersion": "4.2.1",
  "datePublished": "2024-03-15",
  "publisher": { "@id": "https://example.com/#org" },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.7",
    "ratingCount": "342",
    "bestRating": "5"
  },
  "offers": [
    { "@id": "https://example.com/#offer-free" },
    { "@id": "https://example.com/#offer-pro" }
  ]
}

The pitfalls

Offer: pricing surface

Pricing is a high-value rich result trigger. Ship one Offer per pricing tier. We've seen pricing show directly in the SERP for "[product] price" queries when this is set up correctly.

{
  "@type": "Offer",
  "@id": "https://example.com/#offer-pro",
  "name": "Pro",
  "url": "https://example.com/pricing#pro",
  "price": "490",
  "priceCurrency": "THB",
  "priceSpecification": {
    "@type": "UnitPriceSpecification",
    "price": "490",
    "priceCurrency": "THB",
    "unitText": "MONTH",
    "billingDuration": "P1M"
  },
  "availability": "https://schema.org/InStock",
  "validFrom": "2025-01-01",
  "category": "subscription",
  "seller": { "@id": "https://example.com/#org" },
  "eligibleRegion": { "@type": "Country", "name": "TH" }
}

The pitfalls

Review: real, not faked

Individual reviews can be marked up, but only if they appear on the page. The itemReviewed must point back to your SoftwareApplication via @id:

{
  "@type": "Review",
  "itemReviewed": { "@id": "https://example.com/#app" },
  "author": { "@type": "Person", "name": "Somchai T." },
  "datePublished": "2026-01-12",
  "reviewBody": "Cut our PM time by 40% in the first month. Thai support is responsive.",
  "reviewRating": {
    "@type": "Rating",
    "ratingValue": "5",
    "bestRating": "5"
  }
}

The pitfalls

FAQPage: still works in 2026 (with caveats)

FAQPage rich results are no longer shown for most general queries (Google reduced their visibility in 2023), but they're still indexed and they help with AEO/LLM citations significantly — see our SitPlay AEO research. Ship them anyway.

{
  "@type": "FAQPage",
  "@id": "https://example.com/#faq",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Does Acme work with PromptPay?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. PromptPay QR is the default payment method for Thai customers. We also accept credit card and bank transfer (KBank, SCB, BBL)."
      }
    },
    {
      "@type": "Question",
      "name": "Is there a free tier?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. The Free tier supports up to 3 users and unlimited projects. No credit card required. Upgrade to Pro at ฿490/user/month when your team grows."
      }
    }
  ]
}

The pitfalls

BreadcrumbList: cheap and high-leverage

Breadcrumbs are easy to ship and replace your URL in the SERP with a more readable hierarchy. Every SaaS page deeper than the homepage should have one:

{
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Home",     "item": "https://example.com/" },
    { "@type": "ListItem", "position": 2, "name": "Pricing",  "item": "https://example.com/pricing/" },
    { "@type": "ListItem", "position": 3, "name": "Pro plan", "item": "https://example.com/pricing/pro/" }
  ]
}

Putting it together: a real SaaS landing page

Here's the full graph from a recent Bluewich client — anonymized but structurally identical:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://example.com/#org",
      "name": "Acme Co",
      "url": "https://example.com",
      "logo": "https://example.com/logo.png",
      "address": {
        "@type": "PostalAddress",
        "streetAddress": "Sukhumvit 21",
        "addressLocality": "Bangkok",
        "postalCode": "10110",
        "addressCountry": "TH"
      },
      "sameAs": [
        "https://www.linkedin.com/company/acme",
        "https://twitter.com/acme"
      ]
    },
    {
      "@type": "WebSite",
      "@id": "https://example.com/#website",
      "url": "https://example.com",
      "name": "Acme",
      "publisher": { "@id": "https://example.com/#org" },
      "inLanguage": ["en", "th"]
    },
    {
      "@type": "SoftwareApplication",
      "@id": "https://example.com/#app",
      "name": "Acme Project Manager",
      "applicationCategory": "BusinessApplication",
      "operatingSystem": "Web, iOS, Android",
      "aggregateRating": {
        "@type": "AggregateRating",
        "ratingValue": "4.7", "ratingCount": "342"
      },
      "offers": [
        { "@id": "https://example.com/#offer-free" },
        { "@id": "https://example.com/#offer-pro" }
      ]
    },
    {
      "@type": "Offer",
      "@id": "https://example.com/#offer-free",
      "name": "Free",
      "price": "0", "priceCurrency": "THB",
      "availability": "https://schema.org/InStock"
    },
    {
      "@type": "Offer",
      "@id": "https://example.com/#offer-pro",
      "name": "Pro",
      "price": "490", "priceCurrency": "THB",
      "availability": "https://schema.org/InStock"
    },
    {
      "@type": "FAQPage",
      "@id": "https://example.com/#faq",
      "mainEntity": [ /* ...questions */ ]
    },
    {
      "@type": "BreadcrumbList",
      "itemListElement": [ /* ...crumbs */ ]
    }
  ]
}
</script>

Validation we run before deploy

Three tools, in order. We run all three on every commit that touches schema.

  1. Schema.org Validator — strict spec validation. Catches typos and invalid type combinations.
  2. Google Rich Results Test — what Google actually parses. The only one whose results matter for ranking.
  3. Custom diff vs production — same script as the WordPress migration article. Catches regressions.
// Local validator (CI-friendly)
import { validate } from 'schema-dts/dist/validate.js'  // or schemarama

const html = readFileSync('./out/index.html', 'utf8')
const matches = [...html.matchAll(/<script[^>]*ld\+json[^>]*>([\s\S]*?)<\/script>/g)]
for (const m of matches) {
  const parsed = JSON.parse(m[1])
  const errors = validate(parsed)
  if (errors.length) { console.error(errors); process.exit(1) }
}

The most common silent failure

You ship perfect schema. Google ignores it. Why? Cloaking detection. Many SaaS sites render schema server-side but hide the corresponding content behind JavaScript-loaded modals, tabs, or "Show more" interactions. Google sees schema claiming things its bot can't verify in the visible DOM, and quietly drops your rich results.

The fix: every claim in your schema must correspond to text that's visible in the rendered HTML when JS is disabled. Test by viewing source. If the schema says you have 342 reviews and the source has zero <article> tags with reviews, you're cloaking by accident.

What to ship for your SaaS, today

  1. Organization + WebSite on every page (one graph, in the root layout)
  2. SoftwareApplication on the homepage and the main product page
  3. Offer on the pricing page, one per tier, in THB or your primary currency
  4. FAQPage on the FAQ page (and on landing pages with embedded FAQ sections)
  5. BreadcrumbList on every page deeper than home
  6. Review + AggregateRating only when you have real, displayed reviews

Skip Article, Person, HowTo for SaaS landing pages — they don't move the needle and dilute the graph. Save those for blog posts.

If you want a free schema audit on your current SaaS site, email us the URL. We'll return the validation diff + fix list within 48 hours. Or browse the case studies for how this layered into a full 15-day SaaS launch. CRO and conversion-side help comes from Bangkok Digital.

Tags: schema saas json-ld structured-data softwareapplication
// RELATED INSIGHTS
// NEXT.JS · 2026-04-15

Next.js 15 Server Components for SEO: A Pattern Library

14 patterns from 30+ deploys including JSON-LD rendering.

// MOBILE · 2026-03-12

React Native vs Flutter for Thai Market in 2026

Hiring pool, libraries, ASO, Thai tooling.

// HOSTING · 2026-02-08

Hosting on Hostinger LiteSpeed: Performance Notes

Real p75 CWV from 17 sites. When to upgrade VPS.

// MIGRATION · 2026-01-18

WordPress to Next.js Migration Without Ranking Loss

7-step playbook with cutover scripting.

Free schema audit for your SaaS.

Send the URL. We'll return a validation diff and fix list within 48 hours. No pitch.

Get Quote · umma@xx.gg +66 61 093 4014
💬 LINE

Yunmin Agency Network

Bluewich · SitPlay Media · SEO Agency Bangkok · Bangkok Digital

// WEEKLY THAI MARKET INSIGHTS

Get the data we scraped this week.

Rising keywords. SERP shifts. AI citation changes. Bangkok-market specific. No fluff, no sales — one email Tuesday morning.

No spam · Unsubscribe in one click

📱 WhatsApp · 💬 LINE · 📞 +66 61 093 4014

© 2026 · Operated by Yunmin Co., Ltd. · Thai Co. Reg. (pending) · 3rd Floor, 272 Than Thip 3 Alley, Phlabphla, Wang Thonglang, Bangkok 10310

Privacy · Terms · Atelier · umma@xx.gg