From e03c71aa48935f10e95ecd091b449af8648bb965 Mon Sep 17 00:00:00 2001 From: 0xNyk <0xnykcd@googlemail.com> Date: Tue, 15 Sep 2026 01:12:58 +0700 Subject: [PATCH 1/4] feat(landing): DataFast analytics, goals, Polar attribution, crawler tracking - Cookieless DataFast build injected after the load event, served first-party through vercel.json rewrites (/js/script.cookieless.js, /api/events). A head queue stub keeps goals fired before it loads. Cookieless because the landing has no consent banner. - Goals: download_dmg and github_click via data-fast-goal with location; scroll_pricing and scroll_faq scroll goals; license_key_copied on the success page. buy_pro_click is sent by a click handler that holds a plain click for 250ms before following /buy, because DataFast posts goals with an XHR the browser cancels when the page navigates to checkout. - Revenue attribution: the success page carries the tag, which reads the Polar checkout_id and reports a payment event. - middleware.ts (Vercel Routing Middleware) reports known AI crawler requests via @datafast/ai-crawl 1.0.9 (zero deps, audited), scheduled with waitUntil; no-op until DATAFAST_WEBSITE_ID, now set in production. - scripts/datafast-funnels.mjs defines three funnels for the Account API. Evidence (real DataFast script, events captured locally, nothing sent): pageview, download_dmg{hero}, scroll_pricing, buy_pro_click{hero} before navigation at 284ms, payment{polar_checkout_id} and license_key_copied on the success page, zero cookies set. Claude-Session: https://claude.ai/code/session_01CvRJfFeFvF96jhBFujxvHN --- landing/buy/success/index.html | 42 ++++++++++- landing/index.html | 108 +++++++++++++++++++++++---- landing/middleware.ts | 25 +++++++ landing/package.json | 6 +- landing/vercel.json | 10 +++ scripts/datafast-funnels.mjs | 131 +++++++++++++++++++++++++++++++++ 6 files changed, 302 insertions(+), 20 deletions(-) create mode 100644 landing/middleware.ts create mode 100644 scripts/datafast-funnels.mjs diff --git a/landing/buy/success/index.html b/landing/buy/success/index.html index dca957f..fb65b39 100644 --- a/landing/buy/success/index.html +++ b/landing/buy/success/index.html @@ -10,6 +10,29 @@ /> + +
@@ -35,11 +58,21 @@

Activate Dictx Pro

Your License Key

-

- If no key is shown here, copy your key from Polar customer portal or receipt email. +

+ If no key is shown here, copy your key from Polar customer portal or + receipt email.

- Back to Dictx @@ -70,7 +103,8 @@

Your License Key

if (!key) return; try { await navigator.clipboard.writeText(key); - copyStatus.textContent = "Copied. Paste it in Dictx Settings -> About."; + copyStatus.textContent = + "Copied. Paste it in Dictx Settings -> About."; } catch (_error) { copyStatus.textContent = "Copy failed. Select and copy manually."; } diff --git a/landing/index.html b/landing/index.html index 96403f6..055d78a 100644 --- a/landing/index.html +++ b/landing/index.html @@ -20,6 +20,28 @@ href="./apple-touch-icon.png" /> + +
@@ -31,14 +53,21 @@ @@ -56,8 +85,14 @@

