Menu

Docs SEO NEO Version 1.x Structured Data (JSON-LD)

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 @graph generator 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 on renderHead is 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, with name, url, optional logo, description, and sameAs links. Choose Person for 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 publisher reference back to the Organization and the default-language inLanguage.
  • WebPage — the current page, with name, description, url, inLanguage, isPartOf, dateModified, datePublished, and primaryImageOfPage when an OG image resolves.
  • Article — added on pages whose template appears in Article templates in module config (e.g. blog_post,journal_post). Includes headline, description, image, datePublished, dateModified, resolved author, publisher reference, and inLanguage.
  • Book — added on pages whose template appears in Book templates (e.g. book,ebook). Includes name, description, image, optional isbn, numberOfPages, bookFormat (defaults to digital EBook when unset), resolved author, publisher reference, and inLanguage. 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). Includes name, description, image, and url.
  • 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 @type for the Organization node. Includes Person for 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=value maps for multilingual Organization nodes. The @id URIs 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.url on the publisher node and as publisher.logo on Article nodes. Required by Google for certain rich-result eligibility.
  • Publisher sameAs URLs — one URL per line. Used as the publisher's sameAs array — 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_author field override this.

Book templates (publishers)

For sites that catalogue books — print, digital, or both:

  1. Set Book templates in module config to your template name(s), e.g. book,ebook.
  2. Add the mapped fields to that template (or rename them in Book field mapping).
  3. 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 getJsonLd and renderJsonLd hooks.
  • Multilingual — per-language Organization name and description maps.

Last updated