# Implementing JSON-LD Structured Data with Next.js

> Source: https://agilitycms.com/docs/nextjs/implementing-json-ld-structured-data-with-next-js

JSON-LD (JavaScript Object Notation for Linked Data) structured data helps search engines better understand your content and can enhance your search results with rich snippets. Implementing JSON-LD with Agility CMS and Next.js improves your SEO and provides users with more informative search results.

The core concept is straightforward: fetch your dynamic page content from Agility CMS, analyze its content type and fields, then output a JSON-LD `<script>` tag in your page's HTML. While this guide uses Next.js and TypeScript, the same approach works with any framework or language—from PHP to Python, Vue to React, or even vanilla JavaScript. Since you're simply reading content from Agility CMS and rendering a meta tag, the implementation pattern translates universally: get the content, map the fields to the appropriate Schema.org structure, and inject the JSON-LD script into your page markup.

## Why Use JSON-LD?

- Improved SEO: Search engines can better understand and index your content
- Rich Snippets: Enhanced search results with images, ratings, dates, and more
- Better Click-Through Rates: Rich snippets are more visually appealing and informative
- Content Discovery: Helps search engines categorize and surface your content appropriately

## Understanding the Implementation

The implementation consists of two main parts:

1. Content Analysis Function: A utility function that examines your Agility CMS content and generates appropriate JSON-LD markup
2. Page Rendering: Integration with Next.js App Router to include the JSON-LD in your page's HTML

## Step 1: Create the JSON-LD Generator Function

Create a file at `lib/cms-content/getRichSnippet.ts` to generate JSON-LD based on your content type:

```
import { ContentItem } from "@agility/content-fetch"
import { AgilityPageProps } from "@agility/nextjs"
import { DateTime } from "luxon"

/**
 * Get the JSON-LD Rich Snippet for the current page.
 * Using recommendations from nextjs.org here:
 * https://nextjs.org/docs/app/building-your-application/optimizing/metadata#json-ld
 *
 * Google docs on the SEO benefits:
 * https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data
 *
 * Testing Tool: https://search.google.com/test/rich-results
 */
export const getRichSnippet = ({
	sitemapNode,
	page,
	languageCode,
	dynamicPageItem
}: AgilityPageProps): string | null => {
	if (!dynamicPageItem) return null

	// Handle different content types
	if (dynamicPageItem.properties.definitionName === "BlogPost") {
		return generateBlogPostStructuredData(dynamicPageItem)
	}

	if (dynamicPageItem.properties.definitionName === "Event") {
		return generateEventStructuredData(dynamicPageItem)
	}

	if (dynamicPageItem.properties.definitionName === "Article") {
		return generateArticleStructuredData(dynamicPageItem)
	}

	return null
}

```

## Step 2: Implement Structured Data for Blog Posts

Blog posts benefit from `BlogPosting` schema, which can display author information, publish dates, and featured images in search results:

```
const generateBlogPostStructuredData = (post: ContentItem<IPost>): string => {
	const category = post.fields.categories?.fields.title || undefined
	const image = post.fields.postImage?.url || undefined
	const author = post.fields.author?.fields.title || "Agility"
	const authorImage = post.fields.author?.fields.image?.url || undefined

	const datePublished = DateTime.fromISO(post.fields.date)
	const dateModified = DateTime.fromISO(`${post.properties.modified}`)

	const structData: any = {
		"@context": "https://schema.org",
		"@type": "BlogPosting",
		mainEntityOfPage: {
			"@type": "WebPage",
			"@id": "https://google.com/article"
		},
		headline: post.fields.title,
		datePublished: datePublished.toISO(),
		dateModified: dateModified.toISO()
	}

	if (author) {
		structData.author = [
			{
				"@type": "Person",
				name: author,
				url: authorImage
			}
		]
	}

	if (image) {
		structData.image = [image]
	}

	return JSON.stringify(structData)
}

```

## Step 3: Implement Structured Data for Events

