contenir / contenir-seo-mezzio
Mezzio adapter for contenir/contenir-seo: /sitemap.xml and /robots.txt from the site's settings, a head service and seo() view helper for titles, share defaults, verification and JSON-LD, and X-Robots-Tag on hidden copies.
Requires
- php: ~8.3.0 || ~8.4.0 || ~8.5.0
- contenir/contenir-asset-mezzio: ^2.3
- contenir/contenir-db-model: ^2.0.0-rc6
- contenir/contenir-preview: ^0.3
- contenir/contenir-preview-mezzio: ^0.1
- contenir/contenir-resource: ^2.0.0-RC6
- contenir/contenir-seo: ^0.9
- contenir/contenir-workflow-mezzio: ^2.1
- mezzio/mezzio-router: ^3.17
- psr/container: ^1.1 || ^2.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/http-server-handler: ^1.0.2
- psr/http-server-middleware: ^1.0.2
Requires (Dev)
- contenir/contenir-qa-tools: 0.1.x-dev
- infection/infection: ^0.34.1
- laminas/laminas-diactoros: ^3.3
- laminas/laminas-servicemanager: ^3.22 || ^4.0
- laminas/laminas-view: ^2.35
- php-db/phpdb-sqlite: ^0.2.0 || 0.3.x-dev
- phpunit/phpunit: ^11.5.42
Suggests
None
Provides
None
Conflicts
Replaces
None
README
The Mezzio adapter for contenir/contenir-seo. It serves a site's
/sitemap.xml and /robots.txt, gives templates the head's <title>, share defaults, verification tags and JSON-LD,
and hides staging and local copies from search engines.
The settings are set in the CMS, by the SEO add-on. It is the Mezzio counterpart of contenir/contenir-seo-laminas-mvc, and replaces contenir/contenir-sitemap-mezzio.
Requirements
- PHP 8.3, 8.4 or 8.5
- Mezzio with mezzio-router 3.17+ and contenir/contenir-workflow-mezzio 2.1+
- PSR-17 response and stream factories in the container (laminas-diactoros's
ConfigProviderregisters both)
Install
composer require contenir/contenir-seo-mezzio
laminas-component-installer adds Contenir\Seo\Mezzio\ConfigProvider to config/config.php. Then pipe the middleware
early in config/pipeline.php, before routing and before anything that answers requests itself, such as a page cache:
$app->pipe(ErrorHandler::class); $app->pipe(\Contenir\Seo\Mezzio\SeoMiddleware::class); $app->pipe(PageCacheMiddleware::class); $app->pipe(RouteMiddleware::class);
Piping it is what turns the package on. SeoMiddleware:
- answers
GETandHEAD/sitemap.xml,/sitemap-1.xml,/sitemap-2.xml… and/robots.txtitself, so no routes are needed, whichever router the site uses. A disabled sitemap, or a numbered page the sitemap does not have, goes on down the pipeline to be not found; - sends
X-Robots-Tag: noindex, nofollowon every response of a copy of the site hidden from search engines; - records the request, so the head knows the page's host and path.
Settings
A Mezzio site's settings are its top-level config, where the CMS's config/autoload/settings.global.php merges,
with config['settings'] laid over it, where the CMS writes settings.local.php (SiteSettings::fromConfig()).
The SEO add-on's settings are read only when the add-on is enabled in config/autoload/extensions.global.php or
extensions.local.php (a list in the local file replaces the global one, as the CMS reads it). Until then, only
site.url, site.title and site.sitemap.enabled are read, so a site keeps a plain sitemap and robots.txt and
nothing is hidden. The keys are listed in contenir/contenir-seo's README.
Links are built on the site URL (site.url). Only without one do they fall back to the request's scheme and Host
header, which a client can spoof: set the site URL on every live site.
The head
SeoHead, which the seo() view helper returns, builds the site's part of a page's <head>, and the jsonLd()
helper gathers the page's structured data, as headMeta() gathers its tags:
<?php use Contenir\Resource\Core\Metadata\PageMetadata; use Contenir\Seo\Head\Article; $article = PageMetadata::ARTICLE === $page?->type && null !== $page->title ? new Article($page->url, $page->title, $page->description, $page->image, $page->published, $page->updated) : null; ?> <head> <meta charset="utf-8"> <?= $this->seo()->render( title: $page?->title, hasDescription: null !== $page?->description, hasImage: null !== $page?->image, article: $article, ) ?> <?= $pageTags /* the page's own description, canonical, Open Graph and Twitter tags */ ?> <?= $this->jsonLd()->script($nonce) ?>
render() returns, escaped:
- the
<title>from the site's template ("About us | Example"): don't write another; og:site_name, a robots meta tag when the page shouldn't be indexed, and on the home page the verification tags for Google, Bing, Pinterest and Meta;- the site's default description, and its default share image with its alt text and size, for a page that has none of
its own (say so with
hasDescriptionandhasImage).
It adds the page's JSON-LD to jsonLd()'s graph: on the home page, the Organization and WebSite; elsewhere, an
Article for a page that is one (pass article) and the BreadcrumbList from the page's path. A template appends its
own nodes the same way, $this->jsonLd()->append([...]), and the layout writes them all once, in one script element
with $nonce when given. Write it after render() and anything else that appends.
build() returns the same as contenir-seo's Head, for a template that writes its own markup. With
contenir-resource-mezzio, the page's own tags come from
PageMetadata (ResourceAttribute::metadata($request)) and MetaTagRenderer::renderTags(), which leaves the
<title> to this package.
The home page is the page at a home path, / unless contenir_seo.home_paths says otherwise. Template engines other
than laminas-view take Contenir\Seo\Mezzio\SeoHead from the container.
The head is per request, but the container's services are shared: the request is held by CurrentRequest, and the
structured data by the Graph service, so the container must serve one request, as under PHP-FPM, not a
long-running server.
The sitemap
The sitemap lists every page of the contenir-workflow-mezzio navigation that has a route, including pages hidden from
the menu, with each page's lastmod, and then the pages of each SitemapSourceInterface service named in
contenir_seo.sitemap_sources, such as a site's articles, which are not in the page tree:
final class Projects implements Contenir\Seo\Mezzio\SitemapSourceInterface { public function pages(): iterable { foreach ($this->content->projects() as $project) { yield new Contenir\Seo\Sitemap\Page("/work/{$project->getSlug()}", $project->updated ?? $project->created); } } }
Pages on another host, fragments and pages whose robots value is noindex are left out, each location is listed
once, and past 50,000 URLs the sitemap becomes an index of numbered pages.
robots.txt
# robots.txt for https://www.example.com/
User-agent: *
Allow: /
Disallow: /search
Sitemap: https://www.example.com/sitemap.xml
The middle block is the extra rules from the SEO add-on. A copy of the site hidden from search engines gets
Disallow: / instead, and no sitemap line.
Configuration
Nothing is needed. To change the defaults:
return [ 'contenir_seo' => [ 'extensions_config' => 'config/autoload', // where extensions.global.php and .local.php are 'public_path' => 'public', // the document root, to measure the default share image 'home_paths' => ['/'], // the home page's paths 'sitemap_navigation' => true, // list the workflow navigation's pages 'sitemap_images' => true, // list each page's share image, as its head gives it 'sitemap_sources' => [], // services adding pages to the sitemap ], ];
The workflow navigation comes from the strategy workflow_manager.strategy names, as contenir-workflow-mezzio reads it.
Where the site has contenir-resource's ResourceManagerInterface, each page lists its record's share image: the one
PageMetadataBuilder gives the page's head, so the sitemap and the page never disagree.
Differences from the laminas-mvc adapter
| laminas-mvc | Mezzio |
|---|---|
| Routes and a controller for the sitemap and robots.txt | SeoMiddleware answers them, so no router syntax is involved |
seo()->head() writes into headTitle() and headMeta() |
seo()->render() returns the markup, and the template says whether the page has a description and image |
The home page is the home route |
The home page is at a home path (/) |
Settings in config['settings'] |
Settings at the top level, with config['settings'] laid over them |
An MVC finish listener sends X-Robots-Tag |
SeoMiddleware sends it |
Development
The QA toolchain is contenir/contenir-qa-tools.
composer check # everything below composer cs-check # mago format --check && mago lint composer static-analysis # mago analyze composer test # unit suite composer test-integration # integration suite: a real container, laminas-view and files composer mutation-test # Infection
Licence
BSD-3-Clause. See LICENSE.md.