dorsetdigital / silverstripe-schema-manager
Structured data schema registry and helpers for Silverstripe CMS 6.
Package info
github.com/DorsetDigital/silverstripe-schema-manager
Type:silverstripe-vendormodule
pkg:composer/dorsetdigital/silverstripe-schema-manager
Requires
- php: ^8.3
- silverstripe/asset-admin: ^3
- silverstripe/assets: ^3
- silverstripe/cms: ^6
- silverstripe/framework: ^6
- silverstripe/siteconfig: ^6
Requires (Dev)
- phpunit/phpunit: ^11.3
- silverstripe/recipe-testing: ^4
Suggests
- silverstripe/blog: Enable the optional BlogPosting schema extension for Silverstripe Blog posts.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-16 16:32:25 UTC
README
Structured-data management for Silverstripe CMS 6.
Schema Manager combines CMS-managed site information with automatic page schema and a request-scoped registry. Schema.org entities are emitted as a single linked JSON-LD @graph in the page <head>.
Requirements
- PHP 8.3+
- Silverstripe CMS 6
Installation
Once published on Packagist:
composer require dorsetdigital/silverstripe-schema-manager vendor/bin/sake dev/build flush=1
Until then, add the GitHub repository as a Composer VCS repository and require dev-main.
What is automatic?
By default Schema Manager adds site, page and breadcrumb schema to normal ContentController page requests:
Organization
↑ publisher
WebSite
↑ isPartOf
WebPage
BreadcrumbList
Stable entity IDs are based on the canonical site/page URLs:
https://example.com/#organisation
https://example.com/#website
https://example.com/about/#webpage
https://example.com/about/#breadcrumb
The JSON-LD is inserted automatically with Silverstripe Requirements. No template change is required.
Organisation settings
After installing the module and running dev/build, open Settings → Schema in the CMS.
The organisation can be managed with:
- organisation name, with the site title used as a fallback
- legal name
- telephone and email
- logo
- postal address
- external profile URLs (
sameAs), one per line
The organisation data is used to generate the site-wide Organization entity. WebSite schema is derived from SiteConfig and the canonical base URL.
Automatic WebPage schema
Every normal SiteTree page receives a WebPage entity containing its URL, title, description where available, created/modified dates and a reference to the site's WebSite entity.
No page extension needs to be configured for this behaviour.
Automatic breadcrumb schema
Every normal SiteTree page also receives a Schema.org BreadcrumbList built from Silverstripe's standard getBreadcrumbItems() functionality.
The breadcrumb entries use MenuTitle where available, falling back to Title, and include the absolute page URL and list position expected by Schema.org.
Automatic breadcrumb schema can be disabled independently while leaving the public helper available for manual use.
DorsetDigital\SchemaManager\Service\SchemaManager: automatic_breadcrumb_schema: false
A project can then register breadcrumbs itself, for example when it needs different Silverstripe breadcrumb options:
use DorsetDigital\SchemaManager\Control\SchemaRegistry; SchemaRegistry::addBreadCrumbs( $this, maxDepth: 20, stopAtPageType: false, showHidden: false );
Configuration
Each automatic layer can be disabled independently in project YAML:
DorsetDigital\SchemaManager\Service\SchemaManager: automatic_organisation_schema: true automatic_website_schema: true automatic_webpage_schema: true automatic_breadcrumb_schema: true
For example, a project already supplying its own organisation schema can set:
DorsetDigital\SchemaManager\Service\SchemaManager: automatic_organisation_schema: false
FAQ schema
FAQ markup has a convenience helper:
use DorsetDigital\SchemaManager\Control\SchemaRegistry; foreach ($this->FAQs() as $faq) { SchemaRegistry::addFAQ( $faq->Question, $faq->Answer ); }
An explicit page URL can be supplied as the third argument if required:
SchemaRegistry::addFAQ( $faq->Question, $faq->Answer, $this->AbsoluteLink() );
Each call appends another Question to one FAQPage entity for the current page.
Product and service schema
The module includes generic builders for common Product and Service entities. They deliberately accept values rather than depending on a particular e-commerce or service module, so project code remains responsible for mapping its own data model onto Schema.org.
A product can be added with:
use DorsetDigital\SchemaManager\Control\SchemaRegistry; use DorsetDigital\SchemaManager\Model\Schema\ProductSchema; $productSchema = ProductSchema::create( $product->AbsoluteLink(), $product->Title, $product->MetaDescription ) ->setImage($product->Image?->getAbsoluteURL()) ->setSKU($product->SKU) ->setBrand($product->Brand) ->setOffer($product->Price, 'GBP', $product->InStock); SchemaRegistry::add($productSchema);
The product is linked to the automatic WebPage entity using mainEntityOfPage. setOffer() adds a standard Schema.org Offer and can include stock availability.
A service follows the same pattern:
use DorsetDigital\SchemaManager\Control\SchemaRegistry; use DorsetDigital\SchemaManager\Model\Schema\ServiceSchema; $serviceSchema = ServiceSchema::create( $service->AbsoluteLink(), $service->Title, $service->MetaDescription ) ->setAreaServed('United Kingdom'); SchemaRegistry::add($serviceSchema);
Services link to the automatic WebPage entity and, by default, use the site's Organization entity as their provider. setProvider() can override that relationship, and setOffer() can add pricing where appropriate.
Both builders inherit Schema::update(), so less common Schema.org properties can be added without requiring the module to model every possible product or service use case:
$productSchema->update([ 'color' => $product->Colour, 'material' => $product->Material, ]);
Job posting schema
The module also includes a generic JobPostingSchema builder for recruitment and careers projects. As with the product and service builders, project code is responsible for mapping its own job data model onto the schema.
A typical on-site role can be registered with:
use DorsetDigital\SchemaManager\Control\SchemaRegistry; use DorsetDigital\SchemaManager\Model\Schema\JobPostingSchema; $jobSchema = JobPostingSchema::create( $job->AbsoluteLink(), $job->Title, $job->Description ) ->setDatePosted($job->PublishDate) ->setValidThrough($job->ClosingDate) ->setEmploymentType('FULL_TIME') ->setJobLocation( locality: 'Bournemouth', region: 'Dorset', country: 'GB' ) ->setBaseSalaryRange( 40000, 50000, currency: 'GBP', unit: 'YEAR' ); SchemaRegistry::add($jobSchema);
The job posting is linked to the automatic WebPage entity using mainEntityOfPage and uses the site's Organization entity as its hiringOrganization by default. setHiringOrganization() can override that relationship when required.
setJobLocation() creates a Place with a structured PostalAddress. In addition to locality, region and country it accepts optional streetAddress and postalCode arguments.
For remote roles, use setRemote(). An optional country can be supplied to add an applicantLocationRequirements restriction:
$jobSchema->setRemote('GB');
A fixed salary can be added with:
$jobSchema->setBaseSalary( 45000, currency: 'GBP', unit: 'YEAR' );
Salary ranges use setBaseSalaryRange() as shown above. Both methods generate a MonetaryAmount containing a QuantitativeValue; the unit can be changed for hourly, daily, weekly or monthly rates where appropriate.
Like the other typed builders, JobPostingSchema inherits Schema::update() for additional Schema.org properties which are specific to a project's recruitment model.
Optional Silverstripe Blog support
The module does not require silverstripe/blog.
If the project uses Silverstripe Blog, enable the supplied extension in project YAML:
SilverStripe\Blog\Model\BlogPost: extensions: - DorsetDigital\SchemaManager\Extension\BlogPostSchemaExtension
After a configuration flush, BlogPost pages retain their normal WebPage entity and also gain a linked BlogPosting entity containing the headline, publication/modification dates, description and featured image where available.
Adding schema manually
Raw entities can still be registered directly:
use DorsetDigital\SchemaManager\Control\SchemaRegistry; SchemaRegistry::addEntity( 'https://example.com/#service', [ '@type' => 'Service', '@id' => 'https://example.com/#service', 'name' => 'Example service', ] );
If an entity is registered again using the same @id, its data is recursively merged with the existing entity.
Typed schema objects can also be registered:
SchemaRegistry::add($schema);
where $schema extends DorsetDigital\SchemaManager\Model\Schema\Schema.
Extension point
Before page entities are registered, Schema Manager calls this Silverstripe extension hook on the page:
updateSchemaManagerEntities(array &$entities)
Extensions can therefore append, remove or modify typed schema objects without replacing the registry or controller integration.
For example:
use SilverStripe\Core\Extension; class MyPageSchemaExtension extends Extension { public function updateSchemaManagerEntities(array &$entities): void { // Add another Schema object, or update an existing one. } }
The same hook is called on SiteConfig for site-wide entities.
The bundled Blog integration uses this mechanism, so it also provides a reference implementation for future module integrations.
Registry API
SchemaRegistry::add(Schema $schema)
Registers a typed schema object.
SchemaRegistry::addEntity(string $id, array $data)
Registers a raw Schema.org entity. Existing data under the same ID is recursively merged.
SchemaRegistry::addFAQ(string $question, string $answer, ?string $pageURL = null)
Adds a question and accepted answer to the current page's FAQPage entity.
SchemaRegistry::addBreadCrumbs(SiteTree $page, int $maxDepth = 20, bool|string $stopAtPageType = false, bool $showHidden = false)
Adds a BreadcrumbList using Silverstripe's standard breadcrumb hierarchy. The method remains available when automatic breadcrumb schema is disabled.
SchemaRegistry::getGraph(): array
Returns all registered entities.
SchemaRegistry::getSchema(): array
Returns the complete structure containing @context and @graph.
SchemaRegistry::getJSON(): string
Returns JSON suitable for an application/ld+json script element.
SchemaRegistry::flush(): void
Clears the request registry.
Project structure
_config/
schema-manager.yml
src/
Control/
SchemaRegistry.php
Extension/
BlogPostSchemaExtension.php
SchemaControllerExtension.php
SiteConfigSchemaExtension.php
Model/
Schema/
BlogPostingSchema.php
JobPostingSchema.php
OrganisationSchema.php
ProductSchema.php
Schema.php
ServiceSchema.php
WebPageSchema.php
WebsiteSchema.php
Service/
SchemaManager.php
The module is deliberately split into small responsibilities:
- schema classes build individual Schema.org entities
- the registry collects, merges and renders the graph
- Silverstripe extensions decide which entities should be registered for a request
This keeps the public API small while leaving room for additional schema types and project-specific extensions.
License
BSD 3-Clause License.