Events use the `Event` schema type, which can display event details like date, time, location, and registration information:

```
const generateEventStructuredData = (event: ContentItem<IEvent>): string => {
	const startTime = DateTime.fromISO(event.fields.date)
	const endTime = startTime.plus({ hours: 1 })
	const validFrom = new Date()

	const canonicalUrl = `https://agilitycms.com/events/${event.fields.uRL}`
	const extLink = event.fields.externalLink || canonicalUrl

	const presenters = event.fields.presenters?.map((p) => ({
		"@type": "Person",
		name: p.fields.title,
		image: p.fields.image?.url
	}))

	const structData = {
		"@context": "https://schema.org",
		"@type": "Event",
		name: event.fields.title,
		startDate: startTime.toISO(),
		endDate: endTime.toISO(),
		eventAttendanceMode: "https://schema.org/OnlineEventAttendanceMode",
		eventStatus: "https://schema.org/EventScheduled",
		location: {
			"@type": "VirtualLocation",
			url: extLink
		},
		description: event.fields.description,
		offers: {
			"@type": "Offer",
			url: canonicalUrl,
			price: "0",
			priceCurrency: "USD",
			availability: "https://schema.org/InStock",
			validFrom: validFrom.toISOString()
		},
		performer: presenters,
		organizer: {
			"@type": "Organization",
			name: "Agility CMS",
			url: "https://agilitycms.com"
		},
		image: [event.fields.mainImage?.url]
	}

	return JSON.stringify(structData)
}

```

## Step 4: Integrate with Next.js App Router

In your dynamic page component (e.g., `app/[...slug]/page.tsx`), import and use the `getRichSnippet` function:

```
import { getRichSnippet } from "lib/cms-content/getRichSnippet"

export default async function AgilityPage({ params }: { params: { slug: string[] } }) {
  // ... fetch your Agility data

  const jsonLD = getRichSnippet(agilityData)

  return (
    <div
      data-agility-page={agilityData.page?.pageID}
      data-agility-dynamic-content={agilityData.sitemapNode.contentID}
    >
      {jsonLD && (
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: jsonLD }}
        />
      )}

      {/* Your page content */}
      <AgilityPageTemplate {...agilityData} />
    </div>
  )
}

```

## Example Content Types

The implementation supports multiple content types with their respective schema.org types:

| Example Content Model | Schema.org Type | Use Case |
| --- | --- | --- |
| BlogPost | BlogPosting | Blog articles and posts |
| Event | Event | Webinars, conferences, meetups |
| Resource | Article | Whitepapers, guides, documentation |
| CaseStudy | Article | Customer stories and case studies |

You can implement any schema with your content model as long as you have the requisite fields in place.

## Testing Your Structured Data

After implementing JSON-LD, it's crucial to validate that your structured data is correct:

1. Google Rich Results Test: 
2. Schema.org Validator: 
3. Google Search Console: Monitor your rich results performance

## Best Practices

### 1. Include Required Properties

Always include the minimum required properties for each schema type:

- BlogPosting: headline, image, datePublished, dateModified
- Event: name, startDate, location
- Article: headline, image, datePublished, dateModified

### 2. Use Accurate Dates

Ensure dates are in ISO 8601 format:

```
const datePublished = DateTime.fromISO(post.fields.date).toISO()

```

### 3. Provide High-Quality Images

- Use images at least 1200px wide
- Ensure images are relevant to the content
- Include multiple image sizes when possible

### 4. Keep Content Synchronized

The structured data should match your visible page content:

```
// ❌ Don't do this
"headline": "Different title than the page"

// ✅ Do this
"headline": post.fields.title

```

### 5. Handle Missing Data Gracefully

Only include optional properties when data is available:

```
if (author) {
	structData.author = [
		{
			"@type": "Person",
			name: author,
			url: authorImage
		}
	]
}

