It's 2:07 AM, and there's water on your kitchen floor.
Not a little. The pipe under the sink has given up on life, and the puddle is heading for the living room. You're holding a towel in one hand and your phone in the other, typing "emergency plumber near me" with your thumb.
Google gives you two options.
Okafor & Sons Plumbing. 4.9 stars from 212 reviews. Reviews like "Ray saved our basement" and "Fair price, showed up in 40 minutes." There's a phone number. There's a photo of a van from 2019. There is no website.
FlowPro Plumbing. 4.3 stars from 58 reviews. But it has a website: clean, loads fast on your phone, and right at the top in big letters it says 24/7 Emergency Service. Call Now.
You're standing in water at 2 AM. You don't want to guess whether Ray picks up at night. FlowPro told you they do.
You call FlowPro.
Ray, the best plumber in town, will never know he lost that job. He'll never know about the next one either.
Ray is everywhere
Here's the thing: Ray isn't unusual. Drive through any town and look at the busy places. The barber with a queue on Saturday. The roofer whose trucks you see everywhere. The nail salon that's always booked. A lot of them have great reviews and no website at all.
A 2025 Clutch survey found that 83% of small businesses now have a website. That sounds like a lot until you flip it around: about 1 in 6 still don't. And according to the same survey, over a third of those think a website "isn't relevant" because they have social media or a marketplace listing instead.
If you build websites for a living, that's not a sad statistic. That's a list of clients who don't know they need you yet.
And "no website" is only the most obvious kind. Let me tell you about Rosa.
The bakery with a "For Sale" sign on its website
Rosa runs a bakery. There's a line out the door every Saturday morning, mostly for the cinnamon rolls.
Back in 2020, her nephew Marco built her a website. It was nice! Photos of the rolls, opening hours, a little map. Marco registered the domain on his own card, then moved to another city, changed banks, and forgot all about it.
The domain expired. Someone bought it. Today, when a customer finds Rosa's Bakery on Google Maps and taps Website, they land on a page that says "This domain may be for sale", surrounded by ads for car insurance.
Rosa has no idea. Why would she? She never visits her own website. She's busy baking.
This is the sneaky part. A broken website can be worse than no website. When there's no site, a customer just calls. When there's a parked page, or a full-screen browser warning that says "Your connection is not private" because the security certificate expired, the customer thinks one of two things: the bakery closed, or the bakery is sketchy. Either way, they leave.
Why a website still matters (your pitch, ready to use)
Most of these owners have heard "you need a website" before and shrugged. So it helps to know why, in words that make sense to a busy plumber or baker. Here's what I'd say:
1. Your Facebook page is rented, not owned. Facebook decides who sees your posts. Accounts get hacked or locked, and getting them back can take weeks. Booking apps and delivery platforms take a cut of every order and can change their fees whenever they like. A website on your own domain is the one place online that you control.
2. A website answers the questions that make people call. Do you work at night? Do you come to my area? Roughly what does it cost? Ray had 212 glowing reviews, and he still lost the job to a website that answered "are you open right now?" in one sentence.
3. It's where people double-check you. Customers who find you on Google Maps often tap through to your site before calling, especially for bigger jobs like a roof or a wedding cake. No site, or a broken one, makes you look smaller than you are.
4. A broken site is actively hurting you. An expired certificate, a dead domain, a parked page: these don't just fail to help. They push customers to the next result.
5. It's the foundation for everything else. Google listing, social media, ads, flyers with a QR code: they all work better when they point somewhere you own.
You don't need to say all five. Usually one is enough, if it's the right one for that business. Which brings us to the useful part.
Step 1: Spot the leads by hand
You can do this right now, for free, with Google Maps:
- Search a trade and a city:
roofer in Tucson,nail salon in Leeds,barber in Austin. - Open each result and look at the Website button.
- Sort what you find into five buckets:
| What you see | What it means | What you pitch |
|---|---|---|
| No Website button at all | No website | A first website |
| Website goes to Facebook, Instagram, TikTok or Linktree | Social only | A real site they own |
| Website goes to Yelp, Booksy, a booking app or a delivery app | Listing only | A site that isn't rented |
| Website is dead, parked, or shows a security warning | Broken | A fix or a rebuild |
| Website loads but looks like 2012 | Outdated | A redesign |
Pay attention to the reviews too. A business with 200 reviews and no website is busy and can afford you. A business with 2 reviews might not be open next year.
Step 2: Check a website with Python
The first three buckets are easy to see by eye. Broken and outdated sites are harder, because you have to actually open each one, wait for it to load, check it on a phone, and squint at the footer.
So here's a little script that does it for you. It only uses Python's standard library, so there's nothing to install:
import re import socket import ssl import sys import urllib.error import urllib.request from datetime import date from urllib.parse import urlparse SOCIAL = ("facebook.com", "instagram.com", "tiktok.com", "linktr.ee") PARKED = ("domain is for sale", "buy this domain", "this domain may be for sale", "domain parking", "parked free", "is parked") def check(url): if "://" not in url: url = "https://" + url host = urlparse(url).hostname or "" if any(host == s or host.endswith("." + s) for s in SOCIAL): return "SOCIAL ONLY", "the 'website' is a social media page" try: socket.getaddrinfo(host, 443) except socket.gaierror: return "DEAD", "the domain doesn't exist any more" try: req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=15) as r: final_url = r.geturl() html = r.read(500_000).decode("utf-8", "ignore").lower() except urllib.error.HTTPError as e: if e.code in (401, 403, 429, 503): return "UNKNOWN", f"HTTP {e.code}, probably a bot wall, check by hand" return "BROKEN", f"the site answers HTTP {e.code}" except urllib.error.URLError as e: reason = e.reason if isinstance(reason, ssl.SSLCertVerificationError): return "BROKEN", f"certificate problem: {reason.verify_message}" if isinstance(reason, ConnectionRefusedError): return "BROKEN", "the server refuses connections" return "UNKNOWN", f"couldn't connect ({reason}), try again later" except OSError as e: # timeouts, dropped connections return "UNKNOWN", f"{type(e).__name__}, try again later" if any(p in html for p in PARKED): return "BROKEN", "parked page, the domain is for sale" problems = [] if final_url.startswith("http://"): problems.append("no HTTPS") if 'name="viewport"' not in html and "name=viewport" not in html: problems.append("not mobile-ready") notices = re.findall(r"(?:©|©|copyright)([^<]{0,40})", html) years = [int(y) for n in notices for y in re.findall(r"\b20\d\d\b", n)] if years and max(years) <= date.today().year - 4: problems.append(f"copyright {max(years)}") if problems: return "OUTDATED", ", ".join(problems) return "OK", "looks fine" if __name__ == "__main__": for site in sys.argv[1:]: verdict, why = check(site) print(f"{verdict:12} {site} ({why})") Save it as check_site.py and give it some websites. badssl.com has test pages with deliberately broken certificates, which makes it perfect for trying this out:
python3 check_site.py example.com expired.badssl.com http.badssl.com \ https://www.facebook.com/somebakery nope-zz91-bakery.com OK example.com (looks fine) BROKEN expired.badssl.com (certificate problem: certificate has expired) OUTDATED http.badssl.com (no HTTPS) SOCIAL ONLY https://www.facebook.com/somebakery (the 'website' is a social media page) DEAD nope-zz91-bakery.com (the domain doesn't exist any more) What it checks, top to bottom:
- Social only: the "website" is really a Facebook or Instagram page.
- Dead: the domain doesn't resolve any more (hello, Rosa).
- Certificate problems: expired, self-signed, or issued for a different domain. These are the ones that show the scary full-page warning in the browser.
- Errors: the server answers 404 or 500.
- Parked: the page says the domain is for sale.
- Outdated: no HTTPS, no mobile viewport tag (so it looks tiny on a phone), or a copyright year that's 4+ years old.
Step 3: The fine print (a.k.a. things I learned the hard way)
This script is good enough for checking 10 businesses on a Sunday afternoon. Before you trust it with your reputation, though, here are the traps.
A 403 is not a broken website. Lots of sites sit behind Cloudflare or similar services that block anything that looks like a script. Your browser sees a perfectly good website. Your script sees "403 Forbidden". That's why the script says UNKNOWN for 401, 403, 429 and 503 instead of BROKEN. Imagine emailing a business owner "your site is down!" when it isn't. You don't get a second chance at that first impression.
Some "broken" certificates aren't broken for real visitors. Try incomplete-chain.badssl.com. The script says the certificate is broken. Open it in Chrome and it loads fine, because browsers quietly fetch the missing piece of the certificate chain and Python doesn't. If you pitch "your site shows a security warning" to someone whose site works in every browser, you look like you don't know what you're doing. Always open a "broken" site in a real browser before you mention it.
Copyright years are messy. My first version of this script flagged a perfectly modern site as outdated, because its footer said "© 2009-2026" and I was grabbing the first year. Take the newest year in the notice, not the first one. Also, plenty of sites have no copyright line at all, which tells you nothing either way.
Timeouts happen. A slow server today might be fine tomorrow. Treat a timeout as "check again later", never as "broken".
Not every "website" is a website. Google listings sometimes link to a booking page, a delivery app, a Google placeholder site, or a page shared by 15 businesses in the same mall. Those aren't really the owner's site, so for your purposes, they count as "no real website".
Chains don't need you. The local branch of a national franchise has no website of its own because head office runs one. You'll waste a lot of time pitching franchises if you don't filter them out.
The actual hard part
Checking one website is easy. The real work is everything around it:
- Getting the list in the first place. Copying businesses out of Google Maps by hand is slow, and Maps only shows about 120 results per search, so a whole city means searching neighborhood by neighborhood.
- Removing what you don't want. Duplicates (the same business listed twice with the same phone), chains, businesses that closed, businesses with a perfectly fine website.
- Deciding who to call first. A 4.9-star roofer with 200 reviews and no website is a much better lead than a 3.2-star one with 4 reviews. You want them sorted.
- Writing the reason. "No website, 4.9 stars from 212 reviews, Google listing not claimed by the owner" is a cold email that writes itself. Typing that out for 300 businesses is not fun.
- Doing it again next week without seeing the same businesses twice.
Or skip the hard part
That's exactly why I built Websiteless on Apify. You type a trade and a city (roofer, Tucson, AZ), and it gives you back a list of local businesses that need a website, each with:
- which bucket it's in: no website, social only, listing only, broken, or outdated, with the exact problem (expired certificate, parked domain, no mobile layout, and so on)
- a lead score from 0 to 100 that's plain arithmetic: web presence, review count, rating, phone listed, plus gaps in their Google profile (unclaimed listing, no opening hours, hardly any photos)
- a reason to pitch in plain English, ready to drop into a mail merge
- phone, rating, review count, address and Google Maps link, plus emails from the business website if you want them
And it handles the fine print above. It only calls a site broken when a real visitor would see it broken, so bot walls and repairable certificates are never sent to you as leads. Chains, duplicates and businesses with a working website are filtered out, and you don't pay for them. There's an Only new leads mode too, so you can schedule it every Monday and get fresh businesses each week.
It costs $5 per 1,000 leads on Apify's free plan (cheaper on paid plans), and you only pay for actual leads. Apify's free plan comes with monthly credit, so you can try a few cities without paying anything.
A tip from testing it: trades and beauty find the most leads. Handymen, roofers, cleaners, nail salons and barbers often have no site. Dentists and lawyers almost always do.
But honestly? If you just want to check the 10 businesses on your own street, the script above is all you need.
Please pitch like a human
One last thing, because it matters more than any of the code.
These are real people running real businesses, usually on very little time. A lead list is not permission to spam. A few rules that work better anyway:
- Lead with their problem, not your services. "Hi Rosa, when I tap Website on your Google listing, it goes to a page saying your domain is for sale. Did you know?" gets a reply. "We build stunning websites!" doesn't.
- Be specific. Mention the reviews. Mention the trade. Show that you actually looked.
- Follow the cold email and calling rules where you live (and where they live). They differ a lot between countries.
- Take no for an answer. Some businesses genuinely don't want a website. That's fine. Thank them and move on.
Back to 2:07 AM
Six months later. Same pipe, a different house, a different person standing in a puddle.
They search "emergency plumber near me". Okafor & Sons Plumbing comes up first: 4.9 stars, 230 reviews now. And this time there's a Website button.
They tap it. It loads in a second. At the top, in big letters: 24/7 Emergency Service. Ray picks up. Call Now.
They call Ray.
Somewhere, a web designer got paid for a one-page website that took a weekend to build. Ray got a customer at 2 AM. The customer got a dry floor.
Everybody wins. Except FlowPro, probably.
If this helps you land even one local client, it was worth writing. Go find your Ray.
![Embedded Software Engineering — Ch.10: OTA Firmware Update [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6nak2soahu0o1ww35hy.png)
![Embedded Software Engineering — Ch.11: Embedded Security [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8lj1zci0ucpfvn91aqd5.png)


![Embedded Software Engineering — Ch.12: Embedded Case Studies [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fth5r2cczjsot6ae55lq5.png)