@smalk/fastify-ads
v0.1.0
Published
Fastify plugin that serves Smalk native AI-search text ads server-side: injects the JS tracker, replaces the <div class="smalk-ads"> placeholder with live ad HTML, and fires async AI-agent tracking. The LLM-crawler view of your pages.
Maintainers
Readme
@smalk/fastify-ads
Fastify plugin that serves Smalk native AI-search text ads server-side — the way AI crawlers (ChatGPT, Perplexity, Claude, Gemini, Google AI Overviews) actually see your pages.
AI crawlers don't execute JavaScript. Ads must be in the HTML your server returns. This plugin does three things on every HTML response:
- Injects the JS tracker
<script>into<head>(catches headless-browser crawlers + drives ad framing). - Replaces the
<div class="smalk-ads">placeholder with live ad HTML fetched from the Smalk Ad Server (150 ms hard timeout, fail-open). - Fires async AI-agent tracking after the response (never blocks rendering).
It mirrors the official WordPress (smalk-ai-analytics) and Laravel
(smalk/laravel-ads) plugins — same endpoints, same env-var names, same ad-slot
contract.
Not included (yet): the daily cache-freshness sweep (
/active-ad-urls/Last-Modifiedbump + CDN purge). Wire that as a separate cron, or register a CDN credential so Smalk purges for you.
Requirements
- Fastify v5 (
peerDependencies: fastify@^5) - Node ≥ 20
Install
npm install @smalk/fastify-adsUsage
import Fastify from "fastify";
import smalkAds from "@smalk/fastify-ads";
const app = Fastify();
await app.register(smalkAds); // reads SMALK_* env vars
// ...your routes...
await app.listen({ port: 3000 });Add exactly one empty slot in your HTML where you want the ad — class token
smalk-ads, no other attributes:
<div class="smalk-ads"></div>If you register @fastify/compress,
register this plugin first so it rewrites plaintext HTML before compression.
Configuration
Pass options to register() or set env vars. Options override env. Credentials
are required — register() throws at boot if they're missing.
| Option | Env var | Default | Notes |
|---|---|---|---|
| projectKey | SMALK_PROJECT_KEY | — (required) | Workspace project key |
| apiKey | SMALK_API_KEY | — (required) | Api-Key auth |
| apiBase | SMALK_API_BASE | https://api.smalk.ai/api/v1 | Ad-server + tracking base |
| trackerCdn | SMALK_TRACKER_CDN | https://cdn.smalk.ai/tracker.js | Tracker script src |
| trackerEnabled | SMALK_JS_TRACKER_ENABLED | true | Inject tracker <script> |
| adsEnabled | SMALK_ADS_ENABLED | true | Fetch + inject ads |
| trackingEnabled | SMALK_TRACKING_ENABLED | true | Fire /tracking/visit |
| adsTimeoutMs | SMALK_ADS_TIMEOUT_MS | 150 | Ad-server hard timeout |
| trackingTimeoutMs | SMALK_TRACKING_TIMEOUT_MS | 150 | Tracking hard timeout |
| forwardIp | SMALK_TRACKING_FORWARD_IP | false | Forward X-Real-IP (GDPR-off by default) |
| preview | SMALK_ADS_PREVIEW | (unset) | QA only. Forces a fixed preview frame (e.g. shadow, grey, summary_toc) on every ad request — no booking, no impression. Leave unset in production. |
await app.register(smalkAds, {
projectKey: process.env.SMALK_PROJECT_KEY,
apiKey: process.env.SMALK_API_KEY,
adsTimeoutMs: 120,
});Behavior & guarantees
- Fail-open. Any ad-server timeout / error / empty response leaves the
<div class="smalk-ads">placeholder untouched and the page renders normally. Smalk being slow or down never breaks your site. - HTML-only. Only
text/html, 2xx, non-HEAD responses are touched. Streamed and binary responses pass through untouched. - Empty ad → placeholder kept. Smalk uses div presence to verify the slot is installed — the plugin never removes it.
- GDPR. Only
User-Agent,Referer, and (opt-in)X-Real-IPare forwarded to tracking. Nothing else. content-lengthis recomputed (Buffer.byteLength) after the rewrite.
Verify
curl -A "Mozilla/5.0 (compatible; ChatGPT-User/1.0)" https://your.site/article | grep -A2 smalk-adsThe ad HTML should appear inline (not just in DevTools — in raw curl).
Development
npm install
npm run build # tsup → dual ESM/CJS + .d.ts
npm test # node:test + fastify.inject() (in-process, no port)End-to-end (docker, from repo root) — real container hitting a stub Smalk API:
docker compose -f docker-compose.fastify.yml up -d --build
cd plugins/fastify/tests && npm install && npx playwright testEnd-to-end against a live Smalk ad server in preview mode (proves the plugin calls the real endpoint and injects its rendered output — no mock, no booking needed). Point it at a running ad server and pass a real project key + API key:
# boot the local FastAPI ad server (from repo root): docker compose up -d fastapi-tracking
cd plugins/fastify/tests
SMALK_API_BASE=http://localhost:8002/api/v1 \
SMALK_PROJECT_KEY=<project-uuid> SMALK_API_KEY=<api-key> SMALK_ADS_PREVIEW=shadow \
npx playwright test --config real-server/playwright.config.tsCredentials are read from the environment and never committed.
License
MIT
