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.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. Removed deprecatedReflection::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 newModel\SnippetCacheTagsservice 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, mirroringSnippetExpander) and cycle-safe. Theqoliber_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-configuredcustom_urlwildcard 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 throughpreg_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:upgradefailure (implicitly-nullable parameter).Model\ResourceModel\Snippet\Grid\Collection::setItems()declaredarray $items = null— an implicitly-nullable parameter that PHP 8.4 deprecates (E_DEPRECATEDat compile time), failingsetup:upgradeon 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 underQoliber_Coreresources; 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/frameworkversion 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
CategoryValueResolverexposes{{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 thecurrent_categoryregistry entry Magento sets on category pages — the same self-contained pattern asStoreValueResolver— so{{category.name}}resolves on category pages with no block wiring. Registered underVariableExtractorwith prefixcategory. CategoryVariablesprovider — a first-class variable provider registered in both the admin schema-builder pool (SchemaVariables) and theJsonLdrender pool. Category variables now appear in the snippet-builder's variable picker and populate at render time. ImplementsKeyedVariableProviderInterface(getProvidedKeys()returns['category']), sort order 12.- Dedicated brown color for the
categoryvariable group in the admin variables panel (.categoryblock in the schema-editor styles).
Tests
Test/Integration/Model/ValueResolver/CategoryValueResolverTest(14 cases) andTest/Integration/Model/VariableProvider/CategoryVariablesTest(8 cases), plus thecategory_simplefixture 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 defaultmerchant-return-policysnippet now emitsapplicableCountry(ISO 3166-1 alpha-2, sourced from{{config.general.country.default}}so each store's configured country flows through automatically) anditemCondition(defaults tohttps://schema.org/NewCondition).applicableCountryis 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
UpdateMerchantReturnPolicy2025Fieldsrewrites the existing snippet on upgrade. Follows the same overwrite-unconditionally pattern asUpdateOfferStockAvailabilityandUpdateOfferAggregateOffer. 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. InstallTier1Snippetsupdated so fresh installs get the 2025-compliant snippet directly.Qoliber\SeoRichSnippets\Api\KeyedVariableProviderInterfacewithgetProvidedKeys(): 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 callsetActive(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 freshsetup:upgraderuns see the new default.
Fixed
- Multistore cache poisoning —
getCacheKeyInfo()now includesstore_idso stores sharing a URL don't share the same JSON-LD cache entry. - Enabled flag honored —
JsonLdblock now checksqoliber_seo_rich_snippets/settings/enabledat runtime; layout also gates the block viaifconfig, 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
CircularDependencyValidatoris now invoked before persisting a snippet edit. Admin can no longer save a snippet that includes itself or creates a dependency loop.
Performance
JsonLdblock skips unused variable providers. PreviouslygetProviderContext()invoked every registeredVariableProviderInterfaceper snippet render, regardless of whether the snippet template referenced its output. On product pages that meantBreadcrumbsVariableProviderrendered the breadcrumbs block (iterating the product's category path) andCollectionVariablesloaded the reviews collection with rate votes — even when no active snippet used{{breadcrumbs}},{{productReviews}}, etc.ProductVariablesalso redundantly enumerated every product attribute and re-queried reviews already covered byProductValueResolver.- 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 adoptKeyedVariableProviderInterfacestill run unconditionally — no breakage. providerOutputCachememoizes each provider'srenderVariables()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:applicableCountrywired to the Magento config placeholder,itemConditiondefault, existing policy fields preserved, resulting template is still valid JSON, patch is idempotent on re-run.Test/Integration/Block/JsonLdProviderFilteringTestand updates toVariableExtractorTestcovering the provider-skip invariant and the new@djson for X as Yextraction pattern.
2.2.4 — 2026-08-06
SeoImagesFriendlyUrl
Fixed — 2026-08-06
- Magento 2.4.9
setup:di:compilefatal (core signature change) — Magento 2.4.9 widened coreMagento\MediaStorage\Service\ImageResize::resizeFromImageName()from(string $originalImageName)to(string $originalImageName, bool $skipHiddenImages = false), extending its existing$skipHiddenImageswebsite-filter optimisation into this method. Our<preference>override still declared the one-arg signature, so on 2.4.9 PHP raised "Declaration ofQoliber\SeoImagesFriendlyUrl\Service\ImageResize::resizeFromImageName(string): voidmust be compatible withMagento\MediaStorage\Service\ImageResize::resizeFromImageName(string, bool $skipHiddenImages = false)" andbin/magento setup:di:compilefatalled. The override now declares the matchingresizeFromImageName(string $originalImageName, bool $skipHiddenImages = false): voidand forwards$skipHiddenImagestoparent::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'sgetSeoFriendlyValue()— unlikeresolveAltTagSeoValue()— was never gated, so the alt tag was rewritten regardless of the flag). A newisModuleEnabled()accessor exposes the flag to the template.view/frontend/templates/product/view/gallery.phtmlcalled the SEO view model unconditionally. When the module is disabled it now emits Magento's defaultalt="main product photo"and omits the SEOtitleattribute, producing output byte-for-byte identical to the coreMagento_Catalog::product/view/gallery.phtmltemplate (the JSON-init<script>closing braces were also re-indented to match core; functionally unchanged). Enabled behaviour is unchanged.
- Tests —
Test/Unit/ViewModel/Product/View/GalleryTest(4 cases: resolver runs when enabled, input returned untouched when disabled, empty input stays empty when disabled,isModuleEnabled()delegation) andTest/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) andcatalog_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 mediavalue_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 thestore_id = 0SEO 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_varcharand the media-gallery value table, so editing a select attribute (e.g.color, whose value lives incatalog_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) andcatalog_product_entity_text(multiselect) are added, keyed by the product entity id.decimal/datetimevalue tables are deliberately not subscribed — the available-attributes source model only offersselect/multiselect/textfrontend inputs (→int/text/varcharbackend types), so they are unreachable. - Reindex when the filename template or available-attributes config changes — the generated names depend on
image_names_templateandavailable_attributes, but changing them left the scheduled index stale until the next unrelated edit. A newModel\Config\Backend\InvalidateSeoImageIndexbackend model on those two config fields marks the indexer invalid (viaIndexerRegistry) 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
imageno longer exists incatalog_product_entity_media_gallery(adeleteFromSelectanti-join), reaping the rows for deleted images regardless of store scope. - Atomic full reindex (no more empty index on mid-run failure) —
executeFull()used totruncateTable()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 (ActiveTableSwitcher—RENAME 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 underQoliber_Coreresources, so a standalone package needs it explicitly (previously only the root metapackage provided it).
SeoPrettyFilters
Changed
- Declared PHP 8.5 support (
~8.5.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. Removed deprecatedReflection::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 offQoliber_Coreresources, 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/frameworkconstraint); PSR-12 code-style cleanup (no functional change).
SeoPrettyFilters
Fixed
- Category pretty URLs 404 (critical).
CategoryPathResolverbound an array[storeId, 0]to a single namedstore_idplaceholder; PDO does not expand array binds, so the lookup matched no category. Now expands the values into the SQL viastore_id IN (?). - Pagination / sort / limit links pointed back at the current page. The
ProductListToolbarpager plugin ignored the target$paramsit was handed; those now override the current request's values on the generated pretty URL. Implemented via an optional override argument on the concreteRequestParamsProvider— the@apiRequestParamsProviderInterfaceis unchanged. - Complex-mode routing accepted invalid, non-opted-in, and arbitrary segments.
ComplexAttributeResolveraccepted unknown attribute codes as raw params, resolved invalid option values to id0, and consumed any plain segment without anattribute:valueform (so/category/anything.htmlresolved) — 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 byflat_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_filterflag and matched substrings.BooleanAttributeResolverparsed every filterable boolean attribute (regardless of the flag) and matched codes as substrings; it now respectsis_pretty_filterand matches whole tokens only.
Removed
- Dead
relattribute admin config. The brokenreladmin field (asmallintcolumn that storednofollow/noindexstrings 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.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. - Complex-mode routing accepted invalid, non-opted-in, and arbitrary segments. The
ComplexAttributeResolverplugin now rejects unknown attribute codes, invalid option values, and plain segments without anattribute:valueform (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
FlatAttributeResolverandFlatPatternProcessorplugins now order segments byflat_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.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. Removed deprecatedReflection::setAccessible()calls (no effect since PHP 8.1, deprecated in 8.5). - New
Service\GenerativeAi\VerifiableClientInterface(extendsClientInterface) declaringverifyConnection(). The four shipped services implement it andVerify\Modelschecksinstanceof— a non-verifiable provider gets a clean "This provider does not support verification." response. Backward-compatible:ClientInterfacekeeps 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 connectionadmin 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 newService\ResponseSchemaValidator(size-capped before decode; must be a JSON object with exactlymeta_title,meta_description,meta_keywordsas 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 responses —
Advise\Popupstrips markdown code fences beforejson_decode; reconciled the meta-keywords field name (meta_keyword→meta_keywords) across the default prompt, JS and controller whitelist; corrected thecheck-seo.jsdescription / short-description selectors to include Magento'sproduct[...]field-name prefix.
SeoImagesFriendlyUrl
{category.name}template variable — newCategoryResolverexposes{category.name}in the SEO image-name and alt-tag templates, resolving to the current category name from Magento'scurrent_categoryregistry. 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 onTemplateProcessoralongside theproductandstoreresolvers. 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.- Tests —
Test/Unit/Resolver/CategoryResolverTest(5 cases: registry hit, empty registry, non-category value, id-less category, passed-model ignored) andTest/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.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. - IndexNow ownership verification now works — the key file resolves at the site root (
/{key}.txt) via a dedicatedController\Routerregistered ahead of the standard/CMS routers (previously only reachable through the Magento frontName route, which search engines cannot use, leaving ownership unverifiable). Submissions also carrykeyLocation(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_rewritetable (storeBaseUrl + request_path) through a singleModel\CanonicalUrlResolver(backed by a pure, unit-testedModel\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 ofbaseUrl + 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) incomposer.jsonandmodule.xml.
SeoPrettyFilters
Changed
- Selective SEO-friendly filters (opt-out, backward-compatible):
is_pretty_filternow 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_filterdefaults to1), so existing storefronts are unchanged. Set Use for Pretty Filter → No on an attribute to keep it as a standard?attr=valuequery parameter instead. (Opt-in-by-default —is_pretty_filterdefaulting to0— 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=5routing.
Fixed
getActiveFiltersno longer fatals on an active filter whose frontend input has no converter (guarded array access).RequestParamsProvider::addToUrlno 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, thegetActiveFiltersguard, the filter-item + swatch plugins,RequestParamsProviderpreservation, the clear-all lock,AttributeFetcherpretty-only filtering, and the combine + hybrid-route behaviour matrix.
SeoPrettyFiltersElasticSuite
Added
- The
AttributeFetcherplugin now applies the sameis_pretty_filterselectivity 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 reimplementstransformArrayForFilters, 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 togenerateAttributeArray.
Changed
- Requires
qoliber/seo-pretty-filters: ^2.2(uses the newPrettyFilterCheckerInterface).
SeoRichSnippets
Added
- AggregateOffer support for complex product types — configurable, bundle, and grouped products now render
AggregateOfferschema withlowPrice,highPrice, andofferCountinstead of a simpleOffer - 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— returnsnullfor simple/virtual products - Public
isComplexProduct()method onProductValueResolverfor plugin extensibility UpdateOfferAggregateOfferdata patch — updates theoffersnippet template with@djson if/elseconditional 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, andFullJsonOutputTest(total: 172 tests, 789 assertions)
2.1.3 — 2026-07-18
SeoDynamicTags
Fixed
- Declared PHP 8.5 support (
~8.5.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. Removed deprecatedReflection::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/ModifyMetaTagsRobotsTestlocking 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.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. - External video players emitted as media files —
VideoSitemapGeneratorrendered 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
lastmodnot W3C Datetime — the index<lastmod>usedY-m-d H:i:sinstead of the W3C Datetime required by the sitemaps protocol; it now emitsY-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>— theurl_rewritejoin accepted any non-redirect rewrite, so a category-scoped rewrite (…/category/{id}intarget_path) could be emitted instead of the product's root canonical URL. The query now excludes category-scoped rewrites and the root canonicalrequest_pathis 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);HreflangSitemapGeneratorconstructor-injectsQoliber\SeoHrefLang\Model\Config. - Declared the direct dependency on
qoliber/core(^1.0); the admin menu/ACL are defined underQoliber_Coreresources, so a standalone package needs it explicitly (previously only the root metapackage provided it).
SeoCommon
Fixed
- Declared PHP 8.5 support (
~8.5.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. Service/Html/Stripper::toPlainText()decoded HTML entities after stripping tags, so encoded markup such as<img src=x onerror=alert(1)>passed throughstrip_tags()unchanged (no literal tag present) and was only turned into a live<img>tag afterwards byhtml_entity_decode(). Entities are now decoded first — repeating the decode pass until the value stabilises, to also neutralise double-encoded markup like&lt;script&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 underQoliber_Coreresources, so a standalone package requires it explicitly; previously only the root metapackage provided it.
SeoFriendlyProductUrls
Fixed
- Declared PHP 8.5 support (
~8.5.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. Removed deprecatedReflection::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 foreigncms_pageonred-1.html) and collide or overwrite it viainsertOnDuplicateon persistence. Collision resolution now runs through a dedicatedService\CollisionResolverthat 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 (plainINSERT, notinsertOnDuplicate) through a dedicatedService\CollisionRetryPersister: on a duplicate-key /AlreadyExistsException/ integrity violation it reloads the now-occupied paths, re-resolves the key to the next free suffix — keeping theurl_keyattribute in step — and retries, bounded by a configurable cap (maxRetries, default 3, viadi.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_keyattribute — inside the brand-new-rewrite persistence theurl_keyvarchar was written before the (racy)url_rewriteinsert, so when a concurrent race exhausted the retry cap the last attemptedurl_keystayed committed with no matching rewrite row (inconsistent state). The dependenturl_keysync is now a separate step (CollisionRetryPersister::persist()'s new$afterPersistcallback) that runs only after theurl_rewritewrite succeeds and with the key actually persisted — so a failed or retry-exhausted attempt can never leave aurl_keypointing at a request path that was never created, and on success the persistedurl_keyalways matches the persisted rewrite'srequest_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
insertOnDuplicateonurl_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-rowUPDATE … WHERE url_rewrite_id = ?routed throughCollisionRetryPersister: 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 itsurl_rewrite_idsocatalog_url_rewrite_product_categorylinks survive. 301 redirects are written on the freed old path through a newCollisionRetryPersister::persistUnlessTaken()guard — a plainINSERTthat skips and logs, rather than overwrites, a foreign row on a race. - The initial bulk
url_keywrite no longer orphans keys —partialRegenerate()bulk-wrote every entity'surl_key(insertOnDuplicateoncatalog_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'surl_keyhad already been committed with no matching rewrite — the very orphan the$afterPersistchange only closed inside the brand-new-rewrite path. The bulk write is removed;updateUrlRewrite()now categorises every entity and writes itsurl_keyin lockstep with its rewrite: unchanged existing rewrites (path untouched) get their key written directly; changed rewrites get it synced only after the per-PKUPDATEsucceeds; missing rewrites get it only after theINSERTsucceeds. Net invariant: no entity'surl_keyis ever committed unless itsurl_rewriterow was persisted in the same run. - A per-PK rewrite
UPDATEaffecting zero rows is now treated as failure — a concurrent delete could leave the targetedurl_rewriterow gone, making theUPDATE … WHERE url_rewrite_id = ?affect 0 rows; that previously fell through as success and synced an orphanedurl_key. The update now throws when 0 rows are affected, so the persister does not run theurl_keysync (the entity is retried, then skipped) — leaving no orphan. (applyRewriteChangesonly processes rows whoserequest_pathgenuinely 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_rewriterows (a rootkey.htmlplus category-context rows likegear/key.html). The changed-rewrite path previously retried and synced each row independently, so under a race the root could re-resolve tonew-1.html(url_keynew-1) while a category row persisted asgear/new.html(url_keynew) — root path, category path, andurl_keyall disagreeing.applyRewriteChanges()now groups the changed rows byentity_idand persists the whole group under one shared resolved key inside a per-productSAVEPOINT(issued as rawSAVEPOINT/ROLLBACK TO SAVEPOINT/RELEASE SAVEPOINTstatements, because Magento'sbeginTransaction()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 theurl_keysynced once and the 301 redirects created. On exhausting the retries the whole product is skipped — every row keeps its old path, and nourl_key, partial rewrite, or redirect is written. Rows stay UPDATEs (never delete+reinsert), sourl_rewrite_ids — and thecatalog_url_rewrite_product_categorylinks keyed on them — are preserved.
Changed
- Declared the
qoliber/coredependency (^1.0) incomposer.json— the admin menu (etc/adminhtml/menu.xml) and ACL (etc/acl.xml) reference theQoliber_Core::menuresource, andetc/module.xmlalready sequences afterQoliber_Core, but the Composer requirement was missing.
SeoHrefLang
Fixed
- Declared PHP 8.5 support (
~8.5.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. - Resolve localized CMS page alternates by URL key, not entity id —
CmsPageUrlResolverlooked up the same CMS pageentity_idin every target store, so translated pages (which are separate entities sharing anidentifier) were never found and their stores emitted no hreflang alternate. The resolver now reads the current page'sidentifierand matches the page assigned to each target store by that identifier (viaGetPageByIdentifierInterface), 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-cmsdependency (now used byCmsPageUrlResolver) and addedMagento_Cmsto the module load sequence. - Declared the direct
qoliber/core(^1.0) dependency — the admin ACL/menu are defined underQoliber_Coreresources; previously only the root metapackage provided it.
SeoOpengraphTags
Fixed
- Declared PHP 8.5 support (
~8.5.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. - Zero-price products no longer drop
product:price:amount—ResolverProduct::getTags()used to gate the price tags behindif ($finalPrice > 0), so a genuine free product (final price0) silently emitted no price tags at all, indistinguishable from a product with no price info. Now checksgetFinalPrice() !== null, so0correctly emitsproduct:price:amount="0.00"(withproduct:price:currency), while a truly absent price still emits neither tag. product:availabilitynow 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:availabilityis 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/frameworkversion constraint).
SeoAdvisorAi
Changed
- Declared direct Magento module dependencies with bounded version constraints.
- Standardised composer metadata (license and
magento/frameworkversion constraint). - PSR-12 code-style cleanup; no functional change.
SeoAiDiscoverability
Changed
- Declared PHP 8.5 support (
~8.5.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. - Declared direct Magento module dependencies with bounded version constraints.
- Standardised composer metadata (license and
magento/frameworkversion constraint). - Declared PHP 8.4 support in the
phpconstraint (~8.4.0added alongside 8.1–8.3); verified 8.4-clean (no implicitly-nullable parameters,php -land PHPCompatibility pass), no code change required.
SeoCommon
Changed
- Standardised composer metadata (license and
magento/frameworkversion constraint).
SeoDynamicDescriptions
Changed
- Declared PHP 8.5 support (
~8.5.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. Removed deprecatedReflection::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/frameworkversion 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/frameworkversion constraint).
SeoFriendlyProductUrls
Changed
- Declared direct Magento module dependencies with bounded version constraints.
- Standardised composer metadata (license and
magento/frameworkversion constraint). - PSR-12 code-style cleanup; no functional change.
SeoHrefLang
Fixed
- Emit hreflang on all page types, not only the homepage —
HrefLang::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 (idon products/categories,page_idon 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/frameworkversion constraint).
SeoOpengraphTags
Changed
- Declared direct Magento module dependencies with bounded version constraints.
- Standardised composer metadata (license and
magento/frameworkversion constraint). - PSR-12 code-style cleanup; no functional change.
SeoRichSnippets
Fixed
ratingSummarynow correctly returns 1–5 scale for schema.org instead of Magento's raw 0–100 percentageratingValuededuplicated — now callsgetRatingSummary()instead of reimplementing the same conversion logic- Extracted
getRatingSummaryPercent()private method for raw 0–100 value, keepinggetRatingSummary()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 generation —
SitemapGenerationServiceno 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 everychunk_sizeURLs. Memory footprint is now O(1) in catalog size, so stores with 100k+ SKUs no longer OOM. - BREAKING:
SitemapGeneratorInterface::getItems()returnsiterable— generators can now yield items viayieldinstead 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_enabledwrites sitemaps as.xml.gz. Supported by all major search engines and cuts bandwidth for large sitemaps. - Robots.txt sitemap announcement — new plugin
RobotsTxtInjectorappendsSitemap: <url>lines to Magento's corerobots.txtoutput for every sitemap registered on the current store. Crawlers discover sitemaps without merchants manually editingrobots.txt.
Fixed
- Chunk filename always suffixed — single-chunk files now also use the
_1suffix in streaming mode since total chunk count isn't known up front; consistent naming. - DI hygiene: service extraction moved to
_construct— theSitemapmodel previously overrodesetData()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()andClaudeClient::generateContent()now accept a$jsonModeparameter. For Gemini it setsgenerationConfig.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 nowjson_decodethe response without regex-stripping tricks.
Performance / Reliability
- Retry with exponential backoff — new
Qoliber\SeoAdvisorAi\Service\GenerativeAi\RetryHelperretries transient failures (HTTP 429, 5xx, connection errors) with jittered exponential backoff (up to 3 attempts, capped at 5s). Applied toGeminiClientandClaudeClient; protects against brief upstream blips and 429 rate-limit spikes that would previously surface as user-facing errors.
Fixed
- DI registry was empty at runtime —
di.xmlconfigured thegenerativeAiarray onGenerativeAiInterfacebut no preference was registered, and consumers (Popupcontroller,ListModelsCommand,AiProvidersource model,ProductDescriptionGenerator) all injected the concreteGenerativeAiclass — 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 controller —
typewhitelisted toproduct/category/cms_page;attributesnow JSON-decoded, filtered to 7 whitelisted keys, values forced to strings, payload capped at 8KB - GET → POST with CSRF protection —
check-seo.jsnow posts withform_key; controller implementsHttpPostActionInterface
Changed
- BREAKING: Popup controller now requires
POST+form_key; any custom integrations calling it viaGETwill 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.txtper-bot Disallow blocks.RobotsTxtBotRulesInjectorplugs intoMagento\Robots\Model\Robots::afterGetDataand appendsUser-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-TagHTTP header.XRobotsTagHeaderInjectorplugin onMagento\Framework\App\Response\Http::beforeSendResponseemits the configured directive string on every frontend response. Defaultnoai, noimageaisignals 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.txtendpoint per the draft spec. A customController\Routermatches the literal path (standard Magento routing can't handle the dot segment without a URL rewrite entry) and dispatches toController\Llms\Indexwhich serves the admin-configured body astext/plain; charset=utf-8withX-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+nosniffheaders.
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-Extendedis 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:upgradeto 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-out —
ModifyDataPlugin::afterGetDatanow early-exits for fields that no modifier handles (O(1) lookup against a prebuilt index). Previously everyProduct::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\SandboxExtensionwith a tight allow-list (tags:if/for/set; filters:escape/upper/lower/trim/length/default/join/split/replace/striptagsetc.; functions:range). Admin-crafted template strings can no longer invoke PHP or access object methods. - HTML autoescape enabled — Twig
autoescape: htmlis now on, so dynamic values are HTML-escaped by default. - Removed
html_entity_decodeafter render — this step subverted escaping and turned<script>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_keywordsnow passes throughstrip_tags()+ control-char stripping inModifyMetaTagsplugin. - Robots value whitelist —
robotsdirective now validated against canonicalINDEX/NOINDEX, FOLLOW/NOFOLLOWcombinations; arbitrary admin input is discarded.
Tests
preventRelativePath()contract locked —Test/Unit/Model/SeoDynamicTagPathSanitizerTest(13 cases via dataProvider). The method runs on every admin-submittedrequest_pathAND again inbeforeSave()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
RegeneratorandSingleProductUrlGeneratorpreviously only looked atentity_type = 'product'rows. Theurl_rewriteunique key is(request_path, store_id)across ALL entity types, soinsertOnDuplicatecould 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 regeneration —
partialRegeneratenow wrapscatalog_product_entity_varcharandurl_rewritewrites 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 URLs —
HrefLang::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']inview/frontend/templates/hreflang.phtml; now uses$escaper->escapeHtmlAttr()and$escaper->escapeUrl()respectively
SeoImagesFriendlyUrl
-
Schema: multi-store index with unique business key —
qoliber_seo_images_friendly_urlgained astore_idcolumn (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); theAddImageStoreIdUniqueConstraintschema patch dedupes existing rows on upgrade and adds the constraint.GetSeoFriendlyNameFromIndex::execute()gains a$storeIdparameter, preferring store-specific overrides with fallback to store_id=0. -
Performance: LRU-bounded SEO name cache —
GetSeoFriendlyNameFromIndexpreviously 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 state —
Imageasset class used to callReflectionClass::getProperty('miscParams')on every misc-access to read a private field from Magento core. Overrode the parent constructor to capture$miscParamsinto our own property. Removes a fragile integration point that would break on PHP / Magento upgrades. -
Security (CRITICAL XSS): Alt tag escaping —
SeoValueResolver::resolveAltTagSeoValue()now escapes output viaMagento\Framework\Escaper::escapeHtmlAttr(). Prevents stored XSS via product names flowing into imagealtattributes and gallerycaptionJSON -
Filesystem safety (CRITICAL): Atomic symlink creation in
ImageResize— replaces unsafeisExist()+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:
ImageResizedynamic properties —$fileStorageDatabaseand$storeManagerwere 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_IMAGEentry (the btree was dropped fromdb_schema.xml) and the speculative..._IMAGE_STORE_IDentry (never created — the schema patch adds..._IMAGE_STORE_ID_UNQvia 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/UpdateHandlercontract,App\Media+ImageResizepreference wiring, andConfigscope-read alignment. -
Fix (CRITICAL):
App\Media::createLocalCopypassed the cached path to the resizer — our responder handed the fullcatalog/product/cache/<hash>/…/foo.jpgpath toImageResize::resizeFromImageName(), but that method expects the original image path (e.g./m/j/foo.jpg). Core Magento's responder strips thecache/<hash>/prefix via a privategetOriginalImage()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 viacatalog:images:resize, but every subsequent on-demand request fell through tosetPlaceholderImage(). The helper is now included and the call site mirrors core. -
Fix:
ImageResize::resizeFromImageNamewas a silent no-op when the module was disabled — earlyreturnwhenisModuleEnabled()was false meant no image ever got generated, not even by core. Because the DI preference onMagento\MediaStorage\Service\ImageResizestill routes every request through our subclass, "disabled" must mean "delegate to parent" rather than "do nothing". Fixed: the disabled branch now callsparent::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::getUrland theCatalogBlockProductImageFactoryplugin both produced<dir>/<seoName>.<ext>URLs. Multiple originals can share a single SEO name (e.g.mp01-gray_main_1.jpgandmp01-gray_back_1.jpgboth map tocaesar-warm-up-pant-mp01), so the responder couldn't reverse-map the SEO URL to a specific source image. The existingImageResize::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::getOriginalImageprefix-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\Imageoverride hijacked every catalog image asset — the class is the DI preference target forMagento\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()andgetFilePath()were gated only onisModuleEnabled()and unconditionally readmisc['filePath']— which only OURCatalogUrlBuilderpopulates — so every other caller got a URL with an empty filename, collapsing to the theme placeholder (/static/…/placeholder/image.jpg). Now both methods gate onisSeoImage()(the flagCatalogUrlBuildersets 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. WithApp\Media::createLocalCopynow callinggetOriginalImage()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 above —
CatalogUrlBuilderIntegrationTest::testSeoUrlEmbedsOriginalBasenameAsSubdirectory(4-segment URL shape),MediaGetOriginalImageTest(6 dataProvider cases: 3-seg + 4-seg + edge inputs),ImageResizeParentDelegationTest(NotFoundException probe proves parent delegation on disabled), andImageResizeExtractRealFilePathTest(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.orgendpoint. - 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(routeqoliber_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 spyCurlreplaces 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 POSTedurlList; 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 locked —
Test/Integration/Service/NotifierIntegrationTest(5 cases): missing/malformed IndexNow key returnsfalsewithout a network call; valid call dedupes duplicate URLs; empty URL list short-circuits as success; disabled module returnstruewith 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}.txtat 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:altemitted 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:altsatisfies accessibility tooling for social shares.twitter:image:altmirrorsog:image:altfor the same accessibility reason Twitter already mirrorsog:title/og:description.- New
ResolverProductImageMetadataTest(4 unit cases) covering: helper URL + resized dimensions, fallback to helpergetWidth/getHeightwhengetResizedImageInfois null, empty product image suppresses the wholeog:imageblock, empty product name suppresses the alt tag.
Fixed
- Module disable actually works — both
layout_load_beforeobservers (RemoveDefaultTags,SetPrefix) now checkConfig::isEnabled()before mutating layout handles. Previously, disabling the module could leave product pages with no OG tags at all because the Magento corecatalog_product_opengraphhandle was still being removed. - OG block gated via
ifconfig— theqoliber.opengraphblock is now only instantiated whenqoliber_opengraph/settings/enableis on. og:imagenow emits the same SEO-friendly URL the product page uses — previouslyResolverProduct::getImageUrl()produced a raw/media/catalog/product/<file>.jpgURL (not resized, not SEO-rewritten).ResolverProductnow injectsMagento\Catalog\Model\Product\Image\UrlBuilder(the DI preference target forQoliber\SeoImagesFriendlyUrl\…\CatalogUrlBuilder) so the OG image URL goes through the same SEO rewrite the product-page gallery uses. Width/height come fromMagento\Catalog\Helper\Imageusing the stockproduct_page_image_mediumview.xml entry (700x700) — the right size for social previews, and an existing entry we reuse instead of adding a new one.product_page_image_largeis intentionally not used because Luma/blank ship it with no dimensions (self-closing<image .../>), which makes the helper return the theme placeholder.- Template whitespace cleanup —
view/frontend/templates/opengraph.phtmlwas 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 —
$paramValueand$queryParamare now passed throughrawurlencode()before URL concatenation (Model/RequestParamsProvider.php:56)
Fixed
- Canonical URLs no longer leak query params —
AbstractPatternProcessor::getCanonicalArgumentsused 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
q—ResultBlockPluginnow includes the normalized search query in the canonical URL so different searches don't collapse to the same canonical. - Stateless
RequestPathResolver— removed per-instance$resolvedParamscache 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 exit —
Router::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 module —
Test/Unit/Plugin/ShortAttributeResolverTest(3 cases) locks the short-pattern request-path cleanup contract: nullResolvedParamspropagates 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\ElasticsuiteCorepackage 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
priceandfinalPricevariables in product value resolver with smart formatting (trailing zeros removed)shortDescriptionandshort_descriptionvariables with automatic fallback to full description when short description is empty- Per-review
ratingfield inCollectionVariables— 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
nullinstead of blank strings in JSON-LD output - Anonymous reviewer fallback — reviews without a nickname now render as "Anonymous" in both
ReviewValueResolverandCollectionVariables preg_replacereturn values explicitly cast tostringinstripHtml()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
ModifyDataPluginto 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()withPsr\Log\LoggerInterfaceinjected 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
SitemapGeneratorInterfacegenerator pattern - Each entity type (CMS pages, categories, products, images) is now a configurable generator registered via
di.xml - Sitemap generation delegated to
SitemapGenerationServiceorchestrator 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\SitemapGeneratorInterfacecontract for entity generatorsModel\Generator\CmsPageGenerator,CategoryGenerator,ProductGenerator,ImageGeneratorModel\Service\SitemapGenerationServicecore orchestrator- Admin config field "URLs per Sitemap File" with
validate-digitsvalidation
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/modelsendpoint, 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
v1tov1betafor 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-turbotogpt-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
MODELSconstant mapping fromGeminiClient— 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)toSavecontroller andModifyMetaTagsplugin
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_urlsfor the generate endpoint - RequireJS widget with MutationObserver that injects "Generate URL" button next to
url_keylabel and pattern notice below the input field - ACL resource
Qoliber_SeoFriendlyProductUrls::generate_urlfor 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.jsonpsr-4 autoload — fixed missing empty string value and closing braces in autoload configuration - Null safety in URL resolvers — added null guards for
$request->getParam()inProductUrlResolver,CategoryUrlResolver, andCmsPageUrlResolverto prevent type errors
Changed
- Added
readonlyto constructor properties inConfigandHrefLangViewModel - Added
declare(strict_types=1)to all files missing it:ProductUrlResolver,CategoryUrlResolver,CmsPageUrlResolver,ContactPageUrlResolver,AlternateUrlService,UrlResolverInterface
SeoImagesFriendlyUrl
- Strict comparison fix: Changed loose
!=to strict!==inImage::getImageInfo()for image friendly name check - Dead code removal: Removed unused
$productDatavariable inSeoValueResolver::getSeoFriendlyValue()— was populated but never passed to template processor - Docblock fix: Corrected
/**+typo to/**inSeoValueResolverconstructor docblock - Readonly properties: Added
readonlyto all constructor properties inAddDiObjectsToImageModelplugin - Error logging: Added exception logging in
Image::getUrl()catch block — silent exception swallowing now reports the error
SeoOpengraphTags
Fixed
- ResolverCategory
array_filterremoves valid falsy values — replaced barearray_filter($tags)with explicit callback to preserve"0"andfalsevalues, only filtering outnulland empty strings
Changed
SetPrefixobserver modernized to PHP 8.1 constructor property promotion withreadonly- Added
: voidreturn type toexecute()inSetPrefixandRemoveDefaultTagsobservers
SeoPrettyFilters
Added
- Flat URL mode — third URL pattern alongside Short and Complex, with configurable filter order per store view and segment separator (
--or__) FlatAttributeResolverfor resolving flat URL segments into filter parameters using ordered attribute matchingFlatPatternProcessorfor generating flat-format filter URLs with configurable segment separatorFlatSegmentSeparatorsource 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
Routercontroller and Flat mode DI wiring / config resolution (FlatModeTest)
Fixed
- Critical: AttributeFetcher data-wipe bug —
$attributeSeoParamswas wiped immediately after store key initialization due to reversed line order - ComplexAttributeResolver
str_containsargs reversed —str_contains('!', $value)corrected tostr_contains($value, '!') - ComplexAttributeResolver missing
issetguard — added check beforegetOptionIdByValue()to prevent error on unknown attribute codes - ShortPatternProcessor
array_mergecrash —array_merge(...$seoPaths)crashes when$seoPathsis empty; applied ternary guard - AbstractPatternProcessor loose
usortcomparison — replaced verbose comparator with spaceship operator (<=>) - FilterOption loose comparisons — changed
==to===ingetOptionIdByValue()andgetOptionIdBySeoPath() - Yesno converter loose comparison — changed
$optionId == '0'to$optionId === '0' - ModifyMetaRobots silent exception swallowing — added
LoggerInterfacedependency and warning-level logging - RequestPathResolver falsy check on empty array — changed default from
[]tonulland check to=== null
SeoPrettyFiltersElasticSuite
Added
- Flat URL mode support —
FlatAttributeResolverandFlatPatternProcessorplugins for ElasticSuite-specific attribute handling in flat URL mode - Plugin for
FlatAttributeResolver::resolveAttributeValues()handling ElasticSuite multi-select and swatch attribute formats - Plugin for
FlatPatternProcessorhandling ElasticSuite attribute array construction
Fixed
- ComplexAttributeResolver
str_containsargs reversed —str_contains('!', $value)corrected tostr_contains($value, '!') - ComplexAttributeResolver missing
issetguard — added check beforegetOptionIdByValue()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
descriptionandshort_descriptionattributes
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.phpfixture -
MSI-compatible test fixture —
product_with_select_attribute.phpconditionally registers MSI source items usinginterface_exists()check, works with and without MSI modules -
DJson template resolution tests —
@djson if/elsestock 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,
@djsondirective 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:
SnippetInterfacefor snippet data structureSnippetRepositoryInterfacefor snippet CRUD operationsSnippetSearchResultsInterfacefor search resultsValueResolverInterfacefor dynamic value resolutionVariableProviderInterfacefor 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:
SnippetRepositoryfor managing snippet entitiesSnippetExpanderfor processing nested snippetsCircularDependencyValidatorfor preventing infinite loopsVariableExtractorfor parsing variable expressions (supports both{{var}}and@djson if/unless/existsdirectives)- Handle system for layout-based snippet assignment
-
Variable System:
ProductVariables- Product-specific variablesStockVariables- Stock/inventory variables (product.stock.qty,product.stock.isInStock,product.stock.availability)BreadcrumbsVariableProvider- Breadcrumb navigation variablesCollectionVariables- Product collection variablesCustomVariables- User-defined custom variablesEnumVariables- Enumeration valuesStoreConfigVariables- Store configuration accessSystemVariables- System-level variablesSnippetVariables- Cross-snippet references
-
Value Resolvers:
ConfigValueResolver- Resolve configuration valuesProductValueResolver- Resolve product data with 20+ attributes and nestedstock.*path resolutionReviewValueResolver- Resolve product review dataStoreValueResolver- Resolve store information
-
Database Schema:
qoliber_seo_richsnippets_snippettable for snippet storageqoliber_seo_richsnippets_snippet_storefor store relationsqoliber_seo_richsnippets_handlefor 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 snippetsUpdateOfferStockAvailability- Dynamic stock availability in Offer snippet
-
Frontend:
- New
JsonLdblock for rendering snippets with caching - Product image URL resolution via catalog image helper (Friendly URLs compatible)
- Enhanced template with better error handling
- New
-
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,BreadcrumbListServiceContactPointService,LocalBusinessService,MerchantReturnPolicyRichSnippetsServiceOnlineStoreService,OrganizationService,ProductService,WebsiteService- Collection page services, Item list services
- Product-related services (AggregateRating, Brand, Offer, OfferShippingDetails, Review)
RichSnippetsViewModel (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
MoveCategoryDescriptionBelowPaginationthat 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
AcceptedPaymentMethodServicefor payment method rich snippetsOnlineStoreServicefor online store structured data- Collection page support with
CollectionPageService - Search results page support with
SearchResultsPageService OfferShippingDetailsServicefor 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
MerchantReturnPolicyRichSnippetsServicewith advanced features - QM-291: Improved
ProductServicewith better offer handling - QM-291: Updated
ReviewServicewith 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
ReflectionExceptioncatch inImage::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_valuetable 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
AbstractPluginto 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_filtersandseo_headercategory 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_selectioncheck: Filters out Magento'sno_selectionplaceholder value fromog:image
Added
og:site_nametag 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:amountandproduct:price:currencyfor richer social previews - Product availability:
product:availabilitytag when product is in stock - Category image:
og:imagetag on category pages when a category image is set - Twitter Card support:
twitter:card,twitter:title,twitter:description,twitter:imageon product pages — usessummary_large_imagewhen image is available - Template now correctly uses
nameattribute for Twitter tags andpropertyfor OG/product tags
Changed
- All resolvers modernized to PHP 8.1 constructor property promotion
- Tag keys now use full property names (
og:titleinstead oftitle) 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
RichSnippetsResolverInterfacetoRichSnippetsServiceInterface - Restructured service architecture for better maintainability
Added
-
New Services:
ProductService- Complete product rich snippets handlingWebsiteService- Website-level structured dataContactPointService- Contact information structured dataOrganizationService- Organization schema supportMerchantReturnPolicyRichSnippetsService- Return policy structured dataAggregateRatingService- Product rating aggregationBrandRichSnippetsService- 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:
JsonUnescapedserializer for proper JSON output
Changed
- Enhanced
AbstractRichSnippetsServicewith 120+ lines of improvements - Refactored
BreadcrumbListServicefor better efficiency - Improved
OfferServicewith enhanced offer data - Updated
ReviewServicewith better review handling - Renamed template from
richSnippets.phtmltorich-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.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. - Bounded the
qoliber/coredependency 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 andin_arraystrictness); 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_IDtoQOLIBER_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
ClientInterfacefor 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
ComplexPatternProcessorto improve filter URL generation and removal logic - Enhanced
ShortPatternProcessorwith 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\ChatGptclass for OpenAI API communication with configurable models - Implemented
Controller\Adminhtml\Advise\Popupfor handling AJAX requests and AI response processing - Added
Block\Adminhtml\Product\Edit\Button\CheckSeofor 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_sitemapattribute, 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\Sitemapto override XML generation logic and implement entity-based file splitting - Implemented plugin for
Magento\Sitemap\Model\ResourceModel\Cms\Page::getCollection()to filter pages based onuse_in_sitemapattribute - Created abstract value filter with separate implementations for each entity type (Category, Product, CmsPage)
- Added database schema extension with
use_in_sitemapcolumn incms_pagetable - 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_filtersandseo_headercategory attributes during installation
Technical Implementation
- Created
Plugin\AbstractPluginbase class with common SEO formatting logic - Implemented
aroundGetData()interceptor forMagento\Catalog\Model\Categoryto 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_tagstable 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\SeoDynamicTagentity model with resource model and collection for database operations - Implemented
Plugins\App\ModifyMetaTagsas around plugin forActionInterface::execute()to intercept page rendering - Built admin controller structure (
Index\Add,Index\Edit,Index\Save,Index\Delete) for full CRUD functionality - Added
SeoDynamicTagsDataProviderfor 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, andsort_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
DisabledModuleExceptionwhen 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\UrlKeyGeneratorthat parses patterns and replaces placeholders with actual product attribute values - Implemented
Service\Regeneratororchestrating bulk URL regeneration with batch processing support - Built repository layer for product entity IDs, attribute codes, SKUs, and URL rewrite data
- Added
Console\Command\RegenerateUrlswith options:--entity-id-from,--entity-id-to,--store-id - Configured filter chain (ExistingAttributeCodeFilter, MaxProductEntityIdFilter, MinProductEntityIdFilter, UrlKeyFilter) for data validation
- Created
Repository\Product\Attribute\OptionsRepositoryfor fetching select/multiselect attribute labels - Implemented
Repository\Product\Attribute\ProductUrlRewriteRepositoryfor 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\AlternateUrlServicethat 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\HrefLangthat fetches allowed stores, generates alternate URLs, and formats locale codes - Added
Model\Configwith 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\GetTagResolverthat determines page type and returns appropriate resolver implementation - Implemented
Model\ResolverInterfacewith concrete classes for products, categories, and default pages - Built
Model\ResolverProductthat fetches current product from registry and extracts meta description and image URL - Added
Model\ResolverCategoryfor 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\OpengraphTagsfor 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\SeoPrettyFilterscompatibility with ElasticSuite - Dependency on both
qoliber/seo-pretty-filtersandsmile/elasticsuitemodules for proper operation
Technical Implementation
- Created
Plugin\PatternProcessor\ComplexPatternProcessorthat intercepts complex URL pattern generation and removal - Implemented
Plugin\PatternProcessor\ShortPatternProcessorfor simplified pattern handling - Built
Plugin\AttributeFetcherto modify attribute data retrieval for SEO purposes - Added
Plugin\ComplexAttributeResolverfor resolving complex multi-attribute filter combinations - Created
Plugin\ShortAttributeResolverfor simple attribute resolution - Implemented
Plugin\ConverterSelectto convert select attribute rendering to use SEO URLs - Configured around plugins for
aroundGenerateFilterItemUrl()andaroundGenerateRemoveItemUrl()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:
AbstractRichSnippetsResolverbase classProductResolverfor product schemaBreadcrumbListResolverfor navigationLocalBusinessResolverfor business informationOrganizationResolverfor organization dataAggregateRatingResolverfor ratingsOfferResolverfor product offersReviewResolverfor product reviews
-
ViewModel:
RichSnippetsViewModel 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.0added to thephpconstraint); verified 8.5-clean — php8.5-land 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 onqoliber/seo-platform. -
Model\CanonicalResolver— the root of the per-page-type decision tree and the one<preference>forCanonicalPolicyInterface. 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 aCanonicalDecisioncarrying a human-readable reason string. -
Api\CanonicalStrategyInterface+ four branches:Model\Strategy\ProductCanonicalStrategy— configurable child → parent (admin optionqoliber_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 untilqoliber/seo-facet-governancebinds the real per-category rules. -
Model\PageContext\SearchResultsContext— entity-less marker for search pages. -
Model\Config— store-scoped reader;etc/config.xmldefaults (policy enabled, configurable child → parent enabled);etc/adminhtml/system.xmldecision-tree configuration under the Qoliber tab;etc/acl.xmlconfig resource.
SeoPlatform
Added
-
Declared PHP 8.5 support (
~8.5.0added to thephpconstraint); verified 8.5-clean — php8.5-land 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 stablekey()andequals(); 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\UrlSignalProviderInterface—contribute(UrlIdentity): ?SignalContribution; each feature module registers one viadi.xml. -
Model\UrlStateResolver— aggregates all DI-registered providers into a singleUrlState(first non-null contribution wins, by registry order). -
Api\CanonicalPolicyInterface+Model\CanonicalDecision— the single canonical authority contract; every decision carries areasonstring. -
Api\IndexabilityPolicyInterface+Model\IndexabilityDecision— the facet/pagination indexability contract; every decision carries areason. -
Model\SeoFinding— the audit currency (severity, impact, message, url). -
Api\UrlRewriteWriterInterface+Model\UrlRewriteWriter— THE single owner ofurl_rewritewrites, savepoint-atomic per entity (product) group, plusApi\Data\UrlRewriteRequestInterface/Model\UrlRewriteRequestandModel\UrlRewriteWriteResult.
SeoRedirects
Added
-
Declared PHP 8.5 support (
~8.5.0added to thephpconstraint); verified 8.5-clean — php8.5-land the unit suite both pass. -
Initial redirect + 404 lifecycle manager (battleplan Phase 1). Depends on
qoliber/seo-platform; consumes its singleurl_rewritewriter — never writes theurl_rewritetable directly. -
etc/db_schema.xml—qoliber_seo_redirect(301/302/410 rules, unique onrequest_path+store_id, hit counter) andqoliber_seo_404_log(frequency + referrer + user-agent, unique onrequest_path+store_id). -
Service contracts —
Api\Data\RedirectInterface,Api\RedirectRepositoryInterface(+Model\RedirectRepository) andApi\RedirectLookupInterface(+ DB-backedModel\RedirectFinder). -
Model\RedirectPathNormalizer,Model\RedirectMatcher(exact + longest-prefix wildcard) andModel\RedirectChainResolver— folds a redirect chain to its terminal target with a visited-set loop guard and a configurable hop cap. -
404 handling —
Observer\NotFoundHandleron the no-route predispatch serves a matching active redirect (301/302/410) viaModel\NotFound\Responderor records the miss throughModel\NotFoundLogger. -
Auto-redirect —
Observer\{Product,Category,CmsPage}UrlKeyChangeturn a URL-key change into a 301 old → new throughModel\AutoRedirect\RedirectRegistrar, which writes exclusively via the platform'sUrlRewriteWriterInterface. -
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:importandqoliber:seo-redirects:export(CSV) backed by the pureModel\Csv\RedirectCsvConverter. -
Unit tests for the chain/loop guard, the redirect-match lookup, and the CSV import/export round-trip.