---
title: "Smart Background Sound | Audio Element for YOOtheme Pro Websites"
description: "Add optional ambient sound to YOOtheme Pro with smart media detection, accessible controls, browser-friendly playback and no autoplay surprises."
url: "https://www.yoozecase.com/?app_id=1&item_id=19"
date: "2026-10-08T07:30:25+00:00"
language: "en-GB"
---

#  Smart Background Sound

Set the mood. Keep it tasteful.

**Latest Version**
0.9.20
**Requires**
Yootheme Pro 5+, Joomla 5+, WordPress 6.5+, PHP 8.1 or newer
**Release date**
01 October 2026
**Made for**
Yootheme Pro, Joomla, Wordpress
**License**
GPL-2.0-or-later
**Price**
Free

 [ 🛒 Get it for Joomla 👍 ](https://yoozecase.lemonsqueezy.com/checkout/buy/e7db9e86-781b-41ff-a8ea-9bcfc682390b)

 [ 🛒 Get it for WordPress👍 ](https://yoozecase.lemonsqueezy.com/checkout/buy/7e38c651-f07c-4352-bbb1-ca9cfc8968d3)

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

Tasteful ambient sound with a smart mute. Used responsibly, we promise.

##  What's new in version 0.9.20

- **New:** The Status field on the plugin settings page shows the measured state: working, switched off with the Enabled setting, YOOtheme Pro not found, or element files missing. It used to show a fixed text.
- **New:** Install and update read back whether the plugin was enabled. If it was not, a warning explains where to enable it, and the reason is written to the Joomla log.
- **Changed:** LICENSE.txt now contains the full text of the GNU General Public License version 2. The trademark notice moved from LICENSE.txt to NOTICE.txt.
- **Changed:** Copyright lines no longer say "All rights reserved", which read as a contradiction of the GPL.
- **Changed:** Builder icon in the colour of this product (red instead of purple) and at the 30 and 20 px sizes YOOtheme uses in the element picker.
- **Changed:** The release package no longer contains development tooling (check scripts, linter and analyser configuration, composer.json, package.json). None of these files were installed on the site, so nothing changes there.
- **Changed:** Help texts for loop, volume, size, anchor, unit, X and Y distance, entrance animation, theme and label now state the default value, in all six languages. The Enabled setting explains what switching it off does to the Builder and to pages.

 ![Smart Background Sound 1](https://www.yoozecase.com/files/smart-background-sound_1.png)

 [](#)

 [](#)

---

##  Description

### Key Features

- **No Autoplay:** Audio waits for user interaction, protecting visitors from sudden loud noises.
- **Smart Detection:** Includes smart media detection and volume ducking when other media plays.
- **Accessibility:** Built with accessible, privacy-friendly user controls.
- **Platform Support:** Works with YOOtheme Pro on WordPress and Joomla.
- **Cost:** Available for free.

##  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

---

 [ v0.9.20 01/10/2026 ](#) Added- The Status field on the plugin settings page shows the measured state: working, switched off with the Enabled setting, YOOtheme Pro not found, or element files missing. It used to show a fixed text.

- Install and update read back whether the plugin was enabled. If it was not, a warning explains where to enable it, and the reason is written to the Joomla log.

 Changed- LICENSE.txt now contains the full text of the GNU General Public License version 2. The trademark notice moved from LICENSE.txt to NOTICE.txt.
- Copyright lines no longer say "All rights reserved", which read as a contradiction of the GPL.
- Builder icon in the colour of this product (red instead of purple) and at the 30 and 20 px sizes YOOtheme uses in the element picker.
- The release package no longer contains development tooling (check scripts, linter and analyser configuration, composer.json, package.json). None of these files were installed on the site, so nothing changes there.
- Help texts for loop, volume, size, anchor, unit, X and Y distance, entrance animation, theme and label now state the default value, in all six languages. The Enabled setting explains what switching it off does to the Builder and to pages.

 [ v0.9.19 06/08/2026 ](#) Added- NOTICE.txt: data processing section (client-side localStorage for the

- Release tooling: the forbidden-licence-text check in

 Changed- No functional change; the runtime behaviour, the element configuration

 Fixed- NOTICE.txt: removed a leftover pre-GPL restricted-distribution line

 [ v0.9.18 04/08/2026 ](#) Changed- Licensing moved to GPL-2.0-or-later, in line with the YOOzeCase licensing decision of 2 August 2026. The manifest now declares `GNU General Public License version 2 or later`, `LICENSE.txt` carries the full GPL text, and `COPYRIGHT.txt` and `NOTICE.txt` no longer describe the software as proprietary. Every PHP, JS and CSS file carries the GPL header instead of the previous restricted-access notice.

- This changelog is now in English throughout. Roughly 90 entries from 0.2.5 up to 0.9.15 were written in Dutch while the product pages, the README and the plugin itself are English. Code identifiers, file names, language keys and version numbers are unchanged.
- Release tooling refreshed to the current YOOzeCase standard: `scripts/pre-release-grep-check.sh`, `scripts/selftest-grep-check.sh` and `scripts/check-php-matrix.sh` now check for the GPL header rather than requiring the old notice.
- No functional change. Nothing in the runtime behaviour, the element configuration or the stored settings is affected by this release.
- What you buy remains a subscription to downloads, updates and support. The GPL covers the source code; it does not cover the YOOzeCase name, branding, product images or documentation.

 Removed- `TERMS.txt` is no longer part of the package. The commercial terms live at https://www.yoozecase.com/terms; the licence that travels with the code is the GPL.

 [ v0.9.17 02/07/2026 ](#) Fixed- Degraded state is now final: when an audio source fails, late

 [ v0.9.16 02/07/2026 ](#) Added- Audio error handling: when the configured audio source fails to load

- Mixed-content guard: an http:// audio URL on an https site is upgraded
- Update-server feeds: updates.xml and changelog.xml are now delivered

 Fixed- The plugin config switch "Enabled" now actually disables the plugin:

 Security- validateAudioUrl strips control characters (0x00-0x1F, 0x7F) before
- AI warning header harmonised to the canonical text (including the

 [ v0.9.15 22/06/2026 ](#) Added- External audio URL as a validated source: besides a local file, the "Audio file" field now explicitly accepts a direct external audio URL (CDN or your own host). Client-side `&lt;audio&gt;`, no server fetch (no SSRF). New README section "External audio URL" covering the points to watch (HTTPS/mixed content, hotlinking, Content-Type, no YouTube/Spotify because of their terms of service).

 Changed- `resolveAudioUrl` in template.php now validates every URL against a scheme whitelist: `http(s)://`, protocol-relative (`//`), root-relative (`/`) and Joomla routes (`index.php?...`) are allowed; dangerous or non-audio schemes (`javascript:`, `data:`, `vbscript:`, `file:`) are rejected, after which the element falls back cleanly to the degraded button instead of emitting a malformed `src`. Previously every string was passed through unchecked.

 [ v0.9.14 22/06/2026 ](#) Fixed- Visibility pause did not really mute a background tab: since the fade landed (v0.9.6), muting ran through `requestAnimationFrame`, which browsers freeze in a hidden tab, so `audio.muted = true` (set only in the fade completion) was never reached and the music kept playing at full volume for as long as the tab sat in the background. The tab-hidden branch now sets `audio.muted = true` synchronously, independently of the fade (functional audit, item 1).

- Variant 1 (always on) did not show the activation coachmark when the browser blocked autoplay, while variants 2/3/4 do. Variant 1 now has the same 800ms fallback that still shows the coachmark if the button drops to the off state (functional audit, item 2).

 [ v0.9.13 19/06/2026 ](#) Added- Activation coachmark: a one-time, self-dismissing hint next to the button on a first visit (in the off state only), inviting the visitor to start the music. The text comes from the language layer (`\_COACHMARK`, 6 languages) and is remembered site-wide in localStorage (`yc\_sbs\_coach\_seen`) so it does not repeat on every page. New element field "First-visit hint" (Behaviour tab, on by default). Raises activation for the off-by-default variants (improvement audit, item 1).

- Optional event hook: `yc-music:play` and `yc-music:pause` are dispatched as DOM CustomEvents on the element so the site owner can wire up GA4/Matomo themselves. The plugin sends no data of its own. New element field "Emit JS events" (Behaviour tab, off by default) - privacy-clean and provider-independent (improvement audit, item 3).

 Changed- Play/pause affordance: the music note icon morphs into two pause bars via CSS while playing, making it clearer that a second click mutes. Respects `prefers-reduced-motion` (improvement audit, item 1).
- Follow-up to the improvement audit of v0.9.12 (audit\_plg\_system\_yc\_smart\_background\_sound\_v0.9.12.md). No security impact; the event hook is opt-in and sends no data to third parties. Language key parity 99 keys x 6 languages.

 Fixed- End state with loop=off: when a non-repeating track finishes naturally, the plugin now returns the button to the off state (icon, `aria-pressed`, label), so a single click starts it again. Previously the button stayed visually stuck in the playing state (improvement audit, item 5).

 [ v0.9.12 17/06/2026 ](#) Changed- Preflight PHP version message (`script.php`): the hardcoded Dutch text has been replaced by a neutral English literal with an explanation. Deliberately no language layer: preflight runs before the package files (including the language files) are copied, so a Text key would render raw at that point (software audit, item 2; edge case, Joomla 5 already requires PHP 8.1+).

- `update()` in `script.php`: `@unlink()` replaced by `Joomla\\Filesystem\\File::delete()` inside a try/catch, consistent with `cleanupLegacyFiles()`.
- Version sync to 0.9.12 via `tools/bump-version.sh`.
- Follow-up to the software audit of v0.9.11 (audit\_plg\_system\_yc\_smart\_background\_sound\_v0.9.11.md). No security impact; no functional change for visitors.

 Removed- Diagnostic DOM attributes `data-yc-music-raw-type` and `data-yc-music-raw-value` from `template.php` (plus the `$debugType`/`$debugRaw` build-up). They were emitted on every render but never read by the JS (dead debug output) and leaked the internal audio field structure into the public DOM (software audit, item 1; skill 10.4/6.3).
- Unused `public $app` property from the main plugin class.

 [ v0.9.11 14/06/2026 ](#) Added- `element.php`: defensive language load (`$lang-&gt;load(... JPATH\_ADMINISTRATOR)` with a fallback to the plugin path) as a safety net, so the Text keys still resolve if YOOtheme loads the element before the plugin has autoloaded its language.

 Changed- Builder UI fully routed through the Joomla language layer (gate item 8). All field labels, descriptions, choice options and tab titles in `element.php` now go through `Text::\_()` with keys in the six base languages, instead of hardcoded Dutch. The builder config therefore follows the admin language. 78 new keys per language (`...\_B\_\*`), parity 94 keys x 6 languages.
- README fully revised: current `--yc-music-\*` token names (the README still documented the `--music-\*` names that were renamed in v0.9.8), a complete configuration reference per tab, duck detection including the recognised hosts and the auto-resume behaviour, a browser compatibility table, and a Troubleshooting section (sticky `yc\_sbs\_paused\_\*`, autoplay blocking, a WAF that refuses uploads). Installation no longer points at a fixed old zip version number.
- SECURITY.txt, NOTICE.txt and TERMS.txt reviewed; the content remains current and compliant (no change needed).
- Version sync to 0.9.11 across all locations via `tools/bump-version.sh`.

## 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 Background Sound",
            "item": "https://www.yoozecase.com/"
        }
    ]
}
```
