🪟 Windows TippsMichael Linden wechselt zu Coda Audio Deutschland(15.09.2026 um 18:14 Uhr)
🤖 Android TippsMichael Linden wechselt zu Coda Audio Deutschland(15.09.2026 um 18:14 Uhr)
🕵️ SicherheitslückenCVE-2026-82431 | Apache Storm Nimbus authorization (CNNVD-2026-98874501)(15.09.2026 um 18:28 Uhr)
🪟 Windows TippsMichael Linden wechselt zu Coda Audio Deutschland(15.09.2026 um 18:14 Uhr)
🤖 Android TippsMichael Linden wechselt zu Coda Audio Deutschland(15.09.2026 um 18:14 Uhr)
🕵️ SicherheitslückenCVE-2026-82431 | Apache Storm Nimbus authorization (CNNVD-2026-98874501)(15.09.2026 um 18:28 Uhr)

🔧 Programmierung 🕛 vor 1 Monat 7 Min Lesezeit
0

The 4 static-mirror drift classes hiding in your Blazor WASM SEO (and the audit script that finds them)

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

I run a Blazor WebAssembly tax calculator site. To feed Google's crawler before the SPA hydrates, every @page route also has a hand-maintained static HTML mirror at wwwroot/<route>/index.html. That works — the crawler indexes real content, users get instant-paint before Blazor boots.



But this weekend I ran a grep audit across those 100 mirror files. It found 4 distinct drift classes, silently degrading SEO signals for months. If your Blazor WASM site uses the same pattern, you probably have some of these too.






The setup, if you don't already have it



The .razor file is the source of truth. Blazor renders it after hydration. The static HTML mirror is a snapshot of what the page should look like on first paint, for the crawler + for the ~1-2 seconds users wait for WASM to boot. In practice this means every content-page razor has a paired wwwroot/<same-path>/index.html, and every razor edit is supposed to also update the mirror.



Supposed to.



The problem: razor edits happen in normal development flow, mirror edits require you to remember. Over 6 months and ~40 pages, drift accumulates. And it's invisible to dotnet build because the mirror is just static HTML — the compiler doesn't check it.



Here's what my Saturday audit found.






Drift class #1 — missing BreadcrumbList JSON-LD



Discovery: grep -L '"BreadcrumbList"' wwwroot/blog/*/index.html returned 7 files.



BreadcrumbList JSON-LD is what makes Google render "Home › Blog › Article Title" as a visible breadcrumb in the search snippet. Without it in the mirror, the crawler sees no breadcrumb at first index. My <SeoSchema> component in each razor emits it correctly at hydration — but the crawler reads the mirror before hydration. That schema was invisible to Google for 7 blog posts.



Fix: 7 additive <script type="application/ld+json"> blocks injected before </head>, matching the exact 3-level pattern (Home → Blog → Article Title) that my razors emit at runtime. Pattern was copy-paste from an already-working mirror.






Drift class #2 — missing FAQPage JSON-LD despite razor having FaqItems



Discovery: grep -L '"FAQPage"' wwwroot/blog/*/index.html returned 16 files. But when I cross-checked which of those razors had a FaqItems=@(new List<SeoSchema.FaqItem> { ... }) param on the <SeoSchema> component, 6 of the 16 did.



Those 6 razors are actively emitting FAQPage schema at hydration. Their mirrors have zero FAQPage schema. Users get the schema after WASM boots; Google's initial crawl doesn't. FAQPage is one of Google's most-visible SERP features (the expandable Q&A dropdown), and I was leaving CTR on the floor across 6 blogs for months.



Fix: I wrote a Node script that regex-extracted the new("Question", "Answer") tuples from each razor's FaqItems= block, JSON.stringify()'d them into the SIMPLE FAQPage schema format (matching what my already-working mirrors used), and injected before </head>. 38 Q&A pairs baked in across 6 files. All validated via JSON.parse post-write.



The 10 razors without FaqItems= are a separate content-authoring problem — they need FAQ Q&A written from scratch + reviewer signoff on YMYL claims. Different scope, deferred.






Drift class #3 — asymmetric hreflang cluster



Discovery: my sitemap.xml declares 4 hreflang alternates for my old-vs-new-regime cluster:




CODE
<xhtml:link rel="alternate" hreflang="en-IN" href=".../blog/old-vs-new-regime/"/>
<xhtml:link rel="alternate" hreflang="hi-IN" href=".../blog/hi/old-vs-new-regime/"/>
<xhtml:link rel="alternate" hreflang="gu-IN" href=".../blog/gu/old-vs-new-regime/"/>
<xhtml:link rel="alternate" hreflang="x-default" href=".../blog/old-vs-new-regime/"/>






