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