Skip to main content

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

QuestionAnswer
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