But the page HTML on 2 of the 3 language variants (English + Hindi mirrors) had only 3 <link rel="alternate"> tags — missing gu-IN. The Gujarati mirror correctly had all 4. This is an asymmetric hreflang cluster: the sitemap says GU exists, English + Hindi pages don't reference it back. Google's algorithm for hreflang requires bidirectional references to trust the cluster.



Impact: the Gujarati variant may lose SERP visibility in gu-IN searches (no reciprocal reference from English/Hindi means weaker language-cluster signal). Small volume for my site but a real signal loss.



Fix: 2 additive <link rel="alternate" hreflang="gu-IN"> tags. All 3 mirrors now show 4-alternate symmetric cluster.






Drift class #4 — meta keywords copy-paste template



Discovery: I noticed during the FAQPage injection that 5 blog mirrors shared identical <meta name="keywords"> content — the generic homepage keywords like "income tax calculator india fy 2026-27, free tax calculator india no signup..." — even though the blogs were about wildly different topics (NPS 80CCD(1B), PPF vs ELSS, RSU tax, home loan interest, Form 10E).



Cross-checked: the razors had topical keywords. The mirrors had the generic homepage boilerplate. Classic mirror-drift on a low-priority attribute nobody remembered to sync.



Google mostly ignores <meta name="keywords"> since 2009. But:





  • Bing still uses it (per Bing Webmaster docs)

  • Yandex uses it

  • Some AI content classifiers use it

  • Human quality-raters see it — 5 topically-different blog pages sharing identical keywords looks like template-duplicate spam



Fix: extracted each razor's keywords content via regex, wrote it back into the corresponding mirror. 5 files, 5 topical keyword strings replacing 5 identical-homepage-boilerplate strings.






The audit script



This is the one-liner that started the whole weekend:




CODE
# Which blog mirrors are missing BreadcrumbList?
grep -L '"BreadcrumbList"' wwwroot/blog/*/index.html

# Which are missing FAQPage?
grep -L '"FAQPage"' wwwroot/blog/*/index.html

# Which are missing Article?
grep -L '"@type":"Article"' wwwroot/blog/*/index.html

# Cross-check razors: which razors HAVE FaqItems but mirrors DON'T have FAQPage?
# (this is where the "shippable parity fix" list comes from — the razor already
# has the data, the mirror just needs to be synced)






If you have static mirrors for a Blazor WASM SPA and haven't grep'd them for schema completeness in a while, run these three grep commands. I'd bet money you find at least one drift class.






Why this happens



Static mirrors are a duplication. The DRY-in-code violation is what makes them work — the mirror is deliberately a copy, not generated, because generating them at build time (via prerendering) requires either running Blazor in server mode (defeats the point of static WASM) or shipping a whole SSG pipeline (heavier than the drift risk).



My repo made the pragmatic call: hand-maintain the mirror, add a lint that catches divergence for a growing list of known bad patterns.



The lint I have (scripts/lint-mirror-integrity.ps1) catches YMYL content drift — like a mirror saying "80CCD(1B) is allowed in the New Regime" when the razor correctly says it's disallowed (that class of drift once shipped a real integrity issue to production for weeks).



But schema-presence isn't in the lint. Neither is meta-keywords drift, or hreflang symmetry. My Saturday audit was manual grep-driven. I'll add these classes to the lint next.






Prescription



If you have this architecture:





  1. grep -L schema audits are cheap enough to run monthly. Add them as CI checks if you can.


  2. Cross-check razor source-of-truth against mirror — the FaqItems= vs <script>"FAQPage"</script> mismatch was the highest-signal finding.


  3. Sitemap ↔ page HTML hreflang symmetry — if you have multilingual variants, verify both sides declare each other.


  4. Meta keywords is deprecated but not zero-signal — copy-paste template duplication reads as low-effort content to human reviewers, even if Google's algo ignores the tag itself.



Static mirrors solve a real problem (crawler + first-paint UX). They introduce a real risk (drift you don't see). The audit script + lint list keeps that risk bounded.






One more thing



My audit found all 4 classes on a Saturday morning. Shipping the fixes across 13 mirror files took a Node script + a couple of hours. The impact: 6 blog pages became eligible for Google FAQ rich results, 7 became eligible for breadcrumb SERP renders, 2 got proper Article JSON-LD, 5 got topical keywords instead of homepage template.



If you have static mirrors and haven't audited them recently, do it. The scripts above are literally the whole thing.






I build client-side Indian tax calculators at was article #7 in this series.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
2 Quellen
Michael Linden wechselt zu Coda Audio Deutschland
2 Quellen
CVE-2026-18151 | IBM i 7.3/7.4/7.5/7.6 WebSocket Handshake race condition (CNNVD-2026-97778860)
1 Quelle
Razer Prio review: The first mobile controller I've actually wanted to carry everywhere