contenir / contenir-sitemap-mezzio
Mezzio adapter for Contenir sitemaps: PSR-15 handlers for /sitemap.xml and /robots.txt, built from the contenir-workflow-mezzio navigation or any SitemapSourceInterface
Package info
github.com/contenir/contenir-sitemap-mezzio
pkg:composer/contenir/contenir-sitemap-mezzio
Requires
- php: ~8.3.0 || ~8.4.0 || ~8.5.0
- ext-xmlwriter: *
- contenir/contenir-metadata: ^2.0
- contenir/contenir-workflow-mezzio: ^2.1
- laminas/laminas-cache: ^3.12
- laminas/laminas-servicemanager: ^3.22
- mezzio/mezzio: ^3.18
- mezzio/mezzio-router: ^3.17 || ^4.0
- psr/container: ^1.1 || ^2.0
- psr/http-factory: ^1.0.2
- psr/http-message: ^1.1 || ^2.0
- psr/http-server-handler: ^1.0.2
Requires (Dev)
- infection/infection: ^0.34.1
- laminas/laminas-diactoros: ^3.3
- mezzio/mezzio-fastroute: ^3.11
- php-db/phpdb-qa-tools: 0.1.x-dev
- phpunit/phpunit: ^11.5.42
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-07 03:13:09 UTC
README
Mezzio adapter for Contenir CMS sitemaps: PSR-15
handlers that serve /sitemap.xml and /robots.txt, for Mezzio sites that
do not run the full CMS. It is the Mezzio counterpart of
contenir/contenir-sitemap-laminas-mvc.
The sitemap is built from a SitemapSourceInterface. The default source
reads the navigation of
contenir/contenir-workflow-mezzio,
the same resource tree that generates the site's routes, and asks the Mezzio
router for each page's URL. With the bundled MetadataResourceStrategy,
each page's <lastmod> comes from its resource's
contenir-metadata
MetadataInterface::getMetaModified(). Any other source is one small class
away.
Requirements
- PHP 8.3, 8.4 or 8.5, with the
xmlwriterextension - mezzio/mezzio 3.18+ and mezzio/mezzio-router 3.17+ with a router (for example mezzio/mezzio-fastroute)
- A PSR-17
ResponseFactoryInterfaceandStreamFactoryInterfacein the container (laminas-diactoros' ConfigProvider registers both) - contenir/contenir-workflow-mezzio 2.1+ and contenir/contenir-metadata 2.0+ (installed as dependencies; the workflow navigation is only needed if you keep the default source)
Installation
composer require contenir/contenir-sitemap-mezzio
With laminas-component-installer
the Contenir\Sitemap\Mezzio\ConfigProvider is added to
config/config.php for you. Otherwise add it yourself:
$aggregator = new ConfigAggregator([ // ... \Contenir\Workflow\ConfigProvider::class, \Contenir\Sitemap\Mezzio\ConfigProvider::class, // ... ]);
The provider registers the handlers, the default sitemap source and a
delegator on Mezzio\Application that routes GET /sitemap.xml (route
sitemap) and GET /robots.txt (route robots). There is nothing to add
to config/routes.php.
Usage
With contenir-workflow-mezzio
Point the workflow manager at the MetadataResourceStrategy so the sitemap
carries each page's modified date. Everything else is the workflow
configuration you already have:
// config/autoload/workflow.global.php use App\Repository\PageRepositoryAdapter; use Contenir\Sitemap\Mezzio\Strategy\MetadataResourceStrategy; use Contenir\Workflow\Factory\WorkflowApplicationDelegatorFactory; use Mezzio\Application; return [ 'workflow_manager' => [ 'strategy' => MetadataResourceStrategy::class, 'repository' => PageRepositoryAdapter::class, 'cache' => 'FilesystemCache', ], 'dependencies' => [ 'delegators' => [ Application::class => [WorkflowApplicationDelegatorFactory::class], ], ], ];
Your page entities implement both Contenir\Workflow\ResourceInterface and
Contenir\Metadata\MetadataInterface. The sitemap lists every visible page
that has a route, nested pages included, in navigation order:
<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>https://www.example.com/about</loc> <lastmod>2026-03-04T05:06:07+10:00</lastmod> <changefreq>monthly</changefreq> <priority>0.6</priority> </url> </urlset>
With workflow-mezzio's own ResourceStrategy the sitemap works the same,
without <lastmod>.
With your own source
Implement SitemapSourceInterface and alias it:
use Contenir\Sitemap\Mezzio\ChangeFrequency; use Contenir\Sitemap\Mezzio\SitemapEntry; use Contenir\Sitemap\Mezzio\SitemapSourceInterface; final readonly class ProductSitemapSource implements SitemapSourceInterface { public function __construct(private ProductRepository $products) {} public function getEntries(): iterable { foreach ($this->products->findPublished() as $product) { yield new SitemapEntry( "/products/{$product->slug}", // a path, or an absolute http(s) URL $product->updatedAt, // ?DateTimeInterface ChangeFrequency::Weekly, // ?ChangeFrequency 0.8, // ?float, 0.0 to 1.0 ); } } } // config/autoload/sitemap.global.php return [ 'dependencies' => [ 'aliases' => [SitemapSourceInterface::class => ProductSitemapSource::class], 'factories' => [ProductSitemapSource::class => ProductSitemapSourceFactory::class], ], ];
robots.txt
By default /robots.txt reproduces the laminas-mvc module's file:
# robots.txt for https://www.example.com/
User-agent: *
Allow: /
Disallow: /.well-known/
Sitemap: https://www.example.com/sitemap.xml
The rules are configurable; see below.
Configuration
Everything is optional and lives under the sitemap key:
// config/autoload/sitemap.global.php return [ 'sitemap' => [ // Fixed scheme and host (and optional base path) for every URL. // null (the default) takes them from the request. 'base_url' => 'https://www.example.com', // The ResourceStrategyInterface service the default source reads. // Defaults to workflow_manager.strategy, then ResourceStrategy. 'navigation' => null, // Route paths; false leaves the route out. 'routes' => [ 'sitemap' => '/sitemap.xml', 'robots' => '/robots.txt', ], // robots.txt groups, keyed by user agent. Replaces the default rules. 'robots' => [ 'rules' => [ '*' => ['allow' => ['/'], 'disallow' => ['/.well-known/', '/admin/']], 'Googlebot' => ['disallow' => ['/search/']], 'GPTBot' => ['disallow' => ['/']], ], ], ], ];
Invalid values fail when the service is built, with an
InvalidArgumentException naming the key. The
configuration reference lists every key.
Security
- Set
sitemap.base_urlin production. Without it, the URLs in both files are built from the request's scheme and Host header. A forged Host then lands in the response, and in any cache in front of it. With it, the response never depends on the request. - The base URL is validated: an absolute http(s) URL without credentials, query, fragment or whitespace.
- robots.txt values cannot add lines. User agents and paths that are
empty or contain a line break or other control character are rejected
when the handler is built, as are unknown directives (a typo such as
dissallowfails instead of silently allowing everything). - Sitemap locations are checked. A
SitemapEntrytakes only a path starting with/or an absolutehttp/httpsURL, so a source cannot emitjavascript:or protocol-relative locations. URLs are XML-escaped.
Public API
| Class | Purpose |
|---|---|
Handler\SitemapHandler |
PSR-15 handler: application/xml; charset=utf-8 sitemap from the SitemapSourceInterface |
Handler\RobotsHandler |
PSR-15 handler: text/plain; charset=utf-8 robots.txt from the rules, with the Sitemap line |
SitemapSourceInterface |
Supplies the sitemap entries; aliased to WorkflowNavigationSource |
SitemapEntry |
One <url>: location, last modified, change frequency, priority |
ChangeFrequency |
The protocol's <changefreq> values |
Source\WorkflowNavigationSource |
Entries from a contenir-workflow-mezzio navigation, URLs from the Mezzio router |
Strategy\MetadataResourceStrategy |
workflow-mezzio's default strategy, with lastmod from MetadataInterface::getMetaModified() |
Robots\RobotsRules, Robots\RobotsGroup |
The robots.txt groups, validated |
Http\BaseUrl |
The configured or request base URL |
ConfigProvider, Factory\* |
Container wiring, including Factory\SitemapRoutesDelegatorFactory |
Every concrete class is final; SitemapSourceInterface is the extension
point, and workflow-mezzio's AbstractResourceStrategy for strategies.
The docs folder covers each area:
Coming from contenir-sitemap-laminas-mvc
| laminas-mvc | Mezzio |
|---|---|
Module, routes sitemap and robots in router.routes |
ConfigProvider; the same route names, registered by SitemapRoutesDelegatorFactory. Paths in sitemap.routes |
SitemapController::indexAction() |
Handler\SitemapHandler |
SitemapController::robotsAction() |
Handler\RobotsHandler |
SitemapControllerFactory |
Factory\SitemapHandlerFactory, Factory\RobotsHandlerFactory |
Navigation container service cms (SitemapController::CONTAINER), fixed |
sitemap.navigation: any ResourceStrategyInterface service, by default workflow_manager.strategy; or your own SitemapSourceInterface |
laminas-view navigation()->sitemap() helper |
SitemapHandler writes the XML itself; no view layer needed |
serverUrl() helper for absolute URLs |
The request URI, or the fixed sitemap.base_url |
lastmod from the workflow strategy (getMetaModified()) |
Strategy\MetadataResourceStrategy |
| Fixed robots.txt rules | sitemap.robots.rules (the same rules by default) |
robots.txt comment from the home route |
# robots.txt for <base URL>/; no home route needed |
Content-Type application/xml / text/plain |
Unchanged, with charset=utf-8 |
See docs/migration.md for the step-by-step move.
Development
The QA toolchain is contenir/contenir-qa-tools.
Mago is a standalone binary, installed
separately (brew install mago).
composer check # everything below composer cs-check # mago format --check && mago lint composer static-analysis # mago analyze composer test # unit suite: collaborators doubled, no I/O composer test-integration # integration suite: real ServiceManager, Mezzio pipeline and FastRoute router composer test-coverage # both suites, clover.xml for Codecov composer mutation-test # Infection mutation testing over both suites (needs Xdebug or PCOV)
License
BSD-3-Clause. See LICENSE.md.