# Links are not crawlable: what it means and how to fix it

**Audit ID:** `crawlable-anchors` · **Category:** SEO

<!--QA-->
> **Quick answer:** This audit fails when a link has no crawlable destination: a click handler on a div or span, an empty href="#", or href="javascript:void(0)". Googlebot follows only real href values, so those destinations may never be discovered. Fix it by giving every navigational link an actual URL in its href attribute.
<!--/QA-->

This Lighthouse audit fails when the page contains links a crawler cannot follow. The link works when a person clicks it, because a JavaScript handler is doing the navigating. Googlebot never runs that handler the way a mouse does; it reads the `href` attribute, and if the `href` is missing, empty, `#`, or `javascript:void(0)`, there is nothing to follow. Every page reachable only through such a link is, from a crawler's point of view, not reachable at all.

## TL;DR

- **What:** One or more anchors have no resolvable destination in `href`, or the "link" is a `div` or `span` with a click handler.
- **Why it matters:** Crawlers discover pages by following `href` values. An uncrawlable link hides its destination from indexing entirely, which no Lighthouse score reflects.
- **Fix:** Put the real URL in `href`. Use `<button>` for controls that do not navigate.

## What does the crawlable-anchors audit check?

Lighthouse collects every `<a>` element in the rendered DOM and resolves its `href` against the page URL. An anchor fails when the resolved destination is not something a crawler can request. In practice that means:

| Pattern | Result |
|---|---|
| `<a href="/pricing">` | Passes |
| `<a href="https://example.com/x">` | Passes |
| `<a>` with no `href` at all | Fails |
| `<a href="">` | Fails |
| `<a href="#">` | Fails |
| `<a href="javascript:void(0)">` | Fails |
| `<a href="javascript:goTo('/x')">` | Fails |
| `<div onclick="location.href='/x'">` | Fails |
| `<a href="mailto:...">`, `<a href="tel:...">` | Not flagged |

Two details matter. First, the audit runs against the **rendered** DOM, so a link that JavaScript fills in later is judged on its final state, not its source. Second, this is a separate question from whether the destination is any good: a link pointing at a 404 passes `crawlable-anchors` because the `href` resolves. The audit asks only whether a crawler has somewhere to go.

It sits in the SEO category with a **weight of 1**, so the direct score movement is small. That understates it considerably, for the reason in the next section.

## Why do uncrawlable links hurt SEO more than the score suggests?

Crawl discovery is the thing at stake, not a point on a dashboard.

