notACMS 1.1.2 — Canonical URLs and a structured-data builder
1.1.2 redirects default-locale-prefixed URLs to their canonical unprefixed form, replaces inline JSON-LD with a fluent Schema.org builder, and ships two security fixes.
One canonical URL per page
If your site's default locale is en, /en/blog/ and /blog/ were both reachable and rendered the same content. That's two indexable URLs for one page — and search engines pick whichever they like, which is rarely the one you want.
1.1.2 adds DefaultLocaleRedirectListener. Every request to /<default-locale>/... returns a 301 to the unprefixed URL:
GET /en/blog/ → 301 Location: /blog/
GET /en/about/?ref=x → 301 Location: /about/?ref=x
GET /pl/blog/ → 200 (non-default locale, untouched)
The listener runs before Symfony's router, so the redirect happens before the framework even tries to match a route — no wasted work, no 404 noise in logs. Non-default locales (pl, de, fr on this site) are left alone.
This also closes a small SEO leak: backlinks that accidentally include the locale prefix now consolidate into the canonical URL instead of splitting authority.
Schema.org JSON-LD as a fluent builder
Until 1.1.1, every template that needed structured data inlined a PHP array, piped it through |json_encode|raw, and wrapped the result in a <script> tag. Six templates, six near-identical blocks, and each one a fresh chance to forget JSON_UNESCAPED_SLASHES or to ship <script>false</script> if any frontmatter contained an invalid byte.
1.1.2 replaces all of that with a service and two Twig functions:
{% block structured_data %}{{ json_ld(structured_data().blogPosting(
content.title(),
site_base_url ~ content.url(),
content.date(),
content.htmlContent,
site_author.name,
content.image() ? site_base_url ~ content.image() : null
)) }}{% endblock %}
structured_data() returns the builder; chain a typed method (webSite, webPage, blogPosting, collectionPage, contactPage, person, breadcrumbList, organization, imageObject) and the result is a plain array. json_ld(array) encodes it and wraps it in a <script type="application/ld+json"> tag. Empty/null fields are stripped recursively, so optional inputs (an unset author email, a missing image) simply don't appear in the output.
Encoding now uses JSON_THROW_ON_ERROR — bad UTF-8 in frontmatter surfaces as a real exception during render instead of silently shipping a broken <script> tag.
The bare core templates for contact, default, and projects pages now emit Schema.org markup too. Previously bare deploys had weaker SEO than every customisation example we ship; that gap is gone.
See the Customisation guide → Structured data for the full builder API and how to override the structured_data block in your own theme.
Security fixes
Two bugs surfaced during the 1.1.2 review and were fixed before release:
- Open-redirect via path normalisation. Symfony's
Request::getPathInfo()does not collapse repeated slashes, so a request to/<default-locale>//evil.comwould otherwise produceLocation: //evil.com— a protocol-relative redirect that browsers follow cross-origin. The new redirect listener now refuses to issue redirects to anything that isn't a single-slash-rooted local path. - XSS in search results.
assets/search.jsinterpolated the Pagefindexcerptfield into innerHTML without escaping. The demo build was already correct; core had drifted. The two are now in sync. Pagefind's<mark>highlight tags are no longer rendered in core search results — a deliberate trade for the safer baseline.
Also in 1.1.2
- Internal Twig refactor. Sidebar, breadcrumb, post-badge, og-image, and blog-filter logic moved out of templates into dedicated Twig extensions. Templates stay focused on layout; tests can target each helper directly. New customisation surface: see Layout helpers in the customisation guide.
O(1)directory-key lookups onContentTree— a small performance fix on sites with hundreds of pages.docs/customization/old-template/is now deprecated and will be removed in 1.2.0. It was a one-shot 1.0→1.1 transition aid; new customisation work should branch fromcustom-footer/orself-hosted-fonts/instead.- Dependency bumps: Symfony 7.4.8 → 7.4.9 across the bundle, PHPStan 2.1.51 → 2.1.54, PHPUnit 13.1.7 → 13.1.8.
Full changelog
Every change with its category: CHANGELOG.md.