{
  "$type": "site.standard.document",
  "content": {
    "$type": "org.wordpress.html",
    "html": "<div><h2></h2><h2>How it works</h2>\n<p>SEO Fields now holds a <strong>shared <code>Graph</code> instance</strong> per request. You can access it from any template using <code>seoFields.graph</code>, add schema types, set properties — and at render time, the full graph gets output as a single <code>&lt;script type=\"application/ld+json\"&gt;</code> tag. No manual output needed.</p>\n<p>The default schema (organization + section-level type) is still added automatically. Anything you add from your templates merges into the same graph.</p>\n</div><div><h2></h2><p>Add a schema type to the graph using `seoFields.graph`</p><p>The graph reuses the same node per type — calling `.event()` twice returns the same node, so you can set properties across multiple templates without conflicts.</p><pre><code>{% do seoFields.graph.event()\n    .name(entry.title)\n    .description(entry.intro|striptags)\n    .url(entry.url)\n%}</code></pre></div><div><h2></h2><p>This is where it gets powerful. Your entry template and its includes all contribute to the same graph:</p><pre><code>{# _events/_entry.twig #}\n{% do seoFields.graph.event()\n    .name(entry.title)\n    .description(entry.intro|striptags)\n    .url(entry.url)\n%}\n\n{% include '_snippets/_faq' with { faqs: entry.faqBlocks } %}\n\n{# _snippets/_faq.twig #}\n{% if faqs|length %}\n    {% set questions = [] %}\n    {% for faq in faqs.all() %}\n        {% set questions = questions|merge([\n            seoFields.schema.question()\n                .name(faq.title)\n                .acceptedAnswer(\n                    seoFields.schema.answer().text(faq.answer|striptags)\n                )\n        ]) %}\n    {% endfor %}\n    {% do seoFields.graph.fAQPage().mainEntity(questions) %}\n{% endif %}</code></pre></div><div><h2></h2><p>The result is a single JSON-LD script tag containing both the Event and the FAQPage, alongside the default organization node:</p><pre><code>{\n  \"@context\": \"https://schema.org\",\n  \"@graph\": [\n    {\n      \"@type\": \"Organization\",\n      \"@id\": \"#organization\",\n      \"name\": \"My Site\",\n      \"url\": \"https://example.com/\"\n    },\n    {\n      \"@type\": \"Event\",\n      \"@id\": \"#page\",\n      \"name\": \"My Event\",\n      \"description\": \"Event description here\",\n      \"url\": \"https://example.com/events/my-event\"\n    },\n    {\n      \"@type\": \"FAQPage\",\n      \"mainEntity\": [\n        {\n          \"@type\": \"Question\",\n          \"name\": \"How does it work?\",\n          \"acceptedAnswer\": {\n            \"@type\": \"Answer\",\n            \"text\": \"Like this!\"\n          }\n        }\n      ]\n    }\n  ]\n}</code></pre></div><div><h2></h2><h2><code>seoFields.graph</code> vs <code>seoFields.schema</code></h2>\n<p>There are two entry points, and they serve different purposes:</p>\n<ul>\n<li><strong><code>seoFields.graph</code></strong> — the shared Graph instance for the current request. Use this when adding top-level schema types to the page (Event, FAQPage, Article, etc.). Types added here end up in the <code>@graph</code> array.</li>\n<li><strong><code>seoFields.schema</code></strong> — a factory for creating standalone schema types. Use this for nested objects like <code>Question</code> and <code>Answer</code> that get passed as properties to graph nodes.</li>\n</ul>\n</div><div><h2></h2><p>Every schema type supports the fluent API from <a href=\"https://github.com/spatie/schema-org\">spatie/schema-org</a>. You can set any schema.org property as a method call:<br /> </p><pre><code>{% do seoFields.graph.article()\n    .name(entry.title)\n    .author(entry.author.fullName)\n    .datePublished(entry.postDate|date('c'))\n    .image(entry.featuredImage.one().url)\n%}\n\n## For properties that aren't available as named methods, use `setProperty`:\n\n\n{% do seoFields.graph.event()\n    .setProperty('@id', '#my-event')\n    .setProperty('eventAttendanceMode', 'https://schema.org/OfflineEventAttendanceMode')\n%}</code></pre></div><div><h2>What happens without any template code</h2><p>Nothing changes for existing sites. If you don't use <code>seoFields.graph</code> in your templates, the plugin still outputs the same default schema it always has — the organization node plus the section-level type (WebPage, Article, etc.) configured in your SEO Fields settings.</p>\n</div><div><h2>Organisation settings</h2><p>In order to for your sites to have a base level of organizational- or project-level that, the plugin also comes with fields where you can add a name, type, logo and aliases (for socials and other places on the that reflect the organization, company or project)</p>\n</div><div></div><div><h2>Debugging & development</h2><p>Latest, the plugin now adds a panel to Craft's debug bar where you can see the structured data for the page and wether or not the data is valid.</p><p>And lastly, for those building with AI, the documentation for the plugin and these new features are now also <a href=\"https://context7.com/studioespresso/craft-seo-fields\">available on Context7</a>, which makes it easy to instruct Claude Code or Codex to use this feature.</p></div>"
  },
  "description": "Until now, schema rendering in SEO Fields was all-or-nothing: the plugin auto-generates a basic JSON-LD schema based on your section-level settings, or you disable it entirely and roll your own. There was no middle ground — no way to set a main schema type in a template and then progressively add to it from includes.That changes with the new Schema Builder API released in version 5.4.0 .",
  "path": "/articles/2026-schema-builder-api-for-seo-fields",
  "publishedAt": "2026-03-18T21:09:00+01:00",
  "site": "at://did:plc:kq2tuvvnlen4jjcqiw4oprjm/site.standard.publication/3mhyhqc3kcaw7",
  "tags": [
    "Craft CMS"
  ],
  "textContent": "How it works\nSEO Fields now holds a shared Graph instance per request. You can access it from any template using seoFields.graph, add schema types, set properties — and at render time, the full graph gets output as a single &lt;script type=\"application/ld+json\"&gt; tag. No manual output needed.\nThe default schema (organization + section-level type) is still added automatically. Anything you add from your templates merges into the same graph.\nAdd a schema type to the graph using `seoFields.graph`The graph reuses the same node per type — calling `.event()` twice returns the same node, so you can set properties across multiple templates without conflicts.{% do seoFields.graph.event()\n    .name(entry.title)\n    .description(entry.intro|striptags)\n    .url(entry.url)\n%}This is where it gets powerful. Your entry template and its includes all contribute to the same graph:{# _events/_entry.twig #}\n{% do seoFields.graph.event()\n    .name(entry.title)\n    .description(entry.intro|striptags)\n    .url(entry.url)\n%}\n\n{% include '_snippets/_faq' with { faqs: entry.faqBlocks } %}\n\n{# _snippets/_faq.twig #}\n{% if faqs|length %}\n    {% set questions = [] %}\n    {% for faq in faqs.all() %}\n        {% set questions = questions|merge([\n            seoFields.schema.question()\n                .name(faq.title)\n                .acceptedAnswer(\n                    seoFields.schema.answer().text(faq.answer|striptags)\n                )\n        ]) %}\n    {% endfor %}\n    {% do seoFields.graph.fAQPage().mainEntity(questions) %}\n{% endif %}The result is a single JSON-LD script tag containing both the Event and the FAQPage, alongside the default organization node:{\n  \"@context\": \"https://schema.org\",\n  \"@graph\": [\n    {\n      \"@type\": \"Organization\",\n      \"@id\": \"#organization\",\n      \"name\": \"My Site\",\n      \"url\": \"https://example.com/\"\n    },\n    {\n      \"@type\": \"Event\",\n      \"@id\": \"#page\",\n      \"name\": \"My Event\",\n      \"description\": \"Event description here\",\n      \"url\": \"https://example.com/events/my-event\"\n    },\n    {\n      \"@type\": \"FAQPage\",\n      \"mainEntity\": [\n        {\n          \"@type\": \"Question\",\n          \"name\": \"How does it work?\",\n          \"acceptedAnswer\": {\n            \"@type\": \"Answer\",\n            \"text\": \"Like this!\"\n          }\n        }\n      ]\n    }\n  ]\n}seoFields.graph vs seoFields.schema\nThere are two entry points, and they serve different purposes:\n\nseoFields.graph — the shared Graph instance for the current request. Use this when adding top-level schema types to the page (Event, FAQPage, Article, etc.). Types added here end up in the @graph array.\nseoFields.schema — a factory for creating standalone schema types. Use this for nested objects like Question and Answer that get passed as properties to graph nodes.\n\nEvery schema type supports the fluent API from spatie/schema-org. You can set any schema.org property as a method call: {% do seoFields.graph.article()\n    .name(entry.title)\n    .author(entry.author.fullName)\n    .datePublished(entry.postDate|date('c'))\n    .image(entry.featuredImage.one().url)\n%}\n\n## For properties that aren't available as named methods, use `setProperty`:\n\n\n{% do seoFields.graph.event()\n    .setProperty('@id', '#my-event')\n    .setProperty('eventAttendanceMode', 'https://schema.org/OfflineEventAttendanceMode')\n%}What happens without any template codeNothing changes for existing sites. If you don't use seoFields.graph in your templates, the plugin still outputs the same default schema it always has — the organization node plus the section-level type (WebPage, Article, etc.) configured in your SEO Fields settings.\nOrganisation settingsIn order to for your sites to have a base level of organizational- or project-level that, the plugin also comes with fields where you can add a name, type, logo and aliases (for socials and other places on the that reflect the organization, company or project)\nDebugging & developmentLatest, the plugin now adds a panel to Craft's debug bar where you can see the structured data for the page and wether or not the data is valid.And lastly, for those building with AI, the documentation for the plugin and these new features are now also available on Context7, which makes it easy to instruct Claude Code or Codex to use this feature.",
  "title": "Schema Builder API for SEO Fields",
  "updatedAt": "2026-03-26T21:21:38+01:00"
}