<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mehmet</title>
    <description>The latest articles on DEV Community by Mehmet (@iceteameetyou).</description>
    <link>https://dev.to/iceteameetyou</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4102764%2F293e94f5-02fd-4fd0-b37b-c6056461fc38.png</url>
      <title>DEV Community: Mehmet</title>
      <link>https://dev.to/iceteameetyou</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iceteameetyou"/>
    <language>en</language>
    <item>
      <title>I measured how many websites actually publish a contact address</title>
      <dc:creator>Mehmet</dc:creator>
      <pubDate>Mon, 31 Aug 2026 12:41:40 +0000</pubDate>
      <link>https://dev.to/iceteameetyou/i-measured-how-many-websites-actually-publish-a-contact-address-2oh</link>
      <guid>https://dev.to/iceteameetyou/i-measured-how-many-websites-actually-publish-a-contact-address-2oh</guid>
      <description>&lt;p&gt;Every contact scraper on the market says roughly the same thing: it finds contact details on "almost any webpage." I went through six of them last week looking for a number — any number — and found exactly one. A tool called Website Email Extractor mentions "60-80% email hit rate" for business websites, buried in a tips section. Everyone else just says "almost any."&lt;/p&gt;

&lt;p&gt;So I measured mine and I'm publishing the number, including the part where I got it badly wrong.&lt;/p&gt;

&lt;p&gt;The short version&lt;br&gt;
Two samples, both scraped from a datacenter IP with no proxy, no browser, no login.&lt;/p&gt;

&lt;p&gt;On 100 real business websites — restaurants, law firms, dental practices, accountants, estate agents — pulled from OpenStreetMap entries that list a website:&lt;/p&gt;

&lt;p&gt;84% of them responded at all&lt;br&gt;
Of those, 82% gave up an email address or a phone number&lt;br&gt;
Of those, 71% gave up an email specifically&lt;br&gt;
On 200 domains sampled systematically from the Tranco top-1M, which is a very different animal:&lt;/p&gt;

&lt;p&gt;64% responded&lt;br&gt;
Of those, 64% had contact details&lt;br&gt;
41% of the whole list&lt;br&gt;
That gap between 82% and 64% is the entire story, and I spent two days not understanding it.&lt;/p&gt;

&lt;p&gt;The part where I was an idiot&lt;br&gt;
My first measurement came in at 28%. That felt low. I assumed the scraper was bad, so I started fixing the scraper.&lt;/p&gt;

&lt;p&gt;I got it to 41% — I'll go through what worked in a minute — and it still felt low. Then I looked at what was actually in my sample. Parked domains. CDN hostnames. Seven gambling sites, five porn sites, four file-sharing sites. A domain that returned a 200 with an empty body.&lt;/p&gt;

&lt;p&gt;A random slice of the web is not a lead list. Nobody's CRM export looks like that. The competitor who published a number was careful to say "business websites," and I'd been benchmarking against a completely different population.&lt;/p&gt;

&lt;p&gt;So I built a second sample from OpenStreetMap — every node in central London and Berlin tagged as a restaurant, lawyer, dentist, accountant, estate agent or ad agency that also has a website tag. Real businesses that publish a website. 84% responded. 82% had contact details.&lt;/p&gt;

&lt;p&gt;The scraper was mostly fine. The sample was the problem.&lt;/p&gt;

&lt;p&gt;The part where I published a wrong number&lt;br&gt;
This one is worse and it's the reason I'm writing this post.&lt;/p&gt;

&lt;p&gt;For each site the scraper couldn't reach, I wanted to know whether it was dead or just blocking me. Easy enough: retry from a second network. I retried from my own machine, compared the two, and concluded that 64% of unreachable domains were genuinely gone.&lt;/p&gt;

&lt;p&gt;I put that in the docs. I used it to compute a "live sites" denominator, which made my coverage look better.&lt;/p&gt;

