JSON-LD (JavaScript Object Notation for Linked Data) is the recommended format for adding schema.org structured data to blog posts. It lives in a single <script> tag, does not touch your HTML, and — critically — must be hard-coded in static HTML so AI crawlers that skip JavaScript execution can still read it. This guide covers every BlogPosting field, platform-specific setup, and common debugging steps.

Why JSON-LD over Microdata and RDFa

There are three ways to add schema.org markup: JSON-LD, Microdata, and RDFa. Google recommends JSON-LD. AI engines prefer it too. If you are new to the vocabulary itself, read Schema.org Structured Data: A Complete Step-by-Step Guide first.

The key reasons:

  • Separation of concerns — JSON-LD lives in a <script> tag, entirely separate from your HTML structure. You can add, update, or remove it without touching your content markup.
  • Readability — it is plain JSON. Anyone can read, write, and debug it without learning a new attribute syntax.
  • Machine processing — parsers process it in one pass. Microdata requires traversing the DOM; JSON-LD is a self-contained data file.
  • Maintenance — when you update a field (like dateModified), you change one line in the JSON-LD block, not scattered attributes throughout the HTML.

The static HTML rule: why it matters for AI

This is the most common implementation mistake, and it silently voids all your work.

AI crawlers — GPTBot (ChatGPT), PerplexityBot (Perplexity), ClaudeBot (Anthropic) — are built for speed and efficiency. Many of them do not execute JavaScript. They parse the raw HTML your server delivers and move on.

If your JSON-LD is injected by a tag manager, a React useEffect, or any JavaScript that runs after initial page load, these bots receive a page with no structured data. Your schema does not exist from their perspective.

Rule: JSON-LD must be present in the server-delivered HTML, inside <head>, before any JavaScript executes.

How to verify: Press Ctrl+U (Windows) or Cmd+U (Mac) to view your page's raw source HTML. If the <script type="application/ld+json"> block appears there, AI crawlers can read it. If it only appears in browser DevTools → Elements panel after a moment, it is JavaScript-injected and invisible to most AI bots.

If you want to go further with Q&A content, see FAQPage Schema: How to Get Your FAQs Cited by AI Answer Engines.

A production-ready BlogPosting schema

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "BlogPosting",
      "@id": "https://yoursite.com/articles/your-article-slug#article",
      "headline": "Your Article Title",
      "description": "A one-sentence summary of the article for AI extraction.",
      "datePublished": "2026-04-03",
      "dateModified": "2026-05-21",
      "isAccessibleForFree": true,
      "inLanguage": "en",
      "url": "https://yoursite.com/articles/your-article-slug",
      "author": {
        "@type": "Person",
        "name": "Author Full Name",
        "url": "https://yoursite.com/about"
      },
      "publisher": {
        "@type": "Organization",
        "name": "Your Brand Name",
        "@id": "https://yoursite.com/#organization"
      },
      "image": {
        "@type": "ImageObject",
        "url": "https://yoursite.com/articles/your-article-og.png",
        "width": 1200,
        "height": 630
      },
      "speakable": {
        "@type": "SpeakableSpecification",
        "cssSelector": ["h1", ".article-summary"]
      },
      "keywords": "keyword one, keyword two, keyword three"
    },
    {
      "@type": "BreadcrumbList",
      "itemListElement": [
        { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://yoursite.com" },
        { "@type": "ListItem", "position": 2, "name": "Articles", "item": "https://yoursite.com/articles" },
        { "@type": "ListItem", "position": 3, "name": "Your Article Title", "item": "https://yoursite.com/articles/your-article-slug" }
      ]
    }
  ]
}

The complete BlogPosting field reference

Every field you should know, what it does, and whether it is required or recommended:

FieldPurposeRequired?
@typeDeclares this as a BlogPosting entityRequired
@idStable URL fragment identifier for cross-entity linkingRecommended
headlineArticle title (max 110 chars for Google rich results)Required
description1–2 sentence summary — what AI cites as your answer textRequired
datePublishedISO 8601 publication date (YYYY-MM-DD)Required
dateModifiedISO 8601 last update date — keep this currentRecommended
authorPerson or Organization who wrote the articleRequired
publisherOrganization that published the articleRequired
urlCanonical URL of this articleRecommended
imageFeatured image as ImageObject with url, width, heightRecommended
keywordsComma-separated topic tagsRecommended
inLanguageLanguage code ("en", "es", etc.)Recommended
isAccessibleForFreeSet to true for non-paywalled contentRecommended
speakableSpeakableSpecification for voice searchRecommended
wordCountInteger word count of article bodyOptional
articleSectionCategory or section nameOptional
mainEntityOfPageLinks to the WebPage entityOptional

