@moonsio/next-schema
v0.17.0
Published
A comprehensive Schema.org JSON‑LD solution for React/Next.js apps. Strongly typed, composable schemas, and a single `SchemaJson` component that outputs a valid `<script type="application/ld+json">` tag.
Readme
@moonsio/next-schema
A comprehensive Schema.org JSON‑LD solution for React/Next.js apps. Strongly typed, composable schemas, and a single SchemaJson component that outputs a valid <script type="application/ld+json"> tag.
Features
- TypeScript-first: rich types for all supported entities
- Composable: supply only the entities you need; we build a single
@graph - Stable IDs: IDs are generated consistently using a configurable base URL
- Next.js ready: safe to use in App Router or Pages Router (SSR compatible)
- React 18 and 19: published with peer ranges
^18 || ^19
Installation
pnpm add @moonsio/next-schema
# or
npm i @moonsio/next-schemaPeer dependencies you must have in your app:
{
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
}React 19 and where SchemaJson may run
SchemaJson renders an inline <script type="application/ld+json"> (no src). React 19 is stricter about host context: that pattern is meant for trees that correspond to a normal full document (e.g. Next.js page / layout under <html>, or Storybook only if your story is wrapped in a root/layout that provides document-like structure).
Do not rely on adding async or a fake src to “fix” placement—it does not match typical Google JSON-LD examples and can hit the wrong React code path.
Environments that often break or should skip JSON-LD:
- CMS live preview (e.g. Payload) inside an iframe or partial tree
- Storybook canvas without a decorator that mounts
<html>/ your app root - Any embed where there is no real document subtree for a marketing page
What to do instead:
serializeSchemaJsonLd(props)— canonical escaped string; use with your own<script>only where it is safe (usually the public site build).buildSchemaJsonLdObject(props)— same@graphas typed data if you need objects before stringifying.SchemaJsonwithrenderScript={false}— same props asSchemaJson, renders nothing; pair withserializeSchemaJsonLdon the public route only, or omit JSON-LD in admin/preview entirely.
Patching node_modules (e.g. pnpm patch) is a short-term workaround; these APIs exist so every consumer shares one pattern.
Quick start
- Configure the base URL once at app startup (used to build stable
@ids):
// e.g. app/layout.tsx or pages/_app.tsx
import { setBaseUrl } from '@moonsio/next-schema'
setBaseUrl('https://example.com')- Recommended for dynamic, per-route schema (Next.js 15+ App Router): colocate JSON-LD with the page—usually in the body next to content—using either
SchemaJsonorserializeSchemaJsonLd:
// app/blog/[slug]/page.tsx
import { SchemaJson, setBaseUrl } from '@moonsio/next-schema'
setBaseUrl('https://example.com')
export default function ArticlePage() {
return (
<>
<SchemaJson
logo={{
image: { srcUrl: 'https://example.com/logo.png', width: 512, height: 512 },
url: 'https://example.com',
}}
website={{ baseUrl: 'https://example.com', companyName: 'Example Inc.' }}
webpage={{ name: 'Article', description: '…', slug: 'blog/my-post' }}
article={{ /* … */ }}
/>
<article>{/* … */}</article>
</>
)
}Or the same graph as a string (one import, full control over the tag):
import { serializeSchemaJsonLd, setBaseUrl } from '@moonsio/next-schema'
setBaseUrl('https://example.com')
const jsonLd = serializeSchemaJsonLd({
logo: { /* … */ },
website: { /* … */ },
webpage: { /* … */ },
})
export default function Page() {
return (
<>
<script
type="application/ld+json"
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: jsonLd }}
/>
{/* … */}
</>
)
}- Optional — site-wide JSON-LD only in root
layout<head>: useful when the graph is static and the same on every route. If schema depends on route data, root<head>alone is awkward; prefer per-pageSchemaJson/serializeSchemaJsonLdabove.
// app/layout.tsx — static site-wide schema in <head>
import { SchemaJson, setBaseUrl } from '@moonsio/next-schema'
setBaseUrl('https://example.com')
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<SchemaJson
logo={{
image: { srcUrl: 'https://example.com/logo.png', width: 512, height: 512 },
url: 'https://example.com',
}}
website={{ baseUrl: 'https://example.com', companyName: 'Example Inc.' }}
webpage={{ name: 'Home', description: 'Welcome', slug: '' }}
/>
</head>
<body>{children}</body>
</html>
)
}- Admin / preview: avoid emitting JSON-LD in the preview shell, or use
renderScript={false}so the same page component does not render a<script>there:
<SchemaJson {...props} renderScript={!isPreview} />Supported entities
Pass any of the following props to SchemaJson; if provided, a corresponding node will be added to @graph:
addressarticlebreadcrumbbookdataseteducationalOrganizationeventfaqPagehowTologo(recommended)medicalConditionmedicalOrganizationmovienewsArticleorganizationpersonplaceproductrestaurantsoftwareApplicationvideoObjectwebpage(recommended)website(recommended)
Examples
Article
<SchemaJson
logo={{ image:{ srcUrl:'/logo.png', width:512, height:512 }, url:'https://example.com' }}
website={{ baseUrl:'https://example.com', companyName:'Example Inc.' }}
webpage={{ name:'My Article', description:'...', slug:'blog/my-article' }}
article={{
headline: 'My Article',
articleTitle: 'My Article',
description: 'Short description',
slug: 'blog/my-article',
author: 'Person',
datePublished: new Date(),
}}
/>Product
<SchemaJson
logo={{ image:{ srcUrl:'/logo.png', width:512, height:512 }, url:'https://example.com' }}
website={{ baseUrl:'https://example.com', companyName:'Example Inc.' }}
webpage={{ name:'Product', description:'...', slug:'products/widget' }}
product={{
name: 'Widget',
description: 'A great widget',
sku: 'WID-001',
offers: { price: 19.99, priceCurrency: 'USD' },
url: 'products/widget'
}}
/>Event (virtual or physical)
<SchemaJson
logo={{ image:{ srcUrl:'/logo.png', width:512, height:512 }, url:'https://example.com' }}
website={{ baseUrl:'https://example.com', companyName:'Example Inc.' }}
webpage={{ name:'Event', description:'...', slug:'events/launch' }}
event={{
name: 'Launch Event',
description: 'Product launch',
startDate: '2025-01-01T18:00:00Z',
endDate: '2025-01-01T20:00:00Z',
location: { url: 'https://example.com/live' }, // VirtualLocation
url: 'events/launch'
}}
/>API
Components
SchemaJson(props)- Renders one inline
<script type="application/ld+json">(see React 19 and whereSchemaJsonmay run). - Props: all entity props plus optional
id, optionalrenderScript(defaulttrue).
- Renders one inline
Serialization (canonical graph)
serializeSchemaJsonLd(props)— escaped string fordangerouslySetInnerHTML; same graph asSchemaJson. Preferred when you cannot useSchemaJsonor need per-page control in the body.buildSchemaJsonLdObject(props)— typed{ '@context', '@graph' }before stringifying.
Configuration
setBaseUrl(url: string)– must be called before using generators; used to build stable@ids andurls
Types
SchemaJsonProps exposes all optional entity props listed above. You can rely on editor IntelliSense without importing individual schema types.
Next.js integration (15+ App Router)
- Per-route / dynamic pages: prefer
SchemaJsonorserializeSchemaJsonLdnext to page content (often in<body>). - Static site-wide schema: optional root
layout<head>withSchemaJson(see quick start). - Pages Router: render in pages or in
_document.tsxinside<Head>. - SSR-safe when used in a normal document tree (no client-only injection for the default path).
Troubleshooting
- “Base URL is not set. Use
setBaseUrlto configure it.”- Call
setBaseUrl('https://your-domain')once at app startup (layout or_app)
- Call
- Nothing renders
- Ensure you pass at least
logoandwebsite/webpageprops; others are optional
- Ensure you pass at least
- Preview / Storybook / admin errors with inline
<script>- Use
renderScript={false}, or only callserializeSchemaJsonLdon the public site; see React 19 and whereSchemaJsonmay run.
- Use
Versioning & compatibility
- Peer deps:
reactandreact-dom^18 || ^19 - Target: modern Node/Next.js (ESNext output)
Contributing / Development
pnpm install
pnpm build
pnpm test
# Create a changeset for releases
pnpm changesetReleases are automated via GitHub Actions + Changesets. See .github/workflows/release.yml.
License
MIT
