Click tracking
features.clicks decides how a click on a listing’s Visit button is counted: 'ping' adds a
background beacon to the real link, 'redirect' routes the click through /out/<id>/, 'off'
leaves a plain link. Counting needs a listing with a D1 row id, so it only works on a site built
with LISTINGS_SOURCE=d1.
-
Switch it on. The site must already build from the database (Listings from D1).
site.config.ts features: { clicks: 'ping' }, -
Deploy.
Terminal pnpm deploy -
Read one listing page’s link.
Terminal curl -s https://<your domain>/<listingBase>/<slug>/ | grep -o 'ping="/out/[^"]*"'ping="/out/<id>/". Thehrefis still the listing’s own URL.
-
Switch it on.
site.config.ts features: { clicks: 'redirect' }, -
Deploy with
pnpm deploy. -
Read the link and the robots file.
Terminal curl -s https://<your domain>/<listingBase>/<slug>/ | grep -o 'href="/docs/out/[^"]*"'curl -s https://<your domain>/robots.txt | grep outhref="/docs/out/<id>/"andDisallow: /out/.
Verify
Section titled “Verify”| What to check | Where | You should see |
|---|---|---|
| The endpoint | curl -sI https://<your domain>/out/<id>/ | head -1 |
HTTP/2 405 in ping mode (a beacon is a POST); HTTP/2 302 in redirect mode — and this HEAD is not counted |
| A counted click | click Visit in a browser, then pnpm exec wrangler d1 execute DB --remote --command "SELECT listing_id, day, count FROM clicks ORDER BY day DESC LIMIT 5" |
one row per listing per UTC day, count incremented |
| The stats | /admin/stats/ → Top outbound clicks, last 30 days |
the listing and its count |
| Popularity | after the next build, /search/?sort=popularity or any grid |
clicked listings sort first |
How it works
Section titled “How it works”ping keeps the anchor exactly as built — href the listing’s URL, rel from the tier —
and adds ping="/out/<id>/". On a click the browser POSTs Content-Type: text/ping in the
background; the navigation is untouched, no JavaScript is involved, and a broken database costs
a count, not a click. The endpoint accepts only a POST with that content type and a Ping-From
(or Origin) matching the request’s origin or site.url, answers 204, and records after the
response through waitUntil(). Firefox ships pings disabled, so roughly 3% of clicks go
uncounted: popularity is a ranking signal, not analytics. No analytics provider is embedded
anywhere; add whatever you use.
redirect sets href="/docs/out/<id>/" and answers 302 to the listing’s URL. Complete counts, at
the cost of the link: nothing dofollow reaches the listed site, and robots.txt gains
Disallow: /out/. A failed lookup is a 503 here, because there is no target to send the visitor
to — one more reason ping is the default.
What a click stores. One upsert into clicks(listing_id, day, count), keyed by listing and
UTC day: INSERT … ON CONFLICT DO UPDATE SET count = count + 1. No IP, no user agent, no
per-click row, and nothing on a page view. Two statements per counted click — the lookup and
the upsert — and about two rows written per listing per day, which is what fits 8,000 clicks a
day inside the free plan. Every /out/ response is no-store and X-Robots-Tag: noindex.
What is not counted. A HEAD request (a link checker); a user agent matching
bot|crawl|spider|slurp|preview|facebookexternalhit|curl|wget; an id that is not an approved
listing (404); a beacon with the wrong content type (400) or from another site (403). The
filter is deliberately short: a miss inflates one count, a false positive erases a real visitor.
A Markdown listing has no row. listingId is null for a file-loaded entry, so the button
is a plain direct link whatever the flag says. /out/ still answers, but nothing links to it.
Where the numbers show. /admin/stats/ reads the last 30 days live. At build time the D1
loader sums the same window into each listing’s popularity — its log line says
30-day clicks for <n> — which drives listing.defaultSort ('featured' puts featured
listings first, then popularity; 'popular' starts with popularity) and the Most popular
sort on /search/. Counts reach the site at the next build, not the next click.
When it is off or degraded
Section titled “When it is off or degraded”| State | Visitor sees | /admin/ reports |
Fix |
|---|---|---|---|
'off' (the default) |
a plain link; /out/<id>/ answers 404 |
stats: features.clicks is "off", so nothing is counted. |
set the mode |
LISTINGS_SOURCE=files |
a plain link in every mode | stats: No clicks recorded yet in this window. |
build from D1 |
| Firefox in ping mode | the link works; the beacon is not sent | nothing | accept it, or redirect |
| D1 write fails, or the free-plan quota is spent | ping: 204 and the visitor arrives; the count is lost silently | clicks: recording a click for listing <id> failed in the Worker log |
Cost and limits |
| Redirect mode, the lookup fails | 503 “Try again shortly.” | the lookup’s own log line | retry; consider ping |
Past the request quota, tracking calls fail and nothing alerts you, so treat popularity as
approximate near the limit. The report on /admin/ has no clicks check; the stats page is the
signal.
Configuration
Section titled “Configuration”| Field | Default | What it changes |
|---|---|---|
features.clicks |
'off' |
'ping', 'redirect' or 'off': the shape of the outbound anchor and what /out/<id>/ answers |
listing.defaultSort |
'featured' |
'featured' sorts featured first, then by popularity; 'popular' by popularity first; 'newest' by approval date |
listing.outboundRel |
'nofollow' |
the rel on free-tier links; paid and featured tiers are always sponsored |
Directorysrc/
- server/clicks.ts
handleOutbound(), the guards, the upsert - pages/out/[id].ts the route, every method
Directorycomponents/listing/
- OutLink.astro the anchor in each mode
- Detail.astro where it renders
Directorylib/
- seo.ts
robotsDisallow()adds/out/ - listing.ts
outboundRel(), the sort comparators - search.ts the popularity sort on
/search/
- seo.ts
- loaders/d1.ts the 30-day aggregate at build time
- db/schema.ts
clicks - templates/admin/StatsPage.astro the top-clicks list
- server/clicks.ts