Field-by-field notes

headline — the article title exactly as it appears on the page. Maximum 110 characters for Google rich results eligibility. Longer titles are still valid but may not trigger rich results.

description — the most important field for AI citation. This is what AI engines use as the answer text when they cite you. Write it as a complete, standalone sentence that fully answers the primary question of the article. Do not start with "In this article..." or "This guide covers..." — start directly with the answer.

datePublished / dateModified — ISO 8601 format (YYYY-MM-DD). Always keep dateModified current. For time-sensitive topics, stale modification dates reduce AI citation likelihood because the engine prefers fresher sources.

isAccessibleForFree: true — mark this explicitly if your content is free to read. AI engines use it to confirm they can reproduce your content without paywalled concern.

speakable — identifies the sentences a voice assistant should read aloud. Point the cssSelector at your article's first paragraph or a dedicated summary block. Keep the targeted text to 280–350 characters.

@id fragments — use #article, #breadcrumb, etc. as stable identifiers that let engines link entities across the graph without ambiguity.

How to place the script in <head>

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Your Article Title</title>
  
  <!-- Schema.org JSON-LD — must be here, not in <body> or loaded by JS -->
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    "headline": "Your Article Title"
  }
  </script>
</head>
<body>
  ...
</body>
</html>

The script tag's position in <head> means it is the first thing bots parse when arriving on your page. Bots process the head before the body, so your schema is guaranteed to be read regardless of how far down the page the actual article content appears.

Platform-specific implementation guides

WordPress (Rank Math — recommended)

  1. Install the Rank Math SEO plugin
  2. Go to Rank Math → Dashboard → Modules → Schema Markup → Enable
  3. Edit any post → scroll to the "Schema" tab in the post editor
  4. Select "Article" as the schema type, choose "BlogPosting"
  5. Rank Math auto-fills most fields from post metadata (title, author, date, featured image)
  6. Add keywords and a custom description if not using your meta description
  7. Save and validate: Rank Math's Schema Validator is built into the editor

Rank Math generates schema server-side — it appears in <head> as static HTML, invisible to AI crawlers if JavaScript is blocked.

Webflow

  1. Open the page editor → Page Settings (gear icon)
  2. Navigate to "Custom Code" section
  3. Paste your complete JSON-LD block in the "Head Code" field
  4. Publish the page
  5. Verify: View published page source (Ctrl+U) — the script block should appear

Webflow embeds head code statically during publish. It is not JavaScript-injected.

Ghost

  1. Open Ghost Admin → Settings → Code injection → "Site Header" field
  2. Paste your JSON-LD block (applies site-wide as a baseline)
  3. For per-post schema: use the "Code Injection" field inside each post editor
  4. Publish and verify via View Source

For per-post automation, consider Ghost integrations or the Ghost Content API to generate schema from post metadata.

Framer

  1. Open the page in Framer editor
  2. Click the page name in the left panel → "Page Settings"
  3. Navigate to "Custom Code" → "<head> tag"
  4. Paste your JSON-LD block
  5. Publish and verify via View Source

Next.js (App Router — recommended approach)

Use a Server Component to render schema as static HTML. This is what this site uses — each article page generates JSON-LD from its frontmatter at build time.

// app/articles/[slug]/page.tsx
export default function ArticlePage({ params }) {
  const article = getArticle(params.slug);
  
  const schema = {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    "headline": article.title,
    "description": article.description,
    "datePublished": article.publishedAt,
    "dateModified": article.updatedAt,
    // ... other fields
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
      />
      {/* article content */}
    </>
  );
}

The dangerouslySetInnerHTML in a Server Component renders to static HTML at request time. It is not client-side JavaScript.

Multi-entity @graph examples for real blog scenarios

