Skip to content

JSON-LD

Structured data is what tells search engines and AI agents what a page is — not just its words. pageflare emits schema.org JSON-LD on every page by reading the content model, so you get strong structured data without hand-writing <script type="application/ld+json"> blocks.

A typical page ends up with three to four JSON-LD blocks:

  1. Organization — site-wide brand identity (when site.yaml -> brand.organization is set).
  2. Page typeArticle / BlogPosting / WebPage / Product / ProfilePage — derived from frontmatter + content type.
  3. BreadcrumbList — built from the URL path.
  4. Custom overrides — anything you add under seo.jsonld (FAQPage, HowTo, etc.).

Each block is emitted as a separate <script type="application/ld+json" data-pf="..."> tag, in deterministic order. Re-running pageflare produces byte-identical output.

When content/site.yaml declares brand.organization, pageflare emits this block on every page:

site.yaml
brand:
organization:
name: "Acme Inc."
url: "https://acme.dev"
logo: "/content/assets/logos/acme-mark.svg"
same_as:
- "https://github.com/acme"
- "https://x.com/acme"

Becomes:

<script type="application/ld+json" data-pf="jsonld.organization">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Acme Inc.",
"url": "https://acme.dev",
"logo": "https://acme.dev/content/assets/logos/acme-mark.svg",
"sameAs": [
"https://github.com/acme",
"https://x.com/acme"
]
}
</script>

same_as is what links your entity to its public profiles (GitHub, X, LinkedIn, Crunchbase, Wikipedia/Wikidata). It’s the single highest-leverage field for AI Knowledge Graph linkage.

The page-type block is built from the page’s frontmatter and inferred type. pageflare picks the type using this precedence:

  1. Explicit seo.og.type — wins absolutely:

    seo.og.typeschema.org @type
    articleArticle (or BlogPosting for posts/)
    websiteWebPage
    productProduct
    profileProfilePage
  2. Content type posts/ — defaults to BlogPosting.

  3. Everything else — defaults to WebPage.

For a content/posts/hello-world.md like:

---
title: "Hello, world"
description: "First post on the new blog."
publish:
date: 2026-05-01
updated: 2026-05-12
author: "Shiva Kumar"
seo:
og:
image: "/content/assets/og-hello.png"
---

pageflare emits:

{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Hello, world",
"description": "First post on the new blog.",
"url": "https://acme.dev/blog/hello-world/",
"datePublished": "2026-05-01",
"dateModified": "2026-05-12",
"author": { "@type": "Person", "name": "Shiva Kumar" },
"image": "https://acme.dev/content/assets/og-hello.png",
"articleBody": "First post on the new blog. ..."
}

articleBody is the markdown body converted to plain text and capped at 5,000 characters. This gives crawlers a token-efficient summary of the page content.

For non-post pages, the same fields appear but the type is WebPage and there’s no articleBody:

{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "About",
"description": "About the team.",
"url": "https://acme.dev/about/"
}

Product and ProfilePage follow the same skeleton. They’re triggered by seo.og.type: product or seo.og.type: profile respectively, and exist to let you mark up landing pages and team-member pages without manual JSON.

Built from the page URL, regardless of content type:

<!-- /docs/getting-started/installation/ → -->
<script type="application/ld+json" data-pf="jsonld.breadcrumb">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://acme.dev/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Docs",
"item": "https://acme.dev/docs/"
},
{
"@type": "ListItem",
"position": 3,
"name": "Getting Started",
"item": "https://acme.dev/docs/getting-started/"
},
{
"@type": "ListItem",
"position": 4,
"name": "Installation",
"item": "https://acme.dev/docs/getting-started/installation/"
}
]
}
</script>

Segment names are humanized — getting-startedGetting Started. The root is always labeled Home and linked to site.url.

The home page itself (/) gets no BreadcrumbList — a single-item breadcrumb is meaningless.

For schema.org types pageflare doesn’t auto-build — FAQPage, HowTo, Recipe, Event, Course, SoftwareApplication, etc. — declare them under seo.jsonld in frontmatter:

---
title: "Pricing FAQ"
seo:
jsonld:
- "@type": FAQPage
mainEntity:
- "@type": Question
name: "Is there a free tier?"
acceptedAnswer:
"@type": Answer
text: "Yes — the Free tier covers up to 10 pages per project."
- "@type": Question
name: "Can I cancel anytime?"
acceptedAnswer:
"@type": Answer
text: "Yes, billing is monthly with no minimum commitment."
---

Each entry under seo.jsonld is emitted as a separate <script> block with marker data-pf="jsonld.custom.<index>". @context: "https://schema.org" is added automatically when absent.

You can stack multiple custom types on one page — e.g. a recipe page might have Recipe + HowTo + BreadcrumbList.

pageflare seo fix validates seo.jsonld entries on load and warns when an entry is missing @type or isn’t a mapping. This catches the most common authoring mistake before it ships to production.

JSON-LD blocks are emitted in this fixed order, before </head>:

  1. Organization (if brand.organization set)
  2. Page-type block (Article / BlogPosting / WebPage / Product / ProfilePage)
  3. BreadcrumbList (except on the home page)
  4. Custom overrides, in array order

The order is stable across runs — diffing optimized output won’t show spurious churn.

pageflare seo fix --bootstrap-content scans optimized HTML for existing JSON-LD blocks and converts them into seo.jsonld entries in frontmatter. Blocks already marked data-pf="..." are skipped (they’d be regenerated anyway), so the bootstrap captures only hand-authored or upstream-injected JSON-LD.

Validate the emitted blocks with Google’s Rich Results Test or Schema Markup Validator. Both accept a URL or a pasted HTML snippet.