Loading jsonLD in React Router (Framework Mode)
JSON-LD (JavaScript Object Notation for Linked Data) is a standard way to embed structured data within web pages using JSON format. It makes it easy for humans and machines to read, helping search engines understand content better for rich results (like star ratings, event details) and enabling data to be linked across the web for more context, with Google recommending it as the preferred method for structured data.
Mental Model
Mental Model
In React Router v7 (framework mode), there are three key concepts for understanding how data flows from a route to the <head> tag:
1. LOADER (Server-side first, then client on navigation)
├── Fetches data from API
└── Returns data object (e.g., { job: { schema: {...} } })
2. META FUNCTION (Receives loader data as argument)
├── Runs on server during SSR
├── Runs on client during navigation
└── Returns array of meta objects → injected into <head>
3. LAYOUT COMPONENT (Wraps entire HTML document)
├── <Meta /> component renders all meta objects from routes
└── Includes <script type="application/ld+json"> for JSON-LD
React Router <meta> support
| Question | Answer |
|---|---|
How does meta receive data? | React Router calls your meta function and passes MetaArgs object containing data (loader result), params, matches, location, error |
| Where is this documented? | MetaArgs API |
Is script:ld+json officially supported? | Yes! It's in MetaDescriptor type |
| Why spread an array? | To conditionally include items in the returned array without adding null values |
1. Meta Function Signature (MetaArgs)
📖 Official API Docs: https://api.reactrouter.com/v7/interfaces/react_router.MetaArgs
The MetaArgs interface shows the function receives:
interface MetaArgs<Loader, MatchLoaders> {
data: undefined | SerializeFrom<Loader>; // ← Data from YOUR route's loader
error?: unknown; // ← Error if in ErrorBoundary
location: Location<any>; // ← Current location
matches: MetaMatches<MatchLoaders>; // ← All matched routes with their data
params: Params; // ← URL parameters
}
2. Meta Return Types (MetaDescriptor)
📖 Official API Docs: https://api.reactrouter.com/v7/types/react_router.MetaDescriptor.html
The MetaDescriptor type includes support for JSON-LD:
MetaDescriptor:
| { charSet: "utf-8"; }
| { title: string; }
| { content: string; name: string; }
| { content: string; property: string; }
| { content: string; httpEquiv: string; }
| { "script:ld+json": LdJsonObject; } // ← JSON-LD structured data!
// ... other types
3. Route Module Documentation
📖 Official Docs: https://reactrouter.com/start/framework/route-module#meta
Use meta function with script:ld+json (React Router's built-in way)**
Route Module root.jsx Layout
┌────────────────────┐ ┌──────────────────────────┐
│ export const meta │──────────────▶│ <Meta /> component │
│ ({ data }) => [ │ │ automatically handles │
│ { "script:ld+ │ │ all meta objects │
│ json": ... } │ │ including JSON-LD │
│ ] │ └──────────────────────────┘
└────────────────────┘
Analysis
Here's the correct approach using React Router's built-in script:ld+json support:
export const meta = ({ data }) => {
const job = data?.job
const jobable = job?.jobable
return [
{
title: job
? `${jobable?.title} at ${job?.company_name} | Jod Board`
: 'Job Detail | Jod Board'
},
{
name: 'description',
content: job
? `Apply for ${jobable?.title} at ${job?.company_name}. ${jobable?.description?.slice(0, 150)}...`
: 'View detailed information about a job on Jod Board.'
},
{
name: 'robots',
content: 'index, follow'
},
// Conditional array spreading pattern
// - no `schema`: add nothing
// - `schema` present`: spread open the array [{key: 'value'}] to get the object {key: 'value}
...(job?.schema ? [{ 'script:ld+json': job.schema }] : [])
]
}
Why the Spread Operator on an Array?
...(job?.schema ? [{ 'script:ld+json': job.schema }] : [])
This is a conditional array spreading pattern used to optionally include items in an array.
Breaking it down:
// The meta function MUST return an array of MetaDescriptor objects
export const meta = ({ data }) => {
return [
{ title: 'Job Title' }, // Always included
{ name: 'description', content: '...' }, // Always included
// Conditionally include JSON-LD only if schema exists
...(data?.job?.schema
? [{ 'script:ld+json': data.job.schema }] // If true: spread [{ schema }] → adds one item
: [] // If false: spread [] → adds nothing
)
]
}
Visual Explanation:
When job.schema EXISTS:
─────────────────────────
...[{ 'script:ld+json': job.schema }]
↓ spread operator unpacks the array
{ 'script:ld+json': job.schema } ← One item added to the array
When job.schema is NULL/UNDEFINED:
──────────────────────────────────
...[]
↓ spread operator unpacks empty array
(nothing added) ← Zero items added