Guide

Checking rendered DOM for stripped hyperlinks

Learn how to inspect the rendered DOM for stripped hyperlinks, troubleshoot JavaScript framework issues, and ensure Googlebot crawls every link.

Checking rendered DOM for stripped hyperlinks — OptiLink
Key takeaways
  • Raw HTML view-source often lies about which links Googlebot can actually see and crawl.
  • JavaScript framework hydration, client-side routing, and DOM manipulation frequently strip anchor tags post-load.
  • Puppeteer, Playwright, and Chrome Headless are essential for comparing static source against fully rendered DOM states.
  • Shadow DOM boundaries and custom web components require specialized traversal scripts to reveal hidden hyperlinks.
  • Fixing link hydration issues recovers discoverability without needing complete client-side framework refactors.
Quick answer

Client-side JavaScript often overwrites or rehydrates the HTML element containing the link. Frameworks like React, Vue, or Angular might mount a component that replaces static anchor tags with custom click handlers, `<button>` elements, or unlinked text if the routing state or props are mismatched. Additionally, browser extensions or aggressive DOM cleanup scripts can strip attributes dynamically after page load. To fix this, inspect your hydration logic and ensure components preserve native `href` attributes during render.

Why do links appear in view-source but disappear in the rendered DOM?

OptiLink · Key takeaways
Raw HTML view-source often lies about which links Googlebot can actually see and crawl.
JavaScript framework hydration, client-side routing, and DOM manipulation frequently strip anchor tags post-load.
Puppeteer, Playwright, and Chrome Headless are essential for comparing static source against fully rendered DOM states.
Shadow DOM boundaries and custom web components require specialized traversal scripts to reveal hidden hyperlinks.

Most teams we see manage complex web applications lose a measurable share of their internal links not to deliberate sitewide architecture changes, but to silent hydration bugs and post-load DOM manipulation. You inspect the static HTML payload sent from the server, verify the anchor tags are intact, and assume Googlebot follows your internal routing smoothly. Then, months later, key category pages drop out of the index, or new deep pages sit un-crawled in Google Search Console.

When you start checking rendered DOM for stripped hyperlinks, you quickly realize that what your server delivers and what a headless browser keeps in memory are often two completely different realities.

A client-side framework mounts over static markup, dynamic hydration logic fires, a third-party script runs an aggressive DOM cleanup, and suddenly, functional <a href="..."> tags turn into plain text or non-navigable <button> elements. If Googlebot’s rendering engine drops those links before processing the page, your link graph collapses behind the scenes without ever throwing a 404 status code or server error.

Here is how to catch, diagnose, and fix stripped hyperlinks in the live DOM.


Why Static Source Code Lies About Your Link Structure

For years, SEO audits relied on lightweight cURL requests, basic Python scripts, or classic crawling tools fetching raw server responses. If an <a href="/category/product"> tag existed in the view-source: output, the audit passed, the box was checked, and everyone moved on.

Modern web architectures have broken that assumption completely. Server-side rendering (SSR) and static site generation (SSG) output HTML to ensure fast initial page loads, but once the browser receives that payload, client-side JavaScript takes control. During the hydration phase, frameworks like React, Vue, or Angular bind event listeners to the DOM node. If the client-side state doesn’t perfectly match the server-rendered markup—a scenario far more common than most frontend teams like to admit—the framework drops the existing DOM nodes and re-renders them from scratch.

During this re-render, custom components frequently fail to pass down the href attribute, replace standard anchor elements with styled <div> or <button> tags, or fail to render the node entirely if an internal API payload is slow or returns an unexpected schema.

[Server Payload HTML] ──> <a href="/shoes">Shoes</a>
                                │
                        (Hydration Phase)
                                │
[Rendered DOM State]  ──> <span onclick="nav('/shoes')">Shoes</span>  ❌ Link Lost

When search engine crawlers process your site, they execute JavaScript using a headless Chromium browser instance. If your application strips anchor tags during browser execution, Googlebot sees a disconnected page node. The raw HTML promised a link, but the rendered DOM delivered a dead end.


Technical Mechanics of How JavaScript Strips Hyperlinks

Hyperlinks disappear from the DOM in three primary ways: DOM node replacement during reconciliation, attribute stripping during router mount, and shadow boundary isolation inside web components.

1. Rehydration Mismatches and Component Swapping

When a client-side framework hydrates SSR markup, it reconciles the virtual DOM (vDOM) with the real DOM delivered by the server. If a developer builds a navigation component that renders a native <a> tag on the server but relies on a generic ClickableText component on the client, the framework strips the original element and inserts a non-link node into the live DOM tree.

