From 1dfe0e9018092220c59cf939fb225e3c27454aba Mon Sep 17 00:00:00 2001 From: JingWen Fan <106414602+study8677@users.noreply.github.com> Date: Thu, 21 May 2026 17:32:06 +0800 Subject: [PATCH] fix: opt React app shell out of Chrome Translate (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chrome/Google Translate replaces text nodes inside the React-managed DOM, which breaks React's reconciliation and surfaces as `NotFoundError: Failed to execute 'insertBefore' on 'Node'` followed by `#root` being emptied. Users with Chrome Translate active reported `/projects/:id` rendering as a blank white page while `/console` continued to work in the same session. Mitigation: - Mark `` and `#root` with `translate="no"` + `notranslate` so the React subtree is never mutated by Translate. Add a `` hint as well. - Keep the static SEO copy in `
` translatable by opting it back in with `translate="yes"` — it is outside `#root`, so it cannot break React anyway, and we still want crawlers / translated previews to see it. Hardening: - Default `latest.serp` to `[]` in ProjectCommandCenter so a partial / legacy project-summary payload cannot crash the project detail page. Regression test (tests/test_frontend_translate_optout.py) asserts the opt-out markers are present in the shipped `frontend/index.html`. --- frontend/index.html | 15 +++- .../project/ProjectCommandCenter.tsx | 7 +- tests/test_frontend_translate_optout.py | 74 +++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 tests/test_frontend_translate_optout.py diff --git a/frontend/index.html b/frontend/index.html index 59bfa30..c6072ec 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,6 +13,15 @@ content="OpenCMO, AI CMO, SEO audit, GEO audit, AI search visibility, community visibility, open-source growth tools, self-hosted growth console" /> + + @@ -62,8 +71,8 @@ } - -
+ +

aidCMO

OpenCMO free trial: enter one URL and open your own overseas growth console.

@@ -99,7 +108,7 @@

OpenCMO — AI CMO growth console

-
+
diff --git a/frontend/src/components/project/ProjectCommandCenter.tsx b/frontend/src/components/project/ProjectCommandCenter.tsx index 4a24558..0f5080c 100644 --- a/frontend/src/components/project/ProjectCommandCenter.tsx +++ b/frontend/src/components/project/ProjectCommandCenter.tsx @@ -132,7 +132,10 @@ function AgentCard({ } function getLatestSerpTimestamp(latest: LatestScans) { - const timestamps = latest.serp + // Tolerate older/partial payloads where `serp` is missing — without this + // fallback the optional-chained `.map` chain blows up and surfaces as a + // blank project page. + const timestamps = (latest.serp ?? []) .map((snapshot) => snapshot.checked_at) .filter((value): value is string => Boolean(value)) .map((value) => utcDate(value).getTime()); @@ -167,7 +170,7 @@ function hasAnyScanData(latest: LatestScans, latestMonitoring?: MonitoringSummar latest.seo || latest.geo || latest.community || - latest.serp.length > 0 || + (latest.serp?.length ?? 0) > 0 || latestMonitoring, ); } diff --git a/tests/test_frontend_translate_optout.py b/tests/test_frontend_translate_optout.py new file mode 100644 index 0000000..8c6b79b --- /dev/null +++ b/tests/test_frontend_translate_optout.py @@ -0,0 +1,74 @@ +"""Regression tests for issue #26: Chrome Translate blanks the SPA. + +Chrome/Google Translate replaces text nodes inside the React-managed DOM, +which breaks reconciliation and surfaces as +`NotFoundError: Failed to execute 'insertBefore' on 'Node'` followed by +`#root` being emptied. We mitigate this by marking the body and `#root` +with `translate="no"` / `notranslate`, plus a `` hint. The static SEO copy in +`
` opts back in via `translate="yes"` so +crawlers still see it. + +These tests guard those markers so a future edit to `index.html` cannot +silently regress the fix. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +_INDEX_HTML = Path("frontend/index.html") + + +def _read_index_html() -> str: + text = _INDEX_HTML.read_text(encoding="utf-8") + assert text, "frontend/index.html should not be empty" + return text + + +def _strip_html_comments(text: str) -> str: + """Drop HTML comments so the example markup inside our own opt-out comment + block doesn't accidentally satisfy / shadow the real tag regexes. + """ + return re.sub(r"", "", text, flags=re.DOTALL) + + +def test_index_html_has_google_notranslate_meta() -> None: + text = _read_index_html() + assert re.search( + r'', + text, + ), 'expected opt-out hint' + + +def test_body_opts_out_of_translation() -> None: + text = _strip_html_comments(_read_index_html()) + body_match = re.search(r"]*)?>", text) + assert body_match, "frontend/index.html should declare a tag" + attrs = body_match.group(1) or "" + assert 'translate="no"' in attrs, " must carry translate=\"no\"" + assert "notranslate" in attrs, " class list must include 'notranslate'" + + +def test_react_root_opts_out_of_translation() -> None: + text = _strip_html_comments(_read_index_html()) + root_match = re.search(r']*)?>', text) + assert root_match, "frontend/index.html should declare
for React" + attrs = root_match.group(1) or "" + assert 'translate="no"' in attrs, "#root must carry translate=\"no\"" + assert "notranslate" in attrs, "#root must include the 'notranslate' class" + + +def test_static_seo_copy_remains_translatable() -> None: + """SEO/no-JS fallback copy lives outside #root; it should stay translatable + so crawlers and translated previews can still read it. + """ + text = _strip_html_comments(_read_index_html()) + main_match = re.search(r']*)?>', text) + assert main_match, "static SEO copy
should be present" + attrs = main_match.group(1) or "" + assert 'translate="yes"' in attrs, ( + "static SEO copy must explicitly opt back into translation, " + "otherwise the body-level translate=\"no\" would suppress it too" + )