Skip to content

Click tracking

Markdown

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.

  1. Switch it on. The site must already build from the database (Listings from D1).

    site.config.ts
    features: { clicks: 'ping' },
  2. Deploy.

    Terminal
    pnpm deploy
  3. Read one listing page’s link.

    Terminal
    curl -s https://<your domain>/<listingBase>/<slug>/ | grep -o 'ping="/out/[^"]*"'

    ping="/out/<id>/". The href is still the listing’s own URL.

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

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.

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.

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

See features and listing.

  • 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/
    • loaders/d1.ts the 30-day aggregate at build time
    • db/schema.ts clicks
    • templates/admin/StatsPage.astro the top-clicks list