---
title: "Meet Schema Markup, the Module That Won’t Let You Ship Broken JSON-LD"
description: "SanityPress adds a Schema markup module for JSON-LD with built-in validation, plus a faster, server-rendered Custom HTML module that points you to it."
---

![Man in leather jacket getting stopped at TSA check point for missing "@type" property.](https://cdn.sanity.io/images/cyu7k2r0/production/3d3a4f92166986599587af559e44679b290f3ce4-2944x1648.png)

Structured data is one of those things everyone agrees matters and nobody enjoys maintaining. You paste a JSON-LD snippet into a page, it works, and six months later someone edits a comma and Google quietly stops showing your [rich results](https://sanitypress.dev/features/seo).

SanityPress now has a dedicated **[Schema markup](/modules/schema-markup)** module for exactly this. It’s small on purpose: one JSON field, nothing visible on the page, and validation that catches mistakes before they reach production. Along the way, the [Custom HTML module](https://sanitypress.dev/modules/custom-html) got a round of performance upgrades too.

**Main Points**

- The new Schema markup module renders JSON-LD only, with no wrapper markup and nothing visible on the page.

- It validates as you type: invalid JSON, missing `@context`, missing `@type`, and empty entries are flagged right in the Studio.

- Custom HTML now warns you when it spots JSON-LD and suggests the new module instead.

- Custom HTML is now fully server-rendered, with scripts that run exactly once after load.

## Why a separate module?

Until now, the way to add JSON-LD in SanityPress was the [Custom HTML module](https://sanitypress.dev/blog/dont-sleep-on-the-custom-html-module): drop in a `<script type="application/ld+json">` tag and hope for the best. That worked, but it had problems:

- **No guardrails.** A typo in your JSON was invisible until a search console report told you about it weeks later.

- **Wrong tool for the job.** Custom HTML is built for visible content, so it wraps everything in a `<div>` and carries styling options that structured data doesn’t need.

- **Hard to scan.** In the Studio, a JSON-LD block looked like any other chunk of HTML.

The Schema markup module fixes all three.

## What it does

Add the module to any page (or any global module, or directly inside a [blog post](https://sanitypress.dev/blog)’s content), paste your JSON-LD, and that’s it. No `<script>` tag needed; the module writes it for you.

```json
{
  "@context": "https://schema.org",
  "@type": "SoftwareApplication",
  "name": "SanityPress",
  "applicationCategory": "DeveloperApplication",
  "offers": { "@type": "Offer", "price": "0" }
}
```

On the frontend, it renders a single `<script type="application/ld+json">` and nothing else. No wrapper element, no layout impact. Hide the module from its attributes and the markup is left out of the page entirely.

You won’t need it for everything, either. Several [modules](https://sanitypress.dev/modules) already generate structured data from their content: the [Accordion List](https://sanitypress.dev/modules/accordion-list) outputs `FAQPage`, the [Step List](https://sanitypress.dev/modules/step-list) outputs `HowTo`, the [Person List](https://sanitypress.dev/modules/person-list) outputs `Person`, the [Breadcrumbs](https://sanitypress.dev/modules/breadcrumbs) module marks up your trail, and every blog post gets `BlogPosting` automatically. Schema markup covers everything else: `Organization`, `LocalBusiness`, `Product`, `Event`, and whatever else your site needs.

## Validation that actually helps

This is my favorite part. The JSON field is checked on every edit, and the Studio tells you exactly what’s wrong:

| Problem | What you’ll see |
| --- | --- |
| Field left empty | Add JSON-LD markup |
| A stray comma or missing quote | Invalid JSON: (the parser’s exact message) |
| An empty array `[]` | JSON-LD array is empty |
| An entry that isn’t an object | Each JSON-LD entry must be an object |
| Forgot the context | Missing "@context" (e.g. "https://schema.org") |
| Forgot the type | Missing "@type" (or "@graph") |

It handles the shapes you’ll actually use: a single object, an array of objects, or an `@graph` that bundles several entities together. Every entry gets checked, not just the first one.

Validation also guards the frontend. If invalid JSON somehow gets published, the module renders nothing instead of shipping a broken tag to search engines.

## Previews you can read at a glance

In the Studio, each Schema markup block shows the types it contains, pulled from the top level and from any `@graph`. It builds on the [richer module previews](https://sanitypress.dev/blog/definemodule-helper-richer-module-previews) from the `defineModule` helper. So instead of a wall of identical “Custom HTML” rows, you see things like:

- `@type: Organization, WebSite`

- `@type: FAQPage`

- `@type: LocalBusiness`

When a page has three or four of these, that alone saves a lot of clicking.

## Safe by default

A couple of details happen behind the scenes so you don’t have to think about them:

- **No script breakouts.** If a string value ever contains `</script>` or `<!--`, the output escapes it so it can’t break out of the tag.

- **Visual Editing friendly.** Sanity’s [Visual Editing](https://sanitypress.dev/features/previews) adds invisible characters to text for click-to-edit overlays. The module strips those before parsing, so your JSON stays valid in preview and draft mode too.

- **Minified output.** Whatever formatting you use in the editor, the page gets compact JSON.

## Custom HTML now points you the right way

Existing sites probably have JSON-LD tucked inside Custom HTML blocks already. Rather than leave that for you to hunt down, Custom HTML now shows a warning when it finds `application/ld+json` in its code:

> Use the "Schema markup" module for JSON-LD instead

It’s a warning, not an error, so nothing breaks and you can migrate at your own pace. The Custom HTML preview also got smarter: its subtitle now labels what’s inside each block (HTML, JS, JSON-LD, CSS), so you can spot candidates for migration from the module list.

## Custom HTML got faster, too

While I was in there, I reworked how Custom HTML renders:

- **Server-rendered HTML.** Blocks with scripts used to be injected on the client after load, which meant an empty box first and content later. Now the HTML is in the initial page, which is better for search engines and avoids layout shift.

- **Scripts run exactly once.** Scripts are sent in an inactive state and switched on after the page loads. No skipped scripts, no double runs. Module scripts keep their type, and external scripts still run in the order you wrote them.

- **Data blocks stay put.** Non-executable scripts like JSON-LD are left exactly as written, so they’re visible to crawlers in the server HTML.

- **Simpler CSS.** Custom CSS is now a plain server-rendered `<style>` tag instead of a client component, so styles apply on first paint. (For styling regular modules, [scoped CSS per module](https://sanitypress.dev/blog/scoped-css-per-module) is still the way to go.)

## Try it

The Schema markup module is available now. See it in action on the [Schema markup module page](https://sanitypress.dev/modules/schema-markup), and check the [Custom HTML module page](https://sanitypress.dev/modules/custom-html) for the updated behavior.

If you’ve been putting off structured data because it felt fragile, this is a good time to give it another look. And if you’re thinking about how AI agents read your site, pair it with [.md routes and /llms.txt](https://sanitypress.dev/blog/aeo-markdown-routes-ai-agents).

![Man in black leather jacket intrigued by a suspicous lady in red.](https://cdn.sanity.io/images/cyu7k2r0/production/34299c06286509dcab8dd0df56646a1ebeee3a30-2960x1648.png)
