| 开发者 | boni58 |
|---|---|
| 更新时间 | 2026年9月28日 18:14 |
| PHP版本: | 7.4 及以上 |
| WordPress版本: | 6.9 |
| 版权: | GPLv2 or later |
| 版权网址: | 版权信息 |
/llms.txt at your root. It is an answer-engine signal, not a Google one: Google states it ignores the file, the other assistants read it.
12. Puts your catalog in the shopping feeds. One ProductGroup markup with an offer per variant makes your variable products eligible for Google free listings, and a product feed file (Google Merchant Center, Meta, Pinterest) is served by your shop and fetched automatically. The file alone needs no connection: you paste its URL in the platform and it fetches daily.
13. Watches your page experience. Core Web Vitals from real Chrome users (LCP, INP, CLS) plus a Lighthouse score per page template, a ~40-point technical audit of your server, database, assets and images, and one-click fixes for the safe ones — each reversible.
14. Finds your off-site opportunities. Essiow searches for pages that mention your brand without linking to it, lists sites worth contacting, and reports external links that now hit a 404 so you can redirect them in one click. It detects and drafts the e-mail — it never creates a link for you, and buying links is not part of the product.
15. Publishes on a rhythm you set. The content plan turns keywords, your categories and your Search Console gaps into a 14-day calendar: pick how many articles per day or per week, the days, the hour and the output (draft, scheduled or published). An activity register keeps every long action marked until it finishes — closing the tab or reloading the page changes nothing.
16. Gets your shop ready for Google Merchant Center. Most refusals and suspensions are about the shop, not the feed: a return policy left as a draft, the WooCommerce template text still in place, guest checkout closed, prices shown excluding tax, a page nobody can reach. Essiow audits 95 of those points the way a Google reviewer reads your site, tells you what is missing in plain words, and fixes what can safely be fixed — every fix reversible. Connect your Merchant Center account and you also get the account and product problems Google reports, in your own language, with the exact attribute in cause, plus an e-mail the day the account gets suspended. Essiow prepares, diagnoses and fixes; Google decides — no plugin can promise an approved account, which is why the review request stays locked until every blocking check is green (you only get one real appeal).
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
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 folder to /wp-content/plugins/Yes. Create a free account at essiow.com to get your API key and 10 free credits.
No. If you can install a WordPress plugin, you can use Essiow. Everything is done in a few clicks.
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".
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.
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.
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.
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.
Purchased credits never expire. The 10 free credits expire after 30 days.
Yes. Pause / Resume / Cancel buttons appear during a bulk run. Closing the tab also auto-cancels — items already processed remain saved.
Yes. Create a free account and get 10 credits to test all features. No credit card required.
A local audit out of 100 (title and meta lengths, focus keyword, description length and structure, image alt texts, FAQ, schema…). It is computed by the plugin, never costs a credit, and is stored so you can filter and sort the catalog by score. Rating a large catalog happens progressively in the background of the Products page.
No. Nothing runs on the storefront except the optional chat widget, the JSON-LD schemas (cached) and the redirect lookup (cached). Bulk jobs, syncs and automations run through WP-Cron or on the Essiow servers.
Nothing to do: the plugin migrates its own data at the first request after an update (database tables, new options with safe defaults, expired caches). Your API key, settings, optimizations and redirects are kept.
All options, cached data, scheduled tasks, tables (agent conversations and leads, redirects, 404 log, automation queue, content plan, speed runs, off-site findings) and Essiow metadata. If "Restore originals on uninstall" is enabled in Settings, the original titles, descriptions, SEO meta and alt texts are put back and the internal links added by Essiow are removed.
No, and it never will. The Off-site module searches for pages that already mention your brand without linking to it, lists sites worth contacting and reports external links that now hit a 404. It prepares the outreach e-mail; you send it and the other site decides. Nothing is posted, bought or published anywhere on your behalf.
It exposes a clean summary of your catalog to AI assistants. It is an answer-engine signal, not a Google ranking factor: Google has stated it ignores the file. Keep it on if you care about ChatGPT, Perplexity or Claude citing your shop; it costs nothing either way.
Only for the Field tab. Core Web Vitals from real users and the Lighthouse score come from the free PageSpeed Insights API. The technical audit and its fixes work without any key.
No. Essiow serves the feed file at your own domain and you paste that URL in the platform (Google Merchant Center, Meta or Pinterest), which fetches it daily. The Eligibility tab lists what each item is still missing. Connecting your Merchant Center account is a separate, optional step that adds Google's own verdict on top.
No, and no plugin can: Google decides. What Essiow does is remove the reasons for a refusal before you ask — it audits 95 points the way a reviewer reads your shop, explains each failure in plain words, and fixes the ones that can be fixed safely. The review request button stays locked until every blocking check is green, on purpose: you only get one real appeal, and each refusal lengthens the wait.
Your shop, not your feed: HTTPS and reachability, return and shipping policies (published, not the WooCommerce template, complete), contact details and legal identity, the checkout end to end (guest checkout, payment method, terms, order e-mails), prices and tax display, placeholder content, structured data, and your product data (titles, descriptions, images, GTIN, category, variants). The probe reads your shop from your own server, so it cannot see blocking that depends on the visitor's country or IP — those checks say so instead of claiming to be covered.
No. The authorization goes through essiow.com: you sign in with Google, accept the access, and come back. No WordPress.com account, no Cloud project, no API key to create. Disconnecting removes the link on both sides.
No. Optimizations, bulk generations, scans and measurements are registered server-side. The item stays marked until the job finishes, the state comes back after a reload, and the activity centre in the top bar lists everything currently running.
{{…}} marker is never published — that template text is exactly what Google sanctions.wc_get_price_to_display), so the feed can no longer contradict the shop. sale_price is only sent while the sale actually runs, with its effective date. The brand is no longer filled with the site name, and identifier_exists is only sent when the answer is known. New columns: Google product category, age group, gender, size type and system, custom labels 0-4. A title or description written by the AI is declared to Google as such (structured_title / structured_description with digital_source_type), as Merchant Center requires.spawn_cron() seconds after the purge).essiow_gmc_* (22, all covered by the smoke test), the {prefix}essiow_gmc_audit table, the essiow_gmc_audit_weekly and essiow_gmc_daily crons, the essiow_gmc_tabs and essiow_gmc_state filters for adding a tab to the page without touching it, a Merchant Center API mock with six failure modes, an upgrade scenario from 1.3.0 across the 15 admin pages, and an updated inventory in CONTRIBUTING.md and docs/DESIGN.md.ProductGroup with one offer per variant (instead of an aggregate), variant attributes as variesBy, product specifications, and shipping details read from your real WooCommerce shipping zones — which is what Google asks for to consider a variable product for free listings.hreflang with x-default on multilingual sites (Polylang, WPML), complete sharing tags, and business-profile fields (coordinates, price range, area served, Google Business Profile link, X / Twitter handle) feeding the Organization / LocalBusiness schema.essiow_run_upgrade() creates the new tables (speed runs, off-site findings, content plan) and their default options at the first request after the update — no reactivation, no need to open the module's page. Crons for the new modules are armed automatically, and uninstall removes every new table, option and scheduled task./llms.txt described for what it is (an answer-engine signal Google states it ignores), the IndexNow engine list kept accurate (Bing, Yandex, Naver, Seznam, Amazon, Yep), and no backlink-building claim anywhere.essiow_speed_*, essiow_offsite_*, essiow_lost_*, essiow_feeds_*, essiow_plan_* and essiow_activity_*, all covered by the smoke test; a real upgrade scenario from 1.2.0 (bin/upgrade-test.sh) checking data, schema, defaults and crons across the 14 admin pages; bin/shot.sh covering every page; inventory of actions, options, tables, crons and storage keys in CONTRIBUTING.md and docs/DESIGN.md; regenerated translation template._essiow_seo_score, rated progressively in batches), global KPI with score distribution, server-side filters (name / SKU, status, score, category, sort, page size) kept in the URL and in the browser, a fixed paginated table, "select the page" or "select the whole filter" with a floating bar, a preview panel with a factor-by-factor audit, before / after scores, FAQ and restore, and a restyled bulk job (pause, resume, cancel, failed items). Optimizing or restoring no longer reloads the page.active column, migrated automatically). Duplicate regex patterns are refused.#essiow-chat deep link).essiow_maybe_upgrade() runs idempotent migrations at the first request after any update (default options, database tables via dbDelta, expired caches purged, orphan cron hooks removed, essiow_upgraded action for extensions) — no reactivation needed. Uninstall now removes every option, transient, table, cron, post / term / user meta added since 1.1.79 and still restores the original content when the option is enabled.assets/css/pages/<slug>.css (loaded automatically), Essiow_Admin::icon / page_header / kpi / empty_state and EssiowUI.icon / confirm / drawer / bulkBar / pagination / setKpi helpers documented in docs/DESIGN.md, a demo environment (bin/smoke.sh with SEED=1, mock API, bin/shot.sh screenshots), an upgrade scenario (bin/upgrade-test.sh) and an uninstall check (UNINSTALL=1) in the smoke test, the smoke test covering every page, AJAX action, cron and front page, new server filters on the products / categories / articles / redirects lists, and a regenerated translation template.orderby, filter_*, prix, utm…), exclusion des produits noindex du sitemap.priceValidUntil, politique de retour, frais et délais de livraison, AggregateOffer pour les variables — sans doublon avec Yoast WooCommerce / Rank Math / AIOSEO.ORDER BY RAND() retirées, pagination des catégories, chaînes dupliquées et non traduites corrigées._essiow_seo_score).rel="noopener" (+ nofollow hors sources autoritaires).post_name), FAQPage depuis les données structurées (plus de regex cassée par les blocs Gutenberg), BreadcrumbList, BlogPosting enrichi (wordCount, timeRequired, articleSection) — et plus de doublon de schéma quand Yoast / Rank Math / AIOSEO sont actifs.essiow_auto_backlinks).$keywords non défini à la publication (focus keyword vide, exclusion GSC cassée) ; auteur 0 en cron bulk ; score dashboard IndexNow lisait la mauvaise option ; la longueur « court / moyen / long » du bulk était ignorée (2 500 mots forcés)._build_gsc_context_for_article(site, item.keyword) et injecte le résultat dans le prompt — exactement comme l'individuel.wp_context (produits avec image_id/image_url, articles, catégories, pool d'images vedette) au backend Flask. Mais BulkArticleService.create_job reconstruisait le dict de configuration en ne retenant qu'une whitelist de clés (tone, length, language, blog_id, etc.) — wp_context n'était PAS dans la whitelist, donc silencieusement éliminé. Conséquences en cascade :cfg.get('wp_context') → None → products = [], blog_posts = [], featured_image_pool = []<img> générée (l'IA ne pouvait pas inventer des URLs valides)article_data.products (vide aussi) → ne pouvait rien injecter non pluswp_context et site_url dans la whitelist. Les NOUVEAUX jobs bulk recevront enfin le contexte produit complet, et toute la chaîne d'images (génération IA + auto-injection sanitizer + image vedette via attachment_id) fonctionnera bout en bout.config immuable en DB sans wp_context — relancer un nouveau bulk job pour bénéficier du fix.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.<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.<img> minimum, format explicite avec wrap <a href="PRODUCT_URL"> pour cumuler valeur SEO.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 ».data-hint-url sur le bouton (extensible plus tard pour une UI dédiée).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.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.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.attempts et error pour audit.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 :invalid_api_key → « API key invalid or expired. Reconfigure in Settings. »insufficient_credits → « Plan ran out of credits. Upgrade in Settings. »rate_limited → « Too many requests. Try again in a minute. »overloaded → « Server overloaded. Try again in 30 seconds. »'Serveur surchargé · réessayez dans 30s', anglais durci 'No URLs selected', 'Pinged', 'Re-checked', etc.).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.sessionStorage côté navigateur)./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.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._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.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.gsc_oauth_states accumulait une ligne par démarrage de flow OAuth, même ceux abandonnés en route.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.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.BulkOptimizationJob + BulkOptimizationItem. Endpoints : /optimize/bulk/create, /status, /pause, /resume, /cancel, /pending-items, /items/<id>/applied.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.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).<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.<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> :site_url + chemin.# → le <a> est unwrappé (le texte reste, le lien disparaît).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.www.).getimagesize).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.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.keyword, volume, kd, intent, cpc), dict produit (keyword, product_url, product_image_url, product_price).<p>, <h2>, <ul>, <table> distincts (plus de gros bloc HTML brut difficile à éditer). Identique à la génération individuelle.8Sinn eXtraThin HDMI – Cable au lieu de – Cable). Décodage via html_entity_decode partout : sélecteur produits bulk, sélecteur catégories, contexte envoyé au worker.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.🤖 Génération IA : 60% (3/5) · 📝 Publication WP : 40% (2/5) sur une seule ligne.⚡ En cours : "comment choisir un tracteur" — l'utilisateur sait exactement où en est le générateur.completed_count, total_count, generated_count, published_count) avec alias rétro-compat. Plus de « undefined / undefined ».awaiting_wp_publish mais le plugin ne pullait jamais les articles.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.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).toLocaleDateString + heure HH:MM) au lieu de l'ISO brut.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.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.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._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.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._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×.array_intersect (O(n×m)) with array_flip + isset() lookup (O(n)). 5× cumulated gain._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.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.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._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.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."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).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."automesh_cancel, automesh_confirm_cancel, automesh_cancelled (i18n).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.ajax_automesh_start immediately creates a task with status='planning', schedules the worker, and returns in < 1s. No more "Timed out" on click.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.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.automesh_planning, automesh_planning_hint, automesh_failed (i18n).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).ajax_automesh_start reuses it instead of recomputing. One-shot cache (consumed on use).$.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).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.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.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.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 it crashes — instead of a truncated 200 response.ajax_automesh_status now detects when a task has been pending/running for 20s+ without any tick. When stalled, it (a) re-schedules the WP-Cron event and (b) executes ONE batch synchronously in the status request itself — using the user's browser as the cron engine. The polling continues to drive progress. No more silent stalls.total = 0/undefined/NaN by showing … until the first status poll. Stall detection client-side after 60s shows a hint to the user. AJAX timeout extended to 120s for large-site planning.total (was missing before, which caused the same "undefined" bug on tab reload).Clicks: 0 / Impressions: 0 even after a successful sync. The auto-sync trigger logic was buggy.!hasIndex && !hasKpis evaluated to false and the sync never ran — leaving the analytics KPIs forever at 0 even though the database had no analytics rows.!hasKpis). Indexation status is independent of analytics rows — they come from different backend tables and pipelines, so we can't infer one from the other. Now any user with empty analytics gets an automatic sync on first page load.url_to_postid(), which silently fails for the WooCommerce Shop page (/boutique/, /shop/, etc.), the front page (/), and pages with custom permalinks.resolve_url_to_local() with cascading fallbacks: native url_to_postid → front page → blog page → WooCommerce Shop/Cart/Checkout/My-Account/Terms via wc_get_page_id() → get_page_by_path() for slug lookup → /product-category/<slug>/, /category/<slug>/, /tag/<slug>/ for terms. Now correctly resolves /boutique/ → Shop page ID, / → front page ID, etc.set_canonical, set_canonical_bulk, and resolve_url_to_entity (Indexation tab). The cannibalization "Set canonical" button now works for the homepage and the Shop page, which are the two most common cases of cannibalization on a WooCommerce store.essiow_gsc_overview_cache_28, _90, etc.), but the invalidation only deleted the unsuffixed key (essiow_gsc_overview_cache). On reconnect, the first overview fetch returned KPIs at 0 (sync not finished yet) and got cached for 1 hour — every subsequent fetch served the empty cache, never updating despite the data arriving in the database.invalidate_overview_cache() helper that deletes ALL period-suffixed cache keys (_1, _7, _28, _90, _180, _365, _480). Called by all 4 places that previously did the broken single-key delete: ajax_disconnect, ajax_sync, ajax_select_property, cron_nightly_sync.loadScOverview() now accepts a force flag that bypasses the PHP cache. Used after the auto-sync triggered when KPIs are detected as empty, and after the manual "Refresh stats now" button — guarantees the post-sync fetch always sees fresh data, never the old cached zeros.essiow.current_page === 'essiow-search-console'. On Products / Categories pages, the cooldown is silently applied without the toast.quota_cooldown: true, cooldown_until: ts). The JS applies it without waiting for a page reload, so any subsequent inspection batch in the same session is short-circuited.<a> wrapping is removed from the source page (anchor text preserved).essiow_il_unlink_pair (action). Removes <a href="target">…</a> wrappers from a source page's content, keeping the anchor text intact. Direction can be a_to_b, b_to_a or both. Used by the click-edge-to-delete flow.edges array [{from, to}, …] (was previously dropped before caching). Allows the frontend to render the full mesh._inject_at_smart_position returned changed:false (anchor not found in source content, anchor inside an existing <a>, anchor inside an HTML tag), the link was abandoned silently — meaning a planned rescue could fail without any retry. Now we fall back to the "Voir aussi" inline injection mode, which is guaranteed to succeed. Every planned target now becomes a real link.will_receive_incoming / will_emit_outgoing sets.semantic_append was being recorded as a phantom 4th key in the anchor type ratios, breaking the 10/35/55 diversity calculation. Now mapped to semantic for registry purposes._pick_rescue_source) inconsistent with PASSE 0's chain. Both now use the same multi-source chain with a relaxed cap (+2) — orphans abandoned in PASSE 0 get a second chance via the same logic, not a different one.per_page entry was created with the rescue targets — then the main loop re-assigned $per_page[$sig] = [...] with its own MUST/SHOULD candidates, erasing every rescue. This explained why 1.1.49 only resolved -9 orphans instead of -300+. Now the main loop merges with existing rescue targets, respects remaining quota slots, and skips already-rescued sigs to avoid duplicates.essiow_il_automesh_last_backups option, so the Revert button still works months after._essiow_il_backup_<hash>) and one-click Revert button at the end + permanent option to revert the last run from the modal.wp_localize_script strings (s.idx_*).localizeCoverage().\n artifacts in essiow-fr_FR.po. 783 entries, 50.7 KB compiled .mo, zero #-#-#-#-# marker, zero msgfmt error.msguniq left 43 conflict markers #-#-#-#-# essiow-fr_FR.po (Essiow) #-#-#-#-# directly inside msgstr strings of the French .po file. WordPress was rendering them in the UI ("Credits disponibles #-#-#-#-#... Crédits disponibles", "Non classée #-#-#-#-# Non classé", "Générer l'article #-#-#-#-# Générer un article", etc). Each conflict resolved automatically by picking the longest non-empty alternative, then leading/trailing \n markers stripped, then comments removed. Result : zero #-#-#-#-# in the .po, .mo recompiles clean (no warnings, no fatal errors), 648 valid translations.__() translations on the way out. "Crawled but not indexed", "Discovered but not crawled", "Excluded by Google", "Indexation errors" + their descriptions and suggestions all appear in the WP locale.__() calls, so they stayed in English even with French locale. Both now translatable.#, fuzzy entries by default — the FAQ answers ("Yes. The Pause...", "Yes. Since 1.1.30...") and several other long strings were never displayed in French. Removed all fuzzy flags.load_plugin_textdomain() was never called. The .po/.mo files in /languages/ were ignored by WordPress entirely — that's why the plugin stayed in English even when the WordPress locale was French. Now properly loaded on plugins_loaded so every translated string in the plugin (Search Console tabs, Help guide, Indexation tab, Modules cards, Cannibalization actions, FAQ, etc) appears in the WP locale.essiow-fr_FR.po, 70 KB compiled .mo.$(document) event delegation instead of $canvas references → survives canvas DOM rebuilds.ilGraphBindHandlers() is now idempotent (ilHandlersBound flag) and called once at first render.getBoundingClientRect() on every drag — the rectangle was stale after re-render.setTimeout rebinds on tab-switch.wpdb directly, with accent normalization (remove_accents()) and FR/EN/ES stopword filter. Each match now displays N tokens match + a one-click ✨ Optimize button targeting the original GSC query.POST /license/rotate-api-key — old key invalidated immediately on the server, new key auto-saved in essiow_api_key. Use case : suspected leak, or routine rotation hygiene.languages/essiow-fr_FR.po lines 380 and 397). Visible everywhere the Restore button appeared in French.audit_seo_factors() and audit_category_factors() now static — the search-console resolver was instantiating Essiow_Products / Essiow_Categories per row, re-registering all action hooks each call. Performance regression fixed./categorie-produit/) now correctly resolve to terms when setting canonical (the legacy code only matched /product-category/).essiow_inspect_urls AJAX action name corrected to essiow_gsc_inspect_urls — the auto-batch URL Inspection on Products and Categories pages was failing silently for every user. Now correctly fires and populates the indexation badges in real time.audit_seo_factors and audit_category_factors are now static — they were being called from Essiow_Search_Console::resolve_url_to_entity() which instantiated Essiow_Products/Essiow_Categories per row, re-registering all action hooks each time (memory + duplicate-handler bug).❓ Help button appears in the top-right corner of each header, one click away from the dedicated guide.CREDITS_PER_ALT_TEXT = 2 (was implicitly billed as a full product = 1 — the Vision call cost is now accurately billed)./credits/balance returns the cost map so the plugin can display realistic prices everywhere without round-tripping the config.CreditService._costs() centralized — single source of truth for all credit costs across normal billing, Shopify usage billing, and balance display. Removes 3 duplicated cost dicts.--indeterminate CSS animation that ignored width updates, and the JS was looking for a non-existent #essiow-blog-progress-text. Both fixed — 0% → 100% now ticks visibly with status label and elapsed time.– → –, & → &, etc) — applied via html_entity_decode() server-side before caching the graph.previous_clicks, current_clicks, impressions, potential_clicks for each lost-ranking page so the user sees the actual revenue impact, not just an abstract delta.?essiow_keyword=…)+ / − / ⟲ Reset view<a> in both contents (or appends a "Voir aussi" paragraph if none match).% pages with ≥1 incoming (40 pts), % pages with ≥1 outgoing (30 pts), hub ratio 5-20% (10 pts), avg in / out ≥ 2 (20 pts).⬅ in / out ➡ mini-stats inline.runInlineOptimize panel persists state across optimizations — start 5 in a row, watch them complete one by one, dismiss each individually.essiow_resolved_cannibal option) — the next opportunities refresh excludes it automatically. Combined with the live row removal, the user never sees the same resolved pair twice.#tab-X IDs while the view used essiow-tab-il-X. Tabs are now generic — data-tab-prefix on the nav drives the lookup, supporting both Search Console (essiow-sc-) and Internal Links (essiow-tab-il-) conventions.undefined: <date> string on Internal Links → added the missing last_scan translation key + 60+ other translatable strings used by the new modules.resp.data.kpis instead of resp.data.data.kpis (Flask payload is wrapped). All four KPIs (clicks/impressions/CTR/avg pos) and all four opportunity counters now render correctly.Indexing requested state — when a URL is later detected as indexed by GSC URL Inspection, it's automatically removed from essiow_indexing_requests and the post_meta _essiow_indexing_requested_at is cleared. The 🚀 button stops being green-checked and becomes available again.✨ Optimize button that routes via the smart re-optimize handler (so the AI re-runs the full optimization on the matching product/category, addressing every missing factor from the SEO audit). The legacy "Inspect ↗" button is now a 🔎 icon next to Ping/Request._essiow_article_keywords post_meta (exact match + fuzzy substring match for keyword stems ≥ 5 chars). New opportunities surface as you publish articles./categorie-produit/ (French permastruct) now correctly resolves to product_cat terms when setting canonical or auto-clearing indexing state, in addition to the English /product-category/._essiow_canonical_url on the secondary post/term and emits a custom <link rel="canonical"> that overrides Yoast/Rank Math. Dismissed pairs are filtered out of future opportunity lists.essiow_resolve_url, then triggers the actual product/category optimization inline (no more dead-end redirect to an empty Blog form). The GSC query is passed as the focus keyword so the AI targets it directly. For articles, the editor opens with the right context./wp-ajax/essiow_resolve_url) returns {type, id, name, edit_url} for any frontend URL — used by the Re-optimize and Cannibalization actions.max(0, impressions × (target_ctr − current_ctr)) — accurately reflects the realistic gain after a successful re-optimization.clicks×10 + impressions score (revenue-weighted), not just the most-impressed.gsc_connected exposed to JS via wp_localize_script for clean conditional logic across all UI.build_article_prompt() now accepts gsc_context. The Flask backend matches your typed keywords against your real GSC queries (last 28 days) and injects top10 / striking / beyond buckets into the prompt so the AI writes the article around the actual searches your audience makes.essiow_indexing_requests option and re-applied on every page load. No more confusion about which URLs you already submitted._essiow_index_status post_meta/term_meta, and updates the badge in place (Indexed / Crawled-not-indexed / Discovered / Excluded).essiow_last_errors storage. Plugin now logs only to PHP error_log() when WP_DEBUG is on.gpt-4o-mini Vision (1 credit per image)._essiow_optimized set unless force=1. No more re-spending credits on the same product twice./gsc/opportunities, /gsc/page-stats, /gsc/issues and prompt service build_gsc_section() injection helper.[⏸ Pause] button appears next to Cancel while running, switches to [▶ Resume] once paused.navigator.sendBeacon(). Items that had already been optimized stay saved in the database; the pending queue is dropped. The user must restart from scratch if they want to continue — by design.essiow_bulk_*_pause, essiow_bulk_*_resume) for both products and categories.nopriv cancel endpoint for the page-leave beacon (cookies aren't always sent on unload, so we use task_id ownership check via the active option instead of nonce).admin-ajax.php no longer leave Search Console KPIs at 0 forever. The data sync into Flask was already working (1561 rows committed in DB) but the follow-up essiow_gsc_overview call would 503 under Wordfence/Cloudflare load and the JS would just give up — leaving the user looking at 0 clicks / 0 impressions despite having real GSC traffic.postWithRetry() JS helper with exponential backoff (1s → 2s → 4s → 8s) on 503 / 504 / 429 / network errors. Used by both the auto-load and the manual "Refresh stats now" button.loadScOverview() — the page now makes ONE overview call on load instead of two (was: load + check-if-empty as separate calls). Less admin-ajax noise = less Wordfence rate-limiting.loadScPerformance() JS function populates it with the top queries + a dedicated "Striking distance" panel highlighting positions 11-20 (easy SEO wins)./gsc/properties, /gsc/select-property) remain available for any future use.sc-domain: first, then URL-prefix, www, http) when the saved property URL returns 404 from the Search Analytics API. The first variant that responds 200 is silently saved as the new property — the user no longer has to pick the right one manually.siteUnverifiedUser properties (which always 404 on Search Analytics), shows the permission level next to each entry (Domain · Owner, URL prefix · FullUser), and sorts Domain properties first then by permission strength.resp.data.data (double-nested) which caused the toast to always show "0 rows · 0 URLs" even on a successful sync.sc-domain: vs https://, with/without www, http vs https). The auto-pick now matches sc-domain first, then URL prefix variants (https/http × with/without www), then substring fallback. If nothing matches, the plugin asks the user to pick the right property manually.GET /api/v1/gsc/properties and POST /api/v1/gsc/select-property powering the manual selector. The select endpoint security-checks that the property belongs to the connected user before saving.auth_url at the response root but the plugin read it from the data envelope. Standardized: every Search Console endpoint now returns {success, data: {...}}. Connecting Search Console now actually works.cryptography was missing from requirements.txt. AES-256-GCM token encryption would have crashed the GSC service at runtime. Added.app.config['SECRET_KEY'] if neither ESSIOW_GSC_KEY nor SECRET_KEY env vars are set. Avoids hard-crash when the operator forgets the dedicated key._essiow_index_status is updated whenever URL Inspection runs and the URL matches a /product-category/<slug>/ permalink.is_connected() early-return guards on ajax_overview, ajax_indexation, ajax_inspect_urls so users who haven't connected GSC yet see a clean "Search Console is not connected" message instead of an opaque API error.connecting, refresh, recheck, submit_sitemap, index_*, striking_distance, no_data_yet — buttons stay in the user's language across the Search Console workflow.Indexed, Crawled not indexed, Discovered, Excluded, Error, Unknown), last crawl date, and a "Re-check selected" bulk action that triggers the URL Inspection API on demand.Indexation column in the Products list. Every product row shows a colored badge for its real Google indexation status — green for indexed, orange for discovered/crawled-not-indexed, red for excluded, grey for unchecked./<key>.txt on the site root.app.tasks.gsc_nightly_sync pulls Search Analytics + URL Inspection rotation for all connected sites.location.reload() after a successful single optimize was killing every other concurrent in-flight task in the same tab — running 5 manual optimizations in parallel meant 4 of them were aborted by the first one's reload. The row's badge / score / status are now updated in place, exactly like single product optimize already does.running does the toast appear and polling start. If the task is gone (done, cancelled, transient expired), the resume is silently skipped.essiow_active_bulk_* options when the bulk status endpoint sees the underlying task is missing or no longer running. Stale lock options can no longer survive a session — the next page load won't even probe.not found / expired responses before giving up — covers brief object-cache eviction or FPM worker recycling without the user seeing the row turn red mid-flight.max_execution_time (typically 30-60 s on shared hosts) mid-loop — leaving the current item stuck in processing and the queue frozen. Each call now processes EXACTLY ONE item then re-spawns itself via WP-Cron + spawn_cron() for the next. Workers stay well under any max_execution_time value.failed with the exception message and the queue continues. Previously, an exception would kill the worker mid-state-update, leaving items stuck in processing until the 90 s recovery kicked in.unset of the AI response payload + gc_collect_cycles()). On hosts with low memory_limit, this keeps long bulks well below the cap.pending (instead of 30 s) so a non-FPM host where neither cron nor fastcgi_finish_request fires sees processing start within the first poll cycle.processing. After 90 s without a heartbeat, the next worker pass picks it up.removeClass on row badges so visual state can't accumulate stale classes when cycling done → re-run → failed.generate, cancelled_summary) — buttons now stay in the user's language after a generation completes.Queued → In progress… (with a spinner) → Optimized (with new SEO score) — exactly like single-item optimization. The "Optimize" button on each row is automatically disabled while processing.Cancel button on bulk operations. The worker checks the cancellation flag between items so cancelling a 50-item bulk halts after the current item finishes (within ~30 s) instead of running to completion in the background.Failed badge).filemtime()-based cache buster on top of the plugin version. Hard-refreshes the browser/CDN cache the moment the JS file changes, even when an update was pushed without bumping the user-visible version. Fixes "user is on the new plugin but the browser still calls the old AJAX endpoints".essiow_optimize_product, essiow_optimize_category, bulk processors) now also call session_write_close() and raise the PHP time limit before doing any work. Mitigates 504 timeouts on shared hosts where Wordfence WAF or similar serializes admin-ajax requests through a session lock.start → poll → result pattern as blog articles), so single-product and single-category optimizations no longer fail with a 504 Gateway Timeout on hosts behind Cloudflare. The legacy synchronous essiow_optimize_product / essiow_optimize_category endpoints are kept for backward compatibility (used by the bulk optimizer that already handles its own retries).runEssiowAsyncTask() helper in admin.js — same network resilience for products/categories as for articles (30 s XHR timeout, 5 consecutive 504s tolerated, 8 s polling interval).session_write_close() at the very top, so concurrent polls are never blocked behind a worker that holds the session lock.fastcgi_finish_request() when available + (2) WP-Cron with spawn_cron() triggered immediately + (3) a poll-driven fallback in check_status that runs the task itself if it has been pending more than 30 seconds.run_task_inline inside a shutdown action, which keeps the FPM connection open until the AI call completes (10–30 s) — proxies that buffer the entire response would time out at 60 s. The new flow sends the JSON response, calls fastcgi_finish_request(), THEN processes the task in background. The browser sees a sub-second response.fastcgi_finish_request) now uses a 0.5 s timeout instead of 1 s — fewer cases of the parent request hanging while it waits for the loopback to dispatch.cat_links was ignored by the server, which expects category_links). Articles now contain real internal links to your store categories instead of invented or missing ones.blog_posts key — the AI can finally weave links to your real articles into the long category description.BlogPosting schema is now emitted on the front-end of every Essiow-generated article (gated by the existing schema option). Includes headline, description, datePublished, dateModified, author, publisher, image, keywords. Helps rich snippet eligibility for users not running Yoast/Rank Math.<!-- wp:... --> annotation before wp_insert_post.admin-ajax.php ERR_TIMED_OUT on shared / mod_security hosts. Background dispatch now uses a 3-tier strategy: fastcgi_finish_request() (instant, PHP-FPM hosts), HTTP loopback (existing), and WP-Cron fallback (always works). The first available wins; redundant workers are idempotent (transient state guards against double-processing).