In complex enterprise codebases, this often occurs because server-side templates and client-side component definitions drift apart across different releases. The server outputs semantic markup for speed and accessibility, but the client bundle uses an optimized UI component library that relies heavily on event handlers instead of native web standards.

2. Attribute Nullification

Sometimes the <a> tag survives hydration, but its href attribute vanishes or becomes href="javascript:void(0)" or href="#". This typically happens when single-page application (SPA) client-side routers try to handle navigation manually:

// Problematic Component Logic
function NavLink({ to, children }) {
  // If 'to' is undefined or null during initial client render, href drops out
  return (
    <a onClick={() => router.push(to)} className="nav-item">
      {children}
    </a>
  );
}

If the to prop fails to resolve immediately on execution—due to asynchronous state initialization, unparsed GraphQL responses, or deferred context providers—the browser renders an anchor tag without a destination address. Googlebot skips anchors lacking valid, absolute, or relative href strings entirely.

3. Destruction by Third-Party Scripts

Aggressive DOM manipulation libraries, A/B testing frameworks, personalization widgets, ad platforms, and cookie consent managers often parse the page after load, rewriting navigation blocks dynamically. These scripts often strip anchors to attach custom tracking listeners or wrap elements in custom containers, completely hiding the original target destination from headless crawlers.


Manual Inspection: DevTools vs. Google Search Console

Before running site-wide automated scripts, manually verify how individual URLs execute in native and headless environments. You need a systematic workflow to pinpoint exactly where in the execution pipeline a hyperlink disappears.

Step 1: Compare View-Source to Live DevTools

Open your target page in Chrome. Right-click a target navigation link or footer menu item and select Inspect. Look at the active DOM structure in the Elements panel. Next, open view-source:https://yourdomain.com/page in a second tab and search for the exact same container node.

HTML Inspection State Visual Appearance Native <a href> Present? Crawlable by Googlebot?
Static Source Only Rendered link visible Yes in Source / No in DOM No
Live DOM Only Rendered link visible No in Source / Yes in DOM Yes (Delayed Render Queue)
Intact Across Both Rendered link visible Yes in Source / Yes in DOM Yes (Immediate Crawl)
Stripped in Render Text visible / Non-clickable Yes in Source / No in DOM No

Step 2: Use GSC URL Inspection Live Test

Chrome DevTools runs on your local machine with local state, unthrottled CPU, and fast network connections. Googlebot renders under tighter resource budgets, strict execution timeouts, and limited memory allocations.

  1. Open Google Search Console.
  2. Enter the target URL into the top search bar.
  3. Click Test Live URL (do not rely on the cached index state when debugging dynamic code).
  4. Once completed, click View Tested Page.
  5. Switch to the HTML tab and copy the code into a text editor.
  6. Search for your target URL destination inside this rendered snippet.

If you suspect your rendering setup is completely blocking discovery, reading our guide on spotting omitted links in rendered text trees helps diagnose hidden layout issues deeper in the document structure.


Automated Detection: Building a DOM Comparison Pipeline

Manual DevTools checks don’t scale across thousands of template variations. To systematically detect stripped hyperlinks, build an automated script using Node.js and Puppeteer (or Playwright) that fetches both the raw HTTP response and the post-execution DOM state, comparing anchor sets directly.

// automated-dom-checker.js
const puppeteer = require('puppeteer');
const axios = require('axios');
const { JSDOM } = require('jsdom');

async function checkLinkStripping(targetUrl) {
  try {
    // 1. Fetch raw server response (Raw HTML payload before JS execution)
    const rawResponse = await axios.get(targetUrl, {
      headers: { 'User-Agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' }
    });
    const rawDom = new JSDOM(rawResponse.data);
    const rawAnchors = Array.from(rawDom.window.document.querySelectorAll('a[href]'))
                            .map(a => a.getAttribute('href'))
                            .filter(Boolean);

    // 2. Render fully in Headless Chrome (Rendered DOM after JS execution)
    const browser = await puppeteer.launch({ headless: "new" });
    const page = await browser.newPage();

    // Set user agent to Googlebot Mobile
    await page.setUserAgent('Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MHC19J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.199 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)');

    await page.goto(targetUrl, { waitUntil: 'networkidle0', timeout: 30000 });

    const renderedAnchors = await page.evaluate(() => {
      return Array.from(document.querySelectorAll('a[href]'))
                  .map(a => a.getAttribute('href'))
                  .filter(Boolean);
    });

    await browser.close();

    // 3. Diff the arrays to find stripped or newly injected anchors
    const strippedLinks = rawAnchors.filter(href => !renderedAnchors.includes(href));
    const newlyAddedLinks = renderedAnchors.filter(href => !rawAnchors.includes(href));

    return {
      url: targetUrl,
      rawCount: rawAnchors.length,
      renderedCount: renderedAnchors.length,
      strippedCount: strippedLinks.length,
      strippedLinks,
      newlyAddedLinks
    };
  } catch (error) {
    console.error(`Error auditing ${targetUrl}:`, error.message);
    return null;
  }
}