```

## Extending to Other Content Types

To add structured data support for additional content types:

1. Identify the appropriate Schema.org type: Visit 
2. Create a generator function: Follow the pattern established for existing types
3. Add a condition in getRichSnippet: Check for your content type's 
4. Test thoroughly: Use Google's Rich Results Test

Example for a Product content type:

```
if (dynamicPageItem.properties.definitionName === "Product") {
	return generateProductStructuredData(dynamicPageItem)
}

const generateProductStructuredData = (product: ContentItem<IProduct>): string => {
	const structData = {
		"@context": "https://schema.org",
		"@type": "Product",
		name: product.fields.title,
		image: [product.fields.image?.url],
		description: product.fields.description,
		offers: {
			"@type": "Offer",
			priceCurrency: "USD",
			price: product.fields.price,
			availability: "https://schema.org/InStock"
		}
	}

	return JSON.stringify(structData)
}

```

## Common Issues and Solutions

### Issue: Structured Data Not Appearing in Search Results

Solution: Rich results can take several weeks to appear. Ensure:

- Your structured data passes validation
- Your page is indexed by Google
- Your content meets Google's quality guidelines

### Issue: "Missing required field" errors

Solution: Check the [schema.org documentation](https://schema.org/) for required properties and add them:

```
// Required for BlogPosting
structData.headline = post.fields.title
structData.datePublished = datePublished.toISO()
structData.image = [post.fields.postImage?.url]

```

### Issue: Dates not recognized

Solution: Ensure dates are in ISO 8601 format:

```
// ❌ Wrong
"datePublished": "12/25/2023"

// ✅ Correct
"datePublished": "2023-12-25T10:00:00Z"

```

## Other JSON-LD Schema Types

While this guide focuses on common content types like blog posts, events, and articles, Schema.org offers hundreds of structured data types that you can implement. Here are some additional schema types that may be relevant for your Agility CMS website:

### Educational Content

#### Course Schema

Perfect for online courses, training programs, and educational content:

```
const generateCourseStructuredData = (course: ContentItem<ICourse>): string => {
	const structData = {
		"@context": "https://schema.org",
		"@type": "Course",
		name: course.fields.title,
		description: course.fields.description,
		provider: {
			"@type": "Organization",
			name: "Your Organization",
			sameAs: "https://yourwebsite.com"
		},
		offers: {
			"@type": "Offer",
			category: "Paid" // or "Free"
		}
	}

	return JSON.stringify(structData)
}

```

Learn more: [https://schema.org/Course](https://schema.org/Course)

#### EducationalOccupationalProgram

For degree programs, certifications, and professional training:

```
const generateEducationalProgramStructuredData = (program: ContentItem<IProgram>): string => {
	const structData = {
		"@context": "https://schema.org",
		"@type": "EducationalOccupationalProgram",
		name: program.fields.title,
		description: program.fields.description,
		provider: {
			"@type": "EducationalOrganization",
			name: "Your Institution"
		},
		educationalCredentialAwarded: {
			"@type": "EducationalOccupationalCredential",
			credentialCategory: "Certificate" // or "Degree", "Badge", etc.
		},
		timeToComplete: "P6M", // ISO 8601 duration format (6 months)
		occupationalCredentialAwarded: {
			"@type": "EducationalOccupationalCredential",
			credentialCategory: "Professional Certification"
		}
	}

	return JSON.stringify(structData)
}

```

Learn more: [https://schema.org/EducationalOccupationalProgram](https://schema.org/EducationalOccupationalProgram)

### E-Commerce and Products

#### Product Schema

For product pages with pricing and availability:

```
const generateProductStructuredData = (product: ContentItem<IProduct>): string => {
	const structData = {
		"@context": "https://schema.org",
		"@type": "Product",
		name: product.fields.title,
		image: product.fields.images?.map((img) => img.url),
		description: product.fields.description,
		sku: product.fields.sku,
		brand: {
			"@type": "Brand",
			name: product.fields.brand
		},
		offers: {
			"@type": "Offer",
			url: product.fields.url,
			priceCurrency: "USD",
			price: product.fields.price,
			availability: "https://schema.org/InStock",
			seller: {
				"@type": "Organization",
				name: "Your Company"
			}
		},
		aggregateRating: {
			"@type": "AggregateRating",
			ratingValue: product.fields.averageRating,
			reviewCount: product.fields.reviewCount
		}
	}

	return JSON.stringify(structData)
}

