Back to Hub
ENGINEERING •
By Yash Panchal · Last updated:

JSON-LD Recipes: BlogPosting, FAQPage, SoftwareApplication

Two weeks ago I wrote the overview: a static indie site needs three machine-readable files — sitemap, schema, and llms.txt. That post told you what each one is for. This one is the part I left out on purpose: the actual code.

I run NitroIDE, a browser IDE on a static host with no server, no build step, and no plugin system. There is no SEO plugin to install, so every block of structured data on the site is a <script type="application/ld+json"> block I wrote by hand. Three recipes cover almost everything an indie site needs. Copy them, change the values, validate before shipping — and, crucially, keep them honest, because schema that describes content the page does not show is a mismatch waiting to be flagged.

Recipe 1: SoftwareApplication — the product page

This one lives on the homepage and answers a single question: what is this thing? For NitroIDE it names the product, its category, that it runs in a web browser, that it is free, and the features a human would actually recognise from the page. Here is the shape, trimmed to the fields that matter:

{
  "@context": "https://schema.org",
  "@type": "SoftwareApplication",
  "name": "NitroIDE",
  "applicationCategory": "DeveloperApplication",
  "operatingSystem": "Web Browser",
  "url": "https://nitroide.com/",
  "isAccessibleForFree": true,
  "description": "NitroIDE is a zero-latency, client-side browser IDE featuring the Monaco editor, live preview, offline support, and a built-in AI pair-programmer for HTML, CSS, and JavaScript.",
  "featureList": [
    "Client-side HTML, CSS, and JavaScript editing",
    "Instant live preview",
    "ZIP export",
    "Offline-ready browser workspace"
  ],
  "author": { "@type": "Person", "name": "Yash Panchal" }
}

Three rules I follow for this one. First, operatingSystem for a web tool is "Web Browser" — not Windows, not macOS, not a list of platforms the tool never shipped on. Second, every item in featureList must be a feature the homepage actually shows; the moment the list becomes aspirational, the schema is lying in a format machines trust. Third, one block, one product. If you are tempted to add a second SoftwareApplication block for a different page, you probably need a different type for that page, not a duplicate.

The mistake that voids this recipe is version drift. NitroIDE's real block carries a softwareVersion field, and it changes in the same commit as the version it describes. A SoftwareApplication block that still claims last month's version is not a small inaccuracy — it is the product's identity, wrong, in the one place a machine reads it.

Recipe 2: BlogPosting — every article

Every blog article on NitroIDE carries a compact BlogPosting block in its <head>. It is deliberately small: headline, description, author, publisher, the page URL, and two dates. Small is a feature. A BlogPosting block with twenty fields, half of them guessed, is worse than this one, because every guessed field is a future mismatch.

{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "How NitroIDE Renders Code Instantly: srcdoc + postMessage",
  "description": "NitroIDE's preview renders with near-zero latency: one sandboxed iframe, srcdoc rendering, an anti-flicker swap, and postMessage updates. Honest tradeoffs.",
  "author": { "@type": "Person", "name": "Yash Panchal", "url": "https://nitroide.com/about.html#author" },
  "publisher": { "@type": "Organization", "name": "NitroIDE" },
  "mainEntityOfPage": "https://nitroide.com/blog/0ms-preview-srcdoc-postmessage-no-bundler.html",
  "datePublished": "2026-09-26",
  "dateModified": "2026-09-26"
}

Two fields do more work than they look like they should. mainEntityOfPage is the canonical URL of the article itself — it is what ties this block to this page and stops a copied block from describing the wrong URL. And dateModified is the field that makes updates visible: when I correct or extend an article, dateModified moves and datePublished does not. An article that was fixed last week but whose schema still says it is untouched is telling machines it is older than it is.

The mistake that voids this recipe is copy-paste without editing. It is the easiest block to clone across articles, which makes it the easiest to ship with the previous article's headline, description, or URL still in it. Every new article gets a fresh block, and the check is one line: does mainEntityOfPage equal the URL of the page I am looking at?

Recipe 3: FAQPage — only where the page really answers questions

FAQPage is the most abused of the three, so it gets the strictest rule: only use it where the page visibly contains those questions and answers. NitroIDE articles end with a real FAQ section, and the schema mirrors it exactly — same questions, same answers, in the same words. Here is one pair from the preview-architecture article:

{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "How does the preview update so fast?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "One sandboxed srcdoc iframe, incremental postMessage updates with CSS patched in place, and an anti-flicker double-buffer swap — no bundler, no server round-trip."
      }
    }
  ]
}

Note what is not in the answer: no marketing, no link, no call to action. An accepted answer is an answer, and the moment it becomes a pitch, the whole block reads as markup written for the search result rather than for the reader. That is exactly the pattern that gets FAQ rich results withdrawn.

The mistake that voids this recipe is the mirror breaking. The FAQ section on the page changes — a question gets reworded, an answer gets corrected — and the schema does not. Now the page and its schema disagree, in public, in a format designed to be machine-checked. On a static site with no plugin, the fix is procedural, not technical: the visible FAQ and its JSON-LD block change in the same edit, every time, no exceptions. I keep them in the same file, in the same commit, so there is no separate step to forget.

How to validate without a plugin

No plugin means no dashboard warning you. So the validation step is manual and non-negotiable, and it takes about a minute per page:

  1. Paste the page URL into a structured-data testing tool and confirm all three types parse — no syntax errors, no wrong types, no missing required fields.
  2. Read the parsed output against the visible page, field by field. Headline matches the headline. Description matches the page's description. FAQ answers match the visible FAQ word for word.
  3. After any edit to a page, re-check that page's blocks. Schema is not a one-time setup; it is a second copy of facts that change, and a second copy that is not re-checked becomes a stale copy.

One more practical note for static sites: JSON-LD is just JSON inside a script tag, so it breaks exactly like JSON breaks — a trailing comma, an unescaped quote in a description, a smart quote pasted from a word processor. If the testing tool reports nothing at all, the block probably failed to parse as JSON before it failed as schema. Check the JSON first.

The whole recipe card

None of this needs a plugin, a build step, or a server. It needs three small blocks, kept honest, in the same files as the pages they describe. That is the whole trick — and on a static site, it is the only trick that stays true.

Try it

I build NitroIDE — a browser-based IDE where your code never leaves your machine: nitroide.com

⭐ If you enjoyed this, star NitroIDE on GitHub: github.com/nitroideofficial/nitroide

Frequently asked questions

Do I need a plugin to add JSON-LD schema?

No. JSON-LD is just JSON inside a script tag on the page, so on a static site you write the block by hand in the same file as the page it describes — no plugin, build step, or server required.

Which JSON-LD types does a static indie site need?

Three cover almost everything: SoftwareApplication for the product page, BlogPosting for every article, and FAQPage only where the page visibly answers questions.

What voids a BlogPosting block?

Copy-paste without editing. It is the easiest block to clone across articles, which makes it the easiest to ship with the previous article's headline, description, or URL still in it. Check that mainEntityOfPage equals the URL of the page you are looking at.

When should FAQPage schema be used?

Only where the page visibly contains those questions and answers, mirroring them exactly. If the visible FAQ changes and the schema does not, the page and its schema disagree in a format designed to be machine-checked.