squirrelscan
GitHub

tel: and mailto: links: format and href/text mismatches

A phone link whose href differs from the number on screen dials the wrong person. squirrel validates both formats and compares them with the visible text.

tel: and mailto: are URI schemes that hand a phone number or an email address to the device’s own application. RFC 3966 defines the phone form: it requires the global form, a leading + and the full number, whenever the number can be represented that way, permits the visual separators -, ., ( and ), and requires a phone-context parameter for a local number. RFC 6068 defines the mail form, including the ?subject= and ?body= parameters.

The failure that matters is a mismatch. When the href and the visible text carry different numbers or addresses, tapping the link contacts someone other than the person shown on screen. That is usually an edit applied to one and not the other.

What squirrel checks

The rule runs on every crawled page over every a[href] starting with tel: or mailto:, and over Cloudflare’s email address obfuscation links (/cdn-cgi/l/email-protection, on any host), which a browser turns into mailto: addresses. Those count as mailto: links. The address is decoded from the link’s fragment or its data-cfemail attribute and checked like any other, and a link whose address cannot be decoded is counted and never reported as invalid or mismatched. Six check names are possible and at most five appear in one result, since the last only fires when neither link type is present:

  • tel-links warns when a phone value has no digit or +, or is shorter than seven characters. Message: <invalid>/<total> tel: link(s) may be invalid. It passes otherwise with <total> tel: link(s) found.
  • mailto-links warns when an address is present but has no @ or no dot. Message: <invalid>/<total> mailto: link(s) may be invalid. It passes otherwise with <total> mailto: link(s) found. Format validation is skipped only for an empty recipient followed by a ?, the mailto:?subject=… share pattern; a bare mailto: with nothing after it still warns.
  • tel-mismatch warns when the link text contains three or more consecutive digits that do not match the href. Message: <count> tel link(s) have mismatched phone number in href vs display text, each item labelled href "<href number>" ≠ text "<link text>" with a markup snippet.
  • tel-trunk-prefix reports at info status when the link text writes the optional trunk prefix (0) after a calling code, as in +49 (0)6061 630 980 0, and the number without that 0 matches the href. Message: <count> tel link(s) show the optional trunk prefix (0) in an international number; drop it (ITU-T E.123), each item labelled text "<link text>": use "<international form>" with a markup snippet. The link dials the number shown, so this is formatting advice rather than a mismatch, and such a link is never also reported under tel-mismatch. It does not count toward the score and is not listed as an issue in the report. Digit grouping does not matter here, so +33 (0)1 23 45 67 89 qualifies although it has no three consecutive digits.
  • mailto-mismatch warns when the link text looks like an email address and differs from the href, compared case-insensitively. Message: <count> mailto link(s) have mismatched email in href vs display text.
  • tel-mailto reports info when the page has neither kind of link. Message: No tel: or mailto: links found.

Numbers match on an exact digit comparison, on either digit string ending with the other, or on a trunk-prefix comparison. That last one drops a single leading zero from either side and accepts the pair when the shared suffix is at least five digits and the other number carries exactly two or three extra leading digits, the length of a country calling code. It works in both directions and does not require a literal +, so +61414123456 and 0414 123 456 are not a mismatch. Severity for the rule is info, weight 2.

The (0) reading applies only to the link text, never to the href, and only to calling codes whose national trunk prefix is 0 and is dropped when dialling from abroad, according to the ITU-T list of dialling procedures: +31 (Netherlands), +32 (Belgium), +33 (France), +41 (Switzerland), +43 (Austria), +44 (United Kingdom), +49 (Germany), +61 (Australia), +64 (New Zealand) and +353 (Ireland). For every other calling code the text is read literally. Italy (+39), for example, has no trunk prefix: its numbers keep the leading 0 after the country code, so +39 (0)6 1234 567 against tel:+3961234567 is still a mismatch. A German display number that differs from the href in any other digit is a mismatch too.

How to fix it

<a href="tel:+61292223333">(02) 9222 3333</a>
<a href="mailto:sales@example.com">sales@example.com</a>

Put the E.164 form in the href and the locally readable form in the text. Generate both from one stored value so an edit cannot change only one of them. In international notation leave the trunk prefix out of the text as well: +49 6061 630 980 0, not +49 (0)6061 630 980 0.

A share link with no recipient, mailto:?subject=Look%20at%20this, is valid and is deliberately not validated.

Rule IDlinks/tel-mailto
CategoryLinks
ScopePer-page
Severityinfo
Weight2/10

Enable / disable

Disable this rule

[rules]
disable = ["links/tel-mailto"]
[rules]
disable = ["links/*"]

Enable only this rule

[rules]
enable = ["links/tel-mailto"]

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.

Links 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 Links section of the report. Every mismatch is listed with the href, the visible text and a markup snippet. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.

Type to search…

↑↓ navigate↵ openesc close