Back to Blog

How to Find Pages That Link to a URL: Practical Guide

Daniel Zhao

Aug 26, 2026 · Guides · 19 min read

TL;DR

If you need pages that link to one target URL, start with Google Search Console for a site you own, use a site crawler for internal links, and use a page-level backlink index for external or competitor links. Treat every export as a candidate list: open important source pages and verify the live <a href> link, redirect, canonical target, anchor text, and rel attributes before counting it.

backlink-verification-workflow-rola-ip

The phrase can describe three different jobs:

Job What you are looking for Best starting point
Internal inlinks Pages on your own domain that point to a target page Search Console or a site crawler
External backlinks Pages on other domains that cite the target A page-level backlink index, followed by manual verification
URL inventory Every discoverable page on a domain Sitemap, robots.txt, and a bounded crawler

An inventory is not a backlink report. A sitemap can list a page without any HTML page linking to it, and a crawler that follows links can miss an orphan page. Keep source_url (the page containing the link) and target_url (the page being linked to) as separate fields throughout the workflow.

Choose a method before you collect data

Use the smallest method that can answer the question you actually have.

Situation Method What you get Main limitation
You own the property and need a fast first check Google Search Console Links report Google’s view of linking sites/pages for the property It is not a complete backlink database; canonical URLs and duplicates are grouped, and the report is limited
You need all known internal inlinks Site crawler Source page, anchor, link location, response status, and crawl date Scope, robots rules, JavaScript, and orphan pages affect coverage
You need competitor or external referring pages Page-level backlink index Candidate referring pages, domains, anchors, and historical records Index freshness and coverage vary; verify high-value rows on the source page
You have fewer than a few dozen candidates Search operators plus manual HTML checks Quick leads from indexed pages A mention in search results is not proof of a live hyperlink

Prepare a reproducible, authorized run

Before opening a tool or writing code, record:

  • The exact target URL and whether a redirect or canonical URL should count as a match.
  • Whether the source set is internal, external, or both.
  • Your authorization to fetch the source pages. Only verify public pages you are allowed to access, and follow the site’s terms and machine-readable rules.
  • A run date, user agent, timeout, request delay, and maximum number of source pages.
  • The fields you will retain: raw source URL, final source URL, target URL, anchor text, rel, HTTP status, source canonical, verification status, and timestamp.

For a small pilot, use one owned target page and 20–50 candidate source pages. Fetch at most 20 pages in the first pass, wait about one second between requests to the same host, stop on repeated errors, and keep a separate row for blocked, missing, redirected, and unverified pages.

This route is for a property you control.

  1. Open the verified property in Search Console and open the Links report.
  2. In External links, open Top linked pages and select the target page you want to audit.
  3. Open Top sites linking to this page to see the sites that link to the selected page. Export the table when the report offers an export option.
  4. Keep the export date and the selected target URL with the file. Search Console groups pages by canonical URL, combines duplicate links, and does not promise a complete list. Its link tables are limited to 1,000 rows, so treat the result as triage data rather than a census.
  5. For an internal-link audit, compare this view with a crawl of your own site. Search Console’s external-link view is not a substitute for a source-page crawl.

search-console-links-report-rola-ip

Search Console is useful because it is first-party data for your property, but a missing row does not prove that a page has never linked to the target. The source may be outside Google’s current data, the page may be canonicalized differently, or the link may have changed since Google last processed it.

A crawler answers a narrower question well: “Which pages in this permitted crawl contain an anchor to my target?”

Configure the crawl to:

  • Start from the site’s homepage, sitemap URLs, or a supplied URL list.
  • Stay inside the approved host and URL paths.
  • Respect robots.txt and set a conservative delay and concurrency.
  • Record the raw href, resolved target, anchor text, rel, source location, HTTP status, and crawl timestamp.
  • Include a rendered pass only when the site owner has authorized it and static HTML does not contain the link.

Scrapy’s LinkExtractor can filter domains, tags, attributes, and duplicate links. Its link object exposes the absolute URL, text, fragment, and whether nofollow appears in rel. Scrapy’s generated projects also provide a ROBOTSTXT_OBEY setting; keep that behavior enabled for an ordinary audit.

The result is still bounded by your crawl. A page that is not linked from your seeds, is blocked by robots.txt, or renders its navigation only after JavaScript executes may not appear. Label such rows as “not crawled” or “unverified,” not “no backlink.”

