19 KiB
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:
// 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):
// 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:
// 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:
// 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:
- Survey: "Before you go — what's the main reason?" (radio: too expensive / not using it / missing feature / other)
- Save offer based on reason:
too_expensive→ "What if we gave you 25% off for 2 months?" → button calls/api/stripe/apply-discountnot_using→ "Want to pause instead of cancel?" → pause link or infomissing_feature→ "Tell us what's missing" → textarea → submit to/api/feedback, then let them proceedother→ short text → submit to/api/feedback, then proceed
- Confirm: "Still want to cancel? Your plan stays active until [end of billing period]." → button opens Stripe portal
- Post-cancel: Stripe webhook already exists at
/api/stripe/webhook— add a case forcustomer.subscription.deletedthat 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.
// 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:
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:
<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):
"title": "The Free QR Code Generator That Doesn't Expire"
or
"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:
"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
"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 toqr-code-tracking-guideorqr-code-tracking-guide-2026title: update year references in the contentdateModified: set to current date- Add a redirect in
next.config.mjsfrom the old slug to the new one:
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
FAQPageschema block - Statistics with cited sources (e.g., "QR code scans grew X% in 2025 — [source]")
dateModifiedupdated 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:
<!-- 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:
<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:
"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:
{!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.
<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)