&lt;p&gt;Here's what I hadn't accounted for: my second vantage point resolved DNS through a filtering resolver. When a resolver refuses to answer for a domain, the failure is indistinguishable from the domain not existing. Same timeout, same empty result. Plenty of setups do this — a Pi-hole, a corporate resolver with a blocklist, an ISP with malware filtering switched on. Mine was one of them, and I'd never had a reason to notice.&lt;/p&gt;

&lt;p&gt;I re-ran the check against Cloudflare's DNS-over-HTTPS resolver, which bypasses the local resolver entirely:&lt;/p&gt;

&lt;p&gt;Domain resolves, host is alive  66%&lt;br&gt;
Registered, no web server at all    29%&lt;br&gt;
Actually gone   5%&lt;br&gt;
Not 64% dead. 5% dead. Two-thirds of what I'd written off as dead domains were alive and simply not serving me.&lt;/p&gt;

&lt;p&gt;I'd inflated my own number and hadn't noticed, because the error pointed in the flattering direction — the errors that survive are usually the ones you like.&lt;/p&gt;

&lt;p&gt;The general lesson: a machine cannot tell you whether a domain is dead if its resolver is allowed to lie. If you're classifying unreachable hosts, do the check over DoH against a resolver you chose deliberately, not whatever your network hands you.&lt;/p&gt;

&lt;p&gt;What actually moved the number&lt;br&gt;
I tested each change separately against the same 200 sites. Baseline: 56 sites with contact details.&lt;/p&gt;

&lt;p&gt;Trusting the site's own mailto: links (56 → 60). This was the biggest single bug and it was inherited. The email-extraction code came from a different tool of mine that scrapes YouTube channels, where the page you're looking at belongs to a third party — so an address on a different domain is suspicious and gets filtered.&lt;/p&gt;

