---
title: "Smart Grid | Advanced Content Grid Element for YOOtheme Pro Sites"
description: "Build smarter content grids in YOOtheme Pro with favorites, ratings, filters, search, social feeds, Joomla articles and ZOO support for dynamic content."
url: "https://www.yoozecase.com/?item_id=1"
date: "2026-10-08T06:45:18+00:00"
language: "en-GB"
---

#  Smart Grid

We 've created a beast!

**Latest Version**
4.8.1
**Requires**
Yootheme Pro 5+, Joomla 5+, PHP 8.1 or newer
**Release date**
02 October 2026
**Made for**
Yootheme Pro, Joomla
**License**
GPL-2.0-or-later
**Price**
€65.00 (incl. 6 months of updates)

 [ 🛒 Get it for Joomla 👍 ](https://yoozecase.lemonsqueezy.com/checkout/buy/715f7f63-a1b9-490d-9539-3950f48b55c4)

 [ Demo page ](https://www.yoozecase.com/smart-grid-demo)

Ever wish your site could remember what each visitor loves? Smart Grid makes it happen. Every card it shows gets a heart and a 5-star rating - visitors save what they like with a tap, filter the page down to just their favourites in a click, and show you what's actually worth the time. Then fill those cards with anything: your Joomla articles, your ZOO items, hand-picked highlights, or live posts from eight social platforms. One drag-and-drop YOOtheme Pro element, one Joomla install, zero code - and easily the most epic grid you'll ever add to a page.

##  What's new in version 4.8.1

- **Security:** RSS feed descriptions are cleaned with Joomla's own HTML filter. A feed could previously slip a script link or page-covering styling into a description. With the "strip all formatting" setting and no shortening, text that a feed sent as encoded markup was even printed as live markup.
- **Security:** Links that come from a feed or a social platform are only used when they are normal web addresses (http or https). Anything else is left out instead of becoming a clickable link.
- **Security:** When Smart Grid fetches a feed, a picture for a feed item, related links or a SoundCloud feed, every redirect is now checked before it is followed, and very large downloads are stopped. Before, only the first address was checked, and the SoundCloud check could be fooled.
- **Security:** The limits on the public preview, connection-test and article-fetch requests now also count per visitor address, so they can no longer be avoided by dropping the browser cookie.
- **Security:** Error messages and internal details are no longer sent back to visitors; they go to the Joomla log instead.
- **Security:** The administrator diagnostic view escapes everything it shows.
- **Security:** The rating lookup accepts a limited number of items per request.

 ![Smart Grid 1](https://www.yoozecase.com/files/smart-grid_1.png)

 [](#)

 [](#)

---

##  Description

**Give every card a heart and a rating and watch your site finally remember the people who visit.**

Start with the part nobody else gives you: a website that remembers people. Smart Grid puts a little heart on every card it displays. A visitor taps it to save that card - think of it like a bookmark or a wishlist - and from then on they can filter the whole page down to just the things they loved, in a single click. Add the built-in 5-star rating widget and they can score what they like too, the way you'd rate a film or a product online. Those ratings stick around, so you can finally see what your audience actually values - and if you use ZOO, the averages can write straight back into your content. No third-party service, no spreadsheet, no guesswork. Your site remembers, and your visitors feel seen.

So what goes on those cards? Whatever you want - and this is where jaws hit the floor. A YOOtheme Pro "element" is just a block you drag onto a page, Smart Grid is that block, except it can pull its cards from four completely different places. Hand-pick them one by one. Or point it at your Joomla articles and let it build the grid for you (it even notices a YouTube link inside an article and turns that card into a playable video). Or wire it into ZOO, the heavy-duty content app. Or - the showstopper - feed it live posts from eight social platforms at once: YouTube, Bluesky, Mastodon, Reddit, Vimeo, Spotify, SoundCloud and TikTok. You never touch a line of code. You pick a source from a dropdown and the cards appear.

From there it just keeps over-delivering. Twelve one-click layout presets mean it looks designed, not assembled. Cards flip to reveal more, spotlight tiers show off your best content, and media plays right on the card - with only one video or song ever playing at a time, so nothing talks over itself. Visitors filter by tag, search as they type, and page through smoothly. You can even earn from it: drop in Google ads, including native "in-feed" cards that blend into the grid instead of screaming "banner" - and a bonus RSS Feed element rides along in the same download for clean news feeds.

And it's built like grown-ups built it: it heals its own database tables, survives the cheapest shared hosting, shrugs off flaky APIs, and is properly security-hardened. It speaks six languages out of the box and installs from a single zip - no FTP, no cPanel, no phpMyAdmin. Drag it on, pick a source, pick a preset, and you're live before your coffee cools. You came for a grid. You're leaving with a site that remembers, rewards, and sells - quite possibly the most epic plugin you'll ever buy, and we say that with our whole chest.

##  Feature request

 ### What is the next feature for this plugin?

Submit your idea or vote on existing ones.

  #### Top voted

   Website (leave empty)

'; } html += ''; if (oc.title) { html += '##### ' + escapeHtml(oc.title) + '

'; } if (oc.text) { html += '' + escapeHtml(oc.text).replace(/\\n/g, '
') + '

'; } html += '

'; } // Auto winner(s) - top N from the already vote-sorted features if (mode === 'auto_winner' || mode === 'both') { var n = oc.winnerCount | 0; if (n '; for (var i = 0; i ' + (w.image_url ? '![](https://www.yoozecase.com/'%20+%20escapeHtml(w.image_url)%20+%20')

' : '') + '' + (i + 1) + '' + '' + escapeHtml(w.title) + '' + '' + getIcon(this.cfg.voteIcon) + ' ' + (w.votes | 0) + '' + ''; } html += ''; } } if (html === '') { this.elOutcome.hidden = true; return; } this.elOutcome.innerHTML = html; this.elOutcome.hidden = false; }; /** * Roadmap view (v3.2.0): renders the items as status columns, in the * canonical status order from cfg.statuses. Each column is an 12. inside \* the existing .yc-fb-list , holding its own inner  of items - \* this keeps the delegated click/change handlers on elList working \* unchanged. Unknown statuses fall back into the "open" column so no item \* can silently disappear. Empty columns are skipped when \* cfg.roadmapHideEmpty is set. \*/ Board.prototype.renderRoadmap = function (items) { var stats = (this.cfg.statuses && this.cfg.statuses.length) ? this.cfg.statuses : \['open', 'planned', 'in\_progress', 'shipped', 'declined'\]; var labels = this.cfg.statusLabels || {}; var groups = {}; var i, st; for (i = 0; i ' + '##### ' + '' + escapeHtml(labels\[sv\] || sv) + '' + arr.length + '' + '

    ' + '' + itemsHtml + '
    ' + '
'; } return html; }; Board.prototype.renderItem = function (f, index) { var disabled = this.cfg.isClosed ? 'disabled' : ''; var voted = f.voted ? ' yc-voted' : ''; var descHtml = f.description ? '' + escapeHtml(f.description).replace(/\\n/g, '
') + '

' : ''; var thumbHtml = f.image_url ? '![](https://www.yoozecase.com/'%20+%20escapeHtml(f.image_url)%20+%20')

' : ''; // Status workflow (v3.0.0): badge for any non-default status. The // roadmap view (v3.2.0) skips the badge - the column already // communicates the status, a badge would be redundant noise. var st = (f.status || 'open'); var labels = this.cfg.statusLabels || {}; var statusBadge = (!this.isRoadmap && st && st !== 'open' && labels[st]) ? '' + escapeHtml(labels\[st\]) + '' : ''; // Design themes (v3.1.0): arena / neon / pixel render extra per-item // furniture - rank number, avatar (image or initials), a "leader" pill // on rank 1, and a "votes" label. Classic/colour themes are untouched. var t = this.cfg.theme || 'light'; var themed = (t === 'arena' || t === 'neon' || t === 'pixel'); var rank = (typeof index === 'number') ? (index + 1) : 0; var rankHtml = '', leaderHtml = '', avatarHtml = '', votesLabel = ''; if (themed) { rankHtml = '' + pad2(rank) + ''; if (rank === 1) { leaderHtml = '' + escapeHtml(this.cfg.leaderLabel || 'Leader') + ''; } if (f.image_url) { avatarHtml = '![](https://www.yoozecase.com/'%20+%20escapeHtml(f.image_url)%20+%20')

'; } else { avatarHtml = '' + escapeHtml(initialsOf(f.title)) + '

'; } votesLabel = '' + escapeHtml(this.cfg.votesLabel || 'votes') + ''; } // Moderation overlay: subtle corner buttons, only when canModerate. // Buttons appear on hover/focus (styled in CSS). Even in customizer // preview we render them when canModerate is true so the moderator // can see what visitors with moderation rights will see. var modHtml = ''; if (this.cfg.canModerate) { var parts = []; var hideTitle = escapeHtml(this.t('hideTitle', 'Hide this feature')); var deleteTitle = escapeHtml(this.t('deleteTitle', 'Delete this feature')); if (this.cfg.allowSoftHide) { parts.push( '' ); } if (this.cfg.allowHardDelete) { parts.push( '' ); } if (parts.length || (this.cfg.statuses && this.cfg.statuses.length)) { var sParts = parts.slice(); if (this.cfg.statuses && this.cfg.statuses.length) { var opts = ''; for (var si = 0; si ' + escapeHtml(sl) + ''; } var setStatus = escapeHtml(this.t('setStatus', 'Set status')); sParts.push( '' ); } modHtml = '' + sParts.join('') + '

'; } } var mediaHtml = themed ? avatarHtml : thumbHtml; return ( '32. ' + leaderHtml + rankHtml + '' + mediaHtml + '' + '##### ' + escapeHtml(f.title) + '

    ' + statusBadge + descHtml + '

    ' + modHtml + '
' ); }; /** * Detects whether a JSON response looks like a CSRF/token rejection. * Two cases: * 1. com_ajax-level rejection (Joomla's auto-token-check on POST): top * level message contains the JINVALID_TOKEN_NOTICE text and HTTP * status is typically 403. * 2. Plugin-level rejection from requireToken(): payload.error === "Invalid token". */ Board.prototype.isTokenError = function (status, raw, payload) { if (status === 403) return true; var hay = ''; try { hay = JSON.stringify(raw || {}) + '|' + JSON.stringify(payload || {}); } catch (e) {} return /invalid (security )?token/i.test(hay); }; /** * Fetch a fresh CSRF token from the server. Updates this.cfg.csrfToken so * the next call (and any future calls in this Board's lifetime) use the * correct one. Cache-bust with a timestamp so no upstream CDN caches the * response. */ Board.prototype.fetchFreshToken = function () { var self = this; var url = this.cfg.ajaxUrl + '&task=token&_=' + Date.now(); return fetch(url, { method: 'GET', credentials: 'same-origin', headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' } }) .then(function (r) { return r.json(); }) .then(function (data) { var p = self.unwrapAjax(data); if (p && p.ok === true && typeof p.token === 'string' && p.token.length >= 16) { self.cfg.csrfToken = p.token; return p.token; } throw new Error('No fresh token in response'); }); }; /** * Generic POST-with-token helper. Builds FormData from a plain object, * adds the current token, posts. On 403 / token-error response, fetches a * fresh token and retries exactly once. * * @param {string} task The task name (appended to URL and body) * @param {object} fields Key-value form fields (no token) * @return {Promise} Resolves to the unwrapped payload on success. \* Rejects with Error containing .reason on failure. \*/ Board.prototype.postWithToken = function (task, fields) { var self = this; function attempt(token) { var fd = new FormData(); fd.append('task', task); for (var k in fields) { if (Object.prototype.hasOwnProperty.call(fields, k)) { fd.append(k, fields\[k\]); } } fd.append(token, '1'); return fetch(self.cfg.ajaxUrl + '&task=' + encodeURIComponent(task), { method: 'POST', credentials: 'same-origin', body: fd, headers: { 'Accept': 'application/json' } }).then(function (r) { return r.json().then(function (data) { return { status: r.status, raw: data, payload: self.unwrapAjax(data) }; }); }); } return attempt(this.cfg.csrfToken).then(function (res) { // Success path if (res.payload && res.payload.ok === true) { return res.payload; } // Token rejection? Fetch fresh and retry once. if (self.isTokenError(res.status, res.raw, res.payload)) { return self.fetchFreshToken().then(function (fresh) { return attempt(fresh).then(function (res2) { if (res2.payload && res2.payload.ok === true) { return res2.payload; } var err = new Error( (res2.payload && res2.payload.error) || (res2.raw && res2.raw.message) || self.t('requestFailed', 'Request failed.') ); err.payload = res2.payload || null; throw err; }); }); } // Some other error - surface it var err = new Error( (res.payload && res.payload.error) || (res.raw && res.raw.message) || self.t('requestFailed', 'Request failed.') ); err.payload = res.payload || null; throw err; }); }; Board.prototype.handleSubmit = function () { var self = this; if (isInBuilder()) { this.showStatus(this.t('previewSubmit', 'Preview mode - submissions are disabled in the YOOtheme builder. Try this on the published page.'), 'info'); return; } if (this.cfg.isClosed) { this.showStatus(this.cfg.closedMessage || this.t('votingClosed', 'Voting is closed.'), 'info'); return; } var title = (this.elTitle && this.elTitle.value || '').trim(); var desc = (this.elDesc && this.elDesc.value || '').trim(); // Client-side validation: minimum title length. Server re-validates. var minLen = (this.cfg.minTitleLength | 0) || 2; if (title.length for hide/delete, remember parent + sibling for rollback. var li = btn.closest('li'); var parentEl = li ? li.parentNode : null; var nextEl = li ? li.nextSibling : null; if (li && (action === 'hide' || action === 'delete')) { li.remove(); } btn.disabled = true; var task = action === 'hide' ? 'moderate\_hide' : action === 'delete' ? 'moderate\_delete' : action === 'restore' ? 'moderate\_restore' : ''; if (!task) { btn.disabled = false; return; } this.postWithToken(task, { feature\_id: featureId }) .then(function (payload) { // Success - keep the UI as-is (already removed) and refresh // so vote counts, top box, and hidden-features panel reflect // the new state. self.refresh(); if (self.hiddenOpen) { self.hiddenLoaded = false; // force a refetch self.fetchHiddenFeatures(); } var msg = action === 'delete' ? self.t('deleted', 'Feature deleted.') : action === 'hide' ? self.t('hidden', 'Feature hidden.') : action === 'restore' ? self.t('restored', 'Feature restored.') : self.t('done', 'Done.'); self.showStatus(msg, 'success'); setTimeout(function () { self.hideStatus(); }, 3000); }) .catch(function (err) { // Rollback: re-insert the row if we removed it. if (li && parentEl) { if (nextEl) { parentEl.insertBefore(li, nextEl); } else { parentEl.appendChild(li); } } btn.disabled = false; self.showStatus(err && err.message ? err.message : self.t('modFailed', 'Moderation action failed.'), 'error'); }); }; Board.prototype.handleStatusChange = function (featureId, status, sel) { var self = this; if (!featureId || !status) return; sel.disabled = true; this.postWithToken('moderate\_status', { feature\_id: featureId, status: status }) .then(function () { self.refresh(); var label = (self.cfg.statusLabels && self.cfg.statusLabels\[status\]) ? self.cfg.statusLabels\[status\] : status; self.showStatus(self.tf('statusSet', 'Status: %s', label), 'success'); setTimeout(function () { self.hideStatus(); }, 3000); }) .catch(function (err) { sel.disabled = false; self.showStatus(err && err.message ? err.message : self.t('statusFailed', 'Could not set status.'), 'error'); }); }; /\*\* \* Toggle the hidden-features panel open/closed. First open triggers \* an AJAX fetch; subsequent toggles just hide/show the cached list. \*/ Board.prototype.toggleHiddenPanel = function () { if (!this.elHiddenList || !this.elHiddenLabel) return; var arrow = this.elHiddenToggle && this.elHiddenToggle.querySelector('.yc-fb-hidden-arrow'); if (this.hiddenOpen) { this.hiddenOpen = false; this.elHiddenList.hidden = true; this.elHiddenLabel.textContent = this.t('showHidden', 'Show hidden features'); if (arrow) arrow.classList.remove('yc-open'); return; } this.hiddenOpen = true; this.elHiddenList.hidden = false; this.elHiddenLabel.textContent = this.t('hideHidden', 'Hide hidden features'); if (arrow) arrow.classList.add('yc-open'); if (!this.hiddenLoaded) { this.fetchHiddenFeatures(); } }; /\*\* \* Fetch the hidden-features list via the moderator-only endpoint. \* Re-callable to refresh the list after a restore. \*/ Board.prototype.fetchHiddenFeatures = function () { var self = this; if (!this.elHiddenList) return; this.elHiddenList.innerHTML = '2. ' + escapeHtml(this.t('loading', 'Loading...')) + '
'; var url = this.cfg.ajaxUrl + '&task=moderate\_list\_hidden&board\_id=' + encodeURIComponent(this.cfg.boardId); fetch(url, { credentials: 'same-origin', headers: { 'Accept': 'application/json' } }) .then(function (r) { return r.json(); }) .then(function (data) { var payload = self.unwrapAjax(data); if (!payload || payload.ok !== true) { self.elHiddenList.innerHTML = '4. ' + escapeHtml((payload && payload.error) ? payload.error : self.t('hiddenLoadFailed', 'Failed to load hidden features.')) + '
'; return; } self.hiddenLoaded = true; var features = payload.features || \[\]; if (features.length === 0) { self.elHiddenList.innerHTML = '6. ' + escapeHtml(self.t('noHidden', 'No hidden features.')) + '
'; return; } var html = ''; for (var i = 0; i ' + escapeHtml(f.description).replace(/\\n/g, '
') + '' : ''; var restoreTitle = escapeHtml(self.t('restoreTitle', 'Restore this feature')); html += '9. ' + '' + '##### ' + escapeHtml(f.title) + '

    ' + descHtml + '' + escapeHtml(self.tf('votesMeta', '%s votes', f.votes | 0)) + '' + '

    ' + '' + '
'; } self.elHiddenList.innerHTML = html; }) .catch(function () { self.elHiddenList.innerHTML = '11. ' + escapeHtml(self.t('hiddenNetwork', 'Network error while loading hidden features.')) + '
'; }); }; function initAll(scope) { var roots = (scope || document).querySelectorAll('.yc-fb-root'); for (var i = 0; i ---

##  Changelog

---

 [ v4.8.1 Breaking 02/10/2026 ](#) Security- RSS feed descriptions are cleaned with Joomla's own HTML filter. A feed could previously slip a script link or page-covering styling into a description. With the "strip all formatting" setting and no shortening, text that a feed sent as encoded markup was even printed as live markup.

- Links that come from a feed or a social platform are only used when they are normal web addresses (http or https). Anything else is left out instead of becoming a clickable link.
- When Smart Grid fetches a feed, a picture for a feed item, related links or a SoundCloud feed, every redirect is now checked before it is followed, and very large downloads are stopped. Before, only the first address was checked, and the SoundCloud check could be fooled.
- The limits on the public preview, connection-test and article-fetch requests now also count per visitor address, so they can no longer be avoided by dropping the browser cookie.
- Error messages and internal details are no longer sent back to visitors; they go to the Joomla log instead.
- The administrator diagnostic view escapes everything it shows.
- The rating lookup accepts a limited number of items per request.

 [ v4.8.0 Breaking 02/10/2026 ](#) Added- A License tab in the plugin settings. Paste the key from your order confirmation and press Activate to register this site; press Deactivate before moving the key to another site. Uninstalling the plugin releases the key automatically. The key does not switch any feature on or off, and updates arrive with or without it.

 [ v4.7.3 Breaking 02/10/2026 ](#) Changed- Smart Grid is now licensed under the GNU General Public License version 2 or later. The previous licence files and file headers have been replaced, and the separate terms file is no longer part of the package; the commercial terms are at https://www.yoozecase.com/terms.

 Fixed- \*\*Updates are found again.\*\* Up to 4.7.2 the plugin looked for updates at two addresses that no longer exist, so Joomla showed "Could not open update site" and never offered a new version. It now uses https://updates.yoozecase.com/smart-grid/updates.xml. On install the old update sites are moved to the new address or switched off, so they stop showing errors.

 [ v4.7.2 Breaking 24/09/2026 ](#) Changed- \*\*A grid may show fewer ZOO items than before.\*\* Items that are published but restricted to a logged-in group are now hidden from visitors outside that group. If such an item is missing from the YOOtheme builder preview, check the preview with an account that is allowed to see it.

 Security- \*\*Smart Grid now respects Joomla access levels for ZOO items.\*\* A ZOO item set to Registered, Special or any other non-public access level no longer appears on a grid for a visitor who is not allowed to see it. Articles already worked this way.
- \*\*Take the category from the URL only accepts a published category of the grid's own ZOO app.\*\* Any other category number in the address is ignored and the grid shows its saved category.
- \*\*The builder's Preview items button only lists content the visitor may see.\*\* It previously listed the newest titles of every published article and ZOO item, whatever their access level.
- \*\*Removed an unused internal diagnostic request\*\* that could return raw ZOO item data to anyone who knew its name.

 [ v4.7.1 Breaking 21/09/2026 ](#) Fixed- \*\*RSS Feed Display, magazine layout: the four small items had no card background.\*\* They sat transparent next to the two large cards, and on a dark section their headlines were unreadable. They now use the same card background as every other item.

- \*\*The magazine layout ignored the Card background setting.\*\* Its cells were hard-coded white, so choosing Transparent, Glass or Muted changed nothing there. All five layouts now follow the setting, and the headline colour follows with it.

 [ v4.7.0 Breaking 21/09/2026 ](#) Added- \*\*A Custom CSS box for the RSS Feed Display\*\*, under the Advanced tab, with the full list of selectors you can target. The element simply never declared the field before, which is why the box was missing.

 Changed- \*\*The RSS Feed Display's CSS class names now use the `ycrss-` prefix\*\* instead of the old `pdrss-`, finishing the renaming started in 4.0.0. If you wrote your own CSS against `.pdrss-...` names, replace `pdrss-` with `ycrss-`; nothing else about them changed.

 Fixed- \*\*RSS Feed Display: headlines were white on a white card.\*\* Inside a dark section the headline inherited the section's white text while the card kept its own white background. It is now set to a readable colour whenever the card has an opaque background, and still inherits on the Transparent and Glass card styles, where the section really does show through.
- \*\*The Read original link now sits at the bottom of the card\*\*, so the links line up across a row however much text each item has.

 [ v4.6.2 Breaking 18/09/2026 ](#) Fixed- On a coloured category card the expand button and the sub-category links stayed dark, while the title next to them turned white. They now follow the card like the rest of its text: white on a dark card, dark on a light one.

 [ v4.6.1 Breaking 18/09/2026 ](#) Fixed- The \*\*Category opens page\*\* dropdown was always empty, so no target page could be chosen. The list is fetched from the site, where an administrator is not logged in - Joomla keeps the site and administrator logins separate - and the permission check added in 4.6.0 rejected it every time. The list is now filtered by what the visitor may see, like the other pickers, instead of asking for an administrator.

- An empty target-page list now says so, rather than looking like a working dropdown with nothing to offer.

 [ v4.6.0 Breaking 18/09/2026 ](#) Added- \*\*Category pictures.\*\* A picture set on a ZOO category now shows on its card, like any other image.

- \*\*A category can open a page of yours.\*\* Pick the page under \*Category opens page\* (or type an address) and the category id travels with the link as `?yc\_cat=&lt;id&gt;`.
- \*\*Take the category from the URL.\*\* Switch this on for the ZOO items grid that sits on that page and it shows whichever category was clicked. The category saved in its settings stays the fallback.
- \*\*A colour per category.\*\* Off by default. Choose one base colour and every top-level category gets its own, spread evenly around it; sub-categories keep their parent's colour a shade lighter or darker. Solid or gradient.
- Card text is set light or dark from the colour that was chosen, in the same pass, so it is readable from the first frame with no flicker. Measured across 960 combinations: nothing below the 3.0 contrast floor.
- \*\*Sub-categories listed inside the card.\*\* \*Show sub-categories\* gains "List inside the card": the sub-categories appear as small links pinned to the bottom of the parent card, lined up across the row.
- A parent category now links as well as expanding. The expand control is a separate chevron button, so one click never has two meanings.

 Fixed- Card text could be given the worse of the two colours on a mid-tone card. The light/dark decision now uses the luminance where white and black read equally (0.1791) instead of 0.45, which called a mid-tone card dark and put white text on it below the readability floor.

 [ v4.5.1 Breaking 18/09/2026 ](#) Fixed- The \*\*ZOO categories\*\* source could not be set up: choosing it hid the whole ZOO block, so there was no ZOO app to pick and the grid stayed empty. The block now stays visible for both ZOO sources and shows the settings belonging to the one you chose.

- \*\*Start from category\*\* and \*\*Show sub-categories\*\* never reached the settings panel at all. They now appear, with the category picker as a real dropdown.
- \*\*Extra fields text\*\* (added in 4.3.6) had the same problem and has never been editable. It now appears under the extra fields heading, for ZOO grids with extra fields switched on.
- With `?sgdebug=1` an empty category grid now says why: no ZOO app picked, the app has no categories, or the chosen starting category has no children.

## Schema

```json
{
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
        {
            "@type": "ListItem",
            "position": 1,
            "name": "Home",
            "item": "https://www.yoozecase.com"
        },
        {
            "@type": "ListItem",
            "position": 2,
            "name": "Extensions",
            "item": "https://www.yoozecase.com/extensions"
        },
        {
            "@type": "ListItem",
            "position": 3,
            "name": "Smart Grid",
            "item": "https://www.yoozecase.com/"
        }
    ]
}
```