Method 3: Find external or competitor referring pages

For external backlinks, use a tool that exposes page-level referring URLs rather than only a domain-level count. Export the target URL, source page, anchor, first/last seen dates (if supplied), link type, and any status or redirect fields.

Use the same target URL and filters across tools if you are comparing them. Do not compare one provider’s live index with another provider’s historical index and call the larger number “better.” A useful test is:

  1. Export the top 20–50 referring pages for the same target.
  2. Deduplicate by normalized source URL.
  3. Open a fixed sample of 10–20 sources.
  4. Search the rendered page or source for the target and inspect the surrounding text.
  5. Record whether the link is live, redirected, canonicalized, nofollowed, JavaScript-only, or absent.

Ahrefs’ manual-check guidance follows this same principle: a reported backlink is a lead; the referring page is where you confirm the target, anchor, and context. That verification step is the difference between “the index says a link existed” and “the page currently contains a link.”

Method 4: Use search operators for candidate discovery

Search operators are useful when the list is short or you need to find likely mentions before checking them manually. Try a quoted target URL, a distinctive URL path, or a brand-plus-topic phrase. Combine that with site: when you are checking one domain.

Operators discover indexed pages and text. They do not expose a complete backlink graph, and a result can contain a plain-text mention, a redirect, a removed link, or no link at all. Open each candidate, inspect the page HTML, and save the verification status. Do not use the old link: operator as a completeness promise.

Prerequisites and Environment

The verifier uses only the Python standard library and is written for Python 3.10 or newer. Python 3.10 is required for the str | None type syntax used in the annotations.

Check the version before running:

python --version

On Windows, run the commands in PowerShell. On macOS or Linux, use a POSIX shell and replace PowerShell environment-variable syntax with export:

export TARGET_URL="https://example.com/guide"
export SOURCE_FILE="sources.txt"
export OUTPUT_FILE="verified_links.csv"
python verify_links.py

On Windows, the equivalent is:

$env:TARGET_URL = "https://example.com/guide"
$env:SOURCE_FILE = "sources.txt"
$env:OUTPUT_FILE = "verified_links.csv"
python verify_links.py

Create sources.txt as a UTF-8 plain-text file, with one absolute http:// or https:// source-page URL per line. Blank lines, invalid schemes, and duplicate normalized URLs are skipped. Do not place usernames, passwords, API keys, or proxy credentials in this file.

The pilot defaults are intentionally conservative: a 15-second timeout, a 2 MB HTML read limit, one request per second, and no automatic retry. Change those values only after confirming that you are authorized to collect the pages and that the target’s rules allow the traffic.

The package also includes a safe local fixture in demo/ so you can reproduce the sample without contacting a customer site. In a second PowerShell window, serve it with python -m http.server 8765 --directory demo, then run the verifier with TARGET_URL="http://127.0.0.1:8765/target.html", SOURCE_FILE="demo\sources.txt", and OUTPUT_FILE="verified_links.csv". The fixture contains one verified link and one intentional not_found_in_html row; stop the local server when the run is complete.

Output Schema and Sample CSV

The script keeps the URL representations separate:

Field Meaning
source_url URL supplied in sources.txt
final_source_url Source URL after HTTP redirects
source_canonical <link rel="canonical"> found on the source page
target_url Normalized URL requested through TARGET_URL
target_final_url Target URL after the one-time target fetch and redirects
target_canonical Canonical URL found on the target page
raw_href The exact matching href attribute from the source HTML
matched_url Normalized URL produced from raw_href
anchor_text Visible text inside the matching anchor
rel Raw rel value, such as nofollow, ugc, or sponsored
link_location Approximate HTML region such as header, nav, main, article, or footer
found_at UTC timestamp when that source row was verified
verification_status verified, not_found_in_html, blocked_by_robots, stale, or another review state

For example, a redacted run can look like this:

source_url,final_source_url,http_status,target_url,target_final_url,target_canonical,target_resolution_status,matched_url,raw_href,anchor_text,rel,link_location,source_canonical,found_at,verification_status,error
https://docs.example.test/source-a,https://docs.example.test/source-a,200,https://example.test/guide,https://example.test/guide,https://example.test/guide,resolved,https://example.test/guide,/guide#overview,read the guide,,article,https://docs.example.test/source-a,2026-08-26T08:10:00Z,verified,
https://docs.example.test/source-b,https://docs.example.test/source-b,200,https://example.test/guide,https://example.test/guide,https://example.test/guide,resolved,,,,,,https://docs.example.test/source-b,2026-08-26T08:10:01Z,not_found_in_html,

