Changelog
Current version: 1.3.2
Entries are merged from all thirteen module changelogs. A version heading
appears when any module shipped that version; the date is the core
qoliber/multiblog module's date for it.
1.3.2 — 2026-09-27
Multiblog
Fixed
- A post page showed its title twice, the first time as its meta title. The generic page title block (
page.main.title) printed the page's configured title -- the post's meta title when one is set -- as the page's<h1>, andpost/view.phtmlprinted the post title again as an<h2>. Readers saw "10 SEO Tips | Blog" above "10 SEO Tips", and every post page carried, as its main heading, text written for search results. The post layout now removespage.main.titleand the post title is the page's one<h1>; the meta title still sets<title>. Luma and Hyvä alike. - A renamed, added or removed category did not reach cached blog navigation.
Model\Categorylisted its tag ingetIdentities(), which only reaches the full page cache, and declared no$_cacheTag, which is what cleans the app cache on save and delete -- the same gapModel\Instancehad until 1.3.1. Navigation cached in the app cache, the Hyvä header among it, kept a category's old state until a manual flush.Categorynow declares the tag.
Comments
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.2 so the whole suite stays on one version.
GraphQL
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.2 so the whole suite stays on one version.
Hyvä
Fixed
- The Hyvä menu kept deleted blogs, and missed new or renamed ones, until the block cache was cleaned. Hyvä caches the header in the block cache under the cache identities of its menu entries, and the blog and category entries that
Plugin\AddBlogToHyvaNavigationadds carried none. A blog or category save cleared the plugin's own menu data but never the rendered header. Blog entries now carry the blog's tag andqoliber_multiblog_instance, category entries the category's tag andqoliber_multiblog_category, so saving or deleting either refreshes the menu. The menu data moved to a new cache key, so entries cached in the old shape are never read back. - The post title is the page's one
<h1>(it was an<h2>under a second, generic page title). See Qoliber_Multiblog 1.3.2.
Hyvä Commerce
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.2 so the whole suite stays on one version.
Import
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.2 so the whole suite stays on one version.
RSS
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.2 so the whole suite stays on one version.
Search
Changed
- No functional changes. Adds an integration test proving that with the indexer on "Update on Save" a saved post becomes searchable, and a deleted one leaves search, without cron or a reindex.
SEO Open Graph
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.2 so the whole suite stays on one version.
SEO Rich Snippets
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.2 so the whole suite stays on one version.
Sitemap
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.2 so the whole suite stays on one version.
Social Sharing
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.2 so the whole suite stays on one version.
Web API
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.2 so the whole suite stays on one version.
1.3.1 — 2026-09-27
Multiblog
Fixed
- A new blog answered 404 at its route until the cache was flushed, and a deleted one kept answering. The router caches which route belongs to which blog in the app cache (
Router\InstanceRouteCache), misses included, under theqoliber_multiblog_instancetag.Model\Instancelisted that tag ingetIdentities(), but those only reach the full page cache; the app cache is cleaned by a model's$_cacheTag, whichInstancenever set. Any route requested before its blog existed -- a shopper following a link early, a crawler, a preview -- was cached as "no blog here" and kept 404ing after the blog was saved; deleting or renaming a blog left its old route resolving.Instancenow declares the tag, so every save and delete clears the route cache. Covered by an end-to-end test that visits a route before and after its blog is created and deleted.
Comments
Fixed
POST /V1/multiblog/commentsrejected every submission with HTTP 400. Magento's REST layer resolves each field of a request body through the getter's@returnannotation, andApi\Data\CommentInterfacedeclared native return types only, with no docblocks -- so the very first field,post_id, failed with"PostId" is not supported, and no comment could be submitted over the API at all. The interface now carries@param/@returnannotations on every accessor. The API contract test had accepted 400 as a valid answer "in case comments are disabled", which is how this went unnoticed; it now enables comments and requires the submission to be queued aspending.
GraphQL
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.1 so the whole suite stays on one version.
Hyvä
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.1 so the whole suite stays on one version.
Hyvä Commerce
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.1 so the whole suite stays on one version.
Import
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.1 so the whole suite stays on one version.
RSS
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.1 so the whole suite stays on one version.
Search
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.1 so the whole suite stays on one version.
SEO Open Graph
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.1 so the whole suite stays on one version.
SEO Rich Snippets
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.1 so the whole suite stays on one version.
Sitemap
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.1 so the whole suite stays on one version.
Social Sharing
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.1 so the whole suite stays on one version.
Web API
Changed
- No changes to this module. Released alongside Qoliber_Multiblog 1.3.1 so the whole suite stays on one version.
1.3.0 — 2026-09-03
Multiblog
Added
-
Blog image dimensions are configurable, and the rendered
width/heightfinally match the file. Sizes were literals in the templates -- once as the resize target, once as the<img>element's intrinsic size -- so changing one for a project meant overriding templates, and the two could drift apart. Four tiers (multiblog_post_full,multiblog_post_grid,multiblog_post_list,multiblog_category_thumb) plusmultiblog_author_avatarare now declared inetc/view.xml, which any theme overrides in its ownetc/view.xml-- so store views running different themes can differ, somethingdi.xmlcannot express. A layout argument selects a different tier for a fixed placement without touching pixels:XML<argument name="image_id" xsi:type="string">multiblog_post_grid</argument>Relocating the numbers was not enough on its own.
ImageResize::resize()keeps the source aspect ratio and never upscales, so its output is only ever bounded by the requested box: 240x160 asked of a 16:9 source yields 240x135, and the markup claimed 160.getImageData()now measures the file it produced and returns those dimensions alongside the URL, so the attribute and the file agree. Measured across six page types, 0 of 71 blog images declared their true size before; all 71 do now. Wrongwidth/heightare worse than none: the browser reserves the wrong box and the page shifts as each image loads. -
Saving a post now clears every page that renders it. Full page cache invalidation is driven by cache tags, and Magento matches them exactly --
qoliber_multiblog_post_15andqoliber_multiblog_postare unrelated strings, not a prefix and its children.Model\Post::getIdentities()emitted the first form while every listing block emitted the second, so a save invalidated nothing but the post's own page: the blog index, category, tag and author listings, the sidebar widgets and the search results kept serving the old title until somebody flushed by hand.Block\Author\View,Block\Tag\View,Block\Sidebar\RecentPosts,Block\Sidebar\PopularPostsandBlock\Widget\BlogPostsimplemented noIdentityInterfaceat all, so their pages were never tagged in the first place.Each listing block now tags the posts it actually rendered, via the new
Model\Cache\Identityhelpers (post(),instance(),category(),tag(),author(),forPosts()) rather than assembling tag strings by hand -- the concatenation is written once, so the two halves cannot drift again.Post::getIdentities()emits the post's instance, author, categories and tags alongside its own tag, and takes each of those fromgetOrigData()as well as the current values: moving a post to a different category has to clear the page it left as much as the one it joined, and after the save only the new value remains readable.Measured on a store with the full page cache on, renaming a post left four of six surfaces stale before this change (blog index, author, category and search); all six refresh now with no manual flush.
The same exact-match problem was hiding in the catalog integration.
Post::getIdentities()emittedcatalog_product_<id>, but Magento tags a product page withMagento\Catalog\Model\Product::CACHE_TAG, which iscat_p_<id>-- so a post shown on a product page never cleared it. The tag now comes from Magento's own constant, and product ids get the same original-value treatment as the other relations, so unlinking a post from a product clears the product page it disappeared from.Seven further blocks render blog content and declared no identities at all, which is why their pages could not be reached:
Block\Product\RelatedPostsandBlock\Catalog\Category\RelatedPosts(posts shown on catalog pages),Block\Post\PrevNext(the neighbouring posts' titles),Block\Instance\Featured(the featured article, which is chosen by id and need not appear in any listing), andBlock\Sidebar\Categories/Block\Sidebar\TagCloud(category and tag names, rendered on every blog page, so a rename had to clear all of them rather than just that entity's own page). Verified with the full page cache on: with the pre-existing code a product page kept a renamed post's old title indefinitely; it now refreshes on save.
Fixed
- A blog could display another blog's post as its featured article.
Block\Instance\Featured::getFeaturedPost()loaded the configuredfeatured_post_idand checked onlyis_activeandstatus, never that the post belonged to the instance rendering it. A stored id pointing at another blog's post rendered on the storefront regardless -- including on blogs with no posts of their own, which showed a foreign "Featured" article above an empty listing. The getter now returns null when the post'sinstance_iddoes not match the instance being rendered, so existing cross-instance rows stop rendering without needing a data migration. - The admin Featured Article dropdown offered posts from every blog in the installation.
Model\Config\Source\Post::toOptionArray()filtered onis_activeandstatusonly, with noinstance_idfilter, so editing any instance listed every published post and selecting one silently created the cross-blog reference above. Options are now scoped to the instance being edited; a new instance, which has no posts yet, offers none. Qoliber_Multiblog::post_publishcould be bypassed over REST. The permission was enforced inController\Adminhtml\Post\Saveonly, whilePOST/PUT /V1/multiblog/postsresolve toPostRepositoryInterface::save()and require justQoliber_Multiblog::post. An integration token allowed to edit but not publish could therefore publish. A newPlugin\Post\EnforcePublishPermissionapplies the same downgrade-to-draft rule at the repository. Only authenticated admin and integration callers are restricted -- cron and the importer publish through the same method and must keep working -- and editing an already-published post still leaves it published.- Author and tag pages served content from blogs that were disabled or scoped to another store.
Controller\Author\ViewandController\Tag\Viewverified the entity but never its parent instance, unlikeCategory\ViewandPost\View. Both now checkInstanceVisibility::isInstanceVisible(). - Category saves accepted hierarchies that cannot exist.
Model\ResourceModel\Category::_beforeSave()checked only that the parent lived in the same instance, so a category could be made its own parent or moved beneath one of its descendants;paththen contained the category itself andrebuildPathrewrote the subtree from a corrupt value. Both cases are now rejected. - Post duplication changed database state over
GET.Controller\Adminhtml\Post\DuplicateimplementedHttpGetActionInterface, so the action could be triggered by any page that fetches a URL while an administrator is signed in. It now requiresPOST, which Magento gates with the admin form key, and the Duplicate button submits one. - Several images were served as the unresized original with no dimensions at all: the instance hero, the single-post hero, and the author avatar -- the last rendering a full-size upload into a 96px circle. All now go through the resizer. The two heroes also carried
loading="lazy"while being the largest element above the fold; they usefetchpriority="high"instead. Block\Instance\Featured::getResizedCategoryImage()hardcoded 400x250 in its body while every other resize entry point accepted$width/$height, so it was the one surface an integrator could not adjust with a plugin. It now takes optional dimensions and is deprecated in favour ofViewModel\BlogImage::forCategory().
Comments
Added
- Posting or approving a comment now clears the post page it belongs to.
Model\Comment::getIdentities()emitted onlyqoliber_multiblog_comment_<id>, and nothing on the post page ever carried that tag, so with the full page cache on a new comment simply did not appear -- and approving a pending one from the admin changed nothing visible -- until somebody flushed by hand. The comment's own tag could not fix that on its own either: a newly submitted comment has an id that was never part of the cached page, so no tag on that page can match it. The model now also emits the post's tag, which is what actually reaches the page, andBlock\Post\Commentsdeclares the post and the rendered comments as its identities. Verified with the cache on: approving a pending comment made it appear on the post page with no manual flush. Part of the wider invalidation work in Qoliber_Multiblog 1.3.0.
Fixed
- An anonymous caller could post comments as any customer.
POST /V1/multiblog/commentsis declaredanonymousandPublicCommentRepository::submit()readcustomer_idstraight from the request body. Supplying any existing id skipped the entire guest branch -- the "allow guest comments" setting, the name and email requirement, and email validation -- and stored the comment against that customer. The identity is now taken fromUserContextInterfaceand overwrites whatever the payload claims, so an unauthenticated request is always treated as a guest. - The public REST route had no server-side throttle. The storefront controller rate limits through the customer session, which an API client never establishes, so
POST /V1/multiblog/commentscould be called without limit. A newModel\Comment\SubmissionRateLimitkeeps the counter in the cache instead, keyed by the authenticated customer where there is one and otherwise by the client address, normalised and hashed rather than stored in clear. The allowance matches the storefront: 5 submissions per 60 seconds. The counter is a load-modify-save, so it is taken under a lock -- without one, parallel requests can each read the same count, each find room and each write back a single increment, letting a caller exceed the limit precisely when they are trying hardest to. A lock that cannot be acquired fails closed. - Any post with an approved comment returned HTTP 500.
post/comments.phtmlcalledrenderComment()from the markup while declaring it lower down insideif (!function_exists('renderComment')). A function declared inside a conditional is not hoisted, so the call hit an undefined function. The guard had been added to stop a redeclare fatal when the template renders twice in one request -- it fixed that and introduced this. The declaration now sits above its first use and keeps the guard. Only posts that actually had a comment reached the call, which is why it survived the test suite.
GraphQL
Changed
- Version bump to 1.3.0 for the coordinated release. No functional changes in this module.
Hyvä
Fixed
- The Hyva search template carried the same unclosed-wrapper bug fixed in
Qoliber_MultiblogSearch.view/frontend/templates/search/results.phtmlleft<div class="multiblog-search-results">open on both the empty-query and no-results early returns, displacing the sidebar into the main content column on Hyva storefronts. Both paths now close the wrapper before returning. - The frontend
di.xmlevicted every other module from the Hyvä extension list.etc/frontend/di.xmlre-declared themoduleListargument ofQoliber\HyvaModuleRegistration\Observer\RegisterModulesForHyva, which this module's globaletc/di.xmlalready declares. Magento mergesdi.xmlfiles within one scope but replaces an argument when a second scope is applied, so the single-entry frontend copy overwrote the fully merged global list. Everysetup:upgradethen regeneratedapp/etc/hyva-themes.jsoncontaining only this package -- silently, in a generated file, with no error. Siblingqoliber/*-hyvapackages dropped out, their templates were no longer scanned by the Tailwind build, and they shipped unstyled. The duplicate declaration is removed; the area-specificHyva\Theme\Service\Navigationplugin stays. Reported by a partner against 1.2.1. - The Hyva storefront templates now render through the same shared image sizing as Luma, so blog image dimensions are configured once in
Qoliber_Multiblog'setc/view.xmlfor both themes. Previously each Hyva template carried its own hardcoded pixel pairs.
Hyvä Commerce
Changed
- Version bump to 1.3.0 for the coordinated release. No functional changes in this module.
Import
Fixed
- Posts with no title or URL key were imported rather than rejected.
Model\Import\EntityImporter::importPosts()substituted''for both fields, saving rows that cannot be routed to and appear blank in the grid. Such rows are now counted as errors and skipped, and the rest of the chunk still imports.
RSS
Fixed
- Feeds published the name and meta description of blogs that were not visible in the current store. Every feed controller checked
is_activebut not store assignment, so a blog scoped elsewhere still returned a populated200channel -- its post list was empty, but the metadata was disclosed. All four feeds now requireInstanceVisibility::isInstanceVisible(). - The tag, category and author feeds never checked that the requested entity belonged to the instance in the URL, so
.../feed/tag/instance_id/1/tag_id/<tag from blog 2>returned a feed. Each child feed now rejects an entity from another blog.
Search
Added
-
Use HTTPS (
qoliber_multiblog/search/enable_https, default off).Adapter\ClientBuilderhardcoded'scheme' => 'http', so enabling authentication sent the OpenSearch username and password in clear text on every request. Enable this whenever the cluster requires credentials. -
The search results page is cached under the posts it returned.
Block\Search\Resultsdid not implementIdentityInterface, so a results page was stored in the full page cache with no tags identifying its content and no post save could reach it -- a renamed or unpublished post kept appearing, with its old title, until the cache was flushed. It now tags every post in the result set throughQoliber\Multiblog\Model\Cache\Identity::forPosts(). See the Qoliber_Multiblog 1.3.0 notes for the wider invalidation work this belongs to.
Fixed
- The blog sidebar rendered inside the main content column whenever a search returned nothing.
view/frontend/templates/search/results.phtmlopened<div class="multiblog-search-results">and closed it only at the end of the template, but the empty-query and no-results pathsreturnbefore reaching that closing tag, leaving one unclosed div. The browser balanced the document by consumingdiv.column.main's closing tag instead, so the sidebar -- which the2columns-leftlayout emits after main -- became a child of it and rendered indented at reduced width, leaving the left gutter empty. Both early returns now close the wrapper before returning. Searches that returned posts reached the closing tag normally, which is why only the empty cases were affected. Reported by Tjitse (Vendic). - OpenSearch failures were reported as empty result sets.
OpenSearchAdapter::search()caught every exception and returned[], which is indistinguishable from "nothing matched".PostSearch::search()has always wrapped the call in atry/catchthat degrades to MySQL, but no exception ever reached it, so the fallback was dead code and an unreachable cluster produced "0 results" instead. The adapter now rethrows after logging. - A failed bulk index was reported as a complete one.
OpenSearchAdapter::bulkIndex()logged per-document errors and returnedvoid, andIndexer\IndexBuilderthen added every document'spost_idto its indexed list regardless. Magento marked the index valid while documents were missing from search.bulkIndex()now returns the ids OpenSearch accepted, the builder reports only those, and a bulk request that fails outright throws instead of being swallowed. - A document OpenSearch rejected could be deleted from the index.
bulkIndex()reported only the accepted ids, andIndexer\BlogPostIndexer::executeList()reconciles by difference -- any requested id the builder does not return is assumed no longer indexable and is removed. A rejected write was therefore indistinguishable from an unpublished post, so a mapping error silently deleted a live post from search, andexecuteFull()still logged "Full reindex completed".bulkIndex()now returns aBulkIndexResultcarrying accepted and failed ids separately -- counting anything sent but not acknowledged as failed -- andIndexBuilderaborts the build when OpenSearch rejects any write, so the indexer fails loudly instead of reconciling against a short list. - Tag, category and author renames stayed stale in scheduled indexer mode.
Plugin\InvalidateIndexOnRelationChangeskipped invalidation whenever the indexer was scheduled, on the assumption that MView would pick the change up. MView is subscribed to the post tables only, and a rename lives on an entity table whose primary key does not map topost_id-- which is the exact gap this plugin exists to cover -- so nothing invalidated and the old name stayed in search until someone ran a full reindex. It now invalidates in both modes. - Search result thumbnails were served as the unresized original with no
width/height. They now use the sharedmultiblog_post_listtier and declare the dimensions of the file actually produced.
SEO Open Graph
Changed
- Version bump to 1.3.0 for the coordinated release. No functional changes in this module.
SEO Rich Snippets
Changed
- Version bump to 1.3.0 for the coordinated release. No functional changes in this module.
Sitemap
Changed
- Version bump to 1.3.0 for the coordinated release. No functional changes in this module.
Social Sharing
Changed
- Version bump to 1.3.0 for the coordinated release. No functional changes in this module.
Web API
Fixed
- The anonymous public list endpoints applied the caller's
pageSizeunbounded, and omitting it returned every row, so one request could make the server hydrate an entire table. A newModel\Search\PageSizeLimitcaps every public list at 200 and treats an absent value as the maximum rather than "no limit".
1.2.1 — 2026-08-10
Multiblog
Fixed
- Post pages returned HTTP 500 for a published post with an empty
publish_date.Block\Post\PrevNext::getPreviousPost()/getNextPost()passed the null straight intoaddFieldToFilter, which binds''and makes MySQL raiseSQLSTATE[HY000] 1525 Incorrect TIMESTAMP value: '';isEnabled()caught its own exceptions but the getters did not, so the error escaped to the storefront. Both getters now return null when the current post has no publish date — there is no ordering anchor, so there are no previous/next links. Only instances with Show Prev/Next enabled were affected. Posts saved as published with the date left empty are a normal admin outcome, so this was reachable without doing anything unusual.
Changed
- BREAKING (permissions).
Qoliber_Multiblog::post_publish("Publish Posts") is now enforced. It was declared inacl.xmlfrom 1.0.0 but never checked — every post controller,Saveincluded, guards onQoliber_Multiblog::post— so withholding it from a role changed nothing and anyone who could edit a post could publish it. NewModel\Post\PublishPermissionchecks the resource, andController\Adminhtml\Post\Saveapplies it: a user without the permission who submits a post as Published gets it saved as a draft with a warning message. Only transitions into published are restricted; editing a post that is already published leaves it published, so a contributor fixing a typo cannot take live content offline. Review roles that had "Publish Posts" withheld — they could publish before this release and cannot now.
Comments
Changed
- Version bump to 1.2.1 for the coordinated release. No functional changes in this module.
GraphQL
Changed
- Version bump to 1.2.1 for the coordinated release. No functional changes in this module.
Hyvä
Changed
- Version bump to 1.2.1 for the coordinated release. No functional changes in this module.
Hyvä Commerce
Changed
- README corrected: the storefront body swap is
Plugin\Model\PostInterfacePlugin::afterGetContent(), not theBlock\Post\Viewplugin removed after 1.0.x. No functional changes in this module.
Import
Changed
- Version bump to 1.2.1 for the coordinated release. No functional changes in this module.
RSS
Changed
- Version bump to 1.2.1 for the coordinated release. No functional changes in this module.
Search
Changed
- Version bump to 1.2.1 for the coordinated release. No functional changes in this module.
SEO Open Graph
Changed
- Version bump to 1.2.1 for the coordinated release. No functional changes in this module.
SEO Rich Snippets
Changed
- Version bump to 1.2.1 for the coordinated release. No functional changes in this module.
Sitemap
Changed
- Version bump to 1.2.1 for the coordinated release. No functional changes in this module.
Social Sharing
Changed
- Version bump to 1.2.1 for the coordinated release. No functional changes in this module.
Web API
Changed
- Version bump to 1.2.1 for the coordinated release. No functional changes in this module.
1.2.0 — 2026-07-31
Multiblog
Changed
- BREAKING. All blog search moved to
Qoliber_MultiblogSearch:Block\Search\Results,Block\Sidebar\Search,Controller\Search\Results,Model\Search\PostSearch, themultiblog_search_resultslayout, the search and sidebar-search templates, and_search.less. Core no longer ships any search code;/blog/searchreturns 404 without that module installed. Controller\Routerno longer builds controller class names from a hardcodedQoliber\Multiblog\Controller\...namespace. NewModel\Router\ControllerResolvertakes a di-injectable map keyed"<controller>/<action>", so satellite modules can own routes. Unmapped routes fall through to noroute.- The identical
forward()body previously duplicated inController\Router,Model\Router\CategoryUrlResolverandModel\Router\FlatUrlResolvernow lives only inControllerResolver.
Removed
Api\PostSearchInterface— wired in nodi.xml, never implemented, and its declared return type disagreed with the implementation.
Comments
Changed
- Version bump to 1.2.0 for the coordinated release. No functional changes in this module.
GraphQL
Changed
- Version bump to 1.2.0 for the coordinated release. No functional changes in this module.
Hyvä
Changed
- Version bump to 1.2.0 for the coordinated release. No functional changes in this module.
Hyvä Commerce
Changed
- Version bump to 1.2.0 for the coordinated release. No functional changes in this module.
Import
Changed
- Version bump to 1.2.0 for the coordinated release. No functional changes in this module.
RSS
Changed
- Version bump to 1.2.0 for the coordinated release. No functional changes in this module.
Search
Fixed
- Blog search silently degraded to MySQL whenever catalog search ran on a third-party engine.
OpenSearchAdapterresolved its client throughMagento\AdvancedSearch\Model\Client\ClientResolverand its index prefix throughMagento\Elasticsearch\Model\Config, both of which dispatch oncatalog/search/engine. Installing Elasticsuite sets that toelasticsuite, which is not a registered Magento client factory, so the client threwLogicException: There is no such client factory: elasticsuiteand the prefix resolved toNULL(producing the malformed index name_qoliber_multiblog_post). Model\Search\PostSearchfiltered on unqualified columns whileaddInstanceVisibilityFilter()joins tables that also carryis_active,instance_idandmeta_description, raisingSQLSTATE[23000] 1052 Column '<x>' in where clause is ambiguouson the MySQL path. All filter columns are now qualified withmain_table.
Added
- Own configuration section
qoliber_multiblog/search/*(hostname, port, index prefix, HTTP auth, timeout), independent ofcatalog/search. Model\Adapter\ClientBuilderbuilds the OpenSearch client directly from that config.Setup\Patch\Data\SeedSearchConnectionseeds the new section from Elasticsuite'ses_client/servers, then corecatalog/search/opensearch_*, then defaults. Never contacts the cluster and never blockssetup:upgrade.bin/magento qoliber:multiblog:search:checkverifies connectivity and index state on demand.- Blog search now owns its blocks, controller, layout, templates and LESS, moved here from
Qoliber_Multiblog.
Changed
- BREAKING (internal API).
Model\Search\PostSearchmoved to this module and its constructor now takesSearchServiceandLoggerInterface. The OpenSearch-vs-MySQL choice is an internal branch instead of a plugin. - Dropped
Magento_Search,Magento_ElasticsearchandMagento_OpenSearchfrom the module sequence and composer requirements; depends onopensearch-project/opensearch-phpdirectly.
Removed
Plugin\SearchPlugin— thearoundSearchwrapper existed only becausePostSearchlived in another module.
SEO Open Graph
Changed
- Version bump to 1.2.0 for the coordinated release. No functional changes in this module.
SEO Rich Snippets
Changed
- Version bump to 1.2.0 for the coordinated release. No functional changes in this module.
Sitemap
Changed
- Version bump to 1.2.0 for the coordinated release. No functional changes in this module.
Social Sharing
Changed
- Version bump to 1.2.0 for the coordinated release. No functional changes in this module.
Web API
Changed
- Version bump to 1.2.0 for the coordinated release. No functional changes in this module.
1.1.0 — 2026-04-27
Multiblog
Changed
- Admin menu nests under Content (
Magento_Backend::content) instead of being a top-level item. Editors find blog management next to Pages / Blocks / Widgets / Themes where they already look for content entry points. Children (Posts, Categories, Tags, Authors, Comments, Diagnostics) follow automatically because they all referenceparent="Qoliber_Multiblog::multiblog". Controller/Adminhtml/Instance/Save::executenow forwardsis_liveview_enabledfrom the POST payload onto the model. The instance Save controller cherry-picks fields (unlike Post / Category / Author, which use blanketsetData($data)); without this passthrough the Hyvä CMS Editor toggle inMultiblogHyvaCommercelooked enabled in the UI butInstanceRepositoryPlugin::afterSavesaw a null and never persisted the flag toqoliber_multiblog_instance_liveview.
Fixed
- Disabling a blog instance, or scoping it away from the current store, now takes its posts and categories offline on every public surface. Pre-1.1.0 the per-post
is_active/status/publish_datefilters checked the leaf only — posts whose parent instance was disabled remained reachable via direct URL, REST list/get, GraphQL, on-site search (MySQL fallback + OpenSearch path), and RSS feeds. NewPost\Collection::addInstanceVisibilityFilter(int $storeId)andCategory\Collection::addInstanceVisibilityFilter(int $storeId)joinqoliber_multiblog_instance+qoliber_multiblog_instance_storeand filter oninst.is_active = 1 AND inst_store.store_id IN (0, currentStore).Model\Visibility\InstanceVisibility::isInstanceVisible(int)(and the newisInstanceVisibleForStore(int, int)variant for callers that already know the store, e.g. GraphQL resolvers) is the single-entity counterpart used byController/Post/ViewandController/Category/ViewaftergetById(). Fixes audit P0-1. - Moving a category in admin (changing
parent_id) now propagates the new path to all descendants in the same transaction. Pre-1.1.0 only the saved row was rewritten; descendants kept stale paths andHelper\UrlBuilder::getCategoryUrlproduced broken canonical and category URLs and the XML sitemap exposed dead links until each descendant was touched manually. Fixes audit P1-2. - Iterating a Post collection no longer returns empty
category_ids/tag_ids/product_ids/related_post_ids.Collection::_afterLoadnow batch-loads all five relation tables (one SELECT each, grouped bypost_idin PHP). GraphQLmultiblogPostsreturned empty nested arrays before; this also unblocks listing/featured-slider blocks that read relations from the iterated items. Fixes audit P1-1. - Flat-URL resolver no longer accepts a bad trailing segment.
/blog/parent/garbagepreviously matched/blog/parentbecause the loop broke on the first missing segment but still forwarded to the deepest resolved category, splitting canonical signals across two URLs. The walker now requires every segment to resolve and returns null otherwise —Router::matchthen routes tocms/noroute/index. Fixes audit M-1.
Changed
- BREAKING for partial-save callers. Post / Category / Instance ResourceModels no longer rebuild every relation table on every save. Each
saveStoreIds/saveCategoryIds/saveProductIds/saveRelatedPostIds/saveTagIdsis gated by$object->hasData('xxx_ids'). New contract documented on the @api docblock of eachRepositoryInterface::save:- Omit a relation key from the save payload to leave existing assignments untouched.
- Pass
[]to explicitly clear them. Pre-1.1.0, a REST PUT carrying only{ "title": "X" }deleted every store / category / tag / product / related-post assignment for the post. To update the primary-category flag callers must also passcategory_ids(junction rebuild is wholesale). Admin form behaviour is unchanged — the multiblog_post_form always submits the relation arrays. Fixes audit P0-3.
Comments
Fixed
- Frontend comment submission no longer leaks an open redirect via
referer_url. Pre-1.1.0 the controller piped the POST value straight intosetUrl(); form-key validation prevented mass abuse but a phishing chain could still redirect the user off-host after submission.resolveSafeReferer()now accepts only same-origin absolute URLs or absolute paths (/...); anything else falls back to the storefront base URL. Fixes audit P1-3. - Luma
view/frontend/templates/post/comments.phtmlno longer fatals with “Cannot redeclare function renderComment” when the template is rendered twice in one request (nested includes, FPC warm path, multi-instance pages). The file-scoperenderCommentis now wrapped inif (!function_exists('renderComment')). Tier-2 plan is to port the HyvaCommentRendererViewModel pattern to Luma. Fixes audit P2.
Changed
POST /V1/multiblog/commentsis now declared inMultiblogComments/etc/webapi.xml(where thePublicCommentRepositoryInterfaceservice class lives) instead ofMultiblogWebapi/etc/webapi.xml.MultiblogComments'smodule.xmlnow sequencesMagento_Webapiand itscomposer.jsonrequiresmagento/module-webapi. DisablingQoliber_MultiblogCommentsinconfig.phpnow cleanly removes the route at boot rather than 500ing on missing service-class lookup. Fixes audit P0-2.etc/system.xmlwas relocated toetc/adminhtml/system.xml. Magento config-section discovery only loads admin-area files from the adminhtml subfolder, so theqoliber_multiblog/commentsgroup never appeared inStores → Configuration → Qoliber Multiblogeven though the module was enabled. After the move the group renders alongside General/SEO/Sidebar.
Added
qoliber_multiblog_comment.ip_addresscolumn (varchar(45)) is now declared indb_schema.xml.PublicCommentRepository::create()already wrotesetData('ip_address', ...)for moderation/audit purposes, but the column did not exist, so writes silently failed (Magento ResourceModel ignores unknown keys). Audit L-1.
GraphQL
Fixed
multiblogPostsandmultiblogCategoriesqueries respect the new instance-visibility rule. Resolvers now calladdInstanceVisibilityFilter($storeId)after the existing post-store / category-store join, so disabling a blog instance (or scoping it away from the current store) hides its posts and categories from the GraphQL surface in the same way it does on the storefront, REST, search, and RSS. Fixes audit P0-1 (GraphQL surfaces).- Single-entity resolvers
multiblogPost(post_id:)andmultiblogCategory(category_id:)now also enforce parent-instance visibility viaInstanceVisibility::isInstanceVisibleForStore($instanceId, $storeId)— a disabled or scoped-away instance threwNoSuchEntityon REST/storefront but still returned the entity over GraphQL. Fixes audit HIGH (single-resolver visibility). - Nested
category_ids,tag_ids,product_ids,related_post_idsare now populated on items returned bymultiblogPosts. Pre-1.1.0 the underlying Post collection only hydratedstore_ids, so every post in the response had empty arrays for the other four relations — fixed inQoliber_Multiblog 1.1.0by extendingPost\Collection::_afterLoad. Fixes audit P1-1 (consumed here).
Hyvä
Fixed
- Hyvä top-navigation no longer emits duplicate menu entries for an instance/category that is assigned to both "All Store Views" (store_id 0) and a concrete store.
AddBlogToHyvaNavigation::getActiveInstances()andgetInstanceCategories()nowGROUP BYafter joiningqoliber_multiblog_instance_store/qoliber_multiblog_category_store, matching what the Luma menu builder already did. Fixes audit L-2.
Hyvä Commerce
Added
- Hyvä CMS LiveView Editor support extended to instance, category, and author content types — alongside the existing
multiblog_post. Each new entity gets its own provider stack:- Schema:
qoliber_multiblog_<entity>_liveview(draft / published JSON +is_liveview_enabledflag),qoliber_multiblog_<entity>_liveview_version_history, andqoliber_multiblog_<entity>_liveview_tailwindcss(3 tables × 3 entities = 9 new tables, all CASCADE-deleting on the parent entity). - Provider classes:
MultiblogInstanceProvider,MultiblogCategoryProvider,MultiblogAuthorProvider, each registered under a dedicated entity-type key inHyva\CmsLiveviewEditor\Model\ProviderPool. - Frontend swap plugins:
InstanceInterfacePlugin::afterGetDescription,CategoryInterfacePlugin::afterGetDescription,AuthorInterfacePlugin::afterGetBio— same preview/published switching behaviour as the post variant; falls back to the original WYSIWYG body on any error. - DataProvider plugins to hydrate
is_liveview_enabledinto each form, repository plugins to sync that flag back into the liveview row on save. - System config:
hyva_cms/multiblog/{instance,category,author}_enabled+..._enabled_by_defaultflags, gated on the masterhyva_cms/general/enabledswitch.
- Schema:
- Inline editor pattern on all four entity forms (post / instance / category / author), mirroring
cms_page_form.xml:- Toggle (
is_liveview_enabled) renders alongside the WYSIWYG description / content / bio field. - When the toggle is on, the WYSIWYG is hidden and a preview iframe + "Edit with Hyvä CMS" link appear in its place.
- Driven by a Multiblog-specific port of
open-liveview.phtml(Qoliber_MultiblogHyvaCommerce::ui/open-liveview.phtml) that walks theuiRegistryto flip the WYSIWYG field'svisibleobservable directly. The upstreamswitcherConfigrule fires before PageBuilder's wysiwyg field finishes wiring, so the hide call is dropped — our template applies it post-init via thedataProvider.on('data.is_liveview_enabled', …)subscription. Each form's merge XML passes its WYSIWYG target through a newwysiwyg_targetblock argument.
- Toggle (
- Close-editor → entity edit page routing fixed for all four entity types. Each
core-settings/<entity>.phtmltemplate now dispatchesliveview:set-return-urlson Alpineinit(), telling the editor's go-back handler to land back on the admin edit form (or the entity index for new records). New core-settings templates added for instance / category / author (post got the dispatch line added). Without this dispatch the editor'sliveview-composer.phtmlleavesreturnUrls.entityPathat its admin-dashboard default, so closing the editor sent the merchant to the dashboard. liveview_editor.xmllayout now registerscore-settings.multiblog_{instance,category,author}blocks and the matching listings entries, so the editor toolbar shows the correct entity label and the navigator can switch between Multiblog content types.multiblog/{instance,category,author}/editadmin routes added toHyva\CmsLiveviewEditor\Model\Security\IsValidAdminPreviewRequest::allowedRoutesso auto-CSP applies the correct frame-src on multi-domain setups.
Fixed
- PHPStan level-8 cleanup:
LiveviewPostRepository::getByPostIdnow passes the post id toaddFieldToFilteras['eq' => $postId](the int form was rejected);save()checksinstanceof \Magento\Framework\Model\AbstractModelbefore calling$resource->save()instead of using a@varcast;CouldNotSaveExceptionis rethrown only on\Exception(not\Throwable) so the cause-type contract holds.Controller/Adminhtml/Link/MultiblogPosts::executedrops the no-op?? []ongetStoreIds()(declared asarrayin the interface).
Notes
- The toolbar
Block\Adminhtml\<Entity>\Edit\HyvaCmsButtonclasses are still wired but no longer referenced from the form ui_components. The inline "Edit with Hyvä CMS" link in the new preview block supersedes them; the classes stay in the codebase as a fallback API for downstream extensions and may be removed in 1.2.0.
Import
Fixed
- WordPress author import now sets
instance_idon the savedqoliber_multiblog_authorrow. Pre-1.1.0EntityImporter::importAuthors()accepted just($authors, $dryRun, $startOffset)and never propagated the parent import's instance, so imported authors were instance-orphans — author URLs/listings/REST/GraphQL are instance-aware and silently hid them. The signature gains a requiredint $instanceIdbetween the array and the dry-run flag;ImportWordPressCommandalready has the value ($jobId = $importJobTracker->startJob(self::JOB_TYPE, $filePath, $instanceId)) and now threads it through. Fixes audit M-4.
Changed
- BREAKING (internal API).
EntityImporter::importAuthors()signature changed from(array $authors, bool $dryRun = false, int $startOffset = 0)to(array $authors, int $instanceId, bool $dryRun = false, int $startOffset = 0). Direct callers must update; the bundled CLI command was updated.
RSS
Fixed
- All four feed controllers (instance, category, tag, author) now apply the instance-visibility filter to the post collection. Pre-1.1.0 a feed for an instance whose
is_activewas off, or whose store assignment did not include the current store, would still render posts (post-level filters caught most cases but the parent-store leak slipped through). Fixes audit P0-1 (RSS surfaces).
Search
Fixed
Plugin\SearchPlugin::aroundSearch(the OpenSearch hot path) now appliesaddInstanceVisibilityFilter($storeId)to the rebuilt result-set collection. Pre-1.1.0, a post indexed in OpenSearch could surface in search results even after its parent instance was disabled or scoped to a different store. Fixes audit P0-1 (on-site search). Same fix lands on the MySQL fallback inQoliber\Multiblog\Model\Search\PostSearch.SearchService::search()no longer swallows OpenSearch failures and returns[]. The empty-array fallback was indistinguishable from a legitimate "0 hits" result, soSearchPluginrendered "no posts found" during outages instead of falling back to the MySQL LIKE search. The exception now propagates andSearchPlugin's outertry/catchcalls$proceed()to hand the query to the MySQL implementation. Fixes audit M-3a.OpenSearchAdapter::search()no longer drops indexed posts whosepublish_dateis null. Therange publish_date <= nowfilter excluded null values, while the MySQL collection accepts them via[lteq=>now] OR [null=>true]. Replaced with abool > shouldclause covering both branches. Fixes audit M-3b.
Changed
- BREAKING (internal API).
SearchService::__construct()no longer acceptsPsr\Log\LoggerInterface(the dependency was only used by the now-removed swallow-and-log path). Direct constructor callers should drop the second argument; DI wiring is unaffected.
SEO Rich Snippets
Fixed
BlogPostDataExtractornow formats postdatePublished/dateModifiedas UTC explicitly vianew \DateTimeImmutable($date, new \DateTimeZone('UTC')). Pre-1.1.0 the constructor used the host timezone, which produced unstable JSON-LD across servers and tripped the unit-test expectation. Output is now a stableY-m-d\TH:i:s+00:00regardless of where Magento is running.
Sitemap
Fixed
PostItemProviderandCategoryItemProvidernow scope the visible-instance cache by store. Pre-1.1.0 the cache was keyed only byinstance_id, so when the same provider instance was reused across multiple stores during one sitemap run, later stores reused the first store's instance-visibility set — store B's sitemap could include posts whose parent instance was hidden from store B. Cache structure changed toarray<int $storeId, array<int $instanceId, InstanceInterface>>and the existence check / lookup were updated accordingly. Fixes audit M-2.
Social Sharing
Fixed
- Post-page Open Graph / Twitter Card meta tags now render in
<head>. Pre-1.1.0multiblog_post_view.xmlplaced the OpenGraph block in thebefore.body.endcontainer, but social-card crawlers (Facebook scraper, Slack/Discord unfurl, Twitter card validator) only inspect the head — the post-level OG payload was effectively invisible to them. Block moved tohead.additional, matching the category and instance layouts that already used it. Fixes audit M-5.
Web API
Fixed
PublicPostRepository::getListandgetByIdnow apply the instance-visibility rule (post and parent instance must be active and store-scoped). Pre-1.1.0 only the per-postis_active/status/publish_datechecks ran, so a post under a disabled instance was still served via REST. Same fix onPublicCategoryRepository. Fixes audit P0-1 (REST surfaces).
Changed
POST /V1/multiblog/commentsis no longer declared in this module'swebapi.xml. The route lived here historically but the service class belongs toQoliber_MultiblogComments, so disabling Comments left a dangling route entry that 500'd at boot. Route moved toMultiblogComments/etc/webapi.xml(see Comments 1.1.0). Fixes audit P0-2.
1.0.1 — 2026-04-21
Multiblog
Fixed
- Admin entity saves (category, post, instance, tag, author) no longer persist an empty-string primary key through
setData(). The Save controllers null out the empty hidden PK before$model->setData($data), matching Magento coreMagento_Cmsconvention — was silently redirecting to the grid with no success message on new-record saves. - Category WYSIWYG description rendered as HTML-escaped text on the frontend.
Block\Category\View::getFilteredDescription()now runs the content through\Magento\Cms\Model\Template\FilterProvider::getPageFilter()(widgets, directives, inline<style>all parse correctly), mirroring the existing post-content flow. Both the Luma and Hyvacategory/view.phtmltemplates were updated. - Swagger / REST schema generation at
/rest/all/schema?services=...returned HTTP 500 (Each method must have a doc block). Magento'sWebapi\Reflection\TypeProcessorrequires PHPDoc@return/@paramon every service-contract method — PHP 8 native return types are not enough. Added annotations to all methods onApi\Data\{Author,Category,Instance,Post,Tag}Interface(196 methods).
Changed
- UI form
requestFieldNameunified withprimaryFieldName(xxx_id) in all five admin forms, matching the modernMagento_Cmspattern. Admin edit URLs are now/edit/xxx_id/N; Edit / Delete / Preview / Duplicate controllers,GenericButton/DeleteButton/PreviewButton/DuplicateButtonURL builders, and the Post product-grid tabs all read and emit the new param name.
Comments
Fixed
- Swagger / REST schema generation at
/rest/all/schema?services=qoliberMultiblogCommentsPublicCommentRepositoryV1returned HTTP 500 (Each method must have a doc block). Added PHPDoc@return/@paramannotations to all 22 methods onApi\Data\CommentInterfaceso Magento'sWebapi\Reflection\TypeProcessorcan build the schema.
Hyvä
Fixed
- Category description in the Hyva
category/view.phtmltemplate was HTML-escaped, so WYSIWYG widgets, inline<style>, and page-builder directives rendered as literal text. Template now calls$block->getFilteredDescription()(provided byQoliber\Multiblog\Block\Category\View, 1.0.1+) and emits the result with@noEscape. Added the Tailwindprose max-w-nonewrapper for consistent typography on long-form descriptions.
1.0.0 — 2026-04-13
Multiblog
Fixed
- Block cache key collision causing siblings to render the first sibling's HTML when
block_htmlcache was enabled (affectedPost\View,Category\View,Instance\View— addedgetNameInLayout()togetCacheKeyInfo()) - Author URL generation: slug-based, instance auto-resolved from
AuthorInterface, addedgetAuthorUrlBySlug()for cached listing contexts - Post content now processed through CMS/PageBuilder filter
- Product cards on post pages render proper grid with special price strikethrough
- Cross-instance post disclosure: post view verifies matched instance to prevent leaking posts across instances
parameter.implicitlyNullabledeprecations in 3 Grid Collection classes (PHP 8.4 compatibility)- URL suffix config now actually applied (previously dead config)
- Unique
(instance_id, url_key)constraint on tags with dedup data patch
Added
- PHP 8.1–8.4 compatibility range
- URL collision admin page + CLI diagnostic
- Per-post OG overrides (
og_title,og_description,og_image,twitter_card_type) - Image sitemap entries + paginated-listings canonical/robots + locale alternates
- Full
BlogPostingJSON-LD withBreadcrumbList,og:imagedimensions,article:tag - Scoped-to-instance authors (new FK + data patch)
is_primaryflag onqoliber_multiblog_post_categoryfor deterministic canonical- Async view count via
qoliber_multiblog_post_view_log+ hourly rollup cron + bot detection - Block-level cache identities +
PostRelatedContentViewModel extraction - Category-path single-query URL resolution
Routerdecomposed intoFlatUrlResolver+CategoryUrlResolverstrategies- Luma topmenu navigation tree caching
- Meta-title suffix via observer
Changed
- Composer package renamed from
qoliber/module-multiblogtoqoliber/multiblog - Service contract interfaces expanded to cover all persisted fields (BREAKING)
- Router caches instance route lookups
Security
- WordPress import hardened against SSRF and OOM attacks
Comments
Added
- Frontend comment submission with honeypot + rate limit.
- Email notifications to admin on new submissions and to commenter on approval (both configurable).
- Admin moderation controllers: approve, reject, delete, bulk actions.
- Nested-reply rendering with configurable depth (Hyva ViewModel).
- IP address column on
qoliber_multiblog_commentfor moderation context. CommentRepositoryPlugincentralizing side effects (count update + approval notification) viaafterSavewith status transition detection.- Integration tests for submission and moderation flows.
Fixed
- Comment submission enforces full post visibility (
is_active = 1,status = published,publish_date <= now()) and honours instanceallow_commentsflag.
GraphQL
Added
- Batch resolvers on nested relationships (
BatchResolverInterface) — eliminates N+1 acrossauthor,category,tag, and instance traversal. SortInputtype withfield+directionon all list queries.- Author resolvers no longer expose
emailfield (information disclosure).
Changed
- Composer package renamed from
qoliber/module-multiblog-graph-qltoqoliber/multiblog-graph-ql. - PHP constraint bumped to 8.1–8.4.
Security
emailstripped fromMultiblogAuthorGraphQL type.
Hyvä
Added
- Hyvä-compatible templates for instance, category, post, author, tag, and search views.
- Hyvä navigation integration via
AddBlogToHyvaNavigationplugin. - Tailwind-based styling for all frontend blocks, including comment thread and share buttons.
CommentRendererViewModel for configurable nested comment rendering.- Accessibility landmarks (
<nav>,aria-label) on post and listing templates.
Changed
- Composer package renamed to
qoliber/multiblog-hyva. - PHP constraint bumped to 8.1–8.4.
Hyvä Commerce
Added
- Initial release. Bridges Multiblog posts into the Hyvä CMS LiveView Editor.
MultiblogPostProviderimplementsHyva\CmsLiveviewEditor\Api\ProviderInterfaceand is registered as themultiblog_postcontent type in the HyväProviderPool.- Schema:
qoliber_multiblog_post_liveview(draft / published JSON +is_liveview_enabledflag),qoliber_multiblog_post_liveview_version_history, andqoliber_multiblog_post_liveview_tailwindcss. All three CASCADE-delete on the parent post. Qoliber\MultiblogHyvaCommerce\Block\Adminhtml\Post\Edit\HyvaCmsButtonadds an "Open in Hyvä CMS" toolbar button to the admin post edit form, deep-linking intoliveview-editor/page/edit?type=multiblog_post&id=N.Plugin\Block\Post\View::afterGetFilteredContentswaps the post body for the Hyvä CMS-rendered component tree whenis_liveview_enabledis set — Luma and Hyvä themes both inherit the swap because they call the same$block->getFilteredContent()getter.- README documents Tier 1 scope and what is deferred to Tier 2 (instances / categories / tags content types, schedule provider, version history UI, Tailwind JIT, AI translation bridge).
Notes
- Requires an active Hyvä Commerce CMS license — this module is the bridge only and depends on
hyva-themes/commerce-module-cmsfor the editor itself.
Import
Added
- Resumable imports via
qoliber_multiblog_import_jobtable — restart a failed import from the last successfully processed chunk. - Chunked commits with transaction boundaries and duplicate detection.
url_key-based resolution for updating existing entities instead of always inserting.- WordPress author linking by login/email to existing
qoliber_multiblog_authorrows.
Security
- WordPress import hardened against SSRF and OOM attacks: blocked internal IPs, disabled XML entity expansion, enforced size limits on media downloads.
Changed
- Composer package renamed from
qoliber/module-multiblog-importtoqoliber/multiblog-import. - PHP constraint bumped to 8.1–8.4.
RSS
Added
- Server-side feed cache with publish-event invalidation (feeds no longer rebuilt on every request).
- Unit and integration tests covering XML generation and per-scope feeds.
Changed
- Composer package renamed from
qoliber/module-multiblog-rsstoqoliber/multiblog-rss. - PHP constraint bumped to 8.1–8.4.
Search
Added
- Chunked reindex by post ID range (reindex no longer loads all posts in memory).
- MView subscriptions extended to relation tables (
post_category,post_tag) — edits to relations now trigger reindex. - Unit test coverage on search plugin, service, and reindex command.
Changed
- Composer package renamed from
qoliber/module-multiblog-searchtoqoliber/multiblog-search. - PHP constraint bumped to 8.1–8.4.
Sitemap
Added
- Initial release: core sitemap integration extracted from the main module into its own package.
ItemProviderInterfaceimplementation exposing posts, categories, and instances to the Magento sitemap generator.- Image sitemap entries on post items (featured image URL + title).
- Per-entity priority and change-frequency configuration via
system.xml. - Multi-store support: each store's sitemap only contains entities assigned to that store.
Changed
- Replaces the earlier observer-based approach; sitemap entries now surface through the standard Magento sitemap pipeline with correct lastmod handling.
Social Sharing
Fixed
- Open Graph meta tag property names were HTML-encoded (
og:title→og:title), rendering them invisible to crawlers. Property names now allowlist-filtered before emission.
Added
- Listing pages render share buttons when the instance config's
show_on_listingflag is set.
Changed
- Composer package renamed from
qoliber/module-multiblog-social-sharingtoqoliber/multiblog-social-sharing. - PHP constraint bumped to 8.1–8.4.
Web API
Added
- Public REST endpoint for comment submission (
POST /V1/multiblog/comment) with rate limiting and validation. - REST route ownership consolidated into this module (removed duplicate routes from core).
Changed
- Composer package renamed from
qoliber/module-multiblog-webapitoqoliber/multiblog-webapi. - PHP constraint bumped to 8.1–8.4.
0.11.0 — 2026-04-08
Multiblog
Added
- Store scoping on all frontend listing blocks (post collections now join
qoliber_multiblog_post_storeto filter by current store) - Related blog posts block on product pages (
catalog_product_viewlayout) - Post preview from admin with secure token-based frontend rendering (Preview button on post edit form)
- Post duplication controller (Duplicate button on post edit form, creates a draft copy)
Fixed
- Posts restricted to specific stores no longer appear on other stores in instance, category, author, tag, sidebar, search, and prev/next listings
0.10.0 — 2026-04-08
Multiblog
Added
- Category image field for listing and detail pages
- Instance description with configurable position control (above/below posts)
- Pagination-aware canonical URLs for listing pages
- Featured section limited to page 1 only
- Category-based canonical URLs for posts using category URL structure
- Store scoping improvements: removed duplicate core GraphQL schema/resolvers in favour of MultiblogGraphQl companion module
- Social share buttons on instance and category listing layouts (makes
show_on_listingconfig functional)
Fixed
- Removed core
schema.graphqlsandModel/Resolver/directory that duplicated MultiblogGraphQl resolvers without store filtering - Playwright test specs: replaced invalid
toHaveCount({ minimum: 1 })with propertoBeVisible()assertions - Removed outdated
INTEGRATION_TESTING.md(testing documented in main README)
0.9.0 — 2026-04-08
Multiblog
Added
- Multi-instance blog architecture with independent route, category, and post management per instance
- Blog Instance entity with CRUD, admin grid/form, and flexible URL structure configuration
- Post entity with full lifecycle: draft, published, archived statuses
- Category entity with hierarchical tree structure and parent validation
- Tag entity with full CRUD, admin grid/form, tag chips UI with autocomplete in admin
- Comment entity with basic admin scaffolding (no frontend comments UI)
- Standalone Author entity with full CRUD, admin grid/form (name, bio, image, social links, URL slugs)
- Featured post flag (
is_featuredtoggle on posts) - Featured article and featured categories on instance homepage
- Post display modes (list / grid) configurable per instance
- Previous / next post navigation (configurable per instance via
show_prev_nexttoggle) - Navigation menu integration with per-instance show/hide and sort order
- Sidebar widget removal (compare, wishlist) configurable per instance
- Image upload with featured image support and image resize helpers
- UrlBuilder service for consistent URL generation across all templates
- Sidebar widgets: search, recent posts, popular posts, categories tree, tag cloud, archive
- Post search with full-text matching across title, content, excerpt
- Pagination on all listing pages (instance, category, author, tag views)
- Post view counter with configurable frontend display
- Reading time estimation (auto-calculated on save)
- Related products on posts (with product cache tag propagation)
- Related posts
- Scheduled publishing with cron auto-publish every 5 minutes
- SEO: meta title, description, keywords per entity
- SEO: canonical URL rendering on post, category, and instance controllers
- SEO: per-entity meta robots configuration on post, category, and instance controllers
- SEO: basic JSON-LD structured data (BlogPosting/NewsArticle) block and template
- SEO: proper Magento breadcrumbs integration via Breadcrumbs block
- Sitemap integration (posts, categories, instances with priorities)
- Multi-store support with store filtering on all frontend controllers
- Composite DB indexes for routing performance
- Unique constraints on (instance_id, url_key) to prevent URL collisions
- Admin configuration panel (system.xml) with 30+ settings
- ACL resources for granular admin permissions
- Cache identity support with proper cache tags including product references
- Configurable date display, author display, and reading time visibility in templates
- Full integration test suite
- Unit tests for URL builder, reading time, post scheduler
- adminhtml and frontend _module.less CSS files
Planned
- Open Graph tags rendering (config stub exists, rendering deferred to companion module)
Fixed
- Layout XML child blocks now use correct block classes (were rendering empty)
- Template URLs now use UrlBuilder service (were generating broken 404 links)
- PostInterface now declares all methods formally (were only @method annotations)
- Store filtering enforced on all frontend controllers
- Author controller validates author existence before rendering
- Category tree path building moved to _afterSave (was using null ID in _beforeSave)
- Hardcoded status strings replaced with PostInterface constants
- Category/Instance controller indentation errors
- Module composer.json now declares magento/module-sitemap dependency
Security
- Frontend controllers enforce store assignment
- Published + active + date filters on all public-facing queries
GraphQL
Added
- Initial release of the GraphQL API layer for
Qoliber_Multiblog. multiblogInstancequery -- fetch a single blog instance by ID with active and store checks.multiblogInstanceByRoutequery -- fetch a blog instance by URL route with active and store checks.multiblogInstancesquery -- list blog instances with filter support, active and store enforcement.multiblogPostquery -- fetch a single post by ID with published, active, publish date, and store checks.multiblogPostsquery -- list posts with filter support and mandatory published/active/store enforcement.multiblogCategoryquery -- fetch a single category by ID with active and store checks.multiblogCategoriesquery -- list categories with filter support, active and store enforcement.multiblogTagquery -- fetch a single tag by ID.multiblogTagsquery -- list tags with filter support.MultiblogTagGraphQL type andMultiblogTagswrapper type.MultiblogTagFilterInputfilter input type.tag_idsfield added toMultiblogPosttype.FilterApplierhelper class for applyingeq,in, andlikefilter conditions to search criteria.- Store-scoped security: all resolvers read store context from GraphQL extension attributes.
- Post security: posts are only returned when
is_active = 1,status = 'published', andpublish_date <= now(). - Instance/category security: only active entities assigned to the current store are returned.
Fixed
- Filter arguments (
$args['filter']) are now applied to search criteria. The original core module resolvers ignored filter arguments entirely. - Store context is now derived from GraphQL context (
$context->getExtensionAttributes()->getStore()) instead ofStoreManagerInterface, which is the correct approach for GraphQL resolvers.
Import
Added
- WordPress WXR/XML import command (
multiblog:import:wordpress) - CSV import command (
multiblog:import:csv) supporting posts, categories, and tags - CSV export command (
multiblog:export:csv) supporting posts, categories, and tags - Dry-run mode for all import commands
- WordPress post status mapping (publish, draft, private)
- WordPress attachment/featured image import support
- Category parent relationship preservation during import
- Category and tag name-based resolution for post associations
RSS
- Instance RSS feed controller (
/multiblog-rss/feed/instance/instance_id/{id}) - Category RSS feed controller (
/multiblog-rss/feed/category/instance_id/{id}/category_id/{id}) - Author RSS feed controller (
/multiblog-rss/feed/author/instance_id/{id}/author_id/{id}) - Tag RSS feed controller (
/multiblog-rss/feed/tag/instance_id/{id}/tag_id/{id}) - RSS 2.0 XML generator with Atom namespace support
- Shared RSS data provider for building feed data from post collections
- FeedLink block for RSS auto-discovery in HTML head
- Layout XML for instance and category view pages
- Default configuration: enabled with 20 posts per feed
Search
Added
- Initial release of OpenSearch-powered blog search
- BlogPostIndexer with full and partial reindex support
- Mview subscription on
qoliber_multiblog_posttable for real-time indexing - OpenSearch adapter with index management, bulk indexing, and search
- SearchService with availability checks
- Frontend plugin on
PostSearch::searchto route queries through OpenSearch - Automatic fallback to MySQL LIKE search when OpenSearch is unavailable
- CLI command
multiblog:search:reindexfor manual reindexing with progress output - Multi-field search with relevance boosting (title 3x, excerpt/tags/categories 2x)
- Fuzzy matching support via OpenSearch
- Relevance-ordered results preserved in post collection
SEO Open Graph
Added
- Initial release.
PostResolverimplementingResolverInterfacefor blog post OG tags (article type, published_time, author, section).CategoryResolverfor blog category OG tags (website type).InstanceResolverfor blog instance OG tags (website type).AuthorResolverfor author page OG tags (profile type).- DI registration of all resolvers in
GetTagResolverservice with\Proxylazy loading. - Layout handles with OG namespace prefixes for article and profile types.
- Layout overrides to remove MultiblogSocialSharing fallback OG blocks on all view pages.
SEO Rich Snippets
Added
- Initial release.
BlogVariableProviderimplementingVariableProviderInterfacewith 15 blog post variables.BlogValueResolverimplementingValueResolverInterfacefor resolving{{blog.*}}template placeholders.- DI registration for
SchemaVariables,VariableExtractor, andJsonLdblock. - Blog page type registration (post, category, instance, author) in
PageTypesource model. - Layout overrides to remove core Multiblog structured data blocks on all view pages.
Social Sharing
- Social sharing buttons block with configurable platforms
- Open Graph and Twitter Card meta tag fallback for posts, categories, and instances
- Copy Link button with clipboard API support
- Frontend LESS styling for share buttons
Web API
Added
- Initial release
- REST API endpoints for Instance, Post, Category, Tag
- Public endpoints enforce published + active + store filtering
- Admin endpoints with ACL protection