// Execute check
checkLinkStripping('https://example.com/product-page').then(result => {
  console.log('Audit Results:', JSON.stringify(result, null, 2));
});

Running the Script at Scale

When running automated detection pipelines across large staging or production environments, keep these execution requirements in mind:

  • Wait Conditions: Ensure your headless browser script uses waitUntil: 'networkidle0' or explicitly waits for specific hydrated DOM elements. Quitting too early flags false positives because the client script hasn’t finished mounting.
  • User-Agent Alignment: Set the headless browser user agent to emulate Googlebot Desktop or Mobile to catch user-agent-specific hydration branches.
  • Diffing Logic: Focus specifically on anchor targets that existed in the server payload but vanished in the client payload. These represent immediate crawling breakage.

The Special Case: Trapped and Stripped Web Components

Even when JavaScript renders interactive elements cleanly, modern component design systems often hide links inside Web Components, Shadow DOMs, or isolated frames.

Standard DOM queries like document.querySelectorAll('a') fail at shadow root boundaries. If your site uses custom web components, your hydration phase might move links into an un-crawlable shadow tree or strip them in favor of synthetic custom events that dispatch internally.

To properly check shadow trees, your automated scripts must recursively traverse open shadow roots. You can inject a helper function into your evaluation script to surface trapped anchors:

function getAllAnchorsRecursive(root = document) {
  let anchors = Array.from(root.querySelectorAll('a[href]'));
  const elementsWithShadow = root.querySelectorAll('*');

  elementsWithShadow.forEach(el => {
    if (el.shadowRoot) {
      anchors = anchors.concat(getAllAnchorsRecursive(el.shadowRoot));
    }
  });

  return anchors;
}

When auditing custom components, ensure your engineering team avoids closed shadow roots (mode: 'closed') on public content pages, as crawlers cannot inspect or extract nodes inside closed encapsulations. For detailed strategies on overcoming component isolation, refer to our technical breakdown on fixing links trapped in shadow DOM elements.

Similarly, embedded content often suffers from missing anchors when applications shift from native links to dynamic frames. If your architecture relies on external frame loads, read our guide on checking if Googlebot skipped your iframe links to prevent silent crawl loss across domain boundaries.


Costs, Tradeoffs, and Architectural Realities

Fixing stripped hyperlinks usually forces a choice between engineering complexity, infrastructure spend, and performance overhead. There is rarely a zero-cost solution, and choosing the right approach depends on team capacity and codebase constraints.

[Pure Client-Side SPA] ──> Low Server Cost  │ High Crawl Risk / Heavy Stripping
[Hydrated SSR]         ──> Med Server Cost   │ Medium Risk / Mismatch Vulnerable
[Static HTML First]    ──> Low Server Cost   │ Zero Crawl Risk / High Dev Effort

Option A: Dynamic Pre-Rendering at the Edge

Using rendering middleware (e.g., Puppeteer, Playwright, or edge rendering services running on Cloudflare Workers or AWS Lambda@Edge) intercepts requests from search engine bots and serves fully executed HTML snapshots.

  • Pros: Guarantees search engines see the exact post-hydration DOM without altering underlying frontend component libraries.
  • Cons: High infrastructure maintenance, complex caching invalidation rules, risk of serving stale content to crawlers, and recurring compute costs.

Option B: Strict Hydration Guardrails in Frontend Codebase

Enforcing developer standards, unit tests, and framework configuration (e.g., Next.js <Link>, Nuxt <NuxtLink>) ensures components render standard anchors natively across both server and client states.

  • Pros: Long-term clean codebase, zero extra infrastructure costs, optimal page load performance, and smooth user experience.
  • Cons: Requires refactoring legacy component libraries, developer training, strict linting, and continuous integration checks.