The sample is illustrative and uses reserved .test domains; it is not a benchmark. In a real run, keep the raw and normalized values together so a redirect or canonical mismatch can be reviewed instead of silently merged.

Verify candidate pages with Python

The following standard-library script verifies a bounded list of known candidate source pages. It does not discover the public web, bypass access controls, or solve CAPTCHAs. It checks robots.txt, applies a 15-second timeout, reads at most 2 MB of HTML, resolves relative URLs, removes fragments for comparison, and writes a reviewable CSV.

Save the script as verify_links.py, then create a sources.txt file with one authorized source URL per line.

#!/usr/bin/env python3
"""Verify links from a bounded list of public, authorized source pages."""

from __future__ import annotations

import csv
import os
import time
from datetime import datetime, timezone
from html.parser import HTMLParser
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urldefrag, urljoin, urlsplit, urlunsplit
from urllib.request import Request, urlopen
from urllib.robotparser import RobotFileParser

USER_AGENT = "AuthorizedLinkVerifier/1.0 (+https://example.com/contact)"
TIMEOUT_SECONDS = 15
MAX_BYTES = 2_000_000
DELAY_SECONDS = 1.0


def normalize_url(raw_url: str, base_url: str | None = None) -> str:
    if not raw_url:
        return ""
    absolute = urljoin(base_url or "", raw_url.strip())
    absolute, _fragment = urldefrag(absolute)
    try:
        parts = urlsplit(absolute)
        port = parts.port
    except ValueError:
        return ""
    if parts.scheme not in {"http", "https"} or not parts.hostname:
        return ""
    host = parts.hostname.lower()
    default_port = (parts.scheme == "http" and port == 80) or (
        parts.scheme == "https" and port == 443
    )
    netloc = host if not port or default_port else f"{host}:{port}"
    return urlunsplit((parts.scheme, netloc, parts.path or "/", parts.query, ""))


class LinkParser(HTMLParser):
    REGION_TAGS = {"header", "nav", "main", "article", "aside", "footer"}

    def __init__(self, base_url: str) -> None:
        super().__init__(convert_charrefs=True)
        self.base_url = base_url
        self.links: list[dict[str, str]] = []
        self.canonical_url = ""
        self._current: dict[str, str | list[str]] | None = None
        self._region_stack: list[str] = []

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        values = {key.lower(): (value or "") for key, value in attrs}
        tag_name = tag.lower()
        if tag_name in self.REGION_TAGS:
            self._region_stack.append(tag_name)
        if tag_name == "a":
            self._current = {
                "raw_href": values.get("href", ""),
                "anchor_text": [],
                "rel": values.get("rel", ""),
                "link_location": self._region_stack[-1] if self._region_stack else "document",
            }
        elif tag_name == "link" and "canonical" in values.get("rel", "").lower().split():
            self.canonical_url = normalize_url(values.get("href", ""), self.base_url)

    def handle_data(self, data: str) -> None:
        if self._current is not None:
            text_parts = self._current["anchor_text"]
            assert isinstance(text_parts, list)
            text_parts.append(data)

    def handle_endtag(self, tag: str) -> None:
        if tag.lower() == "a" and self._current is not None:
            text_parts = self._current["anchor_text"]
            assert isinstance(text_parts, list)
            self.links.append(
                {
                    "raw_href": str(self._current["raw_href"]),
                    "target_url": normalize_url(
                        str(self._current["raw_href"]), self.base_url
                    ),
                    "anchor_text": " ".join("".join(text_parts).split()),
                    "rel": str(self._current["rel"]),
                    "link_location": str(self._current["link_location"]),
                }
            )
            self._current = None
        if tag.lower() in self.REGION_TAGS and tag.lower() in self._region_stack:
            index = len(self._region_stack) - 1 - self._region_stack[::-1].index(tag.lower())
            self._region_stack.pop(index)


