Originally published at parvejshah.com/blog/building-manifest-v3-ai-chrome-extensions by Parvej Shah.
LinkedIn's DOM Doesn't Just Drift
Most sites' markup drifts gradually over time. LinkedIn's is different: periodically, it ships builds where every semantic class name — feed-shared-update, social-actions-bar, all of it — is replaced with a short hashed token. Same page, same layout, but every selector you wrote against it stops matching overnight.
A content script that hardcodes selectors against a page you don't control is fragile by default. Against a page that occasionally scrambles its own class names on purpose, hardcoded selectors aren't just fragile — they're a losing strategy.
Building Something That Fixes Itself
Instead of chasing LinkedIn's markup by hand every time it changed, the extension eventually grew a self-healing layer: on load, it takes a snapshot of the feed DOM, sends it to an AI model, and asks for a fresh set of selectors.
The snapshot isn't a raw DOM dump. It's pruned aggressively — ads, nav, and sidebars stripped out, tag depth capped, children per node capped, long hashed class names filtered out, and any leaf text content run through PII stripping before being included. The whole thing is budgeted to stay under roughly 3,000 tokens serialized. That's a deliberate choice: sending less data isn't just cheaper, it also means the model isn't accidentally handed a stranger's post content to reason about.
The AI's job is narrow: look at this structural sketch of the page and return three CSS selectors — where posts live, where the post text is, where to inject a button — as JSON. That JSON becomes a "strategy," cached in `chrome.storage.local` and re-validated against the live DOM on every subsequent load before it's trusted again.
## The Fallback Chain
A single AI call is not something you want standing between a user and a working feature, so the actual flow on every page load is five steps deep:
javascript
// 1. Load cached strategy, validate against live DOM
// 2. If invalid/stale, regenerate via AI (rate-limited to 1 call / 30 min)
// 3. If AI unavailable or fails, fall back to a bank of hardcoded selectors
// 4. If that fails too, fall back to content.js's original 5-tier
// heuristic parser (text-content walking, data-view-name anchors)
// 5. Log every outcome to a rolling debug buffer for the popup's debug panel
The rate limit on AI regeneration (once per 30 minutes) exists because a validation failure loop without one would mean an AI call on every page load for every user whenever LinkedIn's DOM was in a bad state — expensive and pointless if the underlying cause won't resolve by calling again immediately.
## When LinkedIn Obfuscates on Purpose
The specific failure mode this was built for shows up as a distinct code path: an `isCorruptedDOM()` check looks for a feed marked with `data-view-name="feed-full-update"` but with none of the expected semantic classes present — the signature of an obfuscated build. When that's detected, the extension stops trying to match classes entirely and instead walks the DOM by structural role: `span[dir="ltr"]` elements tend to survive obfuscation intact, and a `TreeWalker` heuristic can still locate the action bar by matching visible text content ("Like", "Comment", "Repost") rather than any class name at all.
This is the part of the project I'm most proud of technically, and also the part that made it clear where the ceiling was. Text-content heuristics work until LinkedIn changes wording, adds another localization variant, or restructures the markup enough that even structural role stops being a reliable anchor. Every layer we added bought time, not permanence.
## Where It Ended Up
The extension is published and live on the Chrome Web Store today. But active development stopped after the self-healing system's last iteration. LinkedIn's anti-automation posture — DOM obfuscation, and behavior that reads as active bot detection rather than incidental markup churn — kept escalating faster than a side project could track, and at some point the honest call was to stop rather than keep building a more elaborate response to a page actively working against being scraped.
The lesson isn't "don't build content-script extensions." It's that self-healing buys real resilience against ordinary drift, but it doesn't change the fundamental asymmetry: the host page can always change faster than you can adapt, and eventually that stops being a technical problem you can architect your way out of.
---
*Parvej Shah is a Lead Full-Stack Web Developer & Platform Architect based in Dhaka, Bangladesh. Explore full architecture case studies and production code at [parvejshah.com](https://parvejshah.com).*
Top comments (0)