Google finds pages primarily by following links. [Google's own guidance on crawlable links](https://developers.google.com/search/docs/crawling-indexing/links-crawlable) is explicit: a link is followable when it is an `<a>` element with an `href` attribute, and Google may not follow anything else. A collection page reachable only through a `div` with a click handler can sit unindexed indefinitely while the rest of the site ranks normally, and nothing in the Lighthouse report will say so, because the report grades the page in front of it, not the pages it failed to expose.

The knock-on effects:

- **No discovery.** If a URL appears nowhere else (no sitemap entry, no crawlable internal link, no external link), it may never enter the index.
- **No link equity.** PageRank flows along `href` values. A click handler passes none of it, so even a URL that gets indexed via the sitemap arrives with no internal signal behind it.
- **No anchor text signal.** The words in a link tell Google what the destination is about. That signal only travels on a real link, which is why this audit and [link-text](/audits/link-text) are worth fixing together.
- **Broken for keyboard and assistive tech too.** A `div` with an `onclick` is not focusable, not announced as a link, and does not respond to Enter. The SEO fix is usually an accessibility fix.

## How do I fix "links are not crawlable"?

Decide what the element actually is. If it navigates, it is a link and needs an `href`. If it changes state on the current page, it is a button.

```html
<!-- FAILS: fake link built from a div -->
<div class="nav-item" onclick="location.href='/collections/sale'">Sale</div>

<!-- FIXED -->
<a class="nav-item" href="/collections/sale">Sale</a>

<!-- FAILS: the classic placeholder href -->
<a href="#" onclick="openProduct(42)">View product</a>

<!-- FIXED: real destination, handler still free to intercept -->
<a href="/products/42" onclick="openProduct(42); return false;">View product</a>

<!-- FAILS: void(0) is the single most common offender -->
<a href="javascript:void(0)" data-target="/blog/page/2">Next page</a>

<!-- FIXED -->
<a href="/blog/page/2" data-target="/blog/page/2">Next page</a>

<!-- NOT A LINK AT ALL: accordion toggle, no navigation -->
<a href="#" class="accordion">Shipping details</a>

<!-- FIXED: it is a control, so use a button -->
<button type="button" class="accordion" aria-expanded="false">Shipping details</button>
```

The rule that resolves almost every case: **the `href` is the destination, the click handler is an enhancement.** Keep both. A real `href` means the link still works when JavaScript fails, when it is opened in a new tab with a middle click, and when a crawler reads it.

Three patterns worth calling out:

1. **Progressive enhancement, not replacement.** `<a href="/x" onclick="spaNavigate('/x'); return false;">` gives the crawler a URL and the user a client-side transition. Both audiences served by one element.
2. **`href="#"` only for a genuine same-page jump,** and then only with a fragment that exists: `href="#reviews"` pointing at `id="reviews"` is fine and resolves. A bare `#` is not.
3. **Do not paper over it with `role="link"`.** ARIA changes what assistive tech announces; it does not create an `href`, and it does not make the destination crawlable.

## How do I fix this in React, WordPress, or Shopify?

### React / Next.js

Every mainstream router already renders a real anchor. The failures come from hand-rolled navigation:

```jsx
// FAILS: no href in the output HTML
<div className="card" onClick={() => router.push(`/products/${id}`)}>

// FIXED: Next.js Link renders <a href="/products/42"> and still routes client-side
<Link href={`/products/${id}`} className="card">
```

The same holds for `react-router`'s `<Link to="...">`, Nuxt's `<NuxtLink>` and Vue Router's `<RouterLink>`: all three emit a crawlable `href`. If you are calling `router.push`, `navigate()` or `window.location` from an `onClick`, that is the bug. Programmatic navigation is correct after a form submit or a save; it is wrong as the only way to reach a page.

Watch for wrapper components too. A `<Card onClick={...}>` that renders a `div` is uncrawlable no matter how many times it appears; fixing the component fixes every instance.

### WordPress

Themes and page builders are the usual source:

- **Mega menus and tabbed navigation** that use `href="#"` with a JavaScript handler for the top-level items. Point each one at the category or page archive it represents.
- **Page builder buttons.** Elementor, Divi and WPBakery all let you leave the link field empty, which renders `href="#"`. Fill it in or use a button element.
- **"Load more" pagination.** If the AJAX button is the only route to page 2 and beyond, those posts have no crawlable path. Keep numbered pagination links in the markup, even hidden behind the AJAX behaviour.
- **Filter and sort controls** rendered as anchors with `javascript:void(0)`. If they change the current view rather than navigating, they should be `<button>`; if they produce a distinct URL worth indexing, give them that URL.

### Shopify

In a scan of 269 production storefronts, `crawlable-anchors` failed on **30.1% of them (81 of 269)**. The recurring offenders:

- **Variant and swatch selectors** rendered as `<a href="#">` with a JavaScript handler. If each variant has its own `?variant=` URL, link to it; if the selector only updates the current page, make it a `<button>`.
- **Drawer and mega-menu triggers** built as anchors with `javascript:void(0)`. Menu top-level items almost always have a real collection URL sitting right there in the Liquid; use it.
- **Infinite-scroll collection pagination.** Storefronts that replace numbered pagination with an infinite-scroll script frequently leave no crawlable link to collection page 2, so products deep in a large collection are discoverable only via the sitemap. Keep the paginated links in the DOM as a fallback.
- **App-injected widgets.** Review, wishlist and upsell apps inject anchors with placeholder hrefs. These are often non-navigational and belong as buttons, but you usually cannot edit the app's output; report it to the app vendor and confirm whether the widget hides real destinations.

## What crawlable-anchors pitfalls should I avoid?

- **Do not add `href="#"` to silence the "anchor without href" case.** Both fail. The audit wants a destination, not an attribute.
- **Do not turn a control into a link.** An accordion or modal trigger given a fake URL passes nothing and breaks the semantics. `<button>` is the right element, and buttons are not audited here.
- **Do not rely on the XML sitemap to compensate.** A sitemap helps discovery, but it passes no internal link signal and no anchor text, and Google treats it as a hint rather than a guarantee.
- **Do not assume "Google renders JavaScript" covers it.** Google does render pages, and it still extracts links from `href` attributes rather than by simulating clicks. Rendering does not turn a click handler into a followable link.
- **Do not fix the `href` and leave the anchor text generic.** "Click here" with a real URL passes this audit and wastes the relevance signal. See [link-text](/audits/link-text).
- **Do not use `role="link"` on a `div` and call it done.** It fixes the announcement for screen readers and nothing for crawlers.

## How do I verify the fix?

1. Re-run Lighthouse or PageSpeed Insights. The `crawlable-anchors` audit should move into "Passed audits".
2. Check the rendered DOM, not the source: in DevTools, run `$$('a')` in the console and inspect the `href` values. `$$('a:not([href]), a[href=""], a[href="#"], a[href^="javascript:"]')` returns exactly the failing set.
3. Use Google Search Console's URL Inspection on a page that should be reachable through the fixed link, and confirm it reports a referring page rather than "Discovered, currently not indexed".
4. Crawl the site with Screaming Frog or Sitebulb in JavaScript-rendering mode and compare the URLs found by crawling against the URLs in your sitemap. A large gap points at links the crawler could not follow.

## Related audits

- [Links do not have descriptive text](/audits/link-text), the anchor-text half of the same problem: crawlable, but carrying no signal
- [Links do not have a discernible name](/audits/link-name), the accessibility sibling for links with no accessible name
- [Document does not have a valid rel=canonical](/audits/canonical-tag), where the discovered URLs consolidate
- [Document does not have a valid hreflang](/audits/hreflang-tag), the other crawl-and-discovery audit

---

Audit your URL at https://lighthouse-md.com.
