Client-Side Inserted HTML Ignores Your Astro Styles: The Scoped CSS + innerHTML Gotcha

Client-Side Inserted HTML Ignores Your Astro Styles: The Scoped CSS + innerHTML Gotcha

TL;DR — Astro scopes a component's <style> block at compile time by stamping a data-astro-cid-XXXXXXXX attribute onto every element it server-renders, then rewriting selectors to require that attribute. HTML built client-side — insertAdjacentHTML, innerHTML, template literals — never gets that attribute, so scoped styles silently never match it. No error, no warning; the markup looks correct on inspection, it just falls back to bare browser defaults.

Table of Contents
  1. Three times this bit the same site
  2. 1. Infinite-scroll post cards
  3. 2. Upload preview thumbnail
  4. 3. Live comment list
  5. The fix
  6. Rule going forward

Three times this bit the same site

1. Infinite-scroll post cards

New post cards loaded on scroll were built as HTML strings and appended via insertAdjacentHTML. The scoped .feed-card/.avatar rules never matched them — visually, a byline avatar rendered at its native pixel size instead of the intended 32×32px, a full-size image blowing up the card layout.

2. Upload preview thumbnail

A logo/avatar upload preview built with document.createElement("img") then .replaceWith() lost the scoping attribute the original placeholder element had. Result: the preview rendered at full original size instead of the compressed thumbnail size.

3. Live comment list

A comment thread rendered via a template-literal renderCmt() function and inserted with innerHTML produced unstyled comment items — less visually dramatic than the other two since comment avatars were colored initial-letter divs, not oversized <img> tags, so it just looked like plain unstyled text rather than an obviously broken layout.

The fix

- <style>
+ <style is:global>
 	.feed-card {
emdashkits.com

If the block used Astro's :global(...) escape hatch to reach into child markup from inside a scoped block, drop the wrapper once the whole block is already global — leaving it in can produce a selector that matches nothing.

- .post-card-image :global(img) {
+ .post-card-image img {
emdashkits.com

For an element that already exists server-rendered and only needs its attributes swapped (e.g. a preview image's src), update it in place instead of replacing the node — that keeps its original data-astro-cid-* attribute intact. If you must swap element type (a placeholder <span> becoming an <img>), copy the data-astro-cid-* attribute across before calling replaceWith().

Read also:

Rule going forward

  • Before writing client-side JS that appends or clones markup mirroring a server-rendered component's structure, find that component's <style> block.
  • Make it is:global (simplest), or move the shared classes into an existing global stylesheet if they're broadly reused.
  • Verify by actually triggering the client-side path (scroll to load more, upload a file, post a comment) and looking at the result — this bug produces zero errors anywhere, a visual check is the only way to catch it.
Share

Comments

Write a comment

Related Articles

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

July 16, 2026

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

July 16, 2026

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

TL;DR — A custom session-cookie login flow appeared to succeed on localhost (the OTP verified, the response looked fine) but every subsequent request to a login-gated page treated the visitor as logged out. Identical code worked fine on the live HTTPS site. The cookie's Secure attribute was hardcoded to true — and per the cookie spec, browsers never store or send a Secure cookie over a plain, non-HTTPS connection.

Table of Contents
  1. Diagnostic
  2. Root cause
  3. Fix
  4. Lessons learned

Diagnostic

Check the actual Set-Cookie response header and the browser's own cookie storage panel — on localhost over http://, the cookie is sent by the server but never actually stored by the browser.

Root cause

// before -- assumes the app is always served over HTTPS
setCookie("session", token, { secure: true, httpOnly: true });
emdashkits.com

A cookie config that quietly assumes "we're always on HTTPS" breaks the instant you test over plain HTTP, which local dev servers commonly are.

Read also:

Fix

// after -- derive secure from the actual request protocol
const isHttps = request.url.startsWith("https://");
setCookie("session", token, { secure: isHttps, httpOnly: true });
emdashkits.com

Lessons learned

  • Any Secure-flagged cookie needs to key off the real request scheme, not an assumption baked in once at cookie-creation time.
  • "Works in production, silently fails in local dev" is a strong signal to check cookie flags before anything else in an auth flow.
  • Check other cookies in the same codebase for the same hardcoded assumption — if one cookie has this bug, sibling cookies set the same way are worth auditing too.
Share
Previous Article

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Comments

Write a comment

Related Articles

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

July 16, 2026

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

July 16, 2026

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

TL;DR — GA4's gtag.js snippet was installed correctly, the page loaded with no visible JavaScript error, and GA4's real-time report still showed zero activity. The CMS's default Content-Security-Policy had no allowance for analytics domains and no config option to add one — so every request to Google's tracking endpoints was blocked at the browser level before it could fail loudly.

Table of Contents
  1. Diagnostic
  2. Root cause
  3. Lessons learned

Diagnostic

Check the browser's dedicated CSP violation reporting, not the regular console error list — CSP blocks are reported through their own channel, not thrown as normal script errors, so "no console errors" doesn't mean nothing was blocked.

Root cause

The CSP's script-src and connect-src directives had no entry for googletagmanager.com or google-analytics.com, and the CMS exposed no configuration surface to add one — the only way in was patching the CSP directives directly.

// patch-package: add analytics domains to the existing CSP directives
scriptSrc.push("https://www.googletagmanager.com");
connectSrc.push("https://www.google-analytics.com", "https://www.googletagmanager.com");
emdashkits.com
Read also:

Lessons learned

  • "No console errors" is not proof nothing was blocked — CSP violations live in their own reporting surface and are easy to miss if you're only scanning for red error text.
  • Before adding any third-party script tag to a site with a CSP already in place, check the CSP's directives first rather than assuming a silently-empty analytics dashboard means a snippet-installation mistake.
Share
Previous Article

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Next Article

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Comments

Write a comment

Related Articles

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

July 16, 2026

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

July 16, 2026

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)