I was trying to save a lesson from an interactive course into my notes. You know the type. Slides you click through, cards you flip to see the answer, boxes that open up when you tap them, plus the normal static text around all of it.
I did what anyone would do. Ctrl+A, Ctrl+C, paste into Notion. About a third of the content showed up. The slides were missing, the flip cards gave me the front and nothing else, and every "reveal" box was empty.
Why copy-paste fails here
I assumed the missing text wasn't on the page at all, and that it was loaded somewhere else when you click. That turned out to be wrong, at least for this page.
Most of this stuff is already in the HTML. The page just hides it with CSS: display: none on the slides you're not on, backface-visibility: hidden on the back of a flip card, a collapsed panel with zero height. Ctrl+A selects what's rendered, so anything hidden gets skipped. The text is there. The browser just won't hand it over.
The console trick
The fix is to stop asking the browser for the selection and read the DOM directly. Open DevTools, go to the Console, and run a tiny script that walks every text node on the page and copies the lot:
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { acceptNode: (n) => n.parentElement?.closest("script,style,noscript") ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT, }); const out = []; while (walker.nextNode()) { const t = walker.currentNode.textContent.trim(); if (t) out.push(t); } copy(out.join("\n")); It worked on the first real try, which I'll admit I wasn't expecting. Well, the second try. The first version I got had TypeScript type annotations in it, and the console only speaks plain JavaScript, so it threw a syntax error. If you're a TypeScript person like me, this is a good reminder that DevTools doesn't care about your tsconfig.
Okay, but opening DevTools every time is annoying
Pasting a snippet into the console is fine once. It's not fine if you do it ten times a week. It also has holes:
- Content inside an iframe isn't reached, so you get nothing back.
- Shadow DOM (web components) is skipped.
- The output is a flat list of lines with no structure, so headings and bullet points are lost.
So I turned it into a Chrome extension. I built it with Claude, going back and forth on the design, and it ended up small. One button in a popup.
How it works
It's Manifest V3, written in TypeScript. When you click the button, the popup injects the extraction function into the current tab using chrome.scripting.executeScript, with allFrames: true so it runs inside every iframe too. Each frame sends back its text and the popup merges everything into one block and writes it to the clipboard.
A few things I learned along the way that I would have gotten wrong on my own:
The injected function has to be fully self-contained. Chrome serialises it into a string and runs it inside the page, so it can't import anything or use variables from outside itself. If you pull a helper out into another file, it breaks, and the error isn't very helpful.
Write to the clipboard from the popup, not from the page. The popup has the user click behind it, so the browser allows it. Trying it from the injected script runs into focus and permission problems.
Walk the shadow roots. The plain DOM walk doesn't go into shadow DOM, so the extractor checks every element for an open shadowRoot and goes in.
I also made it output rough markdown: # for headings, - for list items, a | b for table rows. The page title and URL go at the top. That makes it paste into Notion without much cleanup. Carousels sometimes clone their slides, so repeated adjacent lines get dropped.
Testing it
I couldn't load it into a real browser from where I was working, so I tested the extractor against a fake page in jsdom. The page had a carousel with hidden slides, a flip card, a collapsed <details> block, a shadow DOM widget and a script tag that should never show up in the output. The test checks each piece of text is there and the script text isn't. It passes, but that's still a fake page, and real pages are weirder. Treat it accordingly.
What it can't do
- Lazy content. If a slide is only added to the page when you click to it, there's nothing to read yet. Click through first, then copy.
- Text inside images. Screenshots and scanned slides need OCR, which is a different problem.
- Some cross-origin iframes. I kept the permissions minimal (
activeTabandscripting), and that may not reach every embedded frame. The fix is a broader host permission, but that makes a Chrome Web Store review harder, so I left it out for now.
Running it
You don't need the Web Store. Build the project, open chrome://extensions, switch on Developer mode, click "Load unpacked" and point it at the dist folder. You only rebuild when you change the code. Chrome remembers the extension after that.
The code is on GitHub: https://github.com/shiv-jirwankar/web-page-content-copy/tree/master. It's small enough to read in one sitting, and the README has the setup steps.
I'm still deciding whether to add a keyboard shortcut and an option that clicks through "next" buttons automatically before copying. The second one is tempting and also a bit scary, because every course site has different buttons. If you have a page where it fails, I'd like to hear about it.
