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" + )