Copy overhaul + qr designs
131
CHECKLISTE_UMSETZUNG_2026-07-27.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Checkliste - Stand nach der Umsetzung
|
||||
|
||||
27. Juli 2026. 118 Dateien, 0 Syntaxfehler, Prisma-Schema valide, beide i18n-JSONs valide, keine langen Striche.
|
||||
|
||||
---
|
||||
|
||||
## FERTIG
|
||||
|
||||
### `/create` - Limit-Moment
|
||||
|
||||
- [x] Redirect auf `/pricing` entfernt, Formularzustand überlebt den 403
|
||||
- [x] `UpgradeModal.tsx` mit drei Anlässen: `limit`, `logo`, `shapes`
|
||||
- [x] Direkt-Checkout mit `returnPath` (pfadvalidiert gegen offene Redirects)
|
||||
- [x] Option "bestehenden Code pausieren", danach Auto-Retry des Speicherns
|
||||
- [x] Option "als statischen Code weitermachen"
|
||||
- [x] Scan-Zahlen der letzten 30 Tage je Code, damit die Pausier-Entscheidung informiert ist
|
||||
- [x] PostHog-Events für alle vier Ausgänge
|
||||
|
||||
### Limit-Logik und gestopfte Löcher
|
||||
|
||||
- [x] `POST /api/qrs` und `GET /api/user/stats` zählen nur noch `status: 'ACTIVE'`
|
||||
- [x] `GET /api/qrs` liefert `scans30d` je Code
|
||||
- [x] `PATCH /api/qrs/[id]` akzeptiert `status` (kannte es vorher nicht, Pausieren wäre wirkungslos gewesen)
|
||||
- [x] Reaktivieren prüft gegen das Kontingent (sonst Limit-Umgehung durch pausieren/neu/reaktivieren)
|
||||
- [x] `bulk-creation` lädt das Restkontingent serverseitig nach jedem Lauf
|
||||
|
||||
### `/bulk-creation`
|
||||
|
||||
- [x] Stille Fehler beendet: fehlgeschlagene Zeilen werden gesammelt statt verschluckt
|
||||
- [x] Ergebnis-Panel mit Zeilennummer, Titel und Grund je Fehlzeile
|
||||
- [x] CSV-Download der fehlenden Zeilen
|
||||
- [x] Grüner Toast nur bei vollständigem Lauf
|
||||
- [x] Drei deutsche Toasts auf Englisch
|
||||
|
||||
### Upgrade-Wege
|
||||
|
||||
- [x] Neue Seite `/upgrade` in der `(app)`-Route-Group, Sidebar bleibt
|
||||
- [x] Alle 6 In-App-Links umgebogen: AppLayout, Dashboard, Settings (2x), Bulk (2x)
|
||||
- [x] `reason`- und `from`-Parameter, `from` geht als `returnPath` in den Checkout
|
||||
|
||||
### QR-Design
|
||||
|
||||
- [x] Showcase von Canvas auf **SVG** portiert (`lib/qr-shapes.ts`)
|
||||
- [x] `StyledQRCode.tsx` für die Live-Vorschau, `lib/render-qr-svg.ts` für Bulk - gemeinsame Shape-Funktionen, können nicht auseinanderlaufen
|
||||
- [x] 11 Modulformen, plangestaffelt: Free `square`, Pro 4, Business alle
|
||||
- [x] Eye-Frames und Eye-Balls getrennt wählbar
|
||||
- [x] Farbverläufe linear und radial, Business
|
||||
- [x] Gesperrte Formen sind klickbar: Vorschau zuerst, Modal danach
|
||||
- [x] Automatische Anhebung der Fehlerkorrektur bei kritischen Formen und bei Logo, mit sichtbarer Begründung
|
||||
- [x] Druckgrößen- und Test-Scan-Hinweis
|
||||
- [x] Toter `toPng`-Aufruf im SVG-Download entfernt
|
||||
|
||||
### Scanbarkeit - gemessen, nicht geschätzt
|
||||
|
||||
Gerendert und mit einem echten Decoder zurückgelesen, 5 Inhalte x mehrere Auflösungen:
|
||||
|
||||
- [x] Alle 11 Modulformen 5/5
|
||||
- [x] Eye-Balls square, rounded, circle, diamond, hexagon 5/5
|
||||
- [x] **star als Eye-Ball 3/5 - entfernt**
|
||||
- [x] **Eye-Frames circle, leaf, flower, hexagon 0/15 - entfernt**
|
||||
- [x] **Rundungsradius 0.28 0/15, auf 0.10 korrigiert (15/15)**
|
||||
- [x] Alle 10 verbleibenden Eye-Kombinationen 30/30
|
||||
- [x] Bulk-Renderer separat geprüft, inklusive Verlauf: 12/12
|
||||
|
||||
### Design-Vorlagen
|
||||
|
||||
- [x] `QRDesignPreset`-Modell in Prisma, Relation am User
|
||||
- [x] `GET/POST/DELETE /api/design-presets`, Business-gated, CSRF-geschützt
|
||||
- [x] Gleicher Name überschreibt statt Duplikat, Obergrenze 50
|
||||
- [x] Speichern und Anwenden in `/create`
|
||||
- [x] Preset-Auswahl im Bulk-Flow: ein Design für den ganzen Upload
|
||||
|
||||
### Retention-Mails
|
||||
|
||||
- [x] Tag-3-Betreff entschärft
|
||||
- [x] Tag-7 zur verhaltensbasierten Limit-Mail umgebaut
|
||||
- [x] Tabellenzeile `CSV export ✓/✓` gestrichen, `Brand colors` ersetzt (war nach der Farbfreigabe falsch)
|
||||
- [x] Neue Erster-Scan-Mail, einziger Trigger ohne Kalender
|
||||
- [x] Tag-30 auf eigene Scan-Zahlen, unbelegte Testimonial-Behauptung gestrichen
|
||||
- [x] Tag-30 entfällt bei null Scans
|
||||
- [x] Cron-Route neu, vier Trigger
|
||||
|
||||
### Farben ab Free und Folge-Copy
|
||||
|
||||
- [x] `canCustomizeColors = true`, neue Gates `canUseShapes`, `canUseLogo`, `canUseFullDesign`
|
||||
- [x] `en.json`, `PricingClient`, Dashboard-Pro-Karte
|
||||
- [x] `/alternatives/flowcode`, 5 Stellen
|
||||
- [x] `/pricing` FAQ: Bulk-Antwort war noch static-only
|
||||
- [x] `competitor-data.ts`: Vergleichszeile führte Farben als Pro
|
||||
|
||||
### Formales
|
||||
|
||||
- [x] 729 lange Striche ersetzt, keine mehr im Quellcode
|
||||
- [x] CRLF-Zeilenenden durchgehend erhalten
|
||||
- [x] SQL-Datei aktualisiert: Block 4 ist jetzt Pflicht, nicht optional
|
||||
|
||||
---
|
||||
|
||||
## NICHT GEBAUT
|
||||
|
||||
### Vor dem Deploy zwingend
|
||||
|
||||
- [ ] **SQL ausführen** (`sql/2026-07-27_cro_retention_design.txt`), alle sechs Blöcke
|
||||
- [ ] **`npx prisma generate`** - ohne das kennt der Client `QRDesignPreset` nicht und `/api/design-presets` wirft zur Laufzeit
|
||||
- [ ] **Block 3.2** - sonst geht die Erster-Scan-Mail an die gesamte Bestandsbasis
|
||||
- [ ] Entscheidung zu Block 3.1
|
||||
|
||||
### Nicht getestet
|
||||
|
||||
- [ ] Kein Browser-Durchlauf. Der Renderer ist gegen einen Decoder geprüft, nicht im echten DOM
|
||||
- [ ] PNG-Download über `html-to-image` mit dem neuen SVG nicht verifiziert
|
||||
- [ ] SVG-Download nicht verifiziert
|
||||
- [ ] Logo-Overlay im neuen Renderer nicht visuell geprüft
|
||||
- [ ] Stripe-Checkout mit `returnPath` nicht live durchlaufen
|
||||
- [ ] Pausieren und Reaktivieren nicht gegen echte Daten
|
||||
- [ ] Preset speichern, laden, auf Bulk anwenden nicht gegen echte Daten
|
||||
|
||||
### Bewusst weggelassen
|
||||
|
||||
- [ ] Rahmen mit Label als Vektor. Die bestehenden Rahmen sind HTML um den QR herum; der SVG-Download fällt dort weiterhin auf PNG zurück und sagt es
|
||||
- [ ] Logo-Formen (Punkt, Herz, Blitz, WLAN) aus der Showcase
|
||||
- [ ] Eye-Frames circle, leaf, flower, hexagon - Messergebnis 0/15
|
||||
- [ ] star als Eye-Ball - 3/5
|
||||
|
||||
### Offene inhaltliche Frage
|
||||
|
||||
- [ ] Tag-30-Mail: gab es die Gespräche mit Pro-Nutzern über Branding? Bei Ja gehört ein wörtliches Zitat rein
|
||||
|
||||
### Vorbehalt zur Messung
|
||||
|
||||
Getestet wurde mit OpenCVs Decoder. Der ist strenger als iPhone- und Android-Kameras. Es ist möglich, dass ein Kreis-Eye-Frame auf echten Geräten funktioniert - aber 0 von 15 ist kein Rauschen, und das ist nichts, was man ungetestet auf Druckmaterial loslässt.
|
||||
71
CTR_REWRITE_2026-07-27.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# SERP-CTR Rewrite — 27. Juli 2026
|
||||
|
||||
Grundlage: GSC-Export 28 Tage (8.944 Impressionen, 52 Klicks, 0,58% CTR, Ø-Position 33,0)
|
||||
Methodik: `direct-response-copywriting` + Positioning Decision vom 26.07.2026 (Direction 1 als Positionierung, Direction 2 als Proof-Layer)
|
||||
|
||||
## Ehrliche Einordnung des 5–10%-Ziels
|
||||
|
||||
Site-weite CTR von 5–10% ist bei Ø-Position 33 **nicht durch Copy erreichbar**. CTR ist primär eine Funktion der Position:
|
||||
|
||||
| Position | Erwartete CTR |
|
||||
|---|---|
|
||||
| 1 | 28–40% |
|
||||
| 3 | 10–11% |
|
||||
| 5 | 5–6% |
|
||||
| 10 | 2–3% |
|
||||
| 15 | 1–2% |
|
||||
| 30+ | <0,3% |
|
||||
|
||||
Die zwei größten Impression-Seiten (`/dynamic-qr-code-generator` 1.694 Imp @ Pos 33,5 und `/qr-code-tracking` 1.023 Imp @ Pos 34,0) haben 0% CTR. Das ist bei Position 33 **normal** — 31% aller Impressionen der Site liegen auf Seite 4 der SERP. Kein Title der Welt repariert das; das ist ein Ranking-Thema.
|
||||
|
||||
**Was Copy realistisch leisten kann:**
|
||||
- Seiten auf Pos 1–15 auf ihren Positions-Benchmark heben → aus 52 Klicks werden ca. 90–120
|
||||
- Die großen Volumenseiten so vorbereiten, dass sie beim Aufstieg auf Seite 1 sofort auf Benchmark klicken statt bei 2%
|
||||
|
||||
5–10% site-weit wird erst realistisch, wenn die Ø-Position unter ~10 liegt. Dann trägt diese Copy den Unterschied.
|
||||
|
||||
## Die drei strukturellen Fehler, die behoben wurden
|
||||
|
||||
**1. Deutsche Halbsätze in englischen Descriptions.** 97% der Impressionen kommen aus USA/Indien/englischsprachigen Märkten. Trotzdem stand in den Tool-Descriptions „Teams QR Code erstellen", „Zoom QR Code erstellen", „TikTok QR Code erstellen", „Erstelle Bitcoin & Ethereum QR Codes", „Standort teilen leicht gemacht". Für einen englischen Sucher liest sich das wie maschinell übersetzter Spam. Das ist die wahrscheinlichste Ursache für 0,61% CTR auf Pos 10 (`barbershops`) und 0,65% auf Pos 17 (`zoom`).
|
||||
|
||||
**2. `| QR Master`-Suffix und 65+ Zeichen lange Titles.** Google schneidet bei ~580px ab. `Free Microsoft Teams QR Code Generator | Join Meetings | QR Master` (66 Zeichen) wurde in der SERP abgeschnitten — der Nutzen verschwand im „…". Alle neuen Titles liegen bei 42–55 Zeichen.
|
||||
|
||||
**3. Null Pattern Interruption.** Jedes Ergebnis in dieser SERP sagt „Free QR Code Generator". Die alten Descriptions sagten „Instant and free", „Free & Easy" — die vagesten möglichen Claims. Neu: konkrete, falsifizierbare Spezifika (3 dynamische Codes gratis, 1.000 Codes aus einem Spreadsheet, 42 Zeichen Wallet-Adresse, EAN-13/UPC-A/Code 128).
|
||||
|
||||
## Wichtigste Änderungen
|
||||
|
||||
| Seite | Imp | CTR | Pos | Kernänderung |
|
||||
|---|---|---|---|---|
|
||||
| `/` | 1.046 | 4,88% | 4,6 | Title: Mechanismus statt Kategorie. Description: 3 verifizierte Zahlen statt Feature-Liste. H1: „The Link Doesn't Have to Stay Wrong Once It's Printed" |
|
||||
| `/qr-code-tracking` | 1.023 | 0% | 34,0 | Title war „QR Code Tracking: Track QR Code Scans" — redundant, kein Nutzenversprechen. Neu: „See Which Placement Drove the Scan" (= Main-Headline-Idee aus Offer Brief §14) |
|
||||
| `/dynamic-qr-code-generator` | 1.694 | 0% | 33,5 | Title war schon gut, blieb. Description auf Schmerz + Zahlen umgestellt |
|
||||
| `/tools/google-review-qr-code` | 566 | 0,18% | 24,8 | Description führt jetzt mit dem Moment: „while the customer is still standing there" |
|
||||
| `/tools/teams-qr-code` | 277 | 3,25% | 9,6 | Title von 66 auf 48 Zeichen, Deutsch raus |
|
||||
| `/qr-code-for/barbershops` | 163 | 0,61% | 10,0 | metaTitle war 68 Zeichen → Fallback griff auf generisches „QR Codes for Barbershops \| QR Master". Jetzt 44 Zeichen mit Nutzen |
|
||||
| `/learn` | 226 | 0,88% | 11,3 | Zielt auf „qr mastery" (130 Imp @ Pos 5,1). Description führt jetzt mit den Problemen statt mit „Learning Hub" |
|
||||
| `/bulk-qr-code-generator` | 296 | 2,36% | 23,1 | Description nennt offen „Output is static — not dynamic or trackable" (Belief 5 / Proof-Layer) |
|
||||
|
||||
Zusätzlich überarbeitet: crypto, facebook, url, vcard, instagram, tiktok, twitter, zoom, geolocation, barcode-generator, custom-qr-code-generator, alternatives/beaconstac, blog/microsoft-teams-qr-code.
|
||||
|
||||
## Zwei Korrekturen an bestehenden Claims
|
||||
|
||||
- **`/alternatives/beaconstac`**: Erster Entwurf enthielt „Not €99+" über Uniqode. Der Offer Brief verifiziert Uniqodes Preis **nicht** (nur „G2-Muster: teuer für kleine Teams"). Behauptung entfernt — genannt werden nur die eigenen, verifizierten Preise €0/€9/€29.
|
||||
- **Pricing-Card**: „Bulk QR Creation (up to 1,000)" → „(up to 1,000, static output)" in `en.json`. Das war Immediate Next Action #1 aus dem Offer Brief (A2) und die größte Transparenz-Lücke auf der Seite.
|
||||
|
||||
## Was als Nächstes mehr bringt als weitere Copy
|
||||
|
||||
1. **Ranking, nicht CTR.** 31% der Impressionen liegen auf Position 30+. Der Hebel dort ist Content/Links, nicht der Title.
|
||||
2. **Die Kündigungs-Policy (Offer Brief A3).** ToS §4 sagt „may affect the availability of dynamic QR codes". Für genau diese Zielgruppe ist das das Erkennungszeichen der schlechten Anbieter. Blockiert den gesamten Proof-Layer.
|
||||
3. **10–15 unabhängige Reviews auf G2/Capterra.** Laut Offer Brief §8 höher priorisiert als jede Copy-Iteration.
|
||||
|
||||
## Messung
|
||||
|
||||
In 4 Wochen im GSC vergleichen — **pro Seite und positionsbereinigt**, nicht site-weit:
|
||||
|
||||
- `/` — Ziel 7–9% (von 4,88%, bei stabiler Pos ~4,6)
|
||||
- `/learn` — Ziel 1,8–2,5% (von 0,88%)
|
||||
- `/qr-code-for/barbershops` — Ziel 2,0–2,5% (von 0,61%)
|
||||
- `/tools/zoom-qr-code` — Ziel 1,2% (von 0,65%)
|
||||
- `/tools/teams-qr-code` — Ziel 4–5% (von 3,25%)
|
||||
|
||||
Wenn die Position sich gleichzeitig verändert, ist der Vergleich der Rohwerte wertlos — dann `gsc-ctr-by-position` verwenden.
|
||||
421
PLAN_CRO_UND_RETENTION_2026-07-27.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# Umsetzungsplan - CRO-Momente, Upgrade-Wege und Retention-Mails
|
||||
|
||||
Stand 27. Juli 2026. Alle Datei- und Zeilenangaben gegen den aktuellen Stand verifiziert.
|
||||
|
||||
**Getroffene Entscheidungen:**
|
||||
- Das Dynamic-Limit zählt künftig nur noch Codes mit `status = ACTIVE`. Pausieren gibt einen Slot frei.
|
||||
- Der Upgrade-Weg im Limit-Moment läuft per Direkt-Checkout aus dem Modal. Kein Redirect auf `/pricing`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 - Der Limit-Moment in `/create`
|
||||
|
||||
### 1.1 Limit-Query auf ACTIVE umstellen
|
||||
|
||||
**Datei:** `src/app/(main)/api/qrs/route.ts`, Zeile 117-121
|
||||
|
||||
```ts
|
||||
const dynamicQRCount = await db.qRCode.count({
|
||||
where: { userId, type: 'DYNAMIC', status: 'ACTIVE' },
|
||||
});
|
||||
```
|
||||
|
||||
Dieselbe Änderung in `src/app/(main)/api/user/stats/route.ts`, sonst zeigt das Dashboard eine andere Zahl als die API durchlässt.
|
||||
|
||||
**Nebenwirkung, die vorher klar sein muss:** Free-Nutzer, die heute pausierte Codes haben, bekommen dadurch rückwirkend Slots frei. Das ist eine Lockerung, keine Verschärfung - es nimmt niemandem etwas weg. Vor dem Deploy einmal zählen, wie viele Nutzer betroffen sind (SQL unten in Abschnitt „Prüf-Queries").
|
||||
|
||||
### 1.2 Das Limit-Modal
|
||||
|
||||
**Neue Datei:** `src/components/app/DynamicLimitModal.tsx`
|
||||
|
||||
Der 403 aus `/api/qrs` liefert bereits `currentCount`, `limit` und `plan` mit. Es braucht keinen zusätzlichen Request.
|
||||
|
||||
**Datei:** `src/app/(main)/(app)/create/page.tsx`, Zeile 456-459 - der Redirect entfällt ersatzlos:
|
||||
|
||||
```ts
|
||||
if (response.status === 403 && responseData.error === 'Limit reached') {
|
||||
setLimitInfo({ current: responseData.currentCount, limit: responseData.limit, plan: responseData.plan });
|
||||
setLimitModalOpen(true);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Der Formular-State bleibt dadurch erhalten. Das ist der eigentliche Fix - alles andere ist Ausgestaltung.
|
||||
|
||||
**Copy des Modals:**
|
||||
|
||||
> ### Dein vierter Code ist fertig. Er braucht nur noch einen Platz.
|
||||
>
|
||||
> Du nutzt alle 3 dynamischen Codes deines kostenlosen Plans. Dieser hier ist gebaut und wartet - du kannst ihn behalten oder einen bestehenden freigeben.
|
||||
>
|
||||
> **[ Diesen Code mit Pro speichern - 9 € / Monat ]**
|
||||
> [ Einen bestehenden Code pausieren ]
|
||||
> [ Stattdessen als statischen Code herunterladen ]
|
||||
>
|
||||
> *Deine 3 aktiven Codes laufen weiter, egal wie du dich entscheidest.*
|
||||
|
||||
Die Zahlen (`vierter`, `3`) kommen aus `limitInfo`, damit das Modal auch für Pro bei 51 stimmt.
|
||||
|
||||
**Warum diese drei Optionen:**
|
||||
|
||||
Der Hauptbutton verkauft keinen Plan, sondern rettet einen konkreten Code, den der Nutzer gerade in der Hand hat. Option 2 ist die ehrliche Alternative innerhalb des Free-Plans - sie kostet ein paar Conversions und kauft dafür Belief 5 aus dem Necessary-Beliefs-Doc. Option 3 ist der Ausweg ohne Verlust: ein statischer Code löst das Problem zu einem guten Teil, kostet nichts, und die Zusage „läuft nie ab" ist verifiziert. Der Schlusssatz ist Risk Reversal genau an der Stelle, an der die Kategorie ihren schlechtesten Ruf hat.
|
||||
|
||||
### 1.3 Direkt-Checkout aus dem Modal
|
||||
|
||||
Der Pro-Button ruft `/api/stripe/checkout` direkt auf, mit `priceId`, `plan: 'PRO'` und `userEmail`.
|
||||
|
||||
**Datei:** `src/app/(main)/api/stripe/checkout/route.ts`, Zeile 63. Die `success_url` ist aktuell fest auf `/dashboard?success=true`. Sie muss eine optionale `returnPath` aus dem Request-Body akzeptieren, damit der Nutzer nach dem Kauf dorthin zurückkommt, wo er war.
|
||||
|
||||
Vor dem Öffnen von Stripe wird der Formularzustand nach `localStorage` geschrieben (`qrm_pending_qr`, mit Zeitstempel). Beim Zurückkommen auf `/create?restored=1` liest die Seite ihn aus, füllt das Formular und zeigt: *„Willkommen zurück. Dein Code steht noch genau so da - jetzt mit Platz."* Danach den Key löschen. Einträge älter als 24 Stunden werden verworfen.
|
||||
|
||||
Kein Draft in der Datenbank. Der Zustand ist ohnehin nur im Browser relevant, und eine Draft-Tabelle wäre Infrastruktur für ein Problem, das `localStorage` löst.
|
||||
|
||||
### 1.4 Option „Code pausieren" im Modal
|
||||
|
||||
Zeigt die aktiven dynamischen Codes des Nutzers mit Titel und Scan-Zahl der letzten 30 Tage. Ein Klick setzt `status = PAUSED` über das bestehende `PATCH /api/qrs/[id]`, danach wird der ursprüngliche POST automatisch wiederholt.
|
||||
|
||||
Die Scan-Zahl daneben ist wichtig: sie macht die Entscheidung informiert statt willkürlich. Wer sieht, dass ein Code seit vier Wochen null Scans hat, pausiert ihn ohne schlechtes Gefühl - und wer sieht, dass alle drei laufen, versteht ohne Verkaufstext, warum Pro sinnvoll ist. Das ist Pointing statt Talking.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 - `/bulk-creation`
|
||||
|
||||
**Datei:** `src/app/(main)/(app)/bulk-creation/page.tsx`, Zeile 232-247
|
||||
|
||||
Die Schleife hat kein `else` zu `if (res.ok)`. Fehlgeschlagene Zeilen verschwinden still, und danach meldet ein grüner Toast `${results.length} dynamische QR-Codes erstellt!` - eine Zahl, die kleiner sein kann als das Hochgeladene, ohne jeden Hinweis.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```ts
|
||||
const failed: { row: number; title: string; reason: string }[] = [];
|
||||
// im else-Zweig: failed.push({ row: i + 1, title, reason: (await res.json()).error })
|
||||
```
|
||||
|
||||
Danach, wenn `failed.length > 0`, statt des Erfolgs-Toasts ein Ergebnis-Panel:
|
||||
|
||||
> **180 von 200 Codes erstellt.**
|
||||
> 20 Zeilen konnten nicht angelegt werden, weil dein Kontingent an dynamischen Codes erschöpft ist. Hier sind sie - du kannst sie als statische Codes erzeugen oder dein Kontingent erhöhen.
|
||||
>
|
||||
> [ Fehlende Zeilen als CSV ] [ Kontingent erhöhen ]
|
||||
|
||||
Zusätzlich in derselben Datei:
|
||||
|
||||
- `remainingDynamic` nach dem Lauf vom Server neu laden statt nur lokal herunterzuzählen (Zeile 251). Das ist die Ursache der Race Condition.
|
||||
- Die Toasts sind auf Deutsch (`'Du hast keine dynamischen QR-Codes mehr übrig...'`, Zeile 215 und 221), während die restliche App-Oberfläche Englisch ist. Auf Englisch umstellen.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 - Upgrade-Wege aus der App
|
||||
|
||||
`/pricing` liegt in der `(marketing)`-Route-Group. Wer im Dashboard auf Upgrade klickt, verliert die Sidebar und landet in der Marketing-Site. Zusätzlich liest `PricingClient.tsx` `searchParams` überhaupt nicht - das `?reason=limit_reached`, das `/create` heute anhängt, wird vollständig ignoriert.
|
||||
|
||||
Nach der getroffenen Entscheidung läuft der Limit-Fall künftig über das Modal, damit ist der wichtigste Fall gelöst. Es bleiben drei In-App-Links auf `/pricing`:
|
||||
|
||||
| Datei | Zeile | Kontext |
|
||||
|---|---|---|
|
||||
| `dashboard/page.tsx` | 340 | Upgrade-Badge im Header |
|
||||
| `create/page.tsx` | 977 | Hinweis „Upgrade to PRO to customize colors" |
|
||||
| `create/page.tsx` | 1106 | Hinweis „Upgrade to PRO to add logos" |
|
||||
|
||||
Die beiden Hinweise in `/create` sollten dasselbe Modal öffnen wie der Limit-Fall, nur mit anderem Aufhänger („Dein Logo gehört in diesen Code"). Der Nutzer ist mitten im Gestalten - ihn dafür aus der Seite zu werfen ist derselbe Fehler wie beim Limit, nur weniger sichtbar.
|
||||
|
||||
Der Dashboard-Badge kann auf `/pricing` zeigen bleiben. Dort ist der Nutzer nicht mitten in einer Aufgabe, es gibt nichts zu verlieren. Nur ein `?from=dashboard` anhängen und in `PricingClient` einen Zurück-Link rendern, damit der Weg zurück nicht über den Browser-Button läuft.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 - Post-Download-Popup auf den Tool-Seiten
|
||||
|
||||
**Datei:** `src/components/marketing/PostDownloadPopup.tsx`
|
||||
|
||||
Timing und die Ablehn-Option `No thanks, keep it static` bleiben unverändert - beides ist richtig gebaut.
|
||||
|
||||
Ersetzt werden Headline und Bullet-Liste:
|
||||
|
||||
> ### Dieser Code zeigt jetzt für immer auf diese URL.
|
||||
>
|
||||
> Bei einem dauerhaften Link ist das genau richtig. Falls sich das Ziel je ändert, brauchst du einen neuen Code und neues Druckmaterial.
|
||||
>
|
||||
> Ein kostenloses Konto gibt dir 3 dynamische Codes: gleiches Bild, Ziel jederzeit änderbar, jeder Scan gezählt.
|
||||
>
|
||||
> **[ Kostenloses Konto anlegen - keine Karte ]**
|
||||
> *Nein danke, statisch reicht*
|
||||
|
||||
Die vier Bullets entfallen. Drei gleichrangige Vorteile sind schwächer als ein Satz, der den einen benennt - in einer Liste aus vier gleich formatierten Punkten ist keiner davon wichtig.
|
||||
|
||||
Neues Prop `variant`, das nur die erste Zeile austauscht:
|
||||
|
||||
| Tool | Erste Zeile |
|
||||
|---|---|
|
||||
| Google Review | Dieser Code zeigt jetzt für immer auf dieses Google-Profil. |
|
||||
| WiFi | Dieser Code enthält jetzt dauerhaft dieses WLAN-Passwort. |
|
||||
| vCard | Dieser Code enthält jetzt dauerhaft diese Kontaktdaten. |
|
||||
| Crypto | Dieser Code enthält jetzt dauerhaft diese Wallet-Adresse. |
|
||||
| Standard | Dieser Code zeigt jetzt für immer auf diese URL. |
|
||||
|
||||
Die WiFi-Variante ist die stärkste, weil sie einen Umstand benennt, den fast niemand vorher bedenkt: Wer das Passwort ändert, hat wertloses Druckmaterial.
|
||||
|
||||
Betroffen sind die 10+ Generator-Komponenten, die `PostDownloadPopup` einbinden - dort jeweils nur das `variant`-Prop ergänzen.
|
||||
|
||||
**Zusätzlich:** `shouldShowDownloadPopup()` prüft einen einzigen `localStorage`-Key. Wer das Popup einmal gesehen hat, sieht es auf keiner anderen Tool-Seite je wieder - auch nicht Monate später in einem anderen Kontext. Vorschlag: Key mit Zeitstempel, Wiedervorlage nach 30 Tagen.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 - Retention-Mails
|
||||
|
||||
**Datei:** `src/lib/email.ts` und `src/app/(main)/api/cron/retention-emails/route.ts`
|
||||
|
||||
### 5.1 Tag-7-Mail: Trigger vom Kalender aufs Verhalten
|
||||
|
||||
Aktuell feuert sie bei `createdAt < 7 Tage` und `qrCount > 0`. Ein Nutzer mit einem einzigen Code bekommt „You're 2 away from the free limit" - eine Verkaufsmail über ein Limit, das ihn nicht drückt, abgeschickt unter deinem Namen.
|
||||
|
||||
Neue Logik:
|
||||
|
||||
| Zustand | Auslöser | Mail |
|
||||
|---|---|---|
|
||||
| aktive dynamische Codes = Limit | sobald erreicht | Limit-Mail, Marker `limitReachedNudgeSentAt` |
|
||||
| 1-2 von 3 belegt, Tag 7 | Tag 7 | keine Upgrade-Mail |
|
||||
| erster Scan liegt vor | 1 Tag danach | Erster-Scan-Mail, Marker `firstScanNudgeSentAt` |
|
||||
|
||||
In der Vergleichstabelle der Limit-Mail entfällt die Zeile `CSV export: Free ✓ / Pro ✓`. Eine Zeile, in der beide Spalten identisch sind, gehört nicht in eine Upgrade-Tabelle - sie verwässert die drei, die einen Unterschied zeigen.
|
||||
|
||||
### 5.2 Neue Mail: erster Scan
|
||||
|
||||
`User.firstScanAt` existiert bereits und wird in `src/app/(main)/r/[slug]/route.ts` (Zeile 150-153 und 263-266) gesetzt. Es fehlt nur ein Versand-Marker.
|
||||
|
||||
Betreff: **Dein Code wurde gerade zum ersten Mal gescannt**
|
||||
|
||||
> Um {Uhrzeit}, auf einem {Gerät}, aus {Land}. Dein Code „{Titel}" ist im Einsatz.
|
||||
>
|
||||
> Ab jetzt zählt jeder weitere Scan mit. In ein paar Tagen siehst du, wann die meisten kommen - und ob sich der Ort lohnt, an dem du den Code platziert hast.
|
||||
>
|
||||
> [ Scans ansehen ]
|
||||
|
||||
Kein Verkaufsargument. Diese Mail hat einen Anlass, der nicht konstruiert ist, und ist der Moment, in dem die Positionierung zum ersten Mal einlöst. Sie ist die einzige in der Sequenz, deren Anlass nicht vom Kalender kommt.
|
||||
|
||||
### 5.3 Tag-30-Mail auf Scan-Daten umbauen
|
||||
|
||||
Zwei Probleme mit der jetzigen Fassung:
|
||||
|
||||
Der Satz *„The one thing I hear most from Pro users who switched after a few weeks: they wish they'd added their brand sooner"* behauptet ein Muster aus Kundengesprächen. Wenn es die gab: echtes Zitat rein. Wenn nicht: **streichen** - das ist ein erfundenes Testimonial in indirekter Rede und verstößt gegen die Beweisregel im Product Context. Bei einer Zielgruppe, die Bewertungsportale liest, ist das die teuerste Art von Satz.
|
||||
|
||||
Und Branding ist der schwächere von zwei verfügbaren Aufhängern. Nach 30 Tagen hat der Nutzer Scan-Daten. Neuer Aufbau:
|
||||
|
||||
> Deine Codes wurden diesen Monat {n}-mal gescannt, {Vorname}.
|
||||
>
|
||||
> Die meisten davon {Wochentag}s. Was du noch nicht sehen kannst: von welchen Geräten sie kamen und aus welchen Orten - und damit, welche deiner Platzierungen die Scans wirklich gebracht hat.
|
||||
>
|
||||
> [ Vollständige Auswertung freischalten ]
|
||||
|
||||
Damit ist der Kaufgrund aus dem hergeleitet, was der Nutzer selbst erlebt hat. Deckt sich mit Offer Brief §10, Option 1 - auf Analytics-Tiefe metern statt auf Code-Anzahl -, und ist die erste Stelle, an der man das testen kann, ohne das Pricing anzufassen.
|
||||
|
||||
### 5.4 Tag-3-Mail: kleine Korrektur
|
||||
|
||||
Betreff `You haven't made one yet` kann als Vorwurf gelesen werden - im Body löst die Headline es auf, im Posteingang steht der Betreff allein. Alternative gleicher Länge: **`Your 3 free codes are still sitting there`**.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 - QR-Design nach Plan gestaffelt
|
||||
|
||||
Neue Staffelung: Farben ab Free, Formen ab Pro, alles ab Business.
|
||||
|
||||
### 6.0 Der technische Blocker, der vorher geklärt sein muss
|
||||
|
||||
`/create` rendert über `QRCodeSVG` aus `qrcode.react` (Zeile 6 und 1233). **Diese Bibliothek kann ausschließlich quadratische Module.** Es gibt heute überhaupt keine Formauswahl im Produkt - nicht weil sie gesperrt wäre, sondern weil der Renderer sie nicht kann.
|
||||
|
||||
Für Formen muss der Renderer also getauscht werden. Es gibt zwei Kandidaten, und sie decken unterschiedlich viel ab:
|
||||
|
||||
| Renderer | Kann | Kann nicht |
|
||||
|---|---|---|
|
||||
| `qr-code-styling` (liegt bereits als Dependency in `package.json`, Zeile 64, ungenutzt) | square, dots, rounded, extra-rounded, classy, classy-rounded, Eye-Styles getrennt, Verläufe, Logo | star, plus, hexagon, diamond, mosaic, liquid |
|
||||
| Canvas-Renderer aus `qr-design-showcase.html` | alle 11 Formen inklusive star, plus, hexagon, diamond, mosaic, liquid, Rahmen mit Label, Logo-Formen | ist eigener Code, muss portiert und gepflegt werden |
|
||||
|
||||
Das trifft sich gut mit der gewünschten Staffelung: **Pro läuft komplett über `qr-code-styling`** - vier Formen, geringer Aufwand, Bibliothek ist schon da. **Business braucht den portierten Showcase-Renderer** für die exotischen Formen. Damit ist der Mehraufwand genau dort, wo auch der höhere Preis ist.
|
||||
|
||||
### 6.1 Free - Farben
|
||||
|
||||
**Datei:** `src/app/(main)/(app)/create/page.tsx`, Zeile 167
|
||||
|
||||
```ts
|
||||
const canCustomizeColors = true; // war: PRO || BUSINESS
|
||||
const canUseShapes = userPlan === 'PRO' || userPlan === 'BUSINESS';
|
||||
const canUseFullDesign = userPlan === 'BUSINESS';
|
||||
const canUseLogo = userPlan === 'PRO' || userPlan === 'BUSINESS';
|
||||
```
|
||||
|
||||
Damit fallen die beiden Zwangsüberschreibungen in Zeile 408-409 weg, die heute für Free-Nutzer stumpf `#000000` und `#FFFFFF` einsetzen. Der Upgrade-Hinweis-Block ab Zeile 972 („Upgrade to PRO to customize colors, add logos, and brand your QR codes") entfällt komplett - er stimmt danach nicht mehr.
|
||||
|
||||
Der Logo-Block ab Zeile 1104 bleibt, wird aber auf `canUseLogo` umgestellt statt auf `canCustomizeColors`, und öffnet künftig das Modal aus Phase 3 statt auf `/pricing` zu verlinken.
|
||||
|
||||
**Der Kontrast-Check bleibt aktiv.** `calculateContrast` gibt es bereits (Zeile 221). Sobald Farben für alle offen sind, wird er wichtiger, nicht unwichtiger - Free-Nutzer sind die Gruppe mit der geringsten Erfahrung darin, was noch scannbar ist. Bei zu geringem Kontrast eine Warnung, kein Verbot: *„Dieser Kontrast ist grenzwertig. Auf gedrucktem Material scannen helle Codes auf hellem Grund oft nicht."*
|
||||
|
||||
### 6.2 Pro - vier Modulformen
|
||||
|
||||
Umsetzung über `qr-code-styling`. Bewusst nur vier, nicht sechs:
|
||||
|
||||
| Auswahl | `dotsOptions.type` | Wofür |
|
||||
|---|---|---|
|
||||
| Eckig (klassisch) | `square` | Standard, höchste Scan-Sicherheit |
|
||||
| Abgerundet | `rounded` | weicher, ohne Lesbarkeit zu kosten |
|
||||
| Punkte | `dots` | modern, deutlich sichtbarer Unterschied |
|
||||
| Fließend | `classy-rounded` | markantester der vier |
|
||||
|
||||
Dazu die Eye-Styles (`cornersSquareOptions`, `cornersDotOptions`) - eckig, abgerundet, Kreis. Die Augen machen optisch mehr aus als die Module selbst und kosten nichts extra, weil dieselbe Bibliothek sie mitbringt.
|
||||
|
||||
Vier Optionen statt aller sechs, weil die Auswahl selbst ein Verkaufsargument ist: Pro fühlt sich vollständig an, Business hat sichtbar mehr. Sechs fast identische Varianten wirken dagegen wie eine lange Liste ohne Unterschied.
|
||||
|
||||
### 6.3 Business - vollständiger Designer
|
||||
|
||||
Portierung des Renderers aus `qr-design-showcase.html` in eine Komponente `src/components/generator/AdvancedQRRenderer.tsx`. Was damit dazukommt:
|
||||
|
||||
- **Modulformen:** diamond, star, hexagon, plus, mosaic, liquid zusätzlich zu den vier aus Pro
|
||||
- **Eye-Frames getrennt:** eckig, abgerundet, Kreis, Blatt, Flower/Squircle, Hexagon
|
||||
- **Eye-Balls getrennt:** eckig, abgerundet, Kreis, Diamant, Stern, Hexagon
|
||||
- **Farbverläufe:** linear und radial statt einfarbig
|
||||
- **Rahmen mit Label:** weiße Karte mit Schatten, Label oben oder unten (z. B. „Speisekarte", „Jetzt bewerten")
|
||||
- **Logo-Formen:** Punkt, Quadrat, Herz, Blitz, WLAN-Symbol
|
||||
- **Fehlerkorrektur-Stufe** frei wählbar (L/M/Q/H)
|
||||
|
||||
**Erweiterungsvorschläge über die Showcase hinaus**, weil beim Business-Kunden die Wiederholbarkeit zählt und nicht die einzelne Spielerei:
|
||||
|
||||
1. **Design-Vorlagen speichern.** Ein einmal gebautes Design als benanntes Preset sichern und auf neue Codes anwenden. Das ist für eine Agentur mit mehreren Kunden der eigentliche Wert - nicht die Sternform, sondern dass Kunde A immer gleich aussieht.
|
||||
2. **Preset auf den Bulk-Flow anwenden.** Ein Preset auswählen und alle 500 Codes eines Uploads tragen es. Das verbindet Phase 2 mit diesem Feature und ist das erste Argument für Business, das nicht nur eine Zahl ist.
|
||||
3. **Print-Vorschau in Originalgröße.** Der Code in 2×2 cm, 4×4 cm und 10×10 cm dargestellt, mit Hinweis ab wann es kritisch wird. Passt zu den Druckangaben, die im Offer Brief bereits als verifizierter Fakt geführt werden.
|
||||
|
||||
### 6.4 Scanbarkeit - der Teil, der nicht optional ist
|
||||
|
||||
Exotische Formen füllen weniger Fläche pro Modul. Star, plus und mosaic reduzieren die Kontrastfläche spürbar, und in Kombination mit einem Logo in der Mitte und einer kleinen Druckgröße kippt die Lesbarkeit. Die Showcase weist bei „H (30%)" schon auf „empfohlen bei Logo" hin - das gehört ins Produkt übernommen, und zwar strenger:
|
||||
|
||||
- Bei star, plus, mosaic oder liquid automatisch auf Fehlerkorrektur **H** hochsetzen und das sichtbar begründen
|
||||
- Bei Logo plus exotischer Form eine Warnung mit Mindestdruckgröße
|
||||
- Ein **Test-Scan-Hinweis** vor dem Download: *„Scanne den Code einmal mit deinem eigenen Handy, bevor du ihn in Druck gibst."*
|
||||
|
||||
Das ist kein Beiwerk, sondern der Proof-Layer aus der Positionierung an einer Stelle, an der es konkret wird. Ein Anbieter, der vor seinem eigenen Feature warnt, wenn es kritisch wird, belegt Belief 5 besser als jede Aussage über Transparenz.
|
||||
|
||||
### 6.5 Was die Umstellung beim Pricing kostet
|
||||
|
||||
Ehrlich gesagt: Farben waren laut Offer Brief §10 einer von genau **drei** Gründen, heute auf Pro zu wechseln (die anderen zwei: mehr als 3 aktive Codes, Device- und Location-Breakdown). Diesen Grund gibt man mit dieser Änderung auf.
|
||||
|
||||
Das ist meiner Einschätzung nach trotzdem richtig, aber aus einem anderen Grund als „großzügiger sein":
|
||||
|
||||
Farben sind kein guter Upgrade-Anlass, weil man sie schon im kostenlosen Zustand *sehen* will, um zu verstehen, ob das Produkt etwas taugt. Ein Free-Nutzer, der nur schwarze Codes bauen kann, hält das Produkt für ein Basiswerkzeug - und bewertet es entsprechend, auch in Vergleichen. Formen sind der bessere Verkäufer, weil der Unterschied größer aussieht und man ihn in der Vorschau zeigen kann, bevor man ihn freischaltet: der Nutzer wählt „Punkte", sieht das Ergebnis live, und erst der Download fragt nach Pro.
|
||||
|
||||
**Folgeänderungen an der Preis-Copy** in `src/i18n/en.json` und `src/app/(main)/(marketing)/pricing/PricingClient.tsx`:
|
||||
|
||||
| Plan | vorher | nachher |
|
||||
|---|---|---|
|
||||
| Free | „Standard QR design templates" | „Deine Farben - Vorder- und Hintergrund frei wählbar" |
|
||||
| Pro | „Custom branding (colors & logos)" | „4 Modulformen, eigene Eye-Styles und dein Logo im Code" |
|
||||
| Business | (kein Design-Punkt) | „Voller Designer: 10 Formen, Verläufe, Rahmen mit Label, speicherbare Design-Vorlagen" |
|
||||
|
||||
Dieselbe Anpassung auf `/custom-qr-code-generator`, dessen Meta-Description aktuell „Put your logo and brand colors into the code itself" sagt - das bleibt korrekt, weil Logo weiterhin Pro ist, aber die Seite sollte künftig zeigen, was auf welcher Stufe geht.
|
||||
|
||||
### 6.6 Keine Datenbankänderung nötig
|
||||
|
||||
`QRCode.style` ist bereits `Json` (siehe `prisma/schema.prisma`). Alle neuen Felder - `dotType`, `eyeFrameType`, `eyeBallType`, `gradient`, `frameLabel`, `logoShape`, `ecLevel` - passen ohne Schemaänderung hinein. Bestehende Codes haben die Felder schlicht nicht und fallen auf die Defaults zurück.
|
||||
|
||||
**Ausnahme:** Wenn die speicherbaren Design-Vorlagen aus 6.3 kommen sollen, braucht es eine eigene Tabelle. SQL dafür steht unten, ist aber optional und kann später nachgezogen werden.
|
||||
|
||||
---
|
||||
|
||||
## SQL - alle nötigen Datenbankänderungen
|
||||
|
||||
Nach der Policy in `CLAUDE.md`: keine Prisma-Migrationen, nur direkte Statements.
|
||||
|
||||
```sql
|
||||
-- Marker für die neuen verhaltensbasierten Retention-Mails
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "limitReachedNudgeSentAt" TIMESTAMP(3);
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "firstScanNudgeSentAt" TIMESTAMP(3);
|
||||
|
||||
-- Index für den Cron: sucht Nutzer mit erstem Scan, die die Mail noch nicht haben
|
||||
CREATE INDEX IF NOT EXISTS "User_firstScanAt_firstScanNudgeSentAt_idx"
|
||||
ON "User" ("firstScanAt", "firstScanNudgeSentAt");
|
||||
|
||||
-- Index für die neue Limit-Query (zählt nur noch ACTIVE)
|
||||
CREATE INDEX IF NOT EXISTS "QRCode_userId_type_status_idx"
|
||||
ON "QRCode" ("userId", "type", "status");
|
||||
```
|
||||
|
||||
Für Phase 6 ist **nichts** davon nötig - `QRCode.style` ist bereits `Json`. Optional, nur falls die speicherbaren Design-Vorlagen aus 6.3 gebaut werden:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS "QRDesignPreset" (
|
||||
"id" TEXT PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL REFERENCES "User"("id") ON DELETE CASCADE,
|
||||
"name" TEXT NOT NULL,
|
||||
"style" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "QRDesignPreset_userId_idx" ON "QRDesignPreset" ("userId");
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "QRDesignPreset_userId_name_key" ON "QRDesignPreset" ("userId", "name");
|
||||
```
|
||||
|
||||
Ausführen über `npm run docker:db` oder:
|
||||
|
||||
```bash
|
||||
docker-compose exec db psql -U postgres -d qrmaster -c 'ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "limitReachedNudgeSentAt" TIMESTAMP(3);'
|
||||
```
|
||||
|
||||
Danach `prisma/schema.prisma` im Block `// Retention email tracking` ergänzen:
|
||||
|
||||
```prisma
|
||||
limitReachedNudgeSentAt DateTime?
|
||||
firstScanNudgeSentAt DateTime?
|
||||
```
|
||||
|
||||
und beim Model `QRCode` den Index:
|
||||
|
||||
```prisma
|
||||
@@index([userId, type, status])
|
||||
```
|
||||
|
||||
Abschließend `npx prisma generate` - kein `migrate`.
|
||||
|
||||
### Prüf-Queries vor dem Deploy
|
||||
|
||||
```sql
|
||||
-- Wie viele Free-Nutzer bekommen durch die ACTIVE-Umstellung Slots frei?
|
||||
SELECT COUNT(DISTINCT u.id)
|
||||
FROM "User" u
|
||||
JOIN "QRCode" q ON q."userId" = u.id
|
||||
WHERE u.plan = 'FREE' AND q.type = 'DYNAMIC' AND q.status = 'PAUSED';
|
||||
|
||||
-- Wie viele Nutzer bekämen die Erster-Scan-Mail beim ersten Cron-Lauf?
|
||||
SELECT COUNT(*) FROM "User"
|
||||
WHERE "firstScanAt" IS NOT NULL AND "firstScanNudgeSentAt" IS NULL;
|
||||
```
|
||||
|
||||
Die zweite Zahl ist wichtig: Beim ersten Lauf würde die neue Mail an **alle** Bestandsnutzer mit Scan-Historie gehen - auch an solche, deren erster Scan Monate zurückliegt. Das wäre kein Anlass mehr, sondern Spam. Der Cron braucht deshalb ein Zeitfenster, etwa `firstScanAt > now() - interval '7 days'`, oder man setzt die Spalte bei Bestandsnutzern einmalig vor:
|
||||
|
||||
```sql
|
||||
UPDATE "User" SET "firstScanNudgeSentAt" = now()
|
||||
WHERE "firstScanAt" IS NOT NULL AND "firstScanAt" < now() - interval '7 days';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
| # | Was | Aufwand | DB |
|
||||
|---|---|---|---|
|
||||
| 1 | Limit-Query auf ACTIVE, beide Endpoints | klein | Index |
|
||||
| 2 | Limit-Modal ohne Redirect | mittel | nein |
|
||||
| 3 | Direkt-Checkout plus `returnPath` und localStorage-Wiederherstellung | mittel | nein |
|
||||
| 4 | `bulk-creation`: Fehlerbehandlung und Ergebnis-Panel | klein | nein |
|
||||
| 5 | Popup-Copy plus `variant` pro Tool-Seite | klein | nein |
|
||||
| 6 | Retention: Trigger umstellen, Tabellen-Zeile raus, Betreff Tag 3 | klein | 1 Spalte |
|
||||
| 7 | Erster-Scan-Mail | mittel | 1 Spalte |
|
||||
| 8 | Tag-30 auf Scan-Daten, unbelegten Satz klären | klein | nein |
|
||||
| 9 | Farben ab Free freischalten, Pricing-Copy nachziehen | klein | nein |
|
||||
| 10 | Pro-Formen über `qr-code-styling`, Renderer-Tausch in `/create` | mittel | nein |
|
||||
| 11 | Business-Designer, Showcase-Renderer portieren | groß | nein |
|
||||
| 12 | Design-Vorlagen speichern und auf Bulk anwenden | mittel | Tabelle |
|
||||
|
||||
1 bis 3 gehören zusammen und sollten gemeinsam live gehen. 4 und 5 sind unabhängig und können jederzeit dazwischen. 6 bis 8 brauchen die SQL-Statements und den einmaligen `UPDATE` für Bestandsnutzer.
|
||||
|
||||
**9 ist der schnellste Gewinn im ganzen Dokument** - eine Zeile (`canCustomizeColors = true`) plus Copy-Anpassung, und das Produkt wirkt für jeden neuen Nutzer sofort weniger nach Basiswerkzeug. Sie sollte nicht auf 10 warten.
|
||||
|
||||
10 ist der Renderer-Tausch und damit der Punkt, an dem `/create` einmal gründlich getestet werden muss - Vorschau, Download PNG, Download SVG, Logo-Overlay und die Frame-Optionen hängen alle daran.
|
||||
|
||||
11 und 12 sind der eigentliche Business-Ausbau. 12 ist dabei wertvoller als 11: eine Agentur kauft nicht die Sternform, sondern dass Kunde A auf 500 Codes gleich aussieht.
|
||||
169
RETENTION_UND_CRO_ANALYSE_2026-07-27.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Retention-Mails & CRO-Momente - Analyse
|
||||
|
||||
Stand 27. Juli 2026. Grundlage: `src/lib/email.ts`, `src/app/(main)/api/cron/retention-emails/route.ts`, `src/components/marketing/PostDownloadPopup.tsx`, `src/app/(main)/(app)/create/page.tsx` sowie Offer Brief §10 und der Necessary-Beliefs-Doc.
|
||||
|
||||
---
|
||||
|
||||
## Teil 1 - Der 3/3-Moment: es gibt kein Popup
|
||||
|
||||
Das Wichtigste zuerst, weil es kein Copy-Problem ist.
|
||||
|
||||
Wenn ein Free-Nutzer den vierten dynamischen Code anlegen will, passiert laut `create/page.tsx` Zeile 456-458 Folgendes:
|
||||
|
||||
```
|
||||
if (response.status === 403 && responseData.error === 'Limit reached') {
|
||||
showToast(responseData.message || 'You have reached your plan limit.', 'error');
|
||||
router.push('/pricing?reason=limit_reached');
|
||||
}
|
||||
```
|
||||
|
||||
Drei Dinge gehen hier schief, in aufsteigender Schwere:
|
||||
|
||||
**1. Roter Fehler-Toast.** Der Nutzer hat nichts falsch gemacht. Er hat das Produkt so benutzt, wie es gedacht ist, und bekommt dafür eine Fehlermeldung im gleichen Stil wie eine kaputte URL. Das ist keine Upgrade-Aufforderung, das ist eine Zurechtweisung.
|
||||
|
||||
**2. Die Arbeit ist weg.** Der Nutzer hat Ziel-URL, Typ, Farben, vielleicht ein Logo eingegeben. Beim Redirect auf `/pricing` ist all das verloren. Wer danach upgradet, muss von vorne anfangen. Das ist der teuerste denkbare Zeitpunkt für Datenverlust: exakt in dem Moment, in dem die Kaufabsicht am höchsten ist.
|
||||
|
||||
**3. Der Redirect ist ein Rauswurf.** Der Nutzer wollte einen QR-Code bauen und landet auf einer Preistabelle ohne Bezug zu dem, was er gerade tat. Kein Weg zurück, kein „so sieht dein Code aus, er ist nur noch nicht gespeichert".
|
||||
|
||||
Aus dem Offer Brief §7: der Engpass ist **Perceived Likelihood**, nicht Preis und nicht Effort. Dieser Ablauf senkt genau die - er zeigt, dass das Produkt in einem kritischen Moment die eigene Arbeit wegwirft.
|
||||
|
||||
### Was stattdessen hin muss
|
||||
|
||||
Ein Modal, das **über** dem fertigen Code aufgeht, nicht statt ihm. Der Code bleibt im Hintergrund sichtbar. Vorschlag:
|
||||
|
||||
> **Dein vierter Code ist fertig. Er braucht nur noch einen Platz.**
|
||||
>
|
||||
> Du nutzt alle 3 dynamischen Codes des Free-Plans. Dieser hier ist gebaut und wartet - du kannst ihn behalten oder einen bestehenden freigeben.
|
||||
>
|
||||
> [ Diesen Code mit Pro speichern - 9 € / Monat ]
|
||||
> [ Einen bestehenden Code ersetzen ]
|
||||
> [ Stattdessen als statischen Code herunterladen - kostenlos, läuft nie ab ]
|
||||
>
|
||||
> Deine 3 aktiven Codes laufen weiter, egal wie du dich entscheidest.
|
||||
|
||||
Warum diese drei Optionen:
|
||||
|
||||
- **Option 1** ist der Kauf, aber formuliert als „diesen konkreten Code retten", nicht als „Plan kaufen". Der Nutzer kauft eine Sache, die er gerade in der Hand hat.
|
||||
- **Option 2** ist die ehrliche Alternative innerhalb des Free-Plans. Sie kostet ein paar Conversions und kauft dafür Glaubwürdigkeit - Belief 5 aus dem Necessary-Beliefs-Doc: der sicherste Anbieter ist der, der offenlegt, was er kann und was nicht.
|
||||
- **Option 3** ist der Ausweg ohne Verlust. Ein statischer Code löst das Problem des Nutzers zu 60 % und kostet nichts, und die Zusage „läuft nie ab" ist verifiziert. Wer sie nimmt, ist nicht verloren - er hat gerade erlebt, dass das Produkt ihm nichts wegnimmt.
|
||||
|
||||
Der letzte Satz ist Risk Reversal an genau der Stelle, wo die Kategorie ihren schlechtesten Ruf hat: das Abschalten bereits gedruckter Codes.
|
||||
|
||||
---
|
||||
|
||||
## Teil 2 - Die Retention-Mails
|
||||
|
||||
Die Copy ist besser als der Durchschnitt. Absenderpersona ist da (Timo, Founder, Reply geht an einen Menschen), keine Ausrufezeichen-Orgie, konkrete Zahlen. Was fehlt, liegt eine Ebene tiefer.
|
||||
|
||||
### Mail 1 - Tag 3, Aktivierung
|
||||
|
||||
Betreff: `You haven't made one yet`
|
||||
Headline: `You haven't made one yet, {Name}. 3 days since signup · 0 QR codes created`
|
||||
|
||||
Das ist die stärkste der drei. Der Betreff ist ein Pattern Interrupt in einem Posteingang voller „Tips & Tricks", und die Zeile `Your 3 free dynamic QR codes are still there. Unused.` ist Pointing statt Talking.
|
||||
|
||||
Zwei Schwächen:
|
||||
|
||||
**Der Betreff kann als Vorwurf gelesen werden.** „You haven't made one yet" mit Doppeldeutigkeit - ist das eine Beobachtung oder eine Rüge? Die Headline im Body löst es auf, der Betreff allein nicht. Alternative, gleiche Länge, ohne Vorwurfsrisiko: `Your 3 free codes are still sitting there`.
|
||||
|
||||
**Es fehlt der Anlass.** Die Mail erklärt, *wie* man einen Code macht (drei Schritte), aber nicht, *wofür der Empfänger sich damals angemeldet hat*. Wenn beim Signup die Quelle bekannt ist - kam er über `/tools/google-review-qr-code`, über die Restaurant-Seite, über Bulk? -, dann gehört Schritt 01 personalisiert. „Paste your URL" ist die generischste denkbare Aufforderung an jemanden, der wegen Google-Reviews kam.
|
||||
|
||||
### Mail 2 - Tag 7, Upgrade
|
||||
|
||||
Betreff: `You're at the free limit` bzw. `You're 2 away from the free limit`
|
||||
Headline: `{n} of 3 free codes used, {Name}.`
|
||||
|
||||
Hier ist der Trigger das Problem, nicht die Worte.
|
||||
|
||||
**Die Mail feuert am Tag 7, unabhängig vom Verhalten.** Die Bedingung im Cron ist `createdAt < 7 Tage` und `qrCount > 0` und `plan = FREE`. Ein Nutzer mit **einem** Code bekommt eine Mail mit dem Betreff „You're 2 away from the free limit" - über ein Limit, das ihn nicht drückt und vielleicht nie drücken wird. Das ist eine Upgrade-Aufforderung an jemanden ohne Schmerz. Solche Mails trainieren Empfänger darauf, den Absender zu ignorieren, und der Absender ist hier der Gründer persönlich - das ist teurer Kredit, den man nicht für eine Fehlzündung ausgeben sollte.
|
||||
|
||||
**Empfehlung: den Trigger vom Kalender aufs Verhalten umstellen.**
|
||||
|
||||
| Zustand | Auslöser | Inhalt |
|
||||
|---|---|---|
|
||||
| 3 von 3 belegt | sobald erreicht, nicht Tag 7 | Limit-Mail wie jetzt, aber sofort im Moment der Relevanz |
|
||||
| 1-2 von 3 belegt | Tag 7 | keine Upgrade-Mail. Stattdessen: was die Scan-Daten des ersten Codes zeigen |
|
||||
| 3 von 3 und erster Code hat Scans | 3 Tage nach Limit | Upgrade mit den eigenen Zahlen des Nutzers als Beweis |
|
||||
|
||||
Das dritte Szenario ist die eigentlich fehlende Mail. Nichts überzeugt einen Marketing-Manager so wie sein eigener erster Datenpunkt: „Dein Code auf dem Flyer hatte 47 Scans, die meisten dienstags zwischen 11 und 14 Uhr." Das ist Pointing in Reinform, und es kostet nichts außer der Query.
|
||||
|
||||
**Und ein inhaltlicher Fehler.** Die Vergleichstabelle listet `CSV export: Free ✓ / Pro ✓`. Eine Zeile, in der beide Spalten identisch sind, ist in einer Upgrade-Tabelle wertlos - sie füllt Platz und verwässert die drei Zeilen, die tatsächlich einen Unterschied zeigen. Streichen.
|
||||
|
||||
### Mail 3 - Tag 30
|
||||
|
||||
Betreff: `{Name}, a month of QR codes - one upgrade worth making`
|
||||
Argument: Pro-Nutzer bereuen, das Branding nicht früher hinzugefügt zu haben.
|
||||
|
||||
**Das Argument ist gut gebaut und nicht belegt.** Der Satz `The one thing I hear most from Pro users who switched after a few weeks: they wish they'd added their brand sooner` behauptet ein Muster aus Kundengesprächen. Wenn diese Gespräche stattgefunden haben: rein damit, am besten mit einem echten Zitat. Wenn nicht, ist das ein erfundenes Testimonial in indirekter Rede und verstößt gegen die Beweisregel aus dem Product Context („keine erfundenen Testimonials"). Bei einer Zielgruppe, die Bewertungsportale liest, ist das die teuerste Art von Satz.
|
||||
|
||||
**Der Aufhänger ist außerdem der schwächere von zwei verfügbaren.** Branding ist ein Ästhetik-Argument. Nach 30 Tagen mit mehreren Codes hat der Nutzer etwas viel Stärkeres in der Hand: Scan-Daten. Der Tag-30-Anlass sollte der erste echte Report sein, nicht ein Logo-Feature. „Deine Codes wurden diesen Monat X-mal gescannt. Hier ist, was du noch nicht sehen kannst" - und dann Device- und Location-Breakdown als das, was Pro freischaltet. Das ist derselbe Kaufgrund, aber hergeleitet aus dem, was der Nutzer selbst erlebt hat.
|
||||
|
||||
Das deckt sich mit Offer Brief §10, Option 1: auf Analytics-Tiefe metern statt auf Code-Anzahl. Diese Mail wäre die erste Stelle, an der man das testen kann, ohne das Pricing anzufassen.
|
||||
|
||||
### Übergreifend: es fehlt die Mail nach dem ersten Scan
|
||||
|
||||
Die Sequenz ist Tag 3, Tag 7, Tag 30 - drei Kalendertermine. Der wichtigste Moment im Lebenszyklus dieses Produkts kommt in keinem davon vor: **der erste Scan eines Codes.** Das ist der Augenblick, in dem aus einem Tool eine Messung wird und in dem das Versprechen der Positionierung zum ersten Mal einlöst. Eine Mail „Dein Code wurde gerade zum ersten Mal gescannt" hat einen Anlass, der nicht konstruiert ist, und braucht überhaupt kein Verkaufsargument.
|
||||
|
||||
---
|
||||
|
||||
## Teil 3 - Das Post-Download-Popup auf den Tool-Seiten
|
||||
|
||||
Aktuell:
|
||||
|
||||
> **Your QR code is downloading!**
|
||||
> Want to make it smarter - for free?
|
||||
>
|
||||
> - Edit the link anytime - QR stays the same
|
||||
> - See who scans, when & where
|
||||
> - Custom colors, logo & frames
|
||||
> - Free plan included - upgrade anytime for more
|
||||
>
|
||||
> [ Create Free Account ] / *No thanks, keep it static*
|
||||
|
||||
### Was funktioniert
|
||||
|
||||
Das Timing ist richtig: nach dem Download, nicht davor. Der Nutzer hat bekommen, wofür er kam - das Popup nimmt ihm nichts weg. Und `No thanks, keep it static` ist eine ehrliche Ablehn-Option ohne Beschämung, was in dieser Kategorie selten ist. Nicht anfassen.
|
||||
|
||||
### Was nicht funktioniert
|
||||
|
||||
**Die Headline sagt nichts.** `Your QR code is downloading!` ist eine Statusmeldung. Der Nutzer sieht den Download in seinem Browser, er braucht keine Bestätigung. Diese Zeile ist die prominenteste Fläche des Popups und verbraucht sie für eine Information, die der Nutzer schon hat.
|
||||
|
||||
**Der Kernnutzen ist die dritte Zeile in einer Bullet-Liste.** `Edit the link anytime - QR stays the same` ist das gesamte Argument des Produkts, versteckt zwischen einem Icon und drei gleichgewichtigen Geschwistern. In einer Liste von vier gleich formatierten Punkten ist keiner davon wichtig.
|
||||
|
||||
**Es fehlt der Anlass in diesem Moment.** Der Nutzer hat gerade einen **statischen** Code heruntergeladen. Der interessante Satz ist nicht „mach ihn schlauer", sondern die Konsequenz dessen, was er gerade getan hat: dieser Code ist ab jetzt festgelegt. Das ist kein Vorwurf und keine Drohung, das ist eine Tatsache über die Datei in seinem Download-Ordner - und exakt Belief 1 aus dem Necessary-Beliefs-Doc.
|
||||
|
||||
**Es ist auf allen Tool-Seiten identisch.** Wer auf `/tools/google-review-qr-code` war, will Bewertungen. Wer auf `/tools/wifi-qr-code` war, will Gäste ins WLAN. Ein Popup, das beiden dasselbe sagt, spricht keinem von beiden.
|
||||
|
||||
### Vorschlag
|
||||
|
||||
> **Dieser Code zeigt jetzt für immer auf diese URL.**
|
||||
>
|
||||
> Das ist bei einem dauerhaften Link genau richtig. Falls sich das Ziel je ändert, brauchst du einen neuen Code und neues Druckmaterial.
|
||||
>
|
||||
> Ein kostenloses Konto gibt dir 3 dynamische Codes: gleiches Bild, Ziel jederzeit änderbar, jeder Scan gezählt.
|
||||
>
|
||||
> [ Kostenloses Konto anlegen - keine Karte ]
|
||||
> *Nein danke, statisch reicht*
|
||||
|
||||
Struktur dahinter: P.I.G.-Opening auf eine Tatsache statt auf ein Feature, dann die Zugeständnis-Zeile („bei einem dauerhaften Link genau richtig") - das ist Offer Brief §9, die Konzession als Glaubwürdigkeits-Move -, dann erst der Mechanismus. Die Bullet-Liste entfällt komplett; drei gleichrangige Vorteile sind schwächer als ein Satz, der den einen benennt.
|
||||
|
||||
Pro Tool-Seite variiert nur die erste Zeile:
|
||||
|
||||
| Tool | Erste Zeile |
|
||||
|---|---|
|
||||
| Google Review | Dieser Code zeigt jetzt für immer auf dieses Google-Profil. |
|
||||
| WiFi | Dieser Code enthält jetzt dauerhaft dieses WLAN-Passwort. |
|
||||
| vCard | Dieser Code enthält jetzt dauerhaft diese Kontaktdaten. |
|
||||
| URL / Standard | Dieser Code zeigt jetzt für immer auf diese URL. |
|
||||
|
||||
Die WiFi-Variante ist die stärkste der vier, weil sie einen Umstand benennt, den fast niemand vorher bedenkt: das Passwort steht in der gedruckten Datei. Wer es ändert, hat wertloses Material.
|
||||
|
||||
---
|
||||
|
||||
## Reihenfolge nach Wirkung pro Aufwand
|
||||
|
||||
1. **Das 3/3-Modal bauen.** Momentan verliert das Produkt an der Stelle mit der höchsten Kaufabsicht die Arbeit des Nutzers. Das ist der einzige Punkt hier, der auch Entwicklungsarbeit ist und nicht nur Text.
|
||||
2. **Trigger der Tag-7-Mail auf Verhalten umstellen.** Query-Änderung im Cron, keine neue Infrastruktur.
|
||||
3. **Popup-Copy und Headline umstellen**, mit Variante pro Tool-Seite.
|
||||
4. **Die „erster Scan"-Mail** ergänzen - der einzige Anlass in der ganzen Sequenz, der nicht vom Kalender kommt.
|
||||
5. **Tag 30 auf Scan-Daten umbauen** statt Branding, und den unbelegten Pro-Nutzer-Satz entweder belegen oder streichen.
|
||||
116
design-variants/bakeoff.html
Normal file
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>QR Master — Design Bake-off</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #141414; --panel: #1c1c1c; --line: #333333; --ink: #ECECEC; --muted: #8A8A8A;
|
||||
--accent: #E8622C; --mono: "IBM Plex Mono","JetBrains Mono",ui-monospace,Consolas,monospace;
|
||||
}
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
html,body { height:100%; background:var(--bg); color:var(--ink); font-family:var(--mono); }
|
||||
.topbar {
|
||||
display:flex; align-items:center; justify-content:space-between; gap:20px;
|
||||
padding:14px 20px; border-bottom:1px solid var(--line); flex-wrap:wrap;
|
||||
}
|
||||
.topbar-left { display:flex; align-items:center; gap:14px; flex-wrap:wrap; }
|
||||
.topbar-label { font-size:10.5px; letter-spacing:0.1em; text-transform:uppercase; color:var(--muted); line-height:1.3; }
|
||||
.collection-btns { display:flex; gap:8px; flex-wrap:wrap; }
|
||||
.cbtn {
|
||||
font-family:var(--mono); font-size:11px; font-weight:600; letter-spacing:0.06em; text-transform:uppercase;
|
||||
background:transparent; border:1px solid var(--line); color:var(--ink);
|
||||
padding:9px 16px; border-radius:3px; cursor:pointer; transition:all .15s ease;
|
||||
}
|
||||
.cbtn:hover { border-color:var(--accent); }
|
||||
.cbtn.active { background:var(--accent); border-color:var(--accent); color:#141414; }
|
||||
.topbar-hint { font-size:10.5px; letter-spacing:0.06em; color:var(--muted); text-transform:uppercase; text-align:right; }
|
||||
.topbar-hint b { color:var(--ink); }
|
||||
|
||||
.compare { display:grid; grid-template-columns:1fr 1fr; height:calc(100vh - 60px); }
|
||||
.pane { display:flex; flex-direction:column; border-right:1px solid var(--line); min-width:0; }
|
||||
.pane:last-child { border-right:none; }
|
||||
.pane-head {
|
||||
display:flex; align-items:center; justify-content:space-between; gap:12px;
|
||||
padding:10px 18px; border-bottom:1px solid var(--line); background:var(--panel);
|
||||
}
|
||||
.pane-head .name { font-size:12px; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; }
|
||||
.pane-head .name.left-accent { color:var(--accent); }
|
||||
.pane-head .name.right-accent { color:#5FA8FF; }
|
||||
.pane-head .tag { font-size:10px; color:var(--muted); letter-spacing:0.08em; text-transform:uppercase; }
|
||||
.pane iframe { flex:1; width:100%; border:none; background:#fff; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.compare { grid-template-columns:1fr; height:auto; }
|
||||
.pane { border-right:none; border-bottom:1px solid var(--line); height:70vh; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="topbar">
|
||||
<div class="topbar-left">
|
||||
<span class="topbar-label">Design<br>Collection:</span>
|
||||
<div class="collection-btns" id="collectionBtns">
|
||||
<button class="cbtn" data-slug="blueprint">Print-Tech</button>
|
||||
<button class="cbtn" data-slug="dither-mono">Dither Mono</button>
|
||||
<button class="cbtn" data-slug="vast-quiet">Vast Quiet</button>
|
||||
<button class="cbtn" data-slug="data-texture">Data-Texture</button>
|
||||
<button class="cbtn" data-slug="classical">Classical</button>
|
||||
<button class="cbtn active" data-slug="quiet-mono">Quiet Mono</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topbar-hint">Left = <b>Light</b> · Right = <b>Dark</b> · Keys 1–6</div>
|
||||
</div>
|
||||
|
||||
<div class="compare">
|
||||
<div class="pane">
|
||||
<div class="pane-head"><span class="name left-accent" id="leftName">Classical</span><span class="tag">Light</span></div>
|
||||
<iframe id="leftFrame" title="Light mode preview"></iframe>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<div class="pane-head"><span class="name right-accent" id="rightName">Classical</span><span class="tag">Dark</span></div>
|
||||
<iframe id="rightFrame" title="Dark mode preview"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var LABELS = {
|
||||
"blueprint": "Print-Tech",
|
||||
"dither-mono": "Dither Mono",
|
||||
"vast-quiet": "Vast Quiet",
|
||||
"data-texture": "Data-Texture",
|
||||
"classical": "Classical",
|
||||
"quiet-mono": "Quiet Mono"
|
||||
};
|
||||
var ORDER = ["blueprint", "dither-mono", "vast-quiet", "data-texture", "classical", "quiet-mono"];
|
||||
var btns = document.querySelectorAll('.cbtn');
|
||||
var leftFrame = document.getElementById('leftFrame');
|
||||
var rightFrame = document.getElementById('rightFrame');
|
||||
var leftName = document.getElementById('leftName');
|
||||
var rightName = document.getElementById('rightName');
|
||||
|
||||
function setCollection(slug) {
|
||||
btns.forEach(function (b) { b.classList.toggle('active', b.getAttribute('data-slug') === slug); });
|
||||
leftFrame.src = 'landing-' + slug + '.html';
|
||||
rightFrame.src = 'landing-' + slug + '.html#dark';
|
||||
leftName.textContent = LABELS[slug];
|
||||
rightName.textContent = LABELS[slug];
|
||||
}
|
||||
|
||||
btns.forEach(function (b) {
|
||||
b.addEventListener('click', function () { setCollection(b.getAttribute('data-slug')); });
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
var idx = parseInt(e.key, 10) - 1;
|
||||
if (idx >= 0 && idx < ORDER.length) setCollection(ORDER[idx]);
|
||||
});
|
||||
|
||||
setCollection('quiet-mono');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2080
design-variants/landing-blueprint.html
Normal file
2088
design-variants/landing-classical.html
Normal file
2092
design-variants/landing-data-texture.html
Normal file
2093
design-variants/landing-dither-mono.html
Normal file
2319
design-variants/landing-quiet-mono.html
Normal file
2085
design-variants/landing-vast-quiet.html
Normal file
2082
landing-design-v2.html
Normal file
@@ -36,6 +36,8 @@ model User {
|
||||
activationNudgeSentAt DateTime?
|
||||
upgradeNudgeSentAt DateTime?
|
||||
thirtyDayNudgeSentAt DateTime?
|
||||
limitReachedNudgeSentAt DateTime?
|
||||
firstScanNudgeSentAt DateTime?
|
||||
|
||||
// RevOps attribution
|
||||
signupSource String?
|
||||
@@ -77,6 +79,7 @@ model User {
|
||||
lastScoredAt DateTime?
|
||||
|
||||
qrCodes QRCode[]
|
||||
designPresets QRDesignPreset[]
|
||||
integrations Integration[]
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
@@ -142,6 +145,7 @@ model QRCode {
|
||||
scans QRScan[]
|
||||
|
||||
@@index([userId, createdAt])
|
||||
@@index([userId, type, status])
|
||||
}
|
||||
|
||||
enum QRType {
|
||||
@@ -222,6 +226,22 @@ model SocialAsset {
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
/// Saved design presets. The point for an agency is not the star shape, it is
|
||||
/// that client A looks identical across 500 codes.
|
||||
model QRDesignPreset {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
name String
|
||||
style Json
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, name])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model UserLifecycleLog {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
|
||||
|
Before Width: | Height: | Size: 933 B After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 13 KiB |
BIN
public/favicon_sticker.png
Normal file
|
After Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 179 KiB |
1044
qr-design-showcase.html
Normal file
1782
qr-master-design.html
Normal file
165
reports/seo-improver/2026-07-26/dataforseo-drei-domains.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# DataForSEO SEO-Analyse – QRMaster, GreenLens Pro, Entscheidomat
|
||||
|
||||
**Datum:** 2026-07-26
|
||||
**Quelle:** DataForSEO API v3, Google Labs / Ranked Keywords / Competitors Domain / Domain Intersection
|
||||
|
||||
## Untersuchungsumfang
|
||||
|
||||
| Domain | Markt / Sprache | Ranked Keywords | Wettbewerberdaten |
|
||||
|---|---|---:|---|
|
||||
| qrmaster.net | Deutschland / Deutsch | 3 | erfolgreich |
|
||||
| qrmaster.net | USA / Englisch | 100 | erfolgreich |
|
||||
| greenlenspro.com | USA / Englisch | 100 | erfolgreich |
|
||||
| entscheidomat.com | Deutschland / Deutsch | 0 | keine Rankingdaten |
|
||||
|
||||
Die Werte sind eine DataForSEO-Momentaufnahme und kein Ersatz für Google Search Console. Rankingdaten wurden als organische Google-Daten abgerufen.
|
||||
|
||||
## 1. QRMaster
|
||||
|
||||
### Deutschland / Deutsch
|
||||
|
||||
Aktuell gemessene Rankings:
|
||||
|
||||
- `qr codes tracking` – Position 97, Suchvolumen 210, `/qr-code-tracking`
|
||||
- `qr-code karten` – Position 52, Suchvolumen 110, `/de/qr-codes-fuer-visitenkarten`
|
||||
- `dynamic qr code generator` – Position 38, Suchvolumen 90, `/dynamic-qr-code-generator`
|
||||
|
||||
Wichtig: Die Rankingdaten enthalten kaum deutschsprachige Kernbegriffe. Das spricht eher für ein Content-/Lokalisierungsproblem als für ein reines Technikproblem.
|
||||
|
||||
### USA / Englisch
|
||||
|
||||
Stärkste aktuell gefundene Themen:
|
||||
|
||||
- `generate a barcode free` – Volumen 40.500, Position 98, `/tools/barcode-generator`
|
||||
- `visiting card with qr code` – 12.100, Position 107, `/use-cases/business-card-qr-codes`
|
||||
- `barcode design online` – 9.900, Position 104, `/tools/barcode-generator`
|
||||
- `create a qr code for a url` / `create qr code from url` – je 8.100, Position 93/91
|
||||
- `qr code generator for url` – 8.100, Position 68
|
||||
- `bitly qr` – 2.900, Position 28, `/alternatives/bitly`
|
||||
- `flowcode qr` – 2.400, Position 31, `/alternatives/flowcode`
|
||||
- `vcard qr generator` – 1.300, Position 31, `/tools/vcard-qr-code`
|
||||
- `vcard qr code generator` – 1.300, Position 89
|
||||
|
||||
### Wichtigste Wettbewerber
|
||||
|
||||
**Deutschland:** qrcode-generator.de, me-qr.com, pageloot.com, qrcodechimp.com, qrcode-tiger.com, qrcodekit.com, the-qrcode-generator.com, qrfy.com, qrcode-monkey.com.
|
||||
|
||||
**USA:** qr-code-generator.com, me-qr.com, qrcodechimp.com, the-qrcode-generator.com, qrstuff.com, uniqode.com, scanova.io, viralqr.com.
|
||||
|
||||
### Keyword-Gaps / Chancen
|
||||
|
||||
Die Domain-Intersection-Abfragen zeigen, dass Wettbewerber unter anderem für folgende relevante Themen ranken, während QRMaster in den abgefragten SERPs nicht gleichzeitig auftaucht:
|
||||
|
||||
- `qr code generator`, `generate a qr code`, `qr code builder`
|
||||
- `qr codes erstellen`, `qr-code erstellen`, `qr-code erzeugen`
|
||||
- `qr code scanner`, `qr code scannen`
|
||||
- `qr codes erstellen kostenlos`
|
||||
- `dynamic qr code`, `dynamic qr code generator`
|
||||
- `qr code tracking`, `qr code analytics`
|
||||
- `vcard qr code generator`
|
||||
|
||||
### Empfehlung
|
||||
|
||||
1. Deutsche, lokalisierte Toolseiten für `QR-Code-Generator`, `dynamischer QR-Code`, `QR-Code-Tracking`, `QR-Code-Scanner` und `QR-Code-Visitenkarte` priorisieren.
|
||||
2. Die US-Startseite nicht primär über Barcode- und Wettbewerberbegriffe wachsen lassen; stattdessen klare englische Landingpages für `qr code generator`, `dynamic qr code generator`, `qr code tracking`, `vcard qr code generator` und `bulk qr code generator` stärken.
|
||||
3. Jede Toolseite mit direkter Definition, Use Cases, Funktionsvergleich statisch/dynamisch, Tracking-Erklärung, FAQ und starken internen Links ausstatten.
|
||||
4. Die bereits auffälligen `/alternatives/bitly`- und `/alternatives/flowcode`-Seiten zu belastbaren Vergleichsseiten mit echten Produktunterschieden ausbauen.
|
||||
|
||||
## 2. GreenLens Pro
|
||||
|
||||
### USA / Englisch
|
||||
|
||||
GreenLens rankt bereits für ein relevantes Pflanzenfoto-/Identifikationsthema, überwiegend über eine Seite:
|
||||
|
||||
- `google lens plant identification app` – Volumen 8.100, Position 32, `/plant-identifier-app`
|
||||
- `identify a plant by picture` – 8.100, Position 79, `/identify-plant-photo`
|
||||
- `identify plant from photo` – 8.100, Position 80
|
||||
- `identify plants by picture` – 8.100, Position 55
|
||||
- `plant id from photo` – 8.100, Position 55
|
||||
- `plant identifier with photo` – 8.100, Position 51
|
||||
- `flower picture identifier` – 1.300, Position 74
|
||||
- `what plant is this by picture` – 1.000, Position 93
|
||||
- `plant finder by photo` – 590, Position 58
|
||||
|
||||
### Wichtigste Wettbewerber
|
||||
|
||||
picturethisai.com, plantnet.org, plant.id, plantsnap.com, apple.com, myplantin.com, lensapp.io sowie Google, YouTube und Reddit als SERP-/Marken-Ökosystem.
|
||||
|
||||
### Keyword-Gaps / Chancen
|
||||
|
||||
Die Wettbewerber decken unter anderem ab:
|
||||
|
||||
- `plant identification`, `plant identifier`, `plant id`
|
||||
- `what type of plant is this`
|
||||
- `flower identifier by picture`
|
||||
- `plant disease identifier`
|
||||
- konkrete Pflanzen- und Symptombegriffe
|
||||
|
||||
Die Domain-Intersection-Daten enthalten außerdem viele botanische Informationsbegriffe. Diese sollten nicht ungefiltert als SEO-Ziele übernommen werden: Für GreenLens sind Identifikation, Diagnose und nächste Pflegeschritte wertvoller als reine Pflanzenlexikon-Queries.
|
||||
|
||||
### Empfehlung
|
||||
|
||||
1. `/identify-plant-photo` als zentrale Money-/Tool-Seite auf Positionen 1–20 entwickeln: klare Antwort, Upload-/Scan-Nutzen, Beispiele, FAQ, App-CTA.
|
||||
2. Separat clusterbare Seiten für `plant identifier`, `flower identifier`, `plant disease identifier`, `brown leaves`, `yellow leaves`, `overwatering` und `root rot` aufbauen.
|
||||
3. Jede Diagnose-Seite muss sichtbar den nächsten Handlungsschritt erklären; nicht nur Pflanzenwissen liefern.
|
||||
4. Interne Links zwischen Identifikation → Diagnose → Pflegeplan → App setzen.
|
||||
5. Google-Lens-Vergleich als Trust-/Alternative-Seite nutzen, aber nicht die gesamte Informationsarchitektur auf Wettbewerberbegriffe stützen.
|
||||
|
||||
## 3. Entscheidomat
|
||||
|
||||
### Deutschland / Deutsch
|
||||
|
||||
DataForSEO meldet aktuell **keine Ranked Keywords** für `entscheidomat.com` im deutschen Google-Markt. Das bedeutet nicht automatisch, dass die Website technisch nicht indexiert ist; es bedeutet, dass DataForSEO in diesem Markt keine verwertbaren Ranking-Keywords geliefert hat.
|
||||
|
||||
### Wettbewerbs- und Gap-Signale
|
||||
|
||||
Vergleichsdaten mit wheelofnames.com, random.org und zufallsgenerator.de zeigen relevante Suchthemen:
|
||||
|
||||
- `glücksrad` – Volumen 246.000
|
||||
- `zufallsgenerator` – 165.000
|
||||
- `würfel` – 90.500
|
||||
- `google drehscheibe` – 40.500
|
||||
- `wheel of names` – 33.100
|
||||
- `spin the wheel` – 33.100
|
||||
- `coin flip` – 40.500, englischer/Internationaler Suchbegriff
|
||||
|
||||
Ein Teil der Daten von random.org ist für Entscheidomat nicht passend, zum Beispiel Passwortgenerator- und englische Random-Number-Begriffe. Diese wurden als Kontamination verworfen.
|
||||
|
||||
### Empfehlung
|
||||
|
||||
1. Jede der sieben Funktionen als eigenständige, crawlbare SEO-Landingpage führen: `glücksrad`, `zufallszahl-generator`, `würfel-online`, `münze-werfen`, `ja-nein-generator`, `namen-auslosen`, `magic-8-ball`.
|
||||
2. Pro Seite einen sichtbaren Definition-Block direkt unter dem H1 ergänzen.
|
||||
3. Suchintention und Tool direkt verbinden: sofort nutzbares Tool plus Erklärung, Beispiele, FAQ und interne Links.
|
||||
4. Die Startseite auf `Zufallsgenerator` und `Entscheidungshilfe` fokussieren; nicht alle sieben Begriffe gleich stark in Title/H1 stapeln.
|
||||
5. Für `glücksrad`, `zufallsgenerator` und `würfel online` zuerst spezifische SEO- und UX-Landingpages optimieren, weil dort die größte erkennbare Nachfrage liegt.
|
||||
|
||||
## Priorisierte Maßnahmen über alle drei Projekte
|
||||
|
||||
### Priorität 1 – sofort
|
||||
|
||||
- QRMaster: englische und deutsche Kern-Toolseiten auf `qr code generator`, `dynamic qr code`, `qr code tracking` und `QR-Code erstellen` ausrichten.
|
||||
- GreenLens: `identify-plant-photo` als zentrale Conversion-/SEO-Seite ausbauen.
|
||||
- Entscheidomat: sieben Tools als eindeutige SEO-Seiten mit sichtbarem HTML-Content und sauberem internen Linking absichern.
|
||||
|
||||
### Priorität 2 – danach
|
||||
|
||||
- Keyword-Mapping erstellen, damit pro Suchintention genau eine kanonische Seite ranken soll.
|
||||
- Titles, H1s, Meta-Descriptions und FAQ-Blöcke auf die DataForSEO-Gaps ausrichten.
|
||||
- Vergleichsseiten und Use Cases nur dort ausbauen, wo sie einen echten Nutzwert und interne Conversion-Verbindung haben.
|
||||
|
||||
### Priorität 3 – Monitoring
|
||||
|
||||
- Nach den Änderungen monatlich dieselben DataForSEO-Abfragen wiederholen.
|
||||
- Zusätzlich Google Search Console je Domain auswerten; insbesondere bei Entscheidomat ist die Nullmessung ohne GSC nicht ausreichend.
|
||||
- Rankings nicht isoliert bewerten: URL, Suchintention, Klicks, Impressions und Conversion gemeinsam messen.
|
||||
|
||||
## Technischer Nachweis
|
||||
|
||||
Die Live-Abfragen liefen mit HTTP 200 und DataForSEO-Task-Status 20000. Erfolgreich verarbeitet wurden:
|
||||
|
||||
- 4 Ranked-Keyword-Abfragen
|
||||
- 4 Competitor-Domain-Abfragen
|
||||
- 12 Domain-Intersection-Abfragen für Wettbewerber-Gaps
|
||||
- 4 Keyword-Metrik-Abfragen für Seed-Cluster
|
||||
|
||||
Die Seed-Volume-Antworten wurden von der ersten Auswertung nicht in die Ergebnisliste übernommen, deshalb stammen die im Bericht genannten Volumina aus Ranked-Keyword- und Domain-Intersection-Daten. Die vier Seed-Requests selbst wurden technisch erfolgreich angenommen, werden aber nicht als separate Primärquelle für die genannten Zahlen verwendet.
|
||||
38
sql/2026-07-27_APPLY.sql
Normal file
@@ -0,0 +1,38 @@
|
||||
-- QR Master, Deploy 27.07.2026 - nur die Statements, die ausgefuehrt werden muessen.
|
||||
-- Alle idempotent, ein zweiter Lauf schadet nicht.
|
||||
-- Danach zwingend: npx prisma generate
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "QRCode_userId_type_status_idx"
|
||||
ON "QRCode" ("userId", "type", "status");
|
||||
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "limitReachedNudgeSentAt" TIMESTAMP(3);
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "firstScanNudgeSentAt" TIMESTAMP(3);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "User_firstScanAt_firstScanNudgeSentAt_idx"
|
||||
ON "User" ("firstScanAt", "firstScanNudgeSentAt");
|
||||
|
||||
-- Schuetzt Bestandsnutzer: ohne diese Zeile geht die "erster Scan"-Mail beim
|
||||
-- ersten Cron-Lauf an jeden Nutzer mit Scan-Historie.
|
||||
UPDATE "User"
|
||||
SET "firstScanNudgeSentAt" = now()
|
||||
WHERE "firstScanAt" IS NOT NULL
|
||||
AND "firstScanAt" < now() - interval '7 days';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "QRDesignPreset" (
|
||||
"id" TEXT PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL REFERENCES "User"("id") ON DELETE CASCADE,
|
||||
"name" TEXT NOT NULL,
|
||||
"style" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "QRDesignPreset_userId_idx"
|
||||
ON "QRDesignPreset" ("userId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "QRDesignPreset_userId_name_key"
|
||||
ON "QRDesignPreset" ("userId", "name");
|
||||
|
||||
COMMIT;
|
||||
280
sql/2026-07-27_cro_retention_design.txt
Normal file
@@ -0,0 +1,280 @@
|
||||
================================================================================
|
||||
QR MASTER - SQL-BEFEHLE ZUM UMSETZUNGSPLAN VOM 27.07.2026
|
||||
Gehoert zu: PLAN_CRO_UND_RETENTION_2026-07-27.md
|
||||
================================================================================
|
||||
|
||||
POLICY (aus CLAUDE.md): Keine Prisma-Migrationen. Alle Schemaaenderungen werden
|
||||
als rohes SQL direkt gegen die laufende PostgreSQL-Instanz ausgefuehrt. Danach
|
||||
prisma/schema.prisma von Hand angleichen und "npx prisma generate" laufen
|
||||
lassen - niemals "npx prisma migrate".
|
||||
|
||||
AUSFUEHREN ueber:
|
||||
npm run docker:db
|
||||
oder einzeln:
|
||||
docker-compose exec db psql -U postgres -d qrmaster -c "<statement>"
|
||||
|
||||
REIHENFOLGE: Block 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6. Alle Bloecke sind Pflicht.
|
||||
Block 5 (prisma generate) darf nicht vergessen werden.
|
||||
|
||||
|
||||
================================================================================
|
||||
BLOCK 0 - VORHER PRUEFEN (liest nur, aendert nichts)
|
||||
================================================================================
|
||||
|
||||
-- 0.1 Wie viele FREE-Nutzer bekommen durch die Umstellung auf "nur ACTIVE
|
||||
-- zaehlen" rueckwirkend Slots frei? Das ist eine Lockerung, nimmt also
|
||||
-- niemandem etwas weg - aber die Zahl sollte man kennen.
|
||||
SELECT COUNT(DISTINCT u.id) AS betroffene_free_nutzer
|
||||
FROM "User" u
|
||||
JOIN "QRCode" q ON q."userId" = u.id
|
||||
WHERE u.plan = 'FREE'
|
||||
AND q.type = 'DYNAMIC'
|
||||
AND q.status = 'PAUSED';
|
||||
|
||||
-- 0.2 Wie viele Nutzer wuerden die neue "erster Scan"-Mail beim ersten
|
||||
-- Cron-Lauf bekommen? WICHTIG: ohne Block 3.2 ginge sie an ALLE
|
||||
-- Bestandsnutzer mit Scan-Historie, auch an solche, deren erster Scan
|
||||
-- Monate zurueckliegt. Das waere kein Anlass mehr, sondern eine
|
||||
-- Massenmail unter deinem Namen.
|
||||
SELECT COUNT(*) AS wuerden_mail_bekommen
|
||||
FROM "User"
|
||||
WHERE "firstScanAt" IS NOT NULL;
|
||||
|
||||
-- 0.3 Verteilung der Plaene - Kontext fuer alles Weitere.
|
||||
SELECT plan, COUNT(*) AS nutzer
|
||||
FROM "User"
|
||||
GROUP BY plan
|
||||
ORDER BY nutzer DESC;
|
||||
|
||||
-- 0.4 Wie viele FREE-Nutzer sitzen aktuell am Limit? Das ist die Zielgruppe
|
||||
-- des neuen Limit-Modals aus Phase 1 und der Limit-Mail aus Phase 5.1.
|
||||
SELECT COUNT(*) AS free_nutzer_am_limit
|
||||
FROM (
|
||||
SELECT u.id
|
||||
FROM "User" u
|
||||
JOIN "QRCode" q ON q."userId" = u.id
|
||||
WHERE u.plan = 'FREE' AND q.type = 'DYNAMIC' AND q.status = 'ACTIVE'
|
||||
GROUP BY u.id
|
||||
HAVING COUNT(q.id) >= 3
|
||||
) t;
|
||||
|
||||
|
||||
================================================================================
|
||||
BLOCK 1 - INDEX FUER DIE NEUE LIMIT-QUERY (Phase 1.1)
|
||||
================================================================================
|
||||
|
||||
-- Die Zaehl-Query in src/app/(main)/api/qrs/route.ts bekommt zusaetzlich
|
||||
-- status = 'ACTIVE'. Dieser Index deckt die neue Bedingung ab.
|
||||
-- Dieselbe Aenderung gilt fuer src/app/(main)/api/user/stats/route.ts.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "QRCode_userId_type_status_idx"
|
||||
ON "QRCode" ("userId", "type", "status");
|
||||
|
||||
|
||||
================================================================================
|
||||
BLOCK 2 - MARKER-SPALTEN FUER DIE NEUEN RETENTION-MAILS (Phase 5)
|
||||
================================================================================
|
||||
|
||||
-- limitReachedNudgeSentAt: Marker fuer die verhaltensbasierte Limit-Mail.
|
||||
-- Ersetzt die kalenderbasierte Tag-7-Mail, die heute auch an Nutzer geht,
|
||||
-- die das Limit gar nicht erreicht haben.
|
||||
-- firstScanNudgeSentAt: Marker fuer die neue "erster Scan"-Mail.
|
||||
-- Die Erkennung selbst braucht nichts Neues - User."firstScanAt" existiert
|
||||
-- bereits und wird in src/app/(main)/r/[slug]/route.ts gesetzt.
|
||||
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "limitReachedNudgeSentAt" TIMESTAMP(3);
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "firstScanNudgeSentAt" TIMESTAMP(3);
|
||||
|
||||
-- Index fuer den Cron-Job, der Nutzer mit erstem Scan ohne Versandmarker sucht.
|
||||
CREATE INDEX IF NOT EXISTS "User_firstScanAt_firstScanNudgeSentAt_idx"
|
||||
ON "User" ("firstScanAt", "firstScanNudgeSentAt");
|
||||
|
||||
|
||||
================================================================================
|
||||
BLOCK 3 - BESTANDSDATEN VORBEREITEN (unbedingt VOR dem ersten Cron-Lauf)
|
||||
================================================================================
|
||||
|
||||
-- 3.1 Nutzer, die schon am Limit sitzen, haben die Limit-Mail nie bekommen
|
||||
-- koennen - es gab sie nicht. Ob sie sie nachtraeglich bekommen sollen,
|
||||
-- ist eine Entscheidung:
|
||||
--
|
||||
-- Variante A - sie sollen die Mail bekommen: dieses Statement NICHT
|
||||
-- ausfuehren. Der erste Cron-Lauf schickt sie an alle, die am Limit sind.
|
||||
-- Bei vielen Bestandsnutzern ist das ein Versand-Peak.
|
||||
--
|
||||
-- Variante B - nur Neufaelle ab jetzt: Statement ausfuehren, dann bekommen
|
||||
-- bestehende Limit-Faelle keine Mail und die Sequenz startet sauber.
|
||||
|
||||
-- Variante B (auskommentiert - bewusst entscheiden und dann aktivieren):
|
||||
-- UPDATE "User" u
|
||||
-- SET "limitReachedNudgeSentAt" = now()
|
||||
-- WHERE u.plan = 'FREE'
|
||||
-- AND (SELECT COUNT(*) FROM "QRCode" q
|
||||
-- WHERE q."userId" = u.id AND q.type = 'DYNAMIC' AND q.status = 'ACTIVE') >= 3;
|
||||
|
||||
-- 3.2 PFLICHT: Bestandsnutzer, deren erster Scan laenger als 7 Tage her ist,
|
||||
-- als "bereits benachrichtigt" markieren. Ohne dieses Statement geht die
|
||||
-- "erster Scan"-Mail beim ersten Lauf an die gesamte Bestandsbasis - mit
|
||||
-- einem Anlass, der Monate zurueckliegt.
|
||||
UPDATE "User"
|
||||
SET "firstScanNudgeSentAt" = now()
|
||||
WHERE "firstScanAt" IS NOT NULL
|
||||
AND "firstScanAt" < now() - interval '7 days';
|
||||
|
||||
-- 3.3 Kontrolle nach 3.2 - sollte eine kleine, plausible Zahl sein.
|
||||
SELECT COUNT(*) AS offene_erster_scan_mails
|
||||
FROM "User"
|
||||
WHERE "firstScanAt" IS NOT NULL
|
||||
AND "firstScanNudgeSentAt" IS NULL;
|
||||
|
||||
|
||||
================================================================================
|
||||
BLOCK 4 - PFLICHT: DESIGN-VORLAGEN
|
||||
================================================================================
|
||||
|
||||
-- Nicht mehr optional: die Design-Presets sind gebaut. Ohne diese Tabelle
|
||||
-- laufen GET/POST/DELETE /api/design-presets und die Preset-Auswahl im
|
||||
-- Bulk-Flow in einen Prisma-Fehler.
|
||||
--
|
||||
-- Die Formen selbst brauchen weiterhin keine Schemaaenderung: QRCode."style"
|
||||
-- ist JSON, moduleShape, eyeFrameShape, eyeBallShape, gradientMode und
|
||||
-- gradientTo liegen dort ohne Migration drin.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "QRDesignPreset" (
|
||||
"id" TEXT PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL REFERENCES "User"("id") ON DELETE CASCADE,
|
||||
"name" TEXT NOT NULL,
|
||||
"style" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "QRDesignPreset_userId_idx"
|
||||
ON "QRDesignPreset" ("userId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "QRDesignPreset_userId_name_key"
|
||||
ON "QRDesignPreset" ("userId", "name");
|
||||
|
||||
|
||||
================================================================================
|
||||
BLOCK 5 - PRISMA-SCHEMA VON HAND ANGLEICHEN (kein SQL, aber Pflichtschritt)
|
||||
================================================================================
|
||||
|
||||
In prisma/schema.prisma, model User, im Block "// Retention email tracking"
|
||||
ergaenzen:
|
||||
|
||||
limitReachedNudgeSentAt DateTime?
|
||||
firstScanNudgeSentAt DateTime?
|
||||
|
||||
In model QRCode ergaenzen:
|
||||
|
||||
@@index([userId, type, status])
|
||||
|
||||
Ausserdem (Block 4 ist Pflicht) - beides ist im Repo bereits eingetragen,
|
||||
diese Angabe dient nur der Kontrolle:
|
||||
|
||||
model QRDesignPreset {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
name String
|
||||
style Json
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, name])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
und in model User die Gegenseite:
|
||||
|
||||
designPresets QRDesignPreset[]
|
||||
|
||||
Danach:
|
||||
|
||||
npx prisma generate
|
||||
|
||||
NICHT "npx prisma migrate" - das wuerde gegen die Policy in CLAUDE.md verstossen.
|
||||
|
||||
|
||||
================================================================================
|
||||
BLOCK 6 - VERIFIKATION NACH DEM DEPLOY
|
||||
================================================================================
|
||||
|
||||
-- 6.1 Sind alle neuen Spalten da?
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'User'
|
||||
AND column_name IN ('limitReachedNudgeSentAt', 'firstScanNudgeSentAt',
|
||||
'activationNudgeSentAt', 'upgradeNudgeSentAt',
|
||||
'thirtyDayNudgeSentAt', 'firstScanAt')
|
||||
ORDER BY column_name;
|
||||
|
||||
-- 6.2 Sind alle neuen Indizes da?
|
||||
SELECT indexname
|
||||
FROM pg_indexes
|
||||
WHERE tablename IN ('User', 'QRCode', 'QRDesignPreset')
|
||||
AND indexname IN ('QRCode_userId_type_status_idx',
|
||||
'User_firstScanAt_firstScanNudgeSentAt_idx',
|
||||
'QRDesignPreset_userId_idx',
|
||||
'QRDesignPreset_userId_name_key')
|
||||
ORDER BY indexname;
|
||||
|
||||
-- 6.3 Nutzt die neue Limit-Query den Index? Sollte "Index Scan" oder
|
||||
-- "Index Only Scan" zeigen, keinen "Seq Scan".
|
||||
EXPLAIN ANALYZE
|
||||
SELECT COUNT(*) FROM "QRCode"
|
||||
WHERE "userId" = (SELECT id FROM "User" LIMIT 1)
|
||||
AND type = 'DYNAMIC'
|
||||
AND status = 'ACTIVE';
|
||||
|
||||
-- 6.4 Wie viele Mails stehen im naechsten Cron-Lauf an? Vor dem ersten
|
||||
-- scharfen Lauf pruefen, damit es keine Ueberraschung gibt.
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM "User"
|
||||
WHERE "firstScanAt" IS NOT NULL AND "firstScanNudgeSentAt" IS NULL)
|
||||
AS erster_scan_mails,
|
||||
(SELECT COUNT(*) FROM "User" u WHERE u.plan = 'FREE'
|
||||
AND u."limitReachedNudgeSentAt" IS NULL
|
||||
AND (SELECT COUNT(*) FROM "QRCode" q
|
||||
WHERE q."userId" = u.id AND q.type = 'DYNAMIC' AND q.status = 'ACTIVE') >= 3)
|
||||
AS limit_mails,
|
||||
(SELECT COUNT(*) FROM "User"
|
||||
WHERE "activationNudgeSentAt" IS NULL
|
||||
AND "createdAt" < now() - interval '3 days')
|
||||
AS aktivierungs_mails;
|
||||
|
||||
|
||||
================================================================================
|
||||
ROLLBACK - falls etwas zurueckgedreht werden muss
|
||||
================================================================================
|
||||
|
||||
-- Die Spalten sind additiv und nullable, ein Rollback ist normalerweise nicht
|
||||
-- noetig. Falls doch: Datenverlust bei den Versandmarkern beachten - danach
|
||||
-- koennten Nutzer Mails ein zweites Mal bekommen.
|
||||
|
||||
-- ALTER TABLE "User" DROP COLUMN IF EXISTS "limitReachedNudgeSentAt";
|
||||
-- ALTER TABLE "User" DROP COLUMN IF EXISTS "firstScanNudgeSentAt";
|
||||
-- DROP INDEX IF EXISTS "User_firstScanAt_firstScanNudgeSentAt_idx";
|
||||
-- DROP INDEX IF EXISTS "QRCode_userId_type_status_idx";
|
||||
-- DROP TABLE IF EXISTS "QRDesignPreset";
|
||||
|
||||
|
||||
================================================================================
|
||||
ZUSAMMENFASSUNG
|
||||
================================================================================
|
||||
|
||||
Pflicht: 2 Spalten, 2 Indizes, 1 UPDATE fuer Bestandsdaten (Block 3.2)
|
||||
Pflicht: 1 Tabelle mit 2 Indizes (Design-Presets, Block 4)
|
||||
Formen: brauchen kein SQL - QRCode."style" ist bereits JSON
|
||||
Phasen 1-4: brauchen kein SQL ausser dem Index aus Block 1
|
||||
|
||||
NACH dem SQL zwingend: npx prisma generate
|
||||
Ohne generate kennt der Prisma-Client das Modell QRDesignPreset nicht und
|
||||
/api/design-presets wirft zur Laufzeit.
|
||||
|
||||
Der einzige Schritt mit echtem Risiko ist Block 3.2. Wird er vergessen, geht
|
||||
die "erster Scan"-Mail an die gesamte Bestandsbasis.
|
||||
|
||||
================================================================================
|
||||
@@ -130,7 +130,7 @@ export default function AppLayout({
|
||||
},
|
||||
{
|
||||
name: t('nav.pricing'),
|
||||
href: '/pricing',
|
||||
href: '/upgrade',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
@@ -166,7 +166,7 @@ export default function AppLayout({
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<img src="/favicon1.png" alt="QR Master" className="w-16 h-16 rounded-full object-cover" />
|
||||
<img src="/logo.svg" alt="QR Master" className="w-16 h-16 rounded-full object-cover" />
|
||||
<span className="text-xl font-bold text-gray-900">QR Master</span>
|
||||
</Link>
|
||||
<button
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import Papa from 'papaparse';
|
||||
import ExcelJS from 'exceljs';
|
||||
@@ -9,6 +10,7 @@ import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { renderStyledQRSvg } from '@/lib/render-qr-svg';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
@@ -39,6 +41,38 @@ export default function BulkCreationPage() {
|
||||
const [userPlan, setUserPlan] = useState<string>('FREE');
|
||||
const [isDynamic, setIsDynamic] = useState(false);
|
||||
const [remainingDynamic, setRemainingDynamic] = useState(0);
|
||||
// Rows the API refused. Previously these vanished silently and the success
|
||||
// toast reported a smaller number with no explanation - the worst kind of
|
||||
// failure for someone who is about to send a batch to print.
|
||||
const [failedRows, setFailedRows] = useState<{ row: number; title: string; reason: string }[]>([]);
|
||||
// A saved design applied to the whole batch. This is the reason presets exist:
|
||||
// 500 codes that all look like the same client, from one upload.
|
||||
const [presets, setPresets] = useState<{ id: string; name: string; style: any }[]>([]);
|
||||
const [presetId, setPresetId] = useState('');
|
||||
|
||||
// Reload the remaining dynamic quota from the server. Counting down locally
|
||||
// drifts as soon as anything is created in another tab, which is how rows
|
||||
// ended up being refused mid-batch in the first place.
|
||||
const refreshQuota = async () => {
|
||||
try {
|
||||
const statsRes = await fetch('/api/user/stats');
|
||||
if (statsRes.ok) {
|
||||
const stats = await statsRes.json();
|
||||
setRemainingDynamic((stats.dynamicLimit || 0) - (stats.dynamicUsed || 0));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing quota:', error);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetch('/api/design-presets')
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((d) => Array.isArray(d) && setPresets(d))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const activeStyle = () => presets.find((p) => p.id === presetId)?.style ?? null;
|
||||
|
||||
// Check user plan and dynamic quota on mount
|
||||
React.useEffect(() => {
|
||||
@@ -177,14 +211,14 @@ export default function BulkCreationPage() {
|
||||
|
||||
// Use qrcode library to generate SVG
|
||||
const QRCode = require('qrcode');
|
||||
const qrSvg = await QRCode.toString(content, {
|
||||
const style = activeStyle();
|
||||
const qrSvg = style
|
||||
? renderStyledQRSvg(String(content), style, 300)
|
||||
: await QRCode.toString(content, {
|
||||
type: 'svg',
|
||||
width: 300,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF',
|
||||
},
|
||||
color: { dark: '#000000', light: '#FFFFFF' },
|
||||
});
|
||||
|
||||
qrCodes.push({
|
||||
@@ -212,20 +246,35 @@ export default function BulkCreationPage() {
|
||||
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');
|
||||
showToast('No dynamic QR codes left on your plan. Free a slot or upgrade to continue.', 'error');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.length > remainingDynamic) {
|
||||
showToast(`Nur ${remainingDynamic} dynamische Codes verfügbar. Es werden nur die ersten ${remainingDynamic} Zeilen verarbeitet.`, 'warning');
|
||||
showToast(
|
||||
`Only ${remainingDynamic} dynamic codes left. The first ${remainingDynamic} rows will be processed - the rest are listed after the run.`,
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const QRCode = require('qrcode');
|
||||
const results: GeneratedQR[] = [];
|
||||
const failures: { row: number; title: string; reason: string }[] = [];
|
||||
|
||||
for (const row of toProcess) {
|
||||
// Rows the plan could not cover are counted from the start, so the summary
|
||||
// reflects the whole upload and not just the slice we attempted.
|
||||
data.slice(toProcess.length).forEach((row, i) => {
|
||||
failures.push({
|
||||
row: toProcess.length + i + 1,
|
||||
title: String(row[mapping.title as keyof typeof row] || 'Untitled'),
|
||||
reason: 'No dynamic code slots left on your plan',
|
||||
});
|
||||
});
|
||||
|
||||
for (let i = 0; i < toProcess.length; i++) {
|
||||
const row = toProcess[i];
|
||||
const title = String(row[mapping.title as keyof typeof row] || 'Untitled');
|
||||
const url = String(row[mapping.content as keyof typeof row] || 'https://example.com');
|
||||
|
||||
@@ -242,23 +291,61 @@ export default function BulkCreationPage() {
|
||||
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 });
|
||||
const style = activeStyle();
|
||||
const svg = style
|
||||
? renderStyledQRSvg(redirectUrl, style, 300)
|
||||
: await QRCode.toString(redirectUrl, { type: 'svg', width: 300, margin: 2 });
|
||||
results.push({ title, content: url, svg, slug: qr.slug, redirectUrl });
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
failures.push({
|
||||
row: i + 1,
|
||||
title,
|
||||
reason: err?.error === 'Limit reached'
|
||||
? 'No dynamic code slots left on your plan'
|
||||
: err?.error || `Request failed (${res.status})`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setGeneratedQRs(results);
|
||||
setRemainingDynamic(prev => Math.max(0, prev - results.length));
|
||||
setFailedRows(failures);
|
||||
await refreshQuota();
|
||||
setStep('complete');
|
||||
showToast(`${results.length} dynamische QR-Codes erstellt!`, 'success');
|
||||
|
||||
if (failures.length > 0) {
|
||||
showToast(
|
||||
`${results.length} of ${data.length} codes created. ${failures.length} row${failures.length === 1 ? '' : 's'} could not be added - see the list below.`,
|
||||
'warning'
|
||||
);
|
||||
} else {
|
||||
showToast(`${results.length} dynamic QR codes created.`, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Dynamic QR generation error:', error);
|
||||
showToast('Fehler beim Erstellen der dynamischen QR-Codes', 'error');
|
||||
showToast('Something went wrong while creating the dynamic QR codes.', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Hands the user back exactly the rows that did not make it, in a format they
|
||||
// can re-upload once they have room. Telling someone what is missing without
|
||||
// giving them the list is only half an apology.
|
||||
const downloadFailedRowsCsv = () => {
|
||||
const header = 'row,title,reason\n';
|
||||
const body = failedRows
|
||||
.map(f => `${f.row},"${f.title.replace(/"/g, '""')}","${f.reason.replace(/"/g, '""')}"`)
|
||||
.join('\n');
|
||||
const blob = new Blob([header + body], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'qrmaster-missing-rows.csv';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const downloadAllQRCodes = async () => {
|
||||
const zip = new JSZip();
|
||||
|
||||
@@ -370,7 +457,7 @@ export default function BulkCreationPage() {
|
||||
<Button variant="outline" onClick={() => window.location.href = '/dashboard'}>
|
||||
Back to Dashboard
|
||||
</Button>
|
||||
<Button onClick={() => window.location.href = '/pricing'}>
|
||||
<Button onClick={() => window.location.href = '/upgrade?reason=bulk&from=/bulk-creation'}>
|
||||
Upgrade to Business
|
||||
</Button>
|
||||
</div>
|
||||
@@ -386,6 +473,26 @@ export default function BulkCreationPage() {
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1>
|
||||
<p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p>
|
||||
|
||||
{/* Apply a saved design to the whole batch. */}
|
||||
{presets.length > 0 && (
|
||||
<div className="mt-4 rounded-xl border border-gray-200 bg-gray-50 p-4">
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">
|
||||
Design preset
|
||||
</label>
|
||||
<Select
|
||||
value={presetId}
|
||||
onChange={(e) => setPresetId(e.target.value)}
|
||||
options={[
|
||||
{ value: '', label: 'Plain black and white' },
|
||||
...presets.map((p) => ({ value: p.id, label: p.name })),
|
||||
]}
|
||||
/>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Applied to every code in this upload, so the whole batch matches.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
@@ -772,11 +879,54 @@ export default function BulkCreationPage() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Generation Complete!</h2>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
{failedRows.length > 0
|
||||
? `${generatedQRs.length} of ${generatedQRs.length + failedRows.length} codes created`
|
||||
: 'Generation complete'}
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-8">
|
||||
Successfully generated {generatedQRs.length} static QR codes
|
||||
{failedRows.length > 0
|
||||
? 'The rows below could not be added. Nothing was silently dropped - here is exactly what is missing.'
|
||||
: `${generatedQRs.length} ${isDynamic ? 'dynamic' : 'static'} QR codes, ready to download.`}
|
||||
</p>
|
||||
|
||||
{failedRows.length > 0 && (
|
||||
<div className="mx-auto mb-8 max-w-3xl rounded-lg border border-amber-200 bg-amber-50 text-left">
|
||||
<div className="border-b border-amber-200 px-5 py-3">
|
||||
<p className="text-sm font-semibold text-amber-900">
|
||||
{failedRows.length} row{failedRows.length === 1 ? '' : 's'} not created
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-amber-800">
|
||||
Check these before you send anything to print.
|
||||
</p>
|
||||
</div>
|
||||
<ul className="max-h-64 divide-y divide-amber-100 overflow-y-auto">
|
||||
{failedRows.slice(0, 50).map((f) => (
|
||||
<li key={`${f.row}-${f.title}`} className="flex items-start justify-between gap-4 px-5 py-2.5">
|
||||
<span className="text-sm text-amber-900">
|
||||
<span className="font-mono text-xs text-amber-700">Row {f.row}</span>{' '}
|
||||
{f.title}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs text-amber-700">{f.reason}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{failedRows.length > 50 && (
|
||||
<p className="px-5 py-2 text-xs text-amber-700">
|
||||
and {failedRows.length - 50} more - download the list to see all of them.
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-3 border-t border-amber-200 px-5 py-3">
|
||||
<Button variant="outline" size="sm" onClick={downloadFailedRowsCsv}>
|
||||
Download missing rows as CSV
|
||||
</Button>
|
||||
<Link href="/upgrade?reason=limit&from=/bulk-creation">
|
||||
<Button variant="primary" size="sm">Raise my limit</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8 max-w-6xl mx-auto">
|
||||
{generatedQRs.slice(0, 8).map((qr, index) => (
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import StyledQRCode from '@/components/generator/StyledQRCode';
|
||||
import { renderStyledQRSvg } from '@/lib/render-qr-svg';
|
||||
import {
|
||||
ModuleShape,
|
||||
EyeFrameShape,
|
||||
EyeBallShape,
|
||||
PRO_MODULE_SHAPES,
|
||||
BUSINESS_MODULE_SHAPES,
|
||||
LOW_COVERAGE_SHAPES,
|
||||
MODULE_SHAPE_LABELS,
|
||||
EYE_FRAME_LABELS,
|
||||
EYE_BALL_LABELS,
|
||||
} from '@/lib/qr-shapes';
|
||||
import { toPng } from 'html-to-image';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
@@ -16,6 +27,10 @@ import { useCsrf } from '@/hooks/useCsrf';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { trackEvent } from '@/components/PostHogProvider';
|
||||
import { appendRedirectParam, sanitizeRedirectPath } from '@/lib/auth-flow';
|
||||
import UpgradeModal, {
|
||||
UpgradeReason,
|
||||
ActiveCodeSummary,
|
||||
} from '@/components/app/UpgradeModal';
|
||||
import {
|
||||
ONBOARDING_DOWNLOAD_COMPLETE_EVENT,
|
||||
ONBOARDING_DOWNLOAD_COMPLETE_KEY,
|
||||
@@ -127,6 +142,20 @@ export default function CreatePage() {
|
||||
const [cornerStyle, setCornerStyle] = useState('square');
|
||||
const [size, setSize] = useState(200);
|
||||
const [frameType, setFrameType] = useState('none');
|
||||
const [moduleShape, setModuleShape] = useState<ModuleShape>('square');
|
||||
const [eyeFrameShape, setEyeFrameShape] = useState<EyeFrameShape>('square');
|
||||
const [eyeBallShape, setEyeBallShape] = useState<EyeBallShape>('square');
|
||||
const [gradientMode, setGradientMode] = useState<'none' | 'linear' | 'radial'>('none');
|
||||
const [gradientTo, setGradientTo] = useState('#7C3AED');
|
||||
const [presets, setPresets] = useState<{ id: string; name: string; style: any }[]>([]);
|
||||
const [presetName, setPresetName] = useState('');
|
||||
|
||||
// Upgrade modal. Replaces the old redirect to /pricing, which destroyed the
|
||||
// form state at the exact moment purchase intent was highest.
|
||||
const [upgradeOpen, setUpgradeOpen] = useState(false);
|
||||
const [upgradeReason, setUpgradeReason] = useState<UpgradeReason>('limit');
|
||||
const [limitInfo, setLimitInfo] = useState<{ current: number; limit: number } | null>(null);
|
||||
const [activeCodes, setActiveCodes] = useState<ActiveCodeSummary[]>([]);
|
||||
|
||||
// Get frame options for current content type
|
||||
const frameOptions = getFrameOptionsForContentType(contentType);
|
||||
@@ -163,8 +192,16 @@ export default function CreatePage() {
|
||||
window.dispatchEvent(new CustomEvent(ONBOARDING_DOWNLOAD_COMPLETE_EVENT));
|
||||
};
|
||||
|
||||
// Check if user can customize colors (PRO+ only)
|
||||
const canCustomizeColors = userPlan === 'PRO' || userPlan === 'BUSINESS';
|
||||
// Design gating by plan.
|
||||
// Colors are free for everyone - a QR code the user cannot color reads as a
|
||||
// basic utility, and that judgement carries into every comparison they make.
|
||||
const canCustomizeColors = true;
|
||||
// Module shapes and eye styles are the PRO driver that replaced colors.
|
||||
const canUseShapes = userPlan === 'PRO' || userPlan === 'BUSINESS';
|
||||
// Logo stays PRO.
|
||||
const canUseLogo = userPlan === 'PRO' || userPlan === 'BUSINESS';
|
||||
// Gradients, frames with labels, logo shapes and the exotic module shapes.
|
||||
const canUseFullDesign = userPlan === 'BUSINESS';
|
||||
|
||||
// Load user plan
|
||||
useEffect(() => {
|
||||
@@ -279,6 +316,38 @@ export default function CreatePage() {
|
||||
const downloadQR = async (format: 'svg' | 'png') => {
|
||||
if (!qrRef.current) return;
|
||||
try {
|
||||
// Unframed codes are re-rendered from the design rather than captured
|
||||
// from the DOM. The on-screen preview is drawn with margin 0 because the
|
||||
// container supplies the visual padding, but a downloaded file needs the
|
||||
// 4-module quiet zone the spec requires - without it a code printed next
|
||||
// to other artwork often will not scan. This also makes the file from
|
||||
// here byte-identical to the one the dashboard produces.
|
||||
if (format === 'png' && frameType === 'none' && contentType !== 'BARCODE') {
|
||||
const svg = renderStyledQRSvg(qrContent, currentDesign(), 1024);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1024;
|
||||
canvas.height = 1024;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.drawImage(img, 0, 0, 1024, 1024);
|
||||
const link = document.createElement('a');
|
||||
link.download = `qrcode-${title || 'download'}.png`;
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
link.click();
|
||||
markDownloadComplete();
|
||||
trackEvent('qr_code_downloaded', {
|
||||
format: 'png',
|
||||
content_type: contentType,
|
||||
qr_type: isDynamic ? 'dynamic' : 'static',
|
||||
plan: userPlan,
|
||||
});
|
||||
};
|
||||
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === 'png') {
|
||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' });
|
||||
const link = document.createElement('a');
|
||||
@@ -293,25 +362,16 @@ export default function CreatePage() {
|
||||
plan: userPlan,
|
||||
});
|
||||
} else {
|
||||
// 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.
|
||||
// html-to-image can generate SVG too.
|
||||
// But usually for SVG users want the vector. Capturing HTML to SVG is possible but complex.
|
||||
// For now, let's just stick to the SVG code export if NO FRAME is selected,
|
||||
// otherwise warn or use toPng (as SVG).
|
||||
// Actually, the previous implementation was good for pure QR.
|
||||
// If frame is selected, we MUST use a raster export (PNG) or complex HTML-to-SVG.
|
||||
// Let's rely on toPng for consistency with frames.
|
||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' });
|
||||
// Wait, exporting HTML to valid vector SVG is hard.
|
||||
// Let's just offer PNG for frames for now to be safe, or just use the same PNG download for both buttons if frame is active?
|
||||
// No, let's try to grab the INNER SVG if no frame, else...
|
||||
// Without a frame the preview already is the finished vector, so the SVG
|
||||
// download is a serialisation of what is on screen. With a frame the
|
||||
// surrounding markup is HTML, which has no faithful vector equivalent -
|
||||
// that case falls back to PNG and says so.
|
||||
if (frameType === 'none') {
|
||||
const svgElement = qrRef.current.querySelector('svg');
|
||||
if (svgElement) {
|
||||
const svgData = contentType === 'BARCODE'
|
||||
? addBarcodeCaptionToSvg(svgElement, 'Scan: iPhone -> Barcode Scanner App | Android -> Google Lens')
|
||||
: new XMLSerializer().serializeToString(svgElement);
|
||||
: renderStyledQRSvg(qrContent, currentDesign(), 512);
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -392,6 +452,126 @@ export default function CreatePage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Load the user's active dynamic codes so the limit modal can offer a way out
|
||||
// that does not cost money. Silent failure is fine here - the modal simply
|
||||
// hides the pause option if this does not come back.
|
||||
const loadActiveCodes = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/qrs');
|
||||
if (!res.ok) return;
|
||||
const all = await res.json();
|
||||
setActiveCodes(
|
||||
(Array.isArray(all) ? all : [])
|
||||
.filter((qr: any) => qr.type === 'DYNAMIC' && qr.status === 'ACTIVE')
|
||||
.map((qr: any) => ({
|
||||
id: qr.id,
|
||||
title: qr.title || 'Untitled',
|
||||
scans30d: qr.scans30d ?? 0,
|
||||
}))
|
||||
);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handlePauseCode = async (id: string) => {
|
||||
const res = await fetchWithCsrf(`/api/qrs/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status: 'PAUSED' }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
throw new Error(data?.error || 'Could not pause that code.');
|
||||
}
|
||||
trackEvent('dynamic_code_paused_for_slot', { qr_id: id });
|
||||
setUpgradeOpen(false);
|
||||
showToast('Slot freed. Saving your code now.', 'success');
|
||||
// The form state was never lost, so the original save just runs again.
|
||||
await handleSubmit({ preventDefault: () => {} } as React.FormEvent);
|
||||
};
|
||||
|
||||
// Last resort that still leaves the user with something usable. A static code
|
||||
// cannot be edited or tracked, but it works forever on every plan - and
|
||||
// saying so here is what makes the rest of the modal credible.
|
||||
const handleDownloadStatic = () => {
|
||||
trackEvent('static_fallback_from_limit', { plan: userPlan });
|
||||
setUpgradeOpen(false);
|
||||
setIsDynamic(false);
|
||||
showToast(
|
||||
'Switched to a static code. It cannot be edited or tracked, but it never expires.',
|
||||
'info'
|
||||
);
|
||||
};
|
||||
|
||||
// The logo belongs in the preset. For an agency, "client A looks the same on
|
||||
// all 500 codes" is mostly about the mark in the middle - a preset that
|
||||
// carries the colours but drops the logo solves the smaller half of the job.
|
||||
const currentDesign = () => ({
|
||||
foregroundColor,
|
||||
backgroundColor,
|
||||
moduleShape,
|
||||
eyeFrameShape,
|
||||
eyeBallShape,
|
||||
gradientMode,
|
||||
gradientTo,
|
||||
frameType,
|
||||
logoUrl: canUseLogo ? logoUrl : '',
|
||||
logoSize,
|
||||
});
|
||||
|
||||
const applyDesign = (style: any) => {
|
||||
if (!style) return;
|
||||
if (style.foregroundColor) setForegroundColor(style.foregroundColor);
|
||||
if (style.backgroundColor) setBackgroundColor(style.backgroundColor);
|
||||
if (style.moduleShape) setModuleShape(style.moduleShape);
|
||||
if (style.eyeFrameShape) setEyeFrameShape(style.eyeFrameShape);
|
||||
if (style.eyeBallShape) setEyeBallShape(style.eyeBallShape);
|
||||
if (style.gradientMode) setGradientMode(style.gradientMode);
|
||||
if (style.gradientTo) setGradientTo(style.gradientTo);
|
||||
if (style.frameType) setFrameType(style.frameType);
|
||||
if (typeof style.logoUrl === 'string' && canUseLogo) setLogoUrl(style.logoUrl);
|
||||
if (style.logoSize) setLogoSize(style.logoSize);
|
||||
};
|
||||
|
||||
const loadPresets = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/design-presets');
|
||||
if (res.ok) setPresets(await res.json());
|
||||
} catch {
|
||||
// presets are a convenience, never block the page on them
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (canUseFullDesign) void loadPresets();
|
||||
}, [canUseFullDesign]);
|
||||
|
||||
const savePreset = async () => {
|
||||
const name = presetName.trim();
|
||||
if (!name) {
|
||||
showToast('Give the preset a name first.', 'error');
|
||||
return;
|
||||
}
|
||||
const res = await fetchWithCsrf('/api/design-presets', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, style: currentDesign() }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setPresetName('');
|
||||
await loadPresets();
|
||||
trackEvent('design_preset_saved', { plan: userPlan });
|
||||
showToast(`Preset "${name}" saved.`, 'success');
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
showToast(err?.message || 'Could not save the preset.', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const deletePreset = async (id: string) => {
|
||||
const res = await fetchWithCsrf(`/api/design-presets?id=${id}`, { method: 'DELETE' });
|
||||
if (res.ok) await loadPresets();
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
@@ -404,12 +584,17 @@ export default function CreatePage() {
|
||||
isStatic: !isDynamic,
|
||||
tags: [],
|
||||
style: {
|
||||
// FREE users can only use black/white
|
||||
foregroundColor: canCustomizeColors ? foregroundColor : '#000000',
|
||||
backgroundColor: canCustomizeColors ? backgroundColor : '#FFFFFF',
|
||||
// Colors are available on every plan, including Free.
|
||||
foregroundColor,
|
||||
backgroundColor,
|
||||
cornerStyle,
|
||||
size,
|
||||
imageSettings: (canCustomizeColors && logoUrl) ? {
|
||||
moduleShape,
|
||||
eyeFrameShape,
|
||||
eyeBallShape,
|
||||
gradientMode,
|
||||
gradientTo,
|
||||
imageSettings: (canUseLogo && logoUrl) ? {
|
||||
src: logoUrl,
|
||||
height: logoSize,
|
||||
width: logoSize,
|
||||
@@ -454,8 +639,20 @@ export default function CreatePage() {
|
||||
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');
|
||||
// Do NOT navigate away. The finished code only exists in this
|
||||
// component's state - a redirect throws away the user's work at the
|
||||
// exact moment they were most willing to pay for it.
|
||||
trackEvent('dynamic_limit_reached', {
|
||||
plan: responseData.plan,
|
||||
current_count: responseData.currentCount,
|
||||
});
|
||||
setLimitInfo({
|
||||
current: responseData.currentCount ?? 3,
|
||||
limit: responseData.limit ?? 3,
|
||||
});
|
||||
setUpgradeReason('limit');
|
||||
void loadActiveCodes();
|
||||
setUpgradeOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -810,8 +1007,8 @@ export default function CreatePage() {
|
||||
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>
|
||||
<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.
|
||||
@@ -834,13 +1031,13 @@ export default function CreatePage() {
|
||||
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>
|
||||
<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>
|
||||
</>
|
||||
@@ -963,22 +1160,211 @@ export default function CreatePage() {
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>{t('create.style')}</CardTitle>
|
||||
{!canCustomizeColors && (
|
||||
<Badge variant="warning">PRO Feature</Badge>
|
||||
)}
|
||||
<Badge variant="success">Free on every plan</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{!canCustomizeColors && (
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg mb-4">
|
||||
<p className="text-sm text-blue-900">
|
||||
<strong>Upgrade to PRO</strong> to customize colors, add logos, and brand your QR codes.
|
||||
|
||||
{/* Module shape. Colors are free; shapes are the Pro driver that
|
||||
replaced them. Locked options stay clickable so the preview
|
||||
shows what is being bought before anyone pays for it. */}
|
||||
<div>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<label className="block text-sm font-medium text-gray-700">Module shape</label>
|
||||
{!canUseShapes && <Badge variant="info">Pro</Badge>}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{([...PRO_MODULE_SHAPES, ...BUSINESS_MODULE_SHAPES] as ModuleShape[]).map((shape) => {
|
||||
const isBusinessOnly = BUSINESS_MODULE_SHAPES.includes(shape);
|
||||
const allowed = shape === 'square'
|
||||
|| (isBusinessOnly ? canUseFullDesign : canUseShapes);
|
||||
return (
|
||||
<button
|
||||
key={shape}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setModuleShape(shape);
|
||||
if (!allowed) {
|
||||
trackEvent('upgrade_prompt_shown', {
|
||||
reason: 'shapes',
|
||||
shape,
|
||||
plan: userPlan,
|
||||
});
|
||||
setUpgradeReason('shapes');
|
||||
setUpgradeOpen(true);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-lg border p-2 text-xs transition-colors',
|
||||
moduleShape === shape
|
||||
? 'border-primary-500 bg-primary-50 text-primary-700'
|
||||
: 'border-gray-200 text-gray-600 hover:border-gray-300',
|
||||
!allowed && 'opacity-60'
|
||||
)}
|
||||
>
|
||||
<span className="block truncate">{MODULE_SHAPE_LABELS[shape]}</span>
|
||||
{!allowed && (
|
||||
<span className="mt-0.5 block text-[10px] text-gray-400">
|
||||
{isBusinessOnly ? 'Business' : 'Pro'}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Eye styles. Only the combinations that survived decoding are
|
||||
offered - see the note in lib/qr-shapes.ts. */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Select
|
||||
label="Eye frame"
|
||||
value={eyeFrameShape}
|
||||
onChange={(e) => {
|
||||
if (!canUseShapes) {
|
||||
setUpgradeReason('shapes');
|
||||
setUpgradeOpen(true);
|
||||
return;
|
||||
}
|
||||
setEyeFrameShape(e.target.value as EyeFrameShape);
|
||||
}}
|
||||
options={Object.entries(EYE_FRAME_LABELS).map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
label="Eye centre"
|
||||
value={eyeBallShape}
|
||||
onChange={(e) => {
|
||||
if (!canUseShapes) {
|
||||
setUpgradeReason('shapes');
|
||||
setUpgradeOpen(true);
|
||||
return;
|
||||
}
|
||||
setEyeBallShape(e.target.value as EyeBallShape);
|
||||
}}
|
||||
options={Object.entries(EYE_BALL_LABELS).map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gradient. Business only - the renderer takes it as a prop, so
|
||||
this is purely a gating and input concern. */}
|
||||
<div>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<label className="block text-sm font-medium text-gray-700">Gradient</label>
|
||||
{!canUseFullDesign && <Badge variant="info">Business</Badge>}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{(['none', 'linear', 'radial'] as const).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (mode !== 'none' && !canUseFullDesign) {
|
||||
trackEvent('upgrade_prompt_shown', { reason: 'shapes', feature: 'gradient', plan: userPlan });
|
||||
setUpgradeReason('shapes');
|
||||
setUpgradeOpen(true);
|
||||
return;
|
||||
}
|
||||
setGradientMode(mode);
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-lg border p-2 text-xs capitalize transition-colors',
|
||||
gradientMode === mode
|
||||
? 'border-primary-500 bg-primary-50 text-primary-700'
|
||||
: 'border-gray-200 text-gray-600 hover:border-gray-300',
|
||||
mode !== 'none' && !canUseFullDesign && 'opacity-60'
|
||||
)}
|
||||
>
|
||||
{mode === 'none' ? 'Solid' : mode}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{gradientMode !== 'none' && (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<label className="text-sm text-gray-700">Second colour</label>
|
||||
<input
|
||||
type="color"
|
||||
value={gradientTo}
|
||||
onChange={(e) => setGradientTo(e.target.value)}
|
||||
className="h-10 w-12 rounded border border-gray-300"
|
||||
/>
|
||||
<Input
|
||||
value={gradientTo}
|
||||
onChange={(e) => setGradientTo(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Scannability. Says what was changed and why, rather than
|
||||
silently raising the error correction behind the user. */}
|
||||
{(LOW_COVERAGE_SHAPES.includes(moduleShape) || logoUrl) && (
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3">
|
||||
<p className="text-sm text-amber-900">
|
||||
{logoUrl
|
||||
? 'Error correction is set to H because this code carries a logo.'
|
||||
: `"${MODULE_SHAPE_LABELS[moduleShape]}" fills less of each module, so error correction has been raised.`}
|
||||
</p>
|
||||
<Link href="/pricing">
|
||||
<Button variant="primary" size="sm" className="mt-2">
|
||||
Upgrade Now
|
||||
<p className="mt-1 text-sm text-amber-800">
|
||||
Print it at 2 x 2 cm or larger, and scan it once with your own
|
||||
phone before you send it to the printer.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Saved presets. Business only. Repeatability is the actual
|
||||
product here - the star shape is not what an agency buys. */}
|
||||
{canUseFullDesign && (
|
||||
<div className="rounded-lg border border-gray-200 p-3">
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">
|
||||
Design presets
|
||||
</label>
|
||||
{presets.length > 0 && (
|
||||
<div className="mb-3 flex flex-wrap gap-2">
|
||||
{presets.map((preset) => (
|
||||
<span
|
||||
key={preset.id}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-gray-200 bg-gray-50 py-1 pl-3 pr-1 text-xs"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyDesign(preset.style)}
|
||||
className="text-gray-700 hover:text-primary-700"
|
||||
>
|
||||
{preset.name}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deletePreset(preset.id)}
|
||||
className="px-1 text-gray-400 hover:text-red-600"
|
||||
aria-label={`Delete preset ${preset.name}`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={presetName}
|
||||
onChange={(e) => setPresetName(e.target.value)}
|
||||
placeholder="Client A"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button type="button" variant="outline" size="sm" onClick={savePreset}>
|
||||
Save current design
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Saving under an existing name overwrites it.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1016,13 +1402,11 @@ export default function CreatePage() {
|
||||
value={foregroundColor}
|
||||
onChange={(e) => setForegroundColor(e.target.value)}
|
||||
className="w-12 h-10 rounded border border-gray-300"
|
||||
disabled={!canCustomizeColors}
|
||||
/>
|
||||
<Input
|
||||
value={foregroundColor}
|
||||
onChange={(e) => setForegroundColor(e.target.value)}
|
||||
className="flex-1"
|
||||
disabled={!canCustomizeColors}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1037,13 +1421,11 @@ export default function CreatePage() {
|
||||
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>
|
||||
@@ -1092,22 +1474,31 @@ export default function CreatePage() {
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Logo</CardTitle>
|
||||
{!canCustomizeColors && (
|
||||
<Badge variant="warning">PRO Feature</Badge>
|
||||
{!canUseLogo && (
|
||||
<Badge variant="info">Pro</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!canCustomizeColors && (
|
||||
{!canUseLogo && (
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg mb-4">
|
||||
<p className="text-sm text-blue-900">
|
||||
<strong>Upgrade to PRO</strong> to add logos to your QR codes.
|
||||
Your logo in the middle of the code tells people whose it is
|
||||
before they decide to trust it.
|
||||
</p>
|
||||
<Link href="/pricing">
|
||||
<Button variant="primary" size="sm" className="mt-2">
|
||||
Upgrade Now
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={() => {
|
||||
trackEvent('upgrade_prompt_shown', { reason: 'logo', plan: userPlan });
|
||||
setUpgradeReason('logo');
|
||||
setUpgradeOpen(true);
|
||||
}}
|
||||
>
|
||||
Add my logo
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
@@ -1119,7 +1510,6 @@ export default function CreatePage() {
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleLogoUpload}
|
||||
disabled={!canCustomizeColors}
|
||||
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
{logoUrl && (
|
||||
@@ -1230,19 +1620,23 @@ export default function CreatePage() {
|
||||
transformOrigin: 'center center',
|
||||
}}
|
||||
>
|
||||
<QRCodeSVG
|
||||
<StyledQRCode
|
||||
value={qrContent}
|
||||
size={size}
|
||||
fgColor={foregroundColor}
|
||||
bgColor={backgroundColor}
|
||||
level="H"
|
||||
includeMargin={false}
|
||||
imageSettings={logoUrl ? {
|
||||
src: logoUrl,
|
||||
height: logoSize,
|
||||
width: logoSize,
|
||||
excavate: excavate,
|
||||
} : undefined}
|
||||
moduleShape={moduleShape}
|
||||
eyeFrameShape={eyeFrameShape}
|
||||
eyeBallShape={eyeBallShape}
|
||||
gradient={
|
||||
gradientMode === 'none'
|
||||
? null
|
||||
: { type: gradientMode, from: foregroundColor, to: gradientTo }
|
||||
}
|
||||
errorCorrection="H"
|
||||
logoUrl={canUseLogo && logoUrl ? logoUrl : undefined}
|
||||
logoScale={logoSize / 200}
|
||||
margin={0}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -1281,6 +1675,17 @@ export default function CreatePage() {
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<UpgradeModal
|
||||
open={upgradeOpen}
|
||||
reason={upgradeReason}
|
||||
currentCount={limitInfo?.current}
|
||||
limit={limitInfo?.limit}
|
||||
activeCodes={activeCodes}
|
||||
onPauseCode={upgradeReason === 'limit' ? handlePauseCode : undefined}
|
||||
onDownloadStatic={upgradeReason === 'limit' ? handleDownloadStatic : undefined}
|
||||
onClose={() => setUpgradeOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ export default function DashboardPage() {
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('dashboard.title')}</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
{!loading && qrCodes.length === 0
|
||||
? 'Start here — create your first QR code in under 2 minutes'
|
||||
? 'Start here - create your first QR code in under 2 minutes'
|
||||
: t('dashboard.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -337,7 +337,7 @@ export default function DashboardPage() {
|
||||
{userPlan} Plan
|
||||
</Badge>
|
||||
{userPlan === 'FREE' && (
|
||||
<Link href="/pricing">
|
||||
<Link href="/upgrade?from=/dashboard">
|
||||
<Button className="bg-primary-600 text-white hover:bg-primary-700">Upgrade</Button>
|
||||
</Link>
|
||||
)}
|
||||
@@ -401,7 +401,7 @@ export default function DashboardPage() {
|
||||
You have {FREE_DYNAMIC_QR_LIMIT} free dynamic QR codes. They redirect wherever you want and track every scan.
|
||||
</p>
|
||||
<Link href="/create">
|
||||
<Button className="bg-primary-600 text-white hover:bg-primary-700">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>
|
||||
</div>
|
||||
) : (
|
||||
@@ -470,7 +470,7 @@ export default function DashboardPage() {
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Custom Branding (Colors & Logo)</span>
|
||||
<span>4 Module Shapes, Eye Styles & Your Logo</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
|
||||
@@ -10,10 +10,11 @@ export const metadata: Metadata = {
|
||||
robots: { index: false, follow: false },
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon1.png', sizes: '512x512', type: 'image/png' },
|
||||
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
||||
{ url: '/favicon.ico', sizes: '16x16 32x32', type: 'image/x-icon' },
|
||||
],
|
||||
shortcut: '/favicon1.png',
|
||||
apple: '/favicon1.png',
|
||||
shortcut: '/favicon.ico',
|
||||
apple: '/logo.svg',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import StyledQRCode from '@/components/generator/StyledQRCode';
|
||||
import { renderStyledQRSvg } from '@/lib/render-qr-svg';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import {
|
||||
ArrowLeft, Edit, ExternalLink, Star, MessageSquare,
|
||||
BarChart3, Copy, Check, Pause, Play
|
||||
BarChart3, Copy, Check, Pause, Play, Download
|
||||
} from 'lucide-react';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
@@ -82,6 +83,55 @@ export default function QRDetailPage() {
|
||||
showToast('Link copied!', 'success');
|
||||
};
|
||||
|
||||
// Download straight from the dashboard. Previously the only way to get a
|
||||
// file back out was to rebuild the code in /create, which also meant the
|
||||
// design had to be recreated from memory.
|
||||
const downloadQR = (format: 'svg' | 'png') => {
|
||||
if (!qrCode) return;
|
||||
const url = `${window.location.origin}/r/${qrCode.slug}`;
|
||||
const style = {
|
||||
foregroundColor: qrCode.style?.foregroundColor,
|
||||
backgroundColor: qrCode.style?.backgroundColor,
|
||||
moduleShape: qrCode.style?.moduleShape,
|
||||
eyeFrameShape: qrCode.style?.eyeFrameShape,
|
||||
eyeBallShape: qrCode.style?.eyeBallShape,
|
||||
gradientMode: qrCode.style?.gradientMode,
|
||||
gradientTo: qrCode.style?.gradientTo,
|
||||
logoUrl: qrCode.style?.imageSettings?.src,
|
||||
logoSize: qrCode.style?.imageSettings?.width,
|
||||
};
|
||||
// 1024px so the PNG is usable in print without a second export step.
|
||||
const svg = renderStyledQRSvg(url, style, format === 'png' ? 1024 : 512);
|
||||
const safeName = (qrCode.title || 'qr-code').replace(/[^a-z0-9]+/gi, '-').toLowerCase();
|
||||
|
||||
if (format === 'svg') {
|
||||
const blob = new Blob([svg], { type: 'image/svg+xml' });
|
||||
const href = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = href;
|
||||
a.download = `${safeName}.svg`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(href);
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1024;
|
||||
canvas.height = 1024;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.drawImage(img, 0, 0, 1024, 1024);
|
||||
const a = document.createElement('a');
|
||||
a.href = canvas.toDataURL('image/png');
|
||||
a.download = `${safeName}.png`;
|
||||
a.click();
|
||||
};
|
||||
img.onerror = () => showToast('Could not render the PNG. Try the SVG instead.', 'error');
|
||||
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
|
||||
};
|
||||
|
||||
const toggleStatus = async () => {
|
||||
if (!qrCode) return;
|
||||
const newStatus = qrCode.status === 'ACTIVE' ? 'PAUSED' : 'ACTIVE';
|
||||
@@ -169,11 +219,32 @@ export default function QRDetailPage() {
|
||||
<Card>
|
||||
<CardContent className="p-6 flex flex-col items-center">
|
||||
<div className="bg-white p-4 rounded-xl shadow-sm mb-4">
|
||||
<QRCodeSVG
|
||||
{/* Renders from the saved style, so the code
|
||||
here looks like the one that was designed -
|
||||
shapes, gradient and logo included. It
|
||||
previously drew a plain black grid and
|
||||
ignored everything but the two colours. */}
|
||||
<StyledQRCode
|
||||
value={qrUrl}
|
||||
size={200}
|
||||
fgColor={qrCode.style?.foregroundColor || '#000000'}
|
||||
bgColor={qrCode.style?.backgroundColor || '#FFFFFF'}
|
||||
moduleShape={qrCode.style?.moduleShape || 'square'}
|
||||
eyeFrameShape={qrCode.style?.eyeFrameShape || 'square'}
|
||||
eyeBallShape={qrCode.style?.eyeBallShape || 'square'}
|
||||
gradient={
|
||||
qrCode.style?.gradientMode && qrCode.style.gradientMode !== 'none'
|
||||
? {
|
||||
type: qrCode.style.gradientMode,
|
||||
from: qrCode.style.foregroundColor || '#000000',
|
||||
to: qrCode.style.gradientTo || '#000000',
|
||||
}
|
||||
: null
|
||||
}
|
||||
errorCorrection="H"
|
||||
logoUrl={qrCode.style?.imageSettings?.src}
|
||||
logoScale={(qrCode.style?.imageSettings?.width ?? 24) / 200}
|
||||
margin={0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -187,6 +258,14 @@ export default function QRDetailPage() {
|
||||
<ExternalLink className="w-4 h-4 mr-2" /> Open Link
|
||||
</Button>
|
||||
</a>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button variant="outline" onClick={() => downloadQR('png')}>
|
||||
<Download className="w-4 h-4 mr-2" /> PNG
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => downloadQR('svg')}>
|
||||
<Download className="w-4 h-4 mr-2" /> SVG
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -356,7 +356,7 @@ export default function SettingsPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => window.location.href = '/pricing'}
|
||||
onClick={() => window.location.href = '/upgrade?from=/settings'}
|
||||
>
|
||||
Manage Subscription
|
||||
</Button>
|
||||
@@ -365,7 +365,7 @@ export default function SettingsPage() {
|
||||
|
||||
{plan === 'FREE' && (
|
||||
<div className="pt-4 border-t">
|
||||
<Button variant="primary" className="w-full" onClick={() => window.location.href = '/pricing'}>
|
||||
<Button variant="primary" className="w-full" onClick={() => window.location.href = '/upgrade?reason=limit&from=/settings'}>
|
||||
Upgrade Plan
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
227
src/app/(main)/(app)/upgrade/page.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { trackEvent } from '@/components/PostHogProvider';
|
||||
import { Check, Loader2 } from 'lucide-react';
|
||||
import {
|
||||
FREE_DYNAMIC_QR_LIMIT,
|
||||
PRO_DYNAMIC_QR_LIMIT,
|
||||
BUSINESS_DYNAMIC_QR_LIMIT,
|
||||
} from '@/lib/plans';
|
||||
|
||||
type PlanKey = 'FREE' | 'PRO' | 'BUSINESS';
|
||||
|
||||
/**
|
||||
* In-app upgrade page.
|
||||
*
|
||||
* /pricing lives in the (marketing) route group, so every upgrade link inside
|
||||
* the app dropped the user out of the product shell and into the public site.
|
||||
* This page sits in the (app) group, keeps the sidebar, and returns the user to
|
||||
* where they came from after checkout.
|
||||
*
|
||||
* Copy note: the plan captions describe the moment each plan stops being enough,
|
||||
* not a feature count. Someone on this page already knows what the product does
|
||||
* - what they are deciding is whether they have crossed a line yet.
|
||||
*/
|
||||
const PLANS: {
|
||||
key: PlanKey;
|
||||
name: string;
|
||||
price: string;
|
||||
period: string;
|
||||
caption: string;
|
||||
features: string[];
|
||||
popular?: boolean;
|
||||
}[] = [
|
||||
{
|
||||
key: 'FREE',
|
||||
name: 'Free',
|
||||
price: '€0',
|
||||
period: 'forever',
|
||||
caption: 'Enough to prove the idea on one or two placements.',
|
||||
features: [
|
||||
`${FREE_DYNAMIC_QR_LIMIT} active dynamic QR codes`,
|
||||
'Unlimited static codes that never expire',
|
||||
'Basic scan tracking',
|
||||
'Your colors, foreground and background',
|
||||
'SVG and PNG download',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'PRO',
|
||||
name: 'Pro',
|
||||
price: '€9',
|
||||
period: 'per month',
|
||||
popular: true,
|
||||
caption: 'When one campaign is no longer the only campaign.',
|
||||
features: [
|
||||
`${PRO_DYNAMIC_QR_LIMIT} dynamic QR codes`,
|
||||
'Scan data by device, location and time',
|
||||
'4 module shapes and custom eye styles',
|
||||
'Your logo in the centre of the code',
|
||||
'Everything in Free',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'BUSINESS',
|
||||
name: 'Business',
|
||||
price: '€29',
|
||||
period: 'per month',
|
||||
caption: 'When codes are produced in batches, not one at a time.',
|
||||
features: [
|
||||
`${BUSINESS_DYNAMIC_QR_LIMIT} dynamic QR codes`,
|
||||
'Bulk creation: 1,000 static or 500 dynamic per upload',
|
||||
'Full designer: 11 module shapes and colour gradients',
|
||||
'Saved design presets, applied to a whole bulk upload',
|
||||
'Priority email support',
|
||||
'Everything in Pro',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const REASON_HEADLINES: Record<string, string> = {
|
||||
limit: 'You are out of dynamic code slots.',
|
||||
shapes: 'Module shapes start on Pro.',
|
||||
logo: 'Your logo belongs inside the code.',
|
||||
bulk: 'Bulk creation is a Business feature.',
|
||||
analytics: 'You are seeing totals, not sources.',
|
||||
};
|
||||
|
||||
export default function UpgradePage() {
|
||||
const searchParams = useSearchParams();
|
||||
const [currentPlan, setCurrentPlan] = useState<PlanKey>('FREE');
|
||||
const [loadingPlan, setLoadingPlan] = useState<PlanKey | null>(null);
|
||||
|
||||
const reason = searchParams.get('reason');
|
||||
const returnTo = searchParams.get('from');
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/user/plan')
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((d) => d?.plan && setCurrentPlan(d.plan))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('canceled') === 'true') {
|
||||
showToast('Checkout canceled. Nothing was charged.', 'info');
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const handleUpgrade = async (plan: PlanKey) => {
|
||||
if (plan === 'FREE') return;
|
||||
setLoadingPlan(plan);
|
||||
trackEvent('upgrade_clicked', { plan, source: 'in_app_upgrade', reason });
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/stripe/create-checkout-session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
plan,
|
||||
billingInterval: 'month',
|
||||
returnPath: returnTo && returnTo.startsWith('/') ? returnTo : '/dashboard',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
throw new Error(data?.error || 'Could not start checkout.');
|
||||
}
|
||||
|
||||
const { url } = await res.json();
|
||||
window.location.href = url;
|
||||
} catch (err: any) {
|
||||
showToast(err?.message || 'Could not start checkout. Please try again.', 'error');
|
||||
setLoadingPlan(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl px-4 py-10 sm:px-6 lg:px-8">
|
||||
<div className="mb-10 max-w-2xl">
|
||||
<h1 className="text-3xl font-bold text-slate-900">
|
||||
{reason && REASON_HEADLINES[reason]
|
||||
? REASON_HEADLINES[reason]
|
||||
: 'Pick the plan that matches what you are running'}
|
||||
</h1>
|
||||
<p className="mt-3 text-base leading-relaxed text-slate-600">
|
||||
Every plan keeps your static codes working forever, and nothing you have
|
||||
already printed stops resolving if you change plans. Cancel any time from
|
||||
Settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{PLANS.map((plan) => {
|
||||
const isCurrent = plan.key === currentPlan;
|
||||
return (
|
||||
<Card
|
||||
key={plan.key}
|
||||
className={plan.popular ? 'border-2 border-primary-500' : undefined}
|
||||
>
|
||||
<CardContent className="flex h-full flex-col p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-slate-900">{plan.name}</h2>
|
||||
{plan.popular && <Badge variant="info">Most popular</Badge>}
|
||||
{isCurrent && <Badge variant="success">Current plan</Badge>}
|
||||
</div>
|
||||
|
||||
<div className="mb-1 flex items-baseline gap-2">
|
||||
<span className="text-4xl font-bold text-slate-900">{plan.price}</span>
|
||||
<span className="text-sm text-slate-500">{plan.period}</span>
|
||||
</div>
|
||||
<p className="mb-6 text-sm text-slate-600">{plan.caption}</p>
|
||||
|
||||
<ul className="mb-8 space-y-3">
|
||||
{plan.features.map((f) => (
|
||||
<li key={f} className="flex items-start gap-2 text-sm text-slate-700">
|
||||
<Check className="mt-0.5 h-4 w-4 shrink-0 text-green-600" />
|
||||
<span>{f}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="mt-auto">
|
||||
{isCurrent ? (
|
||||
<Button variant="outline" className="w-full" disabled>
|
||||
Current plan
|
||||
</Button>
|
||||
) : plan.key === 'FREE' ? (
|
||||
<Button variant="outline" className="w-full" disabled>
|
||||
Included
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant={plan.popular ? 'primary' : 'secondary'}
|
||||
className="w-full"
|
||||
disabled={loadingPlan !== null}
|
||||
onClick={() => handleUpgrade(plan.key)}
|
||||
>
|
||||
{loadingPlan === plan.key ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> Opening checkout...
|
||||
</span>
|
||||
) : (
|
||||
`Upgrade to ${plan.name}`
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-sm text-slate-500">
|
||||
Prices exclude VAT where applicable. Payments run through Stripe - QR Master
|
||||
never sees your card details.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export default function ForgotPasswordPage() {
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<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="/logo.svg" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
|
||||
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Check Your Email</h1>
|
||||
@@ -99,7 +99,7 @@ export default function ForgotPasswordPage() {
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<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="/logo.svg" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
|
||||
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Forgot Password?</h1>
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<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="/logo.svg" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
|
||||
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
||||
</Link>
|
||||
{showPageHeading ? (
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Metadata } from 'next';
|
||||
import LoginClient from './LoginClient';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'QR Master – Smart QR Generator & Analytics',
|
||||
title: 'QR Master - Smart QR Generator & Analytics',
|
||||
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics. Free advanced features, bulk generation, and custom branding available.',
|
||||
robots: {
|
||||
index: false,
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function ResetPasswordPage() {
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<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="/logo.svg" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
|
||||
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Password Reset Successful</h1>
|
||||
@@ -120,7 +120,7 @@ export default function ResetPasswordPage() {
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<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="/logo.svg" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
|
||||
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Reset Your Password</h1>
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function SignupClient() {
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<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="/logo.svg" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
|
||||
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Create Account</h1>
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function MarketingLayout({
|
||||
<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">
|
||||
<Image
|
||||
src="/favicon1.png"
|
||||
src="/logo.svg"
|
||||
alt="QR Master"
|
||||
fill
|
||||
sizes="64px"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'About QR Master | Free QR Code Generator for Businesses',
|
||||
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.',
|
||||
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: {
|
||||
title: 'About QR Master | Free Dynamic QR Codes & Analytics',
|
||||
description: 'Free dynamic QR codes with scan analytics, custom branding, and no reprint headaches. Learn about the team and mission behind QR Master.',
|
||||
@@ -32,7 +32,7 @@ export default function AboutPage() {
|
||||
QR codes should be <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-purple-600">flexible, measurable, and reliable</span>.
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 max-w-2xl mx-auto mb-10 leading-relaxed">
|
||||
QR Master helps teams create dynamic QR codes that can be updated after printing—so you can stop reprinting materials every time something changes. Whether you’re running a menu, an event, or a multi-channel campaign, QR Master turns QR codes into a tool you can manage and measure.
|
||||
QR Master helps teams create dynamic QR codes that can be updated after printing-so you can stop reprinting materials every time something changes. Whether you’re running a menu, an event, or a multi-channel campaign, QR Master turns QR codes into a tool you can manage and measure.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
@@ -53,7 +53,7 @@ export default function AboutPage() {
|
||||
Our Mission
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-4">
|
||||
Create QR codes that work everywhere—and make campaigns measurable.
|
||||
Create QR codes that work everywhere-and make campaigns measurable.
|
||||
</h2>
|
||||
</div>
|
||||
</section>
|
||||
@@ -76,7 +76,7 @@ export default function AboutPage() {
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-3">Dynamic QR Codes</h3>
|
||||
<p className="text-gray-600 leading-relaxed mb-4">
|
||||
Change the destination of a QR code after it’s already printed. Keep your printed materials valid—while you update your content anytime.
|
||||
Change the destination of a QR code after it’s already printed. Keep your printed materials valid-while you update your content anytime.
|
||||
</p>
|
||||
<Link href="/dynamic-qr-code-generator" className="text-blue-600 font-medium hover:underline">
|
||||
Learn about Dynamic QR →
|
||||
@@ -104,7 +104,7 @@ export default function AboutPage() {
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-3">Advanced Analytics</h3>
|
||||
<p className="text-gray-600 leading-relaxed mb-4">
|
||||
Understand QR performance with scan analytics—so you can improve placements and campaigns based on real usage over time.
|
||||
Understand QR performance with scan analytics-so you can improve placements and campaigns based on real usage over time.
|
||||
</p>
|
||||
<Link href="/qr-code-tracking" className="text-blue-600 font-medium hover:underline">
|
||||
See Analytics Features →
|
||||
@@ -230,7 +230,7 @@ export default function AboutPage() {
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<span className="font-semibold w-32">Support hours:</span>
|
||||
<span>Monday–Friday, 9:00–17:00 CET</span>
|
||||
<span>Monday-Friday, 9:00-17:00 CET</span>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<span className="font-semibold w-32">Languages:</span>
|
||||
|
||||
@@ -14,17 +14,17 @@ const competitor = competitors['beaconstac'];
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Beaconstac / Uniqode Alternative for SMBs – QR Master',
|
||||
absolute: 'Beaconstac / Uniqode Alternative - Free, €9 or €29',
|
||||
},
|
||||
description:
|
||||
'Looking for a Beaconstac or Uniqode alternative? QR Master is the lightweight, affordable option for SMBs and freelancers who need dynamic QR codes and analytics without enterprise pricing. From €0 free.',
|
||||
'Uniqode is built for enterprise, and priced for it. If you need dynamic QR codes and scan analytics but not SOC2 and SSO: QR Master is €0, €9 or €29 a month.',
|
||||
keywords:
|
||||
'beaconstac alternative, uniqode alternative, beaconstac pricing, uniqode too expensive, beaconstac smb alternative, dynamic qr code alternative enterprise',
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/alternatives/beaconstac',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Beaconstac / Uniqode Alternative for SMBs – QR Master',
|
||||
title: 'Beaconstac / Uniqode Alternative for SMBs - QR Master',
|
||||
description:
|
||||
'Uniqode (formerly Beaconstac) is excellent for enterprise. If you don\'t need SOC2 and SSO but do need reliable dynamic QR + analytics, QR Master starts free at €0.',
|
||||
url: 'https://www.qrmaster.net/alternatives/beaconstac',
|
||||
@@ -32,7 +32,7 @@ export const metadata: Metadata = {
|
||||
images: ['/og-image.png'],
|
||||
},
|
||||
twitter: {
|
||||
title: 'Beaconstac / Uniqode Alternative for SMBs – QR Master',
|
||||
title: 'Beaconstac / Uniqode Alternative for SMBs - QR Master',
|
||||
description:
|
||||
'Uniqode (formerly Beaconstac) is built for enterprise. QR Master is the affordable alternative for SMBs and freelancers who need the same core QR functionality.',
|
||||
},
|
||||
@@ -44,37 +44,37 @@ const faqItems = [
|
||||
{
|
||||
question: 'What is the difference between Beaconstac and Uniqode?',
|
||||
answer:
|
||||
'They are the same company. Beaconstac rebranded to Uniqode in 2023. The product is the same enterprise QR code management platform — the name changed, not the features or pricing model. When people search for "Beaconstac alternative" or "Uniqode alternative," they are looking for the same thing.',
|
||||
'They are the same company. Beaconstac rebranded to Uniqode in 2023. The product is the same enterprise QR code management platform - the name changed, not the features or pricing model. When people search for "Beaconstac alternative" or "Uniqode alternative," they are looking for the same thing.',
|
||||
},
|
||||
{
|
||||
question: 'Why is Uniqode / Beaconstac considered expensive for SMBs?',
|
||||
answer:
|
||||
'Uniqode\'s entry price is around $5/month, but that tier includes very limited features. To get meaningful analytics, team management, and enough dynamic QR codes for a real use case, you need to spend $49–99/month or more. The enterprise features — SOC2 compliance, SSO/SAML, deep API access — are what justify that pricing for large organizations. For an SMB that needs 50 dynamic QR codes with scan analytics, those enterprise features are not relevant, and paying for them is waste.',
|
||||
'Uniqode\'s entry price is around $5/month, but that tier includes very limited features. To get meaningful analytics, team management, and enough dynamic QR codes for a real use case, you need to spend $49-99/month or more. The enterprise features - SOC2 compliance, SSO/SAML, deep API access - are what justify that pricing for large organizations. For an SMB that needs 50 dynamic QR codes with scan analytics, those enterprise features are not relevant, and paying for them is waste.',
|
||||
},
|
||||
{
|
||||
question: 'Does QR Master have a free plan?',
|
||||
answer:
|
||||
'Yes. QR Master\'s free plan includes 3 active dynamic QR codes, unlimited static QR codes, and basic scan tracking. No credit card required. Uniqode does not offer a free plan — you pay from the first month. QR Master Pro at €9/month includes 50 dynamic QR codes, advanced analytics, and custom branding. Business at €29/month adds bulk creation and 500 dynamic codes.',
|
||||
'Yes. QR Master\'s free plan includes 3 active dynamic QR codes, unlimited static QR codes, and basic scan tracking. No credit card required. Uniqode does not offer a free plan - you pay from the first month. Colors are free on every plan. QR Master Pro at €9/month includes 50 dynamic QR codes, advanced analytics, module shapes and logo embedding. Business at €29/month adds bulk creation and 500 dynamic codes.',
|
||||
},
|
||||
{
|
||||
question: 'Does QR Master support bulk QR code creation like Beaconstac?',
|
||||
answer:
|
||||
'Yes. QR Master Business (€29/month) supports CSV and Excel upload for bulk creation of up to 1,000 unique QR codes per batch. Each code can have a different destination URL, label, and UTM parameters. Beaconstac/Uniqode also supports bulk creation, but the feature is locked behind enterprise pricing tiers.',
|
||||
'Yes. QR Master Business (€29/month) supports CSV and Excel upload for bulk creation: up to 1,000 static codes, or up to 500 dynamic ones per batch, the dynamic cap being the Business allowance. Each code can have a different destination URL, label, and UTM parameters. Beaconstac/Uniqode also supports bulk creation, but the feature is locked behind enterprise pricing tiers.',
|
||||
},
|
||||
{
|
||||
question: 'Is QR Master GDPR-compliant?',
|
||||
answer:
|
||||
'Yes. QR Master hashes IP addresses server-side before any analytics data is stored. No raw IP is ever written to the database. Scan analytics capture device type, time, country-level location, and UTM parameters — without storing personally identifiable information. This is built into the infrastructure and applies to all plans, including the free tier. Uniqode is a US company and requires additional DPA configuration for GDPR compliance.',
|
||||
'Yes. QR Master hashes IP addresses server-side before any analytics data is stored. No raw IP is ever written to the database. Scan analytics capture device type, time, country-level location, and UTM parameters - without storing personally identifiable information. This is built into the infrastructure and applies to all plans, including the free tier. Uniqode is a US company and requires additional DPA configuration for GDPR compliance.',
|
||||
},
|
||||
{
|
||||
question: 'Who should stay on Beaconstac / Uniqode instead of switching?',
|
||||
answer:
|
||||
'Uniqode is genuinely the right tool for large enterprises that need SOC2 Type II certification, SSO/SAML authentication, deep API integrations, and formal vendor security review processes. If your procurement team requires a security certification or your IT team needs to integrate QR code management into enterprise identity systems, Uniqode is built for that. QR Master is not an enterprise compliance platform — it is a clean, fast, affordable tool for teams that need dynamic QR codes and analytics without the enterprise overhead.',
|
||||
'Uniqode is genuinely the right tool for large enterprises that need SOC2 Type II certification, SSO/SAML authentication, deep API integrations, and formal vendor security review processes. If your procurement team requires a security certification or your IT team needs to integrate QR code management into enterprise identity systems, Uniqode is built for that. QR Master is not an enterprise compliance platform - it is a clean, fast, affordable tool for teams that need dynamic QR codes and analytics without the enterprise overhead.',
|
||||
},
|
||||
{
|
||||
question: 'Can I import my codes from Beaconstac into QR Master?',
|
||||
answer:
|
||||
'Beaconstac/Uniqode allows CSV export of your QR code data. You can use that export to re-create your dynamic codes in QR Master using the bulk upload feature (Business plan). For dynamic codes, the redirect URL changes — you will need to update printed materials or digital placements that point to Beaconstac\'s redirect infrastructure. Static codes are permanently encoded in the image and do not need migration — they continue working regardless of your Beaconstac subscription.',
|
||||
'Beaconstac/Uniqode allows CSV export of your QR code data. You can use that export to re-create your dynamic codes in QR Master using the bulk upload feature (Business plan, up to 500 dynamic codes per account). For dynamic codes, the redirect URL changes - you will need to update printed materials or digital placements that point to Beaconstac\'s redirect infrastructure. Static codes are permanently encoded in the image and do not need migration - they continue working regardless of your Beaconstac subscription.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -103,21 +103,21 @@ const relatedLinks = [
|
||||
href: '/dynamic-qr-code-generator',
|
||||
title: 'Dynamic QR Code Generator',
|
||||
description:
|
||||
'Create dynamic QR codes you can update after printing — with scan analytics, custom branding, and dashboard management.',
|
||||
'Create dynamic QR codes you can update after printing - with scan analytics, custom branding, and dashboard management.',
|
||||
ctaLabel: 'Create your first dynamic QR code',
|
||||
},
|
||||
{
|
||||
href: '/qr-code-analytics',
|
||||
title: 'QR Code Analytics',
|
||||
description:
|
||||
'Track device, time, location, and UTM parameters for every scan — without storing raw IPs or PII.',
|
||||
'Track device, time, location, and UTM parameters for every scan - without storing raw IPs or PII.',
|
||||
ctaLabel: 'See QR code analytics',
|
||||
},
|
||||
{
|
||||
href: '/bulk-qr-code-generator',
|
||||
title: 'Bulk QR Code Generator',
|
||||
description:
|
||||
'Generate up to 1,000 unique QR codes in one upload via CSV or Excel. Each code gets its own destination and UTM parameters.',
|
||||
'One upload, a whole batch: up to 1,000 static codes or up to 500 dynamic ones. Each code gets its own destination and UTM parameters.',
|
||||
ctaLabel: 'Explore bulk QR creation',
|
||||
},
|
||||
{
|
||||
@@ -165,11 +165,11 @@ export default function BeaconstacAlternativePage() {
|
||||
</div>
|
||||
<ul className="mb-10 space-y-3">
|
||||
{[
|
||||
'Free plan with 3 active dynamic QR codes — no credit card required',
|
||||
'Pro at €9/month vs Uniqode\'s $49–99/month for comparable features',
|
||||
'GDPR-compliant analytics out of the box — no DPA configuration needed',
|
||||
'Bulk creation up to 1,000 codes on Business (€29/month)',
|
||||
'Simple onboarding — no enterprise setup process',
|
||||
'Free plan with 3 active dynamic QR codes - no credit card required',
|
||||
'Pro at €9/month vs Uniqode\'s $49-99/month for comparable features',
|
||||
'GDPR-compliant analytics out of the box - no DPA configuration needed',
|
||||
'Bulk creation on Business: 1,000 static or 500 dynamic (€29/month)',
|
||||
'Simple onboarding - no enterprise setup process',
|
||||
].map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-3">
|
||||
<span
|
||||
@@ -220,7 +220,7 @@ export default function BeaconstacAlternativePage() {
|
||||
<p className="text-sm text-gray-500">Starter plan with analytics</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-2xl font-bold text-gray-800">$49–99</p>
|
||||
<p className="text-2xl font-bold text-gray-800">$49-99</p>
|
||||
<p className="text-xs text-gray-500">per month</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,28 +258,28 @@ export default function BeaconstacAlternativePage() {
|
||||
<div className="space-y-6 text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
<p>
|
||||
Uniqode (formerly Beaconstac) is genuinely excellent at what it does. The platform is built for large
|
||||
enterprises that operate in compliance-heavy industries — healthcare, finance, government contracting —
|
||||
enterprises that operate in compliance-heavy industries - healthcare, finance, government contracting -
|
||||
where vendors need SOC2 Type II certification, single sign-on integration, formal security review, and
|
||||
a dedicated account team. For those buyers, Uniqode is a legitimate choice.
|
||||
</p>
|
||||
<p>
|
||||
The problem is that all of that infrastructure costs money, and Uniqode passes those costs through in
|
||||
its pricing. The entry plan at around $5/month is misleadingly cheap — it supports so few codes and
|
||||
its pricing. The entry plan at around $5/month is misleadingly cheap - it supports so few codes and
|
||||
offers so few features that almost no real use case fits it. To get 50 dynamic QR codes with proper
|
||||
analytics and team features, you are looking at $49–99/month before you even touch the enterprise
|
||||
analytics and team features, you are looking at $49-99/month before you even touch the enterprise
|
||||
tier.
|
||||
</p>
|
||||
<p>
|
||||
For a restaurant owner who wants to update their digital menu link once a quarter, or a marketing
|
||||
manager running a campaign with 20 QR codes on printed materials, or a freelancer building print
|
||||
campaigns for clients — the SOC2 certification is irrelevant, and $49+/month is a hard number to
|
||||
campaigns for clients - the SOC2 certification is irrelevant, and $49+/month is a hard number to
|
||||
justify when the core functionality needed is “create QR codes, track scans, update
|
||||
destinations.”
|
||||
</p>
|
||||
<p>
|
||||
QR Master is built for that majority use case. It doesn't have SOC2. It doesn't have SSO.
|
||||
What it has is reliable dynamic QR code management, scan analytics with GDPR-compliant tracking, and
|
||||
bulk creation — at a price that makes sense for teams that don't need the enterprise compliance
|
||||
bulk creation - at a price that makes sense for teams that don't need the enterprise compliance
|
||||
layer.
|
||||
</p>
|
||||
</div>
|
||||
@@ -290,7 +290,7 @@ export default function BeaconstacAlternativePage() {
|
||||
<h3 className="mb-2 text-lg font-semibold" style={{ color: '#166534' }}>When you should stay on Uniqode</h3>
|
||||
<p style={{ color: '#27272A' }}>
|
||||
If your organization requires a SOC2-certified QR code vendor, needs SSO/SAML integration, or goes
|
||||
through formal vendor security review — Uniqode is built for exactly that. QR Master is not. This page
|
||||
through formal vendor security review - Uniqode is built for exactly that. QR Master is not. This page
|
||||
is for the much larger group of SMBs and marketing teams who are paying enterprise prices for
|
||||
functionality they could get at a fraction of the cost.
|
||||
</p>
|
||||
@@ -359,16 +359,16 @@ export default function BeaconstacAlternativePage() {
|
||||
<div>
|
||||
<h3 className="mb-4 text-xl font-bold" style={{ color: '#18181B' }}>Pricing structure</h3>
|
||||
<p className="text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
Uniqode starts at around $5/month but that plan is largely a placeholder — it supports so few codes
|
||||
Uniqode starts at around $5/month but that plan is largely a placeholder - it supports so few codes
|
||||
with so few features that most users immediately hit its limits. The next meaningful tier is $49/month
|
||||
or higher. There is no free plan.
|
||||
</p>
|
||||
<p className="mt-4 text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
QR Master free plan includes 3 active dynamic QR codes, unlimited static codes, and basic scan
|
||||
tracking — permanently, without a credit card. Pro at €9/month (cancel anytime) covers 50 dynamic
|
||||
codes with full analytics and custom branding. Business at €29/month adds 500 codes and bulk
|
||||
tracking - permanently, without a credit card. Pro at €9/month (cancel anytime) covers 50 dynamic
|
||||
codes with full analytics, module shapes and logo. Business at €29/month adds 500 codes and bulk
|
||||
creation. The gap between what you get at €9/month on QR Master vs $49/month on Uniqode is significant
|
||||
— not because QR Master has more features, but because it doesn't charge you for enterprise
|
||||
- not because QR Master has more features, but because it doesn't charge you for enterprise
|
||||
infrastructure you don't use.
|
||||
</p>
|
||||
</div>
|
||||
@@ -376,14 +376,14 @@ export default function BeaconstacAlternativePage() {
|
||||
<div>
|
||||
<h3 className="mb-4 text-xl font-bold" style={{ color: '#18181B' }}>Complexity and onboarding</h3>
|
||||
<p className="text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
Uniqode is a mature enterprise platform. The interface reflects that — it is comprehensive, with
|
||||
Uniqode is a mature enterprise platform. The interface reflects that - it is comprehensive, with
|
||||
organization management, user roles, integration settings, and compliance tooling all visible.
|
||||
For an enterprise IT team, that depth is valuable. For a marketing manager or restaurant owner who
|
||||
just needs to create and track 20 dynamic QR codes, it adds overhead without adding value.
|
||||
</p>
|
||||
<p className="mt-4 text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
QR Master is deliberately simpler. Create a QR code, set the destination, download it, and see scans
|
||||
in the dashboard. The workflow is designed around the most common use cases — not around the edge
|
||||
in the dashboard. The workflow is designed around the most common use cases - not around the edge
|
||||
cases that enterprise compliance teams need.
|
||||
</p>
|
||||
</div>
|
||||
@@ -397,7 +397,7 @@ export default function BeaconstacAlternativePage() {
|
||||
</p>
|
||||
<p className="mt-4 text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
QR Master anonymizes scan data at the infrastructure level. IP addresses are hashed server-side with
|
||||
a salt before any data is written — the raw IP is never stored. No configuration required. This
|
||||
a salt before any data is written - the raw IP is never stored. No configuration required. This
|
||||
applies from the free plan upward and is documented in the platform's open codebase.
|
||||
</p>
|
||||
</div>
|
||||
@@ -469,7 +469,7 @@ export default function BeaconstacAlternativePage() {
|
||||
{
|
||||
step: '2',
|
||||
title: 'Create a QR Master account',
|
||||
body: 'The free plan gives you 3 dynamic codes immediately. For larger migrations, start with a Pro (€9/month) or Business (€29/month) plan — Business includes bulk upload from CSV.',
|
||||
body: 'The free plan gives you 3 dynamic codes immediately. For larger migrations, start with a Pro (€9/month) or Business (€29/month) plan - Business includes bulk upload from CSV.',
|
||||
},
|
||||
{
|
||||
step: '3',
|
||||
@@ -479,7 +479,7 @@ export default function BeaconstacAlternativePage() {
|
||||
{
|
||||
step: '4',
|
||||
title: 'Update digital placements',
|
||||
body: 'Replace QR code images on your website, email, and digital materials immediately — these don\'t require a physical reprint.',
|
||||
body: 'Replace QR code images on your website, email, and digital materials immediately - these don\'t require a physical reprint.',
|
||||
},
|
||||
{
|
||||
step: '5',
|
||||
|
||||
@@ -17,7 +17,7 @@ export const metadata: Metadata = {
|
||||
absolute: 'QR Master vs Bitly QR Codes | Bitly Alternative',
|
||||
},
|
||||
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.',
|
||||
keywords:
|
||||
'bitly qr code alternative, bitly qr code limit, bitly alternative qr codes, bitly pricing qr codes, bitly 2 qr codes',
|
||||
alternates: {
|
||||
@@ -26,7 +26,7 @@ export const metadata: Metadata = {
|
||||
openGraph: {
|
||||
title: 'QR Master vs Bitly QR Codes | Bitly Alternative',
|
||||
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',
|
||||
type: 'website',
|
||||
images: ['/og-image.png'],
|
||||
@@ -34,7 +34,7 @@ export const metadata: Metadata = {
|
||||
twitter: {
|
||||
title: 'QR Master vs Bitly QR Codes | Bitly Alternative',
|
||||
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.',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ const atAGlanceRows = [
|
||||
{
|
||||
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.',
|
||||
qrMaster: 'CSV and Excel upload: up to 1,000 static codes, or up to 500 dynamic ones, per batch.',
|
||||
},
|
||||
{
|
||||
useCase: 'QR campaign analytics',
|
||||
@@ -72,17 +72,17 @@ const faqItems = [
|
||||
{
|
||||
question: 'How many QR codes does Bitly allow per plan?',
|
||||
answer:
|
||||
'Bitly\'s free plan allows 1 QR code. Their Core plan (~$10/month) markets "unlimited scans" prominently — but the actual limit that matters is the QR code count: 2 total. If you need a third QR code on that plan, you have to upgrade. Higher plans allow more codes, but the pricing jumps quickly relative to what you get. QR Master\'s Pro plan (€9/month) includes 50 dynamic QR codes with full analytics and no scan caps on redirects.',
|
||||
'Bitly\'s free plan allows 1 QR code. Their Core plan (~$10/month) markets "unlimited scans" prominently - but the actual limit that matters is the QR code count: 2 total. If you need a third QR code on that plan, you have to upgrade. Higher plans allow more codes, but the pricing jumps quickly relative to what you get. QR Master\'s Pro plan (€9/month) includes 50 dynamic QR codes with full analytics and no scan caps on redirects.',
|
||||
},
|
||||
{
|
||||
question: 'Is Bitly good for QR code management?',
|
||||
answer:
|
||||
'Bitly works for QR codes in the sense that it can generate them and track clicks. But the product is built around link management and URL shortening — QR codes are a secondary feature. The workflow, the dashboard, and the pricing model are all designed around links, not QR code-specific use cases like restaurant menus, product packaging, event materials, or bulk creation for print campaigns. If QR codes are your primary use case, a purpose-built platform handles the workflow better.',
|
||||
'Bitly works for QR codes in the sense that it can generate them and track clicks. But the product is built around link management and URL shortening - QR codes are a secondary feature. The workflow, the dashboard, and the pricing model are all designed around links, not QR code-specific use cases like restaurant menus, product packaging, event materials, or bulk creation for print campaigns. If QR codes are your primary use case, a purpose-built platform handles the workflow better.',
|
||||
},
|
||||
{
|
||||
question: 'How does Bitly pricing compare to QR Master for QR codes?',
|
||||
answer:
|
||||
'Bitly\'s free plan allows only 1 QR code. Their Core plan (~$10/month) allows 2 QR codes. Higher plans add more codes but pricing escalates steeply. QR Master\'s free plan includes 3 active dynamic QR codes and unlimited static codes. Pro at €9/month includes 50 dynamic codes with full analytics. Business at €29/month includes 500 codes and bulk creation of up to 1,000 at once. Neither the Pro nor Business plan caps QR code redirects by scan volume.',
|
||||
'Bitly\'s free plan allows only 1 QR code. Their Core plan (~$10/month) allows 2 QR codes. Higher plans add more codes but pricing escalates steeply. QR Master\'s free plan includes 3 active dynamic QR codes and unlimited static codes. Pro at €9/month includes 50 dynamic codes with full analytics. Business at €29/month includes 500 codes and bulk creation of up to 1,000 static codes, or up to 500 dynamic ones at once. Neither the Pro nor Business plan caps QR code redirects by scan volume.',
|
||||
},
|
||||
{
|
||||
question: 'Does QR Master have link shortening like Bitly?',
|
||||
@@ -92,12 +92,12 @@ const faqItems = [
|
||||
{
|
||||
question: 'Can I create QR codes in bulk on QR Master in a way Bitly can\'t?',
|
||||
answer:
|
||||
'Yes. QR Master Business (€29/month) supports CSV and Excel upload for bulk creation of up to 1,000 unique QR codes per batch. Each code in the batch can have a different destination URL, label, campaign name, and UTM parameters. Bitly does not offer bulk QR creation at any plan tier.',
|
||||
'Yes. QR Master Business (€29/month) supports CSV and Excel upload for bulk creation: up to 1,000 static codes, or up to 500 dynamic ones per batch. Each code in the batch can have a different destination URL, label, campaign name, and UTM parameters. Bitly does not offer bulk QR creation at any plan tier.',
|
||||
},
|
||||
{
|
||||
question: 'Does QR Master comply with GDPR for scan analytics?',
|
||||
answer:
|
||||
'Yes. QR Master hashes IP addresses server-side before any scan data is stored. No raw IP address is ever written to the database. Analytics capture device type, scan time, country-level location, and UTM parameters — all without storing personally identifiable information. This is built into the infrastructure and applies from the free plan upward. Bitly is a US company with its own analytics approach — EU businesses should review their DPA for GDPR compliance.',
|
||||
'Yes. QR Master hashes IP addresses server-side before any scan data is stored. No raw IP address is ever written to the database. Analytics capture device type, scan time, country-level location, and UTM parameters - all without storing personally identifiable information. This is built into the infrastructure and applies from the free plan upward. Bitly is a US company with its own analytics approach - EU businesses should review their DPA for GDPR compliance.',
|
||||
},
|
||||
{
|
||||
question: 'What happens to my Bitly QR codes if I cancel?',
|
||||
@@ -131,21 +131,21 @@ const relatedLinks = [
|
||||
href: '/dynamic-qr-code-generator',
|
||||
title: 'Dynamic QR Code Generator',
|
||||
description:
|
||||
'Create QR codes built specifically for print campaigns, menus, packaging, and events — with no scan limits and updateable destinations.',
|
||||
'Create QR codes built specifically for print campaigns, menus, packaging, and events - with no scan limits and updateable destinations.',
|
||||
ctaLabel: 'Create your first QR code',
|
||||
},
|
||||
{
|
||||
href: '/qr-code-analytics',
|
||||
title: 'QR Code Analytics',
|
||||
description:
|
||||
'See scan counts, device types, locations, and UTM attribution for every QR code — with no caps and no upgrades required to see your own data.',
|
||||
'See scan counts, device types, locations, and UTM attribution for every QR code - with no caps and no upgrades required to see your own data.',
|
||||
ctaLabel: 'Explore analytics',
|
||||
},
|
||||
{
|
||||
href: '/bulk-qr-code-generator',
|
||||
title: 'Bulk QR Code Generator',
|
||||
description:
|
||||
'Create up to 1,000 unique QR codes from a CSV or Excel file. Each with its own URL, label, and tracking parameters. No manual creation one-by-one.',
|
||||
'One upload, a whole batch: up to 1,000 static codes or up to 500 dynamic ones. Each with its own URL, label, and tracking parameters. No creating them one by one.',
|
||||
ctaLabel: 'Explore bulk QR creation',
|
||||
},
|
||||
{
|
||||
@@ -186,18 +186,18 @@ export default function BitlyAlternativePage() {
|
||||
A Bitly Alternative That Actually Lets You Create QR Codes
|
||||
</h1>
|
||||
<p className="text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
Bitly is a link shortener. QR codes are a secondary feature — and their Core plan charges $10/month
|
||||
Bitly is a link shortener. QR codes are a secondary feature - and their Core plan charges $10/month
|
||||
for just 2 QR codes total. QR Master is built specifically for QR code management: 50 dynamic codes
|
||||
at €9/month, bulk creation, and GDPR-compliant analytics.
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mb-10 space-y-3">
|
||||
{[
|
||||
'50 dynamic QR codes at €9/month — not 2 codes for $10',
|
||||
'50 dynamic QR codes at €9/month - not 2 codes for $10',
|
||||
'Free plan: 3 dynamic QR codes + unlimited static codes, €0',
|
||||
'Bulk creation from CSV/Excel up to 1,000 codes (Business plan)',
|
||||
'Bulk creation from CSV/Excel: 1,000 static or 500 dynamic (Business)',
|
||||
'Built for QR-specific workflows: menus, packaging, events, campaigns',
|
||||
'GDPR-compliant analytics with hashed IPs — no configuration needed',
|
||||
'GDPR-compliant analytics with hashed IPs - no configuration needed',
|
||||
].map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-3">
|
||||
<span
|
||||
@@ -243,7 +243,7 @@ export default function BitlyAlternativePage() {
|
||||
<div className="space-y-5">
|
||||
<div className="rounded-xl border p-5" style={{ borderColor: '#FECACA', backgroundColor: '#FEF2F2' }}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="font-semibold text-red-800">Bitly Core — ~$10/month</span>
|
||||
<span className="font-semibold text-red-800">Bitly Core - ~$10/month</span>
|
||||
<span className="rounded-full bg-red-100 px-3 py-1 text-sm font-bold text-red-700">2 QR codes</span>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
@@ -258,11 +258,11 @@ export default function BitlyAlternativePage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-red-600">Marketed as "unlimited scans" — but you only get 2 codes total. Need a third? Upgrade.</p>
|
||||
<p className="mt-3 text-xs text-red-600">Marketed as "unlimited scans" - but you only get 2 codes total. Need a third? Upgrade.</p>
|
||||
</div>
|
||||
<div className="rounded-xl border p-5" style={{ borderColor: '#BBF7D0', backgroundColor: '#F0FDF4' }}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="font-semibold text-purple-800">QR Master Pro — €9/month</span>
|
||||
<span className="font-semibold text-purple-800">QR Master Pro - €9/month</span>
|
||||
<span className="rounded-full bg-purple-100 px-3 py-1 text-sm font-bold text-purple-700">50 QR codes</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -333,25 +333,25 @@ export default function BitlyAlternativePage() {
|
||||
Bitly is excellent at what it was designed for: shortening URLs for social media posts, email
|
||||
campaigns, and marketing links where you need a clean, short address. That core product is solid and
|
||||
widely used. The problem starts when QR codes get added as a secondary feature inside a link
|
||||
management tool — the pricing model and workflow both reflect the link-first design.
|
||||
management tool - the pricing model and workflow both reflect the link-first design.
|
||||
</p>
|
||||
<p>
|
||||
The most glaring issue with Bitly for QR codes is the code count cap. Bitly's Core plan
|
||||
(~$10/month) is marketed around "unlimited clicks and scans" — which sounds generous.
|
||||
But that plan allows a total of <strong>2 QR codes</strong>. Two. If you need a third QR code —
|
||||
for a second product, a second location, or a second campaign — you have to jump to a more expensive
|
||||
(~$10/month) is marketed around "unlimited clicks and scans" - which sounds generous.
|
||||
But that plan allows a total of <strong>2 QR codes</strong>. Two. If you need a third QR code -
|
||||
for a second product, a second location, or a second campaign - you have to jump to a more expensive
|
||||
plan. For teams running any meaningful QR code operation, the code count wall is the first thing
|
||||
you hit, not the scan volume.
|
||||
</p>
|
||||
<p>
|
||||
The free plan allows exactly 1 QR code. For comparison, QR Master's free tier gives you 3
|
||||
active dynamic QR codes with basic analytics — and the Pro plan (€9/month) gives you 50. The
|
||||
active dynamic QR codes with basic analytics - and the Pro plan (€9/month) gives you 50. The
|
||||
economics of QR codes on Bitly force rapid upgrades the moment you have a campaign with more than
|
||||
a trivial number of placements.
|
||||
</p>
|
||||
<p>
|
||||
Beyond the code count, Bitly's QR workflow is an afterthought. The interface is built around
|
||||
link management — creating a short link is the primary action, and QR codes are generated as a
|
||||
link management - creating a short link is the primary action, and QR codes are generated as a
|
||||
secondary output from that. There is no bulk QR creation, no QR-specific analytics beyond click
|
||||
counts, and no purpose-built tooling for the workflows that QR codes actually live in: restaurant
|
||||
menus, product packaging, event programs, multi-location flyer campaigns.
|
||||
@@ -363,8 +363,8 @@ export default function BitlyAlternativePage() {
|
||||
>
|
||||
<h3 className="mb-2 text-lg font-semibold" style={{ color: '#C2410C' }}>When Bitly is still the right choice</h3>
|
||||
<p style={{ color: '#27272A' }}>
|
||||
If you already use Bitly heavily for link shortening and genuinely need only 1–2 QR codes with no
|
||||
expectation of growth — staying on Bitly is reasonable. Consolidating tools has value. The problem
|
||||
If you already use Bitly heavily for link shortening and genuinely need only 1-2 QR codes with no
|
||||
expectation of growth - staying on Bitly is reasonable. Consolidating tools has value. The problem
|
||||
starts the moment QR codes become a real part of your workflow, you need more than 2 codes, or you
|
||||
need bulk creation. At that point the pricing math stops making sense.
|
||||
</p>
|
||||
@@ -433,15 +433,15 @@ export default function BitlyAlternativePage() {
|
||||
<div>
|
||||
<h3 className="mb-4 text-xl font-bold" style={{ color: '#18181B' }}>Code count limits and pricing at scale</h3>
|
||||
<p className="text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
Bitly's Core plan (~$10/month) advertises "unlimited scans" — which is technically
|
||||
Bitly's Core plan (~$10/month) advertises "unlimited scans" - which is technically
|
||||
accurate but misleading. The hard limit on that plan is the number of QR codes: 2 total. The free
|
||||
plan gives you 1. For most marketing use cases, running into the code count wall happens before
|
||||
scan volume ever becomes an issue.
|
||||
</p>
|
||||
<p className="mt-4 text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
QR Master Pro (€9/month) gives you 50 active dynamic QR codes with no scan limits on redirects.
|
||||
Business (€29/month) gives you 500 codes plus bulk creation of up to 1,000 unique codes from a
|
||||
single CSV upload. The pricing model is built around QR code management, not link click volume —
|
||||
Business (€29/month) gives you 500 dynamic codes plus bulk creation of up to 1,000 static codes from a
|
||||
single CSV upload. The pricing model is built around QR code management, not link click volume -
|
||||
which means costs are predictable and don't scale with campaign success.
|
||||
</p>
|
||||
</div>
|
||||
@@ -449,14 +449,14 @@ export default function BitlyAlternativePage() {
|
||||
<div>
|
||||
<h3 className="mb-4 text-xl font-bold" style={{ color: '#18181B' }}>QR-specific workflow support</h3>
|
||||
<p className="text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
QR Master is designed around QR code workflows — not link management. This means the platform has
|
||||
QR Master is designed around QR code workflows - not link management. This means the platform has
|
||||
purpose-built generators for specific QR code types: WiFi QR codes, vCard QR codes, restaurant menu
|
||||
QR codes, PDF QR codes, and more. Each type has a tailored input form and generates the correct QR
|
||||
format for that use case. Bitly generates a URL-based QR code — that's the only type available.
|
||||
format for that use case. Bitly generates a URL-based QR code - that's the only type available.
|
||||
</p>
|
||||
<p className="mt-4 text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
Bulk creation is another gap. If you are creating QR codes for a product line, an event with
|
||||
multiple sessions, or a direct mail campaign — creating them one at a time is not viable. QR
|
||||
multiple sessions, or a direct mail campaign - creating them one at a time is not viable. QR
|
||||
Master's Business plan generates up to 1,000 unique codes from a single CSV upload. Bitly has
|
||||
no equivalent.
|
||||
</p>
|
||||
@@ -465,7 +465,7 @@ export default function BitlyAlternativePage() {
|
||||
<div>
|
||||
<h3 className="mb-4 text-xl font-bold" style={{ color: '#18181B' }}>Analytics depth for QR use cases</h3>
|
||||
<p className="text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
Bitly tracks clicks — that is its core analytics model. For QR codes, it reports scan counts in
|
||||
Bitly tracks clicks - that is its core analytics model. For QR codes, it reports scan counts in
|
||||
the same way it reports link clicks. There is no device-type breakdown specific to mobile QR
|
||||
scanning, no distinction between campaign placements, and no UTM parameter injection designed
|
||||
for QR workflows.
|
||||
@@ -473,7 +473,7 @@ export default function BitlyAlternativePage() {
|
||||
<p className="mt-4 text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
QR Master analytics are built around the QR scan as the unit of measurement. Each scan records
|
||||
device type, operating system, country-level location, time, and UTM parameters. The dashboard is
|
||||
organized around QR codes — not links — so you can see scan patterns per code, per campaign, and
|
||||
organized around QR codes - not links - so you can see scan patterns per code, per campaign, and
|
||||
over time in a way that makes sense for print and physical media distribution.
|
||||
</p>
|
||||
</div>
|
||||
@@ -494,7 +494,7 @@ export default function BitlyAlternativePage() {
|
||||
{[
|
||||
'Anyone who hit Bitly\'s code count cap and had to upgrade just to add a third QR code',
|
||||
'Marketing teams needing more than 2 QR codes for under $50/month',
|
||||
'Teams creating QR codes for product packaging, event programs, or retail displays — use cases Bitly has no specific tooling for',
|
||||
'Teams creating QR codes for product packaging, event programs, or retail displays - use cases Bitly has no specific tooling for',
|
||||
'Anyone needing bulk QR creation from CSV or Excel',
|
||||
'EU businesses that need GDPR-compliant tracking without extra configuration',
|
||||
].map((item) => (
|
||||
@@ -512,7 +512,7 @@ export default function BitlyAlternativePage() {
|
||||
</div>
|
||||
<ul className="mt-4 space-y-3">
|
||||
{[
|
||||
'You use Bitly primarily for link shortening and genuinely need only 1–2 QR codes — the cost of a separate QR tool doesn\'t justify the switch',
|
||||
'You use Bitly primarily for link shortening and genuinely need only 1-2 QR codes - the cost of a separate QR tool doesn\'t justify the switch',
|
||||
'You are already on a Bitly enterprise plan and QR codes are a minor part of a broader link management workflow that uses other Bitly features heavily',
|
||||
'Your QR code count stays within Bitly\'s plan limits and you have no bulk creation needs',
|
||||
].map((item) => (
|
||||
@@ -567,7 +567,7 @@ export default function BitlyAlternativePage() {
|
||||
{
|
||||
step: '5',
|
||||
title: 'Plan physical material replacement',
|
||||
body: 'For anything printed — flyers, packaging, business cards, menus — plan replacement into your next print run. Keep your Bitly account active until all physical materials are replaced in circulation.',
|
||||
body: 'For anything printed - flyers, packaging, business cards, menus - plan replacement into your next print run. Keep your Bitly account active until all physical materials are replaced in circulation.',
|
||||
},
|
||||
].map((step) => (
|
||||
<div key={step.step} className="flex gap-6 py-7" style={{ borderBottom: step.step !== '5' ? '1px solid #E4E0D9' : 'none' }}>
|
||||
@@ -596,7 +596,7 @@ export default function BitlyAlternativePage() {
|
||||
<GrowthLinksSection
|
||||
eyebrow="Related pages"
|
||||
title="Explore QR Master"
|
||||
description="See the features purpose-built for QR code workflows — not link shortening with QR as a side feature."
|
||||
description="See the features purpose-built for QR code workflows - not link shortening with QR as a side feature."
|
||||
links={relatedLinks}
|
||||
pageType="commercial"
|
||||
cluster="competitor"
|
||||
@@ -612,7 +612,7 @@ export default function BitlyAlternativePage() {
|
||||
<h2 className="mb-4 text-4xl font-bold">50 QR codes at €9. Not 2 codes for $10.</h2>
|
||||
<p className="mx-auto mb-10 max-w-2xl text-lg" style={{ color: '#A1A1AA' }}>
|
||||
Start free with 3 dynamic QR codes. Pro at €9/month for 50 codes with full analytics.
|
||||
Purpose-built for QR code workflows — not link shortening with QR as an afterthought.
|
||||
Purpose-built for QR code workflows - not link shortening with QR as an afterthought.
|
||||
</p>
|
||||
<div className="flex flex-col justify-center gap-4 sm:flex-row">
|
||||
<TrackedCtaLink
|
||||
|
||||
@@ -17,7 +17,7 @@ export const metadata: Metadata = {
|
||||
absolute: 'QR Master vs Flowcode | Flowcode Alternative Without Forced Branding',
|
||||
},
|
||||
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.',
|
||||
keywords:
|
||||
'flowcode alternative, flowcode branding removal, flowcode white label, flowcode interstitial, flowcode pricing alternative',
|
||||
alternates: {
|
||||
@@ -49,7 +49,7 @@ const atAGlanceRows = [
|
||||
{
|
||||
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.',
|
||||
qrMaster: 'Colors are free on every plan. Module shapes and logo start on Pro at EUR 9/month.',
|
||||
},
|
||||
{
|
||||
useCase: 'Direct scan experience',
|
||||
@@ -59,7 +59,7 @@ const atAGlanceRows = [
|
||||
{
|
||||
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.',
|
||||
qrMaster: 'Business bulk upload: up to 1,000 static codes, or up to 500 dynamic ones.',
|
||||
},
|
||||
{
|
||||
useCase: 'EU privacy posture',
|
||||
@@ -72,37 +72,37 @@ const faqItems = [
|
||||
{
|
||||
question: 'What is the Flowcode interstitial page and why does it matter?',
|
||||
answer:
|
||||
'On Flowcode\'s free tier, when someone scans your QR code, they are briefly shown a Flowcode-branded page before being redirected to your destination. This interstitial serves Flowcode\'s branding to your audience — effectively using your QR code placement to advertise their product. It also affects scan tracking: the interstitial is the page being counted, which can distort your analytics. QR Master sends scanners directly to your destination with no intermediate branded page at any plan level.',
|
||||
'On Flowcode\'s free tier, when someone scans your QR code, they are briefly shown a Flowcode-branded page before being redirected to your destination. This interstitial serves Flowcode\'s branding to your audience - effectively using your QR code placement to advertise their product. It also affects scan tracking: the interstitial is the page being counted, which can distort your analytics. QR Master sends scanners directly to your destination with no intermediate branded page at any plan level.',
|
||||
},
|
||||
{
|
||||
question: 'Does Flowcode put its logo on QR codes in the free tier?',
|
||||
answer:
|
||||
'Yes. Flowcode\'s free tier applies a distinctive round design with Flowcode branding elements. The visual style is recognizable as a Flowcode product, not a neutral QR code. If you want a standard QR code that looks like your brand rather than Flowcode\'s, you need a paid plan. QR Master allows custom colors and logo embedding from the Pro plan (€9/month) and generates standard QR codes — without third-party branding — even on the free tier.',
|
||||
'Yes. Flowcode\'s free tier applies a distinctive round design with Flowcode branding elements. The visual style is recognizable as a Flowcode product, not a neutral QR code. If you want a standard QR code that looks like your brand rather than Flowcode\'s, you need a paid plan. QR Master gives you full color control on the free plan and generates standard QR codes - without third-party branding - at every tier. Module shapes and logo embedding start on Pro (€9/month).',
|
||||
},
|
||||
{
|
||||
question: 'How much does Flowcode cost for white-label QR codes?',
|
||||
answer:
|
||||
'Flowcode\'s pricing for meaningful white-label starts around $49/month. Full team features, brand control, and removal of Flowcode branding typically require their higher-tier plans. QR Master Pro at €9/month includes custom colors, logo embedding, and fully branded QR codes — no Flowcode equivalent visible anywhere.',
|
||||
'Flowcode\'s pricing for meaningful white-label starts around $49/month. Full team features, brand control, and removal of Flowcode branding typically require their higher-tier plans. Colors are free on QR Master. Pro at €9/month adds module shapes and logo embedding, with no QR Master branding visible anywhere at any tier.',
|
||||
},
|
||||
{
|
||||
question: 'Does Flowcode offer bulk QR code creation?',
|
||||
answer:
|
||||
'Flowcode does not have a built-in bulk creation feature for generating many unique codes at once. QR Master Business (€29/month) includes CSV/Excel bulk upload to generate up to 1,000 unique QR codes per batch — each with a different destination URL, label, and UTM parameters.',
|
||||
'Flowcode does not have a built-in bulk creation feature for generating many unique codes at once. QR Master Business (€29/month) includes CSV/Excel bulk upload: up to 1,000 static codes, or up to 500 dynamic ones per batch - each with a different destination URL, label, and UTM parameters.',
|
||||
},
|
||||
{
|
||||
question: 'Is Flowcode GDPR-compliant?',
|
||||
answer:
|
||||
'Flowcode is a US company. GDPR compliance depends on how they handle EU user data and whether their data processing agreements meet EU requirements. QR Master handles GDPR compliance at the infrastructure level: IP addresses are hashed server-side before storage, no personally identifiable scan data is retained, and analytics use anonymized signals only. This is not a setting to enable — it is how the platform works.',
|
||||
'Flowcode is a US company. GDPR compliance depends on how they handle EU user data and whether their data processing agreements meet EU requirements. QR Master handles GDPR compliance at the infrastructure level: IP addresses are hashed server-side before storage, no personally identifiable scan data is retained, and analytics use anonymized signals only. This is not a setting to enable - it is how the platform works.',
|
||||
},
|
||||
{
|
||||
question: 'What happens to my Flowcode QR codes if I cancel?',
|
||||
answer:
|
||||
'Flowcode QR codes point to Flowcode\'s redirect infrastructure. If you cancel your paid plan and drop to the free tier, your QR codes may revert to showing the Flowcode-branded interstitial again. If you close your account entirely, the redirects stop and your printed QR codes become dead ends. Plan your migration before canceling — switch to QR Master and reprint or update digital placements before closing the Flowcode account.',
|
||||
'Flowcode QR codes point to Flowcode\'s redirect infrastructure. If you cancel your paid plan and drop to the free tier, your QR codes may revert to showing the Flowcode-branded interstitial again. If you close your account entirely, the redirects stop and your printed QR codes become dead ends. Plan your migration before canceling - switch to QR Master and reprint or update digital placements before closing the Flowcode account.',
|
||||
},
|
||||
{
|
||||
question: 'Can I import my Flowcode QR codes into QR Master?',
|
||||
answer:
|
||||
'There is no direct import — Flowcode\'s redirect infrastructure is separate from QR Master\'s. You need to re-create each dynamic QR code in QR Master with the same destination URLs. For bulk re-creation, QR Master\'s Business plan allows CSV upload so you can migrate many codes at once rather than one by one. Static QR codes are permanently encoded and do not need migration — they work independently of any platform.',
|
||||
'There is no direct import - Flowcode\'s redirect infrastructure is separate from QR Master\'s. You need to re-create each dynamic QR code in QR Master with the same destination URLs. For bulk re-creation, QR Master\'s Business plan allows CSV upload so you can migrate many codes at once rather than one by one. Static QR codes are permanently encoded and do not need migration - they work independently of any platform.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -131,14 +131,14 @@ const relatedLinks = [
|
||||
href: '/dynamic-qr-code-generator',
|
||||
title: 'Dynamic QR Code Generator',
|
||||
description:
|
||||
'Create QR codes with your own branding — colors, logo, and design — that you can update after printing without replacing the code.',
|
||||
'Create QR codes with your own branding - colors, logo, and design - that you can update after printing without replacing the code.',
|
||||
ctaLabel: 'Create a branded dynamic QR code',
|
||||
},
|
||||
{
|
||||
href: '/qr-code-analytics',
|
||||
title: 'QR Code Analytics',
|
||||
description:
|
||||
'Track every scan with device, time, and location data — sent directly to your destination with no interstitial page in the way.',
|
||||
'Track every scan with device, time, and location data - sent directly to your destination with no interstitial page in the way.',
|
||||
ctaLabel: 'Explore QR analytics',
|
||||
},
|
||||
{
|
||||
@@ -152,7 +152,7 @@ const relatedLinks = [
|
||||
href: '/pricing',
|
||||
title: 'QR Master Pricing',
|
||||
description:
|
||||
'Free for 3 dynamic codes. Pro at €9/month includes 50 dynamic codes, custom branding, and advanced analytics.',
|
||||
'Free for 3 dynamic codes, with full color control. Pro at €9/month includes 50 dynamic codes, module shapes, logo and advanced analytics.',
|
||||
ctaLabel: 'See pricing',
|
||||
},
|
||||
];
|
||||
@@ -187,17 +187,17 @@ export default function FlowcodeAlternativePage() {
|
||||
</h1>
|
||||
<p className="text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
Flowcode's free tier puts their logo on every QR code and routes your scanners through a
|
||||
Flowcode-branded interstitial page. QR Master sends scanners directly to your destination — no
|
||||
Flowcode-branded interstitial page. QR Master sends scanners directly to your destination - no
|
||||
third-party branding, no interstitials, at any plan level.
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mb-10 space-y-3">
|
||||
{[
|
||||
'No Flowcode branding on your QR codes — even on the free plan',
|
||||
'No branded interstitial page — instant redirect, no Flowcode marketing in between',
|
||||
'Custom colors and logo from Pro (€9/month)',
|
||||
'Bulk creation up to 1,000 codes on the Business plan',
|
||||
'GDPR-compliant analytics with hashed IPs — built in, not a setting',
|
||||
'No Flowcode branding on your QR codes - even on the free plan',
|
||||
'No branded interstitial page - instant redirect, no Flowcode marketing in between',
|
||||
'Colors free, shapes and logo from Pro (€9/month)',
|
||||
'Bulk creation on Business: 1,000 static or 500 dynamic',
|
||||
'GDPR-compliant analytics with hashed IPs - built in, not a setting',
|
||||
].map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-3">
|
||||
<span
|
||||
@@ -313,20 +313,20 @@ export default function FlowcodeAlternativePage() {
|
||||
<div className="space-y-6 text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
<p>
|
||||
Flowcode is a well-built product with strong design capabilities. The problem is not the product
|
||||
itself — it's the business model on the free tier. Flowcode monetizes free users by using their QR
|
||||
itself - it's the business model on the free tier. Flowcode monetizes free users by using their QR
|
||||
code placements as advertising inventory for Flowcode's own brand.
|
||||
</p>
|
||||
<p>
|
||||
In practice, this means two things. First, the QR codes generated on the free plan are visually styled
|
||||
as Flowcode products — the distinctive round design with Flowcode design elements makes it clear to
|
||||
as Flowcode products - the distinctive round design with Flowcode design elements makes it clear to
|
||||
anyone familiar with the space that this is a Flowcode code, not a custom QR. If you're a
|
||||
restaurant, a brand, or an agency putting this code on client materials, it is your placement that
|
||||
Flowcode is using to advertise itself.
|
||||
</p>
|
||||
<p>
|
||||
Second — and more consequentially — Flowcode's free tier routes every scan through an interstitial
|
||||
Second - and more consequentially - Flowcode's free tier routes every scan through an interstitial
|
||||
page before the scanner reaches your destination. That page carries Flowcode branding. You are sending
|
||||
customers to your menu, product page, or campaign — but they pass through Flowcode's branded
|
||||
customers to your menu, product page, or campaign - but they pass through Flowcode's branded
|
||||
experience first. The customer's first impression is Flowcode, not you.
|
||||
</p>
|
||||
<p>
|
||||
@@ -342,8 +342,8 @@ export default function FlowcodeAlternativePage() {
|
||||
<h3 className="mb-2 text-lg font-semibold" style={{ color: '#1D4ED8' }}>How QR Master handles this</h3>
|
||||
<p style={{ color: '#27272A' }}>
|
||||
QR Master does not apply third-party branding to QR codes at any plan level. The free tier generates
|
||||
standard QR codes — without a QR Master logo, without a forced visual style, and without an interstitial
|
||||
page. Scanners go directly to your destination. Custom colors and logo embedding are available on Pro
|
||||
standard QR codes - without a QR Master logo, without a forced visual style, and without an interstitial
|
||||
page. Scanners go directly to your destination. Colors are free on every plan. Module shapes and logo embedding start on Pro
|
||||
(€9/month). White-label and your brand are the baseline, not an upgrade.
|
||||
</p>
|
||||
</div>
|
||||
@@ -411,14 +411,14 @@ export default function FlowcodeAlternativePage() {
|
||||
<div>
|
||||
<h3 className="mb-4 text-xl font-bold" style={{ color: '#18181B' }}>Branding control</h3>
|
||||
<p className="text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
Flowcode's free QR codes are visually distinct — the rounded, branded design is recognizable.
|
||||
Flowcode's free QR codes are visually distinct - the rounded, branded design is recognizable.
|
||||
If you are an agency, a restaurant, or a brand putting these on client materials, the Flowcode
|
||||
aesthetic tells your audience that this is a Flowcode product. White-label — where the code looks
|
||||
like yours — requires a paid plan.
|
||||
aesthetic tells your audience that this is a Flowcode product. White-label - where the code looks
|
||||
like yours - requires a paid plan.
|
||||
</p>
|
||||
<p className="mt-4 text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
QR Master generates standard QR codes on all plans. On the free plan, you get a clean standard QR.
|
||||
On Pro (€9/month), you add your brand colors and logo to the center of the code. The baseline is
|
||||
Brand colors are free on every plan. On Pro (€9/month), you add module shapes and your logo in the center of the code. The baseline is
|
||||
always a neutral code that belongs to your brand, not ours.
|
||||
</p>
|
||||
</div>
|
||||
@@ -427,17 +427,17 @@ export default function FlowcodeAlternativePage() {
|
||||
<h3 className="mb-4 text-xl font-bold" style={{ color: '#18181B' }}>Scan experience and interstitials</h3>
|
||||
<p className="text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
The interstitial is a real issue for anyone using QR codes in a customer-facing context. A scanner
|
||||
at a restaurant table or on a product package is primed to go directly to the destination — a menu,
|
||||
at a restaurant table or on a product package is primed to go directly to the destination - a menu,
|
||||
a product page, a contact form. An intermediate page breaks that expectation, even if it only lasts
|
||||
a second or two. It's also a branding signal: Flowcode appears in the path between your brand
|
||||
and your customer.
|
||||
</p>
|
||||
<p className="mt-4 text-lg leading-relaxed" style={{ color: '#52525B' }}>
|
||||
QR Master does not show a branded interstitial page. Like any dynamic QR platform, the redirect
|
||||
runs through QR Master's servers (qrmaster.net) to log the scan — but the scanner sees no marketing
|
||||
runs through QR Master's servers (qrmaster.net) to log the scan - but the scanner sees no marketing
|
||||
content, no QR Master splash page, and no dwell-time promotion. It processes the scan and forwards
|
||||
immediately. The visual and branding of the QR code itself is fully customizable — colors, logo,
|
||||
shape — without any QR Master identity imposed on it.
|
||||
immediately. The visual and branding of the QR code itself is fully customizable - colors, logo,
|
||||
shape - without any QR Master identity imposed on it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -472,7 +472,7 @@ export default function FlowcodeAlternativePage() {
|
||||
'Brands that want QR codes to reflect their identity, not a third-party platform',
|
||||
'Agencies putting QR codes on client materials who can\'t have Flowcode branding visible',
|
||||
'EU businesses that need GDPR-compliant scan tracking without configuration',
|
||||
'Teams that need bulk QR creation — Flowcode has no bulk generation tool',
|
||||
'Teams that need bulk QR creation - Flowcode has no bulk generation tool',
|
||||
'Anyone priced out of Flowcode\'s white-label tier but needing clean, functional QR codes',
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-start gap-2 text-gray-700">
|
||||
@@ -540,7 +540,7 @@ export default function FlowcodeAlternativePage() {
|
||||
{
|
||||
step: '5',
|
||||
title: 'Plan the physical reprint',
|
||||
body: 'For printed materials — menus, flyers, packaging — plan the replacement into your next natural reprint cycle. Keep your Flowcode account active until the reprint is done and distributed.',
|
||||
body: 'For printed materials - menus, flyers, packaging - plan the replacement into your next natural reprint cycle. Keep your Flowcode account active until the reprint is done and distributed.',
|
||||
},
|
||||
].map((step) => (
|
||||
<div key={step.step} className="flex gap-6 py-7" style={{ borderBottom: step.step !== '5' ? '1px solid #E4E0D9' : 'none' }}>
|
||||
|
||||
@@ -14,17 +14,17 @@ const competitor = competitors['qr-code-generator'];
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'QR-Code-Generator.com Alternative – No Bait and Switch | QR Master',
|
||||
absolute: 'QR-Code-Generator.com Alternative - No Bait and Switch | QR Master',
|
||||
},
|
||||
description:
|
||||
'Looking for a QR-Code-Generator.com alternative? QR Master gives you 3 truly free dynamic QR codes — no trial that expires mid-campaign, no forced annual contracts. Transparent pricing from €0.',
|
||||
'Looking for a QR-Code-Generator.com alternative? QR Master gives you 3 truly free dynamic QR codes - no trial that expires mid-campaign, no forced annual contracts. Transparent pricing from €0.',
|
||||
keywords:
|
||||
'qr-code-generator.com alternative, alternative to qr code generator, qr code generator free expired, dynamic qr code deactivated, qr code bait switch',
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/alternatives/qr-code-generator',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'QR-Code-Generator.com Alternative – No Bait and Switch',
|
||||
title: 'QR-Code-Generator.com Alternative - No Bait and Switch',
|
||||
description:
|
||||
'Your dynamic QR code stopped working after two weeks? QR Master offers 3 permanently free dynamic codes, honest pricing, and no hidden trial timers.',
|
||||
url: 'https://www.qrmaster.net/alternatives/qr-code-generator',
|
||||
@@ -32,7 +32,7 @@ export const metadata: Metadata = {
|
||||
images: ['/og-image.png'],
|
||||
},
|
||||
twitter: {
|
||||
title: 'QR-Code-Generator.com Alternative – No Bait and Switch',
|
||||
title: 'QR-Code-Generator.com Alternative - No Bait and Switch',
|
||||
description:
|
||||
'Your dynamic QR code stopped working after two weeks? QR Master offers 3 permanently free dynamic codes and honest pricing.',
|
||||
},
|
||||
@@ -44,37 +44,37 @@ const faqItems = [
|
||||
{
|
||||
question: 'Why did my dynamic QR code from QR-Code-Generator.com stop working?',
|
||||
answer:
|
||||
'QR-Code-Generator.com offers dynamic QR codes on a free trial basis — typically around 14 days. After the trial ends, the code is deactivated. Your printed materials (flyers, menus, packaging) become dead ends. To reactivate, they require purchasing an annual subscription. QR Master does not do this: the 3 free dynamic codes on our free plan stay active as long as your account exists.',
|
||||
'QR-Code-Generator.com offers dynamic QR codes on a free trial basis - typically around 14 days. After the trial ends, the code is deactivated. Your printed materials (flyers, menus, packaging) become dead ends. To reactivate, they require purchasing an annual subscription. QR Master does not do this: the 3 free dynamic codes on our free plan stay active as long as your account exists.',
|
||||
},
|
||||
{
|
||||
question: 'Can I switch from QR-Code-Generator.com without reprinting everything?',
|
||||
answer:
|
||||
'Dynamic QR codes cannot be migrated directly because the destination URL is encoded into the QR code image itself — each provider uses their own redirect infrastructure. If you are still within the deactivation period, create new dynamic codes in QR Master immediately and update your placements (digital ones) or plan your next reprint run. Static QR codes you created on QR-Code-Generator.com remain permanently valid regardless of your subscription status.',
|
||||
'Dynamic QR codes cannot be migrated directly because the destination URL is encoded into the QR code image itself - each provider uses their own redirect infrastructure. If you are still within the deactivation period, create new dynamic codes in QR Master immediately and update your placements (digital ones) or plan your next reprint run. Static QR codes you created on QR-Code-Generator.com remain permanently valid regardless of your subscription status.',
|
||||
},
|
||||
{
|
||||
question: 'What does QR Master give me for free, permanently?',
|
||||
answer:
|
||||
'The QR Master free plan includes 3 active dynamic QR codes with basic scan tracking and unlimited static QR codes — no trial period, no credit card required, no expiration. The 3 dynamic codes are always active. If you need more, Pro starts at €9/month for 50 dynamic codes with full analytics.',
|
||||
'The QR Master free plan includes 3 active dynamic QR codes with basic scan tracking and unlimited static QR codes - no trial period, no credit card required, no expiration. The 3 dynamic codes are always active. If you need more, Pro starts at €9/month for 50 dynamic codes with full analytics.',
|
||||
},
|
||||
{
|
||||
question: 'Is the free plan at QR Master really free, or will it expire like QR-Code-Generator.com?',
|
||||
answer:
|
||||
'The free tier is permanently free within the defined limits. There is no 14-day clock, no activation fee, no "trial" framing. The 3 dynamic codes on the free plan continue working as long as your account is active. We make money from Pro (€9/month) and Business (€29/month) upgrades — not from deactivating free users after they\'ve already printed materials.',
|
||||
'The free tier is permanently free within the defined limits. There is no 14-day clock, no activation fee, no "trial" framing. The 3 dynamic codes on the free plan continue working as long as your account is active. We make money from Pro (€9/month) and Business (€29/month) upgrades - not from deactivating free users after they\'ve already printed materials.',
|
||||
},
|
||||
{
|
||||
question: 'Does QR Master comply with GDPR for scan analytics?',
|
||||
answer:
|
||||
'Yes. QR Master anonymizes IP addresses using server-side hashing with a salt before any analytics data is stored. No personally identifiable IP addresses are recorded. Scan data includes device type, time, country-level location, and UTM parameters — all without storing raw IPs. This is built into the platform, not a bolt-on option.',
|
||||
'Yes. QR Master anonymizes IP addresses using server-side hashing with a salt before any analytics data is stored. No personally identifiable IP addresses are recorded. Scan data includes device type, time, country-level location, and UTM parameters - all without storing raw IPs. This is built into the platform, not a bolt-on option.',
|
||||
},
|
||||
{
|
||||
question: 'What happens to my QR codes if I cancel my QR Master subscription?',
|
||||
answer:
|
||||
'If you downgrade from a paid plan to Free, your dynamic codes are paused (not deleted) if you exceed the 3-code free limit. You choose which 3 to keep active. Static codes are unaffected and remain permanently valid. If you close your account entirely, dynamic codes stop redirecting — which is why we recommend switching to static QR codes for any permanent materials that you cannot update.',
|
||||
'If you downgrade from a paid plan to Free, your dynamic codes are paused (not deleted) if you exceed the 3-code free limit. You choose which 3 to keep active. Static codes are unaffected and remain permanently valid. If you close your account entirely, dynamic codes stop redirecting - which is why we recommend switching to static QR codes for any permanent materials that you cannot update.',
|
||||
},
|
||||
{
|
||||
question: 'Does QR Master support bulk QR code creation?',
|
||||
answer:
|
||||
'Yes. The Business plan (€29/month) includes bulk creation via CSV or Excel upload — up to 1,000 unique QR codes per batch. Each code can have a different destination URL, label, and UTM parameters. QR-Code-Generator.com does not offer bulk creation at any tier.',
|
||||
'Yes. The Business plan (€29/month) includes bulk creation via CSV or Excel upload: up to 1,000 static codes, or up to 500 dynamic ones per batch. Each code can have a different destination URL, label, and UTM parameters. QR-Code-Generator.com does not offer bulk creation at any tier.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -110,7 +110,7 @@ const relatedLinks = [
|
||||
href: '/qr-code-analytics',
|
||||
title: 'QR Code Analytics',
|
||||
description:
|
||||
'See which placements drive scans, which devices your audience uses, and where your codes are being scanned — all in one dashboard.',
|
||||
'See which placements drive scans, which devices your audience uses, and where your codes are being scanned - all in one dashboard.',
|
||||
ctaLabel: 'Explore QR code analytics',
|
||||
},
|
||||
{
|
||||
@@ -158,17 +158,17 @@ export default function QRCodeGeneratorAlternativePage() {
|
||||
</h1>
|
||||
|
||||
<p className="text-lg leading-relaxed mb-8" style={{ color: '#52525B' }}>
|
||||
QR-Code-Generator.com deactivates dynamic QR codes after roughly two weeks — right after you've
|
||||
QR-Code-Generator.com deactivates dynamic QR codes after roughly two weeks - right after you've
|
||||
printed the flyers. QR Master gives you 3 free dynamic codes that stay active permanently.
|
||||
No hidden trial, no forced annual contract.
|
||||
</p>
|
||||
|
||||
<ul className="space-y-3 mb-10">
|
||||
{[
|
||||
'3 permanently active dynamic QR codes — free forever',
|
||||
'3 permanently active dynamic QR codes - free forever',
|
||||
'Transparent pricing: Pro at €9/mo, cancel anytime',
|
||||
'GDPR-compliant analytics with hashed IPs, built in',
|
||||
'Bulk creation up to 1,000 codes (Business plan)',
|
||||
'Bulk creation: 1,000 static or 500 dynamic (Business plan)',
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-start gap-3">
|
||||
<span
|
||||
@@ -185,13 +185,13 @@ export default function QRCodeGeneratorAlternativePage() {
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<TrackedCtaLink
|
||||
href="/signup"
|
||||
ctaLabel="Start Free — No Credit Card"
|
||||
ctaLabel="Start Free - No Credit Card"
|
||||
ctaLocation="hero_primary"
|
||||
pageType="commercial"
|
||||
cluster="competitor"
|
||||
>
|
||||
<Button size="lg" className="w-full h-13 px-8 text-base sm:w-auto">
|
||||
Start Free — No Credit Card
|
||||
Start Free - No Credit Card
|
||||
</Button>
|
||||
</TrackedCtaLink>
|
||||
<TrackedCtaLink
|
||||
@@ -292,7 +292,7 @@ export default function QRCodeGeneratorAlternativePage() {
|
||||
<div className="grid gap-10 md:grid-cols-2">
|
||||
<div className="space-y-5 text-base leading-relaxed" style={{ color: '#52525B' }}>
|
||||
<p>
|
||||
QR-Code-Generator.com markets dynamic QR codes as free to create. And they are — for about two weeks.
|
||||
QR-Code-Generator.com markets dynamic QR codes as free to create. And they are - for about two weeks.
|
||||
After roughly 14 days, those dynamic codes stop redirecting. Anyone who scans them sees a dead page
|
||||
or a prompt to upgrade.
|
||||
</p>
|
||||
@@ -303,7 +303,7 @@ export default function QRCodeGeneratorAlternativePage() {
|
||||
</p>
|
||||
<p>
|
||||
Hundreds of reviews on Trustpilot describe feeling “trapped” because the alternative
|
||||
— replacing all the printed materials — is more expensive. The annual plan costs around
|
||||
- replacing all the printed materials - is more expensive. The annual plan costs around
|
||||
€25.99/month billed yearly, over €300 upfront.
|
||||
</p>
|
||||
</div>
|
||||
@@ -320,7 +320,7 @@ export default function QRCodeGeneratorAlternativePage() {
|
||||
no countdown, no automatic deactivation.
|
||||
</p>
|
||||
<p className="text-base leading-relaxed" style={{ color: '#27272A' }}>
|
||||
If you need more than 3, you upgrade to Pro at €9/month — month-to-month with no forced annual
|
||||
If you need more than 3, you upgrade to Pro at €9/month - month-to-month with no forced annual
|
||||
commitment. Static codes are unlimited and free forever.
|
||||
</p>
|
||||
</div>
|
||||
@@ -396,7 +396,7 @@ export default function QRCodeGeneratorAlternativePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Detailed Comparisons — 3 cards */}
|
||||
{/* Detailed Comparisons - 3 cards */}
|
||||
<section className="py-24" style={{ backgroundColor: '#F8F7F4' }}>
|
||||
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight mb-12" style={{ color: '#111110' }}>
|
||||
@@ -407,7 +407,7 @@ export default function QRCodeGeneratorAlternativePage() {
|
||||
{[
|
||||
{
|
||||
label: 'Pricing transparency',
|
||||
body: 'QR-Code-Generator.com lists dynamic codes as free — true for the 14-day trial. Reactivation requires an annual plan billed upfront (€100–€300+).',
|
||||
body: 'QR-Code-Generator.com lists dynamic codes as free - true for the 14-day trial. Reactivation requires an annual plan billed upfront (€100-€300+).',
|
||||
highlight: 'QR Master pricing is explicit: Free for 3 dynamic codes, Pro at €9/month. No fine print about trial periods or forced billing cycles.',
|
||||
accent: '#D97706',
|
||||
},
|
||||
@@ -456,7 +456,7 @@ export default function QRCodeGeneratorAlternativePage() {
|
||||
},
|
||||
{
|
||||
title: 'Create a free QR Master account',
|
||||
body: 'Sign up at qrmaster.net — no credit card required. The free plan gives you 3 active dynamic QR codes immediately.',
|
||||
body: 'Sign up at qrmaster.net - no credit card required. The free plan gives you 3 active dynamic QR codes immediately.',
|
||||
},
|
||||
{
|
||||
title: 'Re-create your dynamic codes',
|
||||
@@ -464,7 +464,7 @@ export default function QRCodeGeneratorAlternativePage() {
|
||||
},
|
||||
{
|
||||
title: 'Update digital placements first',
|
||||
body: 'Replace the QR code image on your website, email signatures, social media, and digital ads — no reprinting needed.',
|
||||
body: 'Replace the QR code image on your website, email signatures, social media, and digital ads - no reprinting needed.',
|
||||
},
|
||||
{
|
||||
title: 'Plan your reprint cycle',
|
||||
@@ -531,7 +531,7 @@ export default function QRCodeGeneratorAlternativePage() {
|
||||
</p>
|
||||
<ul className="space-y-4">
|
||||
{[
|
||||
'You only need static QR codes — both platforms generate these for free, and static codes never expire',
|
||||
'You only need static QR codes - both platforms generate these for free, and static codes never expire',
|
||||
'You need one quick QR code for a presentation or digital-only use where deactivation doesn\'t matter',
|
||||
'You\'re already on an active annual plan and aren\'t printing new materials anytime soon',
|
||||
].map((item, idx) => (
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function AuthorPage({ params }: { params: { slug: string } }) {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-3xl font-extrabold text-gray-900">{author.name}</h1>
|
||||
<Image src="/favicon1.png" alt="QR Master" width={24} height={24} className="rounded-full object-cover opacity-90" />
|
||||
<Image src="/logo.svg" alt="QR Master" width={24} height={24} className="rounded-full object-cover opacity-90" />
|
||||
</div>
|
||||
<p className="text-lg text-blue-600 font-medium">{author.role}</p>
|
||||
<p className="text-gray-600 max-w-xl">{author.bio}</p>
|
||||
|
||||
@@ -14,10 +14,10 @@ import { MarketingPageTracker } from '@/components/marketing/MarketingAnalytics'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
|
||||
absolute: 'Bulk QR Code Generator - Static or Dynamic, from Excel',
|
||||
},
|
||||
description:
|
||||
'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.',
|
||||
'Upload a CSV or Excel file and get up to 1,000 static QR codes at once, or up to 500 trackable dynamic ones on Business. Preview every row before you generate.',
|
||||
keywords:
|
||||
'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: {
|
||||
@@ -28,17 +28,17 @@ export const metadata: Metadata = {
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
|
||||
title: 'Bulk QR Code Generator - Static or Dynamic, from Excel',
|
||||
description:
|
||||
'Generate up to 1,000 QR codes from CSV, Excel, XLSX, or exported Google Sheets data.',
|
||||
'One spreadsheet in, a full batch out. Up to 1,000 static codes, or up to 500 trackable dynamic ones.',
|
||||
url: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||
type: 'website',
|
||||
images: ['/og-image.png'],
|
||||
},
|
||||
twitter: {
|
||||
title: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
|
||||
title: 'Bulk QR Code Generator - Static or Dynamic, from Excel',
|
||||
description:
|
||||
'Generate up to 1,000 QR codes from CSV, Excel, XLSX, or exported Google Sheets data.',
|
||||
'One spreadsheet in, a full batch out. Up to 1,000 static codes, or up to 500 trackable dynamic ones.',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -59,9 +59,9 @@ const featureCards = [
|
||||
'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 or dynamic output',
|
||||
description:
|
||||
'Bulk creation currently generates static QR codes. These codes do not include post-print editing or tracking.',
|
||||
'Choose per upload. Static for large print batches. Dynamic when the destination has to stay editable and the scans have to be countable.',
|
||||
},
|
||||
{
|
||||
title: 'ZIP download',
|
||||
@@ -117,13 +117,13 @@ const useCases = [
|
||||
{
|
||||
title: 'Product labels and inserts',
|
||||
description:
|
||||
'Generate large static batches for packaging, inserts, manuals, or support labels when every unit needs a QR code.',
|
||||
'Generate a batch for packaging, inserts, manuals, or support labels. Static when the link is permanent, dynamic when the linked page will change.',
|
||||
points: ['One spreadsheet as input', 'Consistent file naming', 'Printable SVG output'],
|
||||
},
|
||||
{
|
||||
title: 'Event materials',
|
||||
description:
|
||||
'Produce batches for badges, handouts, booth materials, or attendee resources when a static QR is enough.',
|
||||
'Produce batches for badges, handouts, booth materials, or attendee resources. Use dynamic codes when the event page changes after print.',
|
||||
points: ['Batch generation from one file', 'Preview before generation', 'Download everything together'],
|
||||
},
|
||||
{
|
||||
@@ -149,7 +149,7 @@ const faqItems = [
|
||||
{
|
||||
question: 'Are bulk-generated QR codes dynamic or trackable?',
|
||||
answer:
|
||||
'No. The current bulk creation flow generates static QR codes, so those codes do not include post-print editing or tracking.',
|
||||
'Both are available. Static is the default and runs up to 1,000 rows per upload. Dynamic codes stay editable after print and are trackable, capped at 500 on Business by your dynamic code allowance.',
|
||||
},
|
||||
{
|
||||
question: 'What file formats can I upload?',
|
||||
@@ -194,12 +194,12 @@ const softwareSchema = {
|
||||
availability: 'https://schema.org/InStock',
|
||||
},
|
||||
description:
|
||||
'Generate up to 1,000 static QR codes from CSV, Excel, XLSX, or exported Google Sheets files in the QR Master Business plan.',
|
||||
'Generate up to 1,000 static QR codes, or up to 500 trackable dynamic codes, from CSV, Excel, XLSX, or exported Google Sheets files on the QR Master Business plan.',
|
||||
featureList: [
|
||||
'CSV, XLS, and XLSX upload',
|
||||
'Excel and Google Sheets CSV export workflow',
|
||||
'Up to 1,000 rows per upload',
|
||||
'Static QR code generation',
|
||||
'Static or dynamic QR code generation',
|
||||
'ZIP download of generated SVG files',
|
||||
'Optional save-to-dashboard step',
|
||||
],
|
||||
@@ -321,7 +321,7 @@ export default function BulkQRCodeGeneratorPage() {
|
||||
<p className="text-xl leading-relaxed text-gray-600">
|
||||
Stop mapping spreadsheet rows to QR codes by hand. Upload a
|
||||
CSV or Excel file, preview every code, and download up to
|
||||
1,000 print-ready static codes in minutes.
|
||||
1,000 print-ready codes in minutes - static, or dynamic when you need to edit and track them later.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -329,7 +329,7 @@ export default function BulkQRCodeGeneratorPage() {
|
||||
{[
|
||||
'CSV, XLS, and XLSX upload',
|
||||
'Up to 1,000 rows per upload',
|
||||
'Static QR code output',
|
||||
'Static or dynamic output per upload',
|
||||
'ZIP download and optional save to dashboard',
|
||||
].map((feature) => (
|
||||
<div key={feature} className="flex items-center gap-3">
|
||||
@@ -384,7 +384,7 @@ export default function BulkQRCodeGeneratorPage() {
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 text-center text-sm text-gray-600">
|
||||
Designed for bulk static output, not dynamic tracking.
|
||||
Static for large print batches. Dynamic when you need tracking.
|
||||
</p>
|
||||
</Card>
|
||||
<div className="absolute -right-4 -top-4 rounded-full bg-green-500 px-4 py-2 text-sm font-semibold text-white shadow-lg">
|
||||
@@ -397,9 +397,9 @@ export default function BulkQRCodeGeneratorPage() {
|
||||
|
||||
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<AnswerFirstBlock
|
||||
whatIsIt="QR Master bulk creation is a spreadsheet-driven Business-plan workflow for generating up to 1,000 static QR codes in one upload. It is useful when you need many printable QR codes quickly, not when you need post-print editing or tracking."
|
||||
whatIsIt="QR Master bulk creation is a spreadsheet-driven Business-plan workflow for generating up to 1,000 static QR codes in one upload, or up to 500 dynamic ones that stay editable and trackable after print."
|
||||
whenToUse={[
|
||||
'You need many static QR codes from one spreadsheet instead of one-by-one creation',
|
||||
'You need many QR codes from one spreadsheet instead of one-by-one creation',
|
||||
'You want SVG files downloaded together as a ZIP archive',
|
||||
'You are preparing labels, inserts, event materials, or other repeatable print batches',
|
||||
]}
|
||||
@@ -565,7 +565,7 @@ export default function BulkQRCodeGeneratorPage() {
|
||||
<div className="container mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
|
||||
<h2 className="mb-6 text-4xl font-bold">Generate bulk QR codes without one-by-one setup</h2>
|
||||
<p className="mb-8 text-xl text-green-100">
|
||||
Use the Business-plan bulk flow when you need a large static QR batch from a single spreadsheet.
|
||||
One spreadsheet in, a full batch out. Static up to 1,000, or dynamic and trackable up to 500 on Business.
|
||||
</p>
|
||||
<div className="flex flex-col justify-center gap-4 sm:flex-row">
|
||||
<Link href="/pricing">
|
||||
|
||||
@@ -94,9 +94,12 @@ export function generateMetadata({ params }: PageProps): Metadata {
|
||||
follow: true,
|
||||
},
|
||||
icons: {
|
||||
icon: [{ url: "/favicon1.png", type: "image/png" }],
|
||||
shortcut: "/favicon1.png",
|
||||
apple: "/favicon1.png",
|
||||
icon: [
|
||||
{ url: "/favicon.svg", type: "image/svg+xml" },
|
||||
{ url: "/favicon.ico", sizes: "16x16 32x32", type: "image/x-icon" },
|
||||
],
|
||||
shortcut: "/favicon.ico",
|
||||
apple: "/logo.svg",
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
import { MiniGenerator } from '@/components/marketing/MiniGenerator';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Free Custom QR Code Generator with Logo & Colors',
|
||||
description: 'Create custom QR codes with your logo, brand colors, and unique frames. Free designer with instant preview. Download PNG/SVG. No signup needed to try.',
|
||||
title: 'Custom QR Code Generator with Logo & Brand Colors',
|
||||
description: 'Put your logo and brand colors into the code itself. Live preview as you design, print-ready SVG export that stays sharp at any size. No signup needed to try.',
|
||||
keywords: [
|
||||
'custom qr code generator',
|
||||
'qr code with logo',
|
||||
@@ -337,7 +337,7 @@ export default function CustomQRCodeGeneratorPage() {
|
||||
{
|
||||
href: '/tools/barcode-generator',
|
||||
title: 'Free Barcode Generator',
|
||||
description: 'Need a 1D barcode for retail or inventory? Create EAN-13, UPC-A, and Code 128 barcodes instantly — no signup required.',
|
||||
description: 'Need a 1D barcode for retail or inventory? Create EAN-13, UPC-A, and Code 128 barcodes instantly - no signup required.',
|
||||
ctaLabel: 'Create a barcode',
|
||||
},
|
||||
{
|
||||
@@ -369,7 +369,7 @@ export default function CustomQRCodeGeneratorPage() {
|
||||
</h1>
|
||||
|
||||
<p className="text-xl text-gray-600 leading-relaxed">
|
||||
Add your logo, choose custom colors, and design unique frames. Professional QR codes in minutes – try it free, no signup required.
|
||||
Add your logo, choose custom colors, and design unique frames. Professional QR codes in minutes - try it free, no signup required.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
@@ -417,7 +417,7 @@ export default function CustomQRCodeGeneratorPage() {
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Your Logo Won't Break the QR Code – Here's Why
|
||||
Your Logo Won't Break the QR Code - Here's Why
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
QR codes have built-in error correction. Our generator uses the highest level (H = 30% redundancy), which means up to 30% of the code can be covered or damaged and still scan perfectly.
|
||||
@@ -681,7 +681,7 @@ export default function CustomQRCodeGeneratorPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* WHY CUSTOM DESIGN MATTERS — STATISTICS */}
|
||||
{/* WHY CUSTOM DESIGN MATTERS - STATISTICS */}
|
||||
<section className="py-16 bg-white">
|
||||
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -692,27 +692,27 @@ export default function CustomQRCodeGeneratorPage() {
|
||||
Why Brand Design in QR Codes Increases Engagement
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-10 max-w-2xl">
|
||||
A <strong>custom QR code</strong> with your brand colors and logo doesn't just look better — it signals trust and gets scanned more often.
|
||||
A <strong>custom QR code</strong> with your brand colors and logo doesn't just look better - it signals trust and gets scanned more often.
|
||||
</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6 mb-8">
|
||||
<div className="bg-purple-50 border border-purple-100 rounded-2xl p-6">
|
||||
<div className="text-4xl font-extrabold text-purple-600 mb-2">+80%</div>
|
||||
<p className="text-gray-700 text-sm leading-relaxed mb-3">
|
||||
Color increases brand recognition by up to 80%. A branded QR code using your brand colors is recognized and associated with your business faster than a generic black-and-white grid — increasing scan intent.
|
||||
Color increases brand recognition by up to 80%. A branded QR code using your brand colors is recognized and associated with your business faster than a generic black-and-white grid - increasing scan intent.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Source: <a href="https://www.loyola.edu/academia/marketing/insights/brand-recognition" target="_blank" rel="noopener noreferrer" className="underline hover:text-gray-700">University of Loyola Maryland</a> — Color & Brand Recognition Study
|
||||
Source: <a href="https://www.loyola.edu/academia/marketing/insights/brand-recognition" target="_blank" rel="noopener noreferrer" className="underline hover:text-gray-700">University of Loyola Maryland</a> - Color & Brand Recognition Study
|
||||
</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">+40%</div>
|
||||
<p className="text-gray-700 text-sm leading-relaxed mb-3">
|
||||
Adding a recognizable brand element — like a logo — to a functional graphic increases user engagement and trust. Familiar visual cues reduce hesitation and increase the likelihood of scanning.
|
||||
Adding a recognizable brand element - like a logo - to a functional graphic increases user engagement and trust. Familiar visual cues reduce hesitation and increase the likelihood of scanning.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Source: <a href="https://www.nngroup.com/articles/visual-design-trust/" target="_blank" rel="noopener noreferrer" className="underline hover:text-gray-700">Nielsen Norman Group</a> — Visual Design and Trust Research
|
||||
Source: <a href="https://www.nngroup.com/articles/visual-design-trust/" target="_blank" rel="noopener noreferrer" className="underline hover:text-gray-700">Nielsen Norman Group</a> - Visual Design and Trust Research
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Dynamic Barcode Generator – Trackable, Editable Barcodes',
|
||||
absolute: 'Dynamic Barcode Generator - Trackable, Editable Barcodes',
|
||||
},
|
||||
description:
|
||||
'Generate dynamic barcodes you can update after printing. Change the redirect URL anytime, track every scan, and manage all barcodes in one dashboard. Free to start.',
|
||||
@@ -30,17 +30,17 @@ export const metadata: Metadata = {
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Dynamic Barcode Generator – Trackable, Editable Barcodes',
|
||||
title: 'Dynamic Barcode Generator - Trackable, Editable Barcodes',
|
||||
description:
|
||||
'Generate dynamic barcodes that redirect to any URL — and can be updated after printing. Track every scan with device, time, and location data.',
|
||||
'Generate dynamic barcodes that redirect to any URL - and can be updated after printing. Track every scan with device, time, and location data.',
|
||||
url: 'https://www.qrmaster.net/dynamic-barcode-generator',
|
||||
type: 'website',
|
||||
images: ['/og-image.png'],
|
||||
},
|
||||
twitter: {
|
||||
title: 'Dynamic Barcode Generator – Trackable, Editable Barcodes',
|
||||
title: 'Dynamic Barcode Generator - Trackable, Editable Barcodes',
|
||||
description:
|
||||
'Generate dynamic barcodes that redirect to any URL — and can be updated after printing. Track every scan with device, time, and location data.',
|
||||
'Generate dynamic barcodes that redirect to any URL - and can be updated after printing. Track every scan with device, time, and location data.',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ const featureCards = [
|
||||
{
|
||||
title: 'Track every smartphone scan',
|
||||
description:
|
||||
'See device type, time, country, and referrer for every scan — in the same dashboard as your QR codes.',
|
||||
'See device type, time, country, and referrer for every scan - in the same dashboard as your QR codes.',
|
||||
},
|
||||
{
|
||||
title: 'CODE128 and CODE39 supported',
|
||||
@@ -111,28 +111,28 @@ const useCases = [
|
||||
description:
|
||||
'Print CODE128 barcodes on event badges that link to personalized schedules, session recordings, or attendee portals. Update the destination as content changes.',
|
||||
example:
|
||||
'Conference badges with a dynamic barcode redirect to an attendee agenda page — updated in real time as the schedule changes. Scanned with smartphone cameras at check-in kiosks.',
|
||||
'Conference badges with a dynamic barcode redirect to an attendee agenda page - updated in real time as the schedule changes. Scanned with smartphone cameras at check-in kiosks.',
|
||||
},
|
||||
{
|
||||
title: 'Packaging inserts & product cards',
|
||||
description:
|
||||
'Link physical inserts inside product boxes to setup guides, how-to videos, or support pages. Update the destination when content moves — no reprint needed.',
|
||||
'Link physical inserts inside product boxes to setup guides, how-to videos, or support pages. Update the destination when content moves - no reprint needed.',
|
||||
example:
|
||||
'A hardware brand updates the setup guide URL six months post-launch to a new video format — without reprinting a single insert.',
|
||||
'A hardware brand updates the setup guide URL six months post-launch to a new video format - without reprinting a single insert.',
|
||||
},
|
||||
{
|
||||
title: 'Digital signage & screens',
|
||||
description:
|
||||
'Display a CODE128 barcode on screens or monitors. Visitors scan with their smartphone and land on a current URL — updated from the dashboard without changing the display.',
|
||||
'Display a CODE128 barcode on screens or monitors. Visitors scan with their smartphone and land on a current URL - updated from the dashboard without changing the display.',
|
||||
example:
|
||||
'A trade-show monitor shows a barcode linking to the current product demo page — redirected to a post-show recording after the event.',
|
||||
'A trade-show monitor shows a barcode linking to the current product demo page - redirected to a post-show recording after the event.',
|
||||
},
|
||||
{
|
||||
title: 'Internal asset tracking (browser-based)',
|
||||
description:
|
||||
'Use CODE128 barcodes on internal assets where staff scan with smartphones to open web-based inventory or maintenance forms. URLs update as systems change.',
|
||||
example:
|
||||
'IT tags company laptops with dynamic barcodes. Staff scan to open the current helpdesk form — URL updated when the ticketing system moves.',
|
||||
'IT tags company laptops with dynamic barcodes. Staff scan to open the current helpdesk form - URL updated when the ticketing system moves.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -140,12 +140,12 @@ const faqItems = [
|
||||
{
|
||||
question: 'What is a dynamic barcode?',
|
||||
answer:
|
||||
'A dynamic barcode (CODE128 or CODE39) encodes a short redirect URL. When scanned with a smartphone camera, it opens a browser and routes through QR Master to your current destination — which you can update without changing the printed barcode. Note: it requires a smartphone scan, not a POS laser scanner.',
|
||||
'A dynamic barcode (CODE128 or CODE39) encodes a short redirect URL. When scanned with a smartphone camera, it opens a browser and routes through QR Master to your current destination - which you can update without changing the printed barcode. Note: it requires a smartphone scan, not a POS laser scanner.',
|
||||
},
|
||||
{
|
||||
question: 'How is a dynamic barcode different from a static one?',
|
||||
answer:
|
||||
'A static barcode directly encodes a fixed value — a number, URL, or product code — that cannot be changed after printing. A dynamic barcode encodes a redirect link so the final destination can be updated at any time from your dashboard.',
|
||||
'A static barcode directly encodes a fixed value - a number, URL, or product code - that cannot be changed after printing. A dynamic barcode encodes a redirect link so the final destination can be updated at any time from your dashboard.',
|
||||
},
|
||||
{
|
||||
question:
|
||||
@@ -166,7 +166,7 @@ const faqItems = [
|
||||
{
|
||||
question: 'Can I change a barcode destination after printing?',
|
||||
answer:
|
||||
'Yes — that is the core value of a dynamic barcode. Log in to your dashboard, find the barcode, and update the redirect URL. Scanners immediately reach the new destination.',
|
||||
'Yes - that is the core value of a dynamic barcode. Log in to your dashboard, find the barcode, and update the redirect URL. Scanners immediately reach the new destination.',
|
||||
},
|
||||
{
|
||||
question: 'Do I need to reprint if the linked page changes?',
|
||||
@@ -225,7 +225,7 @@ const howToSchema = {
|
||||
url: 'https://www.qrmaster.net/authors/timo',
|
||||
},
|
||||
description:
|
||||
'Create a dynamic barcode that redirects to a URL you can update anytime — without reprinting the label.',
|
||||
'Create a dynamic barcode that redirects to a URL you can update anytime - without reprinting the label.',
|
||||
totalTime: 'PT3M',
|
||||
step: [
|
||||
{
|
||||
@@ -252,7 +252,7 @@ const howToSchema = {
|
||||
'@type': 'HowToStep',
|
||||
position: 4,
|
||||
name: 'Update anytime',
|
||||
text: 'Change the destination URL from your dashboard whenever needed — no reprint required.',
|
||||
text: 'Change the destination URL from your dashboard whenever needed - no reprint required.',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -281,14 +281,14 @@ const relatedLinks = [
|
||||
href: '/dynamic-qr-code-generator',
|
||||
title: 'Dynamic QR Code Generator',
|
||||
description:
|
||||
'The same redirect-and-track approach for 2D QR codes — ideal for consumer-facing print materials scanned by smartphones.',
|
||||
'The same redirect-and-track approach for 2D QR codes - ideal for consumer-facing print materials scanned by smartphones.',
|
||||
ctaLabel: 'Create dynamic QR codes',
|
||||
},
|
||||
{
|
||||
href: '/bulk-qr-code-generator',
|
||||
title: 'Bulk Barcode & QR Generator',
|
||||
description:
|
||||
'Upload a CSV and generate hundreds of dynamic barcodes or QR codes in one batch — with tracking for every code.',
|
||||
'Upload a CSV and generate hundreds of dynamic barcodes or QR codes in one batch - with tracking for every code.',
|
||||
ctaLabel: 'Generate codes in bulk',
|
||||
},
|
||||
{
|
||||
@@ -316,7 +316,7 @@ const relatedLinks = [
|
||||
href: '/tools/barcode-generator',
|
||||
title: 'Free Static Barcode Generator',
|
||||
description:
|
||||
'Need a one-time barcode with no redirect? Use the free static barcode generator — no account required.',
|
||||
'Need a one-time barcode with no redirect? Use the free static barcode generator - no account required.',
|
||||
ctaLabel: 'Generate a static barcode',
|
||||
},
|
||||
];
|
||||
@@ -454,17 +454,17 @@ export default function DynamicBarcodeGeneratorPage() {
|
||||
<p className="text-[18px] font-[300] leading-[1.6] text-black max-w-lg">
|
||||
Create CODE128 or CODE39 barcodes that encode a redirect
|
||||
URL. When scanned with a smartphone, they open a browser and
|
||||
redirect to your destination — which you can update anytime
|
||||
redirect to your destination - which you can update anytime
|
||||
without reprinting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pt-2">
|
||||
{[
|
||||
'Scanned by smartphone cameras — redirect opens in the browser',
|
||||
'Scanned by smartphone cameras - redirect opens in the browser',
|
||||
'Update the destination from your dashboard without touching the label',
|
||||
'Track every scan — device, country, time, and UTM data',
|
||||
'Free plan includes 3 active dynamic barcodes — no credit card required',
|
||||
'Track every scan - device, country, time, and UTM data',
|
||||
'Free plan includes 3 active dynamic barcodes - no credit card required',
|
||||
].map((feature) => (
|
||||
<div key={feature} className="flex items-start gap-3">
|
||||
<div className="mt-1 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-[#15be53]/20 border border-[#15be53]/40">
|
||||
@@ -513,7 +513,7 @@ export default function DynamicBarcodeGeneratorPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero visual — barcode mockup */}
|
||||
{/* Hero visual - barcode mockup */}
|
||||
<div className="relative mt-8 lg:mt-0">
|
||||
<div className="rounded-[8px] border border-[#e5edf5] bg-white p-8 shadow-[0_30px_45px_-30px_rgba(50,50,93,0.25),0_18px_36px_-18px_rgba(0,0,0,0.1)]">
|
||||
{/* Simulated barcode SVG */}
|
||||
@@ -948,7 +948,7 @@ export default function DynamicBarcodeGeneratorPage() {
|
||||
How to Create a Dynamic Barcode
|
||||
</h2>
|
||||
<AnswerFirstBlock
|
||||
whatIsIt="A dynamic barcode encodes a short redirect URL into a standard linear format (CODE128, EAN-13, etc.). When scanned, it routes through QR Master — logging the scan and forwarding to your chosen destination, which you can update anytime from the dashboard."
|
||||
whatIsIt="A dynamic barcode encodes a short redirect URL into a standard linear format (CODE128, EAN-13, etc.). When scanned, it routes through QR Master - logging the scan and forwarding to your chosen destination, which you can update anytime from the dashboard."
|
||||
whenToUse={[
|
||||
'Event badges, access passes, or conference materials scanned by smartphone cameras',
|
||||
'Product packaging inserts or cards where visitors scan with their phone to reach a web page',
|
||||
@@ -981,7 +981,7 @@ export default function DynamicBarcodeGeneratorPage() {
|
||||
'Create a QR Master account and open the Create dashboard',
|
||||
'Select "Barcode" as the content type, pick a format, and enter your destination URL',
|
||||
'Download the barcode as SVG or PNG and apply it to your label or packaging',
|
||||
'Update the redirect URL anytime from your dashboard — no reprint required',
|
||||
'Update the redirect URL anytime from your dashboard - no reprint required',
|
||||
],
|
||||
}}
|
||||
/>
|
||||
@@ -1042,7 +1042,7 @@ export default function DynamicBarcodeGeneratorPage() {
|
||||
: 'text-[20px] text-[#ea2261]'
|
||||
}
|
||||
>
|
||||
{item.static ? '✓' : '—'}
|
||||
{item.static ? '✓' : '-'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -1066,7 +1066,7 @@ export default function DynamicBarcodeGeneratorPage() {
|
||||
: 'text-[20px] text-[#ea2261]'
|
||||
}
|
||||
>
|
||||
{item.dynamic ? '✓' : '—'}
|
||||
{item.dynamic ? '✓' : '-'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -1085,7 +1085,7 @@ export default function DynamicBarcodeGeneratorPage() {
|
||||
</h2>
|
||||
<p className="mx-auto max-w-3xl text-[18px] font-[300] leading-[1.6] text-[#64748d]">
|
||||
The same redirect-and-track infrastructure that powers QR
|
||||
Master's dynamic QR codes — now available for standard linear
|
||||
Master's dynamic QR codes - now available for standard linear
|
||||
barcode formats.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1177,7 +1177,7 @@ export default function DynamicBarcodeGeneratorPage() {
|
||||
</h2>
|
||||
<p className="text-[18px] font-[300] leading-[1.6] text-[rgba(255,255,255,0.7)]">
|
||||
Static barcodes are a liability when the page or document
|
||||
behind them changes. Dynamic barcodes eliminate that risk —
|
||||
behind them changes. Dynamic barcodes eliminate that risk -
|
||||
and add scan intelligence to every label.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1243,7 +1243,7 @@ export default function DynamicBarcodeGeneratorPage() {
|
||||
Which barcode format works dynamically?
|
||||
</h2>
|
||||
<p className="text-[18px] font-[300] leading-[1.6] text-[#64748d] max-w-2xl mx-auto">
|
||||
Dynamic barcodes must encode a URL — so only formats that
|
||||
Dynamic barcodes must encode a URL - so only formats that
|
||||
support full ASCII strings work. Numeric-only formats like
|
||||
EAN-13, UPC, and ITF-14 cannot embed a redirect URL and are
|
||||
available as <strong>static barcodes only</strong>.
|
||||
@@ -1297,7 +1297,7 @@ export default function DynamicBarcodeGeneratorPage() {
|
||||
|
||||
<div>
|
||||
<h3 className="text-[14px] font-[400] text-[#64748d] uppercase tracking-wider mb-6 text-center md:text-left">
|
||||
Static only — numeric formats
|
||||
Static only - numeric formats
|
||||
</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{[
|
||||
@@ -1353,7 +1353,7 @@ export default function DynamicBarcodeGeneratorPage() {
|
||||
<GrowthLinksSection
|
||||
eyebrow="Best next workflows"
|
||||
title="See where dynamic barcodes fit your stack"
|
||||
description="Dynamic barcodes work best alongside QR codes, bulk generation, and unified analytics — all available in the same QR Master dashboard."
|
||||
description="Dynamic barcodes work best alongside QR codes, bulk generation, and unified analytics - all available in the same QR Master dashboard."
|
||||
links={relatedLinks}
|
||||
pageType="commercial"
|
||||
cluster="dynamic-barcode"
|
||||
|
||||
@@ -20,7 +20,7 @@ export const metadata: Metadata = {
|
||||
absolute: 'Free Dynamic QR Code Generator: Edit After Print',
|
||||
},
|
||||
description:
|
||||
'Create dynamic QR codes with editable destinations, scan tracking, branding, and post-print updates for menus, flyers, packaging, and campaigns.',
|
||||
'Print once, change the destination anytime. 3 dynamic QR codes free forever, no card. Track scans by time, device and location. For menus, flyers and packaging.',
|
||||
keywords:
|
||||
'dynamic qr code generator, free dynamic qr code generator, best dynamic qr code generator, editable qr code, changeable qr code, qr code tracking, update qr code after printing',
|
||||
alternates: {
|
||||
@@ -33,7 +33,7 @@ export const metadata: Metadata = {
|
||||
openGraph: {
|
||||
title: 'Dynamic QR Code Generator: Edit After Print',
|
||||
description:
|
||||
'Create dynamic QR codes that you can edit after printing. Change the destination URL anytime, track scans, and manage all codes in one dashboard.',
|
||||
'The link does not have to stay wrong once it is printed. Change the destination in seconds, track every scan, and manage all codes in one dashboard.',
|
||||
url: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
||||
type: 'website',
|
||||
images: ['/og-image.png'],
|
||||
@@ -41,7 +41,7 @@ export const metadata: Metadata = {
|
||||
twitter: {
|
||||
title: 'Dynamic QR Code Generator: Edit After Print',
|
||||
description:
|
||||
'Create dynamic QR codes that you can edit after printing. Change the destination URL anytime, track scans, and manage all codes in one dashboard.',
|
||||
'The link does not have to stay wrong once it is printed. Change the destination in seconds, track every scan, and manage all codes in one dashboard.',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -140,7 +140,7 @@ const useCases = [
|
||||
description:
|
||||
'Print event QR codes in advance and update the destination if the venue, schedule, or session link changes before or during the event.',
|
||||
example:
|
||||
'Redirect all printed badges to a new venue map URL the morning of the event — no reprint, no confusion.',
|
||||
'Redirect all printed badges to a new venue map URL the morning of the event - no reprint, no confusion.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -148,12 +148,12 @@ const faqItems = [
|
||||
{
|
||||
question: 'What is a dynamic QR code generator?',
|
||||
answer:
|
||||
'A dynamic QR code generator creates QR codes that route through a managed redirect link instead of encoding the destination directly. That redirect makes it possible to change the final destination after printing — unlike static QR codes, which permanently encode the URL into the image at creation time.',
|
||||
'A dynamic QR code generator creates QR codes that route through a managed redirect link instead of encoding the destination directly. That redirect makes it possible to change the final destination after printing - unlike static QR codes, which permanently encode the URL into the image at creation time.',
|
||||
},
|
||||
{
|
||||
question: 'Can I edit a dynamic QR code after printing?',
|
||||
answer:
|
||||
'Yes — that is the core benefit. You keep the same printed QR image and update the destination URL from your QR Master dashboard at any time. Use it to change a campaign landing page, fix a typo in a URL, or point existing printed materials to a new offer without reprinting a single page.',
|
||||
'Yes - that is the core benefit. You keep the same printed QR image and update the destination URL from your QR Master dashboard at any time. Use it to change a campaign landing page, fix a typo in a URL, or point existing printed materials to a new offer without reprinting a single page.',
|
||||
},
|
||||
{
|
||||
question: 'How many times can I scan a dynamic QR code?',
|
||||
@@ -163,12 +163,12 @@ const faqItems = [
|
||||
{
|
||||
question: 'Do dynamic QR codes expire?',
|
||||
answer:
|
||||
'Some free tools put expiry dates on dynamic QR codes. QR Master does not. Free plan codes stay active as long as the account is active (up to 3 dynamic codes). Pro and Business plan codes have no expiry at all — they remain scannable and editable indefinitely.',
|
||||
'Some free tools put expiry dates on dynamic QR codes. QR Master does not. Free plan codes stay active as long as the account is active (up to 3 dynamic codes). Pro and Business plan codes have no expiry at all - they remain scannable and editable indefinitely.',
|
||||
},
|
||||
{
|
||||
question: "What's the difference between static and dynamic QR codes?",
|
||||
answer:
|
||||
'Static QR codes permanently encode the destination into the image — they are cheap to generate but cannot be changed or tracked after printing. Dynamic QR codes route through a redirect layer: the destination can be updated at any time, every scan is logged with device, time, and location context, and the same printed code can serve multiple campaigns over its lifetime.',
|
||||
'Static QR codes permanently encode the destination into the image - they are cheap to generate but cannot be changed or tracked after printing. Dynamic QR codes route through a redirect layer: the destination can be updated at any time, every scan is logged with device, time, and location context, and the same printed code can serve multiple campaigns over its lifetime.',
|
||||
},
|
||||
{
|
||||
question: 'How do I track scans on a dynamic QR code?',
|
||||
@@ -183,12 +183,12 @@ const faqItems = [
|
||||
{
|
||||
question: 'Is a dynamic QR code free?',
|
||||
answer:
|
||||
'Yes — the Free plan includes 3 active dynamic QR codes at no cost. No credit card required to get started.',
|
||||
'Yes - the Free plan includes 3 active dynamic QR codes at no cost. No credit card required to get started.',
|
||||
},
|
||||
{
|
||||
question: 'How do I convert a static QR code to a dynamic one?',
|
||||
answer:
|
||||
'You cannot convert an existing static QR code — the data is permanently encoded in the image. To switch to dynamic, create a new dynamic QR code in QR Master and replace the printed code.',
|
||||
'You cannot convert an existing static QR code - the data is permanently encoded in the image. To switch to dynamic, create a new dynamic QR code in QR Master and replace the printed code.',
|
||||
},
|
||||
{
|
||||
question: 'What is the best use case for a dynamic QR code?',
|
||||
@@ -203,7 +203,7 @@ const servicesComparison = [
|
||||
href: 'https://www.qrmaster.net',
|
||||
freePlan: '3 active dynamic codes (permanent)',
|
||||
paidFrom: 'EUR 9/month (Pro)',
|
||||
gdpr: 'Built-in — hashed IPs, all plans',
|
||||
gdpr: 'Built-in - hashed IPs, all plans',
|
||||
analytics: 'All plans',
|
||||
bulk: 'Up to 1,000 static codes (Business, EUR 29/mo)',
|
||||
bestFor: 'SMBs and EU businesses',
|
||||
@@ -213,7 +213,7 @@ const servicesComparison = [
|
||||
service: 'Beaconstac / Uniqode',
|
||||
href: 'https://www.uniqode.com',
|
||||
freePlan: 'None',
|
||||
paidFrom: '$49–99/month (functional tier)',
|
||||
paidFrom: '$49-99/month (functional tier)',
|
||||
gdpr: 'Via DPA configuration',
|
||||
analytics: 'Paid plans',
|
||||
bulk: 'Enterprise tier only',
|
||||
@@ -494,7 +494,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||
<p className="text-xl leading-relaxed text-gray-600">
|
||||
A wrong link on printed material usually means a reprint.
|
||||
With a dynamic QR code, you fix the destination online in
|
||||
seconds — the printed flyer, menu, or card never has to
|
||||
seconds - the printed flyer, menu, or card never has to
|
||||
change.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1071,7 +1071,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||
after it has been created and printed. Instead of encoding the
|
||||
final URL directly into the image, a dynamic QR code contains a
|
||||
short managed redirect link. When someone scans the code, the
|
||||
redirect sends them to whatever destination is currently set —
|
||||
redirect sends them to whatever destination is currently set -
|
||||
which means you can update the link, fix a typo, or point the
|
||||
same printed code at a new campaign at any time, without
|
||||
reprinting anything.
|
||||
@@ -1081,7 +1081,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||
codes can also be tracked: each scan is logged with its time,
|
||||
device type, and approximate location, so printed materials
|
||||
become measurable instead of invisible. Static QR codes offer
|
||||
neither of these capabilities — the destination is permanently
|
||||
neither of these capabilities - the destination is permanently
|
||||
baked into the image and no scan data is recorded.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1312,7 +1312,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* WHY DYNAMIC QR — STATISTICS */}
|
||||
{/* WHY DYNAMIC QR - STATISTICS */}
|
||||
<section className="bg-white py-16">
|
||||
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -1337,8 +1337,8 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||
Why Dynamic QR Codes Deliver Better Business Outcomes
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-10 max-w-2xl">
|
||||
The ability to update destinations after printing — and track
|
||||
every scan — transforms a static print asset into a measurable
|
||||
The ability to update destinations after printing - and track
|
||||
every scan - transforms a static print asset into a measurable
|
||||
marketing channel.
|
||||
</p>
|
||||
|
||||
@@ -1348,8 +1348,8 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||
89% vs 33%
|
||||
</div>
|
||||
<p className="text-gray-700 text-sm leading-relaxed mb-3">
|
||||
Companies with strong omnichannel customer engagement —
|
||||
enabled by closed-loop tracking from offline to online —
|
||||
Companies with strong omnichannel customer engagement -
|
||||
enabled by closed-loop tracking from offline to online -
|
||||
retain <strong>89% of their customers</strong>, compared to
|
||||
only 33% for companies with weak omnichannel engagement.
|
||||
</p>
|
||||
@@ -1363,7 +1363,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||
>
|
||||
Aberdeen Group
|
||||
</a>{' '}
|
||||
— Omnichannel Customer Engagement Study
|
||||
- Omnichannel Customer Engagement Study
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1375,7 +1375,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||
of small businesses report printing and direct mail errors as
|
||||
a major source of wasted marketing budget. Dynamic QR codes
|
||||
address the part of that risk caused by a changed
|
||||
destination — update the link online instead of reprinting.
|
||||
destination - update the link online instead of reprinting.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Source:{' '}
|
||||
@@ -1398,7 +1398,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SERVICES COMPARISON — targets "what services offer dynamic QR codes?" AI query */}
|
||||
{/* SERVICES COMPARISON - targets "what services offer dynamic QR codes?" AI query */}
|
||||
<section className="bg-gray-50 py-16">
|
||||
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
@@ -1410,8 +1410,8 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||
Which services offer dynamic QR codes?
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-8 max-w-2xl">
|
||||
Dynamic QR codes — where the destination URL can be changed after
|
||||
printing — are offered by several platforms. Here is how they
|
||||
Dynamic QR codes - where the destination URL can be changed after
|
||||
printing - are offered by several platforms. Here is how they
|
||||
compare on price, privacy, and use case fit.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ function truncateAtWord(text: string, maxLength: number): string {
|
||||
}
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const title = truncateAtWord('QR Code FAQ – Common Questions Answered | QR Master', 60);
|
||||
const title = truncateAtWord('QR Code FAQ - Common Questions Answered | QR Master', 60);
|
||||
const description = truncateAtWord(
|
||||
'Quick answers to common QR code questions: Do QR codes expire? Static vs dynamic? Can they be scanned through laminate? Get clear answers.',
|
||||
160
|
||||
@@ -61,12 +61,12 @@ 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.',
|
||||
'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.',
|
||||
'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?',
|
||||
@@ -81,7 +81,7 @@ const faqs: FAQItemWithRichText[] = [
|
||||
{
|
||||
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.',
|
||||
'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?',
|
||||
@@ -91,7 +91,7 @@ const faqs: FAQItemWithRichText[] = [
|
||||
{
|
||||
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.',
|
||||
'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?',
|
||||
@@ -155,7 +155,7 @@ const faqs: FAQItemWithRichText[] = [
|
||||
{
|
||||
question: 'How does bulk QR creation work today?',
|
||||
answer:
|
||||
'QR Master currently supports bulk QR creation through spreadsheet upload in the Business plan. The flow accepts CSV, XLS, and XLSX files, supports up to 1,000 rows per upload, and generates static QR codes.',
|
||||
'QR Master supports bulk QR creation through spreadsheet upload in the Business plan. The flow accepts CSV, XLS, and XLSX files and handles up to 1,000 rows per upload. You choose static or dynamic output per upload; dynamic is capped at 500 by the Business dynamic code allowance.',
|
||||
answerRich: (
|
||||
<>
|
||||
QR Master currently supports bulk QR creation through spreadsheet upload in the Business plan.
|
||||
|
||||
@@ -80,9 +80,12 @@ export function generateMetadata({ params }: PageProps): Metadata {
|
||||
follow: true,
|
||||
},
|
||||
icons: {
|
||||
icon: [{ url: "/favicon1.png", type: "image/png" }],
|
||||
shortcut: "/favicon1.png",
|
||||
apple: "/favicon1.png",
|
||||
icon: [
|
||||
{ url: "/favicon.svg", type: "image/svg+xml" },
|
||||
{ url: "/favicon.ico", sizes: "16x16 32x32", type: "image/x-icon" },
|
||||
],
|
||||
shortcut: "/favicon.ico",
|
||||
apple: "/logo.svg",
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
|
||||
@@ -9,7 +9,7 @@ const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true';
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL('https://www.qrmaster.net'),
|
||||
title: {
|
||||
default: 'QR Master – Smart QR Generator & Analytics',
|
||||
default: 'QR Master - Smart QR Generator & Analytics',
|
||||
template: '%s | QR Master',
|
||||
},
|
||||
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.',
|
||||
@@ -33,7 +33,7 @@ export const metadata: Metadata = {
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
siteName: 'QR Master',
|
||||
title: 'QR Master – Smart QR Generator & Analytics',
|
||||
title: 'QR Master - Smart QR Generator & Analytics',
|
||||
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.',
|
||||
images: [
|
||||
{
|
||||
|
||||
@@ -3,8 +3,8 @@ import { pillarMeta } from "@/lib/pillar-data";
|
||||
import { getPublishedPosts } from "@/lib/content";
|
||||
|
||||
export const metadata = {
|
||||
title: "QR Mastery: Free QR Code Guides & Tutorials",
|
||||
description: "QR Mastery is the free learning hub by QR Master: step-by-step guides to create, track, and optimize dynamic QR codes. No account needed to start.",
|
||||
title: "QR Mastery - Guides on Tracking, Print Size & Providers",
|
||||
description: "Free guides on the things that go wrong: dead links after printing, scans you cannot attribute, codes too small to read, and providers that switch codes off. No account.",
|
||||
alternates: {
|
||||
canonical: "https://www.qrmaster.net/learn",
|
||||
},
|
||||
@@ -27,7 +27,7 @@ export default function LearnHubPage() {
|
||||
<header className="space-y-4 max-w-3xl">
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold text-gray-900 tracking-tight">QR Mastery: The QR Code Knowledge Hub</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
Master the art of QR codes. Explore our expert guides on generation, tracking, security, and marketing strategies — all free, no account needed.
|
||||
Master the art of QR codes. Explore our expert guides on generation, tracking, security, and marketing strategies - all free, no account needed.
|
||||
</p>
|
||||
<p className="text-gray-600">
|
||||
Ready to put it into practice? Create an editable code with the{" "}
|
||||
|
||||
@@ -813,9 +813,9 @@ export default function NewsletterClient() {
|
||||
<TableBody>
|
||||
{data.segments.rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell>{row.name || '—'}</TableCell>
|
||||
<TableCell>{row.name || '-'}</TableCell>
|
||||
<TableCell>{row.email}</TableCell>
|
||||
<TableCell>{row.emailDomain || '—'}</TableCell>
|
||||
<TableCell>{row.emailDomain || '-'}</TableCell>
|
||||
<TableCell>{row.plan}</TableCell>
|
||||
<TableCell>{row.lifecycleStageLabel}</TableCell>
|
||||
<TableCell>{row.fitScore}</TableCell>
|
||||
@@ -823,16 +823,16 @@ export default function NewsletterClient() {
|
||||
<TableCell>{row.leadScore}</TableCell>
|
||||
<TableCell>{row.signupSourceLabel}</TableCell>
|
||||
<TableCell>{row.signupSourceSelfReportedLabel}</TableCell>
|
||||
<TableCell>{row.signupCampaign || '—'}</TableCell>
|
||||
<TableCell>{row.signupLandingPath || '—'}</TableCell>
|
||||
<TableCell>{row.signupCampaign || '-'}</TableCell>
|
||||
<TableCell>{row.signupLandingPath || '-'}</TableCell>
|
||||
<TableCell>{row.primaryUseCaseLabel}</TableCell>
|
||||
<TableCell>{row.primaryGoalLabel}</TableCell>
|
||||
<TableCell>{row.jobRoleLabel}</TableCell>
|
||||
<TableCell>{row.companyName || '—'}</TableCell>
|
||||
<TableCell>{row.companyName || '-'}</TableCell>
|
||||
<TableCell>{row.teamSizeLabel}</TableCell>
|
||||
<TableCell>{new Date(row.createdAt).toLocaleDateString()}</TableCell>
|
||||
<TableCell>{row.firstQrCreatedAt ? new Date(row.firstQrCreatedAt).toLocaleDateString() : '—'}</TableCell>
|
||||
<TableCell>{row.activationAt ? new Date(row.activationAt).toLocaleDateString() : '—'}</TableCell>
|
||||
<TableCell>{row.firstQrCreatedAt ? new Date(row.firstQrCreatedAt).toLocaleDateString() : '-'}</TableCell>
|
||||
<TableCell>{row.activationAt ? new Date(row.activationAt).toLocaleDateString() : '-'}</TableCell>
|
||||
<TableCell>{row.qrCount}</TableCell>
|
||||
<TableCell>{row.dynamicQrCount}</TableCell>
|
||||
<TableCell>{row.scanCount}</TableCell>
|
||||
@@ -888,7 +888,7 @@ export default function NewsletterClient() {
|
||||
<TableCell>{row.leadScore}</TableCell>
|
||||
<TableCell>{getUseCaseLabel(row.primaryUseCase)}</TableCell>
|
||||
<TableCell>{getRoleLabel(row.jobRole)}</TableCell>
|
||||
<TableCell>{row.companyName || '—'}</TableCell>
|
||||
<TableCell>{row.companyName || '-'}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{row.upgradeBadges.map((badge) => (
|
||||
|
||||
@@ -17,10 +17,10 @@ function truncateAtWord(text: string, maxLength: number): string {
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
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.',
|
||||
'Change where a printed QR code points - without reprinting it. 3 dynamic codes free forever, unlimited static codes that never expire, scan tracking. No card.',
|
||||
160
|
||||
);
|
||||
const brandTitle = 'QR Master - Free Dynamic QR Code Generator with Tracking';
|
||||
const brandTitle = 'QR Master - Free QR Code Generator, Editable After Print';
|
||||
|
||||
return {
|
||||
title: brandTitle,
|
||||
@@ -90,7 +90,7 @@ export default function HomePage() {
|
||||
</p>
|
||||
<p>
|
||||
Features include: Dynamic QR codes with real-time tracking, bulk QR
|
||||
code generation from Excel/CSV, custom branding with colors and logos,
|
||||
code generation from Excel/CSV, free colour control on every plan,
|
||||
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,
|
||||
|
||||
@@ -145,10 +145,10 @@ export default function PricingPage() {
|
||||
`${FREE_DYNAMIC_QR_LIMIT} active dynamic QR codes (8 types available)`,
|
||||
'Unlimited static QR codes',
|
||||
'Basic scan tracking',
|
||||
'Standard QR design templates',
|
||||
'Your colors - foreground and background, free',
|
||||
'Download as SVG/PNG',
|
||||
],
|
||||
caption: 'Good for proving the mechanism on 1–3 placements.',
|
||||
caption: 'Good for proving the mechanism on 1-3 placements.',
|
||||
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Cancel paid plan',
|
||||
buttonVariant: 'outline' as const,
|
||||
disabled: currentPlan === 'FREE',
|
||||
@@ -162,10 +162,10 @@ export default function PricingPage() {
|
||||
period: billingPeriod === 'month' ? 'per month' : 'per year',
|
||||
showDiscount: billingPeriod === 'year',
|
||||
features: [
|
||||
'50 dynamic QR codes — enough for a full campaign, or several at once',
|
||||
'50 dynamic QR codes - enough for a full campaign, or several at once',
|
||||
'Compare scans by device and location, not just a running total',
|
||||
'Unlimited static QR codes',
|
||||
'Custom branding (colors & logos)',
|
||||
'4 module shapes, custom eye styles and your logo',
|
||||
],
|
||||
buttonText: isCurrentPlanWithInterval('PRO', selectedInterval)
|
||||
? 'Current Plan'
|
||||
@@ -187,7 +187,9 @@ export default function PricingPage() {
|
||||
'500 dynamic QR codes',
|
||||
'Unlimited static QR codes',
|
||||
'Everything from Pro',
|
||||
'Bulk QR Creation (up to 1,000, static output)',
|
||||
'Full designer: 11 module shapes and colour gradients',
|
||||
'Saved design presets, applied to a whole bulk upload',
|
||||
'Bulk QR Creation (1,000 static or 500 dynamic)',
|
||||
'Priority email support',
|
||||
'Advanced tracking & insights',
|
||||
],
|
||||
@@ -229,8 +231,8 @@ export default function PricingPage() {
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
Every plan includes unlimited static codes that never expire. Pick
|
||||
based on how many active placements you're tracking right now —
|
||||
not how many you might need someday — and upgrade in seconds if
|
||||
based on how many active placements you're tracking right now -
|
||||
not how many you might need someday - and upgrade in seconds if
|
||||
that changes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ const faqItems = [
|
||||
{
|
||||
question: 'Which plan includes bulk QR creation?',
|
||||
answer:
|
||||
'Bulk QR creation is included in the Business plan. Bulk-created codes are static output, not dynamic or trackable.',
|
||||
'Bulk QR creation is included in the Business plan. You choose static or dynamic per upload: up to 1,000 static codes, or up to 500 dynamic ones, limited by the Business dynamic code allowance.',
|
||||
},
|
||||
{
|
||||
question: 'Which plans include analytics and branding?',
|
||||
@@ -116,7 +116,7 @@ export default function PricingPage() {
|
||||
Compare free QR generators with paid dynamic QR workflows before
|
||||
choosing a plan. The difference usually comes down to how many
|
||||
placements you're running at once, and whether you need to
|
||||
compare them — not just the number of codes.
|
||||
compare them - not just the number of codes.
|
||||
</p>
|
||||
<Link
|
||||
href="/compare/free-vs-paid-qr-code-generator"
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function QRCodeAnalyticsPage() {
|
||||
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.',
|
||||
'Create QR codes with updatable destinations so analytics can inform what to change - without reprinting.',
|
||||
ctaLabel: 'Create dynamic QR code',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function QRCodeForMarketingCampaignsPage() {
|
||||
title="Your client asks which flyer worked. You hand them a scan count."
|
||||
description="Plan QR codes for marketing campaigns around placement tracking, changing destinations, and offline-to-online attribution."
|
||||
eyebrow="Campaign Workflows"
|
||||
intro="A scan count isn't attribution — it's a number with no context. Here's how to actually answer the question."
|
||||
intro="A scan count isn't attribution - it's a number with no context. Here's how to actually answer the question."
|
||||
pageType="commercial"
|
||||
cluster="marketing-campaigns"
|
||||
useCase="marketing-campaigns"
|
||||
@@ -30,7 +30,7 @@ export default function QRCodeForMarketingCampaignsPage() {
|
||||
},
|
||||
]}
|
||||
answer="A campaign QR code should do more than open a page. It should help you compare placements, update the destination when the offer changes, and route offline traffic into a measurable funnel."
|
||||
directAnswer="A scan count is not attribution. It tells you a scan happened — not which placement, which creative, or which channel earned it, and not what happened after the scan. That's still GA4's job, or your CRM's. What a QR code can actually do is make that handoff clean: one code per placement, a destination you can still edit after print, and scan data you can compare across placements instead of one running total."
|
||||
directAnswer="A scan count is not attribution. It tells you a scan happened - not which placement, which creative, or which channel earned it, and not what happened after the scan. That's still GA4's job, or your CRM's. What a QR code can actually do is make that handoff clean: one code per placement, a destination you can still edit after print, and scan data you can compare across placements instead of one running total."
|
||||
authoritySignals={[
|
||||
"One code per placement",
|
||||
"Editable after print",
|
||||
|
||||
@@ -14,7 +14,7 @@ export const metadata: Metadata = {
|
||||
},
|
||||
openGraph: {
|
||||
title: 'QR Code Print Size Guide | QR Master',
|
||||
description: 'Exact QR code dimensions for every print surface — from business cards to billboards.',
|
||||
description: 'Exact QR code dimensions for every print surface - from business cards to billboards.',
|
||||
type: 'article',
|
||||
url: 'https://www.qrmaster.net/qr-code-print-size-guide',
|
||||
},
|
||||
@@ -59,7 +59,7 @@ const faqSchema = {
|
||||
name: 'What is the minimum size for a QR code to be scannable?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'The minimum recommended size is 2 × 2 cm (0.8 × 0.8 in) for QR codes scanned at very close range (under 10 cm), such as on business cards. For most print materials scanned at arm\'s length (30–40 cm), use at least 3 × 3 cm.',
|
||||
text: 'The minimum recommended size is 2 × 2 cm (0.8 × 0.8 in) for QR codes scanned at very close range (under 10 cm), such as on business cards. For most print materials scanned at arm\'s length (30-40 cm), use at least 3 × 3 cm.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -67,7 +67,7 @@ const faqSchema = {
|
||||
name: 'What DPI should I use for printing a QR code?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'For print, export your QR code at a minimum of 300 DPI. For large-format printing (posters, banners), use the vector SVG format instead of PNG — SVG scales to any size without quality loss.',
|
||||
text: 'For print, export your QR code at a minimum of 300 DPI. For large-format printing (posters, banners), use the vector SVG format instead of PNG - SVG scales to any size without quality loss.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -75,7 +75,7 @@ const faqSchema = {
|
||||
name: 'How big should a QR code be on a poster?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'For an A2 poster (420 × 594 mm) viewed at 1–2 metres, use a QR code that is at least 8 × 8 cm. Larger is always better — a 10 × 10 cm code scans reliably from 2–3 metres away.',
|
||||
text: 'For an A2 poster (420 × 594 mm) viewed at 1-2 metres, use a QR code that is at least 8 × 8 cm. Larger is always better - a 10 × 10 cm code scans reliably from 2-3 metres away.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -83,7 +83,7 @@ const faqSchema = {
|
||||
name: 'How big should a QR code be on a business card?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'On a standard business card (85 × 55 mm), a QR code of 2 × 2 cm to 2.5 × 2.5 cm works reliably when held at normal reading distance (15–20 cm).',
|
||||
text: 'On a standard business card (85 × 55 mm), a QR code of 2 × 2 cm to 2.5 × 2.5 cm works reliably when held at normal reading distance (15-20 cm).',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -98,12 +98,12 @@ const faqSchema = {
|
||||
};
|
||||
|
||||
const sizeData = [
|
||||
{ surface: 'Business Card', scanDistance: '15–20 cm', minSize: '2 × 2 cm', recommended: '2.5 × 2.5 cm', dpi: '300 DPI PNG', format: 'PNG or SVG' },
|
||||
{ surface: 'Flyer / Leaflet', scanDistance: '20–30 cm', minSize: '3 × 3 cm', recommended: '4 × 4 cm', dpi: '300 DPI PNG', format: 'PNG or SVG' },
|
||||
{ surface: 'A4 Poster', scanDistance: '30–60 cm', minSize: '5 × 5 cm', recommended: '7 × 7 cm', dpi: '300 DPI PNG', format: 'SVG recommended' },
|
||||
{ surface: 'A2 / A1 Poster', scanDistance: '1–2 m', minSize: '8 × 8 cm', recommended: '10 × 10 cm', dpi: '300 DPI', format: 'SVG required' },
|
||||
{ surface: 'Window / Banner', scanDistance: '1–3 m', minSize: '10 × 10 cm', recommended: '15 × 15 cm', dpi: 'Vector only', format: 'SVG required' },
|
||||
{ surface: 'Billboard', scanDistance: '3–10 m', minSize: '20 × 20 cm', recommended: '30 × 30 cm', dpi: 'Vector only', format: 'SVG required' },
|
||||
{ surface: 'Business Card', scanDistance: '15-20 cm', minSize: '2 × 2 cm', recommended: '2.5 × 2.5 cm', dpi: '300 DPI PNG', format: 'PNG or SVG' },
|
||||
{ surface: 'Flyer / Leaflet', scanDistance: '20-30 cm', minSize: '3 × 3 cm', recommended: '4 × 4 cm', dpi: '300 DPI PNG', format: 'PNG or SVG' },
|
||||
{ surface: 'A4 Poster', scanDistance: '30-60 cm', minSize: '5 × 5 cm', recommended: '7 × 7 cm', dpi: '300 DPI PNG', format: 'SVG recommended' },
|
||||
{ surface: 'A2 / A1 Poster', scanDistance: '1-2 m', minSize: '8 × 8 cm', recommended: '10 × 10 cm', dpi: '300 DPI', format: 'SVG required' },
|
||||
{ surface: 'Window / Banner', scanDistance: '1-3 m', minSize: '10 × 10 cm', recommended: '15 × 15 cm', dpi: 'Vector only', format: 'SVG required' },
|
||||
{ surface: 'Billboard', scanDistance: '3-10 m', minSize: '20 × 20 cm', recommended: '30 × 30 cm', dpi: 'Vector only', format: 'SVG required' },
|
||||
];
|
||||
|
||||
export default function QRCodePrintSizeGuidePage() {
|
||||
@@ -195,7 +195,7 @@ export default function QRCodePrintSizeGuidePage() {
|
||||
</div>
|
||||
<h3 className="font-bold text-slate-900 mb-2">SVG for Everything Larger</h3>
|
||||
<p className="text-sm text-slate-600 leading-relaxed">
|
||||
SVG is a vector format — it scales to any size without quality loss. Use SVG for all A2+ posters, banners, and billboards. QR Master exports SVG directly from the generator.
|
||||
SVG is a vector format - it scales to any size without quality loss. Use SVG for all A2+ posters, banners, and billboards. QR Master exports SVG directly from the generator.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -205,7 +205,7 @@ export default function QRCodePrintSizeGuidePage() {
|
||||
</div>
|
||||
<h3 className="font-bold text-slate-900 mb-2">Error Correction Level</h3>
|
||||
<p className="text-sm text-slate-600 leading-relaxed">
|
||||
Use <strong>Level Q</strong> (25% recovery) or <strong>Level H</strong> (30% recovery) for print — especially if you add a logo. Higher error correction means more modules and a slightly larger minimum size.
|
||||
Use <strong>Level Q</strong> (25% recovery) or <strong>Level H</strong> (30% recovery) for print - especially if you add a logo. Higher error correction means more modules and a slightly larger minimum size.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,11 +282,11 @@ export default function QRCodePrintSizeGuidePage() {
|
||||
},
|
||||
{
|
||||
question: 'What DPI should I use for printing a QR code?',
|
||||
answer: 'Use 300 DPI minimum for all print. For large format (posters, banners, billboards), always use SVG — it is vector-based and scales to any size without quality loss.',
|
||||
answer: 'Use 300 DPI minimum for all print. For large format (posters, banners, billboards), always use SVG - it is vector-based and scales to any size without quality loss.',
|
||||
},
|
||||
{
|
||||
question: 'How big should a QR code be on a poster?',
|
||||
answer: 'For an A2 poster viewed at 1–2 metres, use at least 8 × 8 cm. For A1 posters viewed from further away, 10 × 10 cm is recommended.',
|
||||
answer: 'For an A2 poster viewed at 1-2 metres, use at least 8 × 8 cm. For A1 posters viewed from further away, 10 × 10 cm is recommended.',
|
||||
},
|
||||
{
|
||||
question: 'How big should a QR code be on a business card?',
|
||||
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'QR Code Tracking: Track QR Code Scans',
|
||||
absolute: 'QR Code Tracking - See Which Placement Drove the Scan',
|
||||
},
|
||||
description:
|
||||
'Track QR code scans with dynamic QR tracking. See scan time, device, location context, placements, and privacy-aware analytics for printed campaigns.',
|
||||
'Give every flyer, poster and table tent its own code, then see which one actually worked. Scan data by time, device and location. IPs hashed, no scanner data.',
|
||||
keywords:
|
||||
'qr code tracking, qr code analytics, track qr scans, dynamic qr tracking, qr scan analytics',
|
||||
alternates: {
|
||||
@@ -30,9 +30,9 @@ export const metadata: Metadata = {
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: 'QR Code Tracking: Track QR Code Scans',
|
||||
title: 'QR Code Tracking - See Which Placement Drove the Scan',
|
||||
description:
|
||||
'Track QR code scans with analytics for time, device, and location context. Use dynamic QR codes to measure placements and campaigns.',
|
||||
'One code per placement turns print into measurable media. Scan data by time, device, and location context - for flyers, posters, packaging, and events.',
|
||||
url: 'https://www.qrmaster.net/qr-code-tracking',
|
||||
type: 'website',
|
||||
images: ['/og-image.png'],
|
||||
@@ -193,7 +193,7 @@ const faqItems = [
|
||||
{
|
||||
question: 'Is QR code scan tracking free?',
|
||||
answer:
|
||||
'Yes — the Free plan includes basic scan tracking for up to 3 active dynamic QR codes. Pro and Business plans include extended scan history, more active codes, and full analytics.',
|
||||
'Yes - the Free plan includes basic scan tracking for up to 3 active dynamic QR codes. Pro and Business plans include extended scan history, more active codes, and full analytics.',
|
||||
},
|
||||
{
|
||||
question: 'Can I see unlimited scan history?',
|
||||
@@ -391,7 +391,7 @@ export default function QRCodeTrackingPage() {
|
||||
<p className="text-xl leading-relaxed text-gray-600">
|
||||
Track QR code scans by time, device, and location context
|
||||
with dynamic QR codes, so printed campaigns and physical
|
||||
placements stop being guesswork — see which placement
|
||||
placements stop being guesswork - see which placement
|
||||
worked before you plan the next print run.
|
||||
</p>
|
||||
</div>
|
||||
@@ -502,7 +502,7 @@ export default function QRCodeTrackingPage() {
|
||||
QR codes without tracking vs. with QR Master tracking
|
||||
</h2>
|
||||
<p className="text-slate-400 mb-10 max-w-2xl">
|
||||
Printing a QR code without scan analytics is like running a billboard campaign with no impression data — you spend the budget but can't tell what worked.
|
||||
Printing a QR code without scan analytics is like running a billboard campaign with no impression data - you spend the budget but can't tell what worked.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="bg-slate-800 rounded-2xl p-6 border border-slate-700">
|
||||
@@ -515,10 +515,10 @@ export default function QRCodeTrackingPage() {
|
||||
<h3 className="text-lg font-bold text-white">Without tracking</h3>
|
||||
</div>
|
||||
<ul className="space-y-3 text-slate-400 text-sm">
|
||||
<li className="flex gap-2"><span className="text-red-400 mt-0.5">–</span>You print QR codes and have no idea if anyone scans them</li>
|
||||
<li className="flex gap-2"><span className="text-red-400 mt-0.5">–</span>You can't tell which flyer, sign, or table card performs best</li>
|
||||
<li className="flex gap-2"><span className="text-red-400 mt-0.5">–</span>Broken or outdated destination URLs require a full reprint</li>
|
||||
<li className="flex gap-2"><span className="text-red-400 mt-0.5">–</span>No way to know if your campaign timing or placement was right</li>
|
||||
<li className="flex gap-2"><span className="text-red-400 mt-0.5">-</span>You print QR codes and have no idea if anyone scans them</li>
|
||||
<li className="flex gap-2"><span className="text-red-400 mt-0.5">-</span>You can't tell which flyer, sign, or table card performs best</li>
|
||||
<li className="flex gap-2"><span className="text-red-400 mt-0.5">-</span>Broken or outdated destination URLs require a full reprint</li>
|
||||
<li className="flex gap-2"><span className="text-red-400 mt-0.5">-</span>No way to know if your campaign timing or placement was right</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-emerald-900/30 rounded-2xl p-6 border border-emerald-700/50">
|
||||
@@ -533,7 +533,7 @@ export default function QRCodeTrackingPage() {
|
||||
<ul className="space-y-3 text-slate-300 text-sm">
|
||||
<li className="flex gap-2"><span className="text-emerald-400 mt-0.5">+</span>See exactly which QR codes are being scanned and when</li>
|
||||
<li className="flex gap-2"><span className="text-emerald-400 mt-0.5">+</span>Compare placements: restaurant table A vs. table B, flyer vs. window sign</li>
|
||||
<li className="flex gap-2"><span className="text-emerald-400 mt-0.5">+</span>Know the device mix — 89% of scans are mobile, so you can optimize landing pages</li>
|
||||
<li className="flex gap-2"><span className="text-emerald-400 mt-0.5">+</span>Know the device mix - 89% of scans are mobile, so you can optimize landing pages</li>
|
||||
<li className="flex gap-2"><span className="text-emerald-400 mt-0.5">+</span>Update destinations without reprinting; fix errors in seconds from your dashboard</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -824,7 +824,7 @@ export default function QRCodeTrackingPage() {
|
||||
Industry data
|
||||
</p>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-3">
|
||||
QR code adoption is accelerating — tracking makes that growth measurable
|
||||
QR code adoption is accelerating - tracking makes that growth measurable
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-10 max-w-2xl">
|
||||
As QR scan volumes grow, businesses that track their codes gain compounding insight advantages over those that print blind.
|
||||
@@ -854,7 +854,7 @@ export default function QRCodeTrackingPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* WHY QR TRACKING MATTERS — STATISTICS */}
|
||||
{/* WHY QR TRACKING MATTERS - STATISTICS */}
|
||||
<section className="bg-white py-16">
|
||||
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -879,7 +879,7 @@ export default function QRCodeTrackingPage() {
|
||||
Why Tracking Makes QR Codes Measurable Marketing Assets
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-10 max-w-2xl">
|
||||
Without scan analytics, a printed QR code is invisible — you can't
|
||||
Without scan analytics, a printed QR code is invisible - you can't
|
||||
tell if your campaign placement is working. Tracking turns every
|
||||
scan into actionable data.
|
||||
</p>
|
||||
@@ -890,8 +890,8 @@ export default function QRCodeTrackingPage() {
|
||||
89% vs 33%
|
||||
</div>
|
||||
<p className="text-gray-700 text-sm leading-relaxed mb-3">
|
||||
Companies with strong omnichannel engagement — requiring
|
||||
closed-loop tracking from offline to online — retain{' '}
|
||||
Companies with strong omnichannel engagement - requiring
|
||||
closed-loop tracking from offline to online - retain{' '}
|
||||
<strong>89% of their customers</strong>, compared to 33% for
|
||||
businesses without integrated tracking.
|
||||
</p>
|
||||
@@ -905,7 +905,7 @@ export default function QRCodeTrackingPage() {
|
||||
>
|
||||
Aberdeen Group
|
||||
</a>{' '}
|
||||
— Omnichannel Customer Engagement Study
|
||||
- Omnichannel Customer Engagement Study
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -916,7 +916,7 @@ export default function QRCodeTrackingPage() {
|
||||
<p className="text-gray-700 text-sm leading-relaxed mb-3">
|
||||
of small businesses identify print and direct mail errors as a
|
||||
major source of wasted marketing budget. QR tracking reveals
|
||||
which placements actually drive scans — so you reprint only
|
||||
which placements actually drive scans - so you reprint only
|
||||
what works.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function ReprintCalculatorPage() {
|
||||
|
||||
<p className="text-xl text-slate-600 mb-8 leading-relaxed max-w-2xl mx-auto">
|
||||
Every time a URL changes, static QR codes become useless trash.
|
||||
Dynamic QR codes let you update the destination URL at any time — your printed materials stay usable even when links change.
|
||||
Dynamic QR codes let you update the destination URL at any time - your printed materials stay usable even when links change.
|
||||
</p>
|
||||
|
||||
<div className="flex justify-center">
|
||||
|
||||
@@ -51,7 +51,7 @@ const RESULTS: Record<string, Result> = {
|
||||
'pharmacode': {
|
||||
format: 'Pharmacode',
|
||||
label: 'Pharmacode',
|
||||
description: 'A pharmaceutical packaging standard used to verify correct product packaging. Encodes a single numeric value (3–131071).',
|
||||
description: 'A pharmaceutical packaging standard used to verify correct product packaging. Encodes a single numeric value (3-131071).',
|
||||
example: '12345',
|
||||
color: 'red',
|
||||
},
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function BarcodeGeneratorClient() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="url" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -14,7 +14,7 @@ export function BarcodeGuide() {
|
||||
<BookOpen className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-slate-900 m-0">
|
||||
Barcode Generator – How Barcodes Work and Why They Matter
|
||||
Barcode Generator - How Barcodes Work and Why They Matter
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mb-8 not-prose">
|
||||
@@ -138,7 +138,7 @@ export function BarcodeGuide() {
|
||||
<tr className="border-t border-slate-100 bg-slate-50">
|
||||
<td className="p-3 font-bold text-red-700">Pharmacode</td>
|
||||
<td className="p-3 text-slate-600">Pharmaceutical packaging</td>
|
||||
<td className="p-3 text-slate-500 font-mono text-xs">3–131071 numeric</td>
|
||||
<td className="p-3 text-slate-500 font-mono text-xs">3-131071 numeric</td>
|
||||
<td className="p-3 text-slate-600">Pharma</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -185,7 +185,7 @@ export function BarcodeGuide() {
|
||||
|
||||
<h2>Barcode Accuracy: What the Research Shows</h2>
|
||||
<p>
|
||||
Barcodes are not just convenient — they are scientifically proven to reduce errors across industries. Independent research from logistics, healthcare, and economics consistently shows the same result: switching from manual data entry to barcode scanning produces dramatic accuracy gains.
|
||||
Barcodes are not just convenient - they are scientifically proven to reduce errors across industries. Independent research from logistics, healthcare, and economics consistently shows the same result: switching from manual data entry to barcode scanning produces dramatic accuracy gains.
|
||||
</p>
|
||||
|
||||
<div className="not-prose grid gap-4 my-8">
|
||||
@@ -240,7 +240,7 @@ export function BarcodeGuide() {
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-slate-600"
|
||||
>
|
||||
New England Journal of Medicine — "Effect of Bar-Code Technology on the Safety of Medication Administration"
|
||||
New England Journal of Medicine - "Effect of Bar-Code Technology on the Safety of Medication Administration"
|
||||
</a>
|
||||
{'; '}
|
||||
<a
|
||||
@@ -249,7 +249,7 @@ export function BarcodeGuide() {
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-slate-600"
|
||||
>
|
||||
AHRQ — Barcode Medication Administration (BCMA) research
|
||||
AHRQ - Barcode Medication Administration (BCMA) research
|
||||
</a>
|
||||
{'.'}
|
||||
</p>
|
||||
@@ -267,7 +267,7 @@ export function BarcodeGuide() {
|
||||
Significant Productivity Gains at Checkout (Retail Economics)
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 mb-2">
|
||||
The productivity impact of barcodes extends beyond accuracy. Economic research from the National Bureau of Economic Research (NBER) documents dramatic throughput gains for retailers when switching from manual key-entry to barcode scanners — an effect that transformed checkout speed and operational cost structures across the industry.
|
||||
The productivity impact of barcodes extends beyond accuracy. Economic research from the National Bureau of Economic Research (NBER) documents dramatic throughput gains for retailers when switching from manual key-entry to barcode scanners - an effect that transformed checkout speed and operational cost structures across the industry.
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">
|
||||
Source:{' '}
|
||||
@@ -277,7 +277,7 @@ export function BarcodeGuide() {
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-slate-600"
|
||||
>
|
||||
NBER Working Paper — "Raising the Barcode Scanner: Technology and Productivity in the Retail Sector"
|
||||
NBER Working Paper - "Raising the Barcode Scanner: Technology and Productivity in the Retail Sector"
|
||||
</a>
|
||||
{'.'}
|
||||
</p>
|
||||
@@ -329,7 +329,7 @@ export function BarcodeGuide() {
|
||||
|
||||
<h2>Understanding Check Digits</h2>
|
||||
<p>
|
||||
Most barcodes (like EAN and UPC) include a "Check Digit"—the last number in the sequence. This digit is calculated mathematically from the other numbers to ensure the barcode is scanned correctly. Even if a barcode is slightly damaged or scratched, the scanner uses the check digit to verify the integrity of the data.
|
||||
Most barcodes (like EAN and UPC) include a "Check Digit"-the last number in the sequence. This digit is calculated mathematically from the other numbers to ensure the barcode is scanned correctly. Even if a barcode is slightly damaged or scratched, the scanner uses the check digit to verify the integrity of the data.
|
||||
</p>
|
||||
|
||||
<h2>Best Practices for Printing Barcodes</h2>
|
||||
@@ -363,7 +363,7 @@ export function BarcodeGuide() {
|
||||
},
|
||||
{
|
||||
question: 'Can I download barcodes in vector format (SVG)?',
|
||||
answer: 'Yes — SVG downloads are available. SVG files are vector-based, meaning they can be scaled to any size without losing quality. This is ideal for professional product packaging and labels.',
|
||||
answer: 'Yes - SVG downloads are available. SVG files are vector-based, meaning they can be scaled to any size without losing quality. This is ideal for professional product packaging and labels.',
|
||||
},
|
||||
{
|
||||
question: 'How do I generate a barcode online?',
|
||||
@@ -375,11 +375,11 @@ export function BarcodeGuide() {
|
||||
},
|
||||
{
|
||||
question: 'Can I use these barcodes for Amazon (EAN/UPC)?',
|
||||
answer: 'You can generate the barcode <em>image</em> here if you already have a valid EAN/UPC number. However, you cannot create a globally registered EAN/UPC number here — you must purchase official numbers from GS1 to list products on Amazon or in major retail systems.',
|
||||
answer: 'You can generate the barcode <em>image</em> here if you already have a valid EAN/UPC number. However, you cannot create a globally registered EAN/UPC number here - you must purchase official numbers from GS1 to list products on Amazon or in major retail systems.',
|
||||
},
|
||||
{
|
||||
question: 'What is the difference between a barcode and a QR code?',
|
||||
answer: 'A barcode stores data in one dimension (horizontal bars) and is mainly used for product identification. A QR code stores data in two dimensions (a matrix) and can hold much more information — URLs, contact details, WiFi credentials, and more.',
|
||||
answer: 'A barcode stores data in one dimension (horizontal bars) and is mainly used for product identification. A QR code stores data in two dimensions (a matrix) and can hold much more information - URLs, contact details, WiFi credentials, and more.',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -23,10 +23,10 @@ import {
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Custom Barcode Generator - EAN, UPC, Code 128',
|
||||
absolute: 'Barcode Generator - EAN-13, UPC-A, Code 128, Free',
|
||||
},
|
||||
description:
|
||||
'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.',
|
||||
'Type a number, get a scannable barcode. EAN-13, UPC-A and Code 128 for retail shelves, inventory and labels. Download print-ready PNG or SVG. Free, no signup.',
|
||||
keywords: [
|
||||
'barcode generator',
|
||||
'custom barcode generator',
|
||||
@@ -49,9 +49,8 @@ export const metadata: Metadata = {
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Custom Barcode Generator - EAN, UPC & Code 128',
|
||||
description:
|
||||
'Free online barcode maker for EAN-13, UPC-A, and Code 128. Create scannable custom barcodes in seconds and download PNG or SVG.',
|
||||
title: 'Barcode Generator - EAN-13, UPC-A, Code 128, Free',
|
||||
description: 'Type a number, get a scannable barcode. For retail shelves, inventory and labels.',
|
||||
url: 'https://www.qrmaster.net/tools/barcode-generator',
|
||||
siteName: 'QR Master',
|
||||
locale: 'en_US',
|
||||
@@ -62,9 +61,8 @@ export const metadata: Metadata = {
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Free Barcode Generator',
|
||||
description:
|
||||
'Create custom barcodes in seconds. Download high-quality PNG/SVG.',
|
||||
title: 'Barcode Generator - EAN-13, UPC-A, Code 128, Free',
|
||||
description: 'Type a number, get a scannable barcode. For retail shelves, inventory and labels.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -163,7 +161,7 @@ const jsonLd = {
|
||||
'Can I download barcodes in vector format (SVG)?': {
|
||||
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.',
|
||||
'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.',
|
||||
},
|
||||
'How do I generate a barcode online?': {
|
||||
question: 'How do I generate a barcode online?',
|
||||
@@ -178,7 +176,7 @@ const jsonLd = {
|
||||
'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.',
|
||||
'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.',
|
||||
},
|
||||
'What is the difference between a barcode and a QR code?': {
|
||||
question: 'What is the difference between a barcode and a QR code?',
|
||||
@@ -188,7 +186,7 @@ const jsonLd = {
|
||||
'What barcode format do Amazon and Walmart require?': {
|
||||
question: 'What barcode format do Amazon and Walmart require?',
|
||||
answer:
|
||||
'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.',
|
||||
'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.',
|
||||
},
|
||||
'What is the minimum print size for a scannable barcode?': {
|
||||
question: 'What is the minimum print size for a scannable barcode?',
|
||||
@@ -203,7 +201,7 @@ const jsonLd = {
|
||||
'What is the difference between EAN-13 and UPC-A?': {
|
||||
question: 'What is the difference between EAN-13 and UPC-A?',
|
||||
answer:
|
||||
'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.',
|
||||
'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.',
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -258,7 +256,7 @@ export default function BarcodeGeneratorPage() {
|
||||
<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>
|
||||
</span>
|
||||
Free Tool — Professional & Fast
|
||||
Free Tool - Professional & Fast
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||
@@ -399,13 +397,13 @@ export default function BarcodeGeneratorPage() {
|
||||
Inventory & Logistics
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
Supports letters + numbers — best for internal SKU systems
|
||||
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
|
||||
Barcode vs. QR Code - When to Use Which
|
||||
</h3>
|
||||
<div className="grid md:grid-cols-2 gap-4 text-sm text-slate-700">
|
||||
<div>
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function PhoneGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="url" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function CallQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
@@ -148,7 +148,7 @@ export default function CallQRCodePage() {
|
||||
</h1>
|
||||
|
||||
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||
Create a QR code that opens the phone dialer with your number pre-filled — one scan, no typing.
|
||||
Create a QR code that opens the phone dialer with your number pre-filled - one scan, no typing.
|
||||
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Works on every smartphone.</strong>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ export default function CryptoGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="crypto" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -10,24 +10,24 @@ import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Crypto QR Code Generator | Krypto QR Code Erstellen | QR Master',
|
||||
absolute: 'Crypto Wallet QR Code Generator - BTC, ETH, USDT',
|
||||
},
|
||||
description: 'Create a QR code for your Crypto wallet address. Erstelle Bitcoin & Ethereum QR Codes für einfache Zahlungen. Supports BTC, ETH, USDT & more.',
|
||||
description: 'Turn a wallet address into a QR code so nobody retypes 42 characters. Supports Bitcoin, Ethereum, USDT, Solana and more. Free, no signup, no wallet access.',
|
||||
keywords: ['crypto qr code', 'bitcoin qr generator', 'ethereum qr code', 'crypto wallet qr', 'donation qr code', 'krypto qr code', 'bitcoin qr code erstellen', 'kryptowährung qr code', 'wallet adresse qr code'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/crypto-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Crypto QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes to accept Crypto payments securely. Supports BTC, ETH, SOL.',
|
||||
title: 'Crypto Wallet QR Code Generator - BTC, ETH, USDT',
|
||||
description: 'Turn a wallet address into a QR code so nobody retypes 42 characters. No wallet access.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/crypto-qr-code',
|
||||
images: [{ url: '/og-crypto-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Free Crypto QR Code Generator',
|
||||
description: 'Create secure QR codes for your crypto wallet.',
|
||||
title: 'Crypto Wallet QR Code Generator - BTC, ETH, USDT',
|
||||
description: 'Turn a wallet address into a QR code so nobody retypes 42 characters. No wallet access.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -143,7 +143,7 @@ export default function CryptoQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-orange-400"></span>
|
||||
</span>
|
||||
Free Tool — Secure & Private
|
||||
Free Tool - Secure & Private
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function EmailGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="url" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function EmailPage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-300 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-300"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function EventGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="meeting" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function EventQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-violet-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-violet-400"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function FacebookGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="social" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -10,24 +10,24 @@ import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Facebook QR Code Generator | Get Likes & Follows | QR Master',
|
||||
absolute: 'Facebook QR Code Generator - Page, Profile or Group',
|
||||
},
|
||||
description: 'Create a QR code for your Facebook Page, Profile, or Group. Facebook QR Code erstellen. Scanners follow you instantly. Free & Easy.',
|
||||
description: 'Turn a Facebook Page, profile or group into a scannable QR code. One scan opens it in the app. Add your colors and logo, download PNG or SVG. Free, no signup.',
|
||||
keywords: ['facebook qr code', 'fb qr generator', 'facebook page qr', 'follow qr code', 'social media qr code', 'facebook qr code erstellen', 'facebook seite qr code', 'facebook gruppe qr code', 'facebook profil qr code', 'mehr likes qr code'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/facebook-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Facebook QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes to grow your Facebook audience. Instant app redirect.',
|
||||
title: 'Facebook QR Code Generator - Page, Profile or Group',
|
||||
description: 'One scan opens your Page, profile or group in the app. Your colors, your logo, PNG or SVG.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/facebook-qr-code',
|
||||
images: [{ url: '/og-facebook-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Free Facebook QR Code Generator',
|
||||
description: 'Create QR codes for Facebook. Boost your engagement.',
|
||||
title: 'Facebook QR Code Generator - Page, Profile or Group',
|
||||
description: 'One scan opens your Page, profile or group in the app. Your colors, your logo, PNG or SVG.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -139,7 +139,7 @@ export default function FacebookQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-300 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-300"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
@@ -285,7 +285,7 @@ export default function FacebookQRCodePage() {
|
||||
Where a Facebook QR Code Works Best
|
||||
</h2>
|
||||
<p className="text-slate-600 mb-8 max-w-2xl">
|
||||
Facebook is where local communities, events, and groups live. A QR code bridges the gap between a physical location and your page, group, or event — no searching, no typos.
|
||||
Facebook is where local communities, events, and groups live. A QR code bridges the gap between a physical location and your page, group, or event - no searching, no typos.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-3 gap-6 mb-8">
|
||||
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function GeolocationGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="url" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -10,24 +10,24 @@ import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Geolocation QR Code Generator | Standort & Map Links | QR Master',
|
||||
absolute: 'Location QR Code Generator - Maps & Directions, Free',
|
||||
},
|
||||
description: 'Create a QR code for a specific location. Erstelle einen Map QR Code für Google Maps. Coordinates & Directions instantly. Standort teilen leicht gemacht.',
|
||||
description: 'Turn an address or GPS coordinates into a QR code. One scan opens directions in Google Maps or Apple Maps. For signage, flyers and event wayfinding. No signup.',
|
||||
keywords: ['location qr code', 'maps qr code', 'google maps qr generator', 'geolocation qr', 'coordinates qr code', 'standort qr code', 'google maps qr code erstellen', 'koordinaten qr code', 'wegbeschreibung qr code', 'maps qr code generator'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/geolocation-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Geolocation QR Code Generator | QR Master',
|
||||
description: 'Navigate users to any location with a QR code. Opens directly in Google Maps.',
|
||||
title: 'Location QR Code Generator - Maps & Directions, Free',
|
||||
description: 'One scan opens directions in Google Maps or Apple Maps. For signage and wayfinding.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/geolocation-qr-code',
|
||||
images: [{ url: '/og-geolocation-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Free Geolocation QR Code Generator',
|
||||
description: 'Create QR codes for maps and locations. Instant and free.',
|
||||
title: 'Location QR Code Generator - Maps & Directions, Free',
|
||||
description: 'One scan opens directions in Google Maps or Apple Maps. For signage and wayfinding.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -139,7 +139,7 @@ export default function GeolocationQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function GoogleReviewGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="googleReview" />
|
||||
<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="grid lg:grid-cols-2">
|
||||
|
||||
@@ -10,10 +10,10 @@ import { generateSoftwareAppSchema } from '@/lib/schema-utils';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Google Review QR Code Generator — Free | QR Master',
|
||||
absolute: 'Google Review QR Code Generator - Free, No Signup',
|
||||
},
|
||||
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.',
|
||||
'Ask for the review while the customer is still standing there. One scan opens your Google review form - no searching, no link. Free, no signup, PNG or SVG.',
|
||||
keywords: [
|
||||
'qr code for google reviews',
|
||||
'qr code generator for google reviews',
|
||||
@@ -29,17 +29,15 @@ export const metadata: Metadata = {
|
||||
},
|
||||
},
|
||||
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.',
|
||||
title: 'Google Review QR Code Generator - Free, No Signup',
|
||||
description: 'Ask while the customer is still standing there. One scan opens your Google review form.',
|
||||
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.',
|
||||
title: 'Google Review QR Code Generator - Free, No Signup',
|
||||
description: 'Ask while the customer is still standing there. One scan opens your Google review form.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -103,7 +101,7 @@ const jsonLd = {
|
||||
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.',
|
||||
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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -188,13 +186,13 @@ export default function GoogleReviewQRCodePage() {
|
||||
<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
|
||||
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">
|
||||
Google Review QR Code <br className="hidden lg:block" />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-orange-400">
|
||||
Generator — Free
|
||||
Generator - Free
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
@@ -391,7 +389,7 @@ export default function GoogleReviewQRCodePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* WHY REVIEWS MATTER — STATISTICS */}
|
||||
{/* 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">
|
||||
@@ -405,7 +403,7 @@ export default function GoogleReviewQRCodePage() {
|
||||
</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
|
||||
between a satisfied customer and a published review - the single
|
||||
biggest barrier to getting more reviews.
|
||||
</p>
|
||||
|
||||
@@ -416,8 +414,8 @@ export default function GoogleReviewQRCodePage() {
|
||||
</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 1–3%.
|
||||
<strong>if they are asked</strong> - but most businesses never
|
||||
ask, or ask via email where completion rates drop to 1-3%.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Source:{' '}
|
||||
@@ -439,7 +437,7 @@ export default function GoogleReviewQRCodePage() {
|
||||
<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
|
||||
point of sale - where satisfaction is highest - maximizes this
|
||||
effect.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
@@ -470,13 +468,13 @@ export default function GoogleReviewQRCodePage() {
|
||||
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>
|
||||
<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>
|
||||
<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">The BrightLocal research above shows 70% of consumers will leave a review if asked, and asking in person at the point of sale removes the friction that email follow-ups add. 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 in monthly reviews compounds over a year into a 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>
|
||||
<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>
|
||||
@@ -511,7 +509,7 @@ export default function GoogleReviewQRCodePage() {
|
||||
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.',
|
||||
'Update your review link or redirect to a different page anytime - no reprint needed.',
|
||||
ctaLabel: 'Create dynamic QR code',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function InstagramGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="social" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -11,9 +11,9 @@ import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Instagram QR Code Generator | Get More Followers | QR Master',
|
||||
absolute: 'Instagram QR Code Generator - Free, No Signup',
|
||||
},
|
||||
description: 'Create a free Instagram QR code for your profile. Scanners follow you instantly — no app login required. Customizable & downloadable in seconds.',
|
||||
description: 'Turn your Instagram profile into a scannable QR code. One scan opens your profile in the app - no login needed. Add your colors and logo, download PNG or SVG.',
|
||||
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: {
|
||||
canonical: 'https://www.qrmaster.net/tools/instagram-qr-code',
|
||||
@@ -23,16 +23,16 @@ export const metadata: Metadata = {
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Instagram QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes to grow your Instagram following. Instant app redirect.',
|
||||
title: 'Instagram QR Code Generator - Free, No Signup',
|
||||
description: 'One scan opens your Instagram profile in the app. Your colors, your logo, PNG or SVG.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/instagram-qr-code',
|
||||
images: [{ url: '/og-instagram-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Free Instagram QR Code Generator',
|
||||
description: 'Create QR codes for Instagram. Boost your followers.',
|
||||
title: 'Instagram QR Code Generator - Free, No Signup',
|
||||
description: 'One scan opens your Instagram profile in the app. Your colors, your logo, PNG or SVG.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -146,12 +146,12 @@ export default function InstagramQRCodePage() {
|
||||
<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>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
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>
|
||||
|
||||
<p className="text-lg md:text-xl text-pink-50 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||
@@ -317,7 +317,7 @@ export default function InstagramQRCodePage() {
|
||||
{
|
||||
href: '/tools/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',
|
||||
},
|
||||
{
|
||||
@@ -329,7 +329,7 @@ export default function InstagramQRCodePage() {
|
||||
{
|
||||
href: '/dynamic-qr-code-generator',
|
||||
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',
|
||||
},
|
||||
]}
|
||||
@@ -342,25 +342,25 @@ export default function InstagramQRCodePage() {
|
||||
Where an Instagram QR Code Beats a Profile Link
|
||||
</h2>
|
||||
<p className="text-slate-600 mb-8 max-w-2xl">
|
||||
A profile link only works where people can click. An Instagram QR code works in the physical world — wherever a customer is already looking at your brand but can't tap a link.
|
||||
A profile link only works where people can click. An Instagram QR code works in the physical world - wherever a customer is already looking at your brand but can't tap a link.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-3 gap-6 mb-8">
|
||||
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-bold text-slate-900 mb-2">Cafés, salons & shops</h3>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">
|
||||
Counter cards, mirrors, and window stickers turn walk-in customers into followers while they wait — the moment they're most likely to check your feed.
|
||||
Counter cards, mirrors, and window stickers turn walk-in customers into followers while they wait - the moment they're most likely to check your feed.
|
||||
</p>
|
||||
</article>
|
||||
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-bold text-slate-900 mb-2">Packaging & unboxing</h3>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">
|
||||
A QR code on the package insert catches customers at the unboxing moment — ideal for brands that want user-generated content and repeat buyers.
|
||||
A QR code on the package insert catches customers at the unboxing moment - ideal for brands that want user-generated content and repeat buyers.
|
||||
</p>
|
||||
</article>
|
||||
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-bold text-slate-900 mb-2">Events & pop-ups</h3>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">
|
||||
Booth banners and table displays let visitors follow you in two seconds instead of searching your handle — and misspelling it.
|
||||
Booth banners and table displays let visitors follow you in two seconds instead of searching your handle - and misspelling it.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
@@ -369,7 +369,7 @@ export default function InstagramQRCodePage() {
|
||||
<a href="/dynamic-qr-code-generator" className="font-semibold text-pink-600 hover:underline">dynamic QR code</a>{' '}
|
||||
and compare placements with{' '}
|
||||
<a href="/qr-code-tracking" className="font-semibold text-pink-600 hover:underline">QR code tracking</a>{' '}
|
||||
— scans by time, device, and location.
|
||||
- scans by time, device, and location.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -126,7 +126,7 @@ export default function PayPalGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="crypto" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -140,7 +140,7 @@ export default function PayPalQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-sky-400"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function SMSGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="url" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function SMSQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-amber-400"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function TeamsGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="meeting" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -12,24 +12,24 @@ import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Microsoft Teams QR Code Generator | Join Meetings | QR Master',
|
||||
absolute: 'Microsoft Teams Meeting QR Code Generator - Free',
|
||||
},
|
||||
description: 'Create a QR code for your Microsoft Teams meeting. Teams QR Code erstellen. Attendees scan to join instantly. For hybrid meetings & office displays.',
|
||||
description: 'Turn a Teams meeting link into a QR code. Attendees scan to join - no meeting ID, no copy-paste. Print it for meeting rooms, desks and hybrid event signage.',
|
||||
keywords: ['teams qr code', 'microsoft teams meeting qr', 'join teams qr code', 'meeting room qr', 'teams invitation qr', 'hybrid meeting qr code', 'microsoft teams qr code erstellen', 'teams meeting qr code', 'teams besprechung qr', 'teams beitreten qr'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/teams-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Microsoft Teams QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes for Teams meetings. One scan to join instantly.',
|
||||
title: 'Microsoft Teams Meeting QR Code Generator - Free',
|
||||
description: 'Attendees scan to join. No meeting ID, no copy-paste. Print it for meeting rooms and desks.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/teams-qr-code',
|
||||
images: [{ url: '/og-teams-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Free Microsoft Teams QR Code Generator',
|
||||
description: 'Create Teams meeting QR codes. Instant and free.',
|
||||
title: 'Microsoft Teams Meeting QR Code Generator - Free',
|
||||
description: 'Attendees scan to join. No meeting ID, no copy-paste. Print it for meeting rooms and desks.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -132,7 +132,7 @@ export default function TeamsQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
@@ -267,13 +267,13 @@ export default function TeamsQRCodePage() {
|
||||
<GrowthLinksSection
|
||||
eyebrow="Beyond a single meeting"
|
||||
title="More QR workflows for hybrid offices"
|
||||
description="A meeting link doesn't change often, but the same setup works for anything that does — front-desk WiFi, room booking pages, or a recurring standup you want to update without a new poster."
|
||||
description="A meeting link doesn't change often, but the same setup works for anything that does - front-desk WiFi, room booking pages, or a recurring standup you want to update without a new poster."
|
||||
links={[
|
||||
{
|
||||
href: '/dynamic-qr-code-generator',
|
||||
title: 'Dynamic QR Code Generator',
|
||||
description:
|
||||
'Point one printed code at a new link or meeting anytime — no reprint needed.',
|
||||
'Point one printed code at a new link or meeting anytime - no reprint needed.',
|
||||
ctaLabel: 'Create dynamic QR code',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function TextGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="url" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function TextQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function TiktokGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="social" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -10,24 +10,24 @@ import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free TikTok QR Code Generator | Get Followers | QR Master',
|
||||
absolute: 'TikTok QR Code Generator - Free, No Signup',
|
||||
},
|
||||
description: 'Create a QR code for your TikTok profile. TikTok QR Code erstellen. Scanners follow you instantly. Customize with colors and frames.',
|
||||
description: 'Turn your TikTok profile into a scannable QR code. One scan opens your profile in the app. Add your colors and logo, download PNG or SVG. Free, no signup.',
|
||||
keywords: ['tiktok qr code', 'tik tok qr generator', 'tiktok follow qr', 'social media qr code', 'tiktok profile qr', 'tiktok qr code erstellen', 'tiktok profil qr code', 'mehr tiktok follower', 'tiktok scanncode'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/tiktok-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free TikTok QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes to grow your TikTok following. Instant app redirect.',
|
||||
title: 'TikTok QR Code Generator - Free, No Signup',
|
||||
description: 'One scan opens your TikTok profile in the app. Your colors, your logo, PNG or SVG.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/tiktok-qr-code',
|
||||
images: [{ url: '/og-tiktok-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Free TikTok QR Code Generator',
|
||||
description: 'Create QR codes for TikTok. Get more followers.',
|
||||
title: 'TikTok QR Code Generator - Free, No Signup',
|
||||
description: 'One scan opens your TikTok profile in the app. Your colors, your logo, PNG or SVG.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -140,7 +140,7 @@ export default function TiktokQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-cyan-400"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function TwitterGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="social" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -10,24 +10,24 @@ import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Twitter (X) QR Code Generator | Follow & Connect | QR Master',
|
||||
absolute: 'X (Twitter) QR Code Generator - Free, No Signup',
|
||||
},
|
||||
description: 'Create a QR code for your X (Twitter) profile. Twitter QR Code erstellen. Scanners follow you instantly. Free & Customizable.',
|
||||
description: 'Turn your X profile into a scannable QR code. One scan opens your profile in the app. Add your colors and logo, download PNG or SVG. Free, no signup required.',
|
||||
keywords: ['twitter qr code', 'x qr generator', 'twitter follow qr', 'social media qr code', 'x profile qr', 'twitter qr code erstellen', 'x qr code erstellen', 'twitter profil qr code', 'x profil qr code'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/twitter-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Twitter (X) QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes to grow your X (Twitter) following. Instant app redirect.',
|
||||
title: 'X (Twitter) QR Code Generator - Free, No Signup',
|
||||
description: 'One scan opens your X profile in the app. Your colors, your logo, PNG or SVG.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/twitter-qr-code',
|
||||
images: [{ url: '/og-twitter-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Free Twitter (X) QR Code Generator',
|
||||
description: 'Create QR codes for X. Boost your following.',
|
||||
title: 'X (Twitter) QR Code Generator - Free, No Signup',
|
||||
description: 'One scan opens your X profile in the app. Your colors, your logo, PNG or SVG.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -139,7 +139,7 @@ export default function TwitterQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-500 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
@@ -301,7 +301,7 @@ export default function TwitterQRCodePage() {
|
||||
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-bold text-slate-900 mb-2">Live commentary</h3>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">
|
||||
Sports venues, meetups, and community events display the code where the live conversation happens — the moment people want to join in.
|
||||
Sports venues, meetups, and community events display the code where the live conversation happens - the moment people want to join in.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
@@ -310,7 +310,7 @@ export default function TwitterQRCodePage() {
|
||||
<a href="/dynamic-qr-code-generator" className="font-semibold text-slate-900 hover:underline">dynamic QR code</a>{' '}
|
||||
with{' '}
|
||||
<a href="/qr-code-tracking" className="font-semibold text-slate-900 hover:underline">QR code tracking</a>{' '}
|
||||
— the printed code stays valid even if the destination changes.
|
||||
- the printed code stays valid even if the destination changes.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function URLGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="url" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -10,24 +10,24 @@ import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free URL QR Code Generator | Link to Any Website | QR Master',
|
||||
absolute: 'URL QR Code Generator - Free, No Signup, Never Expires',
|
||||
},
|
||||
description: 'Create a QR code for your website. Erstelle kostenlos einen QR Code für deine Webseite. Static and free forever. Link zu jeder URL.',
|
||||
description: 'Paste any link, get a scannable QR code. Unlimited static codes that never expire, on every plan including free. Download print-ready PNG or SVG. No signup.',
|
||||
keywords: ['url qr code', 'website qr code', 'link qr generator', 'free qr code generator', 'url to qr', 'qr code erstellen', 'link qr code erstellen', 'website qr code generator', 'kostenlos qr code erstellen', 'url zu qr code', 'webseite verlinken qr'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/url-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free URL QR Code Generator | QR Master',
|
||||
description: 'Turn any URL into a QR code. Share websites instantly.',
|
||||
title: 'URL QR Code Generator - Free, No Signup, Never Expires',
|
||||
description: 'Paste any link, get a scannable code. Unlimited static codes that never expire.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/url-qr-code',
|
||||
images: [{ url: '/og-url-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Free URL QR Code Generator',
|
||||
description: 'Create QR codes for any link. Instant and free.',
|
||||
title: 'URL QR Code Generator - Free, No Signup, Never Expires',
|
||||
description: 'Paste any link, get a scannable code. Unlimited static codes that never expire.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -123,7 +123,7 @@ export default function URLQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
@@ -232,13 +232,13 @@ export default function URLQRCodePage() {
|
||||
URL QR Code Use Cases: When to Use a Link QR Code
|
||||
</h2>
|
||||
<div className="prose prose-slate max-w-none">
|
||||
<p className="text-lg text-slate-600 mb-6">A URL QR code is the simplest and most universal type of QR code: scan it, and a browser opens a specific web address. That simplicity is its strength. Any printed material that references a website becomes interactive the moment you add a URL QR code — no app required, no account needed, no special hardware beyond a standard smartphone camera.</p>
|
||||
<p className="text-lg text-slate-600 mb-6">A URL QR code is the simplest and most universal type of QR code: scan it, and a browser opens a specific web address. That simplicity is its strength. Any printed material that references a website becomes interactive the moment you add a URL QR code - no app required, no account needed, no special hardware beyond a standard smartphone camera.</p>
|
||||
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Marketing Materials: Flyers, Brochures & Posters</h3>
|
||||
<p className="text-slate-600 mb-4">Printed marketing materials have a fundamental limitation: they cannot be clicked. A URL QR code solves this by acting as a physical hyperlink. Flyers for an event can link directly to a registration page, eliminating the step of typing a long URL. Brochures can link to a detailed product page, a video demo, or a portfolio. Outdoor posters can point to a landing page with a time-sensitive offer. The critical design principle is placement and contrast: the QR code should appear on a clean background with at least 1 cm of quiet zone around it, and the call-to-action label — such as "Scan to book your spot" — should tell users exactly what they will find before they scan. A URL QR code effectively turns print advertising into a measurable digital funnel.</p>
|
||||
<p className="text-slate-600 mb-4">Printed marketing materials have a fundamental limitation: they cannot be clicked. A URL QR code solves this by acting as a physical hyperlink. Flyers for an event can link directly to a registration page, eliminating the step of typing a long URL. Brochures can link to a detailed product page, a video demo, or a portfolio. Outdoor posters can point to a landing page with a time-sensitive offer. The critical design principle is placement and contrast: the QR code should appear on a clean background with at least 1 cm of quiet zone around it, and the call-to-action label - such as "Scan to book your spot" - should tell users exactly what they will find before they scan. A URL QR code effectively turns print advertising into a measurable digital funnel.</p>
|
||||
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Product Packaging & Labels</h3>
|
||||
<p className="text-slate-600 mb-4">Product packaging is increasingly the first place customers turn for more information after purchase. A URL QR code on a label can link to setup instructions, video tutorials, an FAQ page, a warranty registration form, or a support portal — replacing bulky printed manuals and keeping the information always up to date. For consumables and repeat-purchase products, the QR code can link to a reorder page, transforming packaging from a cost centre into a sales channel. Food and beverage brands use URL QR codes to link to nutritional databases, sourcing information, and sustainability reports. The key advantage over a printed URL is that customers are far more likely to scan than to type a long web address — especially when they are already holding the product in their hands.</p>
|
||||
<p className="text-slate-600 mb-4">Product packaging is increasingly the first place customers turn for more information after purchase. A URL QR code on a label can link to setup instructions, video tutorials, an FAQ page, a warranty registration form, or a support portal - replacing bulky printed manuals and keeping the information always up to date. For consumables and repeat-purchase products, the QR code can link to a reorder page, transforming packaging from a cost centre into a sales channel. Food and beverage brands use URL QR codes to link to nutritional databases, sourcing information, and sustainability reports. The key advantage over a printed URL is that customers are far more likely to scan than to type a long web address - especially when they are already holding the product in their hands.</p>
|
||||
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Dynamic vs Static URL QR Codes</h3>
|
||||
<p className="text-slate-600 mb-4">A static URL QR code encodes the destination directly into the pattern — it is permanent, requires no server, and works forever, but it cannot be changed after printing. If your URL changes, the code breaks. A <a href="/dynamic-qr-code-generator" className="text-indigo-600 underline hover:text-indigo-800">dynamic QR code</a> works differently: it encodes a short redirect URL that you control, so you can update the destination at any time without reprinting the physical code. Dynamic codes are the right choice for anything printed at scale (packaging runs, banners, long-running campaigns) where reprinting after a URL change would be costly. They also unlock <a href="/qr-code-tracking" className="text-indigo-600 underline hover:text-indigo-800">scan analytics</a> — data on how many people scanned, from which device, country, and at what time — which static codes cannot provide. For one-off or low-stakes uses like a personal project or a single event flyer, a free static URL QR code is perfectly sufficient.</p>
|
||||
<p className="text-slate-600 mb-4">A static URL QR code encodes the destination directly into the pattern - it is permanent, requires no server, and works forever, but it cannot be changed after printing. If your URL changes, the code breaks. A <a href="/dynamic-qr-code-generator" className="text-indigo-600 underline hover:text-indigo-800">dynamic QR code</a> works differently: it encodes a short redirect URL that you control, so you can update the destination at any time without reprinting the physical code. Dynamic codes are the right choice for anything printed at scale (packaging runs, banners, long-running campaigns) where reprinting after a URL change would be costly. They also unlock <a href="/qr-code-tracking" className="text-indigo-600 underline hover:text-indigo-800">scan analytics</a> - data on how many people scanned, from which device, country, and at what time - which static codes cannot provide. For one-off or low-stakes uses like a personal project or a single event flyer, a free static URL QR code is perfectly sufficient.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -120,7 +120,7 @@ export default function VCardGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="vcard" />
|
||||
<div className="w-full max-w-6xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -11,9 +11,9 @@ import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free vCard QR Code Generator | QR Master',
|
||||
absolute: 'vCard QR Code Generator - Save to Contacts in One Tap',
|
||||
},
|
||||
description: 'Create a vCard QR code for your business card. Share contact details instantly — customers scan and save with one tap. Free, no signup required.',
|
||||
description: 'Put your contact details on a business card as a QR code. One scan saves you to their phone - no typing, no app. Download print-ready PNG or SVG. No signup.',
|
||||
keywords: ['vcard qr code', 'business card qr code', 'contact qr generator', 'digital business card', 'add to contacts qr', 'visitenkarte qr code', 'digitale visitenkarte erstellen', 'kontakt qr code', 'elektronische visitenkarte', 'vcard erstellen kostenlos'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/vcard-qr-code',
|
||||
@@ -23,16 +23,16 @@ export const metadata: Metadata = {
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free vCard QR Code Generator | QR Master',
|
||||
description: 'Turn your contact info into a QR code. The modern way to share your business card.',
|
||||
title: 'vCard QR Code Generator - Save to Contacts in One Tap',
|
||||
description: 'One scan saves you to their phone. No typing, no app. Print-ready PNG or SVG.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/vcard-qr-code',
|
||||
images: [{ url: '/og-vcard-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Free vCard QR Code Generator',
|
||||
description: 'Create QR codes for contact sharing. Instant and free.',
|
||||
title: 'vCard QR Code Generator - Save to Contacts in One Tap',
|
||||
description: 'One scan saves you to their phone. No typing, no app. Print-ready PNG or SVG.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -105,7 +105,7 @@ const jsonLd = {
|
||||
},
|
||||
'Can I test the QR code before printing?': {
|
||||
question: 'Can I test the QR code before printing?',
|
||||
answer: 'Yes — and you should. Scan the generated code with your own phone camera before sending it to print. If the contact card opens with the correct name, phone, and email, the printed version will behave identically.',
|
||||
answer: 'Yes - and you should. Scan the generated code with your own phone camera before sending it to print. If the contact card opens with the correct name, phone, and email, the printed version will behave identically.',
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -143,7 +143,7 @@ export default function VCardQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-rose-400"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
@@ -258,7 +258,7 @@ export default function VCardQRCodePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* WHY DIGITAL BUSINESS CARDS — STATISTICS */}
|
||||
{/* WHY DIGITAL BUSINESS CARDS - 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">
|
||||
@@ -276,7 +276,7 @@ export default function VCardQRCodePage() {
|
||||
<div className="bg-rose-50 border border-rose-100 rounded-2xl p-6">
|
||||
<div className="text-4xl font-extrabold text-rose-600 mb-2">88%</div>
|
||||
<p className="text-slate-700 text-sm leading-relaxed mb-3">
|
||||
of traditional paper business cards are thrown away within a week of being handed out. A vCard QR code on your card saves contact details instantly — no manual typing, no lost connections.
|
||||
of traditional paper business cards are thrown away within a week of being handed out. A vCard QR code on your card saves contact details instantly - no manual typing, no lost connections.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Source: <a href="https://www.adobe.com/express/learn/blog/business-card-statistics" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">Adobe Business Research</a>
|
||||
@@ -286,10 +286,10 @@ export default function VCardQRCodePage() {
|
||||
<div className="bg-orange-50 border border-orange-100 rounded-2xl p-6">
|
||||
<div className="text-4xl font-extrabold text-orange-600 mb-2">1-tap save</div>
|
||||
<p className="text-slate-700 text-sm leading-relaxed mb-3">
|
||||
Instead of asking someone to manually type your name, phone, and email — a vCard QR code transfers all contact fields (name, phone, email, company, URL) directly into their phone's address book with a single scan.
|
||||
Instead of asking someone to manually type your name, phone, and email - a vCard QR code transfers all contact fields (name, phone, email, company, URL) directly into their phone's address book with a single scan.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
vCard 3.0 / VCF format — supported natively by iOS and Android
|
||||
vCard 3.0 / VCF format - supported natively by iOS and Android
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -307,25 +307,25 @@ export default function VCardQRCodePage() {
|
||||
Where a vCard QR Code Pays Off
|
||||
</h2>
|
||||
<p className="text-slate-600 text-center mb-10 max-w-2xl mx-auto">
|
||||
A vCard QR code works anywhere someone should save your details in seconds — without typing anything.
|
||||
A vCard QR code works anywhere someone should save your details in seconds - without typing anything.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
<article className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="font-bold text-slate-900 mb-2">Business cards</h3>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">
|
||||
Print the QR code on the back of your paper card. Instead of your card ending up in a drawer, your contact lands in the address book — name, phone, email, and company in one tap.
|
||||
Print the QR code on the back of your paper card. Instead of your card ending up in a drawer, your contact lands in the address book - name, phone, email, and company in one tap.
|
||||
</p>
|
||||
</article>
|
||||
<article className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="font-bold text-slate-900 mb-2">Events & trade shows</h3>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">
|
||||
Put it on your badge, booth signage, or presentation slides. Attendees scan while you talk — no fumbling with cards, and no follow-up emails asking for your details.
|
||||
Put it on your badge, booth signage, or presentation slides. Attendees scan while you talk - no fumbling with cards, and no follow-up emails asking for your details.
|
||||
</p>
|
||||
</article>
|
||||
<article className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="font-bold text-slate-900 mb-2">Everyday networking</h3>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">
|
||||
Save it as your phone wallpaper or in your wallet. Any spontaneous meeting becomes a saved contact — perfect for freelancers, sales teams, and consultants.
|
||||
Save it as your phone wallpaper or in your wallet. Any spontaneous meeting becomes a saved contact - perfect for freelancers, sales teams, and consultants.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
@@ -393,11 +393,11 @@ export default function VCardQRCodePage() {
|
||||
/>
|
||||
<FaqItem
|
||||
question="Is the vCard QR code generator free?"
|
||||
answer="Yes. Creating a static vCard QR code is completely free — no signup, no watermark, no expiry. Generate, download, and print as many as you need."
|
||||
answer="Yes. Creating a static vCard QR code is completely free - no signup, no watermark, no expiry. Generate, download, and print as many as you need."
|
||||
/>
|
||||
<FaqItem
|
||||
question="Can I test the QR code before printing?"
|
||||
answer="Yes — scan the generated code with your own phone camera first. If the contact card opens with the correct details, the printed version will behave identically."
|
||||
answer="Yes - scan the generated code with your own phone camera first. If the contact card opens with the correct details, the printed version will behave identically."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@ export default function WhatsappGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="social" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function WhatsappQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-300 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-300"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
@@ -280,13 +280,13 @@ export default function WhatsappQRCodePage() {
|
||||
How to Use WhatsApp QR Codes for Business
|
||||
</h2>
|
||||
<div className="prose prose-slate max-w-none">
|
||||
<p className="text-lg text-slate-600 mb-6">WhatsApp QR codes remove the biggest obstacle between a potential customer and a conversation: saving a phone number. Instead of typing digits manually, customers scan once and land directly in a chat — with your pre-filled message already loaded and ready to send. For businesses that rely on fast, personal communication, that friction reduction is significant.</p>
|
||||
<p className="text-lg text-slate-600 mb-6">WhatsApp QR codes remove the biggest obstacle between a potential customer and a conversation: saving a phone number. Instead of typing digits manually, customers scan once and land directly in a chat - with your pre-filled message already loaded and ready to send. For businesses that rely on fast, personal communication, that friction reduction is significant.</p>
|
||||
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Customer Support & Service Teams</h3>
|
||||
<p className="text-slate-600 mb-4">Support teams can use WhatsApp QR codes on help pages, packaging inserts, and warranty cards to give customers a direct line without publishing a number publicly in plain text. The pre-filled message field is especially powerful here: you can pre-load context such as "Hi, I need help with my order #" so agents receive structured requests from the start. This reduces back-and-forth and speeds up resolution time. Place the QR code at the end of a printed receipt or inside a product box to catch customers at the exact moment they might need help.</p>
|
||||
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Restaurants, Cafes & Retail Stores</h3>
|
||||
<p className="text-slate-600 mb-4">Physical businesses benefit the most from WhatsApp QR codes placed at the point of purchase. A cafe can display a QR code at the counter that pre-fills "I'd like to place a takeaway order" — customers scan, confirm their order via WhatsApp, and staff prepare it without phone calls interrupting busy periods. Restaurants can use QR codes on table cards for reservations or feedback. Retail stores can place them near fitting rooms so shoppers can ask about sizes or alternative products without waiting for staff. The key is pairing the QR code with a clear call-to-action label like "Chat with us on WhatsApp" and placing it at eye level.</p>
|
||||
<p className="text-slate-600 mb-4">Physical businesses benefit the most from WhatsApp QR codes placed at the point of purchase. A cafe can display a QR code at the counter that pre-fills "I'd like to place a takeaway order" - customers scan, confirm their order via WhatsApp, and staff prepare it without phone calls interrupting busy periods. Restaurants can use QR codes on table cards for reservations or feedback. Retail stores can place them near fitting rooms so shoppers can ask about sizes or alternative products without waiting for staff. The key is pairing the QR code with a clear call-to-action label like "Chat with us on WhatsApp" and placing it at eye level.</p>
|
||||
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Marketing Campaigns & Lead Generation</h3>
|
||||
<p className="text-slate-600 mb-4">WhatsApp QR codes on printed flyers, outdoor posters, or event banners create a measurable bridge from offline marketing to a live conversation. Unlike a website URL, a WhatsApp link initiates a direct dialogue — which converts at a much higher rate than a contact form. For lead generation campaigns, pre-fill the message with the campaign name or offer so you can track which placement is driving inbound chats. To measure QR code performance across multiple placements, combine your WhatsApp QR code strategy with <a href="/qr-code-tracking" className="text-[#128C7E] underline hover:text-[#075E54]">QR code scan analytics</a> to see which posters, flyers, or locations generate the most engagement.</p>
|
||||
<p className="text-slate-600 mb-4">WhatsApp QR codes on printed flyers, outdoor posters, or event banners create a measurable bridge from offline marketing to a live conversation. Unlike a website URL, a WhatsApp link initiates a direct dialogue - which converts at a much higher rate than a contact form. For lead generation campaigns, pre-fill the message with the campaign name or offer so you can track which placement is driving inbound chats. To measure QR code performance across multiple placements, combine your WhatsApp QR code strategy with <a href="/qr-code-tracking" className="text-[#128C7E] underline hover:text-[#075E54]">QR code scan analytics</a> to see which posters, flyers, or locations generate the most engagement.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function WiFiGenerator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} variant="wifi" />
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
|
||||
@@ -158,7 +158,7 @@ export default function WiFiQRCodePage() {
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400"></span>
|
||||
</span>
|
||||
Free Tool — No Signup Required
|
||||
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">
|
||||
@@ -289,7 +289,7 @@ export default function WiFiQRCodePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* WHY WIFI QR CODES MATTER — STATISTICS */}
|
||||
{/* WHY WIFI QR CODES 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">
|
||||
@@ -300,14 +300,14 @@ export default function WiFiQRCodePage() {
|
||||
Why WiFi QR Codes Improve Customer Experience
|
||||
</h2>
|
||||
<p className="text-slate-600 mb-10 max-w-2xl">
|
||||
A <strong>WiFi QR code</strong> eliminates the single biggest friction point between your guest and your network — manual password entry.
|
||||
A <strong>WiFi QR code</strong> eliminates the single biggest friction point between your guest and your network - manual password entry.
|
||||
</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6 mb-8">
|
||||
<div className="bg-emerald-50 border border-emerald-100 rounded-2xl p-6">
|
||||
<div className="text-3xl font-extrabold text-emerald-600 mb-2">#1 Amenity</div>
|
||||
<p className="text-slate-700 text-sm leading-relaxed mb-3">
|
||||
Free WiFi is rated the most important hotel amenity by guests — ahead of breakfast, parking, and loyalty points. Instant, frictionless access directly impacts satisfaction scores and repeat bookings.
|
||||
Free WiFi is rated the most important hotel amenity by guests - ahead of breakfast, parking, and loyalty points. Instant, frictionless access directly impacts satisfaction scores and repeat bookings.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Source: <a href="https://www.jdpower.com/business/travel-hospitality/hotel-guest-satisfaction-study" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">J.D. Power Hotel Guest Satisfaction Study</a>
|
||||
@@ -317,10 +317,10 @@ export default function WiFiQRCodePage() {
|
||||
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-6">
|
||||
<div className="text-3xl font-extrabold text-blue-600 mb-2">Effort = Loyalty</div>
|
||||
<p className="text-slate-700 text-sm leading-relaxed mb-3">
|
||||
Reducing customer effort — like eliminating manual password entry — is the single strongest predictor of customer loyalty. The lower the effort, the higher the repeat visit rate and positive word-of-mouth.
|
||||
Reducing customer effort - like eliminating manual password entry - is the single strongest predictor of customer loyalty. The lower the effort, the higher the repeat visit rate and positive word-of-mouth.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Source: <a href="https://hbr.org/2010/07/stop-trying-to-delight-your-customers" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">Harvard Business Review — "Stop Trying to Delight Your Customers"</a> (Customer Effort Score research)
|
||||
Source: <a href="https://hbr.org/2010/07/stop-trying-to-delight-your-customers" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">Harvard Business Review - "Stop Trying to Delight Your Customers"</a> (Customer Effort Score research)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -347,7 +347,7 @@ export default function WiFiQRCodePage() {
|
||||
<div className="space-y-4">
|
||||
<FaqItem
|
||||
question="Is it safe to enter my WiFi password here?"
|
||||
answer="Yes, completely safe. This tool uses client-side processing, meaning your WiFi password never leaves your device. It's processed locally in your browser to generate the QR code—no data is sent to any server."
|
||||
answer="Yes, completely safe. This tool uses client-side processing, meaning your WiFi password never leaves your device. It's processed locally in your browser to generate the QR code-no data is sent to any server."
|
||||
/>
|
||||
<FaqItem
|
||||
question="Do WiFi QR codes work on iPhone and Android?"
|
||||
|
||||