Skip to content

Fix public site deindexing by Google: SSR CV content and move preview banner to JS#163

Merged
vincentmakes merged 1 commit into
mainfrom
claude/fix-google-indexing-XrWaM
May 5, 2026
Merged

Fix public site deindexing by Google: SSR CV content and move preview banner to JS#163
vincentmakes merged 1 commit into
mainfrom
claude/fix-google-indexing-XrWaM

Conversation

@vincentmakes
Copy link
Copy Markdown
Owner

Description

Fixes a critical SEO issue where the public CV site was being deindexed by Google after the multi-language rollout. Two problems combined into a strong "don't index" signal:

  1. Preview banner in raw HTML: The admin "Preview Mode — This is a saved version and not publicly accessible" banner was hardcoded into public-readonly/index.html and only hidden via CSS. Search crawlers read hidden text, so every public page advertised itself as not publicly accessible — a textbook soft-404 signal that caused Google to drop the URL.

  2. Thin content on public homepage: The public homepage was a near-empty shell server-side (<h1>Loading…</h1> with empty section containers). All CV content was hydrated client-side, but Googlebot frequently skips the second render pass on low-authority subdomains and treated the page as thin content.

Solution:

  • Moved preview banner DOM generation to JavaScript — it's now only inserted when window.DATASET_PREVIEW is true (admin preview context), so the banner text never appears in HTML served to public visitors
  • Implemented server-side rendering (SSR) of CV content: servePublicIndex() and serveDatasetPage() now render profile, about, experience, certifications, education, skills, and projects sections into the HTML before sending
  • Client-side JS still hydrates the same nodes on load, so user behavior is unchanged while crawlers see real CV content on first byte
  • Set <html lang> attribute correctly from database settings on the live homepage
  • Added HTML escaping utility and proper i18n keys for preview banner text

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation (changes to docs only — no version bump needed)
  • Translation (new or updated language files)
  • Refactoring (no functional changes)

Checklist

Required for all code changes

  • I have tested my changes locally (npm test passes)
  • Version has been bumped in all 3 files (package.json, package-lock.json, version.json)
  • CHANGELOG.md has been updated with a new entry under the correct version

If adding or changing user-visible strings

  • No hardcoded English — all strings use t('key') in JS or data-i18n in HTML
  • New i18n keys added to en.json and all 7 other locale files (de, fr, nl, es, it, pt, zh)
  • escapeHtml() used for any user-provided content rendered as HTML

If documentation-only change

  • No version bump included (docs changes must not bump the version)

Test Plan

  • Verify public homepage renders CV content in raw HTML (view page source)
  • Verify preview banner only appears in admin preview mode (?preview=...), not on public pages
  • Verify <html lang> attribute matches database language setting on live homepage
  • Verify client-side hydration still works (page remains interactive after load)
  • Verify all 8 language files have new i18n keys for preview banner

https://claude.ai/code/session_012QpBLqtiKoHg8mVfqXLwDj

The multi-language rollout combined two problems into a strong "don't index"
signal that caused Google to drop the public site from its index:

1. The admin "Preview Mode — This is a saved version and not publicly
   accessible" banner was hardcoded in public-readonly/index.html and only
   hidden via CSS. Crawlers read hidden text, so every public page advertised
   itself as not publicly accessible — a textbook soft-404 signal.

2. servePublicIndex returned an empty shell with `<h1>Loading…</h1>` and empty
   section containers; all CV content was hydrated client-side. Googlebot
   often skips the JS render pass on low-authority subdomains and treated the
   page as thin content.

Changes:
- Move preview banner DOM out of the static HTML and inject it from JS only
  when window.DATASET_PREVIEW is true. Add preview.banner_text and
  preview.back_to_admin i18n keys to all 8 locales so the banner stays
  translated when shown.
- Server-render the profile, about, experience, certifications, education,
  skills and projects sections in servePublicIndex (default-dataset path,
  live-DB fallback path) and serveDatasetPage. The client JS still hydrates
  the same nodes, so behaviour is unchanged for users.
- Set `<html lang>` in the live-DB fallback path (it was already correct on
  the default-dataset path).

https://claude.ai/code/session_012QpBLqtiKoHg8mVfqXLwDj
@vincentmakes vincentmakes merged commit 721fdf7 into main May 5, 2026
3 checks passed
@vincentmakes vincentmakes deleted the claude/fix-google-indexing-XrWaM branch May 5, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants