nuxt.fyi#
Spots Nuxt sites being shared on Bluesky and posts screenshots of them.
The aim of nuxt.fyi is to celebrate sites built with Nuxt in the wild.
It watches the Bluesky Jetstream firehose, pulls every link out of every post, and for each new domain asks: is this built with Nuxt? When the answer is yes, it posts a screenshot on Bluesky.
The public dashboard shows everything that's been detected, broken down by version and recency.
Features#
- Detection signals across HTML and a Playwright runtime probe (
<div id="__nuxt">,__NUXT_DATA__,window.__NUXT__,meta[name=generator],/_nuxt/assets) - Screenshots taken in a persistent Chromium context, with layered cookie-banner suppression (GTM consent cookie, CSS overlay, and a pinned
I-Still-Dont-Care-About-Cookiesextension) - Discord + Bluesky notifications with rate-limit-aware posting
- @-mention triggered replies: tag the bot on Bluesky with a URL and it replies with the scan result + screenshot, scanning on the spot if it's a domain we haven't seen yet
- Nuxt 4 dashboard reading the same SQLite database read-only
- Runs on a single Fly.io machine; daemon and dashboard share one volume
- Node 24+ only: native TS execution,
node:sqlite, globalfetchandWebSocket
Try it out locally#
You will need a Discord webhook URL and, optionally, a Bluesky account with an app password for posting.
cp .env.example .env
# edit .env, set DISCORD_WEBHOOK_URL (and BLUESKY_* if you want)
corepack enable
pnpm install
pnpm install-extension
pnpm --filter @nuxt-fyi/scanner exec playwright install chromium
# scanner (terminal 1: screenshot service on :3001)
pnpm dev:scanner
# daemon (terminal 2)
pnpm dev
# dashboard (terminal 3)
pnpm dev:dashboard
Set VERBOSE=1 to log every post the daemon sees.
Dashboard against production data#
To work on the dashboard UI without running the daemon, scanner, or a local database, point it at production:
# proxies every /api/** request to https://nuxt.fyi
pnpm dev:dashboard:prod
# or serve static dummy data for fully offline UI work
pnpm dev:dashboard:fixtures
Override the proxy target with NUXT_UI_ONLY_TARGET. Submissions in this mode hit the
live submit endpoint, so a queued domain is a real scan request.
Manual submissions#
The dashboard's home page has a small form for queueing a domain by hand. Under the hood:
- The dashboard's
POST /api/submitrate-limits per IP (default 5 / minute, tunable viaSUBMIT_RATE_LIMITandSUBMIT_RATE_WINDOW_MS). - It then posts the URL to the daemon's loopback-only submit server (
DAEMON_SUBMIT_PORT, default 3010) using the sharedDAEMON_SUBMIT_TOKENbearer secret. - The daemon canonicalises the domain, checks the global skip list, and either enqueues a
detection job (same path the Jetstream consumer uses) or short-circuits with the
existing result if the domain has been scanned within
RESCAN_AFTER_MS.
Set DAEMON_SUBMIT_TOKEN as a Fly secret on nuxt-fyi to enable it in production; leave
it empty to disable submissions entirely (the daemon refuses every request, the dashboard
returns 503).
NSFW classification#
Screenshots are classified with nsfwjs on the scanner machine at capture time. Each row gets one of three labels:
safe(default): renders normally everywhere.suggestive: renders normally on the dashboard; Bluesky posts get asexualself-label so users can opt to blur via their account preferences.nsfw: dashboard blurs the image with a click-to-reveal overlay; Bluesky posts get apornself-label; Discord posts attach the image as aSPOILER_*file rather than rendering inline in the embed.
Thresholds are tunable via NSFW_PORN_THRESHOLD (default 0.5) and NSFW_SEXY_THRESHOLD
(default 0.6) on the scanner. After tweaking, re-run the backfill with --reclassify to
relabel historical rows.
Architecture#
Three processes split across two Fly apps:
nuxt-fyi(thisfly.toml): daemon (Jetstream consumer + detection) and dashboard (Nuxt 4) in one container, sharing the SQLite volume at/data.nuxt-fyi-scanner(fly.scanner.toml): Nitro v3 service that owns Playwright + the screenshot half of the ImageKit upload. No public ingress; daemon reaches it onnuxt-fyi-scanner.internal:3000via Fly's 6PN private networking. Stays always-on atshared-cpu-2xbecause.internaldoesn't wake stopped machines.- ImageKit: external CDN for screenshots + og:images.
The daemon talks to the scanner via an authenticated HTTP call (SCANNER_TOKEN shared
secret). Scanner outages degrade quality (no screenshot) but don't break the pipeline:
the og:image is still uploaded and the hit is still recorded + posted.
Image hosting#
Screenshots and og:images are uploaded to ImageKit at scan time
so the dashboard can render them through @nuxt/image with on-the-fly resizing. The
dashboard renders ImageKit URLs only; for the rare row where the daemon recorded an
og:image origin but the upload didn't land, a plain <img> falls back to the upstream
URL.
Backfill scripts#
Two idempotent scripts on the daemon side, safe to re-run:
# Rescan Nuxt-confirmed rows missing an ImageKit screenshot, or with an og:image URL
# recorded but no ImageKit copy. Uses the live scanner; ignores RESCAN_AFTER_MS.
pnpm backfill-images --concurrency=2
# NSFW-classify every row that has an image but no nsfw_label. Calls the scanner's
# /classify endpoint. Add --reclassify after threshold tweaks to relabel all rows.
pnpm backfill-nsfw --concurrency=2
Both support --limit=N and --dry-run.
Admin CLI#
On the running Fly machine (fly ssh console -C bash), you can re-scan one or more
domains against the live database, or check what we currently know about one:
cd /app
node src/cli/rescan.ts example.com another.com
# refresh only the screenshot/og:image on an existing Nuxt hit
node src/cli/rescan.ts --screenshot-only example.com
# suppress Discord + Bluesky posts even when the domain is newly detected as Nuxt
node src/cli/rescan.ts --no-notify example.com
# read-only: dump the stored scan, NSFW classification, activity, and notifications.
# Pass --json for machine-readable output.
node src/cli/status.ts example.com
Credits#
Detection heuristics and the GTM consent-cookie trick are lifted from nuxtlabs/vue-telescope-analyzer ❤️
License#
Made with ❤️
Published under MIT License.