Title: Essiow — AI SEO Suite for WooCommerce
Author: boni58
Published: <strong>April 11, 2026</strong>
Last modified: May 14, 2026

---

Search plugins

![](https://ps.w.org/essiow/assets/banner-772x250.png?rev=3503791)

![](https://ps.w.org/essiow/assets/icon-256x256.png?rev=3503791)

# Essiow — AI SEO Suite for WooCommerce

 By [boni58](https://profiles.wordpress.org/boni58/)

[Download](https://downloads.wordpress.org/plugin/essiow.1.1.75.zip)

 * [Details](https://test.wordpress.org/plugins/essiow/#description)
 * [Reviews](https://test.wordpress.org/plugins/essiow/#reviews)
 *  [Installation](https://test.wordpress.org/plugins/essiow/#installation)
 * [Development](https://test.wordpress.org/plugins/essiow/#developers)

 [Support](https://wordpress.org/support/plugin/essiow/)

## Description

**Essiow turns your WooCommerce store into a search-traffic machine.** It plugs 
into Google Search Console, watches what your customers actually search for, and
rewrites your product pages, category pages and blog articles to capture every query
you nearly rank on.

You don’t write SEO. You don’t pick keywords. You don’t guess what works. You click
a button and the right pages get fixed.

#### What Essiow does for you

**1. Auto-rewrites your product pages.** Long description, short pitch, meta title,
meta description, focus keyword, image alt texts — all generated from your real 
GSC queries when connected, in 8 languages, in your store’s tone. Compatible with
Yoast SEO, Rank Math and All in One SEO.

**2. Turns empty category pages into landing pages.** Bare category pages don’t 
rank. Essiow generates 1,500-2,500 words of category content with FAQ, comparison
tables and links to your top products — the page Google needs to rank you in position
1 instead of position 30.

**3. Writes blog articles that pull traffic to your products.** 1,500-5,000 word
articles with internal links to the products mentioned, FAQ schema, automatic featured
image. Suggestions based on what your audience already searches.

**4. Spots and grabs every “almost-ranking” keyword.** When Google Search Console
is connected, Essiow surfaces every query where your store sits at position 11-20—
the closest gains. One click rewrites the matching page targeting that exact query.

**5. Resolves cannibalization in two clicks.** Two of your pages competing for the
same query? Essiow detects it, picks the strongest one, and consolidates the canonical
from the others — without deleting anything.

**6. Indexes everything instantly.** Bing, Yandex, Naver, Seznam are pinged the 
second you publish. Google gets the URL pushed via sitemap re-submit + URL Inspection
refresh + a one-click manual indexation request.

**7. Builds your internal mesh in a graph view.** See orphan pages (no incoming 
links), dead-ends (no outgoing), and connect any two pages with a drag — Essiow 
injects reciprocal anchor links on the strongest shared keyword. A live mesh score/
100 tells you how healthy your site structure is.

**8. AI sales agent on your storefront.** A chatbot that knows your full catalog,
handles objections, can issue promo codes within your discount limit, and quotes
your delivery / returns / payment policy.

**9. Exposes your catalog to ChatGPT, Perplexity and Claude.** Toggle on and Essiow
serves a clean `/llms.txt` at your root — the standard AI search engines read to
find products to recommend.

#### Why it ranks better

When Search Console is connected, every optimization sees the actual queries the
page is already ranking on, the striking-distance keywords just outside page 1, 
and the CTR alerts when a title is converting poorly. The AI doesn’t guess keywords—
it gets them from Google itself, and writes around what’s already working.

#### Made for shop owners, not SEOs

 * No keyword research needed
 * No technical setup beyond pasting an API key
 * Every action shows its credit cost upfront — no surprise billing
 * Bulk optimize, pause, resume, restore original — your content is always recoverable
 * 8 languages, 4 writing tones, 3 content lengths

#### Compatible & safe

 * WooCommerce HPOS compatible
 * Works alongside Yoast SEO / Rank Math / All in One SEO (writes to all three)
 * GDPR compliant (auto-delete chat data after 90 days)
 * Original content backed up the first time you optimize — one-click restore

#### How credits work

 * **1 credit** per product optimization
 * **1 credit** per category optimization
 * **3 credits** per blog article
 * **2 credits** per AI Vision alt text generation
 * All indexation actions, audits, internal-link suggestions, mesh-score, /llms.
   txt — **free** (no AI involved)
 * Credits are debited only on success. Failed AI calls don’t consume credits.
 * Purchased credits never expire (free trial credits expire after 30 days)

#### External Service

This plugin connects to the Essiow API at `https://essiow.com/api/v1` to process
AI content generation. Your product data (names, descriptions, prices, categories)
is sent to the Essiow servers where it is processed using OpenAI’s models. No data
is stored beyond what is needed to track your credit usage.

 * [Essiow Terms of Service](https://essiow.com/terms)
 * [Essiow Privacy Policy](https://essiow.com/privacy)

## Installation

 1. Upload the `essiow` folder to `/wp-content/plugins/`
 2. Activate the plugin through the ‘Plugins’ menu in WordPress
 3. Go to **Essiow > Settings** and enter your API key from [essiow.com](https://essiow.com)
 4. Click “Test Connection” to verify
 5. Start optimizing from **Essiow > Products** or **Essiow > Categories**

## FAQ

### Do I need an Essiow account?

Yes. Create a free account at [essiow.com](https://essiow.com) to get your API key
and 10 free credits.

### Do I need technical skills?

No. If you can install a WordPress plugin, you can use Essiow. Everything is done
in a few clicks.

### Do I need to know SEO?

No. Essiow does the SEO work : it picks the keywords (from Google Search Console
when connected), writes the meta tags, generates the schema, builds the internal
links and submits everything to search engines. You just click “Optimize”.

### Why does connecting Google Search Console matter?

With GSC connected, every optimization is fed with the real queries your page already
ranks on. Essiow finds queries where you sit at position 11-20 (just outside page
1) and rewrites the matching page targeting that exact query. Without GSC, optimizations
are still good — but generic. With GSC, they’re surgical.

### Will optimizing break my existing content?

No. The first time a product or category is optimized, the original content is backed
up automatically. One click in the preview modal restores it.

### Which SEO plugins are supported?

Essiow works with Yoast SEO, Rank Math, and All in One SEO. It writes to all three
formats simultaneously, so switching SEO plugin later does not lose your data.

### Is my data safe?

Your product data is sent to Essiow servers only during optimization. It is processed
in real-time and not stored beyond credit-tracking metadata. Chat conversations 
are auto-deleted after 90 days per GDPR requirements.

### Do credits expire?

Purchased credits never expire. The 10 free credits expire after 30 days.

### Can I cancel a bulk optimization?

Yes. Pause / Resume / Cancel buttons appear during a bulk run. Closing the tab also
auto-cancels — items already processed remain saved.

### Can I try before buying?

Yes. Create a free account and get 10 credits to test all features. No credit card
required.

## Reviews

There are no reviews for this plugin.

## Contributors & Developers

“Essiow — AI SEO Suite for WooCommerce” is open source software. The following people
have contributed to this plugin.

Contributors

 *   [ boni58 ](https://profiles.wordpress.org/boni58/)

[Translate “Essiow — AI SEO Suite for WooCommerce” into your language.](https://translate.wordpress.org/projects/wp-plugins/essiow)

### Interested in development?

[Browse the code](https://plugins.trac.wordpress.org/browser/essiow/), check out
the [SVN repository](https://plugins.svn.wordpress.org/essiow/), or subscribe to
the [development log](https://plugins.trac.wordpress.org/log/essiow/) by [RSS](https://plugins.trac.wordpress.org/log/essiow/?limit=100&mode=stop_on_copy&format=rss).

## Changelog

#### 1.1.75

 * **Fix définitif — images vedette + images inline dans les articles bulk** :
    1. **Featured image via attachment ID** : avant, le plugin recevait une URL `https://
       site.com/wp-content/uploads/2024/10/widget-1024x768.jpg` (taille `large`) et
       tentait `attachment_url_to_postid`  souvent échec car cette fonction n’accepte
       que l’URL ORIGINALE sans suffixe `-WxH`. Désormais le plugin envoie l’`image_id`
       directement dans `featured_image_pool` ; le worker stocke `featured_image_id`
       dans `generated_payload` ; le plugin attache via `set_post_thumbnail($post_id,
       $id)` — **zéro HTTP, indestructible**.
    2. **Auto-injection des images inline** : l’IA esquivait parfois les `<img>` même
       quand on lui listait les produits dans le prompt. Le sanitizer compte maintenant
       les `<img>` valides après génération. Si moins de 3, il injecte automatiquement
       les images des produits restants, placées après les premières `<h2>`, wrappées
       dans des `<a>` vers la page produit pour le SEO. Le plugin reçoit un article
       qui a TOUJOURS au moins 3 images contextuelles, peu importe ce que l’IA a fait.
    3. **Prompt durci** : section IMAGES déplacée en MANDATORY (non-négociable), exige
       3-6 `<img>` minimum, format explicite avec wrap `<a href="PRODUCT_URL">` pour
       cumuler valeur SEO.
 * **Fix HTTP 429 sur ping IndexNow** : avant, un 429 (rate limit) cassait l’opération
   sans recovery. Désormais le retry honore le header `Retry-After` quand IndexNow
   l’envoie, sinon backoff plus long. Côté UI, le toast affiche un message clair(«
   IndexNow rate-limited. Try again in a few minutes. ») au lieu d’un cryptique «
   HTTP 429 ».
 * **Fix bouton « Indexer » qui ouvrait GSC dans une nouvelle fenêtre** : avant,
   après IndexNow + sitemap submit, le plugin ouvrait automatiquement Google Search
   Console sur la page d’inspection de l’URL — Google affichait son texte par défaut«
   URL is not on Google. Couldn’t fetch it… » et l’utilisateur croyait à un échec
   de l’indexation. Désormais aucune ouverture automatique ; le toast affiche un
   récap clair des étapes effectuées (« ✓ IndexNow · Sitemap re-submitted · Indexation
   status refreshed »). L’URL GSC reste accessible si besoin via `data-hint-url`
   sur le bouton (extensible plus tard pour une UI dédiée).
 * **Récap d’étapes détaillé** dans `ajax_request_indexing` : chaque sous-action(
   IndexNow, sitemap, inspection cache) renvoie son statut individuel. Les échecs
   partiels sont annoncés (`⚠ IndexNow rate-limited`) sans planter l’opération globale.

#### 1.1.74

 * **Audit de vérification 1.1.73** : aucun handler legacy orphelin, lock transient
   correct, idempotence du pull garantie, sanitizer appelé avant le débit crédit.
   1 seul vrai bug remonté, corrigé ici.
 * **Fix critique — page-close n’annule plus le job** : avant 1.1.74, le `cancelAllBulksOnUnload`
   annulait toujours les bulks au refresh / fermeture d’onglet via `navigator.sendBeacon`,
   ce qui était directement contraire à l’architecture jobs-serveur déployée en 
   1.1.72-73 (« rien ne s’arrête quand l’utilisateur ferme »). Le handler a été 
   neutralisé : les jobs continuent côté serveur, le polling reprend automatiquement
   au rechargement.
 * **Fix sites HTTP + installs en sous-répertoire** : le sanitizer côté Flask reconstruisait`
   https://{domain}` en ignorant le protocole et le subpath réels (info perdue côté
   serveur). Les sites en HTTP ou dans `/shop/` voyaient tous leurs liens internes
   stripés. Désormais le plugin envoie son `site_url` complet (via `home_url('/')`)
   dans le wp_context et dans le payload de `/optimize/article` — le sanitizer l’utilise
   comme base de résolution.
 * **IndexNow retry exponentiel** : avant, un seul shot avec timeout 5s. Un blip
   réseau ou un 503 transitoire perdait définitivement la soumission. Désormais :
   jusqu’à 3 tentatives avec backoff 0s / 1s / 3s. Les 4xx (clé / URLs invalides)
   court-circuitent — pas de retry sur erreurs déterministes. Timeout porté à 10s.
   Le log historique inclut maintenant `attempts` et `error` pour audit.
 * **Cleanup auto des cannibalisations dismissed/resolved obsolètes** : à chaque
   chargement de la page Search Console, on compare les clés stockées en options
   WP avec les paires (query|primary|secondary) actuellement présentes dans les 
   données GSC live. Toute clé absente du live  entrée stale  supprimée. Évite l’accumulation
   indéfinie (plusieurs centaines par an sur sites actifs).
 * **Helper `apiError(xhr, defaultMsg)` côté JS** : les erreurs AJAX étaient toutes
   affichées comme `'Network error'` quelle que soit la cause. Désormais détection
   automatique via le code HTTP et `responseJSON.data.code` :
    - Status 0  « Cannot reach the server. Check your internet connection. »
    - 401/403 ou code `invalid_api_key`  « API key invalid or expired. Reconfigure
      in Settings. »
    - 402 ou code `insufficient_credits`  « Plan ran out of credits. Upgrade in 
      Settings. »
    - 429 ou code `rate_limited`  « Too many requests. Try again in a minute. »
    - 503/504 ou code `overloaded`  « Server overloaded. Try again in 30 seconds.»
    - 5xx  « Server error. Try again or contact support. »
 * **i18n** : 7 nouvelles strings traduisibles (err_network, err_auth, err_no_credits,
   err_rate_limit, err_overloaded, err_server, no_urls_selected, sync_ok, rows, 
   property_set, rechecked, pinged) + élimination des strings hardcodées (français
   durci `'Serveur surchargé · réessayez dans 30s'`, anglais durci `'No URLs selected'`,`'
   Pinged'`, `'Re-checked'`, etc.).

#### 1.1.73

 * **Audit complet du plugin + backend** — 4 axes audités en parallèle (bulk produits/
   catégories, génération articles, Search Console, UX). 14 bugs et améliorations
   livrés en une release.
 * **Phase 2 wirée sur l’UI** : les boutons « Optimiser sélection » des pages Produits
   et Catégories utilisent désormais le nouveau système jobs serveur (`essiow_bulk_opt_create`).
   Concrètement : vous lancez, vous pouvez fermer l’onglet, vous revenez 1 heure
   plus tard — le job a continué côté serveur, le WP-Cron a appliqué les optimisations
   au fur et à mesure, et l’UI affiche l’état final.
 * **Auto-resume au chargement de la page** : si un job était en cours quand vous
   avez quitté, l’UI redémarre automatiquement le polling et affiche la progression(
   via `sessionStorage` côté navigateur).
 * **Sanitizer HTML appliqué aussi à la génération individuelle** (`/optimize/article`):
   avant 1.1.73, le post-processing des images placeholder et liens relatifs ne 
   tournait que sur le bulk. Les articles générés un par un héritaient des mêmes
   bugs. Maintenant la même protection s’applique partout — `<img src="IMAGE_URL"
   >`  strip ou fallback, `<a href="produit/x">` relatif  URL absolue canonique,
   sinon unwrap.
 * **Race condition sync-pull + WP-Cron pull supprimée** : un transient lock par
   job (`essiow_bulk_pull_lock_{id}`) empêche les deux processus de pull les mêmes
   items simultanément et d’appeler `wp_insert_post` deux fois  plus de duplicates
   côté WP.
 * **Idempotence du pull renforcée** : chaque post WP est marqué `_essiow_bulk_item_id`.
   Si Flask renvoie le même item après un retry, on détecte le post existant et 
   on re-confirme à Flask au lieu d’insérer un duplicate.
 * **Subdirectory install supporté** dans `resolve_url_to_local` : si WordPress 
   est installé dans `/wp/` (ou autre sous-répertoire), GSC renvoie l’URL avec le
   préfixe subdir, mais `url_to_postid()` attend le path relatif. Le résolveur retire
   maintenant le préfixe et retente.
 * **Cleanup hourly des state tokens OAuth Google expirés** (Celery beat) — sans
   ce nettoyage, la table `gsc_oauth_states` accumulait une ligne par démarrage 
   de flow OAuth, même ceux abandonnés en route.
 * **Confirms actionnables** : avant `confirm('Confirm?')`, désormais `confirm('
   Optimize 12 products? Each uses 1 credit and continues running even if you close
   this page.')`. Pareil pour catégories et annulation de job.
 * **Label « Processing: [item] »** affiché dans la barre de progression — vous 
   voyez en direct quel produit/catégorie est en cours d’optimisation.
 * **CSS bouton désactivé cohérent** (opacity 0.55 + cursor not-allowed) — fini 
   les thèmes qui rendent les boutons disabled identiques aux activés.
 * **Config writing_tone/language/length exposée au JS** pour que les jobs bulk 
   utilisent les préférences globales du site automatiquement.

#### 1.1.72

 * **Phase 2 — Optimisations produits/catégories via jobs serveur** (architecture
   jumelle des articles bulk). Le plugin POST la liste d’objets WP à optimiser vers
   Flask, Celery les traite un par un en arrière-plan, le plugin pull les items 
   prêts via WP-Cron (5 min) et les applique localement via `wp_update_post` + `
   update_post_meta` + `update_term_meta` + métas SEO (Yoast / Rank Math / AIOSEO).
   Le crédit est débité en transaction atomique côté Flask au moment du confirm.
 * **Survit à tout** : fermeture d’onglet, perte de connexion, inactivité prolongée,
   crash navigateur, redémarrage WordPress. Le state du job est en DB SQL côté Flask—
   le plugin n’a aucun transient critique à perdre. À la reconnexion, le polling
   reprend où il en était, et même sans reconnexion, le worker Celery continue et
   le WP-Cron applique au fil de l’eau.
 * **Pause / Reprendre / Annuler** propres, lus entre chaque item par le worker 
   Celery. Annulation immédiate sur job inactif (basculement statut serveur instantané,
   identique à 1.1.68 pour les articles).
 * **Nouveau modèle DB Flask** : `BulkOptimizationJob` + `BulkOptimizationItem`.
   Endpoints : `/optimize/bulk/create`, `/status`, `/pause`, `/resume`, `/cancel`,`/
   pending-items`, `/items/<id>/applied`.
 * **Nouvelle classe plugin** `Essiow_Bulk_Optimize` : AJAX handlers `essiow_bulk_opt_*`
   + cron `essiow_cron_bulk_opt_pull` qui pull et applique. Le code legacy (WP-Cron
   transient-based) reste en place pour rétro-compatibilité — la migration UI vers
   les nouveaux endpoints se fait progressivement dans les prochaines releases.

#### 1.1.71

 * **Fix critique — image vedette absente sur les articles bulk** : `download_url()`
   échouait silencieusement sur les hosts mutualisés où le loopback HTTP est bloqué(
   cas très fréquent : mod_security, reverse-proxy hostile, WAF). Résultat : aucune
   image vedette n’était jamais attachée. Désormais, quand l’URL est locale au site,
   on retrouve l’attachment via `attachment_url_to_postid` directement — **aucune
   requête HTTP** — c’est instantané et 100 % fiable. Le téléchargement reste en
   fallback pour les images distantes (CDN externe).
 * **Fix critique — images cassées dans le corps d’article** : l’IA produisait régulièrement
   des `<img src="IMAGE_URL">` littéraux (placeholder du prompt non substitué par
   le vrai URL). Désormais, un post-processing côté Flask scanne chaque `<img>` 
   après génération : ceux qui contiennent un placeholder (`IMAGE_URL`, `PRODUCT_URL`,`
   example.com`, src vide, etc.) sont soit remplacés par l’image vedette du pool,
   soit stripés. Le prompt OpenAI a aussi été reformulé pour interdire explicitement
   les placeholders.
 * **Fix critique — liens internes renvoient à l’accueil** : l’IA générait fréquemment
   des `<a href="produit/widget">` (relatif), qui une fois publiés sur `/mon-article/`,
   deviennent `/mon-article/produit/widget`  404  souvent redirigé vers l’accueil
   par les plugins SEO. Le post-processing résout maintenant chaque `<a href>` :
    - Si l’URL correspond à un produit / article / catégorie envoyé en contexte  
      résolu en URL absolue canonique.
    - Si l’URL est relative et inconnue  résolue via `site_url + chemin`.
    - Si l’URL est vide / placeholder / `#`  le `<a>` est unwrappé (le texte reste,
      le lien disparaît).
 * **Nouveau service** `article_html_sanitizer.py` : module autonome de post-processing
   HTML qui répare tous les artefacts d’IA (placeholders, URLs relatives, ancres
   vides). Logué avec compteurs (`imgs kept=X stripped=Y / links kept=X stripped
   =Y`) pour audit a posteriori.
 * **set_featured_image() durci** :
    - Timeout porté de 5s à 30s.
    - Détection automatique URL locale vs distante (compare hosts normalisés sans`
      www.`).
    - Fallback gracieux si le nom de fichier de l’image est sans extension (déduction
      du type MIME via `getimagesize`).
    - Logs explicites en cas d’échec (avant : silencieux).

**Phase 2 à venir** : migration de la génération individuelle d’articles, de l’optimisation
produits (individuel + bulk), et de l’optimisation catégories (individuel + bulk)
vers l’architecture jobs-serveur identique aux articles bulk — fermeture de l’onglet,
perte de connexion, inactivité : rien ne s’arrête, l’optimisation reprend où elle
s’est arrêtée. Travail de refonte sur plusieurs releases.

#### 1.1.70

 * **Audit complet des 4 sources de génération bulk** (liste de mots-clés, catégories
   WC, produits WC, import CSV SEMrush) + 6 bugs corrigés.
 * **Fix critique — état du sélecteur de source** : taper une liste de mots-clés,
   puis switcher sur « Produits » (sans rien cocher) puis lancer envoyait le mauvais
   payload au backend (source=products + des keywords textuels libres). Désormais,
   le changement d’onglet réinitialise la sélection et l’état visuel — l’utilisateur
   doit re-sélectionner sur le nouveau mode.
 * **Source « Catégories produits »** : le worker reçoit maintenant explicitement
   le nom de la catégorie sous `category_name`. Le prompt active la section « PRODUCT
   CATEGORY CONTEXT » et rédige l’article comme une page pilier de catégorie au 
   lieu d’un article générique sur la requête.
 * **Source « Produits »** : l’URL, l’image et le prix du produit sélectionné sont
   désormais envoyés en contexte au worker. Le produit cible est injecté en tête
   de la liste des produits disponibles (l’IA le mentionne en priorité) et son image
   devient l’image vedette par défaut.
 * **Fix dropdown auteur vide sur WP 5.9+** : `get_users(['who' => 'authors'])` 
   est déprécié depuis WordPress 5.9 et renvoyait un tableau vide  le dropdown auteur
   de la page bulk était vide, impossible de lancer un job. Remplacé par `capability
   => 'edit_posts'`, avec fallback sur l’utilisateur courant si vide.
 * **CPC préservé** dans le pipeline : la valeur Cost-Per-Click parsée depuis les
   exports SEMrush était parsée puis dropped lors de la sanitisation côté PHP. Maintenant
   elle remonte jusqu’au worker (disponible pour de futures heuristiques de priorisation
   des keywords).
 * **Sanitisation des keywords élargie** côté plugin : accepte les 3 formats — strings(
   mode liste/catégories), dict SEMrush (`keyword, volume, kd, intent, cpc`), dict
   produit (`keyword, product_url, product_image_url, product_price`).

#### 1.1.69

 * **Parité complète entre articles bulk et articles individuels**. Avant 1.1.69,
   les articles générés en masse étaient pauvres : pas d’images, pas d’image vedette,
   pas de liens internes vers les produits/catégories, pas de recommandations. Maintenant
   ils sortent identiques à ceux générés un par un.
 * **Contexte WordPress envoyé au worker** : à chaque création de job, le plugin
   collecte et transmet au backend les 30 produits top-ventes (avec URL/prix/image),
   les 20 articles récents et les 15 catégories produits actives. L’IA dispose donc
   du même contexte que pour la génération individuelle — elle peut citer les vrais
   produits, créer des liens internes pertinents, choisir des images réelles.
 * **Image vedette automatique** : chaque article reçoit en featured image une image
   produit de votre catalogue (rotation par position pour varier entre articles 
   d’un même batch).
 * **Conversion en blocs Gutenberg** : le contenu HTML est désormais découpé en 
   blocs `<p>`, `<h2>`, `<ul>`, `<table>` distincts (plus de gros bloc HTML brut
   difficile à éditer). Identique à la génération individuelle.
 * **SEO meta complet** : Yoast SEO, Rank Math et AIOSEO sont tous les trois renseignés(
   titre, description, focus keyword). Avant, seuls titre + description partiels
   étaient settés.
 * **Excerpt automatique** + nettoyage des doublons (h1/h2 du titre, en-têtes “Introduction”
   résiduels).
 * **Nouvelle section « Articles générés en masse »** en bas de la page Bulk : liste
   paginée des articles produits par les jobs, avec score SEO (0-100), statut (publié/
   brouillon), date, nombre de mots, et **boutons Voir / Modifier**. Sélection multiple
   ping IndexNow + demande d’indexation Google, comme dans la liste des articles
   individuels.
 * **Auto-refresh de la liste** à chaque tick de polling — les nouveaux articles
   publiés apparaissent dans la liste sans recharger la page.
 * **Fix accents** : les titres de produits et catégories étaient affichés avec 
   leurs entités HTML brutes (`8Sinn eXtraThin HDMI &#8211; Cable` au lieu de `–
   Cable`). Décodage via `html_entity_decode` partout : sélecteur produits bulk,
   sélecteur catégories, contexte envoyé au worker.

#### 1.1.68

 * **Refonte UX bulk** suivant retours utilisateurs (5 changements majeurs) :
 * 1) **Annulation immédiate** : annuler un job en queued / awaiting_wp_publish /
   paused bascule maintenant le job à `cancelled` instantanément côté serveur. Avant,
   seul le flag `cancel_requested` était mis à true, mais comme aucun worker n’était
   en train de tourner, le status restait inchangé — les boutons Annuler persistaient
   même après reload. Pareil pour Pause sur un job en attente.
 * 2) **Suppression des jauges visuelles** : remplacées par des pourcentages texte
   clairs et compacts. La modale active affiche `🤖 Génération IA : 60% (3/5) · 
   📝 Publication WP : 40% (2/5)` sur une seule ligne.
 * 3) **Générations en cours déplacées en bas** de la page (sous l’historique), 
   comme une barre de statut discrète. L’utilisateur n’est plus visuellement bloqué
   par un gros banner en haut quand il configure une nouvelle génération.
 * 4) **Dialog Détails enrichi** : nouvelle colonne « Titre de l’article » qui affiche
   le titre généré par l’IA pour chaque mot-clé, plus deux boutons d’action explicites—
   👁 **Voir** (lien public vers l’article) et ✎ **Modifier** (admin WP). Indispensable
   pour passer en revue les articles générés.
 * 5) **Auto-refresh après action** : pause / reprendre / annuler déclenchent immédiatement
   un re-fetch du status + un refresh de l’historique + un refresh de la modale 
   Détails si elle est ouverte sur le même job. Plus jamais d’état stale dans l’UI.
 * Auto-fade-out de la barre active 5s après complétion + toast final « ✅ Génération
   terminée — N articles ».

#### 1.1.67

 * **Refonte complète UI Bulk generation** suite aux retours utilisateurs.
 * **2 jauges de progression distinctes** : une pour la génération IA côté SaaS,
   une pour la publication WordPress côté local. Avant, le compteur affichait toujours
   0 jusqu’à la publication WP — pendant tout le temps de génération (5-10 min sur
   des dizaines d’articles), l’utilisateur croyait que rien ne se passait. Maintenant
   la jauge bleue avance dès qu’un article est généré côté SaaS, puis la jauge verte
   avance quand le post WP est créé.
 * **Boutons d’action fonctionnels** avec feedback immédiat : pause / reprendre /
   annuler envoient maintenant un toast de confirmation, forcent un rafraîchissement
   immédiat du status et de l’historique. Plus jamais d’action « silencieuse ».
 * **Dialog « Détails »** sur chaque ligne d’historique + bouton dans la modale 
   active : affiche tous les paramètres du job (statut, source, dates, config IA,
   mots-clés un par un avec leur statut individuel et lien direct vers l’article
   WP créé). Indispensable pour vérifier où en est un job ou consulter ses anciens
   runs.
 * **Bouton « Détails » dans la modale active** également, pour consulter la liste
   des keywords pendant la génération.
 * **Affichage du mot-clé en cours** dans la modale : `⚡ En cours : "comment choisir
   un tracteur"` — l’utilisateur sait exactement où en est le générateur.
 * **Toast final** « Génération terminée — N articles » à la complétion du job (
   au lieu de la modale qui restait éternellement).
 * **Compteurs propres** : on lit maintenant les bons noms de champs (`completed_count`,`
   total_count`, `generated_count`, `published_count`) avec alias rétro-compat. 
   Plus de « undefined / undefined ».

#### 1.1.66

 * **Fix critical** : sur les sites peu visités, WP-Cron ne tournait pas  les articles
   générés côté SaaS restaient en attente et n’étaient jamais publiés. Le job passait
   à `awaiting_wp_publish` mais le plugin ne pullait jamais les articles.
 * **Sync-pull dans le polling** : à chaque poll status (toutes les 5s côté JS),
   si le job est `running` / `awaiting_wp_publish` / `paused`, le plugin déclenche
   un pull synchrone immédiat — il pull les items prêts, fait `wp_insert_post` localement,
   et confirme à Flask (qui débite le crédit). Le navigateur du user devient le 
   moteur de cron, exactement comme on a fait pour l’Automesh sur shared hosting.
 * **Boutons actions dans l’historique** : nouvelle colonne « Actions » avec ⏸ Pause/
   ▶ Reprendre / ✕ Annuler pour chaque job actif. Plus besoin d’attendre que le 
   banner du job actif s’affiche pour le piloter.
 * **Toast FR direct** : « Génération en masse lancée » au lieu du fallback anglais«
   Bulk job started » quand les traductions PHP ne sont pas encore chargées (transition
   de version).

#### 1.1.65

 * **Fix critical (jauge)**: la barre de progression du bulk apparaissait pleine
   5 secondes au refresh avant de revenir à 0. Cause : `total_count` passé en string`'…'`
   au render initial  calcul `(0+0+0)*100/'…'` = NaN  `width: NaN%` CSS invalide
   fallback navigateur à 100%. Tous les compteurs sont désormais castés en `parseInt(...
   10) || 0`, et le render initial part avec `total_count: 0` (jauge à 0%, plus 
   de flash).
 * **Traductions FR** complètes de toute la nouvelle vue Bulk generation. Tous les
   libellés sont en français dans la vue PHP, et les fallbacks JS également (statuts
   du job, sources, labels de progression, boutons pause/reprendre/annuler, configuration
   de génération, etc.).
 * **Format date français** dans l’historique (`toLocaleDateString` + heure HH:MM)
   au lieu de l’ISO brut.
 * Labels lisibles pour status (`queued`  « En file », `running`  « En cours », `
   awaiting_wp_publish`  « Publication WordPress », etc.) et source (`keyword_list`«
   Liste de mots-clés », `collections`  « Catégories produits », etc.) — plus de
   codes techniques affichés à l’utilisateur.

#### 1.1.64

 * **Fix critical (bulk articles)** : audit complet de 1.1.63 — l’implémentation
   initiale n’aurait pas pu fonctionner. Trois problèmes bloquants corrigés.
 * Fix 1 — **API authentication broken**. The bulk handlers used `get_option('essiow_api_key')`
   which reads an option that doesn’t exist in clear (the API key is stored AES-
   encrypted in `essiow_api_key_enc`). They also missed the HMAC anti-replay signature
   headers (`X-Timestamp`, `X-Nonce`, `X-Domain`, `X-Signature`). Every call would
   have failed with 401 Unauthorized.  Refactored all handlers to use the canonical`
   Essiow_API_Client::instance()` which handles decryption + HMAC signing transparently.
 * Fix 2 — **JSON response unwrapping bug**. `Essiow_API_Client::handle_response()`
   returns the decoded body directly (so `$resp['data']` IS the data), but the bulk
   code wrapped on a 3rd level (`$resp['data']['data']['jobs']`), which always evaluated
   to null. Status / list / create / pending-items / confirm-published — all 7 endpoints
   affected.  Aligned with the canonical client structure.
 * Fix 3 — **CSV preview moved to plugin-side parsing**. The original implementation
   tried to POST a multipart upload to Flask without computing the HMAC signature
   for binary content. Replaced by a self-contained PHP CSV parser (header sniffing
   for keyword/volume/KD/intent across English/French/German/Spanish aliases, encoding
   detection with BOM stripping, delimiter auto-detect via comma/semicolon/tab/pipe
   count). No more round-trip to Flask for previews — faster and avoids the signing
   problem entirely.

#### 1.1.63

 * **New: Bulk article generation** for WooCommerce / WordPress. Generate dozens
   to hundreds of SEO articles in one go from four sources:
    - **Product categories**: one article per selected WooCommerce category
    - **Products**: one article per product (up to 500)
    - **Keyword list**: free-form textarea, comma or line separated
    - **SEMrush CSV**: upload your export (Keyword Magic Tool / Organic Research/
      Keyword Gap), preview keywords with volume/KD/intent, select which to keep
 * Server-side orchestration via the Essiow backend. The browser only triggers the
   job — generation continues even if you close the tab. A WP-Cron worker pulls 
   ready articles every 5 minutes and inserts them as WordPress posts (or via wp_insert_post
   triggered immediately on first launch).
 * **1 credit = 1 article published**: credits are debited only after the WordPress
   post is successfully inserted (atomic transaction backend-side). If credits run
   out mid-job, it auto-pauses; refill and resume.
 * **Pause, resume, cancel** any running job at any time. State is persisted server-
   side.
 * **Crash resilient**: if the worker dies mid-generation, the next pull picks up
   where it left off without re-paying for what was already generated.
 * **Drip publishing** option: spread articles over N days (1 every N days) instead
   of all at once — better for SEO patterns and avoids Google seeing a burst.
 * **Per-post config**: author, post category, publish status (immediate or draft),
   tone, length, language, fuzzy dedup toggle.
 * **SEO meta filled out**: Yoast / Rank Math / AIOSEO meta fields are set automatically
   when the SaaS returns SEO title/description.
 * Hard cap of 500 articles per job (anti-blast-radius).
 * Job history with status per row, restart polling automatically when reopening
   the page on an active job.

#### 1.1.62

 * **Performance overhaul** of `_automesh_compute_plan` — targets sub-30s compute
   even on 10k-page sites, so the planning phase fits inside the tight PHP-FPM timeouts
   of shared hosts (EazyWP, low-tier hosts) and stops triggering the 1.1.61 “planning_aborted”
   abort.
 * **Optim 1 — Batch WP term cache** : `wp_get_post_terms()` was called once per
   product (10000 separate SELECTs on a 10k-product site). Now pre-warms the WP 
   term cache via `update_object_term_cache($product_ids, 'product')` — 1-2 SELECTs
   total, the per-product calls become free cache hits.
 * **Optim 2 — Token memoization** : every `_jaccard_titles()` call was re-tokenizing
   both titles from scratch. On a 5000-page site with 100-sibling avg pool, that’s
   500k tokenizations. Added an instance-scoped `_token_cache` keyed by title  ~
   5k tokenizations total. Speedup ~100×.
 * **Optim 3 — Faster Jaccard intersection** : replaced `array_intersect` (O(n×m))
   with `array_flip + isset()` lookup (O(n)). 5× cumulated gain.
 * **Optim 4 — Pool size cap** in `_best_topical_match*()`. When the candidate pool
   exceeds 1000 items, sample 500 random instead of scanning all. Statistically 
   same top-K quality, but O(N²) explosion neutralized on massive sites.
 * **Optim 5 — Skip anchor registry preload entirely above 5000 pages**. The `AUTOMESH_EXACT_RATIO_MAX`(
   15%) guard rail already prevents over-using exact anchors, so an empty initial
   registry produces a balanced mesh from run 1 anyway.
 * Result on a 10k-page site (measured on a dev VM): compute_plan dropped from ~
   180s to ~22s. Memory footprint divided by ~3.
 * Note: if a 10k+ site still hits `planning_aborted` on your host after this update,
   the `max_execution_time` is < 30s. Ask your host to set it to 60s+, or split 
   into smaller imports.

#### 1.1.61

 * **Fix critical**: automesh planning phase stuck in infinite loop (“Preparing 
   your link plan… 5000s+”). On hosts where PHP-FPM kills requests at 60-120s, each
   worker attempt died before saving its progress — and every subsequent status 
   poll relaunched a fresh attempt that died again. No exit, no error, just an ever-
   growing elapsed counter.
 * Three combined fixes:
    1. **Anchor registry sampling for large sites** (> 2000 pages). The biggest CPU/
       memory bottleneck of `_automesh_compute_plan` is `_preload_anchor_registry`,
       which fetches and regex-parses the full HTML content of every page. On 10000
       pages that’s 10000 SQL + 500 MB of regex. Now sampled to 500 random pages on
       large sites — enough to estimate existing anchor-type ratios without killing
       the worker.
    2. **Max 5 planning attempts** counter on the task. If the worker keeps dying mid-
       compute (PHP-FPM timeout, OOM), after 5 retries we mark the task as `failed`
       with a clear log: “PHP-FPM/host kill shorter than required compute time. Contact
       your host to raise max_execution_time and memory_limit, or split your site into
       smaller imports.”
    3. **Cancel button** in the in-progress modal + new `ajax_automesh_cancel` endpoint.
       Lets users escape a stuck task instantly: marks it failed, releases the `essiow_il_automesh_active`
       lock, clears scheduled WP-Cron events. Confirmation prompt explains that already-
       injected links remain (backed up, revertable).
 * New: when a task ends in `failed` state, the UI fetches the detailed log line
   from the backend and shows it in the error toast — actionable info instead of“
   Something went wrong.”
 * New strings: `automesh_cancel`, `automesh_confirm_cancel`, `automesh_cancelled`(
   i18n).

#### 1.1.60

 * **Refactor critical**: the Automesh plan computation is now fully deferred to
   the background worker. On very large sites (10000+ pages), the compute_plan was
   taking > 3 minutes inside the HTTP request, hitting browser timeouts, Cloudflare
   100s upstream limit, and PHP max_execution_time even after our 1.1.59 hardening.
   No amount of timeout-raising could solve that — the compute had to stop blocking
   HTTP entirely.
 * **Architecture after 1.1.60**:
    1. `ajax_automesh_plan` (preview) returns a fast **heuristic estimate** based only
       on the cached graph (orphans count, deadends count, hubs count). ≤ 10s on any
       site size. No compute_plan, no simulate_apply_plan.
    2. `ajax_automesh_start` immediately creates a task with `status='planning'`, schedules
       the worker, and returns in < 1s. No more “Timed out” on click.
    3. `cron_automesh_run` (worker) detects `status='planning'` on its first tick  
       runs `build_graph` + `compute_plan` (the heavy work, up to 5 minutes), then 
       switches to `status='pending'` and processes batches as before.
    4. `ajax_automesh_status` distinguishes `planning` vs `running`. The sync-fallback
       path (shared hosting where WP-Cron is dead) also handles the planning tick —
       same trick: if no tick in 30s, the status request itself runs the worker.
 * **JS UI**: shows “Preparing your link plan…” with an indeterminate progress bar
   during the planning phase, then switches to “X / Y pages processed” once the 
   plan is ready. Elapsed counter visible (so the user knows things are moving).
   Failed-status detection added to clear the modal if the worker crashes during
   planning.
 * New strings: `automesh_planning`, `automesh_planning_hint`, `automesh_failed`(
   i18n).
 * Removed: the 1.1.59 transient cache for the plan — no longer needed since the
   start endpoint doesn’t compute the plan anymore.

#### 1.1.59

 * **Fix**: “Erreur réseau” toast on Automesh when the site has many links (1000
   + pages, 4000+ planned links). Four compounding causes audited and patched:
 * Fix 1 — **`ajax_automesh_plan` was less protected than `ajax_automesh_start`**
   despite doing MORE work (build_graph + compute_plan + simulate_apply_plan + 2
   × score). It had `set_time_limit(180)` only — no `ignore_user_abort`, no `wp_raise_memory_limit`,
   no `try/catch`. Aligned on `ajax_automesh_start` (300s, memory raised, try/catch
   with clear error detail).
 * Fix 2 — **Double-compute bug**: clicking preview then “Run automesh now” recomputed
   the plan twice (60s + 60s = 120s cumulative — kill on Cloudflare/Nginx at 100s
   upstream timeout). Now the preview caches its plan for 5 min, and `ajax_automesh_start`
   reuses it instead of recomputing. One-shot cache (consumed on use).
 * Fix 3 — **JS `$.post` had no `timeout` on the preview**. Default Chrome XHR timeout
   is multi-minute, but Cloudflare/Nginx kill at 100s and the browser sees a network
   error. Explicit `timeout: 180000` (3 min) added, with status-specific error messages(
   504 detected separately to point at the host’s reverse proxy).
 * Fix 4 — **Transient compression**: large plans (> 100 KB serialized) are now `
   gzcompress`‘d before being stored in `wp_options.essiow_il_automesh_plan_cache`.
   Avoids hitting MySQL `max_allowed_packet` (default 4-8 MB on shared hosting),
   which would silently truncate the row and break the worker.
 * All four fixes also help the original EazyWP / shared-hosting scenarios from 
   1.1.58 — they apply to the preview stage which 1.1.58 didn’t cover.

#### 1.1.58

 * **Fix critical**: Automesh stuck on “0 / undefined pages processed” on shared/
   low-spec hosts (EazyWP, Hostinger, low-tier OVH, etc).
 * Two combined root causes :
    1. **Truncated AJAX response** from `ajax_automesh_start` : on a 1000+ page site,
       computing the plan takes 30-60s. PHP’s default `max_execution_time` (30s) on
       shared hosting truncates the JSON response mid-write. The plugin received a 
       partial response with `success:true` but `total` undefined  `0 / undefined` 
       displayed.
    2. **WP-Cron broken / disabled** : many shared hosts disable `DISABLE_WP_CRON` 
       without setting up a real cron, or block loopback HTTP (used by `spawn_cron()`).
       The worker `cron_automesh_run` was scheduled but never executed  0 pages processed
       indefinitely.
 * Fix 1 (server-side hardening) : `ajax_automesh_start` now wraps the plan computation
   in `try/catch`, sets `set_time_limit(300)`, raises memory via `wp_raise_memory_limit('
   admin')`, and returns a clean error with detail if …

## Meta

 *  Version **1.1.75**
 *  Last updated **4 days ago**
 *  Active installations **10+**
 *  WordPress version ** 5.8 or higher **
 *  Tested up to **6.9.4**
 *  PHP version ** 7.4 or higher **
 *  Language
 * [English (US)](https://wordpress.org/plugins/essiow/)
 * Tags
 * [AI](https://test.wordpress.org/plugins/tags/ai/)[chatbot](https://test.wordpress.org/plugins/tags/chatbot/)
   [product descriptions](https://test.wordpress.org/plugins/tags/product-descriptions/)
   [seo](https://test.wordpress.org/plugins/tags/seo/)[woocommerce](https://test.wordpress.org/plugins/tags/woocommerce/)
 *  [Advanced View](https://test.wordpress.org/plugins/essiow/advanced/)

## Ratings

No reviews have been submitted yet.

[Your review](https://wordpress.org/support/plugin/essiow/reviews/#new-post)

[See all reviews](https://wordpress.org/support/plugin/essiow/reviews/)

## Contributors

 *   [ boni58 ](https://profiles.wordpress.org/boni58/)

## Support

Got something to say? Need help?

 [View support forum](https://wordpress.org/support/plugin/essiow/)