---
title: Auto Add Alt And Title | Joomla Image SEO Plugin
description: Automatically add missing alt and title attributes to Joomla images. Improve accessibility, SEO and image optimization with one simple tool.
url: https://www.yoozecase.com/auto-add-alt-and-title
---

# Auto Add Alt And Title

**Latest Version**

1.7.0

**Release date**

30 July 2026

**Made for**

Joomla

**Price**

€0.00+ (incl. 6 months of updates)

[🛒 Get it for Joomla 👍](https://shop.yoozecase.com/checkout/buy/7a1b8776-9e27-46a9-b4be-9cd2a06daa10)

[🛒 Get it for WordPress👍](https://shop.yoozecase.com/checkout/buy/f5898c5f-4ed1-4c25-971a-dd1b7483a577)

[Demo page](https://www.yoozecase.com/auto-add-alt-and-title-demo)

Bulk-fill every missing alt text while you sip your coffee. Titles only when they add something.

![Auto Add Alt And Title](https://www.yoozecase.com/files/auto-add-alt-and-title.png)

---

## Description

Every image on your Joomla site gets a sensible alt text automatically, written into the raw HTML so crawlers and screen readers see it without running any JavaScript. The title attribute works differently, because repeating the alt helps nobody: you get a title only when a caption or link text says something the alt does not already say. Pick where the alt comes from, keep acronyms like NL and FM capitalised, and exclude the images you want left alone.

---

#### Top voted

';
 }
 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/' + escapeHtml(w.image_url) + ')

' : '') +
 '' + (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  inside
 * the existing .yc-fb-list ';
 }
 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/' + escapeHtml(f.image_url) + ')

'
 : '';

 // 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/' + escapeHtml(f.image_url) + ')

';
 } 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 = '';
 }
 }

 var mediaHtml = themed ? avatarHtml : thumbHtml;
 return (
 '' +
 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

## Changelog

---

v1.9.0
 07/08/2026

Added

- Setting "Words that say nothing": the list behind that gate, in six
- Setting "Remove these endings": endings that a platform appends to a file

Changed

- Licence changed from proprietary to GPL-2.0-or-later, in the manifest, in

Fixed

- Alt text derived from a file name is now judged as a whole, not only word by
- The client script applies the same gate, so an image that only enters the DOM

v1.8.1
 02/08/2026

Fixed

- The configure button in the install message showed the raw language key
- User facing strings in the install script fall back to English when a key
- The update feed now carries the readable product name instead of the

v1.8.0
 02/08/2026

Added

- The install and update message now carries a button that opens the plugin
- New language keys `..._INSTALL_CONFIGURE` and

Fixed

- The install message no longer claims the plugin was enabled when the

v1.7.2
 02/08/2026

Fixed

- Regression introduced in 1.7.1: the presence check for an attribute ran on

v1.7.1
 01/08/2026

Added

- Self-healing of the update site link in the install script. After an
- Verification and logging on the automatic enabling: the install script now

Changed

- The lookup of the enclosing figure or link for each image now uses a binary
- The figure and link extraction passes are skipped when the page contains no

Fixed

- The manifest now declares `script.php` as `<scriptfile>`. Without that
- Attributes written without a value no longer produce a duplicate attribute.

v1.7.0
 30/07/2026

Added

- Title enrichment. The title setting is now a mode instead of a switch:

Changed

- `enable_title_sync` accepts 0, 1 and 2. Existing installations keep their
- The dashboard reports the title feature as active for any mode above 0.
- Rationale: a title identical to the alt adds nothing and can be announced
- An existing title is still never overwritten, and an image without a

v1.6.2
 30/07/2026

Added

- New setting "Acronyms to keep capitalised" (acronyms). Words on this list

Changed

- Found during a live pre-flight on allewebradio.nl, where brand names such
- No security impact and no change to the alt resolution order.

v1.6.1
 28/07/2026

Added

- BUILDINFO.txt (build id, timestamp, sha256) shipped in the package.
- changelogurl in the manifest, pointing at the update-server changelog.xml.

Changed

- Title sync (enable_title_sync) now defaults to OFF. A title identical to
- Field descriptions for "Image selector" and "Exclude selector" now state
- No functional or security change to the rewrite engine. Audit v1.6.0

v1.6.0
 14/06/2026

Added

- Content-context alt (alt_from_context, default on). When an image has
- the caption of an enclosing <figure> (figcaption) is used BEFORE the
- the accessible name of an enclosing <a> (aria-label, then title,
- New resolution order: existing alt > duplicate-source alt >
- Rewrite statistics now include alt_fig and alt_anchor counters

Changed

- The link accessible name sits after the file name on purpose. A
- Only context bound by an explicit enclosing figure or anchor is used.
- An existing non-empty alt and a duplicate-source alt always win over

v1.5.0
 12/06/2026

Added

- Server-side exclude: a comma-separated list of class names
- LCP image selector (lcp_selector): comma-separated class names of the
- Rewrite statistics now include an excluded counter (debug mode).

Changed

- Both fields work on class names (and the data-yc-no-alt attribute),
