Made by VeraStack Labs
Contents
Think of a visiting card. You can read it: a name, a phone number, a shop, an area. A person finds it easy. But a machine looking at a photo of the card sees a pile of marks.
Now think of the form a bank gives you, with boxes marked Name, Phone, Address. Nothing is guessed. Each fact sits in a labelled box.
A web page is the visiting card. A search engine, or any other program that reads pages, has to guess which words are the title, which is the date and who wrote it. JSON-LD (opens in a new tab) is the labelled form hidden behind the page, so the guessing is not needed.
The smallest useful example
Here is a complete block. It says that this page is a technical article, and gives its headline, its dates and its author.
<script type="application/ld+json">{ "@context": "https://schema.org", "@type": "TechArticle", "headline": "What happens when you type a URL", "datePublished": "2026-10-18", "dateModified": "2026-10-18", "author": { "@type": "Organization", "name": "VeraStack Labs" }}</script>Let's read it from the outside in.
The <script> tag is how a page carries code. This one is special. Its type is application/ld+json, which is not JavaScript. MDN (opens in a new tab) says that when a script's type is not JavaScript, its content is treated as a data block and the browser does not process it. So the text sits there, quietly, for anything that wants to read it. Visitors never see it.
The part inside is JSON, which stands for JavaScript Object Notation, a plain-text way to write data as names and values. JSON is stricter than JavaScript. Names must be in double quotes, and there are no comments and no trailing commas.
The LD stands for Linked Data. The W3C, the body that writes web standards, defines JSON-LD 1.1 as a JSON-based format to serialise (write out) Linked Data. In plain words: JSON where the names are linked to agreed meanings.
Remember: JSON-LD is ordinary JSON in a script tag whose type tells the browser to leave it alone.
Three keywords worth knowing
Three names in that block start with an @. They are the grammar.
@contextsays which dictionary the words come from. Here it ishttps://schema.org. A context maps short names such asheadlineto full identifiers, so everyone means the same thing by them.@typesays what kind of thing the block describes. Here it isTechArticle(opens in a new tab).@idgives a thing a name that other parts can point to. The block above does not need one. You will see why in a moment.
The dictionary is schema.org (opens in a new tab), a collection of shared vocabularies that webmasters can use to mark up their pages, so that search engines and other applications understand them better. It lists the types and the names that go with each type.
TechArticle is one of those types. schema.org describes it as a technical article, such as a how-to guide or step-by-step instructions, and it is a more specific kind of Article.
Real pages describe more than one thing. When they do, a block can hold a list called @graph, and the things in it can point at each other with @id:
<script type="application/ld+json">{ "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "https://example.com/#studio", "name": "Example Studio" }, { "@type": "WebSite", "@id": "https://example.com/#website", "name": "Example", "publisher": { "@id": "https://example.com/#studio" } } ]}</script>The website says who published it by pointing at the studio's @id. It does not copy the studio's details again. That is the whole trick of @id: say it once, point to it everywhere.
Remember: @context is the dictionary, @type is the kind of thing, and @id is a name tag other things can point to.
Origan's own site, piece by piece
Now the real thing. Every block below was printed by the code that Origan uses to build its own pages. None of it was typed into this article. If the code changes, so does the page.
Every page on this site carries a graph with three things. The first is the studio behind it. Each one is shown here on its own, with an @context added, so that it can be read by itself.
{ "@context": "https://schema.org", "@type": "Organization", "@id": "https://origan.verastack.dev/#studio", "name": "VeraStack Labs", "url": "https://verastack.dev", "logo": "https://origan.verastack.dev/logo.png", "sameAs": [ "https://github.com/verastack-labs", "https://verastack.dev" ], "founder": { "@type": "Person", "name": "Rigan Burnwal" }, "email": "rigan@verastack.dev", "telephone": "+91 93806 54730", "address": { "@type": "PostalAddress", "streetAddress": "Indiranagar", "addressLocality": "Bangalore", "addressCountry": "IN" }}A few things to notice.
@idis an address with a#studioat the end. It does not have to open a page. It is only a stable name for the studio, so other blocks can point at it.sameAslists other places that identify the same studio, such as its GitHub page.- The
addressgives the area and city, not a street. The site's code says why: the area is enough to place the studio. - schema.org's
Organization(opens in a new tab) type has all of these properties:sameAs,founder,logo,address,emailandtelephone.
The second is the website, which points back at the studio.
{ "@context": "https://schema.org", "@type": "WebSite", "@id": "https://origan.verastack.dev/#website", "url": "https://origan.verastack.dev/", "name": "Origan", "description": "Origan keeps a record of what each engineering student has covered, from first semester to the final drive, with a Campus Coach on campus.", "publisher": { "@id": "https://origan.verastack.dev/#studio" }, "inLanguage": "en-IN"}publisher holds nothing but an @id. That one line is the link from the website to the studio.
The code that makes this block also says what it leaves out. It has no price, because there is no public price. It has no star rating and no reviews, because there are no customers to rate it. That is a rule worth copying: say only what is true and visible.
Next, a lesson page. Each lesson of The Switch, the TypeScript course on this site, describes itself like this.
{ "@context": "https://schema.org", "@type": "LearningResource", "name": "Why switch? The honest list", "description": "Should you move from JavaScript to TypeScript? The pros and the costs side by side, three real bugs it catches, what it cannot catch, and a short checklist to decide.", "url": "https://origan.verastack.dev/learn/js-to-ts/why-switch/", "inLanguage": "en", "learningResourceType": "lesson", "teaches": "Why switch? The honest list", "position": 0, "dateModified": "2026-10-17", "isAccessibleForFree": true, "isPartOf": { "@type": "Course", "name": "The Switch: JavaScript to TypeScript", "url": "https://origan.verastack.dev/learn/js-to-ts/" }, "provider": { "@type": "Organization", "name": "VeraStack Labs", "url": "https://verastack.dev" }, "author": { "@type": "Organization", "name": "VeraStack Labs", "url": "https://verastack.dev" }}Look at isPartOf. A lesson says which course it belongs to, and position says where it sits in the order. Look also at dateModified: it is the date the lesson was last updated.
Every page also carries a trail of breadcrumbs, the little "Origan, then Resources, then The Switch, then this lesson" path.
{ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Origan", "item": "https://origan.verastack.dev/" }, { "@type": "ListItem", "position": 2, "name": "Resources", "item": "https://origan.verastack.dev/resources/" }, { "@type": "ListItem", "position": 3, "name": "The Switch", "item": "https://origan.verastack.dev/learn/js-to-ts/" }, { "@type": "ListItem", "position": 4, "name": "Why switch? The honest list", "item": "https://origan.verastack.dev/learn/js-to-ts/why-switch/" } ]}Google's breadcrumb guide (opens in a new tab) says the list must hold the trail in order, that position 1 is the start, and that the last item does not need an item address, because it is the page itself. Origan gives it one anyway.
Last, the block for the page you are reading. It is built from this article's own front matter: the title, the one-line summary, the dates, and the topic pills.
{ "@context": "https://schema.org", "@type": "TechArticle", "headline": "JSON-LD: the hidden data that describes your page", "description": "What JSON-LD is, what @context, @type and @id mean, and how to write it. Every example is the structured data this site really sends, printed from the code that makes it.", "url": "https://origan.verastack.dev/learn/off-the-syllabus/json-ld/", "mainEntityOfPage": "https://origan.verastack.dev/learn/off-the-syllabus/json-ld/", "image": "https://origan.verastack.dev/learn/off-the-syllabus/json-ld/og.png", "inLanguage": "en", "isAccessibleForFree": true, "datePublished": "2026-10-18", "dateModified": "2026-10-18", "articleSection": "Being found", "author": { "@type": "Organization", "name": "VeraStack Labs", "url": "https://verastack.dev" }, "publisher": { "@type": "Organization", "name": "VeraStack Labs", "url": "https://verastack.dev" }, "isPartOf": { "@type": "CollectionPage", "name": "Off the Syllabus: Web and tools", "url": "https://origan.verastack.dev/learn/off-the-syllabus/" }, "about": [ { "@type": "Thing", "name": "JSON-LD" }, { "@type": "Thing", "name": "Structured data" }, { "@type": "Thing", "name": "schema.org" } ]}Two choices are worth explaining. First, dateModified is the "Last checked" date, shown at the top of this page: the day each fact was last compared with its source. Second, the type is TechArticle, which is a kind of Article. Google's article guide (opens in a new tab) lists Article, NewsArticle and BlogPosting as the types it documents, so it never names TechArticle; schema.org does, and defines it as a more specific Article. This site chose the more specific type.
Remember: a good block says only what the visible page already says. Print it from the same data as the page, so the two cannot drift apart.
How the page gets it
The block has to end up inside a script tag, written into the page when it is built. Here is the idea as a small function, with a test that holds it to its promises.
// Turn some data into the script tag a page carries. JSON.stringify writes the// JSON. Every "<" is written as <, which is the same character to JSON// and cannot end the script block early by spelling out a closing script tag.export function jsonLdTag(data: object): string { const json = JSON.stringify(data).replace(/</g, "\\u003c"); return `<script type="application/ld+json">${json}</script>`;}One line deserves a pause: the replace that writes every < as <. The HTML standard (opens in a new tab) says that text like a closing script tag inside a script has to be escaped, because the parser looks for it to end the block. In JSON, < is the same character as <, so the data is unchanged but the danger is gone. Without it, a headline that happened to contain </script> would cut the block short.
A crawler reads the data back like this:
// Read the data back out of a script tag, the way a crawler would.export function readJsonLd(tag: string): unknown { const match = /^<script type="application\/ld\+json">([\s\S]*)<\/script>$/.exec(tag); if (!match) throw new Error("Not a JSON-LD script tag"); return JSON.parse(match[1]);}And this is the check that this site's tests apply to every block it prints: it must be valid JSON, and it must have an @context and an @type.
// The two keys every block needs to mean anything: which vocabulary// (@context), and what kind of thing (@type, or a @graph of things that each have one).export function hasContextAndType(value: unknown): boolean { if (typeof value !== "object" || value === null || Array.isArray(value)) return false; const block = value as Record<string, unknown>; if (typeof block["@context"] !== "string") return false; if (typeof block["@type"] === "string") return true; const graph = block["@graph"]; return Array.isArray(graph) && graph.length > 0 && graph.every((node) => typeof node?.["@type"] === "string");}Where does the tag go? Google says it can sit in the <head> or the <body>.
<head> <title>What happens when you type a URL</title> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "TechArticle", "headline": "What happens when you type a URL" } </script></head>Check your work
Google points to two free tools. The Rich Results Test (opens in a new tab) is the official Google tool for testing your structured data, and it shows which rich results (special-looking search listings) your page can produce. The Schema Markup Validator (opens in a new tab) at validator.schema.org tests any schema.org markup, and does not run Google-specific checks.
A valid block is a start, not a finish. Google says that correct markup is still no promise that your structured data will show up in search results.
Try it yourself
You will read the structured data of a real page in five minutes.
- Open any page of this site, such as the home page, in your browser.
- Open DevTools with
F12, and click the Console tab. - Paste this and press Enter.
const blocks = document.querySelectorAll('script[type="application/ld+json"]');console.log(`${blocks.length} JSON-LD block(s) on this page`);for (const block of blocks) { console.log(JSON.parse(block.textContent));}You will see how many blocks the page has, then each one as an object you can open. The home page has more than one. The first is the graph above.
- Now try a page you built, or any site you like. Does it have a block? If it does, does
@typesay what you expect? - Copy a block and paste it into the Schema Markup Validator. Does it say it is fine?
For a challenge, write the block for your own project's home page: a WebSite with a name and a url. Check it with the tests above.
The honest trade-offs
JSON-LD is useful, and it has real costs.
- It is invisible, so it can lie. Nobody sees the block, so nobody notices when it is wrong. Google's guidelines (opens in a new tab) say not to mark up content that is not visible to readers, and not to mark up misleading content. Print the block from the same data as the page, as this site does.
- It promises nothing. Google says correct markup is no promise of an appearance in search results. Treat it as a way to be understood, not a way to be ranked.
- Google documents fewer types than schema.org has. Google's article page lists only
Article,NewsArticleandBlogPosting, and says there are no required properties, only recommended ones such asheadline,datePublished,dateModified,authorandimage. If your goal is a specific search feature, read Google's page for that feature first. - It is one more thing to keep right. Every field you add is a field that can go stale. A small, true block beats a large one that is half wrong.
- Generators are not a substitute for reading. A plugin can write a block for you, but you are still the person who must check that every line of it is true.
Sources
Every fact above was compared with these pages on 18 October 2026.
- W3C: JSON-LD 1.1 (opens in a new tab)www.w3.org
- schema.org: Getting started (opens in a new tab)schema.org
- schema.org: TechArticle (opens in a new tab)schema.org
- schema.org: Organization (opens in a new tab)schema.org
- Google Search Central: Introduction to structured data markup (opens in a new tab)developers.google.com
- Google Search Central: General structured data guidelines (opens in a new tab)developers.google.com
- Google Search Central: Article structured data (opens in a new tab)developers.google.com
- Google Search Central: Breadcrumb structured data (opens in a new tab)developers.google.com
- Google Search Central: Structured data testing tools (opens in a new tab)developers.google.com
- MDN: the script element (opens in a new tab)developer.mozilla.org
- MDN: JSON, and how it differs from JavaScript (opens in a new tab)developer.mozilla.org
- WHATWG HTML Standard: restrictions for contents of script elements (opens in a new tab)html.spec.whatwg.org
Made by VeraStack Labs