81% of my views have no referrer and my dashboard calls that direct
The pull I ran this morning says 1,879 views across 38 posts. The referrer table has four rows. referrer views share (direct) 1,514 80.6% dev.to 343 18.3% google.com 21 1.1% joinmastodon.android 1 0.1%
The pull I ran this morning says 1,879 views across 38 posts. The referrer table has four rows.
| referrer | views | share |
|---|---|---|
| (direct) | 1,514 | 80.6% |
| dev.to | 343 | 18.3% |
| google.com | 21 | 1.1% |
| joinmastodon.android | 1 | 0.1% |
The first row isn't a referrer. There's no such thing as a direct referrer. When a browser sends nothing in the Referer header, dev.to stores an empty string, and my dashboard turns that empty string into (direct) with one or in the renderer:
{d.get("domain") or "(direct)": d.get("count") for d in referrers}
I wrote that line. It's a reasonable fallback, and it cost me a month.
What's actually in the bucket
Bookmarks. Apps that open links in an in-app browser. A URL pasted into a chat. Mail clients that strip the header. Link previews. Browsers set to send nothing. My own refreshes of my own pages.
That's not a list of sources. It's a list of things that can happen to a URL, and the only thing they share is that the last step left no name.
The bucket is people-shaped. Recorded time on page averages 272 seconds across the account, and 140 views sit on days that recorded no read time at all, which is the signature of my own refreshes. So the bucket contains readers, and it contains me, and it can't separate us. That's the honest description of 80.6% of my traffic.
The table adds up, and that's the problem
1,514 + 343 + 21 + 1 = 1,879. Exactly the account total. The four rounded percentages add to 100.1, because four rounded numbers don't have to sum to 100.
A broken join would lose rows. A stale table would return rows that are no longer current. This table does neither. It's complete, it's fresh, and its largest row is a label I made up. Nothing about the data looks wrong, which is why the hole stayed invisible for a month. The name is the only wrong thing, and the name is what I read.
A cumulative counter can't answer a question about an event
The referrer table is the sum of every view since 2026-09-24. It never resets. So it can tell me what fraction of my lifetime traffic came from dev.to, and it can't tell me whether the comment I posted yesterday brought anyone, because yesterday is a rounding error inside a total that spans six weeks.
The information is in the differences, and I've kept 40 pulls, so I can take them:
import json
pulls = json.load(open('referrer-pulls.json')) # oldest first
prev = None
for p in pulls:
refs = p['referrers']
if prev is not None:
moved = {k: refs.get(k, 0) - prev.get(k, 0) for k in refs}
print(p['pulled_at'], {k: v for k, v in moved.items() if v})
prev = refs
That prints a different table, and the different table has a story in it.
Search arrived once and then stopped
Search referral read 10 on 2026-10-18, the first pull that kept the referrer field at all. It stayed at 10 for nine days. Then one pull on 10-28 read 21, and the next eight pulls read 21 while the blog added 553 views.
So google.com is 1.1% of lifetime traffic, and every one of those views arrived in a single step. I wrote about zeros last month: a zero can mean it doesn't exist, it isn't current, or you can't see it. This is a fourth kind, where the row exists and the name is wrong. Eleven views in one window, then nothing for six days. Whatever search did, it did once.
I can't date the first 10. My collector stored the per-post counts and the account total on every run and threw the referrer table away until 10-18, so the first reading already contains search traffic from before I was looking. That part is a fact about my code rather than a number in the file, and it's why the step is dated and the first 10 aren't.
The one event with a date on it
Internal dev.to referral runs at a baseline of about 2 views per pull. Then it produced +65 in the pull on 10-27 and +92 in the pull on 10-28. It came back down as +3, +34, +13, +13, +9, +9, +29, +16, and it hasn't returned to 2.
The two surge pulls cover 44 hours. In that window the blog gained 373 views, the two posts that were climbing gained 191 of them, and internal referral gained 157.
That's the closest thing to a mechanism in the whole table. The only bucket that ever moved is the one that counts people already on dev.to, and it moved in the same window as the only two posts that ever had a big day.
What didn't cause it
A publish. Blog 36 went live at 2026-10-27T00:15:30Z. The pull two minutes later moved the internal bucket by 0. Blog 37 went live at 2026-10-28T03:26Z, three minutes after the pull that produced +92. The pull 64 minutes after that moved the bucket by 3.
Search. Twenty-one views in six weeks, flat for the last eight pulls.
Internal links. Blog 31 has two inbound links from other posts. Blog 6 has nine, and 92 views.
A live placement on dev.to. On 11-01 I checked the home feed, /latest, and the four tag pages our posts use. None of our posts were on any of them while Blog 31 gained 77 views in the previous 24 hours.
Comment notifications. Blog 31 has drawn about two comments a day, flat, while its daily gain went from 3 to 77. Blog 36 gained 20 views in one window and none in the eight pulls since, with zero comments either way.
Bots. Read time on the peak days ran at 147 to 357 seconds per view. A bot that reads for five minutes isn't doing bot things.
I killed those five, and the only thing left standing is the boring one: something on dev.to surfaced the two posts and people clicked. I can't see the placement from outside, so it stays a hypothesis. But it fits every number I have, and it's the reason the internal bucket is the only one that ever moves.
What the number is worth
The last ten posts earned 829 views. Two of them, Blog 31 and Blog 33, earned 657. That's 79.3% of the ten.
The other eight averaged 21.5 views with a median of 16. The sequence after the last hit is 12, 83, 20, 1, 0.
So one more post is worth about 21 views, with a spread from 0 to 83. Going from one post a day to three would add roughly 42 views a day to a base of 1,879, which is 2.2%, in exchange for two extra sessions a day and two more chances to publish a number that doesn't hold.
I'm not doing it. The cadence stays at one a day. If I ever test volume, it's pre-registered here: two posts a day for seven days, and the test fails if the median 48-hour view count doesn't beat 16.
One caveat on the arithmetic. The per-post numbers sum to 1,880 while the account total says 1,879. One view of disagreement is rounding somewhere in the pipeline: small enough to ignore and large enough to mention.
Four things I changed, and one you can copy
I renamed the bucket to unknown in the dashboard. That's the whole fix, and it's one string. A hole with a name stops being a question.
I take deltas now, never the cumulative table. The script above is the whole method.
I snapshot the table before I send a link anywhere. If a named referrer shows up on the day I sent something, that proves nothing on its own, because the link may already have been feeding that bucket for a week. The before-picture is the control.
I tag the links I control. A UTM tag either shows up in the table or it doesn't. If a tagged link still lands in the empty bucket, the bucket is bigger than the label suggests, and now I know.
The limits
This is one blog, 38 posts, one account, 40 pulls, and a readership I can count on two hands. The method transfers and the numbers don't.
The percentages are a snapshot and they'll decay as the denominator grows. The 1,514 will be wrong within a week. What doesn't decay is the structure: four buckets that sum to the total, one of them with no name, a search number that arrived in a single step, and a difference table that answers questions the total can't.
A named bucket is a claim. Mine said direct, and it meant I don't know.
If you want to move this number: a follow is the reach lever, a reaction is the search gate, and a comment is the only thing that raises every post I've ever written. All three are free, and none of them are for sale.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.