Transcription you can trust in real work.

  • Signed Pro builds
  • @@ -96,7 +131,11 @@

    Obsidian-ready output

    -
    +

    Free or Pro

    Free stays powerful for daily use. Pro removes setup friction and @@ -118,13 +157,17 @@

    Free or Pro

    href="https://github.com/splitlabs/dictx" target="_blank" rel="noreferrer" + data-fast-goal="github_click" + data-fast-goal-location="pricing" >View source

    Pro

    $29 one-time

    -

    Early access: first 100 installs get Pro free

    +

    + Early access: first 100 installs get Pro free +

    • Signed desktop binaries
    • In-app auto-updater
    • @@ -132,7 +175,9 @@

      Free or Pro

    • Professional checkout
    • Priority support contact
    - Get Dictx Pro + Get Dictx Pro
    @@ -185,7 +230,7 @@

    Free or Pro

    -
    +

    FAQ

    Is Dictx still open source? @@ -194,17 +239,16 @@

    FAQ

    What does Pro actually add?

    - Pro is the convenience layer: signed installers, in-app updates, - and priority support. Free still includes the full local dictation + Pro is the convenience layer: signed installers, in-app updates, and + priority support. Free still includes the full local dictation workflow.

    How does the first-100 free Pro claim work?

    - The app claims it automatically on first activation. If your - install lands in the first 100, Pro is enabled instantly at no - cost. + The app claims it automatically on first activation. If your install + lands in the first 100, Pro is enabled instantly at no cost.

    @@ -235,8 +279,16 @@

    FAQ

    signed installers and one-click updates.

    @@ -250,6 +302,32 @@

    FAQ

    document.getElementById("year").textContent = String( new Date().getFullYear(), ); + + // Buy clicks leave the page for checkout at once, and DataFast posts + // goals with a plain XHR that the browser cancels on navigation. So the + // goal is sent here (not via data-fast-goal, which would double-count) + // and a plain click waits briefly before following /buy. Modified + // clicks open a new tab and need no delay. + document.addEventListener("click", function (event) { + var link = event.target.closest("a[data-buy-location]"); + if (!link) return; + window.datafast("buy_pro_click", { + location: link.getAttribute("data-buy-location"), + }); + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) + return; + event.preventDefault(); + setTimeout(function () { + window.location.href = link.href; + }, 250); + }); diff --git a/landing/middleware.ts b/landing/middleware.ts new file mode 100644 index 0000000..b79a06e --- /dev/null +++ b/landing/middleware.ts @@ -0,0 +1,25 @@ +import { trackAICrawlerRequest } from "@datafast/ai-crawl"; + +/** + * DataFast bot traffic (datafa.st/docs/bot-traffic-tracking) as Vercel + * Routing Middleware for this static project. + * + * A local user-agent check on document requests; only known AI crawlers + * produce one background POST, scheduled with context.waitUntil and never + * awaited, so humans never trigger a network call and no response waits on + * DataFast. Returning nothing passes the request through to the static file + * or function untouched. No-op until DATAFAST_WEBSITE_ID is set. + */ +export const config = { + matcher: [ + "/((?!api/|js/|.*\\.(?:png|jpe?g|gif|svg|webp|avif|ico|css|js|map|woff2?|ttf)$).*)", + ], +}; + +export default function middleware( + request: Request, + context: { waitUntil(promise: Promise): void }, +) { + const websiteId = process.env.DATAFAST_WEBSITE_ID?.trim(); + if (websiteId) trackAICrawlerRequest(request, context, { websiteId }); +} diff --git a/landing/package.json b/landing/package.json index 5bbefff..4e87f7d 100644 --- a/landing/package.json +++ b/landing/package.json @@ -1,3 +1,7 @@ { - "type": "commonjs" + "type": "commonjs", + "private": true, + "dependencies": { + "@datafast/ai-crawl": "1.0.9" + } } diff --git a/landing/vercel.json b/landing/vercel.json index 44056bb..e6fd465 100644 --- a/landing/vercel.json +++ b/landing/vercel.json @@ -1,4 +1,14 @@ { + "rewrites": [ + { + "source": "/js/script.cookieless.js", + "destination": "https://datafa.st/js/script.cookieless.js" + }, + { + "source": "/api/events", + "destination": "https://datafa.st/api/events" + } + ], "redirects": [ { "source": "/buy", diff --git a/scripts/datafast-funnels.mjs b/scripts/datafast-funnels.mjs new file mode 100644 index 0000000..2460344 --- /dev/null +++ b/scripts/datafast-funnels.mjs @@ -0,0 +1,131 @@ +#!/usr/bin/env node +/** + * Create the Dictx landing's DataFast conversion funnels through the Account API. + * + * Funnels are dashboard objects, not code, so this is the one place their + * definitions live in the repo. Idempotent: existing funnels (by name) are left + * alone and reported; nothing is ever deleted or updated. Run again after adding + * a funnel below. + * + * DATAFAST_ADMIN_TOKEN=dft_... node scripts/datafast-funnels.mjs [--dry-run] + * + * The token comes from DataFast: Account settings -> API, scopes websites:read, + * funnels:read, funnels:write. It is an account credential: never commit it, + * never put it in Vercel; it is only needed on the machine running this script. + * + * Step semantics (datafa.st/docs/api/account/funnels/create): pageview steps + * match a URL path, goal steps match a goal *name* only. The landing is + * static HTML, so goals come from data-fast-goal attributes and the buy-click + * handler in landing/index.html. + */ + +const API = "https://datafa.st/api/v1/admin"; +const DOMAIN = process.env.DATAFAST_SITE_DOMAIN ?? "dictx.splitlabs.io"; +const DRY_RUN = process.argv.includes("--dry-run"); + +// Names are stable identifiers: renaming one here creates a second funnel. +export const FUNNELS = [ + { + // buy_pro_click is sent by the landing's click handler; payment is the + // provider goal DataFast emits once the payment provider is connected. + name: "Landing to Pro purchase", + steps: [ + { name: "Landing", type: "pageview", url: "/" }, + { name: "Buy Pro clicked", type: "goal", goalName: "buy_pro_click" }, + { name: "Payment", type: "goal", goalName: "payment" }, + ], + }, + { + name: "Pricing to purchase", + steps: [ + { name: "Saw pricing", type: "goal", goalName: "scroll_pricing" }, + { name: "Buy Pro clicked", type: "goal", goalName: "buy_pro_click" }, + { name: "Payment", type: "goal", goalName: "payment" }, + { + name: "License key copied", + type: "goal", + goalName: "license_key_copied", + }, + ], + }, + { + name: "Landing to download", + steps: [ + { name: "Landing", type: "pageview", url: "/" }, + { name: "Download DMG", type: "goal", goalName: "download_dmg" }, + ], + }, +]; + +async function api(path, init = {}) { + const token = process.env.DATAFAST_ADMIN_TOKEN; + if (!token) throw new Error("DATAFAST_ADMIN_TOKEN is not set"); + const response = await fetch(`${API}${path}`, { + ...init, + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + ...(init.headers ?? {}), + }, + }); + const json = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error( + `${init.method ?? "GET"} ${path} -> ${response.status}: ${JSON.stringify( + json, + ).slice(0, 300)}`, + ); + } + return json; +} + +async function main() { + const websites = (await api("/websites")).data ?? []; + const site = websites.find( + (w) => (w.domain ?? w.hostname ?? "").replace(/^www\./, "") === DOMAIN, + ); + if (!site) { + throw new Error( + `no DataFast website for ${DOMAIN}; have: ${websites + .map((w) => w.domain ?? w.hostname) + .join(", ")}`, + ); + } + const websiteId = site._id ?? site.id; + const existing = (await api(`/websites/${websiteId}/funnels`)).data ?? []; + const byName = new Map(existing.map((f) => [f.name, f])); + + for (const funnel of FUNNELS) { + if (byName.has(funnel.name)) { + console.log( + `= ${funnel.name} (exists, ${ + byName.get(funnel.name).steps?.length ?? "?" + } steps)`, + ); + continue; + } + if (DRY_RUN) { + console.log( + `+ ${funnel.name} (would create: ${funnel.steps + .map((s) => s.goalName ?? s.url) + .join(" -> ")})`, + ); + continue; + } + await api(`/websites/${websiteId}/funnels`, { + method: "POST", + body: JSON.stringify(funnel), + }); + console.log(`+ ${funnel.name} (created)`); + } +} + +if ( + process.argv[1] && + import.meta.url.endsWith(process.argv[1].split("/").pop()) +) { + main().catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} From 37c9332028ff966c7fd736ebb6066a7c9a55891e Mon Sep 17 00:00:00 2001 From: 0xNyk <0xnykcd@googlemail.com> Date: Tue, 15 Sep 2026 01:19:36 +0700 Subject: [PATCH 2/4] fix(landing): run crawler middleware on Node.js and type process The first preview build warned that Edge is deprecated for Routing Middleware and type-checking reported process as undeclared. Claude-Session: https://claude.ai/code/session_01CvRJfFeFvF96jhBFujxvHN --- landing/middleware.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/landing/middleware.ts b/landing/middleware.ts index b79a06e..944c8ea 100644 --- a/landing/middleware.ts +++ b/landing/middleware.ts @@ -1,5 +1,9 @@ import { trackAICrawlerRequest } from "@datafast/ai-crawl"; +// Provided by the Vercel runtime; declared so type-checking the middleware +// does not require @types/node in this dependency-free static project. +declare const process: { env: Record }; + /** * DataFast bot traffic (datafa.st/docs/bot-traffic-tracking) as Vercel * Routing Middleware for this static project. @@ -10,7 +14,9 @@ import { trackAICrawlerRequest } from "@datafast/ai-crawl"; * DataFast. Returning nothing passes the request through to the static file * or function untouched. No-op until DATAFAST_WEBSITE_ID is set. */ +// Node.js runtime: Vercel deprecated Edge for Routing Middleware. export const config = { + runtime: "nodejs", matcher: [ "/((?!api/|js/|.*\\.(?:png|jpe?g|gif|svg|webp|avif|ico|css|js|map|woff2?|ttf)$).*)", ], From 70f723321114abfdcc4b87bb4c10f4db3cf7953a Mon Sep 17 00:00:00 2001 From: 0xNyk <0xnykcd@googlemail.com> Date: Tue, 15 Sep 2026 01:22:57 +0700 Subject: [PATCH 3/4] fix(landing): pass requests through from crawler middleware explicitly Returning undefined is not a pass-through on the Node.js runtime: the preview served 500 for matched pages. Return the empty response with x-middleware-next: 1, which is what next() from @vercel/functions returns. Claude-Session: https://claude.ai/code/session_01CvRJfFeFvF96jhBFujxvHN --- landing/middleware.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/landing/middleware.ts b/landing/middleware.ts index 944c8ea..224be84 100644 --- a/landing/middleware.ts +++ b/landing/middleware.ts @@ -11,8 +11,8 @@ declare const process: { env: Record }; * A local user-agent check on document requests; only known AI crawlers * produce one background POST, scheduled with context.waitUntil and never * awaited, so humans never trigger a network call and no response waits on - * DataFast. Returning nothing passes the request through to the static file - * or function untouched. No-op until DATAFAST_WEBSITE_ID is set. + * DataFast. The request always passes through to the static file or function + * untouched. No-op until DATAFAST_WEBSITE_ID is set. */ // Node.js runtime: Vercel deprecated Edge for Routing Middleware. export const config = { @@ -28,4 +28,8 @@ export default function middleware( ) { const websiteId = process.env.DATAFAST_WEBSITE_ID?.trim(); if (websiteId) trackAICrawlerRequest(request, context, { websiteId }); + // Pass through untouched. This is exactly what next() from @vercel/functions + // returns; returning nothing is not a pass-through on the Node.js runtime + // and made every matched page a 500 on the first preview. + return new Response(null, { headers: { "x-middleware-next": "1" } }); } From d09c9d0a2c0c46d6752f98cf4c38f6d095cef151 Mon Sep 17 00:00:00 2001 From: 0xNyk <0xnykcd@googlemail.com> Date: Tue, 15 Sep 2026 01:27:05 +0700 Subject: [PATCH 4/4] fix(landing): run crawler middleware on Edge again On preview the Node.js runtime served every matched HTML page as 500 while the Edge build of the same middleware served 200. Keep the explicit pass-through response and the process type. Claude-Session: https://claude.ai/code/session_01CvRJfFeFvF96jhBFujxvHN --- landing/middleware.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/landing/middleware.ts b/landing/middleware.ts index 224be84..5f5439f 100644 --- a/landing/middleware.ts +++ b/landing/middleware.ts @@ -14,9 +14,10 @@ declare const process: { env: Record }; * DataFast. The request always passes through to the static file or function * untouched. No-op until DATAFAST_WEBSITE_ID is set. */ -// Node.js runtime: Vercel deprecated Edge for Routing Middleware. +// Edge runtime on purpose. Vercel warns that Edge is deprecated for Routing +// Middleware, but on this static "Other" project the Node.js runtime served +// every matched page as a 500 on preview, while Edge served 200. export const config = { - runtime: "nodejs", matcher: [ "/((?!api/|js/|.*\\.(?:png|jpe?g|gif|svg|webp|avif|ico|css|js|map|woff2?|ttf)$).*)", ], @@ -29,7 +30,6 @@ export default function middleware( const websiteId = process.env.DATAFAST_WEBSITE_ID?.trim(); if (websiteId) trackAICrawlerRequest(request, context, { websiteId }); // Pass through untouched. This is exactly what next() from @vercel/functions - // returns; returning nothing is not a pass-through on the Node.js runtime - // and made every matched page a 500 on the first preview. + // returns, so the pass-through does not depend on runtime defaults. return new Response(null, { headers: { "x-middleware-next": "1" } }); }