Schema Builder API for SEO Fields

Studio Espresso March 18, 2026
Source

How it works SEO 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 <script type="application/ld+json"> tag. No manual output needed. The default schema (organization + section-level type) is still added automatically. Anything you add from your templates merges into the same graph. Add a schema type to the graph using seoFields.graphThe 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() .name(entry.title) .description(entry.intro|striptags) .url(entry.url) %}This is where it gets powerful. Your entry template and its includes all contribute to the same graph:{# _events/_entry.twig #} {% do seoFields.graph.event() .name(entry.title) .description(entry.intro|striptags) .url(entry.url) %}

{% include '_snippets/_faq' with { faqs: entry.faqBlocks } %}

{# _snippets/_faq.twig #} {% if faqs|length %} {% set questions = [] %} {% for faq in faqs.all() %} {% set questions = questions|merge([ seoFields.schema.question() .name(faq.title) .acceptedAnswer( seoFields.schema.answer().text(faq.answer|striptags) ) ]) %} {% endfor %} {% do seoFields.graph.fAQPage().mainEntity(questions) %} {% endif %}The result is a single JSON-LD script tag containing both the Event and the FAQPage, alongside the default organization node:{ "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "#organization", "name": "My Site", "url": "https://example.com/" }, { "@type": "Event", "@id": "#page", "name": "My Event", "description": "Event description here", "url": "https://example.com/events/my-event" }, { "@type": "FAQPage", "mainEntity": [ { "@type": "Question", "name": "How does it work?", "acceptedAnswer": { "@type": "Answer", "text": "Like this!" } } ] } ] }seoFields.graph vs seoFields.schema There are two entry points, and they serve different purposes:

seoFields.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. seoFields.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.

Every 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() .name(entry.title) .author(entry.author.fullName) .datePublished(entry.postDate|date('c')) .image(entry.featuredImage.one().url) %}

For properties that aren't available as named methods, use setProperty:

{% do seoFields.graph.event() .setProperty('@id', '#my-event') .setProperty('eventAttendanceMode', 'https://schema.org/OfflineEventAttendanceMode') %}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. Organisation 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) Debugging & 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.

Discussion in the ATmosphere

Loading comments...