Core 1.5.0 Stable
Released on:
Wednesday, 09 September 2026 07:26
The release that gives a store a front page. Five new storefront surfaces — a slideshow hero, category tiles, a brand band, a recently-viewed strip and a Storefront home menu item — plus a Deals page built from the product's own compare-at price, so a landing page is modules rather than an empty article holding hand-written HTML.
Alongside them: every store becomes discoverable to AI agents from its domain alone through /llms.txt, the admin gains an Extensions tab over everything Solidshop has installed, and prices get their decimal places back — a $104.32 product had been rendering as USD104, and being handed to payment gateways as 104 minor units.
Schema: two nullable columns on brands, plus one guarded data migration.
Added
- Storefront home menu item (
view=home) — a landing page is modules, and until now they needed an empty article to sit on. It prints at most a page heading (off when a hero carries theH1) and the store tagline, and does the head's work: title and meta description from the store, canonical on the site root, and one JSON-LD block with an Organization node and a WebSite node whoseSearchActionpoints at the Products menu item. Nodes built bySolidshop\Lib\Seo\StorefrontSchema(pure, unit-tested); a new Tagline field on Store → SEO is printed on the page and published as the slogan. - Slideshow module (
mod_sshop_slideshow, core bundle) — aslidessubform of picture, eyebrow, heading, text and a button linking to a product, a category (RouteHelper::getCategoryRoute(), the URL that listing publishes as canonical), a menu item or a URL. CSS scroll-snap track, so swiping and keyboard scrolling work before any script runs and the first picture isfetchpriority=high; the script adds Previous / Next, dots, an optional 3–60 s timer with Pause and WAI-ARIA carousel semantics (paused on hover, on focus, off-screen and for reduced motion). One slide is a static hero. No slider library in front of the page's largest element. - Category showcase module (
mod_sshop_categories, core bundle) — a 2–6 column grid or a scroll-snap strip of category tiles: everything below a parent to a chosen depth, or categories picked by hand. A tile borrows the first product's picture when the category has none, the optional count uses the listing's own predicates, empty categories are left out, and no tile links to a category the visitor may not view. The query count does not move with the tile count. - Brands module and brand logos (
mod_sshop_brands, core bundle) — brands gain a logo (media field, resolved under the store's media folder), and the module prints a "Shop by brand" band, grid or strip, each logo linking to the listing filtered by that brand. Two queries whatever the band holds; brands with nothing published are left out. The logo plate stays light in both colour schemes, since brand artwork is nearly always dark ink on transparency. - Brands carry a store scope — a nullable
store_idon#__sshop_brands, NULL on every existing row, meaning the brand belongs to the site and every store may use it.Solidshop\Lib\Product\BrandQueryis the single place that says which brands a store may use and resolves a name to an id; the CSV importer and the VirtueMart migrator go through it instead of assuming a brand name is unique across the table. - Recently viewed module (
mod_sshop_recent, core bundle) — the shopper's own trail, written into the session by the site display controller (a guest's product page is served from Joomla's view cache, where the view never runs) and nowhere else: no table, no profile.Solidshop\Lib\Product\RecentlyViewed(pure, unit-tested) holds the rules — a repeat view moves rather than repeats, session junk is dropped, twenty ids are kept so a product that has gone gives up its place instead of shortening the strip. Cards carry the shopper's view levels. - Deals — sale surfaces on the product's own price pair.
price_origin > priceis now one predicate inProductListingQuery, with four surfaces on it: a Products on Sale Only menu item (a Deals page), Show / Hide / Only plus a Discount ordering on the products module, an On sale filter toggle counted like every facet, and a Biggest discount sort key (discount_desc), also on the REST API and the MCPsolidshop_search_productstool. The product row and nothing else: a discount granted at cart time is not a listing predicate. - The filters module folds into a drawer on narrow screens. Below Drawer Below Width (default 992 px) the panel moves into an off-canvas drawer and a Filters button appears on the listing toolbar beside the sort control. Filters still apply live; the primary button reads Show N results, a Clear all sits beside it, and the toolbar button counts the active values. Dialog semantics as the mini-cart drawer — focus trap, Escape, scroll lock, reduced motion, RTL.
0keeps the panel inline. /llms.txtagent-discovery file. Served at the domain root through a parse rule in the core system plugin, and at the raw URLindex.php?option=com_solidshop&view=llms&format=raw(&store_id={id}for one store). Rendered on request from the store: name and summary, country, currencies and languages, the MCP endpoint and its tool names, the REST routes, the sitemap, up to 50 top-level categories with a visible product, policy pages, contact details, merchant notes, plugin sections and a platform credit. Declarative throughout — it states what exists and where, never instructs the agent; merchant text is escaped and headings in it flattened.text/plain, one-hour cache,X-Robots-Tag: noindex, and a physicalllms.txtin the web root always wins.- AI-discovery settings — Options → AI discovery (llms.txt) (serve the file, per-store documents, list categories, platform credit; all on by default), two Store → SEO fields (Store summary for AI agents, also the store description in the MCP
solidshop_get_store_info, and Notes for AI agents), and a System → Dashboard card with the URLs and two group-awarerobots.txtchecks (RFC 9309): which ofGPTBot,OAI-SearchBot,ChatGPT-User,PerplexityBot,ClaudeBotare shut out of the site, and which are shut out of the catalogue API — Joomla's stockrobots.txtshipsDisallow: /api/, so that row is red on every unmodified install and shows theAllow:line that reopens the Solidshop routes. LlmsSectionsEvent(onSolidshopLlmsSections) so a plugin can append a store-aware section, andToolRegistry::CORE_TOOLS, the pinned MCP tool names the file prints, asserted against thetools/listsnapshot so the two cannot drift.- System → Extensions tab — every Solidshop extension on the site with its version, enabled state, pending update and anything wrong with it, then the add-ons published but not installed here. Rows are discovered from
#__extensionsby naming convention plus the three plugin groups we own, so a third-party plugin in them is reported too and the tab cannot go stale against later releases. Updates come from#__updatesincluding the unmapped rowscom_installerhides, and an extension shipping an update server with no update site row is flagged — Joomla would never offer it an update. What discovery cannot know is declared inSolidshop\Lib\Extension\ExtensionCatalog, guarded by two drift tests. - Regenerate images after a media-settings change. Derivatives are named after their size, so changing a size or the aspect ratio left the storefront asking for files that were never written. The media manager now counts the mismatched images and offers Regenerate images: a batched, resumable AJAX run with a written / up-to-date / failed summary, an optional force rewrite and an optional prune of sizes no longer configured. Same run on the command line —
php cli/joomla.php solidshop:media:regeneratewith--store=,--force,--prune,--dry-run.ThumbnailSizesandThumbnailPlannerhold the decisions, pure and unit-tested; uploads now cut all four sizes from one decode. skuon the product page's JSON-LD, on theProductnode and everyOffer; andMedia::storeImageUrl(), one resolver for a store's logo URL.
Fixed
- Prices rendered without decimal places.
Currency::parse()fell back tonumber_decimal_points = 0while the same field's form default is2, and no currency has ever shipped with params — so the fallback was what every store used, and a $104.32 product readUSD104everywhere. Precision is not cosmetic:toFlatAmount()multiplies by10^precisionfor the minor-unit amount a gateway is charged, so the same order was worth 104 minor units — a hundredth of the real total. - Currencies whose ISO 4217 minor unit is not 2 are corrected with it —
0for BIF, CLP, DJF, GNF, ISK, JPY, KMF, KRW, PYG, RWF, UGX, UYI, VND, VUV, XAF, XOF, XPF, ZWD;3for BHD, IQD, JOD, KWD, LYD, OMR, TND. Rows that already carry params are left alone — saving a Locale tab is an opinion an update must not overwrite. - Choosing a variant answered "Variant does not exist" for most combinations. The lookup joins the combinations table once per chosen value and bound each join's placeholder to the loop variable; Joomla's
bind()holds a reference and reads it when the query runs, so every join carried the last value. Only a combination sorting first among those sharing that value ever resolved — shipped since 1.1.0. Each placeholder now binds to its own array slot. - A blank box in the variants grid discarded every variant edit. The grid posts
''for a blank box, and''in aDECIMALcolumn is rejected underSTRICT_TRANS_TABLES, so a variant with no compare-at price failed itsUPDATEand took the new price with it — for every variant of that product. Blank price, compare-at, cost and weight now bind asNULL, blank quantity as0. The failure was silent because the product model ignored the variant model's return; both loops now propagate it. - Product and category pages contributed no breadcrumbs. No storefront view had touched Joomla's pathway, so a product read
Home › Shop— and the breadcrumbs module builds itsBreadcrumbListstructured data from that same pathway, so search engines saw the same gap. A product now appends its category (linked through the route its canonical uses) and its own name, unlinked; a listing narrowed to one category or brand names itself the same way; nothing is added when the menu item already is that page. - The mini-cart drawer's remove button and quantity stepper did nothing on every page but the product and cart views. Both primitives lived in
order.js, which only those views load, whilemod_sshop_cartcorrectly depends oncom_solidshop.corealone; the stepper threw into a silent fetch handler before the badge could sync. Both are generic cart-line behaviour and moved tocore.js. - Option buttons wrapped onto rows with no space between them — the values were inline-block labels in a container with no layout, so their only spacing was markup whitespace, which spaces nothing vertically. Now a wrapping flex row with an 8 px gap.
- The products module ignored its own ordering and Featured settings — the model read only the shopper-facing sort key and defaulted it to popular, so a "Featured products" strip listed the same products as "Best sellers".
ProductListingQuerygainedapplyOrdering()(an unknown key degrades to the default sort rather than failing the page) andapplyFeatured()on show / only / hide; the module's ordering list gained Price and Units Sold. - A facet section with nothing in it printed a header over an empty box — with hide empty filters on, rows were skipped but nothing checked whether all of them had been, and since core ships no review plugin the Rating facet hit this on every storefront. Sections now gate on the rows that will render, in the template and after an AJAX refresh. Also: Clear All Filters left the boxes ticked over an unfiltered grid.
- The four media size settings were never filtered — the attribute was misspelled (
fitler="uint"), so800pxraised a warning on every image and an emptied field took the page down with a TypeError. They are now required, integer-filtered and bounded, andsrcsetcandidates carry the width each file was actually cut at instead of four hard-coded defaults. - Capability badges on the order page were unreadable —
bg-info text-darkwas written against stock Bootstrap's light cyan, but Atum redefines--infoto a dark blue, giving roughly 2.8:1, below WCAG AA. Droppingtext-darkrestores Atum's white at about 5.4:1 in both schemes; four badges were affected, and the order products table gained the edge gutter its neighbouring cards have. - The Stores list reported the country as the store's state — the state join selected the country table's columns.
- The XML sitemap was served as
text/htmlwith noX-Robots-Tag— the view queued both headers and then closed the application before Joomla's response stage flushes them. Found while building/llms.txton the same pattern.
Changed
- The default store a fresh install starts from.
sql/sample.sqlruns unconditionally, so its row is every merchant's first screen — and it shipped asLotus S.p.Awith an Italian address,as the store email and Joomla'sThis email address is being protected from spambots. You need JavaScript enabled to view it. powered_by.pngas the logo, in EUR against the US dollar rate table seeded beside it. NowMy Store, US/USD matching the seeded Springfield IL location,(RFC 2606; the field is the merchant's own notification recipient, so it cannot be blank), and emptyThis email address is being protected from spambots. You need JavaScript enabled to view it. legal_business_nameandphone, both of which print as-is on documents carrying legal weight. Existing installs are untouched. - Fresh installs show a currency's sign rather than its code —
$104.32, notUSD104.32. Seeded onto the currencies that ship published rather than made a global default: how an existing store renders a price is not an update's decision. Switchable per currency under Currencies → Locale. - The sample catalogue has brands, so a plain install has a working Brand facet instead of a filter panel that silently drops the section.
- The filters module renders only on
view=products— a product page that inherits a listing's menu item inherited its module assignment too, and the panel there was a set of links to another page. - All 22 translated locales re-synced against en-GB — 161 keys each (AI discovery, media regeneration, the home menu item, the Extensions tab, the on-sale surfaces, the filters drawer, the products module's new options), rebuilt from the en-GB skeleton so key order matches line for line; existing translations pass through untouched.
Upgrade notes
- Schema.
1.5.0.sqladdsimageand a nullablestore_idto#__sshop_brands(existing rows stay NULL — shared by every store), and setsnumber_decimal_pointson the currencies whose ISO 4217 minor unit is not 2, only whereparamsis unset, so a Locale tab you have saved survives. - Prices change on screen, and in what gateways are charged. A store that never opened Currencies → Locale has been formatting without decimals; the same catalogue now reads
$104.32where it readUSD104. Nothing stored changes — but the flat amount handed to a gateway was a hundredth of the real total for a two-decimal currency and is now correct. Check any price set to compensate, and reconcile gateway records across the update. - The core package gains four site modules —
mod_sshop_slideshow,mod_sshop_categories,mod_sshop_brands,mod_sshop_recent. Joomla installs modules unpublished and position-less; publish and place the ones you want. An update never touches an existing module layout. - Check System → Dashboard → AI discovery. Joomla's stock
robots.txtshipsDisallow: /api/, which shuts AI crawlers out of the catalogue API/llms.txtpoints them at; the card shows the singleAllow:line that reopens the Solidshop routes. The file is on by default and can be switched off under Options → AI discovery. - A storefront with a customised products toolbar keeps the filters button in the module's own position rather than on the toolbar — the drawer needs the slot the shipped toolbar template renders.
Solidshop Core v1.5.0
| File size | 9.67 Mb |
| SHA-256 Signature | fbe1856ec943e286165f647731d6e9bdb1f7f1f5eba36268f47e469b6c20c987 |
| Compatibility |