&lt;p&gt;That rule is exactly wrong here. If I hand the scraper vincheck.by and the page has &lt;a href="mailto:vincheck@inbox.lt"&gt;, that is the contact address. The site is declaring it. My filter was throwing it away because the domain didn't match. Same for megatv.com → &lt;/a&gt;&lt;a href="mailto:megatv@alteregomedia.org"&gt;megatv@alteregomedia.org&lt;/a&gt; (a Greek media group's corporate domain) and abcactionnews.com → &lt;a href="mailto:webteam@wfts.com"&gt;webteam@wfts.com&lt;/a&gt; (the station's parent company).&lt;/p&gt;

&lt;p&gt;Structural markup — mailto:, JSON-LD — now gets accepted regardless of domain. Free text still goes through the strict filter, because an address in body copy really might belong to a commenter.&lt;/p&gt;

&lt;p&gt;Following the site's own navigation (56 → 62). I was trying a fixed list of paths: /contact, /contact-us, /about, /impressum. The leader in this category doesn't do that — I noticed its output includes scrapedUrls: ["&lt;a href="https://apify.com/contact-sales%22" rel="noopener noreferrer"&gt;https://apify.com/contact-sales"&lt;/a&gt;], which no fixed list would ever contain. It reads the page's links.&lt;/p&gt;

&lt;p&gt;Once I did the same, 19 of 67 sites with an email published it on a path no list would have guessed: /pages/contact, /outreach, /company/contact, /en/contacts, /information/contact-us/, and — this one is embarrassing — /contact/ with a trailing slash. My list had /contact.&lt;/p&gt;

&lt;p&gt;Decoding Cloudflare's email protection (email hits 37 → 47). If a site is behind Cloudflare with email obfuscation on, the address is XOR-encoded in a data-cfemail attribute and the page shows [email protected]. First byte is the key, XOR the rest. Ten lines of code, biggest single gain in email hits of anything I tried. There's also a second form where the payload sits in the href hash of a /cdn-cgi/l/email-protection link — I found a site using both at once.&lt;/p&gt;

&lt;p&gt;Trying &lt;a href="http://www" rel="noopener noreferrer"&gt;www&lt;/a&gt;. and http (reachable 113 → 126). One site in ten was being written off purely over a spelling difference.&lt;/p&gt;

&lt;p&gt;Not counting 404s against the page budget (77 → 81). I capped each site at N requests. Turns out most sites were burning that cap on 404s — one site spent seven of eight requests on paths that didn't exist and read exactly one real page before I declared the budget spent. A 404 has no body. It costs almost nothing.&lt;/p&gt;

&lt;p&gt;I switched the budget to count pages that actually loaded, with a separate ceiling on total requests. Requests went up 96%. Cost went up 13%. That gap is the whole argument.&lt;/p&gt;

&lt;p&gt;What didn't work&lt;br&gt;
I think this list is more useful than the one above, and nobody ever publishes it.&lt;/p&gt;

&lt;p&gt;Adding more fixed paths — zero. I tried 27 more: language variants (/contacto, /contatti, /iletisim), trailing-slash versions, /contact.php, /get-in-touch. Across 34 sites and 986 requests: nothing. I tested this twice, under two different budget rules, because the first test wasn't fair — the old budget rule meant extra paths ate the request cap. Still zero.&lt;/p&gt;

&lt;p&gt;Reading sitemap.xml to find the contact page — zero. Sound idea. Of 42 sites, only 3 had a contact URL in their sitemap, only 1 had one that wasn't already linked from the homepage, and that page had no address on it.&lt;/p&gt;

&lt;p&gt;Retrying blocked sites with realistic browser headers — zero. Recovered 3 sites in reachability and zero in contact details. The blocks aren't header-based.&lt;/p&gt;

&lt;p&gt;Datacenter proxy — basically zero. I'd assumed a proxy would fix the ~26% of unreachable sites that were reachable from an ordinary connection. Same 200 sites, same build, only the proxy flag changed: it recovered 2. Of course it did. Apify's datacenter proxy pool is datacenter IP space, and the WAF rules that block me are matching on IP range. I had this written in the docs as a confident prediction before I bothered to test it.&lt;/p&gt;

&lt;p&gt;Loosening the free-text filter — zero. Two extra addresses across 200 sites, no extra sites, and a real noise risk. The strict gate earns its keep.&lt;/p&gt;

&lt;p&gt;Some numbers on what's actually out there&lt;br&gt;
Falling out of all this, on the 200-site random sample:&lt;/p&gt;

&lt;p&gt;5 sites disallowed crawling in robots.txt. On the 100 business sites: zero.&lt;br&gt;
7 sites had a contact form and no address anywhere. Nothing to extract; I now report the form's URL instead of an empty record.&lt;br&gt;
Of the phone numbers found, 79% came from tel: links rather than body text — structural markup is much more reliable than digit-hunting in prose, which is full of dates, prices and SKUs.&lt;br&gt;
Two sites published a phone number only inside a wa.me WhatsApp link.&lt;br&gt;
One measurement note: I ran the 200-site sample four times while working on this. Three consecutive builds scored 76, 77 and 79 sites. That spread is run-to-run noise, not progress. If you're benchmarking a scraper against the live web, run it more than once before you believe a two-point improvement.&lt;/p&gt;

&lt;p&gt;The thing I still can't answer&lt;br&gt;
I don't know how any of this compares to the competition, and I tried to find out.&lt;/p&gt;

&lt;p&gt;Every tool in this category has its source hidden. None publishes a hit rate. There's no public review API. The only way to get a real comparison is to run their actor on my sample and diff the output — and my Apify plan doesn't allow running other people's public actors, so that experiment is blocked for now.&lt;/p&gt;

&lt;p&gt;So: 82% of responding business websites, 71% for email specifically. That's my number. I have no idea whether it's good relative to the field, and I'd rather say that than imply I'm ahead.&lt;/p&gt;

&lt;p&gt;The tool is Website Contact Scraper on Apify. HTTP only, no browser, and every address in the output says whether it came from structural markup or body text — plus, where relevant, that it wasn't found on the site at all and is a naming-convention guess. All the measurements above are in its README, which is the closest thing I have to a warranty.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>programming</category>
      <category>webdev</category>
      <category>dns</category>
    </item>
  </channel>
</rss>
