38 Commits

Author SHA1 Message Date
Timo Knuth
0866c200a0 Weekly SEO 2026-06-08 20:33:38 +02:00
Timo Knuth
a7cbbee084 SEO 2026-05-27 20:37:15 +02:00
Timo Knuth
09f5859af2 Product hunt launch 2026-05-27 14:33:58 +02:00
Timo Knuth
4774f4d51e SEO 2026-05-18 16:00:24 +02:00
Timo Knuth
81d1fdd280 weekly seo 2026-05-11 11:10:30 +02:00
Timo Knuth
35e7e77f6b GSC seo 2026-05-10 23:00:06 +02:00
Timo Knuth
8741edc362 /restaurants 2026-04-30 16:59:27 +02:00
Timo Knuth
152758db92 Fehler 2026-04-29 23:53:47 +02:00
Timo Knuth
105857c348 Remove impeccable A/B variants from Hero -- keep only FlippingCards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 23:51:57 +02:00
Timo Knuth
aab808c553 Impeccable 2026-04-29 20:34:09 +02:00
Timo Knuth
9b31e77daa mehr SEO 2026-04-27 22:21:52 +02:00
Timo Knuth
c4fac0f726 SEO + Stripe 2026-04-27 17:10:30 +02:00
Timo Knuth
11159eb02b stripe promo code 2026-04-27 11:42:09 +02:00
Timo Knuth
c6f20f7f0b Onboarding verbessern 2026-04-23 19:24:33 +02:00
Timo Knuth
eacaef1fbd Refine onboarding UI and fix dashboard checklist progress 2026-04-23 19:03:41 +02:00
fc0e6a0a69 npm run build 2026-04-23 14:52:13 +02:00
Timo Knuth
c7d5f281c5 Fix build issues for meta imports and WSL filesystem 2026-04-23 11:50:09 +02:00
Timo Knuth
6e68408391 Add revops onboarding SQL migration 2026-04-22 22:29:36 +02:00
Timo Knuth
7d2724b65d revops + onboarding 2026-04-22 20:01:46 +02:00
ce724662d4 external 2026-04-21 15:38:49 -05:00
7a7b197a67 Merge branch 'master' of git.bizmatch.net:tknuth/QR-master 2026-04-21 15:29:59 -05:00
ef22e72a82 external: true 2026-04-21 15:29:43 -05:00
Timo Knuth
32935041b3 add 2026-04-21 12:37:18 +02:00
Timo Knuth
aa2628834b Barcode fix 2026-04-17 23:24:22 +02:00
Timo Knuth
5894f4619d Barcode workflow 2026-04-17 22:56:49 +02:00
Timo Knuth
56d63a0146 chore: add gstack skill routing rules to CLAUDE.md 2026-04-17 14:14:07 +02:00
Timo Knuth
1bb782467b Validation error 2026-04-17 09:16:07 +02:00
Timo Knuth
c3efe8ceb9 fehler 2026-04-16 20:28:08 +02:00
c1fa20a234 Merge branch 'master' of git.bizmatch.net:tknuth/QR-master 2026-04-16 12:58:59 -05:00
3cf67582bc port 5435 2026-04-16 12:58:41 -05:00
Timo Knuth
231a85ffa4 Popup free generatoren 2026-04-16 19:34:26 +02:00
Timo Knuth
673eaf7fd3 leads: competitor pain - enforce 7-day freshness filter, no older posts 2026-04-15 12:16:26 +02:00
Timo Knuth
30b1b12e74 leads: add instruction files for restaurant, tradeshow, competitor pain triggers 2026-04-15 12:10:13 +02:00
Timo Knuth
139b87fe93 leads: update instructions - EN/EU geo split, review email back, CLI approval workflow 2026-04-15 11:06:11 +02:00
Timo Knuth
8257866138 leads: simplify agent instructions - CLI review workflow, no email sending 2026-04-15 11:00:24 +02:00
Timo Knuth
8de1411e34 leads: add agent instructions file for scheduled outreach trigger 2026-04-15 10:57:05 +02:00
Timo Knuth
65fe18a718 dynamisch barcode 2026-04-14 19:46:14 +02:00
Timo Knuth
6b73ac5c50 feat: implement pricing strategy, subscription tiers, and core infrastructure for QR code management 2026-04-14 19:34:47 +02:00
186 changed files with 57367 additions and 11572 deletions

234
.agents/pricing-strategy.md Normal file
View File

@@ -0,0 +1,234 @@
# QR Master — Pricing Strategy
*Erstellt: April 2026 | Basiert auf Marktforschung, Competitor-Scraping & SaaS-Benchmarks*
---
## 1. Marktkontext
### QR-Code-Markt 2025/2026
| Metrik | Wert |
|--------|------|
| Globale Marktgröße | $15,3 Mrd. (2025) |
| CAGR bis 2030 | 16,1% |
| US-Smartphone-User die QR scannen | 100+ Mio. monatlich |
| Business-Adoption | 50% der Unternehmen nutzen QR-Codes aktiv |
**Fazit:** Wachstumsmarkt mit noch großem Potenzial, besonders im KMU-Segment.
---
## 2. Wettbewerbs-Pricing-Map
### Vollständige Marktübersicht (aus Firecrawl-Recherche)
| Tool | Preis/Monat | Analytics | Dynamic | Bulk | Branding | Positionierung |
|------|------------|-----------|---------|------|----------|----------------|
| TQRCG | $5 | ✅ | ✅ | ❌ | ✅ | Value-Leader |
| QRStuff | $5 | ❌ | ❌ | ✅ | ❌ | Budget |
| ViralQR | $1,49 | ✅ | ✅ | ❌ | ✅ | Ultra-Budget |
| Beaconstac | $524 | ✅ | ✅ | ✅ | ✅ | SMBEnterprise |
| Bitly QR | $10 | ✅ | ✅ | ❌ | ✅ | Mid-Market |
| Unitag | $10 | ❌ | ✅ | ✅ | ✅ | Mid-Market |
| ZebraQR | $9 | ✅ | ✅ | ❌ | ✅ | Hospitality-Nische |
| QR Tiger | $1215 | ✅ | ✅ | ✅ | ✅ | Mid-Market+ |
| Hovercode | $15 | ✅ | ✅ | ✅ | ✅ | Growth-Fokus |
| Flowcode | $1015 | ✅ | ✅ | ❌ | ✅ | Design-Fokus |
| Scanova | $20 | ✅ | ✅ | ✅ | ✅ | Premium |
| QR Code Chimp | $20 | ✅ | ✅ | ✅ | ✅ | Premium-Design |
| Uniqode | $1030 | ✅ | ✅ | ✅ | ✅ | Enterprise |
| QRFY | $25 | ✅ | ✅ | ✅ | ✅ | Premium-Flat |
| QR Code Generator Pro | $1529 | ✅ | ✅ | ✅ | ✅ | Agency |
### Marktlücke für QR Master
> **Kein einziges Tool unter $12 bietet Analytics + Bulk + Custom Branding + DSGVO gleichzeitig.**
> Das ist exakt QR Masters Sweet Spot.
---
## 3. SaaS-Benchmark-Daten (Industrie)
| Metrik | Benchmark | Quelle |
|--------|-----------|--------|
| Median Entry-Level Preis (SaaS) | $29/mo | Monetizely 2025 |
| Free-to-Paid Conversion | ~5% | RevenueCat 2026 |
| Anteil Jahres-Abos (vs. Monatlich) | 68% annual / 32% monthly | RevenueCat 2026 |
| ARPU (Subscription Apps) | ~$30 | RevenueCat 2026 |
| Freemium-Anteil unter SaaS | 38% der Unternehmen | Monetizely 2025 |
| Hybrid-Pricing-Adoption | 61% | Monetizely 2025 |
| SaaS Churn (SMB) | 35%/Monat | Benchmark |
**Key Insight:** 68% der Subscriber wählen Jahrestarife. Das ist der wichtigste Hebel für Cashflow und Churn-Reduktion.
---
## 4. Value Metric Empfehlung
### Aktuelles Modell: Anzahl dynamischer QR-Codes
**Bewertung: Gut, aber optimierbar.**
Die Anzahl dynamischer Codes skaliert mit dem wahrgenommenen Wert (mehr Codes = mehr Kampagnen = mehr Wert). Jedoch:
- Limit von 8 FREE / 50 PRO / 500 BUSINESS ist nicht intuitiv kommuniziert
- Kunden denken in "Projekten" oder "Kampagnen", nicht in "Codes"
### Empfehlung: Hybrid-Metric einführen
Primär-Metric behalten (Dynamic Codes), aber mit Sekundär-Metriken ergänzen:
| Tier | Primär-Metric | Sekundär-Metriken |
|------|--------------|-------------------|
| FREE | 8 Dynamic Codes | 1 User, Basic Analytics, 30 Tage History |
| PRO | 50 Dynamic Codes | 13 User, Full Analytics, 1 Jahr History, Custom Domain |
| BUSINESS | 500 Dynamic Codes | Unlimitierte User, Advanced Analytics, Bulk, API |
---
## 5. Empfohlene Pricing-Struktur
### Tier-Empfehlung (Monatlich / Jährlich)
#### FREE — Kostenlos, für immer
- **8 dynamische QR-Codes** (klar kommuniziert als "8 Kampagnen")
- Unlimitierte statische Codes
- Basis-Analytics (Scans, Datum)
- QR Master Branding (nicht entfernbar)
- **Ziel:** Acquisition, Habit-Building, Virality durch Branding
#### PRO — €9/Monat (monatlich) | **€7/Monat (jährlich = €84/Jahr)**
*Empfohlen für: Restaurants, lokale Unternehmen, Marketing-Einsteiger*
- **50 dynamische QR-Codes**
- Custom Branding (kein QR Master Logo)
- Vollständige Analytics (Device, Location, OS, UTM)
- 1 Jahr Analytics-History
- Custom Domain für Redirects
- Prioritäts-Support
- **Rationale:** $79 liegt im bewiesenen Sweet Spot ($5$10) für diese Zielgruppe. Beaconstac Starter bei $5 hat nur 100 Scans — wir haben keine Scan-Limits.
#### BUSINESS — €24/Monat (monatlich) | **€19/Monat (jährlich = €228/Jahr)**
*Empfohlen für: Agenturen, Retail-Chains, Event-Organisatoren*
- **500 dynamische QR-Codes**
- Bulk-Upload (Excel/CSV bis 1.000 Zeilen)
- API-Zugriff
- Team-Management (bis 5 User)
- Erweiterte Analytics + Export (CSV, PDF)
- White-Label Option
- DSGVO-Compliance-Report
- **Rationale:** $1924 ist der Bereich wo Scanova ($20), Hovercode ($15) und QR Code Chimp ($20) spielen — aber keiner hat DSGVO + Bulk + Analytics zusammen.
#### ENTERPRISE — Auf Anfrage (ab €99/Monat)
*Für: Corporations, Franchise-Ketten*
- Unlimitierte Codes
- Dedizierter Account Manager
- Custom SLA
- SSO / SAML
- On-Premise Option (optional)
---
## 6. Psychologische Preisgestaltung
### Anchoring-Strategie
Reihenfolge auf Pricing-Page: **BUSINESS → PRO → FREE** (von teuer nach günstig)
→ PRO wirkt dadurch als "vernünftiger Kompromiss"
### Decoy-Effekt
PRO muss der offensichtliche "Best Deal" sein:
- BUSINESS ist 2,7× teurer als PRO aber hat 10× mehr Codes → Nur für Power-User
- FREE hat 6× weniger Codes als PRO → Upgrade liegt nahe
### Jahres-Pricing-Push
- Monatlich: €9 / €24
- Jährlich: €7 / €19 (sparst 22% / 21%)
- **Wichtig:** Jahrespreis prominent anzeigen mit "Spare 2 Monate" statt Prozent
- Default-Toggle: **Jährlich** (da 68% aller Subscriber Jahrestarife wählen)
### Charm vs. Round Pricing
- PRO: **€9** (nicht €10) → Charm Pricing für Conversion
- BUSINESS: **€24** (nicht €25) → Knapp unter psychologischer Grenze
- Jahrestarife: **€84/Jahr** und **€228/Jahr** (rund → Premium-Signal)
---
## 7. Jahres-Discount-Strategie
| Tier | Monatlich | Jährlich | Ersparnis |
|------|-----------|----------|-----------|
| PRO | €9/Mo | €84/Jahr (€7/Mo) | 22% / 2 Monate gratis |
| BUSINESS | €24/Mo | €228/Jahr (€19/Mo) | 21% / 2,5 Monate gratis |
**Kommunikation:** "2 Monate kostenlos bei jährlicher Zahlung" schlägt "20% Rabatt" in A/B-Tests regelmäßig.
---
## 8. Free-Tier-Optimierung
### Ziel des Free-Tiers
Nicht monetarisieren — **qualifizieren und konvertieren**.
### Empfohlene Trigger für Upgrade-Prompts
1. **Code-Limit erreicht** → "Du hast 8/8 Codes verwendet. Upgrade auf PRO für 50 Codes."
2. **Analytics-Feature geklickt** → "Detaillierte Location-Analytics nur in PRO."
3. **Custom Branding versucht** → "Entferne das QR Master Logo — upgrade auf PRO."
4. **Bulk-Upload versucht** → "Bulk-Upload ist nur in BUSINESS verfügbar."
5. **Nach 7 Tagen aktive Nutzung** → In-App Prompt: "Du nutzt QR Master aktiv — hole mehr raus."
### Virality-Mechanismus
- FREE-Codes enthalten subtiles "Made with QR Master" in Metadaten
- QR-Code-Landing-Pages (bei Dynamic Redirects) zeigen "Powered by QR Master" Footer
- Jeder Scan ist eine potenzielle Akquisition
---
## 9. Positioning Statement je Tier
**FREE:**
> "Starte kostenlos mit 8 professionellen QR-Codes — keine Kreditkarte erforderlich."
**PRO:**
> "Für Restaurants, lokale Geschäfte und Marketer: Unbegrenzte Änderungen, echte Analytics, dein Branding — für weniger als ein Mittagessen pro Monat."
**BUSINESS:**
> "Für Agenturen und Retail-Chains: Erstelle 500 Codes auf einmal, per Excel-Upload — DSGVO-konform, skalierbar, professionell."
---
## 10. Pricing Page Struktur (Empfehlung)
### Elemente above the fold
1. **Toggle: Monatlich / Jährlich** (Default: Jährlich)
2. **3 Tier-Karten** in Reihenfolge: FREE → PRO (highlighted "Beliebteste Wahl") → BUSINESS
3. **CTA je Tier:** "Kostenlos starten" / "14 Tage gratis testen" / "Jetzt upgraden"
4. **Trust-Signal:** "Keine Kreditkarte für Free • DSGVO-konform • Jederzeit kündbar"
### Weitere Sektionen
- Feature-Vergleichstabelle (vollständig)
- ROI-Rechner: "Wie viel sparst du durch dynamische QR-Codes vs. Neudruck?"
- FAQ (Objections aus Product-Marketing-Context)
- Testimonials-Sektion (Platzhalter für spätere echte Reviews)
- Enterprise-CTA am Ende
---
## 11. Kurzfristige Maßnahmen (Quick Wins)
| Priorität | Maßnahme | Impact |
|-----------|----------|--------|
| 🔴 Hoch | Jahrestarif als Default auf Pricing-Page setzen | +2030% ARPU sofort |
| 🔴 Hoch | "2 Monate gratis" Kommunikation (statt %) | +Conversion |
| 🟡 Mittel | Upgrade-Prompts bei Feature-Gates einbauen | +Free-to-Paid |
| 🟡 Mittel | 14-Tage PRO Trial (kreditkartenlos) | +Trial Signups |
| 🟢 Niedrig | BUSINESS Jahrespreis auf €228 festlegen | Cashflow |
| 🟢 Niedrig | Enterprise-Kontaktformular ergänzen | Upmarket |
---
## 12. Risiken & Gegenmaßnahmen
| Risiko | Wahrscheinlichkeit | Gegenmaßnahme |
|--------|-------------------|---------------|
| ViralQR mit $1,49 unterbietbar | Mittel | Auf Analytics + DSGVO differenzieren, nicht Preis |
| FREE-User konvertieren nicht | Hoch | Smarte Feature-Gates + E-Mail-Nurturing |
| BUSINESS-Preis zu hoch für KMU | Mittel | Jährlich-Preis betonen: €19/mo fühlt sich zugänglich an |
| Konkurrenten senken Preise | Niedrig | Value-Story stärken, nicht mitziehen |
---
*Datenbasis: Firecrawl-Scraping von 5+ Competitor-Seiten, QR Marktstatistiken 2026, RevenueCat State of Subscription Apps 2026, Monetizely SaaS Benchmark 2025, Product Marketing Context QR Master.*

View File

@@ -0,0 +1,19 @@
#!/bin/bash
# Block skill usage when gstack is not installed globally.
if [ ! -d "$HOME/.claude/skills/gstack/bin" ]; then
cat >&2 <<'MSG'
BLOCKED: gstack is not installed globally.
gstack is required for AI-assisted work in this repo.
Install it:
git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
cd ~/.claude/skills/gstack && ./setup --team
Then restart your AI coding tool.
MSG
echo '{"permissionDecision":"deny","message":"gstack is required but not installed. See stderr for install instructions."}'
exit 0
fi
echo '{}'

15
.claude/settings.json Normal file
View File

@@ -0,0 +1,15 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Skill",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/check-gstack.sh\""
}
]
}
]
}
}

View File

@@ -0,0 +1,60 @@
---
name: awesome-design-md
description: Use VoltAgent's awesome-design-md collection when the user wants UI inspired by a specific brand or asks for a DESIGN.md reference, visual system, or brand-style implementation such as Stripe, Linear, Vercel, Claude, or Supabase. Resolve the brand slug from the installed `design-md/` folder, fetch the matching `getdesign.md` design document for that slug, and apply it as the design-system reference for implementation.
---
# Awesome DESIGN.md
Use this skill to turn the installed `awesome-design-md` collection into a practical design reference workflow.
The local `design-md/` directory is the index of supported brand slugs. Its per-brand `README.md` files are only pointers. The actual design-system document lives at:
```text
https://getdesign.md/<slug>/design-md
```
## Workflow
1. Identify the target brand or closest visual reference.
2. Resolve the brand slug from the local `design-md/` folder.
3. Prefer exact folder names for dotted brands such as `linear.app`, `mistral.ai`, `opencode.ai`, `together.ai`, and `x.ai`.
4. Fetch `https://getdesign.md/<slug>/design-md`.
5. Use the fetched document in one of two ways:
- write or update the project's root `DESIGN.md`
- keep it as an external design reference while implementing UI
6. Preserve the user's product semantics and content model. Borrow visual language, spacing, typography, motion, and component patterns, not product-specific copy.
## Local Source Of Truth
Use the installed folder below to confirm which slugs exist before fetching:
```text
C:\Users\a931627\.claude\skills\awesome-design-md\design-md
```
If needed, list the available slugs with:
```powershell
Get-ChildItem -Name C:\Users\a931627\.claude\skills\awesome-design-md\design-md
```
## Practical Rules
- Treat `DESIGN.md` as a visual system reference, not as code to mirror verbatim.
- If the user asks for "something like X, but lighter, warmer, or more minimal", adapt the reference instead of cloning it literally.
- If multiple brands fit, choose the closest one and state the choice.
- If a slug is missing locally or the remote fetch fails, pick the nearest available brand or ask the user for a replacement target.
- When working inside an existing design system, merge the borrowed visual cues with the established component structure instead of replacing everything.
## Common Slug Examples
- `stripe`
- `vercel`
- `claude`
- `cursor`
- `supabase`
- `linear.app`
- `mistral.ai`
- `opencode.ai`
- `together.ai`
- `x.ai`

39
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,39 @@
## Summary
-
## Change Type
- [ ] QRMaster SEO page
- [ ] QRMaster landing/tool page
- [ ] QRMaster conversion/pricing change
- [ ] GreenLens content/ASO workflow
- [ ] Bug fix
- [ ] Other
## SEO / Content Review
- [ ] Primary search intent is clear.
- [ ] Metadata is present and specific.
- [ ] Exactly one H1 is rendered for each new or changed page.
- [ ] Internal links are added to relevant money pages.
- [ ] CTA is specific to the page/use case.
- [ ] Duplicate or thin content risk was checked.
- [ ] Schema/structured data was added or intentionally skipped.
## Verification
- [ ] Build passes.
- [ ] Lint passes.
- [ ] Links/CTAs checked.
- [ ] Screenshots or notes included for UI changes.
## Codex Review Prompt
For QRMaster SEO/page changes, run:
```text
Use docs/automations/qrmaster-pr-seo-review.md and review this PR for SEO,
conversion, internal linking, duplicate content, schema, and build/lint risk.
```

View File

@@ -1,6 +1,6 @@
name: CI name: CI
on: [push] on: [push, pull_request]
jobs: jobs:
build: build:
@@ -23,4 +23,4 @@ jobs:
run: npm run build run: npm run build
- name: Run linter - name: Run linter
run: npm run lint run: npm run lint

33
.gitignore vendored
View File

@@ -24,12 +24,12 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# local env files # local env files
.env*.local .env*.local
.env .env
# vercel # vercel
.vercel .vercel
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
@@ -38,15 +38,24 @@ next-env.d.ts
# prisma # prisma
# /prisma/migrations/ # Now tracked in Git for deployment # /prisma/migrations/ # Now tracked in Git for deployment
# docker # docker
docker-compose.override.yml docker-compose.override.yml
*.sql *.sql
!prisma/migrations/**/*.sql !prisma/migrations/**/*.sql
/backups/ /backups/
# logs # logs
logs logs
*.log *.log
# project-specific
Leads/
marketing/
output/
remotion/
# local dev script # local dev script
dev-server.js dev-server.js
.gstack/
.env.meta

1
.impeccable-live.json Normal file
View File

@@ -0,0 +1 @@
{"pid":23720,"port":8400,"token":"99ca8ad6-3aa6-44f6-9b64-25921f55724b"}

View File

@@ -299,9 +299,20 @@ AGENT_WORKFLOW.md
CLAUDE.md CLAUDE.md
AGENTS.md AGENTS.md
This allows it to be used by: This allows it to be used by:
- Claude Code Agent Teams - Claude Code Agent Teams
- Codex CLI - Codex CLI
- Gemini Code Assist - Gemini Code Assist
- Cursor Agents - Cursor Agents
---
# gstack
This repo also uses the global `gstack` skill pack for AI-assisted work.
- Claude Code: install to `~/.claude/skills/gstack` and run `./setup --team`
- Codex: gstack skills are installed globally under `~/.codex/skills/gstack-*`
- Prefer gstack skills for structured work when they fit the task, especially `/browse`, `/review`, `/investigate`, `/qa`, `/ship`, and `/cso`
- Do not reference vendored repo paths for gstack; use the global install path instead

View File

@@ -273,9 +273,47 @@ docker-compose exec web npx prisma migrate deploy # Run migrations in container
- Requires external PostgreSQL database (Vercel Postgres, Supabase, etc.) - Requires external PostgreSQL database (Vercel Postgres, Supabase, etc.)
- Redis is optional - Redis is optional
## Additional Resources ## Additional Resources
- README.md - Detailed setup and feature overview - README.md - Detailed setup and feature overview
- DOCKER_SETUP.md - Complete Docker deployment guide - DOCKER_SETUP.md - Complete Docker deployment guide
- prisma/schema.prisma - Database schema and relationships - prisma/schema.prisma - Database schema and relationships
- env.example - Environment variable template - env.example - Environment variable template
## gstack (REQUIRED — global install)
Before doing any AI-assisted work in this repo, verify gstack is installed globally:
```bash
test -d ~/.claude/skills/gstack/bin && echo "GSTACK_OK" || echo "GSTACK_MISSING"
```
If `GSTACK_MISSING`: stop and install it:
```bash
git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
cd ~/.claude/skills/gstack && ./setup --team
```
After install, use gstack skills where appropriate, especially `/browse`, `/review`, `/investigate`, `/qa`, `/ship`, and `/cso`.
Use `~/.claude/skills/gstack/...` for gstack file paths because this repo does not vendor the pack locally.
## Skill routing
When the user's request matches an available skill, ALWAYS invoke it using the Skill
tool as your FIRST action. Do NOT answer directly, do NOT use other tools first.
The skill has specialized workflows that produce better results than ad-hoc answers.
Key routing rules:
- Product ideas, "is this worth building", brainstorming → invoke office-hours
- Bugs, errors, "why is this broken", 500 errors → invoke investigate
- Ship, deploy, push, create PR → invoke ship
- QA, test the site, find bugs → invoke qa
- Code review, check my diff → invoke review
- Update docs after shipping → invoke document-release
- Weekly retro → invoke retro
- Design system, brand → invoke design-consultation
- Visual audit, design polish → invoke design-review
- Architecture review → invoke plan-eng-review
- Save progress, checkpoint, resume → invoke checkpoint
- Code quality, health check → invoke health

322
DESIGN.md Normal file
View File

@@ -0,0 +1,322 @@
# Design System Inspired by Stripe
## 1. Visual Theme & Atmosphere
Stripe's website is the gold standard of fintech design -- a system that manages to feel simultaneously technical and luxurious, precise and warm. The page opens on a clean white canvas (`#ffffff`) with deep navy headings (`#061b31`) and a signature purple (`#533afd`) that functions as both brand anchor and interactive accent. This isn't the cold, clinical purple of enterprise software; it's a rich, saturated violet that reads as confident and premium. The overall impression is of a financial institution redesigned by a world-class type foundry.
The custom `sohne-var` variable font is the defining element of Stripe's visual identity. Every text element enables the OpenType `"ss01"` stylistic set, which modifies character shapes for a distinctly geometric, modern feel. At display sizes (48px-56px), sohne-var runs at weight 300 -- an extraordinarily light weight for headlines that creates an ethereal, almost whispered authority. This is the opposite of the "bold hero headline" convention; Stripe's headlines feel like they don't need to shout. The negative letter-spacing (-1.4px at 56px, -0.96px at 48px) tightens the text into dense, engineered blocks. At smaller sizes, the system also uses weight 300 with proportionally reduced tracking, and tabular numerals via `"tnum"` for financial data display.
What truly distinguishes Stripe is its shadow system. Rather than the flat or single-layer approach of most sites, Stripe uses multi-layer, blue-tinted shadows: the signature `rgba(50,50,93,0.25)` combined with `rgba(0,0,0,0.1)` creates shadows with a cool, almost atmospheric depth -- like elements are floating in a twilight sky. The blue-gray undertone of the primary shadow color (50,50,93) ties directly to the navy-purple brand palette, making even elevation feel on-brand.
**Key Characteristics:**
- sohne-var with OpenType `"ss01"` on all text -- a custom stylistic set that defines the brand's letterforms
- Weight 300 as the signature headline weight -- light, confident, anti-convention
- Negative letter-spacing at display sizes (-1.4px at 56px, progressive relaxation downward)
- Blue-tinted multi-layer shadows using `rgba(50,50,93,0.25)` -- elevation that feels brand-colored
- Deep navy (`#061b31`) headings instead of black -- warm, premium, financial-grade
- Conservative border-radius (4px-8px) -- nothing pill-shaped, nothing harsh
- Ruby (`#ea2261`) and magenta (`#f96bee`) accents for gradient and decorative elements
- `SourceCodePro` as the monospace companion for code and technical labels
## 2. Color Palette & Roles
### Primary
- **Stripe Purple** (`#533afd`): Primary brand color, CTA backgrounds, link text, interactive highlights. A saturated blue-violet that anchors the entire system.
- **Deep Navy** (`#061b31`): `--hds-color-heading-solid`. Primary heading color. Not black, not gray -- a very dark blue that adds warmth and depth to text.
- **Pure White** (`#ffffff`): Page background, card surfaces, button text on dark backgrounds.
### Brand & Dark
- **Brand Dark** (`#1c1e54`): `--hds-color-util-brand-900`. Deep indigo for dark sections, footer backgrounds, and immersive brand moments.
- **Dark Navy** (`#0d253d`): `--hds-color-core-neutral-975`. The darkest neutral -- almost-black with a blue undertone for maximum depth without harshness.
### Accent Colors
- **Ruby** (`#ea2261`): `--hds-color-accentColorMode-ruby-icon-solid`. Warm red-pink for icons, alerts, and accent elements.
- **Magenta** (`#f96bee`): `--hds-color-accentColorMode-magenta-icon-gradientMiddle`. Vivid pink-purple for gradients and decorative highlights.
- **Magenta Light** (`#ffd7ef`): `--hds-color-util-accent-magenta-100`. Tinted surface for magenta-themed cards and badges.
### Interactive
- **Primary Purple** (`#533afd`): Primary link color, active states, selected elements.
- **Purple Hover** (`#4434d4`): Darker purple for hover states on primary elements.
- **Purple Deep** (`#2e2b8c`): `--hds-color-button-ui-iconHover`. Dark purple for icon hover states.
- **Purple Light** (`#b9b9f9`): `--hds-color-action-bg-subduedHover`. Soft lavender for subdued hover backgrounds.
- **Purple Mid** (`#665efd`): `--hds-color-input-selector-text-range`. Range selector and input highlight color.
### Neutral Scale
- **Heading** (`#061b31`): Primary headings, nav text, strong labels.
- **Label** (`#273951`): `--hds-color-input-text-label`. Form labels, secondary headings.
- **Body** (`#64748d`): Secondary text, descriptions, captions.
- **Success Green** (`#15be53`): Status badges, success indicators (with 0.2-0.4 alpha for backgrounds/borders).
- **Success Text** (`#108c3d`): Success badge text color.
- **Lemon** (`#9b6829`): `--hds-color-core-lemon-500`. Warning and highlight accent.
### Surface & Borders
- **Border Default** (`#e5edf5`): Standard border color for cards, dividers, and containers.
- **Border Purple** (`#b9b9f9`): Active/selected state borders on buttons and inputs.
- **Border Soft Purple** (`#d6d9fc`): Subtle purple-tinted borders for secondary elements.
- **Border Magenta** (`#ffd7ef`): Pink-tinted borders for magenta-themed elements.
- **Border Dashed** (`#362baa`): Dashed borders for drop zones and placeholder elements.
### Shadow Colors
- **Shadow Blue** (`rgba(50,50,93,0.25)`): The signature -- blue-tinted primary shadow color.
- **Shadow Dark Blue** (`rgba(3,3,39,0.25)`): Deeper blue shadow for elevated elements.
- **Shadow Black** (`rgba(0,0,0,0.1)`): Secondary shadow layer for depth reinforcement.
- **Shadow Ambient** (`rgba(23,23,23,0.08)`): Soft ambient shadow for subtle elevation.
- **Shadow Soft** (`rgba(23,23,23,0.06)`): Minimal ambient shadow for light lift.
## 3. Typography Rules
### Font Family
- **Primary**: `sohne-var`, with fallback: `SF Pro Display`
- **Monospace**: `SourceCodePro`, with fallback: `SFMono-Regular`
- **OpenType Features**: `"ss01"` enabled globally on all sohne-var text; `"tnum"` for tabular numbers on financial data and captions.
### Hierarchy
| Role | Font | Size | Weight | Line Height | Letter Spacing | Features | Notes |
|------|------|------|--------|-------------|----------------|----------|-------|
| Display Hero | sohne-var | 56px (3.50rem) | 300 | 1.03 (tight) | -1.4px | ss01 | Maximum size, whisper-weight authority |
| Display Large | sohne-var | 48px (3.00rem) | 300 | 1.15 (tight) | -0.96px | ss01 | Secondary hero headlines |
| Section Heading | sohne-var | 32px (2.00rem) | 300 | 1.10 (tight) | -0.64px | ss01 | Feature section titles |
| Sub-heading Large | sohne-var | 26px (1.63rem) | 300 | 1.12 (tight) | -0.26px | ss01 | Card headings, sub-sections |
| Sub-heading | sohne-var | 22px (1.38rem) | 300 | 1.10 (tight) | -0.22px | ss01 | Smaller section heads |
| Body Large | sohne-var | 18px (1.13rem) | 300 | 1.40 | normal | ss01 | Feature descriptions, intro text |
| Body | sohne-var | 16px (1.00rem) | 300-400 | 1.40 | normal | ss01 | Standard reading text |
| Button | sohne-var | 16px (1.00rem) | 400 | 1.00 (tight) | normal | ss01 | Primary button text |
| Button Small | sohne-var | 14px (0.88rem) | 400 | 1.00 (tight) | normal | ss01 | Secondary/compact buttons |
| Link | sohne-var | 14px (0.88rem) | 400 | 1.00 (tight) | normal | ss01 | Navigation links |
| Caption | sohne-var | 13px (0.81rem) | 400 | normal | normal | ss01 | Small labels, metadata |
| Caption Small | sohne-var | 12px (0.75rem) | 300-400 | 1.33-1.45 | normal | ss01 | Fine print, timestamps |
| Caption Tabular | sohne-var | 12px (0.75rem) | 300-400 | 1.33 | -0.36px | tnum | Financial data, numbers |
| Micro | sohne-var | 10px (0.63rem) | 300 | 1.15 (tight) | 0.1px | ss01 | Tiny labels, axis markers |
| Micro Tabular | sohne-var | 10px (0.63rem) | 300 | 1.15 (tight) | -0.3px | tnum | Chart data, small numbers |
| Nano | sohne-var | 8px (0.50rem) | 300 | 1.07 (tight) | normal | ss01 | Smallest labels |
| Code Body | SourceCodePro | 12px (0.75rem) | 500 | 2.00 (relaxed) | normal | -- | Code blocks, syntax |
| Code Bold | SourceCodePro | 12px (0.75rem) | 700 | 2.00 (relaxed) | normal | -- | Bold code, keywords |
| Code Label | SourceCodePro | 12px (0.75rem) | 500 | 2.00 (relaxed) | normal | uppercase | Technical labels |
| Code Micro | SourceCodePro | 9px (0.56rem) | 500 | 1.00 (tight) | normal | ss01 | Tiny code annotations |
### Principles
- **Light weight as signature**: Weight 300 at display sizes is Stripe's most distinctive typographic choice. Where others use 600-700 to command attention, Stripe uses lightness as luxury -- the text is so confident it doesn't need weight to be authoritative.
- **ss01 everywhere**: The `"ss01"` stylistic set is non-negotiable. It modifies specific glyphs (likely alternate `a`, `g`, `l` forms) to create a more geometric, contemporary feel across all sohne-var text.
- **Two OpenType modes**: `"ss01"` for display/body text, `"tnum"` for tabular numerals in financial data. These never overlap -- a number in a paragraph uses ss01, a number in a data table uses tnum.
- **Progressive tracking**: Letter-spacing tightens proportionally with size: -1.4px at 56px, -0.96px at 48px, -0.64px at 32px, -0.26px at 26px, normal at 16px and below.
- **Two-weight simplicity**: Primarily 300 (body and headings) and 400 (UI/buttons). No bold (700) in the primary font -- SourceCodePro uses 500/700 for code contrast.
## 4. Component Stylings
### Buttons
**Primary Purple**
- Background: `#533afd`
- Text: `#ffffff`
- Padding: 8px 16px
- Radius: 4px
- Font: 16px sohne-var weight 400, `"ss01"`
- Hover: `#4434d4` background
- Use: Primary CTA ("Start now", "Contact sales")
**Ghost / Outlined**
- Background: transparent
- Text: `#533afd`
- Padding: 8px 16px
- Radius: 4px
- Border: `1px solid #b9b9f9`
- Font: 16px sohne-var weight 400, `"ss01"`
- Hover: background shifts to `rgba(83,58,253,0.05)`
- Use: Secondary actions
**Transparent Info**
- Background: transparent
- Text: `#2874ad`
- Padding: 8px 16px
- Radius: 4px
- Border: `1px solid rgba(43,145,223,0.2)`
- Use: Tertiary/info-level actions
**Neutral Ghost**
- Background: transparent (`rgba(255,255,255,0)`)
- Text: `rgba(16,16,16,0.3)`
- Padding: 8px 16px
- Radius: 4px
- Outline: `1px solid rgb(212,222,233)`
- Use: Disabled or muted actions
### Cards & Containers
- Background: `#ffffff`
- Border: `1px solid #e5edf5` (standard) or `1px solid #061b31` (dark accent)
- Radius: 4px (tight), 5px (standard), 6px (comfortable), 8px (featured)
- Shadow (standard): `rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px`
- Shadow (ambient): `rgba(23,23,23,0.08) 0px 15px 35px 0px`
- Hover: shadow intensifies, often adding the blue-tinted layer
### Badges / Tags / Pills
**Neutral Pill**
- Background: `#ffffff`
- Text: `#000000`
- Padding: 0px 6px
- Radius: 4px
- Border: `1px solid #f6f9fc`
- Font: 11px weight 400
**Success Badge**
- Background: `rgba(21,190,83,0.2)`
- Text: `#108c3d`
- Padding: 1px 6px
- Radius: 4px
- Border: `1px solid rgba(21,190,83,0.4)`
- Font: 10px weight 300
### Inputs & Forms
- Border: `1px solid #e5edf5`
- Radius: 4px
- Focus: `1px solid #533afd` or purple ring
- Label: `#273951`, 14px sohne-var
- Text: `#061b31`
- Placeholder: `#64748d`
### Navigation
- Clean horizontal nav on white, sticky with blur backdrop
- Brand logotype left-aligned
- Links: sohne-var 14px weight 400, `#061b31` text with `"ss01"`
- Radius: 6px on nav container
- CTA: purple button right-aligned ("Sign in", "Start now")
- Mobile: hamburger toggle with 6px radius
### Decorative Elements
**Dashed Borders**
- `1px dashed #362baa` (purple) for placeholder/drop zones
- `1px dashed #ffd7ef` (magenta) for magenta-themed decorative borders
**Gradient Accents**
- Ruby-to-magenta gradients (`#ea2261` to `#f96bee`) for hero decorations
- Brand dark sections use `#1c1e54` backgrounds with white text
## 5. Layout Principles
### Spacing System
- Base unit: 8px
- Scale: 1px, 2px, 4px, 6px, 8px, 10px, 11px, 12px, 14px, 16px, 18px, 20px
- Notable: The scale is dense at the small end (every 2px from 4-12), reflecting Stripe's precision-oriented UI for financial data
### Grid & Container
- Max content width: approximately 1080px
- Hero: centered single-column with generous padding, lightweight headlines
- Feature sections: 2-3 column grids for feature cards
- Full-width dark sections with `#1c1e54` background for brand immersion
- Code/dashboard previews as contained cards with blue-tinted shadows
### Whitespace Philosophy
- **Precision spacing**: Unlike the vast emptiness of minimalist systems, Stripe uses measured, purposeful whitespace. Every gap is a deliberate typographic choice.
- **Dense data, generous chrome**: Financial data displays (tables, charts) are tightly packed, but the UI chrome around them is generously spaced. This creates a sense of controlled density -- like a well-organized spreadsheet in a beautiful frame.
- **Section rhythm**: White sections alternate with dark brand sections (`#1c1e54`), creating a dramatic light/dark cadence that prevents monotony without introducing arbitrary color.
### Border Radius Scale
- Micro (1px): Fine-grained elements, subtle rounding
- Standard (4px): Buttons, inputs, badges, cards -- the workhorse
- Comfortable (5px): Standard card containers
- Relaxed (6px): Navigation, larger interactive elements
- Large (8px): Featured cards, hero elements
- Compound: `0px 0px 6px 6px` for bottom-rounded containers (tab panels, dropdown footers)
## 6. Depth & Elevation
| Level | Treatment | Use |
|-------|-----------|-----|
| Flat (Level 0) | No shadow | Page background, inline text |
| Ambient (Level 1) | `rgba(23,23,23,0.06) 0px 3px 6px` | Subtle card lift, hover hints |
| Standard (Level 2) | `rgba(23,23,23,0.08) 0px 15px 35px` | Standard cards, content panels |
| Elevated (Level 3) | `rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px` | Featured cards, dropdowns, popovers |
| Deep (Level 4) | `rgba(3,3,39,0.25) 0px 14px 21px -14px, rgba(0,0,0,0.1) 0px 8px 17px -8px` | Modals, floating panels |
| Ring (Accessibility) | `2px solid #533afd` outline | Keyboard focus ring |
**Shadow Philosophy**: Stripe's shadow system is built on a principle of chromatic depth. Where most design systems use neutral gray or black shadows, Stripe's primary shadow color (`rgba(50,50,93,0.25)`) is a deep blue-gray that echoes the brand's navy palette. This creates shadows that don't just add depth -- they add brand atmosphere. The multi-layer approach pairs this blue-tinted shadow with a pure black secondary layer (`rgba(0,0,0,0.1)`) at a different offset, creating a parallax-like depth where the branded shadow sits farther from the element and the neutral shadow sits closer. The negative spread values (-30px, -18px) ensure shadows don't extend beyond the element's footprint horizontally, keeping elevation vertical and controlled.
### Decorative Depth
- Dark brand sections (`#1c1e54`) create immersive depth through background color contrast
- Gradient overlays with ruby-to-magenta transitions for hero decorations
- Shadow color `rgba(0,55,112,0.08)` (`--hds-color-shadow-sm-top`) for top-edge shadows on sticky elements
## 7. Do's and Don'ts
### Do
- Use sohne-var with `"ss01"` on every text element -- the stylistic set IS the brand
- Use weight 300 for all headlines and body text -- lightness is the signature
- Apply blue-tinted shadows (`rgba(50,50,93,0.25)`) for all elevated elements
- Use `#061b31` (deep navy) for headings instead of `#000000` -- the warmth matters
- Keep border-radius between 4px-8px -- conservative rounding is intentional
- Use `"tnum"` for any tabular/financial number display
- Layer shadows: blue-tinted far + neutral close for depth parallax
- Use `#533afd` purple as the primary interactive/CTA color
### Don't
- Don't use weight 600-700 for sohne-var headlines -- weight 300 is the brand voice
- Don't use large border-radius (12px+, pill shapes) on cards or buttons -- Stripe is conservative
- Don't use neutral gray shadows -- always tint with blue (`rgba(50,50,93,...)`)
- Don't skip `"ss01"` on any sohne-var text -- the alternate glyphs define the personality
- Don't use pure black (`#000000`) for headings -- always `#061b31` deep navy
- Don't use warm accent colors (orange, yellow) for interactive elements -- purple is primary
- Don't apply positive letter-spacing at display sizes -- Stripe tracks tight
- Don't use the magenta/ruby accents for buttons or links -- they're decorative/gradient only
## 8. Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|------|-------|-------------|
| Mobile | <640px | Single column, reduced heading sizes, stacked cards |
| Tablet | 640-1024px | 2-column grids, moderate padding |
| Desktop | 1024-1280px | Full layout, 3-column feature grids |
| Large Desktop | >1280px | Centered content with generous margins |
### Touch Targets
- Buttons use comfortable padding (8px-16px vertical)
- Navigation links at 14px with adequate spacing
- Badges have 6px horizontal padding minimum for tap targets
- Mobile nav toggle with 6px radius button
### Collapsing Strategy
- Hero: 56px display -> 32px on mobile, weight 300 maintained
- Navigation: horizontal links + CTAs -> hamburger toggle
- Feature cards: 3-column -> 2-column -> single column stacked
- Dark brand sections: maintain full-width treatment, reduce internal padding
- Financial data tables: horizontal scroll on mobile
- Section spacing: 64px+ -> 40px on mobile
- Typography scale compresses: 56px -> 48px -> 32px hero sizes across breakpoints
### Image Behavior
- Dashboard/product screenshots maintain blue-tinted shadow at all sizes
- Hero gradient decorations simplify on mobile
- Code blocks maintain `SourceCodePro` treatment, may horizontally scroll
- Card images maintain consistent 4px-6px border-radius
## 9. Agent Prompt Guide
### Quick Color Reference
- Primary CTA: Stripe Purple (`#533afd`)
- CTA Hover: Purple Dark (`#4434d4`)
- Background: Pure White (`#ffffff`)
- Heading text: Deep Navy (`#061b31`)
- Body text: Slate (`#64748d`)
- Label text: Dark Slate (`#273951`)
- Border: Soft Blue (`#e5edf5`)
- Link: Stripe Purple (`#533afd`)
- Dark section: Brand Dark (`#1c1e54`)
- Success: Green (`#15be53`)
- Accent decorative: Ruby (`#ea2261`), Magenta (`#f96bee`)
### Example Component Prompts
- "Create a hero section on white background. Headline at 48px sohne-var weight 300, line-height 1.15, letter-spacing -0.96px, color #061b31, font-feature-settings 'ss01'. Subtitle at 18px weight 300, line-height 1.40, color #64748d. Purple CTA button (#533afd, 4px radius, 8px 16px padding, white text) and ghost button (transparent, 1px solid #b9b9f9, #533afd text, 4px radius)."
- "Design a card: white background, 1px solid #e5edf5 border, 6px radius. Shadow: rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px. Title at 22px sohne-var weight 300, letter-spacing -0.22px, color #061b31, 'ss01'. Body at 16px weight 300, #64748d."
- "Build a success badge: rgba(21,190,83,0.2) background, #108c3d text, 4px radius, 1px 6px padding, 10px sohne-var weight 300, border 1px solid rgba(21,190,83,0.4)."
- "Create navigation: white sticky header with backdrop-filter blur(12px). sohne-var 14px weight 400 for links, #061b31 text, 'ss01'. Purple CTA 'Start now' right-aligned (#533afd bg, white text, 4px radius). Nav container 6px radius."
- "Design a dark brand section: #1c1e54 background, white text. Headline 32px sohne-var weight 300, letter-spacing -0.64px, 'ss01'. Body 16px weight 300, rgba(255,255,255,0.7). Cards inside use rgba(255,255,255,0.1) border with 6px radius."
### Iteration Guide
1. Always enable `font-feature-settings: "ss01"` on sohne-var text -- this is the brand's typographic DNA
2. Weight 300 is the default; use 400 only for buttons/links/navigation
3. Shadow formula: `rgba(50,50,93,0.25) 0px Y1 B1 -S1, rgba(0,0,0,0.1) 0px Y2 B2 -S2` where Y1/B1 are larger (far shadow) and Y2/B2 are smaller (near shadow)
4. Heading color is `#061b31` (deep navy), body is `#64748d` (slate), labels are `#273951` (dark slate)
5. Border-radius stays in the 4px-8px range -- never use pill shapes or large rounding
6. Use `"tnum"` for any numbers in tables, charts, or financial displays
7. Dark sections use `#1c1e54` -- not black, not gray, but a deep branded indigo
8. SourceCodePro for code at 12px/500 with 2.00 line-height (very generous for readability)

View File

@@ -51,17 +51,18 @@ ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/node_modules ./node_modules COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder /app/prisma ./prisma COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
COPY --from=builder /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder /app/docker/entrypoint.sh ./docker/entrypoint.sh COPY --from=builder --chown=nextjs:nodejs /app/docker/entrypoint.sh ./docker/entrypoint.sh
RUN chmod +x ./docker/entrypoint.sh RUN chmod +x ./docker/entrypoint.sh
# --- NEU: Ordner erstellen und Rechte an den nextjs User geben --- # Next writes ISR/prerender artifacts under .next/server/app at runtime.
RUN mkdir -p /app/.next/cache && chown nextjs:nodejs /app/.next/cache RUN mkdir -p /app/.next/cache /app/.next/server/app \
&& chown -R nextjs:nodejs /app/.next
USER nextjs USER nextjs

51
PRODUCT.md Normal file
View File

@@ -0,0 +1,51 @@
# Product
## Register
brand
## Users
Two primary personas, both aesthetically-driven professionals who have outgrown generic tools:
**Marketing / Agency Lead** — manages multiple client campaigns simultaneously. Needs organized folders, detailed analytics, and design output that doesn't betray itself as "internet freeware." They evaluate tools by how their outputs look to clients, not just how the tool works internally.
**Modern Restaurateur** — owner of a boutique restaurant, cafe, or hotel. Their physical space is carefully designed; their digital touchpoints must match. They rely on Dynamic QR codes to swap menu PDFs and URLs seasonally without reprinting expensive acrylic table stands. Brand consistency between print and digital is non-negotiable for them.
**Arrival context:** Both land on QR Master after being burned by "free" tools that expired, injected third-party ads, or looked visually cheap. They are actively comparing options and arrive skeptical. They're not discovering QR tools for the first time — they're looking for a permanent, professional home.
## Product Purpose
QR Master is a precision QR code platform — creation, dynamic editing, and analytics — for professionals who refuse to compromise on aesthetics. Success means users choose QR Master not because they have to, but because they want to: the tool itself feels like an extension of their creative workflow, not a clunky utility they tolerate.
## Brand Personality
**Confident, Minimal, Crafted.**
"The Leica of QR Generators." A precision instrument that earns trust through intentionality, not decoration. Every pixel deliberate. Think Linear's pro-tool clarity, Raycast's utilitarian beauty, Framer's implied creative freedom — combined into something that feels high-performance without performing.
Tone of voice: direct and assured. No hedging, no exclamation points for emphasis, no "Amazing!" copy. Let the product speak. Copy is short, specific, and treats the user as a professional.
## Anti-references
- **SEO ad farms** (QR-Code-Generator.com style): cluttered sidebars, aggressive upsell banners, walls of keyword-stuffed text. The opposite of QR Master.
- **Bubbly link-in-bio tools**: neon gradients, Gen-Z playfulness, the Linktree aesthetic. QR Master is for businesses, not social profiles.
- **Legacy enterprise software**: cold gray, mechanical, joyless. High-end is not the same as corporate. QR Master should feel premium, not bureaucratic.
## Design Principles
1. **Precision over decoration.** Every element earns its place. No UX furniture — no gratuitous dividers, decorative gradients, or filler icons. If removing it doesn't hurt, remove it.
2. **Show, don't explain.** The product's quality is demonstrated by how the interface looks and behaves, not described in marketing copy. A beautifully rendered QR preview communicates more than three bullet points about "custom branding."
3. **Premium through restraint.** Sophistication comes from what's removed. More whitespace, fewer words, tighter hierarchy. The instinct to add is the enemy of craft.
4. **Trustworthy at a glance.** Clarity and professionalism must be instantaneous — users arriving skeptical decide in 5 seconds. No clever puzzles, no mystery-meat navigation. Confidence is expressed through legibility.
5. **Tool-like beauty.** Functional elegance, like a well-made physical instrument. Interactions should feel responsive and precise. Animations are subtle cues, not performances. The UI should feel fast even when it isn't.
## Accessibility & Inclusion
- **Standard**: WCAG 2.1 AA
- **Reduced motion**: Full `prefers-reduced-motion` support. Animations (where used) default to subtle fades or scale shifts; no motion for motion's sake.
- **Color contrast**: Critical for a QR creation tool. The dashboard must warn users when foreground/background color combinations produce insufficient contrast — both for readability and QR scannability. This is a functional requirement, not a nice-to-have.

View File

@@ -9,6 +9,8 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=en_US.utf8" POSTGRES_INITDB_ARGS: "-E UTF8 --locale=en_US.utf8"
ports:
- "5435:5432"
volumes: volumes:
- dbdata:/var/lib/postgresql/data - dbdata:/var/lib/postgresql/data
- ./docker/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh - ./docker/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
@@ -91,6 +93,7 @@ services:
retries: 10 retries: 10
networks: networks:
- qrmaster-network - qrmaster-network
# Adminer - Database Management UI (Optional) # Adminer - Database Management UI (Optional)
adminer: adminer:
@@ -116,4 +119,4 @@ volumes:
networks: networks:
qrmaster-network: qrmaster-network:
driver: bridge external: true

View File

@@ -0,0 +1,45 @@
# Codex Automation System
This folder defines reusable Codex workflows for QRMaster and GreenLens Pro.
Use these as operating playbooks when asking Codex to run growth, SEO, content,
or app-store work.
## Active Automations
### QRMaster
1. `qrmaster-pr-seo-review.md`
- Purpose: review every SEO, landing page, and conversion change before it is merged.
- Primary plugins/tools: GitHub, Codex.
- Primary skills: `ai-seo`, `content-strategy`, `careful`, `qa`.
2. `qrmaster-seo-sprint-machine.md`
- Purpose: plan and produce a weekly SEO sprint from keyword backlog to PR-ready work.
- Primary plugins/tools: GitHub, Coupler or CSV exports, Codex.
- Primary skills: `content-strategy`, `ai-seo`, `copywriting`, `qa`.
3. `qrmaster-broken-link-cta-checker.md`
- Purpose: catch broken internal links and broken CTAs after direct `main`
branch changes.
- Primary plugins/tools: Codex, GitHub Actions or local npm script.
- Primary skills: `qa`, `ai-seo`.
### GreenLens Pro
1. `greenlens-pain-mining-machine.md`
- Purpose: turn reviews, comments, competitor messaging, and search questions into
product, ASO, and content opportunities.
- Primary plugins/tools: Codex, Gmail, Coupler or CSV exports.
- Primary skills: `app-store-aso`, `content-strategy`, `copywriting`.
2. `greenlens-viral-slideshow-machine.md`
- Purpose: turn validated plant pains into TikTok, Instagram, and Canva-ready
slideshow assets.
- Primary plugins/tools: Canva, Codex, Gmail/Fyxer for creator briefs.
- Primary skills: `content-strategy`, `copywriting`, `ad-creative`, `app-store-aso`.
## Operating Rule
Do not automate publishing directly. Automate drafts, PRs, reviews, and packaged
outputs first. A human should approve live SEO pages, store metadata, influencer
messages, and paid/conversion changes.

View File

@@ -0,0 +1,160 @@
# GreenLens Pro Pain Mining Machine
## Goal
Turn real plant-owner pains into content, ASO, influencer, landing page, and
product opportunities.
## Why This Exists
GreenLens Pro should be driven by what users actually worry about:
yellow leaves, brown spots, root rot, overwatering, underwatering, pests,
curling leaves, and not knowing what to do next.
## Plugins And Skills
| Need | Use |
|---|---|
| Review/comment exports | Coupler, CSV exports, Codex |
| App Store optimization | `app-store-aso` skill |
| Content clustering | `content-strategy` skill |
| Copy and hooks | `copywriting` skill |
| Support/outreach drafting | Gmail/Fyxer plugin |
| Product issue creation | GitHub plugin |
## Data Sources
Use any available source, but label the source for every pain:
- App Store competitor reviews
- Google Play competitor reviews
- Reddit plant-care threads
- TikTok or Instagram comments
- Google autocomplete or People Also Ask exports
- Support emails or user feedback
- Existing GreenLens analytics or onboarding responses
## Pain Taxonomy
Cluster each item into one primary category:
- Yellow leaves
- Brown spots
- Root rot
- Overwatering
- Underwatering
- Curling leaves
- Drooping leaves
- Pests
- Light problems
- Soil and repotting
- Beginner confusion
- Diagnosis trust
- Price/paywall objection
- App usability issue
## Scoring Model
Score each pain from 0-100:
| Factor | Weight |
|---|---:|
| User urgency | 30 |
| App fit | 25 |
| Content virality | 20 |
| ASO/search value | 15 |
| Product learning value | 10 |
Prioritize urgent, visual, diagnosis-driven pains where GreenLens can credibly
help the user decide what to check next.
## Weekly Output
Produce:
1. Top 20 pains.
2. Top 10 social hooks.
3. Top 5 ASO keyword opportunities.
4. Top 5 blog or landing page ideas.
5. Top 5 product issues or feature hypotheses.
6. Top 10 influencer angles.
## Codex Pain Mining Prompt
```text
Run the GreenLens Pro Pain Mining Machine.
Use:
- docs/automations/greenlens-pain-mining-machine.md
- app-store-aso skill
- content-strategy skill
Input source: [reviews/comments/export/pasted text]
Market: [US / DE / global]
Platform focus: [iOS / Android / both]
Tasks:
1. Extract raw plant-owner pains.
2. Cluster them into the GreenLens pain taxonomy.
3. Score each pain by urgency, app fit, virality, ASO value, and product learning.
4. Convert winners into:
- social hooks
- ASO keyword ideas
- blog/landing page ideas
- product issues
- influencer outreach angles
Do not invent source quotes. If evidence is weak, label it as hypothesis.
```
## Output Template
```markdown
# GreenLens Pain Mining Report
## Source Summary
## Top Pains
| Rank | Pain | Source | Score | Why it matters |
|---|---|---|---:|---|
## Hook Backlog
## ASO Opportunities
## Product Issues
## Influencer Angles
## Next Actions
```
## Product Issue Template
```markdown
Title: [Feature or improvement]
User pain:
[What the user is struggling with]
Hypothesis:
If GreenLens [change], users will [outcome].
Acceptance criteria:
- [criterion]
- [criterion]
- [criterion]
Measurement:
- activation
- scan completion
- paywall conversion
- retention
```
## Success Criteria
- Every recommendation traces back to a real pain or explicitly marked hypothesis.
- Top pains can feed both ASO and social content.
- Product issues are concrete enough for GitHub.

View File

@@ -0,0 +1,139 @@
# GreenLens Pro Viral Slideshow Machine
## Goal
Convert validated plant pains into TikTok, Instagram, and Canva-ready slideshow
assets that drive awareness and app downloads.
## Plugins And Skills
| Need | Use |
|---|---|
| Creative generation | Canva plugin |
| Hook and caption writing | `copywriting`, `ad-creative` skills |
| Content planning | `content-strategy` skill |
| ASO alignment | `app-store-aso` skill |
| Creator briefs and outreach | Gmail/Fyxer plugin |
## Required Input
Use outputs from `greenlens-pain-mining-machine.md`:
- pain cluster
- urgency score
- source evidence
- target audience
- desired CTA
- app positioning angle
## Content Pillars
- Diagnosis before guessing
- Overwatering mistakes
- Yellow leaves
- Brown spots
- Root rot warnings
- Beginner plant rescue
- Plant symptoms explained
- "Do not water yet" warnings
- App scan/use-case demos
## Slideshow Formula
1. Hook: direct warning, contradiction, or curiosity.
2. Problem: show the common wrong assumption.
3. Explanation: simple plant-care reason.
4. Check: what the user should inspect first.
5. Risk: what happens if they guess.
6. GreenLens bridge: scan or diagnose before acting.
7. CTA: download, scan, or save.
## Hook Patterns
- "Do not water your plant before checking this."
- "Yellow leaves do not always mean your plant is thirsty."
- "Brown spots can mean more than sunburn."
- "Your plant was warning you before it started dying."
- "Overwatering often looks like underwatering."
- "Scan before you guess."
## Canva Direction
Use GreenLens as a calm diagnosis-first plant app:
- natural plant photography or close-up symptom imagery
- clear readable overlay text
- botanical but not decorative-only
- show symptoms clearly
- app screenshot or phone mockup only when it explains the action
- avoid vague wellness aesthetics that do not show the plant problem
## Codex Slideshow Prompt
```text
Run the GreenLens Pro Viral Slideshow Machine.
Use:
- docs/automations/greenlens-pain-mining-machine.md
- docs/automations/greenlens-viral-slideshow-machine.md
- app-store-aso skill
- content-strategy skill
- copywriting/ad-creative skills
Input pain cluster: [pain]
Audience: [beginner plant owners / plant rescue followers / houseplant collectors]
CTA: [Download GreenLens Pro / Scan your plant / Save this checklist]
Channel: [TikTok / Instagram / both]
Quantity: [number]
Return for each concept:
1. hook
2. 5-7 slide script
3. visual direction per slide
4. Canva prompt
5. caption
6. hashtags
7. ASO keyword tie-in
8. creator brief version
Rules:
- Keep claims educational, not medical/certain beyond evidence.
- Do not promise perfect diagnosis.
- Make the symptom visually inspectable.
- The app CTA should feel like the next practical step, not a hard sell.
```
## Output Template
```markdown
# GreenLens Slideshow Pack: [Pain Cluster]
## Concept 1: [Hook]
### Slides
1. [text] -- [visual]
2. [text] -- [visual]
3. [text] -- [visual]
4. [text] -- [visual]
5. [text] -- [visual]
6. [text] -- [visual]
7. [text] -- [visual]
### Canva Prompt
### Caption
### Hashtags
### ASO Tie-In
### Creator Brief
```
## Quality Bar
- The first slide must be understandable in under 2 seconds.
- Every slide should be shorter than 12 words when possible.
- The visual must show the symptom or action, not just a plant mood shot.
- The final CTA should match the pain: scan, check, save, or download.

View File

@@ -0,0 +1,57 @@
# QRMaster Broken Link + CTA Checker
## Goal
Catch broken internal links and broken conversion CTAs on `main`, especially
after direct edits without a pull request.
## What It Checks
- Static internal `href` values in source files.
- Static `router.push("/...")` destinations.
- Internal links against known Next.js app routes and files in `public/`.
- CTA-like links such as "Get started", "Create QR", "Start free",
"Generate QR", "Pricing", and "Upgrade".
- Pages that appear to have no obvious CTA link.
## Command
```bash
npm run check:links
```
The command prints a JSON report. It exits with a non-zero status if broken
internal links or broken CTA links are found.
## Known Limits
- Dynamic CMS/blog slugs are allowed by prefix and not fully validated.
- Runtime-only links built from variables are skipped.
- External links are not checked by this local script.
- This is a fast safety check, not a full crawl of the deployed website.
## Codex Automation Prompt
```text
Run the QRMaster Broken Link + CTA Checker.
Use:
- docs/automations/qrmaster-broken-link-cta-checker.md
- scripts/check-links-and-ctas.js
Run npm run check:links. Review the JSON report and summarize:
1. broken internal links
2. broken CTA links
3. important pages without obvious CTAs
4. concrete fixes with file paths
If the script fails, inspect the listed files and propose the smallest safe fix.
Do not modify production configuration automatically.
```
## Success Criteria
- No broken internal links.
- No broken CTA links.
- Important marketing, tool, and pricing pages have a clear CTA.

View File

@@ -0,0 +1,163 @@
# QRMaster PR SEO Review
## Goal
Catch SEO, conversion, content-quality, and technical issues before a QRMaster
page change is merged.
## Use When
- A PR changes landing pages, tool pages, comparison pages, blog posts, metadata,
schema, sitemap behavior, internal links, pricing copy, or CTAs.
- Codex generated new SEO pages from `marketing/programmatic-seo-top-50.md`.
- Existing pages were refreshed from Google Search Console or keyword data.
## Plugins And Skills
| Need | Use |
|---|---|
| Diff and PR review | GitHub plugin |
| Code/content inspection | Codex |
| SEO/AEO review | `ai-seo` skill |
| Content intent and cluster fit | `content-strategy` skill |
| Build/lint verification | GitHub Actions, `qa` skill |
| Careful merge decision | `careful` skill |
## Review Checklist
### 1. Technical SEO
- Exactly one H1 on the rendered page.
- Meta title exists and is specific to the page intent.
- Meta title places the primary keyword near the start where natural.
- Meta title stays under roughly 60 characters unless there is a clear reason.
- Meta description exists and promises the right outcome.
- Meta description includes the target keyword naturally, states the user benefit,
and stays concise enough to avoid likely truncation.
- Canonical URL is correct.
- Page is not accidentally noindexed.
- FAQ, Article, Product, Breadcrumb, or HowTo schema is valid where used.
- Sitemap and internal routing include the page where required.
- No broken internal links or CTA links.
- Language and locale are consistent.
- URL slug is short, descriptive, hyphenated, and does not include stale dates.
- Meaningful images have descriptive alt text and useful filenames where local
image handling allows it.
- Large visual assets are compressed or already optimized.
- Mobile layout is readable and CTAs are tappable.
- The page does not introduce obvious Core Web Vitals risks.
- Robots rules do not block important pages or desired AI/search crawlers.
### 2. Search Intent
- The first screen makes it obvious the page answers the target query.
- The opening paragraph states the problem directly.
- The page matches one primary intent only.
- The content is not a generic rewrite of another QRMaster page.
- The page includes concrete examples for its audience or use case.
- The target keyword intent is labeled as informational, commercial,
transactional, or navigational.
- The page covers the related subtopics a search or AI system would fan out to
for the main query.
- Each H2/H3 section answers the heading directly in the first sentence before
adding background or nuance.
### 3. QRMaster Conversion Fit
- Primary CTA is visible early.
- CTA copy matches the use case, not just generic "Get started".
- The page explains why dynamic QR codes matter when links change after printing.
- Scan analytics or tracking is mentioned when relevant.
- Privacy/GDPR positioning is included where tracking is discussed.
- The copy avoids unsupported claims.
### 4. Internal Linking
- New page links to the relevant money page:
- `/dynamic-qr-code-generator`
- `/qr-code-tracking`
- `/bulk-qr-code-generator`
- `/pricing`
- relevant `/tools/...` page
- Existing related pages should link back to the new page.
- Anchor text is natural and varied.
### 5. AI Search Extractability
- Important answer blocks are self-contained.
- Comparison content uses tables where useful.
- FAQ questions are written in natural user language.
- Definitions answer the query in 40-60 words when possible.
- Claims that need evidence include a source or are framed as product positioning.
- Sections are focused on one question or subtopic at a time.
- Bullet lists, tables, and short paragraphs are used where they improve
extraction and scanning.
- The page can be cited by AI systems without relying on surrounding context.
### 6. E-E-A-T And Quality
- Content is accurate, current, and not copied from competitors.
- Any competitor, pricing, legal, privacy, or compliance claim is verified or
clearly avoided.
- The page adds QRMaster-specific value, examples, workflows, or product context.
- The tone stays direct, useful, and trustworthy.
## Codex Review Prompt
```text
Review this QRMaster PR as an SEO, conversion, and technical quality gate.
Use:
- docs/automations/qrmaster-pr-seo-review.md
- .agents/product-marketing-context.md
- marketing/programmatic-seo-top-50.md
Check the diff for:
1. technical SEO issues
2. search intent mismatch
3. weak or missing CTA
4. duplicate/thin content
5. missing internal links
6. invalid or missing schema
7. weak AI/agentic-search extractability
8. missing visual/mobile/performance considerations
9. build/lint risks
Return findings ordered by severity. For each finding include:
- file path
- exact line if possible
- why it matters
- concrete fix
Also include:
- merge recommendation: approve / request changes
- required follow-up tasks
```
## Review Output Format
```markdown
## QRMaster PR SEO Review
Decision: Request changes
### Findings
1. [High] Missing internal link to `/dynamic-qr-code-generator`
2. [Medium] CTA is too generic for the target intent
3. [Low] FAQ question overlaps with another page
### Required Fixes
- Add contextual link from the "after printing" section to `/dynamic-qr-code-generator`.
- Change CTA from "Get started" to "Create an editable QR code for your flyer".
### Verification
- Build:
- Lint:
- Link/schema check:
```
## Success Criteria
- No high-severity SEO or conversion findings remain.
- Build and lint pass.
- The PR has a clear human approval before merge.

View File

@@ -0,0 +1,158 @@
# QRMaster SEO Sprint: Tracking and Analytics
Run date: 2026-05-11
Automation: QRMaster SEO Sprint Machine
Status: Recommendation package only; do not publish without human approval.
## Input Notes
- Used `docs/automations/qrmaster-seo-sprint-machine.md`, `docs/automations/qrmaster-pr-seo-review.md`, `.agents/product-marketing-context.md`, `marketing/programmatic-seo-top-50.md`, `seo-plan-april.md`, and `seo-keywords.csv`.
- `marketing/keyword-strategy-seo-plan.md` was not present in this worktree. `seo-plan-april.md` appears to be the local keyword strategy fallback and includes the same cluster-level keyword data.
- Existing routing redirects selected legacy `/guide/...` paths to `/learn/...`, so this sprint should avoid creating duplicate guide URLs without a clear canonical/routing decision.
## Cluster Scoring
| Cluster | Product fit /30 | Commercial intent /25 | Differentiation /15 | Cluster leverage /10 | Winability /10 | Effort /10 | Score | Decision |
|---|---:|---:|---:|---:|---:|---:|---:|---|
| Tracking and analytics | 29 | 24 | 15 | 10 | 8 | 8 | 94 | Select |
| Dynamic QR buying decision | 30 | 24 | 13 | 10 | 7 | 7 | 91 | Next best |
| Bulk QR generation | 27 | 19 | 14 | 8 | 9 | 8 | 85 | Hold for later |
| Commercial alternatives | 22 | 25 | 12 | 8 | 6 | 5 | 78 | Needs current competitor verification |
| Restaurant/menu QR | 25 | 16 | 12 | 8 | 7 | 7 | 75 | Good vertical support, weaker immediate demand |
| Custom/design QR | 20 | 18 | 9 | 8 | 8 | 8 | 71 | Tool-led, less differentiated |
| Print reliability | 21 | 13 | 10 | 7 | 9 | 8 | 68 | Useful support content, weaker commercial pull |
## Rationale
Tracking and analytics is the highest-fit weekly cluster because it maps directly to QRMaster's differentiators: dynamic QR redirects, scan analytics, placement comparison, and privacy-first reporting. The keyword set includes `qr code tracking` at 1k-10k monthly volume with +900% 3-month YoY trend, `track qr code scans` with +900% 3-month trend, and `trackable qr code` with the highest CPC ceiling in the file at EUR 34.25. The cluster also has strong internal-link leverage into `/dynamic-qr-code-generator`, `/qr-code-analytics`, `/qr-code-tracking`, `/qr-code-for-marketing-campaigns`, pricing, and use-case pages.
## Selected Work
| Type | URL | Score | Reason |
|---|---|---:|---|
| Refresh | `/qr-code-tracking` | 94 | Money page for `qr code tracking`, `track qr code scans`, and `trackable qr code`; already has schema but should strengthen privacy and placement examples. |
| Refresh | `/qr-code-analytics` | 90 | Needs clearer separation from tracking: analytics should own dashboard interpretation, ROI, and performance insights. |
| Refresh | `/blog/trackable-qr-codes` | 88 | Existing support article should capture `trackable qr code` and link strongly to `/qr-code-tracking`. |
| Refresh | `/blog/utm-parameter-qr-codes` | 86 | Existing support article should capture fan-out intent around GA4, UTM naming, placement comparison, and offline attribution. |
| New support page | `/use-cases/qr-codes-for-review-collection` | 86 | Existing backlog item with natural tracking CTA; use as a measurable review funnel page, not a generic Google reviews tool duplicate. |
## Keyword Intent And Fan-Out
| URL | Primary keyword | Intent | Fan-out subtopics |
|---|---|---|---|
| `/qr-code-tracking` | `qr code tracking` | Commercial | What can be tracked, static vs dynamic tracking, scan count vs unique scans, device/location/time context, privacy/GDPR, placement comparison, UTM pairing, dashboard workflow, pricing limits. |
| `/qr-code-analytics` | `qr code analytics` | Commercial | Analytics dashboard, ROI interpretation, campaign attribution, useful metrics vs vanity metrics, offline-to-online measurement, reporting cadence, route naming, export/share needs. |
| `/blog/trackable-qr-codes` | `trackable qr code` | Informational-commercial | Definition, how tracking works, dynamic redirect layer, privacy limits, examples by placement, pros/cons, setup checklist, when not to track. |
| `/blog/utm-parameter-qr-codes` | `qr code UTM tracking` | Informational | GA4 source/medium/campaign/content conventions, examples for flyers/events/packaging, common mistakes, naming templates, QR destination testing. |
| `/use-cases/qr-codes-for-review-collection` | `QR codes for review collection` | Commercial | Google review link workflow, in-store placement, happy-path routing, feedback triage, scan tracking, QR privacy, dynamic destination updates. |
## 2026 On-Page And Agentic-Search Rules
- Titles/H1s should put the target keyword near the start:
- `/qr-code-tracking`: `QR Code Tracking: Track QR Code Scans`
- `/qr-code-analytics`: `QR Code Analytics: Measure Offline Campaigns`
- `/blog/trackable-qr-codes`: `Trackable QR Codes: What You Can Measure`
- `/blog/utm-parameter-qr-codes`: `QR Code UTM Tracking: GA4 Setup Guide`
- `/use-cases/qr-codes-for-review-collection`: `QR Codes for Review Collection`
- Each H2 should start with a direct answer in the first sentence.
- Add compact tables for `static vs dynamic`, `tracking vs analytics`, and `UTM examples by placement`.
- Add FAQ schema to refreshed informational pages where the existing blog system supports it; preserve SoftwareApplication and HowTo schema on money pages.
- Use self-contained answer blocks of roughly 40-60 words for definitions and "can you track..." questions.
- Visuals should be meaningful: dashboard screenshot/mockup, placement comparison example, UTM naming table, and review-flow diagram.
- Robots/indexing: current `robots.ts` allows major search/AI crawlers and disallows private app/API paths. Keep these pages indexed, sitemap-included, and canonicalized to their final URLs.
- Mobile/speed risks: avoid heavy dashboard imagery; use compressed static images and keep tables horizontally readable on mobile.
## Recommended New Page
### `/use-cases/qr-codes-for-review-collection`
Purpose: Create a commercially useful support page for restaurants, cafes, retail, hotels, and service businesses that want measurable review capture.
Required sections:
1. Direct answer: a review-collection QR code sends satisfied customers to the right review or feedback flow and lets teams measure which physical prompts get scanned.
2. Workflow: in-store sign, receipt, table card, counter card, packaging insert.
3. Dynamic vs static: use dynamic if the review platform, routing rule, or offer changes.
4. Tracking angle: compare scan volume by placement, store, or campaign.
5. Privacy note: describe scan analytics without promising personally identifiable tracking.
6. CTA: `Create a trackable review QR code` to `/qr-code-tracking` or `/signup`.
7. Internal links: `/tools/google-review-qr-code`, `/qr-code-tracking`, `/dynamic-qr-code-generator`, `/restaurants`, `/use-cases/feedback-qr-codes`.
8. Schema: FAQPage + BreadcrumbList; consider HowTo if step-by-step content is included.
## Recommended Page Refreshes
### `/qr-code-tracking`
- Strengthen first-screen answer: "QR code tracking uses a dynamic redirect to record scan time, device context, and approximate location before sending the scanner to the final destination."
- Add a privacy-first section explaining hashed/anonymized IP positioning from QRMaster.
- Add a table: "What QRMaster tracks / what it does not track."
- Add examples for flyer, menu, event booth, packaging, and review collection.
- Link to `/qr-code-analytics`, `/blog/utm-parameter-qr-codes`, `/reprint-calculator`, `/pricing`, and the new review-collection page.
### `/qr-code-analytics`
- Separate from tracking: tracking collects scan events; analytics helps interpret placement and campaign performance.
- Add an "analytics questions" table: which placement worked, when scans peaked, which destination converted, what to reprint.
- Add a section on useful metrics vs vanity metrics.
- Link back to `/qr-code-tracking`, `/qr-code-for-marketing-campaigns`, `/use-cases/flyer-qr-codes`, `/use-cases/packaging-qr-codes`, and `/pricing`.
### `/blog/trackable-qr-codes`
- Refresh title/meta around `trackable qr code`.
- Add a 40-60 word definition block near the top.
- Update FAQ to include "Can a static QR code be tracked?", "Are trackable QR codes GDPR-friendly?", and "Do trackable QR codes need a redirect?"
- Link early to `/qr-code-tracking` with anchor `track QR code scans`.
### `/blog/utm-parameter-qr-codes`
- Add a QR-specific UTM template table by placement.
- Add GA4 naming convention examples.
- Clarify when to use separate QR codes versus one QR with different `utm_content` values.
- Link to `/qr-code-analytics` and `/qr-code-tracking`.
## Internal-Link Plan
| Source | Destination | Anchor |
|---|---|---|
| `/dynamic-qr-code-generator` | `/qr-code-tracking` | `track QR code scans` |
| `/dynamic-qr-code-generator` | `/qr-code-analytics` | `QR code analytics dashboard` |
| `/qr-code-tracking` | `/qr-code-analytics` | `interpret QR scan analytics` |
| `/qr-code-tracking` | `/blog/utm-parameter-qr-codes` | `use UTMs with QR codes` |
| `/qr-code-analytics` | `/qr-code-tracking` | `collect QR scan data` |
| `/qr-code-analytics` | `/qr-code-for-marketing-campaigns` | `measure offline QR campaigns` |
| `/blog/trackable-qr-codes` | `/qr-code-tracking` | `QR code tracking` |
| `/blog/utm-parameter-qr-codes` | `/qr-code-analytics` | `QR code analytics` |
| `/use-cases/feedback-qr-codes` | `/use-cases/qr-codes-for-review-collection` | `review collection QR codes` |
| `/tools/google-review-qr-code` | `/use-cases/qr-codes-for-review-collection` | `review collection workflow` |
## PR Plan
1. Add the new review-collection use-case content in the existing use-case page data/routing pattern.
2. Refresh copy, FAQ, metadata, and link sections on `/qr-code-tracking` and `/qr-code-analytics`.
3. Refresh the two support blog entries without creating duplicate `/guide/...` pages.
4. Add internal links in both directions from money pages, blog support pages, and relevant use-case/tool pages.
5. Update sitemap data only if the new page is not automatically included by the existing use-case sitemap mapping.
6. Run the PR SEO review using `docs/automations/qrmaster-pr-seo-review.md`.
## Verification Checklist
- One H1 per rendered page.
- Primary keyword appears naturally in title, H1, intro, and metadata.
- Canonical URL points to the final public URL.
- Page is public in middleware and included in sitemap.
- No duplicate `/guide/...` URL is introduced without canonical strategy.
- FAQ/schema validates where used.
- Internal links resolve and use natural anchor text.
- Each section begins with a direct answer.
- Mobile tables do not overflow unreadably.
- Visual assets are compressed and include descriptive alt text.
- Robots rules continue to allow target pages and desired search/AI crawlers.
- Build/lint pass before PR.
## Social And Outreach Follow-Up
- LinkedIn post: "Most QR campaigns fail because teams only count scans. The useful question is which printed placement created action."
- X thread: "QR tracking setup in 5 steps: dynamic QR, placement naming, UTM convention, dashboard review, reprint decision."
- Short demo video: show flyer A vs flyer B scan comparison and a destination update without reprinting.
- Outreach angle for marketing newsletters: "Offline attribution checklist for QR campaigns."
- Community answer target: questions around "Can I track a static QR code?" and "How do I track QR codes in GA4?"

View File

@@ -0,0 +1,211 @@
# QRMaster SEO Sprint Machine
## Goal
Run a weekly controlled SEO sprint that chooses the right pages, creates or
updates them, adds internal links, and ships through a reviewed PR.
## Why This Exists
QRMaster should not publish random daily content. The goal is to build
commercially useful SEO clusters around dynamic QR codes, tracking, tool pages,
comparison pages, and industry workflows.
## Plugins And Skills
| Need | Use |
|---|---|
| Repository changes and PRs | GitHub plugin |
| Keyword and performance imports | Coupler, CSV exports, Google Search Console export |
| Page creation and refactors | Codex |
| SEO/content planning | `content-strategy`, `ai-seo` skills |
| Copy generation | `copywriting` skill |
| Verification | GitHub Actions, `qa` skill |
## Weekly Inputs
- Current keyword backlog:
- `marketing/programmatic-seo-top-50.md`
- `marketing/keyword-strategy-seo-plan.md`
- `seo-keywords.csv`
- Existing product positioning:
- `.agents/product-marketing-context.md`
- Performance data when available:
- Google Search Console export
- signup/conversion report
- top landing pages by traffic
- Sprint focus:
- Dynamic QR
- Tracking/analytics
- Restaurant/menu QR
- Print marketing
- Bulk QR
- Comparison/alternatives
## Scoring Model
Score each candidate from 0-100:
| Factor | Weight |
|---|---:|
| Product fit | 30 |
| Commercial intent | 25 |
| Differentiation potential | 15 |
| Cluster leverage | 10 |
| Search winability | 10 |
| Production effort | 10 |
Do not select pages only because they have volume. Prefer pages where QRMaster
can naturally sell dynamic QR, scan tracking, bulk creation, or privacy-first
analytics.
## On-Page And Agentic Search Rules
Every new or refreshed page must follow these checks before review:
- Identify the primary keyword and intent type: informational, commercial,
transactional, or navigational.
- Cover the query fan-out: list the related subtopics an AI/search system would
need to answer the query well.
- Put the primary keyword naturally near the start of the title tag and H1.
- Keep title tags under roughly 60 characters when possible.
- Keep meta descriptions concise, benefit-led, and naturally keyword-aligned.
- Use one clear H1 and a logical H2/H3 hierarchy.
- Start each section with a direct answer to the heading.
- Use short paragraphs, bullets, and comparison tables where they improve
scanning and AI extraction.
- Add descriptive internal links with natural anchor text.
- Add useful visuals, screenshots, or examples where the page needs them.
- Add schema when the page type supports it.
- Check mobile readability, CTA tap targets, and obvious speed risks.
- Verify robots/indexing assumptions for important SEO pages.
## Default Weekly Sprint
1. Select one cluster.
2. Create 3 new pages.
3. Refresh 2 existing pages with impressions, weak CTR, or position 8-20.
4. Add internal links in both directions.
5. Create one GitHub PR.
6. Run `qrmaster-pr-seo-review.md`.
7. Produce social and outreach drafts after the PR is ready.
## Page Types
### Tool Pages
Examples:
- `/tools/pdf-qr-code`
- `/tools/vcard-qr-code`
- `/tools/wifi-qr-code`
- `/tools/menu-qr-code`
- `/tools/google-review-qr-code`
Must include:
- direct tool-oriented hero
- use cases
- dynamic vs static guidance
- FAQ
- CTA into the app
- internal links to related use cases
### Industry Workflow Pages
Examples:
- `/qr-code-for/restaurants/menu-updates`
- `/qr-code-for/events/check-in`
- `/qr-code-for/real-estate/open-house-flyers`
Must include:
- specific audience pain
- example workflow
- print-risk or tracking angle
- CTA matching the industry
### Comparison Pages
Examples:
- `/compare/dynamic-vs-static-qr-codes`
- `/compare/free-vs-paid-qr-code-generator`
- `/compare/flowcode-alternative`
Must include:
- comparison table
- fair positioning
- current facts verified before publishing
- "who this is best for" section
- CTA to the best-fit QRMaster feature
## Codex Sprint Prompt
```text
Run the QRMaster SEO Sprint Machine.
Use:
- docs/automations/qrmaster-seo-sprint-machine.md
- docs/automations/qrmaster-pr-seo-review.md
- .agents/product-marketing-context.md
- marketing/programmatic-seo-top-50.md
- marketing/keyword-strategy-seo-plan.md
Sprint focus: [cluster]
Target output: 3 new SEO/tool pages, 2 page refreshes, internal links, and one PR-ready diff.
Rules:
- Follow existing code and page patterns.
- Do not invent competitor pricing or claims.
- Prioritize dynamic QR, edit-after-print, analytics, bulk, and privacy-first messaging.
- Add metadata, FAQ/schema where local patterns support it.
- Apply the on-page and agentic search rules from the automation doc.
- Keep pages specific enough to avoid thin programmatic content.
- Run build/lint or explain why not.
Return:
1. selected pages and scoring
2. target keyword, intent, and fan-out subtopics per page
3. files changed
4. internal links added
5. PR summary
6. SEO review status
7. follow-up social/outreach package
```
## Sprint Output Template
```markdown
# QRMaster SEO Sprint: [Cluster]
## Selected Work
| Type | URL | Score | Reason |
|---|---|---:|---|
## Keyword Intent And Fan-Out
| URL | Primary keyword | Intent | Fan-out subtopics |
|---|---|---|---|
## New Pages
## Updated Pages
## Internal Links
## PR Summary
## Verification
## Social/Outreach Follow-Up
```
## Success Criteria
- Each new page has clear commercial intent or cluster leverage.
- Refreshed pages have a measurable reason for the update.
- Internal links support money pages.
- PR SEO Review passes before merge.

548
linear.app/DESIGN.md Normal file
View File

@@ -0,0 +1,548 @@
---
version: alpha
name: Linear
description: "A near-black product-focused marketing canvas built around #010102 (the deepest dark surface of any tool in this collection), light gray text (#f7f8f8), and the signature Linear lavender-blue (#5e6ad2) used as the single chromatic accent. The system reads as software-craft documentation: dense, technical, and quietly luxurious. Display type is set in the Linear custom sans (SF Pro Display fallback) at 500700 with measured negative tracking. Cards live as charcoal panels (#0f1011) with hairline borders. The accent lavender appears on the brand mark, focus rings, and a few intentional CTAs — never decoratively. Page rhythm leans on product UI screenshots framed in dark panels rather than atmospheric color."
colors:
primary: "#5e6ad2"
on-primary: "#ffffff"
primary-hover: "#828fff"
primary-focus: "#5e69d1"
ink: "#f7f8f8"
ink-muted: "#d0d6e0"
ink-subtle: "#8a8f98"
ink-tertiary: "#62666d"
canvas: "#010102"
surface-1: "#0f1011"
surface-2: "#141516"
surface-3: "#18191a"
surface-4: "#191a1b"
hairline: "#23252a"
hairline-strong: "#34343a"
hairline-tertiary: "#3e3e44"
inverse-canvas: "#ffffff"
inverse-surface-1: "#f5f6f6"
inverse-surface-2: "#f6f7f7"
inverse-ink: "#000000"
brand-secure: "#7a7fad"
semantic-success: "#27a644"
semantic-overlay: "#000000"
typography:
display-xl:
fontFamily: Linear Display
fontSize: 80px
fontWeight: 600
lineHeight: 1.05
letterSpacing: -3.0px
display-lg:
fontFamily: Linear Display
fontSize: 56px
fontWeight: 600
lineHeight: 1.10
letterSpacing: -1.8px
display-md:
fontFamily: Linear Display
fontSize: 40px
fontWeight: 600
lineHeight: 1.15
letterSpacing: -1.0px
headline:
fontFamily: Linear Display
fontSize: 28px
fontWeight: 600
lineHeight: 1.20
letterSpacing: -0.6px
card-title:
fontFamily: Linear Display
fontSize: 22px
fontWeight: 500
lineHeight: 1.25
letterSpacing: -0.4px
subhead:
fontFamily: Linear Display
fontSize: 20px
fontWeight: 400
lineHeight: 1.40
letterSpacing: -0.2px
body-lg:
fontFamily: Linear Text
fontSize: 18px
fontWeight: 400
lineHeight: 1.50
letterSpacing: -0.1px
body:
fontFamily: Linear Text
fontSize: 16px
fontWeight: 400
lineHeight: 1.50
letterSpacing: -0.05px
body-sm:
fontFamily: Linear Text
fontSize: 14px
fontWeight: 400
lineHeight: 1.50
letterSpacing: 0
caption:
fontFamily: Linear Text
fontSize: 12px
fontWeight: 400
lineHeight: 1.40
letterSpacing: 0
button:
fontFamily: Linear Text
fontSize: 14px
fontWeight: 500
lineHeight: 1.20
letterSpacing: 0
eyebrow:
fontFamily: Linear Text
fontSize: 13px
fontWeight: 500
lineHeight: 1.30
letterSpacing: 0.4px
mono:
fontFamily: Linear Mono
fontSize: 13px
fontWeight: 400
lineHeight: 1.50
letterSpacing: 0
rounded:
xs: 4px
sm: 6px
md: 8px
lg: 12px
xl: 16px
xxl: 24px
pill: 9999px
full: 9999px
spacing:
xxs: 4px
xs: 8px
sm: 12px
md: 16px
lg: 24px
xl: 32px
xxl: 48px
section: 96px
components:
button-primary:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.button}"
rounded: "{rounded.md}"
padding: 8px 14px
button-primary-pressed:
backgroundColor: "{colors.primary-focus}"
textColor: "{colors.on-primary}"
typography: "{typography.button}"
rounded: "{rounded.md}"
button-primary-hover:
backgroundColor: "{colors.primary-hover}"
textColor: "{colors.on-primary}"
typography: "{typography.button}"
rounded: "{rounded.md}"
button-secondary:
backgroundColor: "{colors.surface-1}"
textColor: "{colors.ink}"
typography: "{typography.button}"
rounded: "{rounded.md}"
padding: 8px 14px
button-tertiary:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.button}"
rounded: "{rounded.md}"
padding: 8px 14px
button-inverse:
backgroundColor: "{colors.inverse-canvas}"
textColor: "{colors.inverse-ink}"
typography: "{typography.button}"
rounded: "{rounded.md}"
padding: 8px 14px
pricing-card:
backgroundColor: "{colors.surface-1}"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.lg}"
padding: 24px
pricing-card-featured:
backgroundColor: "{colors.surface-2}"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.lg}"
padding: 24px
feature-card:
backgroundColor: "{colors.surface-1}"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.lg}"
padding: 24px
product-screenshot-card:
backgroundColor: "{colors.surface-1}"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.xl}"
padding: 24px
testimonial-card:
backgroundColor: "{colors.surface-1}"
textColor: "{colors.ink}"
typography: "{typography.body-lg}"
rounded: "{rounded.lg}"
padding: 32px
customer-logo-tile:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink-subtle}"
typography: "{typography.caption}"
rounded: "{rounded.xs}"
padding: 16px
text-input:
backgroundColor: "{colors.surface-1}"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.md}"
padding: 8px 12px
text-input-focused:
backgroundColor: "{colors.surface-1}"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.md}"
padding: 8px 12px
pricing-tab-default:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink-subtle}"
typography: "{typography.button}"
rounded: "{rounded.pill}"
padding: 6px 14px
pricing-tab-selected:
backgroundColor: "{colors.surface-2}"
textColor: "{colors.ink}"
typography: "{typography.button}"
rounded: "{rounded.pill}"
padding: 6px 14px
cta-banner:
backgroundColor: "{colors.surface-1}"
textColor: "{colors.ink}"
typography: "{typography.headline}"
rounded: "{rounded.lg}"
padding: 48px
changelog-row:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.xs}"
padding: 24px 0
status-badge:
backgroundColor: "{colors.surface-2}"
textColor: "{colors.ink-muted}"
typography: "{typography.caption}"
rounded: "{rounded.pill}"
padding: 2px 8px
top-nav:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-sm}"
rounded: "{rounded.xs}"
height: 56px
footer:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink-subtle}"
typography: "{typography.caption}"
rounded: "{rounded.xs}"
padding: 64px 32px
---
## Overview
Linear's marketing canvas is the deepest dark surface in this collection — `{colors.canvas}` is #010102, essentially pure black with a faint blue tint. On top sits a four-step surface ladder (`{colors.surface-1}` through `{colors.surface-4}`) for cards, panels, and lifted tiles, with hairline borders running from `{colors.hairline}` (#23252a) up through `{colors.hairline-strong}` and `{colors.hairline-tertiary}`. Light gray text (`{colors.ink}` #f7f8f8) carries the body and headlines.
The single chromatic accent is **Linear lavender-blue** `{colors.primary}` (#5e6ad2) — used on the brand mark, focus rings, and the primary CTA button. A lighter hover state (`{colors.primary-hover}` #828fff) and a focus-tinted variant (`{colors.primary-focus}` #5e69d1) extend the same hue. Linear avoids saturated greens, oranges, reds, etc. on the marketing canvas — the only semantic color is `{colors.semantic-success}` (#27a644) for status pills and the rare success indicator.
Display type runs Linear's custom sans (with `SF Pro Display` fallback) at weight 500700 with negative letter-spacing scaling from -3.0px at 80px down to 0 at body. The body family is Linear's text cut, and a Linear Mono is reserved for code snippets in product screenshots.
The page rhythm is **dense product screenshots** — Linear's marketing leads with high-fidelity captures of the product UI (issue list, project view, dashboard) framed in `{colors.surface-1}` panels with `{rounded.xl}` 16px corners. The chrome is intentionally minimal so the app screenshots can do the heavy lifting.
**Key Characteristics:**
- **Dark-canvas marketing system** — `{colors.canvas}` (#010102) is the deepest dark in this collection.
- **Lavender-blue brand accent** (`{colors.primary}` #5e6ad2) — used scarcely on brand mark, focus, and the primary CTA.
- Four-step surface ladder (canvas → surface-1 → surface-2 → surface-3 → surface-4) carries hierarchy without shadow.
- Display tracking pulls aggressively negative (-3.0px at 80px); body holds at -0.05px.
- Cards use `{rounded.lg}` 12px corners with 1px hairline borders — never pill, rarely 16px.
- **Product UI screenshots** dominate the page. The marketing chrome is a dark frame for the app.
- No second chromatic color. No atmospheric gradients. No spotlight cards.
## Colors
> Source pages: linear.app (home), /intake, /pricing, /contact/sales, /build.
### Brand & Accent
- **Lavender-Blue** ({colors.primary}): The signature Linear accent — primary CTA, brand mark, link emphasis.
- **Lavender Hover** ({colors.primary-hover}): Lighter lavender (#828fff) — hovered state of the primary CTA.
- **Lavender Focus** ({colors.primary-focus}): Focus-ring tint (#5e69d1) — focused inputs, focused buttons.
- **Brand Secure** ({colors.brand-secure}): Muted lavender-gray (#7a7fad) — used in "Linear Security" surfaces.
### Surface
- **Canvas** ({colors.canvas}): Default page background — #010102, near-pure black with a faint blue tint.
- **Surface 1** ({colors.surface-1}): One step above canvas — feature cards, pricing cards, product screenshot panels.
- **Surface 2** ({colors.surface-2}): Two steps above — featured pricing card, hovered cards.
- **Surface 3** ({colors.surface-3}): Three steps above — line-tertiary backgrounds, sub-nav.
- **Surface 4** ({colors.surface-4}): Four steps above — bg-level-3, deepest lifted surface.
- **Hairline** ({colors.hairline}): 1px borders on cards and dividers.
- **Hairline Strong** ({colors.hairline-strong}): Stronger 1px borders — input focus rings.
- **Hairline Tertiary** ({colors.hairline-tertiary}): Tertiary borders for nested surfaces.
- **Inverse Canvas** ({colors.inverse-canvas}): Pure white — surface of the inverse pill CTA on a small set of section openers.
- **Inverse Surface 1** ({colors.inverse-surface-1}): One step above inverse canvas.
- **Inverse Surface 2** ({colors.inverse-surface-2}): Two steps above inverse canvas.
### Text
- **Ink** ({colors.ink}): All headlines and emphasized body type — light gray #f7f8f8.
- **Ink Muted** ({colors.ink-muted}): Secondary type at #d0d6e0 — meta info on hero panels.
- **Ink Subtle** ({colors.ink-subtle}): Tertiary type at #8a8f98 — deselected pricing tabs, footer columns.
- **Ink Tertiary** ({colors.ink-tertiary}): Quaternary at #62666d — disabled, footnotes.
### Semantic
- **Success Green** ({colors.semantic-success}): Status pills, success indicators. The only semantic color on marketing.
- **Overlay** ({colors.semantic-overlay}): Pure black overlay scrim for modals.
## Typography
### Font Family
- **Linear Display** — Linear's custom display sans; fallback `SF Pro Display, -apple-system, system-ui, Segoe UI, Roboto`. Carries display-xl through subhead.
- **Linear Text** — Linear's custom text sans (a slightly different cut tuned for body sizes); same fallback stack. Carries body sizes, button labels, captions.
- **Linear Mono** — Linear's custom mono; fallback `ui-monospace, SF Mono, Menlo`. Used for code snippets in product screenshots and for status / ID tokens.
The marketing surface treats Display and Text as one continuous voice; the family change is silent.
### Hierarchy
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|---|---|---|---|---|---|
| `{typography.display-xl}` | 80px | 600 | 1.05 | -3.0px | Largest hero headline |
| `{typography.display-lg}` | 56px | 600 | 1.10 | -1.8px | Section opener headlines |
| `{typography.display-md}` | 40px | 600 | 1.15 | -1.0px | Sub-section headlines |
| `{typography.headline}` | 28px | 600 | 1.20 | -0.6px | Pricing tier titles, CTA banner heading |
| `{typography.card-title}` | 22px | 500 | 1.25 | -0.4px | Feature card title |
| `{typography.subhead}` | 20px | 400 | 1.40 | -0.2px | Lead body, intro paragraphs |
| `{typography.body-lg}` | 18px | 400 | 1.50 | -0.1px | Hero subhead, lead paragraphs |
| `{typography.body}` | 16px | 400 | 1.50 | -0.05px | Default body |
| `{typography.body-sm}` | 14px | 400 | 1.50 | 0 | Card body, footer columns |
| `{typography.caption}` | 12px | 400 | 1.40 | 0 | Captions, meta, status |
| `{typography.button}` | 14px | 500 | 1.20 | 0 | All button labels |
| `{typography.eyebrow}` | 13px | 500 | 1.30 | 0.4px | Section eyebrow (slight positive tracking) |
| `{typography.mono}` | 13px | 400 | 1.50 | 0 | Linear Mono for code in product screenshots |
### Principles
- **Aggressive negative tracking on display** (-3.0px at 80px ≈ 4% of size).
- **Single voice from display to body.** Display-xl at 600 → body at 400 — same family, narrower weights.
- **Eyebrow uses positive tracking** (+0.4px) — contrast against the negative-tracked display marks the eyebrow as taxonomy.
- **Mono only in code contexts.** Linear Mono lives inside product screenshots — not on marketing chrome.
### Note on Font Substitutes
Linear's custom typeface isn't publicly distributed; the documented fallback `SF Pro Display, -apple-system, system-ui` is the recommended substitute on macOS. For cross-platform implementation, **Inter** at weight 500 / 600 / 700 is the closest free substitute. **Geist Sans** is also viable. For mono, **JetBrains Mono** or **Geist Mono** at weight 400 closely approximates Linear Mono.
## Layout
### Spacing System
- **Base unit**: 4px.
- **Tokens (front matter)**: `{spacing.xxs}` 4px · `{spacing.xs}` 8px · `{spacing.sm}` 12px · `{spacing.md}` 16px · `{spacing.lg}` 24px · `{spacing.xl}` 32px · `{spacing.xxl}` 48px · `{spacing.section}` 96px.
- Card interior padding: `{spacing.lg}` 24px on feature/pricing cards; `{spacing.xl}` 32px on testimonial cards; `{spacing.xxl}` 48px on CTA banners.
- Pill button padding: 8px vertical · 14px horizontal — Linear's compact button spec.
- Form input padding: 8px vertical · 12px horizontal.
### Grid & Container
- Max content width sits around 1280px.
- Card grids are 3-up at desktop, 2-up at tablet, 1-up at mobile.
- Pricing tier grid is 3-up; comparison strip below shows checkmarks per tier.
- Product screenshot panels span full content width — they're the protagonist.
### Whitespace Philosophy
The dark canvas IS the whitespace. Sections separate by lift onto surface-1 panels, not by gaps in white. Within a panel, generous `{spacing.lg}` 24px gaps between content blocks; `{spacing.section}` 96px between sections.
## Elevation & Depth
| Level | Treatment | Use |
|---|---|---|
| 0 (flat) | No shadow, no border | Default for body type, hero text, footer |
| 1 (charcoal lift) | `{colors.surface-1}` background on canvas, 1px `{colors.hairline}` | Default cards, product panels |
| 2 (surface-2 lift) | `{colors.surface-2}` background, 1px `{colors.hairline-strong}` | Featured pricing card, hovered cards |
| 3 (surface-3 lift) | `{colors.surface-3}` background | Sub-nav, dropdown menus |
| 4 (focus ring) | 2px `{colors.primary-focus}` outline at 50% opacity | Focused input, focused button |
Linear's depth is carried by surface ladder + hairline borders. The brand resists drop shadows on dark almost entirely.
### Decorative Depth
- **Product UI screenshots** dominate as decorative depth.
- **No atmospheric gradients, no spotlight cards.**
- **Subtle white edge highlight** on the top edge of lifted panels — gives the dark surface a faint "pixel rendered" feel.
## Shapes
### Border Radius Scale
| Token | Value | Use |
|---|---|---|
| `{rounded.xs}` | 4px | Small chips, status badges |
| `{rounded.sm}` | 6px | Inline tags |
| `{rounded.md}` | 8px | All buttons, form inputs |
| `{rounded.lg}` | 12px | Pricing cards, feature cards, testimonial cards |
| `{rounded.xl}` | 16px | Product screenshot panels |
| `{rounded.xxl}` | 24px | Oversized CTA banners (rare) |
| `{rounded.pill}` | 9999px | Pricing tab toggles, status pills |
| `{rounded.full}` | 9999px | Avatar circles |
### Photography & Illustration Geometry
- Product UI screenshots dominate; they sit in `{rounded.xl}` 16px tiles with `{spacing.lg}` 24px outer padding.
- Customer logo tiles render at small sizes (~24px logo height) on `{colors.canvas}` with no border.
- Avatar circles in testimonial cards use `{rounded.full}` at 3240px sizes.
## Components
### Buttons
**`button-primary`** — Lavender CTA. The default primary CTA across all pages.
- Background `{colors.primary}`, text `{colors.on-primary}`, type `{typography.button}`, padding 8px 14px, rounded `{rounded.md}`.
- Pressed state lives in `button-primary-pressed` (background shifts to `{colors.primary-focus}`).
- Hover state lives in `button-primary-hover` (background shifts to `{colors.primary-hover}` lighter lavender).
**`button-secondary`** — Charcoal button. Used for secondary CTAs ("Sign in", "Read changelog").
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.button}`, padding 8px 14px, rounded `{rounded.md}`. 1px `{colors.hairline}` border.
**`button-tertiary`** — Plain text button.
- Background `{colors.canvas}`, text `{colors.ink}`, type `{typography.button}`, rounded `{rounded.md}`, padding 8px 14px.
**`button-inverse`** — White-on-dark inverse CTA.
- Background `{colors.inverse-canvas}`, text `{colors.inverse-ink}`, type `{typography.button}`, rounded `{rounded.md}`, padding 8px 14px.
### Pricing Tabs
**`pricing-tab-default`** + **`pricing-tab-selected`** — Pill-toggle on `/pricing`.
- Default: `{colors.canvas}` background, `{colors.ink-subtle}` text, rounded `{rounded.pill}`, padding 6px 14px.
- Selected: `{colors.surface-2}` background, `{colors.ink}` text — selected = surface lift.
### Cards & Containers
**`pricing-card`** — Each tier on `/pricing`.
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.body}`, rounded `{rounded.lg}`, padding 24px. 1px `{colors.hairline}` border.
**`pricing-card-featured`** — Recommended tier — surface lift to surface-2.
- Background `{colors.surface-2}`, otherwise identical structure.
**`feature-card`** — Generic feature highlight tile.
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.body}`, rounded `{rounded.lg}`, padding 24px.
**`product-screenshot-card`** — The dominant card type — frames a high-fidelity Linear app UI screenshot.
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.body}`, rounded `{rounded.xl}`, padding 24px.
**`testimonial-card`** — Customer quote with avatar + name + role.
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.body-lg}`, rounded `{rounded.lg}`, padding 32px.
**`customer-logo-tile`** — Small tile in the customer marquee.
- Background `{colors.canvas}`, text `{colors.ink-subtle}`, type `{typography.caption}`, rounded `{rounded.xs}`, padding 16px.
**`cta-banner`** — Closing CTA panel near page bottom.
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.headline}`, rounded `{rounded.lg}`, padding 48px.
### Inputs & Forms
**`text-input`** + **`text-input-focused`** — Form fields on `/contact/sales` and signup overlays.
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.body}`, rounded `{rounded.md}`, padding 8px 12px.
- Focused state retains the same surface; the focus ring is a 2px `{colors.primary-focus}` outline at 50% opacity.
### Status & Build Page
**`changelog-row`** — Each row in `/build` (changelog page) listing version, date, and changes.
- Background `{colors.canvas}`, text `{colors.ink}`, type `{typography.body}`, rounded `{rounded.xs}`, padding 24px 0. 1px `{colors.hairline}` bottom rule.
**`status-badge`** — Small status pill.
- Background `{colors.surface-2}`, text `{colors.ink-muted}`, type `{typography.caption}`, rounded `{rounded.pill}`, padding 2px 8px.
### Navigation
**`top-nav`** — Sticky dark bar with the Linear wordmark left, primary nav links centered, and a `button-secondary` ("Sign in") + `button-primary` ("Get started") pair right.
- Background `{colors.canvas}`, text `{colors.ink}`, type `{typography.body-sm}`, height 56px.
### Footer
**`footer`** — Dense link grid on `{colors.canvas}` with the Linear wordmark left.
- Background `{colors.canvas}`, text `{colors.ink-subtle}`, type `{typography.caption}`, padding 64px 32px.
## Do's and Don'ts
### Do
- Reserve `{colors.canvas}` (#010102) as the system's anchor surface — the faint blue tint is intentional.
- Use `{colors.primary}` lavender ONLY for: brand mark, primary CTA, focus ring, link emphasis.
- Use the four-step surface ladder for hierarchy. Avoid skipping levels.
- Pair display weight 600 with body weight 400 — Linear resists 700+ display weights.
- Apply negative letter-spacing aggressively on display.
- Use product UI screenshots as the protagonist of every section.
- Compose CTAs as `{rounded.md}` 8px corners.
### Don't
- Don't ship a light-mode marketing page.
- Don't use lavender as a section background or card fill.
- Don't introduce a second chromatic accent (orange, pink, green for marketing).
- Don't add atmospheric gradients or spotlight cards.
- Don't pill-round CTAs.
- Don't use `#000000` true black as the canvas.
- Don't combine multiple bright accents in product screenshot mockups.
## Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|---|---|---|
| Desktop-XL | 1440px | Default desktop layout |
| Desktop | 1280px | Card grid 3-up maintained |
| Tablet | 1024px | Card grid 3-up → 2-up |
| Mobile-Lg | 768px | Pricing comparison becomes accordion; nav hamburger |
| Mobile | 480px | Single-column; display-xl scales 80px → ~36px |
### Touch Targets
- CTAs hold ≥40px tap height across viewports.
- Pricing tab pills hold ≥36px tap height; touch viewports grow to ≥44px.
- Form inputs hold ≥44px tap target on touch.
### Collapsing Strategy
- **Top nav**: links collapse to hamburger below 768px.
- **Card grids**: 3-up → 2-up at 1024px → 1-up below 768px.
- **Pricing comparison**: per-tier accordion below 768px.
- **Display type**: `{typography.display-xl}` 80px scales toward `{typography.display-md}` 40px on mobile.
### Image Behavior
- Product UI screenshots maintain aspect ratio and never crop.
- Customer logos in the marquee may collapse from 6-up to 3-up below 768px.
## Iteration Guide
1. Focus on ONE component at a time and reference it by its `components:` token name.
2. When introducing a section, decide first which surface lift it lives on.
3. Default body to `{typography.body}` at weight 400.
4. Run `npx @google/design.md lint DESIGN.md` after edits.
5. Add new variants as separate component entries.
6. Treat lavender as scarce: brand mark, primary CTA, focus, link emphasis.
7. Lead every section with a product UI screenshot.
## Known Gaps
- The four-step surface ladder values are extracted directly from Linear's `--color-bg-level-3`, `--color-line-tint`, etc. CSS variables; they are Linear's canonical surface spec.
- Form-field error and validation styling is not visible on the inspected pages.
- Light mode is not documented because the marketing site does not ship a light theme.
- Linear's actual product UI uses a richer color-tag palette (red, orange, yellow, green, blue, purple) for issue priorities and project labels — those colors live in the in-product surfaces shown in mockups.
- The custom display, text, and mono families are proprietary; an open-source substitute is acceptable.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@@ -0,0 +1,145 @@
# QR Master Sales Deck
Audience: Marketing Manager
Use case: AE-led first demo / sales presentation
Stage: Discovery to first solution presentation
Format: 11-slide outline with presentation notes
## Slide 1: Print Campaigns Should Not Go Dark After They Ship
Body copy:
- Printed flyers, packaging, menus, and posters still drive action
- Static QR codes break the moment a link, offer, or landing page changes
- Marketing teams lose both flexibility and attribution
Speaker notes:
Lead with the operational reality: print is still valuable, but static QR codes make it fragile. Position the problem as a marketing control issue, not just a design or ops issue.
## Slide 2: The Cost of Static QR Codes Is Bigger Than Reprints
Body copy:
- Reprints create direct waste every time a destination changes
- Campaign teams lose scan-level visibility into offline performance
- Manual updates slow launches and create avoidable errors
Speaker notes:
Use the ROI angle here. QR Master already frames this as reprint waste plus lost measurement. If relevant, quantify with the prospect's own print budget and update frequency.
## Slide 3: Marketing Teams Need Trackable Offline-to-Online Journeys
Body copy:
- Offline campaigns now need the same measurement discipline as digital
- Teams want scan, device, and location insights without adding complexity
- Privacy expectations are higher, especially in Europe
Speaker notes:
This is the urgency slide. The shift is not "QR codes are new" but "QR codes now need to behave like measurable campaign infrastructure."
## Slide 4: QR Master Makes Printed Assets Editable, Trackable, and Privacy-First
Body copy:
- Change QR destinations after printing with dynamic QR codes
- Track scans with analytics designed for marketing use cases
- Stay privacy-conscious with hashed IP handling and no PII-based tracking model
Speaker notes:
Keep this simple. The core promise is control after print, measurable outcomes, and lower compliance anxiety.
## Slide 5: Launch Campaign QR Codes Fast
Body copy:
- Create dynamic or static QR codes in minutes
- Use specialized generators for URL, WiFi, menus, vCards, events, and more
- Download ready-to-use assets for print and packaging workflows
Speaker notes:
Show speed to launch. This matters for marketers running many campaigns with changing assets and deadlines.
## Slide 6: Update Destinations Without Reprinting
Body copy:
- Swap landing pages, PDFs, menus, or promotions after distribution
- Keep the same printed QR code live while the destination evolves
- Reduce wasted inventory, signage, and packaging runs
Speaker notes:
This is the core economic benefit. Tie it to seasonal campaigns, corrected links, changing offers, and localized landing pages.
## Slide 7: Measure What Offline Campaigns Actually Drive
Body copy:
- See scan activity, devices, and location patterns
- Understand which printed assets and campaigns create engagement
- Give marketing a better feedback loop for offline spend
Speaker notes:
Frame analytics as decision support. The point is not dashboards for their own sake; it is knowing what to scale, fix, or stop.
## Slide 8: Scale Beyond One-Off QR Campaigns
Body copy:
- Business plan supports bulk QR creation up to 1,000 rows per upload
- Generate large batches for packaging, retail, events, and distributed campaigns
- Move from ad hoc QR creation to repeatable campaign operations
Speaker notes:
Use this slide when the buyer has many SKUs, locations, or campaigns. For smaller teams, keep it brief and treat it as future-proofing.
## Slide 9: Why Teams Choose QR Master
Body copy:
- Privacy-first approach with hashed IPs and Do Not Track respect
- Bulk creation and advanced analytics in one platform
- More focused than generic design tools, simpler and more cost-conscious than enterprise-heavy alternatives
Speaker notes:
This is where you position against free tools, Canva-style utilities, and more expensive enterprise QR platforms. Stay outcome-focused rather than feature-dense.
## Slide 10: Value and Packaging
Body copy:
- Free: 3 active dynamic QR codes and unlimited static QR codes
- Pro: EUR 9/month or EUR 90/year for 50 dynamic QR codes, advanced analytics, and branding
- Business: EUR 29/month or EUR 290/year for 500 dynamic QR codes, bulk creation, and priority support
- Enterprise: custom for larger rollouts
Speaker notes:
Anchor pricing against reprint waste and attribution value, not against free QR generators. For many prospects, one avoided reprint can justify the upgrade.
## Slide 11: Next Step
Body copy:
- Start with one live campaign, menu, or packaging workflow
- Validate savings, scan visibility, and campaign agility
- Expand to broader printed assets once the first workflow is proven
Speaker notes:
Push toward a concrete next step: free signup, guided walkthrough, or a pilot tied to one real campaign. Avoid vague closes.
## Optional Proof Slide: Replace With Customer Evidence
Use this only when you have real proof.
Suggested content:
- Named customer logo
- Before / after workflow
- One quantified result
- One short buyer quote
Current status:
- Replace composite examples with a real customer story before broad sales use
- Good first targets: restaurant groups, agencies, event operators, or retail packaging teams
## Customization Notes For AEs
- For restaurant buyers, emphasize menu changes and reprint savings earlier.
- For agency buyers, emphasize campaign measurement and client reporting.
- For operations or IT stakeholders, elevate privacy posture and workflow control.
- If the buyer is price-sensitive, open the reprint calculator before the pricing slide.
## Missing Proof To Add Later
- Named customer logos
- Verified customer quote
- Measured ROI or payback period from a live account
- Competitive win story versus Beaconstac, Flowcode, or generic free tools

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,521 @@
# TikTok Top 5 Slideshow Pack for QR Master
This file contains 5 complete `Top 5` TikTok slideshow prompt packs for QR Master.
Why 7 slides here:
- Slide 1 = hook
- Slides 2-6 = the five list items
- Slide 7 = CTA
These prompts are aligned to `C:\Users\a931627\Downloads\DESIGN (3).md` and keep the same premium visual direction as the main slideshow file:
- bright precision-editorial SaaS style
- high white space
- tonal depth instead of hard borders
- glassmorphism and soft blue gradients
- QR code treated as a hero object
## Shared Global Prompt
Use this global prompt before any of the packs below:
```text
Create a premium TikTok slideshow in vertical 9:16 format with built-in text overlay on every slide.
Style: bright, clean, modern, minimal editorial SaaS aesthetic.
Creative direction: Precision Editorial, high white space, intentional asymmetry, tonal depth, calm authority, premium digital craftsmanship.
Color palette: white, soft warm light gray, cobalt blue accents, subtle blue gradients, soft ambient navy-tinted shadows.
Lighting: bright studio daylight, soft glow, polished, airy.
Typography: bold modern sans-serif similar to Inter, large headline, very short text, clean spacing, high contrast, strong hierarchy.
Layout: editorial composition, asymmetrical but balanced, lots of negative space, QR code treated like a hero object.
Text placement: keep the overlay text centered or at minimum positioned within the lower two-thirds of the frame; avoid placing the main text too high near the top edge.
UI style: floating frosted panels, surface shifts instead of hard lines, soft glassmorphism, premium SaaS ad direction.
Important:
- Use exact overlay text provided for each slide.
- Keep text short and correctly spelled.
- Keep the text centered or clearly inside the lower two-thirds of the image.
- No paragraph text.
- No clutter.
- No dark background.
- No generic stock-office look.
- No 1px hard borders.
- Use soft tonal transitions, ghost borders only if necessary.
- Slides 1 and 7 use the avatar.
- Slides 1-6 must not show the QR Master brand name or logo.
- Slide 7 is the only slide allowed to show QR Master branding.
- On slide 1, use the avatar as an unbranded character reference only, with no visible logo text on hat or hoodie.
- On slide 7, show the full branded avatar clearly.
- Keep every slide polished, premium, and TikTok-ready.
```
## Shared Negative Prompt
```text
No dark theme, no messy backgrounds, no harsh shadows, no cheap cartoon look, no toy aesthetic, no cluttered desks, no random props, no heavy borders, no low-detail UI, no gibberish text blocks, no typo-filled posters, no loud neon colors, no generic corporate office scenes
```
## Testing Note
Because these are listicle formats, do not compare them directly against strong pain hooks in the same wave.
Use them as a separate educational test block.
Suggested testing order:
1. Top 5 QR code mistakes businesses still make
2. Top 5 things to check before printing a QR code
3. Top 5 ways dynamic QR codes save money
4. Top 5 QR code use cases for restaurants
5. Top 5 reasons static QR codes stop working
---
## Top 5 Slideshow 01 - Educational
**Hook:** `Top 5 QR code mistakes businesses still make`
**Title:** `Top 5 QR Mistakes`
**Caption + Hashtags:** `Most businesses still make the same QR mistakes again and again. Save this checklist before your next print run. qrmaster.net #QRCode #DynamicQRCode #MarketingTips #SmallBusiness #PrintMarketing #QRMaster`
**Long Description:** `Most businesses still make the same QR code mistakes without even noticing. In this slideshow, I break down 5 common QR mistakes that lead to broken customer journeys, wasted reprints, weak campaign tracking, and messy workflows. If you use QR codes for menus, flyers, packaging, events, or offline marketing, save this and use it as a checklist before your next print run. More smart QR workflows at qrmaster.net #QRCode #DynamicQRCode #MarketingTips #SmallBusiness #PrintMarketing #QRMaster`
### Slide 1
```text
Create a premium hook slide in vertical 9:16.
Scene: bright white editorial background, unbranded avatar holding a phone and pointing toward a floating QR card, soft cobalt glow, clean asymmetrical composition, premium SaaS feel.
Add large clean overlay text:
"Top 5 QR code mistakes businesses still make"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 2
```text
Create a premium list slide in vertical 9:16.
Scene: elegant printed menu and flyer with one static QR code shown as a fixed object, bright white background, soft tonal layering, minimal premium styling.
Add large clean overlay text:
"1. Using static codes for changing content"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 3
```text
Create a premium list slide in vertical 9:16.
Scene: clean close-up of printed QR collateral beside a phone with an outdated destination, white editorial environment, soft ambient shadow.
Add large clean overlay text:
"2. Printing before testing the destination"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 4
```text
Create a premium list slide in vertical 9:16.
Scene: one QR card with no surrounding analytics or performance cues, bright white premium background, minimalist business composition.
Add large clean overlay text:
"3. Tracking nothing after the scan"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 5
```text
Create a premium list slide in vertical 9:16.
Scene: multiple mismatched printed QR assets shown across menu, flyer, and package objects, white background, soft blue reflections, editorial spacing.
Add large clean overlay text:
"4. Using a different code for every update"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 6
```text
Create a premium list slide in vertical 9:16.
Scene: printed QR assets arranged in a clean product-shot layout without a central management view, bright white environment, polished SaaS marketing style.
Add large clean overlay text:
"5. Managing QR codes without one dashboard"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 7
```text
Create a final branded CTA slide in vertical 9:16.
Scene: full QR Master avatar based on the provided image, branded hat and hoodie allowed here, holding phone with QR code, bright premium white background, soft cobalt blue gradient lighting, spacious CTA composition.
Add large clean overlay text:
"Fix all 5 with QR Master"
Add smaller text below:
"qrmaster.net"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
---
## Top 5 Slideshow 02 - Educational
**Hook:** `Top 5 things to check before printing a QR code`
**Title:** `Top 5 Pre-Print Checks`
**Caption + Hashtags:** `Before you print a QR code, check these five things first. Save this for your next menu, flyer, or campaign. qrmaster.net #QRCode #DynamicQRCode #Checklist #PrintMarketing #MarketingTips #QRMaster`
**Long Description:** `Before you print any QR code, there are a few things you should always check first. This slideshow covers 5 simple pre-print checks that can save you from broken links, useless scans, missing analytics, and expensive reprints later. If you use QR codes on menus, flyers, product packaging, posters, or event materials, save this checklist now and come back to it before your next campaign goes live. qrmaster.net #QRCode #DynamicQRCode #Checklist #PrintMarketing #MarketingTips #QRMaster`
### Slide 1
```text
Create a premium hook slide in vertical 9:16.
Scene: bright white editorial background, unbranded avatar gesturing toward a floating QR card and subtle check icons, soft blue glow, clean premium composition.
Add large clean overlay text:
"Top 5 things to check before printing a QR code"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 2
```text
Create a premium checklist slide in vertical 9:16.
Scene: one clean QR card displayed like a gallery object on a bright white set, soft ambient shadow, minimal SaaS ad styling.
Add large clean overlay text:
"1. Will the destination change later?"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 3
```text
Create a premium checklist slide in vertical 9:16.
Scene: printed QR collateral beside a mobile screen with clear scan destination, bright white environment, soft blue reflected light.
Add large clean overlay text:
"2. Did you test the scan on real devices?"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 4
```text
Create a premium checklist slide in vertical 9:16.
Scene: central QR code card with subtle analytics cards nearby, white premium background, glassy UI accents, editorial spacing.
Add large clean overlay text:
"3. Do you need analytics after launch?"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 5
```text
Create a premium checklist slide in vertical 9:16.
Scene: one QR code shown across menu, flyer, and packaging objects in a bright white modular arrangement, soft tonal depth.
Add large clean overlay text:
"4. Can one QR code cover multiple updates?"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 6
```text
Create a premium checklist slide in vertical 9:16.
Scene: clean dashboard-like scene with organized QR assets and calm blue glassmorphism panels, bright white premium environment.
Add large clean overlay text:
"5. Can you manage it all in one place?"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 7
```text
Create a final branded CTA slide in vertical 9:16.
Scene: branded QR Master avatar holding phone with QR code, bright premium white background, soft cobalt blue halo, spacious CTA layout.
Add large clean overlay text:
"Use QR Master before you print"
Add smaller text below:
"qrmaster.net"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
---
## Top 5 Slideshow 03 - Educational
**Hook:** `Top 5 ways dynamic QR codes save money`
**Title:** `Top 5 Ways QR Saves Money`
**Caption + Hashtags:** `Dynamic QR codes are not just more flexible. They can also cut avoidable print and campaign costs. qrmaster.net #QRCode #DynamicQRCode #MarketingCosts #BusinessTips #PrintMarketing #QRMaster`
**Long Description:** `Dynamic QR codes are not only more flexible than static QR codes, they can also save real money. In this slideshow, I break down 5 ways editable and trackable QR workflows reduce waste, avoid unnecessary reprints, improve campaign decisions, and make offline marketing more efficient. If you want better results from menus, flyers, packaging, events, or local campaigns, this is worth saving. qrmaster.net #QRCode #DynamicQRCode #MarketingCosts #BusinessTips #PrintMarketing #QRMaster`
### Slide 1
```text
Create a premium hook slide in vertical 9:16.
Scene: bright white editorial background, unbranded avatar beside a floating QR card with soft cobalt glow and subtle financial dashboard cues, premium SaaS ad style.
Add large clean overlay text:
"Top 5 ways dynamic QR codes save money"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 2
```text
Create a premium list slide in vertical 9:16.
Scene: elegant stack of printed collateral shown in a bright white studio setup, soft ambient shadow, clean product-shot composition.
Add large clean overlay text:
"1. Fewer reprints after small updates"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 3
```text
Create a premium list slide in vertical 9:16.
Scene: one QR card connected to multiple destination states, bright white background, blue-lit glassmorphism, premium editorial spacing.
Add large clean overlay text:
"2. One code works across multiple campaigns"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 4
```text
Create a premium list slide in vertical 9:16.
Scene: elegant analytics cards around a central QR code, white background, soft cobalt glow, refined SaaS dashboard look.
Add large clean overlay text:
"3. You can cut weak campaigns faster"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 5
```text
Create a premium list slide in vertical 9:16.
Scene: clean modular arrangement of menu, flyer, package, and event objects around one QR system, bright white environment, subtle tonal depth.
Add large clean overlay text:
"4. One workflow replaces scattered tools"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 6
```text
Create a premium list slide in vertical 9:16.
Scene: one elegant QR dashboard environment with organized assets and analytics cues, white premium background, soft blue reflections.
Add large clean overlay text:
"5. Better scan data means better decisions"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 7
```text
Create a final branded CTA slide in vertical 9:16.
Scene: full branded QR Master avatar holding phone with QR code, bright premium white background, soft cobalt gradient halo, spacious CTA layout.
Add large clean overlay text:
"Save more with QR Master"
Add smaller text below:
"qrmaster.net"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
---
## Top 5 Slideshow 04 - Educational
**Hook:** `Top 5 QR code use cases for restaurants`
**Title:** `Top 5 Restaurant QR Use Cases`
**Caption + Hashtags:** `Restaurants can do much more with QR codes than just digital menus. Save this if you run food, hospitality, or local campaigns. qrmaster.net #RestaurantMarketing #QRCode #DynamicQRCode #Hospitality #RestaurantTips #QRMaster`
**Long Description:** `Most restaurants only think about QR codes as digital menus, but there are far more useful ways to use them. This slideshow breaks down 5 restaurant QR use cases that can improve guest experience, simplify operations, and support marketing at the same time. If you run a restaurant, cafe, takeaway, or hospitality brand, save this and use it as inspiration for smarter QR workflows. qrmaster.net #RestaurantMarketing #QRCode #DynamicQRCode #Hospitality #RestaurantTips #QRMaster`
### Slide 1
```text
Create a premium niche hook slide in vertical 9:16.
Scene: bright white editorial background, unbranded avatar beside a floating menu-style QR card, soft blue glow, calm restaurant-tech SaaS aesthetic.
Add large clean overlay text:
"Top 5 QR code use cases for restaurants"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 2
```text
Create a premium list slide in vertical 9:16.
Scene: elegant table menu QR setup on a bright white studio set, soft shadows, clean premium composition.
Add large clean overlay text:
"1. Live digital menus"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 3
```text
Create a premium list slide in vertical 9:16.
Scene: refined table card with WiFi-style QR presentation, white editorial background, subtle blue reflections, minimal hospitality-tech look.
Add large clean overlay text:
"2. Guest WiFi access"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 4
```text
Create a premium list slide in vertical 9:16.
Scene: elegant feedback or review QR card in a bright white premium environment, soft ambient shadow, editorial spacing.
Add large clean overlay text:
"3. Reviews and feedback collection"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 5
```text
Create a premium list slide in vertical 9:16.
Scene: promotional table card and campaign flyer with QR code in a bright modular setup, white background, soft cobalt glow.
Add large clean overlay text:
"4. Seasonal offers and campaigns"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 6
```text
Create a premium list slide in vertical 9:16.
Scene: loyalty or contact card concept built around one central QR code, bright white premium background, glassy UI touches, polished SaaS look.
Add large clean overlay text:
"5. Loyalty and contactless customer journeys"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 7
```text
Create a final branded CTA slide in vertical 9:16.
Scene: branded QR Master avatar with phone and QR code, bright premium white background, soft cobalt blue halo, strong restaurant-tech campaign composition.
Add large clean overlay text:
"Restaurants can start with QR Master"
Add smaller text below:
"qrmaster.net"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
---
## Top 5 Slideshow 05 - Educational
**Hook:** `Top 5 reasons static QR codes stop working`
**Title:** `Top 5 Reasons Static QR Codes Fail`
**Caption + Hashtags:** `Static QR codes usually fail because the destination changes, not because the code itself is broken. Save this for your next campaign. qrmaster.net #QRCode #DynamicQRCode #MarketingMistakes #PrintMarketing #BusinessTips #QRMaster`
**Long Description:** `Static QR codes rarely fail because the code itself stops scanning. They usually fail because the business context changes around them. In this slideshow, I show 5 reasons static QR codes stop working over time, from changed links to expired offers to poor tracking and bad scale management. If you use QR codes in print, offline campaigns, hospitality, retail, or events, save this before your next launch. qrmaster.net #QRCode #DynamicQRCode #MarketingMistakes #PrintMarketing #BusinessTips #QRMaster`
### Slide 1
```text
Create a premium hook slide in vertical 9:16.
Scene: bright white editorial background, unbranded avatar pointing toward a floating QR card and subtle warning UI indicators, soft cobalt glow, premium SaaS ad style.
Add large clean overlay text:
"Top 5 reasons static QR codes stop working"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 2
```text
Create a premium list slide in vertical 9:16.
Scene: printed QR card beside a changed mobile destination, bright white editorial environment, clean asymmetrical composition.
Add large clean overlay text:
"1. The destination link changes"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 3
```text
Create a premium list slide in vertical 9:16.
Scene: elegant flyer or menu objects shown in a bright white premium set, soft tonal depth, subtle mismatch with a new campaign state.
Add large clean overlay text:
"2. The offer or content expires"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 4
```text
Create a premium list slide in vertical 9:16.
Scene: one QR code shown across multiple print assets with no update path, bright white background, soft cobalt reflections, polished editorial look.
Add large clean overlay text:
"3. One print run outlives one campaign"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 5
```text
Create a premium list slide in vertical 9:16.
Scene: clean analytics-free QR setup on a bright white SaaS-inspired background, minimalist business composition.
Add large clean overlay text:
"4. There is no data after the scan"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 6
```text
Create a premium list slide in vertical 9:16.
Scene: printed materials and QR objects arranged without a central management environment, white premium background, soft ambient shadow.
Add large clean overlay text:
"5. They are too hard to manage at scale"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```
### Slide 7
```text
Create a final branded CTA slide in vertical 9:16.
Scene: full branded QR Master avatar holding phone with QR code, bright premium white background, soft cobalt blue gradient lighting, spacious CTA composition.
Add large clean overlay text:
"Use QR Master instead"
Add smaller text below:
"qrmaster.net"
Text style: bold, modern, minimal, high contrast, clean editorial layout.
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
```

View File

@@ -0,0 +1,701 @@
# Meta Ads Competitor Analysis Memory
Stand: 2026-05-27
Projekt: qrmaster.net
Zweck: Diese Datei speichert wiederverwendbare Learnings aus Meta-Ads-Analysen der wichtigsten QRMaster-Competitors. Bei neuen Ad-Konzepten fuer qrmaster.net diese Datei zuerst beruecksichtigen.
## Strategischer Gesamtblick
Die wichtigsten Competitors in Meta Ads positionieren sich nicht nur als QR-Code-Generatoren, sondern als Tools fuer Branding, Tracking, Analytics, Kampagnensteuerung und Revenue-Wachstum.
QRMaster sollte sich gegen diese Wettbewerber vor allem ueber folgende Winkel differenzieren:
- Einfachheit: QR-Code in Sekunden erstellen, ohne komplexes Setup.
- Schnelligkeit: "Create in 10/30 seconds" als klarer Hook.
- Kostenloser Einstieg: "Free", "Try free", "No credit card" prominent nutzen.
- SMB-Fokus: kleine Unternehmen, Restaurants, lokale Anbieter, Events, Retail, Hotels.
- Tracking ohne Komplexitaet: Scans, Kundeninteresse und Kampagnenleistung einfach sichtbar machen.
- Branded QR Codes: keine generischen schwarzen Quadrate, sondern Logo, Farben und Style.
- Retargeting/Win-back: Besucher und ehemalige Nutzer gezielt zurueckholen.
## Top Competitors
Analysierte Competitors:
1. Scanova
2. QR TIGER
3. Bitly
4. Uniqode
## Scanova Learnings
Scanova nutzt wenige, aber klare Meta Ads mit mehreren Messaging-Winkeln.
Was funktioniert:
- Objection Handling: "Still not convinced you need a QR Code?"
- Design-Angle: "Tired of boring QR codes?"
- Business Outcome: repeat purchases, social shares, brand moments.
- Problem/Solution: wenig Platz auf Labels, aber viele Informationen.
- Freemium-Risikoabbau: "free to try".
Beste nachzubauende QRMaster-Ads:
### Scanova Ad 1: Objection Handling
Hook:
"Still not convinced you need a QR code?"
QRMaster-Version:
"Noch nicht sicher, ob du einen QR-Code brauchst? Fair. Du brauchst nur einen, wenn du mehr Kundeninteraktion, Echtzeit-Tracking, volle Designkontrolle und aenderbare Links nach dem Drucken willst. QRMaster ist kostenlos testbar."
CTA:
"Kostenlos testen" oder "Learn More"
Warum relevant:
Direkte Einwaende werden akzeptiert statt wegargumentiert. Das eignet sich gut fuer Retargeting und warme Zielgruppen.
### Scanova Ad 2: Design/Emotion
Hook:
"Tired of boring QR codes?"
QRMaster-Version:
"Genug von langweiligen QR-Codes? Mit QRMaster erstellst du schoene, gebrandete QR-Codes mit Logo, Farben und Tracking. Kostenlos starten."
CTA:
"Create Free" oder "Jetzt erstellen"
Warum relevant:
Sehr leicht visuell zu zeigen: generischer QR-Code vs. gebrandeter QRMaster-Code.
### Scanova Ad 3: Label/Packaging Pain
Hook:
"Only 3cm of label space left?"
QRMaster-Version:
"Nur wenig Platz auf Verpackung, Flyer oder Speisekarte? Ein QRMaster-Code bringt deine ganze Story auf eine kleine Flaeche und zeigt dir, wer scannt."
CTA:
"QR-Code erstellen"
Warum relevant:
Sehr konkreter Pain Point fuer Retail, Packaging, Gastronomie und Print.
## QR TIGER Learnings
QR TIGER nutzt langlebige Kampagnen, Video-Creatives und klare Feature-Angles. Einige Ads laufen seit vielen Monaten, was auf Performance hindeutet.
Was funktioniert:
- Konkreter Benefit: "80% more scans" statt vager Claims.
- Custom QR Codes mit Branding.
- Analytics als Kampagnenoptimierung.
- Multi-URL/advanced Features fuer B2B-Marketer.
- Zukunfts-/Compliance-Angle wie GS1 QR Codes.
- 15-45 Sekunden Videos mit schneller Produktdemo.
Beste nachzubauende QRMaster-Ads:
### QR TIGER Ad 1: Custom QR Codes mit Branding
Hook:
"Get 80% more scans with custom QR codes."
QRMaster-Version:
"Erstelle gebrandete QR-Codes in 30 Sekunden. Fuege Logo und Farben hinzu, teile deinen Code und verfolge alle Scans in Echtzeit."
Video-Struktur:
- 0-3s: "QR-Code in 30 Sekunden"
- 3-15s: Logo/Farbe/Design zeigen
- 15-30s: Scan mit Smartphone
- 30-40s: Analytics Dashboard zeigen
- 40-45s: CTA
CTA:
"Kostenlos erstellen" oder "Sign Up"
### QR TIGER Ad 2: Multi-URL / Smart QR
Hook:
"What if your QR code link could change depending on location, language or time?"
QRMaster-Version:
"Was, wenn dein QR-Code smarter arbeitet? Leite Nutzer je nach Kampagne, Sprache oder Geraet weiter und sieh alle Scans in einem Dashboard."
CTA:
"Mehr erfahren"
Nutzen:
Gut fuer B2B/Marketer und fortgeschrittenere Use Cases.
### QR TIGER Ad 3: Future/Urgency
Hook:
"By 2027, barcodes will be replaced with GS1-powered QR codes."
QRMaster-Version:
"QR-Codes werden zum Standard fuer moderne Verpackungen, Menues und Kampagnen. Starte jetzt mit QRMaster, bevor deine Konkurrenz schneller ist."
CTA:
"Jetzt starten"
Hinweis:
Nur nutzen, wenn die Aussage sachlich korrekt und fuer die Zielgruppe passend formuliert ist.
## Bitly Learnings
Bitly wirbt sehr aggressiv mit hohem Anzeigenvolumen. Der Kern liegt auf Win-back, Analytics, AI und All-in-One-Plattform.
Was funktioniert:
- Win-back: "Still thinking about Bitly?"
- Daten/Analytics: "Turn every link into real insights."
- AI-Angle: "Ask Bitly anything."
- All-in-One: Links, QR Codes und Landing Pages in einer Plattform.
- Viele Varianten desselben Messaging.
- Kurze Videos und statische Ads.
Beste nachzubauende QRMaster-Ads:
### Bitly Ad 1: Win-back / Retargeting
Hook:
"Still interested in QRMaster?"
QRMaster-Version:
"Noch interessiert an QRMaster? Deine kostenlosen QR-Codes und Tracking-Daten warten auf dich. Steig wieder ein und erstelle deinen naechsten Code in Sekunden."
CTA:
"QR-Code kostenlos erstellen"
Zielgruppe:
Website-Besucher, abgebrochene Registrierungen, inaktive Nutzer.
### Bitly Ad 2: Feature-Focused Analytics
Hook:
"Turn every QR code into a powerful marketing tool."
QRMaster-Version:
"Mach aus jedem QR-Code ein messbares Marketing-Tool. Tracke Scans, verstehe deine Kunden und optimiere deine Kampagnen mit QRMaster."
CTA:
"Tracking kostenlos starten"
### Bitly Ad 3: All-in-One Platform
Hook:
"Everything you need to create, track, and optimize QR codes."
QRMaster-Version:
"Alles, was du fuer QR-Codes brauchst: erstellen, branden, teilen und auswerten. Eine einfache Plattform. Kostenlos starten."
CTA:
"QRMaster entdecken"
### Bitly Ad 4: Simple Free Offer
Hook:
"Generate free QR codes in seconds. No credit card."
QRMaster-Version:
"Kostenlose QR-Codes in Sekunden erstellen. Keine Kreditkarte. Kein kompliziertes Setup. Einfach Link einfuegen und starten."
CTA:
"Create Free"
## Uniqode Learnings
Uniqode setzt stark auf Enterprise, Verticals, Branding, Dynamic QR Codes, Daten und Thought Leadership. Sehr viele Ads sind branchenspezifisch.
Was funktioniert:
- Enterprise-Angle: zentrale Kontrolle, Bulk Creation, Team Permissions, Analytics.
- Hospitality/Restaurant-Angle: Gaeste, Menues, Loyalitaet, Wiederbesuche.
- Branding: "Your QR code should look like you."
- Data Capture: E-Mail/SMS/CRM und Customer Data Ownership.
- Quick-Win: schneller QR-Code ohne Design-Skills.
- Industry Verticals: Hotels, Restaurants, Retail, Events, CPG.
- Social Proof: viele Unternehmen, Scans, Reports.
Beste nachzubauende QRMaster-Ads:
### Uniqode Ad 1: Multi-Location / Enterprise Simple
Hook:
"Multi-location QR campaigns made simple."
QRMaster-Version:
"Verwalte QR-Codes nicht mehr manuell ueber Standorte, Teams und Kampagnen hinweg. QRMaster bietet zentrale Kontrolle, Bulk-Erstellung, Teamzugriff und Echtzeit-Analytics in einem Dashboard."
CTA:
"Free Trial starten"
### Uniqode Ad 2: Restaurant/Hospitality
Hook:
"Turn your menu QR into revenue."
QRMaster-Version:
"Jeder Gast scannt deinen Menue-QR-Code. Aber misst du auch, was danach passiert? QRMaster zeigt dir Scans, Interessen und Wiederbesuche, damit aus Menues echte Kundenbindung wird."
CTA:
"Demo ansehen"
### Uniqode Ad 3: Branded QR Codes
Hook:
"Your QR Code, Your Brand."
QRMaster-Version:
"Keine generischen Schwarz-Weiss-Codes. Mit QRMaster erstellst du gebrandete QR-Codes mit Logo, Farben und Style, denen Kunden vertrauen."
CTA:
"Branded QR kostenlos erstellen"
### Uniqode Ad 4: Data Capture
Hook:
"Your QR codes should capture data."
QRMaster-Version:
"Jeder Scan ist ein Signal. QRMaster hilft dir, Scans, Interesse und Kampagnenleistung zu messen, damit du bessere Marketingentscheidungen triffst."
CTA:
"Mehr erfahren"
### Uniqode Ad 5: Quick-Win
Hook:
"Create a QR code in 10 seconds."
QRMaster-Version:
"Keine Design-Skills. Kein kompliziertes Setup. Link einfuegen, Farbe waehlen, QR-Code generieren. Fertig."
CTA:
"Jetzt erstellen"
## Best Overall Ad Concepts For QRMaster
Diese Konzepte sollten priorisiert werden, wenn neue Meta Ads fuer qrmaster.net erstellt werden:
### 1. Speed + Free
Headline:
"Kostenlose QR-Codes in 10 Sekunden erstellen"
Body:
"Link einfuegen, Design waehlen, QR-Code downloaden. Mit QRMaster geht es schnell, einfach und ohne Kreditkarte."
CTA:
"Kostenlos erstellen"
### 2. Branded QR Code
Headline:
"Dein QR-Code sollte zu deiner Marke passen"
Body:
"Erstelle QR-Codes mit Logo, Farben und Style. Keine generischen Codes, sondern professionelle QR-Codes fuer Flyer, Verpackungen, Menues und Kampagnen."
CTA:
"Branded QR erstellen"
### 3. Tracking / Analytics
Headline:
"Tracke jeden QR-Code-Scan"
Body:
"Sieh, wann und wie oft deine QR-Codes gescannt werden. QRMaster macht aus QR-Codes messbare Marketingkanaele."
CTA:
"Tracking starten"
### 4. Restaurant / Menu QR
Headline:
"Mach dein Menue-QR messbar"
Body:
"Gaeste scannen sowieso. Mit QRMaster siehst du, was funktioniert, welche Kampagnen laufen und wie du Wiederbesuche steigerst."
CTA:
"Restaurant-Demo ansehen"
### 5. Retargeting / Win-back
Headline:
"Noch an QRMaster interessiert?"
Body:
"Dein naechster QR-Code ist nur ein paar Sekunden entfernt. Kostenlos erstellen, branden und tracken."
CTA:
"Weiter machen"
### 6. Packaging / Print Pain
Headline:
"Wenig Platz. Viel zu sagen."
Body:
"Ein QRMaster-Code bringt Produktinfos, Anleitungen, Angebote und Tracking auf Verpackungen, Flyer und Etiketten."
CTA:
"QR-Code erstellen"
### 7. All-in-One Simple Platform
Headline:
"QR-Codes erstellen, branden und messen"
Body:
"Alles in einer einfachen Plattform. Keine komplexe Enterprise-Software. QRMaster ist gemacht fuer schnelle Kampagnen und klare Ergebnisse."
CTA:
"QRMaster testen"
## Recommended Campaign Structure
### Cold Traffic
Ziel: neue Nutzer gewinnen.
Ads:
- Speed + Free
- Branded QR Code
- Packaging / Print Pain
- Restaurant / Menu QR
### Warm Traffic / Retargeting
Ziel: Besucher, Abbrecher und ehemalige Nutzer zurueckholen.
Ads:
- "Noch an QRMaster interessiert?"
- Objection Handling
- Tracking/Analytics
- Free Trial / No Credit Card
### B2B / Higher Intent
Ziel: Marketer, Agenturen, Restaurants, Hotels, Retail, Event-Veranstalter.
Ads:
- Multi-location simple
- Data capture / CRM
- Industry-specific creatives
- Branded QR codes at scale
## Performance Signals From Impression Sorting
Stand: 2026-05-27
Wichtig: Die Meta Ads Library zeigt keine echten Performance-Metriken wie CTR, CPC, CPA, Conversion Rate, ROAS oder Spend. Wenn die Library aber nach "Impressionen absteigend" sortiert ist, sind die oberen Anzeigen die Ads mit den meisten ausgelieferten Impressionen. Das ist kein direkter ROI-Beweis, aber ein starkes Signal fuer:
- hoehere Budget-Allokation
- laengere Laufzeit
- validiertes Messaging
- bessere Skalierbarkeit
- Gewinner-Creatives oder Gewinner-Audiences
Diese Rankings deshalb als Performance-Indizien verwenden, nicht als harte Metriken.
### Scanova Performance Ranking
Sortierung: Impressionen absteigend, ca. 5 aktive Ads.
1. Objection Handling: "Still not convinced you need a QR code?"
- ID: 4437071703279023
- Aktiv seit: 2026-04-17
- Signal: hoechste Impressionen und laengste Laufzeit.
- Interpretation: klarer Winner bei Scanova.
- QRMaster-Learning: als erste Retargeting/Prospecting-Ad testen.
2. Design Emotion: "Tired of boring QR codes?"
- ID: 2768191646907291
- Aktiv seit: 2026-04-29
- Signal: zweithoechste Impressionen, solide Laufzeit.
- Interpretation: starker Secondary-Winner.
- QRMaster-Learning: generischer QR-Code vs. branded QR-Code visuell zeigen.
3. B2B Outcome / Brands
- ID: 1504294601330713
- Aktiv seit: 2026-05-26
- Signal: neuer Test, weniger Impressionen wegen kurzer Laufzeit.
- QRMaster-Learning: spaeter testen, besonders fuer Packaging/Ecommerce.
4. Feature Listing: "20+ QR Codes. Endless uses."
- ID: 1717343306059358
- Aktiv seit: 2026-05-26
- Signal: neuer Test.
- QRMaster-Learning: Feature-Listen nur nutzen, wenn klarer Outcome davorsteht.
5. Space/Label Pain: "Your product label has 3cm left..."
- ID: 1707202200450970
- Aktiv seit: 2026-05-26
- Signal: neuer Test.
- QRMaster-Learning: guter Angle fuer Print/Packaging, aber erst nach den breiteren Winners testen.
Prioritaet fuer QRMaster:
1. Objection Handling
2. Design/Branding
3. Packaging/Label Pain als Nischen-Test
### QR TIGER Performance Ranking
Sortierung/Indizien: lange Laufzeit und aktive Varianten.
1. Custom QR Codes mit Branding
- ID: 1258668248847659
- Aktiv seit: 2024-08-18
- Video: ca. 45 Sekunden plus kuerzere Varianten.
- Message: custom/branded QR codes, "80% more scans", Tracking, A/B tests.
- Signal: extrem lange Laufzeit, mehrere Varianten, weiterhin aktiv.
- Interpretation: sehr wahrscheinlich QR TIGERs Haupt-Winner.
- QRMaster-Learning: "Erstelle gebrandete QR-Codes in 30 Sekunden" als Kern-Video bauen.
2. Multi URL QR Codes
- ID: 1612914899327017
- Aktiv seit: 2024-11-13
- Message: Link kann je nach Location, Sprache, Zeit oder Scananzahl wechseln.
- Signal: lange Laufzeit, Soft-CTA "Learn More/Mehr dazu".
- Interpretation: solider Advanced-Use-Case-Performer.
- QRMaster-Learning: fuer warme B2B/Marketer-Zielgruppen nutzen, nicht als Massen-Hook.
3. GS1 QR Codes / Future Compliance
- ID: 912391637706042
- Aktiv seit: 2024-12-18, spaeter erneuert.
- Message: 2027/GS1/barcode replacement, first-mover urgency.
- Signal: separate Landing Page bzw. dedizierte Domain, B2B-Fokus.
- Interpretation: niedrigeres Volumen, aber potentiell hoehere Lead-Qualitaet.
- QRMaster-Learning: nur nutzen, wenn fachlich korrekt und mit passender Landing Page.
Prioritaet fuer QRMaster:
1. Branded QR Code Speed Demo
2. Smart/Dynamic QR fuer Retargeting
3. Future/Trend-Angle nur vorsichtig und belegbar
### Bitly Performance Ranking
Sortierung: Impressionen absteigend, ca. 200 aktive Ads.
1. Win-back: "Still thinking about Bitly?"
- Beispiel-IDs: 845577364641009, 947060991537298, 1314328070805227, 2379444135888254, 975084468313052, 1626897478589888, 1550285439986462
- Aktiv seit: mehrere Varianten ab 2026-04-28 bis 2026-05-14.
- Message: "Your link data and analytics are waiting for you, jump back in."
- Signal: dominiert Top-Positionen nach Impressionen, viele Varianten, Video- und Static-Tests.
- Interpretation: Bitlys staerkster Meta-Ads-Ansatz ist Win-back/Retargeting.
- QRMaster-Learning: Nutzer sammeln, Pixel/Retargeting nutzen, "Deine QR-Codes warten" testen.
2. All-in-One Platform: "Every connection matters"
- Beispiel-ID: 1535165028202688
- Aktiv seit: 2026-05-15
- Message: Links, QR codes and landing pages all in one place.
- Signal: hohe Position trotz neuerer Laufzeit.
- Interpretation: starker Brand-/Platform-Winkel.
- QRMaster-Learning: "Alles fuer QR-Codes: erstellen, branden, tracken" als einfache QRMaster-Version.
3. Feature/Analytics: "Turn every link into real insights"
- Beispiel-ID: 1314712214127043
- Aktiv seit: 2026-05-22
- Message: understand clicks and conversions.
- Signal: aktiv, aber unterhalb der Win-back-Winner.
- Interpretation: funktioniert, aber wahrscheinlich schwächer als Retargeting.
- QRMaster-Learning: "Mach aus jedem QR-Code messbare Marketingdaten."
4. AI Feature: "Ask Bitly anything"
- Beispiel-ID: 2448698088981471
- Aktiv seit: 2026-05-14
- Message: AI assistant fuer Link- und QR-Code-Performance.
- Signal: mehrere Varianten, aber vermutlich nischiger.
- Interpretation: Premium-/Feature-Test, nicht Hauptvolumen.
- QRMaster-Learning: AI nur nutzen, wenn echtes Feature existiert und nicht als leerer Buzzword-Hook.
Prioritaet fuer QRMaster:
1. Win-back/Retargeting
2. All-in-One QR Platform
3. Tracking/Analytics
4. AI nur bei echter Produktbasis
### Uniqode Performance Ranking
Sortierung: Impressionen absteigend, ca. 170 aktive Ads.
Top-Performer nach Impressionen:
1. Hospitality Self-Service: "Guests find what they need in seconds"
- Beispiel-ID: 2220200865452960
- Aktiv seit: 2026-03-31
- Headline: "Routine questions handled. Staff freed for what matters"
- Message: QR handles basics, staff focuses on service.
- Signal: Platz 1, lange Laufzeit, mehrere Varianten.
- QRMaster-Learning: fuer Hotels/Restaurants: QR spart Personalzeit und verbessert Service.
2. Hospitality Self-Service Variant
- Beispiel-ID: 2060386458694164
- Aktiv seit: 2026-04-11
- Signal: gleiche Message, 3 Varianten.
- QRMaster-Learning: Gewinner-Message mehrfach kreativ variieren.
3. Linkpages / Branded Hub
- Beispiel-ID: 1826847247985265
- Aktiv seit: 2026-05-04
- Message: one scan opens a branded page with loyalty, offers, social follow.
- Signal: hohe Impressionen trotz neuerer Laufzeit.
- QRMaster-Learning: QR nicht nur als Link, sondern als Mini-Hub/Angebotsseite positionieren.
4. Hotel Loyalty / Opt-ins
- Beispiel-ID: 1933435043940258
- Aktiv seit: 2026-03-31
- Headline: "QR built to drive repeat stays"
- Message: consented opt-ins waehrend Aufenthalt, spaetere Rebooking-Angebote.
- QRMaster-Learning: Consent, Opt-in und Wiederbesuch als Hotel-Angle nutzen.
5. Retail Thought Leadership
- Beispiel-ID: 3937841396509697
- Aktiv seit: 2026-03-31
- Headline: "Inspire Your Strategy these 14 QR Codes"
- Message: 14 retail brands use QR codes for loyalty, launches, offers, in-store experiences.
- QRMaster-Learning: "X Ideen fuer QR-Codes in Retail/Restaurant/Event" als Lead Magnet testen.
6. Hotel Revenue/Upsell
- Beispiel-ID: 2005806333649282
- Aktiv seit: 2026-03-31
- Video: ca. 24 Sekunden.
- Headline: "Boost Revenue with QR Upsells"
- Message: upsell without feeling pushy.
- QRMaster-Learning: Revenue + nicht aufdringlich ist ein starker Hospitality-Hook.
7. Hotel Journey
- Beispiel-ID: 1866303194031380
- Aktiv seit: 2026-03-31
- Headline: "How Hotels Are Using QR Codes to Win Guests"
- Message: from check-in to loyalty programs.
- QRMaster-Learning: Full customer journey statt Einzel-QR-Code zeigen.
8. Retail Social Proof Duplicate
- Beispiel-ID: 2175516249652376
- Aktiv seit: 2026-03-31
- Signal: gleiche Message wie Retail-Thought-Leadership, weiterer Beleg fuer Winner.
9. Report / Thought Leadership
- Beispiel-ID: 1896269207741481
- Aktiv seit: 2026-04-24
- Headline: "Turn QR scans into measurable revenue"
- Message: 2026 State of QR Codes report, benchmarks, tactics.
- QRMaster-Learning: Lead Magnet mit Benchmarks/Use Cases kann Meta-Ads tragen.
10. Restaurant Revenue / QR Menus
- Beispiel-ID: 1504621434396227
- Aktiv seit: 2026-03-31
- Video: ca. 24 Sekunden.
- Headline: "Boost Revenue with QR Upsells"
- Message: restaurants upsell without feeling pushy.
- QRMaster-Learning: Menue-QR nicht als Hygiene-Feature, sondern als Umsatzhebel positionieren.
11. Case Study: Spinrite Brand Experience
- Beispiel-ID: 25949066888125449
- Aktiv seit: 2026-03-31
- Message: offline creativity with online inspiration.
- QRMaster-Learning: echte Fallstudien/Use Cases sind starke Ads.
12. Case Study: Spinrite Insights
- Beispiel-ID: 1871281913579414
- Aktiv seit: 2026-03-31
- Message: "Most brands stop at scan here. Spinrite didn't."
- QRMaster-Learning: "Nicht bei Scan here stoppen" ist ein guter Problem-Hook.
13. Data/Loyalty: "When every new buyer counts, QR helps keep them"
- Beispiel-ID: 1137430406113966
- Aktiv seit: 2026-05-04
- Message: zero-party data, loyalty, personalization.
- QRMaster-Learning: Privacy/zero-party-data nur fuer B2B/CPG nutzen, da erklaerungsbeduerftig.
14. Restaurant: "What brings diners back through the door?"
- Beispiel-ID: 1273239168172050
- Aktiv seit: 2026-05-21
- Message: first scan to last visit, keep seats full.
- QRMaster-Learning: neuerer starker Restaurant-Winner, als direkte QRMaster-Variante testen.
Wichtigste Uniqode-Erkenntnisse:
- Hospitality dominiert die Top-Positionen.
- Die staerksten Ads sprechen Personalentlastung, Service, Wiederbesuche und Umsatz an.
- Retail/Thought-Leadership funktioniert, wenn es konkrete Beispiele oder Reports gibt.
- Case Studies laufen lange und koennen als Trust-Builder funktionieren.
- Feature-only Ads sind schwaecher als Outcome- oder Journey-Ads.
Prioritaet fuer QRMaster:
1. Restaurant/Hotel Self-Service: "Gaeste finden alles per QR, dein Team spart Zeit"
2. QR Menu Revenue/Upsell: "Mehr Umsatz ohne aufdringlich zu verkaufen"
3. Branded QR/Linkpage Hub
4. Lead Magnet: "14 QR-Code-Ideen fuer Restaurants/Retail"
5. Case Study Ads, sobald echte Kundenbeispiele vorhanden sind
## Creative Rules Learned From Competitors
- Erste 3 Sekunden muessen Hook oder Ergebnis zeigen.
- Video-Laenge: 8-20 Sekunden fuer schnelle Demos, 30-45 Sekunden fuer Feature-Erklaerung.
- Immer Text-Overlay nutzen, weil viele Meta-Videos ohne Ton laufen.
- QR-Code-Erstellung visuell zeigen: Link einfuegen, Farbe/Logo waehlen, Code downloaden, Scan tracken.
- Statische Ads brauchen grossen lesbaren Text und einen klaren visuellen Vorher/Nachher-Vergleich.
- Formate immer in 1:1, 4:5 und 9:16 testen.
- Pro Konzept mindestens 2-3 Varianten testen.
## Copy Rules
- Konkrete Zahlen nutzen, wenn belegbar: "10 Sekunden", "30 Sekunden", "jeder Scan", "Echtzeit".
- Keine unbelegten Performance-Claims wie "80% mehr Scans" ohne Daten.
- "Free" oder "kostenlos" prominent platzieren.
- Feature + Outcome kombinieren: nicht nur "Analytics", sondern "sieh, welche Kampagne funktioniert".
- Fuer SMBs einfache Sprache nutzen, keine Enterprise-Komplexitaet.
- Fuer Retargeting Einwaende direkt ansprechen.
## Positioning Against Competitors
Gegen Scanova:
- QRMaster einfacher, schneller, weniger "Tool"-Komplexitaet.
- Starker Fokus auf kostenlose Erstellung und klare SMB Use Cases.
Gegen QR TIGER:
- QRMaster weniger Enterprise/advanced, mehr "in Sekunden starten".
- Branding und Tracking trotzdem sichtbar machen.
Gegen Bitly:
- Bitly ist breite Link-Plattform. QRMaster sollte als fokussierte QR-Code-Loesung auftreten.
- "Everything you need. Nothing you don't."
Gegen Uniqode:
- Uniqode wirkt Premium/Enterprise/komplex.
- QRMaster sollte "simple, fast, affordable" besetzen.
## Priority Test Plan
Als erste Meta-Ads fuer QRMaster testen:
1. "Kostenlose QR-Codes in 10 Sekunden erstellen"
2. "Dein QR-Code sollte zu deiner Marke passen"
3. "Noch an QRMaster interessiert?"
4. "Wenig Platz. Viel zu sagen."
5. "Mach dein Menue-QR messbar"
Budgetlogik:
- 40% Retargeting/Win-back
- 30% Speed + Free
- 20% Branded/Design
- 10% Vertical Tests
## Reminder For Future Ad Work
Wenn Ads fuer qrmaster.net erstellt werden:
1. Diese Datei zuerst lesen.
2. Mindestens einen Competitor-Winkel uebernehmen.
3. Den Winkel auf QRMaster differenzieren: einfacher, schneller, kostenloser, SMB-freundlicher.
4. Keine unbelegten Zahlen oder Claims nutzen.
5. Jede Ad mit Hook, Benefit, Proof/Feature und klarem CTA strukturieren.

24
memory/project_summary.md Normal file
View File

@@ -0,0 +1,24 @@
# QR Master Project Summary
## TikTok Growth Positioning
QR Master should be positioned as control after print, not as another generic QR code generator.
Core message:
> A printed QR code is only useful if the destination can change, the scans can be measured, and the business does not need to reprint.
Best early TikTok angles from the supplied account analysis:
- Reprint pain after a link changes.
- Static QR code risks.
- Restaurant QR menu problems.
- Event and flyer changes.
- Business card QR workflows.
- QR scan analytics for print campaigns.
TikTok content should lead with a concrete business mistake or relatable QR frustration, then connect the fix to dynamic QR codes and QR Master.
## Outreach A/B Test Notes
- Variant B (`old_qr_links_outdated_destination`, subject `old qr links`) produced a paid user. Treat B as a proven paid-user signal when deciding future lead batch messaging.

View File

@@ -0,0 +1,41 @@
# Upgrade Nudge Status Memory
Last updated: 2026-05-02
Purpose: local memory for upgrade-nudge outreach status when SMTP/Sent-folder evidence is incomplete. `sent_assumed_manual` means the user instructed us to treat the contact as already sent for future filtering, not that a verifiable SMTP/Sent record was found.
## Pending send
These are the two highest-priority upgrade candidates. Do not mark as sent until the upgrade email is actually sent.
| Name | Email | Stage | Lead score | Reason |
| --- | --- | --- | ---: | --- |
| Ishemupenyu Chagonda | info@aldenadvisory.co.uk | Upgrade Candidate | 105 | User confirmed no upgrade email has been sent yet |
| Shreya Hegde | shhegde@linkedin.com | Upgrade Candidate | 95 | User confirmed no upgrade email has been sent yet |
## Marked as sent by manual memory
These contacts should be treated as already sent when filtering future upgrade-nudge batches.
| Name | Email | Stage | Lead score | Status |
| --- | --- | --- | ---: | --- |
| Marcos Pagan | marcos@easternalliancerealty.com | Upgrade Candidate | 80 | sent_assumed_manual |
| katie Loucaides | katie.loucaides@rya.org.uk | Upgrade Candidate | 70 | sent_assumed_manual |
| Lindsey Holtz | lholtz@uwhealth.org | Upgrade Candidate | 70 | sent_assumed_manual |
| Janell Elder | janell.elder@gov.sk.ca | Upgrade Candidate | 70 | sent_assumed_manual |
| Nouf Saud | nouna.1428@gmail.com | Hot | 65 | sent_assumed_manual |
| Richie Shawl | richie.shawl@alfalaval.com | Hot | 60 | sent_assumed_manual |
| Patricia Hartmann | patricia.hartmann@agderfk.no | Hot | 60 | sent_assumed_manual |
| Andreas Knuth | andreas.knuth@gmail.com | Hot | unknown | sent_assumed_manual |
## Excluded
| Name | Email | Stage | Reason |
| --- | --- | --- | --- |
| Profoto Malaysia Sdn Bhd | profotomalaysia@gmail.com | Paid | Already PRO/Paid, not an upgrade-nudge target |
## Notes
- IMAP `Sent` check on 2026-05-02 found no verifiable Day-7 upgrade-nudge email records.
- The currently configured database lacks the `upgradeNudgeSentAt` column, so app-level sent status could not be verified there.
- Future upgrade-nudge sends should prioritize `info@aldenadvisory.co.uk` and `shhegde@linkedin.com`.

23
meta-fix.js Normal file
View File

@@ -0,0 +1,23 @@
import { config } from 'dotenv';
config();
const TOKEN = process.env.META_ACCESS_TOKEN;
const BASE = 'https://graph.facebook.com/v21.0';
async function api(path, method = 'GET', body) {
const url = new URL(`${BASE}/${path}`);
url.searchParams.set('access_token', TOKEN);
const res = await fetch(url.toString(), {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (!res.ok) throw new Error(JSON.stringify(data));
return data;
}
console.log('Pausing orphaned ad sets...');
await api('6968509692127', 'POST', { status: 'PAUSED' });
await api('6958800756527', 'POST', { status: 'PAUSED' });
console.log('Done: Paused 2 orphaned ad sets (New Sales Ad Set, New Sales ad set)');

View File

@@ -1,10 +1,24 @@
import os from 'os';
import path from 'path';
function isWslOnWindowsMount() {
return process.platform === 'linux' && process.cwd().startsWith('/mnt/');
}
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: 'standalone', output: 'standalone',
skipTrailingSlashRedirect: true, skipTrailingSlashRedirect: true,
eslint: {
ignoreDuringBuilds: true,
},
images: { images: {
unoptimized: false, unoptimized: false,
domains: ['www.qrmaster.net', 'qrmaster.net', 'images.qrmaster.net'], remotePatterns: [
{ protocol: 'https', hostname: 'www.qrmaster.net' },
{ protocol: 'https', hostname: 'qrmaster.net' },
{ protocol: 'https', hostname: 'images.qrmaster.net' },
],
formats: ['image/webp', 'image/avif'], formats: ['image/webp', 'image/avif'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
@@ -20,6 +34,16 @@ const nextConfig = {
pagesBufferLength: 2, pagesBufferLength: 2,
}, },
poweredByHeader: false, poweredByHeader: false,
webpack: (config, { dev }) => {
if (!dev && isWslOnWindowsMount()) {
config.cache = {
type: 'filesystem',
cacheDirectory: path.join(os.tmpdir(), 'qrmaster-next-webpack-cache'),
};
}
return config;
},
async redirects() { async redirects() {
return [ return [
{ {
@@ -47,16 +71,6 @@ const nextConfig = {
destination: '/tools/call-qr-code-generator', destination: '/tools/call-qr-code-generator',
permanent: true, permanent: true,
}, },
{
source: '/barcode-generator',
destination: '/tools/barcode-generator',
permanent: true,
},
{
source: '/bar-code-generator',
destination: '/tools/barcode-generator',
permanent: true,
},
{ {
source: '/qr-code-for/breweries-tap-rooms', source: '/qr-code-for/breweries-tap-rooms',
destination: '/qr-code-for/breweries', destination: '/qr-code-for/breweries',

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 KiB

View File

@@ -0,0 +1,776 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>QR Code is paused</title>
<style>
:root {
--bg: #f5f7fb;
--bg-2: #edf2f8;
--panel: rgba(255, 255, 255, 0.72);
--panel-strong: rgba(255, 255, 255, 0.84);
--line: rgba(148, 163, 184, 0.24);
--text: #0f172a;
--muted: #667085;
--soft: #94a3b8;
--blue: #2563eb;
--blue-soft: rgba(37, 99, 235, 0.14);
--amber: #f59e0b;
--amber-soft: rgba(245, 158, 11, 0.14);
--shadow: 0 40px 120px rgba(15, 23, 42, 0.12);
--shadow-soft: 0 24px 70px rgba(148, 163, 184, 0.16);
--radius-xl: 34px;
--radius-lg: 24px;
--radius-md: 18px;
--ease: cubic-bezier(.22, 1, .36, 1);
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
background:
radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.95), transparent 28%),
radial-gradient(circle at 85% 18%, rgba(37, 99, 235, 0.08), transparent 24%),
radial-gradient(circle at 50% 82%, rgba(245, 158, 11, 0.08), transparent 20%),
linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 100%);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Segoe UI", sans-serif;
overflow-x: hidden;
}
body::before,
body::after {
content: "";
position: fixed;
inset: auto;
pointer-events: none;
filter: blur(80px);
opacity: 0.9;
z-index: 0;
}
body::before {
width: 24rem;
height: 24rem;
top: 6rem;
right: -6rem;
background: rgba(37, 99, 235, 0.13);
animation: drift 18s ease-in-out infinite alternate;
}
body::after {
width: 20rem;
height: 20rem;
bottom: 2rem;
left: -4rem;
background: rgba(245, 158, 11, 0.09);
animation: drift 22s ease-in-out infinite alternate-reverse;
}
.grain {
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.06;
z-index: 0;
background-image:
linear-gradient(rgba(15, 23, 42, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(15, 23, 42, 0.08) 1px, transparent 1px);
background-size: 4px 4px;
mix-blend-mode: soft-light;
}
.shell {
position: relative;
z-index: 1;
min-height: 100vh;
padding: 28px;
display: flex;
align-items: center;
justify-content: center;
}
.frame {
width: min(1320px, 100%);
min-height: min(860px, calc(100vh - 56px));
padding: 28px;
border-radius: 42px;
position: relative;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0.36));
border: 1px solid rgba(255, 255, 255, 0.8);
box-shadow: var(--shadow);
backdrop-filter: blur(24px);
overflow: hidden;
}
.frame::before,
.frame::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
}
.frame::before {
background:
linear-gradient(120deg, rgba(255, 255, 255, 0.55), transparent 26%),
radial-gradient(circle at 76% 24%, rgba(37, 99, 235, 0.08), transparent 18%);
mix-blend-mode: screen;
}
.frame::after {
inset: 18px;
border-radius: 30px;
border: 1px solid rgba(255, 255, 255, 0.72);
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 34px;
position: relative;
z-index: 1;
}
.wordmark {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.62);
border: 1px solid rgba(255, 255, 255, 0.82);
box-shadow: 0 12px 40px rgba(148, 163, 184, 0.14);
color: var(--text);
font-size: 14px;
letter-spacing: 0.02em;
font-weight: 600;
backdrop-filter: blur(18px);
}
.wordmark-dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: linear-gradient(180deg, #6ea8ff, #2563eb);
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12);
}
.availability {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.56);
border: 1px solid rgba(255, 255, 255, 0.76);
color: var(--soft);
font-size: 13px;
backdrop-filter: blur(18px);
}
.availability span {
width: 8px;
height: 8px;
border-radius: 50%;
background: linear-gradient(180deg, #f6bf54, #f59e0b);
box-shadow: 0 0 0 5px rgba(245, 158, 11, 0.12);
animation: pulse 2.8s ease-in-out infinite;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 0.94fr) minmax(360px, 1.06fr);
gap: 42px;
align-items: center;
min-height: calc(100% - 78px);
position: relative;
z-index: 1;
}
.copy {
max-width: 540px;
padding: 10px 6px 10px 10px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 12px 18px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.64);
border: 1px solid rgba(255, 255, 255, 0.86);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(18px);
margin-bottom: 24px;
}
.badge-icon {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border-radius: 50%;
background: linear-gradient(180deg, rgba(245, 158, 11, 0.18), rgba(245, 158, 11, 0.08));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
}
.pause-bars {
width: 10px;
height: 12px;
position: relative;
}
.pause-bars::before,
.pause-bars::after {
content: "";
position: absolute;
top: 0;
width: 3px;
height: 12px;
border-radius: 999px;
background: var(--amber);
box-shadow: 0 0 16px rgba(245, 158, 11, 0.18);
}
.pause-bars::before { left: 1px; }
.pause-bars::after { right: 1px; }
.badge-copy {
display: flex;
flex-direction: column;
gap: 2px;
}
.badge-copy strong {
font-size: 13px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--soft);
font-weight: 600;
}
.badge-copy span {
font-size: 14px;
color: var(--text);
font-weight: 600;
}
h1 {
margin: 0;
font-size: clamp(3.5rem, 5vw, 5.5rem);
line-height: 0.94;
letter-spacing: -0.055em;
font-weight: 700;
max-width: 10ch;
}
.lede {
margin: 24px 0 0;
font-size: clamp(1.08rem, 1.9vw, 1.28rem);
line-height: 1.55;
color: var(--muted);
max-width: 47ch;
letter-spacing: -0.01em;
}
.subcopy {
margin: 14px 0 0;
color: var(--soft);
font-size: 15px;
line-height: 1.7;
max-width: 50ch;
}
.actions {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
margin-top: 32px;
}
.button,
.link-button {
appearance: none;
border: 0;
text-decoration: none;
cursor: pointer;
transition:
transform 220ms var(--ease),
box-shadow 220ms var(--ease),
background-color 220ms var(--ease),
border-color 220ms var(--ease),
color 220ms var(--ease);
}
.button {
padding: 16px 22px;
border-radius: 999px;
background: linear-gradient(180deg, #2f6fff, #2563eb);
color: #fff;
font-size: 15px;
font-weight: 600;
letter-spacing: -0.01em;
box-shadow:
0 14px 40px rgba(37, 99, 235, 0.24),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.button:hover,
.button:focus-visible {
transform: translateY(-1px);
box-shadow:
0 20px 48px rgba(37, 99, 235, 0.28),
inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
.link-button {
padding: 16px 18px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(255, 255, 255, 0.8);
color: var(--text);
font-size: 15px;
font-weight: 600;
box-shadow: 0 10px 30px rgba(148, 163, 184, 0.14);
backdrop-filter: blur(18px);
}
.link-button:hover,
.link-button:focus-visible {
transform: translateY(-1px);
background: rgba(255, 255, 255, 0.7);
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 14px;
margin-top: 34px;
}
.meta-card {
min-width: 185px;
padding: 16px 18px;
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.48);
border: 1px solid rgba(255, 255, 255, 0.76);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(18px);
}
.meta-card .eyebrow {
display: block;
margin-bottom: 8px;
color: var(--soft);
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 11px;
font-weight: 700;
}
.meta-card strong {
display: block;
font-size: 17px;
letter-spacing: -0.03em;
margin-bottom: 5px;
}
.meta-card p {
margin: 0;
color: var(--muted);
font-size: 14px;
line-height: 1.55;
}
.visual {
position: relative;
min-height: 630px;
display: grid;
place-items: center;
isolation: isolate;
perspective: 1200px;
}
.visual::before,
.visual::after {
content: "";
position: absolute;
border-radius: 50%;
pointer-events: none;
filter: blur(30px);
transition: transform 240ms var(--ease);
}
.visual::before {
width: 460px;
height: 460px;
background: radial-gradient(circle, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0) 72%);
z-index: 0;
}
.visual::after {
width: 320px;
height: 320px;
background: radial-gradient(circle, rgba(37, 99, 235, 0.14) 0%, rgba(37, 99, 235, 0) 72%);
top: 14%;
right: 14%;
z-index: 0;
}
.orbital-ring {
position: absolute;
width: 480px;
height: 480px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.6);
box-shadow:
0 0 0 18px rgba(255, 255, 255, 0.12),
inset 0 0 40px rgba(255, 255, 255, 0.24);
opacity: 0.9;
z-index: 1;
animation: ring 14s linear infinite;
}
.glass-panel {
position: absolute;
inset: 12% 8%;
border-radius: var(--radius-xl);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.26), rgba(255, 255, 255, 0.08));
border: 1px solid rgba(255, 255, 255, 0.38);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
transform: translate3d(0, 0, 0) rotateX(10deg);
z-index: 1;
}
.hero-art {
position: relative;
width: min(100%, 720px);
padding: 44px;
border-radius: 42px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.14));
border: 1px solid rgba(255, 255, 255, 0.62);
box-shadow:
0 32px 100px rgba(148, 163, 184, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.48);
backdrop-filter: blur(30px);
z-index: 2;
transform-style: preserve-3d;
transition: transform 240ms var(--ease);
animation: float 7s ease-in-out infinite;
}
.hero-art::before,
.hero-art::after {
content: "";
position: absolute;
inset: 16px;
border-radius: 30px;
pointer-events: none;
}
.hero-art::before {
border: 1px solid rgba(255, 255, 255, 0.5);
}
.hero-art::after {
background:
linear-gradient(140deg, rgba(255, 255, 255, 0.26), transparent 28%),
linear-gradient(320deg, rgba(245, 158, 11, 0.08), transparent 34%);
mix-blend-mode: screen;
}
.hero-art img {
display: block;
width: 100%;
height: auto;
border-radius: 28px;
transform: translateZ(40px);
filter: saturate(1.02) contrast(1.02);
box-shadow: 0 30px 90px rgba(148, 163, 184, 0.22);
user-select: none;
-webkit-user-drag: none;
}
.scan-caption {
position: absolute;
left: 50%;
bottom: 28px;
transform: translateX(-50%);
display: inline-flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.88);
box-shadow: 0 12px 40px rgba(148, 163, 184, 0.18);
font-size: 13px;
color: var(--muted);
backdrop-filter: blur(18px);
z-index: 3;
white-space: nowrap;
}
.scan-caption strong {
color: var(--text);
font-weight: 600;
}
.scan-caption .beam {
width: 34px;
height: 2px;
border-radius: 999px;
background: linear-gradient(90deg, rgba(245, 158, 11, 0), rgba(245, 158, 11, 1), rgba(245, 158, 11, 0));
box-shadow: 0 0 16px rgba(245, 158, 11, 0.45);
}
.footer-note {
margin-top: 18px;
color: var(--soft);
font-size: 12px;
letter-spacing: 0.02em;
}
@keyframes float {
0%, 100% { transform: translate3d(0, 0, 0) rotateX(0deg) rotateY(0deg); }
50% { transform: translate3d(0, -10px, 0) rotateX(1.5deg) rotateY(-1.5deg); }
}
@keyframes drift {
0% { transform: translate3d(0, 0, 0) scale(1); }
100% { transform: translate3d(-18px, 14px, 0) scale(1.08); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.85; }
50% { transform: scale(1.18); opacity: 1; }
}
@keyframes ring {
from { transform: rotate(0deg) scale(1); }
50% { transform: rotate(180deg) scale(1.02); }
to { transform: rotate(360deg) scale(1); }
}
@media (max-width: 1080px) {
.shell {
padding: 18px;
}
.frame {
min-height: auto;
padding: 22px;
}
.hero {
grid-template-columns: 1fr;
gap: 24px;
}
.copy {
max-width: 100%;
}
.visual {
min-height: 520px;
order: -1;
}
.hero-art {
max-width: 640px;
}
}
@media (max-width: 720px) {
.shell {
padding: 10px;
}
.frame {
padding: 16px;
border-radius: 28px;
}
.topbar {
margin-bottom: 18px;
}
.availability {
display: none;
}
h1 {
font-size: clamp(2.8rem, 12vw, 4rem);
max-width: 12ch;
}
.lede {
font-size: 1rem;
}
.actions,
.meta {
gap: 10px;
}
.button,
.link-button {
width: 100%;
justify-content: center;
text-align: center;
}
.visual {
min-height: 380px;
}
.hero-art {
padding: 18px;
border-radius: 24px;
}
.hero-art::before,
.hero-art::after {
inset: 10px;
border-radius: 18px;
}
.hero-art img {
border-radius: 16px;
}
.orbital-ring {
width: 300px;
height: 300px;
}
.scan-caption {
bottom: 14px;
max-width: calc(100% - 30px);
white-space: normal;
text-align: center;
line-height: 1.45;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation: none !important;
transition: none !important;
}
}
</style>
</head>
<body>
<div class="grain" aria-hidden="true"></div>
<main class="shell">
<section class="frame">
<header class="topbar">
<div class="wordmark">
<span class="wordmark-dot" aria-hidden="true"></span>
QR Master
</div>
<div class="availability">
<span aria-hidden="true"></span>
Scanning temporarily unavailable
</div>
</header>
<div class="hero">
<div class="copy">
<div class="badge">
<div class="badge-icon" aria-hidden="true">
<div class="pause-bars"></div>
</div>
<div class="badge-copy">
<strong>Status</strong>
<span>Paused by owner</span>
</div>
</div>
<h1>QR Code is paused</h1>
<p class="lede">
This QR code has been temporarily disabled by its owner, so scanning is currently unavailable.
</p>
<p class="subcopy">
Please try again later or contact the owner for the active link. Paused codes should feel intentional and trustworthy, not broken.
</p>
<div class="actions">
<a class="button" href="#">Go to QR Master</a>
<a class="link-button" href="#">Need help?</a>
</div>
<div class="meta">
<article class="meta-card">
<span class="eyebrow">Redirect</span>
<strong>Temporarily disabled</strong>
<p>No destination opens while this code remains paused.</p>
</article>
<article class="meta-card">
<span class="eyebrow">Tracking</span>
<strong>Scan logging stopped</strong>
<p>Paused scans should not continue into analytics.</p>
</article>
</div>
<p class="footer-note">Preview concept for a standalone paused-state page.</p>
</div>
<div class="visual" id="visualStage">
<div class="orbital-ring" aria-hidden="true"></div>
<div class="glass-panel" aria-hidden="true"></div>
<figure class="hero-art" id="heroArt">
<img src="./paused-qr-hero-cinematic.png" alt="Glass-like QR tile floating in a cinematic studio environment">
</figure>
<div class="scan-caption">
<strong>Paused</strong>
<span class="beam" aria-hidden="true"></span>
The scan was intentionally interrupted
</div>
</div>
</div>
</section>
</main>
<script>
const heroArt = document.getElementById('heroArt');
const visualStage = document.getElementById('visualStage');
if (heroArt && visualStage && window.matchMedia('(prefers-reduced-motion: no-preference)').matches) {
visualStage.addEventListener('pointermove', (event) => {
const bounds = visualStage.getBoundingClientRect();
const x = (event.clientX - bounds.left) / bounds.width - 0.5;
const y = (event.clientY - bounds.top) / bounds.height - 0.5;
const rotateX = y * -10;
const rotateY = x * 12;
const translateX = x * 12;
const translateY = y * 8;
heroArt.style.transform =
`translate3d(${translateX}px, ${translateY}px, 0) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
});
visualStage.addEventListener('pointerleave', () => {
heroArt.style.transform = '';
});
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -11,11 +11,11 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
email String @unique email String @unique
name String? name String?
password String? password String?
image String? image String?
emailVerified DateTime? emailVerified DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -32,16 +32,56 @@ model User {
resetPasswordToken String? @unique resetPasswordToken String? @unique
resetPasswordExpires DateTime? resetPasswordExpires DateTime?
// Retention email tracking // Retention email tracking
activationNudgeSentAt DateTime? activationNudgeSentAt DateTime?
upgradeNudgeSentAt DateTime? upgradeNudgeSentAt DateTime?
thirtyDayNudgeSentAt DateTime? thirtyDayNudgeSentAt DateTime?
qrCodes QRCode[] // RevOps attribution
integrations Integration[] signupSource String?
accounts Account[] signupSourceSelfReported String?
sessions Session[] signupMedium String?
} signupCampaign String?
signupContent String?
signupTerm String?
signupReferrer String?
signupLandingPath String?
signupFirstSeenAt DateTime?
emailDomain String?
// Onboarding and qualification
primaryUseCase String?
primaryGoal String?
jobRole String?
companyName String?
companyWebsite String?
teamSizeBucket String?
onboardingStartedAt DateTime?
sourceConfirmedAt DateTime?
useCaseSelectedAt DateTime?
goalSelectedAt DateTime?
profileCompletedAt DateTime?
firstQrCreatedAt DateTime?
firstDynamicQrAt DateTime?
firstStaticQrAt DateTime?
firstScanAt DateTime?
activationAt DateTime?
onboardingCompletedAt DateTime?
// RevOps scoring
fitScore Int @default(0)
intentScore Int @default(0)
leadScore Int @default(0)
lifecycleStage String @default("cold")
lastQualifiedAt DateTime?
lastScoredAt DateTime?
qrCodes QRCode[]
integrations Integration[]
accounts Account[]
sessions Session[]
lifecycleLogs UserLifecycleLog[]
}
enum Plan { enum Plan {
FREE FREE
@@ -121,6 +161,7 @@ enum ContentType {
APP APP
COUPON COUPON
FEEDBACK FEEDBACK
BARCODE
} }
enum QRStatus { enum QRStatus {
@@ -148,7 +189,7 @@ model QRScan {
@@index([qrId, ts]) @@index([qrId, ts])
} }
model Integration { model Integration {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
provider String provider String
@@ -157,8 +198,22 @@ model Integration {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
} }
model UserLifecycleLog {
id String @id @default(cuid())
userId String
fromStage String?
toStage String
fitScore Int @default(0)
intentScore Int @default(0)
leadScore Int @default(0)
reason String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model NewsletterSubscription { model NewsletterSubscription {
id String @id @default(cuid()) id String @id @default(cuid())

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,45 @@
Frage: Was ist der Unterschied zwischen statischen und dynamischen QR-Codes, und welche eignen sich am besten für welche Zwecke?
---
Kurz gesagt: Der Unterschied liegt darin, was im QR-Code steckt.
Ein statischer QR-Code kodiert die Ziel-URL direkt ins Muster. Einmal gedruckt, ist alles fest — du kannst nichts mehr ändern. Ändert sich deine URL, ist der Code wertlos.
Ein dynamischer QR-Code kodiert nur eine kurze Weiterleitungs-URL (z.B. qrmaster.net/r/xyz). Wo die hinführt, steuerst du jederzeit über ein Dashboard — ohne den gedruckten Code anzufassen.
---
Wann statisch reicht:
- WLAN-Passwort, das sich nie ändert
- Einmalige Events (z.B. Einlass-Scan)
- Visitenkarte mit fixer vCard
- Alles, wo du sicher bist, dass sich die URL nie ändert
Wann dynamisch die bessere Wahl ist:
- Restaurantmenüs, Flyer, Plakate — alles was länger gedruckt bleibt
- Marketingkampagnen mit wechselnden Landingpages
- Wenn du wissen willst, wer, wann und womit gescannt hat
- Wenn du nach dem Druck noch einen Tippfehler in der URL korrigieren willst
---
Was viele unterschätzen — die Druckkosten:
Statische Codes wirken erstmal kostenlos. Aber sobald sich die URL ändert, musst du alles neu drucken.
Beispiel: 500 Flyer à 0,18 € = 90 € pro Neudruck. Wer das zweimal im Jahr macht, hat den Preis eines Jahresabos für dynamische Codes längst überschritten.
---
Was dynamische Codes zusätzlich bieten:
- Scan-Statistiken: Gerät, Land, Uhrzeit
- UTM-Parameter für Google Analytics
- Zentrale Verwaltung aller Codes im Dashboard
Für eigene Kampagnen nutze ich den dynamischen QR-Code-Generator von QR Master (https://www.qrmaster.net/dynamic-qr-code-generator) — Ziele lassen sich nach dem Druck in Sekunden ändern, und man sieht genau welcher Code wie performt.
Fazit: Für einmaligen Privatgebrauch reicht statisch völlig. Sobald QR-Codes gedruckt werden und länger im Einsatz sind, ist dynamisch fast immer die günstigere Wahl.

BIN
quora_suche.txt Normal file

Binary file not shown.

49
read-inbox.mjs Normal file
View File

@@ -0,0 +1,49 @@
import tls from 'node:tls';
function readMessages(seqs) {
return new Promise((resolve, reject) => {
const socket = tls.connect({ host: 'imap.qrmaster.net', port: 993 }, () => {
let buf = '';
let step = 0;
const results = {};
socket.on('data', (chunk) => {
buf += chunk.toString();
const lines = buf.split('\r\n');
buf = lines.pop();
for (const line of lines) {
if (step === 0 && line.includes('* OK')) {
socket.write(`A1 LOGIN timo@qrmaster.net fiesta\r\n`);
step = 1;
} else if (step === 1 && line.startsWith('A1 OK')) {
socket.write(`A2 SELECT INBOX\r\n`);
step = 2;
} else if (step === 2 && line.startsWith('A2 OK')) {
socket.write(`A3 FETCH ${seqs.join(',')} (BODY.PEEK[1])\r\n`);
step = 3;
} else if (step === 3) {
if (line.startsWith('A3 OK')) {
socket.write(`A4 LOGOUT\r\n`);
resolve(results);
}
const m = line.match(/^\* (\d+) FETCH/);
if (m) results[m[1]] = { body: '' };
const curr = Object.keys(results).at(-1);
if (curr && line && !line.match(/^\* \d+ FETCH/) && !line.startsWith('A3') && line !== ')') {
results[curr].body += line + '\n';
}
}
}
});
socket.on('error', reject);
});
});
}
const r = await readMessages([954, 990, 997]);
for (const [seq, msg] of Object.entries(r)) {
console.log(`\n=== SEQ ${seq} ===`);
console.log(msg.body.slice(0, 1500));
}

View File

@@ -1,137 +1,185 @@
const { spawnSync } = require('child_process'); const { spawnSync } = require('child_process');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const repoRoot = path.resolve(__dirname, '..'); const repoRoot = path.resolve(__dirname, '..');
const prismaSchemaPath = path.join(repoRoot, 'prisma', 'schema.prisma'); const prismaSchemaPath = path.join(repoRoot, 'prisma', 'schema.prisma');
const generatedSchemaPath = path.join( const generatedClientDir = path.join(repoRoot, 'node_modules', '.prisma', 'client');
repoRoot, const generatedSchemaPath = path.join(generatedClientDir, 'schema.prisma');
'node_modules',
'.prisma', function readFileIfExists(filePath) {
'client', try {
'schema.prisma' return fs.readFileSync(filePath, 'utf8');
); } catch (error) {
if (error && error.code === 'ENOENT') {
function readFileIfExists(filePath) { return null;
try { }
return fs.readFileSync(filePath, 'utf8');
} catch (error) { throw error;
if (error && error.code === 'ENOENT') { }
return null; }
}
function normalizeSchema(schema) {
throw error; return schema.replace(/\s+/g, '');
} }
}
function schemasMatch() {
function normalizeSchema(schema) { const sourceSchema = readFileIfExists(prismaSchemaPath);
return schema.replace(/\s+/g, ''); const generatedSchema = readFileIfExists(generatedSchemaPath);
}
return Boolean(
function schemasMatch() { sourceSchema &&
const sourceSchema = readFileIfExists(prismaSchemaPath); generatedSchema &&
const generatedSchema = readFileIfExists(generatedSchemaPath); normalizeSchema(sourceSchema) === normalizeSchema(generatedSchema)
);
return Boolean( }
sourceSchema &&
generatedSchema && function run(command, args, options = {}) {
normalizeSchema(sourceSchema) === normalizeSchema(generatedSchema) const shouldUseShell =
); process.platform === 'win32' && command.toLowerCase().endsWith('.cmd');
}
const result = spawnSync(command, args, {
function run(command, args, options = {}) { cwd: repoRoot,
const shouldUseShell = encoding: 'utf8',
process.platform === 'win32' && command.toLowerCase().endsWith('.cmd'); stdio: 'pipe',
shell: shouldUseShell,
const result = spawnSync(command, args, { env: {
cwd: repoRoot, ...process.env,
encoding: 'utf8', ...options.env,
stdio: 'pipe', },
shell: shouldUseShell, });
env: {
...process.env, if (result.stdout) {
...options.env, process.stdout.write(result.stdout);
}, }
});
if (result.stderr) {
if (result.stdout) { process.stderr.write(result.stderr);
process.stdout.write(result.stdout); }
}
return result;
if (result.stderr) { }
process.stderr.write(result.stderr);
} function isWSL() {
return (
return result; process.platform === 'linux' &&
} fs.existsSync('/proc/version') &&
fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft')
function isWindowsPrismaRenameLock(output) { );
const text = [output.stdout, output.stderr] }
.filter(Boolean)
.join('\n'); function isWindowsPrismaRenameLock(output) {
const text = [output.stdout, output.stderr]
return ( .filter(Boolean)
process.platform === 'win32' && .join('\n');
text.includes('EPERM: operation not permitted, rename') &&
text.includes('query_engine-windows.dll.node') return (
); process.platform === 'win32' &&
} text.includes('EPERM: operation not permitted, rename') &&
text.includes('query_engine-windows.dll.node')
function runPrismaGenerate() { );
const prismaBin = }
process.platform === 'win32'
? path.join(repoRoot, 'node_modules', '.bin', 'prisma.cmd') function isPrismaCopyfileEio(output) {
: path.join(repoRoot, 'node_modules', '.bin', 'prisma'); const text = [output.stdout, output.stderr]
.filter(Boolean)
const result = run(prismaBin, ['generate']); .join('\n');
if (result.error) { return (
throw result.error; text.includes('EIO: i/o error, copyfile') &&
} (text.includes('libquery_engine-') || text.includes('query_engine-'))
);
if ((result.status ?? 1) === 0) { }
return 0;
} function cleanupPrismaTempFiles() {
if (!fs.existsSync(generatedClientDir)) {
if (!isWindowsPrismaRenameLock(result) || !schemasMatch()) { return;
return result.status ?? 1; }
}
for (const entry of fs.readdirSync(generatedClientDir)) {
console.warn( if (!entry.includes('.tmp')) {
'\nPrisma generate hit a Windows file lock, but the generated client already matches prisma/schema.prisma. Continuing with the existing client.\n' continue;
); }
return 0; try {
} fs.rmSync(path.join(generatedClientDir, entry), { force: true });
} catch (error) {
function runNextBuild() { console.warn(`Failed to remove stale Prisma temp file ${entry}:`, error);
const nextBin = }
process.platform === 'win32' }
? path.join(repoRoot, 'node_modules', '.bin', 'next.cmd') }
: path.join(repoRoot, 'node_modules', '.bin', 'next');
function runPrismaGenerate() {
// WSL needs more aggressive memory settings const prismaBin =
const isWSL = process.platform === 'linux' && require('fs').existsSync('/proc/version') && process.platform === 'win32'
require('fs').readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); ? path.join(repoRoot, 'node_modules', '.bin', 'prisma.cmd')
: path.join(repoRoot, 'node_modules', '.bin', 'prisma');
const memoryLimit = isWSL ? '8192' : '4096';
if (isWSL()) {
return run(nextBin, ['build'], { cleanupPrismaTempFiles();
env: { }
NODE_OPTIONS: `--max-old-space-size=${memoryLimit}`,
SKIP_ENV_VALIDATION: 'true', let result = run(prismaBin, ['generate']);
},
}); if (result.error) {
} throw result.error;
}
const prismaExitCode = runPrismaGenerate();
if (prismaExitCode !== 0) { if ((result.status ?? 1) === 0) {
process.exit(prismaExitCode); return 0;
} }
const nextResult = runNextBuild(); const retryablePrismaFsError =
if (nextResult.error) { isWindowsPrismaRenameLock(result) || isPrismaCopyfileEio(result);
throw nextResult.error;
} if (retryablePrismaFsError) {
cleanupPrismaTempFiles();
process.exit(nextResult.status ?? 1); result = run(prismaBin, ['generate']);
if (result.error) {
throw result.error;
}
if ((result.status ?? 1) === 0) {
return 0;
}
}
if (!retryablePrismaFsError || !schemasMatch()) {
return result.status ?? 1;
}
console.warn(
'\nPrisma generate hit a filesystem copy/rename issue, but the generated client already matches prisma/schema.prisma. Continuing with the existing client.\n'
);
return 0;
}
function runNextBuild() {
const nextBin =
process.platform === 'win32'
? path.join(repoRoot, 'node_modules', '.bin', 'next.cmd')
: path.join(repoRoot, 'node_modules', '.bin', 'next');
const memoryLimit = isWSL() ? '8192' : '4096';
return run(nextBin, ['build'], {
env: {
NODE_OPTIONS: `--max-old-space-size=${memoryLimit}`,
SKIP_ENV_VALIDATION: 'true',
},
});
}
const prismaExitCode = runPrismaGenerate();
if (prismaExitCode !== 0) {
process.exit(prismaExitCode);
}
const nextResult = runNextBuild();
if (nextResult.error) {
throw nextResult.error;
}
process.exit(nextResult.status ?? 1);

View File

@@ -0,0 +1,249 @@
const fs = require("fs");
const path = require("path");
const root = process.cwd();
const scanDirs = ["src", "marketing", "articles", "blog-posts-improved"];
const sourceExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".md", ".mdx"]);
const publicDir = path.join(root, "public");
const appDir = path.join(root, "src", "app");
const ignoredPrefixes = [
"/api/",
"/_next/",
"/auth/",
"/r/",
"/qr/",
"/scan/",
];
const knownDynamicPrefixes = [
"/blog/",
"/learn/",
"/authors/",
"/qr-code-for/",
"/use-cases/",
];
const ctaPatterns = [
/get started/i,
/start free/i,
/try free/i,
/create.*qr/i,
/generate.*qr/i,
/sign up/i,
/pricing/i,
/upgrade/i,
/create.*free/i,
/start tracking/i,
/create.*editable/i,
];
const nonConversionPageParts = [
"/contact/",
"/cookie-policy/",
"/privacy/",
"/press/",
"/authors/",
"/blog/",
"/newsletter/",
];
const findings = [];
const ctas = [];
function walk(dir, files = []) {
if (!fs.existsSync(dir)) return files;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === ".git") continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, files);
} else {
files.push(fullPath);
}
}
return files;
}
function toPosix(value) {
return value.split(path.sep).join("/");
}
function routeFromPageFile(file) {
const rel = toPosix(path.relative(appDir, file));
if (!rel.endsWith("/page.tsx") && !rel.endsWith("/page.ts") && !rel.endsWith("/route.ts")) {
return null;
}
const parts = rel.split("/");
parts.pop();
const routeParts = parts.filter((part) => {
if (!part) return false;
if (part.startsWith("(") && part.endsWith(")")) return false;
return true;
});
if (routeParts.some((part) => part.startsWith("[") && part.endsWith("]"))) return null;
return "/" + routeParts.join("/");
}
function collectRoutes() {
const routes = new Set(["/"]);
for (const file of walk(appDir)) {
const route = routeFromPageFile(file);
if (route) routes.add(route === "/" ? "/" : route.replace(/\/$/, ""));
}
for (const file of walk(publicDir)) {
const rel = "/" + toPosix(path.relative(publicDir, file));
routes.add(rel);
}
return routes;
}
function normalizeHref(rawHref) {
if (!rawHref) return null;
let href = rawHref.trim();
if (!href || href.startsWith("#")) return null;
if (/^(https?:|mailto:|tel:|sms:|javascript:|data:)/i.test(href)) return null;
if (!href.startsWith("/")) return null;
href = href.split("#")[0].split("?")[0];
if (href.length > 1) href = href.replace(/\/$/, "");
return href || "/";
}
function isAllowedDynamicHref(href) {
if (ignoredPrefixes.some((prefix) => href.startsWith(prefix))) return true;
if (href.includes("[") || href.includes("${") || href.includes("`")) return true;
return knownDynamicPrefixes.some((prefix) => href.startsWith(prefix) && href !== prefix.replace(/\/$/, ""));
}
function lineNumber(content, index) {
return content.slice(0, index).split(/\r?\n/).length;
}
function extractHrefMatches(content) {
const matches = [];
const patterns = [
/href\s*=\s*["']([^"']+)["']/g,
/href\s*=\s*{\s*["']([^"']+)["']\s*}/g,
/router\.push\(\s*["']([^"']+)["']\s*\)/g,
];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
matches.push({ href: match[1], index: match.index });
}
}
return matches;
}
function extractAnchors(content) {
const anchors = [];
const linkPattern = /<Link\b[\s\S]*?href\s*=\s*(?:["']([^"']+)["']|{\s*["']([^"']+)["']\s*})[\s\S]*?>([\s\S]*?)<\/Link>/g;
const anchorPattern = /<a\b[\s\S]*?href\s*=\s*(?:["']([^"']+)["']|{\s*["']([^"']+)["']\s*})[\s\S]*?>([\s\S]*?)<\/a>/g;
for (const pattern of [linkPattern, anchorPattern]) {
let match;
while ((match = pattern.exec(content)) !== null) {
const href = match[1] || match[2];
const text = match[3]
.replace(/<[^>]*>/g, " ")
.replace(/\{[^}]*\}/g, " ")
.replace(/\s+/g, " ")
.trim();
anchors.push({ href, text, index: match.index });
}
}
return anchors;
}
function sourceFiles() {
const files = [];
for (const dir of scanDirs) {
for (const file of walk(path.join(root, dir))) {
if (sourceExtensions.has(path.extname(file))) files.push(file);
}
}
return files;
}
function check() {
const routes = collectRoutes();
for (const file of sourceFiles()) {
const content = fs.readFileSync(file, "utf8");
const rel = toPosix(path.relative(root, file));
for (const item of extractHrefMatches(content)) {
const href = normalizeHref(item.href);
if (!href) continue;
if (routes.has(href) || isAllowedDynamicHref(href)) continue;
findings.push({
type: "broken-internal-link",
file: rel,
line: lineNumber(content, item.index),
href,
});
}
for (const anchor of extractAnchors(content)) {
const text = anchor.text || "";
if (!ctaPatterns.some((pattern) => pattern.test(text))) continue;
const href = normalizeHref(anchor.href);
const status = !href
? "external-or-non-http"
: routes.has(href) || isAllowedDynamicHref(href)
? "ok"
: "broken";
ctas.push({
file: rel,
line: lineNumber(content, anchor.index),
text: text.slice(0, 100),
href: anchor.href,
status,
});
}
}
const brokenCtas = ctas.filter((cta) => cta.status === "broken");
const weakFiles = sourceFiles().filter((file) => {
const rel = toPosix(path.relative(root, file));
if (!rel.includes("src/app/") || !rel.endsWith("/page.tsx")) return false;
if (!rel.includes("(marketing)")) return false;
if (rel.includes("[")) return false;
if (nonConversionPageParts.some((part) => rel.includes(part))) return false;
const content = fs.readFileSync(file, "utf8");
return !extractAnchors(content).some((anchor) =>
ctaPatterns.some((pattern) => pattern.test(anchor.text || "")),
);
});
const report = {
checkedAt: new Date().toISOString(),
routeCount: routes.size,
filesChecked: sourceFiles().length,
brokenInternalLinks: findings,
ctaSummary: {
total: ctas.length,
broken: brokenCtas.length,
sample: ctas.slice(0, 50),
},
pagesWithoutObviousCta: weakFiles.map((file) => toPosix(path.relative(root, file))).slice(0, 100),
};
console.log(JSON.stringify(report, null, 2));
if (findings.length > 0 || brokenCtas.length > 0) {
process.exitCode = 1;
}
}
check();

View File

@@ -0,0 +1,421 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const OUTPUT_DIR = path.resolve(process.cwd(), 'output', 'outreach');
const TARGET_PER_NICHE = Number(process.env.LEADS_PER_NICHE || 200);
const CONCURRENCY = Number(process.env.LEAD_FETCH_CONCURRENCY || 8);
const OVERPASS_DELAY_MS = Number(process.env.OVERPASS_DELAY_MS || 20000);
const OVERPASS_429_DELAY_MS = Number(process.env.OVERPASS_429_DELAY_MS || 90000);
const OVERPASS_MAX_ATTEMPTS = Number(process.env.OVERPASS_MAX_ATTEMPTS || 6);
const OVERPASS_URLS = [
'https://overpass-api.de/api/interpreter',
];
const metros = [
['New York', 'NY', 40.7128, -74.006],
['Los Angeles', 'CA', 34.0522, -118.2437],
['Chicago', 'IL', 41.8781, -87.6298],
['Houston', 'TX', 29.7604, -95.3698],
['Phoenix', 'AZ', 33.4484, -112.074],
['Philadelphia', 'PA', 39.9526, -75.1652],
['San Antonio', 'TX', 29.4241, -98.4936],
['San Diego', 'CA', 32.7157, -117.1611],
['Dallas', 'TX', 32.7767, -96.797],
['San Jose', 'CA', 37.3382, -121.8863],
['Austin', 'TX', 30.2672, -97.7431],
['Jacksonville', 'FL', 30.3322, -81.6557],
['Fort Worth', 'TX', 32.7555, -97.3308],
['Columbus', 'OH', 39.9612, -82.9988],
['Charlotte', 'NC', 35.2271, -80.8431],
['San Francisco', 'CA', 37.7749, -122.4194],
['Seattle', 'WA', 47.6062, -122.3321],
['Denver', 'CO', 39.7392, -104.9903],
['Miami', 'FL', 25.7617, -80.1918],
['Nashville', 'TN', 36.1627, -86.7816],
];
const niches = [
{
id: 'photographers',
label: 'Photographers',
targetUseCase: 'portfolio, booking, print cards, event galleries',
queries: [
['craft', 'photographer'],
['shop', 'photo_studio'],
['shop', 'photo'],
],
},
{
id: 'restaurants',
label: 'Restaurants',
targetUseCase: 'menu QR codes, table tents, review QR codes, coupons',
queries: [
['amenity', 'restaurant'],
['amenity', 'cafe'],
],
},
{
id: 'real_estate',
label: 'Real Estate',
targetUseCase: 'yard signs, flyers, open houses, property sheets',
queries: [
['office', 'estate_agent'],
],
},
{
id: 'events_venues',
label: 'Events & Venues',
targetUseCase: 'tickets, schedules, check-in, feedback and post-event links',
queries: [
['amenity', 'events_venue'],
['amenity', 'theatre'],
['amenity', 'conference_centre'],
['tourism', 'attraction'],
],
},
{
id: 'wellness_beauty',
label: 'Wellness & Beauty',
targetUseCase: 'booking links, price lists, reviews, loyalty offers',
queries: [
['shop', 'beauty'],
['shop', 'hairdresser'],
['leisure', 'fitness_centre'],
['amenity', 'spa'],
],
},
];
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function csvEscape(value) {
const text = String(value ?? '');
if (/[",\n\r]/.test(text)) {
return `"${text.replaceAll('"', '""')}"`;
}
return text;
}
function normalizeWebsite(raw) {
if (!raw) return '';
let value = String(raw).trim();
if (!value) return '';
if (value.startsWith('mailto:') || value.includes('@') && !value.includes('/')) return '';
if (!/^https?:\/\//i.test(value)) value = `https://${value}`;
try {
const url = new URL(value);
if (!url.hostname.includes('.')) return '';
url.hash = '';
return url.toString().replace(/\/$/, '');
} catch {
return '';
}
}
function getTag(tags, names) {
for (const name of names) {
if (tags?.[name]) return tags[name];
}
return '';
}
function buildOverpassQuery(niche, metro, offset) {
const [, , lat, lon] = metro;
const radius = 25000 + offset * 10000;
const clauses = niche.queries.flatMap(([key, value]) => [
`nwr(around:${radius},${lat},${lon})["${key}"="${value}"]["website"];`,
`nwr(around:${radius},${lat},${lon})["${key}"="${value}"]["contact:website"];`,
`nwr(around:${radius},${lat},${lon})["${key}"="${value}"]["email"];`,
`nwr(around:${radius},${lat},${lon})["${key}"="${value}"]["contact:email"];`,
]).join('\n');
return `[out:json][timeout:45];
(
${clauses}
);
out tags center ${Math.min(TARGET_PER_NICHE * 2, 500)};`;
}
async function fetchOverpass(query, attempt = 0) {
const endpoint = OVERPASS_URLS[attempt % OVERPASS_URLS.length];
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 90000);
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' },
body: new URLSearchParams({ data: query }),
signal: controller.signal,
});
if (!response.ok) {
if (response.status === 429 && attempt < OVERPASS_MAX_ATTEMPTS) {
const waitMs = OVERPASS_429_DELAY_MS + attempt * 30000;
console.warn(`Overpass rate limited; waiting ${Math.round(waitMs / 1000)}s before retry ${attempt + 1}/${OVERPASS_MAX_ATTEMPTS}`);
await sleep(waitMs);
return fetchOverpass(query, attempt + 1);
}
if (attempt < OVERPASS_MAX_ATTEMPTS) {
await sleep(5000 * (attempt + 1));
return fetchOverpass(query, attempt + 1);
}
throw new Error(`Overpass ${response.status} ${response.statusText}`);
}
return response.json();
} catch (error) {
if (attempt < OVERPASS_MAX_ATTEMPTS) {
await sleep(5000 * (attempt + 1));
return fetchOverpass(query, attempt + 1);
}
throw error;
} finally {
clearTimeout(timer);
}
}
function elementToLead(element, niche, metro) {
const tags = element.tags || {};
const website = normalizeWebsite(getTag(tags, ['contact:website', 'website', 'url']));
const email = getTag(tags, ['contact:email', 'email']);
const phone = getTag(tags, ['contact:phone', 'phone']);
const street = [tags['addr:housenumber'], tags['addr:street']].filter(Boolean).join(' ');
const city = tags['addr:city'] || metro[0];
const state = tags['addr:state'] || metro[1];
return {
niche: niche.id,
niche_label: niche.label,
company: tags.name || '',
website,
email,
phone,
city,
state,
country: 'US',
street,
source: 'OpenStreetMap Overpass',
source_id: `${element.type}/${element.id}`,
source_url: `https://www.openstreetmap.org/${element.type}/${element.id}`,
personalization_signal: '',
qr_use_case: niche.targetUseCase,
lead_score: 0,
email_source: email ? 'osm' : '',
opt_out_required: 'yes',
};
}
function visibleTextEmails(text) {
const normalized = text
.replaceAll('[at]', '@')
.replaceAll('(at)', '@')
.replaceAll(' at ', '@')
.replaceAll('[dot]', '.')
.replaceAll('(dot)', '.')
.replaceAll(' dot ', '.');
const matches = normalized.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g) || [];
return [...new Set(matches.map((email) => email.toLowerCase()))]
.filter((email) => !email.endsWith('.png') && !email.endsWith('.jpg') && !email.includes('example.com'))
.filter((email) => !email.includes('wixpress.com') && !email.includes('sentry.io'));
}
function extractContactLinks(html, baseUrl) {
const links = [];
const regex = /href=["']([^"']+)["']/gi;
let match;
while ((match = regex.exec(html))) {
const href = match[1];
if (/^(mailto:|tel:)/i.test(href)) continue;
if (!/(contact|about|team|booking|book|wedding|private-events|catering|visit|location)/i.test(href)) continue;
try {
const url = new URL(href, baseUrl);
if (url.hostname === new URL(baseUrl).hostname) {
url.hash = '';
links.push(url.toString());
}
} catch {
// Ignore malformed links.
}
}
return [...new Set(links)].slice(0, 3);
}
async function fetchText(url) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(url, {
headers: {
'user-agent': 'QR Master lead research bot (+https://qrmaster.net/contact)',
accept: 'text/html,application/xhtml+xml',
},
signal: controller.signal,
redirect: 'follow',
});
if (!response.ok) return '';
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('text/html')) return '';
return await response.text();
} catch {
return '';
} finally {
clearTimeout(timer);
}
}
async function enrichLead(lead) {
if (!lead.website || lead.email) {
return scoreLead(lead);
}
const homepage = await fetchText(lead.website);
const emails = visibleTextEmails(homepage);
const contactLinks = extractContactLinks(homepage, lead.website);
for (const link of contactLinks) {
if (emails.length > 0) break;
const html = await fetchText(link);
emails.push(...visibleTextEmails(html));
}
const uniqueEmails = [...new Set(emails)];
if (uniqueEmails.length > 0) {
lead.email = uniqueEmails[0];
lead.email_source = 'website';
}
return scoreLead(lead);
}
function scoreLead(lead) {
let score = 30;
if (lead.website) score += 20;
if (lead.email) score += 30;
if (lead.phone) score += 5;
if (!/(gmail|yahoo|hotmail|outlook|icloud)\.com$/i.test(lead.email || '')) score += lead.email ? 10 : 0;
if (lead.niche === 'real_estate' || lead.niche === 'restaurants') score += 5;
const signalByNiche = {
photographers: `${lead.company} can use dynamic QR codes on print cards, gallery cards, event handouts, and portfolio links.`,
restaurants: `${lead.company} can use dynamic QR codes for menus, table tents, reviews, coupons, and seasonal specials.`,
real_estate: `${lead.company} can use dynamic QR codes on yard signs, flyers, property sheets, and open house material.`,
events_venues: `${lead.company} can use dynamic QR codes for schedules, ticketing, venue maps, check-in, and post-event feedback.`,
wellness_beauty: `${lead.company} can use dynamic QR codes for booking pages, service menus, price lists, reviews, and loyalty offers.`,
};
lead.lead_score = Math.min(score, 100);
lead.personalization_signal = signalByNiche[lead.niche] || '';
return lead;
}
async function mapLimit(items, limit, mapper) {
const results = [];
let index = 0;
async function worker() {
while (index < items.length) {
const current = index++;
results[current] = await mapper(items[current], current);
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return results;
}
async function collectNiche(niche) {
const leadsByKey = new Map();
for (let pass = 0; pass < 2 && leadsByKey.size < TARGET_PER_NICHE * 2; pass++) {
for (const metro of metros) {
if (leadsByKey.size >= TARGET_PER_NICHE * 2) break;
const query = buildOverpassQuery(niche, metro, pass);
try {
const data = await fetchOverpass(query);
for (const element of data.elements || []) {
const lead = elementToLead(element, niche, metro);
if (!lead.company) continue;
if (!lead.website && !lead.email) continue;
const key = lead.website || `${lead.company}|${lead.city}|${lead.state}`.toLowerCase();
if (!leadsByKey.has(key)) leadsByKey.set(key, lead);
}
} catch (error) {
console.warn(`[${niche.id}] ${metro[0]} skipped: ${error.message}`);
}
await sleep(OVERPASS_DELAY_MS);
}
}
const rawLeads = [...leadsByKey.values()].slice(0, TARGET_PER_NICHE * 2);
console.log(`[${niche.id}] collected ${rawLeads.length}; enriching...`);
const enriched = await mapLimit(rawLeads, CONCURRENCY, enrichLead);
return enriched
.filter((lead) => lead.website || lead.email)
.sort((a, b) => b.lead_score - a.lead_score)
.slice(0, TARGET_PER_NICHE);
}
function toCsv(leads) {
const headers = [
'niche',
'niche_label',
'company',
'website',
'email',
'email_source',
'phone',
'city',
'state',
'country',
'street',
'lead_score',
'qr_use_case',
'personalization_signal',
'source',
'source_id',
'source_url',
'opt_out_required',
];
return [
headers.join(','),
...leads.map((lead) => headers.map((header) => csvEscape(lead[header])).join(',')),
].join('\n');
}
async function main() {
await fs.mkdir(OUTPUT_DIR, { recursive: true });
const allLeads = [];
for (const niche of niches) {
const leads = await collectNiche(niche);
allLeads.push(...leads);
const dated = new Date().toISOString().slice(0, 10);
await fs.writeFile(path.join(OUTPUT_DIR, `qrmaster-us-leads-${niche.id}-${dated}.csv`), toCsv(leads), 'utf8');
await fs.writeFile(path.join(OUTPUT_DIR, `qrmaster-us-leads-${niche.id}-${dated}.json`), JSON.stringify(leads, null, 2), 'utf8');
console.log(`[${niche.id}] kept ${leads.length}`);
}
const byKey = new Map();
for (const lead of allLeads) {
const key = lead.email || lead.website || `${lead.company}|${lead.city}|${lead.state}`.toLowerCase();
if (!byKey.has(key)) byKey.set(key, lead);
}
const deduped = [...byKey.values()].sort((a, b) => b.lead_score - a.lead_score);
const dated = new Date().toISOString().slice(0, 10);
const csvPath = path.join(OUTPUT_DIR, `qrmaster-us-leads-${dated}.csv`);
const jsonPath = path.join(OUTPUT_DIR, `qrmaster-us-leads-${dated}.json`);
await fs.writeFile(csvPath, toCsv(deduped), 'utf8');
await fs.writeFile(jsonPath, JSON.stringify(deduped, null, 2), 'utf8');
const summary = niches.map((niche) => {
const leads = deduped.filter((lead) => lead.niche === niche.id);
const withEmail = leads.filter((lead) => lead.email).length;
return `${niche.label}: ${leads.length} leads, ${withEmail} emails`;
}).join('\n');
console.log(`\nWrote ${deduped.length} leads`);
console.log(csvPath);
console.log(jsonPath);
console.log(summary);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

View File

@@ -0,0 +1,326 @@
import { promises as dns } from "node:dns";
import { readdir, readFile, mkdir, writeFile, stat } from "node:fs/promises";
import path from "node:path";
const root = process.cwd();
const leadRoot = path.resolve(root, process.argv[2] || "Leads");
const excludeFile = path.resolve(root, process.argv[3] || "Leads/lead_emails_1000_2026-05-25.csv");
const outputDir = path.resolve(root, process.argv[4] || "Leads/validated");
const dateStamp = new Date().toISOString().slice(0, 10);
const emailPattern = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
const strictEmailPattern = /^[A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?(?:\.[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?)+$/i;
const allowedExtensions = new Set([".csv", ".txt", ".md", ".json"]);
const generatedPrefixes = [
"lead_email_validation_all_",
"lead_email_validation_valid_remaining_",
"lead_email_validation_unknown_remaining_",
"lead_email_validation_invalid_",
"lead_email_validation_summary_",
];
const blockedLeadDomains = new Set([
"qrmaster.net",
]);
const empiricalHighConfidenceDomains = new Set([
"gmail.com",
"googlemail.com",
"accor.com",
"hotelbb.com",
"losteria.de",
"breizhcafe.com",
]);
const empiricalLowConfidenceDomains = new Set([
"aon.at",
"countryinn.com",
"hilton.com",
"hyatt.com",
"motel-one.com",
"novum-hotels.de",
"riu.com",
]);
function csvCell(value) {
const text = String(value ?? "");
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
}
function toCsv(rows, columns) {
const lines = [columns.map(csvCell).join(",")];
for (const row of rows) {
lines.push(columns.map((column) => csvCell(row[column])).join(","));
}
return `${lines.join("\r\n")}\r\n`;
}
async function collectInputFiles(inputPath) {
const inputStat = await stat(inputPath);
if (inputStat.isFile()) {
return [inputPath];
}
if (!inputStat.isDirectory()) {
throw new Error(`Input path is not a file or directory: ${inputPath}`);
}
return walkFiles(inputPath);
}
async function walkFiles(dir) {
const entries = await readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...await walkFiles(fullPath));
continue;
}
if (!entry.isFile()) continue;
if (!allowedExtensions.has(path.extname(entry.name).toLowerCase())) continue;
if (generatedPrefixes.some((prefix) => entry.name.startsWith(prefix))) continue;
files.push(fullPath);
}
return files.sort((a, b) => a.localeCompare(b));
}
async function extractEmailsFromFile(filePath) {
try {
const content = await readFile(filePath, "utf8");
return [...content.matchAll(emailPattern)].map((match) =>
match[0].trim().replace(/\.+$/, "").toLowerCase(),
);
} catch {
return [];
}
}
async function loadExcludedEmails(filePathsArg) {
const excluded = new Set();
const filePaths = String(filePathsArg || "")
.split(";")
.map((filePath) => filePath.trim())
.filter(Boolean);
for (const filePath of filePaths) {
try {
await stat(filePath);
} catch {
continue;
}
const emails = await extractEmailsFromFile(filePath);
for (const email of emails) excluded.add(email);
}
return excluded;
}
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("dns_timeout")), ms);
}),
]);
}
async function checkDomain(domain) {
try {
const mxRecords = await withTimeout(dns.resolveMx(domain), 2500);
if (mxRecords.length > 0) {
return {
dns_status: "mx",
mx_hosts: mxRecords
.sort((a, b) => a.priority - b.priority)
.map((record) => record.exchange)
.join(";"),
reason: "domain_has_mx",
};
}
} catch {
// Fall through to A lookup. Some domains can receive via address fallback.
}
try {
const aRecords = await withTimeout(dns.resolve4(domain), 2000);
if (aRecords.length > 0) {
return {
dns_status: "a_only",
mx_hosts: "",
reason: "domain_has_a_record_but_no_mx",
};
}
} catch {
// Classified below.
}
return {
dns_status: "no_dns",
mx_hosts: "",
reason: "no_mx_or_a_record",
};
}
async function mapLimit(items, limit, worker) {
const results = new Map();
let index = 0;
async function runWorker() {
while (index < items.length) {
const currentIndex = index++;
const item = items[currentIndex];
if ((currentIndex + 1) % 100 === 0) {
console.log(`DNS checked ${currentIndex + 1} / ${items.length} domains...`);
}
results.set(item, await worker(item));
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runWorker));
return results;
}
function getConfidence(status, domain) {
if (status !== "valid") {
return {
confidence: "reject",
confidence_reason: "not_dns_valid",
};
}
if (empiricalLowConfidenceDomains.has(domain)) {
return {
confidence: "low",
confidence_reason: "empirical_low_smartlead_valid_rate",
};
}
if (empiricalHighConfidenceDomains.has(domain)) {
return {
confidence: "high",
confidence_reason: "empirical_high_smartlead_valid_rate",
};
}
return {
confidence: "medium",
confidence_reason: "dns_valid_unproven_domain",
};
}
await mkdir(outputDir, { recursive: true });
const excludeEmails = await loadExcludedEmails(excludeFile);
const files = await collectInputFiles(leadRoot);
const emailSources = new Map();
for (const file of files) {
const emails = await extractEmailsFromFile(file);
for (const email of emails) {
if (!emailSources.has(email)) emailSources.set(email, []);
const sources = emailSources.get(email);
if (sources.length < 5) sources.push(file);
}
}
const domains = [...new Set(
[...emailSources.keys()]
.filter((email) => strictEmailPattern.test(email))
.map((email) => email.split("@")[1]),
)].sort((a, b) => a.localeCompare(b));
console.log(`Files scanned: ${files.length}`);
console.log(`Unique emails found: ${emailSources.size}`);
console.log(`Domains to check: ${domains.length}`);
const dnsResults = await mapLimit(domains, 80, checkDomain);
const results = [...emailSources.keys()].sort((a, b) => a.localeCompare(b)).map((email) => {
const syntaxValid = strictEmailPattern.test(email);
const domain = email.includes("@") ? email.split("@")[1] : "";
const reserved = /^(example|test|invalid|localhost)(\.|$)/i.test(domain);
const dnsResult = dnsResults.get(domain);
let status = "invalid";
let reason = "invalid_syntax";
let dnsStatus = "";
let mxHosts = "";
if (syntaxValid && blockedLeadDomains.has(domain)) {
reason = "internal_or_generated_domain";
} else if (syntaxValid && reserved) {
reason = "reserved_or_test_domain";
} else if (syntaxValid && dnsResult?.dns_status === "mx") {
status = "valid";
reason = dnsResult.reason;
dnsStatus = dnsResult.dns_status;
mxHosts = dnsResult.mx_hosts;
} else if (syntaxValid && dnsResult?.dns_status === "a_only") {
status = "unknown";
reason = dnsResult.reason;
dnsStatus = dnsResult.dns_status;
} else if (syntaxValid) {
reason = dnsResult?.reason || "dns_not_checked";
dnsStatus = dnsResult?.dns_status || "";
}
const confidenceResult = getConfidence(status, domain);
return {
email,
status,
reason,
confidence: confidenceResult.confidence,
confidence_reason: confidenceResult.confidence_reason,
domain,
dns_status: dnsStatus,
mx_hosts: mxHosts,
already_uploaded: excludeEmails.has(email) ? "true" : "false",
source_count: emailSources.get(email).length,
first_source: emailSources.get(email)[0],
};
});
const allOut = path.join(outputDir, `lead_email_validation_all_${dateStamp}.csv`);
const validOut = path.join(outputDir, `lead_email_validation_valid_remaining_${dateStamp}.csv`);
const highConfidenceOut = path.join(outputDir, `lead_email_validation_high_confidence_remaining_${dateStamp}.csv`);
const unknownOut = path.join(outputDir, `lead_email_validation_unknown_remaining_${dateStamp}.csv`);
const invalidOut = path.join(outputDir, `lead_email_validation_invalid_${dateStamp}.csv`);
const summaryOut = path.join(outputDir, `lead_email_validation_summary_${dateStamp}.txt`);
const validRemaining = results.filter((row) => row.status === "valid" && row.already_uploaded !== "true");
const highConfidenceRemaining = results.filter((row) =>
row.status === "valid" &&
row.confidence === "high" &&
row.already_uploaded !== "true"
);
const unknownRemaining = results.filter((row) => row.status === "unknown" && row.already_uploaded !== "true");
const invalid = results.filter((row) => row.status === "invalid");
await writeFile(
allOut,
toCsv(results, ["email", "status", "reason", "confidence", "confidence_reason", "domain", "dns_status", "mx_hosts", "already_uploaded", "source_count", "first_source"]),
"utf8",
);
await writeFile(validOut, toCsv(validRemaining.map(({ email }) => ({ email })), ["email"]), "utf8");
await writeFile(highConfidenceOut, toCsv(highConfidenceRemaining.map(({ email }) => ({ email })), ["email"]), "utf8");
await writeFile(unknownOut, toCsv(unknownRemaining, ["email", "reason", "domain"]), "utf8");
await writeFile(invalidOut, toCsv(invalid, ["email", "reason", "domain"]), "utf8");
const summary = [
`Lead email validation summary - ${dateStamp}`,
`Lead root: ${leadRoot}`,
`Files scanned: ${files.length}`,
`Unique emails found: ${results.length}`,
`Already uploaded/excluded: ${results.filter((row) => row.already_uploaded === "true").length}`,
`Valid total: ${results.filter((row) => row.status === "valid").length}`,
`Valid remaining: ${validRemaining.length}`,
`High-confidence valid remaining: ${highConfidenceRemaining.length}`,
`Unknown remaining: ${unknownRemaining.length}`,
`Invalid total: ${invalid.length}`,
`All report: ${allOut}`,
`Valid remaining upload file: ${validOut}`,
`High-confidence upload file: ${highConfidenceOut}`,
`Unknown remaining review file: ${unknownOut}`,
`Invalid report: ${invalidOut}`,
"",
].join("\n");
await writeFile(summaryOut, summary, "utf8");
console.log(summary);

312
social-content-30-days.md Normal file
View File

@@ -0,0 +1,312 @@
# 30-Day Social Content Plan for QR Master
## Positioning
- Product: QR Master
- Angle: Dynamic QR codes, scan analytics, bulk creation, privacy-first workflows
- Goal: Brand awareness, traffic, signups, and founder-style credibility
- Audience: Restaurants, agencies, events, retail/packaging, operations-heavy SMBs
## Content Pillars
1. Pain and cost of static QR codes
2. Dynamic QR value and flexibility
3. Analytics and measurable ROI
4. Bulk creation and operational scale
5. Privacy-first / GDPR-friendly trust
6. Build in public / founder narrative
## Content Mix Target
- Educational: 18 days (60%)
- Storytelling: 8 days (27%)
- Selling: 4 days (13%)
- Note: This is close to the requested 60/25/15 split and avoids forcing weak promo posts.
## A/B Testing Setup
Use the month as a simple content experiment instead of 30 disconnected posts.
### Test 1: Hook Style
- A: Pain-led hook
- B: ROI-led hook
- Primary metric: Engagement rate on X
- Secondary metric: Profile clicks
### Test 2: CTA Style
- A: Soft CTA ("Curious how your team handles this?")
- B: Direct CTA ("Try QR Master")
- Primary metric: Link clicks
- Guardrail: Engagement rate should not drop sharply
### Test 3: Proof Angle
- A: Cost-saving proof
- B: Privacy/GDPR proof
- Primary metric: Saves and comments
### Test 4: Visual Angle
- A: Real-world print use case
- B: Product/dashboard visual
- Primary metric: Instagram saves
### Tracking Notes
- Run A/B by alternating styles every other relevant day
- Do not change the post mid-test
- Review after Days 10, 20, and 30
- Best early KPI set: impressions, engagement rate, profile visits, link clicks, comments
---
## 30-Day Calendar
### Day 1
- Content type: Selling
- X: Static QR codes are easy to generate. The expensive part starts when the link changes after print. QR Master helps you update destinations after print, track scans, and avoid unnecessary reprints.
- Facebook: Most businesses do not have a QR code problem. They have a QR management problem. QR Master helps teams update links after print, measure scans, and stay flexible without starting over every time something changes.
- Instagram: Printing a QR code is easy. Managing it after the link changes is where it gets expensive. QR Master gives you dynamic QR codes, scan analytics, and a more professional workflow. Link in bio.
- Image prompt: Premium 4:5 SaaS visual showing printed flyers and menus with QR codes beside a smartphone dashboard, modern cafe or business setting, high-end product photography, soft daylight, bold headline space, deep green and charcoal palette.
- X Community: Build in Public: We are building QR Master around one simple pain point: the real problem is not creating QR codes, it is managing them after they are already printed.
### Day 2
- Content type: Educational
- X: A wrong link on a printed flyer is not a design mistake. It is an operational cost. Dynamic QR codes fix that.
- Facebook: One small URL change can turn printed materials into waste. Dynamic QR codes let your team update the destination without reprinting everything.
- Instagram: One printed QR code. Multiple future changes. That is the difference between static and dynamic.
- Image prompt: Minimal close-up of a printed flyer with a QR code and a red "old link" concept contrasted with a clean updated mobile dashboard.
- X Community: Startup Community: Simple products win when they remove expensive friction. For us, that friction is reprinting because one QR destination changed.
### Day 3
- Content type: Educational
- X: If you cannot measure scans, your QR code is just decoration. Analytics turns it into a channel.
- Facebook: QR codes become more valuable when they are measurable. Scan analytics help you understand which campaigns, materials, and locations are actually working.
- Instagram: A QR code should not just send traffic. It should give you visibility.
- Image prompt: Sleek phone screen with analytics metrics next to printed marketing assets, premium SaaS dashboard aesthetic.
- X Community: No Code Community: Curious how no-code teams handle QR tracking today. Most tools generate the code. Fewer help manage and measure it.
### Day 4
- Content type: Educational
- X: Restaurants do not want to reprint menus every time something changes. They want one QR code that keeps working.
- Facebook: For restaurants, dynamic QR codes are not a nice-to-have. They are a practical way to handle menu updates without reprinting every time.
- Instagram: One menu QR. Update anytime. Less printing, less stress, more flexibility.
- Image prompt: Elegant restaurant table with menu stand QR code, phone showing updated menu destination, warm lighting, premium hospitality look.
- X Community: Build in Public: One of our strongest use cases is restaurants. The pain is obvious: menu changes are frequent, reprints are annoying, and speed matters.
### Day 5
- Content type: Educational
- X: Bulk QR creation is underrated. Creating one code is easy. Creating 500 cleanly is a workflow.
- Facebook: Bulk creation matters when your team works with packaging, labels, campaigns, or events at scale. That is where simple generators usually fall apart.
- Instagram: One QR code is a task. 500 QR codes is an operation.
- Image prompt: Spreadsheet to QR workflow visual, CSV rows transforming into branded QR code sheets, modern SaaS illustration-photo hybrid.
- X Community: Startup Community: A lot of software looks useful in demos. Bulk workflows are where you find out whether it is a product or just a feature.
### Day 6
- Content type: Educational
- X: Privacy matters more than marketers admit. If your QR analytics ignore GDPR realities, that becomes a risk, not a feature.
- Facebook: Many teams want scan data, but they also want a cleaner privacy story. That is why privacy-first analytics matter.
- Instagram: Better analytics should not come with worse privacy.
- Image prompt: Clean dashboard plus subtle privacy shield iconography, professional B2B look, no cyber-security cliches, muted green palette.
- X Community: No Code Community: How are builders here balancing analytics and privacy? That tradeoff shows up fast once QR workflows become client-facing.
### Day 7
- Content type: Storytelling
- X: Build in public note: one of our clearest positioning lessons has been this. We are not trying to be another QR code generator. We are building QR Master as a management layer for printed-to-digital workflows.
- Facebook: Our product direction is simple: less focus on generating a code, more focus on changing, measuring, and scaling it after launch.
- Instagram: Less QR generator. More QR workflow.
- Image prompt: Founder-style product shot with dashboard on laptop and printed assets on desk, calm European startup aesthetic.
- X Community: Build in Public: Positioning insight: "QR generator" is crowded. "Professional QR workflow" is much more interesting.
### Day 8
- Content type: Educational
- X: Pain-led test: Static QR codes are cheap until the campaign URL changes. Then they get expensive fast.
- Facebook: Static QR codes seem low-cost at first. The real cost appears later when you need to update the destination and your materials are already printed.
- Instagram: Cheap to generate. Expensive to fix later.
- Image prompt: Bold split-scene visual showing cheap creation on one side, expensive reprint boxes on the other.
- X Community: Startup Community: People buy the "easy setup." They stay for the avoided operational mess.
### Day 9
- Content type: Educational
- X: ROI-led test: A dynamic QR code can save far more in reprint cost than it costs to use.
- Facebook: The ROI of dynamic QR codes is not theoretical. It comes from avoiding waste, moving faster, and keeping printed assets flexible.
- Instagram: Dynamic QR codes are not just more flexible. They are often the cheaper decision.
- Image prompt: ROI-focused business visual with printed assets, subtle savings graph, clean premium B2B layout.
- X Community: No Code Community: Great no-code workflows reduce manual rework. Dynamic QR management fits that exact pattern.
### Day 10
- Content type: Storytelling
- X: One pattern we keep noticing while building QR Master: businesses already have offline attention. The missing piece is knowing what happens after the scan.
- Facebook: A recurring insight from this space is that printed materials already do part of the job. What businesses often lack is visibility into what happens after someone scans.
- Instagram: Offline attention is already there. Measurement is the missing layer.
- Image prompt: Product packaging with QR code connected visually to analytics dashboard, premium retail look.
- X Community: Build in Public: Big theme we keep coming back to: printed materials should not be dead ends.
### Day 11
- Content type: Educational
- X: Most QR tools optimize for generation. Businesses actually need flexibility after launch.
- Facebook: The real business value is not in making the first QR code. It is in being able to adapt when your link, offer, or content changes later.
- Instagram: The first QR code is easy. The second version is where the product matters.
- Image prompt: Clean product UI showing edit destination flow, minimal dashboard-first composition.
- X Community: Startup Community: Good SaaS often wins by focusing on the "after setup" problem.
### Day 12
- Content type: Educational
- X: Menus, event flyers, table cards, packaging inserts, product labels. Same pattern: print once, update later.
- Facebook: We like QR workflows because they solve the same operational problem across very different industries: printed assets need flexibility.
- Instagram: Print once. Update later. Repeat without chaos.
- Image prompt: Collage of multiple real-world QR use cases, menus, event badges, labels, packaging, cohesive premium visual treatment.
- X Community: No Code Community: Cross-industry tools usually win when the workflow pain is the same even if the use case looks different.
### Day 13
- Content type: Selling
- X: Direct CTA test: If your team relies on printed assets, try QR Master and stop treating every link change like a mini-crisis.
- Facebook: If printed materials are part of your workflow, QR Master helps turn them into something more flexible, measurable, and easier to manage.
- Instagram: If print is still part of your business, your QR workflow should be better than "hope the link never changes."
- Image prompt: Conversion-focused SaaS ad visual with CTA space, clean printed collateral and dashboard.
- X Community: Build in Public: Testing more direct CTA language this week to see whether practical urgency beats softer education.
### Day 14
- Content type: Storytelling
- X: One question we keep coming back to while building QR Master: how are teams actually handling QR updates after materials are already printed?
- Facebook: The more we look at real QR workflows, the more this question matters: what does your team do when a flyer, menu, or package is already out in the world and the destination changes?
- Instagram: Quick founder question: how are you handling QR updates after print today?
- Image prompt: Question-led social visual with neutral high-end workspace, printed assets and phone, softer editorial composition.
- X Community: Startup Community: Sometimes the best growth post is just a clear question around a painful workflow.
### Day 15
- Content type: Educational
- X: QR analytics are especially useful for campaigns that live partly offline. You can finally see what print is doing.
- Facebook: For teams running print campaigns, QR analytics add something valuable: measurable outcomes instead of guesswork.
- Instagram: Print does not have to be unmeasurable anymore.
- Image prompt: Marketing campaign board, flyer, poster, and analytics dashboard visual, sophisticated marketing ops style.
- X Community: No Code Community: Offline-to-online attribution is still messy. QR analytics can simplify part of it.
### Day 16
- Content type: Educational
- X: Privacy proof test: Scan tracking should not force businesses into a weak privacy position. Better analytics and better privacy can coexist.
- Facebook: Privacy-conscious analytics matter for teams that need measurement without creating unnecessary legal or trust issues.
- Instagram: Better data. Cleaner privacy story.
- Image prompt: Refined dashboard visual with subtle compliance or privacy cues, no overly technical design.
- X Community: Build in Public: We think privacy-first messaging is underused in SaaS until buyers start asking hard questions.
### Day 17
- Content type: Educational
- X: Cost-saving proof test: Reprinting is not just annoying. It is a hidden cost line that dynamic QR codes reduce.
- Facebook: One of the clearest benefits of dynamic QR codes is simple: fewer reprints, less waste, fewer operational delays.
- Instagram: Reprints are a hidden tax on bad QR workflows.
- Image prompt: Stacked reprint boxes and invoices contrasted with one reusable dynamic QR code concept.
- X Community: Startup Community: Cost savings posts feel less exciting, but they often convert better because the pain is immediate.
### Day 18
- Content type: Educational
- X: Agencies need QR workflows too. Campaigns change. Landing pages change. Tracking matters. Scale matters.
- Facebook: Agencies work across multiple campaigns and client assets. That makes dynamic management and analytics much more valuable than one-off QR generation.
- Instagram: Agency-friendly QR workflows are less about design and more about change management.
- Image prompt: Agency desk scene with campaign mockups, client materials, and dashboard metrics, polished B2B style.
- X Community: No Code Community: Agencies using no-code stacks still run into the same QR issue: clients change things after launch.
### Day 19
- Content type: Storytelling
- X: Watching event workflows makes the value of dynamic QR codes obvious fast. Schedules shift, pages update, logistics move quickly, and static links do not keep up.
- Facebook: Event teams are one of the clearest reminders that QR workflows are operational, not just visual. Plans shift, registration pages change, and materials are already out in the world.
- Instagram: Event workflows make one thing obvious: static links age badly.
- Image prompt: Event badge, poster, and registration QR concept with energetic but premium event aesthetic.
- X Community: Build in Public: Events keep reminding us why "print once, update later" is such a durable use case.
### Day 20
- Content type: Educational
- X: A QR code should be part of a workflow, not a one-time asset.
- Facebook: Businesses get more value from QR codes when they treat them as living assets tied to campaigns, updates, analytics, and operations.
- Instagram: Stop thinking of QR codes as files. Start thinking of them as workflows.
- Image prompt: Clean systems-style visual showing QR code lifecycle from creation to update to analytics.
- X Community: Startup Community: Framing matters. Turning a "file" into a "workflow" changes the whole product category.
### Day 21
- Content type: Storytelling
- X: Build in public lesson: we keep seeing the same insight. Simpler products get stronger when the pain is operational, specific, and expensive.
- Facebook: Product clarity improves when you stay close to a narrow operational pain point. For us, that is managing QR destinations after print.
- Instagram: Specific pain points beat vague product categories.
- Image prompt: Founder desk with notes, dashboard, and printed QR materials, documentary startup mood.
- X Community: Build in Public: Another reminder that niche pain points are often more valuable than broad feature lists.
### Day 22
- Content type: Educational
- X: Most QR code conversations focus on the front end: create, style, download. The better conversation starts after that.
- Facebook: The real workflow begins after the QR code is created. That is where updates, measurement, scale, and responsibility show up.
- Instagram: Creation is step one. Management is the product.
- Image prompt: Step-one vs step-two contrast visual, with generator UI fading into management dashboard.
- X Community: Startup Community: Good positioning often starts by shifting the buyer's frame from setup to ongoing management.
### Day 23
- Content type: Educational
- X: Packaging is an underrated QR use case. Once labels are printed, flexibility matters even more.
- Facebook: Product packaging often needs durable, scalable QR workflows because changes later are expensive and slow. Dynamic QR management becomes much more valuable there.
- Instagram: Packaging turns QR mistakes into real inventory pain.
- Image prompt: Premium product packaging close-up with QR code linked to mobile experience and analytics.
- X Community: No Code Community: Packaging workflows are a good example of where "simple generator" stops being enough.
### Day 24
- Content type: Educational
- X: If your QR code points to a page you know will change, making it static is usually the wrong decision.
- Facebook: A useful rule of thumb: if the destination may change later, the QR code should probably be dynamic from day one.
- Instagram: If the destination can change, the QR should too.
- Image prompt: Minimal rule-of-thumb visual, modern typography-led design with QR asset and phone.
- X Community: Build in Public: Strong product messaging often comes from one obvious rule buyers can remember instantly.
### Day 25
- Content type: Storytelling
- X: One thing we have learned from talking about QR workflows publicly: the frustrating part is almost never creating the code. What is the most annoying part after that for your team?
- Facebook: The more conversations we have around QR workflows, the clearer the pattern becomes. The frustrating part is usually not creation. It is everything that happens after. What is the most annoying part for your team?
- Instagram: What is the most annoying part of QR management after the code is already live?
- Image prompt: Community-question visual, clean desk setup with comments or chat overlay concept.
- X Community: Startup Community: Asking for workflow pain often gives better product insight than asking for feature requests.
### Day 26
- Content type: Selling
- X: Direct CTA: If you are still rebuilding or reprinting every time a QR destination changes, it is probably time for a better setup.
- Facebook: Teams that rely on print, packaging, menus, or campaigns need more than a basic QR generator. That is exactly where QR Master fits.
- Instagram: If QR changes still create operational stress, your setup is too fragile.
- Image prompt: Strong CTA ad visual with real-world printed assets and dashboard, premium enterprise-lite feel.
- X Community: No Code Community: The best tools remove repetitive rework. QR workflow pain is full of repetitive rework.
### Day 27
- Content type: Educational
- X: Analytics are not just for dashboards. They help teams make better decisions about campaigns, placement, and performance.
- Facebook: Scan analytics are useful because they give teams feedback loops. Better placement, better campaign decisions, better understanding of what performs.
- Instagram: Better QR analytics means better decisions, not just prettier charts.
- Image prompt: Dashboard-centric visual with practical metrics overlay, sophisticated marketing operations style.
- X Community: Build in Public: We are leaning into "analytics as decisions," not just "analytics as reporting."
### Day 28
- Content type: Storytelling
- X: QR Master sits in an interesting space between marketing ops, print workflows, and privacy-conscious SaaS. That mix has become clearer the more we build and talk to people.
- Facebook: Some products become clearer over time. Ours looks less like a simple utility and more like infrastructure for print-to-digital workflows the more conversations we have.
- Instagram: Utility at first glance. Workflow product underneath. That became clearer over time.
- Image prompt: Abstract but premium ecosystem visual connecting print, mobile, and dashboard layers.
- X Community: Startup Community: Category clarity often appears after enough user conversations, not before.
### Day 29
- Content type: Storytelling
- X: One thing this category teaches quickly: most businesses do not care about dynamic QR codes until the day they really need them. Then the value becomes obvious fast.
- Facebook: Dynamic QR codes are one of those categories that feel optional until the first broken link, campaign change, or urgent update after print. Then the whole value proposition clicks.
- Instagram: Optional until the first mistake. Essential right after.
- Image prompt: Tension-driven visual with urgent update scenario, premium but emotionally clear composition.
- X Community: No Code Community: This is one of those "you do not care until you absolutely care" workflow categories.
### Day 30
- Content type: Selling
- X: Month-end takeaway: QR codes are not just assets. They are decision points between static friction and dynamic flexibility. That is the category we are building for with QR Master. If that sounds like your workflow, try it.
- Facebook: After a month of talking about QR workflows, the pattern is clear: businesses want flexibility after print, measurement without guesswork, and workflows that scale without adding chaos. That is exactly what QR Master is built for.
- Instagram: QR Master is built for one simple outcome: make printed QR workflows easier to change, measure, and scale.
- Image prompt: Premium month-end brand visual summarizing QR Master with printed assets, analytics dashboard, and trust-focused SaaS composition.
- X Community: Build in Public: Month-end positioning summary: we are not building for "make a QR code." We are building for "manage what happens after print."
---
## Weekly Review Template
- Top 3 posts by engagement rate
- Top 3 posts by profile clicks
- Best-performing hook type: pain-led or ROI-led
- Best-performing CTA type: soft or direct
- Best-performing proof angle: cost-saving or privacy
- Best-performing visual style: real-world or dashboard
## Reuse Notes
- Turn the best X posts into threads in month 2
- Turn the best Facebook posts into landing page angles
- Turn the best Instagram posts into carousel scripts
- Use top-performing community questions as future feature research prompts

View File

@@ -0,0 +1,263 @@
BEGIN;
-- 1 user columns
ALTER TABLE "User"
ADD COLUMN IF NOT EXISTS "signupSource" TEXT,
ADD COLUMN IF NOT EXISTS "signupSourceSelfReported" TEXT,
ADD COLUMN IF NOT EXISTS "signupMedium" TEXT,
ADD COLUMN IF NOT EXISTS "signupCampaign" TEXT,
ADD COLUMN IF NOT EXISTS "signupContent" TEXT,
ADD COLUMN IF NOT EXISTS "signupTerm" TEXT,
ADD COLUMN IF NOT EXISTS "signupReferrer" TEXT,
ADD COLUMN IF NOT EXISTS "signupLandingPath" TEXT,
ADD COLUMN IF NOT EXISTS "signupFirstSeenAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "emailDomain" TEXT,
ADD COLUMN IF NOT EXISTS "primaryUseCase" TEXT,
ADD COLUMN IF NOT EXISTS "primaryGoal" TEXT,
ADD COLUMN IF NOT EXISTS "jobRole" TEXT,
ADD COLUMN IF NOT EXISTS "companyName" TEXT,
ADD COLUMN IF NOT EXISTS "companyWebsite" TEXT,
ADD COLUMN IF NOT EXISTS "teamSizeBucket" TEXT,
ADD COLUMN IF NOT EXISTS "onboardingStartedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "sourceConfirmedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "useCaseSelectedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "goalSelectedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "profileCompletedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "firstQrCreatedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "firstDynamicQrAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "firstStaticQrAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "firstScanAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "activationAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "onboardingCompletedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "fitScore" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS "intentScore" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS "leadScore" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS "lifecycleStage" TEXT NOT NULL DEFAULT 'cold',
ADD COLUMN IF NOT EXISTS "lastQualifiedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "lastScoredAt" TIMESTAMP(3);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'User_lifecycleStage_check'
) THEN
ALTER TABLE "User"
ADD CONSTRAINT "User_lifecycleStage_check"
CHECK ("lifecycleStage" IN (
'cold',
'activated',
'warm',
'hot',
'upgrade_candidate',
'paid'
));
END IF;
END $$;
-- 2 lifecycle log table
CREATE TABLE IF NOT EXISTS "UserLifecycleLog" (
"id" TEXT PRIMARY KEY,
"userId" TEXT NOT NULL,
"fromStage" TEXT,
"toStage" TEXT NOT NULL,
"fitScore" INTEGER NOT NULL DEFAULT 0,
"intentScore" INTEGER NOT NULL DEFAULT 0,
"leadScore" INTEGER NOT NULL DEFAULT 0,
"reason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "UserLifecycleLog_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "User"("id")
ON DELETE CASCADE ON UPDATE CASCADE
);
-- 3 indexes
CREATE INDEX IF NOT EXISTS "User_signupSource_idx" ON "User" ("signupSource");
CREATE INDEX IF NOT EXISTS "User_signupSourceSelfReported_idx" ON "User" ("signupSourceSelfReported");
CREATE INDEX IF NOT EXISTS "User_signupCampaign_idx" ON "User" ("signupCampaign");
CREATE INDEX IF NOT EXISTS "User_signupLandingPath_idx" ON "User" ("signupLandingPath");
CREATE INDEX IF NOT EXISTS "User_emailDomain_idx" ON "User" ("emailDomain");
CREATE INDEX IF NOT EXISTS "User_primaryUseCase_idx" ON "User" ("primaryUseCase");
CREATE INDEX IF NOT EXISTS "User_primaryGoal_idx" ON "User" ("primaryGoal");
CREATE INDEX IF NOT EXISTS "User_jobRole_idx" ON "User" ("jobRole");
CREATE INDEX IF NOT EXISTS "User_teamSizeBucket_idx" ON "User" ("teamSizeBucket");
CREATE INDEX IF NOT EXISTS "User_lifecycleStage_idx" ON "User" ("lifecycleStage");
CREATE INDEX IF NOT EXISTS "User_leadScore_idx" ON "User" ("leadScore" DESC);
CREATE INDEX IF NOT EXISTS "User_activationAt_idx" ON "User" ("activationAt");
CREATE INDEX IF NOT EXISTS "User_firstScanAt_idx" ON "User" ("firstScanAt");
CREATE INDEX IF NOT EXISTS "User_firstQrCreatedAt_idx" ON "User" ("firstQrCreatedAt");
CREATE INDEX IF NOT EXISTS "User_lastQualifiedAt_idx" ON "User" ("lastQualifiedAt");
CREATE INDEX IF NOT EXISTS "UserLifecycleLog_userId_idx" ON "UserLifecycleLog" ("userId");
CREATE INDEX IF NOT EXISTS "UserLifecycleLog_toStage_idx" ON "UserLifecycleLog" ("toStage");
CREATE INDEX IF NOT EXISTS "UserLifecycleLog_createdAt_idx" ON "UserLifecycleLog" ("createdAt" DESC);
-- 4 backfill
UPDATE "User"
SET "emailDomain" = lower(split_part("email", '@', 2))
WHERE "email" IS NOT NULL
AND ("emailDomain" IS NULL OR "emailDomain" = '');
UPDATE "User" u
SET
"firstQrCreatedAt" = q."firstQrAt",
"firstDynamicQrAt" = q."firstDynamicQrAt",
"firstStaticQrAt" = q."firstStaticQrAt",
"onboardingCompletedAt" = COALESCE(u."onboardingCompletedAt", q."firstQrAt")
FROM (
SELECT
"userId",
MIN("createdAt") AS "firstQrAt",
MIN("createdAt") FILTER (WHERE "type" = 'DYNAMIC') AS "firstDynamicQrAt",
MIN("createdAt") FILTER (WHERE "type" = 'STATIC') AS "firstStaticQrAt"
FROM "QRCode"
GROUP BY "userId"
) q
WHERE u."id" = q."userId";
UPDATE "User" u
SET
"firstScanAt" = s."firstScanAt",
"activationAt" = COALESCE(u."activationAt", s."firstScanAt")
FROM (
SELECT
q."userId",
MIN(s."ts") AS "firstScanAt"
FROM "QRCode" q
INNER JOIN "QRScan" s ON s."qrId" = q."id"
GROUP BY q."userId"
) s
WHERE u."id" = s."userId";
-- 5 scoring
WITH qr_stats AS (
SELECT
q."userId",
COUNT(*) AS qr_count,
COUNT(*) FILTER (WHERE q."type" = 'DYNAMIC') AS dynamic_count,
COUNT(DISTINCT q."contentType") AS content_type_count,
COUNT(*) FILTER (WHERE q."contentType" IN ('BARCODE','PDF','VCARD','COUPON','FEEDBACK')) AS businessish_type_count
FROM "QRCode" q
GROUP BY q."userId"
),
scan_stats AS (
SELECT
q."userId",
COUNT(s."id") AS scan_count
FROM "QRCode" q
LEFT JOIN "QRScan" s ON s."qrId" = q."id"
GROUP BY q."userId"
),
scored AS (
SELECT
u."id",
COALESCE(qs.qr_count, 0) AS qr_count,
COALESCE(qs.dynamic_count, 0) AS dynamic_count,
COALESCE(qs.content_type_count, 0) AS content_type_count,
COALESCE(qs.businessish_type_count, 0) AS businessish_type_count,
COALESCE(ss.scan_count, 0) AS scan_count
FROM "User" u
LEFT JOIN qr_stats qs ON qs."userId" = u."id"
LEFT JOIN scan_stats ss ON ss."userId" = u."id"
)
UPDATE "User" u
SET
"fitScore" =
(CASE
WHEN lower(split_part(u."email", '@', 2)) IN ('gmail.com','yahoo.com','hotmail.com','outlook.com','icloud.com') THEN -15
WHEN u."email" IS NOT NULL THEN 20
ELSE 0
END)
+
(CASE
WHEN u."primaryUseCase" IN ('marketing_campaign','bulk_qr','menu_pdf','barcode') THEN 10
ELSE 0
END)
+
(CASE
WHEN u."primaryGoal" IN ('track_printed_campaigns','generate_leads','manage_multiple_qr_codes') THEN 10
ELSE 0
END)
+
(CASE
WHEN u."jobRole" IN ('founder_owner','marketing_manager','agency_freelancer','operations') THEN 10
ELSE 0
END)
+
(CASE
WHEN u."companyName" IS NOT NULL AND u."companyName" <> '' THEN 5
ELSE 0
END)
+
(CASE
WHEN u."teamSizeBucket" IN ('6_20','21_100','100_plus') THEN 10
ELSE 0
END),
"intentScore" =
(CASE WHEN u."firstQrCreatedAt" IS NOT NULL THEN 20 ELSE -10 END)
+
(CASE WHEN u."firstDynamicQrAt" IS NOT NULL THEN 20 ELSE 0 END)
+
(CASE WHEN COALESCE(s.qr_count, 0) >= 3 THEN 15 ELSE 0 END)
+
(CASE WHEN COALESCE(s.scan_count, 0) > 0 THEN 10 ELSE 0 END)
+
(CASE WHEN COALESCE(s.businessish_type_count, 0) > 0 THEN 10 ELSE 0 END)
+
(CASE WHEN COALESCE(s.content_type_count, 0) >= 2 THEN 10 ELSE 0 END),
"lastScoredAt" = CURRENT_TIMESTAMP
FROM scored s
WHERE u."id" = s."id";
UPDATE "User"
SET "leadScore" = COALESCE("fitScore", 0) + COALESCE("intentScore", 0);
UPDATE "User"
SET
"lifecycleStage" = CASE
WHEN "plan" IN ('PRO','BUSINESS') THEN 'paid'
WHEN "leadScore" >= 70 THEN 'upgrade_candidate'
WHEN "leadScore" >= 55 THEN 'hot'
WHEN "leadScore" >= 30 THEN 'warm'
WHEN "activationAt" IS NOT NULL THEN 'activated'
ELSE 'cold'
END,
"lastQualifiedAt" = CASE
WHEN "leadScore" >= 55 OR "plan" IN ('PRO','BUSINESS') THEN CURRENT_TIMESTAMP
ELSE "lastQualifiedAt"
END;
-- 6 reporting queries
-- Acquisition overview
SELECT
COALESCE("signupSource", 'unknown') AS source,
COUNT(*) AS signups,
COUNT(*) FILTER (WHERE "firstQrCreatedAt" IS NOT NULL) AS first_qr,
COUNT(*) FILTER (WHERE "activationAt" IS NOT NULL) AS activated,
COUNT(*) FILTER (WHERE "lifecycleStage" = 'hot') AS hot,
COUNT(*) FILTER (WHERE "lifecycleStage" = 'upgrade_candidate') AS upgrade_candidates,
COUNT(*) FILTER (WHERE "lifecycleStage" = 'paid') AS paid
FROM "User"
GROUP BY 1
ORDER BY signups DESC;
-- Onboarding funnel
SELECT
COUNT(*) AS signup,
COUNT(*) FILTER (WHERE "sourceConfirmedAt" IS NOT NULL) AS source_confirmed,
COUNT(*) FILTER (WHERE "useCaseSelectedAt" IS NOT NULL) AS use_case_selected,
COUNT(*) FILTER (WHERE "goalSelectedAt" IS NOT NULL) AS goal_selected,
COUNT(*) FILTER (WHERE "profileCompletedAt" IS NOT NULL) AS profile_completed,
COUNT(*) FILTER (WHERE "firstQrCreatedAt" IS NOT NULL) AS first_qr_created,
COUNT(*) FILTER (WHERE "firstDynamicQrAt" IS NOT NULL) AS first_dynamic_qr_created,
COUNT(*) FILTER (WHERE "activationAt" IS NOT NULL) AS activated
FROM "User";
-- Lifecycle summary
SELECT
"lifecycleStage",
COUNT(*) AS users
FROM "User"
GROUP BY 1
ORDER BY users DESC;
COMMIT;

View File

@@ -200,10 +200,10 @@ export default function AppLayout({
</aside> </aside>
{/* Main content */} {/* Main content */}
<div className="lg:ml-64"> <div className="lg:ml-64">
{/* Top bar */} {/* Top bar */}
<header className="bg-white border-b border-gray-200"> <header className="bg-white border-b border-gray-200">
<div className="flex items-center justify-between px-4 py-3"> <div className="flex items-center justify-between px-4 py-3">
<button <button
className="lg:hidden" className="lg:hidden"
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
@@ -213,24 +213,24 @@ export default function AppLayout({
</svg> </svg>
</button> </button>
<div className="flex items-center space-x-4 ml-auto"> <div className="flex items-center space-x-4 ml-auto">
{/* User Menu */} {/* User Menu */}
<Dropdown <Dropdown
align="right" align="right"
trigger={ trigger={
<button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900"> <button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-primary-600"> <span className="text-sm font-medium text-primary-600">
{getUserInitials()} {getUserInitials()}
</span> </span>
</div> </div>
<span className="hidden md:block font-medium"> <span className="hidden md:block font-medium">
{getDisplayName()} {getDisplayName()}
</span> </span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </button>
} }
> >
<DropdownItem onClick={handleSignOut}> <DropdownItem onClick={handleSignOut}>
@@ -242,9 +242,9 @@ export default function AppLayout({
</header> </header>
{/* Page content */} {/* Page content */}
<main className="p-6"> <main className="p-6">
{children} {children}
</main> </main>
{/* Footer */} {/* Footer */}
<Footer variant="dashboard" /> <Footer variant="dashboard" />

View File

@@ -22,8 +22,10 @@ interface BulkQRData {
interface GeneratedQR { interface GeneratedQR {
title: string; title: string;
content: string; // Original URL content: string;
svg: string; // SVG markup svg: string;
slug?: string;
redirectUrl?: string;
} }
export default function BulkCreationPage() { export default function BulkCreationPage() {
@@ -35,16 +37,25 @@ export default function BulkCreationPage() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [generatedQRs, setGeneratedQRs] = useState<GeneratedQR[]>([]); const [generatedQRs, setGeneratedQRs] = useState<GeneratedQR[]>([]);
const [userPlan, setUserPlan] = useState<string>('FREE'); const [userPlan, setUserPlan] = useState<string>('FREE');
const [isDynamic, setIsDynamic] = useState(false);
const [remainingDynamic, setRemainingDynamic] = useState(0);
// Check user plan on mount // Check user plan and dynamic quota on mount
React.useEffect(() => { React.useEffect(() => {
const checkPlan = async () => { const checkPlan = async () => {
try { try {
const response = await fetch('/api/user/plan'); const [planRes, statsRes] = await Promise.all([
if (response.ok) { fetch('/api/user/plan'),
const data = await response.json(); fetch('/api/user/stats'),
]);
if (planRes.ok) {
const data = await planRes.json();
setUserPlan(data.plan || 'FREE'); setUserPlan(data.plan || 'FREE');
} }
if (statsRes.ok) {
const stats = await statsRes.json();
setRemainingDynamic((stats.dynamicLimit || 0) - (stats.dynamicUsed || 0));
}
} catch (error) { } catch (error) {
console.error('Error checking plan:', error); console.error('Error checking plan:', error);
} }
@@ -196,6 +207,58 @@ export default function BulkCreationPage() {
} }
}; };
const generateDynamicQRCodes = async () => {
setLoading(true);
const toProcess = remainingDynamic > 0 ? data.slice(0, remainingDynamic) : [];
if (toProcess.length === 0) {
showToast('Du hast keine dynamischen QR-Codes mehr übrig. Bitte upgrade deinen Plan.', 'error');
setLoading(false);
return;
}
if (data.length > remainingDynamic) {
showToast(`Nur ${remainingDynamic} dynamische Codes verfügbar. Es werden nur die ersten ${remainingDynamic} Zeilen verarbeitet.`, 'warning');
}
try {
const QRCode = require('qrcode');
const results: GeneratedQR[] = [];
for (const row of toProcess) {
const title = String(row[mapping.title as keyof typeof row] || 'Untitled');
const url = String(row[mapping.content as keyof typeof row] || 'https://example.com');
const res = await fetchWithCsrf('/api/qrs', {
method: 'POST',
body: JSON.stringify({
title,
contentType: 'URL',
content: { url },
isStatic: false,
}),
});
if (res.ok) {
const qr = await res.json();
const redirectUrl = `${window.location.origin}/r/${qr.slug}`;
const svg = await QRCode.toString(redirectUrl, { type: 'svg', width: 300, margin: 2 });
results.push({ title, content: url, svg, slug: qr.slug, redirectUrl });
}
}
setGeneratedQRs(results);
setRemainingDynamic(prev => Math.max(0, prev - results.length));
setStep('complete');
showToast(`${results.length} dynamische QR-Codes erstellt!`, 'success');
} catch (error) {
console.error('Dynamic QR generation error:', error);
showToast('Fehler beim Erstellen der dynamischen QR-Codes', 'error');
} finally {
setLoading(false);
}
};
const downloadAllQRCodes = async () => { const downloadAllQRCodes = async () => {
const zip = new JSZip(); const zip = new JSZip();
@@ -204,12 +267,25 @@ export default function BulkCreationPage() {
zip.file(fileName, qr.svg); zip.file(fileName, qr.svg);
}); });
// Add metadata CSV for dynamic QR codes
const hasDynamic = generatedQRs.some(qr => qr.slug);
if (hasDynamic) {
const csvRows = ['title,original_url,redirect_url,slug'];
generatedQRs.forEach(qr => {
if (qr.slug) {
csvRows.push(`"${qr.title}","${qr.content}","${qr.redirectUrl}","${qr.slug}"`);
}
});
zip.file('metadata.csv', csvRows.join('\n'));
}
const blob = await zip.generateAsync({ type: 'blob' }); const blob = await zip.generateAsync({ type: 'blob' });
saveAs(blob, 'qr-codes-bulk.zip'); saveAs(blob, 'qr-codes-bulk.zip');
showToast('Download started!', 'success'); showToast('Download started!', 'success');
}; };
const saveQRCodesToDatabase = async () => { const saveQRCodesToDatabase = async () => {
if (isDynamic) return; // dynamic codes are already saved during generation
setLoading(true); setLoading(true);
try { try {
@@ -274,8 +350,8 @@ export default function BulkCreationPage() {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
// Show upgrade prompt if not Business plan // Show upgrade prompt if not Business or Enterprise plan
if (userPlan !== 'BUSINESS') { if (userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE') {
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<Card className="mt-12"> <Card className="mt-12">
@@ -309,6 +385,39 @@ export default function BulkCreationPage() {
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1> <h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1>
<p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p> <p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p>
{/* Static / Dynamic Toggle */}
<div className="mt-4 flex items-center gap-4 p-4 bg-gray-50 rounded-xl border border-gray-200">
<span className="text-sm font-medium text-gray-700">QR Code Type:</span>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
checked={!isDynamic}
onChange={() => setIsDynamic(false)}
className="accent-primary-600"
/>
<span className="text-sm font-medium">Static</span>
<span className="text-xs text-gray-500">(download only, no tracking)</span>
</label>
<label className={`flex items-center gap-2 ${userPlan === 'BUSINESS' || userPlan === 'ENTERPRISE' ? 'cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
<input
type="radio"
checked={isDynamic}
onChange={() => setIsDynamic(true)}
disabled={userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE'}
className="accent-primary-600"
/>
<span className="text-sm font-medium">Dynamic</span>
{isDynamic && remainingDynamic > 0 && (
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">
{remainingDynamic} verbleibend
</span>
)}
{(userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE') && (
<span className="text-xs text-amber-600">(Business Plan erforderlich)</span>
)}
</label>
</div>
</div> </div>
{/* Template Warning Banner */} {/* Template Warning Banner */}
@@ -641,8 +750,13 @@ export default function BulkCreationPage() {
<Button variant="outline" onClick={() => setStep('upload')}> <Button variant="outline" onClick={() => setStep('upload')}>
Back Back
</Button> </Button>
<Button onClick={generateStaticQRCodes} loading={loading}> <Button
Generate {data.length} Static QR Codes onClick={isDynamic ? generateDynamicQRCodes : generateStaticQRCodes}
loading={loading}
>
{isDynamic
? `Generate ${Math.min(data.length, remainingDynamic)} Dynamic QR Codes`
: `Generate ${data.length} Static QR Codes`}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@@ -704,12 +818,14 @@ export default function BulkCreationPage() {
</svg> </svg>
Download All as ZIP Download All as ZIP
</Button> </Button>
<Button onClick={saveQRCodesToDatabase} loading={loading}> {!isDynamic && (
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <Button onClick={saveQRCodesToDatabase} loading={loading}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" /> <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</svg> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
Save QR Codes </svg>
</Button> Save QR Codes
</Button>
)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -1,8 +1,8 @@
'use client'; 'use client';
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { toPng } from 'html-to-image'; import { toPng } from 'html-to-image';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
@@ -11,12 +11,19 @@ import { Select } from '@/components/ui/Select';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { calculateContrast, cn } from '@/lib/utils'; import { calculateContrast, cn } from '@/lib/utils';
import { useTranslation } from '@/hooks/useTranslation'; import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf'; import { useCsrf } from '@/hooks/useCsrf';
import { showToast } from '@/components/ui/Toast'; import { showToast } from '@/components/ui/Toast';
import { import { trackEvent } from '@/components/PostHogProvider';
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload import { appendRedirectParam, sanitizeRedirectPath } from '@/lib/auth-flow';
} from 'lucide-react'; import {
ONBOARDING_DOWNLOAD_COMPLETE_EVENT,
ONBOARDING_DOWNLOAD_COMPLETE_KEY,
} from '@/lib/revops';
import {
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload, Barcode as BarcodeIcon
} from 'lucide-react';
import Barcode from 'react-barcode';
// Tooltip component for form field help // Tooltip component for form field help
const Tooltip = ({ text }: { text: string }) => ( const Tooltip = ({ text }: { text: string }) => (
@@ -61,9 +68,47 @@ const getFrameOptionsForContentType = (contentType: string) => {
} }
}; };
export default function CreatePage() { // Injects a caption <text> element below a barcode SVG and expands its height/viewBox.
const router = useRouter(); // Used so the "scanner app" hint is baked into the downloaded SVG.
const { t } = useTranslation(); function addBarcodeCaptionToSvg(svgElement: SVGElement, caption: string): string {
const cloned = svgElement.cloneNode(true) as SVGElement;
const NS = 'http://www.w3.org/2000/svg';
const widthAttr = cloned.getAttribute('width');
const heightAttr = cloned.getAttribute('height');
const width = widthAttr ? parseFloat(widthAttr) : 200;
const height = heightAttr ? parseFloat(heightAttr) : 100;
const extraHeight = 18;
cloned.setAttribute('height', String(height + extraHeight));
const viewBox = cloned.getAttribute('viewBox');
if (viewBox) {
const parts = viewBox.split(/\s+/);
if (parts.length === 4) {
cloned.setAttribute(
'viewBox',
`${parts[0]} ${parts[1]} ${parts[2]} ${parseFloat(parts[3]) + extraHeight}`
);
}
}
const text = document.createElementNS(NS, 'text');
text.setAttribute('x', String(width / 2));
text.setAttribute('y', String(height + 12));
text.setAttribute('text-anchor', 'middle');
text.setAttribute('font-size', '9');
text.setAttribute('font-family', 'Arial, Helvetica, sans-serif');
text.setAttribute('fill', '#666666');
text.textContent = caption;
cloned.appendChild(text);
return new XMLSerializer().serializeToString(cloned);
}
export default function CreatePage() {
const router = useRouter();
const searchParams = useSearchParams();
const { t } = useTranslation();
const { fetchWithCsrf } = useCsrf(); const { fetchWithCsrf } = useCsrf();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@@ -107,14 +152,23 @@ export default function CreatePage() {
const [excavate, setExcavate] = useState(true); const [excavate, setExcavate] = useState(true);
// QR preview // QR preview
const [qrDataUrl, setQrDataUrl] = useState(''); const [qrDataUrl, setQrDataUrl] = useState('');
const markDownloadComplete = () => {
if (typeof window === 'undefined') {
return;
}
localStorage.setItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY, '1');
window.dispatchEvent(new CustomEvent(ONBOARDING_DOWNLOAD_COMPLETE_EVENT));
};
// Check if user can customize colors (PRO+ only) // Check if user can customize colors (PRO+ only)
const canCustomizeColors = userPlan === 'PRO' || userPlan === 'BUSINESS'; const canCustomizeColors = userPlan === 'PRO' || userPlan === 'BUSINESS';
// Load user plan // Load user plan
useEffect(() => { useEffect(() => {
const fetchUserPlan = async () => { const fetchUserPlan = async () => {
try { try {
const response = await fetch('/api/user/plan'); const response = await fetch('/api/user/plan');
if (response.ok) { if (response.ok) {
@@ -125,8 +179,44 @@ export default function CreatePage() {
console.error('Error fetching user plan:', error); console.error('Error fetching user plan:', error);
} }
}; };
fetchUserPlan(); fetchUserPlan();
}, []); }, []);
useEffect(() => {
const queryContentType = searchParams.get('contentType');
const useCase = searchParams.get('useCase');
const titleParam = searchParams.get('title');
const isDynamicParam = searchParams.get('dynamic');
if (queryContentType) {
setContentType(queryContentType);
}
if (titleParam) {
setTitle(titleParam);
}
if (isDynamicParam) {
setIsDynamic(isDynamicParam === '1');
}
if (useCase === 'menu_pdf') {
setContent((prev: any) => ({ ...prev, fileUrl: prev.fileUrl || '' }));
} else if (useCase === 'contact_card') {
setContent((prev: any) => ({
...prev,
firstName: prev.firstName || '',
lastName: prev.lastName || '',
}));
} else if (useCase === 'barcode') {
setContent((prev: any) => ({
...prev,
format: prev.format || 'CODE128',
}));
} else if (queryContentType === 'URL') {
setContent((prev: any) => ({ ...prev, url: prev.url || '' }));
}
}, [searchParams]);
const contrast = calculateContrast(foregroundColor, backgroundColor); const contrast = calculateContrast(foregroundColor, backgroundColor);
const hasGoodContrast = contrast >= 4.5; const hasGoodContrast = contrast >= 4.5;
@@ -140,6 +230,7 @@ export default function CreatePage() {
{ value: 'APP', label: 'App Download', icon: Smartphone }, { value: 'APP', label: 'App Download', icon: Smartphone },
{ value: 'COUPON', label: 'Coupon / Discount', icon: Ticket }, { value: 'COUPON', label: 'Coupon / Discount', icon: Ticket },
{ value: 'FEEDBACK', label: 'Feedback / Review', icon: Star }, { value: 'FEEDBACK', label: 'Feedback / Review', icon: Star },
{ value: 'BARCODE', label: 'Barcode', icon: BarcodeIcon },
]; ];
// Get QR content based on content type // Get QR content based on content type
@@ -170,12 +261,15 @@ export default function CreatePage() {
return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`; return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`;
case 'FEEDBACK': case 'FEEDBACK':
return content.feedbackUrl || 'https://example.com/feedback'; return content.feedbackUrl || 'https://example.com/feedback';
case 'BARCODE':
return isDynamic ? (content.url || '') : (content.value || '');
default: default:
return 'https://example.com'; return 'https://example.com';
} }
}; };
const qrContent = getQRContent(); const qrContent = getQRContent();
const previewScale = contentType === 'BARCODE' ? 1 : Math.min(1, 240 / Math.max(size, 1));
const getFrameLabel = () => { const getFrameLabel = () => {
const frame = frameOptions.find((f: { id: string; label: string }) => f.id === frameType); const frame = frameOptions.find((f: { id: string; label: string }) => f.id === frameType);
@@ -185,13 +279,20 @@ export default function CreatePage() {
const downloadQR = async (format: 'svg' | 'png') => { const downloadQR = async (format: 'svg' | 'png') => {
if (!qrRef.current) return; if (!qrRef.current) return;
try { try {
if (format === 'png') { if (format === 'png') {
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' }); const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' });
const link = document.createElement('a'); const link = document.createElement('a');
link.download = `qrcode-${title || 'download'}.png`; link.download = `qrcode-${title || 'download'}.png`;
link.href = dataUrl; link.href = dataUrl;
link.click(); link.click();
} else { markDownloadComplete();
trackEvent('qr_code_downloaded', {
format: 'png',
content_type: contentType,
qr_type: isDynamic ? 'dynamic' : 'static',
plan: userPlan,
});
} else {
// For SVG, we might still want to use the library or just toPng if SVG export of HTML is not needed // For SVG, we might still want to use the library or just toPng if SVG export of HTML is not needed
// Simplest is to check if we can export the SVG element directly but that misses the frame HTML. // Simplest is to check if we can export the SVG element directly but that misses the frame HTML.
// html-to-image can generate SVG too. // html-to-image can generate SVG too.
@@ -208,24 +309,41 @@ export default function CreatePage() {
if (frameType === 'none') { if (frameType === 'none') {
const svgElement = qrRef.current.querySelector('svg'); const svgElement = qrRef.current.querySelector('svg');
if (svgElement) { if (svgElement) {
const svgData = new XMLSerializer().serializeToString(svgElement); const svgData = contentType === 'BARCODE'
? addBarcodeCaptionToSvg(svgElement, 'Scan: iPhone -> Barcode Scanner App | Android -> Google Lens')
: new XMLSerializer().serializeToString(svgElement);
const blob = new Blob([svgData], { type: 'image/svg+xml' }); const blob = new Blob([svgData], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = `qrcode-${title || 'download'}.svg`; a.download = `qrcode-${title || 'download'}.svg`;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} markDownloadComplete();
} else { trackEvent('qr_code_downloaded', {
showToast('SVG download not available with frames yet. Downloading PNG instead.', 'info'); format: 'svg',
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' }); content_type: contentType,
const link = document.createElement('a'); qr_type: isDynamic ? 'dynamic' : 'static',
link.download = `qrcode-${title || 'download'}.png`; plan: userPlan,
link.href = dataUrl; });
link.click(); }
} } else {
} showToast('SVG download not available with frames yet. Downloading PNG instead.', 'info');
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' });
const link = document.createElement('a');
link.download = `qrcode-${title || 'download'}.png`;
link.href = dataUrl;
link.click();
markDownloadComplete();
trackEvent('qr_code_downloaded', {
format: 'png',
content_type: contentType,
qr_type: isDynamic ? 'dynamic' : 'static',
plan: userPlan,
fallback_from: 'svg_with_frame',
});
}
}
} catch (err) { } catch (err) {
console.error('Error downloading QR code:', err); console.error('Error downloading QR code:', err);
showToast('Error downloading QR code', 'error'); showToast('Error downloading QR code', 'error');
@@ -311,18 +429,38 @@ export default function CreatePage() {
const responseData = await response.json(); const responseData = await response.json();
console.log('RESPONSE DATA:', responseData); console.log('RESPONSE DATA:', responseData);
if (response.ok) { if (response.ok) {
showToast(`QR Code "${title}" created successfully!`, 'success'); trackEvent('qr_code_created', {
content_type: contentType,
// Wait a moment so user sees the toast, then redirect qr_type: isDynamic ? 'dynamic' : 'static',
setTimeout(() => { plan: userPlan,
router.push('/dashboard'); has_logo: Boolean(logoUrl),
router.refresh(); frame_type: frameType,
}, 1000); });
} else {
console.error('Error creating QR code:', responseData); showToast(`QR Code "${title}" created successfully!`, 'success');
showToast(responseData.error || 'Error creating QR code', 'error');
} // Wait a moment so user sees the toast, then redirect
setTimeout(() => {
const redirectTarget = sanitizeRedirectPath(searchParams.get('redirect'));
if (searchParams.get('onboarding') === '1') {
router.push(appendRedirectParam('/onboarding', redirectTarget, { step: '8' }));
} else {
router.push('/dashboard');
}
router.refresh();
}, 1000);
} else {
console.error('Error creating QR code:', responseData);
if (response.status === 403 && responseData.error === 'Limit reached') {
showToast(responseData.message || 'You have reached your plan limit.', 'error');
router.push('/pricing?reason=limit_reached');
return;
}
showToast(responseData.error || 'Error creating QR code', 'error');
}
} catch (error) { } catch (error) {
console.error('Error creating QR code:', error); console.error('Error creating QR code:', error);
showToast('Error creating QR code. Please try again.', 'error'); showToast('Error creating QR code. Please try again.', 'error');
@@ -642,6 +780,73 @@ export default function CreatePage() {
/> />
</> </>
); );
case 'BARCODE':
return (
<>
{isDynamic ? (
<>
<div className="rounded-lg bg-blue-50 border border-blue-200 p-3 text-sm text-blue-800 space-y-2">
<p>
<strong>How dynamic barcodes work:</strong> The barcode encodes a short redirect URL
(e.g. <span className="font-mono text-xs">qrmaster.net/r/</span>) that you can update anytime.
</p>
<p className="rounded border border-amber-300 bg-amber-50 p-2 text-xs text-amber-900">
<strong>📱 Scanner tip:</strong> Use a <strong>barcode scanner app</strong> on iPhone
(iOS Camera doesn't auto-open links from barcodes). Android Google Lens / Camera works
out of the box. Print min. 5&nbsp;cm wide for reliable scanning.
</p>
</div>
<Input
label="Destination URL"
value={content.url || ''}
onChange={(e) => setContent({ ...content, url: e.target.value })}
placeholder="https://example.com"
required
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Barcode Format</label>
<select
value={['CODE128', 'CODE39'].includes(content.format) ? content.format : 'CODE128'}
onChange={(e) => setContent({ ...content, format: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="CODE128">CODE128 — General purpose (recommended)</option>
<option value="CODE39">CODE39 — Industrial / logistics</option>
</select>
<p className="text-xs text-gray-500 mt-1">
Only URL-capable formats available. EAN-13, UPC, and ITF-14 encode numbers only and cannot embed a redirect URL.
</p>
</div>
</>
) : (
<>
<Input
label="Barcode Value"
value={content.value || ''}
onChange={(e) => setContent({ ...content, value: e.target.value })}
placeholder="123456789012"
required
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Barcode Format</label>
<select
value={content.format || 'CODE128'}
onChange={(e) => setContent({ ...content, format: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="CODE128">CODE128 — General purpose (recommended)</option>
<option value="EAN13">EAN-13 — Retail products (international)</option>
<option value="UPC">UPC — Retail products (USA/Canada)</option>
<option value="CODE39">CODE39 — Industrial / logistics</option>
<option value="ITF14">ITF-14 — Shipping containers</option>
<option value="MSI">MSI — Shelf labeling / inventory</option>
<option value="pharmacode">Pharmacode — Pharmaceutical packaging</option>
</select>
</div>
</>
)}
</>
);
default: default:
return null; return null;
} }
@@ -799,49 +1004,51 @@ export default function CreatePage() {
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Foreground Color Foreground Color
</label> </label>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <input
type="color" type="color"
value={foregroundColor} value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)} onChange={(e) => setForegroundColor(e.target.value)}
className="w-12 h-10 rounded border border-gray-300" className="w-12 h-10 rounded border border-gray-300"
disabled={!canCustomizeColors} disabled={!canCustomizeColors}
/> />
<Input <Input
value={foregroundColor} value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)} onChange={(e) => setForegroundColor(e.target.value)}
className="flex-1" className="flex-1"
disabled={!canCustomizeColors} disabled={!canCustomizeColors}
/> />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Background Color
</label>
<div className="flex items-center space-x-2">
<input
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="w-12 h-10 rounded border border-gray-300"
disabled={!canCustomizeColors}
/>
<Input
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="flex-1"
disabled={!canCustomizeColors}
/>
</div>
</div>
</div> </div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Background Color
</label>
<div className="flex items-center space-x-2">
<input
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="w-12 h-10 rounded border border-gray-300"
disabled={!canCustomizeColors}
/>
<Input
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="flex-1"
disabled={!canCustomizeColors}
/>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Select <Select
@@ -972,16 +1179,15 @@ export default function CreatePage() {
<CardTitle>{t('create.preview')}</CardTitle> <CardTitle>{t('create.preview')}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="text-center"> <CardContent className="text-center">
<div id="create-qr-preview" className="flex justify-center mb-4"> <div id="create-qr-preview" className="flex justify-center mb-4 w-full min-w-0 overflow-hidden">
{/* WRAPPER FOR REF AND FRAME */} {/* WRAPPER FOR REF AND FRAME */}
<div <div
ref={qrRef} ref={qrRef}
className="relative bg-white rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300" className="relative flex w-full min-w-0 max-w-full flex-col items-center justify-center rounded-xl bg-white p-3 transition-all duration-300 sm:p-4"
style={{ style={{
minWidth: '280px', minHeight: '220px',
minHeight: '280px', }}
}} >
>
{/* Frame Label */} {/* Frame Label */}
{getFrameLabel() && ( {getFrameLabel() && (
<div <div
@@ -992,11 +1198,41 @@ export default function CreatePage() {
</div> </div>
)} )}
{qrContent ? ( {contentType === 'BARCODE' ? (
<div className={cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}> qrContent ? (
<QRCodeSVG <div className="p-2 bg-white w-full max-w-full [&_svg]:!w-full [&_svg]:!h-auto [&_svg]:!max-w-full">
value={qrContent} <Barcode
size={size} key={`${qrContent}-${content.format}-${foregroundColor}`}
value={qrContent}
format={content.format || 'CODE128'}
lineColor={foregroundColor}
background={backgroundColor}
width={2}
height={80}
margin={10}
displayValue={true}
fontSize={14}
/>
<p className="mt-2 text-center text-[10px] leading-tight text-gray-600 px-2">
Scan: iPhone → Barcode Scanner App · Android → Google Lens / Camera
</p>
</div>
) : (
<div className="w-[200px] h-[200px] bg-gray-100 rounded flex items-center justify-center text-gray-500">
Enter barcode value
</div>
)
) : qrContent ? (
<div
className={cornerStyle === 'rounded' ? 'overflow-hidden rounded-lg' : ''}
style={{
transform: `scale(${previewScale})`,
transformOrigin: 'center center',
}}
>
<QRCodeSVG
value={qrContent}
size={size}
fgColor={foregroundColor} fgColor={foregroundColor}
bgColor={backgroundColor} bgColor={backgroundColor}
level="H" level="H"
@@ -1047,4 +1283,4 @@ export default function CreatePage() {
</form> </form>
</div> </div>
); );
} }

View File

@@ -7,12 +7,15 @@ import { StatsGrid } from '@/components/dashboard/StatsGrid';
import { QRCodeCard } from '@/components/dashboard/QRCodeCard'; import { QRCodeCard } from '@/components/dashboard/QRCodeCard';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { useTranslation } from '@/hooks/useTranslation'; import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf'; import { useCsrf } from '@/hooks/useCsrf';
import { showToast } from '@/components/ui/Toast'; import { showToast } from '@/components/ui/Toast';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/Dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/Dialog';
import { QrCode } from 'lucide-react'; import { QrCode } from 'lucide-react';
import { trackEvent, identifyUser } from '@/components/PostHogProvider';
import { FREE_DYNAMIC_QR_LIMIT } from '@/lib/plans';
import { OnboardingChecklist } from '@/components/dashboard/OnboardingChecklist';
interface QRCodeData { interface QRCodeData {
id: string; id: string;
@@ -44,7 +47,8 @@ export default function DashboardPage() {
conversionRate: 0, conversionRate: 0,
uniqueScans: 0, uniqueScans: 0,
}); });
const [analyticsData, setAnalyticsData] = useState<any>(null); const [analyticsData, setAnalyticsData] = useState<any>(null);
const [onboardingState, setOnboardingState] = useState<any>(null);
const blogPosts = [ const blogPosts = [
@@ -117,12 +121,11 @@ export default function DashboardPage() {
// Store in localStorage for consistency // Store in localStorage for consistency
localStorage.setItem('user', JSON.stringify(user)); localStorage.setItem('user', JSON.stringify(user));
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider'); identifyUser(user.id, {
identifyUser(user.id, { email: user.email,
email: user.email, name: user.name,
name: user.name, plan: user.plan || 'FREE',
plan: user.plan || 'FREE', provider: 'google',
provider: 'google',
}); });
trackEvent(isNewUser ? 'user_signup' : 'user_login', { trackEvent(isNewUser ? 'user_signup' : 'user_login', {
@@ -143,25 +146,35 @@ export default function DashboardPage() {
}, [searchParams, router]); }, [searchParams, router]);
// Check for successful payment and verify session // Check for successful payment and verify session
useEffect(() => { useEffect(() => {
const success = searchParams.get('success'); const success = searchParams.get('success');
if (success === 'true') { const sessionId = searchParams.get('session_id');
const verifySession = async () => {
try { if (success === 'true' && sessionId) {
const response = await fetch('/api/stripe/verify-session', { const verifySession = async () => {
method: 'POST', try {
}); const response = await fetch('/api/stripe/verify-session', {
method: 'POST',
if (response.ok) { headers: {
const data = await response.json(); 'Content-Type': 'application/json',
setUserPlan(data.plan); },
setUpgradedPlan(data.plan); body: JSON.stringify({ sessionId }),
setShowUpgradeDialog(true); });
// Remove success parameter from URL
router.replace('/dashboard'); if (response.ok) {
} else { const data = await response.json();
console.error('Failed to verify session:', await response.text()); setUserPlan(data.plan);
} setUpgradedPlan(data.plan);
setShowUpgradeDialog(true);
trackEvent('upgrade_completed', {
plan: data.plan,
source: 'stripe_checkout',
});
// Remove success parameter from URL
router.replace('/dashboard');
} else {
console.error('Failed to verify session:', await response.text());
}
} catch (error) { } catch (error) {
console.error('Error verifying session:', error); console.error('Error verifying session:', error);
} }
@@ -212,13 +225,19 @@ export default function DashboardPage() {
setUserPlan(userData.plan || 'FREE'); setUserPlan(userData.plan || 'FREE');
} }
// Fetch analytics data for trends (last 30 days = month comparison) // Fetch analytics data for trends (last 30 days = month comparison)
const analyticsResponse = await fetch('/api/analytics/summary?range=30'); const analyticsResponse = await fetch('/api/analytics/summary?range=30');
if (analyticsResponse.ok) { if (analyticsResponse.ok) {
const analytics = await analyticsResponse.json(); const analytics = await analyticsResponse.json();
setAnalyticsData(analytics); setAnalyticsData(analytics);
} }
} catch (error) {
const onboardingResponse = await fetch('/api/onboarding');
if (onboardingResponse.ok) {
const onboardingData = await onboardingResponse.json();
setOnboardingState(onboardingData);
}
} catch (error) {
console.error('Error fetching data:', error); console.error('Error fetching data:', error);
setQrCodes([]); setQrCodes([]);
setStats({ setStats({
@@ -301,27 +320,11 @@ export default function DashboardPage() {
} }
}; };
const getPlanBadgeColor = (plan: string) => { return (
switch (plan) { <div className="space-y-6">
case 'PRO':
return 'info';
case 'BUSINESS':
return 'warning';
default:
return 'default';
}
};
const getPlanEmoji = (plan: string) => {
// No emojis anymore
return '';
};
return (
<div className="space-y-8">
{/* Header with Plan Badge */} {/* Header with Plan Badge */}
<div className="flex items-start justify-between"> <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div> <div className="min-w-0">
<h1 className="text-3xl font-bold text-gray-900">{t('dashboard.title')}</h1> <h1 className="text-3xl font-bold text-gray-900">{t('dashboard.title')}</h1>
<p className="text-gray-600 mt-2"> <p className="text-gray-600 mt-2">
{!loading && qrCodes.length === 0 {!loading && qrCodes.length === 0
@@ -329,21 +332,23 @@ export default function DashboardPage() {
: t('dashboard.subtitle')} : t('dashboard.subtitle')}
</p> </p>
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex flex-wrap items-center gap-3">
<Badge variant={getPlanBadgeColor(userPlan)} className="text-lg px-4 py-2"> <Badge className="border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700">
{userPlan} Plan {userPlan} Plan
</Badge> </Badge>
{userPlan === 'FREE' && ( {userPlan === 'FREE' && (
<Link href="/pricing"> <Link href="/pricing">
<Button variant="primary">Upgrade</Button> <Button className="bg-primary-600 text-white hover:bg-primary-700">Upgrade</Button>
</Link> </Link>
)} )}
</div> </div>
</div> </div>
{/* Stats Grid */} {/* Stats Grid */}
<StatsGrid <OnboardingChecklist state={onboardingState} />
stats={stats}
<StatsGrid
stats={stats}
trends={{ trends={{
totalScans: analyticsData?.summary.scansTrend, totalScans: analyticsData?.summary.scansTrend,
comparisonPeriod: analyticsData?.summary.comparisonPeriod || 'month' comparisonPeriod: analyticsData?.summary.comparisonPeriod || 'month'
@@ -352,9 +357,9 @@ export default function DashboardPage() {
{/* Recent QR Codes */} {/* Recent QR Codes */}
<div> <div>
<div className="flex items-center justify-between mb-6"> <div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-xl font-semibold text-gray-900">{t('dashboard.recent_codes')}</h2> <h2 className="text-xl font-semibold text-gray-900">{t('dashboard.recent_codes')}</h2>
<div className="flex gap-3"> <div className="flex flex-wrap gap-3">
{qrCodes.length > 0 && ( {qrCodes.length > 0 && (
<Button <Button
variant="outline" variant="outline"
@@ -364,12 +369,12 @@ export default function DashboardPage() {
> >
{deletingAll ? 'Deleting...' : 'Delete All'} {deletingAll ? 'Deleting...' : 'Delete All'}
</Button> </Button>
)} )}
<Link href="/create"> <Link href="/create">
<Button>Create New QR Code</Button> <Button className="bg-primary-600 text-white hover:bg-primary-700">Create New QR Code</Button>
</Link> </Link>
</div> </div>
</div> </div>
{loading ? ( {loading ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -389,14 +394,14 @@ export default function DashboardPage() {
))} ))}
</div> </div>
) : qrCodes.length === 0 ? ( ) : qrCodes.length === 0 ? (
<div className="text-center py-16 border-2 border-dashed border-gray-200 rounded-xl"> <div className="rounded-[24px] border border-dashed border-gray-200 py-16 text-center">
<QrCode className="w-12 h-12 text-gray-300 mx-auto mb-4" /> <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> <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"> <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. You have {FREE_DYNAMIC_QR_LIMIT} free dynamic QR codes. They redirect wherever you want and track every scan.
</p> </p>
<Link href="/create"> <Link href="/create">
<Button>Create QR Code it takes 90 seconds</Button> <Button className="bg-primary-600 text-white hover:bg-primary-700">Create QR Code it takes 90 seconds</Button>
</Link> </Link>
</div> </div>
) : ( ) : (
@@ -521,4 +526,4 @@ export default function DashboardPage() {
</Dialog> </Dialog>
</div> </div>
); );
} }

View File

@@ -165,6 +165,8 @@ export default function SettingsPage() {
return { dynamic: 50, price: '€9', period: 'per month' }; return { dynamic: 50, price: '€9', period: 'per month' };
case 'BUSINESS': case 'BUSINESS':
return { dynamic: 500, price: '€29', period: 'per month' }; return { dynamic: 500, price: '€29', period: 'per month' };
case 'ENTERPRISE':
return { dynamic: 99999, price: 'Custom', period: 'per month' };
default: default:
return { dynamic: 3, price: '€0', period: 'forever' }; return { dynamic: 3, price: '€0', period: 'forever' };
} }

View File

@@ -5,23 +5,26 @@ import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardContent } from '@/components/ui/Card'; import { Card, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation'; import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf'; import { useCsrf } from '@/hooks/useCsrf';
import { appendRedirectParam, sanitizeRedirectPath } from '@/lib/auth-flow';
type LoginClientProps = { type LoginClientProps = {
showPageHeading?: boolean; showPageHeading?: boolean;
}; };
export default function LoginClient({ showPageHeading = true }: LoginClientProps) { export default function LoginClient({ showPageHeading = true }: LoginClientProps) {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { t } = useTranslation(); const { t } = useTranslation();
const { fetchWithCsrf, loading: csrfLoading } = useCsrf(); const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const redirectTarget = sanitizeRedirectPath(searchParams.get('redirect'));
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -56,10 +59,12 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
console.error('PostHog tracking error:', error); console.error('PostHog tracking error:', error);
} }
// Check for redirect parameter // Check for redirect parameter
const redirectUrl = searchParams.get('redirect') || '/dashboard'; const redirectUrl = data.needsOnboarding
router.push(redirectUrl); ? appendRedirectParam('/onboarding', redirectTarget)
router.refresh(); : (redirectTarget || '/dashboard');
router.push(redirectUrl);
router.refresh();
} else { } else {
setError(data.error || 'Invalid email or password'); setError(data.error || 'Invalid email or password');
} }
@@ -70,24 +75,24 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
} }
}; };
const handleGoogleSignIn = () => { const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route // Redirect to Google OAuth API route
window.location.href = '/api/auth/google'; window.location.href = appendRedirectParam('/api/auth/google', redirectTarget);
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="text-center mb-8"> <div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6"> <Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" /> <img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
<span className="text-2xl font-bold text-gray-900">QR Master</span> <span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link> </Link>
{showPageHeading ? ( {showPageHeading ? (
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1> <h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
) : ( ) : (
<h2 className="text-3xl font-bold text-gray-900">Welcome Back</h2> <h2 className="text-3xl font-bold text-gray-900">Welcome Back</h2>
)} )}
<p className="text-gray-600 mt-2">Sign in to your account</p> <p className="text-gray-600 mt-2">Sign in to your account</p>
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors"> <Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
Back to Home Back to Home
@@ -112,14 +117,37 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
required required
/> />
<Input <div className="space-y-1">
label="Password" <label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
type="password" <div className="relative">
value={password} <input
onChange={(e) => setPassword(e.target.value)} id="password"
placeholder="••••••••" type={showPassword ? 'text' : 'password'}
required value={password}
/> onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
tabIndex={-1}
>
{showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
</div>
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="flex items-center"> <label className="flex items-center">
@@ -175,9 +203,9 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
Don't have an account?{' '} Don't have an account?{' '}
<Link href="/signup" className="text-primary-600 hover:text-primary-700 font-medium"> <Link href={appendRedirectParam('/signup', redirectTarget)} className="text-primary-600 hover:text-primary-700 font-medium">
Sign up Sign up
</Link> </Link>
</p> </p>
</div> </div>
</CardContent> </CardContent>

View File

@@ -1,24 +1,29 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation'; import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf'; import { useCsrf } from '@/hooks/useCsrf';
import { appendRedirectParam, sanitizeRedirectPath } from '@/lib/auth-flow';
export default function SignupClient() {
const router = useRouter(); export default function SignupClient() {
const { t } = useTranslation(); const router = useRouter();
const { fetchWithCsrf } = useCsrf(); const searchParams = useSearchParams();
const { t } = useTranslation();
const { fetchWithCsrf } = useCsrf();
const [name, setName] = useState(''); const [name, setName] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const redirectTarget = sanitizeRedirectPath(searchParams.get('redirect'));
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -66,9 +71,9 @@ export default function SignupClient() {
console.error('PostHog tracking error:', error); console.error('PostHog tracking error:', error);
} }
// Redirect to dashboard // Redirect to onboarding
router.push('/dashboard'); router.push(appendRedirectParam('/onboarding', redirectTarget));
router.refresh(); router.refresh();
} else { } else {
setError(data.error || 'Failed to create account'); setError(data.error || 'Failed to create account');
} }
@@ -79,19 +84,19 @@ export default function SignupClient() {
} }
}; };
const handleGoogleSignIn = () => { const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route // Redirect to Google OAuth API route
window.location.href = '/api/auth/google'; window.location.href = appendRedirectParam('/api/auth/google', redirectTarget);
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="text-center mb-8"> <div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6"> <Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" /> <img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
<span className="text-2xl font-bold text-gray-900">QR Master</span> <span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link> </Link>
<h1 className="text-3xl font-bold text-gray-900">Create Account</h1> <h1 className="text-3xl font-bold text-gray-900">Create Account</h1>
<p className="text-gray-600 mt-2">Start creating QR codes in seconds</p> <p className="text-gray-600 mt-2">Start creating QR codes in seconds</p>
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors"> <Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
@@ -126,23 +131,69 @@ export default function SignupClient() {
required required
/> />
<Input <div className="space-y-1">
label="Password" <label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
type="password" <div className="relative">
value={password} <input
onChange={(e) => setPassword(e.target.value)} id="password"
placeholder="••••••••" type={showPassword ? 'text' : 'password'}
required value={password}
/> onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
tabIndex={-1}
>
{showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
</div>
</div>
<Input <div className="space-y-1">
label="Confirm Password" <label htmlFor="confirm-password" className="block text-sm font-medium text-gray-700">Confirm Password</label>
type="password" <div className="relative">
value={confirmPassword} <input
onChange={(e) => setConfirmPassword(e.target.value)} id="confirm-password"
placeholder="••••••••" type={showConfirmPassword ? 'text' : 'password'}
required value={confirmPassword}
/> onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
tabIndex={-1}
>
{showConfirmPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
</div>
</div>
<Button type="submit" className="w-full" loading={loading}> <Button type="submit" className="w-full" loading={loading}>
Create Account Create Account
@@ -186,11 +237,11 @@ export default function SignupClient() {
</form> </form>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
Already have an account?{' '} Already have an account?{' '}
<Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium"> <Link href={appendRedirectParam('/login', redirectTarget)} className="text-primary-600 hover:text-primary-700 font-medium">
Sign in Sign in
</Link> </Link>
</p> </p>
</div> </div>
</CardContent> </CardContent>

View File

@@ -86,10 +86,11 @@ export default function MarketingLayout({
<ul> <ul>
<li><a href="/">Home</a></li> <li><a href="/">Home</a></li>
<li><Link href="/pricing">{t.nav.pricing}</Link></li> <li><Link href="/pricing">{t.nav.pricing}</Link></li>
<li><Link href="/blog">{t.nav.blog}</Link></li> <li><Link href="/blog">{t.nav.blog}</Link></li>
<li><Link href="/learn">{t.nav.learn}</Link></li> <li><Link href="/learn">{t.nav.learn}</Link></li>
<li><Link href="/use-cases">Use Cases</Link></li> <li><Link href="/use-cases">Use Cases</Link></li>
<li><Link href="/faq">{t.nav.faq}</Link></li> <li><Link href="/restaurants">Restaurant Menu QR Codes</Link></li>
<li><Link href="/faq">{t.nav.faq}</Link></li>
<li><Link href="/about">{t.nav.about}</Link></li> <li><Link href="/about">{t.nav.about}</Link></li>
<li><Link href="/contact">{t.nav.contact}</Link></li> <li><Link href="/contact">{t.nav.contact}</Link></li>
<li><Link href="/login">{t.nav.login}</Link></li> <li><Link href="/login">{t.nav.login}</Link></li>
@@ -146,7 +147,7 @@ export default function MarketingLayout({
<Link href="/" className="flex items-center space-x-3 group"> <Link href="/" className="flex items-center space-x-3 group">
<div className="relative w-16 h-16 overflow-hidden rounded-full shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200"> <div className="relative w-16 h-16 overflow-hidden rounded-full shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
<Image <Image
src="/favicon1.png" src="/logo.svg"
alt="QR Master" alt="QR Master"
fill fill
sizes="64px" sizes="64px"

View File

@@ -8,11 +8,11 @@ import { CheckCircle2, Shield, Users, BarChart3, Globe, Lock } from 'lucide-reac
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto'; import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'About QR Master | The Team & Mission Behind the Platform', title: 'About QR Master | Free QR Code Generator for Businesses',
description: 'QR Master is built for measurable campaigns and secure QR code operations. Learn about our mission, values, and why businesses trust us.', description: 'QR Master helps businesses create, track, and manage QR codes at scale — free dynamic QR codes, real analytics, and no hidden limits. Learn who we are.',
openGraph: { openGraph: {
title: 'About QR Master | Dynamic QR Codes & Analytics', title: 'About QR Master | Free Dynamic QR Codes & Analytics',
description: 'We help businesses create, manage, and track QR codes at scale. Transparent pricing, privacy-first, and built for reliability.', description: 'Free dynamic QR codes with scan analytics, custom branding, and no reprint headaches. Learn about the team and mission behind QR Master.',
url: 'https://www.qrmaster.net/about', url: 'https://www.qrmaster.net/about',
type: 'website', type: 'website',
images: ['/og-image.png'], images: ['/og-image.png'],
@@ -112,11 +112,11 @@ export default function AboutPage() {
</div> </div>
</div> </div>
<div className="mt-12 text-center"> <div className="mt-12 text-center">
<Link href="/dynamic-qr-code-generator"> <Link href="/dynamic-qr-code-generator">
<Button size="lg">Create QR Code</Button> <Button size="lg">Create QR Code</Button>
</Link> </Link>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -14,7 +14,7 @@ const competitor = competitors['bitly'];
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
absolute: 'Bitly QR Code Alternative Purpose-Built for QR Campaigns | QR Master', absolute: 'QR Master vs Bitly QR Codes | Bitly Alternative',
}, },
description: description:
'Looking for a Bitly alternative for QR codes? Bitly\'s Core plan costs $10/month but only allows 2 QR codes total. QR Master is purpose-built for QR code management — 50 codes at €9/month, bulk creation, GDPR analytics. From €0.', 'Looking for a Bitly alternative for QR codes? Bitly\'s Core plan costs $10/month but only allows 2 QR codes total. QR Master is purpose-built for QR code management — 50 codes at €9/month, bulk creation, GDPR analytics. From €0.',
@@ -24,7 +24,7 @@ export const metadata: Metadata = {
canonical: 'https://www.qrmaster.net/alternatives/bitly', canonical: 'https://www.qrmaster.net/alternatives/bitly',
}, },
openGraph: { openGraph: {
title: 'Bitly QR Code Alternative Purpose-Built for QR Campaigns', title: 'QR Master vs Bitly QR Codes | Bitly Alternative',
description: description:
'Bitly\'s Core plan costs $10/month but only gives you 2 QR codes. QR Master gives you 50 dynamic QR codes at €9/month — purpose-built for QR workflows, bulk creation, and GDPR analytics.', 'Bitly\'s Core plan costs $10/month but only gives you 2 QR codes. QR Master gives you 50 dynamic QR codes at €9/month — purpose-built for QR workflows, bulk creation, and GDPR analytics.',
url: 'https://www.qrmaster.net/alternatives/bitly', url: 'https://www.qrmaster.net/alternatives/bitly',
@@ -32,7 +32,7 @@ export const metadata: Metadata = {
images: ['/og-image.png'], images: ['/og-image.png'],
}, },
twitter: { twitter: {
title: 'Bitly QR Code Alternative Purpose-Built for QR Campaigns', title: 'QR Master vs Bitly QR Codes | Bitly Alternative',
description: description:
'Bitly gives you 2 QR codes for $10/month. QR Master gives you 50 at €9/month — purpose-built for real QR campaigns, not link shortening with QR as an afterthought.', 'Bitly gives you 2 QR codes for $10/month. QR Master gives you 50 at €9/month — purpose-built for real QR campaigns, not link shortening with QR as an afterthought.',
}, },
@@ -40,6 +40,34 @@ export const metadata: Metadata = {
const comparisonRows = competitor.features; const comparisonRows = competitor.features;
const atAGlanceRows = [
{
useCase: 'A couple of QR codes',
bitly: 'Reasonable if you already pay for Bitly and only need 1-2 QR codes.',
qrMaster: 'Free plan includes 3 active dynamic QR codes plus unlimited static codes.',
},
{
useCase: 'Marketing campaign with many placements',
bitly: 'Entry plans hit QR count limits quickly.',
qrMaster: 'Pro includes 50 dynamic QR codes; Business includes 500.',
},
{
useCase: 'Bulk QR creation',
bitly: 'No dedicated bulk QR generator.',
qrMaster: 'CSV and Excel upload creates up to 1,000 unique QR codes per batch.',
},
{
useCase: 'QR campaign analytics',
bitly: 'Strong link analytics, but QR is a secondary workflow.',
qrMaster: 'QR-first scan analytics with device, country, time, and UTM context.',
},
{
useCase: 'Migration risk',
bitly: 'Printed codes depend on Bitly redirect infrastructure.',
qrMaster: 'Re-create destinations before canceling Bitly and replace codes on the next print cycle.',
},
];
const faqItems = [ const faqItems = [
{ {
question: 'How many QR codes does Bitly allow per plan?', question: 'How many QR codes does Bitly allow per plan?',
@@ -256,6 +284,41 @@ export default function BitlyAlternativePage() {
</div> </div>
</section> </section>
<section className="border-b bg-white py-20" style={{ borderColor: '#E4E0D9' }}>
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
<h2 className="mb-3 text-3xl font-bold tracking-tight sm:text-4xl" style={{ color: '#111110' }}>
QR Master vs Bitly at a glance
</h2>
<p className="mb-10 text-lg" style={{ color: '#71717A' }}>
Bitly is strongest as a link management platform. QR Master is stronger when QR codes are the main
campaign asset and you need predictable pricing, bulk creation, and QR-specific reporting.
</p>
<div className="overflow-hidden rounded-xl border" style={{ borderColor: '#E4E0D9' }}>
<div className="grid grid-cols-1 md:grid-cols-3" style={{ backgroundColor: '#F8F7F4' }}>
{['Use case', 'Bitly', 'QR Master'].map((heading) => (
<div key={heading} className="p-4 text-xs font-semibold uppercase tracking-wider" style={{ color: '#71717A' }}>
{heading}
</div>
))}
</div>
{atAGlanceRows.map((row, index) => (
<div
key={row.useCase}
className="grid grid-cols-1 md:grid-cols-3"
style={{
borderTop: '1px solid #E4E0D9',
backgroundColor: index % 2 === 0 ? '#FFFFFF' : '#FAFAF8',
}}
>
<div className="p-4 text-sm font-semibold" style={{ color: '#18181B' }}>{row.useCase}</div>
<div className="p-4 text-sm" style={{ color: '#52525B' }}>{row.bitly}</div>
<div className="p-4 text-sm font-medium" style={{ color: '#166534' }}>{row.qrMaster}</div>
</div>
))}
</div>
</div>
</section>
{/* Why Bitly is the Wrong Tool */} {/* Why Bitly is the Wrong Tool */}
<section className="py-24" style={{ backgroundColor: '#F8F7F4' }}> <section className="py-24" style={{ backgroundColor: '#F8F7F4' }}>
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8"> <div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">

View File

@@ -14,7 +14,7 @@ const competitor = competitors['flowcode'];
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
absolute: 'Flowcode Alternative No Forced Branding or Scan Interstitials | QR Master', absolute: 'QR Master vs Flowcode | Flowcode Alternative Without Forced Branding',
}, },
description: description:
'Looking for a Flowcode alternative? QR Master gives you clean, customizable QR codes without Flowcode\'s logo or scan-hijacking interstitial pages — from €0 free, Pro at €9/month.', 'Looking for a Flowcode alternative? QR Master gives you clean, customizable QR codes without Flowcode\'s logo or scan-hijacking interstitial pages — from €0 free, Pro at €9/month.',
@@ -24,7 +24,7 @@ export const metadata: Metadata = {
canonical: 'https://www.qrmaster.net/alternatives/flowcode', canonical: 'https://www.qrmaster.net/alternatives/flowcode',
}, },
openGraph: { openGraph: {
title: 'Flowcode Alternative No Forced Branding or Scan Interstitials', title: 'QR Master vs Flowcode | Flowcode Alternative Without Forced Branding',
description: description:
'Flowcode puts its logo on your QR codes and routes scans through its own branded page. QR Master gives you full branding control from the start.', 'Flowcode puts its logo on your QR codes and routes scans through its own branded page. QR Master gives you full branding control from the start.',
url: 'https://www.qrmaster.net/alternatives/flowcode', url: 'https://www.qrmaster.net/alternatives/flowcode',
@@ -32,7 +32,7 @@ export const metadata: Metadata = {
images: ['/og-image.png'], images: ['/og-image.png'],
}, },
twitter: { twitter: {
title: 'Flowcode Alternative No Forced Branding or Scan Interstitials', title: 'QR Master vs Flowcode | Flowcode Alternative Without Forced Branding',
description: description:
'Flowcode puts its logo on your QR codes and routes scans through its own branded page. QR Master gives you full branding control from the start.', 'Flowcode puts its logo on your QR codes and routes scans through its own branded page. QR Master gives you full branding control from the start.',
}, },
@@ -40,6 +40,34 @@ export const metadata: Metadata = {
const comparisonRows = competitor.features; const comparisonRows = competitor.features;
const atAGlanceRows = [
{
useCase: 'Free branded QR codes',
flowcode: 'Flowcode branding can appear in the code style and scan path on lower tiers.',
qrMaster: 'No forced QR Master logo or branded interstitial page.',
},
{
useCase: 'White-label brand control',
flowcode: 'Meaningful white-label control is typically tied to higher paid tiers.',
qrMaster: 'Custom colors and logo support start on Pro at EUR 9/month.',
},
{
useCase: 'Direct scan experience',
flowcode: 'Lower-tier scans may pass through a Flowcode-branded interstitial.',
qrMaster: 'Dynamic codes redirect directly to the destination after scan logging.',
},
{
useCase: 'Bulk QR creation',
flowcode: 'No built-in CSV or Excel bulk QR generator.',
qrMaster: 'Business supports up to 1,000 unique QR codes per bulk upload.',
},
{
useCase: 'EU privacy posture',
flowcode: 'US platform; EU teams should review data processing terms.',
qrMaster: 'Scan analytics hash IPs server-side before storage.',
},
];
const faqItems = [ const faqItems = [
{ {
question: 'What is the Flowcode interstitial page and why does it matter?', question: 'What is the Flowcode interstitial page and why does it matter?',
@@ -238,6 +266,41 @@ export default function FlowcodeAlternativePage() {
</div> </div>
</section> </section>
<section className="border-b bg-white py-20" style={{ borderColor: '#E4E0D9' }}>
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
<h2 className="mb-3 text-3xl font-bold tracking-tight sm:text-4xl" style={{ color: '#111110' }}>
QR Master vs Flowcode at a glance
</h2>
<p className="mb-10 text-lg" style={{ color: '#71717A' }}>
Flowcode is design-forward. QR Master is the cleaner fit when you need a neutral QR code, direct scan
experience, bulk creation, and predictable pricing.
</p>
<div className="overflow-hidden rounded-xl border" style={{ borderColor: '#E4E0D9' }}>
<div className="grid grid-cols-1 md:grid-cols-3" style={{ backgroundColor: '#F8F7F4' }}>
{['Use case', 'Flowcode', 'QR Master'].map((heading) => (
<div key={heading} className="p-4 text-xs font-semibold uppercase tracking-wider" style={{ color: '#71717A' }}>
{heading}
</div>
))}
</div>
{atAGlanceRows.map((row, index) => (
<div
key={row.useCase}
className="grid grid-cols-1 md:grid-cols-3"
style={{
borderTop: '1px solid #E4E0D9',
backgroundColor: index % 2 === 0 ? '#FFFFFF' : '#FAFAF8',
}}
>
<div className="p-4 text-sm font-semibold" style={{ color: '#18181B' }}>{row.useCase}</div>
<div className="p-4 text-sm" style={{ color: '#52525B' }}>{row.flowcode}</div>
<div className="p-4 text-sm font-medium" style={{ color: '#166534' }}>{row.qrMaster}</div>
</div>
))}
</div>
</div>
</section>
{/* The Real Problem with Flowcode */} {/* The Real Problem with Flowcode */}
<section className="py-24" style={{ backgroundColor: '#F8F7F4' }}> <section className="py-24" style={{ backgroundColor: '#F8F7F4' }}>
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8"> <div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">

View File

@@ -4,13 +4,14 @@ import Link from 'next/link';
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import SeoJsonLd from '@/components/SeoJsonLd'; import SeoJsonLd from '@/components/SeoJsonLd';
import { breadcrumbSchema } from '@/lib/schema'; import { breadcrumbSchema } from '@/lib/schema';
import { FAQSection } from '@/components/aeo/FAQSection';
import { MarketingPageTracker, TrackedCtaLink } from '@/components/marketing/MarketingAnalytics'; import { MarketingPageTracker, TrackedCtaLink } from '@/components/marketing/MarketingAnalytics';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
absolute: 'QR Code Platform Alternatives | QR Master', absolute: 'QR Master Alternatives: Compare QR Code Platforms',
}, },
description: description:
'Compare QR Master with QR-Code-Generator.com, Flowcode, Beaconstac / Uniqode, and Bitly. Find the right QR alternative for pricing, branding, analytics, and bulk creation.', 'Compare QR Master with QR-Code-Generator.com, Flowcode, Beaconstac / Uniqode, and Bitly. Find the right QR alternative for pricing, branding, analytics, and bulk creation.',
@@ -18,7 +19,7 @@ export const metadata: Metadata = {
canonical: 'https://www.qrmaster.net/alternatives', canonical: 'https://www.qrmaster.net/alternatives',
}, },
openGraph: { openGraph: {
title: 'QR Code Platform Alternatives | QR Master', title: 'QR Master Alternatives: Compare QR Code Platforms',
description: description:
'Clean comparisons for the most common QR code platform alternatives, from pricing traps to branding limits and enterprise overkill.', 'Clean comparisons for the most common QR code platform alternatives, from pricing traps to branding limits and enterprise overkill.',
url: 'https://www.qrmaster.net/alternatives', url: 'https://www.qrmaster.net/alternatives',
@@ -26,7 +27,7 @@ export const metadata: Metadata = {
images: ['/og-image.png'], images: ['/og-image.png'],
}, },
twitter: { twitter: {
title: 'QR Code Platform Alternatives | QR Master', title: 'QR Master Alternatives: Compare QR Code Platforms',
description: description:
'Compare QR Master with Flowcode, Bitly, Beaconstac / Uniqode, and QR-Code-Generator.com.', 'Compare QR Master with Flowcode, Bitly, Beaconstac / Uniqode, and QR-Code-Generator.com.',
}, },
@@ -46,6 +47,43 @@ const pageSchema = {
'Comparison pages for QR Master versus major QR code and adjacent link-management platforms.', 'Comparison pages for QR Master versus major QR code and adjacent link-management platforms.',
}; };
const faqItems = [
{
question: 'What is the best QR code platform alternative?',
answer:
'The best alternative depends on the workflow. QR Master is strongest when you need dynamic QR codes, clean branding, bulk creation, and privacy-friendly analytics without enterprise pricing.',
},
{
question: 'When should I choose QR Master over Bitly?',
answer:
'Choose QR Master when QR codes are the primary workflow. Bitly is useful for short links, but its QR code limits and link-first dashboard become restrictive for campaigns with many QR codes.',
},
{
question: 'When should I choose QR Master over Flowcode?',
answer:
'Choose QR Master when you want a direct scan experience without third-party branding or branded interstitial pages. QR Master keeps the scan flow focused on your destination.',
},
{
question: 'Is Beaconstac / Uniqode better than QR Master?',
answer:
'Beaconstac / Uniqode can make sense for enterprise teams that need a broad QR platform. QR Master is usually a better fit for smaller teams that want dynamic QR codes, analytics, and bulk creation at a simpler price point.',
},
];
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
'@id': 'https://www.qrmaster.net/alternatives#faq',
mainEntity: faqItems.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
};
const alternativePages = [ const alternativePages = [
{ {
href: '/alternatives/qr-code-generator', href: '/alternatives/qr-code-generator',
@@ -77,10 +115,61 @@ const alternativePages = [
}, },
]; ];
const comparisonRows = [
{
platform: 'Bitly',
href: '/alternatives/bitly',
price: 'Link-first paid plans',
freePlan: 'Limited QR use',
dynamicCodes: 'QR feature inside link management',
analytics: 'Link analytics first',
gdpr: 'Review DPA for EU use',
bulk: 'No QR-first bulk workflow',
branding: 'Bitly-branded link context',
qrMasterAngle: 'QR-first workflow with 50 dynamic codes on Pro',
},
{
platform: 'Flowcode',
href: '/alternatives/flowcode',
price: 'Brand and team tiers',
freePlan: 'Limited free path',
dynamicCodes: 'Strong QR design workflow',
analytics: 'Paid campaign analytics',
gdpr: 'US vendor; review data terms',
bulk: 'Best for larger plans',
branding: 'Design-forward, possible platform branding',
qrMasterAngle: 'Clean redirects and brand control without a branded scan page',
},
{
platform: 'Beaconstac / Uniqode',
href: '/alternatives/beaconstac',
price: 'Enterprise-oriented tiers',
freePlan: 'Trial or limited evaluation',
dynamicCodes: 'Advanced enterprise QR platform',
analytics: 'Advanced paid analytics',
gdpr: 'DPA and configuration review needed',
bulk: 'Strong, often enterprise-oriented',
branding: 'Enterprise controls',
qrMasterAngle: 'Simpler dynamic QR, analytics, and bulk creation for SMB teams',
},
{
platform: 'QR-Code-Generator.com',
href: '/alternatives/qr-code-generator',
price: 'Paid dynamic QR after trial',
freePlan: 'Static QR path',
dynamicCodes: 'Dynamic QR behind paid upgrade',
analytics: 'Paid analytics',
gdpr: 'Review vendor terms',
bulk: 'Paid workflow',
branding: 'Template-driven branding',
qrMasterAngle: 'Free dynamic codes and transparent paid tiers',
},
];
export default function AlternativesHubPage() { export default function AlternativesHubPage() {
return ( return (
<> <>
<SeoJsonLd data={[pageSchema, breadcrumbSchema(breadcrumbItems)]} /> <SeoJsonLd data={[pageSchema, faqSchema, breadcrumbSchema(breadcrumbItems)]} />
<MarketingPageTracker pageType="commercial" cluster="competitor" /> <MarketingPageTracker pageType="commercial" cluster="competitor" />
<div className="min-h-screen" style={{ backgroundColor: '#F8F7F4', color: '#18181B' }}> <div className="min-h-screen" style={{ backgroundColor: '#F8F7F4', color: '#18181B' }}>
<section className="border-b bg-white" style={{ borderColor: '#E4E0D9' }}> <section className="border-b bg-white" style={{ borderColor: '#E4E0D9' }}>
@@ -175,6 +264,79 @@ export default function AlternativesHubPage() {
</div> </div>
</div> </div>
</section> </section>
<section className="border-y bg-white py-24" style={{ borderColor: '#E4E0D9' }}>
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-12 max-w-3xl">
<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl" style={{ color: '#111110' }}>
QR Master vs Bitly, Flowcode, Beaconstac and other QR platforms
</h2>
<p className="text-lg" style={{ color: '#71717A' }}>
Use this table to pick the comparison page that matches your search intent: pricing, branding, bulk
creation, analytics, or migration risk.
</p>
</div>
<div className="overflow-x-auto rounded-xl border" style={{ borderColor: '#E4E0D9' }}>
<table className="w-full min-w-[980px] text-sm">
<thead style={{ backgroundColor: '#F8F7F4' }}>
<tr>
{[
'Platform',
'Price',
'Free plan',
'Dynamic codes',
'Analytics',
'GDPR / EU fit',
'Bulk',
'Branding',
'Compare',
].map((heading) => (
<th
key={heading}
className="p-4 text-left text-xs font-semibold uppercase tracking-wider"
style={{ color: '#71717A' }}
>
{heading}
</th>
))}
</tr>
</thead>
<tbody>
{comparisonRows.map((row, index) => (
<tr
key={row.platform}
style={{
borderTop: '1px solid #E4E0D9',
backgroundColor: index % 2 === 0 ? '#FFFFFF' : '#FAFAF8',
}}
>
<td className="p-4 font-semibold" style={{ color: '#18181B' }}>{row.platform}</td>
<td className="p-4" style={{ color: '#52525B' }}>{row.price}</td>
<td className="p-4" style={{ color: '#52525B' }}>{row.freePlan}</td>
<td className="p-4" style={{ color: '#52525B' }}>{row.dynamicCodes}</td>
<td className="p-4" style={{ color: '#52525B' }}>{row.analytics}</td>
<td className="p-4" style={{ color: '#52525B' }}>{row.gdpr}</td>
<td className="p-4" style={{ color: '#52525B' }}>{row.bulk}</td>
<td className="p-4" style={{ color: '#52525B' }}>{row.branding}</td>
<td className="p-4">
<Link href={row.href} className="font-semibold" style={{ color: '#166534' }}>
QR Master vs {row.platform}
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</section>
<section className="py-24" style={{ backgroundColor: '#F8F7F4' }}>
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<FAQSection items={faqItems} title="Common questions about QR platform alternatives" />
</div>
</section>
</div> </div>
</> </>
); );

View File

@@ -12,13 +12,13 @@ import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
import { MarketingPageTracker } from '@/components/marketing/MarketingAnalytics'; import { MarketingPageTracker } from '@/components/marketing/MarketingAnalytics';
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
absolute: 'Bulk QR Code Generator - Create QR Codes from a Spreadsheet', absolute: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
}, },
description: description:
'Generate up to 1,000 static QR codes from CSV or Excel files in the QR Master Business plan. Upload, preview, download as ZIP, or save the batch to your dashboard.', 'Generate up to 1,000 QR codes from Excel, CSV, XLSX, or exported Google Sheets data. Upload, preview, batch-create, download ZIP files, or save to your dashboard.',
keywords: keywords:
'bulk qr code generator, qr code from excel, csv qr code generator, bulk qr codes, spreadsheet qr generation', 'bulk qr code generator, bulk qr code generator excel, batch qr code generator, qr code from excel, csv qr code generator, bulk qr generator, bulk qr code generator in google sheets, spreadsheet qr generation',
alternates: { alternates: {
canonical: 'https://www.qrmaster.net/bulk-qr-code-generator', canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
languages: { languages: {
@@ -27,36 +27,41 @@ export const metadata: Metadata = {
}, },
}, },
openGraph: { openGraph: {
title: 'Bulk QR Code Generator - Create QR Codes from a Spreadsheet', title: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
description: description:
'Generate up to 1,000 static QR codes from CSV or Excel files in the QR Master Business plan.', 'Generate up to 1,000 QR codes from CSV, Excel, XLSX, or exported Google Sheets data.',
url: 'https://www.qrmaster.net/bulk-qr-code-generator', url: 'https://www.qrmaster.net/bulk-qr-code-generator',
type: 'website', type: 'website',
images: ['/og-image.png'], images: ['/og-image.png'],
}, },
twitter: { twitter: {
title: 'Bulk QR Code Generator - Create QR Codes from a Spreadsheet', title: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
description: description:
'Generate up to 1,000 static QR codes from CSV or Excel files in the QR Master Business plan.', 'Generate up to 1,000 QR codes from CSV, Excel, XLSX, or exported Google Sheets data.',
}, },
}; };
const featureCards = [ const featureCards = [
{ {
title: 'Spreadsheet upload', title: 'Spreadsheet upload',
description: description:
'Upload CSV, XLS, or XLSX files and map the title and content columns before generating the batch.', 'Upload CSV, XLS, or XLSX files from Excel, Numbers, Airtable, or a Google Sheets export and map the title and content columns before generating the batch.',
}, },
{
title: 'Excel and Google Sheets workflow',
description:
'Prepare rows in Excel or Google Sheets, export to CSV/XLSX, then generate the full QR code batch from one clean spreadsheet.',
},
{ {
title: 'Up to 1,000 rows per upload', title: 'Up to 1,000 rows per upload',
description: description:
'The current bulk creation flow limits each upload to 1,000 rows so the batch stays predictable and reviewable.', 'The current bulk creation flow limits each upload to 1,000 rows so the batch stays predictable and reviewable.',
}, },
{ {
title: 'Static QR output', title: 'Static QR output',
description: description:
'Bulk creation currently generates static QR codes. These codes do not include post-print editing or tracking.', 'Bulk creation currently generates static QR codes. These codes do not include post-print editing or tracking.',
}, },
{ {
title: 'ZIP download', title: 'ZIP download',
description: description:
@@ -67,16 +72,26 @@ const featureCards = [
description: description:
'After generation, you can save the batch into your QR Master dashboard for later management.', 'After generation, you can save the batch into your QR Master dashboard for later management.',
}, },
{ {
title: 'Business plan access', title: 'Business plan access',
description: description:
'The current bulk creation workflow is available to Business plan subscribers and is positioned around scale, not single-code creation.', 'The current bulk creation workflow is available to Business plan subscribers and is positioned around scale, not single-code creation.',
}, },
]; ];
const inputExamples = [ const inputExamples = [
{ {
title: 'Website URLs', title: 'Google Sheets export',
content: 'Name,URL\nMenu QR,https://example.com/menu\nFlyer QR,https://example.com/flyer',
note: 'Export a Google Sheet as CSV and upload it as a batch QR code generator input.',
},
{
title: 'Excel product list',
content: 'SKU,URL\nSKU-1001,https://example.com/products/1001',
note: 'Use Excel or XLSX rows when each product, insert, or label needs its own QR code.',
},
{
title: 'Website URLs',
content: 'https://example.com/product', content: 'https://example.com/product',
note: 'Useful for product pages, flyers, support pages, or campaign destinations.', note: 'Useful for product pages, flyers, support pages, or campaign destinations.',
}, },
@@ -135,11 +150,21 @@ const faqItems = [
answer: answer:
'No. The current bulk creation flow generates static QR codes, so those codes do not include post-print editing or tracking.', 'No. The current bulk creation flow generates static QR codes, so those codes do not include post-print editing or tracking.',
}, },
{ {
question: 'What file formats can I upload?', question: 'What file formats can I upload?',
answer: answer:
'The current flow accepts CSV, XLS, and XLSX files.', 'The current flow accepts CSV, XLS, and XLSX files.',
}, },
{
question: 'Can I use Google Sheets as the source?',
answer:
'Yes. Prepare the batch in Google Sheets, export it as CSV or XLSX, then upload that file to QR Master. The workflow is the same as an Excel upload.',
},
{
question: 'Is this a batch QR code generator?',
answer:
'Yes. QR Master bulk creation is a batch QR code generator for spreadsheet rows. Each row becomes one QR code, and the finished batch can be downloaded together.',
},
{ {
question: 'Which plan includes bulk QR creation?', question: 'Which plan includes bulk QR creation?',
answer: answer:
@@ -160,11 +185,12 @@ const softwareSchema = {
priceCurrency: 'EUR', priceCurrency: 'EUR',
availability: 'https://schema.org/InStock', availability: 'https://schema.org/InStock',
}, },
description: description:
'Generate up to 1,000 static QR codes from CSV or Excel files in the QR Master Business plan.', 'Generate up to 1,000 static QR codes from CSV, Excel, XLSX, or exported Google Sheets files in the QR Master Business plan.',
featureList: [ featureList: [
'CSV, XLS, and XLSX upload', 'CSV, XLS, and XLSX upload',
'Up to 1,000 rows per upload', 'Excel and Google Sheets CSV export workflow',
'Up to 1,000 rows per upload',
'Static QR code generation', 'Static QR code generation',
'ZIP download of generated SVG files', 'ZIP download of generated SVG files',
'Optional save-to-dashboard step', 'Optional save-to-dashboard step',
@@ -393,9 +419,52 @@ export default function BulkQRCodeGeneratorPage() {
</p> </p>
</div> </div>
<div className="container mx-auto max-w-5xl px-4 pb-8 sm:px-6 lg:px-8"> <section className="bg-white py-16">
<FAQSection items={faqItems} title="Bulk QR questions" /> <div className="container mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
</div> <div className="mb-10 max-w-3xl">
<p className="mb-3 text-sm font-semibold uppercase tracking-wider text-green-600">
Excel, CSV, and Google Sheets
</p>
<h2 className="text-3xl font-bold text-gray-900">
Batch QR code generation from spreadsheet rows
</h2>
<p className="mt-4 text-lg leading-relaxed text-gray-600">
Use QR Master as a bulk QR code generator when your source data
already lives in Excel, a CSV export, or Google Sheets. Keep one
row per QR code, map the columns, preview the batch, then export
the generated SVG files together.
</p>
</div>
<div className="grid gap-6 md:grid-cols-3">
{[
{
title: '1. Prepare columns',
body: 'Use columns such as title, URL, SKU, campaign, or location. The content column is what the QR code encodes.',
},
{
title: '2. Export clean data',
body: 'Save Excel as XLSX or export Google Sheets to CSV. Avoid merged cells and keep one QR code per row.',
},
{
title: '3. Generate the batch',
body: 'Upload the file, map title and content, preview the rows, and download the finished QR codes as a ZIP.',
},
].map((step) => (
<Card key={step.title} className="p-6">
<h3 className="mb-2 text-xl font-semibold text-gray-900">
{step.title}
</h3>
<p className="text-gray-600">{step.body}</p>
</Card>
))}
</div>
</div>
</section>
<div className="container mx-auto max-w-5xl px-4 pb-8 sm:px-6 lg:px-8">
<FAQSection items={faqItems} title="Bulk QR questions" />
</div>
<section className="bg-gray-50 py-20"> <section className="bg-gray-50 py-20">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -14,9 +14,9 @@ function truncateAtWord(text: string, maxLength: number): string {
} }
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const title = truncateAtWord('QR Master FAQ: Dynamic, Tracking, Bulk, and Print', 60); const title = truncateAtWord('QR Code FAQ Common Questions Answered | QR Master', 60);
const description = truncateAtWord( const description = truncateAtWord(
'Answers about dynamic QR codes, scan tracking, privacy, bulk creation, and print setup.', 'Quick answers to common QR code questions: Do QR codes expire? Static vs dynamic? Can they be scanned through laminate? Get clear answers.',
160 160
); );
@@ -58,6 +58,41 @@ type FAQItemWithRichText = {
}; };
const faqs: FAQItemWithRichText[] = [ const faqs: FAQItemWithRichText[] = [
{
question: 'Do QR codes expire?',
answer:
'Static QR codes never expire — the destination is permanently encoded in the image and works indefinitely. Dynamic QR codes remain active as long as your subscription is active. QR Master keeps static QR codes functional forever, including on the free plan.',
},
{
question: 'Will QR codes become obsolete?',
answer:
'QR codes are unlikely to become obsolete in the near future. Adoption has accelerated since 2020 — Statista reports that QR code usage grew by over 750% between 2018 and 2023. Every major smartphone camera app now natively reads QR codes without a separate app, removing the main adoption barrier.',
},
{
question: 'Will QR codes replace barcodes?',
answer:
'QR codes will not fully replace barcodes. Barcodes remain dominant in high-speed retail checkout due to laser scanner compatibility and established infrastructure. QR codes excel for consumer-facing use cases: menus, marketing, payments, and product pages. Both formats coexist across different industries.',
},
{
question: 'Do QR codes work through laminate?',
answer:
'Yes. QR codes work through standard laminate because the scanner reads the contrast pattern, not the physical surface. Matte laminate is preferable to gloss, which can cause glare under direct lighting. Avoid laminate with metallic or tinted finishes that alter contrast.',
},
{
question: 'Do QR codes work with a cracked phone screen?',
answer:
'Usually yes, as long as the camera can still capture the QR code image. Minor cracks often do not prevent scanning. A heavily cracked screen that distorts the camera view may cause scanning failures. The QR code itself is not affected — only the device reading it matters.',
},
{
question: 'When were QR codes invented?',
answer:
'QR codes were invented in 1994 by Masahiro Hara at Denso Wave, a Toyota subsidiary in Japan. They were originally designed to track automotive parts during manufacturing. QR stands for "Quick Response." The format was made publicly available royalty-free, which enabled widespread global adoption.',
},
{
question: 'Can QR codes run out?',
answer:
'No — QR codes cannot run out. The QR code standard supports approximately 10^9 unique combinations for a typical URL, far more than could ever be used. Generating a new QR code does not "use up" anything from a shared pool. Each code is generated independently.',
},
{ {
question: 'What is a dynamic QR code?', question: 'What is a dynamic QR code?',
answer: answer:
@@ -210,7 +245,7 @@ export default function FAQPage() {
<p className="mb-4 text-xl text-gray-600"> <p className="mb-4 text-xl text-gray-600">
Answers about dynamic QR codes, scan tracking, privacy, bulk creation, and print setup. Answers about dynamic QR codes, scan tracking, privacy, bulk creation, and print setup.
</p> </p>
<p className="text-sm text-gray-500">Last updated: March 12, 2026</p> <p className="text-sm text-gray-500">Last updated: April 20, 2026</p>
</div> </div>
<div className="space-y-6"> <div className="space-y-6">

View File

@@ -19,10 +19,11 @@ export const metadata: Metadata = {
: { index: false, follow: false }, : { index: false, follow: false },
icons: { icons: {
icon: [ icon: [
{ url: '/favicon1.png', sizes: '2048x2048', type: 'image/png' }, { url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/favicon.ico', sizes: '16x16 32x32', type: 'image/x-icon' },
], ],
shortcut: '/favicon1.png', shortcut: '/favicon.ico',
apple: '/favicon1.png', apple: '/logo.svg',
}, },
twitter: { twitter: {
card: 'summary_large_image', card: 'summary_large_image',

View File

@@ -2,16 +2,16 @@ import Link from "next/link";
import { pillarMeta } from "@/lib/pillar-data"; import { pillarMeta } from "@/lib/pillar-data";
import { getPublishedPosts } from "@/lib/content"; import { getPublishedPosts } from "@/lib/content";
export const metadata = { export const metadata = {
title: "Learn QR Code Mastery | QR Master Hub", title: "QR Code Tutorials & Guides QR Master",
description: "Guides, use cases, tracking deep-dives, and security best practices for dynamic QR codes.", description: "Free step-by-step QR code guides: create, track, and optimize dynamic QR codes for your business. No account needed to start.",
alternates: { alternates: {
canonical: "https://www.qrmaster.net/learn", canonical: "https://www.qrmaster.net/learn",
}, },
openGraph: { openGraph: {
url: "https://www.qrmaster.net/learn", url: "https://www.qrmaster.net/learn",
}, },
}; };
export default function LearnHubPage() { export default function LearnHubPage() {
const posts = getPublishedPosts(); const posts = getPublishedPosts();

File diff suppressed because it is too large Load Diff

View File

@@ -1,93 +1,134 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import SeoJsonLd from '@/components/SeoJsonLd'; import SeoJsonLd from '@/components/SeoJsonLd';
import { organizationSchema, websiteSchema, softwareApplicationSchema, reviewSchema, aggregateRatingSchema } from '@/lib/schema'; import {
import { getFeaturedTestimonials, getAggregateRating } from '@/lib/testimonial-data'; websiteSchema,
import HomePageClient from '@/components/marketing/HomePageClient'; softwareApplicationSchema,
} from '@/lib/schema';
function truncateAtWord(text: string, maxLength: number): string { import { getAggregateRating } from '@/lib/testimonial-data';
if (text.length <= maxLength) return text; import HomePageClient from '@/components/marketing/HomePageClient';
const truncated = text.slice(0, maxLength);
const lastSpace = truncated.lastIndexOf(' '); function truncateAtWord(text: string, maxLength: number): string {
return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated; if (text.length <= maxLength) return text;
} const truncated = text.slice(0, maxLength);
const lastSpace = truncated.lastIndexOf(' ');
export async function generateMetadata(): Promise<Metadata> { return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated;
const title = truncateAtWord('QR Master: Dynamic QR Generator', 60); }
const description = truncateAtWord(
'Create dynamic QR codes, track scans, and scale campaigns with secure analytics. Free advanced features, bulk generation, and custom branding available.', export async function generateMetadata(): Promise<Metadata> {
160 const description = truncateAtWord(
); 'QR Master is a free dynamic QR code generator with tracking, editable destinations, custom branding, and bulk QR creation. Create static QR codes without signup.',
160
return { );
title, const brandTitle = 'QR Master - Free Dynamic QR Code Generator with Tracking';
description,
keywords: ['qr generator', 'free qr code generator', 'custom qr code generator', 'qr code maker', 'online qr code generator', 'dynamic qr code', 'qr code with logo'], return {
alternates: { title: brandTitle,
canonical: 'https://www.qrmaster.net/', description,
languages: { keywords: [
'x-default': 'https://www.qrmaster.net/', 'qr generator',
en: 'https://www.qrmaster.net/', 'free qr code generator',
de: 'https://www.qrmaster.net/qr-code-erstellen', 'custom qr code generator',
}, 'qr code maker',
}, 'online qr code generator',
openGraph: { 'dynamic qr code',
title, 'qr code with logo',
description, 'barcode generator',
url: 'https://www.qrmaster.net/', 'free barcode generator',
type: 'website', 'qr master',
images: [ 'qrmaster',
{ 'qr code master',
url: 'https://www.qrmaster.net/og-image.png', ],
width: 1200, alternates: {
height: 630, canonical: 'https://www.qrmaster.net/',
alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform', languages: {
}, 'x-default': 'https://www.qrmaster.net/',
], en: 'https://www.qrmaster.net/',
}, de: 'https://www.qrmaster.net/qr-code-erstellen',
twitter: { },
title, },
description, openGraph: {
}, title: brandTitle,
}; description,
} url: 'https://www.qrmaster.net/',
type: 'website',
export default function HomePage() { images: [
const featuredTestimonials = getFeaturedTestimonials(); {
const aggregateRating = getAggregateRating(); url: 'https://www.qrmaster.net/og-image.png',
const reviewSchemas = featuredTestimonials.map(t => reviewSchema(t)); width: 1200,
height: 630,
return ( alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform',
<> },
<SeoJsonLd data={[ ],
websiteSchema(), },
organizationSchema(), twitter: {
softwareApplicationSchema(aggregateRating), title: brandTitle,
aggregateRatingSchema(aggregateRating), description,
...reviewSchemas },
]} /> };
}
{/* Server-rendered SEO content for crawlers */}
<div className="sr-only" aria-hidden="false"> export default function HomePage() {
const aggregateRating = getAggregateRating();
<p>
Create professional QR codes for your business with QR Master. Our dynamic QR code generator return (
lets you create trackable QR codes, edit destinations anytime, and view detailed analytics. <>
Perfect for restaurants, retail, events, and marketing campaigns. <SeoJsonLd
</p> data={[
<p> websiteSchema(),
Features include: Dynamic QR codes with real-time tracking, bulk QR code generation from Excel/CSV, softwareApplicationSchema(aggregateRating),
custom branding with colors and logos, advanced scan analytics showing device types and locations, ]}
vCard QR codes for digital business cards, restaurant menu QR codes, and a free{' '} />
<a href="/tools/barcode-generator">barcode generator</a> for EAN-13, UPC-A, and Code 128 barcodes.
</p> {/* Server-rendered SEO content for crawlers */}
<p> <div className="sr-only" aria-hidden="false">
Start free with 3 active dynamic QR codes and unlimited static codes. Upgrade to Pro for 50 codes <p>
with advanced analytics, or Business for 500 codes with bulk creation and priority support. Create professional QR codes for your business with QR Master. Our
</p> dynamic QR code generator lets you create trackable QR codes, edit
</div> destinations anytime, and view detailed analytics. Perfect for
restaurants, retail, events, and marketing campaigns.
<HomePageClient /> </p>
</> <p>
); Features include: Dynamic QR codes with real-time tracking, bulk QR
} code generation from Excel/CSV, custom branding with colors and logos,
advanced scan analytics showing device types and locations, vCard QR
codes for digital business cards, restaurant menu QR codes, and a free{' '}
<a href="/tools/barcode-generator">barcode generator</a> for EAN-13,
UPC-A, and Code 128 barcodes.
</p>
<p>
Popular QR Master workflows include the{' '}
<a href="/dynamic-qr-code-generator">
free dynamic QR code generator
</a>
, <a href="/qr-code-tracking">QR code tracking</a>, and the{' '}
<a href="/custom-qr-code-generator">custom QR code generator</a> for
branded print campaigns.
</p>
<p>
Start free with 3 active dynamic QR codes and unlimited static codes.
Upgrade to Pro for 50 codes with advanced analytics, or Business for
500 codes with bulk creation and priority support.
</p>
<p>
Frequently used QR Master tools and industry workflows include the{' '}
<a href="/tools/teams-qr-code">Teams QR code generator</a>,{' '}
<a href="/tools/wifi-qr-code">WiFi QR code generator</a>,{' '}
<a href="/qr-code-erstellen">German QR code generator</a>, and{' '}
<a href="/qr-code-for/barbershops">QR codes for barbershops</a>.
</p>
<p>
High-intent QR Master pages include the{' '}
<a href="/bulk-qr-code-generator">
bulk QR code generator for Excel and CSV files
</a>
, <a href="/alternatives">QR code platform alternatives</a>,{' '}
<a href="/alternatives/beaconstac">Beaconstac alternative</a>, and{' '}
<a href="/alternatives/bitly">Bitly QR code alternative</a>.
</p>
</div>
<HomePageClient />
</>
);
}

View File

@@ -1,269 +1,325 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { showToast } from '@/components/ui/Toast'; import { showToast } from '@/components/ui/Toast';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { BillingToggle } from '@/components/ui/BillingToggle'; import { BillingToggle } from '@/components/ui/BillingToggle';
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto'; import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
import { trackEvent } from '@/components/PostHogProvider';
export default function PricingPage() { import { FREE_DYNAMIC_QR_LIMIT } from '@/lib/plans';
const router = useRouter();
const [loading, setLoading] = useState<string | null>(null); export default function PricingPage() {
const [currentPlan, setCurrentPlan] = useState<string>('FREE'); const router = useRouter();
const [currentInterval, setCurrentInterval] = useState<'month' | 'year' | null>(null); const [loading, setLoading] = useState<string | null>(null);
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month'); const [currentPlan, setCurrentPlan] = useState<string>('FREE');
const [currentInterval, setCurrentInterval] = useState<
useEffect(() => { 'month' | 'year' | null
// Fetch current user plan >(null);
const fetchUserPlan = async () => { const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
try {
const response = await fetch('/api/user/plan'); useEffect(() => {
if (response.ok) { // Fetch current user plan
const data = await response.json(); const fetchUserPlan = async () => {
setCurrentPlan(data.plan || 'FREE'); try {
setCurrentInterval(data.interval || null); const response = await fetch('/api/user/plan');
} if (response.ok) {
} catch (error) { const data = await response.json();
console.error('Error fetching user plan:', error); setCurrentPlan(data.plan || 'FREE');
} setCurrentInterval(data.interval || null);
}; }
} catch (error) {
fetchUserPlan(); console.error('Error fetching user plan:', error);
}, []); }
};
const handleUpgrade = async (plan: 'PRO' | 'BUSINESS') => {
setLoading(plan); fetchUserPlan();
}, []);
try {
const response = await fetch('/api/stripe/create-checkout-session', { const handleUpgrade = async (plan: 'PRO' | 'BUSINESS') => {
method: 'POST', setLoading(plan);
headers: {
'Content-Type': 'application/json', try {
}, trackEvent('upgrade_clicked', {
body: JSON.stringify({ plan,
plan, billing_interval: billingPeriod,
billingInterval: billingPeriod === 'month' ? 'month' : 'year', source: 'pricing_page',
}), current_plan: currentPlan,
}); });
if (!response.ok) { const response = await fetch('/api/stripe/create-checkout-session', {
throw new Error('Failed to create checkout session'); method: 'POST',
} headers: {
'Content-Type': 'application/json',
const { url } = await response.json(); },
window.location.href = url; body: JSON.stringify({
} catch (error) { plan,
console.error('Error creating checkout session:', error); billingInterval: billingPeriod === 'month' ? 'month' : 'year',
showToast('Failed to start checkout. Please try again.', 'error'); }),
setLoading(null); });
}
}; if (!response.ok) {
const errorData = await response.json().catch(() => null);
const handleDowngrade = async () => { throw new Error(errorData?.error || 'Failed to create checkout session');
// Show confirmation dialog }
const confirmed = window.confirm(
'Are you sure you want to downgrade to the Free plan? Your subscription will be canceled immediately and you will lose access to premium features.' const { url } = await response.json();
); window.location.href = url;
} catch (error: any) {
if (!confirmed) { console.error('Error creating checkout session:', error);
return; showToast(error?.message || 'Failed to start checkout. Please try again.', 'error');
} setLoading(null);
}
setLoading('FREE'); };
try { const handleDowngrade = async () => {
const response = await fetch('/api/stripe/cancel-subscription', { // Show confirmation dialog
method: 'POST', const confirmed = window.confirm(
headers: { 'Are you sure you want to downgrade to the Free plan? Your subscription will be canceled immediately and you will lose access to premium features.'
'Content-Type': 'application/json', );
},
}); if (!confirmed) {
return;
if (!response.ok) { }
const error = await response.json();
throw new Error(error.error || 'Failed to cancel subscription'); setLoading('FREE');
}
try {
showToast('Successfully downgraded to Free plan', 'success'); const response = await fetch('/api/stripe/cancel-subscription', {
method: 'POST',
// Refresh to update the plan headers: {
setTimeout(() => { 'Content-Type': 'application/json',
window.location.reload(); },
}, 1500); });
} catch (error: any) {
console.error('Error canceling subscription:', error); if (!response.ok) {
showToast(error.message || 'Failed to downgrade. Please try again.', 'error'); const error = await response.json();
setLoading(null); throw new Error(error.error || 'Failed to cancel subscription');
} }
};
showToast('Successfully downgraded to Free plan', 'success');
// Helper function to check if this is the user's exact current plan (plan + interval)
const isCurrentPlanWithInterval = (planType: string, interval: 'month' | 'year') => { // Refresh to update the plan
return currentPlan === planType && currentInterval === interval; setTimeout(() => {
}; window.location.reload();
}, 1500);
// Helper function to check if user has this plan but different interval } catch (error: any) {
const hasPlanDifferentInterval = (planType: string) => { console.error('Error canceling subscription:', error);
return currentPlan === planType && currentInterval && currentInterval !== billingPeriod; showToast(
}; error.message || 'Failed to downgrade. Please try again.',
'error'
const selectedInterval = billingPeriod === 'month' ? 'month' : 'year'; );
setLoading(null);
const plans = [ }
{ };
key: 'free',
name: 'Free', // Helper function to check if this is the user's exact current plan (plan + interval)
price: '€0', const isCurrentPlanWithInterval = (
period: 'forever', planType: string,
showDiscount: false, interval: 'month' | 'year'
features: [ ) => {
'3 active dynamic QR codes (8 types available)', return currentPlan === planType && currentInterval === interval;
'Unlimited static QR codes', };
'Basic scan tracking',
'Standard QR design templates', // Helper function to check if user has this plan but different interval
'Download as SVG/PNG', const hasPlanDifferentInterval = (planType: string) => {
], return (
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free', currentPlan === planType &&
buttonVariant: 'outline' as const, currentInterval &&
disabled: currentPlan === 'FREE', currentInterval !== billingPeriod
popular: false, );
onDowngrade: handleDowngrade, };
},
{ const selectedInterval = billingPeriod === 'month' ? 'month' : 'year';
key: 'pro',
name: 'Pro', const plans = [
price: billingPeriod === 'month' ? '€9' : '€90', {
period: billingPeriod === 'month' ? 'per month' : 'per year', key: 'free',
showDiscount: billingPeriod === 'year', name: 'Free',
features: [ price: '€0',
'50 dynamic QR codes', period: 'forever',
'Unlimited static QR codes', showDiscount: false,
'Advanced analytics (scans, devices, locations)', features: [
'Custom branding (colors & logos)', `${FREE_DYNAMIC_QR_LIMIT} active dynamic QR codes (8 types available)`,
], 'Unlimited static QR codes',
buttonText: isCurrentPlanWithInterval('PRO', selectedInterval) 'Basic scan tracking',
? 'Current Plan' 'Standard QR design templates',
: hasPlanDifferentInterval('PRO') 'Download as SVG/PNG',
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}` ],
: 'Upgrade to Pro', buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free',
buttonVariant: 'primary' as const, buttonVariant: 'outline' as const,
disabled: isCurrentPlanWithInterval('PRO', selectedInterval), disabled: currentPlan === 'FREE',
popular: true, popular: false,
onUpgrade: () => handleUpgrade('PRO'), onDowngrade: handleDowngrade,
}, },
{ {
key: 'business', key: 'pro',
name: 'Business', name: 'Pro',
price: billingPeriod === 'month' ? '€29' : '€290', price: billingPeriod === 'month' ? '€9' : '€90',
period: billingPeriod === 'month' ? 'per month' : 'per year', period: billingPeriod === 'month' ? 'per month' : 'per year',
showDiscount: billingPeriod === 'year', showDiscount: billingPeriod === 'year',
features: [ features: [
'500 dynamic QR codes', '50 dynamic QR codes',
'Unlimited static QR codes', 'Unlimited static QR codes',
'Everything from Pro', 'Advanced analytics (scans, devices, locations)',
'Bulk QR Creation (up to 1,000)', 'Custom branding (colors & logos)',
'Priority email support', ],
'Advanced tracking & insights', buttonText: isCurrentPlanWithInterval('PRO', selectedInterval)
], ? 'Current Plan'
buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval) : hasPlanDifferentInterval('PRO')
? 'Current Plan' ? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: hasPlanDifferentInterval('BUSINESS') : 'Upgrade to Pro',
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}` buttonVariant: 'primary' as const,
: 'Upgrade to Business', disabled: isCurrentPlanWithInterval('PRO', selectedInterval),
buttonVariant: 'primary' as const, popular: true,
disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval), onUpgrade: () => handleUpgrade('PRO'),
popular: false, },
onUpgrade: () => handleUpgrade('BUSINESS'), {
}, key: 'business',
]; name: 'Business',
price: billingPeriod === 'month' ? '€29' : '€290',
return ( period: billingPeriod === 'month' ? 'per month' : 'per year',
<div className="container mx-auto px-4 py-12"> showDiscount: billingPeriod === 'year',
<div className="text-center mb-12"> features: [
<h1 className="text-4xl font-bold text-gray-900 mb-4"> '500 dynamic QR codes',
Choose Your Plan 'Unlimited static QR codes',
</h1> 'Everything from Pro',
<p className="text-xl text-gray-600"> 'Bulk QR Creation (up to 1,000)',
Select the perfect plan for your QR code needs 'Priority email support',
</p> 'Advanced tracking & insights',
</div> ],
buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval)
<div className="flex justify-center mb-8"> ? 'Current Plan'
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} /> : hasPlanDifferentInterval('BUSINESS')
</div> ? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: 'Upgrade to Business',
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto"> buttonVariant: 'primary' as const,
{plans.map((plan) => ( disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval),
<Card popular: false,
key={plan.key} onUpgrade: () => handleUpgrade('BUSINESS'),
className={plan.popular ? 'border-primary-500 shadow-xl relative' : ''} },
> {
{plan.popular && ( key: 'enterprise',
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2"> name: 'Enterprise',
<Badge variant="info" className="px-3 py-1"> price: 'Custom',
Most Popular period: '',
</Badge> showDiscount: false,
</div> features: [
)} '∞ dynamic QR codes',
'Unlimited static QR codes',
<CardHeader className="text-center pb-8"> 'Everything from Business',
<CardTitle className="text-2xl mb-4"> 'Dedicated Account Manager',
{plan.name} ],
</CardTitle> buttonText: 'Contact Us',
<div className="flex flex-col items-center"> buttonVariant: 'outline' as const,
<div className="flex items-baseline justify-center"> disabled: false,
<span className="text-4xl font-bold"> popular: false,
{plan.price} onUpgrade: () => (window.location.href = 'mailto:timo@qrmaster.net'),
</span> },
<span className="text-gray-600 ml-2"> ];
{plan.period}
</span> return (
</div> <div className="container mx-auto px-4 py-12">
{plan.showDiscount && ( <div className="text-center mb-12">
<Badge variant="success" className="mt-2"> <h1 className="text-4xl font-bold text-gray-900 mb-4">
Save 16% Choose Your Plan
</Badge> </h1>
)} <p className="text-xl text-gray-600">
</div> Select the perfect plan for your QR code needs
</CardHeader> </p>
</div>
<CardContent className="space-y-6">
<ul className="space-y-3"> <div className="flex justify-center mb-8">
{plan.features.map((feature: string, index: number) => ( <BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
<li key={index} className="flex items-start space-x-3"> </div>
<svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto">
</svg> {plans.map((plan) => (
<span className="text-gray-700">{feature}</span> <Card
</li> key={plan.key}
))} className={
</ul> plan.popular ? 'border-primary-500 shadow-xl relative' : ''
}
<Button >
variant={plan.buttonVariant} {plan.popular && (
className="w-full" <div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
size="lg" <Badge variant="info" className="px-3 py-1">
disabled={plan.disabled || loading === plan.key.toUpperCase()} Most Popular
onClick={plan.key === 'free' ? (plan as any).onDowngrade : (plan as any).onUpgrade} </Badge>
> </div>
{loading === plan.key.toUpperCase() ? 'Processing...' : plan.buttonText} )}
</Button>
</CardContent> <CardHeader className="text-center pb-8">
</Card> <CardTitle className="text-2xl mb-4">{plan.name}</CardTitle>
))} <div className="flex flex-col items-center">
</div> <div className="flex items-baseline justify-center">
<span className="text-4xl font-bold">{plan.price}</span>
<div className="text-center mt-12"> <span className="text-gray-600 ml-2">{plan.period}</span>
<p className="text-gray-600"> </div>
All plans include unlimited static QR codes and basic customization. {plan.showDiscount && (
</p> <Badge variant="success" className="mt-2">
<p className="text-gray-600 mt-2"> Save 16%
Need help choosing? <ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</ObfuscatedMailto> </Badge>
</p> )}
</div> </div>
</div> </CardHeader>
);
} <CardContent className="space-y-6">
<ul className="space-y-3">
{plan.features.map((feature: string, index: number) => (
<li key={index} className="flex items-start space-x-3">
<svg
className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-gray-700">{feature}</span>
</li>
))}
</ul>
<Button
variant={plan.buttonVariant}
className="w-full"
size="lg"
disabled={plan.disabled || loading === plan.key.toUpperCase()}
onClick={
plan.key === 'free'
? (plan as any).onDowngrade
: (plan as any).onUpgrade
}
>
{loading === plan.key.toUpperCase()
? 'Processing...'
: plan.buttonText}
</Button>
</CardContent>
</Card>
))}
</div>
<div className="text-center mt-12">
<p className="text-gray-600">
All plans include unlimited static QR codes and basic customization.
</p>
<p className="text-gray-600 mt-2">
Need help choosing?{' '}
<ObfuscatedMailto
email="support@qrmaster.net"
className="text-primary-600 hover:text-primary-700 underline"
>
Contact our team
</ObfuscatedMailto>
</p>
</div>
</div>
);
}

View File

@@ -1,179 +1,227 @@
import type { Metadata } from "next"; import type { Metadata } from 'next';
import { import {
buildUseCaseMetadata, buildUseCaseMetadata,
UseCasePageTemplate, UseCasePageTemplate,
} from "@/components/marketing/UseCasePageTemplate"; } from '@/components/marketing/UseCasePageTemplate';
import { GrowthLinksSection } from "@/components/marketing/GrowthLinksSection"; import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
export const metadata: Metadata = buildUseCaseMetadata({ export const metadata: Metadata = buildUseCaseMetadata({
title: "QR Code Analytics", title: 'QR Code Analytics: Measure Offline Campaigns',
description: description:
"Measure QR code scans by placement, timing, device context, and campaign route so offline workflows become reportable.", 'Use QR code analytics to interpret scan data, compare placements, measure offline campaigns, and decide what to update or reprint next.',
canonicalPath: "/qr-code-analytics", canonicalPath: '/qr-code-analytics',
}); });
const softwareSchema = { const softwareSchema = {
"@context": "https://schema.org", '@context': 'https://schema.org',
"@type": "SoftwareApplication", '@type': 'SoftwareApplication',
"@id": "https://www.qrmaster.net/qr-code-analytics#software", '@id': 'https://www.qrmaster.net/qr-code-analytics#software',
name: "QR Master - QR Code Analytics", name: 'QR Master - QR Code Analytics',
applicationCategory: "BusinessApplication", applicationCategory: 'BusinessApplication',
operatingSystem: "Web Browser", operatingSystem: 'Web Browser',
offers: { offers: {
"@type": "Offer", '@type': 'Offer',
price: "0", price: '0',
priceCurrency: "USD", priceCurrency: 'USD',
availability: "https://schema.org/InStock", availability: 'https://schema.org/InStock',
}, },
description: description:
"QR analytics software for measuring scans by placement, timing, device context, and offline campaign routing.", 'QR analytics software for measuring scans by placement, timing, device context, and offline campaign routing.',
featureList: [ featureList: [
"Placement-level scan reporting", 'Placement-level scan reporting',
"Device and timing context", 'Device and timing context',
"Offline-to-online campaign attribution", 'Offline-to-online campaign attribution',
"Scan visibility across print workflows", 'Scan visibility across print workflows',
"Destination updates without reprinting", 'Destination updates without reprinting',
], ],
}; };
export default function QRCodeAnalyticsPage() { export default function QRCodeAnalyticsPage() {
return ( return (
<> <>
<UseCasePageTemplate <UseCasePageTemplate
title="QR Code Analytics" title="QR Code Analytics: Measure Offline Campaigns"
description="Measure QR code scans by placement, timing, device context, and campaign route so offline workflows become reportable." description="Use QR code analytics to interpret scan data, compare placements, measure offline campaigns, and decide what to update or reprint next."
eyebrow="Analytics" eyebrow="Analytics interpretation"
intro="QR code analytics matters when a scan is not just a click, but evidence that a sign, flyer, package, or service prompt is doing its job in the real world." intro="QR code analytics turns scan events into decisions: which printed placement worked, which destination needs work, and what should be changed before the next print run."
pageType="commercial" pageType="commercial"
cluster="qr-analytics" cluster="qr-analytics"
useCase="qr-analytics" useCase="qr-analytics"
breadcrumbs={[ breadcrumbs={[
{ name: "Home", url: "/" }, { name: 'Home', url: '/' },
{ name: "QR Code Analytics", url: "/qr-code-analytics" }, { name: 'QR Code Analytics', url: '/qr-code-analytics' },
]} ]}
answer="QR code analytics helps you understand which printed placements, campaigns, and post-scan routes generate useful activity so you can improve what gets reprinted, redistributed, or scaled next." answer="QR code analytics helps teams interpret QR scan data by placement, timing, device context, and campaign route so offline marketing decisions are based on evidence rather than raw scan counts alone."
whenToUse={[ whenToUse={[
"You need more than raw scan counts from campaigns, packaging, or offline placements.", 'You need more than raw scan counts from campaigns, packaging, or offline placements.',
"You want to compare where scans happen and which printed surfaces actually drive action.", 'You want to compare where scans happen and which printed surfaces actually drive action.',
"You need a clearer bridge between QR scans and business outcomes such as signup, offers, or support engagement.", 'You need a clearer bridge between QR scans and business outcomes such as signup, offers, or support engagement.',
]} ]}
comparisonItems={[ comparisonItems={[
{ label: "Placement visibility", text: "Usually blended", value: true }, {
{ label: "Post-print reporting", text: "Weak", value: true }, label: 'Tracking collects scan events',
{ label: "Campaign comparison", text: "Manual or partial", value: true }, text: 'Input data',
]} value: true,
howToSteps={[ },
"Create QR flows that map to real placements or workflow contexts instead of one generic code for every use case.", {
"Track scans with enough context to compare signs, flyers, inserts, or support surfaces cleanly.", label: 'Analytics explains performance',
"Use the reporting to decide which destinations, offers, or print placements deserve the next round of investment.", text: 'Decision layer',
]} value: true,
primaryCta={{ },
href: "/signup", {
label: "Start measuring QR scans", label: 'Reprint decisions',
}} text: 'Based on evidence',
secondaryCta={{ value: true,
href: "/use-cases", },
label: "Browse measured workflows", ]}
}} howToSteps={[
workflowTitle="What useful QR analytics should help you answer" 'Create QR flows that map to real placements or workflow contexts instead of one generic code for every use case.',
workflowIntro="The point of analytics is not to produce dashboards for their own sake. It is to make better decisions about what to print again, where to place it, and what happens after the scan." 'Track scans with enough context to compare signs, flyers, inserts, or support surfaces cleanly.',
workflowCards={[ 'Use the reporting to decide which destinations, offers, or print placements deserve the next round of investment.',
{ ]}
title: "Placement comparison", primaryCta={{
description: href: '/signup',
"Separate flyer, packaging, sign, event, or service-surface traffic so you know which printed context actually creates useful scan activity.", label: 'Start measuring QR scans',
}, }}
{ secondaryCta={{
title: "Post-print flexibility", href: '/use-cases',
description: label: 'Browse measured workflows',
"Review performance and then improve the destination, offer, or next action without replacing every physical code already in circulation.", }}
}, workflowTitle="Questions QR analytics should answer"
{ workflowIntro="The point of analytics is not to produce dashboards for their own sake. It is to decide what to print again, where to place it, and what should happen after the scan."
title: "Operational reporting", workflowCards={[
description: {
"Give marketing, operations, or support teams a better view of what physical QR programs are doing once they are live in the field.", title: 'Which placement worked?',
}, description:
]} 'Separate flyer, packaging, sign, event, or service-surface traffic so you know which printed context actually creates useful scan activity.',
checklistTitle="QR analytics checklist" },
checklist={[ {
"Define which placements or workflow surfaces should be compared before launching the QR program.", title: 'What should change next?',
"Use naming or routing that lets scans be grouped by real business context, not only by one generic campaign.", description:
"Make the first post-scan step relevant enough that a scan can become a useful action, not a bounce.", 'Review performance and then improve the destination, offer, or next action without replacing every physical code already in circulation.',
"Review analytics before reprinting so the next batch reflects real-world performance.", },
]} {
supportLinks={[ title: 'What should be reprinted?',
{ description:
href: "/use-cases/packaging-qr-codes", 'Give marketing, operations, or support teams a clearer view of which physical QR programs deserve another batch.',
title: "Use case: Packaging QR Codes", },
description: ]}
"See how packaging scans can become a measurable post-purchase signal instead of a blind spot.", checklistTitle="QR analytics checklist"
}, checklist={[
{ 'Define which placements or workflow surfaces should be compared before launching the QR program.',
href: "/use-cases/flyer-qr-codes", 'Use naming or routing that lets scans be grouped by real business context, not only by one generic campaign.',
title: "Use case: Flyer QR Codes", 'Make the first post-scan step relevant enough that a scan can become a useful action, not a bounce.',
description: 'Review analytics before reprinting so the next batch reflects real-world performance.',
"Useful when scan performance needs to be reviewed by distribution point or campaign wave.", ]}
}, supportLinks={[
{ {
href: "/blog/trackable-qr-codes", href: '/use-cases/packaging-qr-codes',
title: "Trackable QR Codes", title: 'Use case: Packaging QR Codes',
description: description:
"Support article for understanding what measurable QR setups should capture and why it matters.", 'See how packaging scans can become a measurable post-purchase signal instead of a blind spot.',
}, },
]} {
faq={[ href: '/qr-code-for-marketing-campaigns',
{ title: 'QR Codes for Marketing Campaigns',
question: "What can QR code analytics show?", description:
answer: 'Plan campaign QR workflows around attribution, creative testing, and print distribution.',
"QR code analytics can show scan activity by placement, time, device context, and campaign route so teams can see which physical programs are actually performing.", },
}, {
{ href: '/use-cases/flyer-qr-codes',
question: "Why are QR code analytics useful for offline campaigns?", title: 'Use case: Flyer QR Codes',
answer: description:
"They help turn offline placements such as flyers, packaging, signs, or event materials into something measurable instead of relying on assumptions about what worked.", 'Useful when scan performance needs to be reviewed by distribution point or campaign wave.',
}, },
{ {
question: "Do I need dynamic QR codes for analytics?", href: '/blog/utm-parameter-qr-codes',
answer: title: 'UTM Parameters with QR Codes',
"In most cases yes, because analytics usually depends on a managed redirect or reporting layer that also lets you update destinations without reprinting.", description:
}, 'Use GA4 campaign parameters when QR scan data needs to connect to post-scan conversions.',
]} },
schemaData={[softwareSchema]} {
/> href: '/blog/trackable-qr-codes',
<GrowthLinksSection title: 'Trackable QR Codes',
eyebrow="Related workflows" description:
title="Analytics is only the start" 'Support article for understanding what measurable QR setups should capture and why it matters.',
description="Tracking scans is more useful when it connects to destination flexibility, campaign comparison, and cost planning." },
links={[ ]}
{ faq={[
href: '/qr-code-tracking', {
title: 'QR Code Tracking', question: 'What can QR code analytics show?',
description: 'See device, time, and location context for every scan. Understand which placements drive real activity.', answer:
ctaLabel: 'Track QR code scans', 'QR code analytics can show scan activity by placement, time, device context, and campaign route so teams can see which physical programs are actually performing.',
}, },
{ {
href: '/dynamic-qr-code-generator', question:
title: 'Dynamic QR Code Generator', 'What is the difference between QR tracking and QR analytics?',
description: 'Create QR codes with updatable destinations so analytics can inform what to change — without reprinting.', answer:
ctaLabel: 'Create dynamic QR code', 'QR tracking collects scan events. QR analytics interprets those events so teams can compare placements, understand campaign performance, and decide what to update or reprint.',
}, },
{ {
href: '/reprint-calculator', question: 'Why are QR code analytics useful for offline campaigns?',
title: 'Reprint Cost Calculator', answer:
description: 'Calculate how much static reprints are costing vs one dynamic QR subscription.', 'They help turn offline placements such as flyers, packaging, signs, or event materials into something measurable instead of relying on assumptions about what worked.',
ctaLabel: 'Calculate reprint savings', },
}, {
{ question: 'Do I need dynamic QR codes for analytics?',
href: '/pricing', answer:
title: 'Compare Plans', 'In most cases yes, because analytics usually depends on a managed redirect or reporting layer that also lets you update destinations without reprinting.',
description: 'See which plan gives you the scan volume, analytics depth, and QR code count your workflows need.', },
ctaLabel: 'Compare plans', ]}
}, schemaData={[softwareSchema]}
]} />
pageType="commercial" <GrowthLinksSection
cluster="qr-analytics" eyebrow="Related workflows"
/> title="Analytics is only the start"
</> description="Tracking scans is more useful when it connects to destination flexibility, campaign comparison, and cost planning."
); links={[
} {
href: '/qr-code-tracking',
title: 'QR Code Tracking',
description:
'See device, time, and location context for every scan. Understand which placements drive real activity.',
ctaLabel: 'Track QR code scans',
},
{
href: '/dynamic-qr-code-generator',
title: 'Dynamic QR Code Generator',
description:
'Create QR codes with updatable destinations so analytics can inform what to change — without reprinting.',
ctaLabel: 'Create dynamic QR code',
},
{
href: '/reprint-calculator',
title: 'Reprint Cost Calculator',
description:
'Calculate how much static reprints are costing vs one dynamic QR subscription.',
ctaLabel: 'Calculate reprint savings',
},
{
href: '/qr-code-for-marketing-campaigns',
title: 'QR Codes for Marketing Campaigns',
description:
'Plan offline campaigns where each placement has a measurable QR route and follow-up action.',
ctaLabel: 'Measure offline QR campaigns',
},
{
href: '/use-cases/qr-codes-for-review-collection',
title: 'QR Codes for Review Collection',
description:
'Compare receipts, table cards, packaging, and counters as measurable review-request placements.',
ctaLabel: 'Measure review QR placements',
},
{
href: '/pricing',
title: 'Compare Plans',
description:
'See which plan gives you the scan volume, analytics depth, and QR code count your workflows need.',
ctaLabel: 'Compare plans',
},
]}
pageType="commercial"
cluster="qr-analytics"
/>
</>
);
}

View File

@@ -133,6 +133,8 @@ const highlightedLinks = [
{ href: "/qr-code-tracking", title: "QR Code Tracking", description: "Measure scans from flyers, property signage, retail displays, trade shows, and offline campaigns." }, { href: "/qr-code-tracking", title: "QR Code Tracking", description: "Measure scans from flyers, property signage, retail displays, trade shows, and offline campaigns." },
{ href: "/bulk-qr-code-generator", title: "Bulk QR Code Generator", description: "Best fit for packaging, product lines, multi-location rollouts, and large campaigns." }, { href: "/bulk-qr-code-generator", title: "Bulk QR Code Generator", description: "Best fit for packaging, product lines, multi-location rollouts, and large campaigns." },
{ href: "/tools/wifi-qr-code", title: "WiFi QR Code Tool", description: "Popular for hotels, cafes, coworking spaces, clinics, and guest-facing venues." }, { href: "/tools/wifi-qr-code", title: "WiFi QR Code Tool", description: "Popular for hotels, cafes, coworking spaces, clinics, and guest-facing venues." },
{ href: "/tools/teams-qr-code", title: "Teams QR Code Tool", description: "Useful for offices, meeting rooms, coworking spaces, and event check-in workflows." },
{ href: "/qr-code-for/barbershops", title: "QR Codes for Barbershops", description: "A high-intent local service page for bookings, reviews, WiFi, and social profile QR codes." },
]; ];
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -244,7 +246,7 @@ export default function IndustryOverviewPage() {
<section className="py-16"> <section className="py-16">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid gap-6 lg:grid-cols-4"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{highlightedLinks.map((item) => ( {highlightedLinks.map((item) => (
<Link <Link
key={item.href} key={item.href}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,872 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import {
ArrowRight,
BarChart3,
Calculator,
Check,
Clock3,
FileText,
Link2,
Printer,
QrCode,
RefreshCw,
ShieldCheck,
} from "lucide-react";
import Breadcrumbs from "@/components/Breadcrumbs";
import SeoJsonLd from "@/components/SeoJsonLd";
import {
MarketingPageTracker,
TrackedCtaLink,
} from "@/components/marketing/MarketingAnalytics";
import { Button } from "@/components/ui/Button";
import { breadcrumbSchema, faqPageSchema, softwareApplicationSchema } from "@/lib/schema";
const SITE_URL = "https://www.qrmaster.net";
const PAGE_URL = `${SITE_URL}/restaurants`;
const HERO_IMAGE = "/restaurants-hero-wide.webp";
const OG_IMAGE = "/restaurants-hero-og.jpg";
const CAMPAIGN_SIGNUP =
"/signup?utm_source=meta&utm_medium=paid_social&utm_campaign=restaurant_menu_landing&utm_content=restaurants_page_cta";
const FAQ = [
{
question: "What is a dynamic QR code for restaurant menus?",
answer:
"A dynamic QR code lets a restaurant keep the same printed QR code while changing the destination behind it. You can update a menu PDF, menu page, or ordering link without replacing table tents, flyers, or window signs.",
},
{
question: "Can I change menu prices after the QR code is printed?",
answer:
"Yes. With QR Master, the printed QR code points to a managed redirect. When prices, dishes, or PDFs change, you update the destination in the dashboard and keep the same printed code.",
},
{
question: "Is QR Master useful for small restaurants?",
answer:
"Yes. Small restaurants can start with a free account, create a dynamic menu QR code, and use scan analytics to see whether guests actually use the menu link.",
},
{
question: "Do I need to reprint my menu QR code every time the PDF changes?",
answer:
"No. If the code is dynamic, the printed QR code can stay on the table while the destination changes online.",
},
{
question: "Should a restaurant menu QR code be static or dynamic?",
answer:
"A static QR code is acceptable only when the destination will never change. Restaurants usually need a dynamic QR code because menus, prices, PDFs, opening hours, and ordering links change after print.",
},
{
question: "Can I use a QR code for a menu PDF?",
answer:
"Yes. You can point a dynamic QR code to a menu PDF and replace that PDF later. The QR code on the table can stay the same while the file or destination is updated in QR Master.",
},
{
question: "Can I track scans from table tents, flyers, and window signs separately?",
answer:
"Yes. Use separate dynamic QR codes or tagged destination URLs for each placement. That lets you compare tables, flyers, receipts, window signs, and campaign materials instead of treating every scan as the same source.",
},
{
question: "Does a restaurant menu QR code need a landing page?",
answer:
"If the QR code is for guests at the table, it can open the menu directly. If the QR code is used in ads or flyers, a focused landing page often works better because it can explain the offer before asking visitors to sign up or order.",
},
{
question: "Can I add UTM parameters to restaurant QR codes?",
answer:
"Yes. UTM parameters are useful when you want to measure traffic from different printed placements or Meta ad campaigns. QR Master can point dynamic codes to tagged URLs so analytics tools can separate sources and campaigns.",
},
{
question: "Is scan analytics privacy-friendly for restaurant guests?",
answer:
"QR Master is designed for privacy-conscious scan analytics. Restaurant teams can see practical scan context such as timing and device patterns without turning a menu QR code into intrusive guest tracking.",
},
{
question: "What should a restaurant put on a QR code table tent?",
answer:
"Use short, direct wording such as Scan for our menu, View today's menu, or Order from your table. Keep the QR code large enough to scan, leave quiet space around it, and test it from normal table distance before printing.",
},
{
question: "How much can a dynamic menu QR code save on reprints?",
answer:
"It depends on your print volume and how often the menu changes. As a simple example, 30 table tents at EUR 2.50 each reprinted twice per year equals EUR 150 before flyers, window signs, design time, or staff coordination are included.",
},
];
const relatedResources = [
{
href: "/reprint-calculator",
title: "QR code reprint calculator",
text: "Estimate how much print work changes when you stop replacing QR materials.",
},
{
href: "/dynamic-qr-code-generator",
title: "Dynamic QR code generator",
text: "Create editable QR codes for links, PDFs, campaigns, and menu updates.",
},
{
href: "/qr-code-tracking",
title: "QR code tracking",
text: "Compare scans from table tents, flyers, receipts, and window signs.",
},
{
href: "/qr-code-print-size-guide",
title: "QR code print size guide",
text: "Pick a practical size before sending table cards or menus to print.",
},
];
const schemaData = [
breadcrumbSchema([
{ name: "Home", url: "/" },
{ name: "Restaurants", url: "/restaurants" },
]),
faqPageSchema(FAQ),
softwareApplicationSchema(),
{
"@context": "https://schema.org",
"@type": "WebPage",
"@id": `${PAGE_URL}#webpage`,
url: PAGE_URL,
name: "Restaurant Menu QR Codes",
description:
"Create a dynamic restaurant menu QR code that can be updated after print, with scan analytics and a free start.",
inLanguage: "en",
isPartOf: {
"@id": `${SITE_URL}/#website`,
},
about: [
"dynamic QR codes",
"restaurant menu QR codes",
"QR code analytics",
"menu PDF updates",
"QR code reprint savings",
],
significantLink: relatedResources.map((resource) => `${SITE_URL}${resource.href}`),
primaryImageOfPage: {
"@type": "ImageObject",
url: `${SITE_URL}${HERO_IMAGE}`,
},
},
{
"@context": "https://schema.org",
"@type": "HowTo",
name: "How to create a restaurant menu QR code that can be updated",
description:
"Create one dynamic QR code, print it on your table materials, then update the menu destination whenever prices or dishes change.",
step: [
{
"@type": "HowToStep",
position: 1,
name: "Create a dynamic QR code",
text: "Create a QR Master account and choose a dynamic QR code for your restaurant menu.",
},
{
"@type": "HowToStep",
position: 2,
name: "Add your menu destination",
text: "Link the QR code to a menu PDF, menu page, ordering link, or hosted restaurant menu.",
},
{
"@type": "HowToStep",
position: 3,
name: "Print the QR code once",
text: "Place the same QR code on table tents, printed menus, flyers, window signs, or receipts.",
},
{
"@type": "HowToStep",
position: 4,
name: "Update the destination later",
text: "Change the destination in QR Master when prices, dishes, opening hours, or menu files change.",
},
],
},
];
export const metadata: Metadata = {
title: {
absolute: "Restaurant Menu QR Codes | QR Master",
},
description:
"Create one restaurant menu QR code, update the destination after print, and track scans. Built for menus, table tents, flyers, and price changes.",
alternates: {
canonical: PAGE_URL,
languages: {
"x-default": PAGE_URL,
en: PAGE_URL,
},
},
openGraph: {
title: "Restaurant Menu QR Codes | QR Master",
description:
"Update restaurant menu links, PDFs, and prices without reprinting your QR code.",
url: PAGE_URL,
type: "website",
images: [
{
url: `${SITE_URL}${OG_IMAGE}`,
width: 1200,
height: 630,
alt: "Restaurant owner using QR Master to update a menu QR code",
},
],
},
twitter: {
card: "summary_large_image",
title: "Restaurant Menu QR Codes | QR Master",
description:
"Update restaurant menu links, PDFs, and prices without reprinting your QR code.",
images: [`${SITE_URL}${OG_IMAGE}`],
},
};
const proofPoints = [
"Change menu PDFs after print",
"Use one QR code across tables and flyers",
"Track scans without exposing guest data",
];
const comparisonRows = [
{
label: "Menu price changes",
static: "Reprint table materials",
dynamic: "Update the destination online",
},
{
label: "Seasonal dishes",
static: "Old QR code points to old menu",
dynamic: "Same QR code points to the new menu",
},
{
label: "Scan analytics",
static: "No reliable usage data",
dynamic: "See scans, device types, and timing",
},
{
label: "Campaign tracking",
static: "Hard to compare placements",
dynamic: "Use tagged links for tables, flyers, and windows",
},
];
const steps = [
{
icon: QrCode,
title: "Create one dynamic code",
text: "Start with a menu QR code that points through QR Master, not directly to a file you cannot change later.",
},
{
icon: FileText,
title: "Connect your menu",
text: "Use a PDF, website menu, ordering page, or any link your guests should open from the table.",
},
{
icon: RefreshCw,
title: "Update when prices change",
text: "Switch the destination from your dashboard while the printed QR code stays where it is.",
},
];
const useCases = [
"Table tents and counter displays",
"Printed menus with a digital backup",
"Window signs for after-hours browsing",
"Flyers for lunch specials or events",
"Receipts with feedback and review links",
"Seasonal menu PDFs",
];
const reprintExampleRows = [
{
label: "Table tents",
math: "30 pieces x EUR 2.50 x 2 menu changes",
value: "EUR 150",
},
{
label: "Lunch flyers",
math: "500 pieces x EUR 0.20 x 1 outdated link",
value: "EUR 100",
},
{
label: "Window and counter signs",
math: "6 pieces x EUR 8.00 x 1 update",
value: "EUR 48",
},
];
const restaurantScenarios = [
{
type: "Small cafe",
profile: "12 tables, seasonal drinks, one counter sign, and a simple PDF menu.",
printRisk:
"A coffee price update or new brunch menu can make table cards outdated before the next print run.",
qrMasterFit:
"Use one dynamic menu QR code for table cards and update the PDF when prices or specials change.",
},
{
type: "Independent restaurant",
profile: "30 tables, lunch flyers, takeaway inserts, and a menu that changes several times per year.",
printRisk:
"Printed flyers and table tents can send guests to old menu files, old prices, or inactive ordering links.",
qrMasterFit:
"Keep separate dynamic QR codes for tables, flyers, and takeaway materials so scan analytics show which placement works.",
},
{
type: "Multi-location group",
profile: "Several locations, local menus, shared brand materials, and recurring print coordination.",
printRisk:
"One changed PDF or location-specific menu can trigger edits across many printed assets.",
qrMasterFit:
"Manage destinations centrally while each location keeps its printed QR materials stable.",
},
];
const whyItWorks = [
{
title: "Editable destinations reduce print dependency",
text: "A dynamic QR code separates the printed code from the menu destination. That is why a PDF, menu page, or ordering link can change without replacing the physical table card.",
href: "/dynamic-qr-code-generator",
linkLabel: "Dynamic QR codes",
},
{
title: "Tracking separates placements",
text: "Restaurants can use different dynamic codes or tagged URLs for tables, flyers, receipts, and window signs. That makes scan data useful for decisions instead of blending every scan into one number.",
href: "/qr-code-tracking",
linkLabel: "QR code tracking",
},
{
title: "Print sizing protects the guest experience",
text: "The code still has to scan reliably from the table. Correct print size, quiet space, contrast, and test scans matter before sending table tents or menus to print.",
href: "/qr-code-print-size-guide",
linkLabel: "Print size guide",
},
];
function SectionHeading({
eyebrow,
title,
text,
inverse = false,
}: {
eyebrow: string;
title: string;
text: string;
inverse?: boolean;
}) {
return (
<div className="max-w-3xl">
<p
className={`mb-3 text-xs font-semibold uppercase tracking-[0.22em] ${
inverse ? "text-blue-300" : "text-blue-700"
}`}
>
{eyebrow}
</p>
<h2
className={`text-3xl font-semibold tracking-tight sm:text-4xl ${
inverse ? "text-white" : "text-slate-950"
}`}
>
{title}
</h2>
<p
className={`mt-4 text-base leading-7 sm:text-lg ${
inverse ? "text-slate-300" : "text-slate-600"
}`}
>
{text}
</p>
</div>
);
}
export default function RestaurantsPage() {
return (
<>
<SeoJsonLd data={schemaData} />
<MarketingPageTracker
pageType="commercial"
cluster="restaurants"
useCase="restaurant-menu"
/>
<main className="bg-[#fbfcff] text-slate-950">
<section className="overflow-hidden border-b border-slate-200 bg-[linear-gradient(135deg,#f8fbff_0%,#edf4ff_48%,#fff7ed_100%)]">
<div className="mx-auto grid max-w-7xl gap-4 px-4 pb-8 pt-5 sm:px-6 lg:grid-cols-[minmax(0,0.92fr)_minmax(500px,1.08fr)] lg:items-start lg:px-8">
<Breadcrumbs
items={[
{ name: "Home", url: "/" },
{ name: "Restaurants", url: "/restaurants" },
]}
className="lg:col-span-2 [&_a]:text-slate-500 [&_a:hover]:text-slate-950 [&_span]:text-slate-500 [&_[aria-current=page]]:text-slate-950"
/>
<div className="max-w-3xl pt-0 lg:pt-1">
<p className="mb-4 inline-flex rounded-md border border-blue-200 bg-white/80 px-3 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-blue-700 shadow-sm">
Dynamic menu QR codes for restaurants
</p>
<h1 className="max-w-3xl text-4xl font-semibold leading-[1.02] tracking-tight text-slate-950 sm:text-5xl lg:text-[4.15rem] xl:text-[4.65rem]">
Update your menu QR code without reprinting.
</h1>
<p className="mt-3 max-w-2xl text-base leading-7 text-slate-700 sm:text-lg">
Keep one printed QR code on the table. Change prices, menu PDFs,
and ordering links from QR Master when your restaurant changes.
</p>
<div className="mt-4 flex flex-col gap-3 sm:flex-row">
<TrackedCtaLink
href={CAMPAIGN_SIGNUP}
ctaLabel="Create free menu QR code"
ctaLocation="restaurants_hero_primary"
pageType="commercial"
cluster="restaurants"
useCase="restaurant-menu"
>
<Button className="w-full rounded-md bg-blue-600 px-6 py-3 text-base font-semibold text-white hover:bg-blue-700 sm:w-auto">
Create free menu QR code
</Button>
</TrackedCtaLink>
<TrackedCtaLink
href="/reprint-calculator"
ctaLabel="Calculate reprint savings"
ctaLocation="restaurants_hero_secondary"
pageType="commercial"
cluster="restaurants"
useCase="restaurant-menu"
>
<Button
variant="outline"
className="w-full rounded-md border-blue-200 bg-white/80 px-6 py-3 text-base text-blue-700 hover:bg-white sm:w-auto"
>
Calculate reprint savings
</Button>
</TrackedCtaLink>
</div>
</div>
<div className="relative pt-0 lg:pt-4">
<div className="absolute -inset-4 rounded-[2rem] bg-blue-200/25 blur-2xl" />
<div className="relative overflow-hidden rounded-xl border border-white/80 bg-white p-2 shadow-[0_28px_80px_-45px_rgba(15,23,42,0.45)]">
<Image
src={HERO_IMAGE}
alt="Restaurant owner reviewing menu updates with QR Master dashboard"
width={1920}
height={1080}
priority
sizes="(min-width: 1024px) 52vw, 100vw"
className="aspect-video w-full rounded-lg object-cover"
/>
</div>
<div className="relative mt-4 grid gap-2 text-sm text-slate-700 sm:grid-cols-3">
{proofPoints.map((point) => (
<div
key={point}
className="flex items-start gap-2 rounded-md border border-blue-100 bg-white/82 px-3 py-2 shadow-sm"
>
<Check className="mt-0.5 h-4 w-4 shrink-0 text-blue-700" />
<span>{point}</span>
</div>
))}
</div>
</div>
</div>
</section>
<section className="border-b border-slate-200 bg-white">
<div className="mx-auto grid max-w-7xl gap-8 px-4 py-12 sm:px-6 lg:grid-cols-[0.72fr_1fr] lg:px-8">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-blue-700">
Direct answer
</p>
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-slate-950">
What is a restaurant menu QR code?
</h2>
</div>
<p className="max-w-3xl text-xl leading-9 text-slate-700">
A restaurant menu QR code lets guests open your digital menu from
a table, flyer, or window sign. A dynamic menu QR code is better
for restaurants because the printed code stays the same while the
menu PDF, menu page, or ordering link can change later.
</p>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Why dynamic matters"
title="Menus change faster than printed materials."
text="Static QR codes are fine for links that never move. Restaurants need more room to adapt: prices change, dishes sell out, PDFs are replaced, and delivery links move."
/>
<div className="mt-10 overflow-hidden rounded-lg border border-slate-200 bg-white shadow-[0_24px_54px_-44px_rgba(15,23,42,0.45)]">
<div className="hidden grid-cols-[1fr_1fr_1fr] border-b border-slate-200 bg-slate-50 text-sm font-semibold text-slate-700 md:grid">
<div className="p-4">Restaurant change</div>
<div className="border-l border-slate-200 p-4">Static QR code</div>
<div className="border-l border-slate-200 p-4">QR Master dynamic code</div>
</div>
{comparisonRows.map((row) => (
<div
key={row.label}
className="grid grid-cols-1 border-b border-slate-200 last:border-b-0 md:grid-cols-[1fr_1fr_1fr]"
>
<div className="bg-white p-4 font-medium text-slate-950">
{row.label}
</div>
<div className="border-t border-slate-200 p-4 text-slate-600 md:border-l md:border-t-0">
{row.static}
</div>
<div className="border-t border-slate-200 p-4 text-slate-950 md:border-l md:border-t-0">
<span className="inline-flex items-start gap-2">
<Check className="mt-1 h-4 w-4 shrink-0 text-green-600" />
{row.dynamic}
</span>
</div>
</div>
))}
</div>
</section>
<section className="border-y border-blue-100 bg-white">
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-16 sm:px-6 lg:grid-cols-[0.85fr_1.15fr] lg:px-8">
<div>
<SectionHeading
eyebrow="Example reprint math"
title="A few small menu changes can become a real print bill."
text="This is an illustrative restaurant scenario, not a guaranteed saving. The useful point is simple: every QR code destination change that stays digital removes one print-dependent fix."
/>
<TrackedCtaLink
href="/reprint-calculator"
ctaLabel="Open reprint calculator"
ctaLocation="restaurants_reprint_example"
pageType="commercial"
cluster="restaurants"
useCase="restaurant-menu"
>
<Button className="mt-7 rounded-md bg-blue-600 px-6 py-3 text-white hover:bg-blue-700">
Open reprint calculator
</Button>
</TrackedCtaLink>
</div>
<div className="rounded-lg border border-slate-200 bg-slate-50 p-5 shadow-[0_22px_58px_-44px_rgba(15,23,42,0.45)]">
<div className="flex items-center gap-3 border-b border-slate-200 pb-4">
<div className="flex h-11 w-11 items-center justify-center rounded-md bg-blue-100 text-blue-700">
<Calculator className="h-6 w-6" />
</div>
<div>
<h3 className="text-lg font-semibold text-slate-950">
Sample yearly avoidable reprint cost
</h3>
<p className="text-sm text-slate-600">
Restaurant with table tents, flyers, and a few fixed signs.
</p>
</div>
</div>
<div className="divide-y divide-slate-200">
{reprintExampleRows.map((row) => (
<div
key={row.label}
className="grid gap-2 py-4 sm:grid-cols-[0.8fr_1.35fr_0.5fr] sm:items-center"
>
<div className="font-medium text-slate-950">{row.label}</div>
<div className="text-sm text-slate-600">{row.math}</div>
<div className="font-semibold text-slate-950 sm:text-right">
{row.value}
</div>
</div>
))}
</div>
<div className="mt-3 rounded-md border border-blue-200 bg-white p-4">
<div className="flex items-center justify-between gap-4">
<span className="font-semibold text-slate-950">
Example total
</span>
<span className="text-2xl font-semibold text-blue-700">
EUR 298/year
</span>
</div>
<p className="mt-3 text-sm leading-6 text-slate-600">
A dynamic restaurant QR code does not remove printing
entirely. It prevents a menu PDF, price change, or ordering
link change from forcing another print run.
</p>
</div>
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
<div className="mb-10">
<SectionHeading
eyebrow="Restaurant scenarios"
title="The same QR code workflow scales from cafe to multi-location group."
text="These examples show where dynamic menu QR codes usually create the most practical value: menu updates, placement tracking, and less coordination around printed materials."
/>
</div>
<div className="grid gap-5 lg:grid-cols-3">
{restaurantScenarios.map((scenario) => (
<article
key={scenario.type}
className="rounded-lg border border-slate-200 bg-white p-6 shadow-[0_20px_54px_-42px_rgba(15,23,42,0.45)]"
>
<h3 className="text-xl font-semibold text-slate-950">
{scenario.type}
</h3>
<p className="mt-3 text-sm leading-6 text-slate-600">
{scenario.profile}
</p>
<div className="mt-6 space-y-5">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">
Print risk
</p>
<p className="mt-2 leading-7 text-slate-700">
{scenario.printRisk}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">
QR Master fit
</p>
<p className="mt-2 leading-7 text-slate-700">
{scenario.qrMasterFit}
</p>
</div>
</div>
</article>
))}
</div>
</section>
<section className="border-y border-blue-100 bg-[linear-gradient(135deg,#eef5ff_0%,#ffffff_48%,#fff4e6_100%)] py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Workflow"
title="Print once. Update whenever the menu changes."
text="The QR code on the table should not be the fragile part of your restaurant workflow. Keep print stable and move the flexible work into the dashboard."
/>
<div className="mt-12 grid gap-5 md:grid-cols-3">
{steps.map((step, index) => (
<div
key={step.title}
className="rounded-lg border border-blue-100 bg-white/86 p-6 shadow-[0_18px_48px_-36px_rgba(37,99,235,0.55)]"
>
<div className="mb-8 flex items-center justify-between">
<div className="flex h-11 w-11 items-center justify-center rounded-md bg-blue-50 text-blue-700">
<step.icon className="h-6 w-6" />
</div>
<span className="font-mono text-sm text-blue-500">
0{index + 1}
</span>
</div>
<h3 className="text-xl font-semibold text-slate-950">
{step.title}
</h3>
<p className="mt-4 leading-7 text-slate-600">{step.text}</p>
</div>
))}
</div>
</div>
</section>
<section className="mx-auto grid max-w-7xl gap-12 px-4 py-20 sm:px-6 lg:grid-cols-[0.9fr_1.1fr] lg:px-8">
<div>
<SectionHeading
eyebrow="For restaurant operators"
title="Built for the places where QR codes actually live."
text="A restaurant QR code is not only a link. It is part of the table, the menu, the receipt, and the guest experience."
/>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<TrackedCtaLink
href={CAMPAIGN_SIGNUP}
ctaLabel="Start with a free dynamic code"
ctaLocation="restaurants_mid_primary"
pageType="commercial"
cluster="restaurants"
useCase="restaurant-menu"
>
<Button className="rounded-md bg-blue-600 px-6 py-3 text-white hover:bg-blue-700">
Start with a free dynamic code
</Button>
</TrackedCtaLink>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{useCases.map((item) => (
<div
key={item}
className="flex items-start gap-3 rounded-lg border border-slate-200 bg-white p-4"
>
<QrCode className="mt-1 h-5 w-5 shrink-0 text-blue-700" />
<span className="leading-7 text-slate-700">{item}</span>
</div>
))}
</div>
</section>
<section className="border-y border-slate-200 bg-white">
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-16 sm:px-6 lg:grid-cols-4 lg:px-8">
{[
{
icon: Printer,
label: "Reprint control",
text: "Avoid replacing table materials just because the destination changed.",
},
{
icon: Link2,
label: "Editable destination",
text: "Swap PDFs, menu pages, ordering links, and campaign URLs.",
},
{
icon: BarChart3,
label: "Scan analytics",
text: "See whether guests scan, when they scan, and which devices they use.",
},
{
icon: ShieldCheck,
label: "Privacy aware",
text: "QR Master is built around privacy-conscious analytics for business use.",
},
].map((item) => (
<div key={item.label}>
<item.icon className="h-7 w-7 text-blue-700" />
<h3 className="mt-5 text-lg font-semibold text-slate-950">
{item.label}
</h3>
<p className="mt-3 text-sm leading-6 text-slate-600">
{item.text}
</p>
</div>
))}
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
<div className="mb-10 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<SectionHeading
eyebrow="Related workflows"
title="Go deeper before you print."
text="These QR Master guides connect the restaurant page to the practical jobs behind it: editable codes, tracking, print sizing, and reprint planning."
/>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{relatedResources.map((resource) => (
<Link
key={resource.href}
href={resource.href}
className="group rounded-lg border border-slate-200 bg-white p-5 shadow-sm transition hover:-translate-y-0.5 hover:border-blue-200 hover:shadow-[0_18px_40px_-34px_rgba(37,99,235,0.65)]"
>
<span className="flex items-center justify-between gap-4 text-base font-semibold text-slate-950">
{resource.title}
<ArrowRight className="h-4 w-4 shrink-0 text-slate-400 transition group-hover:translate-x-1 group-hover:text-blue-700" />
</span>
<span className="mt-3 block text-sm leading-6 text-slate-600">
{resource.text}
</span>
</Link>
))}
</div>
</section>
<section className="border-y border-blue-100 bg-[linear-gradient(135deg,#f7fbff_0%,#ffffff_52%,#fff7ed_100%)]">
<div className="mx-auto grid max-w-7xl gap-8 px-4 py-16 sm:px-6 lg:grid-cols-[0.7fr_1.3fr] lg:px-8">
<div>
<SectionHeading
eyebrow="Why this works"
title="Dynamic QR codes turn a print problem into a dashboard update."
text="For restaurants, the QR code is usually printed before the menu stops changing. The strongest workflow keeps the physical code stable and moves updates, tracking, and testing into QR Master."
/>
</div>
<div className="grid gap-4">
{whyItWorks.map((item) => (
<div
key={item.title}
className="rounded-lg border border-blue-100 bg-white/88 p-5 shadow-sm"
>
<h3 className="text-lg font-semibold text-slate-950">
{item.title}
</h3>
<p className="mt-3 leading-7 text-slate-600">{item.text}</p>
<Link
href={item.href}
className="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-blue-700 hover:text-blue-800"
>
{item.linkLabel}
<ArrowRight className="h-4 w-4" />
</Link>
</div>
))}
</div>
</div>
</section>
<section className="mx-auto max-w-5xl px-4 py-20 sm:px-6 lg:px-8">
<div className="mb-10 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<SectionHeading
eyebrow="Questions"
title="Restaurant menu QR code FAQ"
text="Short answers for the decisions restaurant owners usually need to make before printing QR materials."
/>
<div className="flex items-center gap-2 text-sm text-slate-500">
<Clock3 className="h-4 w-4" />
<span>Last updated April 30, 2026</span>
</div>
</div>
<div className="divide-y divide-slate-200 rounded-lg border border-slate-200 bg-white">
{FAQ.map((item) => (
<details key={item.question} className="group p-5 open:bg-slate-50">
<summary className="flex cursor-pointer list-none items-center justify-between gap-6 text-base font-semibold text-slate-950">
{item.question}
<ArrowRight className="h-4 w-4 shrink-0 text-slate-400 transition-transform group-open:rotate-90" />
</summary>
<p className="mt-4 max-w-3xl leading-7 text-slate-600">
{item.answer}
</p>
</details>
))}
</div>
</section>
<section className="border-y border-blue-100 bg-[linear-gradient(135deg,#f7fbff_0%,#ffffff_55%,#fff7ed_100%)] py-16">
<div className="mx-auto flex max-w-7xl flex-col gap-8 px-4 sm:px-6 lg:flex-row lg:items-end lg:justify-between lg:px-8">
<div className="max-w-3xl">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-blue-700">
Ready for the next menu change
</p>
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-slate-950 sm:text-5xl">
Keep the QR code. Change the menu.
</h2>
<p className="mt-5 text-lg leading-8 text-slate-600">
Create a free QR Master account and test the restaurant menu
workflow before your next print run.
</p>
</div>
<TrackedCtaLink
href={CAMPAIGN_SIGNUP}
ctaLabel="Create free menu QR code"
ctaLocation="restaurants_footer_primary"
pageType="commercial"
cluster="restaurants"
useCase="restaurant-menu"
>
<Button className="w-full rounded-md bg-blue-600 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-blue-200 hover:bg-blue-700 sm:w-auto">
Create free menu QR code
</Button>
</TrackedCtaLink>
</div>
</section>
</main>
</>
);
}

View File

@@ -11,6 +11,7 @@ import { showToast } from '@/components/ui/Toast';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { toPng, toSvg, toBlob } from 'html-to-image'; import { toPng, toSvg, toBlob } from 'html-to-image';
import { trackEvent } from '@/components/PostHogProvider'; import { trackEvent } from '@/components/PostHogProvider';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors // Brand Colors
const BRAND = { const BRAND = {
@@ -54,6 +55,7 @@ export default function BarcodeGeneratorClient() {
const [lineColor, setLineColor] = useState('#000000'); const [lineColor, setLineColor] = useState('#000000');
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showPopup, setShowPopup] = useState(false);
const barcodeRef = useRef<HTMLDivElement>(null); const barcodeRef = useRef<HTMLDivElement>(null);
@@ -109,6 +111,7 @@ export default function BarcodeGeneratorClient() {
document.body.removeChild(link); document.body.removeChild(link);
showToast(`Barcode downloaded as ${extension.toUpperCase()}`, 'success'); showToast(`Barcode downloaded as ${extension.toUpperCase()}`, 'success');
if (shouldShowDownloadPopup()) setShowPopup(true);
trackEvent('barcode_downloaded', { trackEvent('barcode_downloaded', {
format: format, format: format,
extension: extension, extension: extension,
@@ -160,6 +163,8 @@ export default function BarcodeGeneratorClient() {
]; ];
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
@@ -453,5 +458,6 @@ export default function BarcodeGeneratorClient() {
</Link> </Link>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -18,7 +18,7 @@ export function BarcodeGuide() {
</h2> </h2>
</div> </div>
<p className="text-xs text-slate-400 mb-8 not-prose"> <p className="text-xs text-slate-400 mb-8 not-prose">
By <strong className="text-slate-600">Timo Knuth</strong>, QR Master · Last updated: June 2025 · GS1-verified content By <strong className="text-slate-600">Timo Knuth</strong>, QR Master · Last updated: May 2026 · GS1-verified content
</p> </p>
<p className="lead text-xl text-slate-600"> <p className="lead text-xl text-slate-600">

View File

@@ -1,314 +1,573 @@
import React from 'react';
import React from 'react'; import type { Metadata } from 'next';
import type { Metadata } from 'next'; import BarcodeGeneratorClient from './BarcodeGeneratorClient';
import BarcodeGeneratorClient from './BarcodeGeneratorClient'; import { BarcodeGuide } from './BarcodeGuide';
import { BarcodeGuide } from './BarcodeGuide'; import {
import { Barcode as BarcodeIcon, Shield, Zap, Printer, Download, Share2, Sparkles, Sliders, Check } from 'lucide-react'; Barcode as BarcodeIcon,
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema'; Shield,
import { RelatedTools } from '@/components/marketing/RelatedTools'; Zap,
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils'; Printer,
Download,
// SEO Optimized Metadata Share2,
export const metadata: Metadata = { Sparkles,
title: { Sliders,
absolute: 'Free Barcode Generator Online EAN, UPC, Code 128', Check,
}, } from 'lucide-react';
description: 'Free online barcode generator for EAN-13, UPC-A, and Code 128 barcodes. Create scannable labels for retail, inventory, and logistics instantly. Download PNG or SVG — no signup required.', import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
keywords: ['barcode generator', 'online barcode maker', 'create barcode free', 'ean-13 generator', 'upc-a generator', 'code 128 generator', 'barcode creator', 'printable barcodes'], import { RelatedTools } from '@/components/marketing/RelatedTools';
alternates: { import {
canonical: 'https://www.qrmaster.net/tools/barcode-generator', generateSoftwareAppSchema,
languages: { generateFaqSchema,
'x-default': 'https://www.qrmaster.net/tools/barcode-generator', } from '@/lib/schema-utils';
en: 'https://www.qrmaster.net/tools/barcode-generator',
}, // SEO Optimized Metadata
}, export const metadata: Metadata = {
openGraph: { title: {
title: 'Barcode Generator: Create EAN, UPC & Code 128', absolute: 'Free Custom Barcode Generator - EAN, UPC, Code 128',
description: 'Barcode Generator: Create professional labels instantly. Free & Secured.', },
url: 'https://www.qrmaster.net/tools/barcode-generator', description:
siteName: 'QR Master', 'Free custom barcode generator and barcode maker for EAN-13, UPC-A, UPC barcode, and Code 128. Create scannable labels for retail and inventory, then download PNG or SVG.',
locale: 'en_US', keywords: [
type: 'website', 'barcode generator',
images: [{ url: '/barcode-generator-preview.png', width: 1200, height: 630 }], 'custom barcode generator',
}, 'online barcode generator',
twitter: { 'free online barcode generator',
card: 'summary_large_image', 'barcode maker',
title: 'Free Barcode Generator', 'upc barcode generator',
description: 'Create custom barcodes in seconds. Download high-quality PNG/SVG.', 'ean-13 generator',
}, 'upc-a generator',
robots: { 'code 128 generator',
index: true, 'barcode creator',
follow: true, 'create barcode free',
}, 'printable barcodes',
}; ],
alternates: {
// JSON-LD Structured Data canonical: 'https://www.qrmaster.net/tools/barcode-generator',
const jsonLd = { languages: {
'@context': 'https://schema.org', 'x-default': 'https://www.qrmaster.net/tools/barcode-generator',
'@graph': [ en: 'https://www.qrmaster.net/tools/barcode-generator',
generateSoftwareAppSchema( },
'Barcode Generator', },
'Generate custom printable barcodes instantly for EAN, UPC, Code 128 and more.', openGraph: {
'/barcode-generator-preview.png', title: 'Free Custom Barcode Generator - EAN, UPC & Code 128',
'UtilitiesApplication' description:
), 'Free online barcode maker for EAN-13, UPC-A, and Code 128. Create scannable custom barcodes in seconds and download PNG or SVG.',
{ url: 'https://www.qrmaster.net/tools/barcode-generator',
'@type': 'HowTo', siteName: 'QR Master',
name: 'How to Create a Barcode', locale: 'en_US',
datePublished: '2024-06-01', type: 'website',
dateModified: '2025-06-01', images: [
author: { { url: '/barcode-generator-preview.png', width: 1200, height: 630 },
'@type': 'Person', ],
name: 'Timo Knuth', },
url: 'https://www.qrmaster.net/authors/timo', twitter: {
}, card: 'summary_large_image',
description: 'Create custom barcodes for products or inventory.', title: 'Free Barcode Generator',
step: [ description:
{ 'Create custom barcodes in seconds. Download high-quality PNG/SVG.',
'@type': 'HowToStep', },
position: 1, robots: {
name: 'Enter Content', index: true,
text: 'Type or paste the numeric or alphanumeric data for your barcode.', follow: true,
}, },
{ };
'@type': 'HowToStep',
position: 2, // JSON-LD Structured Data
name: 'Select Format', const jsonLd = {
text: 'Choose the appropriate barcode type (e.g., Code 128 for general use, EAN-13 for retail).', '@context': 'https://schema.org',
}, '@graph': [
{ generateSoftwareAppSchema(
'@type': 'HowToStep', 'Barcode Generator',
position: 3, 'Generate custom printable barcodes instantly for EAN, UPC, Code 128 and more.',
name: 'Customize Design', '/barcode-generator-preview.png',
text: 'Adjust the height and width of the barcode to fit your needs.', 'UtilitiesApplication'
}, ),
{ {
'@type': 'HowToStep', '@type': 'WebPage',
position: 4, '@id': 'https://www.qrmaster.net/tools/barcode-generator',
name: 'Toggle Text', name: 'Free Custom Barcode Generator Online - EAN, UPC, Code 128',
text: 'Decide if you want the human-readable value to appear below the barcode.', description:
}, 'A barcode generator converts any number or text into a scannable barcode image for retail labels, inventory, and product packaging. Supports EAN-13, UPC-A, and Code 128.',
{ speakable: {
'@type': 'HowToStep', '@type': 'SpeakableSpecification',
position: 5, cssSelector: ['.bg-blue-50', 'h1'],
name: 'Download & Print', },
text: 'Save your barcode as PNG or SVG and print it for labels or inventory.', author: {
}, '@type': 'Person',
], name: 'Timo Knuth',
totalTime: 'PT20S', url: 'https://www.qrmaster.net/authors/timo',
}, },
generateFaqSchema({ dateModified: '2026-05-10',
'What is a Barcode Generator?': { },
question: 'What is a Barcode Generator?', {
answer: 'A Barcode Generator is an online tool that converts numbers or text into scannable barcode images that can be used for products, labels, and inventory systems.', '@type': 'HowTo',
}, name: 'How to Create a Barcode',
'Is this barcode generator free to use?': { datePublished: '2024-06-01',
question: 'Is this barcode generator free to use?', dateModified: '2026-05-10',
answer: 'Yes, our online barcode generator is completely free to use with no hidden costs or sign-ups required. You can generate, download, and print barcodes instantly.', author: {
}, '@type': 'Person',
'Which barcode format should I use?': { name: 'Timo Knuth',
question: 'Which barcode format should I use?', url: 'https://www.qrmaster.net/authors/timo',
answer: 'EAN-13 is standard for retail in Europe/Global. UPC-A is standard for retail in USA/Canada. Code 128 is best for logistics and internal tracking as it supports letters and numbers.', },
}, description: 'Create custom barcodes for products or inventory.',
'Can I download barcodes in vector format (SVG)?': { step: [
question: 'Can I download barcodes in vector format (SVG)?', {
answer: 'Yes! We offer SVG downloads. SVG files are vector-based, meaning they can be scaled to any size without losing quality—perfect for professional product packaging.', '@type': 'HowToStep',
}, position: 1,
'How do I generate a barcode online?': { name: 'Enter Content',
question: 'How do I generate a barcode online?', text: 'Type or paste the numeric or alphanumeric data for your barcode.',
answer: 'To generate a barcode online, enter your product number or text, select the desired barcode format (such as EAN-13 or Code 128), and click the generate button. The barcode will be created instantly.', },
}, {
'Are generated barcodes scannable?': { '@type': 'HowToStep',
question: 'Are generated barcodes scannable?', position: 2,
answer: 'Yes, barcodes generated with a proper barcode generator are fully scannable. We generate standard-compliant barcodes readable by any standard optical or laser barcode scanner.', name: 'Select Format',
}, text: 'Choose the appropriate barcode type (e.g., Code 128 for general use, EAN-13 for retail).',
'Can I use these barcodes for Amazon (EAN/UPC)?': { },
question: 'Can I use these barcodes for Amazon (EAN/UPC)?', {
answer: 'You can generate the image for Amazon here if you already have your EAN/UPC number. However, you cannot "create" a valid global EAN number here—you must purchase those official numbers from GS1 to sell on major platforms like Amazon.', '@type': 'HowToStep',
}, position: 3,
'What is the difference between a barcode and a QR code?': { name: 'Customize Design',
question: 'What is the difference between a barcode and a QR code?', text: 'Adjust the height and width of the barcode to fit your needs.',
answer: 'A barcode stores data horizontally (1D) and is mainly used for product IDs. A QR code stores data in 2D (matrix) and can hold much more information, such as URLs, vCards, or WiFi credentials.', },
}, {
}), '@type': 'HowToStep',
], position: 4,
}; name: 'Toggle Text',
text: 'Decide if you want the human-readable value to appear below the barcode.',
export default function BarcodeGeneratorPage() { },
return ( {
<> '@type': 'HowToStep',
<script position: 5,
type="application/ld+json" name: 'Download & Print',
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} text: 'Save your barcode as PNG or SVG and print it for labels or inventory.',
/> },
<ToolBreadcrumb toolName="Barcode Generator" toolSlug="barcode-generator" /> ],
totalTime: 'PT20S',
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}> },
generateFaqSchema({
{/* HERO SECTION */} 'What is a Barcode Generator?': {
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-slate-900"> question: 'What is a Barcode Generator?',
<div className="absolute inset-0 opacity-10"> answer:
{/* Barcode Pattern */} 'A Barcode Generator is an online tool that converts numbers or text into scannable barcode images that can be used for products, labels, and inventory systems.',
<svg className="w-full h-full" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg"> },
<defs> 'Is this barcode generator free to use?': {
<pattern id="barcode_pattern" width="60" height="60" patternUnits="userSpaceOnUse"> question: 'Is this barcode generator free to use?',
<path d="M5 0 V 60 M15 0 V 60 M20 0 V 60 M35 0 V 60 M40 0 V 60 M55 0 V 60" stroke="white" strokeWidth="2" strokeOpacity="0.5" /> answer:
</pattern> 'Yes, our online barcode generator is completely free to use with no hidden costs or sign-ups required. You can generate, download, and print barcodes instantly.',
</defs> },
<rect width="100%" height="100%" fill="url(#barcode_pattern)" /> 'Which barcode format should I use?': {
</svg> question: 'Which barcode format should I use?',
</div> answer:
'EAN-13 is standard for retail in Europe/Global. UPC-A is standard for retail in USA/Canada. Code 128 is best for logistics and internal tracking as it supports letters and numbers.',
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10"> },
<div className="text-center lg:text-left"> 'Can I download barcodes in vector format (SVG)?': {
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default"> question: 'Can I download barcodes in vector format (SVG)?',
<span className="flex h-2 w-2 relative"> answer:
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span> 'Yes! We offer SVG downloads. SVG files are vector-based, meaning they can be scaled to any size without losing quality—perfect for professional product packaging.',
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-400"></span> },
</span> 'How do I generate a barcode online?': {
Free Tool Professional & Fast question: 'How do I generate a barcode online?',
</div> answer:
'To generate a barcode online, enter your product number or text, select the desired barcode format (such as EAN-13 or Code 128), and click the generate button. The barcode will be created instantly.',
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6"> },
Free Online <span className="text-blue-400">Barcode Generator</span> 'Are generated barcodes scannable?': {
</h1> question: 'Are generated barcodes scannable?',
answer:
<p className="text-lg md:text-xl text-slate-400 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed"> 'Yes, barcodes generated with a proper barcode generator are fully scannable. We generate standard-compliant barcodes readable by any standard optical or laser barcode scanner.',
A <strong>barcode generator</strong> converts any number or text into a scannable barcode image ready to download, print, and use on products, labels, or inventory systems. },
<span className="text-white block sm:inline mt-2 sm:mt-0"> Supports EAN-13, UPC-A, and Code 128. No signup required.</span> 'Can I use these barcodes for Amazon (EAN/UPC)?': {
</p> question: 'Can I use these barcodes for Amazon (EAN/UPC)?',
answer:
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80"> 'You can generate the image for Amazon here if you already have your EAN/UPC number. However, you cannot "create" a valid global EAN number here—you must purchase those official numbers from GS1 to sell on major platforms like Amazon.',
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm"> },
<Check className="w-4 h-4 text-blue-400" /> 'What is the difference between a barcode and a QR code?': {
Retail Ready question: 'What is the difference between a barcode and a QR code?',
</div> answer:
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm"> 'A barcode stores data horizontally (1D) and is mainly used for product IDs. A QR code stores data in 2D (matrix) and can hold much more information, such as URLs, vCards, or WiFi credentials.',
<Check className="w-4 h-4 text-blue-400" /> },
Vector SVG Export 'What barcode format do Amazon and Walmart require?': {
</div> question: 'What barcode format do Amazon and Walmart require?',
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm"> answer:
<Check className="w-4 h-4 text-blue-400" /> 'Amazon and Walmart require UPC-A (12 digits) for products sold in the United States and Canada, and EAN-13 (13 digits) for products sold internationally. You must purchase official GS1-registered numbers to sell on these platforms — you cannot self-generate valid retail UPC/EAN numbers.',
No Registration },
</div> 'What is the minimum print size for a scannable barcode?': {
</div> question: 'What is the minimum print size for a scannable barcode?',
</div> answer:
'The GS1 standard recommends a minimum width of 25.9mm (1 inch) for EAN-13 barcodes on retail packaging. Smaller sizes increase scan failure rates. For internal inventory labels, Code 128 can be printed as narrow as 15mm wide while remaining reliably scannable with modern handheld scanners.',
{/* Visual Abstract */} },
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]"> 'Can I use Code 128 for inventory management?': {
<div className="absolute w-[500px] h-[500px] bg-blue-500/10 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" /> question: 'Can I use Code 128 for inventory management?',
answer:
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-2 hover:-rotate-1 transition-all duration-700 group"> 'Yes. Code 128 is the most widely used barcode format for inventory, logistics, and warehousing because it supports both letters and numbers, has high data density, and is readable by virtually all laser and 2D scanners. It is the recommended format for internal SKU systems, warehouse bin labels, and shipping labels.',
<div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent rounded-3xl" /> },
'What is the difference between EAN-13 and UPC-A?': {
<div className="w-full bg-gradient-to-br from-blue-400 to-indigo-600 rounded-xl shadow-lg p-5 mb-6 relative overflow-hidden text-white"> question: 'What is the difference between EAN-13 and UPC-A?',
<div className="flex justify-between items-start mb-4"> answer:
<BarcodeIcon className="w-8 h-8 opacity-80" /> 'EAN-13 (13 digits) is the international retail standard used in Europe, Asia, and globally. UPC-A (12 digits) is the North American retail standard used in the US and Canada. An EAN-13 barcode starting with a 0 is actually a UPC-A code — all UPC-A codes are a subset of EAN-13. Most modern POS scanners read both formats.',
<div className="bg-white/20 px-2 py-1 rounded text-xs font-bold uppercase tracking-wider">Label</div> },
</div> }),
<div className="text-xl font-bold tracking-wider mb-1">PROD-98234</div> ],
<div className="text-xs opacity-70">Inventory ID</div> };
</div>
export default function BarcodeGeneratorPage() {
<div className="w-48 h-32 bg-white rounded-xl p-4 shadow-inner relative overflow-hidden flex flex-col items-center justify-center"> return (
<div className="w-full h-16 bg-black flex gap-1 mb-2"> <>
{[2, 4, 1, 3, 2, 1, 4, 2, 1, 3].map((w, i) => ( <script
<div key={i} className="bg-black flex-1" style={{ flex: w }} /> type="application/ld+json"
))} dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
</div> />
<div className="text-[10px] font-mono font-bold tracking-widest uppercase">98234001A</div> <ToolBreadcrumb
</div> toolName="Barcode Generator"
toolSlug="barcode-generator"
{/* Floating Badge */} />
<div className="absolute -bottom-6 -right-6 bg-slate-900 border border-white/10 py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
<div className="bg-blue-500/20 p-2 rounded-full"> <div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
<Printer className="w-5 h-5 text-blue-500" /> {/* HERO SECTION */}
</div> <section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-slate-900">
<div className="text-left"> <div className="absolute inset-0 opacity-10">
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Ready</div> {/* Barcode Pattern */}
<div className="text-sm font-bold text-white">Print Instantly</div> <svg
</div> className="w-full h-full"
</div> width="100%"
</div> height="100%"
</div> xmlns="http://www.w3.org/2000/svg"
</div> >
</section> <defs>
<pattern
{/* GENERATOR SECTION */} id="barcode_pattern"
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8"> width="60"
<BarcodeGeneratorClient /> height="60"
</section> patternUnits="userSpaceOnUse"
>
{/* HOW IT WORKS */} <path
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white"> d="M5 0 V 60 M15 0 V 60 M20 0 V 60 M35 0 V 60 M40 0 V 60 M55 0 V 60"
<div className="max-w-4xl mx-auto"> stroke="white"
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12"> strokeWidth="2"
How Our Barcode Generator Works strokeOpacity="0.5"
</h2> />
</pattern>
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8"> </defs>
<article className="text-center"> <rect width="100%" height="100%" fill="url(#barcode_pattern)" />
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4"> </svg>
<Sliders className="w-6 h-6 text-white" /> </div>
</div>
<h3 className="font-bold text-slate-900 mb-2">1. Configure</h3> <div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
<p className="text-slate-600 text-xs leading-relaxed"> <div className="text-center lg:text-left">
Enter your data and select the format. <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
</p> <span className="flex h-2 w-2 relative">
</article> <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-400"></span>
<article className="text-center"> </span>
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4"> Free Tool Professional & Fast
<Sparkles className="w-6 h-6 text-white" /> </div>
</div>
<h3 className="font-bold text-slate-900 mb-2">2. Customize</h3> <h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
<p className="text-slate-600 text-xs leading-relaxed"> Free Custom{' '}
Adjust height, width and text display. <span className="text-blue-400">Barcode Generator</span>
</p> <span className="block text-2xl md:text-3xl font-semibold text-slate-300 mt-2">
</article> Barcode Maker for EAN, UPC & Code 128
</span>
<article className="text-center"> </h1>
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Zap className="w-6 h-6 text-white" /> <p className="text-lg md:text-xl text-slate-400 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
</div> A <strong>free custom barcode generator</strong> and{' '}
<h3 className="font-bold text-slate-900 mb-2">3. Preview</h3> <strong>barcode maker</strong> for EAN-13, UPC-A, UPC barcodes,
<p className="text-slate-600 text-xs leading-relaxed"> and Code 128. Convert any number or text into a scannable
See your barcode update in real-time. barcode, download PNG or SVG, print instantly, no signup
</p> required.
</article> </p>
<article className="text-center"> <div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4"> <div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
<Download className="w-6 h-6 text-white" /> <Check className="w-4 h-4 text-blue-400" />
</div> Retail Ready
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3> </div>
<p className="text-slate-600 text-xs leading-relaxed"> <div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
Save as professional PNG or SVG. <Check className="w-4 h-4 text-blue-400" />
</p> Vector SVG Export
</article> </div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
<article className="text-center"> <Check className="w-4 h-4 text-blue-400" />
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4"> No Registration
<Printer className="w-6 h-6 text-white" /> </div>
</div> </div>
<h3 className="font-bold text-slate-900 mb-2">5. Print</h3> </div>
<p className="text-slate-600 text-xs leading-relaxed">
Print labels directly from your browser. {/* Visual Abstract */}
</p> <div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
</article> <div className="absolute w-[500px] h-[500px] bg-blue-500/10 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
</div>
</div> <div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-2 hover:-rotate-1 transition-all duration-700 group">
</section> <div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent rounded-3xl" />
{/* RELATED TOOLS */} <div className="w-full bg-gradient-to-br from-blue-400 to-indigo-600 rounded-xl shadow-lg p-5 mb-6 relative overflow-hidden text-white">
<RelatedTools /> <div className="flex justify-between items-start mb-4">
<BarcodeIcon className="w-8 h-8 opacity-80" />
{/* SEO GUIDE */} <div className="bg-white/20 px-2 py-1 rounded text-xs font-bold uppercase tracking-wider">
<BarcodeGuide /> Label
</div>
</div> </div>
</> <div className="text-xl font-bold tracking-wider mb-1">
); PROD-98234
} </div>
<div className="text-xs opacity-70">Inventory ID</div>
</div>
<div className="w-48 h-32 bg-white rounded-xl p-4 shadow-inner relative overflow-hidden flex flex-col items-center justify-center">
<div className="w-full h-16 bg-black flex gap-1 mb-2">
{[2, 4, 1, 3, 2, 1, 4, 2, 1, 3].map((w, i) => (
<div
key={i}
className="bg-black flex-1"
style={{ flex: w }}
/>
))}
</div>
<div className="text-[10px] font-mono font-bold tracking-widest uppercase">
98234001A
</div>
</div>
{/* Floating Badge */}
<div className="absolute -bottom-6 -right-6 bg-slate-900 border border-white/10 py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
<div className="bg-blue-500/20 p-2 rounded-full">
<Printer className="w-5 h-5 text-blue-500" />
</div>
<div className="text-left">
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">
Ready
</div>
<div className="text-sm font-bold text-white">
Print Instantly
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* GENERATOR SECTION */}
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
<BarcodeGeneratorClient />
</section>
{/* AI-EXTRACTABLE DEFINITION + STATS BLOCK */}
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-white border-b border-slate-100">
<div className="max-w-4xl mx-auto">
<div className="bg-blue-50 border-l-4 border-blue-500 rounded-xl p-6 mb-8">
<h2 className="text-xl font-bold text-slate-900 mb-2">
What is a Barcode Generator?
</h2>
<p className="text-slate-700 leading-relaxed">
A <strong>custom barcode generator</strong> is an online tool
that converts a number or text string into a scannable barcode
image (EAN-13, UPC-A, or Code 128). The generated barcode can be
downloaded as PNG or SVG and printed on product labels,
packaging, or inventory stickers for use with any standard
barcode scanner.
</p>
</div>
<div className="grid md:grid-cols-3 gap-6 mb-8">
<div className="text-center p-5 rounded-xl bg-slate-50 border border-slate-200">
<div className="text-3xl font-extrabold text-blue-600 mb-1">
EAN-13
</div>
<div className="text-sm font-semibold text-slate-700 mb-1">
Global Retail Standard
</div>
<div className="text-xs text-slate-500">
Used on over 5 billion product labels worldwide (GS1, 2024)
</div>
</div>
<div className="text-center p-5 rounded-xl bg-slate-50 border border-slate-200">
<div className="text-3xl font-extrabold text-blue-600 mb-1">
UPC-A
</div>
<div className="text-sm font-semibold text-slate-700 mb-1">
North America Standard
</div>
<div className="text-xs text-slate-500">
Required by Amazon, Walmart, Target for product listings
</div>
</div>
<div className="text-center p-5 rounded-xl bg-slate-50 border border-slate-200">
<div className="text-3xl font-extrabold text-blue-600 mb-1">
Code 128
</div>
<div className="text-sm font-semibold text-slate-700 mb-1">
Inventory &amp; Logistics
</div>
<div className="text-xs text-slate-500">
Supports letters + numbers best for internal SKU systems
</div>
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl p-5">
<h3 className="font-bold text-slate-900 mb-2">
Barcode vs. QR Code When to Use Which
</h3>
<div className="grid md:grid-cols-2 gap-4 text-sm text-slate-700">
<div>
<p className="font-semibold text-slate-800 mb-1">
Use a barcode for:
</p>
<ul className="space-y-1 list-disc list-inside text-slate-600">
<li>Product SKUs and retail checkout</li>
<li>Warehouse shelf and bin labels</li>
<li>Inventory counting and stock control</li>
<li>Order fulfillment and packing verification</li>
</ul>
</div>
<div>
<p className="font-semibold text-slate-800 mb-1">
Use a QR code for:
</p>
<ul className="space-y-1 list-disc list-inside text-slate-600">
<li>Restaurant menus and digital content</li>
<li>Marketing campaigns and landing pages</li>
<li>Review collection and customer feedback</li>
<li>Product setup guides and support pages</li>
</ul>
</div>
</div>
</div>
</div>
</section>
{/* HOW IT WORKS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
How Our Barcode Generator Works
</h2>
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Sliders className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">1. Configure</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Enter your data and select the format.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Sparkles className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">2. Customize</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Adjust height, width and text display.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Zap className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">3. Preview</h3>
<p className="text-slate-600 text-xs leading-relaxed">
See your barcode update in real-time.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Download className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Save as professional PNG or SVG.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Printer className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">5. Print</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Print labels directly from your browser.
</p>
</article>
</div>
</div>
</section>
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-slate-50 border-y border-slate-200">
<div className="max-w-5xl mx-auto">
<div className="max-w-3xl mb-10">
<p className="text-sm font-semibold text-blue-600 uppercase tracking-wider mb-3">
Custom barcode generator
</p>
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Create custom barcodes for labels, inventory, and product
workflows
</h2>
<p className="text-lg text-slate-600 leading-relaxed">
Use the barcode maker when you already have a product number,
SKU, inventory ID, or internal reference and need a printable
barcode image. Choose the format, adjust the dimensions, show or
hide the text value, then export a clean PNG or SVG for labels.
</p>
</div>
<div className="grid md:grid-cols-3 gap-6">
<article className="rounded-xl border border-slate-200 bg-white p-6">
<h3 className="font-bold text-slate-900 mb-2">Retail labels</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Generate EAN-13 or UPC-A barcode images for packaging and
shelf labels when you already have valid GS1-issued numbers.
</p>
</article>
<article className="rounded-xl border border-slate-200 bg-white p-6">
<h3 className="font-bold text-slate-900 mb-2">
Inventory codes
</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Use Code 128 for alphanumeric SKUs, warehouse bins, assets,
and internal tracking labels that need scanner-friendly codes.
</p>
</article>
<article className="rounded-xl border border-slate-200 bg-white p-6">
<h3 className="font-bold text-slate-900 mb-2">
Printable exports
</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Download SVG for crisp print layouts or PNG for fast use in
docs, product sheets, and simple label workflows.
</p>
</article>
</div>
<p className="mt-8 text-sm text-slate-600">
Need a code for a web page instead of a retail scanner? Use the{' '}
<a
href="/dynamic-qr-code-generator"
className="font-semibold text-blue-700 underline"
>
dynamic QR code generator
</a>{' '}
if the destination may change later, or the{' '}
<a
href="/qr-code-tracking"
className="font-semibold text-blue-700 underline"
>
QR code tracking
</a>{' '}
workflow when you need scan analytics.
</p>
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* SEO GUIDE */}
<BarcodeGuide />
</div>
</>
);
}

View File

@@ -13,6 +13,7 @@ import {
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors // Brand Colors
const BRAND = { const BRAND = {
@@ -45,6 +46,7 @@ export default function PhoneGenerator() {
const [phone, setPhone] = useState(''); const [phone, setPhone] = useState('');
const [qrColor, setQrColor] = useState(BRAND.richBlue); const [qrColor, setQrColor] = useState(BRAND.richBlue);
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null); const qrRef = useRef<HTMLDivElement>(null);
@@ -74,6 +76,7 @@ export default function PhoneGenerator() {
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
if (shouldShowDownloadPopup()) setShowPopup(true);
}; };
const getFrameLabel = () => { const getFrameLabel = () => {
@@ -82,6 +85,8 @@ export default function PhoneGenerator() {
}; };
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
@@ -243,5 +248,6 @@ export default function PhoneGenerator() {
</Link> </Link>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -15,6 +15,7 @@ import { Input } from '@/components/ui/Input';
import { Select } from '@/components/ui/Select'; import { Select } from '@/components/ui/Select';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import AdBanner from '@/components/ads/AdBanner'; import AdBanner from '@/components/ads/AdBanner';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors // Brand Colors
const BRAND = { const BRAND = {
@@ -58,6 +59,7 @@ export default function CryptoGenerator() {
const [qrMode, setQrMode] = useState<'universal' | 'wallet'>('universal'); const [qrMode, setQrMode] = useState<'universal' | 'wallet'>('universal');
const [qrColor, setQrColor] = useState('#F7931A'); const [qrColor, setQrColor] = useState('#F7931A');
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null); const qrRef = useRef<HTMLDivElement>(null);
@@ -123,6 +125,7 @@ export default function CryptoGenerator() {
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
if (shouldShowDownloadPopup()) setShowPopup(true);
}; };
const getFrameLabel = () => { const getFrameLabel = () => {
@@ -131,6 +134,8 @@ export default function CryptoGenerator() {
}; };
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
@@ -370,5 +375,6 @@ export default function CryptoGenerator() {
</Link> </Link>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -14,6 +14,7 @@ import {
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors // Brand Colors
const BRAND = { const BRAND = {
@@ -47,6 +48,7 @@ export default function EmailGenerator() {
subject: '', subject: '',
body: '' body: ''
}); });
const [showPopup, setShowPopup] = useState(false);
const [qrColor, setQrColor] = useState('#dc2626'); const [qrColor, setQrColor] = useState('#dc2626');
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
@@ -88,6 +90,7 @@ export default function EmailGenerator() {
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
if (shouldShowDownloadPopup()) setShowPopup(true);
}; };
const getFrameLabel = () => { const getFrameLabel = () => {
@@ -100,6 +103,8 @@ export default function EmailGenerator() {
}; };
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
@@ -292,5 +297,6 @@ export default function EmailGenerator() {
</Link> </Link>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -15,6 +15,7 @@ import {
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors // Brand Colors
const BRAND = { const BRAND = {
@@ -51,6 +52,7 @@ export default function EventGenerator() {
const [qrColor, setQrColor] = useState(BRAND.primary); const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null); const qrRef = useRef<HTMLDivElement>(null);
@@ -102,6 +104,7 @@ export default function EventGenerator() {
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
if (shouldShowDownloadPopup()) setShowPopup(true);
}; };
const getFrameLabel = () => { const getFrameLabel = () => {
@@ -110,6 +113,8 @@ export default function EventGenerator() {
}; };
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
@@ -326,5 +331,6 @@ export default function EventGenerator() {
</Link> </Link>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -14,6 +14,7 @@ import {
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors // Brand Colors
const BRAND = { const BRAND = {
@@ -46,6 +47,7 @@ export default function FacebookGenerator() {
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const [qrColor, setQrColor] = useState('#1877F2'); // Default to FB Blue const [qrColor, setQrColor] = useState('#1877F2'); // Default to FB Blue
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null); const qrRef = useRef<HTMLDivElement>(null);
@@ -73,6 +75,7 @@ export default function FacebookGenerator() {
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
if (shouldShowDownloadPopup()) setShowPopup(true);
}; };
const getFrameLabel = () => { const getFrameLabel = () => {
@@ -81,6 +84,8 @@ export default function FacebookGenerator() {
}; };
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
@@ -243,5 +248,6 @@ export default function FacebookGenerator() {
</Link> </Link>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -14,6 +14,7 @@ import {
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors // Brand Colors
const BRAND = { const BRAND = {
@@ -46,6 +47,7 @@ export default function GeolocationGenerator() {
const [longitude, setLongitude] = useState(''); const [longitude, setLongitude] = useState('');
const [qrColor, setQrColor] = useState(BRAND.primary); const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null); const qrRef = useRef<HTMLDivElement>(null);
@@ -76,6 +78,7 @@ export default function GeolocationGenerator() {
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
if (shouldShowDownloadPopup()) setShowPopup(true);
}; };
const getFrameLabel = () => { const getFrameLabel = () => {
@@ -101,6 +104,8 @@ export default function GeolocationGenerator() {
}; };
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
@@ -288,5 +293,6 @@ export default function GeolocationGenerator() {
</Link> </Link>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -6,6 +6,7 @@ import { toPng } from 'html-to-image';
import { Star, Download, AlertCircle } from 'lucide-react'; import { Star, Download, AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
const QR_COLORS = [ const QR_COLORS = [
{ name: 'Google Blue', value: '#1A73E8' }, { name: 'Google Blue', value: '#1A73E8' },
@@ -41,6 +42,7 @@ export default function GoogleReviewGenerator() {
const [qrColor, setQrColor] = useState('#1A73E8'); const [qrColor, setQrColor] = useState('#1A73E8');
const [frameType, setFrameType] = useState('review'); const [frameType, setFrameType] = useState('review');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null); const qrRef = useRef<HTMLDivElement>(null);
@@ -76,12 +78,15 @@ export default function GoogleReviewGenerator() {
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
if (shouldShowDownloadPopup()) setShowPopup(true);
}; };
const frameLabel = FRAME_OPTIONS.find(f => f.id === frameType && f.id !== 'none')?.label ?? null; const frameLabel = FRAME_OPTIONS.find(f => f.id === frameType && f.id !== 'none')?.label ?? null;
const isReady = reviewUrl && !error && isValidGoogleReviewLink(reviewUrl); const isReady = reviewUrl && !error && isValidGoogleReviewLink(reviewUrl);
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100"> <div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
<div className="grid lg:grid-cols-2"> <div className="grid lg:grid-cols-2">
@@ -209,5 +214,6 @@ export default function GoogleReviewGenerator() {
</div> </div>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -9,411 +9,596 @@ import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
import { generateSoftwareAppSchema } from '@/lib/schema-utils'; import { generateSoftwareAppSchema } from '@/lib/schema-utils';
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
absolute: 'Google Review QR Code Generator — Free | QR Master', absolute: 'Google Review QR Code Generator — Free | QR Master',
}, },
description: 'Create a QR code for your Google Reviews in seconds. Customers scan once and land directly on your review form. Free, no signup required.', description:
keywords: ['qr code for google reviews', 'qr code generator for google reviews', 'google review qr code', 'google maps review qr code', 'get more google reviews'], 'Create a QR code for your Google Reviews in seconds. Customers scan once and land directly on your review form. Free, no signup required.',
alternates: { keywords: [
canonical: 'https://www.qrmaster.net/tools/google-review-qr-code', 'qr code for google reviews',
languages: { 'qr code generator for google reviews',
'x-default': 'https://www.qrmaster.net/tools/google-review-qr-code', 'google review qr code',
en: 'https://www.qrmaster.net/tools/google-review-qr-code', 'google maps review qr code',
}, 'get more google reviews',
}, ],
openGraph: { alternates: {
title: 'Google Review QR Code Generator — Free | QR Master', canonical: 'https://www.qrmaster.net/tools/google-review-qr-code',
description: 'Create a QR code that takes customers directly to your Google review form. More reviews, less friction.', languages: {
type: 'website', 'x-default': 'https://www.qrmaster.net/tools/google-review-qr-code',
url: 'https://www.qrmaster.net/tools/google-review-qr-code', en: 'https://www.qrmaster.net/tools/google-review-qr-code',
},
twitter: {
card: 'summary_large_image',
title: 'Google Review QR Code Generator — Free',
description: 'Create a QR code that takes customers directly to your Google review form.',
},
robots: {
index: true,
follow: true,
}, },
},
openGraph: {
title: 'Google Review QR Code Generator — Free | QR Master',
description:
'Create a QR code that takes customers directly to your Google review form. More reviews, less friction.',
type: 'website',
url: 'https://www.qrmaster.net/tools/google-review-qr-code',
},
twitter: {
card: 'summary_large_image',
title: 'Google Review QR Code Generator — Free',
description:
'Create a QR code that takes customers directly to your Google review form.',
},
robots: {
index: true,
follow: true,
},
}; };
const jsonLd = { const jsonLd = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@graph': [ '@graph': [
generateSoftwareAppSchema( generateSoftwareAppSchema(
'Google Review QR Code Generator', 'Google Review QR Code Generator',
'Generate a QR code that links directly to your Google review form. Customers scan once and can leave a review immediately.', 'Generate a QR code that links directly to your Google review form. Customers scan once and can leave a review immediately.',
'/og-image.png' '/og-image.png'
), ),
{
'@type': 'HowTo',
name: 'How to Create a Google Review QR Code',
datePublished: '2024-01-01',
dateModified: '2026-04-27',
author: {
'@type': 'Person',
name: 'Timo Knuth',
url: 'https://www.qrmaster.net/authors/timo',
},
description:
'Generate a QR code that sends customers directly to your Google review form.',
step: [
{ {
'@type': 'HowTo', '@type': 'HowToStep',
name: 'How to Create a Google Review QR Code', position: 1,
datePublished: '2024-01-01', name: 'Find your Google Review link',
dateModified: '2025-06-01', text: 'Open Google Maps, search for your business, click Share → Copy link. Or use Google Business Profile → Get more reviews.',
author: {
'@type': 'Person',
name: 'Timo Knuth',
url: 'https://www.qrmaster.net/authors/timo',
},
description: 'Generate a QR code that sends customers directly to your Google review form.',
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Find your Google Review link',
text: 'Open Google Maps, search for your business, click Share → Copy link. Or use Google Business Profile → Get more reviews.',
},
{
'@type': 'HowToStep',
position: 2,
name: 'Paste the link into the generator',
text: 'Paste your Google review URL into the field above. The generator accepts g.page, google.com, and maps.app.goo.gl links.',
},
{
'@type': 'HowToStep',
position: 3,
name: 'Customize and download',
text: 'Choose a color and frame label (e.g. "Leave a Review"), then download as PNG or SVG.',
},
{
'@type': 'HowToStep',
position: 4,
name: 'Display the QR code',
text: 'Print the code on receipts, table cards, packaging, or your window. Customers scan once to review.',
},
],
totalTime: 'PT60S',
}, },
{ {
'@type': 'FAQPage', '@type': 'HowToStep',
mainEntity: [ position: 2,
{ name: 'Paste the link into the generator',
'@type': 'Question', text: 'Paste your Google review URL into the field above. The generator accepts g.page, google.com, and maps.app.goo.gl links.',
name: 'How do I find my Google Review link?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Open Google Maps → search for your business → click Share → Copy link. Alternatively, go to your Google Business Profile dashboard → click "Get more reviews" — this gives you a direct review shortlink.',
},
},
{
'@type': 'Question',
name: 'Does this Google Review QR code expire?',
acceptedAnswer: {
'@type': 'Answer',
text: 'No. This is a static QR code that directly encodes your Google review URL. It will work as long as your Google Business Profile is active.',
},
},
{
'@type': 'Question',
name: 'Can I track how many people scanned the QR code?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Not with a static QR code. If you need scan analytics (device, location, time), create a dynamic QR code with tracking through QR Master.',
},
},
{
'@type': 'Question',
name: 'What happens when a customer scans the QR code?',
acceptedAnswer: {
'@type': 'Answer',
text: 'They are taken directly to your Google review form. If they are logged into a Google account on their phone, they can leave a review immediately with no extra steps.',
},
},
{
'@type': 'Question',
name: 'Where should I display the Google Review QR code?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Best placements: receipts, table tent cards (restaurants), checkout counters, packaging inserts, and your shop window. The moment after a positive experience is the best time to ask for a review.',
},
},
],
}, },
{ {
'@type': 'BreadcrumbList', '@type': 'HowToStep',
itemListElement: [ position: 3,
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://www.qrmaster.net' }, name: 'Customize and download',
{ '@type': 'ListItem', position: 2, name: 'QR Code Tools', item: 'https://www.qrmaster.net/tools' }, text: 'Choose a color and frame label (e.g. "Leave a Review"), then download as PNG or SVG.',
{ '@type': 'ListItem', position: 3, name: 'Google Review QR Code Generator', item: 'https://www.qrmaster.net/tools/google-review-qr-code' },
],
}, },
], {
'@type': 'HowToStep',
position: 4,
name: 'Display the QR code',
text: 'Print the code on receipts, table cards, packaging, or your window. Customers scan once to review.',
},
],
totalTime: 'PT60S',
},
{
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'How do I find my Google Review link?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Open Google Maps → search for your business → click Share → Copy link. Alternatively, go to your Google Business Profile dashboard → click "Get more reviews" — this gives you a direct review shortlink.',
},
},
{
'@type': 'Question',
name: 'Does this Google Review QR code expire?',
acceptedAnswer: {
'@type': 'Answer',
text: 'No. This is a static QR code that directly encodes your Google review URL. It will work as long as your Google Business Profile is active.',
},
},
{
'@type': 'Question',
name: 'Can I track how many people scanned the QR code?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Not with a static QR code. If you need scan analytics (device, location, time), create a dynamic QR code with tracking through QR Master.',
},
},
{
'@type': 'Question',
name: 'What happens when a customer scans the QR code?',
acceptedAnswer: {
'@type': 'Answer',
text: 'They are taken directly to your Google review form. If they are logged into a Google account on their phone, they can leave a review immediately with no extra steps.',
},
},
{
'@type': 'Question',
name: 'Where should I display the Google Review QR code?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Best placements: receipts, table tent cards (restaurants), checkout counters, packaging inserts, and your shop window. The moment after a positive experience is the best time to ask for a review.',
},
},
],
},
{
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: 'https://www.qrmaster.net',
},
{
'@type': 'ListItem',
position: 2,
name: 'QR Code Tools',
item: 'https://www.qrmaster.net/tools',
},
{
'@type': 'ListItem',
position: 3,
name: 'Google Review QR Code Generator',
item: 'https://www.qrmaster.net/tools/google-review-qr-code',
},
],
},
],
}; };
export default function GoogleReviewQRCodePage() { export default function GoogleReviewQRCodePage() {
return ( return (
<> <>
<script <script
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/> />
<ToolBreadcrumb toolName="Google Review QR Code Generator" toolSlug="google-review-qr-code" /> <ToolBreadcrumb
toolName="Google Review QR Code Generator"
toolSlug="google-review-qr-code"
/>
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}> <div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
{/* HERO */}
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-[#1A1265]">
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
<div className="text-center lg:text-left">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 border border-white/10">
<span className="flex h-2 w-2 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-yellow-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-yellow-400"></span>
</span>
Free Tool No Signup Required
</div>
{/* HERO */} <h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-[#1A1265]"> Google Review QR Code <br className="hidden lg:block" />
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10"> <span className="text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-orange-400">
<div className="text-center lg:text-left"> Generator Free
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 border border-white/10"> </span>
<span className="flex h-2 w-2 relative"> </h1>
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-yellow-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-yellow-400"></span>
</span>
Free Tool No Signup Required
</div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6"> <p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
Google Review QR Code <br className="hidden lg:block" /> Customers scan once and land directly on your Google review
<span className="text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-orange-400">Generator Free</span> form.
</h1> <strong className="text-white block sm:inline mt-2 sm:mt-0">
{' '}
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed"> More reviews, less friction.
Customers scan once and land directly on your Google review form. </strong>
<strong className="text-white block sm:inline mt-2 sm:mt-0"> More reviews, less friction.</strong> </p>
</p>
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5">
<Star className="w-4 h-4 text-yellow-400" />
Direct to Review Form
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5">
<MapPin className="w-4 h-4 text-emerald-400" />
Works for Any Business
</div>
</div>
</div>
{/* Hero QR Visual */}
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
<div className="absolute w-[500px] h-[500px] bg-yellow-500/20 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform -rotate-3 hover:rotate-0 transition-all duration-700 group">
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner mb-6 relative overflow-hidden flex items-center justify-center">
<QRCodeSVG value="https://www.qrmaster.net/tools/google-review-qr-code" size={170} fgColor="#1A73E8" level="Q" />
</div>
<div className="w-full bg-white/10 rounded-xl p-4 backdrop-blur-sm border border-white/10">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-yellow-500/20 flex items-center justify-center">
<Star className="w-4 h-4 text-yellow-300" />
</div>
<div className="space-y-1 w-full">
<div className="h-1.5 w-3/4 bg-white/30 rounded-full" />
<div className="h-1.5 w-1/2 bg-white/20 rounded-full" />
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* GENERATOR */}
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
<GoogleReviewGenerator />
</section>
{/* HOW TO FIND YOUR REVIEW LINK */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
How to Find Your Google Review Link
</h2>
<p className="text-slate-600 text-center mb-12 max-w-2xl mx-auto">
You need your Google Business review URL before creating the QR code. Here are two ways to get it.
</p>
<div className="grid md:grid-cols-2 gap-8">
<article className="p-6 bg-slate-50 rounded-2xl border border-slate-100">
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center mb-4">
<Search className="w-6 h-6 text-blue-600" />
</div>
<h3 className="font-bold text-slate-900 mb-3">Method 1: Google Maps</h3>
<ol className="space-y-2 text-sm text-slate-600 list-none">
<li><span className="font-semibold text-slate-800">1.</span> Open Google Maps</li>
<li><span className="font-semibold text-slate-800">2.</span> Search for your business name</li>
<li><span className="font-semibold text-slate-800">3.</span> Click the <strong>Share</strong> button</li>
<li><span className="font-semibold text-slate-800">4.</span> Copy the short link (starts with <code className="bg-white px-1 rounded text-xs">maps.app.goo.gl</code> or <code className="bg-white px-1 rounded text-xs">g.page</code>)</li>
<li><span className="font-semibold text-slate-800">5.</span> Paste into the generator above</li>
</ol>
</article>
<article className="p-6 bg-slate-50 rounded-2xl border border-slate-100">
<div className="w-12 h-12 rounded-xl bg-yellow-100 flex items-center justify-center mb-4">
<Share2 className="w-6 h-6 text-yellow-600" />
</div>
<h3 className="font-bold text-slate-900 mb-3">Method 2: Google Business Profile</h3>
<ol className="space-y-2 text-sm text-slate-600 list-none">
<li><span className="font-semibold text-slate-800">1.</span> Sign in to <strong>business.google.com</strong></li>
<li><span className="font-semibold text-slate-800">2.</span> Select your business location</li>
<li><span className="font-semibold text-slate-800">3.</span> Click <strong>"Get more reviews"</strong></li>
<li><span className="font-semibold text-slate-800">4.</span> Copy the review shortlink provided</li>
<li><span className="font-semibold text-slate-800">5.</span> Paste into the generator above</li>
</ol>
</article>
</div>
</div>
</section>
{/* USE CASES */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-5xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Who Uses Google Review QR Codes?
</h2>
<p className="text-slate-600 text-center mb-12">
Any business where the moment of satisfaction happens in person.
</p>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{[
{ icon: '🍽️', title: 'Restaurants & Cafés', text: 'Print on receipts or table cards. Ask after the meal when the experience is fresh.' },
{ icon: '🏨', title: 'Hotels & Guesthouses', text: 'Place on checkout envelopes or in-room cards. Capture reviews at checkout.' },
{ icon: '🏥', title: 'Clinics & Salons', text: 'Display at reception. Patients and clients who had a great experience can review in seconds.' },
{ icon: '🛍️', title: 'Retail & Shops', text: 'Include in packaging or display at checkout. Turn happy shoppers into reviewers.' },
].map((item) => (
<article key={item.title} className="p-6 bg-white rounded-2xl border border-slate-100 shadow-sm">
<div className="text-3xl mb-3">{item.icon}</div>
<h3 className="font-bold text-slate-900 mb-2">{item.title}</h3>
<p className="text-sm text-slate-600 leading-relaxed">{item.text}</p>
</article>
))}
</div>
</div>
</section>
{/* WHY REVIEWS MATTER — STATISTICS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="w-5 h-5 text-yellow-500" />
<span className="text-sm font-semibold text-yellow-600 uppercase tracking-wider">Research-backed impact</span>
</div>
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Why Google Reviews Matter for Your Business
</h2>
<p className="text-slate-600 mb-10 max-w-2xl">
A <strong>Google Review QR code</strong> reduces the friction between a satisfied customer and a published review the single biggest barrier to getting more reviews.
</p>
<div className="grid md:grid-cols-2 gap-6 mb-8">
<div className="bg-yellow-50 border border-yellow-100 rounded-2xl p-6">
<div className="text-4xl font-extrabold text-yellow-600 mb-2">70%</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
of consumers will leave a review for a business <strong>if they are asked</strong> but most businesses never ask, or ask via email where completion rates drop to 13%.
</p>
<p className="text-xs text-slate-500">
Source: <a href="https://brightlocal.com/research/local-consumer-review-survey/" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">BrightLocal Local Consumer Review Survey</a>
</p>
</div>
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-6">
<div className="text-4xl font-extrabold text-blue-600 mb-2">+270%</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
increase in conversion rates for products and businesses with reviews compared to those without. Capturing reviews at the point of sale where satisfaction is highest maximizes this effect.
</p>
<p className="text-xs text-slate-500">
Source: <a href="https://spiegel.medill.northwestern.edu/online-reviews/" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">Spiegel Research Center, Northwestern University</a>
</p>
</div>
</div>
<p className="text-xs text-slate-400 italic">
By Timo Knuth, QR Master · Last updated: June 2025 · Based on independent academic and industry research
</p>
</div>
</section>
<GrowthLinksSection
eyebrow="Level up your local marketing"
title="More QR workflows for local businesses"
description="Review QR codes work best alongside dynamic destination management and scan tracking."
links={[
{
href: '/qr-code-for/restaurants',
title: 'QR Codes for Restaurants',
description: 'Menu, ordering, and review QR workflows built for food service businesses.',
ctaLabel: 'Restaurant QR workflows',
},
{
href: '/qr-code-for/hotels',
title: 'QR Codes for Hotels',
description: 'Check-in, room service, and review QR setups for hospitality.',
ctaLabel: 'Hotel QR workflows',
},
{
href: '/dynamic-qr-code-generator',
title: 'Dynamic QR Code Generator',
description: 'Update your review link or redirect to a different page anytime — no reprint needed.',
ctaLabel: 'Create dynamic QR code',
},
{
href: '/qr-code-tracking',
title: 'QR Code Tracking',
description: 'See exactly how many people scanned your review QR code and from which location.',
ctaLabel: 'Track QR code scans',
},
]}
pageType="commercial"
cluster="google-review-qr"
/>
<RelatedTools />
{/* FAQ */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Frequently Asked Questions
</h2>
<p className="text-slate-600 text-center mb-10">
Common questions about Google Review QR codes.
</p>
<div className="space-y-4">
{[
{
question: 'How do I find my Google Review link?',
answer: 'Open Google Maps → search your business → click Share → Copy link. Or go to Google Business Profile → "Get more reviews" for a direct shortlink.',
},
{
question: 'Does the Google Review QR code expire?',
answer: 'No. This is a static QR code that encodes your Google review URL directly. It works indefinitely as long as your Google Business Profile is active.',
},
{
question: 'Can I track how many people scanned it?',
answer: 'Not with a static QR code. For scan analytics (device, location, time), create a dynamic QR code with tracking through QR Master.',
},
{
question: 'What happens when a customer scans the QR code?',
answer: 'They are taken directly to your Google review form. If they are logged into a Google account, they can leave a star rating and review immediately.',
},
{
question: 'Where should I put the Google Review QR code?',
answer: 'Best placements: receipts, table cards, checkout counters, packaging inserts, and your shop window. Ask for reviews right after the positive experience.',
},
].map((item) => (
<details key={item.question} className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
{item.question}
<span className="transition group-open:rotate-180 text-slate-400">
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
<path d="M6 9l6 6 6-6" />
</svg>
</span>
</summary>
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
{item.answer}
</div>
</details>
))}
</div>
</div>
</section>
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5">
<Star className="w-4 h-4 text-yellow-400" />
Direct to Review Form
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5">
<MapPin className="w-4 h-4 text-emerald-400" />
Works for Any Business
</div>
</div>
</div> </div>
</>
); {/* Hero QR Visual */}
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
<div className="absolute w-[500px] h-[500px] bg-yellow-500/20 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform -rotate-3 hover:rotate-0 transition-all duration-700 group">
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner mb-6 relative overflow-hidden flex items-center justify-center">
<QRCodeSVG
value="https://www.qrmaster.net/tools/google-review-qr-code"
size={170}
fgColor="#1A73E8"
level="Q"
/>
</div>
<div className="w-full bg-white/10 rounded-xl p-4 backdrop-blur-sm border border-white/10">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-yellow-500/20 flex items-center justify-center">
<Star className="w-4 h-4 text-yellow-300" />
</div>
<div className="space-y-1 w-full">
<div className="h-1.5 w-3/4 bg-white/30 rounded-full" />
<div className="h-1.5 w-1/2 bg-white/20 rounded-full" />
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* GENERATOR */}
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
<GoogleReviewGenerator />
</section>
{/* HOW TO FIND YOUR REVIEW LINK */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
How to Find Your Google Review Link
</h2>
<p className="text-slate-600 text-center mb-12 max-w-2xl mx-auto">
You need your Google Business review URL before creating the QR
code. Here are two ways to get it.
</p>
<div className="grid md:grid-cols-2 gap-8">
<article className="p-6 bg-slate-50 rounded-2xl border border-slate-100">
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center mb-4">
<Search className="w-6 h-6 text-blue-600" />
</div>
<h3 className="font-bold text-slate-900 mb-3">
Method 1: Google Maps
</h3>
<ol className="space-y-2 text-sm text-slate-600 list-none">
<li>
<span className="font-semibold text-slate-800">1.</span>{' '}
Open Google Maps
</li>
<li>
<span className="font-semibold text-slate-800">2.</span>{' '}
Search for your business name
</li>
<li>
<span className="font-semibold text-slate-800">3.</span>{' '}
Click the <strong>Share</strong> button
</li>
<li>
<span className="font-semibold text-slate-800">4.</span>{' '}
Copy the short link (starts with{' '}
<code className="bg-white px-1 rounded text-xs">
maps.app.goo.gl
</code>{' '}
or{' '}
<code className="bg-white px-1 rounded text-xs">
g.page
</code>
)
</li>
<li>
<span className="font-semibold text-slate-800">5.</span>{' '}
Paste into the generator above
</li>
</ol>
</article>
<article className="p-6 bg-slate-50 rounded-2xl border border-slate-100">
<div className="w-12 h-12 rounded-xl bg-yellow-100 flex items-center justify-center mb-4">
<Share2 className="w-6 h-6 text-yellow-600" />
</div>
<h3 className="font-bold text-slate-900 mb-3">
Method 2: Google Business Profile
</h3>
<ol className="space-y-2 text-sm text-slate-600 list-none">
<li>
<span className="font-semibold text-slate-800">1.</span>{' '}
Sign in to <strong>business.google.com</strong>
</li>
<li>
<span className="font-semibold text-slate-800">2.</span>{' '}
Select your business location
</li>
<li>
<span className="font-semibold text-slate-800">3.</span>{' '}
Click <strong>"Get more reviews"</strong>
</li>
<li>
<span className="font-semibold text-slate-800">4.</span>{' '}
Copy the review shortlink provided
</li>
<li>
<span className="font-semibold text-slate-800">5.</span>{' '}
Paste into the generator above
</li>
</ol>
</article>
</div>
</div>
</section>
{/* USE CASES */}
<section
className="py-16 px-4 sm:px-6 lg:px-8"
style={{ backgroundColor: '#EBEBDF' }}
>
<div className="max-w-5xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Who Uses Google Review QR Codes?
</h2>
<p className="text-slate-600 text-center mb-12">
Any business where the moment of satisfaction happens in person.
</p>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{[
{
icon: '🍽️',
title: 'Restaurants & Cafés',
text: 'Print on receipts or table cards. Ask after the meal when the experience is fresh.',
},
{
icon: '🏨',
title: 'Hotels & Guesthouses',
text: 'Place on checkout envelopes or in-room cards. Capture reviews at checkout.',
},
{
icon: '🏥',
title: 'Clinics & Salons',
text: 'Display at reception. Patients and clients who had a great experience can review in seconds.',
},
{
icon: '🛍️',
title: 'Retail & Shops',
text: 'Include in packaging or display at checkout. Turn happy shoppers into reviewers.',
},
].map((item) => (
<article
key={item.title}
className="p-6 bg-white rounded-2xl border border-slate-100 shadow-sm"
>
<div className="text-3xl mb-3">{item.icon}</div>
<h3 className="font-bold text-slate-900 mb-2">
{item.title}
</h3>
<p className="text-sm text-slate-600 leading-relaxed">
{item.text}
</p>
</article>
))}
</div>
</div>
</section>
{/* WHY REVIEWS MATTER — STATISTICS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="w-5 h-5 text-yellow-500" />
<span className="text-sm font-semibold text-yellow-600 uppercase tracking-wider">
Research-backed impact
</span>
</div>
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Why Google Reviews Matter for Your Business
</h2>
<p className="text-slate-600 mb-10 max-w-2xl">
A <strong>Google Review QR code</strong> reduces the friction
between a satisfied customer and a published review the single
biggest barrier to getting more reviews.
</p>
<div className="grid md:grid-cols-2 gap-6 mb-8">
<div className="bg-yellow-50 border border-yellow-100 rounded-2xl p-6">
<div className="text-4xl font-extrabold text-yellow-600 mb-2">
70%
</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
of consumers will leave a review for a business{' '}
<strong>if they are asked</strong> but most businesses never
ask, or ask via email where completion rates drop to 13%.
</p>
<p className="text-xs text-slate-500">
Source:{' '}
<a
href="https://brightlocal.com/research/local-consumer-review-survey/"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-slate-700"
>
BrightLocal Local Consumer Review Survey
</a>
</p>
</div>
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-6">
<div className="text-4xl font-extrabold text-blue-600 mb-2">
+270%
</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
increase in conversion rates for products and businesses with
reviews compared to those without. Capturing reviews at the
point of sale where satisfaction is highest maximizes this
effect.
</p>
<p className="text-xs text-slate-500">
Source:{' '}
<a
href="https://spiegel.medill.northwestern.edu/online-reviews/"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-slate-700"
>
Spiegel Research Center, Northwestern University
</a>
</p>
</div>
</div>
<p className="text-xs text-slate-400 italic">
By Timo Knuth, QR Master · Last updated: June 2025 · Based on
independent academic and industry research
</p>
</div>
</section>
{/* SEO Content Block */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white border-t border-slate-100">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 mb-8">
Why Google Review QR Codes Work Better Than Asking Verbally
</h2>
<div className="prose prose-slate max-w-none">
<p className="text-lg text-slate-600 mb-6">Verbally asking for a review creates a promise customers intend to keep but rarely fulfill. The moment they leave your business, the intention fades. A Google Review QR code shortens the gap between the moment of satisfaction and the act of leaving a review to a single scan while the experience is still fresh and the customer is still engaged.</p>
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">The Best Placement for Google Review QR Codes</h3>
<p className="text-slate-600 mb-4">Placement is everything. The highest-performing locations are those where customers are already pausing: on printed receipts so they see it while reviewing the bill, on table tent cards at restaurants between ordering and paying, on the front door or exit so it is the last thing they see when leaving satisfied, and on packaging inserts inside product boxes that customers open at home after a purchase. Display the QR code at roughly A5 size with a clear label such as "Happy with your visit? Leave us a Google Review" customers do not need instructions beyond that. Checkout counters and front desk areas work especially well because staff can gesture toward the code while the customer is already in a positive frame of mind.</p>
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">How Many More Reviews Will You Get?</h3>
<p className="text-slate-600 mb-4">Research consistently shows that reducing friction is the primary lever for increasing review volume. Businesses that deploy Google Review QR codes at the point of sale typically see 3 to 5 times more reviews compared to relying on email follow-ups alone, where completion rates often fall below 2%. The reason is timing: a QR code captures the customer at peak satisfaction, requiring no extra steps beyond scanning and tapping the star rating. Email review requests, by contrast, arrive hours or days later when the emotional high has passed and competing priorities fill the inbox. Even a modest increase from 5 to 20 reviews per month compounds over a year into a significantly stronger local search presence, since Google's ranking algorithm weighs both review count and recency.</p>
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Responding to Reviews: What to Do After You Collect Them</h3>
<p className="text-slate-600 mb-4">Collecting reviews is only half the strategy. Responding to every review — positive and negative — signals to Google that your business is active and engaged, which supports local ranking. For positive reviews, a brief personalised thank-you (mentioning a specific detail if possible) reinforces the relationship. For critical reviews, acknowledge the issue, apologise where appropriate, and invite further contact offline. Google surfaces response rate and speed in its quality signals, so even a short reply within 24 hours outperforms silence. To understand which QR code placements are driving the most scans before reviewers land on Google, use <a href="/qr-code-tracking" className="text-blue-600 underline hover:text-blue-800">QR code scan tracking</a> to measure volume by location and time of day.</p>
</div>
</div>
</section>
<GrowthLinksSection
eyebrow="Level up your local marketing"
title="More QR workflows for local businesses"
description="Review QR codes work best alongside dynamic destination management and scan tracking."
links={[
{
href: '/qr-code-for/restaurants',
title: 'QR Codes for Restaurants',
description:
'Menu, ordering, and review QR workflows built for food service businesses.',
ctaLabel: 'Restaurant QR workflows',
},
{
href: '/qr-code-for/hotels',
title: 'QR Codes for Hotels',
description:
'Check-in, room service, and review QR setups for hospitality.',
ctaLabel: 'Hotel QR workflows',
},
{
href: '/use-cases/qr-codes-for-review-collection',
title: 'QR Codes for Review Collection',
description:
'Plan receipts, table cards, packaging, and counters as measurable review-request placements.',
ctaLabel: 'Build review collection workflow',
},
{
href: '/dynamic-qr-code-generator',
title: 'Dynamic QR Code Generator',
description:
'Update your review link or redirect to a different page anytime — no reprint needed.',
ctaLabel: 'Create dynamic QR code',
},
{
href: '/qr-code-tracking',
title: 'QR Code Tracking',
description:
'See exactly how many people scanned your review QR code and from which location.',
ctaLabel: 'Track QR code scans',
},
]}
pageType="commercial"
cluster="google-review-qr"
/>
<RelatedTools />
{/* FAQ */}
<section
className="py-16 px-4 sm:px-6 lg:px-8"
style={{ backgroundColor: '#EBEBDF' }}
>
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Frequently Asked Questions
</h2>
<p className="text-slate-600 text-center mb-10">
Common questions about Google Review QR codes.
</p>
<div className="space-y-4">
{[
{
question: 'How do I find my Google Review link?',
answer:
'Open Google Maps → search your business → click Share → Copy link. Or go to Google Business Profile → "Get more reviews" for a direct shortlink.',
},
{
question: 'Does the Google Review QR code expire?',
answer:
'No. This is a static QR code that encodes your Google review URL directly. It works indefinitely as long as your Google Business Profile is active.',
},
{
question: 'Can I track how many people scanned it?',
answer:
'Not with a static QR code. For scan analytics (device, location, time), create a dynamic QR code with tracking through QR Master.',
},
{
question: 'What happens when a customer scans the QR code?',
answer:
'They are taken directly to your Google review form. If they are logged into a Google account, they can leave a star rating and review immediately.',
},
{
question: 'Where should I put the Google Review QR code?',
answer:
'Best placements: receipts, table cards, checkout counters, packaging inserts, and your shop window. Ask for reviews right after the positive experience.',
},
].map((item) => (
<details
key={item.question}
className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden"
>
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
{item.question}
<span className="transition group-open:rotate-180 text-slate-400">
<svg
fill="none"
height="20"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="20"
>
<path d="M6 9l6 6 6-6" />
</svg>
</span>
</summary>
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
{item.answer}
</div>
</details>
))}
</div>
</div>
</section>
</div>
</>
);
} }

View File

@@ -13,6 +13,7 @@ import {
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors // Brand Colors
const BRAND = { const BRAND = {
@@ -45,6 +46,7 @@ export default function InstagramGenerator() {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [qrColor, setQrColor] = useState('#E1306C'); const [qrColor, setQrColor] = useState('#E1306C');
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null); const qrRef = useRef<HTMLDivElement>(null);
@@ -78,6 +80,7 @@ export default function InstagramGenerator() {
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
if (shouldShowDownloadPopup()) setShowPopup(true);
}; };
const getFrameLabel = () => { const getFrameLabel = () => {
@@ -86,6 +89,8 @@ export default function InstagramGenerator() {
}; };
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
@@ -248,5 +253,6 @@ export default function InstagramGenerator() {
</Link> </Link>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -1,397 +1,397 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import InstagramGenerator from './InstagramGenerator'; import InstagramGenerator from './InstagramGenerator';
import { Instagram, Shield, Zap, Smartphone, Camera, Heart, Download, Share2, TrendingUp } from 'lucide-react'; import { Instagram, Shield, Zap, Smartphone, Camera, Heart, Download, Share2, TrendingUp } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema'; import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools'; import { RelatedTools } from '@/components/marketing/RelatedTools';
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection'; import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils'; import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
// SEO Optimized Metadata // SEO Optimized Metadata
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
absolute: 'Free Instagram QR Code Generator | Get More Followers | QR Master', absolute: 'Free Instagram QR Code Generator | Get More Followers | QR Master',
}, },
description: 'Create a free Instagram QR code for your profile. Scanners follow you instantly — no app login required. Customizable & downloadable in seconds.', description: 'Create a free Instagram QR code for your profile. Scanners follow you instantly — no app login required. Customizable & downloadable in seconds.',
keywords: ['instagram qr code', 'insta qr generator', 'ig nametag generator', 'instagram follow qr', 'social media qr code', 'qr code for instagram', 'instagram profile qr code', 'insta qr code', 'instagram nametag generator'], keywords: ['instagram qr code', 'insta qr generator', 'ig nametag generator', 'instagram follow qr', 'social media qr code', 'qr code for instagram', 'instagram profile qr code', 'insta qr code', 'instagram nametag generator'],
alternates: { alternates: {
canonical: 'https://www.qrmaster.net/tools/instagram-qr-code', canonical: 'https://www.qrmaster.net/tools/instagram-qr-code',
languages: { languages: {
'x-default': 'https://www.qrmaster.net/tools/instagram-qr-code', 'x-default': 'https://www.qrmaster.net/tools/instagram-qr-code',
en: 'https://www.qrmaster.net/tools/instagram-qr-code', en: 'https://www.qrmaster.net/tools/instagram-qr-code',
}, },
}, },
openGraph: { openGraph: {
title: 'Free Instagram QR Code Generator | QR Master', title: 'Free Instagram QR Code Generator | QR Master',
description: 'Generate QR codes to grow your Instagram following. Instant app redirect.', description: 'Generate QR codes to grow your Instagram following. Instant app redirect.',
type: 'website', type: 'website',
url: 'https://www.qrmaster.net/tools/instagram-qr-code', url: 'https://www.qrmaster.net/tools/instagram-qr-code',
images: [{ url: '/og-instagram-generator.png', width: 1200, height: 630 }], images: [{ url: '/og-instagram-generator.png', width: 1200, height: 630 }],
}, },
twitter: { twitter: {
card: 'summary_large_image', card: 'summary_large_image',
title: 'Free Instagram QR Code Generator', title: 'Free Instagram QR Code Generator',
description: 'Create QR codes for Instagram. Boost your followers.', description: 'Create QR codes for Instagram. Boost your followers.',
}, },
robots: { robots: {
index: true, index: true,
follow: true, follow: true,
}, },
}; };
// JSON-LD Structured Data // JSON-LD Structured Data
const jsonLd = { const jsonLd = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@graph': [ '@graph': [
generateSoftwareAppSchema( generateSoftwareAppSchema(
'Instagram QR Code Generator', 'Instagram QR Code Generator',
'Generate QR codes that direct users to an Instagram profile or post.', 'Generate QR codes that direct users to an Instagram profile or post.',
'/og-instagram-generator.png' '/og-instagram-generator.png'
), ),
{ {
'@type': 'HowTo', '@type': 'HowTo',
name: 'How to Create an Instagram QR Code', name: 'How to Create an Instagram QR Code',
description: 'Create a QR code that opens an Instagram profile.', description: 'Create a QR code that opens an Instagram profile.',
datePublished: '2024-01-01', datePublished: '2024-01-01',
dateModified: '2025-06-01', dateModified: '2026-04-27',
author: { author: {
'@type': 'Person', '@type': 'Person',
name: 'Timo Knuth', name: 'Timo Knuth',
url: 'https://www.qrmaster.net/authors/timo', url: 'https://www.qrmaster.net/authors/timo',
}, },
step: [ step: [
{ {
'@type': 'HowToStep', '@type': 'HowToStep',
position: 1, position: 1,
name: 'Enter Username', name: 'Enter Username',
text: 'Type your Instagram handle (e.g. @yourbrand) or paste your profile link.', text: 'Type your Instagram handle (e.g. @yourbrand) or paste your profile link.',
}, },
{ {
'@type': 'HowToStep', '@type': 'HowToStep',
position: 2, position: 2,
name: 'Customize', name: 'Customize',
text: 'Choose a gradient color that matches the Instagram vibe or your own brand.', text: 'Choose a gradient color that matches the Instagram vibe or your own brand.',
}, },
{ {
'@type': 'HowToStep', '@type': 'HowToStep',
position: 3, position: 3,
name: 'Download', name: 'Download',
text: 'Save the QR code image.', text: 'Save the QR code image.',
}, },
{ {
'@type': 'HowToStep', '@type': 'HowToStep',
position: 4, position: 4,
name: 'Test', name: 'Test',
text: 'Scan the code to ensure it opens the correct profile.', text: 'Scan the code to ensure it opens the correct profile.',
}, },
{ {
'@type': 'HowToStep', '@type': 'HowToStep',
position: 5, position: 5,
name: 'Share', name: 'Share',
text: 'Put it on your packaging, business cards, or storefront.', text: 'Put it on your packaging, business cards, or storefront.',
}, },
], ],
totalTime: 'PT30S', totalTime: 'PT30S',
}, },
generateFaqSchema({ generateFaqSchema({
'Is this an Instagram Nametag?': { 'Is this an Instagram Nametag?': {
question: 'Is this an Instagram Nametag?', question: 'Is this an Instagram Nametag?',
answer: 'It works similarly! While Instagram has its own internal "Nametag" or "QR Code" feature, our generator allows you to create a standard QR code that is more customizable and can be tracked with our Dynamic plans.', answer: 'It works similarly! While Instagram has its own internal "Nametag" or "QR Code" feature, our generator allows you to create a standard QR code that is more customizable and can be tracked with our Dynamic plans.',
}, },
'Does it open the Instagram app?': { 'Does it open the Instagram app?': {
question: 'Does it open the Instagram app?', question: 'Does it open the Instagram app?',
answer: 'Yes. When scanned on a mobile device with Instagram installed, it will deep-link directly to the profile in the app.', answer: 'Yes. When scanned on a mobile device with Instagram installed, it will deep-link directly to the profile in the app.',
}, },
'Can I link to a specific photo or reel?': { 'Can I link to a specific photo or reel?': {
question: 'Can I link to a specific photo or reel?', question: 'Can I link to a specific photo or reel?',
answer: 'Yes! Instead of your username, just paste the full link to the specific post or reel.', answer: 'Yes! Instead of your username, just paste the full link to the specific post or reel.',
}, },
'Is it free?': { 'Is it free?': {
question: 'Is it free?', question: 'Is it free?',
answer: 'Yes, generating this QR code is 100% free.', answer: 'Yes, generating this QR code is 100% free.',
}, },
'Can I track scans?': { 'Can I track scans?': {
question: 'Can I track scans?', question: 'Can I track scans?',
answer: 'Not with this static tool. If you need scan analytics, consider using our Dynamic QR Code solution.', answer: 'Not with this static tool. If you need scan analytics, consider using our Dynamic QR Code solution.',
}, },
}), }),
], ],
}; };
export default function InstagramQRCodePage() { export default function InstagramQRCodePage() {
return ( return (
<> <>
<script <script
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/> />
<ToolBreadcrumb toolName="Instagram QR Code Generator" toolSlug="instagram-qr-code" /> <ToolBreadcrumb toolName="Instagram QR Code Generator" toolSlug="instagram-qr-code" />
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}> <div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
{/* HERO SECTION */} {/* HERO SECTION */}
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-gradient-to-br from-[#833AB4] via-[#FD1D1D] to-[#FCA145]"> <section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-gradient-to-br from-[#833AB4] via-[#FD1D1D] to-[#FCA145]">
<div className="absolute inset-0 opacity-10"> <div className="absolute inset-0 opacity-10">
<svg className="w-full h-full" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none"> <svg className="w-full h-full" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none">
<circle cx="0" cy="0" r="40" fill="white" fillOpacity="0.1" /> <circle cx="0" cy="0" r="40" fill="white" fillOpacity="0.1" />
<circle cx="100" cy="100" r="50" fill="white" fillOpacity="0.1" /> <circle cx="100" cy="100" r="50" fill="white" fillOpacity="0.1" />
</svg> </svg>
</div> </div>
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10"> <div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
<div className="text-center lg:text-left"> <div className="text-center lg:text-left">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default"> <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
<span className="flex h-2 w-2 relative"> <span className="flex h-2 w-2 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-pink-300 opacity-75"></span> <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-pink-300 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-pink-300"></span> <span className="relative inline-flex rounded-full h-2 w-2 bg-pink-300"></span>
</span> </span>
Free Tool No Signup Required Free Tool No Signup Required
</div> </div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6"> <h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
Instagram QR Code Generator<br className="hidden lg:block" /> Instagram QR Code Generator<br className="hidden lg:block" />
<span className="text-white drop-shadow-md"> Boost Your Following</span> <span className="text-white drop-shadow-md"> Boost Your Following</span>
</h1> </h1>
<p className="text-lg md:text-xl text-pink-50 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed"> <p className="text-lg md:text-xl text-pink-50 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
Connect physically to digitally. Let customers scan to follow your Instagram profile instantly. Connect physically to digitally. Let customers scan to follow your Instagram profile instantly.
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Grow your brand effortlessly.</strong> <strong className="text-white block sm:inline mt-2 sm:mt-0"> Grow your brand effortlessly.</strong>
</p> </p>
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80"> <div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm"> <div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
<Heart className="w-4 h-4 text-pink-200" /> <Heart className="w-4 h-4 text-pink-200" />
More Likes More Likes
</div> </div>
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm"> <div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
<Zap className="w-4 h-4 text-yellow-200" /> <Zap className="w-4 h-4 text-yellow-200" />
Instant Follow Instant Follow
</div> </div>
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm"> <div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
<Smartphone className="w-4 h-4 text-white" /> <Smartphone className="w-4 h-4 text-white" />
App Deep Link App Deep Link
</div> </div>
</div> </div>
</div> </div>
{/* Visual Abstract */} {/* Visual Abstract */}
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]"> <div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
<div className="absolute w-[500px] h-[500px] bg-white/10 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" /> <div className="absolute w-[500px] h-[500px] bg-white/10 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform -rotate-3 hover:rotate-0 transition-all duration-700 group"> <div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform -rotate-3 hover:rotate-0 transition-all duration-700 group">
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" /> <div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
<div className="w-full bg-white rounded-xl shadow-lg p-4 mb-6 relative overflow-hidden flex flex-col items-center"> <div className="w-full bg-white rounded-xl shadow-lg p-4 mb-6 relative overflow-hidden flex flex-col items-center">
<div className="w-16 h-16 rounded-full p-[2px] bg-gradient-to-tr from-[#FCA145] via-[#FD1D1D] to-[#833AB4] mb-2"> <div className="w-16 h-16 rounded-full p-[2px] bg-gradient-to-tr from-[#FCA145] via-[#FD1D1D] to-[#833AB4] mb-2">
<div className="w-full h-full rounded-full bg-white p-1"> <div className="w-full h-full rounded-full bg-white p-1">
<div className="w-full h-full rounded-full bg-slate-200" /> <div className="w-full h-full rounded-full bg-slate-200" />
</div> </div>
</div> </div>
<div className="text-sm font-bold text-slate-900">@yourbrand</div> <div className="text-sm font-bold text-slate-900">@yourbrand</div>
</div> </div>
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center"> <div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
<QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#E1306C" level="Q" /> <QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#E1306C" level="Q" />
</div> </div>
{/* Floating Badge */} {/* Floating Badge */}
<div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300"> <div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
<div className="bg-gradient-to-tr from-[#FCA145] to-[#E1306C] p-2 rounded-full text-white"> <div className="bg-gradient-to-tr from-[#FCA145] to-[#E1306C] p-2 rounded-full text-white">
<Camera className="w-5 h-5" /> <Camera className="w-5 h-5" />
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Profile</div> <div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Profile</div>
<div className="text-sm font-bold text-slate-900">Following</div> <div className="text-sm font-bold text-slate-900">Following</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
{/* GENERATOR SECTION */} {/* GENERATOR SECTION */}
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8"> <section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
<InstagramGenerator /> <InstagramGenerator />
</section> </section>
{/* HOW IT WORKS */} {/* HOW IT WORKS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white"> <section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12"> <h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
How Instagram QR Codes Work How Instagram QR Codes Work
</h2> </h2>
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8"> <div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
<article className="text-center"> <article className="text-center">
<div className="w-14 h-14 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4"> <div className="w-14 h-14 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
<Instagram className="w-7 h-7 text-[#E1306C]" /> <Instagram className="w-7 h-7 text-[#E1306C]" />
</div> </div>
<h3 className="font-bold text-slate-900 mb-2">1. Username</h3> <h3 className="font-bold text-slate-900 mb-2">1. Username</h3>
<p className="text-slate-600 text-sm"> <p className="text-slate-600 text-sm">
Enter your Instagram handle. No need to login or connect your account. Enter your Instagram handle. No need to login or connect your account.
</p> </p>
</article> </article>
<article className="text-center"> <article className="text-center">
<div className="w-14 h-14 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4"> <div className="w-14 h-14 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
<Smartphone className="w-7 h-7 text-[#E1306C]" /> <Smartphone className="w-7 h-7 text-[#E1306C]" />
</div> </div>
<h3 className="font-bold text-slate-900 mb-2">2. Print</h3> <h3 className="font-bold text-slate-900 mb-2">2. Print</h3>
<p className="text-slate-600 text-sm"> <p className="text-slate-600 text-sm">
Add the QR code to your packaging, business cards, or table tents. Add the QR code to your packaging, business cards, or table tents.
</p> </p>
</article> </article>
<article className="text-center"> <article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4"> <div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
<Download className="w-6 h-6 text-[#E1306C]" /> <Download className="w-6 h-6 text-[#E1306C]" />
</div> </div>
<h3 className="font-bold text-slate-900 mb-2">3. Download</h3> <h3 className="font-bold text-slate-900 mb-2">3. Download</h3>
<p className="text-slate-600 text-xs leading-relaxed"> <p className="text-slate-600 text-xs leading-relaxed">
Save your custom QR code. Save your custom QR code.
</p> </p>
</article> </article>
<article className="text-center"> <article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4"> <div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
<Heart className="w-6 h-6 text-[#E1306C]" /> <Heart className="w-6 h-6 text-[#E1306C]" />
</div> </div>
<h3 className="font-bold text-slate-900 mb-2">4. Scan</h3> <h3 className="font-bold text-slate-900 mb-2">4. Scan</h3>
<p className="text-slate-600 text-xs leading-relaxed"> <p className="text-slate-600 text-xs leading-relaxed">
Fans scan to instantly visit your profile. Fans scan to instantly visit your profile.
</p> </p>
</article> </article>
<article className="text-center"> <article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4"> <div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
<Share2 className="w-6 h-6 text-[#E1306C]" /> <Share2 className="w-6 h-6 text-[#E1306C]" />
</div> </div>
<h3 className="font-bold text-slate-900 mb-2">5. Grow</h3> <h3 className="font-bold text-slate-900 mb-2">5. Grow</h3>
<p className="text-slate-600 text-xs leading-relaxed"> <p className="text-slate-600 text-xs leading-relaxed">
Convert offline traffic into followers. Convert offline traffic into followers.
</p> </p>
</article> </article>
</div> </div>
</div> </div>
</section> </section>
{/* STATS SECTION */} {/* STATS SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-[#833AB4]/5 via-[#FD1D1D]/5 to-[#FCA145]/5"> <section className="py-16 px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-[#833AB4]/5 via-[#FD1D1D]/5 to-[#FCA145]/5">
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="text-center mb-10"> <div className="text-center mb-10">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[#E1306C]/10 text-[#E1306C] text-sm font-semibold mb-4"> <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[#E1306C]/10 text-[#E1306C] text-sm font-semibold mb-4">
<TrendingUp className="w-4 h-4" /> <TrendingUp className="w-4 h-4" />
Why Instagram QR Codes Work Why Instagram QR Codes Work
</div> </div>
<h2 className="text-3xl font-bold text-slate-900 mb-3">The Numbers Behind the Strategy</h2> <h2 className="text-3xl font-bold text-slate-900 mb-3">The Numbers Behind the Strategy</h2>
<p className="text-slate-500 text-sm max-w-xl mx-auto">Independent research on Instagram reach and QR code effectiveness.</p> <p className="text-slate-500 text-sm max-w-xl mx-auto">Independent research on Instagram reach and QR code effectiveness.</p>
</div> </div>
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6">
<div className="bg-white rounded-2xl p-7 shadow-sm border border-slate-100"> <div className="bg-white rounded-2xl p-7 shadow-sm border border-slate-100">
<div className="text-4xl font-extrabold text-[#E1306C] mb-2">2 Billion</div> <div className="text-4xl font-extrabold text-[#E1306C] mb-2">2 Billion</div>
<div className="font-semibold text-slate-800 mb-1">Monthly Active Users on Instagram</div> <div className="font-semibold text-slate-800 mb-1">Monthly Active Users on Instagram</div>
<p className="text-slate-500 text-sm">Instagram has over 2 billion monthly active users globally, making it one of the largest social platforms for brand discovery. A single well-placed QR code taps directly into that audience.</p> <p className="text-slate-500 text-sm">Instagram has over 2 billion monthly active users globally, making it one of the largest social platforms for brand discovery. A single well-placed QR code taps directly into that audience.</p>
<div className="mt-4 text-xs text-slate-400">Source: Meta, Instagram Press (2023)</div> <div className="mt-4 text-xs text-slate-400">Source: Meta, Instagram Press (2023)</div>
</div> </div>
<div className="bg-white rounded-2xl p-7 shadow-sm border border-slate-100"> <div className="bg-white rounded-2xl p-7 shadow-sm border border-slate-100">
<div className="text-4xl font-extrabold text-[#833AB4] mb-2">83%</div> <div className="text-4xl font-extrabold text-[#833AB4] mb-2">83%</div>
<div className="font-semibold text-slate-800 mb-1">of Instagram Users Discover New Brands There</div> <div className="font-semibold text-slate-800 mb-1">of Instagram Users Discover New Brands There</div>
<p className="text-slate-500 text-sm">83% of Instagram users say they discover new products and brands on the platform. An Instagram QR code on your packaging or storefront converts that discovery moment offline online.</p> <p className="text-slate-500 text-sm">83% of Instagram users say they discover new products and brands on the platform. An Instagram QR code on your packaging or storefront converts that discovery moment offline online.</p>
<div className="mt-4 text-xs text-slate-400">Source: Facebook for Business / Instagram Business (2019)</div> <div className="mt-4 text-xs text-slate-400">Source: Facebook for Business / Instagram Business (2019)</div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<RelatedTools /> <RelatedTools />
<GrowthLinksSection <GrowthLinksSection
eyebrow="Grow Your Social Presence" eyebrow="Grow Your Social Presence"
title="More Tools to Build Your Brand Online" title="More Tools to Build Your Brand Online"
description="Combine your Instagram QR code with other social and marketing tools from QR Master." description="Combine your Instagram QR code with other social and marketing tools from QR Master."
pageType="use_case" pageType="use_case"
cluster="social-media" cluster="social-media"
useCase="instagram-qr-code" useCase="instagram-qr-code"
links={[ links={[
{ {
href: '/tools/whatsapp-qr-code', href: '/tools/whatsapp-qr-code',
title: 'WhatsApp QR Code', title: 'WhatsApp QR Code',
description: 'Let customers message you instantly — no number sharing required.', description: 'Let customers message you instantly — no number sharing required.',
ctaLabel: 'Create WhatsApp QR', ctaLabel: 'Create WhatsApp QR',
}, },
{ {
href: '/tools/vcard-qr-code', href: '/tools/vcard-qr-code',
title: 'Digital Business Card', title: 'Digital Business Card',
description: 'Turn your vCard into a scannable QR code with all your contact details.', description: 'Turn your vCard into a scannable QR code with all your contact details.',
ctaLabel: 'Create vCard QR', ctaLabel: 'Create vCard QR',
}, },
{ {
href: '/dynamic-qr-code-generator', href: '/dynamic-qr-code-generator',
title: 'Dynamic QR Codes', title: 'Dynamic QR Codes',
description: 'Track how many people scan your Instagram QR code — by day, device, and city.', description: 'Track how many people scan your Instagram QR code — by day, device, and city.',
ctaLabel: 'Try Dynamic QR', ctaLabel: 'Try Dynamic QR',
}, },
]} ]}
/> />
{/* FAQ SECTION */} {/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}> <section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto"> <div className="max-w-3xl mx-auto">
<p className="text-center text-xs text-slate-400 mb-8"> <p className="text-center text-xs text-slate-400 mb-8">
By <a href="/authors/timo" className="underline hover:text-slate-600">Timo Knuth</a> · Last updated: June 2025 By <a href="/authors/timo" className="underline hover:text-slate-600">Timo Knuth</a> · Last updated: June 2025
</p> </p>
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4"> <h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Frequently Asked Questions Frequently Asked Questions
</h2> </h2>
<p className="text-slate-600 text-center mb-10"> <p className="text-slate-600 text-center mb-10">
Common questions about Instagram QR codes. Common questions about Instagram QR codes.
</p> </p>
<div className="space-y-4"> <div className="space-y-4">
<FaqItem <FaqItem
question="Does this work for private accounts?" question="Does this work for private accounts?"
answer="Yes, the link will take users to your profile. If your account is private, they will still have to request to follow you." answer="Yes, the link will take users to your profile. If your account is private, they will still have to request to follow you."
/> />
<FaqItem <FaqItem
question="Can I link to a Story?" question="Can I link to a Story?"
answer="Yes, but Stories expire after 24 hours (unless saved as a Highlight). Linking to a Highlight or your main Profile is usually better for printed materials." answer="Yes, but Stories expire after 24 hours (unless saved as a Highlight). Linking to a Highlight or your main Profile is usually better for printed materials."
/> />
<FaqItem <FaqItem
question="Can I customize the frame?" question="Can I customize the frame?"
answer="Yes, we offer several frame options like 'Follow Us' or 'Scan Me' to encourage action." answer="Yes, we offer several frame options like 'Follow Us' or 'Scan Me' to encourage action."
/> />
<FaqItem <FaqItem
question="Does it expire?" question="Does it expire?"
answer="No. The QR code will work as long as your Instagram username remains the same." answer="No. The QR code will work as long as your Instagram username remains the same."
/> />
<FaqItem <FaqItem
question="Can I track scans?" question="Can I track scans?"
answer="Not with this static tool. If you need scan analytics, consider using our Dynamic QR Code solution." answer="Not with this static tool. If you need scan analytics, consider using our Dynamic QR Code solution."
/> />
</div> </div>
</div> </div>
</section> </section>
</div> </div>
</> </>
); );
} }
function FaqItem({ question, answer }: { question: string; answer: string }) { function FaqItem({ question, answer }: { question: string; answer: string }) {
return ( return (
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden"> <details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors"> <summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
{question} {question}
<span className="transition group-open:rotate-180 text-slate-400"> <span className="transition group-open:rotate-180 text-slate-400">
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20"> <svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
<path d="M6 9l6 6 6-6" /> <path d="M6 9l6 6 6-6" />
</svg> </svg>
</span> </span>
</summary> </summary>
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm"> <div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
{answer} {answer}
</div> </div>
</details> </details>
); );
} }

View File

@@ -14,6 +14,7 @@ import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { Select } from '@/components/ui/Select'; import { Select } from '@/components/ui/Select';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors - PayPal Blue // Brand Colors - PayPal Blue
const BRAND = { const BRAND = {
@@ -64,6 +65,7 @@ export default function PayPalGenerator() {
const [currency, setCurrency] = useState('EUR'); const [currency, setCurrency] = useState('EUR');
const [qrColor, setQrColor] = useState(BRAND.primary); const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null); const qrRef = useRef<HTMLDivElement>(null);
@@ -114,6 +116,7 @@ export default function PayPalGenerator() {
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
if (shouldShowDownloadPopup()) setShowPopup(true);
}; };
const getFrameLabel = () => { const getFrameLabel = () => {
@@ -122,6 +125,8 @@ export default function PayPalGenerator() {
}; };
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
@@ -338,5 +343,6 @@ export default function PayPalGenerator() {
</Link> </Link>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -13,6 +13,7 @@ import {
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors // Brand Colors
const BRAND = { const BRAND = {
@@ -46,6 +47,7 @@ export default function SMSGenerator() {
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [qrColor, setQrColor] = useState(BRAND.primary); const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null); const qrRef = useRef<HTMLDivElement>(null);
@@ -76,6 +78,7 @@ export default function SMSGenerator() {
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
if (shouldShowDownloadPopup()) setShowPopup(true);
}; };
const getFrameLabel = () => { const getFrameLabel = () => {
@@ -84,6 +87,8 @@ export default function SMSGenerator() {
}; };
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
@@ -262,5 +267,6 @@ export default function SMSGenerator() {
</Link> </Link>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -14,6 +14,7 @@ import {
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors - Microsoft Teams Purple // Brand Colors - Microsoft Teams Purple
const BRAND = { const BRAND = {
@@ -53,6 +54,7 @@ export default function TeamsGenerator() {
const [userEmail, setUserEmail] = useState(''); const [userEmail, setUserEmail] = useState('');
const [qrColor, setQrColor] = useState(BRAND.primary); const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null); const qrRef = useRef<HTMLDivElement>(null);
@@ -96,6 +98,7 @@ export default function TeamsGenerator() {
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
if (shouldShowDownloadPopup()) setShowPopup(true);
}; };
const getFrameLabel = () => { const getFrameLabel = () => {
@@ -104,6 +107,8 @@ export default function TeamsGenerator() {
}; };
return ( return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
@@ -314,5 +319,6 @@ export default function TeamsGenerator() {
</Link> </Link>
</div> </div>
</div> </div>
</>
); );
} }

Some files were not shown because too many files have changed in this diff Show More