- Published on
How Job Assistant is built - Frontend
- Authors

- Name
- Zee Lu
Introduction
Building a Chrome Extension that interacts with complex, dynamic sites like LinkedIn is vastly different from building a standalone web app. The "host" environment is hostile: the DOM changes unpredictably, styles leak in and out, and persistence is tricky.
In this post, I'll share how I solved the three biggest frontend challenges in Job Assistant: Extraction, Isolation, and Caching.
1. Parsing the "Hostile" DOM (LinkedIn Extraction)
Websites like LinkedIn don't provide an API for their job posts. To get the data we need (Title, Company, Description), we have to scrape the DOM directly from the extension.
The challenge? A/B Testing and Redesigns.
LinkedIn constantly changes class names, layout structures, and attributes. Hardcoding a single selector like .job-title guarantees failure within a week.
The "Strata" Strategy
I implemented a prioritized extraction strategy. Instead of relying on one selector, I define "Strata" of reliability:
private extractTitle(root: Element): string | null {
const titleSelectors = [
// Priority 1: Semantic, stable attributes (if they exist)
'h1[data-test-id="job-detail-title"]',
// Priority 2: Standard class names (likely to change)
'h1.jobs-unified-top-card__job-title',
// Priority 3: Structural fallbacks (DOM hierarchy)
'div.jobs-search__job-details--wrapper h1',
'main h1',
];
return findFirstElement(root, titleSelectors);
}
This functions like a waterfall:
- Semantic IDs: We look for data-test-id or stable attributes first.
- Known Classes: We check widely used class names.
- Heuristics: If all else fails, we look for "the first H1 in the main tag".
We apply similar logic for Company Name validation. For example, simply finding a link isn't enough; we validate that it links to a company page (/company/) or look for specific "verified" badges to confirm we aren't picking up a random "Apply" link.
2. Winning the CSS War (Style Isolation)
When you inject a UI into a page, you fight two wars:
- Inbound Leaks: The page's CSS ruining your nice buttons.
- Outbound Leaks: Your nice Tailwind classes messing up the page's layout.
Why not Shadow DOM?
Standard advice is "Use Shadow DOM". It works great for style encapsulation but introduces major headaches for events, focus management, and third-party libraries (like tooltips) that expect to find the document root.
The Scoped "Aggressive Reset" Solution
Instead of Shadow DOM, I used a Namespace Isolation technique.
- Root Scoping: All extension UI lives inside #job-assistant-root.
- Aggressive Reset: I inject a <style> tag that resets everything at the root level.
#job-assistant-root {
all: initial !important; /* The nuclear option */
font-family: -apple-system, sans-serif !important;
/* ... */
}
/* Reset all children to inherit only from our root */
#job-assistant-root * {
box-sizing: border-box !important;
font-family: inherit !important;
}
This creates a "clean room" environment. By forcing !important on our resets, we override even the most specific host styles. And because we prefix every single rule in our Tailwind build with #job-assistant-root (using tailwindcss nesting or prefixing features), our styles never accidentally color a LinkedIn button pink.
3. Caching: The "Local-First" LRU
We need to store everything: extracted job descriptions, generated resumes, and chat history. localStorage is sync and size-limited; chrome.storage.sync is tiny (100KB).
Storage Architecture
We use chrome.storage.local (5MB+) as our primary database. But unlimited storage produces unlimited garbage, so I implemented an LRU (Least Recently Used) Eviction Policy directly in the storage service.
In the storage service, we implement a check before saving:
const MAX_CVS = 50;
async function saveStorage(storage: CVStorage): Promise<void> {
const entries = Object.entries(storage.cvs);
if (entries.length > MAX_CVS) {
// Sort by Last Accessed/Updated
const sorted = entries.sort((a, b) => {
return new Date(b[1].updatedAt).getTime() - new Date(a[1].updatedAt).getTime();
});
// Keep top 50, discard the rest
const kept = sorted.slice(0, MAX_CVS);
storage.cvs = Object.fromEntries(kept);
}
await chrome.storage.local.set({ [CV_STORAGE_KEY]: storage });
}
This ensures the extension stays lightweight and fast without user maintenance. We also implement Version Control for resumes—keeping the last 5 iterations of any tailored CV so users can "undo" bad AI generations without bloating storage.
Conclusion
Frontend engineering for extensions is about defensive programming. You can't trust the DOM, you can't trust the CSS, and you can't assume you have infinite storage. By building robust fallback strategies and strict cleanup policies, Job Assistant feels reliable even on the messiest job boards.