Iron Goo
Iron Goo results card: 134 bilvio.com URL surfaces rewritten in native voice across four languages, plus an AEO baseline.
Bilvio · bilvio.com

Native-Voice Recovery and AI-Search Infrastructure on bilvio.com: An End-to-End SEO + AEO Case Study

Published May 9, 2026
134
URLs rewritten
4
Languages
14
Days, full ship

Background

Bilvio is a Turkish B2B export-customer-acquisition platform serving Turkish exporters who sell into international markets. The platform's primary site at bilvio.com runs on multilingual WordPress (Polylang) with five active languages: Turkish (original), English, Russian, Simplified Chinese, and Arabic. The translated languages target Western European procurement managers, US importers, Russian-speaking CIS buyers, mainland Chinese B2B sourcing teams, and Arabic-speaking MENA exporters.

In May 2026 we ran a multi-week SEO and AEO engagement on bilvio.com. The brief: the translated content (EN, RU, ZH, AR) was technically correct but unmistakably translated, with DeepL artifacts, idioms that read literally, and metadata still in Turkish on pages that were supposed to be English. Inbound conversion from non-Turkish search was underperforming relative to traffic. The fix had to happen in place (no replatform, no schema migration, no DNS work) and could not touch the Turkish original content.

This case study walks through what we found, what we shipped, and what we learned. We are publishing the post-mortem because most of the lessons are general: about multilingual content recovery on WordPress, about AEO infrastructure on legacy stacks, and about the operational discipline that lets you ship 134 URLs of native-voice content rewrites across four languages plus a full AEO baseline inside two weeks.


Where we started

The legacy stack

bilvio.com runs on a fairly conventional Turkish corporate WordPress stack:

  • WordPress 6.x on nginx + PHP 7.4 FPM (we discovered the 7.4 detail mid-engagement, and it cost us a deploy; more on that below).
  • Polylang free version for multilingual routing. Default language (Turkish) has no URL prefix; non-default languages (EN, RU, ZH, AR) use /<lang>/.
  • Yoast SEO for on-page metadata and schema.
  • Elementor + the Hello Elementor parent theme for page composition.
  • Cloudflare in front of the origin for DNS, CDN, and edge TLS.

The platform side of bilvio (the actual customer-acquisition application) runs on a separate Python/Django/gunicorn service on the same server, mounted at a different path. The WordPress install is the marketing surface; the platform is the product. The two coexist but do not share a runtime, and the engagement scope was strict: WordPress install only, never touch the platform app.

What the multilingual surface actually looked like

The frozen pre-engagement snapshot (wget mirror of the public site, sitemap-bounded, served from a Cloudflare-proxied subdomain we control as the "before" reference) captured 391 sitemap URLs resolving to 381 unique HTML files across the five languages.

The translation quality across the non-Turkish languages followed a predictable pattern:

  • Turkish (TR): original, native, untouched. Out of scope for our work.
  • English (EN): mostly literal, often word-for-word from Turkish. Idioms came through verbatim. Headlines like "Home - Export Customer Finding" instead of "Home - Export Customer Acquisition". Service page H1s mixed Turkish phrases with English. Some <title> tags were still in Turkish on URLs at /en/.... Meta descriptions where they existed often read as transliterated Turkish.
  • Russian, Chinese, Arabic: DeepL or comparable machine-translation output. Technically correct grammar. No idiomatic register. No marketing voice. The "translated" tell that a procurement professional native to that language can spot in five seconds.

The structural surfaces (Yoast titles, meta descriptions, OpenGraph tags, alt text, slugs) were even worse than the body content because they had been edited least often.


The audit: six clusters of issues

A pre-engagement audit using Search Console data, a fresh full crawl, the frozen wget mirror at our control subdomain, and a per-language sample of pages surfaced six clusters of issues.

1. Yoast metadata still in Turkish on translated URLs

The single most visible problem. The English homepage <title> read "Home - İhracat Müşteri Bulma" (Turkish phrase). OG titles, alt text, meta descriptions across roughly 138 items × 4 Yoast fields (title, meta description, OG title, OG description) were either Turkish, transliterated Turkish, or missing entirely.

