Marketing Site + Trial Update Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Rewrite the public marketing site for v2 fact engine, switch trial to 14-day with CC collection via Stripe, and add subscription card injection in the app feed.
Architecture: Next.js rewrite proxy for feed data (public site → app API), Stripe Checkout for trial signup with payment method collection, lightweight card components in public app, modal dialog for card click CTA.
Tech Stack: Next.js 16 (App Router), Tailwind v4, Stripe Checkout, Drizzle ORM, Zod
Task 1: Trial Duration Migration (5.1)
Files:
- Create:
supabase/migrations/0114_trial_14_days.sql
Step 1: Write the migration
-- ============================================================================
-- MIGRATION 0114: Update Trial Duration to 14 Days
--
-- Switches Eko+ trial from 30 days to 14 days. The subscribe page and Stripe
-- checkout will be updated in subsequent tasks to use this value.
--
-- Changes:
-- - Update plan_definitions.trial_days from 30 to 14 for 'plus' plan
-- ============================================================================
UPDATE plan_definitions SET trial_days = 14 WHERE plan_key = 'plus';
Step 2: Regenerate migrations index
Run: bun run migrations:index
Expected: supabase/migrations-index.md updated with 0114 entry
Step 3: Verify migrations check passes
Run: bun run migrations:check
Expected: PASS
Step 4: Commit
git add supabase/migrations/0114_trial_14_days.sql supabase/migrations-index.md
git commit -m "feat(db): add migration 0114 to update trial to 14 days"
Task 2: Add getPlanTrialDays Query (5.1 continued)
Files:
- Modify:
packages/db/src/drizzle/fact-engine-queries.ts - Modify:
packages/db/src/index.ts - Modify:
apps/web/app/actions/subscription.ts
Step 1: Add query function to fact-engine-queries.ts
Add after the createTrialSubscription function (around line 1054):
/**
* Get trial days for a plan from plan_definitions.
* Returns 0 if plan not found.
*/
export async function getPlanTrialDays(planKey: string): Promise<number> {
const db = getDrizzleClient()
const plan = await db.query.planDefinitions.findFirst({
where: eq(planDefinitions.planKey, planKey),
columns: { trialDays: true },
})
return plan?.trialDays ?? 0
}
Step 2: Export from packages/db/src/index.ts
Add getPlanTrialDays to the direct exports from ./drizzle/fact-engine-queries (alphabetical order in the existing export block).
Step 3: Update subscription action to use dynamic trial days
In apps/web/app/actions/subscription.ts, change:
// Before
import { createTrialSubscription, drizzleQueries } from '@eko/db'
const subscription = await createTrialSubscription(user.id, 'plus', 30)
To:
// After
import { createTrialSubscription, drizzleQueries, getPlanTrialDays } from '@eko/db'
const trialDays = await getPlanTrialDays('plus')
const subscription = await createTrialSubscription(user.id, 'plus', trialDays || 14)
Step 4: Run typecheck
Run: bun run typecheck
Expected: No errors
Step 5: Commit
git add packages/db/src/drizzle/fact-engine-queries.ts packages/db/src/index.ts apps/web/app/actions/subscription.ts
git commit -m "feat(db): add getPlanTrialDays query, use dynamic trial days in action"
Task 3: Stripe Checkout — Allow Plus Plan + Payment Method Collection (5.2)
Files:
- Modify:
apps/web/app/api/stripe/checkout/route.ts - Modify:
packages/stripe/src/checkout.ts
Step 1: Update checkout route to accept 'plus' plan and billingPeriod
In apps/web/app/api/stripe/checkout/route.ts, change the Zod schema and pass trial days:
const CheckoutRequestSchema = z.object({
planKey: z.enum(['base', 'pro', 'team', 'plus']),
billingPeriod: z.enum(['monthly', 'annual']).optional(),
})
In the POST handler, after parsing, pass billingPeriod and trialDays:
const { planKey, billingPeriod } = parsed.data
const { sessionId, url } = await createCheckoutSession(supabase, {
userId: user.id,
userEmail: user.email!,
planKey: planKey as PlanKey,
billingPeriod,
trialDays: planKey === 'plus' ? 14 : undefined,
successUrl,
cancelUrl,
})
Step 2: Add payment_method_collection to Stripe session
In packages/stripe/src/checkout.ts, inside createCheckoutSession, add payment_method_collection to the session create call:
const session = await stripe.client.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
payment_method_collection: 'always',
line_items: [{ price: priceId, quantity: 1 }],
// ... rest unchanged
})
Step 3: Run typecheck
Run: bun run typecheck
Expected: No errors
Step 4: Commit
git add apps/web/app/api/stripe/checkout/route.ts packages/stripe/src/checkout.ts
git commit -m "feat(stripe): allow plus plan checkout with payment method collection"
Task 4: Subscribe Page — Stripe Redirect + Copy Updates (5.2 continued)
Files:
- Modify:
apps/web/app/subscribe/_components/subscribe-page.tsx
Step 1: Replace local trial with Stripe checkout redirect
Replace the handleStartTrial function and update copy:
'use client'
import { Button } from '@eko/ui'
import Link from 'next/link'
import { useState } from 'react'
interface SubscribePageProps {
isAuthenticated: boolean
hasActiveSubscription: boolean
}
const FREE_FEATURES = ['Browse the daily feed', 'See headlines and topics', 'View category filters']
const PLUS_FEATURES = [
'Full card details and insights',
'Multiple choice trivia quizzes',
'Open recall practice',
'Bookmark up to 10 cards',
'Validation sources',
'14-day free trial',
]
export function SubscribePage({ isAuthenticated, hasActiveSubscription }: SubscribePageProps) {
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const handleStartTrial = async (billingPeriod: 'monthly' | 'annual' = 'monthly') => {
setError(null)
setIsLoading(true)
try {
const res = await fetch('/api/stripe/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ planKey: 'plus', billingPeriod }),
})
const data = await res.json()
if (!res.ok) {
setError(data.error || 'Something went wrong')
return
}
// Redirect to Stripe Checkout
window.location.href = data.url
} catch {
setError('Something went wrong. Please try again.')
} finally {
setIsLoading(false)
}
}
// ... rest of component unchanged except:
// - CTA button text: 'Start 14-day free trial' (was 'Start 30-day free trial')
// - CTA onClick: handleStartTrial() (no change in signature)
// - Footer text: 'No charge for 14 days. Cancel anytime.' (was 'No credit card required...')
}
Key changes to the JSX:
- Line with
'Start 30-day free trial'→'Start 14-day free trial' - Footer
<p>→'No charge for 14 days. Cancel anytime.'
Step 2: Run typecheck and lint
Run: bun run typecheck && bun run lint
Expected: No errors
Step 3: Commit
git add apps/web/app/subscribe/_components/subscribe-page.tsx
git commit -m "feat(subscribe): redirect to Stripe checkout for trial with CC collection"
Task 5: Feed Proxy Rewrite (5.8)
Files:
- Modify:
apps/public/next.config.ts
Step 1: Add rewrite rule
import { withSentryConfig } from '@sentry/nextjs'
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
transpilePackages: ['@eko/config', '@eko/shared', '@eko/ui-public'],
async rewrites() {
return [
{
source: '/api/feed',
destination: process.env.FEED_API_URL || 'https://app.eko.day/api/feed',
},
]
},
}
export default withSentryConfig(nextConfig, {
org: 'lab90-llc',
project: 'eko-public',
silent: !process.env.CI,
widenClientFileUpload: true,
webpack: {
treeshake: {
removeDebugLogging: true,
},
},
})
Step 2: Run typecheck
Run: bun run typecheck
Expected: No errors
Step 3: Commit
git add apps/public/next.config.ts
git commit -m "feat(public): add feed API rewrite proxy"
Task 6: Public Site Layout — Nav, Footer, Metadata (5.3)
Files:
- Create:
apps/public/app/_components/site-nav.tsx - Create:
apps/public/app/_components/site-footer.tsx - Modify:
apps/public/app/layout.tsx
Step 1: Create site nav component
apps/public/app/_components/site-nav.tsx:
import Link from 'next/link'
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://app.eko.day'
const NAV_LINKS = [
{ href: '/features', label: 'Features' },
{ href: '/pricing', label: 'Pricing' },
{ href: '/about', label: 'About' },
]
export function SiteNav() {
return (
<header className="border-b">
<nav className="mx-auto flex max-w-6xl items-center justify-between px-4 py-3 sm:px-6">
<Link href="/" className="text-xl font-bold tracking-tight">
Eko
</Link>
<div className="flex items-center gap-6">
<div className="hidden items-center gap-4 sm:flex">
{NAV_LINKS.map((link) => (
<Link
key={link.href}
href={link.href}
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
>
{link.label}
</Link>
))}
</div>
<div className="flex items-center gap-2">
<a
href={`${APP_URL}/login`}
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
Sign in
</a>
<a
href={`${APP_URL}/subscribe`}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Start free trial
</a>
</div>
</div>
</nav>
</header>
)
}
Step 2: Create site footer component
apps/public/app/_components/site-footer.tsx:
import Link from 'next/link'
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://app.eko.day'
export function SiteFooter() {
return (
<footer className="border-t">
<div className="mx-auto max-w-6xl px-4 py-8 sm:px-6">
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} Eko. All rights reserved.
</p>
<div className="flex gap-4">
<Link href="/features" className="text-sm text-muted-foreground hover:text-foreground">
Features
</Link>
<Link href="/pricing" className="text-sm text-muted-foreground hover:text-foreground">
Pricing
</Link>
<Link href="/about" className="text-sm text-muted-foreground hover:text-foreground">
About
</Link>
<a
href={`${APP_URL}/login`}
className="text-sm text-muted-foreground hover:text-foreground"
>
Sign in
</a>
</div>
</div>
</div>
</footer>
)
}
Step 3: Update layout with nav, footer, and v2 metadata
Replace apps/public/app/layout.tsx:
import type { Metadata } from 'next'
import { Red_Hat_Text } from 'next/font/google'
import { SiteFooter } from './_components/site-footer'
import { SiteNav } from './_components/site-nav'
import './globals.css'
const redHatText = Red_Hat_Text({
subsets: ['latin'],
variable: '--font-sans',
display: 'swap',
})
export const metadata: Metadata = {
title: 'Eko — Challenge what you know. Learn what you don\'t.',
description:
'Eko delivers daily knowledge challenges powered by verified facts from real news. Test yourself across 36+ categories with spaced repetition.',
icons: {
icon: [
{ url: '/brand/raster/favicon-16.png', sizes: '16x16', type: 'image/png' },
{ url: '/brand/raster/favicon-32.png', sizes: '32x32', type: 'image/png' },
],
apple: [{ url: '/brand/raster/apple-touch-icon.png', sizes: '180x180', type: 'image/png' }],
},
openGraph: {
title: 'Eko — Challenge what you know. Learn what you don\'t.',
description:
'Daily knowledge challenges powered by verified facts from real news. 36+ categories. Spaced repetition.',
type: 'website',
siteName: 'Eko',
},
twitter: {
card: 'summary_large_image',
title: 'Eko — Challenge what you know. Learn what you don\'t.',
description:
'Daily knowledge challenges powered by verified facts from real news. 36+ categories. Spaced repetition.',
},
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${redHatText.variable} flex min-h-screen flex-col font-sans antialiased`}>
<SiteNav />
<main className="flex-1">{children}</main>
<SiteFooter />
</body>
</html>
)
}
Step 4: Run typecheck
Run: bun run typecheck
Expected: No errors
Step 5: Commit
git add apps/public/app/_components/site-nav.tsx apps/public/app/_components/site-footer.tsx apps/public/app/layout.tsx
git commit -m "feat(public): add site nav, footer, and v2 metadata"
Task 7: Public Home Page with Feed + Card Modal (5.3)
Files:
- Create:
apps/public/app/_components/public-fact-card.tsx - Create:
apps/public/app/_components/card-modal.tsx - Modify:
apps/public/app/page.tsx
Step 1: Create public fact card component
apps/public/app/_components/public-fact-card.tsx:
A lightweight card component that renders title, topic, and published date. No auth, no status badges, no interactions. Clicking opens the modal.
interface PublicFactCardProps {
title: string
topicCategory: {
name: string
slug: string
icon: string | null
color: string | null
} | null
publishedAt: string | null
imageUrl?: string | null
onClick: () => void
}
export function PublicFactCard({
title,
topicCategory,
publishedAt,
imageUrl,
onClick,
}: PublicFactCardProps) {
const timeAgo = publishedAt ? getTimeAgo(new Date(publishedAt)) : null
return (
<button
type="button"
onClick={onClick}
className="group flex flex-col rounded-xl border bg-card p-4 text-left transition-shadow hover:shadow-md"
>
{imageUrl && (
<div className="mb-3 aspect-video overflow-hidden rounded-lg bg-muted">
<img
src={imageUrl}
alt=""
className="h-full w-full object-cover transition-transform group-hover:scale-105"
/>
</div>
)}
{topicCategory && (
<span
className="mb-2 w-fit rounded-full px-2 py-0.5 text-xs font-medium"
style={{
backgroundColor: topicCategory.color ? `${topicCategory.color}20` : undefined,
color: topicCategory.color ?? undefined,
}}
>
{topicCategory.icon} {topicCategory.name}
</span>
)}
<h3 className="mb-2 line-clamp-3 text-sm font-semibold leading-snug">{title}</h3>
{timeAgo && (
<span className="mt-auto text-xs text-muted-foreground">{timeAgo}</span>
)}
</button>
)
}
function getTimeAgo(date: Date): string {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000)
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`
return `${Math.floor(seconds / 86400)}d ago`
}
Step 2: Create card modal component
apps/public/app/_components/card-modal.tsx:
'use client'
import { useEffect, useRef } from 'react'
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://app.eko.day'
interface CardModalProps {
isOpen: boolean
onClose: () => void
title: string
topicName: string | null
topicIcon: string | null
}
export function CardModal({ isOpen, onClose, title, topicName, topicIcon }: CardModalProps) {
const dialogRef = useRef<HTMLDialogElement>(null)
useEffect(() => {
const dialog = dialogRef.current
if (!dialog) return
if (isOpen && !dialog.open) dialog.showModal()
if (!isOpen && dialog.open) dialog.close()
}, [isOpen])
return (
<dialog
ref={dialogRef}
onClose={onClose}
className="w-full max-w-md rounded-xl border bg-card p-0 shadow-lg backdrop:bg-black/50"
>
<div className="p-6">
{topicName && (
<span className="mb-3 inline-block text-xs font-medium text-muted-foreground">
{topicIcon} {topicName}
</span>
)}
<h2 className="mb-4 text-lg font-bold leading-snug">{title}</h2>
<p className="mb-6 text-sm text-muted-foreground">
Sign up to unlock the full insight, take the challenge, and track what you know.
</p>
<div className="flex flex-col gap-2">
<a
href={`${APP_URL}/subscribe`}
className="rounded-md bg-primary px-4 py-2.5 text-center text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Start 14-day free trial
</a>
<a
href={`${APP_URL}/login`}
className="rounded-md border px-4 py-2.5 text-center text-sm text-muted-foreground transition-colors hover:bg-muted"
>
Already have an account? Sign in
</a>
</div>
</div>
<button
type="button"
onClick={onClose}
className="absolute right-3 top-3 rounded-md p-1 text-muted-foreground hover:text-foreground"
aria-label="Close"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
</dialog>
)
}
Step 3: Update home page
Replace apps/public/app/page.tsx:
import { PublicFeed } from './_components/public-feed'
const FEED_URL = process.env.FEED_API_URL || 'https://app.eko.day/api/feed'
export const metadata = {
title: 'Eko — Challenge what you know',
}
interface FeedResponse {
facts: Array<{
id: string
title: string
topicCategory: {
name: string
slug: string
icon: string | null
color: string | null
} | null
publishedAt: string | null
imageUrl?: string | null
}>
}
export default async function HomePage() {
let facts: FeedResponse['facts'] = []
try {
const res = await fetch(`${FEED_URL}?limit=24`, {
next: { revalidate: 300 },
})
if (res.ok) {
const data: FeedResponse = await res.json()
facts = data.facts
}
} catch {
// Graceful degradation — show hero without feed
}
return (
<div>
{/* Hero */}
<section className="mx-auto max-w-6xl px-4 py-16 text-center sm:px-6 sm:py-24">
<h1 className="mb-4 text-4xl font-bold tracking-tight sm:text-5xl">
Challenge what you know.
<br />
<span className="text-primary">Learn what you don't.</span>
</h1>
<p className="mx-auto mb-8 max-w-2xl text-lg text-muted-foreground">
Daily knowledge challenges powered by verified facts from real news.
36+ categories. Spaced repetition that actually works.
</p>
<a
href={`${process.env.NEXT_PUBLIC_APP_URL || 'https://app.eko.day'}/subscribe`}
className="inline-block rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Start your 14-day free trial
</a>
</section>
{/* Live Feed */}
{facts.length > 0 && (
<section className="mx-auto max-w-6xl px-4 pb-16 sm:px-6">
<h2 className="mb-6 text-center text-2xl font-bold">Today's challenges</h2>
<PublicFeed facts={facts} />
</section>
)}
</div>
)
}
Step 4: Create PublicFeed client component
apps/public/app/_components/public-feed.tsx:
'use client'
import { useState } from 'react'
import { CardModal } from './card-modal'
import { PublicFactCard } from './public-fact-card'
interface FeedFact {
id: string
title: string
topicCategory: {
name: string
slug: string
icon: string | null
color: string | null
} | null
publishedAt: string | null
imageUrl?: string | null
}
interface PublicFeedProps {
facts: FeedFact[]
}
export function PublicFeed({ facts }: PublicFeedProps) {
const [selectedFact, setSelectedFact] = useState<FeedFact | null>(null)
return (
<>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{facts.map((fact) => (
<PublicFactCard
key={fact.id}
title={fact.title}
topicCategory={fact.topicCategory}
publishedAt={fact.publishedAt}
imageUrl={fact.imageUrl}
onClick={() => setSelectedFact(fact)}
/>
))}
</div>
<CardModal
isOpen={selectedFact !== null}
onClose={() => setSelectedFact(null)}
title={selectedFact?.title ?? ''}
topicName={selectedFact?.topicCategory?.name ?? null}
topicIcon={selectedFact?.topicCategory?.icon ?? null}
/>
</>
)
}
Step 5: Run typecheck
Run: bun run typecheck
Expected: No errors
Step 6: Commit
git add apps/public/app/_components/public-fact-card.tsx apps/public/app/_components/card-modal.tsx apps/public/app/_components/public-feed.tsx apps/public/app/page.tsx
git commit -m "feat(public): add home page with live feed and card modal"
Task 8: Pricing Page (5.4)
Files:
- Create:
apps/public/app/pricing/_components/pricing-toggle.tsx - Modify:
apps/public/app/pricing/page.tsx
Step 1: Create pricing toggle client component
apps/public/app/pricing/_components/pricing-toggle.tsx:
'use client'
import { useState } from 'react'
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://app.eko.day'
const FREE_FEATURES = [
'Browse the daily feed',
'See headlines and topics',
'View category filters',
]
const PLUS_FEATURES = [
'Everything in Free',
'Full card details and insights',
'Multiple choice trivia quizzes',
'Open recall practice',
'Bookmark up to 10 cards',
'Validation sources',
'Spaced repetition tracking',
]
export function PricingToggle() {
const [annual, setAnnual] = useState(false)
return (
<div>
{/* Toggle */}
<div className="mb-10 flex items-center justify-center gap-3">
<span className={`text-sm ${!annual ? 'font-medium text-foreground' : 'text-muted-foreground'}`}>
Monthly
</span>
<button
type="button"
onClick={() => setAnnual(!annual)}
className={`relative h-6 w-11 rounded-full transition-colors ${annual ? 'bg-primary' : 'bg-muted-foreground/30'}`}
role="switch"
aria-checked={annual}
>
<span
className={`absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${annual ? 'translate-x-5' : ''}`}
/>
</button>
<span className={`text-sm ${annual ? 'font-medium text-foreground' : 'text-muted-foreground'}`}>
Annual <span className="text-xs text-primary">(save 27%)</span>
</span>
</div>
{/* Plans */}
<div className="mx-auto grid max-w-3xl gap-6 md:grid-cols-2">
{/* Free */}
<div className="rounded-xl border p-6">
<h3 className="mb-1 text-xl font-semibold">Free</h3>
<div className="mb-4">
<span className="text-3xl font-bold">$0</span>
<span className="text-muted-foreground">/forever</span>
</div>
<ul className="mb-6 space-y-3">
{FREE_FEATURES.map((f) => (
<li key={f} className="flex items-start gap-2 text-sm">
<CheckIcon className="text-muted-foreground" />
<span>{f}</span>
</li>
))}
</ul>
<a
href={`${APP_URL}/signup`}
className="block rounded-md border px-4 py-2.5 text-center text-sm font-medium transition-colors hover:bg-muted"
>
Sign up free
</a>
</div>
{/* Eko+ */}
<div className="relative rounded-xl border-2 border-primary p-6">
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<span className="rounded-full bg-primary px-3 py-1 text-xs font-medium text-primary-foreground">
Recommended
</span>
</div>
<h3 className="mb-1 text-xl font-semibold">Eko+</h3>
<div className="mb-1">
<span className="text-3xl font-bold">{annual ? '$34.99' : '$3.99'}</span>
<span className="text-muted-foreground">{annual ? '/year' : '/month'}</span>
</div>
{!annual && (
<p className="mb-4 text-xs text-muted-foreground">or $34.99/year (save 27%)</p>
)}
{annual && (
<p className="mb-4 text-xs text-muted-foreground">$2.92/month, billed annually</p>
)}
<ul className="mb-6 space-y-3">
{PLUS_FEATURES.map((f) => (
<li key={f} className="flex items-start gap-2 text-sm">
<CheckIcon className="text-primary" />
<span>{f}</span>
</li>
))}
</ul>
<a
href={`${APP_URL}/subscribe`}
className="block rounded-md bg-primary px-4 py-2.5 text-center text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Start 14-day free trial
</a>
<p className="mt-3 text-center text-xs text-muted-foreground">
No charge for 14 days. Cancel anytime.
</p>
</div>
</div>
</div>
)
}
function CheckIcon({ className }: { className?: string }) {
return (
<svg
className={`mt-0.5 h-4 w-4 shrink-0 ${className}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
)
}
Step 2: Update pricing page
Replace apps/public/app/pricing/page.tsx:
import { PricingToggle } from './_components/pricing-toggle'
export const metadata = { title: 'Pricing | Eko' }
export default function PricingPage() {
return (
<div className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
<div className="mb-10 text-center">
<h1 className="mb-3 text-3xl font-bold sm:text-4xl">Simple, transparent pricing</h1>
<p className="text-muted-foreground">
Browse for free. Unlock everything with Eko+ across 36+ topic categories.
</p>
</div>
<PricingToggle />
</div>
)
}
Step 3: Run typecheck
Run: bun run typecheck
Expected: No errors
Step 4: Commit
git add apps/public/app/pricing/page.tsx apps/public/app/pricing/_components/pricing-toggle.tsx
git commit -m "feat(public): add pricing page with plan comparison and annual toggle"
Task 9: Features Page (5.5)
Files:
- Modify:
apps/public/app/features/page.tsx
Step 1: Replace stub with features content
export const metadata = { title: 'Features | Eko' }
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://app.eko.day'
const FEATURES = [
{
icon: '🎯',
title: 'Challenge System',
description:
'Every fact becomes a challenge. Multiple choice, open recall, true/false — formats that make you think, not just read.',
},
{
icon: '🔄',
title: 'Spaced Repetition',
description:
'Facts you struggle with come back more often. Facts you master fade into review. Your feed adapts to what you actually know.',
},
{
icon: '⚖️',
title: 'Score Disputes & Rewards',
description:
'Think a question was unfair? Dispute it. Earn milestone badges as your knowledge grows across categories.',
},
{
icon: '🌍',
title: '36+ Topic Categories',
description:
'Science, history, geography, economics, technology, health, culture, and more. New verified facts every day from real news sources.',
},
]
export default function FeaturesPage() {
return (
<div className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
<div className="mb-16 text-center">
<h1 className="mb-4 text-3xl font-bold sm:text-4xl">
Knowledge that sticks
</h1>
<p className="mx-auto max-w-2xl text-lg text-muted-foreground">
Eko turns verified news facts into daily challenges. Not flashcards. Not quizzes.
Challenges that make you curious.
</p>
</div>
<div className="grid gap-8 sm:grid-cols-2">
{FEATURES.map((feature) => (
<div key={feature.title} className="rounded-xl border p-6">
<span className="mb-3 block text-3xl">{feature.icon}</span>
<h3 className="mb-2 text-lg font-semibold">{feature.title}</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
{feature.description}
</p>
</div>
))}
</div>
<div className="mt-16 text-center">
<a
href={`${APP_URL}/subscribe`}
className="inline-block rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Start your 14-day free trial
</a>
</div>
</div>
)
}
Step 2: Run typecheck
Run: bun run typecheck
Expected: No errors
Step 3: Commit
git add apps/public/app/features/page.tsx
git commit -m "feat(public): add features page with challenge system descriptions"
Task 10: About Page (5.6)
Files:
- Modify:
apps/public/app/about/page.tsx
Step 1: Replace stub with about content
export const metadata = { title: 'About | Eko' }
const STEPS = [
{
step: '1',
title: 'News ingestion',
description: 'We monitor hundreds of news sources around the clock, clustering stories into coherent topics.',
},
{
step: '2',
title: 'AI fact extraction',
description: 'Our fact engine extracts structured, verifiable facts and scores them for notability and learning value.',
},
{
step: '3',
title: 'Multi-tier verification',
description: 'Every fact passes through validation tiers — from authoritative API checks to AI cross-referencing.',
},
{
step: '4',
title: 'Challenge cards',
description: 'Verified facts become challenge cards delivered through spaced repetition, tuned to what you know.',
},
]
export default function AboutPage() {
return (
<div className="mx-auto max-w-4xl px-4 py-16 sm:px-6">
<div className="mb-16">
<h1 className="mb-4 text-3xl font-bold sm:text-4xl">
Knowledge should be earned, not scrolled past.
</h1>
<p className="text-lg leading-relaxed text-muted-foreground">
Eko is a knowledge platform that turns the daily news into something you actually
remember. We believe the best way to learn isn't reading — it's being
challenged. Every fact in your feed is verified, structured, and delivered at the
right time for your brain to hold onto it.
</p>
</div>
<div className="mb-16">
<h2 className="mb-8 text-2xl font-bold">How the fact engine works</h2>
<div className="grid gap-6 sm:grid-cols-2">
{STEPS.map((step) => (
<div key={step.step} className="flex gap-4">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-sm font-bold text-primary-foreground">
{step.step}
</span>
<div>
<h3 className="mb-1 font-semibold">{step.title}</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
{step.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
)
}
Step 2: Run typecheck
Run: bun run typecheck
Expected: No errors
Step 3: Commit
git add apps/public/app/about/page.tsx
git commit -m "feat(public): add about page with mission and fact engine overview"
Task 11: Subscription Card Injection (5.7)
Files:
- Create:
apps/web/app/feed/_components/subscription-card.tsx - Modify:
apps/web/app/feed/_components/card-feed.tsx
Step 1: Create subscription card component
apps/web/app/feed/_components/subscription-card.tsx:
import Link from 'next/link'
const BENEFITS = [
'Full card details and challenge quizzes',
'Spaced repetition that adapts to you',
'Track progress across 36+ categories',
]
export function SubscriptionCard() {
return (
<div className="flex flex-col rounded-xl border-2 border-primary/30 bg-gradient-to-br from-primary/5 to-primary/10 p-5">
<h3 className="mb-2 text-base font-bold">Unlock the full Eko experience</h3>
<ul className="mb-4 space-y-2">
{BENEFITS.map((b) => (
<li key={b} className="flex items-start gap-2 text-sm text-muted-foreground">
<svg
className="mt-0.5 h-4 w-4 shrink-0 text-primary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<title>Included</title>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
<span>{b}</span>
</li>
))}
</ul>
<Link
href="/subscribe"
className="mt-auto rounded-md bg-primary px-4 py-2.5 text-center text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Start 14-day free trial
</Link>
<p className="mt-2 text-center text-xs text-muted-foreground">
No charge for 14 days. Cancel anytime.
</p>
</div>
)
}
Step 2: Modify CardFeed to inject subscription cards
In apps/web/app/feed/_components/card-feed.tsx, add the isAuthenticated prop and inject cards:
Add to imports:
import { SubscriptionCard } from './subscription-card'
Update the interface:
interface CardFeedProps {
initialFacts: FactRecord[]
topicSlug?: string
isAuthenticated?: boolean
}
Update the component signature:
export function CardFeed({ initialFacts, topicSlug, isAuthenticated = true }: CardFeedProps) {
Replace the <CardGrid> rendering block (the facts.map) with injection logic:
<CardGrid>
{facts.map((fact, index) => {
const items = []
// Inject subscription card at position 4, then every 24
if (!isAuthenticated && (index === 4 || (index > 4 && (index - 4) % 24 === 0))) {
items.push(<SubscriptionCard key={`sub-${index}`} />)
}
items.push(
<FactCard
key={fact.id}
id={fact.id}
title={fact.title}
facts={fact.facts}
notabilityScore={fact.notabilityScore}
topicCategory={fact.topicCategory}
publishedAt={fact.publishedAt}
userStatus={fact.userStatus}
imageUrl={fact.imageUrl}
difficulty={fact.difficulty}
/>,
)
return items
})}
{loading && ['a', 'b', 'c', 'd'].map((id) => <CardSkeleton key={id} />)}
</CardGrid>
Step 3: Pass isAuthenticated from parent page
Check the feed page that renders <CardFeed> and ensure it passes isAuthenticated based on the user's auth state. The feed page server component should check auth and pass it down.
Step 4: Run typecheck and lint
Run: bun run typecheck && bun run lint
Expected: No errors
Step 5: Commit
git add apps/web/app/feed/_components/subscription-card.tsx apps/web/app/feed/_components/card-feed.tsx
git commit -m "feat(feed): add subscription card injection for logged-out users"
Task 12: Quality Verification + Changelog (5.Q)
Files:
- Modify:
docs/changelog/unreleased.md
Step 1: Run full build
Run: bun run build
Expected: All apps build successfully
Step 2: Run typecheck
Run: bun run typecheck
Expected: No errors
Step 3: Run lint
Run: bun run lint
Expected: No errors
Step 4: Run tests
Run: bun run test
Expected: All pass
Step 5: Update changelog
Add to docs/changelog/unreleased.md under today's date:
## February 16, 2026
### Features
- **Subscribe**: Switch to 14-day trial with Stripe Checkout and CC collection
- **Public Site**: Add home page with live feed and card modal CTA
- **Public Site**: Add pricing page with plan comparison and annual toggle
- **Public Site**: Add features page with challenge system descriptions
- **Public Site**: Add about page with mission and fact engine overview
- **Feed**: Add subscription card injection for logged-out users
### Database
- **Migration 0114**: Update trial_days from 30 to 14 for Eko+ plan
Step 6: Commit
git add docs/changelog/unreleased.md
git commit -m "docs: update changelog for marketing site and trial update"
Step 7: Run full CI check
Run: bun run ci
Expected: All checks pass