def robots_allows(source_url: str) -> tuple[bool, str]:
    parts = urlsplit(source_url)
    robots_url = urlunsplit((parts.scheme, parts.netloc, "/robots.txt", "", ""))
    parser = RobotFileParser(robots_url)
    try:
        parser.read()
    except HTTPError as exc:
        if exc.code == 404:
            return True, "no_robots_file"
        return False, f"robots_http_{exc.code}"
    except (OSError, URLError):
        return False, "robots_unavailable"
    if not parser.can_fetch(USER_AGENT, source_url):
        return False, "blocked_by_robots"
    return True, "allowed"


def utc_now() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def resolve_target_metadata(target_url: str) -> dict[str, str]:
    """Resolve the target once and extract its HTML canonical, if available."""
    metadata = {
        "target_url": normalize_url(target_url),
        "target_final_url": "",
        "target_canonical": "",
        "target_resolution_status": "not_attempted",
    }
    if not metadata["target_url"]:
        metadata["target_resolution_status"] = "invalid_target"
        return metadata
    allowed, policy_status = robots_allows(metadata["target_url"])
    if not allowed:
        metadata["target_resolution_status"] = policy_status
        return metadata
    request = Request(
        metadata["target_url"],
        headers={
            "User-Agent": USER_AGENT,
            "Accept": "text/html,application/xhtml+xml;q=0.9,*/*;q=0.1",
        },
    )
    try:
        with urlopen(request, timeout=TIMEOUT_SECONDS) as response:
            metadata["target_final_url"] = normalize_url(response.geturl())
            if response.headers.get_content_type() not in {"text/html", "application/xhtml+xml"}:
                metadata["target_resolution_status"] = "non_html"
                return metadata
            body = response.read(MAX_BYTES).decode(
                response.headers.get_content_charset() or "utf-8", errors="replace"
            )
    except HTTPError as exc:
        metadata["target_resolution_status"] = f"http_{exc.code}"
        return metadata
    except (OSError, URLError, TimeoutError) as exc:
        metadata["target_resolution_status"] = type(exc).__name__
        return metadata
    parser = LinkParser(metadata["target_final_url"] or metadata["target_url"])
    try:
        parser.feed(body)
    except Exception as exc:
        metadata["target_resolution_status"] = f"parse_{type(exc).__name__}"
        return metadata
    metadata["target_canonical"] = parser.canonical_url
    metadata["target_resolution_status"] = "resolved"
    return metadata


def verify_source(source_url: str, target_metadata: dict[str, str]) -> dict[str, str]:
    found_at = utc_now()
    target_url = target_metadata["target_url"]
    result = {
        "source_url": source_url,
        "final_source_url": "",
        "http_status": "",
        "target_url": target_url,
        "target_final_url": target_metadata["target_final_url"],
        "target_canonical": target_metadata["target_canonical"],
        "target_resolution_status": target_metadata["target_resolution_status"],
        "matched_url": "",
        "raw_href": "",
        "anchor_text": "",
        "rel": "",
        "link_location": "",
        "source_canonical": "",
        "found_at": found_at,
        "verification_status": "",
        "error": "",
    }
    allowed, policy_status = robots_allows(source_url)
    if not allowed:
        result["verification_status"] = policy_status
        return result

    request = Request(
        source_url,
        headers={
            "User-Agent": USER_AGENT,
            "Accept": "text/html,application/xhtml+xml;q=0.9,*/*;q=0.1",
        },
    )
    try:
        with urlopen(request, timeout=TIMEOUT_SECONDS) as response:
            result["final_source_url"] = response.geturl()
            result["http_status"] = str(getattr(response, "status", ""))
            if response.headers.get_content_type() not in {"text/html", "application/xhtml+xml"}:
                result["verification_status"] = "non_html"
                return result
            body = response.read(MAX_BYTES).decode(
                response.headers.get_content_charset() or "utf-8", errors="replace"
            )
    except HTTPError as exc:
        result["http_status"] = str(exc.code)
        result["verification_status"] = "fetch_error"
        result["error"] = f"HTTP {exc.code}"
        return result
    except (OSError, URLError, TimeoutError) as exc:
        result["verification_status"] = "fetch_error"
        result["error"] = type(exc).__name__
        return result

    parser = LinkParser(result["final_source_url"] or source_url)
    try:
        parser.feed(body)
    except Exception as exc:
        result["verification_status"] = "parse_error"
        result["error"] = type(exc).__name__
        return result

    result["source_canonical"] = parser.canonical_url
    target_keys = {
        key
        for key in {
            target_metadata["target_url"],
            target_metadata["target_final_url"],
            target_metadata["target_canonical"],
        }
        if key
    }
    for link in parser.links:
        if link["target_url"] in target_keys:
            result["matched_url"] = link["target_url"]
            result["raw_href"] = link["raw_href"]
            result["anchor_text"] = link["anchor_text"]
            result["rel"] = link["rel"]
            result["link_location"] = link["link_location"]
            result["verification_status"] = "verified"
            return result
    result["verification_status"] = "not_found_in_html"
    return result


