Structured Data in Practice: Building a JSON-LD Generator for SEO
Structured Data in Practice: Building a JSON-LD Generator for SEO#
When working on website SEO, you often hear about “structured data”. Simply put, it’s a machine-readable format to tell search engines: this is a product, an article, or an event. The star ratings, price info, and event times in Google search results? That’s structured data at work.
JSON-LD (JavaScript Object Notation for Linked Data) is Google’s recommended format for structured data. This article shares how to build a JSON-LD generator and the technical details involved.
Core Structure of JSON-LD#
A minimal JSON-LD looks like this:
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Article Title",
"author": {
"@type": "Person",
"name": "Author Name"
}
}
@context specifies the vocabulary, @type specifies the type. Schema.org defines hundreds of types:
- Article / BlogPosting: Articles
- Product: Products
- Organization: Organizations
- FAQ: Frequently Asked Questions
- HowTo: Tutorial steps
- Event: Events
- BreadcrumbList: Breadcrumb navigation
Dynamic Form Generation#
Different types require different fields. Product needs price and availability; Article needs publish date and author. The solution is a field mapping:
const SCHEMA_FIELDS = {
Article: ['headline', 'description', 'image', 'authorName', 'datePublished'],
Product: ['name', 'description', 'image', 'brand', 'price', 'currency', 'availability'],
Organization: ['name', 'url', 'logo', 'sameAs'],
Event: ['name', 'startDate', 'endDate', 'location', 'description']
}
Render forms dynamically based on selected type:
function renderFields(schemaType: string) {
const fields = SCHEMA_FIELDS[schemaType]
return fields.map(key => (
<input
key={key}
placeholder={FIELD_LABELS[key]}
onChange={(e) => updateFormData(key, e.target.value)}
/>
))
}
Nested Object Construction#
JSON-LD supports nested structures. For example, Article’s author field is a Person object:
function buildArticleSchema(formData: FormData) {
const result: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': 'Article'
}
if (formData.authorName?.trim()) {
result.author = {
'@type': 'Person',
name: formData.authorName.trim()
}
}
if (formData.publisherName?.trim()) {
result.publisher = {
'@type': 'Organization',
name: formData.publisherName.trim(),
logo: formData.publisherLogo ? {
'@type': 'ImageObject',
url: formData.publisherLogo.trim()
} : undefined
}
}
return result
}
A key detail: only add fields to output when user has filled them, avoiding empty values.
Array Type Handling#
FAQ and HowTo types involve array data. Take FAQ as example:
interface FaqItem {
question: string
answer: string
}
function buildFaqSchema(items: FaqItem[]) {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: items
.filter(item => item.question.trim() && item.answer.trim())
.map(item => ({
'@type': 'Question',
name: item.question.trim(),
acceptedAnswer: {
'@type': 'Answer',
text: item.answer.trim()
}
}))
}
}
UI needs dynamic add/remove:
const [faqItems, setFaqItems] = useState([{ question: '', answer: '' }])
const addItem = () => setFaqItems([...faqItems, { question: '', answer: '' }])
const removeItem = (index: number) => setFaqItems(
faqItems.filter((_, i) => i !== index)
)
Empty Value Cleaning Algorithm#
Users might fill then delete, leaving empty strings in form data. A recursive clean function handles this:
function cleanEmpty(obj: unknown): unknown {
if (obj === null || obj === undefined || obj === '') return undefined
if (Array.isArray(obj)) {
const cleaned = obj.map(cleanEmpty).filter(v => v !== undefined)
return cleaned.length > 0 ? cleaned : undefined
}
if (typeof obj === 'object') {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(obj)) {
const cleaned = cleanEmpty(value)
if (cleaned !== undefined) {
result[key] = cleaned
}
}
return Object.keys(result).length > 0 ? result : undefined
}
return obj
}
This recursively processes objects and arrays, removing all empty values for clean output.
BreadcrumbList Position Property#
Breadcrumb structure is special—it requires position field for ordering:
function buildBreadcrumbSchema(items: { name: string; url: string }[]) {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1, // Position starts at 1
name: item.name,
item: item.url
}))
}
}
Output:
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "/" },
{ "@type": "ListItem", "position": 2, "name": "Tools", "item": "/tools" },
{ "@type": "ListItem", "position": 3, "name": "JSON-LD Generator", "item": "/tools/jsonld-generator" }
]
}
Validating JSON-LD#
Google provides Rich Results Test to validate structured data. Link directly in code:
<a
href="https://search.google.com/test/rich-results"
target="_blank"
rel="noopener noreferrer"
>
Test in Google
</a>
Common errors:
- Missing required fields: Article needs headline and author
- Wrong date format: Must be ISO 8601 (
2026-05-07T09:30:00+08:00) - Incomplete URLs: Using relative instead of absolute URLs
- Type typos: Case-sensitive,
Articlenotarticle
The Result#
Based on these ideas, I built a JSON-LD Generator: https://jsokit.com/tools/jsonld-generator
Supports 8 common Schema types:
- Article / BlogPosting: Article publishing
- Product: Product display
- Organization: Business info
- FAQ: FAQ pages
- HowTo: Tutorial steps
- Event: Events
- BreadcrumbList: Breadcrumb navigation
Generated output can be directly copied into webpage <script type="application/ld+json"> tags.
Related: Meta Tag Generator | Sitemap Generator