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.

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:

  1. Missing required fields: Article needs headline and author
  2. Wrong date format: Must be ISO 8601 (2026-05-07T09:30:00+08:00)
  3. Incomplete URLs: Using relative instead of absolute URLs
  4. Type typos: Case-sensitive, Article not article

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