def main() -> None:
    target_url = os.environ.get("TARGET_URL", "").strip()
    source_file = Path(os.environ.get("SOURCE_FILE", "sources.txt"))
    output_file = Path(os.environ.get("OUTPUT_FILE", "verified_links.csv"))
    target_metadata = resolve_target_metadata(target_url)
    if not target_metadata["target_url"]:
        raise SystemExit("Set TARGET_URL to a valid http(s) URL before running.")
    if not source_file.exists():
        raise SystemExit(f"Source file not found: {source_file}")

    fields = [
        "source_url", "final_source_url", "http_status", "target_url",
        "target_final_url", "target_canonical", "target_resolution_status",
        "matched_url", "raw_href", "anchor_text", "rel", "link_location",
        "source_canonical", "found_at", "verification_status", "error",
    ]
    seen: set[str] = set()
    with source_file.open("r", encoding="utf-8") as handle, output_file.open(
        "w", newline="", encoding="utf-8"
    ) as output:
        writer = csv.DictWriter(output, fieldnames=fields)
        writer.writeheader()
        for line in handle:
            source_url = line.strip()
            source_key = normalize_url(source_url)
            if not source_key or source_key in seen:
                continue
            seen.add(source_key)
            writer.writerow(verify_source(source_url, target_metadata))
            output.flush()
            time.sleep(DELAY_SECONDS)


if __name__ == "__main__":
    main()

Run it in PowerShell without putting credentials in the command line:

$env:TARGET_URL = "https://example.com/guide"
$env:SOURCE_FILE = "sources.txt"
$env:OUTPUT_FILE = "verified_links.csv"
python verify_links.py

The expected output is a CSV row for each unique source URL. A verified row contains the normalized matching target, the original raw_href, anchor text, rel, approximate link location, source canonical, target final URL, target canonical, and UTC found_at timestamp. not_found_in_html means the fetched static HTML did not contain any accepted target representation; it does not prove that a JavaScript-rendered page never links to it. blocked_by_robots, robots_unavailable, non_html, and fetch_error are review states, not zero-link evidence.

Redacted local Python fixture run showing the CSV output fields and verification statuses

Original local-fixture capture, not a production benchmark. It shows python verify_links.py, verified_links.csv, http_status, raw_href, anchor_text, rel, link_location, found_at, and verification status. Captured 2026-08-26; Rola IP watermark included.

The script uses Python’s urljoin to resolve relative links. Because urljoin accepts an absolute href, the code validates the resulting scheme and host before treating it as a comparison key. The RobotFileParser.can_fetch() check is a permission signal, not a legal authorization decision.

Keep these values separate when they differ:

  1. The raw href found in the source page.
  2. The normalized target_url used for comparison.
  3. The source page’s final_source_url after redirects.
  4. The source page’s source_canonical value.
  5. The target page’s target_final_url after the one-time target fetch.
  6. The target page’s target_canonical value.

Do not silently replace one with another. A redirect may be a valid link to the intended resource, while a canonical tag is only a page preference. The script accepts a match against the requested, redirected, or canonical target representation and records which normalized matched_url was found. Decide whether that policy fits your audit before counting results, and expose the policy in the CSV.

For JavaScript-rendered links, first save the static result as not_found_in_html. If you own or are authorized to test the page, run a browser-rendered pass and record rendered_html as a separate method. Do not increase concurrency or add stealth behavior to force a response from a site that is blocking your client.

Cost and Scale Model

There is no universal winner because these methods return different kinds of data and bill different units. Use one worksheet with 1,000 target/source queries as the denominator. Count both candidate rows and rows verified live; do not substitute a provider’s domain count for a page count.