Scenario 1: Blog post with FAQ section

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "BlogPosting",
      "@id": "https://yoursite.com/articles/slug#article",
      "headline": "...",
      "description": "...",
      "datePublished": "2026-04-03",
      "dateModified": "2026-05-21"
    },
    {
      "@type": "FAQPage",
      "@id": "https://yoursite.com/articles/slug#faq",
      "mainEntity": [
        {
          "@type": "Question",
          "name": "Question text?",
          "acceptedAnswer": { "@type": "Answer", "text": "Answer text." }
        }
      ]
    },
    {
      "@type": "BreadcrumbList",
      "itemListElement": [
        { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://yoursite.com" },
        { "@type": "ListItem", "position": 2, "name": "Blog", "item": "https://yoursite.com/articles" },
        { "@type": "ListItem", "position": 3, "name": "Article Title", "item": "https://yoursite.com/articles/slug" }
      ]
    }
  ]
}

Scenario 2: How-to post

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "BlogPosting",
      "@id": "https://yoursite.com/articles/slug#article",
      "headline": "How to Add JSON-LD to WordPress"
    },
    {
      "@type": "HowTo",
      "@id": "https://yoursite.com/articles/slug#howto",
      "name": "How to Add JSON-LD to WordPress",
      "totalTime": "PT10M",
      "step": [
        { "@type": "HowToStep", "position": 1, "name": "Install Rank Math", "text": "..." },
        { "@type": "HowToStep", "position": 2, "name": "Enable Schema Module", "text": "..." },
        { "@type": "HowToStep", "position": 3, "name": "Configure BlogPosting type", "text": "..." }
      ]
    }
  ]
}

Debugging JSON-LD: 6 common errors

Error 1: Trailing comma

{
  "headline": "Title",
  "description": "Desc",   ← trailing comma before }
}

Fix: Remove the trailing comma. Use a JSON linter at jsonlint.com.

Error 2: Single quotes instead of double quotes

{ 'headline': 'Title' }  ← invalid JSON

Fix: JSON requires double quotes for all strings and property names.

Error 3: Unescaped special characters If your headline or description contains quotes, apostrophes, or backslashes, they must be escaped:

"headline": "What's the Best Way to \"Fix\" Schema?"  ← use \" for quotes

Error 4: Missing @context Every top-level JSON-LD block must include "@context": "https://schema.org". Without it, parsers don't know the vocabulary.

Error 5: Wrong date format datePublished: "April 3, 2026" is invalid. Use ISO 8601: "2026-04-03" or "2026-04-03T00:00:00Z".

Error 6: Schema only appears in DevTools, not View Source This means your JSON-LD is JavaScript-injected. Move it to a server-rendered location.

Validating your implementation

After adding the script, always validate before deploying:

  1. JSON syntax — paste raw JSON into jsonlint.com
  2. Schema.org Validatorvalidator.schema.org — checks structural correctness
  3. Google Rich Results Test — confirms BlogPosting is eligible for rich results
  4. View sourceCtrl+U — confirm the JSON-LD appears in the raw HTML, not injected after load

If the schema only appears after a delay when using browser DevTools' "Elements" panel but is absent in "View Source", it is being injected by JavaScript. Fix this before indexing.

Common questions about JSON-LD for blogs

Does every blog post need its own JSON-LD?

Yes. Each post has a unique URL, title, description, and publication date. A single generic schema at the site level does not substitute for per-article structured data. Use a template in your CMS or static site generator to auto-generate the schema from post frontmatter — don't write it by hand for every post.

What happens if my JSON-LD has an error?

A syntax error (missing comma, unclosed bracket) silently invalidates the entire block. The page still renders normally for users, but bots skip the schema. Always validate with the Schema.org Validator before and after any CMS update that touches the head section.

Should I use Article or BlogPosting?

BlogPosting is a subtype of Article — it is more specific and preferred for blog content. Use Article for editorial or news content, BlogPosting for blog posts, and NewsArticle for time-sensitive journalism. The difference is minor for AI citation purposes, but precision helps.

How do I handle the author field if I am an organization, not a person?

Use "@type": "Organization" instead of "@type": "Person" in the author field. If your homepage already defines your Organization with an @id, reference it:

"author": {
  "@type": "Organization",
  "@id": "https://yoursite.com/#organization"
}

This links the article to your brand entity in the AI knowledge graph rather than treating it as an anonymous organization.