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 the H1) 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 whose SearchAction points at the Products menu item. Nodes built by Solidshop\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) — a slides subform 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 is fetchpriority=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_id on #__sshop_brands, NULL on every existing row, meaning the brand belongs to the site and every store may use it. Solidshop\Lib\Product\BrandQuery is 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 > price is now one predicate in ProductListingQuery, 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 MCP solidshop_search_products tool. 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. 0 keeps the panel inline.
  • /llms.txt agent-discovery file. Served at the domain root through a parse rule in the core system plugin, and at the raw URL index.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 physical llms.txt in 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-aware robots.txt checks (RFC 9309): which of GPTBot, OAI-SearchBot, ChatGPT-User, PerplexityBot, ClaudeBot are shut out of the site, and which are shut out of the catalogue API — Joomla's stock robots.txt ships Disallow: /api/, so that row is red on every unmodified install and shows the Allow: line that reopens the Solidshop routes.
  • LlmsSectionsEvent (onSolidshopLlmsSections) so a plugin can append a store-aware section, and ToolRegistry::CORE_TOOLS, the pinned MCP tool names the file prints, asserted against the tools/list snapshot 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 #__extensions by 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 #__updates including the unmapped rows com_installer hides, 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 in Solidshop\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:regenerate with --store=, --force, --prune, --dry-run. ThumbnailSizes and ThumbnailPlanner hold the decisions, pure and unit-tested; uploads now cut all four sizes from one decode.
  • sku on the product page's JSON-LD, on the Product node and every Offer; and Media::storeImageUrl(), one resolver for a store's logo URL.

Fixed

  • Prices rendered without decimal places. Currency::parse() fell back to number_decimal_points = 0 while the same field's form default is 2, and no currency has ever shipped with params — so the fallback was what every store used, and a $104.32 product read USD104 everywhere. Precision is not cosmetic: toFlatAmount() multiplies by 10^precision for 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 — 0 for BIF, CLP, DJF, GNF, ISK, JPY, KMF, KRW, PYG, RWF, UGX, UYI, VND, VUV, XAF, XOF, XPF, ZWD; 3 for 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 a DECIMAL column is rejected under STRICT_TRANS_TABLES, so a variant with no compare-at price failed its UPDATE and took the new price with it — for every variant of that product. Blank price, compare-at, cost and weight now bind as NULL, blank quantity as 0. 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 its BreadcrumbList structured 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, while mod_sshop_cart correctly depends on com_solidshop.core alone; the stepper threw into a silent fetch handler before the badge could sync. Both are generic cart-line behaviour and moved to core.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". ProductListingQuery gained applyOrdering() (an unknown key degrades to the default sort rather than failing the page) and applyFeatured() 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"), so 800px raised a warning on every image and an emptied field took the page down with a TypeError. They are now required, integer-filtered and bounded, and srcset candidates 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-dark was written against stock Bootstrap's light cyan, but Atum redefines --info to a dark blue, giving roughly 2.8:1, below WCAG AA. Dropping text-dark restores 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/html with no X-Robots-Tag — the view queued both headers and then closed the application before Joomla's response stage flushes them. Found while building /llms.txt on the same pattern.

Changed

  • The default store a fresh install starts from. sql/sample.sql runs unconditionally, so its row is every merchant's first screen — and it shipped as Lotus S.p.A with an Italian address, This email address is being protected from spambots. You need JavaScript enabled to view it. as the store email and Joomla's powered_by.png as the logo, in EUR against the US dollar rate table seeded beside it. Now My Store, US/USD matching the seeded Springfield IL location, This email address is being protected from spambots. You need JavaScript enabled to view it. (RFC 2606; the field is the merchant's own notification recipient, so it cannot be blank), and empty legal_business_name and phone, 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, not USD104.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.sql adds image and a nullable store_id to #__sshop_brands (existing rows stay NULL — shared by every store), and sets number_decimal_points on the currencies whose ISO 4217 minor unit is not 2, only where params is 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.32 where it read USD104. 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.txt ships Disallow: /api/, which shuts AI crawlers out of the catalogue API /llms.txt points them at; the card shows the single Allow: 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

Joomla! 6.0 Joomla! 6.1

File size 9.67 Mb
SHA-256 Signature fbe1856ec943e286165f647731d6e9bdb1f7f1f5eba36268f47e469b6c20c987
Compatibility Joomla! 6.0 Joomla! 6.1