Method Cash cost per 1,000 queries Result quantity per 1,000 queries Cache / repeat runs Failed or empty query billing Safe concurrency and maintenance
Search Console N/A as a 1,000-query API unit; report export has no per-request charge Google’s sampled rows for an owned property; candidate and live counts are not equivalent Export date matters; no user-controlled cache No per-request charge, but report limits can hide rows Low engineering cost; UI and limits need monitoring
Site crawler Infrastructure and operator cost for 1,000 source fetches Pages discovered inside the seed/scope; verified live rows require a second check Local cache is optional; record cache age HTTP errors consume crawl time, not a provider credit Moderate concurrency; robots, deduplication, and JavaScript maintenance
Backlink index/API Provider’s published price converted to 1,000 target/source lookups or rows Indexed referring pages; report candidate and verified-live counts separately Provider freshness and cache rules vary Mark unknown when empty, failed, or retried requests are billed differently High scale with low code, but schema, freshness, and price need monitoring
Search operators Search-request price (if any) plus analyst time for 1,000 checks Indexed candidates, not verified links Search results change; no stable cache contract Empty/noisy results cost analyst time Low setup, high manual maintenance
Python verifier Your compute, bandwidth, and operator cost for 1,000 source fetches Verified rows from the supplied candidate list You control the local cache and retry policy Timeouts and blocked pages still consume requests Low-to-moderate scale; code and HTML edge cases need tests

Normalize every method to the same columns: cash_cost, candidate_rows, verified_live_rows, cache_age, failed_query_charge, safe_concurrency, and maintenance_hours, all measured over 1,000 queries. If a provider does not disclose comparable billing, cache, or failed-request rules, write “unknown” rather than estimating it—and do not announce a winner.

How to Run a Result Quality Test

Run the same 10–20 target/source checks through each method you plan to use. Compare:

  • Field completeness: percentage of rows with source URL, target URL, anchor, rel, and status.
  • Verification precision: verified live links divided by candidate rows.
  • Duplicate rate: repeated source-target pairs after normalization.
  • Empty-result rate: runs that return no candidates when a known link exists.
  • Position or freshness difference: for search-derived candidates, note ranking/ordering changes; for backlink tools, record first/last seen dates when supplied.
  • JavaScript-only rate: links found only after rendering.

Use one fixed sample, one country, one target-URL policy, one timeout, and one retry rule. This section defines a test method; it does not claim a benchmark result for any provider. Report the numbers as your sample’s result, not as a universal provider ranking.

Coverage, Freshness, and Recheck Schedule

Treat every source as a different observation window:

Source Suggested recheck What to record
Search Console Monthly for an active site; after a major migration Export date, selected target, row count, and canonical grouping
Backlink index At the provider’s documented refresh cadence or before a campaign decision Provider, target mode, first/last seen values, index date, and export filters
Authorized crawler After template changes, releases, or at least monthly for high-value sites Crawl scope, robots result, page count, status distribution, and cache age
Python verifier Immediately after a migration or link-remediation batch; then on a defined cadence found_at, response status, redirect chain, HTML verification, and retry policy

Use stable status labels so a dashboard does not turn a policy block into a missing link:

  • verified_live: the source HTML currently contains an accepted target representation.
  • stale: an earlier report found the link, but the latest check is outside the freshness window.
  • redirected: the source or target resolves to a different URL and needs review.
  • blocked: robots, authorization, or an access response prevented verification.
  • not_found_in_html: fetched static HTML did not contain the target.
  • unverified: the source was not checked or the result requires a rendered/authorized pass.

The Python script writes found_at for each verification row. A backlink database’s last_seen date is a different field and should not be renamed to found_at.

Privacy, authorization, and the limited role of a proxy

Do not upload confidential URL lists to a free extractor. Verify only public pages you are authorized to collect, follow robots.txt and site terms, and keep credentials out of URLs, logs, screenshots, and CSVs.

Rola IP is not a backlink index and cannot prove that a source page links to a target. It may be considered for an authorized regional QA or high-volume public-page collection workflow where the target site allows the traffic. Rola documents describe country targeting on rotating residential, datacenter, and mobile networks, with state/city targeting available on rotating residential; session parameters can preserve continuity when a test needs it. Those capabilities do not guarantee access, bypass CAPTCHAs, or override a site’s rules. Keep proxy routing separate from the verifier and validate the actual exit location before interpreting regional results. See the Rola IP proxy network guide, parameter reference, and Python integration guide for configuration details.

Common failures and fixes

