462 lines
18 KiB
Markdown
462 lines
18 KiB
Markdown
# QR Master — Growth TODO
|
||
|
||
Based on: 3-month audit (PostHog + GSC + AI citations + full codebase review, April 2026)
|
||
Domain age at audit: ~3 months | Visitors (90d): 169 | AI citations (33d): 1,849
|
||
|
||
---
|
||
|
||
## How to use this file
|
||
|
||
Sections are ordered by impact-per-hour-of-work. Don't skip to section 5 before section 1.
|
||
Each task has the exact file to touch and what to change. No hand-waving.
|
||
|
||
---
|
||
|
||
## Section 1 — Retention (do this week, nothing else matters until it's done)
|
||
|
||
Cohort data shows ~10% week-1 retention, near-zero by week 3. Root cause: **no email is sent after signup**. Zero. The `sendWelcomeEmail` function does not exist. Users sign up and hear nothing.
|
||
|
||
### 1.1 — Welcome email sequence (3 emails)
|
||
|
||
**File to edit:** `src/lib/email.ts`
|
||
Add three new exported functions after the existing `sendPasswordResetEmail`:
|
||
|
||
```ts
|
||
// Email 1: Sent immediately on signup
|
||
export async function sendWelcomeEmail(email: string, name: string) {}
|
||
|
||
// Email 2: Sent on Day 3 if user has 0 QR codes (check via cron or on login)
|
||
export async function sendActivationNudgeEmail(email: string, name: string) {}
|
||
|
||
// Email 3: Sent on Day 7 if user has ≥1 QR code but is still on FREE plan
|
||
export async function sendUpgradeNudgeEmail(email: string, name: string, qrCount: number) {}
|
||
```
|
||
|
||
**Email 1 — Welcome (Day 0)**
|
||
- Subject: `Your QR Master account is ready`
|
||
- Body: One action only → create first QR code. Link to `/create`.
|
||
- Mention the 3 free dynamic codes.
|
||
- 4–5 sentences max, plain layout.
|
||
- Sender: `Timo from QR Master <timo@qrmaster.net>`
|
||
|
||
**Email 2 — Activation nudge (Day 3, no QR codes created)**
|
||
- Subject: `Still haven't made your first QR code?`
|
||
- Trigger condition: `user.createdAt < now - 3 days AND qrCodes.count === 0`
|
||
- Body: One screenshot or step-by-step (3 steps). One CTA → `/create`.
|
||
- Don't send if they've already created one.
|
||
|
||
**Email 3 — Upgrade nudge (Day 7, has codes, still FREE)**
|
||
- Subject: `You've created {n} QR codes — here's what you're missing`
|
||
- Trigger condition: `qrCodes.count >= 1 AND plan === 'FREE' AND createdAt < now - 7 days`
|
||
- Body: 3 specific things they can't do on free: analytics breakdown, custom branding, more than 3 codes.
|
||
- CTA → `/pricing`
|
||
|
||
**File to edit:** `src/app/(main)/api/auth/signup/route.ts`
|
||
Add `sendWelcomeEmail` call after the user is created (line ~88, after `db.user.create`):
|
||
|
||
```ts
|
||
// After db.user.create(...)
|
||
try {
|
||
await sendWelcomeEmail(user.email, user.name ?? 'there');
|
||
} catch (emailError) {
|
||
// Don't fail signup if email fails
|
||
console.error('Welcome email failed:', emailError);
|
||
}
|
||
```
|
||
|
||
**For emails 2 and 3:** Either add a cron job (`/api/cron/retention-emails`) or check + send on each login in `/api/auth/simple-login` route. Cron is cleaner. Vercel cron syntax:
|
||
|
||
```json
|
||
// vercel.json
|
||
{
|
||
"crons": [{ "path": "/api/cron/retention-emails", "schedule": "0 10 * * *" }]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 1.2 — Cancel flow (before Stripe portal)
|
||
|
||
**File to edit:** `src/app/(main)/(app)/settings/page.tsx`
|
||
Right now: button → Stripe portal. Users cancel without any friction or reason capture.
|
||
|
||
Replace the `Manage Subscription` button with a handler that opens a modal first:
|
||
|
||
```tsx
|
||
// Add state
|
||
const [showCancelModal, setShowCancelModal] = useState(false);
|
||
const [cancelReason, setCancelReason] = useState('');
|
||
|
||
// Replace direct portal redirect with:
|
||
const handleManageSubscription = () => {
|
||
if (plan !== 'FREE') {
|
||
setShowCancelModal(true); // intercept — show exit survey first
|
||
} else {
|
||
// FREE users can just go to portal normally
|
||
openStripePortal();
|
||
}
|
||
};
|
||
```
|
||
|
||
**Cancel modal — 4 steps:**
|
||
|
||
1. **Survey:** "Before you go — what's the main reason?" (radio: too expensive / not using it / missing feature / other)
|
||
2. **Save offer** based on reason:
|
||
- `too_expensive` → *"What if we gave you 25% off for 2 months?"* → button calls `/api/stripe/apply-discount`
|
||
- `not_using` → *"Want to pause instead of cancel?"* → pause link or info
|
||
- `missing_feature` → *"Tell us what's missing"* → textarea → submit to `/api/feedback`, then let them proceed
|
||
- `other` → short text → submit to `/api/feedback`, then proceed
|
||
3. **Confirm:** "Still want to cancel? Your plan stays active until [end of billing period]." → button opens Stripe portal
|
||
4. **Post-cancel:** Stripe webhook already exists at `/api/stripe/webhook` — add a case for `customer.subscription.deleted` that logs the cancellation reason and (optionally) triggers a win-back email in 30 days.
|
||
|
||
**Save the reasons to DB.** Add a `cancellationReason` field to the User model in `prisma/schema.prisma` so you actually learn why people leave.
|
||
|
||
---
|
||
|
||
## Section 2 — Signup flow (2–3 hours of work, immediate conversion lift)
|
||
|
||
Current form: Name → Email → Password → Confirm Password → [Create Account] → divider → Google button
|
||
Problem: Google should be first. Confirm Password should not exist.
|
||
|
||
### 2.1 — Move Google OAuth above the form
|
||
|
||
**File:** `src/app/(main)/(auth)/signup/SignupClient.tsx`
|
||
|
||
Move the Google button block (currently after the `<form>`) to above the `<form>`. The divider "Or continue with" becomes "Or sign up with email" below the Google button.
|
||
|
||
```tsx
|
||
// New order in the Card:
|
||
<CardContent className="p-6">
|
||
{/* Google FIRST */}
|
||
<Button type="button" variant="outline" className="w-full" onClick={handleGoogleSignIn}>
|
||
{/* Google SVG */} Sign up with Google
|
||
</Button>
|
||
|
||
<div className="relative my-6">
|
||
{/* divider */}
|
||
<span className="px-2 bg-white text-gray-500">Or sign up with email</span>
|
||
</div>
|
||
|
||
{/* Email form SECOND */}
|
||
<form onSubmit={handleSubmit}>
|
||
...
|
||
</form>
|
||
</CardContent>
|
||
```
|
||
|
||
### 2.2 — Remove the Confirm Password field
|
||
|
||
**File:** `src/app/(main)/(auth)/signup/SignupClient.tsx`
|
||
|
||
Delete the `confirmPassword` state, the `confirmPassword` Input field, and the `if (password !== confirmPassword)` check. Replace with a password visibility toggle on the Password field instead:
|
||
|
||
```tsx
|
||
const [showPassword, setShowPassword] = useState(false);
|
||
|
||
// In the Input:
|
||
<div className="relative">
|
||
<Input
|
||
label="Password"
|
||
type={showPassword ? 'text' : 'password'}
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
placeholder="••••••••"
|
||
required
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowPassword(!showPassword)}
|
||
className="absolute right-3 top-9 text-gray-400 hover:text-gray-600"
|
||
>
|
||
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
```
|
||
|
||
### 2.3 — Add value reinforcement to the signup page
|
||
|
||
**File:** `src/app/(main)/(auth)/signup/SignupClient.tsx`
|
||
|
||
Change the subtitle under "Create Account" from:
|
||
> *"Start creating QR codes in seconds"*
|
||
|
||
To:
|
||
> *"No credit card required — 3 dynamic QR codes free forever"*
|
||
|
||
Also remove the prominent "← Back to Home" bordered button. It's visually competing with the form. Replace with a small text link at the bottom of the card:
|
||
|
||
```tsx
|
||
<p className="text-center text-xs text-gray-400 mt-4">
|
||
Already have an account? <Link href="/login">Sign in</Link> · <Link href="/">Back to home</Link>
|
||
</p>
|
||
```
|
||
|
||
---
|
||
|
||
## Section 3 — Homepage copy (half a day)
|
||
|
||
### 3.1 — Headline rewrite
|
||
|
||
**File:** `src/i18n/en.json`
|
||
Current: `"title": "Create QR Codes That Work Everywhere"`
|
||
|
||
Replace with one of (pick based on your gut for the audience):
|
||
|
||
```json
|
||
"title": "The Free QR Code Generator That Doesn't Expire"
|
||
```
|
||
or
|
||
```json
|
||
"title": "Track Every Scan. Update Every Link. Free Forever."
|
||
```
|
||
|
||
Subtitle — current: *"Generate static and dynamic QR codes with tracking, custom branding, and bulk generation. Free forever."*
|
||
Replace:
|
||
```json
|
||
"subtitle": "Dynamic QR codes you can edit anytime without reprinting — see exactly who scanned, where, and on what device. Starts free, no credit card."
|
||
```
|
||
|
||
### 3.2 — Feature bullets: features → outcomes
|
||
|
||
**File:** `src/i18n/en.json`, `hero.features` array
|
||
|
||
```json
|
||
"features": [
|
||
"Change your link anytime — no reprinting needed",
|
||
"See scan location, device, and time of day",
|
||
"Your QR codes work forever, even on the free plan",
|
||
"Match your brand: custom colors in under a minute"
|
||
]
|
||
```
|
||
|
||
### 3.3 — Hero right column: replace animation with product screenshot
|
||
|
||
**File:** `src/components/marketing/Hero.tsx`
|
||
|
||
The flipping cards grid (lines ~130–175) should be replaced with either:
|
||
- A static screenshot of the analytics dashboard (real scan data, anonymized)
|
||
- A live mini-generator: paste a URL → QR code renders inline (highest conversion)
|
||
|
||
A live mini-generator in the hero is the highest-impact change on the whole homepage. Even a simplified version that just renders a basic QR code and links to `/create` for customization would work.
|
||
|
||
### 3.4 — Add a social proof number to the hero
|
||
|
||
**File:** `src/components/marketing/Hero.tsx` or `src/components/marketing/StatsStrip.tsx`
|
||
|
||
Add one concrete number directly in or immediately below the hero — before the fold. Options:
|
||
- `"1M+ QR codes generated"` (if true or close)
|
||
- `"Trusted by 500+ businesses"` (if defensible)
|
||
- `"Cited by ChatGPT, Perplexity & Google AI"` (actually true based on your AI data — this is a unique claim almost no competitor can make)
|
||
|
||
The last one is genuinely differentiated and verified.
|
||
|
||
---
|
||
|
||
## Section 4 — Content: the 5 posts that will move GSC rankings
|
||
|
||
These are ordered by current GSC impression data — they already have Google's attention, they just need the content to justify moving up.
|
||
|
||
### 4.1 — Barcode generator companion post
|
||
|
||
**Current situation:** `/tools/barcode-generator` has 1,160 GSC impressions at position 71. No blog post supports it.
|
||
|
||
**Create:** `src/app/(main)/(marketing)/blog/barcode-vs-qr-code/` (or add to `src/lib/blog-data.ts`)
|
||
**Title:** *"Barcode vs QR Code: What's the Difference and When to Use Each"*
|
||
**Target query:** `barcode vs qr code` / `difference between barcode and qr code`
|
||
**Must include:** a comparison table (scanners required, data capacity, use cases, editability), a "when to use each" section, internal links to `/tools/barcode-generator` and `/dynamic-qr-code-generator`.
|
||
|
||
### 4.2 — Teams QR code blog post
|
||
|
||
**Current situation:** `/tools/teams-qr-code` gets 20 PostHog visitors with 0% bounce and ranks ~position 22 in GSC — the closest page to page 1 you have.
|
||
|
||
**Create:** blog post targeting `microsoft teams qr code` / `teams meeting qr code`
|
||
**Title:** *"How to Create a Microsoft Teams QR Code for Instant Meeting Joins"*
|
||
**Internal link:** back to `/tools/teams-qr-code` from the post and vice versa (the tool page should link to this post for content depth).
|
||
|
||
### 4.3 — WiFi QR code post update
|
||
|
||
**Current situation:** `/tools/wifi-qr-code` ranks ~position 44 (close to page 4). 14 PostHog visitors, 0% bounce.
|
||
|
||
**File:** The wifi tool page has German keywords mixed into English metadata (`wlan qr code erstellen`, `wifi passwort qr code`). Decide: is this page targeting English or German? Mixed intent hurts both. For the English version, clean up to English-only keywords. For German, create a dedicated `/de/` route (there's already a German path at `(marketing-de)`).
|
||
|
||
**Action:** Add a content section below the WiFi generator tool with at minimum: a "common WiFi QR code uses" section (restaurants, hotels, offices, Airbnb), a FAQ block (3–5 questions), and an internal link to the main QR generator.
|
||
|
||
### 4.4 — Fix the 2025-dated content
|
||
|
||
**File:** `src/lib/blog-data.ts`
|
||
|
||
The post with slug `qr-code-tracking-guide-2025` has `2025` in its URL. Update:
|
||
- `slug`: change to `qr-code-tracking-guide` or `qr-code-tracking-guide-2026`
|
||
- `title`: update year references in the content
|
||
- `dateModified`: set to current date
|
||
- Add a redirect in `next.config.mjs` from the old slug to the new one:
|
||
|
||
```js
|
||
async redirects() {
|
||
return [
|
||
{
|
||
source: '/blog/qr-code-tracking-guide-2025',
|
||
destination: '/blog/qr-code-tracking-guide-2026',
|
||
permanent: true,
|
||
},
|
||
];
|
||
},
|
||
```
|
||
|
||
### 4.5 — "Best QR code generator 2026" — make it actually compete
|
||
|
||
**File:** `src/lib/blog-data.ts` — post slug `best-qr-code-generator-2026` exists but has ~9 GSC position and 0 clicks from 38 impressions.
|
||
|
||
This post needs to be the most comprehensive comparison page on the site. It currently isn't doing the job. It needs:
|
||
- A feature comparison table: QR Master vs Bitly vs QR Code Generator vs Beaconstac (columns: free plan limits, dynamic QR, analytics, branding, bulk, pricing)
|
||
- Honest pros/cons for each (being fake-neutral is immediately obvious and AI systems penalise it)
|
||
- A `FAQPage` schema block
|
||
- Statistics with cited sources (e.g., "QR code scans grew X% in 2025 — [source]")
|
||
- `dateModified` updated to current month
|
||
|
||
This post alone, done properly, is the highest-value AI-citation target on the whole site.
|
||
|
||
---
|
||
|
||
## Section 5 — AI SEO quick fixes (1–2 hours)
|
||
|
||
These are code-level changes, not content work.
|
||
|
||
### 5.1 — Add cited sources to every statistic in blog posts
|
||
|
||
**File:** `src/lib/blog-data.ts`
|
||
|
||
Every blog post `content` field that contains a statistic without a source link needs one. Example fix in the restaurant post:
|
||
|
||
```html
|
||
<!-- Before -->
|
||
<p>over 60% of restaurants that adopted QR menus during 2020–2021 kept them afterward</p>
|
||
|
||
<!-- After -->
|
||
<p>over 60% of restaurants that adopted QR menus during 2020–2021 kept them afterward
|
||
<a href="https://nationalrestaurantassociation.org/..." target="_blank" rel="noopener">[NRA, 2022]</a></p>
|
||
```
|
||
|
||
Per Princeton GEO research: adding cited sources increases AI citation rate by +40%. This is the single highest-ROI AI SEO action available.
|
||
|
||
### 5.2 — Add definition blocks to tool pages
|
||
|
||
**Which pages:** barcode generator, WiFi QR generator, Teams QR generator, URL QR generator
|
||
**What to add:** A one-paragraph definition in the first `<p>` tag that directly answers "What is a [X] generator?" in 40–60 words. AI systems extract from the opening paragraph first.
|
||
|
||
Example for barcode generator:
|
||
```html
|
||
<p>A barcode generator creates machine-readable linear barcodes (EAN-13, UPC-A, Code 128)
|
||
from numeric or alphanumeric data. Unlike QR codes, barcodes store data in parallel lines
|
||
and are scanned by dedicated readers. Use this free tool to generate printable barcodes
|
||
for retail products, inventory labels, or shipping.</p>
|
||
```
|
||
|
||
### 5.3 — Add `dateModified` display to blog post pages
|
||
|
||
**File:** whichever component renders the blog post header — search for `publishDate` or `datePublished` usage in `src/app/(main)/(marketing)/blog/`
|
||
|
||
AI tools penalise undated content. Every post already has `dateModified` in `blog-data.ts` — it just needs to render visibly on the page. Add "Last updated: [date]" next to the author line. It's one line of JSX.
|
||
|
||
---
|
||
|
||
## Section 6 — Pricing page (1 hour)
|
||
|
||
**File:** `src/i18n/en.json`, pricing section + the Pricing component
|
||
|
||
### 6.1 — Add upgrade trigger context to the Pro plan
|
||
|
||
Under the Pro plan title, add a single explanatory sentence that explains *when* someone should upgrade:
|
||
|
||
```json
|
||
"pro": {
|
||
"trigger": "When you hit 3 dynamic QR codes or need scan analytics"
|
||
}
|
||
```
|
||
|
||
Render this in the Pricing component under the plan title, above the price.
|
||
|
||
### 6.2 — Free plan: explain the limit as a feature, not a restriction
|
||
|
||
Current: `"3 active dynamic QR codes (8 types available)"`
|
||
Rewrite: `"3 dynamic QR codes — enough to start. Unlimited static codes."`
|
||
|
||
The current copy leads with the limit. Lead with what they get.
|
||
|
||
### 6.3 — Annual pricing
|
||
|
||
There's no annual discount mentioned anywhere in the pricing UI. If Stripe supports annual billing, surface it. A toggle (Monthly / Annual — save 20%) on the pricing page typically lifts plan revenue by 15–30% by locking in longer commitments. Check `src/lib/stripe.ts` for whether annual price IDs exist.
|
||
|
||
---
|
||
|
||
## Section 7 — Dashboard first-run experience (2–3 hours)
|
||
|
||
### 7.1 — Empty state when user has 0 QR codes
|
||
|
||
**File:** `src/app/(main)/(app)/dashboard/page.tsx`
|
||
|
||
Currently, a new user lands on a dashboard with stats showing all zeros and an empty grid. There's a loading skeleton for QR codes but no empty state when `qrCodes.length === 0`.
|
||
|
||
Add an empty state component:
|
||
|
||
```tsx
|
||
{!loading && qrCodes.length === 0 && (
|
||
<div className="text-center py-16 border-2 border-dashed border-gray-200 rounded-xl">
|
||
<QrCode className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
||
<h3 className="text-lg font-semibold text-gray-700 mb-2">Create your first QR code</h3>
|
||
<p className="text-gray-500 mb-6 max-w-sm mx-auto">
|
||
You have 3 free dynamic QR codes. They redirect wherever you want and track every scan.
|
||
</p>
|
||
<Link href="/create">
|
||
<Button>Create QR Code — it takes 90 seconds</Button>
|
||
</Link>
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
### 7.2 — Remove the mock QR codes from the dashboard code
|
||
|
||
**File:** `src/app/(main)/(app)/dashboard/page.tsx`, lines ~45–100
|
||
|
||
There's a `mockQRCodes` array defined but it doesn't appear to be rendered (the real fetch path is used). Verify and remove the dead code to keep the file clean.
|
||
|
||
### 7.3 — Dashboard subtitle should be contextual
|
||
|
||
**File:** `src/app/(main)/(app)/dashboard/page.tsx`
|
||
|
||
Current subtitle: `{t('dashboard.subtitle')}` → "Manage and track your QR codes"
|
||
|
||
For users with 0 QR codes, show: *"Start here — create your first QR code in under 2 minutes"*
|
||
For users with QR codes, show the current text.
|
||
|
||
```tsx
|
||
<p className="text-gray-600 mt-2">
|
||
{qrCodes.length === 0
|
||
? 'Start here — create your first QR code in under 2 minutes'
|
||
: t('dashboard.subtitle')}
|
||
</p>
|
||
```
|
||
|
||
---
|
||
|
||
## Metrics to watch
|
||
|
||
| Thing changed | Metric to check | Where |
|
||
|---|---|---|
|
||
| Welcome email | Cohort retention week 1 | PostHog → Retention |
|
||
| Signup form (Google first, remove confirm PW) | Signup completion rate | PostHog → Funnels |
|
||
| Cancel flow | Cancelled users / month | Stripe dashboard |
|
||
| Hero headline | Bounce rate on `/` | PostHog → Paths |
|
||
| Barcode + Teams blog posts | GSC impressions & position | Search Console |
|
||
| Cited statistics in posts | AI citations/day | AI perf CSV |
|
||
| Empty state dashboard | `/create` conversion from dashboard | PostHog → Paths |
|
||
|
||
---
|
||
|
||
## What not to do right now
|
||
|
||
- Don't build new QR code types — the product breadth is already sufficient for the current user base
|
||
- Don't chase Google Ads or paid social — organic signals are still too early to know which pages convert, and CAC will be high with a 10% retention rate
|
||
- Don't redesign the homepage visually — copy and CRO changes will outperform a design refresh at this stage
|
||
- Don't add more blog posts until the existing ones have cited sources and proper date signals (Sections 4.4 and 5.1)
|