I just dropped a single code doc to the canvas with everything you asked:

client/src/pages/Public/BusinessPlanTemplateDetailsPage.tsx — full React page for a single template (preview, metadata, sections, Download button, “Use in AI Builder” CTA).

server/services/analytics.ts — tiny helper that writes download events to Firestore (analytics/downloads/events) with slug, version, mode (public/signed), timestamp, UA, referrer, and SHA-256 IP hash (no raw IP stored).

server/routes/bpTemplatesPublic.ts — full updated router: download endpoint now logs analytics and still uses the auto public→signed fallback.

Wire-up (quick)

Server

Ensure you’re using the updated bpTemplatesPublic.ts (replace the existing one).

Nothing else to mount—routes stay at:

GET /api/bp-templates-firebase/list

GET /api/bp-templates-firebase/master

GET /api/bp-templates-firebase/:slug

GET /api/bp-templates-firebase/:slug/download (now logs analytics)

Client

Add route to your app router:

import BusinessPlanTemplateDetailsPage from "@/pages/Public/BusinessPlanTemplateDetailsPage";
<Route path="/business-templates/:slug" element={<BusinessPlanTemplateDetailsPage />} />


Link to details from the gallery cards (or it already exists if you used the previous page).

Test

# Details page JSON works
curl -s http://localhost:5000/api/bp-templates-firebase/business-plan-general-blank-template | jq '.slug,.currentVersion'

# Download triggers analytics (302 then 200)
curl -I http://localhost:5000/api/bp-templates-firebase/business-plan-general-blank-template/download

# Health remains green
curl -s http://localhost:5000/health/bp-templates | jq '.ok,.urls.mode,.statuses'


Firestore (analytics)

Events land under: analytics / downloads / events / <auto-id>

Fields: slug, version, mode, ts, ua, ref, ipHash

Optional index (if you’ll query by slug + recent first):

Console → Firestore → Indexes → Add single-field:

Collection: analytics/downloads/events

Field: slug (Ascending)

Field: ts (Descending)

Privacy note

We store hash of IP, not the raw IP. If you prefer no IP at all, set ip: null where logDownload(...) is called.