Symptom Likely cause Fix
Search Console has fewer rows than a tool Canonical grouping, duplicate aggregation, or report limits Treat GSC as a first-party sample; verify important candidates elsewhere
A tool reports a link that is not on the page Index lag, deleted link, redirect, or canonical mismatch Fetch the source, follow redirects, inspect HTML, and label the row
The script returns not_found_in_html JavaScript-only link, wrong target variant, or page changed Compare normalized/redirect/canonical values; use permitted rendering if needed
Every row is robots_unavailable The site blocks or times out on robots.txt Stop and review authorization; do not disable the policy check to force a result
A crawler loops through filters or calendars URL parameters create crawl traps Set allow/deny rules, canonicalize carefully, and cap pages per host
Results mix mentions and links Search operators return text references Open the page and confirm a real <a href> element
A target looks different by country Locale, proxy exit, and page personalization differ Record country, method, timestamp, and exit IP; do not attribute differences to the link graph without a controlled test

What to Do After You Find the Linking Pages

Finding the source pages is the input to an SEO action, not the action itself. Sort the verified rows by business impact and link state:

  • A source points to an old URL: update the source to the preferred new URL when you control the page. Keep the redirect for external sources you cannot edit.
  • A source points to a 404 or soft-404 page: repair the source link or create a relevant replacement target. Do not redirect every broken URL to a generic homepage.
  • A high-traffic or high-conversion source is linking: prioritize it before low-value navigation links. Preserve the surrounding context and anchor intent when you update it.
  • Several canonical versions appear: choose one preferred target, update internal links to that version, and keep the raw and canonical values in the audit history.
  • The link uses nofollow, ugc, or sponsored: classify it separately from an ordinary followed link. It can still be useful for discovery and referral traffic, but it should not be reported as the same link type.
  • The source belongs to a competitor or a third party: treat it as a content, partnership, or outreach lead. It is not an internal-link task, and you should not edit or crawl beyond the access you are authorized to use.

A practical triage score can combine source traffic, source relevance, target status, link type, and remediation effort. Keep the score explainable; a giant authority metric without an action attached is just another export.

Find Pages Linking to an Old URL After a Migration

Migration audits need two target keys: the old URL and the preferred new URL.

  1. Export or crawl pages that reference the old URL before changing templates.
  2. Check the old URL’s HTTP response and record whether it returns a 301, 308, 404, or an unexpected 200 page.
  3. Verify whether the new URL is the target’s canonical and whether source pages already link to it directly.
  4. Update the internal source pages you control, then re-run the same candidate list.
  5. Keep external sources in a separate outreach or referral queue; do not assume that a redirect makes every external link equally valuable.

The before/after comparison should show fewer source pages linking to the old URL, more direct links to the new URL, no new 404s, and a stable canonical policy. A redirect is a routing signal; it is not a substitute for fixing important internal links.

Choose Your Next Step

  • You already use Google Search Console: export the selected target page’s linking data, then verify a fixed sample of source pages.
  • You are starting a new external SERP/backlink project: test one supported page-level backlink API against 10–20 known candidates and compare verification precision, freshness, fields, and cost before scaling.
  • You own the site or a limited domain: run a bounded crawler from the sitemap and key templates, then use the Python verifier for pages that matter to a migration, orphan-page, or internal-link audit.

Measurement After Publication

Track more than the head keyword. Review:

  • The number of related long-tail queries entering the top 20.
  • Downloads, copies, or repository clicks for verify_links.py.
  • Scroll depth through the Search Console, backlink-index, and Python sections.
  • Visits to the Rola proxy-configuration documentation as an assisted action, not the article’s only success metric.
  • Answer Share of Voice for “which pages link to this URL?” and “how do I verify a backlink?”
  • Organic registrations or MQLs attributed or assisted by the article.

Review at 30, 60, and 90 days. If the page gains long-tail impressions and code engagement but produces little commercial demand, keep it as a supporting SEO/data-audit article. If the broad query remains ambiguous, split the next article around the clearer jobs: “find internal links to a page” and “verify whether a backlink exists.”

Conclusion

The reliable way to find pages that link to a URL is a two-stage workflow: use the right discovery source for the job, then verify the source page. Search Console is a sensible first step for owned properties, crawlers expose internal inlinks, and backlink indexes help with external pages. A bounded Python verifier makes the final decision auditable without pretending that any single tool can see every link.

Frequently asked questions