Structured Data (JSON-LD)
Beta. The @graph SEO NEO emits — Organization, WebSite, WebPage, optional Article, Person, and BreadcrumbList — how it's wired, how to configure it, and how to extend or disable it.
Beta feature. The JSON-LD@graphgenerator is included in 1.x and$page->seoneo->renderSchema()will return output, but the API shape, default graph composition, and hook surface are not yet committed for 1.0 stable and may change in point releases. For production sites that need structured data right now, hand-rolling JSON-LD via a hook onrenderHeadis the recommended path until the API stabilises.
What SEO NEO emits
When JSON-LD is enabled, SEO NEO appends a <script type="application/ld+json"> block containing a Schema.org @graph array. The graph uses canonical @id URIs to wire nodes together, so the page, site, author, and breadcrumb trail are a single linked entity rather than four separate blobs.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://example.com/#organization",
"name": "Your Site",
"url": "https://example.com/"
},
{
"@type": "WebSite",
"@id": "https://example.com/#website",
"publisher": { "@id": "https://example.com/#organization" },
"inLanguage": "en"
},
{
"@type": "WebPage",
"@id": "https://example.com/page/#webpage",
"name": "Page Title | Your Site",
"inLanguage": "en",
"isPartOf": { "@id": "https://example.com/#website" },
"datePublished": "2026-05-12T11:49:20+01:00",
"dateModified": "2026-05-14T15:37:31+01:00"
},
{
"@type": "BreadcrumbList",
"@id": "https://example.com/page/#breadcrumb",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "item": { "@id": "https://example.com/", "name": "Home" } },
{ "@type": "ListItem", "position": 2, "item": { "@id": "https://example.com/section/", "name": "Section" } },
{ "@type": "ListItem", "position": 3, "item": { "@id": "https://example.com/page/", "name": "Page Title" } }
]
}
]
}
</script>
Default graph nodes
- Organization (or
Person,LocalBusiness,NewsMediaOrganization,EducationalOrganization— set via Publisher type in module config) — the site-wide publisher, withname,url, optionallogo,description, andsameAslinks. ChoosePersonfor personal-brand sites where the publisher is an individual; the node still appears in the same slot in the graph. - WebSite — the site itself, with a
publisherreference back to the Organization and the default-languageinLanguage. - WebPage — the current page, with
name,description,url,inLanguage,isPartOf,dateModified,datePublished, andprimaryImageOfPagewhen an OG image resolves. - Article — added on pages whose template appears in Article templates in module config (e.g.
blog_post,journal_post). Includesheadline,description,image,datePublished,dateModified, resolved author, publisher reference, andinLanguage. - Book — added on pages whose template appears in Book templates (e.g.
book,ebook). Includesname,description,image, optionalisbn,numberOfPages,bookFormat(defaults to digitalEBookwhen unset), resolvedauthor, publisher reference, andinLanguage. Field names are configurable under Book field mapping in module config. - Person — added on pages whose template appears in Person templates (defaults to
user, so PW User pages used as author bios are recognised). Includesname,description,image, andurl. - BreadcrumbList — added when Emit BreadcrumbList is enabled (on by default) and the page has at least one parent.
Configuration
All JSON-LD settings live in Modules > Configure > SeoNeo > Structured data (JSON-LD):
- Emit JSON-LD — master switch. Enabled by default. Article templates are blank by default; add templates when you want Article nodes.
- Pretty-print JSON-LD — on by default. Disable for minified production output.
- Publisher type — the
@typefor the Organization node. IncludesPersonfor solo / personal-brand sites. - Article templates — comma-separated template names that get an Article node (e.g.
blog_post,news_item). - Book templates — comma-separated template names that get a Book node (e.g.
book,ebook). Useful for publishers and catalogues. - Book field mapping — map your ProcessWire field names for author, ISBN, format, cover image, synopsis, published date, and page count. Defaults:
book_author,book_isbn,book_format,cover_image,book_synopsis,book_published,book_pages. - Person templates — template names that get a Person node (default:
user). - Emit BreadcrumbList — adds the breadcrumb trail. On by default.
- Publisher name (per language) / Publisher description (per language) —
langname=valuemaps for multilingual Organization nodes. The@idURIs stay language-invariant (schema.org best practice); only the descriptive properties translate. - Publisher logo URL — absolute URL to a square logo image. Emitted as
logo.urlon the publisher node and aspublisher.logoon Article nodes. Required by Google for certain rich-result eligibility. - Publisher sameAs URLs — one URL per line. Used as the publisher's
sameAsarray — typically your social profiles (LinkedIn, X, GitHub, Crunchbase, Wikipedia). Adding these helps search engines build a confident knowledge-graph entity for your brand. - Default Article author — select a ProcessWire User page as fallback author on Article nodes. Pages with a
seoneo_authorfield override this.
Book templates (publishers)
For sites that catalogue books — print, digital, or both:
- Set Book templates in module config to your template name(s), e.g.
book,ebook. - Add the mapped fields to that template (or rename them in Book field mapping).
- Save a book page and inspect view-source for
"@type": "Book"in the SeoNeo JSON-LD block.
Format values for the book_format field: ebook / digital / epub / pdf → Schema.org EBook; paperback / softcover; hardcover / hardback; audiobook / audio. When the field is empty, SEO NEO defaults to EBook — suitable for digital-only catalogues.
Authors: plain text (comma-separated), a Page reference to an author bio page, or fall back to the Article default author / site author / site name.
For series volumes, pricing, or multiple editions per title, extend the graph via the getJsonLd hook — the built-in Book node covers the common publisher catalogue case.
Hooks
Use Book templates, Article templates, and Person templates in module config for the built-in nodes. Hooks are for extending that graph, adding types we do not ship yet, or per-site edge cases — not as a replacement for config when the built-in mapping fits.
Two hookable entry points: SeoNeo::getJsonLd (modify the @graph array) and SeoNeo::renderJsonLd (suppress or replace the rendered <script> tag). Drop examples in site/ready.php unless you prefer a dedicated module.
Extend the built-in Book node
When Book templates is configured, SEO NEO already emits a Book node. Enrich it — for example series membership or external catalogue URLs (sameAs):
wire()->addHookAfter('SeoNeo::getJsonLd', function($event) {
$page = $event->arguments(0);
if($page->template->name !== 'book') return;
$data = $event->return;
if(!is_array($data['@graph'] ?? null)) return;
foreach($data['@graph'] as &$node) {
if(($node['@type'] ?? '') !== 'Book') continue;
// Page reference: book_series
if($page->book_series && $page->book_series->id) {
$node['isPartOf'] = [
'@type' => 'CreativeWorkSeries',
'name' => (string) $page->book_series->title,
'url' => (string) $page->book_series->httpUrl,
];
}
// Textarea: one external URL per line (Goodreads, Amazon, etc.)
$sameAs = array_values(array_filter(array_map(
'trim',
explode("\n", (string) $page->book_sameas)
)));
if($sameAs) $node['sameAs'] = $sameAs;
break;
}
unset($node);
$event->return = $data;
});
Add a FAQPage node
wire()->addHookAfter('SeoNeo::getJsonLd', function($event) {
$page = $event->arguments(0);
if($page->template->name !== 'faq') return;
$data = $event->return;
$data['@graph'][] = [
'@type' => 'FAQPage',
'mainEntity' => [/* Question / Answer items */],
];
$event->return = $data;
});
Manual Book node (fallback)
Prefer Book templates in module config. Use this only when config does not fit — leave Book templates empty or you may get duplicate Book nodes.
wire()->addHookAfter('SeoNeo::getJsonLd', function($event) {
$page = $event->arguments(0);
if($page->template->name !== 'book') return;
$data = $event->return;
$url = $page->seoneo->canonical;
if($url === '') return;
$data['@graph'][] = [
'@type' => 'Book',
'@id' => $url . '#book',
'name' => (string) $page->title,
'url' => $url,
'description' => $page->seoneo->description,
'image' => $page->seoneo->ogImage,
'bookFormat' => 'https://schema.org/EBook',
'author' => ['@type' => 'Person', 'name' => (string) $page->book_author],
'mainEntityOfPage' => ['@id' => $url . '#webpage'],
];
$event->return = $data;
});
Suppress BreadcrumbList on the homepage
wire()->addHookAfter('SeoNeo::getJsonLd', function($event) {
$page = $event->arguments(0);
if($page->template->name === 'homepage') {
$data = $event->return;
$data['@graph'] = array_values(array_filter(
$data['@graph'],
fn($node) => ($node['@type'] ?? '') !== 'BreadcrumbList'
));
$event->return = $data;
}
});
Disabling JSON-LD
The Emit JSON-LD checkbox in module config is the master switch. Untick it and no <script type="application/ld+json"> block is emitted on any page. For per-page or per-template suppression, hook ___renderJsonLd($page) and return an empty string in the cases you want to skip.
See also
- Configuration — publisher type, article templates, and BreadcrumbList settings.
- Hooks & Customisation — extending or overriding the graph with
getJsonLdandrenderJsonLdhooks. - Multilingual — per-language Organization name and description maps.
Last updated