Skip to content
Issue docs

Links to localhost, private IPs or staging hosts

Importantlink_to_local_or_staging_hostIssue 168

What is this issue?

A page links to an address that only works on a developer's machine, inside a private network, or on a staging environment:

  • http://localhost:3000/pricing
  • http://127.0.0.1/admin
  • http://192.168.1.20/report.pdf
  • http://printer.local/
  • https://staging.example.com/launch

For a public visitor every one of these is broken. localhost and 127.0.0.1 mean "my own computer", so the link opens the reader's machine, not yours. Private addresses (10.x, 172.16–31.x, 192.168.x) and link-local ones (169.254.x) only exist inside a local network. A staging host either does not answer the public, or answers with a copy of the site that was never meant to be seen — or indexed.

For a page to pass this check:

  • No link on it points at localhost, a private or link-local IP address, a local-only name (.local, .test, .invalid, a bare name like http://intranet/), or a staging host.

Example: content written in a staging CMS and published with the staging domain still in the links: https://staging.example.com/blog/launch instead of https://example.com/blog/launch.

Why it matters

  • The link is broken for everyone. No visitor can reach localhost on your server; they reach their own machine, which almost certainly has nothing there.

  • It leaks addresses you would rather keep private. A staging hostname or an internal IP in a public page tells anyone reading the source where your non-production systems live.

  • Search engines can index staging. A link from a public page is exactly how a crawler discovers a staging site. Once found, its pages can be indexed and compete with your real ones as duplicates.

  • It usually points at a process gap. Links like these arrive from content drafted on staging, environment variables that were not set at build time, or copy-pasted development URLs. One occurrence is rarely the only one.

Effect on the health score

This is an important issue under the Link Integrity lens. It needs no link checking, so it is reported on every plan.

How to fix it

  1. Replace each address with its public, production equivalent.

    <!-- Before -->
    <a href="http://localhost:3000/pricing">See pricing</a>
    <a href="https://staging.example.com/blog/launch">Read the launch post</a>
    
    <!-- After -->
    <a href="/pricing">See pricing</a>
    <a href="https://example.com/blog/launch">Read the launch post</a>
  2. Prefer root-relative links for your own pages. /pricing works on every environment and can never point at the wrong one.

  3. Fix the source, not just the page. Check where the address came from: a CMS "site URL" setting on staging, a build-time environment variable such as NEXT_PUBLIC_SITE_URL or BASE_URL left at its development default, or content copied from a staging preview.

  4. Search the whole site. Search the database or content export for localhost, 127.0.0.1, 192.168., and your staging hostname; one occurrence rarely travels alone.

  5. Keep staging out of search. Put staging behind authentication, or at least serve X-Robots-Tag: noindex on every staging response.

Examples

Example 1: A development URL left in a button

Fails (loopback):

<a class="button" href="http://localhost:3000/signup">Start free trial</a>

Passes:

<a class="button" href="/signup">Start free trial</a>

Fails (staging-host), on https://example.com/blog:

<a href="https://staging.example.com/blog/launch">our launch post</a>

Example 3: A file on an office network

Fails (private-ip):

<a href="http://192.168.1.20/files/price-list.pdf">Download the price list</a>

Example 4: Your own development environment

Fails (staging-host), on https://example.com:

<a href="https://dev.example.com/docs">Documentation</a>

Example 5: Someone else's developer portal

Passes, on https://example.com:

<a href="https://dev.vendor.com/docs">Vendor API docs</a>

dev. only counts on your own domain.

Example 6: A company whose name is the word

Passes:

<a href="https://stage.com/">Stage, our event partner</a>

stage.com is a registrable domain, not a stage subdomain.

How PixyScan detects this

  1. Reads every external link the crawl recorded. Any link to a host other than the page's own is stored as an external link.

  2. Classifies each distinct address, without requesting it. The decision is made from the address alone:

    Reason Matches
    loopback localhost, *.localhost, 127.0.0.0/8, ::1
    private-ip 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, IPv6 fc00::/7
    link-local 169.254.0.0/16, IPv6 fe80::/10
    local-name *.local, *.test, *.invalid, 0.0.0.0, a single-label host such as http://intranet/
    staging-host see below
  3. The staging rule is deliberately conservative. A host is a staging host when one of its subdomain labels is staging, stage, stg, preprod or uat, whole or as a hyphenated part (shop-staging.example.com, myapp-staging.herokuapp.com). The registrable domain itself is never read, so a company whose site is stage.com is not reported. Labels like beta, sandbox, preview, qa and demo are not used: they are routinely public.

  4. dev. and test. only count on your own domain. dev.example.com linked from example.com is your development environment. dev.some-vendor.com is usually that vendor's public developer portal, so it is not reported.

  5. A staging site is not judged against itself. If the site being scanned is itself a staging host, the staging rule is switched off for that scan — a staging environment linking to staging is expected. The localhost and private-address rules still apply.

  6. One finding per page. Each page carrying such links gets one finding listing them (up to 20, with the full count), each with its reason.

  7. Never requests the unreachable ones. Localhost, private, link-local and local-only addresses are excluded from external link checking entirely: a request from PixyScan to them would reach PixyScan's own network, not yours. They do not use your link-check allowance. Staging hosts are public and are still checked like any other link.

  8. Respects your lens settings. Nothing is raised on a scan with the Link Integrity lens switched off.

What we store

Storage Level

Page Level


Database Table / Prisma Model

PageExternalLink (page_external_links). The finding is stored on audit_issues.details.


Fields Used

Field Type Description
page_external_links.external_url String The address linked to. The whole check is decided from it
page_external_links.url_id String The page carrying the link, and the page the finding is raised against
sites.url String The scanned site's own address, for the "same domain" and "staging site" rules. Falls back to the first page the crawl recorded

Unreachable addresses (loopback, private, link-local, local-only) are never requested, so their status_code stays null ("not checked").


Stored Fields on the finding

Field Type Description
message String How many such links the page carries
recommendation String What to change
links Array Up to 20 of { externalUrl, reason }, sorted by address. reason is loopback, private-ip, link-local, local-name or staging-host
linkCount Int How many distinct such addresses the page links to
truncated Boolean True when there were more than 20

Detection Dependencies

  • External Links — every link to another host recorded by the crawl
  • Site configuration — the site's own address

Further reading