66 min readAug 10, 2026by jakub

Changelog

Current version: 2.1.2

This timeline merges the release history of every module in the suite. Each version heading lists the modules that shipped that version, with their own release notes.

2.4.2 — 2026-07-13

SeoRichSnippets

Fixed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass. Removed deprecated Reflection::setAccessible() calls (no effect since PHP 8.1, deprecated in 8.5).
  • Editing a child snippet now invalidates the cached parent JSON-LD. Parent snippets render nested fragments via {{snippet.identifier}}, but the page cache only carried the page-matching parent's tag, and saving a child fragment (e.g. an Offer) cleaned only its own — unused — tag. The already-cached Product JSON-LD therefore stayed stale. A new Model\SnippetCacheTags service now (a) adds every nested child's tag to the block's render identities (Block\JsonLd::getIdentities()), and (b) on repository save/delete cleans the saved snippet's tag plus the tags of every ancestor snippet that embeds it (Model\SnippetRepository). Both walks are depth-guarded (max 5, mirroring SnippetExpander) and cycle-safe. The qoliber_richsnippets_<id> tag scheme is unchanged, so existing cache entries invalidate as before.
  • Custom URL wildcard patterns now quote regex metacharacters. Block\JsonLd::matchesUrlPattern() builds a regex from a snippet's admin-configured custom_url wildcard pattern, but only escaped / and expanded * — any other regex metacharacter in the pattern (., +, (, etc.) was interpolated raw. A literal . silently matched any character, a literal + either false-matched or (compared literally against itself) failed to match at all, and an unbalanced ( broke regex compilation outright and always failed the match. Each literal segment between * wildcards is now passed through preg_quote(..., '/') before being joined back with .*, so * remains the only special token and every other character in the pattern is matched literally.
  • PHP 8.4 setup:upgrade failure (implicitly-nullable parameter). Model\ResourceModel\Snippet\Grid\Collection::setItems() declared array $items = null — an implicitly-nullable parameter that PHP 8.4 deprecates (E_DEPRECATED at compile time), failing setup:upgrade on 8.4. It is now explicitly typed ?array. (gitlab qoliber/seo-rich-snippets#2)

Changed

  • Declared the direct qoliber/core (^1.0) dependency — the admin menu/ACL are defined under Qoliber_Core resources; previously only the root metapackage provided it.

2.4.1 — 2026-07-13

SeoRichSnippets

Changed

  • Declared direct Magento module dependencies with bounded version constraints.
  • Standardised composer metadata (license and magento/framework version constraint).
  • PSR-12 code-style cleanup; no functional change.

2.4.0 — 2026-07-03

SeoRichSnippets

Added

  • Category variables for product-listing / category pages — new CategoryValueResolver exposes {{category.*}} in snippet templates: id, name, url, urlKey, image, description (HTML-stripped), metaTitle, metaDescription, productCount, plus a camelCase→snake_case fallback for any other category attribute. The category is read as-is from the request context, falling back to the current_category registry entry Magento sets on category pages — the same self-contained pattern as StoreValueResolver — so {{category.name}} resolves on category pages with no block wiring. Registered under VariableExtractor with prefix category.
  • CategoryVariables provider — a first-class variable provider registered in both the admin schema-builder pool (SchemaVariables) and the JsonLd render pool. Category variables now appear in the snippet-builder's variable picker and populate at render time. Implements KeyedVariableProviderInterface (getProvidedKeys() returns ['category']), sort order 12.
  • Dedicated brown color for the category variable group in the admin variables panel (.category block in the schema-editor styles).

Tests

  • Test/Integration/Model/ValueResolver/CategoryValueResolverTest (14 cases) and Test/Integration/Model/VariableProvider/CategoryVariablesTest (8 cases), plus the category_simple fixture and rollback. Suite total: 212 tests, 863 assertions.

2.3.0 — 2026-04-20

SeoRichSnippets

First release cutting from 2.2.1. Combines correctness fixes, a breaking-default change, a performance rework of the variable-provider pipeline, and Google's 2025 Merchant Listings compliance additions.

Added

  • Google 2025 Merchant Listings compliance for MerchantReturnPolicy — the default merchant-return-policy snippet now emits applicableCountry (ISO 3166-1 alpha-2, sourced from {{config.general.country.default}} so each store's configured country flows through automatically) and itemCondition (defaults to https://schema.org/NewCondition). applicableCountry is required by Google as of 2025 — without it the whole return policy is dropped from Product rich results, not just the missing field.
  • New data patch UpdateMerchantReturnPolicy2025Fields rewrites the existing snippet on upgrade. Follows the same overwrite-unconditionally pattern as UpdateOfferStockAvailability and UpdateOfferAggregateOffer. Since the snippet ships inactive by default (see Changed below), merchants who activated + customised it locally will see their edits clobbered on upgrade — consistent with how the suite handles default-template migrations.
  • InstallTier1Snippets updated so fresh installs get the 2025-compliant snippet directly.
  • Qoliber\SeoRichSnippets\Api\KeyedVariableProviderInterface with getProvidedKeys(): array — variable providers now declare the top-level template keys they populate so the render pipeline can skip providers the active template doesn't reference.

Changed

  • BREAKING: Default snippets install as INACTIVE — all 27 snippets across the 5 InstallTier* data patches now call setActive(0). Previously these shipped as active on a fresh install and emitted hardcoded claims like "free shipping", "US destination", and "30-day returns" as structured data — which constituted false structured-data representations for most merchants. Merchants must now explicitly activate each snippet after reviewing its content. Existing installs are unaffected — only fresh setup:upgrade runs see the new default.

Fixed

  • Multistore cache poisoninggetCacheKeyInfo() now includes store_id so stores sharing a URL don't share the same JSON-LD cache entry.
  • Enabled flag honoredJsonLd block now checks qoliber_seo_rich_snippets/settings/enabled at runtime; layout also gates the block via ifconfig, so the block is never instantiated when disabled. Merchants can now actually turn JSON-LD output off.
  • Circular dependency validator wired to Save controller — the existing CircularDependencyValidator is now invoked before persisting a snippet edit. Admin can no longer save a snippet that includes itself or creates a dependency loop.

Performance

  • JsonLd block skips unused variable providers. Previously getProviderContext() invoked every registered VariableProviderInterface per snippet render, regardless of whether the snippet template referenced its output. On product pages that meant BreadcrumbsVariableProvider rendered the breadcrumbs block (iterating the product's category path) and CollectionVariables loaded the reviews collection with rate votes — even when no active snippet used {{breadcrumbs}}, {{productReviews}}, etc. ProductVariables also redundantly enumerated every product attribute and re-queried reviews already covered by ProductValueResolver.
  • The block now extracts the keys referenced by the active snippet template (via {{var}}, @djson if/unless/exists, and the newly-captured @djson for X as Y) and skips any provider whose declared keys aren't used. Third-party providers that don't adopt KeyedVariableProviderInterface still run unconditionally — no breakage.
  • providerOutputCache memoizes each provider's renderVariables() output per block instance instead of the previous merged cache, so multiple snippets on the same page share results without cross-contamination when they need different key subsets.

Tests

  • Test/Integration/Setup/Patch/Data/UpdateMerchantReturnPolicy2025FieldsTest — 5 cases: applicableCountry wired to the Magento config placeholder, itemCondition default, existing policy fields preserved, resulting template is still valid JSON, patch is idempotent on re-run.
  • Test/Integration/Block/JsonLdProviderFilteringTest and updates to VariableExtractorTest covering the provider-skip invariant and the new @djson for X as Y extraction pattern.

2.2.4 — 2026-08-06

SeoImagesFriendlyUrl

Fixed — 2026-08-06
  • Magento 2.4.9 setup:di:compile fatal (core signature change) — Magento 2.4.9 widened core Magento\MediaStorage\Service\ImageResize::resizeFromImageName() from (string $originalImageName) to (string $originalImageName, bool $skipHiddenImages = false), extending its existing $skipHiddenImages website-filter optimisation into this method. Our <preference> override still declared the one-arg signature, so on 2.4.9 PHP raised "Declaration of Qoliber\SeoImagesFriendlyUrl\Service\ImageResize::resizeFromImageName(string): void must be compatible with Magento\MediaStorage\Service\ImageResize::resizeFromImageName(string, bool $skipHiddenImages = false)" and bin/magento setup:di:compile fatalled. The override now declares the matching resizeFromImageName(string $originalImageName, bool $skipHiddenImages = false): void and forwards $skipHiddenImages to parent:: in the module-disabled passthrough. Because the added parameter is optional, the override stays signature-compatible with pre-2.4.9 core as well (verified by linking against the real 2.4.8 core) — no minimum-Magento bump. The enabled SEO resize path is unchanged: it processes every view image exactly as before and does not apply the website filter, so no image output regresses.

2.2.3 — 2026-07-18

SeoImagesFriendlyUrl

Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass. Removed deprecated Reflection::setAccessible() calls (no effect since PHP 8.1, deprecated in 8.5).

Fixed — 2026-07-18
  • Frontend gallery now honours the module-enabled flag (MF-3) — two frontend spots still mutated the product gallery even when the module was disabled, so a merchant who turned the module off did not fall back to vanilla Magento output. Both are now gated on Config::isModuleEnabled():
    • ViewModel\Product\View\Gallery::getImageAltTag() unconditionally ran the SEO alt-tag resolver. It now short-circuits when the module is disabled and returns the passed-in value untouched (the resolver's getSeoFriendlyValue() — unlike resolveAltTagSeoValue() — was never gated, so the alt tag was rewritten regardless of the flag). A new isModuleEnabled() accessor exposes the flag to the template.
    • view/frontend/templates/product/view/gallery.phtml called the SEO view model unconditionally. When the module is disabled it now emits Magento's default alt="main product photo" and omits the SEO title attribute, producing output byte-for-byte identical to the core Magento_Catalog::product/view/gallery.phtml template (the JSON-init <script> closing braces were also re-indented to match core; functionally unchanged). Enabled behaviour is unchanged.
  • TestsTest/Unit/ViewModel/Product/View/GalleryTest (4 cases: resolver runs when enabled, input returned untouched when disabled, empty input stays empty when disabled, isModuleEnabled() delegation) and Test/Integration/Block/Product/View/GalleryFallbackTest (renders the gallery block with the module disabled and asserts the output matches the vanilla core template).

2.2.2 — 2026-07-14

SeoImagesFriendlyUrl

Fixed — 2026-07-13
  • Scheduled index stays fresh on name/SKU edits (indexer) — the SEO image filename is derived from product attributes (name, SKU, ...), but the mview only subscribed to media-gallery changes, so editing a product name, SKU or the configured filename attribute never reindexed existing image names. The mview now subscribes to catalog_product_entity (SKU + static columns), catalog_product_entity_varchar (name and custom varchar filename attributes) and catalog_product_entity_media_gallery_value (new/removed images), all mapped to the product entity id. Partial reindex is now keyed on the product entity link field instead of the media value_id, so those edits trigger a reindex of the affected products' image names.
  • Deterministic SEO name under multi-store EAV (indexer)loadProductDataBySql() joined the EAV value tables without a store scope, so multiple localized values multiplied and an arbitrary store's value could be written as the store_id = 0 SEO name. The value joins are now constrained to the default/admin scope (store_id = 0) — the same scope the index is written at — yielding exactly one deterministic value per product regardless of how many store-specific values exist.
  • Complete image-index invalidation across all reachable EAV value tables (mview) — round-1 only subscribed the mview to catalog_product_entity, catalog_product_entity_varchar and the media-gallery value table, so editing a select attribute (e.g. color, whose value lives in catalog_product_entity_int) or a multiselect attribute (catalog_product_entity_text) never reindexed the affected product's image names. The mview now subscribes to every EAV backend value table the filename/alt template can read: catalog_product_entity_int (select/boolean) and catalog_product_entity_text (multiselect) are added, keyed by the product entity id. decimal/datetime value tables are deliberately not subscribed — the available-attributes source model only offers select/multiselect/text frontend inputs (→ int/text/varchar backend types), so they are unreachable.
  • Reindex when the filename template or available-attributes config changes — the generated names depend on image_names_template and available_attributes, but changing them left the scheduled index stale until the next unrelated edit. A new Model\Config\Backend\InvalidateSeoImageIndex backend model on those two config fields marks the indexer invalid (via IndexerRegistry) whenever the saved value actually changed, so the next reindex regenerates all names.
  • Obsolete index rows removed when images are deleted (partial reindex) — the reindex only upserted the product's current images, so an image the admin deleted kept a stale index row forever. Partial reindex now also prunes index rows whose image no longer exists in catalog_product_entity_media_gallery (a deleteFromSelect anti-join), reaping the rows for deleted images regardless of store scope.
  • Atomic full reindex (no more empty index on mid-run failure)executeFull() used to truncateTable() up-front and then repopulate, so any failure part-way through left the live index empty or partial. It now builds the index in a replica table, populates it fully, and swaps it into place atomically (ActiveTableSwitcherRENAME TABLE) only on success; on failure the half-built replica is dropped and the live index is untouched. Partial reindex behaviour is unchanged (it still writes to the live table).
Dependencies — 2026-07-13
  • Declared the direct qoliber/core (^1.0) dependency — the admin menu/ACL are defined under Qoliber_Core resources, so a standalone package needs it explicitly (previously only the root metapackage provided it).

SeoPrettyFilters

Changed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass. Removed deprecated Reflection::setAccessible() calls (no effect since PHP 8.1, deprecated in 8.5).
  • Declared the direct qoliber/core (^1.0) dependency. The admin menu/ACL hang off Qoliber_Core resources, so a standalone package requires it explicitly; previously only the root metapackage provided it.

2.2.1 — 2026-07-11

SeoImagesFriendlyUrl

  • Dependencies — declared direct Magento module dependencies with bounded version constraints.
  • Maintenance — standardised composer metadata (license, magento/framework constraint); PSR-12 code-style cleanup (no functional change).

SeoPrettyFilters

Fixed

  • Category pretty URLs 404 (critical). CategoryPathResolver bound an array [storeId, 0] to a single named store_id placeholder; PDO does not expand array binds, so the lookup matched no category. Now expands the values into the SQL via store_id IN (?).
  • Pagination / sort / limit links pointed back at the current page. The ProductListToolbar pager plugin ignored the target $params it was handed; those now override the current request's values on the generated pretty URL. Implemented via an optional override argument on the concrete RequestParamsProvider — the @api RequestParamsProviderInterface is unchanged.
  • Complex-mode routing accepted invalid, non-opted-in, and arbitrary segments. ComplexAttributeResolver accepted unknown attribute codes as raw params, resolved invalid option values to id 0, and consumed any plain segment without an attribute:value form (so /category/anything.html resolved) — all creating arbitrary duplicate category URLs. Unknown codes, invalid values, and plain segments are now left unconsumed so the router rejects the route.
  • Flat-mode URLs could be generated but not resolved. Generation ordered segments by layered-navigation position while parsing enforced flat_filter_order; a divergent order, an empty order, or a pretty attribute omitted from the order produced unresolvable URLs. Generation now orders by flat_filter_order; the resolver appends any omitted pretty attributes in position order; and both fall back to position order when no order is configured.
  • Boolean filters ignored the is_pretty_filter flag and matched substrings. BooleanAttributeResolver parsed every filterable boolean attribute (regardless of the flag) and matched codes as substrings; it now respects is_pretty_filter and matches whole tokens only.

Removed

  • Dead rel attribute admin config. The broken rel admin field (a smallint column that stored nofollow/noindex strings and was never read) and its source model have been removed. The underlying column is retained so the 2.x upgrade performs no destructive schema change; it is simply no longer written or read, and is slated for removal in 3.0.

Dependencies

  • Declared direct Magento module dependencies with bounded version constraints.

SeoPrettyFiltersElasticSuite

Fixed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass.
  • Complex-mode routing accepted invalid, non-opted-in, and arbitrary segments. The ComplexAttributeResolver plugin now rejects unknown attribute codes, invalid option values, and plain segments without an attribute:value form (mirrors the base module), and drops bogus values from multiselect pairs while keeping the valid ones.
  • Flat-mode URLs could be generated but not resolved. The FlatAttributeResolver and FlatPatternProcessor plugins now order segments by flat_filter_order, append pretty attributes omitted from the order in position order, and fall back to position order when none is configured — so generated flat URLs round-trip (mirrors the base module).

Dependencies

  • Declared direct Magento module dependencies with bounded version constraints.

SeoRichSnippets

Fixed

  • JsonLd block cache key — added product ID, category ID, and request URI to getCacheKeyInfo() to prevent different product/category pages from sharing the same cached schema output
  • JsonLd block cache invalidation — added product and category cache tags to getIdentities() so full-page cache invalidates correctly when product/category data changes

2.2.0 — 2026-07-13

SeoAdvisorAi

Added

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass. Removed deprecated Reflection::setAccessible() calls (no effect since PHP 8.1, deprecated in 8.5).
  • New Service\GenerativeAi\VerifiableClientInterface (extends ClientInterface) declaring verifyConnection(). The four shipped services implement it and Verify\Models checks instanceof — a non-verifiable provider gets a clean "This provider does not support verification." response. Backward-compatible: ClientInterface keeps its original method set, so a third-party provider implementing only the base interface still loads.
  • Claude and xAI Grok are now usable in the "Check SEO" workflow — added their Verify connection admin fields (system.xml), taught the verify block to resolve all four provider codes, and drove verification through the DI provider registry so every configured provider can reach verified status.
  • Credential-change verification invalidation — the four API-key fields use a dedicated encrypted backend model (Model\Config\Backend\EncryptedApiKey) that clears a provider's verification status, error, stored model list and provider model cache when — and only when — the decrypted credential actually changes on save (re-saving the obscured placeholder or the identical key is a no-op).
  • Strict AI response schema — the OpenAI-compatible providers (ChatGPT, Grok) now request native JSON output (response_format: {"type":"json_object"}), joining Gemini and Claude, and every response is validated by a new Service\ResponseSchemaValidator (size-capped before decode; must be a JSON object with exactly meta_title, meta_description, meta_keywords as strings; missing/mistyped/extra fields rejected).

Fixed

  • Invalid credentials could be marked "verified" — verification previously accepted any returned model list, including the static fallback and globally cached lists independent of the API key. verifyConnection() now performs a fresh, cache-bypassing authenticated model-list call and never substitutes the fallback list, so only a genuine API success counts; on a missing key or API failure it throws and the controller records an error status.
  • Fragile structured responsesAdvise\Popup strips markdown code fences before json_decode; reconciled the meta-keywords field name (meta_keywordmeta_keywords) across the default prompt, JS and controller whitelist; corrected the check-seo.js description / short-description selectors to include Magento's product[...] field-name prefix.

SeoImagesFriendlyUrl

  • {category.name} template variable — new CategoryResolver exposes {category.name} in the SEO image-name and alt-tag templates, resolving to the current category name from Magento's current_category registry. It is populated only on category (PLP) pages; when the registry is empty (product pages, indexing) nothing is loaded, so there is no performance cost. Registered on TemplateProcessor alongside the product and store resolvers. Because image filenames are generated at index time (no registry context), {category.name} is intended for the alt-tag template, where it reflects the browsing category on listing pages.
  • TestsTest/Unit/Resolver/CategoryResolverTest (5 cases: registry hit, empty registry, non-category value, id-less category, passed-model ignored) and Test/Integration/Resolver/CategoryTemplateResolutionTest (3 cases: end-to-end {category.name} resolution, empty when no current category, dangling-separator trimming).

SeoIndexNow

Added

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass.
  • IndexNow ownership verification now works — the key file resolves at the site root (/{key}.txt) via a dedicated Controller\Router registered ahead of the standard/CMS routers (previously only reachable through the Magento frontName route, which search engines cannot use, leaving ownership unverifiable). Submissions also carry keyLocation (the public key-file URL) so engines can locate the key when verifying.
  • Store-aware canonical URL resolution — product, category and CMS notifications resolve the canonical URL from Magento's own url_rewrite table (storeBaseUrl + request_path) through a single Model\CanonicalUrlResolver (backed by a pure, unit-tested Model\CanonicalRewriteSelector), so localized per-store url keys, custom rewrites and non-default store paths produce the correct URL. Only the ROOT canonical is submitted (no /category/{id}-scoped duplicates), chosen deterministically; a suffix-aware build is the fallback when no rewrite row exists.

Fixed

  • Product notifications now include the per-store product URL suffix (catalog/seo/product_url_suffix, e.g. .html) instead of baseUrl + url_key, which produced a non-canonical/nonexistent URL.
  • CMS pages assigned to "All Store Views" (store id 0) are now expanded to every active store view and notified per store, instead of being skipped.
  • Category saves no longer submit an admin-scope (store 0) URL. Category::getStoreIds() appends store 0 alongside the concrete stores; StoreExpander::excludeAdminStore() now drops it.

Changed

  • Declared the direct dependency on Magento_UrlRewrite (magento/module-url-rewrite) in composer.json and module.xml.

SeoPrettyFilters

Changed

  • Selective SEO-friendly filters (opt-out, backward-compatible): is_pretty_filter now governs which attributes produce SEO-friendly path segments. Previously the flag existed in the schema but was never read, so every filterable attribute produced pretty path segments unconditionally. The flag is now honoured, but all filterable attributes remain SEO-friendly by default (is_pretty_filter defaults to 1), so existing storefronts are unchanged. Set Use for Pretty Filter → No on an attribute to keep it as a standard ?attr=value query parameter instead. (Opt-in-by-default — is_pretty_filter defaulting to 0 — is deferred to 3.0 to avoid a backward-incompatible change in 2.x.)

Added

  • PrettyFilterChecker (Api/Service/PrettyFilterCheckerInterface) — the single source of truth for whether an attribute produces pretty URLs (is_pretty_filter = 1), used at every build/parse seam.
  • Pretty and non-pretty filters now coexist. A pretty filter link preserves the shopper's active non-pretty filters as query params (RequestParamsProvider), and a non-pretty filter link falls through to core (keeping the pretty path via _current=true). Verified across add / select / remove / clear / multiselect and hybrid /(brand-1).html?color=5 routing.

Fixed

  • getActiveFilters no longer fatals on an active filter whose frontend input has no converter (guarded array access).
  • RequestParamsProvider::addToUrl no longer emits a doubled ? when the URL already carries a query string.
  • Parse resolvers only match pretty attributes' seo paths (option universe filtered in AttributeFetcher), reducing the substring collisions behind intermittent 404s when combining filter values.

Tests

  • ~22 new unit + integration tests: PrettyFilterChecker, the getActiveFilters guard, the filter-item + swatch plugins, RequestParamsProvider preservation, the clear-all lock, AttributeFetcher pretty-only filtering, and the combine + hybrid-route behaviour matrix.

SeoPrettyFiltersElasticSuite

Added

  • The AttributeFetcher plugin now applies the same is_pretty_filter selectivity as the base module, so attributes flagged Use for Pretty Filter → No are excluded from the SEO path universe for ElasticSuite stores too (the plugin reimplements transformArrayForFilters, so it needs the filter independently). Backward-compatible: all filterable attributes remain SEO-friendly by default. All build-side selective behaviour is inherited from the base module via the pattern-processor delegation to generateAttributeArray.

Changed

  • Requires qoliber/seo-pretty-filters: ^2.2 (uses the new PrettyFilterCheckerInterface).

SeoRichSnippets

Added

  • AggregateOffer support for complex product types — configurable, bundle, and grouped products now render AggregateOffer schema with lowPrice, highPrice, and offerCount instead of a simple Offer
  • Product type detection resolver paths: product.typeId, product.isSimple, product.isConfigurable, product.isBundle, product.isGrouped, product.isComplexProduct
  • Offer pricing resolver paths: product.offer.lowPrice, product.offer.highPrice, product.offer.offerCount — returns null for simple/virtual products
  • Public isComplexProduct() method on ProductValueResolver for plugin extensibility
  • UpdateOfferAggregateOffer data patch — updates the offer snippet template with @djson if/else conditional rendering based on product type
  • Test fixtures for configurable (3 children at $19.99/$24.99/$29.99), bundle (2 options, 4 selections), and grouped (3 children at $12.99/$17.50/$22.00) products
  • 12 new integration tests across ProductValueResolverTest, DjsonTemplateResolutionTest, and FullJsonOutputTest (total: 172 tests, 789 assertions)

2.1.3 — 2026-07-18

SeoDynamicTags

Fixed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass. Removed deprecated Reflection::setAccessible() calls (no effect since PHP 8.1, deprecated in 8.5).
  • Robots directive whitelist too strict (MF-1)ModifyMetaTags::sanitizeRobots() only accepted the exact single-space canonical strings (INDEX, FOLLOW, ...), so admin-entered values with different whitespacing, casing, or token order (INDEX,FOLLOW, index, follow, INDEX, FOLLOW , FOLLOW, INDEX) normalised to something outside the whitelist and were silently dropped, even though Google treats these as equivalent. The normalisation now tokenises on comma, trims/upper-cases each token, and reconstructs the canonical "{INDEX|NOINDEX}, {FOLLOW|NOFOLLOW}" form regardless of token order, while still rejecting unknown directives, a missing half, duplicate directives, or extra tokens (whitelist security intent unchanged).

Tests

  • Added Test/Unit/Plugin/App/ModifyMetaTagsRobotsTest locking the fixed normalisation behaviour (accepts the whitespace/case/order variants, rejects unknown/duplicate/partial input).
  • Extended Test/Integration/Plugin/App/MetaTagSanitizationTest::robotsInputProvider() with the same variants.

2.1.2 — 2026-07-13

SeoAdvancedSitemaps

Fixed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass.
  • External video players emitted as media filesVideoSitemapGenerator rendered every product video URL inside <video:content_loc>, which Google reserves for actual video-file bytes. Embedded/external player URLs (YouTube, Vimeo, Dailymotion, Wistia, …) are now classified and emitted as <video:player_loc>; only recognised direct video-file URLs use <video:content_loc>.
  • Store base path dropped from child sitemap URLs — child sitemap URLs in the index rebuilt only scheme/host/port and discarded any store base path (e.g. /shop/), pointing crawlers at 404s. The base path is now preserved.
  • Stale sitemap chunk files never removed — after catalogue shrinkage or a gzip-mode change (.xml.xml.gz), obsolete child chunks lingered in the sitemap directory. Orphaned chunks for the active generator prefixes are now deleted after each generation run.
  • Sitemap-index lastmod not W3C Datetime — the index <lastmod> used Y-m-d H:i:s instead of the W3C Datetime required by the sitemaps protocol; it now emits Y-m-d\TH:i:sP (e.g. 2026-07-13T08:09:10+00:00).
  • Video sitemap: default (store 0) metadata could shadow the store-view value — when a product video carried ProductVideo metadata at both the default (store 0) and the store-view scope, both rows were emitted. The store-specific row is now preferred at the sitemap's store scope, falling back to store 0 only when no override exists.
  • Video sitemap: category-scoped product URL could be chosen as <loc> — the url_rewrite join accepted any non-redirect rewrite, so a category-scoped rewrite (…/category/{id} in target_path) could be emitted instead of the product's root canonical URL. The query now excludes category-scoped rewrites and the root canonical request_path is chosen deterministically.
  • Video sitemap: duplicate <video:video> entries per product — a product with the same video registered under several gallery entries (or spanning store scopes) produced repeated <video:video> blocks. Videos are now de-duplicated per product by gallery entry and resolved video URL.

Changed

  • Declared the direct dependency on magento/module-product-video (^100.4); the video sitemap generator reads ProductVideo gallery metadata.
  • Declared the direct dependency on qoliber/seo-href-lang (^2.1); HreflangSitemapGenerator constructor-injects Qoliber\SeoHrefLang\Model\Config.
  • Declared the direct dependency on qoliber/core (^1.0); the admin menu/ACL are defined under Qoliber_Core resources, so a standalone package needs it explicitly (previously only the root metapackage provided it).

SeoCommon

Fixed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass.
  • Service/Html/Stripper::toPlainText() decoded HTML entities after stripping tags, so encoded markup such as &lt;img src=x onerror=alert(1)&gt; passed through strip_tags() unchanged (no literal tag present) and was only turned into a live <img> tag afterwards by html_entity_decode(). Entities are now decoded first — repeating the decode pass until the value stabilises, to also neutralise double-encoded markup like &amp;lt;script&amp;gt; — and tags are stripped from the fully decoded value, so encoded markup is neutralised instead of being recreated.

SeoDynamicTags

Changed

  • Declared the direct qoliber/core (^1.0) dependency. The admin menu/ACL are defined under Qoliber_Core resources, so a standalone package requires it explicitly; previously only the root metapackage provided it.

SeoFriendlyProductUrls

Fixed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass. Removed deprecated Reflection::setAccessible() calls (no effect since PHP 8.1, deprecated in 8.5).
  • Batch collision resolution now checks suffixed candidates — during batch regeneration the dedup query only loaded the base request paths, so when a collision forced a -1/-2/… suffix the resolver never verified that suffixed path was free. It could select an already-occupied rewrite (e.g. a foreign cms_page on red-1.html) and collide or overwrite it via insertOnDuplicate on persistence. Collision resolution now runs through a dedicated Service\CollisionResolver that checks every candidate — base and suffixed — against an in-memory taken-set seeded from all existing rewrites (base and suffixed variants) and grown as each choice is reserved, guaranteeing the chosen path is free before persistence.
  • Concurrent collision retry at persistence (tier-2) — the preflight resolver prevents collisions within a run, but a concurrent regeneration process can seize a request path between preflight and the write, tripping the url_rewrite (request_path, store_id) unique key. Brand-new product rewrites are now inserted one entity at a time (plain INSERT, not insertOnDuplicate) through a dedicated Service\CollisionRetryPersister: on a duplicate-key / AlreadyExistsException / integrity violation it reloads the now-occupied paths, re-resolves the key to the next free suffix — keeping the url_key attribute in step — and retries, bounded by a configurable cap (maxRetries, default 3, via di.xml); if the path is still contended after the cap the individual product is logged and skipped so one hot key can never stall the whole batch.
  • Retry no longer orphans the url_key attribute — inside the brand-new-rewrite persistence the url_key varchar was written before the (racy) url_rewrite insert, so when a concurrent race exhausted the retry cap the last attempted url_key stayed committed with no matching rewrite row (inconsistent state). The dependent url_key sync is now a separate step (CollisionRetryPersister::persist()'s new $afterPersist callback) that runs only after the url_rewrite write succeeds and with the key actually persisted — so a failed or retry-exhausted attempt can never leave a url_key pointing at a request path that was never created, and on success the persisted url_key always matches the persisted rewrite's request_path.
  • Existing-rewrite updates and 301 redirects no longer clobber foreign rows — renaming an existing product rewrite and writing its 301 history redirect still used blind batch insertOnDuplicate on url_rewrite, which silently overwrote a foreign entity's row (CMS page, category, custom rewrite, or another product) whenever a concurrent writer had taken the target (request_path, store_id) — the same corruption vector the tier-2 fix closed for brand-new rewrites. Existing rewrites are now moved with a per-row UPDATE … WHERE url_rewrite_id = ? routed through CollisionRetryPersister: an UPDATE scoped to the row's own primary key can never modify a foreign row (a raced target surfaces as a duplicate-key violation that is re-resolved and retried, or skipped after the cap), and because it stays an UPDATE the row keeps its url_rewrite_id so catalog_url_rewrite_product_category links survive. 301 redirects are written on the freed old path through a new CollisionRetryPersister::persistUnlessTaken() guard — a plain INSERT that skips and logs, rather than overwrites, a foreign row on a race.
  • The initial bulk url_key write no longer orphans keyspartialRegenerate() bulk-wrote every entity's url_key (insertOnDuplicate on catalog_product_entity_varchar) up front, inside the transaction, before the guarded rewrite persistence ran. So when a rewrite raced out and was skipped, that entity's url_key had already been committed with no matching rewrite — the very orphan the $afterPersist change only closed inside the brand-new-rewrite path. The bulk write is removed; updateUrlRewrite() now categorises every entity and writes its url_key in lockstep with its rewrite: unchanged existing rewrites (path untouched) get their key written directly; changed rewrites get it synced only after the per-PK UPDATE succeeds; missing rewrites get it only after the INSERT succeeds. Net invariant: no entity's url_key is ever committed unless its url_rewrite row was persisted in the same run.
  • A per-PK rewrite UPDATE affecting zero rows is now treated as failure — a concurrent delete could leave the targeted url_rewrite row gone, making the UPDATE … WHERE url_rewrite_id = ? affect 0 rows; that previously fell through as success and synced an orphaned url_key. The update now throws when 0 rows are affected, so the persister does not run the url_key sync (the entity is retried, then skipped) — leaving no orphan. (applyRewriteChanges only processes rows whose request_path genuinely changes, so a benign "value already identical" 0-row update cannot occur here.)
  • Multi-rewrite products are now updated atomically under one shared key — a product can own several url_rewrite rows (a root key.html plus category-context rows like gear/key.html). The changed-rewrite path previously retried and synced each row independently, so under a race the root could re-resolve to new-1.html (url_key new-1) while a category row persisted as gear/new.html (url_key new) — root path, category path, and url_key all disagreeing. applyRewriteChanges() now groups the changed rows by entity_id and persists the whole group under one shared resolved key inside a per-product SAVEPOINT (issued as raw SAVEPOINT / ROLLBACK TO SAVEPOINT / RELEASE SAVEPOINT statements, because Magento's beginTransaction() nesting only tracks depth and emits no real savepoint SQL). If any row in the group fails (duplicate-key collision or a 0-row UPDATE), the group is rolled back to the savepoint and retried under the next key; only once the entire group persists is the url_key synced once and the 301 redirects created. On exhausting the retries the whole product is skipped — every row keeps its old path, and no url_key, partial rewrite, or redirect is written. Rows stay UPDATEs (never delete+reinsert), so url_rewrite_ids — and the catalog_url_rewrite_product_category links keyed on them — are preserved.

Changed

  • Declared the qoliber/core dependency (^1.0) in composer.json — the admin menu (etc/adminhtml/menu.xml) and ACL (etc/acl.xml) reference the Qoliber_Core::menu resource, and etc/module.xml already sequences after Qoliber_Core, but the Composer requirement was missing.

SeoHrefLang

Fixed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass.
  • Resolve localized CMS page alternates by URL key, not entity idCmsPageUrlResolver looked up the same CMS page entity_id in every target store, so translated pages (which are separate entities sharing an identifier) were never found and their stores emitted no hreflang alternate. The resolver now reads the current page's identifier and matches the page assigned to each target store by that identifier (via GetPageByIdentifierInterface), preserving behaviour when a store genuinely shares the same entity and dropping stores with no matching page — consistent with the module's broken-alternate filtering.

Dependencies

  • Declared the direct magento/module-cms dependency (now used by CmsPageUrlResolver) and added Magento_Cms to the module load sequence.
  • Declared the direct qoliber/core (^1.0) dependency — the admin ACL/menu are defined under Qoliber_Core resources; previously only the root metapackage provided it.

SeoOpengraphTags

Fixed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass.
  • Zero-price products no longer drop product:price:amountResolverProduct::getTags() used to gate the price tags behind if ($finalPrice > 0), so a genuine free product (final price 0) silently emitted no price tags at all, indistinguishable from a product with no price info. Now checks getFinalPrice() !== null, so 0 correctly emits product:price:amount = "0.00" (with product:price:currency), while a truly absent price still emits neither tag.
  • product:availability now explicitly reflects out-of-stock — previously the tag was only ever added when $product->isAvailable() was true, so out-of-stock products omitted the tag entirely (rather than telling crawlers/social previews the product is unavailable). product:availability is now always emitted: "in stock" or "out of stock".

Added

  • New ResolverProductPriceAvailabilityTest (5 unit cases) locking: zero price → "0.00" + currency; null/absent price → no price tags at all; a regular price is formatted to two decimals; in-stock → "in stock"; out-of-stock → "out of stock".

2.1.1 — 2026-07-13

SeoAdvancedSitemaps

Changed

  • Declared direct Magento module dependencies with bounded version constraints.
  • Standardised composer metadata (license and magento/framework version constraint).

SeoAdvisorAi

Changed

  • Declared direct Magento module dependencies with bounded version constraints.
  • Standardised composer metadata (license and magento/framework version constraint).
  • PSR-12 code-style cleanup; no functional change.

SeoAiDiscoverability

Changed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass.
  • Declared direct Magento module dependencies with bounded version constraints.
  • Standardised composer metadata (license and magento/framework version constraint).
  • Declared PHP 8.4 support in the php constraint (~8.4.0 added alongside 8.1–8.3); verified 8.4-clean (no implicitly-nullable parameters, php -l and PHPCompatibility pass), no code change required.

SeoCommon

Changed

  • Standardised composer metadata (license and magento/framework version constraint).

SeoDynamicDescriptions

Changed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass. Removed deprecated Reflection::setAccessible() calls (no effect since PHP 8.1, deprecated in 8.5).
  • Declared direct Magento module dependencies with bounded version constraints.
  • Standardised composer metadata (license and magento/framework version constraint).
  • PSR-12 code-style cleanup; no functional change.

SeoDynamicTags

Changed

  • Declared direct Magento module dependencies with bounded version constraints.
  • Standardised composer metadata (license and magento/framework version constraint).

SeoFriendlyProductUrls

Changed

  • Declared direct Magento module dependencies with bounded version constraints.
  • Standardised composer metadata (license and magento/framework version constraint).
  • PSR-12 code-style cleanup; no functional change.

SeoHrefLang

Fixed

  • Emit hreflang on all page types, not only the homepageHrefLang::isNonCanonicalUrl() now inspects the GET query string (getQueryValue()) instead of the merged param bag (getParams()). The merged bag also holds Magento's internal routing params (id on products/categories, page_id on CMS pages), which are not facet variants; treating them as such wrongly suppressed hreflang on every non-homepage URL.

Dependencies

  • Declared direct Magento module dependencies with bounded version constraints.

SeoIndexNow

Changed

  • Declared direct Magento module dependencies with bounded version constraints.
  • Standardised composer metadata (license and magento/framework version constraint).

SeoOpengraphTags

Changed

  • Declared direct Magento module dependencies with bounded version constraints.
  • Standardised composer metadata (license and magento/framework version constraint).
  • PSR-12 code-style cleanup; no functional change.

SeoRichSnippets

Fixed

  • ratingSummary now correctly returns 1–5 scale for schema.org instead of Magento's raw 0–100 percentage
  • ratingValue deduplicated — now calls getRatingSummary() instead of reimplementing the same conversion logic
  • Extracted getRatingSummaryPercent() private method for raw 0–100 value, keeping getRatingSummary() for the converted 1–5 value

Added

  • Integration tests for rating scale: testRatingSummaryReturnsOneToFiveScale, testRatingValueReturnsOneToFiveScale, testRatingSummaryAndRatingValueAreEqual, testRatingSummaryIsNullWithoutReviews, testReviewCountReturnsCorrectCount

2.1.0 — 2026-04-20

SeoAdvancedSitemaps

Performance

  • Streaming sitemap generationSitemapGenerationService no longer materializes each generator's full item list before chunking. Items are iterated and written to the output stream one at a time; new files open every chunk_size URLs. Memory footprint is now O(1) in catalog size, so stores with 100k+ SKUs no longer OOM.
  • BREAKING: SitemapGeneratorInterface::getItems() returns iterable — generators can now yield items via yield instead of returning a full array. Plain-array returns still work (arrays are iterable), so existing custom generators need only change their return type hint.

Added

  • Gzip sitemap output — new admin config field qoliber_seo_advanced_sitemaps/settings/gzip_enabled writes sitemaps as .xml.gz. Supported by all major search engines and cuts bandwidth for large sitemaps.
  • Robots.txt sitemap announcement — new plugin RobotsTxtInjector appends Sitemap: <url> lines to Magento's core robots.txt output for every sitemap registered on the current store. Crawlers discover sitemaps without merchants manually editing robots.txt.

Fixed

  • Chunk filename always suffixed — single-chunk files now also use the _1 suffix in streaming mode since total chunk count isn't known up front; consistent naming.
  • DI hygiene: service extraction moved to _construct — the Sitemap model previously overrode setData() to pull injected services from the data bag on every field mutation, which also risked leaking service objects into DB serialization. Extraction now happens once in Magento's init hook and the keys are unset from the data bag afterwards.

SeoAdvisorAi

Added

  • Structured output (JSON mode)GeminiClient::generateContent() and ClaudeClient::generateContent() now accept a $jsonMode parameter. For Gemini it sets generationConfig.responseMimeType = "application/json"; for Claude it injects a strict system directive plus an assistant-turn prefix { so the model cannot wrap the response in markdown fences. Callers can now json_decode the response without regex-stripping tricks.

Performance / Reliability

  • Retry with exponential backoff — new Qoliber\SeoAdvisorAi\Service\GenerativeAi\RetryHelper retries transient failures (HTTP 429, 5xx, connection errors) with jittered exponential backoff (up to 3 attempts, capped at 5s). Applied to GeminiClient and ClaudeClient; protects against brief upstream blips and 429 rate-limit spikes that would previously surface as user-facing errors.

Fixed

  • DI registry was empty at runtimedi.xml configured the generativeAi array on GenerativeAiInterface but no preference was registered, and consumers (Popup controller, ListModelsCommand, AiProvider source model, ProductDescriptionGenerator) all injected the concrete GenerativeAi class — which got the empty-array default. AI features were silently broken. Added preference from interface to concrete and unified consumer injection on the interface.

Security

  • Prompt injection (HIGH): Input validation in Popup controllertype whitelisted to product/category/cms_page; attributes now JSON-decoded, filtered to 7 whitelisted keys, values forced to strings, payload capped at 8KB
  • GET → POST with CSRF protectioncheck-seo.js now posts with form_key; controller implements HttpPostActionInterface

Changed

  • BREAKING: Popup controller now requires POST + form_key; any custom integrations calling it via GET will need to be updated

SeoAiDiscoverability

First public release of Qoliber SEO AI Discoverability. Version aligned with the rest of the Qoliber SEO suite for a coordinated release.

Added

Three independently-togglable surfaces, all gated behind a master switch that ships OFF — per-site decisions about which AI crawlers to block and which directives to emit are editorial, not technical, and merchants must opt in after reviewing defaults.

  • robots.txt per-bot Disallow blocks. RobotsTxtBotRulesInjector plugs into Magento\Robots\Model\Robots::afterGetData and appends User-agent: <bot>\nDisallow: / blocks for each configured user-agent. Default list: GPTBot, ChatGPT-User, OAI-SearchBot (OpenAI); ClaudeBot, anthropic-ai (Anthropic); CCBot (Common Crawl — feeds most open LLMs); PerplexityBot; Google-Extended (Gemini/Vertex training opt-out that leaves Google Search unaffected); FacebookBot (Meta LLaMa); Bytespider (TikTok/Doubao); Amazonbot (Alexa/Rufus). Merchants can trim or extend in admin.
  • X-Robots-Tag HTTP header. XRobotsTagHeaderInjector plugin on Magento\Framework\App\Response\Http::beforeSendResponse emits the configured directive string on every frontend response. Default noai, noimageai signals AI training opt-out without affecting traditional search indexing. Overwrites any header set earlier in the request so merchant config wins over core/third-party additions.
  • /llms.txt endpoint per the draft spec. A custom Controller\Router matches the literal path (standard Magento routing can't handle the dot segment without a URL rewrite entry) and dispatches to Controller\Llms\Index which serves the admin-configured body as text/plain; charset=utf-8 with X-Content-Type-Options: nosniff. Returns 404 when disabled so crawlers treat the path as non-existent.
  • Admin configuration under Stores → Configuration → Qoliber → SEO: AI Discoverability. Per-store scope, four groups: General (master switch), robots.txt, X-Robots-Tag, llms.txt. ACL resource Qoliber_SeoAiDiscoverability::config.

Tests

  • 13 integration tests across three files:
    • Test/Integration/Plugin/RobotsTxtBotRulesInjectorTest (5 cases) — master/feature gating, one block per bot, empty list emits nothing, whitespace and duplicates deduped.
    • Test/Integration/Plugin/XRobotsTagHeaderInjectorTest (4 cases) — disabled paths, empty directives, header emission, overwrite behaviour.
    • Test/Integration/Controller/LlmsIndexTest (4 cases) — 404 on master-off, 404 on feature-off, 200 with configured body, text/plain + nosniff headers.

Notes

  • The module does not block bot traffic at the request layer — it only publishes the opt-out signals. Bots that don't honour robots.txt / X-Robots-Tag (the "bad" ones) won't be affected. For active blocking, merchants still need a WAF rule set.
  • Google-Extended is specifically the Gemini/Vertex training opt-out. It does not affect ranking in Google Search; the Googlebot that feeds Search is a separate user-agent. This is the single most important entry to keep in the default list — removing it inadvertently opts merchants back into training without affecting Search.
  • After installing, merchants must run bin/magento setup:upgrade to register the module, then enable the master switch and each feature in admin.

SeoCommon

First public release of Qoliber SEO Common. Version aligned with the rest of the Qoliber SEO suite for a coordinated release — SeoCommon is the shared helper library the other modules depend on, so the version jump matches the suite rather than starting independently at 1.0.0.

Added

Shared utility services consumed by other Qoliber SEO modules:

  • Service/Html/Stripper — HTML-to-plain-text conversion used by OG/meta output paths.
  • Service/Pattern/PlaceholderReplacer{key} placeholder expansion for dynamic tag patterns.
  • Service/Image/PathParser — catalog/media image path decomposition (basename, extension, directory) with safe handling of paths containing multiple dots.
  • Service/GenerativeAi/* — provider-agnostic abstraction consumed by SeoAdvisorAi and ProductDescriptionGenerator.

Notes

  • SeoCommon has no frontend or admin presence of its own. It's a dependency shipped alongside the rest of the suite; installing it without other Qoliber SEO modules has no effect.

SeoDynamicDescriptions

Performance

  • Hot-path plugin bail-outModifyDataPlugin::afterGetData now early-exits for fields that no modifier handles (O(1) lookup against a prebuilt index). Previously every Product::getData() / Category::getData() call iterated the modifier pool — this runs thousands of times per page.
  • Request-scope memoization — results for (event_prefix, entity_id, field) are cached per request, so Twig rendering for the same field runs at most once. Combined with the bail-out, this eliminates the single biggest TTFB cost of the module.

Security

  • CRITICAL: Twig sandbox — category description / SEO header rendering now uses Twig\Extension\SandboxExtension with a tight allow-list (tags: if/for/set; filters: escape/upper/lower/trim/length/default/join/split/replace/striptags etc.; functions: range). Admin-crafted template strings can no longer invoke PHP or access object methods.
  • HTML autoescape enabled — Twig autoescape: html is now on, so dynamic values are HTML-escaped by default.
  • Removed html_entity_decode after render — this step subverted escaping and turned &lt;script&gt; back into live HTML.
  • Safe template context — category context passed to Twig is now filtered to scalar/null values only; model methods and relations are no longer exposed to templates.
  • Request param whitelist + bounds — reserved Magento keys (p, product_list_*, q, id, form_key, etc.) are stripped from filter variables; filter values are trimmed to 128 chars and stripped of control characters to prevent SEO-poisoning payloads.

SeoDynamicTags

First release cutting from 2.0.0. Ships the stored-XSS fix, robots whitelist, and locks the preventRelativePath() normalisation contract as executable spec (the method had no test coverage before).

Security

  • Stored XSS in meta tag output — admin input for meta_title, meta_description, meta_keywords now passes through strip_tags() + control-char stripping in ModifyMetaTags plugin.
  • Robots value whitelistrobots directive now validated against canonical INDEX/NOINDEX, FOLLOW/NOFOLLOW combinations; arbitrary admin input is discarded.

Tests

  • preventRelativePath() contract lockedTest/Unit/Model/SeoDynamicTagPathSanitizerTest (13 cases via dataProvider). The method runs on every admin-submitted request_path AND again in beforeSave() as last-line defence; before this release, its normalisation rules were undocumented. Tests cover: no leading slash, trailing slashes, triple slashes, empty input, traversal artefact preservation, dot segments, query-like suffixes. The canonical output rule is now spelled out as executable spec: "produce exactly one leading slash, no trailing slash, empty collapses to /".

SeoFriendlyProductUrls

Security

  • CRITICAL: url_rewrite collision prevention — dedup checks in both Regenerator and SingleProductUrlGenerator previously only looked at entity_type = 'product' rows. The url_rewrite unique key is (request_path, store_id) across ALL entity types, so insertOnDuplicate could silently overwrite CMS pages, categories, or custom rewrites that shared a path. Dedup now scans ALL entity types and resolves conflicts before writing.

Fixed

  • Transactional batch regenerationpartialRegenerate now wraps catalog_product_entity_varchar and url_rewrite writes in a single DB transaction; partial failures roll back cleanly instead of leaving URL keys and rewrites out of sync.

SeoHrefLang

Fixed

  • Suppress hreflang on non-canonical URLsHrefLang::getTags() now returns [] when the request carries filter, sort, or pagination query parameters. Previously the module emitted hreflang clusters on faceted pages, diluting the canonical signal.
  • Broken-alternate filtering — stores that resolved to no URL for the current entity are now dropped from the tag list instead of emitting <link href=""> entries.

Security

  • XSS (CRITICAL): Escape hreflang output — removed /* @noEscape */ on $tag['locale'] and $tag['url'] in view/frontend/templates/hreflang.phtml; now uses $escaper->escapeHtmlAttr() and $escaper->escapeUrl() respectively

SeoImagesFriendlyUrl

  • Schema: multi-store index with unique business keyqoliber_seo_images_friendly_url gained a store_id column (0 = all stores) and a composite unique constraint on (image, store_id). Pre-2.1.0 installs accumulated duplicate rows (4.3x observed on seed data); the AddImageStoreIdUniqueConstraint schema patch dedupes existing rows on upgrade and adds the constraint. GetSeoFriendlyNameFromIndex::execute() gains a $storeId parameter, preferring store-specific overrides with fallback to store_id=0.

  • Performance: LRU-bounded SEO name cacheGetSeoFriendlyNameFromIndex previously grew an unbounded in-memory map of (image → seo_name) pairs. On a category listing with thousands of products, this leaked memory across requests in long-lived FPM workers. Now capped at 2000 entries with LRU eviction.

  • Removed reflection on parent private stateImage asset class used to call ReflectionClass::getProperty('miscParams') on every misc-access to read a private field from Magento core. Overrode the parent constructor to capture $miscParams into our own property. Removes a fragile integration point that would break on PHP / Magento upgrades.

  • Security (CRITICAL XSS): Alt tag escapingSeoValueResolver::resolveAltTagSeoValue() now escapes output via Magento\Framework\Escaper::escapeHtmlAttr(). Prevents stored XSS via product names flowing into image alt attributes and gallery caption JSON

  • Filesystem safety (CRITICAL): Atomic symlink creation in ImageResize — replaces unsafe isExist() + createSymlink() check-then-act with a temp-file + rename() pattern. Concurrent resize workers can no longer corrupt links or race on missing symlinks

  • PHP 8.2+ compliance: ImageResize dynamic properties$fileStorageDatabase and $storeManager were assigned without property declarations, triggering a PHP 8.2 deprecation and a fatal error on PHP 9. Now declared explicitly as typed protected properties.

  • DB schema whitelist cleanup — removed the stale QOLIBER_SEO_IMAGES_FRIENDLY_URL_IMAGE entry (the btree was dropped from db_schema.xml) and the speculative ..._IMAGE_STORE_ID entry (never created — the schema patch adds ..._IMAGE_STORE_ID_UNQ via raw DDL, which intentionally stays out of the whitelist).

  • Integration test coverage for 2.1.0 surface — 27 new test cases across 6 files (32 integration tests total, up from 5). Covers: multi-store index lookup with fallback, schema-patch dedupe + idempotency + unique-constraint enforcement, URL-builder DI preference and flag gating, indexer full-reindex + partial-reindex non-interference, gallery CreateHandler / UpdateHandler contract, App\Media + ImageResize preference wiring, and Config scope-read alignment.

  • Fix (CRITICAL): App\Media::createLocalCopy passed the cached path to the resizer — our responder handed the full catalog/product/cache/<hash>/…/foo.jpg path to ImageResize::resizeFromImageName(), but that method expects the original image path (e.g. /m/j/foo.jpg). Core Magento's responder strips the cache/<hash>/ prefix via a private getOriginalImage() helper before calling the resizer; our override never ported that helper. Symptom: only the first image of a product gallery rendered — Magento had pre-generated it via catalog:images:resize, but every subsequent on-demand request fell through to setPlaceholderImage(). The helper is now included and the call site mirrors core.

  • Fix: ImageResize::resizeFromImageName was a silent no-op when the module was disabled — early return when isModuleEnabled() was false meant no image ever got generated, not even by core. Because the DI preference on Magento\MediaStorage\Service\ImageResize still routes every request through our subclass, "disabled" must mean "delegate to parent" rather than "do nothing". Fixed: the disabled branch now calls parent::resizeFromImageName() so core image generation works exactly as if the module weren't installed.

  • Fix (CRITICAL): SEO URL scheme was ambiguous (3-segment form)CatalogUrlBuilder::getUrl and the CatalogBlockProductImageFactory plugin both produced <dir>/<seoName>.<ext> URLs. Multiple originals can share a single SEO name (e.g. mp01-gray_main_1.jpg and mp01-gray_back_1.jpg both map to caesar-warm-up-pant-mp01), so the responder couldn't reverse-map the SEO URL to a specific source image. The existing ImageResize::extractRealFilePath() was coded for a 4-segment form <dir>/<originalBasename>/<seoName>.<ext> that the URL generator never produced. Both URL builders now embed the original basename as a subdirectory — the 4-segment form the reverse-mapping was always designed for. Disambiguation is now by construction.

  • Fix: App\Media::getOriginalImage prefix-strip regex was too rigid — core Magento's hardcoded last-3-segments regex worked for the non-SEO tail but truncated the SEO 4-segment tail. Replaced with a /cache/<hash>/ prefix-strip that preserves whatever tail follows, so 3- and 4-segment forms both round-trip.

  • Fix (CRITICAL): Model\View\Asset\Image override hijacked every catalog image asset — the class is the DI preference target for Magento\Catalog\Model\View\Asset\Image, meaning every caller that builds a catalog image asset (OG meta-tag resolver, admin grids, swatches, email templates) routed through us. getUrl() and getFilePath() were gated only on isModuleEnabled() and unconditionally read misc['filePath'] — which only OUR CatalogUrlBuilder populates — so every other caller got a URL with an empty filename, collapsing to the theme placeholder (/static/…/placeholder/image.jpg). Now both methods gate on isSeoImage() (the flag CatalogUrlBuilder sets explicitly). Non-SEO callers pass through to parent unchanged.

  • Fix: broken gallery thumbnails on product pages — composite symptom of the two fixes above: on a product page with multiple images, only the first rendered. Magento had pre-generated that one via catalog:images:resize, but request-time resize for the other thumbnails failed because our responder handed the wrong path to the resizer. With App\Media::createLocalCopy now calling getOriginalImage() before the resizer (mirroring core), all gallery thumbnails resolve correctly regardless of whether the module is enabled or disabled.

  • Regression lock for the four fixes aboveCatalogUrlBuilderIntegrationTest::testSeoUrlEmbedsOriginalBasenameAsSubdirectory (4-segment URL shape), MediaGetOriginalImageTest (6 dataProvider cases: 3-seg + 4-seg + edge inputs), ImageResizeParentDelegationTest (NotFoundException probe proves parent delegation on disabled), and ImageResizeExtractRealFilePathTest (reverse-mapping for the 4-segment form). Integration total now 35 tests; unit 24.

SeoIndexNow

First public release of Qoliber SEO IndexNow. Version aligned with the rest of the Qoliber SEO suite for a coordinated release.

Added

  • Push URL-change notifications to Bing, Yandex, Naver, Seznam and any other IndexNow-compatible search engine via the generic api.indexnow.org endpoint.
  • Observer-based triggers on product, category, and CMS page save (each individually toggle-able).
  • Qoliber\SeoIndexNow\Service\Notifier — best-effort, fire-and-forget HTTP submission with 5s total timeout; never blocks the triggering save.
  • Ownership-verification key file served via Controller\Key\Index (route qoliber_index_now/key/index) — admin only needs to paste a key into config, no separate file upload.
  • Admin configuration under Stores > Configuration > Qoliber > SEO: IndexNow.
  • ACL resource Qoliber_SeoIndexNow::config.

Tests

  • Observer chain locked across all three save paths:
    • Test/Integration/Observer/ProductSaveObserverTest (5 cases)
    • Test/Integration/Observer/CategorySaveObserverTest (5 cases)
    • Test/Integration/Observer/CmsPageSaveObserverTest (6 cases) A spy Curl replaces the real HTTP client via ObjectManager's shared-instance swap so the observers exercise the full save path without hitting the network. Covers: URL-key becomes the path segment in the POSTed urlList; master switch off → zero calls; per-feature flags (notify_on_product_save / notify_on_category_save / notify_on_cms_save) off → zero calls; missing entity in event short-circuits; empty url_key / identifier short-circuits; store_id=0 excluded from CMS submissions.
  • Notifier gating lockedTest/Integration/Service/NotifierIntegrationTest (5 cases): missing/malformed IndexNow key returns false without a network call; valid call dedupes duplicate URLs; empty URL list short-circuits as success; disabled module returns true with no HTTP call.

Notes

  • IndexNow is supported by Microsoft Bing, Yandex, Naver, Seznam (launched 2021). Google does not yet support it.
  • To prove ownership, also configure your web server to serve /{key}.txt at the site root with the key as the body; this module's controller handles it automatically for standard Magento-routed requests.

SeoOpengraphTags

First release cutting from 2.0.0. Bundles the prior correctness fixes with the 2025/2026 social-preview compliance additions.

Added

  • og:image:width, og:image:height, og:image:alt emitted on product pages — Facebook and LinkedIn require the dimensions to render the large-card preview; without them LinkedIn silently downgrades to a 40x40 thumbnail. og:image:alt satisfies accessibility tooling for social shares.
  • twitter:image:alt mirrors og:image:alt for the same accessibility reason Twitter already mirrors og:title/og:description.
  • New ResolverProductImageMetadataTest (4 unit cases) covering: helper URL + resized dimensions, fallback to helper getWidth/getHeight when getResizedImageInfo is null, empty product image suppresses the whole og:image block, empty product name suppresses the alt tag.

Fixed

  • Module disable actually works — both layout_load_before observers (RemoveDefaultTags, SetPrefix) now check Config::isEnabled() before mutating layout handles. Previously, disabling the module could leave product pages with no OG tags at all because the Magento core catalog_product_opengraph handle was still being removed.
  • OG block gated via ifconfig — the qoliber.opengraph block is now only instantiated when qoliber_opengraph/settings/enable is on.
  • og:image now emits the same SEO-friendly URL the product page uses — previously ResolverProduct::getImageUrl() produced a raw /media/catalog/product/<file>.jpg URL (not resized, not SEO-rewritten). ResolverProduct now injects Magento\Catalog\Model\Product\Image\UrlBuilder (the DI preference target for Qoliber\SeoImagesFriendlyUrl\…\CatalogUrlBuilder) so the OG image URL goes through the same SEO rewrite the product-page gallery uses. Width/height come from Magento\Catalog\Helper\Image using the stock product_page_image_medium view.xml entry (700x700) — the right size for social previews, and an existing entry we reuse instead of adding a new one. product_page_image_large is intentionally not used because Luma/blank ship it with no dimensions (self-closing <image .../>), which makes the helper return the theme placeholder.
  • Template whitespace cleanupview/frontend/templates/opengraph.phtml was producing a dozen spaces of leading indentation on every <meta> line (visible in page source). Restructured to flat output; each tag now starts at column 0.

Notes

  • The OG image URL matches the product-page gallery URL in both states of the SEO module: with the module enabled you get the 4-segment SEO form (cache/<hash>/<dir>/<originalBase>/<seoName>.<ext>); with the module disabled you get the standard Magento resized form (cache/<hash>/<dir>/<file>.<ext>). Either way it's a resized variant, never the raw source file.

SeoPrettyFilters

Security

  • Reflected XSS in RequestParamsProvider$paramValue and $queryParam are now passed through rawurlencode() before URL concatenation (Model/RequestParamsProvider.php:56)

Fixed

  • Canonical URLs no longer leak query paramsAbstractPatternProcessor::getCanonicalArguments used to append any unresolvable query parameter (e.g. ?sortby=price, ?utm_source=x) to the canonical. Canonicals now always point to the clean, unfiltered category URL.
  • Search result canonical preserves qResultBlockPlugin now includes the normalized search query in the canonical URL so different searches don't collapse to the same canonical.
  • Stateless RequestPathResolver — removed per-instance $resolvedParams cache that would leak between requests in long-running PHP processes; resolution state is now method-local. New unit test covers the no-leak contract.

Performance

  • Router early exitRouter::match() now bails out immediately for paths under known non-catalog prefixes (admin/, rest/, graphql, static/, media/, checkout/, etc.) before running the full resolver chain. Avoids thousands of wasted resolver calls per page load on admin and AJAX requests.

SeoPrettyFiltersElasticSuite

First release cutting from 2.0.0. Introduces the first test coverage for the module; no behavioural changes.

Tests

  • First test coverage for this moduleTest/Unit/Plugin/ShortAttributeResolverTest (3 cases) locks the short-pattern request-path cleanup contract: null ResolvedParams propagates unchanged, the configured separator is stripped from the path, stray comma artefacts from upstream multi-select filters are removed.
  • Integration tests for the other eight plugins require the Smile\ElasticsuiteCore package which isn't always present in the development environment — deferred to a follow-up release that runs against an ElasticSuite-equipped test fixture. Plugin source doesn't import from Smile, so more unit-test coverage is feasible at zero-infrastructure cost.

SeoRichSnippets

Added

  • price and finalPrice variables in product value resolver with smart formatting (trailing zeros removed)
  • shortDescription and short_description variables with automatic fallback to full description when short description is empty
  • Per-review rating field in CollectionVariables — computes average rating from individual rating votes on a 1–5 scale

Fixed

  • HTML stripping now applied to all product attribute values for schema.org compliance, not just textarea/text inputs
  • Empty string attribute values now return null instead of blank strings in JSON-LD output
  • Anonymous reviewer fallback — reviews without a nickname now render as "Anonymous" in both ReviewValueResolver and CollectionVariables
  • preg_replace return values explicitly cast to string in stripHtml() for strict type safety
  • Removed sleep(1) calls from review test fixture to speed up test execution

2.0.6 — 2026-03-06

SeoDynamicDescriptions

Fixed

  • Twig template error logging: Twig rendering exceptions are now logged with category ID and error message instead of being silently swallowed — aids debugging broken templates without breaking the storefront

2.0.5 — 2025-07-03

SeoDynamicDescriptions

Fixed

  • PageBuilder content formatting fix that properly handles and formats PageBuilder HTML content in category descriptions
  • Product page header overwriting prevention that ensures dynamic descriptions don't incorrectly override product page headings
  • Enhanced modifier architecture to prevent cross-contamination between category and product page SEO elements

Changed

  • Improved ModifyDataPlugin to better distinguish between category and product contexts
  • Refactored modifier pool to handle different page types more reliably

2.0.4 — 2025-XX-XX

SeoDynamicDescriptions

Changed

  • Updated composer.json metadata

2.0.3 — 2025-XX-XX

SeoDynamicDescriptions

Fixed

  • Code quality improvements and formatting consistency

2.0.2 — 2025-04-02

SeoDynamicDescriptions

Changed

  • Updated package author information

2.0.1 — 2026-02-25

SeoDynamicDescriptions

Changed

  • Automated version update

SeoImagesFriendlyUrl

  • Proper logger injection: Replaced error_log() with Psr\Log\LoggerInterface injected via setter-injection plugin — uses Magento's standard logging channel (var/log/system.log) with exception and image ID context

SeoRichSnippets

Changed

  • Version bump for Private Packagist configuration

2.0.0 — 2026-03-10

SeoAdvancedSitemaps

Changed

  • Complete rewrite: replaced ValueFilter plugin architecture with clean SitemapGeneratorInterface generator pattern
  • Each entity type (CMS pages, categories, products, images) is now a configurable generator registered via di.xml
  • Sitemap generation delegated to SitemapGenerationService orchestrator with streaming XML writes
  • Configurable pagination: URLs per sitemap file (default 1500) via admin config
  • Output naming: sitemap_cms.xml, sitemap_categories.xml, sitemap_products_1.xml, etc.

Added

  • Api\SitemapGeneratorInterface contract for entity generators
  • Model\Generator\CmsPageGenerator, CategoryGenerator, ProductGenerator, ImageGenerator
  • Model\Service\SitemapGenerationService core orchestrator
  • Admin config field "URLs per Sitemap File" with validate-digits validation

Removed

  • Plugin\ValueFilter\AbstractValueFilter, Category, CmsPage, Product — replaced by generators

SeoAdvisorAi

Added

  • Anthropic Claude AI integration with native API client, dynamic model fetching via /v1/models endpoint, and 24h cache
  • xAI Grok integration using OpenAI-compatible SDK with custom base URL, supporting Grok 4 and Grok 3 model families
  • Dynamic model fetching for Gemini — available models are now fetched from the Google API and cached for 24 hours, with fallback to a static list
  • Admin configuration fields for Claude (API key, model selection) and Grok (API key, model selection)
  • Source model classes for Claude and Grok admin dropdowns

Changed

  • BREAKING: Gemini API upgraded from v1 to v1beta for access to latest models
  • BREAKING: Gemini model config values now store actual model IDs (e.g., gemini-2.5-flash) instead of mapped keys (e.g., gemini_flash). Existing Gemini model selections will need to be re-saved
  • Updated ChatGPT model list to 2026 models (GPT-4.1, o3, o4-mini)
  • Updated Gemini fallback models to include Gemini 2.5 Flash and Gemini 2.5 Pro
  • Default ChatGPT model changed from gpt-3.5-turbo to gpt-4.1-mini
  • Modernized all service classes to use PHP 8.1 constructor property promotion
  • Renamed Gemini provider display name from "Gemini" to "Google Gemini"

Removed

  • Removed hardcoded MODELS constant mapping from GeminiClient — model IDs are now passed through directly

SeoDynamicDescriptions

Added

  • Major architecture refactoring with introduction of Modifier pattern for better code organization and maintainability
  • Modifier classes for category descriptions, meta descriptions, meta keywords, meta titles, and SEO headers
  • Product-specific modifiers for meta descriptions and meta titles
  • Factory pattern for creating modifiers dynamically based on context
  • Improved plugin integration with Output helper for better HTML rendering

Changed

  • Migrated from direct plugin implementation to modifier-based architecture
  • Enhanced code quality and reduced code duplication across SEO field processors

SeoDynamicTags

Fixed

  • Save controller missing default redirect — added $resultRedirect->setPath('*/*/index') before POST check to ensure redirect is always set
  • ModifyMetaTags assignment in conditional — extracted $page = $this->getRequest()->getParam('p') before conditional blocks for clarity

Changed

  • Added declare(strict_types=1) to Save controller and ModifyMetaTags plugin

SeoFriendlyProductUrls

Added

  • Admin UI: Generate URL button on product edit page (Search Engine Optimization section) — generates SEO-friendly URL key directly from the admin panel without CLI
  • Single product URL generation service (Service\SingleProductUrlGenerator) with support for both saved products (generate()) and unsaved products from form data (generateFromFormData())
  • Admin AJAX controller (Controller\Adminhtml\Generate\UrlKey) returning JSON with generated URL key and configured pattern
  • ViewModel (ViewModel\GenerateUrlData) providing pattern configuration to the template
  • Admin route qoliber_seo_friendly_urls for the generate endpoint
  • RequireJS widget with MutationObserver that injects "Generate URL" button next to url_key label and pattern notice below the input field
  • ACL resource Qoliber_SeoFriendlyProductUrls::generate_url for granular permission control
  • LESS styling for generate button and pattern notice in adminhtml

SeoHrefLang

Fixed

  • Critical: Config property accessed as method$this->allowedStoreUrls()[$storeId] corrected to $this->allowedStoreUrls[$storeId] (was calling non-existent method instead of reading property)
  • Malformed composer.json psr-4 autoload — fixed missing empty string value and closing braces in autoload configuration
  • Null safety in URL resolvers — added null guards for $request->getParam() in ProductUrlResolver, CategoryUrlResolver, and CmsPageUrlResolver to prevent type errors

Changed

  • Added readonly to constructor properties in Config and HrefLang ViewModel
  • Added declare(strict_types=1) to all files missing it: ProductUrlResolver, CategoryUrlResolver, CmsPageUrlResolver, ContactPageUrlResolver, AlternateUrlService, UrlResolverInterface

SeoImagesFriendlyUrl

  • Strict comparison fix: Changed loose != to strict !== in Image::getImageInfo() for image friendly name check
  • Dead code removal: Removed unused $productData variable in SeoValueResolver::getSeoFriendlyValue() — was populated but never passed to template processor
  • Docblock fix: Corrected /**+ typo to /** in SeoValueResolver constructor docblock
  • Readonly properties: Added readonly to all constructor properties in AddDiObjectsToImageModel plugin
  • Error logging: Added exception logging in Image::getUrl() catch block — silent exception swallowing now reports the error

SeoOpengraphTags

Fixed

  • ResolverCategory array_filter removes valid falsy values — replaced bare array_filter($tags) with explicit callback to preserve "0" and false values, only filtering out null and empty strings

Changed

  • SetPrefix observer modernized to PHP 8.1 constructor property promotion with readonly
  • Added : void return type to execute() in SetPrefix and RemoveDefaultTags observers

SeoPrettyFilters

Added

  • Flat URL mode — third URL pattern alongside Short and Complex, with configurable filter order per store view and segment separator (-- or __)
  • FlatAttributeResolver for resolving flat URL segments into filter parameters using ordered attribute matching
  • FlatPatternProcessor for generating flat-format filter URLs with configurable segment separator
  • FlatSegmentSeparator source model for admin separator selection
  • Admin config fields: "Flat Segment Separator" and "Flat Filter Order" (comma-separated attribute codes)
  • Unit tests for FilterOption, Yesno, RequestPathResolver, FlatAttributeResolver, AttributeFetcher
  • Integration tests for Router controller and Flat mode DI wiring / config resolution (FlatModeTest)

Fixed

  • Critical: AttributeFetcher data-wipe bug$attributeSeoParams was wiped immediately after store key initialization due to reversed line order
  • ComplexAttributeResolver str_contains args reversedstr_contains('!', $value) corrected to str_contains($value, '!')
  • ComplexAttributeResolver missing isset guard — added check before getOptionIdByValue() to prevent error on unknown attribute codes
  • ShortPatternProcessor array_merge crasharray_merge(...$seoPaths) crashes when $seoPaths is empty; applied ternary guard
  • AbstractPatternProcessor loose usort comparison — replaced verbose comparator with spaceship operator (<=>)
  • FilterOption loose comparisons — changed == to === in getOptionIdByValue() and getOptionIdBySeoPath()
  • Yesno converter loose comparison — changed $optionId == '0' to $optionId === '0'
  • ModifyMetaRobots silent exception swallowing — added LoggerInterface dependency and warning-level logging
  • RequestPathResolver falsy check on empty array — changed default from [] to null and check to === null

SeoPrettyFiltersElasticSuite

Added

  • Flat URL mode supportFlatAttributeResolver and FlatPatternProcessor plugins for ElasticSuite-specific attribute handling in flat URL mode
  • Plugin for FlatAttributeResolver::resolveAttributeValues() handling ElasticSuite multi-select and swatch attribute formats
  • Plugin for FlatPatternProcessor handling ElasticSuite attribute array construction

Fixed

  • ComplexAttributeResolver str_contains args reversedstr_contains('!', $value) corrected to str_contains($value, '!')
  • ComplexAttributeResolver missing isset guard — added check before getOptionIdByValue() for multi-value attribute case

SeoRichSnippets

Fixed

  • Last breadcrumb item having empty URL in JSON-LD output — now uses current page URL
  • HTML tags no longer leak into schema.org JSON-LD output for description and short_description attributes

Added

  • Complete test suite (161 integration + 14 unit = 175 tests, 733+ assertions)

  • Full JSON string comparison tests (FullJsonOutputTest) — loads real installed snippets from DB, expands all nested child snippets (up to 4 levels deep), resolves variables, processes through DJson, and compares the complete final JSON-LD output

  • HTML stripping from product descriptions for schema.org compliance — <b>, <p>, <div>, <a>, <br> tags stripped, HTML entities decoded, whitespace normalized

  • BreadcrumbsVariableProvider unit tests — 8 tests covering both layout block and catalog helper code paths, verifying last breadcrumb gets current URL

  • Product with image test using Magento's built-in product_with_image.php fixture

  • MSI-compatible test fixture — product_with_select_attribute.php conditionally registers MSI source items using interface_exists() check, works with and without MSI modules

  • DJson template resolution tests — @djson if/else stock conditionals, optional field inclusion/exclusion, nested snippet expansion, full JSON-LD validation

  • Product attribute resolution tests — select/multiselect attribute text label resolution, HTML stripping, camelCase to snake_case attribute mapping

  • Variable extractor tests — HTML stripping verification, HTML entity decoding, @djson directive variable extraction

  • Schema block rendering tests — all 30 installed snippet templates verified

  • Nested snippets tests — circular dependency detection, multi-level expansion, missing snippet handling

  • Value resolver integration tests — complete flow from template through variable resolution, nested snippet expansion, to final JSON output

  • BREAKING: Complete architectural rewrite of the module

  • API Layer:

    • SnippetInterface for snippet data structure
    • SnippetRepositoryInterface for snippet CRUD operations
    • SnippetSearchResultsInterface for search results
    • ValueResolverInterface for dynamic value resolution
    • VariableProviderInterface for variable system
  • Admin Interface:

    • Complete adminhtml UI for snippet management
    • Snippet listing grid with filtering and mass actions
    • Snippet edit form with advanced schema editor
    • CodeMirror-based JSON editor with syntax highlighting
    • Variables panel with autocomplete support
    • Save, delete, and mass delete operations
    • Inline editing support
  • Core Features:

    • SnippetRepository for managing snippet entities
    • SnippetExpander for processing nested snippets
    • CircularDependencyValidator for preventing infinite loops
    • VariableExtractor for parsing variable expressions (supports both {{var}} and @djson if/unless/exists directives)
    • Handle system for layout-based snippet assignment
  • Variable System:

    • ProductVariables - Product-specific variables
    • StockVariables - Stock/inventory variables (product.stock.qty, product.stock.isInStock, product.stock.availability)
    • BreadcrumbsVariableProvider - Breadcrumb navigation variables
    • CollectionVariables - Product collection variables
    • CustomVariables - User-defined custom variables
    • EnumVariables - Enumeration values
    • StoreConfigVariables - Store configuration access
    • SystemVariables - System-level variables
    • SnippetVariables - Cross-snippet references
  • Value Resolvers:

    • ConfigValueResolver - Resolve configuration values
    • ProductValueResolver - Resolve product data with 20+ attributes and nested stock.* path resolution
    • ReviewValueResolver - Resolve product review data
    • StoreValueResolver - Resolve store information
  • Database Schema:

    • qoliber_seo_richsnippets_snippet table for snippet storage
    • qoliber_seo_richsnippets_snippet_store for store relations
    • qoliber_seo_richsnippets_handle for layout handle assignments
    • Complete database schema with proper indexes and constraints
  • Data Patches:

    • InstallTier1Snippets - Core snippets (Product, Organization, WebSite)
    • InstallTier2Snippets - Enhanced snippets (Breadcrumbs, Offers)
    • InstallTier3Snippets - Advanced snippets (Reviews, Ratings)
    • InstallTier4Snippets - Business snippets (LocalBusiness, Store policies)
    • InstallTier5Snippets - Collection and search page snippets
    • UpdateOfferStockAvailability - Dynamic stock availability in Offer snippet
  • Frontend:

    • New JsonLd block for rendering snippets with caching
    • Product image URL resolution via catalog image helper (Friendly URLs compatible)
    • Enhanced template with better error handling
  • Admin Assets:

    • CodeMirror library integration
    • Custom CSS for schema editor
    • JavaScript widgets for variables panel
    • DJSON logo and branding

Removed

  • Old Service Layer (replaced with new architecture):
    • AbstractRichSnippetsService, AcceptedPaymentMethodService, BreadcrumbListService
    • ContactPointService, LocalBusinessService, MerchantReturnPolicyRichSnippetsService
    • OnlineStoreService, OrganizationService, ProductService, WebsiteService
    • Collection page services, Item list services
    • Product-related services (AggregateRating, Brand, Offer, OfferShippingDetails, Review)
  • RichSnippets ViewModel (replaced with Block-based approach)
  • Hardcoded layouts for category, product, and search pages (replaced with flexible handle-based system)
  • Extensive system.xml configuration (600+ lines, replaced with admin UI)

1.2.4 — 2024-XX-XX

SeoDynamicDescriptions

Changed

  • Package maintenance update

1.2.3 — 2024-XX-XX

SeoDynamicDescriptions

Changed

  • Package maintenance update

1.2.2 — 2024-XX-XX

SeoDynamicDescriptions

Changed

  • Package maintenance update

1.2.1 — 2025-10-21

SeoDynamicDescriptions

Changed

  • Package maintenance update

SeoRichSnippets

Fixed

  • QM-290: Added null value protection in ProductService to prevent errors

1.2.0 — 2025-10-20

SeoDynamicDescriptions

Added

  • Category description positioning control that allows moving category descriptions below product pagination for improved user experience
  • Frontend event observer MoveCategoryDescriptionBelowPagination that relocates description block dynamically
  • Configuration option to enable/disable description repositioning

Changed

  • Enhanced layout handling with custom event system for description placement

SeoRichSnippets

Added

  • QM-291: Advanced e-commerce features

    • AcceptedPaymentMethodService for payment method rich snippets
    • OnlineStoreService for online store structured data
    • Collection page support with CollectionPageService
    • Search results page support with SearchResultsPageService
    • OfferShippingDetailsService for shipping information
    • Item list functionality with ItemListService
  • QM-291: New configuration options

    • Item condition enumeration (New, Used, Refurbished, Damaged)
    • Refund type enumeration
    • Return fees enumeration
    • Return label source enumeration
    • Return method enumeration
    • Enhanced merchant return policy configuration (400+ lines)
  • QM-291: Enhanced layout support

    • Added layouts for category views
    • Added layouts for product views
    • Added layouts for search results

Changed

  • QM-291: Enhanced MerchantReturnPolicyRichSnippetsService with advanced features
  • QM-291: Improved ProductService with better offer handling
  • QM-291: Updated ReviewService with enhanced review data
  • QM-289: Refactored ProductService for better code quality

1.1.16 — 2026-01-21

SeoPrettyFilters

Fixed

  • Category resolver improvements and bug fixes (QM-314)
  • Category path resolver enhanced to handle edge cases

Changed

  • Code quality improvements across multiple files
  • Refactored request params provider
  • Enhanced attribute fetcher functionality
  • Improved pattern processor logic
  • Updated CMS page block plugin
  • Optimized result block plugin
  • Improved layered navigation block navigation state handling

1.1.15 — 2025-10-28

SeoPrettyFilters

Fixed

  • Pattern processor bug fix in abstract pattern processor

1.1.14 — 2025-10-20

SeoPrettyFilters

Fixed

  • Request params provider improvements
  • Category URL handling enhancements

SeoRichSnippets

Fixed

  • Minor ProductService improvements

1.1.13 — 2025-07-18

SeoPrettyFilters

Changed

  • CMS page block plugin enhancements

SeoRichSnippets

Fixed

  • QM-204: Fixed product image handling for Hyva theme compatibility

1.1.12 — 2025-07-14

SeoPrettyFilters

Changed

  • Improved CMS page block plugin
  • Result block plugin code improvements

SeoRichSnippets

Fixed

  • LocalBusinessService improvements

1.1.11 — 2025-07-14

SeoPrettyFilters

Added

  • New CMS page block plugin for better CMS page integration

Changed

  • Result block plugin improvements

SeoRichSnippets

Fixed

  • LocalBusinessService fixes

1.1.10 — 2025-07-09

SeoPrettyFilters

Fixed

  • Layered navigation block navigation state fixes
  • Result block plugin improvements

SeoRichSnippets

Fixed

  • ProductService minor improvements

1.1.9 — 2025-07-09

SeoPrettyFilters

Fixed

  • Layered navigation block navigation state cleanup

SeoRichSnippets

Changed

  • QM-109: Enhanced AbstractRichSnippetsService with better error handling and code structure

1.1.8 — 2025-07-08

SeoPrettyFilters

Added

  • New result block plugin for enhanced filtering
  • Pattern processor interface improvements

Changed

  • Enhanced abstract pattern processor with additional functionality
  • Complex pattern processor improvements
  • Short pattern processor enhancements
  • Layered navigation block navigation state improvements

SeoRichSnippets

Changed

  • QM-96: Updated license information in composer.json

1.1.7 — 2025-07-03

SeoImagesFriendlyUrl

  • Security fix: Parameterized SQL query in ModelProductGalleryCreateHandler — replaced direct string concatenation in WHERE clause with bound parameters to prevent SQL injection
  • Performance fix: Added in-memory cache to GetSeoFriendlyNameFromIndex — eliminates N+1 query problem when rendering multiple product images on a single page
  • Stability fix: Added ReflectionException catch in Image::getMisc() — prevents fatal error if parent class property changes in future Magento versions

SeoPrettyFilters

Added

  • Catalog search controller result index plugin for search page support (QM-199)
  • Request params provider interface and implementation
  • Support for search result pages with SEO-friendly URLs

Changed

  • Refactored URL generator for better modularity
  • Improved catalog product list toolbar plugin
  • Major improvements to abstract pattern processor
  • Enhanced pattern processing logic

Removed

  • Removed unused code from Router controller

SeoRichSnippets

Fixed

  • QM-91: Fixed offer generation in AbstractRichSnippetsService

1.1.6 — 2025-05-27

SeoImagesFriendlyUrl

  • Limit frontend resize generation to current store/theme when available.
  • Exclude SEO-friendly name from image cache hash to reduce duplicate variants.
  • Use indexed SEO name for product image rendering to avoid multiple URLs.

SeoPrettyFilters

Changed

  • Updated author information in composer.json and source files

SeoRichSnippets

Fixed

  • LocalBusinessService code improvements

1.1.5 — 2025-03-12

SeoPrettyFilters

Changed

  • Pattern processor improvements and refactoring (QM-128)
  • Suffix resolver optimization
  • Abstract pattern processor code cleanup
  • Enhanced configuration handling

SeoRichSnippets

Changed

  • Enhanced AbstractRichSnippetsService
  • Improved LocalBusinessService
  • Updated ReviewService with better data handling

1.1.4 — 2025-02-14

SeoPrettyFilters

Changed

  • Code improvements in select converter
  • Filter option model enhancements
  • Short attribute resolver improvements
  • Attribute fetcher refactoring and optimizations

SeoRichSnippets

Changed

  • Minor improvements and dependency updates

1.1.3 — 2025-01-15

SeoPrettyFilters

Changed

  • License update (QM-96)

SeoRichSnippets

Added

  • QM-79: Enhanced LocalBusinessService with 95+ lines of new functionality

Removed

  • Moved some functionality from OrganizationService to LocalBusinessService

1.1.2 — 2024-12-20

SeoImagesFriendlyUrl

  • Removing catalog_product_entity_media_gallery_value table dependency

SeoPrettyFilters

Fixed

  • Code cleanup and formatting improvements
  • Boolean attribute resolver fixes
  • Category path resolver improvements
  • Attribute fetcher optimizations
  • Category URL service improvements

SeoRichSnippets

Added

  • IT-31: Added source repository URL to composer.json

1.1.1 — 2024-XX-XX

SeoDynamicDescriptions

Changed

  • Updated license to Apache-2.0

SeoPrettyFilters

Added

  • Meta robots configuration options (QM-78)
  • Meta tag source model for admin configuration
  • Frontend plugin to modify meta robots tags
  • New admin system configuration for SEO meta tags

Changed

  • Extended config interface with meta robots methods
  • Enhanced config model with meta tag handling
  • Updated ACL and menu configuration

SeoRichSnippets

Added

  • QM-69: Enhanced organization schema support
  • Added configuration for organization legal name and same-as URLs
  • Improved LocalBusinessService integration with organization data

Changed

  • Updated AbstractRichSnippetsService for better organization handling
  • Enhanced i18n translations

1.1.0 — 2026-03-06

SeoDynamicDescriptions

Added

  • Enhanced filter attribute parsing that extracts selected filter values from URL parameters to use in dynamic SEO content
  • Backup field mechanism that falls back to alternative fields (like category name) when primary SEO field is empty
  • Multi-attribute support in SEO templates allowing use of multiple category attributes (e.g., {name}, {description}) in meta tags
  • ElasticSuite integration detection to properly handle layered navigation with ElasticSearch-powered filtering
  • Configuration option for moving category descriptions below pagination
  • Support for dynamic replacement of attribute placeholders with actual filtered values

Changed

  • Refactored AbstractPlugin to support multiple backup fields and more sophisticated text replacement
  • Enhanced meta title, meta description, and page heading generation with filter-aware content
  • Improved SEO filters attribute handling to dynamically insert selected filter labels into templates
  • Updated category form UI with better attribute management

Technical Implementation

  • Added formatSeo() method that processes category attributes and replaces template placeholders with real values
  • Implemented attribute text caching to improve performance when processing multiple placeholders
  • Enhanced EAV config usage to fetch attribute options and labels for dropdown/select attributes
  • Created data patches for seo_filters and seo_header category attributes

SeoHrefLang

Added

  • x-default hreflang tag: Configurable store view selection for hreflang="x-default" — tells search engines which version to show when user's language doesn't match any available store
  • Admin config field "x-default Store View" under Stores > Configuration > Qoliber SEO > HrefLangs > Settings

SeoOpengraphTags

Fixed

  • Null safety in ResolverProduct: getCurrentProduct() now returns nullable ?Product — prevents fatal error when registry has no product
  • Null safety in ResolverCategory: getCurrentCategory() now returns nullable ?Category — prevents fatal error when registry has no category
  • Product image no_selection check: Filters out Magento's no_selection placeholder value from og:image

Added

  • og:site_name tag on all pages — uses store name from admin config with fallback to store view name
  • Product-specific OG type: Product pages now render og:type="product" instead of "website"
  • Product price tags: product:price:amount and product:price:currency for richer social previews
  • Product availability: product:availability tag when product is in stock
  • Category image: og:image tag on category pages when a category image is set
  • Twitter Card support: twitter:card, twitter:title, twitter:description, twitter:image on product pages — uses summary_large_image when image is available
  • Template now correctly uses name attribute for Twitter tags and property for OG/product tags

Changed

  • All resolvers modernized to PHP 8.1 constructor property promotion
  • Tag keys now use full property names (og:title instead of title) for flexibility with mixed tag types

SeoPrettyFilters

Added

  • Source URL to composer.json (IT-31)

Changed

  • Version update to 1.1.0

SeoRichSnippets

Changed

  • Major refactoring: Migrated from Resolver pattern to Service pattern
  • Renamed RichSnippetsResolverInterface to RichSnippetsServiceInterface
  • Restructured service architecture for better maintainability

Added

  • New Services:

    • ProductService - Complete product rich snippets handling
    • WebsiteService - Website-level structured data
    • ContactPointService - Contact information structured data
    • OrganizationService - Organization schema support
    • MerchantReturnPolicyRichSnippetsService - Return policy structured data
    • AggregateRatingService - Product rating aggregation
    • BrandRichSnippetsService - Brand information
  • New Configuration:

    • Brand attribute selection
    • Product description source configuration
    • Item condition settings
    • Return policy enumeration
    • General settings with comments
    • 200+ lines of new system configuration
  • New Utilities:

    • JsonUnescaped serializer for proper JSON output

Changed

  • Enhanced AbstractRichSnippetsService with 120+ lines of improvements
  • Refactored BreadcrumbListService for better efficiency
  • Improved OfferService with enhanced offer data
  • Updated ReviewService with better review handling
  • Renamed template from richSnippets.phtml to rich-snippets.phtml
  • Updated DI configuration with new service bindings
  • Enhanced module configuration structure

Removed

  • AggregateRatingResolver (replaced with AggregateRatingService)
  • OrganizationResolver (replaced with OrganizationService)
  • ProductResolver (replaced with ProductService)

1.0.11 — 2026-07-14

AttributeSeoPath

Changed

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass.
  • Bounded the qoliber/core dependency constraint (*^1.0) so the package resolves against a compatible Core major instead of any version.
  • Declared the supported PHP versions in composer.json (~8.1.0||~8.2.0||~8.3.0||~8.4.0, previously unconstrained), including PHP 8.4; verified 8.4-clean, no code change required.
  • PHPStan level-8 cleanups (strict comparisons over empty(), redundant-cast and in_array strictness); no behaviour change.

1.0.10 — 2025-10-29

AttributeSeoPath

Fixed

  • Fixed interface implementation for admin panel compatibility with Adobe Commerce Cloud edition
  • Resolved dependency injection configuration for admin scope

Changed

  • Moved di.xml configuration to frontend scope for better separation of concerns
  • Added separate di.xml for adminhtml scope

1.0.9 — 2026-03-06

AttributeSeoPath

Changed

  • Moved di.xml to frontend directory to ensure compatibility with Adobe Commerce Cloud edition
  • Improved module configuration structure for multi-scope deployments

SeoDynamicTags

Fixed

  • DB constraint name typo: Renamed unique constraint from QOLIBER_REDIRECTS_REQUEST_PATH_STORE_ID to QOLIBER_SEO_DYNAMIC_TAGS_REQUEST_PATH_STORE_ID — fixes naming inconsistency left over from copy-paste

SeoFriendlyProductUrls

Fixed

  • 301 redirect preservation: Scoped redirect cleanup to only delete redirects created by this module (description = 'Generated by Qoliber_SeoFriendlyProductUrls'), preventing accidental deletion of manually-created 301 redirects
  • URL conflict detection: Added deduplication of generated URL keys — resolves within-batch duplicates and checks against existing URL rewrites to prevent products from overwriting each other's URLs

1.0.8 — 2025-06-23

AttributeSeoPath

Changed

  • Updated author information in all module files to Qoliber standards
  • Improved code documentation and file headers

Fixed

  • Fixed repository implementations for better data retrieval performance

SeoDynamicTags

Changed

  • Updated package author information

SeoFriendlyProductUrls

Changed

  • Package maintenance update

1.0.7 — 2025-06-23

AttributeSeoPath

Changed

  • Updated author information across all module components

SeoDynamicTags

Changed

  • Updated license to Apache-2.0

SeoFriendlyProductUrls

Changed

  • Package maintenance update

1.0.6 — 2025-XX-XX

AttributeSeoPath

Fixed

  • Fixed adminhtml URL keys tab display and functionality
  • Improved URL key management interface in attribute edit form

SeoAdvisorAi

Changed

  • Updated composer.json metadata

Fixed

  • Enhanced error handling for AI provider API failures with detailed error messages and status codes

SeoDynamicTags

Fixed

  • Tag reinitialization for package versioning

SeoFriendlyProductUrls

Changed

  • Updated package author information

SeoHrefLang

Changed

  • Updated package author information

1.0.5 — 2025-03-14

AttributeSeoPath

Changed

  • Code quality improvements and refactoring

SeoAdvisorAi

Added

  • Google Gemini AI integration as alternative to ChatGPT, offering built-in client support for Gemini 1.5 Flash and Gemini 1.5 Pro models
  • Custom Gemini API client (GeminiClient) with native support for Google's Generative Language API, including quota violation handling and retry mechanisms
  • Advanced error handling with detailed extraction of API error codes, messages, and retry-after information for better debugging
  • Multi-provider architecture refactoring that allows switching between AI providers (ChatGPT, Gemini) through system configuration
  • Configuration option to select AI model variant (Flash vs Pro for Gemini, GPT-3.5 vs GPT-4 for ChatGPT)

Changed

  • Refactored ChatGPT client to implement common ClientInterface for better extensibility
  • Enhanced AI response processing with improved Parsedown markdown-to-HTML conversion
  • Updated admin configuration to support provider selection and model configuration

SeoDynamicTags

Changed

  • Version update

SeoFriendlyProductUrls

Changed

  • Automated version update

SeoHrefLang

Changed

  • Updated license to Apache-2.0

SeoPrettyFiltersElasticSuite

Changed

  • Package maintenance update

1.0.4 — 2025-02-14

AttributeSeoPath

Fixed

  • Fixed attribute frontend input validator to properly handle different attribute types
  • Improved validation logic for SEO-friendly URL generation

SeoAdvancedSitemaps

Changed

  • Updated package author information

SeoAdvisorAi

Changed

  • Updated license to Apache-2.0

SeoDynamicDescriptions

Fixed

  • Tag reinitialization for package versioning

Added

  • Source URL to composer.json

SeoDynamicTags

Changed

  • Version update

SeoFriendlyProductUrls

Changed

  • Updated license to Apache-2.0

SeoHrefLang

Fixed

  • Tag update for package versioning

SeoImagesFriendlyUrl

  • Fixed backward compatibility issue with Magento\Catalog\Model\View\Asset\Image

SeoOpengraphTags

Changed

  • Updated license to Apache-2.0

SeoPrettyFilters

Added

  • LICENSE.md file with license information
  • Updated composer.json with license reference

SeoPrettyFiltersElasticSuite

Added

  • Enhanced pattern processing with improved handling of complex attribute combinations in filter URLs
  • Better error handling and validation in pattern processors

Changed

  • Refactored ComplexPatternProcessor to improve filter URL generation and removal logic
  • Enhanced ShortPatternProcessor with better attribute array construction for multi-select filters
  • Improved price filter handling to maintain price ranges in clean URL format

Fixed

  • Fixed issues with complex attribute resolution when combining multiple filter types
  • Corrected SEO path removal logic to ensure proper filter toggling in URLs

1.0.3 — 2025-01-15

AttributeSeoPath

Changed

  • Updated license information to Qoliber commercial license

SeoAdvancedSitemaps

Changed

  • Updated license to Apache-2.0

SeoAdvisorAi

Fixed

  • Tag reinitialization for package versioning

SeoDynamicDescriptions

Changed

  • Version update

SeoDynamicTags

Changed

  • Version update

SeoFriendlyProductUrls

Changed

  • Version update

SeoHrefLang

Changed

  • Version update

SeoOpengraphTags

Fixed

  • Tag update for package versioning

Added

  • Source URL to composer.json

SeoPrettyFilters

Changed

  • Abstract pattern processor refactoring and improvements
  • Admin menu configuration updates

SeoPrettyFiltersElasticSuite

Changed

  • Updated license to Apache-2.0

1.0.2 — 2025-01-15

AttributeSeoPath

Changed

  • Updated license information in composer.json

SeoAdvancedSitemaps

Fixed

  • Tag reinitialization for package versioning

SeoAdvisorAi

Changed

  • Updated composer.json configuration

Added

  • Source URL to composer.json

SeoDynamicDescriptions

Changed

  • Version update

SeoDynamicTags

Changed

  • Version update

Added

  • Source URL to composer.json

SeoFriendlyProductUrls

Changed

  • Version update

SeoHrefLang

Changed

  • Version update

SeoOpengraphTags

Changed

  • Version update

SeoPrettyFilters

Fixed

  • Short pattern processor fixes
  • Admin menu configuration corrections

SeoPrettyFiltersElasticSuite

Fixed

  • Tag update for package versioning

Added

  • Source URL to composer.json

SeoRichSnippets

Changed

  • Enhanced composer.json with complete package metadata
  • Added authors information
  • Added license reference

1.0.1 — 2024-XX-XX

AttributeSeoPath

Fixed

  • Fixed attribute save observer for proper URL key persistence
  • Improved EAV attribute option collection plugin for better store-specific handling

SeoAdvancedSitemaps

Fixed

  • Corrected PHP version requirement from ">-8.1" to ">=8.1" in composer.json

Added

  • Source URL to composer.json for package repository reference

SeoAdvisorAi

Added

  • AI-powered SEO advisor that analyzes product and category content using ChatGPT to provide SEO optimization recommendations
  • "Check SEO" button integration in product edit form (Catalog > Products > Edit) that opens a popup with AI-generated SEO suggestions
  • Real-time SEO analysis that evaluates meta titles, meta descriptions, product descriptions, and keywords for search engine optimization best practices
  • ChatGPT integration service with configurable API key and model selection (GPT-3.5-turbo, GPT-4) through admin system configuration
  • Markdown-to-HTML conversion of AI responses using Parsedown library for clean, formatted SEO suggestions in admin panel
  • Dynamic prompt assembly that sends product/category attributes (name, description, meta data) to AI for contextual analysis
  • AJAX-based popup interface that displays SEO recommendations without page reload, improving admin user experience
  • Admin system configuration panel (Stores > Configuration > Qoliber SEO > AI Advisor) for API credentials and provider settings
  • ACL resource for controlling access to SEO advisor functionality
  • Generative AI service architecture with provider abstraction for future AI integrations

Technical Implementation

  • Created Service\ChatGpt class for OpenAI API communication with configurable models
  • Implemented Controller\Adminhtml\Advise\Popup for handling AJAX requests and AI response processing
  • Added Block\Adminhtml\Product\Edit\Button\CheckSeo for product form button integration
  • Configured admin routes for SEO advisor popup functionality
  • JavaScript component (view/adminhtml/web/js/check-seo.js) for handling button clicks and popup display
  • UI component XML configuration to inject button into product form layout

SeoDynamicDescriptions

Changed

  • Version update

SeoDynamicTags

Changed

  • Version update

SeoFriendlyProductUrls

Changed

  • Version update

SeoHrefLang

Changed

  • Version update

SeoImagesFriendlyUrl

  • Qoliber_Core module dependency has been added.

SeoOpengraphTags

Changed

  • Version update

SeoPrettyFilters

Changed

  • Pattern processor interface improvements
  • Abstract pattern processor enhancements
  • Complex pattern processor updates
  • Short pattern processor improvements

SeoPrettyFiltersElasticSuite

Changed

  • Version update

SeoRichSnippets

Fixed

  • Minor configuration corrections in system.xml
  • Version bump in composer.json

1.0.0 — 2024-11-17

AttributeSeoPath

Added

  • Initial release of Attribute SEO Path module
  • Implemented SEO-friendly URL paths for product attribute filter values
  • Added custom URL key management for attribute options in admin panel
  • Implemented database schema for storing attribute-specific URL keys (qoliber_attribute_url_keys table)
  • Added database schema for storing option-specific SEO paths (qoliber_option_seo_path table)
  • Implemented AbstractFilter plugin for transforming filter URLs to SEO-friendly format
  • Added FilterInterfacePlugin for layered navigation URL optimization
  • Implemented attribute URL keys repository for managing custom attribute URLs
  • Added option SEO path repository for managing option-specific URL rewrites
  • Implemented adminhtml UI component for managing attribute URL keys in attribute edit form
  • Added frontend input validator to restrict SEO path functionality to applicable attribute types
  • Enabled store-view specific SEO URL configuration for multi-store environments
  • Added admin tab in attribute edit interface for configuring SEO URL keys
  • Implemented automatic URL key generation based on attribute option labels
  • Added support for SEO-friendly URLs in catalog layered navigation

SeoAdvancedSitemaps

Added

  • Advanced sitemap splitting functionality that generates separate XML files for different entity types (products, categories, CMS pages) instead of one large sitemap file
  • Automatic sitemap index generation that creates a master sitemap.xml file referencing all individual entity sitemaps for better organization and crawlability
  • Enhanced CMS page sitemap filtering with custom use_in_sitemap attribute, allowing administrators to explicitly control which CMS pages appear in sitemaps through the admin panel
  • Custom sitemap file naming convention where each entity type gets its own file (e.g., categories.xml, products.xml, pages.xml) for easier identification and management
  • Multi-sitemap support for large stores that prevents sitemap file size limitations by automatically splitting large entity collections across multiple numbered files
  • Configurable module toggle in admin system configuration (Stores > Configuration > Qoliber SEO > Advanced Sitemaps) to enable/disable advanced sitemap features
  • Automatic sitemap generation for all stores via data patch that creates sitemap configurations for existing store views during module installation
  • Admin UI integration with dedicated ACL resource for sitemap management permissions
  • Plugin architecture for intercepting default Magento sitemap generation and applying custom filtering rules
  • Value filter plugins for categories, CMS pages, and products to organize sitemap items by entity type
  • Category form UI component integration for managing category-specific sitemap settings

Technical Implementation

  • Extended \Magento\Sitemap\Model\Sitemap to override XML generation logic and implement entity-based file splitting
  • Implemented plugin for Magento\Sitemap\Model\ResourceModel\Cms\Page::getCollection() to filter pages based on use_in_sitemap attribute
  • Created abstract value filter with separate implementations for each entity type (Category, Product, CmsPage)
  • Added database schema extension with use_in_sitemap column in cms_page table
  • Configured dependency injection for sitemap model preference and plugin interception points

SeoAdvisorAi

Added

  • Initial project structure with .gitignore

SeoDynamicDescriptions

Added

  • Dynamic category SEO content generation that automatically modifies meta titles, meta descriptions, headings, and descriptions based on selected layered navigation filters
  • Template-based SEO customization using custom category attributes (seo_filters, seo_header) where merchants can define templates with attribute placeholders
  • Filter-aware meta tag generation that updates page meta information when customers apply category filters, improving SEO for filtered category pages
  • Automatic placeholder replacement that substitutes category attribute codes (e.g., {name}, {color}) with actual filtered values
  • ElasticSuite compatibility with detection of ElasticSearch-powered layered navigation
  • Plugin architecture intercepting category data retrieval for description, meta_description, meta_keywords, meta_title, page_heading, and custom SEO header
  • Admin configuration toggle (Stores > Configuration > Qoliber SEO > Dynamic Descriptions) to enable/disable module
  • Category form UI components for managing SEO filter templates and header templates
  • Data patches creating seo_filters and seo_header category attributes during installation

Technical Implementation

  • Created Plugin\AbstractPlugin base class with common SEO formatting logic
  • Implemented aroundGetData() interceptor for Magento\Catalog\Model\Category to modify SEO fields dynamically
  • Added filter manager integration for text processing and URL-safe formatting
  • EAV configuration for retrieving attribute options and labels for template substitution
  • Request parameter parsing to extract active filter selections from layered navigation

SeoDynamicTags

Added

  • Dynamic meta tag injection system that allows administrators to create custom meta tags (meta description, meta keywords, Open Graph tags) for any page type via admin panel
  • URL pattern matching that applies meta tags based on configurable URL patterns (exact match or wildcard patterns) for flexible page targeting
  • Admin grid interface (Marketing > SEO > Dynamic Tags) for managing SEO tag rules with CRUD operations (create, read, update, delete)
  • Page type filtering with support for different indexable types (products, categories, CMS pages, custom pages) to target specific content
  • Priority-based tag application where multiple matching rules can be defined with tags applied based on configuration order
  • Active/inactive status control allowing administrators to enable/disable tag rules without deletion
  • Meta tag override functionality that intercepts Magento's default meta tag rendering and injects custom values when URL patterns match
  • Customer group context awareness that evaluates current customer session for personalized tag application
  • Database-backed tag storage with qoliber_seo_dynamic_tags table for persistent tag configurations
  • Admin UI components with listing grid and form for intuitive tag management
  • ACL resources for granular permission control over dynamic tag management
  • Action column in listing grid with edit and delete actions for quick management

Technical Implementation

  • Created Model\SeoDynamicTag entity model with resource model and collection for database operations
  • Implemented Plugins\App\ModifyMetaTags as around plugin for ActionInterface::execute() to intercept page rendering
  • Built admin controller structure (Index\Add, Index\Edit, Index\Save, Index\Delete) for full CRUD functionality
  • Added SeoDynamicTagsDataProvider for UI component data provisioning
  • Configured UI components (seodynamictags_listing.xml, seodynamictags_form.xml) with columns for URL pattern, page type, status, and meta tag content
  • Database schema with fields for url_pattern, indexable_type, text (meta content), is_active, and sort_order
  • Frontend DI configuration for plugin interception on layout result pages
  • Admin menu integration under Marketing > SEO section

SeoFriendlyProductUrls

Added

  • Pattern-based product URL generation that creates SEO-friendly product URLs using custom attribute combinations (e.g., {brand}-{name}-{sku})
  • Console command for bulk URL regeneration (bin/magento qoliber:seo:regenerate-urls) with options for entity ID range and specific stores
  • Attribute placeholder system where merchants configure URL patterns using attribute codes in curly braces (e.g., {color}-{size}-{product_name})
  • Multi-attribute URL composition that combines product attributes (text, select, multiselect) into structured, keyword-rich URLs
  • Store-specific URL generation with separate URL patterns configurable per store view for multilingual sites
  • Entity ID range filtering for processing specific product ID ranges during regeneration to optimize performance
  • URL rewrite integration that creates proper Magento URL rewrites when regenerating product URLs
  • Select and multiselect attribute support that converts option IDs to human-readable labels in URLs (e.g., "red" instead of "93")
  • URL key validation and filtering to ensure generated URLs comply with web standards (lowercase, hyphenated, URL-safe characters)
  • Configurable URL pattern via admin system configuration (Stores > Configuration > Qoliber SEO > Friendly Product URLs)
  • Disabled module safety mechanism that throws DisabledModuleException when attempting operations with module disabled
  • Single-store mode detection that automatically filters operations to current store in single-store installations
  • Repository architecture for efficient attribute data fetching with separate repositories for products, attributes, and store data
  • Filter system for validating attribute codes, entity IDs, store IDs, and URL patterns before processing
  • Service layer (Regenerator, UrlKeyGenerator, Reindexer) for orchestrating URL generation workflow

Technical Implementation

  • Created Service\UrlKeyGenerator that parses patterns and replaces placeholders with actual product attribute values
  • Implemented Service\Regenerator orchestrating bulk URL regeneration with batch processing support
  • Built repository layer for product entity IDs, attribute codes, SKUs, and URL rewrite data
  • Added Console\Command\RegenerateUrls with options: --entity-id-from, --entity-id-to, --store-id
  • Configured filter chain (ExistingAttributeCodeFilter, MaxProductEntityIdFilter, MinProductEntityIdFilter, UrlKeyFilter) for data validation
  • Created Repository\Product\Attribute\OptionsRepository for fetching select/multiselect attribute labels
  • Implemented Repository\Product\Attribute\ProductUrlRewriteRepository for URL rewrite persistence
  • Added admin configuration backend model for pattern validation
  • Configured DI for filter repository and service classes with proper dependency injection

SeoHrefLang

Added

  • Automatic hreflang tag generation for multi-language stores that helps search engines understand language and regional targeting of pages
  • Product page hreflang support that generates alternate URLs for the same product across different store views and languages
  • Category page hreflang support that creates language alternatives for category pages to prevent duplicate content penalties
  • CMS page hreflang support for static content pages with language/region variants
  • Contact page hreflang support with dedicated resolver for contact form pages across stores
  • Homepage hreflang support that generates alternate language versions for store homepages
  • Automatic locale detection that converts Magento locale codes (e.g., en_US) to proper hreflang format (en-us)
  • Store-specific URL resolution using dedicated resolvers for different page types (ProductUrlResolver, CategoryUrlResolver, CmsPageUrlResolver, ContactPageUrlResolver)
  • Configurable store inclusion via admin panel (Stores > Configuration > Qoliber SEO > HrefLang) to select which stores appear in hreflang tags
  • x-default tag support for specifying default language version when user's language preference doesn't match available options
  • Template-based output with dedicated phtml template for rendering hreflang link tags in HTML head section
  • Checkout page exclusion via layout XML to prevent hreflang on checkout pages where it's not beneficial
  • ViewModel architecture (HrefLang) that coordinates tag generation and passes data to templates
  • AlternateUrlService that determines current page type and dispatches to appropriate URL resolver
  • Request-aware resolver pattern that identifies page type from full action name (e.g., catalog_product_view, cms_page_view)

Technical Implementation

  • Created Service\AlternateUrlService that uses action name matching to select appropriate URL resolver
  • Implemented URL resolver interfaces with concrete classes for product, category, CMS page, and contact page
  • Built ViewModel\HrefLang that fetches allowed stores, generates alternate URLs, and formats locale codes
  • Added Model\Config with methods for retrieving store configurations, locale codes, and base URLs
  • Configured layout XML (view/frontend/layout/default.xml) to inject hreflang template in page head
  • Template file (view/frontend/templates/hreflang.phtml) rendering link rel="alternate" tags for each language
  • DI configuration mapping action names to resolver implementations
  • ACL and admin menu structure for module configuration access
  • Multi-store locale code transformation (underscore to hyphen, lowercase formatting)

SeoImagesFriendlyUrl

  • initial module release

SeoOpengraphTags

Added

  • Open Graph meta tag generation for product, category, and CMS pages to optimize social media sharing previews
  • Product-specific OG tags including og:title, og:description, og:image, og:url, and og:type for rich product previews on Facebook, Twitter, LinkedIn
  • Automatic product image detection that extracts product base image URL and adds it to og:image tag for visual social sharing
  • Category page OG tag support with title, description, and URL tags for category landing pages
  • CMS page OG tag support for static content pages to ensure proper social media representation
  • Default tag resolver that provides fallback OG tags for pages without specific resolvers
  • Resolver pattern architecture with page-type-specific tag generators (ResolverProduct, ResolverCategory, ResolverDefault)
  • Configurable og:title prefix via admin configuration (Stores > Configuration > Qoliber SEO > Open Graph Tags) to add site branding to shared titles
  • Automatic meta description fallback where product/page meta descriptions are used for og:description when available
  • Default Open Graph tag removal observer that strips Magento's default OG tags to prevent duplication
  • Prefix addition observer that prepends configured text to og:title tags (e.g., "MyStore - Product Name")
  • Template-based rendering with dedicated phtml template for clean OG tag output in HTML head
  • Multiple layout handles (qoliber_opengraph, qoliber_product_opengraph) for different page type contexts
  • ViewModel (OpengraphTags) that coordinates resolver selection and tag generation
  • Admin configuration toggle to enable/disable Open Graph tag generation globally

Technical Implementation

  • Created Service\GetTagResolver that determines page type and returns appropriate resolver implementation
  • Implemented Model\ResolverInterface with concrete classes for products, categories, and default pages
  • Built Model\ResolverProduct that fetches current product from registry and extracts meta description and image URL
  • Added Model\ResolverCategory for category-specific tag generation
  • Created Observer\RemoveDefaultTags (event: layout_load_before) to remove default Magento OG tags
  • Implemented Observer\SetPrefix (event: layout_generate_blocks_after) to add configurable prefix to titles
  • Configured ViewModel\OpengraphTags for checking module status and getting appropriate resolver
  • Template file (view/frontend/templates/opengraph.phtml) iterating through tags and rendering meta property elements
  • DI configuration for resolver pool with product, category, and default implementations
  • Layout XML files for injecting OG template on product and generic pages
  • ACL resource for admin access control

SeoPrettyFilters

Added

  • Initial release of SEO Pretty Filters module
  • SEO-friendly URL generation for Magento 2 layered navigation
  • Support for multiple URL generation modes (short and complex)
  • Attribute to SEO path converter interface and implementations
  • Filter option management
  • URL resolver and generator
  • Category path resolver
  • Pattern processors for URL generation
  • Attribute resolver system
  • Boolean attribute resolver
  • Price resolver
  • Suffix resolver
  • Service layer for attribute fetching and URL generation
  • Admin interface for attribute SEO configuration
  • Frontend plugins for catalog layer filter items
  • Swatches support for layered navigation
  • Product list toolbar integration
  • Configurable URL patterns
  • Support for select, price, and yes/no attribute types

SeoPrettyFiltersElasticSuite

Added

  • ElasticSuite integration for Qoliber SeoPrettyFilters that extends SEO-friendly filter URL functionality to Smile ElasticSuite module
  • Complex pattern processing for ElasticSuite-specific filter attribute handling with support for multi-select filters
  • Short pattern processing for simplified filter URL generation compatible with ElasticSuite's architecture
  • Attribute fetcher plugin that intercepts ElasticSuite attribute retrieval to apply SEO transformations
  • Select dropdown converter plugin that modifies select/dropdown attribute rendering to use SEO-friendly URLs
  • Price filter support with special handling to maintain price range values in clean URL format (e.g., /price-100-200/ instead of /price?min=100&max=200)
  • Multi-select filter URL generation that combines multiple selected values into cohesive URL segments
  • Filter removal URL generation that creates proper URLs when deselecting filter options while maintaining other active filters
  • Complex attribute array construction that builds hierarchical filter structures for nested attribute combinations
  • Frontend input type detection to handle different attribute types (price, select, multiselect) with appropriate URL patterns
  • Plugin architecture specifically designed for Qoliber\SeoPrettyFilters compatibility with ElasticSuite
  • Dependency on both qoliber/seo-pretty-filters and smile/elasticsuite modules for proper operation

Technical Implementation

  • Created Plugin\PatternProcessor\ComplexPatternProcessor that intercepts complex URL pattern generation and removal
  • Implemented Plugin\PatternProcessor\ShortPatternProcessor for simplified pattern handling
  • Built Plugin\AttributeFetcher to modify attribute data retrieval for SEO purposes
  • Added Plugin\ComplexAttributeResolver for resolving complex multi-attribute filter combinations
  • Created Plugin\ShortAttributeResolver for simple attribute resolution
  • Implemented Plugin\ConverterSelect to convert select attribute rendering to use SEO URLs
  • Configured around plugins for aroundGenerateFilterItemUrl() and aroundGenerateRemoveItemUrl() methods
  • Added specialized handling for price range filters to preserve min/max values in URL structure
  • DI configuration with plugin interception points for ElasticSuite and SeoPrettyFilters classes
  • Attribute code to frontend input mapping for determining appropriate URL generation strategy

SeoRichSnippets

Added

  • Initial release of Qoliber SEO Rich Snippets module

  • Core Features:

    • Product structured data support
    • Breadcrumb list schema
    • Organization schema
    • Local business schema
    • Aggregate rating support
    • Product offers with pricing
    • Product reviews integration
  • Resolvers:

    • AbstractRichSnippetsResolver base class
    • ProductResolver for product schema
    • BreadcrumbListResolver for navigation
    • LocalBusinessResolver for business information
    • OrganizationResolver for organization data
    • AggregateRatingResolver for ratings
    • OfferResolver for product offers
    • ReviewResolver for product reviews
  • ViewModel:

    • RichSnippets ViewModel for template rendering
  • Frontend:

    • Layout integration for default pages
    • Checkout page support
    • JSON-LD output template
  • Admin:

    • ACL permissions for module access
    • Admin menu integration
    • Basic system configuration
  • Module Structure:

    • Proper module registration
    • Dependency injection configuration
    • Basic configuration values

0.9.0 — 2026-07-18

SeoCanonical

Added

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass.

  • Initial release: the single canonical authority for the Qoliber SEO Suite (battleplan §5.2 / §6 Phase 0, promoted T2-B). Implements the platform's Qoliber\SeoPlatform\Api\CanonicalPolicyInterface; depends only on qoliber/seo-platform.

  • Model\CanonicalResolver — the root of the per-page-type decision tree and the one <preference> for CanonicalPolicyInterface. Walks DI-registered strategy branches in priority order; falls back to a self-referential canonical when the policy is disabled or the subject is unclassified. Every path returns a CanonicalDecision carrying a human-readable reason string.

  • Api\CanonicalStrategyInterface + four branches:

    • Model\Strategy\ProductCanonicalStrategy — configurable child → parent (admin option qoliber_seo_canonical/product/configurable_child_to_parent), else self.
    • Model\Strategy\CategoryCanonicalStrategy — facet-aware placeholder: unapproved facet combinations canonicalise to the facet-free category root.
    • Model\Strategy\CmsPageCanonicalStrategy — self-referential (facet-free).
    • Model\Strategy\SearchCanonicalStrategy — canonical to the store base URL.
  • Api\CanonicalUrlResolverInterface + Model\CanonicalUrlResolver — builds the absolute self / facet-free root / base / configurable-parent URLs the tree emits.

  • Api\FacetApprovalPolicyInterface + Model\FacetApprovalPolicy — conservative, allowlist-driven placeholder that approves no facet combination until qoliber/seo-facet-governance binds the real per-category rules.

  • Model\PageContext\SearchResultsContext — entity-less marker for search pages.

  • Model\Config — store-scoped reader; etc/config.xml defaults (policy enabled, configurable child → parent enabled); etc/adminhtml/system.xml decision-tree configuration under the Qoliber tab; etc/acl.xml config resource.

SeoPlatform

Added

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass.

  • Initial control-plane contract for the Qoliber SEO Suite (battleplan §5.2). Depends only on qoliber/core + qoliber/seo-common; ships no UI.

  • Model\UrlIdentity — immutable (url, storeId) value object with a stable key() and equals(); the normalisation invariant every module shares.

  • Api\Data\SignalContributionInterface + Model\SignalContribution — one provider's partial, per-URL signal contribution (immutable value object).

  • Model\UrlState — the aggregated, resolved per-URL signal state (httpStatus, canonical, robots, sitemapMembership, hreflangCluster, structuredDataStatus, indexNowStatus) with contributing-source provenance.

  • Api\UrlSignalProviderInterfacecontribute(UrlIdentity): ?SignalContribution; each feature module registers one via di.xml.

  • Model\UrlStateResolver — aggregates all DI-registered providers into a single UrlState (first non-null contribution wins, by registry order).

  • Api\CanonicalPolicyInterface + Model\CanonicalDecision — the single canonical authority contract; every decision carries a reason string.

  • Api\IndexabilityPolicyInterface + Model\IndexabilityDecision — the facet/pagination indexability contract; every decision carries a reason.

  • Model\SeoFinding — the audit currency (severity, impact, message, url).

  • Api\UrlRewriteWriterInterface + Model\UrlRewriteWriter — THE single owner of url_rewrite writes, savepoint-atomic per entity (product) group, plus Api\Data\UrlRewriteRequestInterface / Model\UrlRewriteRequest and Model\UrlRewriteWriteResult.

SeoRedirects

Added

  • Declared PHP 8.5 support (~8.5.0 added to the php constraint); verified 8.5-clean — php8.5 -l and the unit suite both pass.

  • Initial redirect + 404 lifecycle manager (battleplan Phase 1). Depends on qoliber/seo-platform; consumes its single url_rewrite writer — never writes the url_rewrite table directly.

  • etc/db_schema.xmlqoliber_seo_redirect (301/302/410 rules, unique on request_path + store_id, hit counter) and qoliber_seo_404_log (frequency + referrer + user-agent, unique on request_path + store_id).

  • Service contracts — Api\Data\RedirectInterface, Api\RedirectRepositoryInterface (+ Model\RedirectRepository) and Api\RedirectLookupInterface (+ DB-backed Model\RedirectFinder).

  • Model\RedirectPathNormalizer, Model\RedirectMatcher (exact + longest-prefix wildcard) and Model\RedirectChainResolver — folds a redirect chain to its terminal target with a visited-set loop guard and a configurable hop cap.

  • 404 handling — Observer\NotFoundHandler on the no-route predispatch serves a matching active redirect (301/302/410) via Model\NotFound\Responder or records the miss through Model\NotFoundLogger.

  • Auto-redirect — Observer\{Product,Category,CmsPage}UrlKeyChange turn a URL-key change into a 301 old → new through Model\AutoRedirect\RedirectRegistrar, which writes exclusively via the platform's UrlRewriteWriterInterface.

  • Admin UI — listing grid (qoliber_seo_redirect_listing) with inline edit, mass delete and per-row edit/delete, a create/edit form, menu.xml, acl.xml, routes.xml, system configuration, and the CRUD controllers.

  • CLI — qoliber:seo-redirects:import and qoliber:seo-redirects:export (CSV) backed by the pure Model\Csv\RedirectCsvConverter.

  • Unit tests for the chain/loop guard, the redirect-match lookup, and the CSV import/export round-trip.

Changelog — SEO Suite — SEO Suite — Extensions | qoliber Docs