squirrelscan
GitHub

localhost in production: dev hosts left in the markup

A link or image pointing at localhost or a preview deployment is broken for every visitor. squirrel classifies the host and says whether it is yours.

What development host leakage is

A development host in production markup is a URL that was correct on someone’s machine and is not correct on the live site. It reaches production through a hard-coded template value, a CMS field authored while working locally, or a base-URL environment variable that never got a production value.

The consequence depends on where it sits. In an href or a src it is a broken reference a visitor can hit right now. In body copy it is a mention rather than a break. A preview deployment URL is a third case: it usually resolves, so nothing looks broken, but it points visitors and crawlers at a copy of the site nobody is maintaining.

What squirrel checks

The rule runs on every crawled page and emits a single check named dev-leakage:

  • Skipped when there is no parsed document. Message: No document available.
  • Skipped when the page URL cannot be parsed. Message: Page URL could not be parsed.
  • Skipped when the audited origin is itself a development or local host, including a local-only name such as myapp.test or box.local. Message: Audited origin is itself a non-production host (localhost:3000), so its own URLs are not leakage.
  • Skipped when a work cap cut the scan short and the part scanned held nothing. Message: Page too large to scan in full; no development hosts in the part scanned.
  • Pass when the whole page was scanned and nothing was found. Message: No development, private or preview hosts referenced.
  • Warn or fail otherwise. Message: 3 development host reference(s) on a production page (localhost, preview-host): example http://localhost:3000/api.

Six kinds are classified: localhost for a loopback host, private-ip for a private address range, local-name for a name only a local machine or network resolves, preview-host for a deployment-preview domain, dev-subdomain for a development subdomain of the audited site, and insecure-self-link for an http:// link back to your own host. Each hit also records whether it came from an attribute or from body copy, and whether the host is unmistakably this site’s own.

A local-name is a Laravel Valet or Herd site such as myapp.test, a name under .example, .invalid, .local, .lan, .internal, .home.arpa, .localdomain or .home and the other local-only zones, or a single-label host such as intranet. A production page that links one, often a leftover from a CMS preview, points visitors at a site only its author can reach. The external link check never requests these names, so they also appear as info under refused-external-links in links/broken-external-links, and this rule is what makes them stand out: local-name always warns, because the local site could be anyone’s. In body copy a local name counts only when written as a URL with http:// or https://, since config.local or app.test is as often a file name. .onion and .alt names are not local names: they are reachable through Tor and other non-DNS resolvers.

The scan has work caps: 50,000 elements, 2,000 href and src values, 1,000 attribute hits, the first 500,000 characters of copy, and a limit on matches per pattern in that copy. The finding’s details carry scanTruncated, which is true when a cap cut the scan short, because a kind past the cap may be missing from its items. When a capped scan found nothing, the rule skips the page rather than passing it: a pass would say the page is clean, and on a re-audit it would close an earlier finding that may sit past the cap. The skip has skipReason: "scan-truncated", the page counts as not evaluated, and its earlier findings stay open.

The status is fail when a live href or src points at a host that is clearly this site’s own development artifact, since that is a reference a visitor can follow into nothing. An insecure-self-link is treated as soft and warns instead, as does any mention in copy and any preview host that could belong to someone else. Severity for the rule is warning, weight 6. The rule is skipped on soft 404 pages.

How to fix it

<a href="/api/docs">API documentation</a>

Replace the origin with a relative path where the target is on this site. That makes the reference correct in every environment at once, which is the actual fix; changing one absolute URL to the production one leaves the same bug in the next environment.

Point a staging or preview link at the production equivalent, and check whether that staging site is publicly indexable while you are there. Make an http:// self-link relative or https://, since the redirect it costs every visitor is avoidable.

Rule IDcontent/dev-leakage
CategoryContent
ScopePer-page
Severitywarning
Weight6/10

Enable / disable

Disable this rule

[rules]
disable = ["content/dev-leakage"]

Disable all Content rules

[rules]
disable = ["content/*"]

Enable only this rule

[rules]
enable = ["content/dev-leakage"]

enable replaces the default ["*"], so this is the only rule selected. Leave disable out: a rule that matches any disable pattern is skipped even when enable names it. Only an enabled setting under a rule’s own [rule_options] table overrides both lists.

Content findings ship in every audit next to the SEO, performance and agent experience rules. See Fix SEO issues with AI for how an agent works through a report.

References

Check your site

Run squirrel audit https://example.com and open the Content section of the report. Each page is listed with the host kinds found and an example URL. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.

Type to search…

↑↓ navigate↵ openesc close