```

Learn more: [https://schema.org/Product](https://schema.org/Product)

### Business and Organizations

#### LocalBusiness Schema

For physical locations, stores, and service areas:

```
const generateLocalBusinessStructuredData = (business: ContentItem<IBusiness>): string => {
	const structData = {
		"@context": "https://schema.org",
		"@type": "LocalBusiness",
		name: business.fields.name,
		image: business.fields.image?.url,
		address: {
			"@type": "PostalAddress",
			streetAddress: business.fields.streetAddress,
			addressLocality: business.fields.city,
			addressRegion: business.fields.state,
			postalCode: business.fields.zipCode,
			addressCountry: business.fields.country
		},
		geo: {
			"@type": "GeoCoordinates",
			latitude: business.fields.latitude,
			longitude: business.fields.longitude
		},
		telephone: business.fields.phone,
		openingHoursSpecification: [
			{
				"@type": "OpeningHoursSpecification",
				dayOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
				opens: "09:00",
				closes: "17:00"
			}
		]
	}

	return JSON.stringify(structData)
}

```

Learn more: [https://schema.org/LocalBusiness](https://schema.org/LocalBusiness)

### Media and Creative Works

#### VideoObject Schema

For video content and tutorials:

```
const generateVideoStructuredData = (video: ContentItem<IVideo>): string => {
	const structData = {
		"@context": "https://schema.org",
		"@type": "VideoObject",
		name: video.fields.title,
		description: video.fields.description,
		thumbnailUrl: video.fields.thumbnail?.url,
		uploadDate: video.fields.publishDate,
		duration: video.fields.duration, // ISO 8601 format: "PT1H30M"
		contentUrl: video.fields.videoUrl,
		embedUrl: video.fields.embedUrl,
		interactionStatistic: {
			"@type": "InteractionCounter",
			interactionType: { "@type": "WatchAction" },
			userInteractionCount: video.fields.viewCount
		}
	}

	return JSON.stringify(structData)
}

```

Learn more: [https://schema.org/VideoObject](https://schema.org/VideoObject)

#### Podcast Schema

For podcast episodes and series:

```
const generatePodcastStructuredData = (podcast: ContentItem<IPodcast>): string => {
	const structData = {
		"@context": "https://schema.org",
		"@type": "PodcastEpisode",
		name: podcast.fields.title,
		description: podcast.fields.description,
		datePublished: podcast.fields.publishDate,
		associatedMedia: {
			"@type": "MediaObject",
			contentUrl: podcast.fields.audioUrl
		},
		partOfSeries: {
			"@type": "PodcastSeries",
			name: podcast.fields.seriesName,
			url: podcast.fields.seriesUrl
		},
		actor: podcast.fields.hosts?.map((host) => ({
			"@type": "Person",
			name: host.fields.name
		}))
	}

	return JSON.stringify(structData)
}

```

Learn more: [https://schema.org/PodcastEpisode](https://schema.org/PodcastEpisode)

### Reviews and Ratings

#### Review Schema

For product or service reviews:

```
const generateReviewStructuredData = (review: ContentItem<IReview>): string => {
	const structData = {
		"@context": "https://schema.org",
		"@type": "Review",
		itemReviewed: {
			"@type": "Product",
			name: review.fields.productName
		},
		author: {
			"@type": "Person",
			name: review.fields.authorName
		},
		reviewRating: {
			"@type": "Rating",
			ratingValue: review.fields.rating,
			bestRating: "5",
			worstRating: "1"
		},
		reviewBody: review.fields.reviewText,
		datePublished: review.fields.publishDate
	}

	return JSON.stringify(structData)
}