This is the kind of defect that costs CTR in the SERPs every single day and is invisible to anyone reviewing the site in a browser. It only surfaces when you read the head of a translated URL in a SERP snippet preview, or when an AI agent retrieves the URL and the first thing it reads is a Turkish title for an English page.

2. DeepL-shaped body content across RU, ZH, AR

Roughly 31 English posts and 7 service pages carried the issues described above. Roughly 33 Russian posts, 21 Chinese posts, and 21 Arabic posts had been run through DeepL or a comparable machine-translation tool. The body content was grammatically correct, structurally intact, and unmistakably machine-translated to anyone reading in those languages with commercial intent.

This is the silent killer for B2B export sites: procurement professionals are highly attuned to corporate translation quality, and a translated-feeling page reads as "this vendor does not actually operate in our market". The cost is not measurable in any single metric. It compounds across every visit by a serious buyer.

3. Polylang free-version slug collisions

A handful of legitimate cross-language content sharing patterns were broken by Polylang free's same-slug-per-language limitation. EN had blog-2 (Polylang's automatic disambiguator) instead of blog. Two posts had duplicate canonicals pointing at sibling posts in different languages. Several post titles were lower-case in places where they should have been title-case.

4. AEO infrastructure: absent

  • llms.txt: 404 across all five languages.
  • robots.txt: Yoast default, no explicit AI agent allowlist (which means in 2026 the major AI crawlers are blocked by implicit-default for the platform's most commercially interesting audience: international procurement teams who increasingly start research in ChatGPT, Perplexity, and Claude).
  • Organization schema with @inLanguage per language: not present. Yoast emits Article and WebPage per-page; it does not generate a per-language Organization node.
  • AI traffic logging: no infrastructure. We had no way to count which bots were already crawling and which had given up.
  • /raw endpoints: none. Token-efficient Markdown surfaces for AI agents did not exist.

Estimated audit-script posture score baseline: roughly 5-10 / 100 on our internal AEO scoring rubric.

5. Operational risk surface

The site shares a server with a separate Python/Django platform application. SSH access is gated through a credentials file that an unfamiliar contractor would not know existed. Any heavyweight intervention (theme edits, plugin installs, DB modifications) carries the risk of touching the platform app or breaking the slug-share workaround that was already in place. The engagement plan needed to be aggressively scoped to the WordPress install path only, with rollback-ready commits at every step.

6. No structural before/after capture

Most replatform engagements skip baseline capture and then have nothing to show the client at the 30-day or 60-day review. We treated baseline capture as a launch-blocking deliverable: a frozen wget mirror at a Cloudflare-proxied control subdomain, with defense-in-depth noindex at both per-page <meta> and X-Robots-Tag HTTP header so the snapshot itself never competes in the SERPs. The mirror is the durable "before" reference for the rest of the engagement and for the client review at the end.


Strategy

We made several deliberate strategic calls upfront. Each one shaped the rest of the engagement.

English is the canonical pivot for translations

Rather than translate from Turkish into each target language, we translated from English into Russian, Chinese, and Arabic, after first rewriting English to native-procurement-grade quality. The reasoning:

  • Our author capacity for native-grade output is highest in English across all four target audiences.
  • Translating EN → RU/ZH/AR yields more idiomatic output than TR → RU/ZH/AR (which would double-distance through Turkish comprehension at each step).
  • The Western procurement audience served by the English version is the highest-commercial-value cohort, so English deserves source-of-truth status.

Turkish stayed read-only. Read it for semantic ground-truth (especially where the current English translated an idiom literally), never edit it.

Per-language voice contracts

Each target-language pass adapted register and voice for that market, not as a literal English clone. We wrote per-language style guides covering:

  • Russian: formal commercial register; no transliterated Anglicisms where a native Russian B2B phrase exists; "Bilvio" stays Latin-script as the brand name.
  • Chinese (Mandarin Simplified): mainland business register; no Taiwanese or Hong Kong variants; standard B2B vocabulary for trade-platform features.
  • Arabic (MSA, Modern Standard Arabic): formal business register; right-to-left layout considerations preserved; dir="rtl" attributes added to package-table widgets where missing.
  • English: native procurement-grade voice; concrete-over-vague; preserve all facts, brand names, and HTML tags from source content.

The style guides also enforced a strict editorial standard (no em-dashes, none of the overused corporate filler like leverage, utilize, seamless, robust, comprehensive), with a final per-language proofing pass for the local equivalents.

Edits go through WP-CLI over SSH or mu-plugin file deploys

No direct DB writes. No WP admin browser editing for anything that could be batched. The methodology:

  • Yoast metadata fixes (title, meta description, OG): WP-CLI commands over SSH, batched per language, with per-post meta backups to /tmp/backup_meta_<id>_pre.json before any update. Roughly 552 metadata field updates in a single SSH session.
  • Body content (Elementor-managed): server-side PHP scripts that walk the _elementor_data JSON tree, locate the largest text-editor widget by widget_id, replace its content with the rewritten markup, and blank smaller redundant widgets. Re-save via update_post_meta.
  • Critical structural fixes (blog slug, canonicals, title casing): one-off wp eval invocations with backup/restore wrappers.
  • AEO infrastructure: single mu-plugin file at wp-content/mu-plugins/bilvio-aeo.php, removable in one rm command.

This approach has two material benefits. First, every change is recorded in a per-language .txt change log with before/after pairs (the client uses these for review). Second, the entire engagement state can be re-applied by a single shell script (restore-all.sh, ~3 minutes end-to-end) which is the engagement's durable rollforward artifact.

Don't touch what we cannot test

Three hard rules carried forward at every step:

  1. Don't touch Turkish content. TR is original, read-only.
  2. Don't touch the platform backend. The Python/Django app at /root/bilvioBackend/ is a separate concern.
  3. Mu-plugin must be removable in one command. rm wp-content/mu-plugins/bilvio-aeo.php reverts the entire AEO ship. No DB writes, no theme edits, no custom tables.

Execution

Yoast metadata phase: 552 field updates across 4 languages

The Yoast metadata phase ran first because it is the highest-impact intervention with the smallest blast radius. Every Yoast <title>, meta description, OG title, and OG description on every translated URL was rewritten and verified live through Cloudflare.

  • English: 39 of 41 items updated (excluding kvkk legal page + deneme test page).
  • Russian: 41 of 42 (excluding kvkk), EN-canonical-pivot translations.
  • Chinese: 29 of 30 (excluding kvkk), EN-canonical-pivot translations in Mandarin Simplified B2B register.
  • Arabic: 29 of 30 (excluding kvkk), EN-canonical-pivot translations in MSA B2B register.

Total: 138 items × 4 Yoast fields = 552 metadata field updates, all verified live with a curl matrix that fetched each URL through Cloudflare and grep'd the <title> and <meta name="description"> against the expected post-update strings. 100% match.

Per-post backups of pre-update Yoast meta saved to /tmp/bilvio_backups_<lang>_posts/ with a small restore.php to reverse any single update if the client objected to a specific framing.

Per-language change logs at notes/changes-{en,ru,zh,ar}.txt in the Turkish-labeled before/after format the client requested. Each entry: page URL, list of before/after pairs, numbered by page.

Structural fixes (one-time)

Bundled into the metadata phase because they touched the same infrastructure:

  • EN blog slug blog-2blog: Polylang free-version disambiguation artifact. Fixed by writing a small mu-plugin (bilvio-pll-slug-share.php) that handles Polylang's same-slug-per-language limitation cleanly. The plugin coexists with the AEO mu-plugin we later shipped.
  • Redirection plugin rule (/en/blog-2//en/blog/) for legacy URL preservation.
  • Duplicate canonicals fixed: post 7188 (EN, an unfortunate "what-is-a-potential-customer-2") canonicalized to its sibling 7184. Post 7189 (RU duplicate) canonicalized to 7185.
  • Post title casing: EN "academy" → "Academy", RU "академия" → "Академия", RU "блог" → "Блог". One-off wp post update per fix.
  • Pages flagged but not modified (client decision): post 7151 (empty body), posts 7188/7189 (canonical duplicates the client may want to merge or delete), KVKK pages across all languages (per client directive).

EN body content phase: 7 pages + 31 posts

The 7 EN pages (homepage, about-us, exports, contact, academy, export-marketing-system, research-china) were rewritten to native-procurement-grade English: 169 widget-level rewrites across the 7 pages, applied via a server-side script that walked _elementor_data and replaced the largest text-editor widget per page with the rewritten markup. Three widgets were deferred for client decision (homepage SEO keyword cloud with mixed-language tags, exports pricing table with TR/EN mix, research-china search form HTML).

The 31 EN posts were rewritten to a single standard: native B2B trade-analyst English, with every fact, brand name, and HTML tag preserved from the source content, and the same editorial standard (no em-dashes, no corporate filler) enforced on every line before it went live.

The application path:

  • 21 posts written into both _elementor_data (largest text-editor widget) and post_content (the WP body fallback).
  • 8 posts had corrupt _elementor_data JSON (Turkish unicode escape issues on the legacy import). Fell back to post_content only.
  • 1 post had empty _elementor_data. post_content only.
  • 1 post used a theme-post-content widget rather than Elementor. post_content only.

Restore wrapper updated with Step 4b for re-application. Idempotent.

RU + ZH + AR posts body phase: 75 posts across 3 languages

The same approach, scaled across three languages: 33 Russian, 21 Chinese, and 21 Arabic posts, each rewritten from the corrected English source into native commercial register for its market, and each held to the same editorial standard before going live.

Apply infrastructure: a language-parameterized PHP walker at notes/_apply-lang-posts-bodies.php that reads _apply-<lang>-posts-payload.json, locates the post by ID, walks _elementor_data, replaces the largest text-editor widget with new content, blanks redundant smaller widgets, and re-saves. Falls back to post_content-only for corrupt-Elementor posts.

Per-language Elementor coverage:

  • RU: 21 Elementor + post_content, 12 post_content only.
  • ZH: 20 Elementor + post_content, 1 post_content only.
  • AR: 20 Elementor + post_content, 1 post_content only.

The Elementor-walker bug worth flagging publicly. The first apply attempt found zero widgets across all 75 posts. The recursive walker assumed the top-level _elementor_data JSON was a single node. It is a list of sections at the top level. The fix was a one-line array_is_list() check that branches into a list-iteration path when the top-level is a list, and a single-node path otherwise. Re-applied successfully on the second pass.

RU + ZH + AR pages body phase: 21 pages (3 languages × 7 pages)

The page-body rewrite used a different pattern from the post-body rewrite because pages are more visually structured and have widget-level translations that need exact id-matching.

A walker script extracted the EN-rewritten and target-language-original Elementor JSON tree pairs by tree position, producing a per-language translation worksheet of roughly 185 widget-pair entries per language ({page, widget_id, field, en_text, target_text}). Each en_text was translated into native target voice, and an apply script located each widget by widget_id, set settings[field] = new_text, and re-saved. The tabs/accordion pattern (tabs[N].field) was handled with a small special case.

Coverage: all 558 widget translations (187 RU + 186 ZH + 185 AR) applied with zero misses. Live-verified on /ru/, /zh/, /ar/, /ru/контакты/, /zh/学院/ through Cloudflare.

Apply infrastructure: notes/_extract-page-text-pairs.py, notes/_apply-page-translations.py, notes/_apply-page-translations.php. All 21 rewritten Elementor JSONs saved to notes/_elementor-{ru,zh,ar}-{label}-rewritten.json as a durable artifact.

We also did some structural cleanups in flight: the ZH pages had Mozilla simple-translate extension residue plus ChatGPT-export data-start/data-end artifacts in a few widgets (the client had previously used those tools to evaluate translations and the residue had been committed by accident); we stripped them. The AR pages gained dir="rtl" on package-table widgets where it was missing.

AEO V1 phase: single mu-plugin, five surfaces

After the content quality phase landed, AEO infrastructure went on top.

Single mu-plugin at wp-content/mu-plugins/bilvio-aeo.php. 861 lines of PHP, PHP 7.4-compatible by construction, single file with no Composer dependency. Removable in one rm command.

Internal organization, all classes within Bilvio\AEO namespace, all in one file:

  • AiAgents: constant arrays for 12 AI agent UA regex patterns (GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, anthropic-ai, Claude-Web, PerplexityBot, Perplexity-User, Google-Extended, Applebot-Extended, cohere-ai, Bytespider) and 5 AI referrer hostnames (chatgpt.com, claude.ai, perplexity.ai, gemini.google.com, copilot.microsoft.com).
  • TrafficLogger: hooks init priority 1, matches request UA and referrer against AiAgents, writes a structured JSON line to PHP error_log via error_log("[BILVIO_AEO] " . json_encode($entry)). The PHP-FPM stderr passthrough lands these entries in /var/log/nginx/error.log on this server, which we discovered during deploy.
  • RobotsTxt: hooks robots_txt filter priority 20, appends 12-agent allowlist. (This hook turned out to be a fallback because of the static-file bypass we hit during deploy. More below.)
  • RewriteRules: hooks init priority 11 (after Polylang priority 10 has installed its rules), registers /llms.txt rewrite rules per language and an EN-only /<en>/<slug>/raw/? rewrite rule for posts and pages.
  • Router: hooks parse_request priority 5, dispatches matched query vars to the right handler, sends response, exits cleanly.
  • LlmsTxtHandler: generates per-language llms.txt content from the DB (org description in target language, top 10 recent posts, 7 service pages, available-formats note, refresh timestamp). Cached as a WP transient for 5 minutes. ?nocache=1 bypasses the cache for ad-hoc verification.
  • RawHandler: resolves slug to a published EN post or page (via pll_get_post for Polylang language disambiguation), walks _elementor_data to extract text-bearing widget content, runs the result through HtmlToMd, prepends frontmatter (title, source URL, language, as-of timestamp, token estimate), returns Markdown with correct headers.
  • HtmlToMd: custom 100-line DOMDocument-based HTML→Markdown converter. Handles headings (h1-h6), paragraphs, lists (ul/ol/li), links (a), images (img), strong/em, blockquote, code/pre, br, hr. Strips Elementor wrapper divs. No Composer dependency.
  • Schema: hooks wpseo_schema_graph (Yoast's filter), adds an Organization node with @inLanguage set to the BCP47 code matching pll_current_language() (tr-TR / en-US / ru-RU / zh-CN / ar-SA). Merges into Yoast's existing graph, does not replace.

Surface coverage after deploy:

  • /llms.txt at 5 URLs (root for TR, /en/llms.txt, /ru/llms.txt, /zh/llms.txt, /ar/llms.txt). Each returns 200 with dynamically generated Markdown content sized between 525 and 974 tokens depending on language byte-expansion.
  • 38 EN /raw endpoints (31 EN posts + 7 EN service pages). Each returns 200 with Content-Type: text/markdown; charset=utf-8, Cache-Control: public, max-age=3600, X-Data-As-Of: <ISO>, X-Robots-Tag: noindex, follow. The noindex, follow keeps /raw out of search engine results (search engines should index the HTML version) but lets agents follow links.
  • JSON-LD Organization schema on each language homepage with @inLanguage BCP47, merged into Yoast's existing schema graph without duplication. Validates clean on schema.org's validator.
  • AI traffic logging active. Test curls with ClaudeBot, GPTBot, and PerplexityBot user-agent strings produce structured JSON entries in /var/log/nginx/error.log tagged [BILVIO_AEO] for monthly review.
  • robots.txt 12-agent allowlist appended directly to the static file at /var/www/html/proje/robots.txt with # BILVIO_AEO / # END BILVIO_AEO marker brackets so the change is idempotent and reversible.

Posture jump captured at notes/aeo-audit.md:

SurfaceBaselinePost-deploy
/llms.txt × 5 langs404 across all200 + per-language content (525-974 tokens each)
robots.txt AI allowlistabsentpresent (after Cloudflare cache purge)
/raw endpoint404200, text/markdown, ~240 tokens (sample)
JSON-LD Organization @inLanguageabsentpresent on all 5 lang homepages
AI traffic loggingabsentactive (verified via test curls)

Internal AEO scoring rubric: baseline approximately 5-10 / 100; post-deploy approximately 70-80 / 100. The jump took roughly a day of focused work plus the deploy hardening described below.


Specific bugs and lessons worth documenting

A few issues we hit and fixed are worth flagging publicly because we see them on other multilingual WordPress sites all the time.

PHP version mismatch: a target-host lint is mandatory

The first deploy attempt of bilvio-aeo.php 500'd the entire site. The mu-plugin used PHP 8+ syntax (match expressions, str_ends_with(), array_is_list()) which PHP 7.4 rejects with a parse error. Local PHP-lint had passed because our default machine runs PHP 8.3.

Rolled back in roughly 3 minutes by rm'ing the mu-plugin. Rewrote in 7.4-compatible syntax (match to array-plus-isset lookups, str_ends_with to substr checks, array_is_list to a manual isset($node[0]) && is_array($node[0]) pattern). Re-deployed clean.

The lesson, now an enforced rule: lint against the target host's PHP binary before scp, every time. Specifically:

scp aeo.php client:/tmp/aeo-lint-test.php
ssh client "php7.4 -l /tmp/aeo-lint-test.php"

If the target lints clean, ship. If not, fix before scp. Skipping this step is what caused the site-wide 500.

Static robots.txt bypasses the WP filter

The mu-plugin's RobotsTxt::augment() filter was registered correctly, would have fired if reached, and produced the expected output in standalone testing. Public-facing curl of bilvio.com/robots.txt showed the file unchanged.

The cause: nginx serves /var/www/html/proje/robots.txt directly from disk via a location = /robots.txt { try_files $uri =404; } block. The WordPress robots_txt filter never fires for the public URL because WordPress is never invoked. Even if nginx routed the URL through PHP, Yoast's robots_txt filter at priority 99999 would have steamrolled our priority 20 augmentation.

Fix: append the AI allowlist directly to the static file at /var/www/html/proje/robots.txt with # BILVIO_AEO and # END BILVIO_AEO marker brackets. Idempotent, grep-able, reversible (sed -i '/# BILVIO_AEO/,/# END BILVIO_AEO/d' robots.txt).

The lesson: probe the actual serving path of /robots.txt before relying on WordPress filters to augment it. The in-PHP filter chain is a fallback. The static-file append is the reliable path.

Cloudflare's 4-hour robots.txt TTL

After the origin robots.txt change, public clients saw the stale version until Cloudflare's cache expired. Origin-direct curl (curl --resolve bilvio.com:443:<origin-ip> https://bilvio.com/robots.txt) showed the change immediately; public-facing curl through Cloudflare lagged for hours.

Fix: purge robots.txt (and all /llms.txt URLs across the five languages) from the Cloudflare dashboard immediately after origin changes. If you have a CF API token, a one-line curl against /zones/$ZONE_ID/purge_cache handles it without dashboard work.

The Elementor walker top-level-list bug

The first apply attempt of the RU/ZH/AR body rewrites found zero widgets across all 75 posts. The recursive walker we had built assumed the top-level _elementor_data JSON tree was a single node. It is a list of sections at the top level (each section contains columns, each column contains widgets, recursive).

Fix: a one-line array_is_list() check at the top of the recursive walker. If the input is a list, iterate and recurse into each element. If it is a single node, walk it directly. Re-applied successfully on the second pass with zero misses.

The lesson: the top of any Elementor JSON tree is a list, not a node. Defensive list-handling at the entry point of any Elementor walker.

Don't trust engagement memory about server stack

Bilvio's engagement intake had captured the server stack as "LiteSpeed Cache" (a common stack on shared Turkish hosting). Reality, probed at deploy time: nginx + PHP 7.4 FPM + Cloudflare. php -i | grep error_log returned empty (no explicit error_log directive in php.ini), which meant PHP-FPM stderr passthrough sends error_log() output to /var/log/nginx/error.log rather than a php-error.log we had assumed existed.

The lesson, now a pre-deploy gate: probe the server stack before writing platform-specific code. Specifically:

curl -sI "https://CLIENT/" | head -10
ssh client "ls /etc/nginx/sites-enabled/ /etc/apache2/sites-enabled/ 2>/dev/null"
ssh client "systemctl status nginx php-fpm apache2 litespeed 2>&1 | head -5"
ssh client "ls /var/run/php/*.sock"
ssh client "php -i | grep error_log"

The output tells you which web server, which PHP version, which FPM socket, and where logs land. Plan the deploy against the actual stack, not the intake-form stack.


Results and what we are tracking

We are deliberately not publishing specific traffic-recovery percentages in this post yet. The content quality and AEO ship landed in May 2026; we want a full Google reindexing cycle and at least 60 days of AI agent crawl before quoting numbers we can stand behind.

What we can say now, with confidence:

  • 134 unique URL surfaces with native-voice rewrites across English, Russian, Chinese, and Arabic, all live and Cloudflare-verified.
  • 552 Yoast metadata field updates across 138 items in 4 languages, all verified live through Cloudflare with 100% match against the expected post-update strings.
  • 558 page-body widget translations across the RU, ZH, and AR service pages, applied with zero widget-id misses.
  • AEO V1 surfaces complete: /llms.txt × 5 languages, 38 EN /raw endpoints, JSON-LD Organization with @inLanguage on all language homepages, 12-agent robots.txt allowlist, AI traffic logging active.
  • Posture jump from approximately 5-10 / 100 on our internal AEO scoring rubric to approximately 70-80 / 100.
  • Idempotent restore script at restore-all.sh that re-applies the entire engagement (Yoast meta + body content + AEO infrastructure) in approximately 3 minutes if any of it ever needs to be re-deployed.
  • Per-language change logs (notes/changes-{en,ru,zh,ar}.txt, 1,100+ lines each) in the Turkish-labeled before/after format the client requested.
  • No regressions on the Turkish original content. A 5-page spot-check post-deploy confirmed structural parity with the frozen pre-engagement snapshot at our control subdomain.

The +30 and +60 day AI-client query rerun is the metric that actually tells us whether the AEO bet pays off. We run three standard queries ("What does bilvio.com offer?", "How do I find international export buyers?", "Compare platforms for finding export customers") across Claude Desktop, ChatGPT, Perplexity, and Gemini at baseline, +7d, +30d, and +60d, and log results in notes/aeo-real-world-checks.md. The trajectory of citation rate and discovery rate across those checkpoints is the engagement's real KPI.


Lessons we would give to anyone doing a similar engagement

  1. Probe the server stack before writing platform-specific code. Don't trust intake-form claims. Linting against the wrong PHP version is what causes site-wide 500s on the first deploy. Probing takes 30 seconds; recovering from a bad deploy takes 30 minutes and burns client trust.
  2. Static robots.txt is the public-facing surface, regardless of what WordPress thinks. If nginx (or Apache, or LiteSpeed) serves /robots.txt from disk, your WP filter chain is a fallback, not the path. Append directly to the static file with marker brackets. Idempotent and reversible.
  3. English as the canonical translation pivot is the right call for B2B export sites. Translate EN to RU/ZH/AR rather than TR to RU/ZH/AR. The cost is one extra step on the English source; the benefit is markedly higher idiomatic quality across the target languages.
  4. Per-language voice contracts are non-optional. Formal Russian commercial register reads completely differently from formal English; mainland Mandarin business register is not Taiwanese business register; MSA Arabic is not colloquial Arabic. Commit the per-language style guides at engagement start, hold every language to them, and proof against them before publishing.
  5. The Elementor JSON top-level is a list, not a node. Build the walker defensively. The one-line array_is_list() check at the entry of any recursive Elementor walker prevents the most common debug-time waste on these engagements.
  6. A single-file removable-in-one-command mu-plugin is the correct AEO shape for WordPress engagements. No Composer. No theme edits. No DB writes. rm reverts everything. The tradeoff (no shared utility library across mu-plugins on different clients) is worth the safety.
  7. Capture baseline before you touch anything. A frozen wget mirror at a control subdomain, served with defense-in-depth noindex (both <meta> and X-Robots-Tag HTTP header), is the reference for the rest of the engagement and the only durable artifact you have at the 30 and 60 day review. Skip this and you have no story at the end.
  8. AI client query checks are the real KPI, not the audit-script posture score. Server-side green lights confirm the infrastructure is shipped. Citation rate at +30 and +60 days confirms the AEO bet is paying off. Don't conflate the two. Don't skip the +30 / +60 rerun because the audit looks green at +0.

Where the site is now

bilvio.com is live with 134 URLs of native-voice content rewrites across English, Russian, Simplified Chinese, and Arabic; 552 Yoast metadata field updates; a single removable mu-plugin delivering five AEO surfaces (llms.txt, robots.txt allowlist, /raw endpoints, JSON-LD Organization @inLanguage, AI traffic logging); and a fully idempotent restore script that re-applies the entire engagement in approximately three minutes.

The Turkish original content is untouched. The platform application is untouched. The mu-plugin is removable in one rm command.

If you are running a multilingual WordPress B2B site and would like to compare notes, our door is open.