Add Umami analytics and broaden beta copy for Android.

Optional NEXT_PUBLIC_UMAMI_* env vars load tracking when set; marketing pages now mention iOS TestFlight and Google Play internal testing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 17:30:03 -04:00
co-authored by Cursor
parent d6040e2a09
commit bbdef0faa6
8 changed files with 83 additions and 22 deletions
+4
View File
@@ -16,3 +16,7 @@
# Password required to view the site (login page at /login). # Password required to view the site (login page at /login).
# Leave empty/unset to disable the gate. # Leave empty/unset to disable the gate.
SITE_PASSWORD="" SITE_PASSWORD=""
# Umami analytics (optional). Omit NEXT_PUBLIC_UMAMI_WEBSITE_ID to disable tracking.
# NEXT_PUBLIC_UMAMI_WEBSITE_ID="your-website-uuid"
# NEXT_PUBLIC_UMAMI_SCRIPT_URL="https://cloud.umami.is/script.js"
+13 -4
View File
@@ -1,6 +1,6 @@
# Medscribe Web # Medscribe Web
Marketing homepage for [Medscribe](../README.md) — a privacy-first, on-device medical companion for iOS. Marketing homepage for [Medscribe](../README.md) — a privacy-first, on-device medical companion for iOS and Android.
Built with [create-t3-app](https://create.t3.gg/) (Next.js + TypeScript + Tailwind). Single-page site; no database or API routes. Built with [create-t3-app](https://create.t3.gg/) (Next.js + TypeScript + Tailwind). Single-page site; no database or API routes.
@@ -15,6 +15,15 @@ bun run dev
Open [http://localhost:3000](http://localhost:3000). Open [http://localhost:3000](http://localhost:3000).
### Analytics (optional)
Set in `.env` (see `.env.example`):
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` — website UUID from your Umami dashboard
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` — optional; defaults to `https://cloud.umami.is/script.js` (use your self-hosted URL if needed)
Leave the website ID unset to disable tracking (default for local dev).
## Build ## Build
```bash ```bash
@@ -26,10 +35,10 @@ Screenshots live in `public/screenshots/`. They are wrapped in an iPhone-style f
### Capturing raw screenshots for the site ### Capturing raw screenshots for the site
Export **in-app content only** from the simulator or device — no simulator chrome or macOS capture toolbar. A normal in-app iOS status bar/tab bar is fine. Export **in-app content only** from the simulator or device — no simulator chrome or macOS capture toolbar. A normal in-app status bar/tab bar is fine.
1. Run the app on **iPhone 17 Pro** (or 16 Pro) simulator or device. 1. Run the app on **iPhone 17 Pro** (or 16 Pro) simulator or device, or on an **Android** emulator/device.
2. Capture each screen (⌘S in Simulator saves to Desktop). 2. Capture each screen (⌘S in Simulator saves to Desktop; `adb exec-out screencap -p` on Android).
3. Drop PNGs into `public/screenshots/` and update paths in `src/app/page.tsx` / `src/components/feature-showcase.tsx`. 3. Drop PNGs into `public/screenshots/` and update paths in `src/app/page.tsx` / `src/components/feature-showcase.tsx`.
The frame dimensions assume tall iPhone screenshots and are controlled in `src/components/phone-mockup.tsx`. Use `captureMode="simulator"` only for old captures that already include device chrome. The frame dimensions assume tall iPhone screenshots and are controlled in `src/components/phone-mockup.tsx`. Use `captureMode="simulator"` only for old captures that already include device chrome.
@@ -9,6 +9,7 @@ import { MedscribeLogo } from "~/components/medscribe-logo";
import { PhoneMockup } from "~/components/phone-mockup"; import { PhoneMockup } from "~/components/phone-mockup";
import { PrintButton } from "~/components/print-button"; import { PrintButton } from "~/components/print-button";
import { SectionLabel } from "~/components/section-label"; import { SectionLabel } from "~/components/section-label";
import { BETA_FOOTER_NOTE, BETA_SUMMARY } from "~/lib/beta";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { import {
Card, Card,
@@ -326,9 +327,7 @@ export default function ExecutiveSummaryPage() {
{/* Beta */} {/* Beta */}
<section className="flex flex-col gap-5 rounded-xl border border-border-soft bg-surface p-6 sm:flex-row sm:items-center sm:justify-between sm:p-8"> <section className="flex flex-col gap-5 rounded-xl border border-border-soft bg-surface p-6 sm:flex-row sm:items-center sm:justify-between sm:p-8">
<p className="max-w-xl text-sm leading-relaxed text-muted-foreground"> <p className="max-w-xl text-sm leading-relaxed text-muted-foreground">
A beta is available on iOS through TestFlight. Everything runs {BETA_SUMMARY} If you&apos;d like to try it, reach out.
privately on the device, so no health data leaves the phone. If
you&apos;d like to try it, reach out.
</p> </p>
<PrintButton size="lg" className="shrink-0" /> <PrintButton size="lg" className="shrink-0" />
</section> </section>
@@ -459,9 +458,7 @@ export default function ExecutiveSummaryPage() {
<footer className="mt-auto flex items-center justify-between border-t border-teal-900/10 pt-1.5 text-[10pt] text-slate-500"> <footer className="mt-auto flex items-center justify-between border-t border-teal-900/10 pt-1.5 text-[10pt] text-slate-500">
<p>Sean O&apos;Connor</p> <p>Sean O&apos;Connor</p>
<p className="text-slate-400"> <p className="text-slate-400">{BETA_FOOTER_NOTE}</p>
A beta is available on iOS through TestFlight.
</p>
</footer> </footer>
</article> </article>
</> </>
+11 -9
View File
@@ -13,6 +13,11 @@ import {
import { FeatureShowcase } from "~/components/feature-showcase"; import { FeatureShowcase } from "~/components/feature-showcase";
import { PhoneMockup } from "~/components/phone-mockup"; import { PhoneMockup } from "~/components/phone-mockup";
import { SectionLabel } from "~/components/section-label"; import { SectionLabel } from "~/components/section-label";
import {
BETA_CTA_BODY,
BETA_CTA_TITLE,
BETA_INVITE_MAILTO,
} from "~/lib/beta";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { import {
@@ -119,8 +124,9 @@ export default function HomePage() {
</div> </div>
<div className="animate-fade-in-up-delay-2 flex flex-wrap gap-2"> <div className="animate-fade-in-up-delay-2 flex flex-wrap gap-2">
<Badge variant="success">On TestFlight</Badge> <Badge variant="success">Beta available</Badge>
<Badge variant="outline">iOS</Badge> <Badge variant="outline">iOS · TestFlight</Badge>
<Badge variant="outline">Android · Play internal</Badge>
<Badge variant="outline">Emergency-ready</Badge> <Badge variant="outline">Emergency-ready</Badge>
<Badge variant="outline">No cloud AI</Badge> <Badge variant="outline">No cloud AI</Badge>
</div> </div>
@@ -319,11 +325,9 @@ export default function HomePage() {
<Card className="overflow-hidden border-primary-button/20 bg-primary-button text-white"> <Card className="overflow-hidden border-primary-button/20 bg-primary-button text-white">
<CardHeader className="gap-4 sm:flex-row sm:items-center sm:justify-between"> <CardHeader className="gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-2"> <div className="space-y-2">
<CardTitle className="text-white">Try Medscribe on iOS</CardTitle> <CardTitle className="text-white">{BETA_CTA_TITLE}</CardTitle>
<CardDescription className="text-white/75"> <CardDescription className="text-white/75">
The app is live on TestFlight. Reach out for an invite if {BETA_CTA_BODY}
you&apos;re managing medications, visits, or emergency context for
someone you love.
</CardDescription> </CardDescription>
</div> </div>
<Button <Button
@@ -332,9 +336,7 @@ export default function HomePage() {
variant="outline" variant="outline"
className="border-white/30 bg-white text-primary-button hover:bg-white/90" className="border-white/30 bg-white text-primary-button hover:bg-white/90"
> >
<a href="mailto:sean@soconnor.dev?subject=Medscribe%20TestFlight%20invite%20request&body=Hi%20Sean%2C%0D%0A%0D%0AI%27d%20like%20a%20TestFlight%20invite%20to%20try%20Medscribe.%20I%27m%20managing%20medications%2C%20visits%2C%20or%20emergency%20context%20for%20someone%20I%20care%20for.%0D%0A%0D%0AThanks%21"> <a href={BETA_INVITE_MAILTO}>Request an invite</a>
Request an invite
</a>
</Button> </Button>
</CardHeader> </CardHeader>
</Card> </Card>
+6 -1
View File
@@ -3,6 +3,8 @@ import "~/styles/globals.css";
import { type Metadata } from "next"; import { type Metadata } from "next";
import { Instrument_Sans, Playfair_Display } from "next/font/google"; import { Instrument_Sans, Playfair_Display } from "next/font/google";
import { UmamiAnalytics } from "~/components/umami-analytics";
const instrumentSans = Instrument_Sans({ const instrumentSans = Instrument_Sans({
subsets: ["latin"], subsets: ["latin"],
variable: "--font-instrument-sans", variable: "--font-instrument-sans",
@@ -42,7 +44,10 @@ export default function RootLayout({
}: Readonly<{ children: React.ReactNode }>) { }: Readonly<{ children: React.ReactNode }>) {
return ( return (
<html lang="en" className={`${instrumentSans.variable} ${playfair.variable}`}> <html lang="en" className={`${instrumentSans.variable} ${playfair.variable}`}>
<body className="min-h-screen">{children}</body> <body className="min-h-screen">
{children}
<UmamiAnalytics />
</body>
</html> </html>
); );
} }
+25
View File
@@ -0,0 +1,25 @@
import Script from "next/script";
import { env } from "~/env";
const DEFAULT_UMAMI_SCRIPT_URL = "https://cloud.umami.is/script.js";
/**
* Privacy-friendly page analytics via Umami. Enabled when
* `NEXT_PUBLIC_UMAMI_WEBSITE_ID` is set in `.env`.
*/
export function UmamiAnalytics() {
const websiteId = env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
if (!websiteId) return null;
const src = env.NEXT_PUBLIC_UMAMI_SCRIPT_URL ?? DEFAULT_UMAMI_SCRIPT_URL;
return (
<Script
defer
src={src}
data-website-id={websiteId}
strategy="afterInteractive"
/>
);
}
+6 -2
View File
@@ -18,7 +18,10 @@ export const env = createEnv({
* `NEXT_PUBLIC_`. * `NEXT_PUBLIC_`.
*/ */
client: { client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(), /** Umami website ID — omit to disable analytics. */
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(),
/** Umami script URL (defaults to Umami Cloud). */
NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.string().url().optional(),
}, },
/** /**
@@ -28,7 +31,8 @@ export const env = createEnv({
runtimeEnv: { runtimeEnv: {
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
SITE_PASSWORD: process.env.SITE_PASSWORD, SITE_PASSWORD: process.env.SITE_PASSWORD,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
}, },
/** /**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
+15
View File
@@ -0,0 +1,15 @@
/** Shared beta / early-access copy for the marketing site. */
export const BETA_SUMMARY =
"A beta is available on iOS (TestFlight) and Android (Google Play internal testing). Everything runs privately on the device, so no health data leaves the phone.";
export const BETA_CTA_TITLE = "Try Medscribe";
export const BETA_CTA_BODY =
"The app is in beta on iOS and Android. Reach out for an invite if you're managing medications, visits, or emergency context for someone you love.";
export const BETA_INVITE_MAILTO =
"mailto:sean@soconnor.dev?subject=Medscribe%20beta%20invite%20request&body=Hi%20Sean%2C%0D%0A%0D%0AI%27d%20like%20a%20beta%20invite%20to%20try%20Medscribe%20(iOS%20or%20Android).%20I%27m%20managing%20medications%2C%20visits%2C%20or%20emergency%20context%20for%20someone%20I%20care%20for.%0D%0A%0D%0AThanks%21";
export const BETA_FOOTER_NOTE =
"Beta on iOS (TestFlight) and Android (Google Play internal testing).";