To remove every ordinary <a> element from the current page, use page.$$eval('a', ...) to select the anchors in one page-context callback and call remove() on each one. This deletes the links and their contents from the rendered DOM. If you want to keep the text and only prevent navigation, remove or change the href attribute instead.
Remove all anchor elements
page.$$eval is a direct fit for a bulk edit: it selects all elements matching a selector, passes the resulting array to one callback in the page, and returns the callback’s result to Node.js. The callback below counts the matches before removing them, then returns that count.
const removedCount = await page.$$eval('a', anchors => {
const count = anchors.length;
anchors.forEach(anchor => anchor.remove());
return count;
});
console.log(`Removed ${removedCount} links`);
The selector 'a' matches anchor elements regardless of whether their destination is internal or external, and whether they have an href attribute. The operation changes the live document in the browser tab; it does not edit the website’s files or server response.
Run it in a complete Puppeteer script
This CommonJS example launches a browser, opens a page, removes the anchors after navigation completes, prints the number matched, and closes the browser even if an error occurs. Replace the example URL with the page you control or are authorized to inspect.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minute#1 Best Overall
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const removedCount = await page.$$eval('a', anchors => {
const count = anchors.length;
anchors.forEach(anchor => anchor.remove());
return count;
});
console.log(`Removed ${removedCount} links`);
} finally {
await browser.close();
}
})();
The removal runs after page.goto resolves. domcontentloaded waits for the document to be parsed, but page scripts may still add or replace content afterward. If the links are inserted later, wait for the relevant content or perform another removal after it appears. The script does not need to save or return the page’s modified HTML unless that is a separate requirement.
Keep the text but disable the links
anchor.remove() removes the element and its children, so linked words, images, or other nested content disappear too. When the goal is to make the page non-navigable while preserving what it displays, remove the href attributes instead:
const changedCount = await page.$$eval('a[href]', anchors => {
const count = anchors.length;
anchors.forEach(anchor => anchor.removeAttribute('href'));
return count;
});
console.log(`Disabled ${changedCount} link destinations`);
This variant selects only anchors that currently have an href. Their text and child markup remain in place. It does not remove other link-related attributes or event handlers, so if the page has custom click behavior, inspect that behavior separately rather than assuming that removing href disables every possible action.
Choose the right Puppeteer API
Use page.$$eval for all matches
page.$$eval(selector, callback) passes the selector’s matching elements as an array to a single callback in the page context. It is the concise option for a bulk operation such as deleting all anchors or changing each matching element’s attribute.
Rank #2
Use page.evaluate for custom document logic
If selection depends on more than a single selector, query the document inside page.evaluate. The function runs in the page context, and Puppeteer returns its result to Node.js. For example, this removes anchors whose visible text matches a condition:
const removedCount = await page.evaluate(() => {
const anchors = [...document.querySelectorAll('a')];
const matches = anchors.filter(anchor => anchor.textContent?.trim() === 'Read more');
matches.forEach(anchor => anchor.remove());
return matches.length;
});
Keep the work that touches document inside the callback: browser-page objects and Node.js variables are not automatically available in that page context. Return plain values, such as a number or string, when the Node.js script needs a result.
Use page.$eval only when you intend to handle one match
page.$eval(selector, callback) applies its callback to the first matching element. It throws if nothing matches, so it is not a safe substitute for a bulk operation when the selector might find zero elements. If only one element is expected but may be absent, check for a match before using it, or use an API and control flow that explicitly handles the missing case.
Limit the removal to a specific part of the page
Removing every anchor can strip navigation, buttons implemented as links, and links in unrelated sections. When only one region should change, scope the selector to a stable container. For example, if the page has a container with the ID article-body:
const removedCount = await page.$$eval('#article-body a', anchors => {
const count = anchors.length;
anchors.forEach(anchor => anchor.remove());
return count;
});
Use a selector that identifies the intended region, and check that it still matches the page’s markup before relying on it. If a site’s layout changes or uses a different container, the selector may match nothing or the wrong links. A count of zero is a useful signal to inspect the selector and the page state rather than treating the run as proof that the intended content was processed.
Handle pages that change after loading
The callback edits the DOM as it exists when the evaluation runs. It does not establish a permanent rule against later changes: scripts can insert new anchors, replace a modified section, or navigate to another document. If the page builds the target content asynchronously, wait for a meaningful selector or other known page condition before removing links, then verify that the target is present.
If new anchors appear after the first pass, run the same operation again after the page update. A repeated pass is often simpler than monitoring all mutations. An observer-based approach can be appropriate when content arrives continuously, but it needs a clearly defined lifetime and scope so it does not keep modifying unrelated content. The supplied removal callback itself does not watch for later mutations.
Frames and shadow roots need separate attention
A document-level 'a' query should not be assumed to include every link visible on screen. Content in an iframe belongs to a separate document context; select or evaluate in the relevant Puppeteer frame if those links are in scope. A simple document query also does not automatically traverse shadow roots. Puppeteer documents deep selector support, and open shadow roots can also be traversed deliberately when necessary. Closed or otherwise inaccessible content may require a different strategy.
Recommended Free Tools
Rank #4
Decide whether the task concerns only the top-level document or also specific embedded content. Then target those contexts explicitly and report counts per context if the result needs to be audited.
Troubleshoot common results
- The visible text vanished too. That is expected from
anchor.remove(), which removes the anchor and its child content. Use theremoveAttribute('href')version if the content should remain. - The count is zero. Check that navigation has reached the page containing the links, that the selector matches its markup, and that the links are not inside a separate frame or shadow root. If scripts add them later, wait for the relevant content and run the operation afterward.
- The links return after the callback. A page script may have inserted new anchors or rebuilt the modified area. Run the removal after that update, or use a deliberate observer strategy if repeated updates are part of the page behavior.
page.$evalthrows. It requires a matching element. Usepage.$$evalfor zero-or-more matches, or explicitly check for the single element before applying a one-element callback.- The top page is clean but an embedded area is not. Inspect whether that area is an iframe or shadow-root content and query the appropriate context; the top-level query does not cover every such context automatically.
- The page’s appearance changed unexpectedly. Removing anchors also removes their child nodes and can alter layout. Scope the selector more narrowly or preserve the elements and remove their destinations instead.
Performance, reliability, and scope
For a single bulk edit, one $$eval call avoids issuing a separate Node.js-to-page operation for every matching anchor. The returned number tells you how many elements the callback selected at that moment; it does not certify that later scripts will leave the document unchanged. If the operation is part of a larger automated workflow, log the page URL and count alongside the workflow’s own status so that a zero-match result can be distinguished from a successful edit.
DOM removal is temporary and local to the current rendered page. A reload or navigation creates a document from the website again, and the change does not persist on the site for other visitors. If you need a saved artifact, capture or serialize the modified page separately after making the change; this code alone only performs the in-memory DOM edit.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If your actual goal is a clean screenshot or PDF rather than changing a page’s DOM in an automation workflow, ScreenshotNeo can capture a URL with one GET request. It is not a replacement for Puppeteer code that must mutate the DOM or return the changed markup. Its optional clean-shot steps accept cookie or consent banners like a visitor and remove 60+ known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. It also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Example cURL call, using the documented API endpoint and parameters:
Best Value
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for request options. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Sign up for the free plan to try it without a card.
Frequently Asked Questions
Does the returned count include links nested inside another anchor?
It counts every anchor matched in the initial selection before removal, including any nested matches present in the DOM. Nested anchor markup is non-conforming HTML, so do not use the count as a measure of distinct clickable areas.
Does this treat mail, telephone, and download links differently?
No. The selector matches anchors without classifying their destination. Narrow the selector or add a condition based on the element’s attributes if you want to leave particular kinds of links untouched.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