```

Learn more: [https://schema.org/Review](https://schema.org/Review)

### FAQ and Q&A

#### FAQPage Schema

For frequently asked questions pages:

```
const generateFAQStructuredData = (faq: ContentItem<IFAQ>): string => {
	const structData = {
		"@context": "https://schema.org",
		"@type": "FAQPage",
		mainEntity: faq.fields.questions?.map((q) => ({
			"@type": "Question",
			name: q.fields.question,
			acceptedAnswer: {
				"@type": "Answer",
				text: q.fields.answer
			}
		}))
	}

	return JSON.stringify(structData)
}

```

Learn more: [https://schema.org/FAQPage](https://schema.org/FAQPage)

### Recipe Content

#### Recipe Schema

For cooking recipes and food content:

```
const generateRecipeStructuredData = (recipe: ContentItem<IRecipe>): string => {
	const structData = {
		"@context": "https://schema.org",
		"@type": "Recipe",
		name: recipe.fields.title,
		image: recipe.fields.images?.map((img) => img.url),
		author: {
			"@type": "Person",
			name: recipe.fields.author
		},
		datePublished: recipe.fields.publishDate,
		description: recipe.fields.description,
		prepTime: recipe.fields.prepTime, // ISO 8601: "PT30M"
		cookTime: recipe.fields.cookTime,
		totalTime: recipe.fields.totalTime,
		recipeYield: recipe.fields.servings,
		recipeIngredient: recipe.fields.ingredients,
		recipeInstructions: recipe.fields.instructions?.map((step, index) => ({
			"@type": "HowToStep",
			position: index + 1,
			text: step
		})),
		nutrition: {
			"@type": "NutritionInformation",
			calories: recipe.fields.calories
		}
	}

	return JSON.stringify(structData)
}

```

Learn more: [https://schema.org/Recipe](https://schema.org/Recipe)

### Job Postings

#### JobPosting Schema

For career and recruitment pages:

```
const generateJobPostingStructuredData = (job: ContentItem<IJobPosting>): string => {
	const structData = {
		"@context": "https://schema.org",
		"@type": "JobPosting",
		title: job.fields.title,
		description: job.fields.description,
		datePosted: job.fields.postDate,
		validThrough: job.fields.expiryDate,
		employmentType: job.fields.employmentType, // "FULL_TIME", "PART_TIME", etc.
		hiringOrganization: {
			"@type": "Organization",
			name: "Your Company",
			sameAs: "https://yourwebsite.com",
			logo: "https://yourwebsite.com/logo.png"
		},
		jobLocation: {
			"@type": "Place",
			address: {
				"@type": "PostalAddress",
				streetAddress: job.fields.streetAddress,
				addressLocality: job.fields.city,
				addressRegion: job.fields.state,
				postalCode: job.fields.zipCode,
				addressCountry: job.fields.country
			}
		},
		baseSalary: {
			"@type": "MonetaryAmount",
			currency: "USD",
			value: {
				"@type": "QuantitativeValue",
				value: job.fields.salary,
				unitText: "YEAR"
			}
		}
	}

	return JSON.stringify(structData)
}

```

Learn more: [https://schema.org/JobPosting](https://schema.org/JobPosting)

### Implementation Tips for Custom Schema Types

When implementing any of these schema types:

1. Check Google's Support: Not all Schema.org types are supported by Google for rich results. Check 
2. Use Required Properties: Each schema type has required and recommended properties. Always include required properties and as many recommended ones as possible.
3. Test Thoroughly: Use the 
4. Match Content to Schema: Choose the schema type that most closely matches your content. Don't force content into an inappropriate schema type.
5. Keep It Updated: Schema.org evolves over time. Check the 

### Exploring More Schema Types

Schema.org contains over 800 types covering a wide range of content. Visit [https://schema.org/docs/full.html](https://schema.org/docs/full.html) to explore the complete hierarchy and find the schema types that best match your content needs.
