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
applicationCategorymust use Schema.org-recognized values. "SaaS" isn't one. UseBusinessApplication,FinanceApplication,DesignApplication,EducationalApplication, etc.operatingSystemmatters for app store-style results. "Web" alone is fine; "Web, iOS 16+, Android 10+" is better when you have native apps (see our RN vs Flutter article).aggregateRatingmust reference real reviews on the same page. If you put a 4.7 rating with 342 reviews and the page shows zero reviews, Google will flag it as deceptive. Either render the reviews or skip the rating.
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
- Currency must be ISO 4217. Use
THB, not฿. UseUSD, not$. priceCurrencyon the Offer must matchpriceCurrencyon the priceSpecification. Mismatches silently break parsing.- Don't omit
availability. Default behavior varies by parser; explicit is safer. - Annual + monthly should be two separate Offers, not one Offer with a date range. Google treats them differently.
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
- Self-serving reviews are policy-violating. Google's structured data guidelines explicitly prohibit reviews authored by your own organization. Use third-party reviews (G2, Capterra, Trustpilot) or genuine customer testimonials only.
- Reviews must reference an item. A standalone Review with no
itemReviewedis rejected. - Don't mix Reviews and aggregateRating without alignment. If you have 5 displayed Reviews averaging 4.6 and you claim aggregateRating 4.9 across 200 reviews, Google may penalize the inconsistency.
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
- FAQ content must be visible on the page. Hidden in a tab or accordion is fine; not in the DOM at all is invalid.
- One FAQPage per URL. If you split the FAQ into separate URLs, each gets its own FAQPage.
- Don't use FAQ schema for Q&A content where users post answers. That's
QAPage, a different type.
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.
- Schema.org Validator — strict spec validation. Catches typos and invalid type combinations.
- Google Rich Results Test — what Google actually parses. The only one whose results matter for ranking.
- 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
- Organization + WebSite on every page (one graph, in the root layout)
- SoftwareApplication on the homepage and the main product page
- Offer on the pricing page, one per tier, in THB or your primary currency
- FAQPage on the FAQ page (and on landing pages with embedded FAQ sections)
- BreadcrumbList on every page deeper than home
- 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.
schema saas json-ld structured-data softwareapplication