Approach Crawl Reliability Infrastructure Cost Developer Effort Maintenance Risk
Edge Pre-rendering High High Low Medium
Framework Native Anchors High Low High Low
Hybrid SSR + Progressive Hydration High Medium Medium Low
Client-Side Rendering Only Low Very Low Low High

Worked Example: Auditing an Ecommerce Category Template

To illustrate how stripped hyperlinks occur and get resolved in real environments, consider this scenario from a mid-sized e-commerce site migration.

The Problem

During a migration from a legacy monolithic CMS to a headless React frontend, a client’s product listing pages (PLP) suffered a drop in indexation across deep product pages over a three-month period. The server correctly rendered product cards, but Google Search Console showed “Discovered - currently not indexed” for thousands of deep product URLs.

Server Response (SSR Payload):
<div class="product-card">
  <a href="/products/blue-widget">
    <img src="/img/blue.jpg" alt="Blue Widget" />
    <h3>Blue Widget</h3>
  </a>
</div>

Post-Hydration DOM (Browser / Googlebot View):
<div class="product-card">
  <div class="card-inner" onclick="navigateTo('/products/blue-widget')">
    <img src="/img/blue.jpg" alt="Blue Widget" />
    <h3>Blue Widget</h3>
  </div>
</div>

Diagnostic Steps

  1. Source Inspection: A basic cURL command confirmed <a href="/products/blue-widget"> was present in the initial server HTML payload.
  2. Puppeteer Execution Diffing: The automated script revealed rawCount: 48 links in the static grid, but renderedCount: 0 links inside the main product grid container after client-side JavaScript execution.
  3. Root Cause Analysis: The frontend team used an interactive Card component that converted native anchors into custom click handlers during client hydration to prevent full page reloads and enable quick-view modal popups.

The Solution

The engineering team updated the Card component to use Next.js native <Link href="/products/blue-widget"> wrapper, maintaining semantic HTML markup while preserving single-page routing performance and interactive modal triggers via progressive enhancement.

// Fixed React Component State
export default function ProductCard({ product }) {
  return (
    <div className="product-card">
      <Link href={`/products/${product.slug}`}>
        <img src={product.image} alt={product.name} />
        <h3>{product.name}</h3>
      </Link>
      <button onClick={() => openQuickView(product.id)}>
        Quick View
      </button>
    </div>
  );
}

Result

Within three crawl cycles following the deployment, Googlebot re-indexed the deep product catalog, restoring internal link equity flow and organic impressions across category structures.


What to Do This Week: Actionable Checklist

If you suspect client-side execution is hiding internal links from search crawlers, follow this action plan to audit and remediate your templates:

  1. Identify High-Value Templates: Isolate primary site templates (category listings, product pages, article index pages, location hubs).
  2. Run DevTools Link Extraction: Open Chrome DevTools Console on a target page and execute this snippet to inspect live anchors: javascript console.table(Array.from(document.querySelectorAll('a[href]')).map(a => ({ text: a.innerText.trim(), href: a.getAttribute('href') })));
  3. Compare Against Server Payload: Disable JavaScript in DevTools (Command + Shift + P -> type Disable JavaScript) and refresh. Re-run the snippet and compare output counts.
  4. Audit Third-Party Scripts: Disable marketing, tag management, and personalization scripts one by one in DevTools Network/Request Blocking to verify if external scripts are removing anchor tags post-load.
  5. Enforce Native Links in Linting: Establish a team linting rule using eslint-plugin-jsx-a11y (specifically jsx-a11y/anchor-is-valid) to block pull requests containing click handlers on non-link elements.
  6. Validate via Google Search Console: Submit modified URLs to GSC’s URL Inspection Tool, run a Live Test, and manually inspect the raw rendered HTML tab for valid <a href="..."> tags.

If manually writing Puppeteer diffing pipelines and inspecting complex DOM states takes too much engineering time, OptiLink automates DOM rendering checks across your entire site structure, pointing out stripped hyperlinks, hydration discrepancies, and crawling blockers automatically.

Frequently asked questions

Why do links appear in view-source but disappear in the rendered DOM?

Client-side JavaScript often overwrites or rehydrates the HTML element containing the link. Frameworks like React, Vue, or Angular might mount a component that replaces static anchor tags with custom click handlers, `<button>` elements, or unlinked text if the routing state or props are mismatched. Additionally, browser extensions or aggressive DOM cleanup scripts can strip attributes dynamically after page load. To fix this, inspect your hydration logic and ensure components preserve native `href` attributes during render.

Does Googlebot crawl links that only exist before JavaScript executes?

