Content scripts
Inject on user action
Use chrome.scripting and activeTab to run the content script only on the page where the user requests highlighting.
There are two ways to get a content script into a page. You can declare it in the manifest with a list of URL patterns, and Chrome injects it on every matching page automatically. Or you can inject it from code, when the user asks.
Page Notes doesn’t need to run on every website. It runs when the user clicks Highlight. So we inject on demand. That’s programmatic injection.
Two permissions, two jobs
Programmatic injection needs scripting and activeTab together, and they do different things.
scripting turns on the chrome.scripting API. activeTab gives temporary access to the tab the user just invoked us on. Without the second one, scripting has no page it’s allowed to touch.
This pair is what lets us avoid host permissions entirely. No install-time warning about reading data on all websites.
Injecting a file
From the popup, after the user clicks Highlight, we already have tab from the previous lessons. Injecting our packaged file takes one call:
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content.js']
})
The call resolves when the script has run. It rejects on restricted pages, on a missing tab, or when the tab navigated to another origin and the activeTab grant expired. Catch that and show a message.
Do the injection inside the user’s action, right after the click. Don’t store a tab ID and inject later from the worker. The grant may be gone by then.
files or func
executeScript also accepts func, a function to run in the page. It’s handy for one-liners, but there’s a catch: Chrome serializes the function and runs it in the page. It doesn’t carry its closure along. Any variable from the popup you use inside it is undefined there. You have to pass values through the args option.
I prefer files. The code is a real file, reviewers can read it, and it can grow. Pick one approach for the project and stick with it.
Injecting twice
Here’s the bug you’ll hit first. The user clicks Highlight twice, and content.js runs twice. Two panels appear.
Make initialization idempotent, which means running it again changes nothing. The mark we set in the previous lesson does exactly that: if data-page-notes is already on the document, skip setup. The alternative is one installed message listener that handles every later command, which we’ll build next.
Add a Highlight button to the popup now. Click it five times fast. Navigate to another site while the popup is open and click again. Try it on chrome://extensions. At the end you want exactly one panel on the page, a clear message for the failures, and zero unhandled promise rejections in the console.
Lesson completed