Googlebot renders pages using a modern headless Chromium browser, but rendering happens in a secondary processing queue. If a link exists in the static HTML but gets explicitly removed, overwritten, or destroyed during JavaScript execution and DOM hydration, Google's rendered view will lack that link. Consequently, Googlebot will not discover or pass PageRank through links that are missing from the final rendered DOM state, making static presence alone insufficient.

Can shadow DOM hide hyperlinks from search engine crawlers?

Yes, standard DOM queries and simple crawlers cannot inspect elements inside closed shadow roots. While modern Googlebot can render shadow DOM components, links inside shadow trees must use native `<a>` tags with valid `href` attributes rather than synthetic JavaScript event listeners. If shadow DOM encapsulation prevents your framework from rendering standard anchors into the light DOM, search crawlers may fail to extract the destinations, breaking your internal link architecture.

What is the difference between checking view-source and inspecting the DOM in DevTools?

View-source displays the raw HTML payload sent directly from the web server before any browser processing occurs. Inspecting the element in Chrome DevTools shows the live, rendered DOM, which reflects all modifications made by JavaScript execution, CSS parsing, browser corrections, and framework hydration. Search engines index and follow links based on the final rendered DOM, which makes DevTools or headless browser automation necessary for accurate link audits.

How long does Googlebot take to render pages and discover rendered links?

Googlebot renders virtually all pages it crawls using a two-wave indexing system, but the delay between fetching raw HTML and executing JavaScript varies. While raw HTML processing happens immediately, headless Chromium rendering can lag anywhere from a few seconds to several days depending on Googlebot's processing queue capacity and site crawl demand. If your links rely entirely on post-execution JavaScript without static HTML fallbacks, link discovery and internal PageRank flow will experience significant, unpredictable latency.

Frequently asked questions

What is the ideal anchor text ratio for a new site?

A safe starting ratio is 50% branded, 30% naked URLs, 15% partial-match, and 5% exact-match, but adjust based on your niche competitiveness.

Can I use exact-match anchor text after a penalty?

Yes, but only after you’ve disavowed toxic links and rebuilt the profile with 80%+ branded anchors for 3–6 months to restore trust.

How many variations of anchor text should I use per linking domain?

Limit to 2–3 variations per domain to avoid looking manipulative; rotate them across your outreach campaigns.

Is it okay to ask for a specific anchor text in outreach?

You can suggest it, but most editors will ignore the request or rewrite it; focus on providing value so they naturally use your brand or URL.

Frequently asked questions

What is the best anchor text for SEO?

The best anchor text describes the linked page's content clearly and naturally, using relevant keywords without stuffing.

How many types of anchor text are there?

There are six primary types: exact match, partial match, branded, naked URL, image alt, and generic.

What is the best practice for anchor text?

Use varied, contextually relevant anchors that accurately reflect the destination content while avoiding repetitive patterns.

Why is anchor text important in SEO?

It helps search engines understand the topic and relevance of linked pages, boosting topical authority when done correctly.

Frequently asked questions

What is the ideal anchor text ratio for SEO?

Most teams find a 3:1 ratio of partial-match to branded anchors works without triggering filters, but the real answer depends on your niche and existing profile.

Can I use exact-match anchor text after a penalty?

Yes, but only sparingly and usually after rebuilding trust with a heavy branded and partial-match base first.

How many different anchors should I use per page?

There is no magic number, but aiming for at least 5–7 unique variations per target page keeps the profile looking organic.

Is it safe to use naked URLs as anchors?

Absolutely; they are the safest anchor type and often look natural in editorial content, especially for homepages and high-authority references.

Should I nofollow all keyword-rich anchors?

No, a natural mix includes some follow links, but keep the ratio low—around 10–15%—to avoid over-optimization signals.

Frequently asked questions

How do I check my anchor text distribution?

Open Google Search Console, go to Links to your site, and click the 'Anchor text' report. It shows your top anchors and their share of the profile, though it caps at 1,000 links.

What is a healthy anchor text distribution?

Most sites we audit fall into a 50-70% branded/natural mix with exact match anchors under 10%. If your exact match ratio creeps past 15%, it’s a red flag for over-optimization.

Can too much exact match anchor text hurt rankings?

Yes. Google’s Penguin and manual spam filters flag profiles where exact match anchors exceed 15-20%. The safest approach is to keep them under 10% and rely on brand, URL, and related variations for the bulk of your links.

How often should I audit anchor text?

We recommend a light audit after every major link insertion campaign and a full deep-dive quarterly. If you’ve just recovered from a penalty, check monthly until the profile stabilizes.