Compare commits
38 Commits
82101ca08f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0866c200a0 | ||
|
|
a7cbbee084 | ||
|
|
09f5859af2 | ||
|
|
4774f4d51e | ||
|
|
81d1fdd280 | ||
|
|
35e7e77f6b | ||
|
|
8741edc362 | ||
|
|
152758db92 | ||
|
|
105857c348 | ||
|
|
aab808c553 | ||
|
|
9b31e77daa | ||
|
|
c4fac0f726 | ||
|
|
11159eb02b | ||
|
|
c6f20f7f0b | ||
|
|
eacaef1fbd | ||
| fc0e6a0a69 | |||
|
|
c7d5f281c5 | ||
|
|
6e68408391 | ||
|
|
7d2724b65d | ||
| ce724662d4 | |||
| 7a7b197a67 | |||
| ef22e72a82 | |||
|
|
32935041b3 | ||
|
|
aa2628834b | ||
|
|
5894f4619d | ||
|
|
56d63a0146 | ||
|
|
1bb782467b | ||
|
|
c3efe8ceb9 | ||
| c1fa20a234 | |||
| 3cf67582bc | |||
|
|
231a85ffa4 | ||
|
|
673eaf7fd3 | ||
|
|
30b1b12e74 | ||
|
|
139b87fe93 | ||
|
|
8257866138 | ||
|
|
8de1411e34 | ||
|
|
65fe18a718 | ||
|
|
6b73ac5c50 |
234
.agents/pricing-strategy.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# QR Master — Pricing Strategy
|
||||||
|
*Erstellt: April 2026 | Basiert auf Marktforschung, Competitor-Scraping & SaaS-Benchmarks*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Marktkontext
|
||||||
|
|
||||||
|
### QR-Code-Markt 2025/2026
|
||||||
|
| Metrik | Wert |
|
||||||
|
|--------|------|
|
||||||
|
| Globale Marktgröße | $15,3 Mrd. (2025) |
|
||||||
|
| CAGR bis 2030 | 16,1% |
|
||||||
|
| US-Smartphone-User die QR scannen | 100+ Mio. monatlich |
|
||||||
|
| Business-Adoption | 50% der Unternehmen nutzen QR-Codes aktiv |
|
||||||
|
|
||||||
|
**Fazit:** Wachstumsmarkt mit noch großem Potenzial, besonders im KMU-Segment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Wettbewerbs-Pricing-Map
|
||||||
|
|
||||||
|
### Vollständige Marktübersicht (aus Firecrawl-Recherche)
|
||||||
|
|
||||||
|
| Tool | Preis/Monat | Analytics | Dynamic | Bulk | Branding | Positionierung |
|
||||||
|
|------|------------|-----------|---------|------|----------|----------------|
|
||||||
|
| TQRCG | $5 | ✅ | ✅ | ❌ | ✅ | Value-Leader |
|
||||||
|
| QRStuff | $5 | ❌ | ❌ | ✅ | ❌ | Budget |
|
||||||
|
| ViralQR | $1,49 | ✅ | ✅ | ❌ | ✅ | Ultra-Budget |
|
||||||
|
| Beaconstac | $5–24 | ✅ | ✅ | ✅ | ✅ | SMB–Enterprise |
|
||||||
|
| Bitly QR | $10 | ✅ | ✅ | ❌ | ✅ | Mid-Market |
|
||||||
|
| Unitag | $10 | ❌ | ✅ | ✅ | ✅ | Mid-Market |
|
||||||
|
| ZebraQR | $9 | ✅ | ✅ | ❌ | ✅ | Hospitality-Nische |
|
||||||
|
| QR Tiger | $12–15 | ✅ | ✅ | ✅ | ✅ | Mid-Market+ |
|
||||||
|
| Hovercode | $15 | ✅ | ✅ | ✅ | ✅ | Growth-Fokus |
|
||||||
|
| Flowcode | $10–15 | ✅ | ✅ | ❌ | ✅ | Design-Fokus |
|
||||||
|
| Scanova | $20 | ✅ | ✅ | ✅ | ✅ | Premium |
|
||||||
|
| QR Code Chimp | $20 | ✅ | ✅ | ✅ | ✅ | Premium-Design |
|
||||||
|
| Uniqode | $10–30 | ✅ | ✅ | ✅ | ✅ | Enterprise |
|
||||||
|
| QRFY | $25 | ✅ | ✅ | ✅ | ✅ | Premium-Flat |
|
||||||
|
| QR Code Generator Pro | $15–29 | ✅ | ✅ | ✅ | ✅ | Agency |
|
||||||
|
|
||||||
|
### Marktlücke für QR Master
|
||||||
|
> **Kein einziges Tool unter $12 bietet Analytics + Bulk + Custom Branding + DSGVO gleichzeitig.**
|
||||||
|
> Das ist exakt QR Masters Sweet Spot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SaaS-Benchmark-Daten (Industrie)
|
||||||
|
|
||||||
|
| Metrik | Benchmark | Quelle |
|
||||||
|
|--------|-----------|--------|
|
||||||
|
| Median Entry-Level Preis (SaaS) | $29/mo | Monetizely 2025 |
|
||||||
|
| Free-to-Paid Conversion | ~5% | RevenueCat 2026 |
|
||||||
|
| Anteil Jahres-Abos (vs. Monatlich) | 68% annual / 32% monthly | RevenueCat 2026 |
|
||||||
|
| ARPU (Subscription Apps) | ~$30 | RevenueCat 2026 |
|
||||||
|
| Freemium-Anteil unter SaaS | 38% der Unternehmen | Monetizely 2025 |
|
||||||
|
| Hybrid-Pricing-Adoption | 61% | Monetizely 2025 |
|
||||||
|
| SaaS Churn (SMB) | 3–5%/Monat | Benchmark |
|
||||||
|
|
||||||
|
**Key Insight:** 68% der Subscriber wählen Jahrestarife. Das ist der wichtigste Hebel für Cashflow und Churn-Reduktion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Value Metric Empfehlung
|
||||||
|
|
||||||
|
### Aktuelles Modell: Anzahl dynamischer QR-Codes
|
||||||
|
**Bewertung: Gut, aber optimierbar.**
|
||||||
|
|
||||||
|
Die Anzahl dynamischer Codes skaliert mit dem wahrgenommenen Wert (mehr Codes = mehr Kampagnen = mehr Wert). Jedoch:
|
||||||
|
- Limit von 8 FREE / 50 PRO / 500 BUSINESS ist nicht intuitiv kommuniziert
|
||||||
|
- Kunden denken in "Projekten" oder "Kampagnen", nicht in "Codes"
|
||||||
|
|
||||||
|
### Empfehlung: Hybrid-Metric einführen
|
||||||
|
Primär-Metric behalten (Dynamic Codes), aber mit Sekundär-Metriken ergänzen:
|
||||||
|
|
||||||
|
| Tier | Primär-Metric | Sekundär-Metriken |
|
||||||
|
|------|--------------|-------------------|
|
||||||
|
| FREE | 8 Dynamic Codes | 1 User, Basic Analytics, 30 Tage History |
|
||||||
|
| PRO | 50 Dynamic Codes | 1–3 User, Full Analytics, 1 Jahr History, Custom Domain |
|
||||||
|
| BUSINESS | 500 Dynamic Codes | Unlimitierte User, Advanced Analytics, Bulk, API |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Empfohlene Pricing-Struktur
|
||||||
|
|
||||||
|
### Tier-Empfehlung (Monatlich / Jährlich)
|
||||||
|
|
||||||
|
#### FREE — Kostenlos, für immer
|
||||||
|
- **8 dynamische QR-Codes** (klar kommuniziert als "8 Kampagnen")
|
||||||
|
- Unlimitierte statische Codes
|
||||||
|
- Basis-Analytics (Scans, Datum)
|
||||||
|
- QR Master Branding (nicht entfernbar)
|
||||||
|
- **Ziel:** Acquisition, Habit-Building, Virality durch Branding
|
||||||
|
|
||||||
|
#### PRO — €9/Monat (monatlich) | **€7/Monat (jährlich = €84/Jahr)**
|
||||||
|
*Empfohlen für: Restaurants, lokale Unternehmen, Marketing-Einsteiger*
|
||||||
|
- **50 dynamische QR-Codes**
|
||||||
|
- Custom Branding (kein QR Master Logo)
|
||||||
|
- Vollständige Analytics (Device, Location, OS, UTM)
|
||||||
|
- 1 Jahr Analytics-History
|
||||||
|
- Custom Domain für Redirects
|
||||||
|
- Prioritäts-Support
|
||||||
|
- **Rationale:** $7–9 liegt im bewiesenen Sweet Spot ($5–$10) für diese Zielgruppe. Beaconstac Starter bei $5 hat nur 100 Scans — wir haben keine Scan-Limits.
|
||||||
|
|
||||||
|
#### BUSINESS — €24/Monat (monatlich) | **€19/Monat (jährlich = €228/Jahr)**
|
||||||
|
*Empfohlen für: Agenturen, Retail-Chains, Event-Organisatoren*
|
||||||
|
- **500 dynamische QR-Codes**
|
||||||
|
- Bulk-Upload (Excel/CSV bis 1.000 Zeilen)
|
||||||
|
- API-Zugriff
|
||||||
|
- Team-Management (bis 5 User)
|
||||||
|
- Erweiterte Analytics + Export (CSV, PDF)
|
||||||
|
- White-Label Option
|
||||||
|
- DSGVO-Compliance-Report
|
||||||
|
- **Rationale:** $19–24 ist der Bereich wo Scanova ($20), Hovercode ($15) und QR Code Chimp ($20) spielen — aber keiner hat DSGVO + Bulk + Analytics zusammen.
|
||||||
|
|
||||||
|
#### ENTERPRISE — Auf Anfrage (ab €99/Monat)
|
||||||
|
*Für: Corporations, Franchise-Ketten*
|
||||||
|
- Unlimitierte Codes
|
||||||
|
- Dedizierter Account Manager
|
||||||
|
- Custom SLA
|
||||||
|
- SSO / SAML
|
||||||
|
- On-Premise Option (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Psychologische Preisgestaltung
|
||||||
|
|
||||||
|
### Anchoring-Strategie
|
||||||
|
Reihenfolge auf Pricing-Page: **BUSINESS → PRO → FREE** (von teuer nach günstig)
|
||||||
|
→ PRO wirkt dadurch als "vernünftiger Kompromiss"
|
||||||
|
|
||||||
|
### Decoy-Effekt
|
||||||
|
PRO muss der offensichtliche "Best Deal" sein:
|
||||||
|
- BUSINESS ist 2,7× teurer als PRO aber hat 10× mehr Codes → Nur für Power-User
|
||||||
|
- FREE hat 6× weniger Codes als PRO → Upgrade liegt nahe
|
||||||
|
|
||||||
|
### Jahres-Pricing-Push
|
||||||
|
- Monatlich: €9 / €24
|
||||||
|
- Jährlich: €7 / €19 (sparst 22% / 21%)
|
||||||
|
- **Wichtig:** Jahrespreis prominent anzeigen mit "Spare 2 Monate" statt Prozent
|
||||||
|
- Default-Toggle: **Jährlich** (da 68% aller Subscriber Jahrestarife wählen)
|
||||||
|
|
||||||
|
### Charm vs. Round Pricing
|
||||||
|
- PRO: **€9** (nicht €10) → Charm Pricing für Conversion
|
||||||
|
- BUSINESS: **€24** (nicht €25) → Knapp unter psychologischer Grenze
|
||||||
|
- Jahrestarife: **€84/Jahr** und **€228/Jahr** (rund → Premium-Signal)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Jahres-Discount-Strategie
|
||||||
|
|
||||||
|
| Tier | Monatlich | Jährlich | Ersparnis |
|
||||||
|
|------|-----------|----------|-----------|
|
||||||
|
| PRO | €9/Mo | €84/Jahr (€7/Mo) | 22% / 2 Monate gratis |
|
||||||
|
| BUSINESS | €24/Mo | €228/Jahr (€19/Mo) | 21% / 2,5 Monate gratis |
|
||||||
|
|
||||||
|
**Kommunikation:** "2 Monate kostenlos bei jährlicher Zahlung" schlägt "20% Rabatt" in A/B-Tests regelmäßig.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Free-Tier-Optimierung
|
||||||
|
|
||||||
|
### Ziel des Free-Tiers
|
||||||
|
Nicht monetarisieren — **qualifizieren und konvertieren**.
|
||||||
|
|
||||||
|
### Empfohlene Trigger für Upgrade-Prompts
|
||||||
|
1. **Code-Limit erreicht** → "Du hast 8/8 Codes verwendet. Upgrade auf PRO für 50 Codes."
|
||||||
|
2. **Analytics-Feature geklickt** → "Detaillierte Location-Analytics nur in PRO."
|
||||||
|
3. **Custom Branding versucht** → "Entferne das QR Master Logo — upgrade auf PRO."
|
||||||
|
4. **Bulk-Upload versucht** → "Bulk-Upload ist nur in BUSINESS verfügbar."
|
||||||
|
5. **Nach 7 Tagen aktive Nutzung** → In-App Prompt: "Du nutzt QR Master aktiv — hole mehr raus."
|
||||||
|
|
||||||
|
### Virality-Mechanismus
|
||||||
|
- FREE-Codes enthalten subtiles "Made with QR Master" in Metadaten
|
||||||
|
- QR-Code-Landing-Pages (bei Dynamic Redirects) zeigen "Powered by QR Master" Footer
|
||||||
|
- Jeder Scan ist eine potenzielle Akquisition
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Positioning Statement je Tier
|
||||||
|
|
||||||
|
**FREE:**
|
||||||
|
> "Starte kostenlos mit 8 professionellen QR-Codes — keine Kreditkarte erforderlich."
|
||||||
|
|
||||||
|
**PRO:**
|
||||||
|
> "Für Restaurants, lokale Geschäfte und Marketer: Unbegrenzte Änderungen, echte Analytics, dein Branding — für weniger als ein Mittagessen pro Monat."
|
||||||
|
|
||||||
|
**BUSINESS:**
|
||||||
|
> "Für Agenturen und Retail-Chains: Erstelle 500 Codes auf einmal, per Excel-Upload — DSGVO-konform, skalierbar, professionell."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Pricing Page Struktur (Empfehlung)
|
||||||
|
|
||||||
|
### Elemente above the fold
|
||||||
|
1. **Toggle: Monatlich / Jährlich** (Default: Jährlich)
|
||||||
|
2. **3 Tier-Karten** in Reihenfolge: FREE → PRO (highlighted "Beliebteste Wahl") → BUSINESS
|
||||||
|
3. **CTA je Tier:** "Kostenlos starten" / "14 Tage gratis testen" / "Jetzt upgraden"
|
||||||
|
4. **Trust-Signal:** "Keine Kreditkarte für Free • DSGVO-konform • Jederzeit kündbar"
|
||||||
|
|
||||||
|
### Weitere Sektionen
|
||||||
|
- Feature-Vergleichstabelle (vollständig)
|
||||||
|
- ROI-Rechner: "Wie viel sparst du durch dynamische QR-Codes vs. Neudruck?"
|
||||||
|
- FAQ (Objections aus Product-Marketing-Context)
|
||||||
|
- Testimonials-Sektion (Platzhalter für spätere echte Reviews)
|
||||||
|
- Enterprise-CTA am Ende
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Kurzfristige Maßnahmen (Quick Wins)
|
||||||
|
|
||||||
|
| Priorität | Maßnahme | Impact |
|
||||||
|
|-----------|----------|--------|
|
||||||
|
| 🔴 Hoch | Jahrestarif als Default auf Pricing-Page setzen | +20–30% ARPU sofort |
|
||||||
|
| 🔴 Hoch | "2 Monate gratis" Kommunikation (statt %) | +Conversion |
|
||||||
|
| 🟡 Mittel | Upgrade-Prompts bei Feature-Gates einbauen | +Free-to-Paid |
|
||||||
|
| 🟡 Mittel | 14-Tage PRO Trial (kreditkartenlos) | +Trial Signups |
|
||||||
|
| 🟢 Niedrig | BUSINESS Jahrespreis auf €228 festlegen | Cashflow |
|
||||||
|
| 🟢 Niedrig | Enterprise-Kontaktformular ergänzen | Upmarket |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Risiken & Gegenmaßnahmen
|
||||||
|
|
||||||
|
| Risiko | Wahrscheinlichkeit | Gegenmaßnahme |
|
||||||
|
|--------|-------------------|---------------|
|
||||||
|
| ViralQR mit $1,49 unterbietbar | Mittel | Auf Analytics + DSGVO differenzieren, nicht Preis |
|
||||||
|
| FREE-User konvertieren nicht | Hoch | Smarte Feature-Gates + E-Mail-Nurturing |
|
||||||
|
| BUSINESS-Preis zu hoch für KMU | Mittel | Jährlich-Preis betonen: €19/mo fühlt sich zugänglich an |
|
||||||
|
| Konkurrenten senken Preise | Niedrig | Value-Story stärken, nicht mitziehen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Datenbasis: Firecrawl-Scraping von 5+ Competitor-Seiten, QR Marktstatistiken 2026, RevenueCat State of Subscription Apps 2026, Monetizely SaaS Benchmark 2025, Product Marketing Context QR Master.*
|
||||||
19
.claude/hooks/check-gstack.sh
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Block skill usage when gstack is not installed globally.
|
||||||
|
if [ ! -d "$HOME/.claude/skills/gstack/bin" ]; then
|
||||||
|
cat >&2 <<'MSG'
|
||||||
|
BLOCKED: gstack is not installed globally.
|
||||||
|
gstack is required for AI-assisted work in this repo.
|
||||||
|
|
||||||
|
Install it:
|
||||||
|
git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
|
||||||
|
cd ~/.claude/skills/gstack && ./setup --team
|
||||||
|
|
||||||
|
Then restart your AI coding tool.
|
||||||
|
MSG
|
||||||
|
echo '{"permissionDecision":"deny","message":"gstack is required but not installed. See stderr for install instructions."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo '{}'
|
||||||
15
.claude/settings.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Skill",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/check-gstack.sh\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
60
.codex-temp/awesome-design-md-SKILL.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
name: awesome-design-md
|
||||||
|
description: Use VoltAgent's awesome-design-md collection when the user wants UI inspired by a specific brand or asks for a DESIGN.md reference, visual system, or brand-style implementation such as Stripe, Linear, Vercel, Claude, or Supabase. Resolve the brand slug from the installed `design-md/` folder, fetch the matching `getdesign.md` design document for that slug, and apply it as the design-system reference for implementation.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Awesome DESIGN.md
|
||||||
|
|
||||||
|
Use this skill to turn the installed `awesome-design-md` collection into a practical design reference workflow.
|
||||||
|
|
||||||
|
The local `design-md/` directory is the index of supported brand slugs. Its per-brand `README.md` files are only pointers. The actual design-system document lives at:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://getdesign.md/<slug>/design-md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Identify the target brand or closest visual reference.
|
||||||
|
2. Resolve the brand slug from the local `design-md/` folder.
|
||||||
|
3. Prefer exact folder names for dotted brands such as `linear.app`, `mistral.ai`, `opencode.ai`, `together.ai`, and `x.ai`.
|
||||||
|
4. Fetch `https://getdesign.md/<slug>/design-md`.
|
||||||
|
5. Use the fetched document in one of two ways:
|
||||||
|
- write or update the project's root `DESIGN.md`
|
||||||
|
- keep it as an external design reference while implementing UI
|
||||||
|
6. Preserve the user's product semantics and content model. Borrow visual language, spacing, typography, motion, and component patterns, not product-specific copy.
|
||||||
|
|
||||||
|
## Local Source Of Truth
|
||||||
|
|
||||||
|
Use the installed folder below to confirm which slugs exist before fetching:
|
||||||
|
|
||||||
|
```text
|
||||||
|
C:\Users\a931627\.claude\skills\awesome-design-md\design-md
|
||||||
|
```
|
||||||
|
|
||||||
|
If needed, list the available slugs with:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-ChildItem -Name C:\Users\a931627\.claude\skills\awesome-design-md\design-md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Practical Rules
|
||||||
|
|
||||||
|
- Treat `DESIGN.md` as a visual system reference, not as code to mirror verbatim.
|
||||||
|
- If the user asks for "something like X, but lighter, warmer, or more minimal", adapt the reference instead of cloning it literally.
|
||||||
|
- If multiple brands fit, choose the closest one and state the choice.
|
||||||
|
- If a slug is missing locally or the remote fetch fails, pick the nearest available brand or ask the user for a replacement target.
|
||||||
|
- When working inside an existing design system, merge the borrowed visual cues with the established component structure instead of replacing everything.
|
||||||
|
|
||||||
|
## Common Slug Examples
|
||||||
|
|
||||||
|
- `stripe`
|
||||||
|
- `vercel`
|
||||||
|
- `claude`
|
||||||
|
- `cursor`
|
||||||
|
- `supabase`
|
||||||
|
- `linear.app`
|
||||||
|
- `mistral.ai`
|
||||||
|
- `opencode.ai`
|
||||||
|
- `together.ai`
|
||||||
|
- `x.ai`
|
||||||
39
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
## Summary
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
## Change Type
|
||||||
|
|
||||||
|
- [ ] QRMaster SEO page
|
||||||
|
- [ ] QRMaster landing/tool page
|
||||||
|
- [ ] QRMaster conversion/pricing change
|
||||||
|
- [ ] GreenLens content/ASO workflow
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] Other
|
||||||
|
|
||||||
|
## SEO / Content Review
|
||||||
|
|
||||||
|
- [ ] Primary search intent is clear.
|
||||||
|
- [ ] Metadata is present and specific.
|
||||||
|
- [ ] Exactly one H1 is rendered for each new or changed page.
|
||||||
|
- [ ] Internal links are added to relevant money pages.
|
||||||
|
- [ ] CTA is specific to the page/use case.
|
||||||
|
- [ ] Duplicate or thin content risk was checked.
|
||||||
|
- [ ] Schema/structured data was added or intentionally skipped.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- [ ] Build passes.
|
||||||
|
- [ ] Lint passes.
|
||||||
|
- [ ] Links/CTAs checked.
|
||||||
|
- [ ] Screenshots or notes included for UI changes.
|
||||||
|
|
||||||
|
## Codex Review Prompt
|
||||||
|
|
||||||
|
For QRMaster SEO/page changes, run:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Use docs/automations/qrmaster-pr-seo-review.md and review this PR for SEO,
|
||||||
|
conversion, internal linking, duplicate content, schema, and build/lint risk.
|
||||||
|
```
|
||||||
|
|
||||||
2
.github/workflows/ci.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on: [push]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
9
.gitignore
vendored
@@ -48,5 +48,14 @@ docker-compose.override.yml
|
|||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# project-specific
|
||||||
|
Leads/
|
||||||
|
marketing/
|
||||||
|
output/
|
||||||
|
remotion/
|
||||||
|
|
||||||
# local dev script
|
# local dev script
|
||||||
dev-server.js
|
dev-server.js
|
||||||
|
.gstack/
|
||||||
|
|
||||||
|
.env.meta
|
||||||
|
|||||||
1
.impeccable-live.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"pid":23720,"port":8400,"token":"99ca8ad6-3aa6-44f6-9b64-25921f55724b"}
|
||||||
11
AGENTS.md
@@ -305,3 +305,14 @@ This allows it to be used by:
|
|||||||
- Codex CLI
|
- Codex CLI
|
||||||
- Gemini Code Assist
|
- Gemini Code Assist
|
||||||
- Cursor Agents
|
- Cursor Agents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# gstack
|
||||||
|
|
||||||
|
This repo also uses the global `gstack` skill pack for AI-assisted work.
|
||||||
|
|
||||||
|
- Claude Code: install to `~/.claude/skills/gstack` and run `./setup --team`
|
||||||
|
- Codex: gstack skills are installed globally under `~/.codex/skills/gstack-*`
|
||||||
|
- Prefer gstack skills for structured work when they fit the task, especially `/browse`, `/review`, `/investigate`, `/qa`, `/ship`, and `/cso`
|
||||||
|
- Do not reference vendored repo paths for gstack; use the global install path instead
|
||||||
|
|||||||
38
CLAUDE.md
@@ -279,3 +279,41 @@ docker-compose exec web npx prisma migrate deploy # Run migrations in container
|
|||||||
- DOCKER_SETUP.md - Complete Docker deployment guide
|
- DOCKER_SETUP.md - Complete Docker deployment guide
|
||||||
- prisma/schema.prisma - Database schema and relationships
|
- prisma/schema.prisma - Database schema and relationships
|
||||||
- env.example - Environment variable template
|
- env.example - Environment variable template
|
||||||
|
|
||||||
|
## gstack (REQUIRED — global install)
|
||||||
|
|
||||||
|
Before doing any AI-assisted work in this repo, verify gstack is installed globally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test -d ~/.claude/skills/gstack/bin && echo "GSTACK_OK" || echo "GSTACK_MISSING"
|
||||||
|
```
|
||||||
|
|
||||||
|
If `GSTACK_MISSING`: stop and install it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
|
||||||
|
cd ~/.claude/skills/gstack && ./setup --team
|
||||||
|
```
|
||||||
|
|
||||||
|
After install, use gstack skills where appropriate, especially `/browse`, `/review`, `/investigate`, `/qa`, `/ship`, and `/cso`.
|
||||||
|
Use `~/.claude/skills/gstack/...` for gstack file paths because this repo does not vendor the pack locally.
|
||||||
|
|
||||||
|
## Skill routing
|
||||||
|
|
||||||
|
When the user's request matches an available skill, ALWAYS invoke it using the Skill
|
||||||
|
tool as your FIRST action. Do NOT answer directly, do NOT use other tools first.
|
||||||
|
The skill has specialized workflows that produce better results than ad-hoc answers.
|
||||||
|
|
||||||
|
Key routing rules:
|
||||||
|
- Product ideas, "is this worth building", brainstorming → invoke office-hours
|
||||||
|
- Bugs, errors, "why is this broken", 500 errors → invoke investigate
|
||||||
|
- Ship, deploy, push, create PR → invoke ship
|
||||||
|
- QA, test the site, find bugs → invoke qa
|
||||||
|
- Code review, check my diff → invoke review
|
||||||
|
- Update docs after shipping → invoke document-release
|
||||||
|
- Weekly retro → invoke retro
|
||||||
|
- Design system, brand → invoke design-consultation
|
||||||
|
- Visual audit, design polish → invoke design-review
|
||||||
|
- Architecture review → invoke plan-eng-review
|
||||||
|
- Save progress, checkpoint, resume → invoke checkpoint
|
||||||
|
- Code quality, health check → invoke health
|
||||||
|
|||||||
322
DESIGN.md
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
# Design System Inspired by Stripe
|
||||||
|
|
||||||
|
## 1. Visual Theme & Atmosphere
|
||||||
|
|
||||||
|
Stripe's website is the gold standard of fintech design -- a system that manages to feel simultaneously technical and luxurious, precise and warm. The page opens on a clean white canvas (`#ffffff`) with deep navy headings (`#061b31`) and a signature purple (`#533afd`) that functions as both brand anchor and interactive accent. This isn't the cold, clinical purple of enterprise software; it's a rich, saturated violet that reads as confident and premium. The overall impression is of a financial institution redesigned by a world-class type foundry.
|
||||||
|
|
||||||
|
The custom `sohne-var` variable font is the defining element of Stripe's visual identity. Every text element enables the OpenType `"ss01"` stylistic set, which modifies character shapes for a distinctly geometric, modern feel. At display sizes (48px-56px), sohne-var runs at weight 300 -- an extraordinarily light weight for headlines that creates an ethereal, almost whispered authority. This is the opposite of the "bold hero headline" convention; Stripe's headlines feel like they don't need to shout. The negative letter-spacing (-1.4px at 56px, -0.96px at 48px) tightens the text into dense, engineered blocks. At smaller sizes, the system also uses weight 300 with proportionally reduced tracking, and tabular numerals via `"tnum"` for financial data display.
|
||||||
|
|
||||||
|
What truly distinguishes Stripe is its shadow system. Rather than the flat or single-layer approach of most sites, Stripe uses multi-layer, blue-tinted shadows: the signature `rgba(50,50,93,0.25)` combined with `rgba(0,0,0,0.1)` creates shadows with a cool, almost atmospheric depth -- like elements are floating in a twilight sky. The blue-gray undertone of the primary shadow color (50,50,93) ties directly to the navy-purple brand palette, making even elevation feel on-brand.
|
||||||
|
|
||||||
|
**Key Characteristics:**
|
||||||
|
- sohne-var with OpenType `"ss01"` on all text -- a custom stylistic set that defines the brand's letterforms
|
||||||
|
- Weight 300 as the signature headline weight -- light, confident, anti-convention
|
||||||
|
- Negative letter-spacing at display sizes (-1.4px at 56px, progressive relaxation downward)
|
||||||
|
- Blue-tinted multi-layer shadows using `rgba(50,50,93,0.25)` -- elevation that feels brand-colored
|
||||||
|
- Deep navy (`#061b31`) headings instead of black -- warm, premium, financial-grade
|
||||||
|
- Conservative border-radius (4px-8px) -- nothing pill-shaped, nothing harsh
|
||||||
|
- Ruby (`#ea2261`) and magenta (`#f96bee`) accents for gradient and decorative elements
|
||||||
|
- `SourceCodePro` as the monospace companion for code and technical labels
|
||||||
|
|
||||||
|
## 2. Color Palette & Roles
|
||||||
|
|
||||||
|
### Primary
|
||||||
|
- **Stripe Purple** (`#533afd`): Primary brand color, CTA backgrounds, link text, interactive highlights. A saturated blue-violet that anchors the entire system.
|
||||||
|
- **Deep Navy** (`#061b31`): `--hds-color-heading-solid`. Primary heading color. Not black, not gray -- a very dark blue that adds warmth and depth to text.
|
||||||
|
- **Pure White** (`#ffffff`): Page background, card surfaces, button text on dark backgrounds.
|
||||||
|
|
||||||
|
### Brand & Dark
|
||||||
|
- **Brand Dark** (`#1c1e54`): `--hds-color-util-brand-900`. Deep indigo for dark sections, footer backgrounds, and immersive brand moments.
|
||||||
|
- **Dark Navy** (`#0d253d`): `--hds-color-core-neutral-975`. The darkest neutral -- almost-black with a blue undertone for maximum depth without harshness.
|
||||||
|
|
||||||
|
### Accent Colors
|
||||||
|
- **Ruby** (`#ea2261`): `--hds-color-accentColorMode-ruby-icon-solid`. Warm red-pink for icons, alerts, and accent elements.
|
||||||
|
- **Magenta** (`#f96bee`): `--hds-color-accentColorMode-magenta-icon-gradientMiddle`. Vivid pink-purple for gradients and decorative highlights.
|
||||||
|
- **Magenta Light** (`#ffd7ef`): `--hds-color-util-accent-magenta-100`. Tinted surface for magenta-themed cards and badges.
|
||||||
|
|
||||||
|
### Interactive
|
||||||
|
- **Primary Purple** (`#533afd`): Primary link color, active states, selected elements.
|
||||||
|
- **Purple Hover** (`#4434d4`): Darker purple for hover states on primary elements.
|
||||||
|
- **Purple Deep** (`#2e2b8c`): `--hds-color-button-ui-iconHover`. Dark purple for icon hover states.
|
||||||
|
- **Purple Light** (`#b9b9f9`): `--hds-color-action-bg-subduedHover`. Soft lavender for subdued hover backgrounds.
|
||||||
|
- **Purple Mid** (`#665efd`): `--hds-color-input-selector-text-range`. Range selector and input highlight color.
|
||||||
|
|
||||||
|
### Neutral Scale
|
||||||
|
- **Heading** (`#061b31`): Primary headings, nav text, strong labels.
|
||||||
|
- **Label** (`#273951`): `--hds-color-input-text-label`. Form labels, secondary headings.
|
||||||
|
- **Body** (`#64748d`): Secondary text, descriptions, captions.
|
||||||
|
- **Success Green** (`#15be53`): Status badges, success indicators (with 0.2-0.4 alpha for backgrounds/borders).
|
||||||
|
- **Success Text** (`#108c3d`): Success badge text color.
|
||||||
|
- **Lemon** (`#9b6829`): `--hds-color-core-lemon-500`. Warning and highlight accent.
|
||||||
|
|
||||||
|
### Surface & Borders
|
||||||
|
- **Border Default** (`#e5edf5`): Standard border color for cards, dividers, and containers.
|
||||||
|
- **Border Purple** (`#b9b9f9`): Active/selected state borders on buttons and inputs.
|
||||||
|
- **Border Soft Purple** (`#d6d9fc`): Subtle purple-tinted borders for secondary elements.
|
||||||
|
- **Border Magenta** (`#ffd7ef`): Pink-tinted borders for magenta-themed elements.
|
||||||
|
- **Border Dashed** (`#362baa`): Dashed borders for drop zones and placeholder elements.
|
||||||
|
|
||||||
|
### Shadow Colors
|
||||||
|
- **Shadow Blue** (`rgba(50,50,93,0.25)`): The signature -- blue-tinted primary shadow color.
|
||||||
|
- **Shadow Dark Blue** (`rgba(3,3,39,0.25)`): Deeper blue shadow for elevated elements.
|
||||||
|
- **Shadow Black** (`rgba(0,0,0,0.1)`): Secondary shadow layer for depth reinforcement.
|
||||||
|
- **Shadow Ambient** (`rgba(23,23,23,0.08)`): Soft ambient shadow for subtle elevation.
|
||||||
|
- **Shadow Soft** (`rgba(23,23,23,0.06)`): Minimal ambient shadow for light lift.
|
||||||
|
|
||||||
|
## 3. Typography Rules
|
||||||
|
|
||||||
|
### Font Family
|
||||||
|
- **Primary**: `sohne-var`, with fallback: `SF Pro Display`
|
||||||
|
- **Monospace**: `SourceCodePro`, with fallback: `SFMono-Regular`
|
||||||
|
- **OpenType Features**: `"ss01"` enabled globally on all sohne-var text; `"tnum"` for tabular numbers on financial data and captions.
|
||||||
|
|
||||||
|
### Hierarchy
|
||||||
|
|
||||||
|
| Role | Font | Size | Weight | Line Height | Letter Spacing | Features | Notes |
|
||||||
|
|------|------|------|--------|-------------|----------------|----------|-------|
|
||||||
|
| Display Hero | sohne-var | 56px (3.50rem) | 300 | 1.03 (tight) | -1.4px | ss01 | Maximum size, whisper-weight authority |
|
||||||
|
| Display Large | sohne-var | 48px (3.00rem) | 300 | 1.15 (tight) | -0.96px | ss01 | Secondary hero headlines |
|
||||||
|
| Section Heading | sohne-var | 32px (2.00rem) | 300 | 1.10 (tight) | -0.64px | ss01 | Feature section titles |
|
||||||
|
| Sub-heading Large | sohne-var | 26px (1.63rem) | 300 | 1.12 (tight) | -0.26px | ss01 | Card headings, sub-sections |
|
||||||
|
| Sub-heading | sohne-var | 22px (1.38rem) | 300 | 1.10 (tight) | -0.22px | ss01 | Smaller section heads |
|
||||||
|
| Body Large | sohne-var | 18px (1.13rem) | 300 | 1.40 | normal | ss01 | Feature descriptions, intro text |
|
||||||
|
| Body | sohne-var | 16px (1.00rem) | 300-400 | 1.40 | normal | ss01 | Standard reading text |
|
||||||
|
| Button | sohne-var | 16px (1.00rem) | 400 | 1.00 (tight) | normal | ss01 | Primary button text |
|
||||||
|
| Button Small | sohne-var | 14px (0.88rem) | 400 | 1.00 (tight) | normal | ss01 | Secondary/compact buttons |
|
||||||
|
| Link | sohne-var | 14px (0.88rem) | 400 | 1.00 (tight) | normal | ss01 | Navigation links |
|
||||||
|
| Caption | sohne-var | 13px (0.81rem) | 400 | normal | normal | ss01 | Small labels, metadata |
|
||||||
|
| Caption Small | sohne-var | 12px (0.75rem) | 300-400 | 1.33-1.45 | normal | ss01 | Fine print, timestamps |
|
||||||
|
| Caption Tabular | sohne-var | 12px (0.75rem) | 300-400 | 1.33 | -0.36px | tnum | Financial data, numbers |
|
||||||
|
| Micro | sohne-var | 10px (0.63rem) | 300 | 1.15 (tight) | 0.1px | ss01 | Tiny labels, axis markers |
|
||||||
|
| Micro Tabular | sohne-var | 10px (0.63rem) | 300 | 1.15 (tight) | -0.3px | tnum | Chart data, small numbers |
|
||||||
|
| Nano | sohne-var | 8px (0.50rem) | 300 | 1.07 (tight) | normal | ss01 | Smallest labels |
|
||||||
|
| Code Body | SourceCodePro | 12px (0.75rem) | 500 | 2.00 (relaxed) | normal | -- | Code blocks, syntax |
|
||||||
|
| Code Bold | SourceCodePro | 12px (0.75rem) | 700 | 2.00 (relaxed) | normal | -- | Bold code, keywords |
|
||||||
|
| Code Label | SourceCodePro | 12px (0.75rem) | 500 | 2.00 (relaxed) | normal | uppercase | Technical labels |
|
||||||
|
| Code Micro | SourceCodePro | 9px (0.56rem) | 500 | 1.00 (tight) | normal | ss01 | Tiny code annotations |
|
||||||
|
|
||||||
|
### Principles
|
||||||
|
- **Light weight as signature**: Weight 300 at display sizes is Stripe's most distinctive typographic choice. Where others use 600-700 to command attention, Stripe uses lightness as luxury -- the text is so confident it doesn't need weight to be authoritative.
|
||||||
|
- **ss01 everywhere**: The `"ss01"` stylistic set is non-negotiable. It modifies specific glyphs (likely alternate `a`, `g`, `l` forms) to create a more geometric, contemporary feel across all sohne-var text.
|
||||||
|
- **Two OpenType modes**: `"ss01"` for display/body text, `"tnum"` for tabular numerals in financial data. These never overlap -- a number in a paragraph uses ss01, a number in a data table uses tnum.
|
||||||
|
- **Progressive tracking**: Letter-spacing tightens proportionally with size: -1.4px at 56px, -0.96px at 48px, -0.64px at 32px, -0.26px at 26px, normal at 16px and below.
|
||||||
|
- **Two-weight simplicity**: Primarily 300 (body and headings) and 400 (UI/buttons). No bold (700) in the primary font -- SourceCodePro uses 500/700 for code contrast.
|
||||||
|
|
||||||
|
## 4. Component Stylings
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
**Primary Purple**
|
||||||
|
- Background: `#533afd`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Padding: 8px 16px
|
||||||
|
- Radius: 4px
|
||||||
|
- Font: 16px sohne-var weight 400, `"ss01"`
|
||||||
|
- Hover: `#4434d4` background
|
||||||
|
- Use: Primary CTA ("Start now", "Contact sales")
|
||||||
|
|
||||||
|
**Ghost / Outlined**
|
||||||
|
- Background: transparent
|
||||||
|
- Text: `#533afd`
|
||||||
|
- Padding: 8px 16px
|
||||||
|
- Radius: 4px
|
||||||
|
- Border: `1px solid #b9b9f9`
|
||||||
|
- Font: 16px sohne-var weight 400, `"ss01"`
|
||||||
|
- Hover: background shifts to `rgba(83,58,253,0.05)`
|
||||||
|
- Use: Secondary actions
|
||||||
|
|
||||||
|
**Transparent Info**
|
||||||
|
- Background: transparent
|
||||||
|
- Text: `#2874ad`
|
||||||
|
- Padding: 8px 16px
|
||||||
|
- Radius: 4px
|
||||||
|
- Border: `1px solid rgba(43,145,223,0.2)`
|
||||||
|
- Use: Tertiary/info-level actions
|
||||||
|
|
||||||
|
**Neutral Ghost**
|
||||||
|
- Background: transparent (`rgba(255,255,255,0)`)
|
||||||
|
- Text: `rgba(16,16,16,0.3)`
|
||||||
|
- Padding: 8px 16px
|
||||||
|
- Radius: 4px
|
||||||
|
- Outline: `1px solid rgb(212,222,233)`
|
||||||
|
- Use: Disabled or muted actions
|
||||||
|
|
||||||
|
### Cards & Containers
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Border: `1px solid #e5edf5` (standard) or `1px solid #061b31` (dark accent)
|
||||||
|
- Radius: 4px (tight), 5px (standard), 6px (comfortable), 8px (featured)
|
||||||
|
- Shadow (standard): `rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px`
|
||||||
|
- Shadow (ambient): `rgba(23,23,23,0.08) 0px 15px 35px 0px`
|
||||||
|
- Hover: shadow intensifies, often adding the blue-tinted layer
|
||||||
|
|
||||||
|
### Badges / Tags / Pills
|
||||||
|
**Neutral Pill**
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Text: `#000000`
|
||||||
|
- Padding: 0px 6px
|
||||||
|
- Radius: 4px
|
||||||
|
- Border: `1px solid #f6f9fc`
|
||||||
|
- Font: 11px weight 400
|
||||||
|
|
||||||
|
**Success Badge**
|
||||||
|
- Background: `rgba(21,190,83,0.2)`
|
||||||
|
- Text: `#108c3d`
|
||||||
|
- Padding: 1px 6px
|
||||||
|
- Radius: 4px
|
||||||
|
- Border: `1px solid rgba(21,190,83,0.4)`
|
||||||
|
- Font: 10px weight 300
|
||||||
|
|
||||||
|
### Inputs & Forms
|
||||||
|
- Border: `1px solid #e5edf5`
|
||||||
|
- Radius: 4px
|
||||||
|
- Focus: `1px solid #533afd` or purple ring
|
||||||
|
- Label: `#273951`, 14px sohne-var
|
||||||
|
- Text: `#061b31`
|
||||||
|
- Placeholder: `#64748d`
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
- Clean horizontal nav on white, sticky with blur backdrop
|
||||||
|
- Brand logotype left-aligned
|
||||||
|
- Links: sohne-var 14px weight 400, `#061b31` text with `"ss01"`
|
||||||
|
- Radius: 6px on nav container
|
||||||
|
- CTA: purple button right-aligned ("Sign in", "Start now")
|
||||||
|
- Mobile: hamburger toggle with 6px radius
|
||||||
|
|
||||||
|
### Decorative Elements
|
||||||
|
**Dashed Borders**
|
||||||
|
- `1px dashed #362baa` (purple) for placeholder/drop zones
|
||||||
|
- `1px dashed #ffd7ef` (magenta) for magenta-themed decorative borders
|
||||||
|
|
||||||
|
**Gradient Accents**
|
||||||
|
- Ruby-to-magenta gradients (`#ea2261` to `#f96bee`) for hero decorations
|
||||||
|
- Brand dark sections use `#1c1e54` backgrounds with white text
|
||||||
|
|
||||||
|
## 5. Layout Principles
|
||||||
|
|
||||||
|
### Spacing System
|
||||||
|
- Base unit: 8px
|
||||||
|
- Scale: 1px, 2px, 4px, 6px, 8px, 10px, 11px, 12px, 14px, 16px, 18px, 20px
|
||||||
|
- Notable: The scale is dense at the small end (every 2px from 4-12), reflecting Stripe's precision-oriented UI for financial data
|
||||||
|
|
||||||
|
### Grid & Container
|
||||||
|
- Max content width: approximately 1080px
|
||||||
|
- Hero: centered single-column with generous padding, lightweight headlines
|
||||||
|
- Feature sections: 2-3 column grids for feature cards
|
||||||
|
- Full-width dark sections with `#1c1e54` background for brand immersion
|
||||||
|
- Code/dashboard previews as contained cards with blue-tinted shadows
|
||||||
|
|
||||||
|
### Whitespace Philosophy
|
||||||
|
- **Precision spacing**: Unlike the vast emptiness of minimalist systems, Stripe uses measured, purposeful whitespace. Every gap is a deliberate typographic choice.
|
||||||
|
- **Dense data, generous chrome**: Financial data displays (tables, charts) are tightly packed, but the UI chrome around them is generously spaced. This creates a sense of controlled density -- like a well-organized spreadsheet in a beautiful frame.
|
||||||
|
- **Section rhythm**: White sections alternate with dark brand sections (`#1c1e54`), creating a dramatic light/dark cadence that prevents monotony without introducing arbitrary color.
|
||||||
|
|
||||||
|
### Border Radius Scale
|
||||||
|
- Micro (1px): Fine-grained elements, subtle rounding
|
||||||
|
- Standard (4px): Buttons, inputs, badges, cards -- the workhorse
|
||||||
|
- Comfortable (5px): Standard card containers
|
||||||
|
- Relaxed (6px): Navigation, larger interactive elements
|
||||||
|
- Large (8px): Featured cards, hero elements
|
||||||
|
- Compound: `0px 0px 6px 6px` for bottom-rounded containers (tab panels, dropdown footers)
|
||||||
|
|
||||||
|
## 6. Depth & Elevation
|
||||||
|
|
||||||
|
| Level | Treatment | Use |
|
||||||
|
|-------|-----------|-----|
|
||||||
|
| Flat (Level 0) | No shadow | Page background, inline text |
|
||||||
|
| Ambient (Level 1) | `rgba(23,23,23,0.06) 0px 3px 6px` | Subtle card lift, hover hints |
|
||||||
|
| Standard (Level 2) | `rgba(23,23,23,0.08) 0px 15px 35px` | Standard cards, content panels |
|
||||||
|
| Elevated (Level 3) | `rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px` | Featured cards, dropdowns, popovers |
|
||||||
|
| Deep (Level 4) | `rgba(3,3,39,0.25) 0px 14px 21px -14px, rgba(0,0,0,0.1) 0px 8px 17px -8px` | Modals, floating panels |
|
||||||
|
| Ring (Accessibility) | `2px solid #533afd` outline | Keyboard focus ring |
|
||||||
|
|
||||||
|
**Shadow Philosophy**: Stripe's shadow system is built on a principle of chromatic depth. Where most design systems use neutral gray or black shadows, Stripe's primary shadow color (`rgba(50,50,93,0.25)`) is a deep blue-gray that echoes the brand's navy palette. This creates shadows that don't just add depth -- they add brand atmosphere. The multi-layer approach pairs this blue-tinted shadow with a pure black secondary layer (`rgba(0,0,0,0.1)`) at a different offset, creating a parallax-like depth where the branded shadow sits farther from the element and the neutral shadow sits closer. The negative spread values (-30px, -18px) ensure shadows don't extend beyond the element's footprint horizontally, keeping elevation vertical and controlled.
|
||||||
|
|
||||||
|
### Decorative Depth
|
||||||
|
- Dark brand sections (`#1c1e54`) create immersive depth through background color contrast
|
||||||
|
- Gradient overlays with ruby-to-magenta transitions for hero decorations
|
||||||
|
- Shadow color `rgba(0,55,112,0.08)` (`--hds-color-shadow-sm-top`) for top-edge shadows on sticky elements
|
||||||
|
|
||||||
|
## 7. Do's and Don'ts
|
||||||
|
|
||||||
|
### Do
|
||||||
|
- Use sohne-var with `"ss01"` on every text element -- the stylistic set IS the brand
|
||||||
|
- Use weight 300 for all headlines and body text -- lightness is the signature
|
||||||
|
- Apply blue-tinted shadows (`rgba(50,50,93,0.25)`) for all elevated elements
|
||||||
|
- Use `#061b31` (deep navy) for headings instead of `#000000` -- the warmth matters
|
||||||
|
- Keep border-radius between 4px-8px -- conservative rounding is intentional
|
||||||
|
- Use `"tnum"` for any tabular/financial number display
|
||||||
|
- Layer shadows: blue-tinted far + neutral close for depth parallax
|
||||||
|
- Use `#533afd` purple as the primary interactive/CTA color
|
||||||
|
|
||||||
|
### Don't
|
||||||
|
- Don't use weight 600-700 for sohne-var headlines -- weight 300 is the brand voice
|
||||||
|
- Don't use large border-radius (12px+, pill shapes) on cards or buttons -- Stripe is conservative
|
||||||
|
- Don't use neutral gray shadows -- always tint with blue (`rgba(50,50,93,...)`)
|
||||||
|
- Don't skip `"ss01"` on any sohne-var text -- the alternate glyphs define the personality
|
||||||
|
- Don't use pure black (`#000000`) for headings -- always `#061b31` deep navy
|
||||||
|
- Don't use warm accent colors (orange, yellow) for interactive elements -- purple is primary
|
||||||
|
- Don't apply positive letter-spacing at display sizes -- Stripe tracks tight
|
||||||
|
- Don't use the magenta/ruby accents for buttons or links -- they're decorative/gradient only
|
||||||
|
|
||||||
|
## 8. Responsive Behavior
|
||||||
|
|
||||||
|
### Breakpoints
|
||||||
|
| Name | Width | Key Changes |
|
||||||
|
|------|-------|-------------|
|
||||||
|
| Mobile | <640px | Single column, reduced heading sizes, stacked cards |
|
||||||
|
| Tablet | 640-1024px | 2-column grids, moderate padding |
|
||||||
|
| Desktop | 1024-1280px | Full layout, 3-column feature grids |
|
||||||
|
| Large Desktop | >1280px | Centered content with generous margins |
|
||||||
|
|
||||||
|
### Touch Targets
|
||||||
|
- Buttons use comfortable padding (8px-16px vertical)
|
||||||
|
- Navigation links at 14px with adequate spacing
|
||||||
|
- Badges have 6px horizontal padding minimum for tap targets
|
||||||
|
- Mobile nav toggle with 6px radius button
|
||||||
|
|
||||||
|
### Collapsing Strategy
|
||||||
|
- Hero: 56px display -> 32px on mobile, weight 300 maintained
|
||||||
|
- Navigation: horizontal links + CTAs -> hamburger toggle
|
||||||
|
- Feature cards: 3-column -> 2-column -> single column stacked
|
||||||
|
- Dark brand sections: maintain full-width treatment, reduce internal padding
|
||||||
|
- Financial data tables: horizontal scroll on mobile
|
||||||
|
- Section spacing: 64px+ -> 40px on mobile
|
||||||
|
- Typography scale compresses: 56px -> 48px -> 32px hero sizes across breakpoints
|
||||||
|
|
||||||
|
### Image Behavior
|
||||||
|
- Dashboard/product screenshots maintain blue-tinted shadow at all sizes
|
||||||
|
- Hero gradient decorations simplify on mobile
|
||||||
|
- Code blocks maintain `SourceCodePro` treatment, may horizontally scroll
|
||||||
|
- Card images maintain consistent 4px-6px border-radius
|
||||||
|
|
||||||
|
## 9. Agent Prompt Guide
|
||||||
|
|
||||||
|
### Quick Color Reference
|
||||||
|
- Primary CTA: Stripe Purple (`#533afd`)
|
||||||
|
- CTA Hover: Purple Dark (`#4434d4`)
|
||||||
|
- Background: Pure White (`#ffffff`)
|
||||||
|
- Heading text: Deep Navy (`#061b31`)
|
||||||
|
- Body text: Slate (`#64748d`)
|
||||||
|
- Label text: Dark Slate (`#273951`)
|
||||||
|
- Border: Soft Blue (`#e5edf5`)
|
||||||
|
- Link: Stripe Purple (`#533afd`)
|
||||||
|
- Dark section: Brand Dark (`#1c1e54`)
|
||||||
|
- Success: Green (`#15be53`)
|
||||||
|
- Accent decorative: Ruby (`#ea2261`), Magenta (`#f96bee`)
|
||||||
|
|
||||||
|
### Example Component Prompts
|
||||||
|
- "Create a hero section on white background. Headline at 48px sohne-var weight 300, line-height 1.15, letter-spacing -0.96px, color #061b31, font-feature-settings 'ss01'. Subtitle at 18px weight 300, line-height 1.40, color #64748d. Purple CTA button (#533afd, 4px radius, 8px 16px padding, white text) and ghost button (transparent, 1px solid #b9b9f9, #533afd text, 4px radius)."
|
||||||
|
- "Design a card: white background, 1px solid #e5edf5 border, 6px radius. Shadow: rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px. Title at 22px sohne-var weight 300, letter-spacing -0.22px, color #061b31, 'ss01'. Body at 16px weight 300, #64748d."
|
||||||
|
- "Build a success badge: rgba(21,190,83,0.2) background, #108c3d text, 4px radius, 1px 6px padding, 10px sohne-var weight 300, border 1px solid rgba(21,190,83,0.4)."
|
||||||
|
- "Create navigation: white sticky header with backdrop-filter blur(12px). sohne-var 14px weight 400 for links, #061b31 text, 'ss01'. Purple CTA 'Start now' right-aligned (#533afd bg, white text, 4px radius). Nav container 6px radius."
|
||||||
|
- "Design a dark brand section: #1c1e54 background, white text. Headline 32px sohne-var weight 300, letter-spacing -0.64px, 'ss01'. Body 16px weight 300, rgba(255,255,255,0.7). Cards inside use rgba(255,255,255,0.1) border with 6px radius."
|
||||||
|
|
||||||
|
### Iteration Guide
|
||||||
|
1. Always enable `font-feature-settings: "ss01"` on sohne-var text -- this is the brand's typographic DNA
|
||||||
|
2. Weight 300 is the default; use 400 only for buttons/links/navigation
|
||||||
|
3. Shadow formula: `rgba(50,50,93,0.25) 0px Y1 B1 -S1, rgba(0,0,0,0.1) 0px Y2 B2 -S2` where Y1/B1 are larger (far shadow) and Y2/B2 are smaller (near shadow)
|
||||||
|
4. Heading color is `#061b31` (deep navy), body is `#64748d` (slate), labels are `#273951` (dark slate)
|
||||||
|
5. Border-radius stays in the 4px-8px range -- never use pill shapes or large rounding
|
||||||
|
6. Use `"tnum"` for any numbers in tables, charts, or financial displays
|
||||||
|
7. Dark sections use `#1c1e54` -- not black, not gray, but a deep branded indigo
|
||||||
|
8. SourceCodePro for code at 12px/500 with 2.00 line-height (very generous for readability)
|
||||||
17
Dockerfile
@@ -51,17 +51,18 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
|||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
|
||||||
COPY --from=builder /app/prisma ./prisma
|
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
||||||
COPY --from=builder /app/.next/standalone ./
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
COPY --from=builder /app/.next/static ./.next/static
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||||
COPY --from=builder /app/docker/entrypoint.sh ./docker/entrypoint.sh
|
COPY --from=builder --chown=nextjs:nodejs /app/docker/entrypoint.sh ./docker/entrypoint.sh
|
||||||
|
|
||||||
RUN chmod +x ./docker/entrypoint.sh
|
RUN chmod +x ./docker/entrypoint.sh
|
||||||
|
|
||||||
# --- NEU: Ordner erstellen und Rechte an den nextjs User geben ---
|
# Next writes ISR/prerender artifacts under .next/server/app at runtime.
|
||||||
RUN mkdir -p /app/.next/cache && chown nextjs:nodejs /app/.next/cache
|
RUN mkdir -p /app/.next/cache /app/.next/server/app \
|
||||||
|
&& chown -R nextjs:nodejs /app/.next
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
|
|||||||
51
PRODUCT.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Product
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
brand
|
||||||
|
|
||||||
|
## Users
|
||||||
|
|
||||||
|
Two primary personas, both aesthetically-driven professionals who have outgrown generic tools:
|
||||||
|
|
||||||
|
**Marketing / Agency Lead** — manages multiple client campaigns simultaneously. Needs organized folders, detailed analytics, and design output that doesn't betray itself as "internet freeware." They evaluate tools by how their outputs look to clients, not just how the tool works internally.
|
||||||
|
|
||||||
|
**Modern Restaurateur** — owner of a boutique restaurant, cafe, or hotel. Their physical space is carefully designed; their digital touchpoints must match. They rely on Dynamic QR codes to swap menu PDFs and URLs seasonally without reprinting expensive acrylic table stands. Brand consistency between print and digital is non-negotiable for them.
|
||||||
|
|
||||||
|
**Arrival context:** Both land on QR Master after being burned by "free" tools that expired, injected third-party ads, or looked visually cheap. They are actively comparing options and arrive skeptical. They're not discovering QR tools for the first time — they're looking for a permanent, professional home.
|
||||||
|
|
||||||
|
## Product Purpose
|
||||||
|
|
||||||
|
QR Master is a precision QR code platform — creation, dynamic editing, and analytics — for professionals who refuse to compromise on aesthetics. Success means users choose QR Master not because they have to, but because they want to: the tool itself feels like an extension of their creative workflow, not a clunky utility they tolerate.
|
||||||
|
|
||||||
|
## Brand Personality
|
||||||
|
|
||||||
|
**Confident, Minimal, Crafted.**
|
||||||
|
|
||||||
|
"The Leica of QR Generators." A precision instrument that earns trust through intentionality, not decoration. Every pixel deliberate. Think Linear's pro-tool clarity, Raycast's utilitarian beauty, Framer's implied creative freedom — combined into something that feels high-performance without performing.
|
||||||
|
|
||||||
|
Tone of voice: direct and assured. No hedging, no exclamation points for emphasis, no "Amazing!" copy. Let the product speak. Copy is short, specific, and treats the user as a professional.
|
||||||
|
|
||||||
|
## Anti-references
|
||||||
|
|
||||||
|
- **SEO ad farms** (QR-Code-Generator.com style): cluttered sidebars, aggressive upsell banners, walls of keyword-stuffed text. The opposite of QR Master.
|
||||||
|
- **Bubbly link-in-bio tools**: neon gradients, Gen-Z playfulness, the Linktree aesthetic. QR Master is for businesses, not social profiles.
|
||||||
|
- **Legacy enterprise software**: cold gray, mechanical, joyless. High-end is not the same as corporate. QR Master should feel premium, not bureaucratic.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **Precision over decoration.** Every element earns its place. No UX furniture — no gratuitous dividers, decorative gradients, or filler icons. If removing it doesn't hurt, remove it.
|
||||||
|
|
||||||
|
2. **Show, don't explain.** The product's quality is demonstrated by how the interface looks and behaves, not described in marketing copy. A beautifully rendered QR preview communicates more than three bullet points about "custom branding."
|
||||||
|
|
||||||
|
3. **Premium through restraint.** Sophistication comes from what's removed. More whitespace, fewer words, tighter hierarchy. The instinct to add is the enemy of craft.
|
||||||
|
|
||||||
|
4. **Trustworthy at a glance.** Clarity and professionalism must be instantaneous — users arriving skeptical decide in 5 seconds. No clever puzzles, no mystery-meat navigation. Confidence is expressed through legibility.
|
||||||
|
|
||||||
|
5. **Tool-like beauty.** Functional elegance, like a well-made physical instrument. Interactions should feel responsive and precise. Animations are subtle cues, not performances. The UI should feel fast even when it isn't.
|
||||||
|
|
||||||
|
## Accessibility & Inclusion
|
||||||
|
|
||||||
|
- **Standard**: WCAG 2.1 AA
|
||||||
|
- **Reduced motion**: Full `prefers-reduced-motion` support. Animations (where used) default to subtle fades or scale shifts; no motion for motion's sake.
|
||||||
|
- **Color contrast**: Critical for a QR creation tool. The dashboard must warn users when foreground/background color combinations produce insufficient contrast — both for readability and QR scannability. This is a functional requirement, not a nice-to-have.
|
||||||
@@ -9,6 +9,8 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=en_US.utf8"
|
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=en_US.utf8"
|
||||||
|
ports:
|
||||||
|
- "5435:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- dbdata:/var/lib/postgresql/data
|
- dbdata:/var/lib/postgresql/data
|
||||||
- ./docker/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
|
- ./docker/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
|
||||||
@@ -92,6 +94,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- qrmaster-network
|
- qrmaster-network
|
||||||
|
|
||||||
|
|
||||||
# Adminer - Database Management UI (Optional)
|
# Adminer - Database Management UI (Optional)
|
||||||
adminer:
|
adminer:
|
||||||
image: adminer:latest
|
image: adminer:latest
|
||||||
@@ -116,4 +119,4 @@ volumes:
|
|||||||
|
|
||||||
networks:
|
networks:
|
||||||
qrmaster-network:
|
qrmaster-network:
|
||||||
driver: bridge
|
external: true
|
||||||
|
|||||||
45
docs/automations/README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Codex Automation System
|
||||||
|
|
||||||
|
This folder defines reusable Codex workflows for QRMaster and GreenLens Pro.
|
||||||
|
Use these as operating playbooks when asking Codex to run growth, SEO, content,
|
||||||
|
or app-store work.
|
||||||
|
|
||||||
|
## Active Automations
|
||||||
|
|
||||||
|
### QRMaster
|
||||||
|
|
||||||
|
1. `qrmaster-pr-seo-review.md`
|
||||||
|
- Purpose: review every SEO, landing page, and conversion change before it is merged.
|
||||||
|
- Primary plugins/tools: GitHub, Codex.
|
||||||
|
- Primary skills: `ai-seo`, `content-strategy`, `careful`, `qa`.
|
||||||
|
|
||||||
|
2. `qrmaster-seo-sprint-machine.md`
|
||||||
|
- Purpose: plan and produce a weekly SEO sprint from keyword backlog to PR-ready work.
|
||||||
|
- Primary plugins/tools: GitHub, Coupler or CSV exports, Codex.
|
||||||
|
- Primary skills: `content-strategy`, `ai-seo`, `copywriting`, `qa`.
|
||||||
|
|
||||||
|
3. `qrmaster-broken-link-cta-checker.md`
|
||||||
|
- Purpose: catch broken internal links and broken CTAs after direct `main`
|
||||||
|
branch changes.
|
||||||
|
- Primary plugins/tools: Codex, GitHub Actions or local npm script.
|
||||||
|
- Primary skills: `qa`, `ai-seo`.
|
||||||
|
|
||||||
|
### GreenLens Pro
|
||||||
|
|
||||||
|
1. `greenlens-pain-mining-machine.md`
|
||||||
|
- Purpose: turn reviews, comments, competitor messaging, and search questions into
|
||||||
|
product, ASO, and content opportunities.
|
||||||
|
- Primary plugins/tools: Codex, Gmail, Coupler or CSV exports.
|
||||||
|
- Primary skills: `app-store-aso`, `content-strategy`, `copywriting`.
|
||||||
|
|
||||||
|
2. `greenlens-viral-slideshow-machine.md`
|
||||||
|
- Purpose: turn validated plant pains into TikTok, Instagram, and Canva-ready
|
||||||
|
slideshow assets.
|
||||||
|
- Primary plugins/tools: Canva, Codex, Gmail/Fyxer for creator briefs.
|
||||||
|
- Primary skills: `content-strategy`, `copywriting`, `ad-creative`, `app-store-aso`.
|
||||||
|
|
||||||
|
## Operating Rule
|
||||||
|
|
||||||
|
Do not automate publishing directly. Automate drafts, PRs, reviews, and packaged
|
||||||
|
outputs first. A human should approve live SEO pages, store metadata, influencer
|
||||||
|
messages, and paid/conversion changes.
|
||||||
160
docs/automations/greenlens-pain-mining-machine.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# GreenLens Pro Pain Mining Machine
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Turn real plant-owner pains into content, ASO, influencer, landing page, and
|
||||||
|
product opportunities.
|
||||||
|
|
||||||
|
## Why This Exists
|
||||||
|
|
||||||
|
GreenLens Pro should be driven by what users actually worry about:
|
||||||
|
yellow leaves, brown spots, root rot, overwatering, underwatering, pests,
|
||||||
|
curling leaves, and not knowing what to do next.
|
||||||
|
|
||||||
|
## Plugins And Skills
|
||||||
|
|
||||||
|
| Need | Use |
|
||||||
|
|---|---|
|
||||||
|
| Review/comment exports | Coupler, CSV exports, Codex |
|
||||||
|
| App Store optimization | `app-store-aso` skill |
|
||||||
|
| Content clustering | `content-strategy` skill |
|
||||||
|
| Copy and hooks | `copywriting` skill |
|
||||||
|
| Support/outreach drafting | Gmail/Fyxer plugin |
|
||||||
|
| Product issue creation | GitHub plugin |
|
||||||
|
|
||||||
|
## Data Sources
|
||||||
|
|
||||||
|
Use any available source, but label the source for every pain:
|
||||||
|
|
||||||
|
- App Store competitor reviews
|
||||||
|
- Google Play competitor reviews
|
||||||
|
- Reddit plant-care threads
|
||||||
|
- TikTok or Instagram comments
|
||||||
|
- Google autocomplete or People Also Ask exports
|
||||||
|
- Support emails or user feedback
|
||||||
|
- Existing GreenLens analytics or onboarding responses
|
||||||
|
|
||||||
|
## Pain Taxonomy
|
||||||
|
|
||||||
|
Cluster each item into one primary category:
|
||||||
|
|
||||||
|
- Yellow leaves
|
||||||
|
- Brown spots
|
||||||
|
- Root rot
|
||||||
|
- Overwatering
|
||||||
|
- Underwatering
|
||||||
|
- Curling leaves
|
||||||
|
- Drooping leaves
|
||||||
|
- Pests
|
||||||
|
- Light problems
|
||||||
|
- Soil and repotting
|
||||||
|
- Beginner confusion
|
||||||
|
- Diagnosis trust
|
||||||
|
- Price/paywall objection
|
||||||
|
- App usability issue
|
||||||
|
|
||||||
|
## Scoring Model
|
||||||
|
|
||||||
|
Score each pain from 0-100:
|
||||||
|
|
||||||
|
| Factor | Weight |
|
||||||
|
|---|---:|
|
||||||
|
| User urgency | 30 |
|
||||||
|
| App fit | 25 |
|
||||||
|
| Content virality | 20 |
|
||||||
|
| ASO/search value | 15 |
|
||||||
|
| Product learning value | 10 |
|
||||||
|
|
||||||
|
Prioritize urgent, visual, diagnosis-driven pains where GreenLens can credibly
|
||||||
|
help the user decide what to check next.
|
||||||
|
|
||||||
|
## Weekly Output
|
||||||
|
|
||||||
|
Produce:
|
||||||
|
|
||||||
|
1. Top 20 pains.
|
||||||
|
2. Top 10 social hooks.
|
||||||
|
3. Top 5 ASO keyword opportunities.
|
||||||
|
4. Top 5 blog or landing page ideas.
|
||||||
|
5. Top 5 product issues or feature hypotheses.
|
||||||
|
6. Top 10 influencer angles.
|
||||||
|
|
||||||
|
## Codex Pain Mining Prompt
|
||||||
|
|
||||||
|
```text
|
||||||
|
Run the GreenLens Pro Pain Mining Machine.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- docs/automations/greenlens-pain-mining-machine.md
|
||||||
|
- app-store-aso skill
|
||||||
|
- content-strategy skill
|
||||||
|
|
||||||
|
Input source: [reviews/comments/export/pasted text]
|
||||||
|
Market: [US / DE / global]
|
||||||
|
Platform focus: [iOS / Android / both]
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
1. Extract raw plant-owner pains.
|
||||||
|
2. Cluster them into the GreenLens pain taxonomy.
|
||||||
|
3. Score each pain by urgency, app fit, virality, ASO value, and product learning.
|
||||||
|
4. Convert winners into:
|
||||||
|
- social hooks
|
||||||
|
- ASO keyword ideas
|
||||||
|
- blog/landing page ideas
|
||||||
|
- product issues
|
||||||
|
- influencer outreach angles
|
||||||
|
|
||||||
|
Do not invent source quotes. If evidence is weak, label it as hypothesis.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# GreenLens Pain Mining Report
|
||||||
|
|
||||||
|
## Source Summary
|
||||||
|
|
||||||
|
## Top Pains
|
||||||
|
| Rank | Pain | Source | Score | Why it matters |
|
||||||
|
|---|---|---|---:|---|
|
||||||
|
|
||||||
|
## Hook Backlog
|
||||||
|
|
||||||
|
## ASO Opportunities
|
||||||
|
|
||||||
|
## Product Issues
|
||||||
|
|
||||||
|
## Influencer Angles
|
||||||
|
|
||||||
|
## Next Actions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Product Issue Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Title: [Feature or improvement]
|
||||||
|
|
||||||
|
User pain:
|
||||||
|
[What the user is struggling with]
|
||||||
|
|
||||||
|
Hypothesis:
|
||||||
|
If GreenLens [change], users will [outcome].
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
- [criterion]
|
||||||
|
- [criterion]
|
||||||
|
- [criterion]
|
||||||
|
|
||||||
|
Measurement:
|
||||||
|
- activation
|
||||||
|
- scan completion
|
||||||
|
- paywall conversion
|
||||||
|
- retention
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- Every recommendation traces back to a real pain or explicitly marked hypothesis.
|
||||||
|
- Top pains can feed both ASO and social content.
|
||||||
|
- Product issues are concrete enough for GitHub.
|
||||||
|
|
||||||
139
docs/automations/greenlens-viral-slideshow-machine.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# GreenLens Pro Viral Slideshow Machine
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Convert validated plant pains into TikTok, Instagram, and Canva-ready slideshow
|
||||||
|
assets that drive awareness and app downloads.
|
||||||
|
|
||||||
|
## Plugins And Skills
|
||||||
|
|
||||||
|
| Need | Use |
|
||||||
|
|---|---|
|
||||||
|
| Creative generation | Canva plugin |
|
||||||
|
| Hook and caption writing | `copywriting`, `ad-creative` skills |
|
||||||
|
| Content planning | `content-strategy` skill |
|
||||||
|
| ASO alignment | `app-store-aso` skill |
|
||||||
|
| Creator briefs and outreach | Gmail/Fyxer plugin |
|
||||||
|
|
||||||
|
## Required Input
|
||||||
|
|
||||||
|
Use outputs from `greenlens-pain-mining-machine.md`:
|
||||||
|
|
||||||
|
- pain cluster
|
||||||
|
- urgency score
|
||||||
|
- source evidence
|
||||||
|
- target audience
|
||||||
|
- desired CTA
|
||||||
|
- app positioning angle
|
||||||
|
|
||||||
|
## Content Pillars
|
||||||
|
|
||||||
|
- Diagnosis before guessing
|
||||||
|
- Overwatering mistakes
|
||||||
|
- Yellow leaves
|
||||||
|
- Brown spots
|
||||||
|
- Root rot warnings
|
||||||
|
- Beginner plant rescue
|
||||||
|
- Plant symptoms explained
|
||||||
|
- "Do not water yet" warnings
|
||||||
|
- App scan/use-case demos
|
||||||
|
|
||||||
|
## Slideshow Formula
|
||||||
|
|
||||||
|
1. Hook: direct warning, contradiction, or curiosity.
|
||||||
|
2. Problem: show the common wrong assumption.
|
||||||
|
3. Explanation: simple plant-care reason.
|
||||||
|
4. Check: what the user should inspect first.
|
||||||
|
5. Risk: what happens if they guess.
|
||||||
|
6. GreenLens bridge: scan or diagnose before acting.
|
||||||
|
7. CTA: download, scan, or save.
|
||||||
|
|
||||||
|
## Hook Patterns
|
||||||
|
|
||||||
|
- "Do not water your plant before checking this."
|
||||||
|
- "Yellow leaves do not always mean your plant is thirsty."
|
||||||
|
- "Brown spots can mean more than sunburn."
|
||||||
|
- "Your plant was warning you before it started dying."
|
||||||
|
- "Overwatering often looks like underwatering."
|
||||||
|
- "Scan before you guess."
|
||||||
|
|
||||||
|
## Canva Direction
|
||||||
|
|
||||||
|
Use GreenLens as a calm diagnosis-first plant app:
|
||||||
|
|
||||||
|
- natural plant photography or close-up symptom imagery
|
||||||
|
- clear readable overlay text
|
||||||
|
- botanical but not decorative-only
|
||||||
|
- show symptoms clearly
|
||||||
|
- app screenshot or phone mockup only when it explains the action
|
||||||
|
- avoid vague wellness aesthetics that do not show the plant problem
|
||||||
|
|
||||||
|
## Codex Slideshow Prompt
|
||||||
|
|
||||||
|
```text
|
||||||
|
Run the GreenLens Pro Viral Slideshow Machine.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- docs/automations/greenlens-pain-mining-machine.md
|
||||||
|
- docs/automations/greenlens-viral-slideshow-machine.md
|
||||||
|
- app-store-aso skill
|
||||||
|
- content-strategy skill
|
||||||
|
- copywriting/ad-creative skills
|
||||||
|
|
||||||
|
Input pain cluster: [pain]
|
||||||
|
Audience: [beginner plant owners / plant rescue followers / houseplant collectors]
|
||||||
|
CTA: [Download GreenLens Pro / Scan your plant / Save this checklist]
|
||||||
|
Channel: [TikTok / Instagram / both]
|
||||||
|
Quantity: [number]
|
||||||
|
|
||||||
|
Return for each concept:
|
||||||
|
1. hook
|
||||||
|
2. 5-7 slide script
|
||||||
|
3. visual direction per slide
|
||||||
|
4. Canva prompt
|
||||||
|
5. caption
|
||||||
|
6. hashtags
|
||||||
|
7. ASO keyword tie-in
|
||||||
|
8. creator brief version
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Keep claims educational, not medical/certain beyond evidence.
|
||||||
|
- Do not promise perfect diagnosis.
|
||||||
|
- Make the symptom visually inspectable.
|
||||||
|
- The app CTA should feel like the next practical step, not a hard sell.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# GreenLens Slideshow Pack: [Pain Cluster]
|
||||||
|
|
||||||
|
## Concept 1: [Hook]
|
||||||
|
|
||||||
|
### Slides
|
||||||
|
1. [text] -- [visual]
|
||||||
|
2. [text] -- [visual]
|
||||||
|
3. [text] -- [visual]
|
||||||
|
4. [text] -- [visual]
|
||||||
|
5. [text] -- [visual]
|
||||||
|
6. [text] -- [visual]
|
||||||
|
7. [text] -- [visual]
|
||||||
|
|
||||||
|
### Canva Prompt
|
||||||
|
|
||||||
|
### Caption
|
||||||
|
|
||||||
|
### Hashtags
|
||||||
|
|
||||||
|
### ASO Tie-In
|
||||||
|
|
||||||
|
### Creator Brief
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quality Bar
|
||||||
|
|
||||||
|
- The first slide must be understandable in under 2 seconds.
|
||||||
|
- Every slide should be shorter than 12 words when possible.
|
||||||
|
- The visual must show the symptom or action, not just a plant mood shot.
|
||||||
|
- The final CTA should match the pain: scan, check, save, or download.
|
||||||
|
|
||||||
57
docs/automations/qrmaster-broken-link-cta-checker.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# QRMaster Broken Link + CTA Checker
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Catch broken internal links and broken conversion CTAs on `main`, especially
|
||||||
|
after direct edits without a pull request.
|
||||||
|
|
||||||
|
## What It Checks
|
||||||
|
|
||||||
|
- Static internal `href` values in source files.
|
||||||
|
- Static `router.push("/...")` destinations.
|
||||||
|
- Internal links against known Next.js app routes and files in `public/`.
|
||||||
|
- CTA-like links such as "Get started", "Create QR", "Start free",
|
||||||
|
"Generate QR", "Pricing", and "Upgrade".
|
||||||
|
- Pages that appear to have no obvious CTA link.
|
||||||
|
|
||||||
|
## Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check:links
|
||||||
|
```
|
||||||
|
|
||||||
|
The command prints a JSON report. It exits with a non-zero status if broken
|
||||||
|
internal links or broken CTA links are found.
|
||||||
|
|
||||||
|
## Known Limits
|
||||||
|
|
||||||
|
- Dynamic CMS/blog slugs are allowed by prefix and not fully validated.
|
||||||
|
- Runtime-only links built from variables are skipped.
|
||||||
|
- External links are not checked by this local script.
|
||||||
|
- This is a fast safety check, not a full crawl of the deployed website.
|
||||||
|
|
||||||
|
## Codex Automation Prompt
|
||||||
|
|
||||||
|
```text
|
||||||
|
Run the QRMaster Broken Link + CTA Checker.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- docs/automations/qrmaster-broken-link-cta-checker.md
|
||||||
|
- scripts/check-links-and-ctas.js
|
||||||
|
|
||||||
|
Run npm run check:links. Review the JSON report and summarize:
|
||||||
|
1. broken internal links
|
||||||
|
2. broken CTA links
|
||||||
|
3. important pages without obvious CTAs
|
||||||
|
4. concrete fixes with file paths
|
||||||
|
|
||||||
|
If the script fails, inspect the listed files and propose the smallest safe fix.
|
||||||
|
Do not modify production configuration automatically.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- No broken internal links.
|
||||||
|
- No broken CTA links.
|
||||||
|
- Important marketing, tool, and pricing pages have a clear CTA.
|
||||||
|
|
||||||
163
docs/automations/qrmaster-pr-seo-review.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# QRMaster PR SEO Review
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Catch SEO, conversion, content-quality, and technical issues before a QRMaster
|
||||||
|
page change is merged.
|
||||||
|
|
||||||
|
## Use When
|
||||||
|
|
||||||
|
- A PR changes landing pages, tool pages, comparison pages, blog posts, metadata,
|
||||||
|
schema, sitemap behavior, internal links, pricing copy, or CTAs.
|
||||||
|
- Codex generated new SEO pages from `marketing/programmatic-seo-top-50.md`.
|
||||||
|
- Existing pages were refreshed from Google Search Console or keyword data.
|
||||||
|
|
||||||
|
## Plugins And Skills
|
||||||
|
|
||||||
|
| Need | Use |
|
||||||
|
|---|---|
|
||||||
|
| Diff and PR review | GitHub plugin |
|
||||||
|
| Code/content inspection | Codex |
|
||||||
|
| SEO/AEO review | `ai-seo` skill |
|
||||||
|
| Content intent and cluster fit | `content-strategy` skill |
|
||||||
|
| Build/lint verification | GitHub Actions, `qa` skill |
|
||||||
|
| Careful merge decision | `careful` skill |
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
|
||||||
|
### 1. Technical SEO
|
||||||
|
|
||||||
|
- Exactly one H1 on the rendered page.
|
||||||
|
- Meta title exists and is specific to the page intent.
|
||||||
|
- Meta title places the primary keyword near the start where natural.
|
||||||
|
- Meta title stays under roughly 60 characters unless there is a clear reason.
|
||||||
|
- Meta description exists and promises the right outcome.
|
||||||
|
- Meta description includes the target keyword naturally, states the user benefit,
|
||||||
|
and stays concise enough to avoid likely truncation.
|
||||||
|
- Canonical URL is correct.
|
||||||
|
- Page is not accidentally noindexed.
|
||||||
|
- FAQ, Article, Product, Breadcrumb, or HowTo schema is valid where used.
|
||||||
|
- Sitemap and internal routing include the page where required.
|
||||||
|
- No broken internal links or CTA links.
|
||||||
|
- Language and locale are consistent.
|
||||||
|
- URL slug is short, descriptive, hyphenated, and does not include stale dates.
|
||||||
|
- Meaningful images have descriptive alt text and useful filenames where local
|
||||||
|
image handling allows it.
|
||||||
|
- Large visual assets are compressed or already optimized.
|
||||||
|
- Mobile layout is readable and CTAs are tappable.
|
||||||
|
- The page does not introduce obvious Core Web Vitals risks.
|
||||||
|
- Robots rules do not block important pages or desired AI/search crawlers.
|
||||||
|
|
||||||
|
### 2. Search Intent
|
||||||
|
|
||||||
|
- The first screen makes it obvious the page answers the target query.
|
||||||
|
- The opening paragraph states the problem directly.
|
||||||
|
- The page matches one primary intent only.
|
||||||
|
- The content is not a generic rewrite of another QRMaster page.
|
||||||
|
- The page includes concrete examples for its audience or use case.
|
||||||
|
- The target keyword intent is labeled as informational, commercial,
|
||||||
|
transactional, or navigational.
|
||||||
|
- The page covers the related subtopics a search or AI system would fan out to
|
||||||
|
for the main query.
|
||||||
|
- Each H2/H3 section answers the heading directly in the first sentence before
|
||||||
|
adding background or nuance.
|
||||||
|
|
||||||
|
### 3. QRMaster Conversion Fit
|
||||||
|
|
||||||
|
- Primary CTA is visible early.
|
||||||
|
- CTA copy matches the use case, not just generic "Get started".
|
||||||
|
- The page explains why dynamic QR codes matter when links change after printing.
|
||||||
|
- Scan analytics or tracking is mentioned when relevant.
|
||||||
|
- Privacy/GDPR positioning is included where tracking is discussed.
|
||||||
|
- The copy avoids unsupported claims.
|
||||||
|
|
||||||
|
### 4. Internal Linking
|
||||||
|
|
||||||
|
- New page links to the relevant money page:
|
||||||
|
- `/dynamic-qr-code-generator`
|
||||||
|
- `/qr-code-tracking`
|
||||||
|
- `/bulk-qr-code-generator`
|
||||||
|
- `/pricing`
|
||||||
|
- relevant `/tools/...` page
|
||||||
|
- Existing related pages should link back to the new page.
|
||||||
|
- Anchor text is natural and varied.
|
||||||
|
|
||||||
|
### 5. AI Search Extractability
|
||||||
|
|
||||||
|
- Important answer blocks are self-contained.
|
||||||
|
- Comparison content uses tables where useful.
|
||||||
|
- FAQ questions are written in natural user language.
|
||||||
|
- Definitions answer the query in 40-60 words when possible.
|
||||||
|
- Claims that need evidence include a source or are framed as product positioning.
|
||||||
|
- Sections are focused on one question or subtopic at a time.
|
||||||
|
- Bullet lists, tables, and short paragraphs are used where they improve
|
||||||
|
extraction and scanning.
|
||||||
|
- The page can be cited by AI systems without relying on surrounding context.
|
||||||
|
|
||||||
|
### 6. E-E-A-T And Quality
|
||||||
|
|
||||||
|
- Content is accurate, current, and not copied from competitors.
|
||||||
|
- Any competitor, pricing, legal, privacy, or compliance claim is verified or
|
||||||
|
clearly avoided.
|
||||||
|
- The page adds QRMaster-specific value, examples, workflows, or product context.
|
||||||
|
- The tone stays direct, useful, and trustworthy.
|
||||||
|
|
||||||
|
## Codex Review Prompt
|
||||||
|
|
||||||
|
```text
|
||||||
|
Review this QRMaster PR as an SEO, conversion, and technical quality gate.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- docs/automations/qrmaster-pr-seo-review.md
|
||||||
|
- .agents/product-marketing-context.md
|
||||||
|
- marketing/programmatic-seo-top-50.md
|
||||||
|
|
||||||
|
Check the diff for:
|
||||||
|
1. technical SEO issues
|
||||||
|
2. search intent mismatch
|
||||||
|
3. weak or missing CTA
|
||||||
|
4. duplicate/thin content
|
||||||
|
5. missing internal links
|
||||||
|
6. invalid or missing schema
|
||||||
|
7. weak AI/agentic-search extractability
|
||||||
|
8. missing visual/mobile/performance considerations
|
||||||
|
9. build/lint risks
|
||||||
|
|
||||||
|
Return findings ordered by severity. For each finding include:
|
||||||
|
- file path
|
||||||
|
- exact line if possible
|
||||||
|
- why it matters
|
||||||
|
- concrete fix
|
||||||
|
|
||||||
|
Also include:
|
||||||
|
- merge recommendation: approve / request changes
|
||||||
|
- required follow-up tasks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Review Output Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## QRMaster PR SEO Review
|
||||||
|
|
||||||
|
Decision: Request changes
|
||||||
|
|
||||||
|
### Findings
|
||||||
|
1. [High] Missing internal link to `/dynamic-qr-code-generator`
|
||||||
|
2. [Medium] CTA is too generic for the target intent
|
||||||
|
3. [Low] FAQ question overlaps with another page
|
||||||
|
|
||||||
|
### Required Fixes
|
||||||
|
- Add contextual link from the "after printing" section to `/dynamic-qr-code-generator`.
|
||||||
|
- Change CTA from "Get started" to "Create an editable QR code for your flyer".
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Build:
|
||||||
|
- Lint:
|
||||||
|
- Link/schema check:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- No high-severity SEO or conversion findings remain.
|
||||||
|
- Build and lint pass.
|
||||||
|
- The PR has a clear human approval before merge.
|
||||||
158
docs/automations/qrmaster-seo-sprint-2026-05-11.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# QRMaster SEO Sprint: Tracking and Analytics
|
||||||
|
|
||||||
|
Run date: 2026-05-11
|
||||||
|
Automation: QRMaster SEO Sprint Machine
|
||||||
|
Status: Recommendation package only; do not publish without human approval.
|
||||||
|
|
||||||
|
## Input Notes
|
||||||
|
|
||||||
|
- Used `docs/automations/qrmaster-seo-sprint-machine.md`, `docs/automations/qrmaster-pr-seo-review.md`, `.agents/product-marketing-context.md`, `marketing/programmatic-seo-top-50.md`, `seo-plan-april.md`, and `seo-keywords.csv`.
|
||||||
|
- `marketing/keyword-strategy-seo-plan.md` was not present in this worktree. `seo-plan-april.md` appears to be the local keyword strategy fallback and includes the same cluster-level keyword data.
|
||||||
|
- Existing routing redirects selected legacy `/guide/...` paths to `/learn/...`, so this sprint should avoid creating duplicate guide URLs without a clear canonical/routing decision.
|
||||||
|
|
||||||
|
## Cluster Scoring
|
||||||
|
|
||||||
|
| Cluster | Product fit /30 | Commercial intent /25 | Differentiation /15 | Cluster leverage /10 | Winability /10 | Effort /10 | Score | Decision |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|---:|---|
|
||||||
|
| Tracking and analytics | 29 | 24 | 15 | 10 | 8 | 8 | 94 | Select |
|
||||||
|
| Dynamic QR buying decision | 30 | 24 | 13 | 10 | 7 | 7 | 91 | Next best |
|
||||||
|
| Bulk QR generation | 27 | 19 | 14 | 8 | 9 | 8 | 85 | Hold for later |
|
||||||
|
| Commercial alternatives | 22 | 25 | 12 | 8 | 6 | 5 | 78 | Needs current competitor verification |
|
||||||
|
| Restaurant/menu QR | 25 | 16 | 12 | 8 | 7 | 7 | 75 | Good vertical support, weaker immediate demand |
|
||||||
|
| Custom/design QR | 20 | 18 | 9 | 8 | 8 | 8 | 71 | Tool-led, less differentiated |
|
||||||
|
| Print reliability | 21 | 13 | 10 | 7 | 9 | 8 | 68 | Useful support content, weaker commercial pull |
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
Tracking and analytics is the highest-fit weekly cluster because it maps directly to QRMaster's differentiators: dynamic QR redirects, scan analytics, placement comparison, and privacy-first reporting. The keyword set includes `qr code tracking` at 1k-10k monthly volume with +900% 3-month YoY trend, `track qr code scans` with +900% 3-month trend, and `trackable qr code` with the highest CPC ceiling in the file at EUR 34.25. The cluster also has strong internal-link leverage into `/dynamic-qr-code-generator`, `/qr-code-analytics`, `/qr-code-tracking`, `/qr-code-for-marketing-campaigns`, pricing, and use-case pages.
|
||||||
|
|
||||||
|
## Selected Work
|
||||||
|
|
||||||
|
| Type | URL | Score | Reason |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| Refresh | `/qr-code-tracking` | 94 | Money page for `qr code tracking`, `track qr code scans`, and `trackable qr code`; already has schema but should strengthen privacy and placement examples. |
|
||||||
|
| Refresh | `/qr-code-analytics` | 90 | Needs clearer separation from tracking: analytics should own dashboard interpretation, ROI, and performance insights. |
|
||||||
|
| Refresh | `/blog/trackable-qr-codes` | 88 | Existing support article should capture `trackable qr code` and link strongly to `/qr-code-tracking`. |
|
||||||
|
| Refresh | `/blog/utm-parameter-qr-codes` | 86 | Existing support article should capture fan-out intent around GA4, UTM naming, placement comparison, and offline attribution. |
|
||||||
|
| New support page | `/use-cases/qr-codes-for-review-collection` | 86 | Existing backlog item with natural tracking CTA; use as a measurable review funnel page, not a generic Google reviews tool duplicate. |
|
||||||
|
|
||||||
|
## Keyword Intent And Fan-Out
|
||||||
|
|
||||||
|
| URL | Primary keyword | Intent | Fan-out subtopics |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/qr-code-tracking` | `qr code tracking` | Commercial | What can be tracked, static vs dynamic tracking, scan count vs unique scans, device/location/time context, privacy/GDPR, placement comparison, UTM pairing, dashboard workflow, pricing limits. |
|
||||||
|
| `/qr-code-analytics` | `qr code analytics` | Commercial | Analytics dashboard, ROI interpretation, campaign attribution, useful metrics vs vanity metrics, offline-to-online measurement, reporting cadence, route naming, export/share needs. |
|
||||||
|
| `/blog/trackable-qr-codes` | `trackable qr code` | Informational-commercial | Definition, how tracking works, dynamic redirect layer, privacy limits, examples by placement, pros/cons, setup checklist, when not to track. |
|
||||||
|
| `/blog/utm-parameter-qr-codes` | `qr code UTM tracking` | Informational | GA4 source/medium/campaign/content conventions, examples for flyers/events/packaging, common mistakes, naming templates, QR destination testing. |
|
||||||
|
| `/use-cases/qr-codes-for-review-collection` | `QR codes for review collection` | Commercial | Google review link workflow, in-store placement, happy-path routing, feedback triage, scan tracking, QR privacy, dynamic destination updates. |
|
||||||
|
|
||||||
|
## 2026 On-Page And Agentic-Search Rules
|
||||||
|
|
||||||
|
- Titles/H1s should put the target keyword near the start:
|
||||||
|
- `/qr-code-tracking`: `QR Code Tracking: Track QR Code Scans`
|
||||||
|
- `/qr-code-analytics`: `QR Code Analytics: Measure Offline Campaigns`
|
||||||
|
- `/blog/trackable-qr-codes`: `Trackable QR Codes: What You Can Measure`
|
||||||
|
- `/blog/utm-parameter-qr-codes`: `QR Code UTM Tracking: GA4 Setup Guide`
|
||||||
|
- `/use-cases/qr-codes-for-review-collection`: `QR Codes for Review Collection`
|
||||||
|
- Each H2 should start with a direct answer in the first sentence.
|
||||||
|
- Add compact tables for `static vs dynamic`, `tracking vs analytics`, and `UTM examples by placement`.
|
||||||
|
- Add FAQ schema to refreshed informational pages where the existing blog system supports it; preserve SoftwareApplication and HowTo schema on money pages.
|
||||||
|
- Use self-contained answer blocks of roughly 40-60 words for definitions and "can you track..." questions.
|
||||||
|
- Visuals should be meaningful: dashboard screenshot/mockup, placement comparison example, UTM naming table, and review-flow diagram.
|
||||||
|
- Robots/indexing: current `robots.ts` allows major search/AI crawlers and disallows private app/API paths. Keep these pages indexed, sitemap-included, and canonicalized to their final URLs.
|
||||||
|
- Mobile/speed risks: avoid heavy dashboard imagery; use compressed static images and keep tables horizontally readable on mobile.
|
||||||
|
|
||||||
|
## Recommended New Page
|
||||||
|
|
||||||
|
### `/use-cases/qr-codes-for-review-collection`
|
||||||
|
|
||||||
|
Purpose: Create a commercially useful support page for restaurants, cafes, retail, hotels, and service businesses that want measurable review capture.
|
||||||
|
|
||||||
|
Required sections:
|
||||||
|
|
||||||
|
1. Direct answer: a review-collection QR code sends satisfied customers to the right review or feedback flow and lets teams measure which physical prompts get scanned.
|
||||||
|
2. Workflow: in-store sign, receipt, table card, counter card, packaging insert.
|
||||||
|
3. Dynamic vs static: use dynamic if the review platform, routing rule, or offer changes.
|
||||||
|
4. Tracking angle: compare scan volume by placement, store, or campaign.
|
||||||
|
5. Privacy note: describe scan analytics without promising personally identifiable tracking.
|
||||||
|
6. CTA: `Create a trackable review QR code` to `/qr-code-tracking` or `/signup`.
|
||||||
|
7. Internal links: `/tools/google-review-qr-code`, `/qr-code-tracking`, `/dynamic-qr-code-generator`, `/restaurants`, `/use-cases/feedback-qr-codes`.
|
||||||
|
8. Schema: FAQPage + BreadcrumbList; consider HowTo if step-by-step content is included.
|
||||||
|
|
||||||
|
## Recommended Page Refreshes
|
||||||
|
|
||||||
|
### `/qr-code-tracking`
|
||||||
|
|
||||||
|
- Strengthen first-screen answer: "QR code tracking uses a dynamic redirect to record scan time, device context, and approximate location before sending the scanner to the final destination."
|
||||||
|
- Add a privacy-first section explaining hashed/anonymized IP positioning from QRMaster.
|
||||||
|
- Add a table: "What QRMaster tracks / what it does not track."
|
||||||
|
- Add examples for flyer, menu, event booth, packaging, and review collection.
|
||||||
|
- Link to `/qr-code-analytics`, `/blog/utm-parameter-qr-codes`, `/reprint-calculator`, `/pricing`, and the new review-collection page.
|
||||||
|
|
||||||
|
### `/qr-code-analytics`
|
||||||
|
|
||||||
|
- Separate from tracking: tracking collects scan events; analytics helps interpret placement and campaign performance.
|
||||||
|
- Add an "analytics questions" table: which placement worked, when scans peaked, which destination converted, what to reprint.
|
||||||
|
- Add a section on useful metrics vs vanity metrics.
|
||||||
|
- Link back to `/qr-code-tracking`, `/qr-code-for-marketing-campaigns`, `/use-cases/flyer-qr-codes`, `/use-cases/packaging-qr-codes`, and `/pricing`.
|
||||||
|
|
||||||
|
### `/blog/trackable-qr-codes`
|
||||||
|
|
||||||
|
- Refresh title/meta around `trackable qr code`.
|
||||||
|
- Add a 40-60 word definition block near the top.
|
||||||
|
- Update FAQ to include "Can a static QR code be tracked?", "Are trackable QR codes GDPR-friendly?", and "Do trackable QR codes need a redirect?"
|
||||||
|
- Link early to `/qr-code-tracking` with anchor `track QR code scans`.
|
||||||
|
|
||||||
|
### `/blog/utm-parameter-qr-codes`
|
||||||
|
|
||||||
|
- Add a QR-specific UTM template table by placement.
|
||||||
|
- Add GA4 naming convention examples.
|
||||||
|
- Clarify when to use separate QR codes versus one QR with different `utm_content` values.
|
||||||
|
- Link to `/qr-code-analytics` and `/qr-code-tracking`.
|
||||||
|
|
||||||
|
## Internal-Link Plan
|
||||||
|
|
||||||
|
| Source | Destination | Anchor |
|
||||||
|
|---|---|---|
|
||||||
|
| `/dynamic-qr-code-generator` | `/qr-code-tracking` | `track QR code scans` |
|
||||||
|
| `/dynamic-qr-code-generator` | `/qr-code-analytics` | `QR code analytics dashboard` |
|
||||||
|
| `/qr-code-tracking` | `/qr-code-analytics` | `interpret QR scan analytics` |
|
||||||
|
| `/qr-code-tracking` | `/blog/utm-parameter-qr-codes` | `use UTMs with QR codes` |
|
||||||
|
| `/qr-code-analytics` | `/qr-code-tracking` | `collect QR scan data` |
|
||||||
|
| `/qr-code-analytics` | `/qr-code-for-marketing-campaigns` | `measure offline QR campaigns` |
|
||||||
|
| `/blog/trackable-qr-codes` | `/qr-code-tracking` | `QR code tracking` |
|
||||||
|
| `/blog/utm-parameter-qr-codes` | `/qr-code-analytics` | `QR code analytics` |
|
||||||
|
| `/use-cases/feedback-qr-codes` | `/use-cases/qr-codes-for-review-collection` | `review collection QR codes` |
|
||||||
|
| `/tools/google-review-qr-code` | `/use-cases/qr-codes-for-review-collection` | `review collection workflow` |
|
||||||
|
|
||||||
|
## PR Plan
|
||||||
|
|
||||||
|
1. Add the new review-collection use-case content in the existing use-case page data/routing pattern.
|
||||||
|
2. Refresh copy, FAQ, metadata, and link sections on `/qr-code-tracking` and `/qr-code-analytics`.
|
||||||
|
3. Refresh the two support blog entries without creating duplicate `/guide/...` pages.
|
||||||
|
4. Add internal links in both directions from money pages, blog support pages, and relevant use-case/tool pages.
|
||||||
|
5. Update sitemap data only if the new page is not automatically included by the existing use-case sitemap mapping.
|
||||||
|
6. Run the PR SEO review using `docs/automations/qrmaster-pr-seo-review.md`.
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
- One H1 per rendered page.
|
||||||
|
- Primary keyword appears naturally in title, H1, intro, and metadata.
|
||||||
|
- Canonical URL points to the final public URL.
|
||||||
|
- Page is public in middleware and included in sitemap.
|
||||||
|
- No duplicate `/guide/...` URL is introduced without canonical strategy.
|
||||||
|
- FAQ/schema validates where used.
|
||||||
|
- Internal links resolve and use natural anchor text.
|
||||||
|
- Each section begins with a direct answer.
|
||||||
|
- Mobile tables do not overflow unreadably.
|
||||||
|
- Visual assets are compressed and include descriptive alt text.
|
||||||
|
- Robots rules continue to allow target pages and desired search/AI crawlers.
|
||||||
|
- Build/lint pass before PR.
|
||||||
|
|
||||||
|
## Social And Outreach Follow-Up
|
||||||
|
|
||||||
|
- LinkedIn post: "Most QR campaigns fail because teams only count scans. The useful question is which printed placement created action."
|
||||||
|
- X thread: "QR tracking setup in 5 steps: dynamic QR, placement naming, UTM convention, dashboard review, reprint decision."
|
||||||
|
- Short demo video: show flyer A vs flyer B scan comparison and a destination update without reprinting.
|
||||||
|
- Outreach angle for marketing newsletters: "Offline attribution checklist for QR campaigns."
|
||||||
|
- Community answer target: questions around "Can I track a static QR code?" and "How do I track QR codes in GA4?"
|
||||||
211
docs/automations/qrmaster-seo-sprint-machine.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# QRMaster SEO Sprint Machine
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Run a weekly controlled SEO sprint that chooses the right pages, creates or
|
||||||
|
updates them, adds internal links, and ships through a reviewed PR.
|
||||||
|
|
||||||
|
## Why This Exists
|
||||||
|
|
||||||
|
QRMaster should not publish random daily content. The goal is to build
|
||||||
|
commercially useful SEO clusters around dynamic QR codes, tracking, tool pages,
|
||||||
|
comparison pages, and industry workflows.
|
||||||
|
|
||||||
|
## Plugins And Skills
|
||||||
|
|
||||||
|
| Need | Use |
|
||||||
|
|---|---|
|
||||||
|
| Repository changes and PRs | GitHub plugin |
|
||||||
|
| Keyword and performance imports | Coupler, CSV exports, Google Search Console export |
|
||||||
|
| Page creation and refactors | Codex |
|
||||||
|
| SEO/content planning | `content-strategy`, `ai-seo` skills |
|
||||||
|
| Copy generation | `copywriting` skill |
|
||||||
|
| Verification | GitHub Actions, `qa` skill |
|
||||||
|
|
||||||
|
## Weekly Inputs
|
||||||
|
|
||||||
|
- Current keyword backlog:
|
||||||
|
- `marketing/programmatic-seo-top-50.md`
|
||||||
|
- `marketing/keyword-strategy-seo-plan.md`
|
||||||
|
- `seo-keywords.csv`
|
||||||
|
- Existing product positioning:
|
||||||
|
- `.agents/product-marketing-context.md`
|
||||||
|
- Performance data when available:
|
||||||
|
- Google Search Console export
|
||||||
|
- signup/conversion report
|
||||||
|
- top landing pages by traffic
|
||||||
|
- Sprint focus:
|
||||||
|
- Dynamic QR
|
||||||
|
- Tracking/analytics
|
||||||
|
- Restaurant/menu QR
|
||||||
|
- Print marketing
|
||||||
|
- Bulk QR
|
||||||
|
- Comparison/alternatives
|
||||||
|
|
||||||
|
## Scoring Model
|
||||||
|
|
||||||
|
Score each candidate from 0-100:
|
||||||
|
|
||||||
|
| Factor | Weight |
|
||||||
|
|---|---:|
|
||||||
|
| Product fit | 30 |
|
||||||
|
| Commercial intent | 25 |
|
||||||
|
| Differentiation potential | 15 |
|
||||||
|
| Cluster leverage | 10 |
|
||||||
|
| Search winability | 10 |
|
||||||
|
| Production effort | 10 |
|
||||||
|
|
||||||
|
Do not select pages only because they have volume. Prefer pages where QRMaster
|
||||||
|
can naturally sell dynamic QR, scan tracking, bulk creation, or privacy-first
|
||||||
|
analytics.
|
||||||
|
|
||||||
|
## On-Page And Agentic Search Rules
|
||||||
|
|
||||||
|
Every new or refreshed page must follow these checks before review:
|
||||||
|
|
||||||
|
- Identify the primary keyword and intent type: informational, commercial,
|
||||||
|
transactional, or navigational.
|
||||||
|
- Cover the query fan-out: list the related subtopics an AI/search system would
|
||||||
|
need to answer the query well.
|
||||||
|
- Put the primary keyword naturally near the start of the title tag and H1.
|
||||||
|
- Keep title tags under roughly 60 characters when possible.
|
||||||
|
- Keep meta descriptions concise, benefit-led, and naturally keyword-aligned.
|
||||||
|
- Use one clear H1 and a logical H2/H3 hierarchy.
|
||||||
|
- Start each section with a direct answer to the heading.
|
||||||
|
- Use short paragraphs, bullets, and comparison tables where they improve
|
||||||
|
scanning and AI extraction.
|
||||||
|
- Add descriptive internal links with natural anchor text.
|
||||||
|
- Add useful visuals, screenshots, or examples where the page needs them.
|
||||||
|
- Add schema when the page type supports it.
|
||||||
|
- Check mobile readability, CTA tap targets, and obvious speed risks.
|
||||||
|
- Verify robots/indexing assumptions for important SEO pages.
|
||||||
|
|
||||||
|
## Default Weekly Sprint
|
||||||
|
|
||||||
|
1. Select one cluster.
|
||||||
|
2. Create 3 new pages.
|
||||||
|
3. Refresh 2 existing pages with impressions, weak CTR, or position 8-20.
|
||||||
|
4. Add internal links in both directions.
|
||||||
|
5. Create one GitHub PR.
|
||||||
|
6. Run `qrmaster-pr-seo-review.md`.
|
||||||
|
7. Produce social and outreach drafts after the PR is ready.
|
||||||
|
|
||||||
|
## Page Types
|
||||||
|
|
||||||
|
### Tool Pages
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `/tools/pdf-qr-code`
|
||||||
|
- `/tools/vcard-qr-code`
|
||||||
|
- `/tools/wifi-qr-code`
|
||||||
|
- `/tools/menu-qr-code`
|
||||||
|
- `/tools/google-review-qr-code`
|
||||||
|
|
||||||
|
Must include:
|
||||||
|
|
||||||
|
- direct tool-oriented hero
|
||||||
|
- use cases
|
||||||
|
- dynamic vs static guidance
|
||||||
|
- FAQ
|
||||||
|
- CTA into the app
|
||||||
|
- internal links to related use cases
|
||||||
|
|
||||||
|
### Industry Workflow Pages
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `/qr-code-for/restaurants/menu-updates`
|
||||||
|
- `/qr-code-for/events/check-in`
|
||||||
|
- `/qr-code-for/real-estate/open-house-flyers`
|
||||||
|
|
||||||
|
Must include:
|
||||||
|
|
||||||
|
- specific audience pain
|
||||||
|
- example workflow
|
||||||
|
- print-risk or tracking angle
|
||||||
|
- CTA matching the industry
|
||||||
|
|
||||||
|
### Comparison Pages
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `/compare/dynamic-vs-static-qr-codes`
|
||||||
|
- `/compare/free-vs-paid-qr-code-generator`
|
||||||
|
- `/compare/flowcode-alternative`
|
||||||
|
|
||||||
|
Must include:
|
||||||
|
|
||||||
|
- comparison table
|
||||||
|
- fair positioning
|
||||||
|
- current facts verified before publishing
|
||||||
|
- "who this is best for" section
|
||||||
|
- CTA to the best-fit QRMaster feature
|
||||||
|
|
||||||
|
## Codex Sprint Prompt
|
||||||
|
|
||||||
|
```text
|
||||||
|
Run the QRMaster SEO Sprint Machine.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- docs/automations/qrmaster-seo-sprint-machine.md
|
||||||
|
- docs/automations/qrmaster-pr-seo-review.md
|
||||||
|
- .agents/product-marketing-context.md
|
||||||
|
- marketing/programmatic-seo-top-50.md
|
||||||
|
- marketing/keyword-strategy-seo-plan.md
|
||||||
|
|
||||||
|
Sprint focus: [cluster]
|
||||||
|
Target output: 3 new SEO/tool pages, 2 page refreshes, internal links, and one PR-ready diff.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Follow existing code and page patterns.
|
||||||
|
- Do not invent competitor pricing or claims.
|
||||||
|
- Prioritize dynamic QR, edit-after-print, analytics, bulk, and privacy-first messaging.
|
||||||
|
- Add metadata, FAQ/schema where local patterns support it.
|
||||||
|
- Apply the on-page and agentic search rules from the automation doc.
|
||||||
|
- Keep pages specific enough to avoid thin programmatic content.
|
||||||
|
- Run build/lint or explain why not.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
1. selected pages and scoring
|
||||||
|
2. target keyword, intent, and fan-out subtopics per page
|
||||||
|
3. files changed
|
||||||
|
4. internal links added
|
||||||
|
5. PR summary
|
||||||
|
6. SEO review status
|
||||||
|
7. follow-up social/outreach package
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sprint Output Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# QRMaster SEO Sprint: [Cluster]
|
||||||
|
|
||||||
|
## Selected Work
|
||||||
|
| Type | URL | Score | Reason |
|
||||||
|
|---|---|---:|---|
|
||||||
|
|
||||||
|
## Keyword Intent And Fan-Out
|
||||||
|
|
||||||
|
| URL | Primary keyword | Intent | Fan-out subtopics |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
## New Pages
|
||||||
|
|
||||||
|
## Updated Pages
|
||||||
|
|
||||||
|
## Internal Links
|
||||||
|
|
||||||
|
## PR Summary
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
## Social/Outreach Follow-Up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- Each new page has clear commercial intent or cluster leverage.
|
||||||
|
- Refreshed pages have a measurable reason for the update.
|
||||||
|
- Internal links support money pages.
|
||||||
|
- PR SEO Review passes before merge.
|
||||||
548
linear.app/DESIGN.md
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
---
|
||||||
|
version: alpha
|
||||||
|
name: Linear
|
||||||
|
description: "A near-black product-focused marketing canvas built around #010102 (the deepest dark surface of any tool in this collection), light gray text (#f7f8f8), and the signature Linear lavender-blue (#5e6ad2) used as the single chromatic accent. The system reads as software-craft documentation: dense, technical, and quietly luxurious. Display type is set in the Linear custom sans (SF Pro Display fallback) at 500–700 with measured negative tracking. Cards live as charcoal panels (#0f1011) with hairline borders. The accent lavender appears on the brand mark, focus rings, and a few intentional CTAs — never decoratively. Page rhythm leans on product UI screenshots framed in dark panels rather than atmospheric color."
|
||||||
|
|
||||||
|
colors:
|
||||||
|
primary: "#5e6ad2"
|
||||||
|
on-primary: "#ffffff"
|
||||||
|
primary-hover: "#828fff"
|
||||||
|
primary-focus: "#5e69d1"
|
||||||
|
ink: "#f7f8f8"
|
||||||
|
ink-muted: "#d0d6e0"
|
||||||
|
ink-subtle: "#8a8f98"
|
||||||
|
ink-tertiary: "#62666d"
|
||||||
|
canvas: "#010102"
|
||||||
|
surface-1: "#0f1011"
|
||||||
|
surface-2: "#141516"
|
||||||
|
surface-3: "#18191a"
|
||||||
|
surface-4: "#191a1b"
|
||||||
|
hairline: "#23252a"
|
||||||
|
hairline-strong: "#34343a"
|
||||||
|
hairline-tertiary: "#3e3e44"
|
||||||
|
inverse-canvas: "#ffffff"
|
||||||
|
inverse-surface-1: "#f5f6f6"
|
||||||
|
inverse-surface-2: "#f6f7f7"
|
||||||
|
inverse-ink: "#000000"
|
||||||
|
brand-secure: "#7a7fad"
|
||||||
|
semantic-success: "#27a644"
|
||||||
|
semantic-overlay: "#000000"
|
||||||
|
|
||||||
|
typography:
|
||||||
|
display-xl:
|
||||||
|
fontFamily: Linear Display
|
||||||
|
fontSize: 80px
|
||||||
|
fontWeight: 600
|
||||||
|
lineHeight: 1.05
|
||||||
|
letterSpacing: -3.0px
|
||||||
|
display-lg:
|
||||||
|
fontFamily: Linear Display
|
||||||
|
fontSize: 56px
|
||||||
|
fontWeight: 600
|
||||||
|
lineHeight: 1.10
|
||||||
|
letterSpacing: -1.8px
|
||||||
|
display-md:
|
||||||
|
fontFamily: Linear Display
|
||||||
|
fontSize: 40px
|
||||||
|
fontWeight: 600
|
||||||
|
lineHeight: 1.15
|
||||||
|
letterSpacing: -1.0px
|
||||||
|
headline:
|
||||||
|
fontFamily: Linear Display
|
||||||
|
fontSize: 28px
|
||||||
|
fontWeight: 600
|
||||||
|
lineHeight: 1.20
|
||||||
|
letterSpacing: -0.6px
|
||||||
|
card-title:
|
||||||
|
fontFamily: Linear Display
|
||||||
|
fontSize: 22px
|
||||||
|
fontWeight: 500
|
||||||
|
lineHeight: 1.25
|
||||||
|
letterSpacing: -0.4px
|
||||||
|
subhead:
|
||||||
|
fontFamily: Linear Display
|
||||||
|
fontSize: 20px
|
||||||
|
fontWeight: 400
|
||||||
|
lineHeight: 1.40
|
||||||
|
letterSpacing: -0.2px
|
||||||
|
body-lg:
|
||||||
|
fontFamily: Linear Text
|
||||||
|
fontSize: 18px
|
||||||
|
fontWeight: 400
|
||||||
|
lineHeight: 1.50
|
||||||
|
letterSpacing: -0.1px
|
||||||
|
body:
|
||||||
|
fontFamily: Linear Text
|
||||||
|
fontSize: 16px
|
||||||
|
fontWeight: 400
|
||||||
|
lineHeight: 1.50
|
||||||
|
letterSpacing: -0.05px
|
||||||
|
body-sm:
|
||||||
|
fontFamily: Linear Text
|
||||||
|
fontSize: 14px
|
||||||
|
fontWeight: 400
|
||||||
|
lineHeight: 1.50
|
||||||
|
letterSpacing: 0
|
||||||
|
caption:
|
||||||
|
fontFamily: Linear Text
|
||||||
|
fontSize: 12px
|
||||||
|
fontWeight: 400
|
||||||
|
lineHeight: 1.40
|
||||||
|
letterSpacing: 0
|
||||||
|
button:
|
||||||
|
fontFamily: Linear Text
|
||||||
|
fontSize: 14px
|
||||||
|
fontWeight: 500
|
||||||
|
lineHeight: 1.20
|
||||||
|
letterSpacing: 0
|
||||||
|
eyebrow:
|
||||||
|
fontFamily: Linear Text
|
||||||
|
fontSize: 13px
|
||||||
|
fontWeight: 500
|
||||||
|
lineHeight: 1.30
|
||||||
|
letterSpacing: 0.4px
|
||||||
|
mono:
|
||||||
|
fontFamily: Linear Mono
|
||||||
|
fontSize: 13px
|
||||||
|
fontWeight: 400
|
||||||
|
lineHeight: 1.50
|
||||||
|
letterSpacing: 0
|
||||||
|
|
||||||
|
rounded:
|
||||||
|
xs: 4px
|
||||||
|
sm: 6px
|
||||||
|
md: 8px
|
||||||
|
lg: 12px
|
||||||
|
xl: 16px
|
||||||
|
xxl: 24px
|
||||||
|
pill: 9999px
|
||||||
|
full: 9999px
|
||||||
|
|
||||||
|
spacing:
|
||||||
|
xxs: 4px
|
||||||
|
xs: 8px
|
||||||
|
sm: 12px
|
||||||
|
md: 16px
|
||||||
|
lg: 24px
|
||||||
|
xl: 32px
|
||||||
|
xxl: 48px
|
||||||
|
section: 96px
|
||||||
|
|
||||||
|
components:
|
||||||
|
button-primary:
|
||||||
|
backgroundColor: "{colors.primary}"
|
||||||
|
textColor: "{colors.on-primary}"
|
||||||
|
typography: "{typography.button}"
|
||||||
|
rounded: "{rounded.md}"
|
||||||
|
padding: 8px 14px
|
||||||
|
button-primary-pressed:
|
||||||
|
backgroundColor: "{colors.primary-focus}"
|
||||||
|
textColor: "{colors.on-primary}"
|
||||||
|
typography: "{typography.button}"
|
||||||
|
rounded: "{rounded.md}"
|
||||||
|
button-primary-hover:
|
||||||
|
backgroundColor: "{colors.primary-hover}"
|
||||||
|
textColor: "{colors.on-primary}"
|
||||||
|
typography: "{typography.button}"
|
||||||
|
rounded: "{rounded.md}"
|
||||||
|
button-secondary:
|
||||||
|
backgroundColor: "{colors.surface-1}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.button}"
|
||||||
|
rounded: "{rounded.md}"
|
||||||
|
padding: 8px 14px
|
||||||
|
button-tertiary:
|
||||||
|
backgroundColor: "{colors.canvas}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.button}"
|
||||||
|
rounded: "{rounded.md}"
|
||||||
|
padding: 8px 14px
|
||||||
|
button-inverse:
|
||||||
|
backgroundColor: "{colors.inverse-canvas}"
|
||||||
|
textColor: "{colors.inverse-ink}"
|
||||||
|
typography: "{typography.button}"
|
||||||
|
rounded: "{rounded.md}"
|
||||||
|
padding: 8px 14px
|
||||||
|
pricing-card:
|
||||||
|
backgroundColor: "{colors.surface-1}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.body}"
|
||||||
|
rounded: "{rounded.lg}"
|
||||||
|
padding: 24px
|
||||||
|
pricing-card-featured:
|
||||||
|
backgroundColor: "{colors.surface-2}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.body}"
|
||||||
|
rounded: "{rounded.lg}"
|
||||||
|
padding: 24px
|
||||||
|
feature-card:
|
||||||
|
backgroundColor: "{colors.surface-1}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.body}"
|
||||||
|
rounded: "{rounded.lg}"
|
||||||
|
padding: 24px
|
||||||
|
product-screenshot-card:
|
||||||
|
backgroundColor: "{colors.surface-1}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.body}"
|
||||||
|
rounded: "{rounded.xl}"
|
||||||
|
padding: 24px
|
||||||
|
testimonial-card:
|
||||||
|
backgroundColor: "{colors.surface-1}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.body-lg}"
|
||||||
|
rounded: "{rounded.lg}"
|
||||||
|
padding: 32px
|
||||||
|
customer-logo-tile:
|
||||||
|
backgroundColor: "{colors.canvas}"
|
||||||
|
textColor: "{colors.ink-subtle}"
|
||||||
|
typography: "{typography.caption}"
|
||||||
|
rounded: "{rounded.xs}"
|
||||||
|
padding: 16px
|
||||||
|
text-input:
|
||||||
|
backgroundColor: "{colors.surface-1}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.body}"
|
||||||
|
rounded: "{rounded.md}"
|
||||||
|
padding: 8px 12px
|
||||||
|
text-input-focused:
|
||||||
|
backgroundColor: "{colors.surface-1}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.body}"
|
||||||
|
rounded: "{rounded.md}"
|
||||||
|
padding: 8px 12px
|
||||||
|
pricing-tab-default:
|
||||||
|
backgroundColor: "{colors.canvas}"
|
||||||
|
textColor: "{colors.ink-subtle}"
|
||||||
|
typography: "{typography.button}"
|
||||||
|
rounded: "{rounded.pill}"
|
||||||
|
padding: 6px 14px
|
||||||
|
pricing-tab-selected:
|
||||||
|
backgroundColor: "{colors.surface-2}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.button}"
|
||||||
|
rounded: "{rounded.pill}"
|
||||||
|
padding: 6px 14px
|
||||||
|
cta-banner:
|
||||||
|
backgroundColor: "{colors.surface-1}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.headline}"
|
||||||
|
rounded: "{rounded.lg}"
|
||||||
|
padding: 48px
|
||||||
|
changelog-row:
|
||||||
|
backgroundColor: "{colors.canvas}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.body}"
|
||||||
|
rounded: "{rounded.xs}"
|
||||||
|
padding: 24px 0
|
||||||
|
status-badge:
|
||||||
|
backgroundColor: "{colors.surface-2}"
|
||||||
|
textColor: "{colors.ink-muted}"
|
||||||
|
typography: "{typography.caption}"
|
||||||
|
rounded: "{rounded.pill}"
|
||||||
|
padding: 2px 8px
|
||||||
|
top-nav:
|
||||||
|
backgroundColor: "{colors.canvas}"
|
||||||
|
textColor: "{colors.ink}"
|
||||||
|
typography: "{typography.body-sm}"
|
||||||
|
rounded: "{rounded.xs}"
|
||||||
|
height: 56px
|
||||||
|
footer:
|
||||||
|
backgroundColor: "{colors.canvas}"
|
||||||
|
textColor: "{colors.ink-subtle}"
|
||||||
|
typography: "{typography.caption}"
|
||||||
|
rounded: "{rounded.xs}"
|
||||||
|
padding: 64px 32px
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Linear's marketing canvas is the deepest dark surface in this collection — `{colors.canvas}` is #010102, essentially pure black with a faint blue tint. On top sits a four-step surface ladder (`{colors.surface-1}` through `{colors.surface-4}`) for cards, panels, and lifted tiles, with hairline borders running from `{colors.hairline}` (#23252a) up through `{colors.hairline-strong}` and `{colors.hairline-tertiary}`. Light gray text (`{colors.ink}` #f7f8f8) carries the body and headlines.
|
||||||
|
|
||||||
|
The single chromatic accent is **Linear lavender-blue** `{colors.primary}` (#5e6ad2) — used on the brand mark, focus rings, and the primary CTA button. A lighter hover state (`{colors.primary-hover}` #828fff) and a focus-tinted variant (`{colors.primary-focus}` #5e69d1) extend the same hue. Linear avoids saturated greens, oranges, reds, etc. on the marketing canvas — the only semantic color is `{colors.semantic-success}` (#27a644) for status pills and the rare success indicator.
|
||||||
|
|
||||||
|
Display type runs Linear's custom sans (with `SF Pro Display` fallback) at weight 500–700 with negative letter-spacing scaling from -3.0px at 80px down to 0 at body. The body family is Linear's text cut, and a Linear Mono is reserved for code snippets in product screenshots.
|
||||||
|
|
||||||
|
The page rhythm is **dense product screenshots** — Linear's marketing leads with high-fidelity captures of the product UI (issue list, project view, dashboard) framed in `{colors.surface-1}` panels with `{rounded.xl}` 16px corners. The chrome is intentionally minimal so the app screenshots can do the heavy lifting.
|
||||||
|
|
||||||
|
**Key Characteristics:**
|
||||||
|
- **Dark-canvas marketing system** — `{colors.canvas}` (#010102) is the deepest dark in this collection.
|
||||||
|
- **Lavender-blue brand accent** (`{colors.primary}` #5e6ad2) — used scarcely on brand mark, focus, and the primary CTA.
|
||||||
|
- Four-step surface ladder (canvas → surface-1 → surface-2 → surface-3 → surface-4) carries hierarchy without shadow.
|
||||||
|
- Display tracking pulls aggressively negative (-3.0px at 80px); body holds at -0.05px.
|
||||||
|
- Cards use `{rounded.lg}` 12px corners with 1px hairline borders — never pill, rarely 16px.
|
||||||
|
- **Product UI screenshots** dominate the page. The marketing chrome is a dark frame for the app.
|
||||||
|
- No second chromatic color. No atmospheric gradients. No spotlight cards.
|
||||||
|
|
||||||
|
## Colors
|
||||||
|
|
||||||
|
> Source pages: linear.app (home), /intake, /pricing, /contact/sales, /build.
|
||||||
|
|
||||||
|
### Brand & Accent
|
||||||
|
- **Lavender-Blue** ({colors.primary}): The signature Linear accent — primary CTA, brand mark, link emphasis.
|
||||||
|
- **Lavender Hover** ({colors.primary-hover}): Lighter lavender (#828fff) — hovered state of the primary CTA.
|
||||||
|
- **Lavender Focus** ({colors.primary-focus}): Focus-ring tint (#5e69d1) — focused inputs, focused buttons.
|
||||||
|
- **Brand Secure** ({colors.brand-secure}): Muted lavender-gray (#7a7fad) — used in "Linear Security" surfaces.
|
||||||
|
|
||||||
|
### Surface
|
||||||
|
- **Canvas** ({colors.canvas}): Default page background — #010102, near-pure black with a faint blue tint.
|
||||||
|
- **Surface 1** ({colors.surface-1}): One step above canvas — feature cards, pricing cards, product screenshot panels.
|
||||||
|
- **Surface 2** ({colors.surface-2}): Two steps above — featured pricing card, hovered cards.
|
||||||
|
- **Surface 3** ({colors.surface-3}): Three steps above — line-tertiary backgrounds, sub-nav.
|
||||||
|
- **Surface 4** ({colors.surface-4}): Four steps above — bg-level-3, deepest lifted surface.
|
||||||
|
- **Hairline** ({colors.hairline}): 1px borders on cards and dividers.
|
||||||
|
- **Hairline Strong** ({colors.hairline-strong}): Stronger 1px borders — input focus rings.
|
||||||
|
- **Hairline Tertiary** ({colors.hairline-tertiary}): Tertiary borders for nested surfaces.
|
||||||
|
- **Inverse Canvas** ({colors.inverse-canvas}): Pure white — surface of the inverse pill CTA on a small set of section openers.
|
||||||
|
- **Inverse Surface 1** ({colors.inverse-surface-1}): One step above inverse canvas.
|
||||||
|
- **Inverse Surface 2** ({colors.inverse-surface-2}): Two steps above inverse canvas.
|
||||||
|
|
||||||
|
### Text
|
||||||
|
- **Ink** ({colors.ink}): All headlines and emphasized body type — light gray #f7f8f8.
|
||||||
|
- **Ink Muted** ({colors.ink-muted}): Secondary type at #d0d6e0 — meta info on hero panels.
|
||||||
|
- **Ink Subtle** ({colors.ink-subtle}): Tertiary type at #8a8f98 — deselected pricing tabs, footer columns.
|
||||||
|
- **Ink Tertiary** ({colors.ink-tertiary}): Quaternary at #62666d — disabled, footnotes.
|
||||||
|
|
||||||
|
### Semantic
|
||||||
|
- **Success Green** ({colors.semantic-success}): Status pills, success indicators. The only semantic color on marketing.
|
||||||
|
- **Overlay** ({colors.semantic-overlay}): Pure black overlay scrim for modals.
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
### Font Family
|
||||||
|
|
||||||
|
- **Linear Display** — Linear's custom display sans; fallback `SF Pro Display, -apple-system, system-ui, Segoe UI, Roboto`. Carries display-xl through subhead.
|
||||||
|
- **Linear Text** — Linear's custom text sans (a slightly different cut tuned for body sizes); same fallback stack. Carries body sizes, button labels, captions.
|
||||||
|
- **Linear Mono** — Linear's custom mono; fallback `ui-monospace, SF Mono, Menlo`. Used for code snippets in product screenshots and for status / ID tokens.
|
||||||
|
|
||||||
|
The marketing surface treats Display and Text as one continuous voice; the family change is silent.
|
||||||
|
|
||||||
|
### Hierarchy
|
||||||
|
|
||||||
|
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `{typography.display-xl}` | 80px | 600 | 1.05 | -3.0px | Largest hero headline |
|
||||||
|
| `{typography.display-lg}` | 56px | 600 | 1.10 | -1.8px | Section opener headlines |
|
||||||
|
| `{typography.display-md}` | 40px | 600 | 1.15 | -1.0px | Sub-section headlines |
|
||||||
|
| `{typography.headline}` | 28px | 600 | 1.20 | -0.6px | Pricing tier titles, CTA banner heading |
|
||||||
|
| `{typography.card-title}` | 22px | 500 | 1.25 | -0.4px | Feature card title |
|
||||||
|
| `{typography.subhead}` | 20px | 400 | 1.40 | -0.2px | Lead body, intro paragraphs |
|
||||||
|
| `{typography.body-lg}` | 18px | 400 | 1.50 | -0.1px | Hero subhead, lead paragraphs |
|
||||||
|
| `{typography.body}` | 16px | 400 | 1.50 | -0.05px | Default body |
|
||||||
|
| `{typography.body-sm}` | 14px | 400 | 1.50 | 0 | Card body, footer columns |
|
||||||
|
| `{typography.caption}` | 12px | 400 | 1.40 | 0 | Captions, meta, status |
|
||||||
|
| `{typography.button}` | 14px | 500 | 1.20 | 0 | All button labels |
|
||||||
|
| `{typography.eyebrow}` | 13px | 500 | 1.30 | 0.4px | Section eyebrow (slight positive tracking) |
|
||||||
|
| `{typography.mono}` | 13px | 400 | 1.50 | 0 | Linear Mono for code in product screenshots |
|
||||||
|
|
||||||
|
### Principles
|
||||||
|
|
||||||
|
- **Aggressive negative tracking on display** (-3.0px at 80px ≈ 4% of size).
|
||||||
|
- **Single voice from display to body.** Display-xl at 600 → body at 400 — same family, narrower weights.
|
||||||
|
- **Eyebrow uses positive tracking** (+0.4px) — contrast against the negative-tracked display marks the eyebrow as taxonomy.
|
||||||
|
- **Mono only in code contexts.** Linear Mono lives inside product screenshots — not on marketing chrome.
|
||||||
|
|
||||||
|
### Note on Font Substitutes
|
||||||
|
|
||||||
|
Linear's custom typeface isn't publicly distributed; the documented fallback `SF Pro Display, -apple-system, system-ui` is the recommended substitute on macOS. For cross-platform implementation, **Inter** at weight 500 / 600 / 700 is the closest free substitute. **Geist Sans** is also viable. For mono, **JetBrains Mono** or **Geist Mono** at weight 400 closely approximates Linear Mono.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
### Spacing System
|
||||||
|
|
||||||
|
- **Base unit**: 4px.
|
||||||
|
- **Tokens (front matter)**: `{spacing.xxs}` 4px · `{spacing.xs}` 8px · `{spacing.sm}` 12px · `{spacing.md}` 16px · `{spacing.lg}` 24px · `{spacing.xl}` 32px · `{spacing.xxl}` 48px · `{spacing.section}` 96px.
|
||||||
|
- Card interior padding: `{spacing.lg}` 24px on feature/pricing cards; `{spacing.xl}` 32px on testimonial cards; `{spacing.xxl}` 48px on CTA banners.
|
||||||
|
- Pill button padding: 8px vertical · 14px horizontal — Linear's compact button spec.
|
||||||
|
- Form input padding: 8px vertical · 12px horizontal.
|
||||||
|
|
||||||
|
### Grid & Container
|
||||||
|
|
||||||
|
- Max content width sits around 1280px.
|
||||||
|
- Card grids are 3-up at desktop, 2-up at tablet, 1-up at mobile.
|
||||||
|
- Pricing tier grid is 3-up; comparison strip below shows checkmarks per tier.
|
||||||
|
- Product screenshot panels span full content width — they're the protagonist.
|
||||||
|
|
||||||
|
### Whitespace Philosophy
|
||||||
|
|
||||||
|
The dark canvas IS the whitespace. Sections separate by lift onto surface-1 panels, not by gaps in white. Within a panel, generous `{spacing.lg}` 24px gaps between content blocks; `{spacing.section}` 96px between sections.
|
||||||
|
|
||||||
|
## Elevation & Depth
|
||||||
|
|
||||||
|
| Level | Treatment | Use |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 (flat) | No shadow, no border | Default for body type, hero text, footer |
|
||||||
|
| 1 (charcoal lift) | `{colors.surface-1}` background on canvas, 1px `{colors.hairline}` | Default cards, product panels |
|
||||||
|
| 2 (surface-2 lift) | `{colors.surface-2}` background, 1px `{colors.hairline-strong}` | Featured pricing card, hovered cards |
|
||||||
|
| 3 (surface-3 lift) | `{colors.surface-3}` background | Sub-nav, dropdown menus |
|
||||||
|
| 4 (focus ring) | 2px `{colors.primary-focus}` outline at 50% opacity | Focused input, focused button |
|
||||||
|
|
||||||
|
Linear's depth is carried by surface ladder + hairline borders. The brand resists drop shadows on dark almost entirely.
|
||||||
|
|
||||||
|
### Decorative Depth
|
||||||
|
|
||||||
|
- **Product UI screenshots** dominate as decorative depth.
|
||||||
|
- **No atmospheric gradients, no spotlight cards.**
|
||||||
|
- **Subtle white edge highlight** on the top edge of lifted panels — gives the dark surface a faint "pixel rendered" feel.
|
||||||
|
|
||||||
|
## Shapes
|
||||||
|
|
||||||
|
### Border Radius Scale
|
||||||
|
|
||||||
|
| Token | Value | Use |
|
||||||
|
|---|---|---|
|
||||||
|
| `{rounded.xs}` | 4px | Small chips, status badges |
|
||||||
|
| `{rounded.sm}` | 6px | Inline tags |
|
||||||
|
| `{rounded.md}` | 8px | All buttons, form inputs |
|
||||||
|
| `{rounded.lg}` | 12px | Pricing cards, feature cards, testimonial cards |
|
||||||
|
| `{rounded.xl}` | 16px | Product screenshot panels |
|
||||||
|
| `{rounded.xxl}` | 24px | Oversized CTA banners (rare) |
|
||||||
|
| `{rounded.pill}` | 9999px | Pricing tab toggles, status pills |
|
||||||
|
| `{rounded.full}` | 9999px | Avatar circles |
|
||||||
|
|
||||||
|
### Photography & Illustration Geometry
|
||||||
|
|
||||||
|
- Product UI screenshots dominate; they sit in `{rounded.xl}` 16px tiles with `{spacing.lg}` 24px outer padding.
|
||||||
|
- Customer logo tiles render at small sizes (~24px logo height) on `{colors.canvas}` with no border.
|
||||||
|
- Avatar circles in testimonial cards use `{rounded.full}` at 32–40px sizes.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
**`button-primary`** — Lavender CTA. The default primary CTA across all pages.
|
||||||
|
- Background `{colors.primary}`, text `{colors.on-primary}`, type `{typography.button}`, padding 8px 14px, rounded `{rounded.md}`.
|
||||||
|
- Pressed state lives in `button-primary-pressed` (background shifts to `{colors.primary-focus}`).
|
||||||
|
- Hover state lives in `button-primary-hover` (background shifts to `{colors.primary-hover}` lighter lavender).
|
||||||
|
|
||||||
|
**`button-secondary`** — Charcoal button. Used for secondary CTAs ("Sign in", "Read changelog").
|
||||||
|
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.button}`, padding 8px 14px, rounded `{rounded.md}`. 1px `{colors.hairline}` border.
|
||||||
|
|
||||||
|
**`button-tertiary`** — Plain text button.
|
||||||
|
- Background `{colors.canvas}`, text `{colors.ink}`, type `{typography.button}`, rounded `{rounded.md}`, padding 8px 14px.
|
||||||
|
|
||||||
|
**`button-inverse`** — White-on-dark inverse CTA.
|
||||||
|
- Background `{colors.inverse-canvas}`, text `{colors.inverse-ink}`, type `{typography.button}`, rounded `{rounded.md}`, padding 8px 14px.
|
||||||
|
|
||||||
|
### Pricing Tabs
|
||||||
|
|
||||||
|
**`pricing-tab-default`** + **`pricing-tab-selected`** — Pill-toggle on `/pricing`.
|
||||||
|
- Default: `{colors.canvas}` background, `{colors.ink-subtle}` text, rounded `{rounded.pill}`, padding 6px 14px.
|
||||||
|
- Selected: `{colors.surface-2}` background, `{colors.ink}` text — selected = surface lift.
|
||||||
|
|
||||||
|
### Cards & Containers
|
||||||
|
|
||||||
|
**`pricing-card`** — Each tier on `/pricing`.
|
||||||
|
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.body}`, rounded `{rounded.lg}`, padding 24px. 1px `{colors.hairline}` border.
|
||||||
|
|
||||||
|
**`pricing-card-featured`** — Recommended tier — surface lift to surface-2.
|
||||||
|
- Background `{colors.surface-2}`, otherwise identical structure.
|
||||||
|
|
||||||
|
**`feature-card`** — Generic feature highlight tile.
|
||||||
|
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.body}`, rounded `{rounded.lg}`, padding 24px.
|
||||||
|
|
||||||
|
**`product-screenshot-card`** — The dominant card type — frames a high-fidelity Linear app UI screenshot.
|
||||||
|
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.body}`, rounded `{rounded.xl}`, padding 24px.
|
||||||
|
|
||||||
|
**`testimonial-card`** — Customer quote with avatar + name + role.
|
||||||
|
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.body-lg}`, rounded `{rounded.lg}`, padding 32px.
|
||||||
|
|
||||||
|
**`customer-logo-tile`** — Small tile in the customer marquee.
|
||||||
|
- Background `{colors.canvas}`, text `{colors.ink-subtle}`, type `{typography.caption}`, rounded `{rounded.xs}`, padding 16px.
|
||||||
|
|
||||||
|
**`cta-banner`** — Closing CTA panel near page bottom.
|
||||||
|
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.headline}`, rounded `{rounded.lg}`, padding 48px.
|
||||||
|
|
||||||
|
### Inputs & Forms
|
||||||
|
|
||||||
|
**`text-input`** + **`text-input-focused`** — Form fields on `/contact/sales` and signup overlays.
|
||||||
|
- Background `{colors.surface-1}`, text `{colors.ink}`, type `{typography.body}`, rounded `{rounded.md}`, padding 8px 12px.
|
||||||
|
- Focused state retains the same surface; the focus ring is a 2px `{colors.primary-focus}` outline at 50% opacity.
|
||||||
|
|
||||||
|
### Status & Build Page
|
||||||
|
|
||||||
|
**`changelog-row`** — Each row in `/build` (changelog page) listing version, date, and changes.
|
||||||
|
- Background `{colors.canvas}`, text `{colors.ink}`, type `{typography.body}`, rounded `{rounded.xs}`, padding 24px 0. 1px `{colors.hairline}` bottom rule.
|
||||||
|
|
||||||
|
**`status-badge`** — Small status pill.
|
||||||
|
- Background `{colors.surface-2}`, text `{colors.ink-muted}`, type `{typography.caption}`, rounded `{rounded.pill}`, padding 2px 8px.
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
**`top-nav`** — Sticky dark bar with the Linear wordmark left, primary nav links centered, and a `button-secondary` ("Sign in") + `button-primary` ("Get started") pair right.
|
||||||
|
- Background `{colors.canvas}`, text `{colors.ink}`, type `{typography.body-sm}`, height 56px.
|
||||||
|
|
||||||
|
### Footer
|
||||||
|
|
||||||
|
**`footer`** — Dense link grid on `{colors.canvas}` with the Linear wordmark left.
|
||||||
|
- Background `{colors.canvas}`, text `{colors.ink-subtle}`, type `{typography.caption}`, padding 64px 32px.
|
||||||
|
|
||||||
|
## Do's and Don'ts
|
||||||
|
|
||||||
|
### Do
|
||||||
|
|
||||||
|
- Reserve `{colors.canvas}` (#010102) as the system's anchor surface — the faint blue tint is intentional.
|
||||||
|
- Use `{colors.primary}` lavender ONLY for: brand mark, primary CTA, focus ring, link emphasis.
|
||||||
|
- Use the four-step surface ladder for hierarchy. Avoid skipping levels.
|
||||||
|
- Pair display weight 600 with body weight 400 — Linear resists 700+ display weights.
|
||||||
|
- Apply negative letter-spacing aggressively on display.
|
||||||
|
- Use product UI screenshots as the protagonist of every section.
|
||||||
|
- Compose CTAs as `{rounded.md}` 8px corners.
|
||||||
|
|
||||||
|
### Don't
|
||||||
|
|
||||||
|
- Don't ship a light-mode marketing page.
|
||||||
|
- Don't use lavender as a section background or card fill.
|
||||||
|
- Don't introduce a second chromatic accent (orange, pink, green for marketing).
|
||||||
|
- Don't add atmospheric gradients or spotlight cards.
|
||||||
|
- Don't pill-round CTAs.
|
||||||
|
- Don't use `#000000` true black as the canvas.
|
||||||
|
- Don't combine multiple bright accents in product screenshot mockups.
|
||||||
|
|
||||||
|
## Responsive Behavior
|
||||||
|
|
||||||
|
### Breakpoints
|
||||||
|
|
||||||
|
| Name | Width | Key Changes |
|
||||||
|
|---|---|---|
|
||||||
|
| Desktop-XL | 1440px | Default desktop layout |
|
||||||
|
| Desktop | 1280px | Card grid 3-up maintained |
|
||||||
|
| Tablet | 1024px | Card grid 3-up → 2-up |
|
||||||
|
| Mobile-Lg | 768px | Pricing comparison becomes accordion; nav hamburger |
|
||||||
|
| Mobile | 480px | Single-column; display-xl scales 80px → ~36px |
|
||||||
|
|
||||||
|
### Touch Targets
|
||||||
|
|
||||||
|
- CTAs hold ≥40px tap height across viewports.
|
||||||
|
- Pricing tab pills hold ≥36px tap height; touch viewports grow to ≥44px.
|
||||||
|
- Form inputs hold ≥44px tap target on touch.
|
||||||
|
|
||||||
|
### Collapsing Strategy
|
||||||
|
|
||||||
|
- **Top nav**: links collapse to hamburger below 768px.
|
||||||
|
- **Card grids**: 3-up → 2-up at 1024px → 1-up below 768px.
|
||||||
|
- **Pricing comparison**: per-tier accordion below 768px.
|
||||||
|
- **Display type**: `{typography.display-xl}` 80px scales toward `{typography.display-md}` 40px on mobile.
|
||||||
|
|
||||||
|
### Image Behavior
|
||||||
|
|
||||||
|
- Product UI screenshots maintain aspect ratio and never crop.
|
||||||
|
- Customer logos in the marquee may collapse from 6-up to 3-up below 768px.
|
||||||
|
|
||||||
|
## Iteration Guide
|
||||||
|
|
||||||
|
1. Focus on ONE component at a time and reference it by its `components:` token name.
|
||||||
|
2. When introducing a section, decide first which surface lift it lives on.
|
||||||
|
3. Default body to `{typography.body}` at weight 400.
|
||||||
|
4. Run `npx @google/design.md lint DESIGN.md` after edits.
|
||||||
|
5. Add new variants as separate component entries.
|
||||||
|
6. Treat lavender as scarce: brand mark, primary CTA, focus, link emphasis.
|
||||||
|
7. Lead every section with a product UI screenshot.
|
||||||
|
|
||||||
|
## Known Gaps
|
||||||
|
|
||||||
|
- The four-step surface ladder values are extracted directly from Linear's `--color-bg-level-3`, `--color-line-tint`, etc. CSS variables; they are Linear's canonical surface spec.
|
||||||
|
- Form-field error and validation styling is not visible on the inspected pages.
|
||||||
|
- Light mode is not documented because the marketing site does not ship a light theme.
|
||||||
|
- Linear's actual product UI uses a richer color-tag palette (red, orange, yellow, green, blue, purple) for issue priorities and project labels — those colors live in the in-product surfaces shown in mockups.
|
||||||
|
- The custom display, text, and mono families are proprietary; an open-source substitute is acceptable.
|
||||||
BIN
marketing/ChatGPT Image 17. Apr. 2026, 23_41_02.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
marketing/ChatGPT Image 18. Apr. 2026, 16_27_52.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
145
marketing/sales-deck-marketing-manager.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# QR Master Sales Deck
|
||||||
|
|
||||||
|
Audience: Marketing Manager
|
||||||
|
Use case: AE-led first demo / sales presentation
|
||||||
|
Stage: Discovery to first solution presentation
|
||||||
|
Format: 11-slide outline with presentation notes
|
||||||
|
|
||||||
|
## Slide 1: Print Campaigns Should Not Go Dark After They Ship
|
||||||
|
|
||||||
|
Body copy:
|
||||||
|
- Printed flyers, packaging, menus, and posters still drive action
|
||||||
|
- Static QR codes break the moment a link, offer, or landing page changes
|
||||||
|
- Marketing teams lose both flexibility and attribution
|
||||||
|
|
||||||
|
Speaker notes:
|
||||||
|
Lead with the operational reality: print is still valuable, but static QR codes make it fragile. Position the problem as a marketing control issue, not just a design or ops issue.
|
||||||
|
|
||||||
|
## Slide 2: The Cost of Static QR Codes Is Bigger Than Reprints
|
||||||
|
|
||||||
|
Body copy:
|
||||||
|
- Reprints create direct waste every time a destination changes
|
||||||
|
- Campaign teams lose scan-level visibility into offline performance
|
||||||
|
- Manual updates slow launches and create avoidable errors
|
||||||
|
|
||||||
|
Speaker notes:
|
||||||
|
Use the ROI angle here. QR Master already frames this as reprint waste plus lost measurement. If relevant, quantify with the prospect's own print budget and update frequency.
|
||||||
|
|
||||||
|
## Slide 3: Marketing Teams Need Trackable Offline-to-Online Journeys
|
||||||
|
|
||||||
|
Body copy:
|
||||||
|
- Offline campaigns now need the same measurement discipline as digital
|
||||||
|
- Teams want scan, device, and location insights without adding complexity
|
||||||
|
- Privacy expectations are higher, especially in Europe
|
||||||
|
|
||||||
|
Speaker notes:
|
||||||
|
This is the urgency slide. The shift is not "QR codes are new" but "QR codes now need to behave like measurable campaign infrastructure."
|
||||||
|
|
||||||
|
## Slide 4: QR Master Makes Printed Assets Editable, Trackable, and Privacy-First
|
||||||
|
|
||||||
|
Body copy:
|
||||||
|
- Change QR destinations after printing with dynamic QR codes
|
||||||
|
- Track scans with analytics designed for marketing use cases
|
||||||
|
- Stay privacy-conscious with hashed IP handling and no PII-based tracking model
|
||||||
|
|
||||||
|
Speaker notes:
|
||||||
|
Keep this simple. The core promise is control after print, measurable outcomes, and lower compliance anxiety.
|
||||||
|
|
||||||
|
## Slide 5: Launch Campaign QR Codes Fast
|
||||||
|
|
||||||
|
Body copy:
|
||||||
|
- Create dynamic or static QR codes in minutes
|
||||||
|
- Use specialized generators for URL, WiFi, menus, vCards, events, and more
|
||||||
|
- Download ready-to-use assets for print and packaging workflows
|
||||||
|
|
||||||
|
Speaker notes:
|
||||||
|
Show speed to launch. This matters for marketers running many campaigns with changing assets and deadlines.
|
||||||
|
|
||||||
|
## Slide 6: Update Destinations Without Reprinting
|
||||||
|
|
||||||
|
Body copy:
|
||||||
|
- Swap landing pages, PDFs, menus, or promotions after distribution
|
||||||
|
- Keep the same printed QR code live while the destination evolves
|
||||||
|
- Reduce wasted inventory, signage, and packaging runs
|
||||||
|
|
||||||
|
Speaker notes:
|
||||||
|
This is the core economic benefit. Tie it to seasonal campaigns, corrected links, changing offers, and localized landing pages.
|
||||||
|
|
||||||
|
## Slide 7: Measure What Offline Campaigns Actually Drive
|
||||||
|
|
||||||
|
Body copy:
|
||||||
|
- See scan activity, devices, and location patterns
|
||||||
|
- Understand which printed assets and campaigns create engagement
|
||||||
|
- Give marketing a better feedback loop for offline spend
|
||||||
|
|
||||||
|
Speaker notes:
|
||||||
|
Frame analytics as decision support. The point is not dashboards for their own sake; it is knowing what to scale, fix, or stop.
|
||||||
|
|
||||||
|
## Slide 8: Scale Beyond One-Off QR Campaigns
|
||||||
|
|
||||||
|
Body copy:
|
||||||
|
- Business plan supports bulk QR creation up to 1,000 rows per upload
|
||||||
|
- Generate large batches for packaging, retail, events, and distributed campaigns
|
||||||
|
- Move from ad hoc QR creation to repeatable campaign operations
|
||||||
|
|
||||||
|
Speaker notes:
|
||||||
|
Use this slide when the buyer has many SKUs, locations, or campaigns. For smaller teams, keep it brief and treat it as future-proofing.
|
||||||
|
|
||||||
|
## Slide 9: Why Teams Choose QR Master
|
||||||
|
|
||||||
|
Body copy:
|
||||||
|
- Privacy-first approach with hashed IPs and Do Not Track respect
|
||||||
|
- Bulk creation and advanced analytics in one platform
|
||||||
|
- More focused than generic design tools, simpler and more cost-conscious than enterprise-heavy alternatives
|
||||||
|
|
||||||
|
Speaker notes:
|
||||||
|
This is where you position against free tools, Canva-style utilities, and more expensive enterprise QR platforms. Stay outcome-focused rather than feature-dense.
|
||||||
|
|
||||||
|
## Slide 10: Value and Packaging
|
||||||
|
|
||||||
|
Body copy:
|
||||||
|
- Free: 3 active dynamic QR codes and unlimited static QR codes
|
||||||
|
- Pro: EUR 9/month or EUR 90/year for 50 dynamic QR codes, advanced analytics, and branding
|
||||||
|
- Business: EUR 29/month or EUR 290/year for 500 dynamic QR codes, bulk creation, and priority support
|
||||||
|
- Enterprise: custom for larger rollouts
|
||||||
|
|
||||||
|
Speaker notes:
|
||||||
|
Anchor pricing against reprint waste and attribution value, not against free QR generators. For many prospects, one avoided reprint can justify the upgrade.
|
||||||
|
|
||||||
|
## Slide 11: Next Step
|
||||||
|
|
||||||
|
Body copy:
|
||||||
|
- Start with one live campaign, menu, or packaging workflow
|
||||||
|
- Validate savings, scan visibility, and campaign agility
|
||||||
|
- Expand to broader printed assets once the first workflow is proven
|
||||||
|
|
||||||
|
Speaker notes:
|
||||||
|
Push toward a concrete next step: free signup, guided walkthrough, or a pilot tied to one real campaign. Avoid vague closes.
|
||||||
|
|
||||||
|
## Optional Proof Slide: Replace With Customer Evidence
|
||||||
|
|
||||||
|
Use this only when you have real proof.
|
||||||
|
|
||||||
|
Suggested content:
|
||||||
|
- Named customer logo
|
||||||
|
- Before / after workflow
|
||||||
|
- One quantified result
|
||||||
|
- One short buyer quote
|
||||||
|
|
||||||
|
Current status:
|
||||||
|
- Replace composite examples with a real customer story before broad sales use
|
||||||
|
- Good first targets: restaurant groups, agencies, event operators, or retail packaging teams
|
||||||
|
|
||||||
|
## Customization Notes For AEs
|
||||||
|
|
||||||
|
- For restaurant buyers, emphasize menu changes and reprint savings earlier.
|
||||||
|
- For agency buyers, emphasize campaign measurement and client reporting.
|
||||||
|
- For operations or IT stakeholders, elevate privacy posture and workflow control.
|
||||||
|
- If the buyer is price-sensitive, open the reprint calculator before the pricing slide.
|
||||||
|
|
||||||
|
## Missing Proof To Add Later
|
||||||
|
|
||||||
|
- Named customer logos
|
||||||
|
- Verified customer quote
|
||||||
|
- Measured ROI or payback period from a live account
|
||||||
|
- Competitive win story versus Beaconstac, Flowcode, or generic free tools
|
||||||
1742
marketing/tiktok-slideshow-20-pack-prompts.md
Normal file
2150
marketing/tiktok-slideshow-25-pack-prompts.md
Normal file
521
marketing/tiktok-top5-slideshow-pack.md
Normal file
@@ -0,0 +1,521 @@
|
|||||||
|
# TikTok Top 5 Slideshow Pack for QR Master
|
||||||
|
|
||||||
|
This file contains 5 complete `Top 5` TikTok slideshow prompt packs for QR Master.
|
||||||
|
|
||||||
|
Why 7 slides here:
|
||||||
|
- Slide 1 = hook
|
||||||
|
- Slides 2-6 = the five list items
|
||||||
|
- Slide 7 = CTA
|
||||||
|
|
||||||
|
These prompts are aligned to `C:\Users\a931627\Downloads\DESIGN (3).md` and keep the same premium visual direction as the main slideshow file:
|
||||||
|
- bright precision-editorial SaaS style
|
||||||
|
- high white space
|
||||||
|
- tonal depth instead of hard borders
|
||||||
|
- glassmorphism and soft blue gradients
|
||||||
|
- QR code treated as a hero object
|
||||||
|
|
||||||
|
## Shared Global Prompt
|
||||||
|
|
||||||
|
Use this global prompt before any of the packs below:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium TikTok slideshow in vertical 9:16 format with built-in text overlay on every slide.
|
||||||
|
Style: bright, clean, modern, minimal editorial SaaS aesthetic.
|
||||||
|
Creative direction: Precision Editorial, high white space, intentional asymmetry, tonal depth, calm authority, premium digital craftsmanship.
|
||||||
|
Color palette: white, soft warm light gray, cobalt blue accents, subtle blue gradients, soft ambient navy-tinted shadows.
|
||||||
|
Lighting: bright studio daylight, soft glow, polished, airy.
|
||||||
|
Typography: bold modern sans-serif similar to Inter, large headline, very short text, clean spacing, high contrast, strong hierarchy.
|
||||||
|
Layout: editorial composition, asymmetrical but balanced, lots of negative space, QR code treated like a hero object.
|
||||||
|
Text placement: keep the overlay text centered or at minimum positioned within the lower two-thirds of the frame; avoid placing the main text too high near the top edge.
|
||||||
|
UI style: floating frosted panels, surface shifts instead of hard lines, soft glassmorphism, premium SaaS ad direction.
|
||||||
|
Important:
|
||||||
|
- Use exact overlay text provided for each slide.
|
||||||
|
- Keep text short and correctly spelled.
|
||||||
|
- Keep the text centered or clearly inside the lower two-thirds of the image.
|
||||||
|
- No paragraph text.
|
||||||
|
- No clutter.
|
||||||
|
- No dark background.
|
||||||
|
- No generic stock-office look.
|
||||||
|
- No 1px hard borders.
|
||||||
|
- Use soft tonal transitions, ghost borders only if necessary.
|
||||||
|
- Slides 1 and 7 use the avatar.
|
||||||
|
- Slides 1-6 must not show the QR Master brand name or logo.
|
||||||
|
- Slide 7 is the only slide allowed to show QR Master branding.
|
||||||
|
- On slide 1, use the avatar as an unbranded character reference only, with no visible logo text on hat or hoodie.
|
||||||
|
- On slide 7, show the full branded avatar clearly.
|
||||||
|
- Keep every slide polished, premium, and TikTok-ready.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shared Negative Prompt
|
||||||
|
|
||||||
|
```text
|
||||||
|
No dark theme, no messy backgrounds, no harsh shadows, no cheap cartoon look, no toy aesthetic, no cluttered desks, no random props, no heavy borders, no low-detail UI, no gibberish text blocks, no typo-filled posters, no loud neon colors, no generic corporate office scenes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Note
|
||||||
|
|
||||||
|
Because these are listicle formats, do not compare them directly against strong pain hooks in the same wave.
|
||||||
|
|
||||||
|
Use them as a separate educational test block.
|
||||||
|
|
||||||
|
Suggested testing order:
|
||||||
|
1. Top 5 QR code mistakes businesses still make
|
||||||
|
2. Top 5 things to check before printing a QR code
|
||||||
|
3. Top 5 ways dynamic QR codes save money
|
||||||
|
4. Top 5 QR code use cases for restaurants
|
||||||
|
5. Top 5 reasons static QR codes stop working
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Top 5 Slideshow 01 - Educational
|
||||||
|
|
||||||
|
**Hook:** `Top 5 QR code mistakes businesses still make`
|
||||||
|
|
||||||
|
**Title:** `Top 5 QR Mistakes`
|
||||||
|
|
||||||
|
**Caption + Hashtags:** `Most businesses still make the same QR mistakes again and again. Save this checklist before your next print run. qrmaster.net #QRCode #DynamicQRCode #MarketingTips #SmallBusiness #PrintMarketing #QRMaster`
|
||||||
|
|
||||||
|
**Long Description:** `Most businesses still make the same QR code mistakes without even noticing. In this slideshow, I break down 5 common QR mistakes that lead to broken customer journeys, wasted reprints, weak campaign tracking, and messy workflows. If you use QR codes for menus, flyers, packaging, events, or offline marketing, save this and use it as a checklist before your next print run. More smart QR workflows at qrmaster.net #QRCode #DynamicQRCode #MarketingTips #SmallBusiness #PrintMarketing #QRMaster`
|
||||||
|
|
||||||
|
### Slide 1
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium hook slide in vertical 9:16.
|
||||||
|
Scene: bright white editorial background, unbranded avatar holding a phone and pointing toward a floating QR card, soft cobalt glow, clean asymmetrical composition, premium SaaS feel.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"Top 5 QR code mistakes businesses still make"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 2
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: elegant printed menu and flyer with one static QR code shown as a fixed object, bright white background, soft tonal layering, minimal premium styling.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"1. Using static codes for changing content"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 3
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: clean close-up of printed QR collateral beside a phone with an outdated destination, white editorial environment, soft ambient shadow.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"2. Printing before testing the destination"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 4
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: one QR card with no surrounding analytics or performance cues, bright white premium background, minimalist business composition.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"3. Tracking nothing after the scan"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 5
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: multiple mismatched printed QR assets shown across menu, flyer, and package objects, white background, soft blue reflections, editorial spacing.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"4. Using a different code for every update"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 6
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: printed QR assets arranged in a clean product-shot layout without a central management view, bright white environment, polished SaaS marketing style.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"5. Managing QR codes without one dashboard"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 7
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a final branded CTA slide in vertical 9:16.
|
||||||
|
Scene: full QR Master avatar based on the provided image, branded hat and hoodie allowed here, holding phone with QR code, bright premium white background, soft cobalt blue gradient lighting, spacious CTA composition.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"Fix all 5 with QR Master"
|
||||||
|
Add smaller text below:
|
||||||
|
"qrmaster.net"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Top 5 Slideshow 02 - Educational
|
||||||
|
|
||||||
|
**Hook:** `Top 5 things to check before printing a QR code`
|
||||||
|
|
||||||
|
**Title:** `Top 5 Pre-Print Checks`
|
||||||
|
|
||||||
|
**Caption + Hashtags:** `Before you print a QR code, check these five things first. Save this for your next menu, flyer, or campaign. qrmaster.net #QRCode #DynamicQRCode #Checklist #PrintMarketing #MarketingTips #QRMaster`
|
||||||
|
|
||||||
|
**Long Description:** `Before you print any QR code, there are a few things you should always check first. This slideshow covers 5 simple pre-print checks that can save you from broken links, useless scans, missing analytics, and expensive reprints later. If you use QR codes on menus, flyers, product packaging, posters, or event materials, save this checklist now and come back to it before your next campaign goes live. qrmaster.net #QRCode #DynamicQRCode #Checklist #PrintMarketing #MarketingTips #QRMaster`
|
||||||
|
|
||||||
|
### Slide 1
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium hook slide in vertical 9:16.
|
||||||
|
Scene: bright white editorial background, unbranded avatar gesturing toward a floating QR card and subtle check icons, soft blue glow, clean premium composition.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"Top 5 things to check before printing a QR code"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 2
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium checklist slide in vertical 9:16.
|
||||||
|
Scene: one clean QR card displayed like a gallery object on a bright white set, soft ambient shadow, minimal SaaS ad styling.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"1. Will the destination change later?"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 3
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium checklist slide in vertical 9:16.
|
||||||
|
Scene: printed QR collateral beside a mobile screen with clear scan destination, bright white environment, soft blue reflected light.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"2. Did you test the scan on real devices?"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 4
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium checklist slide in vertical 9:16.
|
||||||
|
Scene: central QR code card with subtle analytics cards nearby, white premium background, glassy UI accents, editorial spacing.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"3. Do you need analytics after launch?"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 5
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium checklist slide in vertical 9:16.
|
||||||
|
Scene: one QR code shown across menu, flyer, and packaging objects in a bright white modular arrangement, soft tonal depth.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"4. Can one QR code cover multiple updates?"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 6
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium checklist slide in vertical 9:16.
|
||||||
|
Scene: clean dashboard-like scene with organized QR assets and calm blue glassmorphism panels, bright white premium environment.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"5. Can you manage it all in one place?"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 7
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a final branded CTA slide in vertical 9:16.
|
||||||
|
Scene: branded QR Master avatar holding phone with QR code, bright premium white background, soft cobalt blue halo, spacious CTA layout.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"Use QR Master before you print"
|
||||||
|
Add smaller text below:
|
||||||
|
"qrmaster.net"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Top 5 Slideshow 03 - Educational
|
||||||
|
|
||||||
|
**Hook:** `Top 5 ways dynamic QR codes save money`
|
||||||
|
|
||||||
|
**Title:** `Top 5 Ways QR Saves Money`
|
||||||
|
|
||||||
|
**Caption + Hashtags:** `Dynamic QR codes are not just more flexible. They can also cut avoidable print and campaign costs. qrmaster.net #QRCode #DynamicQRCode #MarketingCosts #BusinessTips #PrintMarketing #QRMaster`
|
||||||
|
|
||||||
|
**Long Description:** `Dynamic QR codes are not only more flexible than static QR codes, they can also save real money. In this slideshow, I break down 5 ways editable and trackable QR workflows reduce waste, avoid unnecessary reprints, improve campaign decisions, and make offline marketing more efficient. If you want better results from menus, flyers, packaging, events, or local campaigns, this is worth saving. qrmaster.net #QRCode #DynamicQRCode #MarketingCosts #BusinessTips #PrintMarketing #QRMaster`
|
||||||
|
|
||||||
|
### Slide 1
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium hook slide in vertical 9:16.
|
||||||
|
Scene: bright white editorial background, unbranded avatar beside a floating QR card with soft cobalt glow and subtle financial dashboard cues, premium SaaS ad style.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"Top 5 ways dynamic QR codes save money"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 2
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: elegant stack of printed collateral shown in a bright white studio setup, soft ambient shadow, clean product-shot composition.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"1. Fewer reprints after small updates"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 3
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: one QR card connected to multiple destination states, bright white background, blue-lit glassmorphism, premium editorial spacing.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"2. One code works across multiple campaigns"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 4
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: elegant analytics cards around a central QR code, white background, soft cobalt glow, refined SaaS dashboard look.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"3. You can cut weak campaigns faster"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 5
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: clean modular arrangement of menu, flyer, package, and event objects around one QR system, bright white environment, subtle tonal depth.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"4. One workflow replaces scattered tools"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 6
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: one elegant QR dashboard environment with organized assets and analytics cues, white premium background, soft blue reflections.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"5. Better scan data means better decisions"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 7
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a final branded CTA slide in vertical 9:16.
|
||||||
|
Scene: full branded QR Master avatar holding phone with QR code, bright premium white background, soft cobalt gradient halo, spacious CTA layout.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"Save more with QR Master"
|
||||||
|
Add smaller text below:
|
||||||
|
"qrmaster.net"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Top 5 Slideshow 04 - Educational
|
||||||
|
|
||||||
|
**Hook:** `Top 5 QR code use cases for restaurants`
|
||||||
|
|
||||||
|
**Title:** `Top 5 Restaurant QR Use Cases`
|
||||||
|
|
||||||
|
**Caption + Hashtags:** `Restaurants can do much more with QR codes than just digital menus. Save this if you run food, hospitality, or local campaigns. qrmaster.net #RestaurantMarketing #QRCode #DynamicQRCode #Hospitality #RestaurantTips #QRMaster`
|
||||||
|
|
||||||
|
**Long Description:** `Most restaurants only think about QR codes as digital menus, but there are far more useful ways to use them. This slideshow breaks down 5 restaurant QR use cases that can improve guest experience, simplify operations, and support marketing at the same time. If you run a restaurant, cafe, takeaway, or hospitality brand, save this and use it as inspiration for smarter QR workflows. qrmaster.net #RestaurantMarketing #QRCode #DynamicQRCode #Hospitality #RestaurantTips #QRMaster`
|
||||||
|
|
||||||
|
### Slide 1
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium niche hook slide in vertical 9:16.
|
||||||
|
Scene: bright white editorial background, unbranded avatar beside a floating menu-style QR card, soft blue glow, calm restaurant-tech SaaS aesthetic.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"Top 5 QR code use cases for restaurants"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 2
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: elegant table menu QR setup on a bright white studio set, soft shadows, clean premium composition.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"1. Live digital menus"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 3
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: refined table card with WiFi-style QR presentation, white editorial background, subtle blue reflections, minimal hospitality-tech look.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"2. Guest WiFi access"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 4
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: elegant feedback or review QR card in a bright white premium environment, soft ambient shadow, editorial spacing.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"3. Reviews and feedback collection"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 5
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: promotional table card and campaign flyer with QR code in a bright modular setup, white background, soft cobalt glow.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"4. Seasonal offers and campaigns"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 6
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: loyalty or contact card concept built around one central QR code, bright white premium background, glassy UI touches, polished SaaS look.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"5. Loyalty and contactless customer journeys"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 7
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a final branded CTA slide in vertical 9:16.
|
||||||
|
Scene: branded QR Master avatar with phone and QR code, bright premium white background, soft cobalt blue halo, strong restaurant-tech campaign composition.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"Restaurants can start with QR Master"
|
||||||
|
Add smaller text below:
|
||||||
|
"qrmaster.net"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Top 5 Slideshow 05 - Educational
|
||||||
|
|
||||||
|
**Hook:** `Top 5 reasons static QR codes stop working`
|
||||||
|
|
||||||
|
**Title:** `Top 5 Reasons Static QR Codes Fail`
|
||||||
|
|
||||||
|
**Caption + Hashtags:** `Static QR codes usually fail because the destination changes, not because the code itself is broken. Save this for your next campaign. qrmaster.net #QRCode #DynamicQRCode #MarketingMistakes #PrintMarketing #BusinessTips #QRMaster`
|
||||||
|
|
||||||
|
**Long Description:** `Static QR codes rarely fail because the code itself stops scanning. They usually fail because the business context changes around them. In this slideshow, I show 5 reasons static QR codes stop working over time, from changed links to expired offers to poor tracking and bad scale management. If you use QR codes in print, offline campaigns, hospitality, retail, or events, save this before your next launch. qrmaster.net #QRCode #DynamicQRCode #MarketingMistakes #PrintMarketing #BusinessTips #QRMaster`
|
||||||
|
|
||||||
|
### Slide 1
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium hook slide in vertical 9:16.
|
||||||
|
Scene: bright white editorial background, unbranded avatar pointing toward a floating QR card and subtle warning UI indicators, soft cobalt glow, premium SaaS ad style.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"Top 5 reasons static QR codes stop working"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 2
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: printed QR card beside a changed mobile destination, bright white editorial environment, clean asymmetrical composition.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"1. The destination link changes"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 3
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: elegant flyer or menu objects shown in a bright white premium set, soft tonal depth, subtle mismatch with a new campaign state.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"2. The offer or content expires"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 4
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: one QR code shown across multiple print assets with no update path, bright white background, soft cobalt reflections, polished editorial look.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"3. One print run outlives one campaign"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 5
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: clean analytics-free QR setup on a bright white SaaS-inspired background, minimalist business composition.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"4. There is no data after the scan"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 6
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a premium list slide in vertical 9:16.
|
||||||
|
Scene: printed materials and QR objects arranged without a central management environment, white premium background, soft ambient shadow.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"5. They are too hard to manage at scale"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slide 7
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create a final branded CTA slide in vertical 9:16.
|
||||||
|
Scene: full branded QR Master avatar holding phone with QR code, bright premium white background, soft cobalt blue gradient lighting, spacious CTA composition.
|
||||||
|
Add large clean overlay text:
|
||||||
|
"Use QR Master instead"
|
||||||
|
Add smaller text below:
|
||||||
|
"qrmaster.net"
|
||||||
|
Text style: bold, modern, minimal, high contrast, clean editorial layout.
|
||||||
|
Text placement: centered or within the lower two-thirds of the frame, never top-heavy.
|
||||||
|
```
|
||||||
701
memory/meta_ads_competitor_analysis.md
Normal file
@@ -0,0 +1,701 @@
|
|||||||
|
# Meta Ads Competitor Analysis Memory
|
||||||
|
|
||||||
|
Stand: 2026-05-27
|
||||||
|
Projekt: qrmaster.net
|
||||||
|
Zweck: Diese Datei speichert wiederverwendbare Learnings aus Meta-Ads-Analysen der wichtigsten QRMaster-Competitors. Bei neuen Ad-Konzepten fuer qrmaster.net diese Datei zuerst beruecksichtigen.
|
||||||
|
|
||||||
|
## Strategischer Gesamtblick
|
||||||
|
|
||||||
|
Die wichtigsten Competitors in Meta Ads positionieren sich nicht nur als QR-Code-Generatoren, sondern als Tools fuer Branding, Tracking, Analytics, Kampagnensteuerung und Revenue-Wachstum.
|
||||||
|
|
||||||
|
QRMaster sollte sich gegen diese Wettbewerber vor allem ueber folgende Winkel differenzieren:
|
||||||
|
|
||||||
|
- Einfachheit: QR-Code in Sekunden erstellen, ohne komplexes Setup.
|
||||||
|
- Schnelligkeit: "Create in 10/30 seconds" als klarer Hook.
|
||||||
|
- Kostenloser Einstieg: "Free", "Try free", "No credit card" prominent nutzen.
|
||||||
|
- SMB-Fokus: kleine Unternehmen, Restaurants, lokale Anbieter, Events, Retail, Hotels.
|
||||||
|
- Tracking ohne Komplexitaet: Scans, Kundeninteresse und Kampagnenleistung einfach sichtbar machen.
|
||||||
|
- Branded QR Codes: keine generischen schwarzen Quadrate, sondern Logo, Farben und Style.
|
||||||
|
- Retargeting/Win-back: Besucher und ehemalige Nutzer gezielt zurueckholen.
|
||||||
|
|
||||||
|
## Top Competitors
|
||||||
|
|
||||||
|
Analysierte Competitors:
|
||||||
|
|
||||||
|
1. Scanova
|
||||||
|
2. QR TIGER
|
||||||
|
3. Bitly
|
||||||
|
4. Uniqode
|
||||||
|
|
||||||
|
## Scanova Learnings
|
||||||
|
|
||||||
|
Scanova nutzt wenige, aber klare Meta Ads mit mehreren Messaging-Winkeln.
|
||||||
|
|
||||||
|
Was funktioniert:
|
||||||
|
|
||||||
|
- Objection Handling: "Still not convinced you need a QR Code?"
|
||||||
|
- Design-Angle: "Tired of boring QR codes?"
|
||||||
|
- Business Outcome: repeat purchases, social shares, brand moments.
|
||||||
|
- Problem/Solution: wenig Platz auf Labels, aber viele Informationen.
|
||||||
|
- Freemium-Risikoabbau: "free to try".
|
||||||
|
|
||||||
|
Beste nachzubauende QRMaster-Ads:
|
||||||
|
|
||||||
|
### Scanova Ad 1: Objection Handling
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Still not convinced you need a QR code?"
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Noch nicht sicher, ob du einen QR-Code brauchst? Fair. Du brauchst nur einen, wenn du mehr Kundeninteraktion, Echtzeit-Tracking, volle Designkontrolle und aenderbare Links nach dem Drucken willst. QRMaster ist kostenlos testbar."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Kostenlos testen" oder "Learn More"
|
||||||
|
|
||||||
|
Warum relevant:
|
||||||
|
Direkte Einwaende werden akzeptiert statt wegargumentiert. Das eignet sich gut fuer Retargeting und warme Zielgruppen.
|
||||||
|
|
||||||
|
### Scanova Ad 2: Design/Emotion
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Tired of boring QR codes?"
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Genug von langweiligen QR-Codes? Mit QRMaster erstellst du schoene, gebrandete QR-Codes mit Logo, Farben und Tracking. Kostenlos starten."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Create Free" oder "Jetzt erstellen"
|
||||||
|
|
||||||
|
Warum relevant:
|
||||||
|
Sehr leicht visuell zu zeigen: generischer QR-Code vs. gebrandeter QRMaster-Code.
|
||||||
|
|
||||||
|
### Scanova Ad 3: Label/Packaging Pain
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Only 3cm of label space left?"
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Nur wenig Platz auf Verpackung, Flyer oder Speisekarte? Ein QRMaster-Code bringt deine ganze Story auf eine kleine Flaeche und zeigt dir, wer scannt."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"QR-Code erstellen"
|
||||||
|
|
||||||
|
Warum relevant:
|
||||||
|
Sehr konkreter Pain Point fuer Retail, Packaging, Gastronomie und Print.
|
||||||
|
|
||||||
|
## QR TIGER Learnings
|
||||||
|
|
||||||
|
QR TIGER nutzt langlebige Kampagnen, Video-Creatives und klare Feature-Angles. Einige Ads laufen seit vielen Monaten, was auf Performance hindeutet.
|
||||||
|
|
||||||
|
Was funktioniert:
|
||||||
|
|
||||||
|
- Konkreter Benefit: "80% more scans" statt vager Claims.
|
||||||
|
- Custom QR Codes mit Branding.
|
||||||
|
- Analytics als Kampagnenoptimierung.
|
||||||
|
- Multi-URL/advanced Features fuer B2B-Marketer.
|
||||||
|
- Zukunfts-/Compliance-Angle wie GS1 QR Codes.
|
||||||
|
- 15-45 Sekunden Videos mit schneller Produktdemo.
|
||||||
|
|
||||||
|
Beste nachzubauende QRMaster-Ads:
|
||||||
|
|
||||||
|
### QR TIGER Ad 1: Custom QR Codes mit Branding
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Get 80% more scans with custom QR codes."
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Erstelle gebrandete QR-Codes in 30 Sekunden. Fuege Logo und Farben hinzu, teile deinen Code und verfolge alle Scans in Echtzeit."
|
||||||
|
|
||||||
|
Video-Struktur:
|
||||||
|
- 0-3s: "QR-Code in 30 Sekunden"
|
||||||
|
- 3-15s: Logo/Farbe/Design zeigen
|
||||||
|
- 15-30s: Scan mit Smartphone
|
||||||
|
- 30-40s: Analytics Dashboard zeigen
|
||||||
|
- 40-45s: CTA
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Kostenlos erstellen" oder "Sign Up"
|
||||||
|
|
||||||
|
### QR TIGER Ad 2: Multi-URL / Smart QR
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"What if your QR code link could change depending on location, language or time?"
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Was, wenn dein QR-Code smarter arbeitet? Leite Nutzer je nach Kampagne, Sprache oder Geraet weiter und sieh alle Scans in einem Dashboard."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Mehr erfahren"
|
||||||
|
|
||||||
|
Nutzen:
|
||||||
|
Gut fuer B2B/Marketer und fortgeschrittenere Use Cases.
|
||||||
|
|
||||||
|
### QR TIGER Ad 3: Future/Urgency
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"By 2027, barcodes will be replaced with GS1-powered QR codes."
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"QR-Codes werden zum Standard fuer moderne Verpackungen, Menues und Kampagnen. Starte jetzt mit QRMaster, bevor deine Konkurrenz schneller ist."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Jetzt starten"
|
||||||
|
|
||||||
|
Hinweis:
|
||||||
|
Nur nutzen, wenn die Aussage sachlich korrekt und fuer die Zielgruppe passend formuliert ist.
|
||||||
|
|
||||||
|
## Bitly Learnings
|
||||||
|
|
||||||
|
Bitly wirbt sehr aggressiv mit hohem Anzeigenvolumen. Der Kern liegt auf Win-back, Analytics, AI und All-in-One-Plattform.
|
||||||
|
|
||||||
|
Was funktioniert:
|
||||||
|
|
||||||
|
- Win-back: "Still thinking about Bitly?"
|
||||||
|
- Daten/Analytics: "Turn every link into real insights."
|
||||||
|
- AI-Angle: "Ask Bitly anything."
|
||||||
|
- All-in-One: Links, QR Codes und Landing Pages in einer Plattform.
|
||||||
|
- Viele Varianten desselben Messaging.
|
||||||
|
- Kurze Videos und statische Ads.
|
||||||
|
|
||||||
|
Beste nachzubauende QRMaster-Ads:
|
||||||
|
|
||||||
|
### Bitly Ad 1: Win-back / Retargeting
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Still interested in QRMaster?"
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Noch interessiert an QRMaster? Deine kostenlosen QR-Codes und Tracking-Daten warten auf dich. Steig wieder ein und erstelle deinen naechsten Code in Sekunden."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"QR-Code kostenlos erstellen"
|
||||||
|
|
||||||
|
Zielgruppe:
|
||||||
|
Website-Besucher, abgebrochene Registrierungen, inaktive Nutzer.
|
||||||
|
|
||||||
|
### Bitly Ad 2: Feature-Focused Analytics
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Turn every QR code into a powerful marketing tool."
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Mach aus jedem QR-Code ein messbares Marketing-Tool. Tracke Scans, verstehe deine Kunden und optimiere deine Kampagnen mit QRMaster."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Tracking kostenlos starten"
|
||||||
|
|
||||||
|
### Bitly Ad 3: All-in-One Platform
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Everything you need to create, track, and optimize QR codes."
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Alles, was du fuer QR-Codes brauchst: erstellen, branden, teilen und auswerten. Eine einfache Plattform. Kostenlos starten."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"QRMaster entdecken"
|
||||||
|
|
||||||
|
### Bitly Ad 4: Simple Free Offer
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Generate free QR codes in seconds. No credit card."
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Kostenlose QR-Codes in Sekunden erstellen. Keine Kreditkarte. Kein kompliziertes Setup. Einfach Link einfuegen und starten."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Create Free"
|
||||||
|
|
||||||
|
## Uniqode Learnings
|
||||||
|
|
||||||
|
Uniqode setzt stark auf Enterprise, Verticals, Branding, Dynamic QR Codes, Daten und Thought Leadership. Sehr viele Ads sind branchenspezifisch.
|
||||||
|
|
||||||
|
Was funktioniert:
|
||||||
|
|
||||||
|
- Enterprise-Angle: zentrale Kontrolle, Bulk Creation, Team Permissions, Analytics.
|
||||||
|
- Hospitality/Restaurant-Angle: Gaeste, Menues, Loyalitaet, Wiederbesuche.
|
||||||
|
- Branding: "Your QR code should look like you."
|
||||||
|
- Data Capture: E-Mail/SMS/CRM und Customer Data Ownership.
|
||||||
|
- Quick-Win: schneller QR-Code ohne Design-Skills.
|
||||||
|
- Industry Verticals: Hotels, Restaurants, Retail, Events, CPG.
|
||||||
|
- Social Proof: viele Unternehmen, Scans, Reports.
|
||||||
|
|
||||||
|
Beste nachzubauende QRMaster-Ads:
|
||||||
|
|
||||||
|
### Uniqode Ad 1: Multi-Location / Enterprise Simple
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Multi-location QR campaigns made simple."
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Verwalte QR-Codes nicht mehr manuell ueber Standorte, Teams und Kampagnen hinweg. QRMaster bietet zentrale Kontrolle, Bulk-Erstellung, Teamzugriff und Echtzeit-Analytics in einem Dashboard."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Free Trial starten"
|
||||||
|
|
||||||
|
### Uniqode Ad 2: Restaurant/Hospitality
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Turn your menu QR into revenue."
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Jeder Gast scannt deinen Menue-QR-Code. Aber misst du auch, was danach passiert? QRMaster zeigt dir Scans, Interessen und Wiederbesuche, damit aus Menues echte Kundenbindung wird."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Demo ansehen"
|
||||||
|
|
||||||
|
### Uniqode Ad 3: Branded QR Codes
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Your QR Code, Your Brand."
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Keine generischen Schwarz-Weiss-Codes. Mit QRMaster erstellst du gebrandete QR-Codes mit Logo, Farben und Style, denen Kunden vertrauen."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Branded QR kostenlos erstellen"
|
||||||
|
|
||||||
|
### Uniqode Ad 4: Data Capture
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Your QR codes should capture data."
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Jeder Scan ist ein Signal. QRMaster hilft dir, Scans, Interesse und Kampagnenleistung zu messen, damit du bessere Marketingentscheidungen triffst."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Mehr erfahren"
|
||||||
|
|
||||||
|
### Uniqode Ad 5: Quick-Win
|
||||||
|
|
||||||
|
Hook:
|
||||||
|
"Create a QR code in 10 seconds."
|
||||||
|
|
||||||
|
QRMaster-Version:
|
||||||
|
"Keine Design-Skills. Kein kompliziertes Setup. Link einfuegen, Farbe waehlen, QR-Code generieren. Fertig."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Jetzt erstellen"
|
||||||
|
|
||||||
|
## Best Overall Ad Concepts For QRMaster
|
||||||
|
|
||||||
|
Diese Konzepte sollten priorisiert werden, wenn neue Meta Ads fuer qrmaster.net erstellt werden:
|
||||||
|
|
||||||
|
### 1. Speed + Free
|
||||||
|
|
||||||
|
Headline:
|
||||||
|
"Kostenlose QR-Codes in 10 Sekunden erstellen"
|
||||||
|
|
||||||
|
Body:
|
||||||
|
"Link einfuegen, Design waehlen, QR-Code downloaden. Mit QRMaster geht es schnell, einfach und ohne Kreditkarte."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Kostenlos erstellen"
|
||||||
|
|
||||||
|
### 2. Branded QR Code
|
||||||
|
|
||||||
|
Headline:
|
||||||
|
"Dein QR-Code sollte zu deiner Marke passen"
|
||||||
|
|
||||||
|
Body:
|
||||||
|
"Erstelle QR-Codes mit Logo, Farben und Style. Keine generischen Codes, sondern professionelle QR-Codes fuer Flyer, Verpackungen, Menues und Kampagnen."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Branded QR erstellen"
|
||||||
|
|
||||||
|
### 3. Tracking / Analytics
|
||||||
|
|
||||||
|
Headline:
|
||||||
|
"Tracke jeden QR-Code-Scan"
|
||||||
|
|
||||||
|
Body:
|
||||||
|
"Sieh, wann und wie oft deine QR-Codes gescannt werden. QRMaster macht aus QR-Codes messbare Marketingkanaele."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Tracking starten"
|
||||||
|
|
||||||
|
### 4. Restaurant / Menu QR
|
||||||
|
|
||||||
|
Headline:
|
||||||
|
"Mach dein Menue-QR messbar"
|
||||||
|
|
||||||
|
Body:
|
||||||
|
"Gaeste scannen sowieso. Mit QRMaster siehst du, was funktioniert, welche Kampagnen laufen und wie du Wiederbesuche steigerst."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Restaurant-Demo ansehen"
|
||||||
|
|
||||||
|
### 5. Retargeting / Win-back
|
||||||
|
|
||||||
|
Headline:
|
||||||
|
"Noch an QRMaster interessiert?"
|
||||||
|
|
||||||
|
Body:
|
||||||
|
"Dein naechster QR-Code ist nur ein paar Sekunden entfernt. Kostenlos erstellen, branden und tracken."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"Weiter machen"
|
||||||
|
|
||||||
|
### 6. Packaging / Print Pain
|
||||||
|
|
||||||
|
Headline:
|
||||||
|
"Wenig Platz. Viel zu sagen."
|
||||||
|
|
||||||
|
Body:
|
||||||
|
"Ein QRMaster-Code bringt Produktinfos, Anleitungen, Angebote und Tracking auf Verpackungen, Flyer und Etiketten."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"QR-Code erstellen"
|
||||||
|
|
||||||
|
### 7. All-in-One Simple Platform
|
||||||
|
|
||||||
|
Headline:
|
||||||
|
"QR-Codes erstellen, branden und messen"
|
||||||
|
|
||||||
|
Body:
|
||||||
|
"Alles in einer einfachen Plattform. Keine komplexe Enterprise-Software. QRMaster ist gemacht fuer schnelle Kampagnen und klare Ergebnisse."
|
||||||
|
|
||||||
|
CTA:
|
||||||
|
"QRMaster testen"
|
||||||
|
|
||||||
|
## Recommended Campaign Structure
|
||||||
|
|
||||||
|
### Cold Traffic
|
||||||
|
|
||||||
|
Ziel: neue Nutzer gewinnen.
|
||||||
|
|
||||||
|
Ads:
|
||||||
|
- Speed + Free
|
||||||
|
- Branded QR Code
|
||||||
|
- Packaging / Print Pain
|
||||||
|
- Restaurant / Menu QR
|
||||||
|
|
||||||
|
### Warm Traffic / Retargeting
|
||||||
|
|
||||||
|
Ziel: Besucher, Abbrecher und ehemalige Nutzer zurueckholen.
|
||||||
|
|
||||||
|
Ads:
|
||||||
|
- "Noch an QRMaster interessiert?"
|
||||||
|
- Objection Handling
|
||||||
|
- Tracking/Analytics
|
||||||
|
- Free Trial / No Credit Card
|
||||||
|
|
||||||
|
### B2B / Higher Intent
|
||||||
|
|
||||||
|
Ziel: Marketer, Agenturen, Restaurants, Hotels, Retail, Event-Veranstalter.
|
||||||
|
|
||||||
|
Ads:
|
||||||
|
- Multi-location simple
|
||||||
|
- Data capture / CRM
|
||||||
|
- Industry-specific creatives
|
||||||
|
- Branded QR codes at scale
|
||||||
|
|
||||||
|
## Performance Signals From Impression Sorting
|
||||||
|
|
||||||
|
Stand: 2026-05-27
|
||||||
|
|
||||||
|
Wichtig: Die Meta Ads Library zeigt keine echten Performance-Metriken wie CTR, CPC, CPA, Conversion Rate, ROAS oder Spend. Wenn die Library aber nach "Impressionen absteigend" sortiert ist, sind die oberen Anzeigen die Ads mit den meisten ausgelieferten Impressionen. Das ist kein direkter ROI-Beweis, aber ein starkes Signal fuer:
|
||||||
|
|
||||||
|
- hoehere Budget-Allokation
|
||||||
|
- laengere Laufzeit
|
||||||
|
- validiertes Messaging
|
||||||
|
- bessere Skalierbarkeit
|
||||||
|
- Gewinner-Creatives oder Gewinner-Audiences
|
||||||
|
|
||||||
|
Diese Rankings deshalb als Performance-Indizien verwenden, nicht als harte Metriken.
|
||||||
|
|
||||||
|
### Scanova Performance Ranking
|
||||||
|
|
||||||
|
Sortierung: Impressionen absteigend, ca. 5 aktive Ads.
|
||||||
|
|
||||||
|
1. Objection Handling: "Still not convinced you need a QR code?"
|
||||||
|
- ID: 4437071703279023
|
||||||
|
- Aktiv seit: 2026-04-17
|
||||||
|
- Signal: hoechste Impressionen und laengste Laufzeit.
|
||||||
|
- Interpretation: klarer Winner bei Scanova.
|
||||||
|
- QRMaster-Learning: als erste Retargeting/Prospecting-Ad testen.
|
||||||
|
|
||||||
|
2. Design Emotion: "Tired of boring QR codes?"
|
||||||
|
- ID: 2768191646907291
|
||||||
|
- Aktiv seit: 2026-04-29
|
||||||
|
- Signal: zweithoechste Impressionen, solide Laufzeit.
|
||||||
|
- Interpretation: starker Secondary-Winner.
|
||||||
|
- QRMaster-Learning: generischer QR-Code vs. branded QR-Code visuell zeigen.
|
||||||
|
|
||||||
|
3. B2B Outcome / Brands
|
||||||
|
- ID: 1504294601330713
|
||||||
|
- Aktiv seit: 2026-05-26
|
||||||
|
- Signal: neuer Test, weniger Impressionen wegen kurzer Laufzeit.
|
||||||
|
- QRMaster-Learning: spaeter testen, besonders fuer Packaging/Ecommerce.
|
||||||
|
|
||||||
|
4. Feature Listing: "20+ QR Codes. Endless uses."
|
||||||
|
- ID: 1717343306059358
|
||||||
|
- Aktiv seit: 2026-05-26
|
||||||
|
- Signal: neuer Test.
|
||||||
|
- QRMaster-Learning: Feature-Listen nur nutzen, wenn klarer Outcome davorsteht.
|
||||||
|
|
||||||
|
5. Space/Label Pain: "Your product label has 3cm left..."
|
||||||
|
- ID: 1707202200450970
|
||||||
|
- Aktiv seit: 2026-05-26
|
||||||
|
- Signal: neuer Test.
|
||||||
|
- QRMaster-Learning: guter Angle fuer Print/Packaging, aber erst nach den breiteren Winners testen.
|
||||||
|
|
||||||
|
Prioritaet fuer QRMaster:
|
||||||
|
1. Objection Handling
|
||||||
|
2. Design/Branding
|
||||||
|
3. Packaging/Label Pain als Nischen-Test
|
||||||
|
|
||||||
|
### QR TIGER Performance Ranking
|
||||||
|
|
||||||
|
Sortierung/Indizien: lange Laufzeit und aktive Varianten.
|
||||||
|
|
||||||
|
1. Custom QR Codes mit Branding
|
||||||
|
- ID: 1258668248847659
|
||||||
|
- Aktiv seit: 2024-08-18
|
||||||
|
- Video: ca. 45 Sekunden plus kuerzere Varianten.
|
||||||
|
- Message: custom/branded QR codes, "80% more scans", Tracking, A/B tests.
|
||||||
|
- Signal: extrem lange Laufzeit, mehrere Varianten, weiterhin aktiv.
|
||||||
|
- Interpretation: sehr wahrscheinlich QR TIGERs Haupt-Winner.
|
||||||
|
- QRMaster-Learning: "Erstelle gebrandete QR-Codes in 30 Sekunden" als Kern-Video bauen.
|
||||||
|
|
||||||
|
2. Multi URL QR Codes
|
||||||
|
- ID: 1612914899327017
|
||||||
|
- Aktiv seit: 2024-11-13
|
||||||
|
- Message: Link kann je nach Location, Sprache, Zeit oder Scananzahl wechseln.
|
||||||
|
- Signal: lange Laufzeit, Soft-CTA "Learn More/Mehr dazu".
|
||||||
|
- Interpretation: solider Advanced-Use-Case-Performer.
|
||||||
|
- QRMaster-Learning: fuer warme B2B/Marketer-Zielgruppen nutzen, nicht als Massen-Hook.
|
||||||
|
|
||||||
|
3. GS1 QR Codes / Future Compliance
|
||||||
|
- ID: 912391637706042
|
||||||
|
- Aktiv seit: 2024-12-18, spaeter erneuert.
|
||||||
|
- Message: 2027/GS1/barcode replacement, first-mover urgency.
|
||||||
|
- Signal: separate Landing Page bzw. dedizierte Domain, B2B-Fokus.
|
||||||
|
- Interpretation: niedrigeres Volumen, aber potentiell hoehere Lead-Qualitaet.
|
||||||
|
- QRMaster-Learning: nur nutzen, wenn fachlich korrekt und mit passender Landing Page.
|
||||||
|
|
||||||
|
Prioritaet fuer QRMaster:
|
||||||
|
1. Branded QR Code Speed Demo
|
||||||
|
2. Smart/Dynamic QR fuer Retargeting
|
||||||
|
3. Future/Trend-Angle nur vorsichtig und belegbar
|
||||||
|
|
||||||
|
### Bitly Performance Ranking
|
||||||
|
|
||||||
|
Sortierung: Impressionen absteigend, ca. 200 aktive Ads.
|
||||||
|
|
||||||
|
1. Win-back: "Still thinking about Bitly?"
|
||||||
|
- Beispiel-IDs: 845577364641009, 947060991537298, 1314328070805227, 2379444135888254, 975084468313052, 1626897478589888, 1550285439986462
|
||||||
|
- Aktiv seit: mehrere Varianten ab 2026-04-28 bis 2026-05-14.
|
||||||
|
- Message: "Your link data and analytics are waiting for you, jump back in."
|
||||||
|
- Signal: dominiert Top-Positionen nach Impressionen, viele Varianten, Video- und Static-Tests.
|
||||||
|
- Interpretation: Bitlys staerkster Meta-Ads-Ansatz ist Win-back/Retargeting.
|
||||||
|
- QRMaster-Learning: Nutzer sammeln, Pixel/Retargeting nutzen, "Deine QR-Codes warten" testen.
|
||||||
|
|
||||||
|
2. All-in-One Platform: "Every connection matters"
|
||||||
|
- Beispiel-ID: 1535165028202688
|
||||||
|
- Aktiv seit: 2026-05-15
|
||||||
|
- Message: Links, QR codes and landing pages all in one place.
|
||||||
|
- Signal: hohe Position trotz neuerer Laufzeit.
|
||||||
|
- Interpretation: starker Brand-/Platform-Winkel.
|
||||||
|
- QRMaster-Learning: "Alles fuer QR-Codes: erstellen, branden, tracken" als einfache QRMaster-Version.
|
||||||
|
|
||||||
|
3. Feature/Analytics: "Turn every link into real insights"
|
||||||
|
- Beispiel-ID: 1314712214127043
|
||||||
|
- Aktiv seit: 2026-05-22
|
||||||
|
- Message: understand clicks and conversions.
|
||||||
|
- Signal: aktiv, aber unterhalb der Win-back-Winner.
|
||||||
|
- Interpretation: funktioniert, aber wahrscheinlich schwächer als Retargeting.
|
||||||
|
- QRMaster-Learning: "Mach aus jedem QR-Code messbare Marketingdaten."
|
||||||
|
|
||||||
|
4. AI Feature: "Ask Bitly anything"
|
||||||
|
- Beispiel-ID: 2448698088981471
|
||||||
|
- Aktiv seit: 2026-05-14
|
||||||
|
- Message: AI assistant fuer Link- und QR-Code-Performance.
|
||||||
|
- Signal: mehrere Varianten, aber vermutlich nischiger.
|
||||||
|
- Interpretation: Premium-/Feature-Test, nicht Hauptvolumen.
|
||||||
|
- QRMaster-Learning: AI nur nutzen, wenn echtes Feature existiert und nicht als leerer Buzzword-Hook.
|
||||||
|
|
||||||
|
Prioritaet fuer QRMaster:
|
||||||
|
1. Win-back/Retargeting
|
||||||
|
2. All-in-One QR Platform
|
||||||
|
3. Tracking/Analytics
|
||||||
|
4. AI nur bei echter Produktbasis
|
||||||
|
|
||||||
|
### Uniqode Performance Ranking
|
||||||
|
|
||||||
|
Sortierung: Impressionen absteigend, ca. 170 aktive Ads.
|
||||||
|
|
||||||
|
Top-Performer nach Impressionen:
|
||||||
|
|
||||||
|
1. Hospitality Self-Service: "Guests find what they need in seconds"
|
||||||
|
- Beispiel-ID: 2220200865452960
|
||||||
|
- Aktiv seit: 2026-03-31
|
||||||
|
- Headline: "Routine questions handled. Staff freed for what matters"
|
||||||
|
- Message: QR handles basics, staff focuses on service.
|
||||||
|
- Signal: Platz 1, lange Laufzeit, mehrere Varianten.
|
||||||
|
- QRMaster-Learning: fuer Hotels/Restaurants: QR spart Personalzeit und verbessert Service.
|
||||||
|
|
||||||
|
2. Hospitality Self-Service Variant
|
||||||
|
- Beispiel-ID: 2060386458694164
|
||||||
|
- Aktiv seit: 2026-04-11
|
||||||
|
- Signal: gleiche Message, 3 Varianten.
|
||||||
|
- QRMaster-Learning: Gewinner-Message mehrfach kreativ variieren.
|
||||||
|
|
||||||
|
3. Linkpages / Branded Hub
|
||||||
|
- Beispiel-ID: 1826847247985265
|
||||||
|
- Aktiv seit: 2026-05-04
|
||||||
|
- Message: one scan opens a branded page with loyalty, offers, social follow.
|
||||||
|
- Signal: hohe Impressionen trotz neuerer Laufzeit.
|
||||||
|
- QRMaster-Learning: QR nicht nur als Link, sondern als Mini-Hub/Angebotsseite positionieren.
|
||||||
|
|
||||||
|
4. Hotel Loyalty / Opt-ins
|
||||||
|
- Beispiel-ID: 1933435043940258
|
||||||
|
- Aktiv seit: 2026-03-31
|
||||||
|
- Headline: "QR built to drive repeat stays"
|
||||||
|
- Message: consented opt-ins waehrend Aufenthalt, spaetere Rebooking-Angebote.
|
||||||
|
- QRMaster-Learning: Consent, Opt-in und Wiederbesuch als Hotel-Angle nutzen.
|
||||||
|
|
||||||
|
5. Retail Thought Leadership
|
||||||
|
- Beispiel-ID: 3937841396509697
|
||||||
|
- Aktiv seit: 2026-03-31
|
||||||
|
- Headline: "Inspire Your Strategy these 14 QR Codes"
|
||||||
|
- Message: 14 retail brands use QR codes for loyalty, launches, offers, in-store experiences.
|
||||||
|
- QRMaster-Learning: "X Ideen fuer QR-Codes in Retail/Restaurant/Event" als Lead Magnet testen.
|
||||||
|
|
||||||
|
6. Hotel Revenue/Upsell
|
||||||
|
- Beispiel-ID: 2005806333649282
|
||||||
|
- Aktiv seit: 2026-03-31
|
||||||
|
- Video: ca. 24 Sekunden.
|
||||||
|
- Headline: "Boost Revenue with QR Upsells"
|
||||||
|
- Message: upsell without feeling pushy.
|
||||||
|
- QRMaster-Learning: Revenue + nicht aufdringlich ist ein starker Hospitality-Hook.
|
||||||
|
|
||||||
|
7. Hotel Journey
|
||||||
|
- Beispiel-ID: 1866303194031380
|
||||||
|
- Aktiv seit: 2026-03-31
|
||||||
|
- Headline: "How Hotels Are Using QR Codes to Win Guests"
|
||||||
|
- Message: from check-in to loyalty programs.
|
||||||
|
- QRMaster-Learning: Full customer journey statt Einzel-QR-Code zeigen.
|
||||||
|
|
||||||
|
8. Retail Social Proof Duplicate
|
||||||
|
- Beispiel-ID: 2175516249652376
|
||||||
|
- Aktiv seit: 2026-03-31
|
||||||
|
- Signal: gleiche Message wie Retail-Thought-Leadership, weiterer Beleg fuer Winner.
|
||||||
|
|
||||||
|
9. Report / Thought Leadership
|
||||||
|
- Beispiel-ID: 1896269207741481
|
||||||
|
- Aktiv seit: 2026-04-24
|
||||||
|
- Headline: "Turn QR scans into measurable revenue"
|
||||||
|
- Message: 2026 State of QR Codes report, benchmarks, tactics.
|
||||||
|
- QRMaster-Learning: Lead Magnet mit Benchmarks/Use Cases kann Meta-Ads tragen.
|
||||||
|
|
||||||
|
10. Restaurant Revenue / QR Menus
|
||||||
|
- Beispiel-ID: 1504621434396227
|
||||||
|
- Aktiv seit: 2026-03-31
|
||||||
|
- Video: ca. 24 Sekunden.
|
||||||
|
- Headline: "Boost Revenue with QR Upsells"
|
||||||
|
- Message: restaurants upsell without feeling pushy.
|
||||||
|
- QRMaster-Learning: Menue-QR nicht als Hygiene-Feature, sondern als Umsatzhebel positionieren.
|
||||||
|
|
||||||
|
11. Case Study: Spinrite Brand Experience
|
||||||
|
- Beispiel-ID: 25949066888125449
|
||||||
|
- Aktiv seit: 2026-03-31
|
||||||
|
- Message: offline creativity with online inspiration.
|
||||||
|
- QRMaster-Learning: echte Fallstudien/Use Cases sind starke Ads.
|
||||||
|
|
||||||
|
12. Case Study: Spinrite Insights
|
||||||
|
- Beispiel-ID: 1871281913579414
|
||||||
|
- Aktiv seit: 2026-03-31
|
||||||
|
- Message: "Most brands stop at scan here. Spinrite didn't."
|
||||||
|
- QRMaster-Learning: "Nicht bei Scan here stoppen" ist ein guter Problem-Hook.
|
||||||
|
|
||||||
|
13. Data/Loyalty: "When every new buyer counts, QR helps keep them"
|
||||||
|
- Beispiel-ID: 1137430406113966
|
||||||
|
- Aktiv seit: 2026-05-04
|
||||||
|
- Message: zero-party data, loyalty, personalization.
|
||||||
|
- QRMaster-Learning: Privacy/zero-party-data nur fuer B2B/CPG nutzen, da erklaerungsbeduerftig.
|
||||||
|
|
||||||
|
14. Restaurant: "What brings diners back through the door?"
|
||||||
|
- Beispiel-ID: 1273239168172050
|
||||||
|
- Aktiv seit: 2026-05-21
|
||||||
|
- Message: first scan to last visit, keep seats full.
|
||||||
|
- QRMaster-Learning: neuerer starker Restaurant-Winner, als direkte QRMaster-Variante testen.
|
||||||
|
|
||||||
|
Wichtigste Uniqode-Erkenntnisse:
|
||||||
|
|
||||||
|
- Hospitality dominiert die Top-Positionen.
|
||||||
|
- Die staerksten Ads sprechen Personalentlastung, Service, Wiederbesuche und Umsatz an.
|
||||||
|
- Retail/Thought-Leadership funktioniert, wenn es konkrete Beispiele oder Reports gibt.
|
||||||
|
- Case Studies laufen lange und koennen als Trust-Builder funktionieren.
|
||||||
|
- Feature-only Ads sind schwaecher als Outcome- oder Journey-Ads.
|
||||||
|
|
||||||
|
Prioritaet fuer QRMaster:
|
||||||
|
1. Restaurant/Hotel Self-Service: "Gaeste finden alles per QR, dein Team spart Zeit"
|
||||||
|
2. QR Menu Revenue/Upsell: "Mehr Umsatz ohne aufdringlich zu verkaufen"
|
||||||
|
3. Branded QR/Linkpage Hub
|
||||||
|
4. Lead Magnet: "14 QR-Code-Ideen fuer Restaurants/Retail"
|
||||||
|
5. Case Study Ads, sobald echte Kundenbeispiele vorhanden sind
|
||||||
|
|
||||||
|
## Creative Rules Learned From Competitors
|
||||||
|
|
||||||
|
- Erste 3 Sekunden muessen Hook oder Ergebnis zeigen.
|
||||||
|
- Video-Laenge: 8-20 Sekunden fuer schnelle Demos, 30-45 Sekunden fuer Feature-Erklaerung.
|
||||||
|
- Immer Text-Overlay nutzen, weil viele Meta-Videos ohne Ton laufen.
|
||||||
|
- QR-Code-Erstellung visuell zeigen: Link einfuegen, Farbe/Logo waehlen, Code downloaden, Scan tracken.
|
||||||
|
- Statische Ads brauchen grossen lesbaren Text und einen klaren visuellen Vorher/Nachher-Vergleich.
|
||||||
|
- Formate immer in 1:1, 4:5 und 9:16 testen.
|
||||||
|
- Pro Konzept mindestens 2-3 Varianten testen.
|
||||||
|
|
||||||
|
## Copy Rules
|
||||||
|
|
||||||
|
- Konkrete Zahlen nutzen, wenn belegbar: "10 Sekunden", "30 Sekunden", "jeder Scan", "Echtzeit".
|
||||||
|
- Keine unbelegten Performance-Claims wie "80% mehr Scans" ohne Daten.
|
||||||
|
- "Free" oder "kostenlos" prominent platzieren.
|
||||||
|
- Feature + Outcome kombinieren: nicht nur "Analytics", sondern "sieh, welche Kampagne funktioniert".
|
||||||
|
- Fuer SMBs einfache Sprache nutzen, keine Enterprise-Komplexitaet.
|
||||||
|
- Fuer Retargeting Einwaende direkt ansprechen.
|
||||||
|
|
||||||
|
## Positioning Against Competitors
|
||||||
|
|
||||||
|
Gegen Scanova:
|
||||||
|
- QRMaster einfacher, schneller, weniger "Tool"-Komplexitaet.
|
||||||
|
- Starker Fokus auf kostenlose Erstellung und klare SMB Use Cases.
|
||||||
|
|
||||||
|
Gegen QR TIGER:
|
||||||
|
- QRMaster weniger Enterprise/advanced, mehr "in Sekunden starten".
|
||||||
|
- Branding und Tracking trotzdem sichtbar machen.
|
||||||
|
|
||||||
|
Gegen Bitly:
|
||||||
|
- Bitly ist breite Link-Plattform. QRMaster sollte als fokussierte QR-Code-Loesung auftreten.
|
||||||
|
- "Everything you need. Nothing you don't."
|
||||||
|
|
||||||
|
Gegen Uniqode:
|
||||||
|
- Uniqode wirkt Premium/Enterprise/komplex.
|
||||||
|
- QRMaster sollte "simple, fast, affordable" besetzen.
|
||||||
|
|
||||||
|
## Priority Test Plan
|
||||||
|
|
||||||
|
Als erste Meta-Ads fuer QRMaster testen:
|
||||||
|
|
||||||
|
1. "Kostenlose QR-Codes in 10 Sekunden erstellen"
|
||||||
|
2. "Dein QR-Code sollte zu deiner Marke passen"
|
||||||
|
3. "Noch an QRMaster interessiert?"
|
||||||
|
4. "Wenig Platz. Viel zu sagen."
|
||||||
|
5. "Mach dein Menue-QR messbar"
|
||||||
|
|
||||||
|
Budgetlogik:
|
||||||
|
|
||||||
|
- 40% Retargeting/Win-back
|
||||||
|
- 30% Speed + Free
|
||||||
|
- 20% Branded/Design
|
||||||
|
- 10% Vertical Tests
|
||||||
|
|
||||||
|
## Reminder For Future Ad Work
|
||||||
|
|
||||||
|
Wenn Ads fuer qrmaster.net erstellt werden:
|
||||||
|
|
||||||
|
1. Diese Datei zuerst lesen.
|
||||||
|
2. Mindestens einen Competitor-Winkel uebernehmen.
|
||||||
|
3. Den Winkel auf QRMaster differenzieren: einfacher, schneller, kostenloser, SMB-freundlicher.
|
||||||
|
4. Keine unbelegten Zahlen oder Claims nutzen.
|
||||||
|
5. Jede Ad mit Hook, Benefit, Proof/Feature und klarem CTA strukturieren.
|
||||||
24
memory/project_summary.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# QR Master Project Summary
|
||||||
|
|
||||||
|
## TikTok Growth Positioning
|
||||||
|
|
||||||
|
QR Master should be positioned as control after print, not as another generic QR code generator.
|
||||||
|
|
||||||
|
Core message:
|
||||||
|
|
||||||
|
> A printed QR code is only useful if the destination can change, the scans can be measured, and the business does not need to reprint.
|
||||||
|
|
||||||
|
Best early TikTok angles from the supplied account analysis:
|
||||||
|
|
||||||
|
- Reprint pain after a link changes.
|
||||||
|
- Static QR code risks.
|
||||||
|
- Restaurant QR menu problems.
|
||||||
|
- Event and flyer changes.
|
||||||
|
- Business card QR workflows.
|
||||||
|
- QR scan analytics for print campaigns.
|
||||||
|
|
||||||
|
TikTok content should lead with a concrete business mistake or relatable QR frustration, then connect the fix to dynamic QR codes and QR Master.
|
||||||
|
|
||||||
|
## Outreach A/B Test Notes
|
||||||
|
|
||||||
|
- Variant B (`old_qr_links_outdated_destination`, subject `old qr links`) produced a paid user. Treat B as a proven paid-user signal when deciding future lead batch messaging.
|
||||||
41
memory/upgrade_nudge_status.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Upgrade Nudge Status Memory
|
||||||
|
|
||||||
|
Last updated: 2026-05-02
|
||||||
|
|
||||||
|
Purpose: local memory for upgrade-nudge outreach status when SMTP/Sent-folder evidence is incomplete. `sent_assumed_manual` means the user instructed us to treat the contact as already sent for future filtering, not that a verifiable SMTP/Sent record was found.
|
||||||
|
|
||||||
|
## Pending send
|
||||||
|
|
||||||
|
These are the two highest-priority upgrade candidates. Do not mark as sent until the upgrade email is actually sent.
|
||||||
|
|
||||||
|
| Name | Email | Stage | Lead score | Reason |
|
||||||
|
| --- | --- | --- | ---: | --- |
|
||||||
|
| Ishemupenyu Chagonda | info@aldenadvisory.co.uk | Upgrade Candidate | 105 | User confirmed no upgrade email has been sent yet |
|
||||||
|
| Shreya Hegde | shhegde@linkedin.com | Upgrade Candidate | 95 | User confirmed no upgrade email has been sent yet |
|
||||||
|
|
||||||
|
## Marked as sent by manual memory
|
||||||
|
|
||||||
|
These contacts should be treated as already sent when filtering future upgrade-nudge batches.
|
||||||
|
|
||||||
|
| Name | Email | Stage | Lead score | Status |
|
||||||
|
| --- | --- | --- | ---: | --- |
|
||||||
|
| Marcos Pagan | marcos@easternalliancerealty.com | Upgrade Candidate | 80 | sent_assumed_manual |
|
||||||
|
| katie Loucaides | katie.loucaides@rya.org.uk | Upgrade Candidate | 70 | sent_assumed_manual |
|
||||||
|
| Lindsey Holtz | lholtz@uwhealth.org | Upgrade Candidate | 70 | sent_assumed_manual |
|
||||||
|
| Janell Elder | janell.elder@gov.sk.ca | Upgrade Candidate | 70 | sent_assumed_manual |
|
||||||
|
| Nouf Saud | nouna.1428@gmail.com | Hot | 65 | sent_assumed_manual |
|
||||||
|
| Richie Shawl | richie.shawl@alfalaval.com | Hot | 60 | sent_assumed_manual |
|
||||||
|
| Patricia Hartmann | patricia.hartmann@agderfk.no | Hot | 60 | sent_assumed_manual |
|
||||||
|
| Andreas Knuth | andreas.knuth@gmail.com | Hot | unknown | sent_assumed_manual |
|
||||||
|
|
||||||
|
## Excluded
|
||||||
|
|
||||||
|
| Name | Email | Stage | Reason |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Profoto Malaysia Sdn Bhd | profotomalaysia@gmail.com | Paid | Already PRO/Paid, not an upgrade-nudge target |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- IMAP `Sent` check on 2026-05-02 found no verifiable Day-7 upgrade-nudge email records.
|
||||||
|
- The currently configured database lacks the `upgradeNudgeSentAt` column, so app-level sent status could not be verified there.
|
||||||
|
- Future upgrade-nudge sends should prioritize `info@aldenadvisory.co.uk` and `shhegde@linkedin.com`.
|
||||||
23
meta-fix.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { config } from 'dotenv';
|
||||||
|
config();
|
||||||
|
|
||||||
|
const TOKEN = process.env.META_ACCESS_TOKEN;
|
||||||
|
const BASE = 'https://graph.facebook.com/v21.0';
|
||||||
|
|
||||||
|
async function api(path, method = 'GET', body) {
|
||||||
|
const url = new URL(`${BASE}/${path}`);
|
||||||
|
url.searchParams.set('access_token', TOKEN);
|
||||||
|
const res = await fetch(url.toString(), {
|
||||||
|
method,
|
||||||
|
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(JSON.stringify(data));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Pausing orphaned ad sets...');
|
||||||
|
await api('6968509692127', 'POST', { status: 'PAUSED' });
|
||||||
|
await api('6958800756527', 'POST', { status: 'PAUSED' });
|
||||||
|
console.log('Done: Paused 2 orphaned ad sets (New Sales Ad Set, New Sales ad set)');
|
||||||
@@ -1,10 +1,24 @@
|
|||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
function isWslOnWindowsMount() {
|
||||||
|
return process.platform === 'linux' && process.cwd().startsWith('/mnt/');
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
skipTrailingSlashRedirect: true,
|
skipTrailingSlashRedirect: true,
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
images: {
|
images: {
|
||||||
unoptimized: false,
|
unoptimized: false,
|
||||||
domains: ['www.qrmaster.net', 'qrmaster.net', 'images.qrmaster.net'],
|
remotePatterns: [
|
||||||
|
{ protocol: 'https', hostname: 'www.qrmaster.net' },
|
||||||
|
{ protocol: 'https', hostname: 'qrmaster.net' },
|
||||||
|
{ protocol: 'https', hostname: 'images.qrmaster.net' },
|
||||||
|
],
|
||||||
formats: ['image/webp', 'image/avif'],
|
formats: ['image/webp', 'image/avif'],
|
||||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||||
@@ -20,6 +34,16 @@ const nextConfig = {
|
|||||||
pagesBufferLength: 2,
|
pagesBufferLength: 2,
|
||||||
},
|
},
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
|
webpack: (config, { dev }) => {
|
||||||
|
if (!dev && isWslOnWindowsMount()) {
|
||||||
|
config.cache = {
|
||||||
|
type: 'filesystem',
|
||||||
|
cacheDirectory: path.join(os.tmpdir(), 'qrmaster-next-webpack-cache'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -47,16 +71,6 @@ const nextConfig = {
|
|||||||
destination: '/tools/call-qr-code-generator',
|
destination: '/tools/call-qr-code-generator',
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: '/barcode-generator',
|
|
||||||
destination: '/tools/barcode-generator',
|
|
||||||
permanent: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: '/bar-code-generator',
|
|
||||||
destination: '/tools/barcode-generator',
|
|
||||||
permanent: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
source: '/qr-code-for/breweries-tap-rooms',
|
source: '/qr-code-for/breweries-tap-rooms',
|
||||||
destination: '/qr-code-for/breweries',
|
destination: '/qr-code-for/breweries',
|
||||||
|
|||||||
BIN
output/imagegen/onboarding-mockup-v1.png
Normal file
|
After Width: | Height: | Size: 1014 KiB |
776
output/paused-qr-preview/index.html
Normal file
@@ -0,0 +1,776 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>QR Code is paused</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f5f7fb;
|
||||||
|
--bg-2: #edf2f8;
|
||||||
|
--panel: rgba(255, 255, 255, 0.72);
|
||||||
|
--panel-strong: rgba(255, 255, 255, 0.84);
|
||||||
|
--line: rgba(148, 163, 184, 0.24);
|
||||||
|
--text: #0f172a;
|
||||||
|
--muted: #667085;
|
||||||
|
--soft: #94a3b8;
|
||||||
|
--blue: #2563eb;
|
||||||
|
--blue-soft: rgba(37, 99, 235, 0.14);
|
||||||
|
--amber: #f59e0b;
|
||||||
|
--amber-soft: rgba(245, 158, 11, 0.14);
|
||||||
|
--shadow: 0 40px 120px rgba(15, 23, 42, 0.12);
|
||||||
|
--shadow-soft: 0 24px 70px rgba(148, 163, 184, 0.16);
|
||||||
|
--radius-xl: 34px;
|
||||||
|
--radius-lg: 24px;
|
||||||
|
--radius-md: 18px;
|
||||||
|
--ease: cubic-bezier(.22, 1, .36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.95), transparent 28%),
|
||||||
|
radial-gradient(circle at 85% 18%, rgba(37, 99, 235, 0.08), transparent 24%),
|
||||||
|
radial-gradient(circle at 50% 82%, rgba(245, 158, 11, 0.08), transparent 20%),
|
||||||
|
linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 100%);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Segoe UI", sans-serif;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before,
|
||||||
|
body::after {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: auto;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: blur(80px);
|
||||||
|
opacity: 0.9;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
width: 24rem;
|
||||||
|
height: 24rem;
|
||||||
|
top: 6rem;
|
||||||
|
right: -6rem;
|
||||||
|
background: rgba(37, 99, 235, 0.13);
|
||||||
|
animation: drift 18s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::after {
|
||||||
|
width: 20rem;
|
||||||
|
height: 20rem;
|
||||||
|
bottom: 2rem;
|
||||||
|
left: -4rem;
|
||||||
|
background: rgba(245, 158, 11, 0.09);
|
||||||
|
animation: drift 22s ease-in-out infinite alternate-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grain {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.06;
|
||||||
|
z-index: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(15, 23, 42, 0.08) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(15, 23, 42, 0.08) 1px, transparent 1px);
|
||||||
|
background-size: 4px 4px;
|
||||||
|
mix-blend-mode: soft-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
width: min(1320px, 100%);
|
||||||
|
min-height: min(860px, calc(100vh - 56px));
|
||||||
|
padding: 28px;
|
||||||
|
border-radius: 42px;
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0.36));
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame::before,
|
||||||
|
.frame::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame::before {
|
||||||
|
background:
|
||||||
|
linear-gradient(120deg, rgba(255, 255, 255, 0.55), transparent 26%),
|
||||||
|
radial-gradient(circle at 76% 24%, rgba(37, 99, 235, 0.08), transparent 18%);
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame::after {
|
||||||
|
inset: 18px;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 34px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordmark {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.62);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.82);
|
||||||
|
box-shadow: 0 12px 40px rgba(148, 163, 184, 0.14);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
font-weight: 600;
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordmark-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(180deg, #6ea8ff, #2563eb);
|
||||||
|
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.availability {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.56);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.76);
|
||||||
|
color: var(--soft);
|
||||||
|
font-size: 13px;
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.availability span {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(180deg, #f6bf54, #f59e0b);
|
||||||
|
box-shadow: 0 0 0 5px rgba(245, 158, 11, 0.12);
|
||||||
|
animation: pulse 2.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 0.94fr) minmax(360px, 1.06fr);
|
||||||
|
gap: 42px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: calc(100% - 78px);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
max-width: 540px;
|
||||||
|
padding: 10px 6px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.64);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.86);
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(180deg, rgba(245, 158, 11, 0.18), rgba(245, 158, 11, 0.08));
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause-bars {
|
||||||
|
width: 10px;
|
||||||
|
height: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause-bars::before,
|
||||||
|
.pause-bars::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 3px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--amber);
|
||||||
|
box-shadow: 0 0 16px rgba(245, 158, 11, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause-bars::before { left: 1px; }
|
||||||
|
.pause-bars::after { right: 1px; }
|
||||||
|
|
||||||
|
.badge-copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-copy strong {
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--soft);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-copy span {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(3.5rem, 5vw, 5.5rem);
|
||||||
|
line-height: 0.94;
|
||||||
|
letter-spacing: -0.055em;
|
||||||
|
font-weight: 700;
|
||||||
|
max-width: 10ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lede {
|
||||||
|
margin: 24px 0 0;
|
||||||
|
font-size: clamp(1.08rem, 1.9vw, 1.28rem);
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--muted);
|
||||||
|
max-width: 47ch;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subcopy {
|
||||||
|
margin: 14px 0 0;
|
||||||
|
color: var(--soft);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.7;
|
||||||
|
max-width: 50ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
.link-button {
|
||||||
|
appearance: none;
|
||||||
|
border: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
transform 220ms var(--ease),
|
||||||
|
box-shadow 220ms var(--ease),
|
||||||
|
background-color 220ms var(--ease),
|
||||||
|
border-color 220ms var(--ease),
|
||||||
|
color 220ms var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 16px 22px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(180deg, #2f6fff, #2563eb);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
box-shadow:
|
||||||
|
0 14px 40px rgba(37, 99, 235, 0.24),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover,
|
||||||
|
.button:focus-visible {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow:
|
||||||
|
0 20px 48px rgba(37, 99, 235, 0.28),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button {
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 10px 30px rgba(148, 163, 184, 0.14);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button:hover,
|
||||||
|
.link-button:focus-visible {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-card {
|
||||||
|
min-width: 185px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: rgba(255, 255, 255, 0.48);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.76);
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-card .eyebrow {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--soft);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-card strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 17px;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual {
|
||||||
|
position: relative;
|
||||||
|
min-height: 630px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
isolation: isolate;
|
||||||
|
perspective: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual::before,
|
||||||
|
.visual::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: blur(30px);
|
||||||
|
transition: transform 240ms var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual::before {
|
||||||
|
width: 460px;
|
||||||
|
height: 460px;
|
||||||
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0) 72%);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual::after {
|
||||||
|
width: 320px;
|
||||||
|
height: 320px;
|
||||||
|
background: radial-gradient(circle, rgba(37, 99, 235, 0.14) 0%, rgba(37, 99, 235, 0) 72%);
|
||||||
|
top: 14%;
|
||||||
|
right: 14%;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbital-ring {
|
||||||
|
position: absolute;
|
||||||
|
width: 480px;
|
||||||
|
height: 480px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 18px rgba(255, 255, 255, 0.12),
|
||||||
|
inset 0 0 40px rgba(255, 255, 255, 0.24);
|
||||||
|
opacity: 0.9;
|
||||||
|
z-index: 1;
|
||||||
|
animation: ring 14s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-panel {
|
||||||
|
position: absolute;
|
||||||
|
inset: 12% 8%;
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.26), rgba(255, 255, 255, 0.08));
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.38);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
|
||||||
|
transform: translate3d(0, 0, 0) rotateX(10deg);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-art {
|
||||||
|
position: relative;
|
||||||
|
width: min(100%, 720px);
|
||||||
|
padding: 44px;
|
||||||
|
border-radius: 42px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.14));
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.62);
|
||||||
|
box-shadow:
|
||||||
|
0 32px 100px rgba(148, 163, 184, 0.22),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.48);
|
||||||
|
backdrop-filter: blur(30px);
|
||||||
|
z-index: 2;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transition: transform 240ms var(--ease);
|
||||||
|
animation: float 7s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-art::before,
|
||||||
|
.hero-art::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 16px;
|
||||||
|
border-radius: 30px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-art::before {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-art::after {
|
||||||
|
background:
|
||||||
|
linear-gradient(140deg, rgba(255, 255, 255, 0.26), transparent 28%),
|
||||||
|
linear-gradient(320deg, rgba(245, 158, 11, 0.08), transparent 34%);
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-art img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 28px;
|
||||||
|
transform: translateZ(40px);
|
||||||
|
filter: saturate(1.02) contrast(1.02);
|
||||||
|
box-shadow: 0 30px 90px rgba(148, 163, 184, 0.22);
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-caption {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 28px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.88);
|
||||||
|
box-shadow: 0 12px 40px rgba(148, 163, 184, 0.18);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
z-index: 3;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-caption strong {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-caption .beam {
|
||||||
|
width: 34px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, rgba(245, 158, 11, 0), rgba(245, 158, 11, 1), rgba(245, 158, 11, 0));
|
||||||
|
box-shadow: 0 0 16px rgba(245, 158, 11, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-note {
|
||||||
|
margin-top: 18px;
|
||||||
|
color: var(--soft);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translate3d(0, 0, 0) rotateX(0deg) rotateY(0deg); }
|
||||||
|
50% { transform: translate3d(0, -10px, 0) rotateX(1.5deg) rotateY(-1.5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes drift {
|
||||||
|
0% { transform: translate3d(0, 0, 0) scale(1); }
|
||||||
|
100% { transform: translate3d(-18px, 14px, 0) scale(1.08); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { transform: scale(1); opacity: 0.85; }
|
||||||
|
50% { transform: scale(1.18); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ring {
|
||||||
|
from { transform: rotate(0deg) scale(1); }
|
||||||
|
50% { transform: rotate(180deg) scale(1.02); }
|
||||||
|
to { transform: rotate(360deg) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1080px) {
|
||||||
|
.shell {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
min-height: auto;
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual {
|
||||||
|
min-height: 520px;
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-art {
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.shell {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.availability {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2.8rem, 12vw, 4rem);
|
||||||
|
max-width: 12ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lede {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions,
|
||||||
|
.meta {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
.link-button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual {
|
||||||
|
min-height: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-art {
|
||||||
|
padding: 18px;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-art::before,
|
||||||
|
.hero-art::after {
|
||||||
|
inset: 10px;
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-art img {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbital-ring {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-caption {
|
||||||
|
bottom: 14px;
|
||||||
|
max-width: calc(100% - 30px);
|
||||||
|
white-space: normal;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="grain" aria-hidden="true"></div>
|
||||||
|
<main class="shell">
|
||||||
|
<section class="frame">
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="wordmark">
|
||||||
|
<span class="wordmark-dot" aria-hidden="true"></span>
|
||||||
|
QR Master
|
||||||
|
</div>
|
||||||
|
<div class="availability">
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
Scanning temporarily unavailable
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="hero">
|
||||||
|
<div class="copy">
|
||||||
|
<div class="badge">
|
||||||
|
<div class="badge-icon" aria-hidden="true">
|
||||||
|
<div class="pause-bars"></div>
|
||||||
|
</div>
|
||||||
|
<div class="badge-copy">
|
||||||
|
<strong>Status</strong>
|
||||||
|
<span>Paused by owner</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>QR Code is paused</h1>
|
||||||
|
|
||||||
|
<p class="lede">
|
||||||
|
This QR code has been temporarily disabled by its owner, so scanning is currently unavailable.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="subcopy">
|
||||||
|
Please try again later or contact the owner for the active link. Paused codes should feel intentional and trustworthy, not broken.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<a class="button" href="#">Go to QR Master</a>
|
||||||
|
<a class="link-button" href="#">Need help?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
<article class="meta-card">
|
||||||
|
<span class="eyebrow">Redirect</span>
|
||||||
|
<strong>Temporarily disabled</strong>
|
||||||
|
<p>No destination opens while this code remains paused.</p>
|
||||||
|
</article>
|
||||||
|
<article class="meta-card">
|
||||||
|
<span class="eyebrow">Tracking</span>
|
||||||
|
<strong>Scan logging stopped</strong>
|
||||||
|
<p>Paused scans should not continue into analytics.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="footer-note">Preview concept for a standalone paused-state page.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="visual" id="visualStage">
|
||||||
|
<div class="orbital-ring" aria-hidden="true"></div>
|
||||||
|
<div class="glass-panel" aria-hidden="true"></div>
|
||||||
|
<figure class="hero-art" id="heroArt">
|
||||||
|
<img src="./paused-qr-hero-cinematic.png" alt="Glass-like QR tile floating in a cinematic studio environment">
|
||||||
|
</figure>
|
||||||
|
<div class="scan-caption">
|
||||||
|
<strong>Paused</strong>
|
||||||
|
<span class="beam" aria-hidden="true"></span>
|
||||||
|
The scan was intentionally interrupted
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const heroArt = document.getElementById('heroArt');
|
||||||
|
const visualStage = document.getElementById('visualStage');
|
||||||
|
|
||||||
|
if (heroArt && visualStage && window.matchMedia('(prefers-reduced-motion: no-preference)').matches) {
|
||||||
|
visualStage.addEventListener('pointermove', (event) => {
|
||||||
|
const bounds = visualStage.getBoundingClientRect();
|
||||||
|
const x = (event.clientX - bounds.left) / bounds.width - 0.5;
|
||||||
|
const y = (event.clientY - bounds.top) / bounds.height - 0.5;
|
||||||
|
|
||||||
|
const rotateX = y * -10;
|
||||||
|
const rotateY = x * 12;
|
||||||
|
const translateX = x * 12;
|
||||||
|
const translateY = y * 8;
|
||||||
|
|
||||||
|
heroArt.style.transform =
|
||||||
|
`translate3d(${translateX}px, ${translateY}px, 0) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
visualStage.addEventListener('pointerleave', () => {
|
||||||
|
heroArt.style.transform = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
output/paused-qr-preview/paused-qr-hero-cinematic.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
@@ -37,10 +37,50 @@ model User {
|
|||||||
upgradeNudgeSentAt DateTime?
|
upgradeNudgeSentAt DateTime?
|
||||||
thirtyDayNudgeSentAt DateTime?
|
thirtyDayNudgeSentAt DateTime?
|
||||||
|
|
||||||
|
// RevOps attribution
|
||||||
|
signupSource String?
|
||||||
|
signupSourceSelfReported String?
|
||||||
|
signupMedium String?
|
||||||
|
signupCampaign String?
|
||||||
|
signupContent String?
|
||||||
|
signupTerm String?
|
||||||
|
signupReferrer String?
|
||||||
|
signupLandingPath String?
|
||||||
|
signupFirstSeenAt DateTime?
|
||||||
|
emailDomain String?
|
||||||
|
|
||||||
|
// Onboarding and qualification
|
||||||
|
primaryUseCase String?
|
||||||
|
primaryGoal String?
|
||||||
|
jobRole String?
|
||||||
|
companyName String?
|
||||||
|
companyWebsite String?
|
||||||
|
teamSizeBucket String?
|
||||||
|
onboardingStartedAt DateTime?
|
||||||
|
sourceConfirmedAt DateTime?
|
||||||
|
useCaseSelectedAt DateTime?
|
||||||
|
goalSelectedAt DateTime?
|
||||||
|
profileCompletedAt DateTime?
|
||||||
|
firstQrCreatedAt DateTime?
|
||||||
|
firstDynamicQrAt DateTime?
|
||||||
|
firstStaticQrAt DateTime?
|
||||||
|
firstScanAt DateTime?
|
||||||
|
activationAt DateTime?
|
||||||
|
onboardingCompletedAt DateTime?
|
||||||
|
|
||||||
|
// RevOps scoring
|
||||||
|
fitScore Int @default(0)
|
||||||
|
intentScore Int @default(0)
|
||||||
|
leadScore Int @default(0)
|
||||||
|
lifecycleStage String @default("cold")
|
||||||
|
lastQualifiedAt DateTime?
|
||||||
|
lastScoredAt DateTime?
|
||||||
|
|
||||||
qrCodes QRCode[]
|
qrCodes QRCode[]
|
||||||
integrations Integration[]
|
integrations Integration[]
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
|
lifecycleLogs UserLifecycleLog[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Plan {
|
enum Plan {
|
||||||
@@ -121,6 +161,7 @@ enum ContentType {
|
|||||||
APP
|
APP
|
||||||
COUPON
|
COUPON
|
||||||
FEEDBACK
|
FEEDBACK
|
||||||
|
BARCODE
|
||||||
}
|
}
|
||||||
|
|
||||||
enum QRStatus {
|
enum QRStatus {
|
||||||
@@ -160,6 +201,20 @@ model Integration {
|
|||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model UserLifecycleLog {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
fromStage String?
|
||||||
|
toStage String
|
||||||
|
fitScore Int @default(0)
|
||||||
|
intentScore Int @default(0)
|
||||||
|
leadScore Int @default(0)
|
||||||
|
reason String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
model NewsletterSubscription {
|
model NewsletterSubscription {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
email String @unique
|
email String @unique
|
||||||
|
|||||||
BIN
public/Background__White_#FFFFFF_202604201334.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/Move_the_4-5_202604201400.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/Screenshot 2026-04-20 132947.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
public/Screenshot 2026-04-22 123347.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
public/Split-screen_product_comparison_202604201333.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
public/images/review-collection-qr-hero.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/marketing/qrmaster-hero-generated-v1.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/meta-restaurant-dashboard-ad.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/restaurants-hero-og.jpg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
public/restaurants-hero-wide.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/restaurants-hero-wide.webp
Normal file
|
After Width: | Height: | Size: 99 KiB |
2897
qrmaster-lighthouse.report.html
Normal file
15107
qrmaster-lighthouse.report.json
Normal file
45
quora_antwort_statisch_dynamisch.txt
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
Frage: Was ist der Unterschied zwischen statischen und dynamischen QR-Codes, und welche eignen sich am besten für welche Zwecke?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Kurz gesagt: Der Unterschied liegt darin, was im QR-Code steckt.
|
||||||
|
|
||||||
|
Ein statischer QR-Code kodiert die Ziel-URL direkt ins Muster. Einmal gedruckt, ist alles fest — du kannst nichts mehr ändern. Ändert sich deine URL, ist der Code wertlos.
|
||||||
|
|
||||||
|
Ein dynamischer QR-Code kodiert nur eine kurze Weiterleitungs-URL (z.B. qrmaster.net/r/xyz). Wo die hinführt, steuerst du jederzeit über ein Dashboard — ohne den gedruckten Code anzufassen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Wann statisch reicht:
|
||||||
|
|
||||||
|
- WLAN-Passwort, das sich nie ändert
|
||||||
|
- Einmalige Events (z.B. Einlass-Scan)
|
||||||
|
- Visitenkarte mit fixer vCard
|
||||||
|
- Alles, wo du sicher bist, dass sich die URL nie ändert
|
||||||
|
|
||||||
|
Wann dynamisch die bessere Wahl ist:
|
||||||
|
|
||||||
|
- Restaurantmenüs, Flyer, Plakate — alles was länger gedruckt bleibt
|
||||||
|
- Marketingkampagnen mit wechselnden Landingpages
|
||||||
|
- Wenn du wissen willst, wer, wann und womit gescannt hat
|
||||||
|
- Wenn du nach dem Druck noch einen Tippfehler in der URL korrigieren willst
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Was viele unterschätzen — die Druckkosten:
|
||||||
|
|
||||||
|
Statische Codes wirken erstmal kostenlos. Aber sobald sich die URL ändert, musst du alles neu drucken.
|
||||||
|
|
||||||
|
Beispiel: 500 Flyer à 0,18 € = 90 € pro Neudruck. Wer das zweimal im Jahr macht, hat den Preis eines Jahresabos für dynamische Codes längst überschritten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Was dynamische Codes zusätzlich bieten:
|
||||||
|
|
||||||
|
- Scan-Statistiken: Gerät, Land, Uhrzeit
|
||||||
|
- UTM-Parameter für Google Analytics
|
||||||
|
- Zentrale Verwaltung aller Codes im Dashboard
|
||||||
|
|
||||||
|
Für eigene Kampagnen nutze ich den dynamischen QR-Code-Generator von QR Master (https://www.qrmaster.net/dynamic-qr-code-generator) — Ziele lassen sich nach dem Druck in Sekunden ändern, und man sieht genau welcher Code wie performt.
|
||||||
|
|
||||||
|
Fazit: Für einmaligen Privatgebrauch reicht statisch völlig. Sobald QR-Codes gedruckt werden und länger im Einsatz sind, ist dynamisch fast immer die günstigere Wahl.
|
||||||
BIN
quora_suche.txt
Normal file
49
read-inbox.mjs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import tls from 'node:tls';
|
||||||
|
|
||||||
|
function readMessages(seqs) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const socket = tls.connect({ host: 'imap.qrmaster.net', port: 993 }, () => {
|
||||||
|
let buf = '';
|
||||||
|
let step = 0;
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
socket.on('data', (chunk) => {
|
||||||
|
buf += chunk.toString();
|
||||||
|
const lines = buf.split('\r\n');
|
||||||
|
buf = lines.pop();
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (step === 0 && line.includes('* OK')) {
|
||||||
|
socket.write(`A1 LOGIN timo@qrmaster.net fiesta\r\n`);
|
||||||
|
step = 1;
|
||||||
|
} else if (step === 1 && line.startsWith('A1 OK')) {
|
||||||
|
socket.write(`A2 SELECT INBOX\r\n`);
|
||||||
|
step = 2;
|
||||||
|
} else if (step === 2 && line.startsWith('A2 OK')) {
|
||||||
|
socket.write(`A3 FETCH ${seqs.join(',')} (BODY.PEEK[1])\r\n`);
|
||||||
|
step = 3;
|
||||||
|
} else if (step === 3) {
|
||||||
|
if (line.startsWith('A3 OK')) {
|
||||||
|
socket.write(`A4 LOGOUT\r\n`);
|
||||||
|
resolve(results);
|
||||||
|
}
|
||||||
|
const m = line.match(/^\* (\d+) FETCH/);
|
||||||
|
if (m) results[m[1]] = { body: '' };
|
||||||
|
const curr = Object.keys(results).at(-1);
|
||||||
|
if (curr && line && !line.match(/^\* \d+ FETCH/) && !line.startsWith('A3') && line !== ')') {
|
||||||
|
results[curr].body += line + '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = await readMessages([954, 990, 997]);
|
||||||
|
for (const [seq, msg] of Object.entries(r)) {
|
||||||
|
console.log(`\n=== SEQ ${seq} ===`);
|
||||||
|
console.log(msg.body.slice(0, 1500));
|
||||||
|
}
|
||||||
@@ -4,13 +4,8 @@ const path = require('path');
|
|||||||
|
|
||||||
const repoRoot = path.resolve(__dirname, '..');
|
const repoRoot = path.resolve(__dirname, '..');
|
||||||
const prismaSchemaPath = path.join(repoRoot, 'prisma', 'schema.prisma');
|
const prismaSchemaPath = path.join(repoRoot, 'prisma', 'schema.prisma');
|
||||||
const generatedSchemaPath = path.join(
|
const generatedClientDir = path.join(repoRoot, 'node_modules', '.prisma', 'client');
|
||||||
repoRoot,
|
const generatedSchemaPath = path.join(generatedClientDir, 'schema.prisma');
|
||||||
'node_modules',
|
|
||||||
'.prisma',
|
|
||||||
'client',
|
|
||||||
'schema.prisma'
|
|
||||||
);
|
|
||||||
|
|
||||||
function readFileIfExists(filePath) {
|
function readFileIfExists(filePath) {
|
||||||
try {
|
try {
|
||||||
@@ -65,6 +60,14 @@ function run(command, args, options = {}) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWSL() {
|
||||||
|
return (
|
||||||
|
process.platform === 'linux' &&
|
||||||
|
fs.existsSync('/proc/version') &&
|
||||||
|
fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isWindowsPrismaRenameLock(output) {
|
function isWindowsPrismaRenameLock(output) {
|
||||||
const text = [output.stdout, output.stderr]
|
const text = [output.stdout, output.stderr]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -77,13 +80,46 @@ function isWindowsPrismaRenameLock(output) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPrismaCopyfileEio(output) {
|
||||||
|
const text = [output.stdout, output.stderr]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return (
|
||||||
|
text.includes('EIO: i/o error, copyfile') &&
|
||||||
|
(text.includes('libquery_engine-') || text.includes('query_engine-'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupPrismaTempFiles() {
|
||||||
|
if (!fs.existsSync(generatedClientDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of fs.readdirSync(generatedClientDir)) {
|
||||||
|
if (!entry.includes('.tmp')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.rmSync(path.join(generatedClientDir, entry), { force: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to remove stale Prisma temp file ${entry}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function runPrismaGenerate() {
|
function runPrismaGenerate() {
|
||||||
const prismaBin =
|
const prismaBin =
|
||||||
process.platform === 'win32'
|
process.platform === 'win32'
|
||||||
? path.join(repoRoot, 'node_modules', '.bin', 'prisma.cmd')
|
? path.join(repoRoot, 'node_modules', '.bin', 'prisma.cmd')
|
||||||
: path.join(repoRoot, 'node_modules', '.bin', 'prisma');
|
: path.join(repoRoot, 'node_modules', '.bin', 'prisma');
|
||||||
|
|
||||||
const result = run(prismaBin, ['generate']);
|
if (isWSL()) {
|
||||||
|
cleanupPrismaTempFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = run(prismaBin, ['generate']);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
throw result.error;
|
throw result.error;
|
||||||
@@ -93,12 +129,28 @@ function runPrismaGenerate() {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isWindowsPrismaRenameLock(result) || !schemasMatch()) {
|
const retryablePrismaFsError =
|
||||||
|
isWindowsPrismaRenameLock(result) || isPrismaCopyfileEio(result);
|
||||||
|
|
||||||
|
if (retryablePrismaFsError) {
|
||||||
|
cleanupPrismaTempFiles();
|
||||||
|
result = run(prismaBin, ['generate']);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((result.status ?? 1) === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!retryablePrismaFsError || !schemasMatch()) {
|
||||||
return result.status ?? 1;
|
return result.status ?? 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(
|
console.warn(
|
||||||
'\nPrisma generate hit a Windows file lock, but the generated client already matches prisma/schema.prisma. Continuing with the existing client.\n'
|
'\nPrisma generate hit a filesystem copy/rename issue, but the generated client already matches prisma/schema.prisma. Continuing with the existing client.\n'
|
||||||
);
|
);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -110,11 +162,7 @@ function runNextBuild() {
|
|||||||
? path.join(repoRoot, 'node_modules', '.bin', 'next.cmd')
|
? path.join(repoRoot, 'node_modules', '.bin', 'next.cmd')
|
||||||
: path.join(repoRoot, 'node_modules', '.bin', 'next');
|
: path.join(repoRoot, 'node_modules', '.bin', 'next');
|
||||||
|
|
||||||
// WSL needs more aggressive memory settings
|
const memoryLimit = isWSL() ? '8192' : '4096';
|
||||||
const isWSL = process.platform === 'linux' && require('fs').existsSync('/proc/version') &&
|
|
||||||
require('fs').readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft');
|
|
||||||
|
|
||||||
const memoryLimit = isWSL ? '8192' : '4096';
|
|
||||||
|
|
||||||
return run(nextBin, ['build'], {
|
return run(nextBin, ['build'], {
|
||||||
env: {
|
env: {
|
||||||
|
|||||||
249
scripts/check-links-and-ctas.js
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const root = process.cwd();
|
||||||
|
const scanDirs = ["src", "marketing", "articles", "blog-posts-improved"];
|
||||||
|
const sourceExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".md", ".mdx"]);
|
||||||
|
const publicDir = path.join(root, "public");
|
||||||
|
const appDir = path.join(root, "src", "app");
|
||||||
|
|
||||||
|
const ignoredPrefixes = [
|
||||||
|
"/api/",
|
||||||
|
"/_next/",
|
||||||
|
"/auth/",
|
||||||
|
"/r/",
|
||||||
|
"/qr/",
|
||||||
|
"/scan/",
|
||||||
|
];
|
||||||
|
|
||||||
|
const knownDynamicPrefixes = [
|
||||||
|
"/blog/",
|
||||||
|
"/learn/",
|
||||||
|
"/authors/",
|
||||||
|
"/qr-code-for/",
|
||||||
|
"/use-cases/",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ctaPatterns = [
|
||||||
|
/get started/i,
|
||||||
|
/start free/i,
|
||||||
|
/try free/i,
|
||||||
|
/create.*qr/i,
|
||||||
|
/generate.*qr/i,
|
||||||
|
/sign up/i,
|
||||||
|
/pricing/i,
|
||||||
|
/upgrade/i,
|
||||||
|
/create.*free/i,
|
||||||
|
/start tracking/i,
|
||||||
|
/create.*editable/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
const nonConversionPageParts = [
|
||||||
|
"/contact/",
|
||||||
|
"/cookie-policy/",
|
||||||
|
"/privacy/",
|
||||||
|
"/press/",
|
||||||
|
"/authors/",
|
||||||
|
"/blog/",
|
||||||
|
"/newsletter/",
|
||||||
|
];
|
||||||
|
|
||||||
|
const findings = [];
|
||||||
|
const ctas = [];
|
||||||
|
|
||||||
|
function walk(dir, files = []) {
|
||||||
|
if (!fs.existsSync(dir)) return files;
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === ".git") continue;
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walk(fullPath, files);
|
||||||
|
} else {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPosix(value) {
|
||||||
|
return value.split(path.sep).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function routeFromPageFile(file) {
|
||||||
|
const rel = toPosix(path.relative(appDir, file));
|
||||||
|
if (!rel.endsWith("/page.tsx") && !rel.endsWith("/page.ts") && !rel.endsWith("/route.ts")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = rel.split("/");
|
||||||
|
parts.pop();
|
||||||
|
const routeParts = parts.filter((part) => {
|
||||||
|
if (!part) return false;
|
||||||
|
if (part.startsWith("(") && part.endsWith(")")) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (routeParts.some((part) => part.startsWith("[") && part.endsWith("]"))) return null;
|
||||||
|
return "/" + routeParts.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRoutes() {
|
||||||
|
const routes = new Set(["/"]);
|
||||||
|
for (const file of walk(appDir)) {
|
||||||
|
const route = routeFromPageFile(file);
|
||||||
|
if (route) routes.add(route === "/" ? "/" : route.replace(/\/$/, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of walk(publicDir)) {
|
||||||
|
const rel = "/" + toPosix(path.relative(publicDir, file));
|
||||||
|
routes.add(rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHref(rawHref) {
|
||||||
|
if (!rawHref) return null;
|
||||||
|
let href = rawHref.trim();
|
||||||
|
if (!href || href.startsWith("#")) return null;
|
||||||
|
if (/^(https?:|mailto:|tel:|sms:|javascript:|data:)/i.test(href)) return null;
|
||||||
|
|
||||||
|
if (!href.startsWith("/")) return null;
|
||||||
|
href = href.split("#")[0].split("?")[0];
|
||||||
|
if (href.length > 1) href = href.replace(/\/$/, "");
|
||||||
|
return href || "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedDynamicHref(href) {
|
||||||
|
if (ignoredPrefixes.some((prefix) => href.startsWith(prefix))) return true;
|
||||||
|
if (href.includes("[") || href.includes("${") || href.includes("`")) return true;
|
||||||
|
return knownDynamicPrefixes.some((prefix) => href.startsWith(prefix) && href !== prefix.replace(/\/$/, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineNumber(content, index) {
|
||||||
|
return content.slice(0, index).split(/\r?\n/).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHrefMatches(content) {
|
||||||
|
const matches = [];
|
||||||
|
const patterns = [
|
||||||
|
/href\s*=\s*["']([^"']+)["']/g,
|
||||||
|
/href\s*=\s*{\s*["']([^"']+)["']\s*}/g,
|
||||||
|
/router\.push\(\s*["']([^"']+)["']\s*\)/g,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(content)) !== null) {
|
||||||
|
matches.push({ href: match[1], index: match.index });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAnchors(content) {
|
||||||
|
const anchors = [];
|
||||||
|
const linkPattern = /<Link\b[\s\S]*?href\s*=\s*(?:["']([^"']+)["']|{\s*["']([^"']+)["']\s*})[\s\S]*?>([\s\S]*?)<\/Link>/g;
|
||||||
|
const anchorPattern = /<a\b[\s\S]*?href\s*=\s*(?:["']([^"']+)["']|{\s*["']([^"']+)["']\s*})[\s\S]*?>([\s\S]*?)<\/a>/g;
|
||||||
|
|
||||||
|
for (const pattern of [linkPattern, anchorPattern]) {
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(content)) !== null) {
|
||||||
|
const href = match[1] || match[2];
|
||||||
|
const text = match[3]
|
||||||
|
.replace(/<[^>]*>/g, " ")
|
||||||
|
.replace(/\{[^}]*\}/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
anchors.push({ href, text, index: match.index });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return anchors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceFiles() {
|
||||||
|
const files = [];
|
||||||
|
for (const dir of scanDirs) {
|
||||||
|
for (const file of walk(path.join(root, dir))) {
|
||||||
|
if (sourceExtensions.has(path.extname(file))) files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
const routes = collectRoutes();
|
||||||
|
|
||||||
|
for (const file of sourceFiles()) {
|
||||||
|
const content = fs.readFileSync(file, "utf8");
|
||||||
|
const rel = toPosix(path.relative(root, file));
|
||||||
|
|
||||||
|
for (const item of extractHrefMatches(content)) {
|
||||||
|
const href = normalizeHref(item.href);
|
||||||
|
if (!href) continue;
|
||||||
|
if (routes.has(href) || isAllowedDynamicHref(href)) continue;
|
||||||
|
|
||||||
|
findings.push({
|
||||||
|
type: "broken-internal-link",
|
||||||
|
file: rel,
|
||||||
|
line: lineNumber(content, item.index),
|
||||||
|
href,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const anchor of extractAnchors(content)) {
|
||||||
|
const text = anchor.text || "";
|
||||||
|
if (!ctaPatterns.some((pattern) => pattern.test(text))) continue;
|
||||||
|
|
||||||
|
const href = normalizeHref(anchor.href);
|
||||||
|
const status = !href
|
||||||
|
? "external-or-non-http"
|
||||||
|
: routes.has(href) || isAllowedDynamicHref(href)
|
||||||
|
? "ok"
|
||||||
|
: "broken";
|
||||||
|
|
||||||
|
ctas.push({
|
||||||
|
file: rel,
|
||||||
|
line: lineNumber(content, anchor.index),
|
||||||
|
text: text.slice(0, 100),
|
||||||
|
href: anchor.href,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const brokenCtas = ctas.filter((cta) => cta.status === "broken");
|
||||||
|
const weakFiles = sourceFiles().filter((file) => {
|
||||||
|
const rel = toPosix(path.relative(root, file));
|
||||||
|
if (!rel.includes("src/app/") || !rel.endsWith("/page.tsx")) return false;
|
||||||
|
if (!rel.includes("(marketing)")) return false;
|
||||||
|
if (rel.includes("[")) return false;
|
||||||
|
if (nonConversionPageParts.some((part) => rel.includes(part))) return false;
|
||||||
|
|
||||||
|
const content = fs.readFileSync(file, "utf8");
|
||||||
|
return !extractAnchors(content).some((anchor) =>
|
||||||
|
ctaPatterns.some((pattern) => pattern.test(anchor.text || "")),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const report = {
|
||||||
|
checkedAt: new Date().toISOString(),
|
||||||
|
routeCount: routes.size,
|
||||||
|
filesChecked: sourceFiles().length,
|
||||||
|
brokenInternalLinks: findings,
|
||||||
|
ctaSummary: {
|
||||||
|
total: ctas.length,
|
||||||
|
broken: brokenCtas.length,
|
||||||
|
sample: ctas.slice(0, 50),
|
||||||
|
},
|
||||||
|
pagesWithoutObviousCta: weakFiles.map((file) => toPosix(path.relative(root, file))).slice(0, 100),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(JSON.stringify(report, null, 2));
|
||||||
|
|
||||||
|
if (findings.length > 0 || brokenCtas.length > 0) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check();
|
||||||
421
scripts/scrape-us-qrmaster-leads.mjs
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const OUTPUT_DIR = path.resolve(process.cwd(), 'output', 'outreach');
|
||||||
|
const TARGET_PER_NICHE = Number(process.env.LEADS_PER_NICHE || 200);
|
||||||
|
const CONCURRENCY = Number(process.env.LEAD_FETCH_CONCURRENCY || 8);
|
||||||
|
const OVERPASS_DELAY_MS = Number(process.env.OVERPASS_DELAY_MS || 20000);
|
||||||
|
const OVERPASS_429_DELAY_MS = Number(process.env.OVERPASS_429_DELAY_MS || 90000);
|
||||||
|
const OVERPASS_MAX_ATTEMPTS = Number(process.env.OVERPASS_MAX_ATTEMPTS || 6);
|
||||||
|
const OVERPASS_URLS = [
|
||||||
|
'https://overpass-api.de/api/interpreter',
|
||||||
|
];
|
||||||
|
|
||||||
|
const metros = [
|
||||||
|
['New York', 'NY', 40.7128, -74.006],
|
||||||
|
['Los Angeles', 'CA', 34.0522, -118.2437],
|
||||||
|
['Chicago', 'IL', 41.8781, -87.6298],
|
||||||
|
['Houston', 'TX', 29.7604, -95.3698],
|
||||||
|
['Phoenix', 'AZ', 33.4484, -112.074],
|
||||||
|
['Philadelphia', 'PA', 39.9526, -75.1652],
|
||||||
|
['San Antonio', 'TX', 29.4241, -98.4936],
|
||||||
|
['San Diego', 'CA', 32.7157, -117.1611],
|
||||||
|
['Dallas', 'TX', 32.7767, -96.797],
|
||||||
|
['San Jose', 'CA', 37.3382, -121.8863],
|
||||||
|
['Austin', 'TX', 30.2672, -97.7431],
|
||||||
|
['Jacksonville', 'FL', 30.3322, -81.6557],
|
||||||
|
['Fort Worth', 'TX', 32.7555, -97.3308],
|
||||||
|
['Columbus', 'OH', 39.9612, -82.9988],
|
||||||
|
['Charlotte', 'NC', 35.2271, -80.8431],
|
||||||
|
['San Francisco', 'CA', 37.7749, -122.4194],
|
||||||
|
['Seattle', 'WA', 47.6062, -122.3321],
|
||||||
|
['Denver', 'CO', 39.7392, -104.9903],
|
||||||
|
['Miami', 'FL', 25.7617, -80.1918],
|
||||||
|
['Nashville', 'TN', 36.1627, -86.7816],
|
||||||
|
];
|
||||||
|
|
||||||
|
const niches = [
|
||||||
|
{
|
||||||
|
id: 'photographers',
|
||||||
|
label: 'Photographers',
|
||||||
|
targetUseCase: 'portfolio, booking, print cards, event galleries',
|
||||||
|
queries: [
|
||||||
|
['craft', 'photographer'],
|
||||||
|
['shop', 'photo_studio'],
|
||||||
|
['shop', 'photo'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'restaurants',
|
||||||
|
label: 'Restaurants',
|
||||||
|
targetUseCase: 'menu QR codes, table tents, review QR codes, coupons',
|
||||||
|
queries: [
|
||||||
|
['amenity', 'restaurant'],
|
||||||
|
['amenity', 'cafe'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'real_estate',
|
||||||
|
label: 'Real Estate',
|
||||||
|
targetUseCase: 'yard signs, flyers, open houses, property sheets',
|
||||||
|
queries: [
|
||||||
|
['office', 'estate_agent'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'events_venues',
|
||||||
|
label: 'Events & Venues',
|
||||||
|
targetUseCase: 'tickets, schedules, check-in, feedback and post-event links',
|
||||||
|
queries: [
|
||||||
|
['amenity', 'events_venue'],
|
||||||
|
['amenity', 'theatre'],
|
||||||
|
['amenity', 'conference_centre'],
|
||||||
|
['tourism', 'attraction'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wellness_beauty',
|
||||||
|
label: 'Wellness & Beauty',
|
||||||
|
targetUseCase: 'booking links, price lists, reviews, loyalty offers',
|
||||||
|
queries: [
|
||||||
|
['shop', 'beauty'],
|
||||||
|
['shop', 'hairdresser'],
|
||||||
|
['leisure', 'fitness_centre'],
|
||||||
|
['amenity', 'spa'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function csvEscape(value) {
|
||||||
|
const text = String(value ?? '');
|
||||||
|
if (/[",\n\r]/.test(text)) {
|
||||||
|
return `"${text.replaceAll('"', '""')}"`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWebsite(raw) {
|
||||||
|
if (!raw) return '';
|
||||||
|
let value = String(raw).trim();
|
||||||
|
if (!value) return '';
|
||||||
|
if (value.startsWith('mailto:') || value.includes('@') && !value.includes('/')) return '';
|
||||||
|
if (!/^https?:\/\//i.test(value)) value = `https://${value}`;
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
if (!url.hostname.includes('.')) return '';
|
||||||
|
url.hash = '';
|
||||||
|
return url.toString().replace(/\/$/, '');
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTag(tags, names) {
|
||||||
|
for (const name of names) {
|
||||||
|
if (tags?.[name]) return tags[name];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOverpassQuery(niche, metro, offset) {
|
||||||
|
const [, , lat, lon] = metro;
|
||||||
|
const radius = 25000 + offset * 10000;
|
||||||
|
const clauses = niche.queries.flatMap(([key, value]) => [
|
||||||
|
`nwr(around:${radius},${lat},${lon})["${key}"="${value}"]["website"];`,
|
||||||
|
`nwr(around:${radius},${lat},${lon})["${key}"="${value}"]["contact:website"];`,
|
||||||
|
`nwr(around:${radius},${lat},${lon})["${key}"="${value}"]["email"];`,
|
||||||
|
`nwr(around:${radius},${lat},${lon})["${key}"="${value}"]["contact:email"];`,
|
||||||
|
]).join('\n');
|
||||||
|
|
||||||
|
return `[out:json][timeout:45];
|
||||||
|
(
|
||||||
|
${clauses}
|
||||||
|
);
|
||||||
|
out tags center ${Math.min(TARGET_PER_NICHE * 2, 500)};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOverpass(query, attempt = 0) {
|
||||||
|
const endpoint = OVERPASS_URLS[attempt % OVERPASS_URLS.length];
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), 90000);
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' },
|
||||||
|
body: new URLSearchParams({ data: query }),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 429 && attempt < OVERPASS_MAX_ATTEMPTS) {
|
||||||
|
const waitMs = OVERPASS_429_DELAY_MS + attempt * 30000;
|
||||||
|
console.warn(`Overpass rate limited; waiting ${Math.round(waitMs / 1000)}s before retry ${attempt + 1}/${OVERPASS_MAX_ATTEMPTS}`);
|
||||||
|
await sleep(waitMs);
|
||||||
|
return fetchOverpass(query, attempt + 1);
|
||||||
|
}
|
||||||
|
if (attempt < OVERPASS_MAX_ATTEMPTS) {
|
||||||
|
await sleep(5000 * (attempt + 1));
|
||||||
|
return fetchOverpass(query, attempt + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Overpass ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt < OVERPASS_MAX_ATTEMPTS) {
|
||||||
|
await sleep(5000 * (attempt + 1));
|
||||||
|
return fetchOverpass(query, attempt + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementToLead(element, niche, metro) {
|
||||||
|
const tags = element.tags || {};
|
||||||
|
const website = normalizeWebsite(getTag(tags, ['contact:website', 'website', 'url']));
|
||||||
|
const email = getTag(tags, ['contact:email', 'email']);
|
||||||
|
const phone = getTag(tags, ['contact:phone', 'phone']);
|
||||||
|
const street = [tags['addr:housenumber'], tags['addr:street']].filter(Boolean).join(' ');
|
||||||
|
const city = tags['addr:city'] || metro[0];
|
||||||
|
const state = tags['addr:state'] || metro[1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
niche: niche.id,
|
||||||
|
niche_label: niche.label,
|
||||||
|
company: tags.name || '',
|
||||||
|
website,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
country: 'US',
|
||||||
|
street,
|
||||||
|
source: 'OpenStreetMap Overpass',
|
||||||
|
source_id: `${element.type}/${element.id}`,
|
||||||
|
source_url: `https://www.openstreetmap.org/${element.type}/${element.id}`,
|
||||||
|
personalization_signal: '',
|
||||||
|
qr_use_case: niche.targetUseCase,
|
||||||
|
lead_score: 0,
|
||||||
|
email_source: email ? 'osm' : '',
|
||||||
|
opt_out_required: 'yes',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleTextEmails(text) {
|
||||||
|
const normalized = text
|
||||||
|
.replaceAll('[at]', '@')
|
||||||
|
.replaceAll('(at)', '@')
|
||||||
|
.replaceAll(' at ', '@')
|
||||||
|
.replaceAll('[dot]', '.')
|
||||||
|
.replaceAll('(dot)', '.')
|
||||||
|
.replaceAll(' dot ', '.');
|
||||||
|
const matches = normalized.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g) || [];
|
||||||
|
return [...new Set(matches.map((email) => email.toLowerCase()))]
|
||||||
|
.filter((email) => !email.endsWith('.png') && !email.endsWith('.jpg') && !email.includes('example.com'))
|
||||||
|
.filter((email) => !email.includes('wixpress.com') && !email.includes('sentry.io'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractContactLinks(html, baseUrl) {
|
||||||
|
const links = [];
|
||||||
|
const regex = /href=["']([^"']+)["']/gi;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html))) {
|
||||||
|
const href = match[1];
|
||||||
|
if (/^(mailto:|tel:)/i.test(href)) continue;
|
||||||
|
if (!/(contact|about|team|booking|book|wedding|private-events|catering|visit|location)/i.test(href)) continue;
|
||||||
|
try {
|
||||||
|
const url = new URL(href, baseUrl);
|
||||||
|
if (url.hostname === new URL(baseUrl).hostname) {
|
||||||
|
url.hash = '';
|
||||||
|
links.push(url.toString());
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed links.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...new Set(links)].slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchText(url) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), 10000);
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'user-agent': 'QR Master lead research bot (+https://qrmaster.net/contact)',
|
||||||
|
accept: 'text/html,application/xhtml+xml',
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
redirect: 'follow',
|
||||||
|
});
|
||||||
|
if (!response.ok) return '';
|
||||||
|
const contentType = response.headers.get('content-type') || '';
|
||||||
|
if (!contentType.includes('text/html')) return '';
|
||||||
|
return await response.text();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enrichLead(lead) {
|
||||||
|
if (!lead.website || lead.email) {
|
||||||
|
return scoreLead(lead);
|
||||||
|
}
|
||||||
|
|
||||||
|
const homepage = await fetchText(lead.website);
|
||||||
|
const emails = visibleTextEmails(homepage);
|
||||||
|
const contactLinks = extractContactLinks(homepage, lead.website);
|
||||||
|
|
||||||
|
for (const link of contactLinks) {
|
||||||
|
if (emails.length > 0) break;
|
||||||
|
const html = await fetchText(link);
|
||||||
|
emails.push(...visibleTextEmails(html));
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueEmails = [...new Set(emails)];
|
||||||
|
if (uniqueEmails.length > 0) {
|
||||||
|
lead.email = uniqueEmails[0];
|
||||||
|
lead.email_source = 'website';
|
||||||
|
}
|
||||||
|
|
||||||
|
return scoreLead(lead);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreLead(lead) {
|
||||||
|
let score = 30;
|
||||||
|
if (lead.website) score += 20;
|
||||||
|
if (lead.email) score += 30;
|
||||||
|
if (lead.phone) score += 5;
|
||||||
|
if (!/(gmail|yahoo|hotmail|outlook|icloud)\.com$/i.test(lead.email || '')) score += lead.email ? 10 : 0;
|
||||||
|
if (lead.niche === 'real_estate' || lead.niche === 'restaurants') score += 5;
|
||||||
|
|
||||||
|
const signalByNiche = {
|
||||||
|
photographers: `${lead.company} can use dynamic QR codes on print cards, gallery cards, event handouts, and portfolio links.`,
|
||||||
|
restaurants: `${lead.company} can use dynamic QR codes for menus, table tents, reviews, coupons, and seasonal specials.`,
|
||||||
|
real_estate: `${lead.company} can use dynamic QR codes on yard signs, flyers, property sheets, and open house material.`,
|
||||||
|
events_venues: `${lead.company} can use dynamic QR codes for schedules, ticketing, venue maps, check-in, and post-event feedback.`,
|
||||||
|
wellness_beauty: `${lead.company} can use dynamic QR codes for booking pages, service menus, price lists, reviews, and loyalty offers.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
lead.lead_score = Math.min(score, 100);
|
||||||
|
lead.personalization_signal = signalByNiche[lead.niche] || '';
|
||||||
|
return lead;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mapLimit(items, limit, mapper) {
|
||||||
|
const results = [];
|
||||||
|
let index = 0;
|
||||||
|
async function worker() {
|
||||||
|
while (index < items.length) {
|
||||||
|
const current = index++;
|
||||||
|
results[current] = await mapper(items[current], current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectNiche(niche) {
|
||||||
|
const leadsByKey = new Map();
|
||||||
|
for (let pass = 0; pass < 2 && leadsByKey.size < TARGET_PER_NICHE * 2; pass++) {
|
||||||
|
for (const metro of metros) {
|
||||||
|
if (leadsByKey.size >= TARGET_PER_NICHE * 2) break;
|
||||||
|
const query = buildOverpassQuery(niche, metro, pass);
|
||||||
|
try {
|
||||||
|
const data = await fetchOverpass(query);
|
||||||
|
for (const element of data.elements || []) {
|
||||||
|
const lead = elementToLead(element, niche, metro);
|
||||||
|
if (!lead.company) continue;
|
||||||
|
if (!lead.website && !lead.email) continue;
|
||||||
|
const key = lead.website || `${lead.company}|${lead.city}|${lead.state}`.toLowerCase();
|
||||||
|
if (!leadsByKey.has(key)) leadsByKey.set(key, lead);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[${niche.id}] ${metro[0]} skipped: ${error.message}`);
|
||||||
|
}
|
||||||
|
await sleep(OVERPASS_DELAY_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawLeads = [...leadsByKey.values()].slice(0, TARGET_PER_NICHE * 2);
|
||||||
|
console.log(`[${niche.id}] collected ${rawLeads.length}; enriching...`);
|
||||||
|
const enriched = await mapLimit(rawLeads, CONCURRENCY, enrichLead);
|
||||||
|
return enriched
|
||||||
|
.filter((lead) => lead.website || lead.email)
|
||||||
|
.sort((a, b) => b.lead_score - a.lead_score)
|
||||||
|
.slice(0, TARGET_PER_NICHE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCsv(leads) {
|
||||||
|
const headers = [
|
||||||
|
'niche',
|
||||||
|
'niche_label',
|
||||||
|
'company',
|
||||||
|
'website',
|
||||||
|
'email',
|
||||||
|
'email_source',
|
||||||
|
'phone',
|
||||||
|
'city',
|
||||||
|
'state',
|
||||||
|
'country',
|
||||||
|
'street',
|
||||||
|
'lead_score',
|
||||||
|
'qr_use_case',
|
||||||
|
'personalization_signal',
|
||||||
|
'source',
|
||||||
|
'source_id',
|
||||||
|
'source_url',
|
||||||
|
'opt_out_required',
|
||||||
|
];
|
||||||
|
return [
|
||||||
|
headers.join(','),
|
||||||
|
...leads.map((lead) => headers.map((header) => csvEscape(lead[header])).join(',')),
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||||
|
const allLeads = [];
|
||||||
|
for (const niche of niches) {
|
||||||
|
const leads = await collectNiche(niche);
|
||||||
|
allLeads.push(...leads);
|
||||||
|
const dated = new Date().toISOString().slice(0, 10);
|
||||||
|
await fs.writeFile(path.join(OUTPUT_DIR, `qrmaster-us-leads-${niche.id}-${dated}.csv`), toCsv(leads), 'utf8');
|
||||||
|
await fs.writeFile(path.join(OUTPUT_DIR, `qrmaster-us-leads-${niche.id}-${dated}.json`), JSON.stringify(leads, null, 2), 'utf8');
|
||||||
|
console.log(`[${niche.id}] kept ${leads.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byKey = new Map();
|
||||||
|
for (const lead of allLeads) {
|
||||||
|
const key = lead.email || lead.website || `${lead.company}|${lead.city}|${lead.state}`.toLowerCase();
|
||||||
|
if (!byKey.has(key)) byKey.set(key, lead);
|
||||||
|
}
|
||||||
|
const deduped = [...byKey.values()].sort((a, b) => b.lead_score - a.lead_score);
|
||||||
|
const dated = new Date().toISOString().slice(0, 10);
|
||||||
|
const csvPath = path.join(OUTPUT_DIR, `qrmaster-us-leads-${dated}.csv`);
|
||||||
|
const jsonPath = path.join(OUTPUT_DIR, `qrmaster-us-leads-${dated}.json`);
|
||||||
|
await fs.writeFile(csvPath, toCsv(deduped), 'utf8');
|
||||||
|
await fs.writeFile(jsonPath, JSON.stringify(deduped, null, 2), 'utf8');
|
||||||
|
|
||||||
|
const summary = niches.map((niche) => {
|
||||||
|
const leads = deduped.filter((lead) => lead.niche === niche.id);
|
||||||
|
const withEmail = leads.filter((lead) => lead.email).length;
|
||||||
|
return `${niche.label}: ${leads.length} leads, ${withEmail} emails`;
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
console.log(`\nWrote ${deduped.length} leads`);
|
||||||
|
console.log(csvPath);
|
||||||
|
console.log(jsonPath);
|
||||||
|
console.log(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
326
scripts/validate-lead-emails.mjs
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
import { promises as dns } from "node:dns";
|
||||||
|
import { readdir, readFile, mkdir, writeFile, stat } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const root = process.cwd();
|
||||||
|
const leadRoot = path.resolve(root, process.argv[2] || "Leads");
|
||||||
|
const excludeFile = path.resolve(root, process.argv[3] || "Leads/lead_emails_1000_2026-05-25.csv");
|
||||||
|
const outputDir = path.resolve(root, process.argv[4] || "Leads/validated");
|
||||||
|
const dateStamp = new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
const emailPattern = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
||||||
|
const strictEmailPattern = /^[A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?(?:\.[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?)+$/i;
|
||||||
|
const allowedExtensions = new Set([".csv", ".txt", ".md", ".json"]);
|
||||||
|
const generatedPrefixes = [
|
||||||
|
"lead_email_validation_all_",
|
||||||
|
"lead_email_validation_valid_remaining_",
|
||||||
|
"lead_email_validation_unknown_remaining_",
|
||||||
|
"lead_email_validation_invalid_",
|
||||||
|
"lead_email_validation_summary_",
|
||||||
|
];
|
||||||
|
const blockedLeadDomains = new Set([
|
||||||
|
"qrmaster.net",
|
||||||
|
]);
|
||||||
|
const empiricalHighConfidenceDomains = new Set([
|
||||||
|
"gmail.com",
|
||||||
|
"googlemail.com",
|
||||||
|
"accor.com",
|
||||||
|
"hotelbb.com",
|
||||||
|
"losteria.de",
|
||||||
|
"breizhcafe.com",
|
||||||
|
]);
|
||||||
|
const empiricalLowConfidenceDomains = new Set([
|
||||||
|
"aon.at",
|
||||||
|
"countryinn.com",
|
||||||
|
"hilton.com",
|
||||||
|
"hyatt.com",
|
||||||
|
"motel-one.com",
|
||||||
|
"novum-hotels.de",
|
||||||
|
"riu.com",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function csvCell(value) {
|
||||||
|
const text = String(value ?? "");
|
||||||
|
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCsv(rows, columns) {
|
||||||
|
const lines = [columns.map(csvCell).join(",")];
|
||||||
|
for (const row of rows) {
|
||||||
|
lines.push(columns.map((column) => csvCell(row[column])).join(","));
|
||||||
|
}
|
||||||
|
return `${lines.join("\r\n")}\r\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectInputFiles(inputPath) {
|
||||||
|
const inputStat = await stat(inputPath);
|
||||||
|
if (inputStat.isFile()) {
|
||||||
|
return [inputPath];
|
||||||
|
}
|
||||||
|
if (!inputStat.isDirectory()) {
|
||||||
|
throw new Error(`Input path is not a file or directory: ${inputPath}`);
|
||||||
|
}
|
||||||
|
return walkFiles(inputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function walkFiles(dir) {
|
||||||
|
const entries = await readdir(dir, { withFileTypes: true });
|
||||||
|
const files = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
files.push(...await walkFiles(fullPath));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile()) continue;
|
||||||
|
if (!allowedExtensions.has(path.extname(entry.name).toLowerCase())) continue;
|
||||||
|
if (generatedPrefixes.some((prefix) => entry.name.startsWith(prefix))) continue;
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
return files.sort((a, b) => a.localeCompare(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEmailsFromFile(filePath) {
|
||||||
|
try {
|
||||||
|
const content = await readFile(filePath, "utf8");
|
||||||
|
return [...content.matchAll(emailPattern)].map((match) =>
|
||||||
|
match[0].trim().replace(/\.+$/, "").toLowerCase(),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadExcludedEmails(filePathsArg) {
|
||||||
|
const excluded = new Set();
|
||||||
|
const filePaths = String(filePathsArg || "")
|
||||||
|
.split(";")
|
||||||
|
.map((filePath) => filePath.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
for (const filePath of filePaths) {
|
||||||
|
try {
|
||||||
|
await stat(filePath);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const emails = await extractEmailsFromFile(filePath);
|
||||||
|
for (const email of emails) excluded.add(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
return excluded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withTimeout(promise, ms) {
|
||||||
|
return Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error("dns_timeout")), ms);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkDomain(domain) {
|
||||||
|
try {
|
||||||
|
const mxRecords = await withTimeout(dns.resolveMx(domain), 2500);
|
||||||
|
if (mxRecords.length > 0) {
|
||||||
|
return {
|
||||||
|
dns_status: "mx",
|
||||||
|
mx_hosts: mxRecords
|
||||||
|
.sort((a, b) => a.priority - b.priority)
|
||||||
|
.map((record) => record.exchange)
|
||||||
|
.join(";"),
|
||||||
|
reason: "domain_has_mx",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to A lookup. Some domains can receive via address fallback.
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const aRecords = await withTimeout(dns.resolve4(domain), 2000);
|
||||||
|
if (aRecords.length > 0) {
|
||||||
|
return {
|
||||||
|
dns_status: "a_only",
|
||||||
|
mx_hosts: "",
|
||||||
|
reason: "domain_has_a_record_but_no_mx",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Classified below.
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dns_status: "no_dns",
|
||||||
|
mx_hosts: "",
|
||||||
|
reason: "no_mx_or_a_record",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mapLimit(items, limit, worker) {
|
||||||
|
const results = new Map();
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
async function runWorker() {
|
||||||
|
while (index < items.length) {
|
||||||
|
const currentIndex = index++;
|
||||||
|
const item = items[currentIndex];
|
||||||
|
if ((currentIndex + 1) % 100 === 0) {
|
||||||
|
console.log(`DNS checked ${currentIndex + 1} / ${items.length} domains...`);
|
||||||
|
}
|
||||||
|
results.set(item, await worker(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runWorker));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfidence(status, domain) {
|
||||||
|
if (status !== "valid") {
|
||||||
|
return {
|
||||||
|
confidence: "reject",
|
||||||
|
confidence_reason: "not_dns_valid",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empiricalLowConfidenceDomains.has(domain)) {
|
||||||
|
return {
|
||||||
|
confidence: "low",
|
||||||
|
confidence_reason: "empirical_low_smartlead_valid_rate",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empiricalHighConfidenceDomains.has(domain)) {
|
||||||
|
return {
|
||||||
|
confidence: "high",
|
||||||
|
confidence_reason: "empirical_high_smartlead_valid_rate",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
confidence: "medium",
|
||||||
|
confidence_reason: "dns_valid_unproven_domain",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
const excludeEmails = await loadExcludedEmails(excludeFile);
|
||||||
|
const files = await collectInputFiles(leadRoot);
|
||||||
|
const emailSources = new Map();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const emails = await extractEmailsFromFile(file);
|
||||||
|
for (const email of emails) {
|
||||||
|
if (!emailSources.has(email)) emailSources.set(email, []);
|
||||||
|
const sources = emailSources.get(email);
|
||||||
|
if (sources.length < 5) sources.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const domains = [...new Set(
|
||||||
|
[...emailSources.keys()]
|
||||||
|
.filter((email) => strictEmailPattern.test(email))
|
||||||
|
.map((email) => email.split("@")[1]),
|
||||||
|
)].sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
|
console.log(`Files scanned: ${files.length}`);
|
||||||
|
console.log(`Unique emails found: ${emailSources.size}`);
|
||||||
|
console.log(`Domains to check: ${domains.length}`);
|
||||||
|
|
||||||
|
const dnsResults = await mapLimit(domains, 80, checkDomain);
|
||||||
|
|
||||||
|
const results = [...emailSources.keys()].sort((a, b) => a.localeCompare(b)).map((email) => {
|
||||||
|
const syntaxValid = strictEmailPattern.test(email);
|
||||||
|
const domain = email.includes("@") ? email.split("@")[1] : "";
|
||||||
|
const reserved = /^(example|test|invalid|localhost)(\.|$)/i.test(domain);
|
||||||
|
const dnsResult = dnsResults.get(domain);
|
||||||
|
|
||||||
|
let status = "invalid";
|
||||||
|
let reason = "invalid_syntax";
|
||||||
|
let dnsStatus = "";
|
||||||
|
let mxHosts = "";
|
||||||
|
|
||||||
|
if (syntaxValid && blockedLeadDomains.has(domain)) {
|
||||||
|
reason = "internal_or_generated_domain";
|
||||||
|
} else if (syntaxValid && reserved) {
|
||||||
|
reason = "reserved_or_test_domain";
|
||||||
|
} else if (syntaxValid && dnsResult?.dns_status === "mx") {
|
||||||
|
status = "valid";
|
||||||
|
reason = dnsResult.reason;
|
||||||
|
dnsStatus = dnsResult.dns_status;
|
||||||
|
mxHosts = dnsResult.mx_hosts;
|
||||||
|
} else if (syntaxValid && dnsResult?.dns_status === "a_only") {
|
||||||
|
status = "unknown";
|
||||||
|
reason = dnsResult.reason;
|
||||||
|
dnsStatus = dnsResult.dns_status;
|
||||||
|
} else if (syntaxValid) {
|
||||||
|
reason = dnsResult?.reason || "dns_not_checked";
|
||||||
|
dnsStatus = dnsResult?.dns_status || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const confidenceResult = getConfidence(status, domain);
|
||||||
|
|
||||||
|
return {
|
||||||
|
email,
|
||||||
|
status,
|
||||||
|
reason,
|
||||||
|
confidence: confidenceResult.confidence,
|
||||||
|
confidence_reason: confidenceResult.confidence_reason,
|
||||||
|
domain,
|
||||||
|
dns_status: dnsStatus,
|
||||||
|
mx_hosts: mxHosts,
|
||||||
|
already_uploaded: excludeEmails.has(email) ? "true" : "false",
|
||||||
|
source_count: emailSources.get(email).length,
|
||||||
|
first_source: emailSources.get(email)[0],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const allOut = path.join(outputDir, `lead_email_validation_all_${dateStamp}.csv`);
|
||||||
|
const validOut = path.join(outputDir, `lead_email_validation_valid_remaining_${dateStamp}.csv`);
|
||||||
|
const highConfidenceOut = path.join(outputDir, `lead_email_validation_high_confidence_remaining_${dateStamp}.csv`);
|
||||||
|
const unknownOut = path.join(outputDir, `lead_email_validation_unknown_remaining_${dateStamp}.csv`);
|
||||||
|
const invalidOut = path.join(outputDir, `lead_email_validation_invalid_${dateStamp}.csv`);
|
||||||
|
const summaryOut = path.join(outputDir, `lead_email_validation_summary_${dateStamp}.txt`);
|
||||||
|
|
||||||
|
const validRemaining = results.filter((row) => row.status === "valid" && row.already_uploaded !== "true");
|
||||||
|
const highConfidenceRemaining = results.filter((row) =>
|
||||||
|
row.status === "valid" &&
|
||||||
|
row.confidence === "high" &&
|
||||||
|
row.already_uploaded !== "true"
|
||||||
|
);
|
||||||
|
const unknownRemaining = results.filter((row) => row.status === "unknown" && row.already_uploaded !== "true");
|
||||||
|
const invalid = results.filter((row) => row.status === "invalid");
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
allOut,
|
||||||
|
toCsv(results, ["email", "status", "reason", "confidence", "confidence_reason", "domain", "dns_status", "mx_hosts", "already_uploaded", "source_count", "first_source"]),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
await writeFile(validOut, toCsv(validRemaining.map(({ email }) => ({ email })), ["email"]), "utf8");
|
||||||
|
await writeFile(highConfidenceOut, toCsv(highConfidenceRemaining.map(({ email }) => ({ email })), ["email"]), "utf8");
|
||||||
|
await writeFile(unknownOut, toCsv(unknownRemaining, ["email", "reason", "domain"]), "utf8");
|
||||||
|
await writeFile(invalidOut, toCsv(invalid, ["email", "reason", "domain"]), "utf8");
|
||||||
|
|
||||||
|
const summary = [
|
||||||
|
`Lead email validation summary - ${dateStamp}`,
|
||||||
|
`Lead root: ${leadRoot}`,
|
||||||
|
`Files scanned: ${files.length}`,
|
||||||
|
`Unique emails found: ${results.length}`,
|
||||||
|
`Already uploaded/excluded: ${results.filter((row) => row.already_uploaded === "true").length}`,
|
||||||
|
`Valid total: ${results.filter((row) => row.status === "valid").length}`,
|
||||||
|
`Valid remaining: ${validRemaining.length}`,
|
||||||
|
`High-confidence valid remaining: ${highConfidenceRemaining.length}`,
|
||||||
|
`Unknown remaining: ${unknownRemaining.length}`,
|
||||||
|
`Invalid total: ${invalid.length}`,
|
||||||
|
`All report: ${allOut}`,
|
||||||
|
`Valid remaining upload file: ${validOut}`,
|
||||||
|
`High-confidence upload file: ${highConfidenceOut}`,
|
||||||
|
`Unknown remaining review file: ${unknownOut}`,
|
||||||
|
`Invalid report: ${invalidOut}`,
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
await writeFile(summaryOut, summary, "utf8");
|
||||||
|
|
||||||
|
console.log(summary);
|
||||||
312
social-content-30-days.md
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
# 30-Day Social Content Plan for QR Master
|
||||||
|
|
||||||
|
## Positioning
|
||||||
|
- Product: QR Master
|
||||||
|
- Angle: Dynamic QR codes, scan analytics, bulk creation, privacy-first workflows
|
||||||
|
- Goal: Brand awareness, traffic, signups, and founder-style credibility
|
||||||
|
- Audience: Restaurants, agencies, events, retail/packaging, operations-heavy SMBs
|
||||||
|
|
||||||
|
## Content Pillars
|
||||||
|
1. Pain and cost of static QR codes
|
||||||
|
2. Dynamic QR value and flexibility
|
||||||
|
3. Analytics and measurable ROI
|
||||||
|
4. Bulk creation and operational scale
|
||||||
|
5. Privacy-first / GDPR-friendly trust
|
||||||
|
6. Build in public / founder narrative
|
||||||
|
|
||||||
|
## Content Mix Target
|
||||||
|
- Educational: 18 days (60%)
|
||||||
|
- Storytelling: 8 days (27%)
|
||||||
|
- Selling: 4 days (13%)
|
||||||
|
- Note: This is close to the requested 60/25/15 split and avoids forcing weak promo posts.
|
||||||
|
|
||||||
|
## A/B Testing Setup
|
||||||
|
Use the month as a simple content experiment instead of 30 disconnected posts.
|
||||||
|
|
||||||
|
### Test 1: Hook Style
|
||||||
|
- A: Pain-led hook
|
||||||
|
- B: ROI-led hook
|
||||||
|
- Primary metric: Engagement rate on X
|
||||||
|
- Secondary metric: Profile clicks
|
||||||
|
|
||||||
|
### Test 2: CTA Style
|
||||||
|
- A: Soft CTA ("Curious how your team handles this?")
|
||||||
|
- B: Direct CTA ("Try QR Master")
|
||||||
|
- Primary metric: Link clicks
|
||||||
|
- Guardrail: Engagement rate should not drop sharply
|
||||||
|
|
||||||
|
### Test 3: Proof Angle
|
||||||
|
- A: Cost-saving proof
|
||||||
|
- B: Privacy/GDPR proof
|
||||||
|
- Primary metric: Saves and comments
|
||||||
|
|
||||||
|
### Test 4: Visual Angle
|
||||||
|
- A: Real-world print use case
|
||||||
|
- B: Product/dashboard visual
|
||||||
|
- Primary metric: Instagram saves
|
||||||
|
|
||||||
|
### Tracking Notes
|
||||||
|
- Run A/B by alternating styles every other relevant day
|
||||||
|
- Do not change the post mid-test
|
||||||
|
- Review after Days 10, 20, and 30
|
||||||
|
- Best early KPI set: impressions, engagement rate, profile visits, link clicks, comments
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 30-Day Calendar
|
||||||
|
|
||||||
|
### Day 1
|
||||||
|
- Content type: Selling
|
||||||
|
- X: Static QR codes are easy to generate. The expensive part starts when the link changes after print. QR Master helps you update destinations after print, track scans, and avoid unnecessary reprints.
|
||||||
|
- Facebook: Most businesses do not have a QR code problem. They have a QR management problem. QR Master helps teams update links after print, measure scans, and stay flexible without starting over every time something changes.
|
||||||
|
- Instagram: Printing a QR code is easy. Managing it after the link changes is where it gets expensive. QR Master gives you dynamic QR codes, scan analytics, and a more professional workflow. Link in bio.
|
||||||
|
- Image prompt: Premium 4:5 SaaS visual showing printed flyers and menus with QR codes beside a smartphone dashboard, modern cafe or business setting, high-end product photography, soft daylight, bold headline space, deep green and charcoal palette.
|
||||||
|
- X Community: Build in Public: We are building QR Master around one simple pain point: the real problem is not creating QR codes, it is managing them after they are already printed.
|
||||||
|
|
||||||
|
### Day 2
|
||||||
|
- Content type: Educational
|
||||||
|
- X: A wrong link on a printed flyer is not a design mistake. It is an operational cost. Dynamic QR codes fix that.
|
||||||
|
- Facebook: One small URL change can turn printed materials into waste. Dynamic QR codes let your team update the destination without reprinting everything.
|
||||||
|
- Instagram: One printed QR code. Multiple future changes. That is the difference between static and dynamic.
|
||||||
|
- Image prompt: Minimal close-up of a printed flyer with a QR code and a red "old link" concept contrasted with a clean updated mobile dashboard.
|
||||||
|
- X Community: Startup Community: Simple products win when they remove expensive friction. For us, that friction is reprinting because one QR destination changed.
|
||||||
|
|
||||||
|
### Day 3
|
||||||
|
- Content type: Educational
|
||||||
|
- X: If you cannot measure scans, your QR code is just decoration. Analytics turns it into a channel.
|
||||||
|
- Facebook: QR codes become more valuable when they are measurable. Scan analytics help you understand which campaigns, materials, and locations are actually working.
|
||||||
|
- Instagram: A QR code should not just send traffic. It should give you visibility.
|
||||||
|
- Image prompt: Sleek phone screen with analytics metrics next to printed marketing assets, premium SaaS dashboard aesthetic.
|
||||||
|
- X Community: No Code Community: Curious how no-code teams handle QR tracking today. Most tools generate the code. Fewer help manage and measure it.
|
||||||
|
|
||||||
|
### Day 4
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Restaurants do not want to reprint menus every time something changes. They want one QR code that keeps working.
|
||||||
|
- Facebook: For restaurants, dynamic QR codes are not a nice-to-have. They are a practical way to handle menu updates without reprinting every time.
|
||||||
|
- Instagram: One menu QR. Update anytime. Less printing, less stress, more flexibility.
|
||||||
|
- Image prompt: Elegant restaurant table with menu stand QR code, phone showing updated menu destination, warm lighting, premium hospitality look.
|
||||||
|
- X Community: Build in Public: One of our strongest use cases is restaurants. The pain is obvious: menu changes are frequent, reprints are annoying, and speed matters.
|
||||||
|
|
||||||
|
### Day 5
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Bulk QR creation is underrated. Creating one code is easy. Creating 500 cleanly is a workflow.
|
||||||
|
- Facebook: Bulk creation matters when your team works with packaging, labels, campaigns, or events at scale. That is where simple generators usually fall apart.
|
||||||
|
- Instagram: One QR code is a task. 500 QR codes is an operation.
|
||||||
|
- Image prompt: Spreadsheet to QR workflow visual, CSV rows transforming into branded QR code sheets, modern SaaS illustration-photo hybrid.
|
||||||
|
- X Community: Startup Community: A lot of software looks useful in demos. Bulk workflows are where you find out whether it is a product or just a feature.
|
||||||
|
|
||||||
|
### Day 6
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Privacy matters more than marketers admit. If your QR analytics ignore GDPR realities, that becomes a risk, not a feature.
|
||||||
|
- Facebook: Many teams want scan data, but they also want a cleaner privacy story. That is why privacy-first analytics matter.
|
||||||
|
- Instagram: Better analytics should not come with worse privacy.
|
||||||
|
- Image prompt: Clean dashboard plus subtle privacy shield iconography, professional B2B look, no cyber-security cliches, muted green palette.
|
||||||
|
- X Community: No Code Community: How are builders here balancing analytics and privacy? That tradeoff shows up fast once QR workflows become client-facing.
|
||||||
|
|
||||||
|
### Day 7
|
||||||
|
- Content type: Storytelling
|
||||||
|
- X: Build in public note: one of our clearest positioning lessons has been this. We are not trying to be another QR code generator. We are building QR Master as a management layer for printed-to-digital workflows.
|
||||||
|
- Facebook: Our product direction is simple: less focus on generating a code, more focus on changing, measuring, and scaling it after launch.
|
||||||
|
- Instagram: Less QR generator. More QR workflow.
|
||||||
|
- Image prompt: Founder-style product shot with dashboard on laptop and printed assets on desk, calm European startup aesthetic.
|
||||||
|
- X Community: Build in Public: Positioning insight: "QR generator" is crowded. "Professional QR workflow" is much more interesting.
|
||||||
|
|
||||||
|
### Day 8
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Pain-led test: Static QR codes are cheap until the campaign URL changes. Then they get expensive fast.
|
||||||
|
- Facebook: Static QR codes seem low-cost at first. The real cost appears later when you need to update the destination and your materials are already printed.
|
||||||
|
- Instagram: Cheap to generate. Expensive to fix later.
|
||||||
|
- Image prompt: Bold split-scene visual showing cheap creation on one side, expensive reprint boxes on the other.
|
||||||
|
- X Community: Startup Community: People buy the "easy setup." They stay for the avoided operational mess.
|
||||||
|
|
||||||
|
### Day 9
|
||||||
|
- Content type: Educational
|
||||||
|
- X: ROI-led test: A dynamic QR code can save far more in reprint cost than it costs to use.
|
||||||
|
- Facebook: The ROI of dynamic QR codes is not theoretical. It comes from avoiding waste, moving faster, and keeping printed assets flexible.
|
||||||
|
- Instagram: Dynamic QR codes are not just more flexible. They are often the cheaper decision.
|
||||||
|
- Image prompt: ROI-focused business visual with printed assets, subtle savings graph, clean premium B2B layout.
|
||||||
|
- X Community: No Code Community: Great no-code workflows reduce manual rework. Dynamic QR management fits that exact pattern.
|
||||||
|
|
||||||
|
### Day 10
|
||||||
|
- Content type: Storytelling
|
||||||
|
- X: One pattern we keep noticing while building QR Master: businesses already have offline attention. The missing piece is knowing what happens after the scan.
|
||||||
|
- Facebook: A recurring insight from this space is that printed materials already do part of the job. What businesses often lack is visibility into what happens after someone scans.
|
||||||
|
- Instagram: Offline attention is already there. Measurement is the missing layer.
|
||||||
|
- Image prompt: Product packaging with QR code connected visually to analytics dashboard, premium retail look.
|
||||||
|
- X Community: Build in Public: Big theme we keep coming back to: printed materials should not be dead ends.
|
||||||
|
|
||||||
|
### Day 11
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Most QR tools optimize for generation. Businesses actually need flexibility after launch.
|
||||||
|
- Facebook: The real business value is not in making the first QR code. It is in being able to adapt when your link, offer, or content changes later.
|
||||||
|
- Instagram: The first QR code is easy. The second version is where the product matters.
|
||||||
|
- Image prompt: Clean product UI showing edit destination flow, minimal dashboard-first composition.
|
||||||
|
- X Community: Startup Community: Good SaaS often wins by focusing on the "after setup" problem.
|
||||||
|
|
||||||
|
### Day 12
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Menus, event flyers, table cards, packaging inserts, product labels. Same pattern: print once, update later.
|
||||||
|
- Facebook: We like QR workflows because they solve the same operational problem across very different industries: printed assets need flexibility.
|
||||||
|
- Instagram: Print once. Update later. Repeat without chaos.
|
||||||
|
- Image prompt: Collage of multiple real-world QR use cases, menus, event badges, labels, packaging, cohesive premium visual treatment.
|
||||||
|
- X Community: No Code Community: Cross-industry tools usually win when the workflow pain is the same even if the use case looks different.
|
||||||
|
|
||||||
|
### Day 13
|
||||||
|
- Content type: Selling
|
||||||
|
- X: Direct CTA test: If your team relies on printed assets, try QR Master and stop treating every link change like a mini-crisis.
|
||||||
|
- Facebook: If printed materials are part of your workflow, QR Master helps turn them into something more flexible, measurable, and easier to manage.
|
||||||
|
- Instagram: If print is still part of your business, your QR workflow should be better than "hope the link never changes."
|
||||||
|
- Image prompt: Conversion-focused SaaS ad visual with CTA space, clean printed collateral and dashboard.
|
||||||
|
- X Community: Build in Public: Testing more direct CTA language this week to see whether practical urgency beats softer education.
|
||||||
|
|
||||||
|
### Day 14
|
||||||
|
- Content type: Storytelling
|
||||||
|
- X: One question we keep coming back to while building QR Master: how are teams actually handling QR updates after materials are already printed?
|
||||||
|
- Facebook: The more we look at real QR workflows, the more this question matters: what does your team do when a flyer, menu, or package is already out in the world and the destination changes?
|
||||||
|
- Instagram: Quick founder question: how are you handling QR updates after print today?
|
||||||
|
- Image prompt: Question-led social visual with neutral high-end workspace, printed assets and phone, softer editorial composition.
|
||||||
|
- X Community: Startup Community: Sometimes the best growth post is just a clear question around a painful workflow.
|
||||||
|
|
||||||
|
### Day 15
|
||||||
|
- Content type: Educational
|
||||||
|
- X: QR analytics are especially useful for campaigns that live partly offline. You can finally see what print is doing.
|
||||||
|
- Facebook: For teams running print campaigns, QR analytics add something valuable: measurable outcomes instead of guesswork.
|
||||||
|
- Instagram: Print does not have to be unmeasurable anymore.
|
||||||
|
- Image prompt: Marketing campaign board, flyer, poster, and analytics dashboard visual, sophisticated marketing ops style.
|
||||||
|
- X Community: No Code Community: Offline-to-online attribution is still messy. QR analytics can simplify part of it.
|
||||||
|
|
||||||
|
### Day 16
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Privacy proof test: Scan tracking should not force businesses into a weak privacy position. Better analytics and better privacy can coexist.
|
||||||
|
- Facebook: Privacy-conscious analytics matter for teams that need measurement without creating unnecessary legal or trust issues.
|
||||||
|
- Instagram: Better data. Cleaner privacy story.
|
||||||
|
- Image prompt: Refined dashboard visual with subtle compliance or privacy cues, no overly technical design.
|
||||||
|
- X Community: Build in Public: We think privacy-first messaging is underused in SaaS until buyers start asking hard questions.
|
||||||
|
|
||||||
|
### Day 17
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Cost-saving proof test: Reprinting is not just annoying. It is a hidden cost line that dynamic QR codes reduce.
|
||||||
|
- Facebook: One of the clearest benefits of dynamic QR codes is simple: fewer reprints, less waste, fewer operational delays.
|
||||||
|
- Instagram: Reprints are a hidden tax on bad QR workflows.
|
||||||
|
- Image prompt: Stacked reprint boxes and invoices contrasted with one reusable dynamic QR code concept.
|
||||||
|
- X Community: Startup Community: Cost savings posts feel less exciting, but they often convert better because the pain is immediate.
|
||||||
|
|
||||||
|
### Day 18
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Agencies need QR workflows too. Campaigns change. Landing pages change. Tracking matters. Scale matters.
|
||||||
|
- Facebook: Agencies work across multiple campaigns and client assets. That makes dynamic management and analytics much more valuable than one-off QR generation.
|
||||||
|
- Instagram: Agency-friendly QR workflows are less about design and more about change management.
|
||||||
|
- Image prompt: Agency desk scene with campaign mockups, client materials, and dashboard metrics, polished B2B style.
|
||||||
|
- X Community: No Code Community: Agencies using no-code stacks still run into the same QR issue: clients change things after launch.
|
||||||
|
|
||||||
|
### Day 19
|
||||||
|
- Content type: Storytelling
|
||||||
|
- X: Watching event workflows makes the value of dynamic QR codes obvious fast. Schedules shift, pages update, logistics move quickly, and static links do not keep up.
|
||||||
|
- Facebook: Event teams are one of the clearest reminders that QR workflows are operational, not just visual. Plans shift, registration pages change, and materials are already out in the world.
|
||||||
|
- Instagram: Event workflows make one thing obvious: static links age badly.
|
||||||
|
- Image prompt: Event badge, poster, and registration QR concept with energetic but premium event aesthetic.
|
||||||
|
- X Community: Build in Public: Events keep reminding us why "print once, update later" is such a durable use case.
|
||||||
|
|
||||||
|
### Day 20
|
||||||
|
- Content type: Educational
|
||||||
|
- X: A QR code should be part of a workflow, not a one-time asset.
|
||||||
|
- Facebook: Businesses get more value from QR codes when they treat them as living assets tied to campaigns, updates, analytics, and operations.
|
||||||
|
- Instagram: Stop thinking of QR codes as files. Start thinking of them as workflows.
|
||||||
|
- Image prompt: Clean systems-style visual showing QR code lifecycle from creation to update to analytics.
|
||||||
|
- X Community: Startup Community: Framing matters. Turning a "file" into a "workflow" changes the whole product category.
|
||||||
|
|
||||||
|
### Day 21
|
||||||
|
- Content type: Storytelling
|
||||||
|
- X: Build in public lesson: we keep seeing the same insight. Simpler products get stronger when the pain is operational, specific, and expensive.
|
||||||
|
- Facebook: Product clarity improves when you stay close to a narrow operational pain point. For us, that is managing QR destinations after print.
|
||||||
|
- Instagram: Specific pain points beat vague product categories.
|
||||||
|
- Image prompt: Founder desk with notes, dashboard, and printed QR materials, documentary startup mood.
|
||||||
|
- X Community: Build in Public: Another reminder that niche pain points are often more valuable than broad feature lists.
|
||||||
|
|
||||||
|
### Day 22
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Most QR code conversations focus on the front end: create, style, download. The better conversation starts after that.
|
||||||
|
- Facebook: The real workflow begins after the QR code is created. That is where updates, measurement, scale, and responsibility show up.
|
||||||
|
- Instagram: Creation is step one. Management is the product.
|
||||||
|
- Image prompt: Step-one vs step-two contrast visual, with generator UI fading into management dashboard.
|
||||||
|
- X Community: Startup Community: Good positioning often starts by shifting the buyer's frame from setup to ongoing management.
|
||||||
|
|
||||||
|
### Day 23
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Packaging is an underrated QR use case. Once labels are printed, flexibility matters even more.
|
||||||
|
- Facebook: Product packaging often needs durable, scalable QR workflows because changes later are expensive and slow. Dynamic QR management becomes much more valuable there.
|
||||||
|
- Instagram: Packaging turns QR mistakes into real inventory pain.
|
||||||
|
- Image prompt: Premium product packaging close-up with QR code linked to mobile experience and analytics.
|
||||||
|
- X Community: No Code Community: Packaging workflows are a good example of where "simple generator" stops being enough.
|
||||||
|
|
||||||
|
### Day 24
|
||||||
|
- Content type: Educational
|
||||||
|
- X: If your QR code points to a page you know will change, making it static is usually the wrong decision.
|
||||||
|
- Facebook: A useful rule of thumb: if the destination may change later, the QR code should probably be dynamic from day one.
|
||||||
|
- Instagram: If the destination can change, the QR should too.
|
||||||
|
- Image prompt: Minimal rule-of-thumb visual, modern typography-led design with QR asset and phone.
|
||||||
|
- X Community: Build in Public: Strong product messaging often comes from one obvious rule buyers can remember instantly.
|
||||||
|
|
||||||
|
### Day 25
|
||||||
|
- Content type: Storytelling
|
||||||
|
- X: One thing we have learned from talking about QR workflows publicly: the frustrating part is almost never creating the code. What is the most annoying part after that for your team?
|
||||||
|
- Facebook: The more conversations we have around QR workflows, the clearer the pattern becomes. The frustrating part is usually not creation. It is everything that happens after. What is the most annoying part for your team?
|
||||||
|
- Instagram: What is the most annoying part of QR management after the code is already live?
|
||||||
|
- Image prompt: Community-question visual, clean desk setup with comments or chat overlay concept.
|
||||||
|
- X Community: Startup Community: Asking for workflow pain often gives better product insight than asking for feature requests.
|
||||||
|
|
||||||
|
### Day 26
|
||||||
|
- Content type: Selling
|
||||||
|
- X: Direct CTA: If you are still rebuilding or reprinting every time a QR destination changes, it is probably time for a better setup.
|
||||||
|
- Facebook: Teams that rely on print, packaging, menus, or campaigns need more than a basic QR generator. That is exactly where QR Master fits.
|
||||||
|
- Instagram: If QR changes still create operational stress, your setup is too fragile.
|
||||||
|
- Image prompt: Strong CTA ad visual with real-world printed assets and dashboard, premium enterprise-lite feel.
|
||||||
|
- X Community: No Code Community: The best tools remove repetitive rework. QR workflow pain is full of repetitive rework.
|
||||||
|
|
||||||
|
### Day 27
|
||||||
|
- Content type: Educational
|
||||||
|
- X: Analytics are not just for dashboards. They help teams make better decisions about campaigns, placement, and performance.
|
||||||
|
- Facebook: Scan analytics are useful because they give teams feedback loops. Better placement, better campaign decisions, better understanding of what performs.
|
||||||
|
- Instagram: Better QR analytics means better decisions, not just prettier charts.
|
||||||
|
- Image prompt: Dashboard-centric visual with practical metrics overlay, sophisticated marketing operations style.
|
||||||
|
- X Community: Build in Public: We are leaning into "analytics as decisions," not just "analytics as reporting."
|
||||||
|
|
||||||
|
### Day 28
|
||||||
|
- Content type: Storytelling
|
||||||
|
- X: QR Master sits in an interesting space between marketing ops, print workflows, and privacy-conscious SaaS. That mix has become clearer the more we build and talk to people.
|
||||||
|
- Facebook: Some products become clearer over time. Ours looks less like a simple utility and more like infrastructure for print-to-digital workflows the more conversations we have.
|
||||||
|
- Instagram: Utility at first glance. Workflow product underneath. That became clearer over time.
|
||||||
|
- Image prompt: Abstract but premium ecosystem visual connecting print, mobile, and dashboard layers.
|
||||||
|
- X Community: Startup Community: Category clarity often appears after enough user conversations, not before.
|
||||||
|
|
||||||
|
### Day 29
|
||||||
|
- Content type: Storytelling
|
||||||
|
- X: One thing this category teaches quickly: most businesses do not care about dynamic QR codes until the day they really need them. Then the value becomes obvious fast.
|
||||||
|
- Facebook: Dynamic QR codes are one of those categories that feel optional until the first broken link, campaign change, or urgent update after print. Then the whole value proposition clicks.
|
||||||
|
- Instagram: Optional until the first mistake. Essential right after.
|
||||||
|
- Image prompt: Tension-driven visual with urgent update scenario, premium but emotionally clear composition.
|
||||||
|
- X Community: No Code Community: This is one of those "you do not care until you absolutely care" workflow categories.
|
||||||
|
|
||||||
|
### Day 30
|
||||||
|
- Content type: Selling
|
||||||
|
- X: Month-end takeaway: QR codes are not just assets. They are decision points between static friction and dynamic flexibility. That is the category we are building for with QR Master. If that sounds like your workflow, try it.
|
||||||
|
- Facebook: After a month of talking about QR workflows, the pattern is clear: businesses want flexibility after print, measurement without guesswork, and workflows that scale without adding chaos. That is exactly what QR Master is built for.
|
||||||
|
- Instagram: QR Master is built for one simple outcome: make printed QR workflows easier to change, measure, and scale.
|
||||||
|
- Image prompt: Premium month-end brand visual summarizing QR Master with printed assets, analytics dashboard, and trust-focused SaaS composition.
|
||||||
|
- X Community: Build in Public: Month-end positioning summary: we are not building for "make a QR code." We are building for "manage what happens after print."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Weekly Review Template
|
||||||
|
- Top 3 posts by engagement rate
|
||||||
|
- Top 3 posts by profile clicks
|
||||||
|
- Best-performing hook type: pain-led or ROI-led
|
||||||
|
- Best-performing CTA type: soft or direct
|
||||||
|
- Best-performing proof angle: cost-saving or privacy
|
||||||
|
- Best-performing visual style: real-world or dashboard
|
||||||
|
|
||||||
|
## Reuse Notes
|
||||||
|
- Turn the best X posts into threads in month 2
|
||||||
|
- Turn the best Facebook posts into landing page angles
|
||||||
|
- Turn the best Instagram posts into carousel scripts
|
||||||
|
- Use top-performing community questions as future feature research prompts
|
||||||
263
sql/revops_onboarding_v1.sql
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- 1 user columns
|
||||||
|
ALTER TABLE "User"
|
||||||
|
ADD COLUMN IF NOT EXISTS "signupSource" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "signupSourceSelfReported" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "signupMedium" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "signupCampaign" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "signupContent" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "signupTerm" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "signupReferrer" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "signupLandingPath" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "signupFirstSeenAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "emailDomain" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "primaryUseCase" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "primaryGoal" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "jobRole" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "companyName" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "companyWebsite" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "teamSizeBucket" TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS "onboardingStartedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "sourceConfirmedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "useCaseSelectedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "goalSelectedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "profileCompletedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "firstQrCreatedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "firstDynamicQrAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "firstStaticQrAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "firstScanAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "activationAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "onboardingCompletedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "fitScore" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS "intentScore" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS "leadScore" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS "lifecycleStage" TEXT NOT NULL DEFAULT 'cold',
|
||||||
|
ADD COLUMN IF NOT EXISTS "lastQualifiedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN IF NOT EXISTS "lastScoredAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'User_lifecycleStage_check'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "User"
|
||||||
|
ADD CONSTRAINT "User_lifecycleStage_check"
|
||||||
|
CHECK ("lifecycleStage" IN (
|
||||||
|
'cold',
|
||||||
|
'activated',
|
||||||
|
'warm',
|
||||||
|
'hot',
|
||||||
|
'upgrade_candidate',
|
||||||
|
'paid'
|
||||||
|
));
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 2 lifecycle log table
|
||||||
|
CREATE TABLE IF NOT EXISTS "UserLifecycleLog" (
|
||||||
|
"id" TEXT PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"fromStage" TEXT,
|
||||||
|
"toStage" TEXT NOT NULL,
|
||||||
|
"fitScore" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"intentScore" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"leadScore" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"reason" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "UserLifecycleLog_userId_fkey"
|
||||||
|
FOREIGN KEY ("userId") REFERENCES "User"("id")
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3 indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_signupSource_idx" ON "User" ("signupSource");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_signupSourceSelfReported_idx" ON "User" ("signupSourceSelfReported");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_signupCampaign_idx" ON "User" ("signupCampaign");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_signupLandingPath_idx" ON "User" ("signupLandingPath");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_emailDomain_idx" ON "User" ("emailDomain");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_primaryUseCase_idx" ON "User" ("primaryUseCase");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_primaryGoal_idx" ON "User" ("primaryGoal");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_jobRole_idx" ON "User" ("jobRole");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_teamSizeBucket_idx" ON "User" ("teamSizeBucket");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_lifecycleStage_idx" ON "User" ("lifecycleStage");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_leadScore_idx" ON "User" ("leadScore" DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_activationAt_idx" ON "User" ("activationAt");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_firstScanAt_idx" ON "User" ("firstScanAt");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_firstQrCreatedAt_idx" ON "User" ("firstQrCreatedAt");
|
||||||
|
CREATE INDEX IF NOT EXISTS "User_lastQualifiedAt_idx" ON "User" ("lastQualifiedAt");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "UserLifecycleLog_userId_idx" ON "UserLifecycleLog" ("userId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "UserLifecycleLog_toStage_idx" ON "UserLifecycleLog" ("toStage");
|
||||||
|
CREATE INDEX IF NOT EXISTS "UserLifecycleLog_createdAt_idx" ON "UserLifecycleLog" ("createdAt" DESC);
|
||||||
|
|
||||||
|
-- 4 backfill
|
||||||
|
UPDATE "User"
|
||||||
|
SET "emailDomain" = lower(split_part("email", '@', 2))
|
||||||
|
WHERE "email" IS NOT NULL
|
||||||
|
AND ("emailDomain" IS NULL OR "emailDomain" = '');
|
||||||
|
|
||||||
|
UPDATE "User" u
|
||||||
|
SET
|
||||||
|
"firstQrCreatedAt" = q."firstQrAt",
|
||||||
|
"firstDynamicQrAt" = q."firstDynamicQrAt",
|
||||||
|
"firstStaticQrAt" = q."firstStaticQrAt",
|
||||||
|
"onboardingCompletedAt" = COALESCE(u."onboardingCompletedAt", q."firstQrAt")
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
"userId",
|
||||||
|
MIN("createdAt") AS "firstQrAt",
|
||||||
|
MIN("createdAt") FILTER (WHERE "type" = 'DYNAMIC') AS "firstDynamicQrAt",
|
||||||
|
MIN("createdAt") FILTER (WHERE "type" = 'STATIC') AS "firstStaticQrAt"
|
||||||
|
FROM "QRCode"
|
||||||
|
GROUP BY "userId"
|
||||||
|
) q
|
||||||
|
WHERE u."id" = q."userId";
|
||||||
|
|
||||||
|
UPDATE "User" u
|
||||||
|
SET
|
||||||
|
"firstScanAt" = s."firstScanAt",
|
||||||
|
"activationAt" = COALESCE(u."activationAt", s."firstScanAt")
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
q."userId",
|
||||||
|
MIN(s."ts") AS "firstScanAt"
|
||||||
|
FROM "QRCode" q
|
||||||
|
INNER JOIN "QRScan" s ON s."qrId" = q."id"
|
||||||
|
GROUP BY q."userId"
|
||||||
|
) s
|
||||||
|
WHERE u."id" = s."userId";
|
||||||
|
|
||||||
|
-- 5 scoring
|
||||||
|
WITH qr_stats AS (
|
||||||
|
SELECT
|
||||||
|
q."userId",
|
||||||
|
COUNT(*) AS qr_count,
|
||||||
|
COUNT(*) FILTER (WHERE q."type" = 'DYNAMIC') AS dynamic_count,
|
||||||
|
COUNT(DISTINCT q."contentType") AS content_type_count,
|
||||||
|
COUNT(*) FILTER (WHERE q."contentType" IN ('BARCODE','PDF','VCARD','COUPON','FEEDBACK')) AS businessish_type_count
|
||||||
|
FROM "QRCode" q
|
||||||
|
GROUP BY q."userId"
|
||||||
|
),
|
||||||
|
scan_stats AS (
|
||||||
|
SELECT
|
||||||
|
q."userId",
|
||||||
|
COUNT(s."id") AS scan_count
|
||||||
|
FROM "QRCode" q
|
||||||
|
LEFT JOIN "QRScan" s ON s."qrId" = q."id"
|
||||||
|
GROUP BY q."userId"
|
||||||
|
),
|
||||||
|
scored AS (
|
||||||
|
SELECT
|
||||||
|
u."id",
|
||||||
|
COALESCE(qs.qr_count, 0) AS qr_count,
|
||||||
|
COALESCE(qs.dynamic_count, 0) AS dynamic_count,
|
||||||
|
COALESCE(qs.content_type_count, 0) AS content_type_count,
|
||||||
|
COALESCE(qs.businessish_type_count, 0) AS businessish_type_count,
|
||||||
|
COALESCE(ss.scan_count, 0) AS scan_count
|
||||||
|
FROM "User" u
|
||||||
|
LEFT JOIN qr_stats qs ON qs."userId" = u."id"
|
||||||
|
LEFT JOIN scan_stats ss ON ss."userId" = u."id"
|
||||||
|
)
|
||||||
|
UPDATE "User" u
|
||||||
|
SET
|
||||||
|
"fitScore" =
|
||||||
|
(CASE
|
||||||
|
WHEN lower(split_part(u."email", '@', 2)) IN ('gmail.com','yahoo.com','hotmail.com','outlook.com','icloud.com') THEN -15
|
||||||
|
WHEN u."email" IS NOT NULL THEN 20
|
||||||
|
ELSE 0
|
||||||
|
END)
|
||||||
|
+
|
||||||
|
(CASE
|
||||||
|
WHEN u."primaryUseCase" IN ('marketing_campaign','bulk_qr','menu_pdf','barcode') THEN 10
|
||||||
|
ELSE 0
|
||||||
|
END)
|
||||||
|
+
|
||||||
|
(CASE
|
||||||
|
WHEN u."primaryGoal" IN ('track_printed_campaigns','generate_leads','manage_multiple_qr_codes') THEN 10
|
||||||
|
ELSE 0
|
||||||
|
END)
|
||||||
|
+
|
||||||
|
(CASE
|
||||||
|
WHEN u."jobRole" IN ('founder_owner','marketing_manager','agency_freelancer','operations') THEN 10
|
||||||
|
ELSE 0
|
||||||
|
END)
|
||||||
|
+
|
||||||
|
(CASE
|
||||||
|
WHEN u."companyName" IS NOT NULL AND u."companyName" <> '' THEN 5
|
||||||
|
ELSE 0
|
||||||
|
END)
|
||||||
|
+
|
||||||
|
(CASE
|
||||||
|
WHEN u."teamSizeBucket" IN ('6_20','21_100','100_plus') THEN 10
|
||||||
|
ELSE 0
|
||||||
|
END),
|
||||||
|
"intentScore" =
|
||||||
|
(CASE WHEN u."firstQrCreatedAt" IS NOT NULL THEN 20 ELSE -10 END)
|
||||||
|
+
|
||||||
|
(CASE WHEN u."firstDynamicQrAt" IS NOT NULL THEN 20 ELSE 0 END)
|
||||||
|
+
|
||||||
|
(CASE WHEN COALESCE(s.qr_count, 0) >= 3 THEN 15 ELSE 0 END)
|
||||||
|
+
|
||||||
|
(CASE WHEN COALESCE(s.scan_count, 0) > 0 THEN 10 ELSE 0 END)
|
||||||
|
+
|
||||||
|
(CASE WHEN COALESCE(s.businessish_type_count, 0) > 0 THEN 10 ELSE 0 END)
|
||||||
|
+
|
||||||
|
(CASE WHEN COALESCE(s.content_type_count, 0) >= 2 THEN 10 ELSE 0 END),
|
||||||
|
"lastScoredAt" = CURRENT_TIMESTAMP
|
||||||
|
FROM scored s
|
||||||
|
WHERE u."id" = s."id";
|
||||||
|
|
||||||
|
UPDATE "User"
|
||||||
|
SET "leadScore" = COALESCE("fitScore", 0) + COALESCE("intentScore", 0);
|
||||||
|
|
||||||
|
UPDATE "User"
|
||||||
|
SET
|
||||||
|
"lifecycleStage" = CASE
|
||||||
|
WHEN "plan" IN ('PRO','BUSINESS') THEN 'paid'
|
||||||
|
WHEN "leadScore" >= 70 THEN 'upgrade_candidate'
|
||||||
|
WHEN "leadScore" >= 55 THEN 'hot'
|
||||||
|
WHEN "leadScore" >= 30 THEN 'warm'
|
||||||
|
WHEN "activationAt" IS NOT NULL THEN 'activated'
|
||||||
|
ELSE 'cold'
|
||||||
|
END,
|
||||||
|
"lastQualifiedAt" = CASE
|
||||||
|
WHEN "leadScore" >= 55 OR "plan" IN ('PRO','BUSINESS') THEN CURRENT_TIMESTAMP
|
||||||
|
ELSE "lastQualifiedAt"
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 6 reporting queries
|
||||||
|
-- Acquisition overview
|
||||||
|
SELECT
|
||||||
|
COALESCE("signupSource", 'unknown') AS source,
|
||||||
|
COUNT(*) AS signups,
|
||||||
|
COUNT(*) FILTER (WHERE "firstQrCreatedAt" IS NOT NULL) AS first_qr,
|
||||||
|
COUNT(*) FILTER (WHERE "activationAt" IS NOT NULL) AS activated,
|
||||||
|
COUNT(*) FILTER (WHERE "lifecycleStage" = 'hot') AS hot,
|
||||||
|
COUNT(*) FILTER (WHERE "lifecycleStage" = 'upgrade_candidate') AS upgrade_candidates,
|
||||||
|
COUNT(*) FILTER (WHERE "lifecycleStage" = 'paid') AS paid
|
||||||
|
FROM "User"
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY signups DESC;
|
||||||
|
|
||||||
|
-- Onboarding funnel
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS signup,
|
||||||
|
COUNT(*) FILTER (WHERE "sourceConfirmedAt" IS NOT NULL) AS source_confirmed,
|
||||||
|
COUNT(*) FILTER (WHERE "useCaseSelectedAt" IS NOT NULL) AS use_case_selected,
|
||||||
|
COUNT(*) FILTER (WHERE "goalSelectedAt" IS NOT NULL) AS goal_selected,
|
||||||
|
COUNT(*) FILTER (WHERE "profileCompletedAt" IS NOT NULL) AS profile_completed,
|
||||||
|
COUNT(*) FILTER (WHERE "firstQrCreatedAt" IS NOT NULL) AS first_qr_created,
|
||||||
|
COUNT(*) FILTER (WHERE "firstDynamicQrAt" IS NOT NULL) AS first_dynamic_qr_created,
|
||||||
|
COUNT(*) FILTER (WHERE "activationAt" IS NOT NULL) AS activated
|
||||||
|
FROM "User";
|
||||||
|
|
||||||
|
-- Lifecycle summary
|
||||||
|
SELECT
|
||||||
|
"lifecycleStage",
|
||||||
|
COUNT(*) AS users
|
||||||
|
FROM "User"
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY users DESC;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -22,8 +22,10 @@ interface BulkQRData {
|
|||||||
|
|
||||||
interface GeneratedQR {
|
interface GeneratedQR {
|
||||||
title: string;
|
title: string;
|
||||||
content: string; // Original URL
|
content: string;
|
||||||
svg: string; // SVG markup
|
svg: string;
|
||||||
|
slug?: string;
|
||||||
|
redirectUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BulkCreationPage() {
|
export default function BulkCreationPage() {
|
||||||
@@ -35,16 +37,25 @@ export default function BulkCreationPage() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [generatedQRs, setGeneratedQRs] = useState<GeneratedQR[]>([]);
|
const [generatedQRs, setGeneratedQRs] = useState<GeneratedQR[]>([]);
|
||||||
const [userPlan, setUserPlan] = useState<string>('FREE');
|
const [userPlan, setUserPlan] = useState<string>('FREE');
|
||||||
|
const [isDynamic, setIsDynamic] = useState(false);
|
||||||
|
const [remainingDynamic, setRemainingDynamic] = useState(0);
|
||||||
|
|
||||||
// Check user plan on mount
|
// Check user plan and dynamic quota on mount
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const checkPlan = async () => {
|
const checkPlan = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/user/plan');
|
const [planRes, statsRes] = await Promise.all([
|
||||||
if (response.ok) {
|
fetch('/api/user/plan'),
|
||||||
const data = await response.json();
|
fetch('/api/user/stats'),
|
||||||
|
]);
|
||||||
|
if (planRes.ok) {
|
||||||
|
const data = await planRes.json();
|
||||||
setUserPlan(data.plan || 'FREE');
|
setUserPlan(data.plan || 'FREE');
|
||||||
}
|
}
|
||||||
|
if (statsRes.ok) {
|
||||||
|
const stats = await statsRes.json();
|
||||||
|
setRemainingDynamic((stats.dynamicLimit || 0) - (stats.dynamicUsed || 0));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking plan:', error);
|
console.error('Error checking plan:', error);
|
||||||
}
|
}
|
||||||
@@ -196,6 +207,58 @@ export default function BulkCreationPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateDynamicQRCodes = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const toProcess = remainingDynamic > 0 ? data.slice(0, remainingDynamic) : [];
|
||||||
|
|
||||||
|
if (toProcess.length === 0) {
|
||||||
|
showToast('Du hast keine dynamischen QR-Codes mehr übrig. Bitte upgrade deinen Plan.', 'error');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length > remainingDynamic) {
|
||||||
|
showToast(`Nur ${remainingDynamic} dynamische Codes verfügbar. Es werden nur die ersten ${remainingDynamic} Zeilen verarbeitet.`, 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const QRCode = require('qrcode');
|
||||||
|
const results: GeneratedQR[] = [];
|
||||||
|
|
||||||
|
for (const row of toProcess) {
|
||||||
|
const title = String(row[mapping.title as keyof typeof row] || 'Untitled');
|
||||||
|
const url = String(row[mapping.content as keyof typeof row] || 'https://example.com');
|
||||||
|
|
||||||
|
const res = await fetchWithCsrf('/api/qrs', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
title,
|
||||||
|
contentType: 'URL',
|
||||||
|
content: { url },
|
||||||
|
isStatic: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const qr = await res.json();
|
||||||
|
const redirectUrl = `${window.location.origin}/r/${qr.slug}`;
|
||||||
|
const svg = await QRCode.toString(redirectUrl, { type: 'svg', width: 300, margin: 2 });
|
||||||
|
results.push({ title, content: url, svg, slug: qr.slug, redirectUrl });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setGeneratedQRs(results);
|
||||||
|
setRemainingDynamic(prev => Math.max(0, prev - results.length));
|
||||||
|
setStep('complete');
|
||||||
|
showToast(`${results.length} dynamische QR-Codes erstellt!`, 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Dynamic QR generation error:', error);
|
||||||
|
showToast('Fehler beim Erstellen der dynamischen QR-Codes', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const downloadAllQRCodes = async () => {
|
const downloadAllQRCodes = async () => {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
@@ -204,12 +267,25 @@ export default function BulkCreationPage() {
|
|||||||
zip.file(fileName, qr.svg);
|
zip.file(fileName, qr.svg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add metadata CSV for dynamic QR codes
|
||||||
|
const hasDynamic = generatedQRs.some(qr => qr.slug);
|
||||||
|
if (hasDynamic) {
|
||||||
|
const csvRows = ['title,original_url,redirect_url,slug'];
|
||||||
|
generatedQRs.forEach(qr => {
|
||||||
|
if (qr.slug) {
|
||||||
|
csvRows.push(`"${qr.title}","${qr.content}","${qr.redirectUrl}","${qr.slug}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
zip.file('metadata.csv', csvRows.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
const blob = await zip.generateAsync({ type: 'blob' });
|
const blob = await zip.generateAsync({ type: 'blob' });
|
||||||
saveAs(blob, 'qr-codes-bulk.zip');
|
saveAs(blob, 'qr-codes-bulk.zip');
|
||||||
showToast('Download started!', 'success');
|
showToast('Download started!', 'success');
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveQRCodesToDatabase = async () => {
|
const saveQRCodesToDatabase = async () => {
|
||||||
|
if (isDynamic) return; // dynamic codes are already saved during generation
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -274,8 +350,8 @@ export default function BulkCreationPage() {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show upgrade prompt if not Business plan
|
// Show upgrade prompt if not Business or Enterprise plan
|
||||||
if (userPlan !== 'BUSINESS') {
|
if (userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE') {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<Card className="mt-12">
|
<Card className="mt-12">
|
||||||
@@ -309,6 +385,39 @@ export default function BulkCreationPage() {
|
|||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1>
|
<h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1>
|
||||||
<p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p>
|
<p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p>
|
||||||
|
|
||||||
|
{/* Static / Dynamic Toggle */}
|
||||||
|
<div className="mt-4 flex items-center gap-4 p-4 bg-gray-50 rounded-xl border border-gray-200">
|
||||||
|
<span className="text-sm font-medium text-gray-700">QR Code Type:</span>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={!isDynamic}
|
||||||
|
onChange={() => setIsDynamic(false)}
|
||||||
|
className="accent-primary-600"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">Static</span>
|
||||||
|
<span className="text-xs text-gray-500">(download only, no tracking)</span>
|
||||||
|
</label>
|
||||||
|
<label className={`flex items-center gap-2 ${userPlan === 'BUSINESS' || userPlan === 'ENTERPRISE' ? 'cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={isDynamic}
|
||||||
|
onChange={() => setIsDynamic(true)}
|
||||||
|
disabled={userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE'}
|
||||||
|
className="accent-primary-600"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">Dynamic</span>
|
||||||
|
{isDynamic && remainingDynamic > 0 && (
|
||||||
|
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">
|
||||||
|
{remainingDynamic} verbleibend
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{(userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE') && (
|
||||||
|
<span className="text-xs text-amber-600">(Business Plan erforderlich)</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Template Warning Banner */}
|
{/* Template Warning Banner */}
|
||||||
@@ -641,8 +750,13 @@ export default function BulkCreationPage() {
|
|||||||
<Button variant="outline" onClick={() => setStep('upload')}>
|
<Button variant="outline" onClick={() => setStep('upload')}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={generateStaticQRCodes} loading={loading}>
|
<Button
|
||||||
Generate {data.length} Static QR Codes
|
onClick={isDynamic ? generateDynamicQRCodes : generateStaticQRCodes}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
{isDynamic
|
||||||
|
? `Generate ${Math.min(data.length, remainingDynamic)} Dynamic QR Codes`
|
||||||
|
: `Generate ${data.length} Static QR Codes`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -704,12 +818,14 @@ export default function BulkCreationPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
Download All as ZIP
|
Download All as ZIP
|
||||||
</Button>
|
</Button>
|
||||||
|
{!isDynamic && (
|
||||||
<Button onClick={saveQRCodesToDatabase} loading={loading}>
|
<Button onClick={saveQRCodesToDatabase} loading={loading}>
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
||||||
</svg>
|
</svg>
|
||||||
Save QR Codes
|
Save QR Codes
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { toPng } from 'html-to-image';
|
import { toPng } from 'html-to-image';
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
@@ -14,9 +14,16 @@ import { calculateContrast, cn } from '@/lib/utils';
|
|||||||
import { useTranslation } from '@/hooks/useTranslation';
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
import { useCsrf } from '@/hooks/useCsrf';
|
import { useCsrf } from '@/hooks/useCsrf';
|
||||||
import { showToast } from '@/components/ui/Toast';
|
import { showToast } from '@/components/ui/Toast';
|
||||||
|
import { trackEvent } from '@/components/PostHogProvider';
|
||||||
|
import { appendRedirectParam, sanitizeRedirectPath } from '@/lib/auth-flow';
|
||||||
import {
|
import {
|
||||||
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload
|
ONBOARDING_DOWNLOAD_COMPLETE_EVENT,
|
||||||
|
ONBOARDING_DOWNLOAD_COMPLETE_KEY,
|
||||||
|
} from '@/lib/revops';
|
||||||
|
import {
|
||||||
|
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload, Barcode as BarcodeIcon
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import Barcode from 'react-barcode';
|
||||||
|
|
||||||
// Tooltip component for form field help
|
// Tooltip component for form field help
|
||||||
const Tooltip = ({ text }: { text: string }) => (
|
const Tooltip = ({ text }: { text: string }) => (
|
||||||
@@ -61,8 +68,46 @@ const getFrameOptionsForContentType = (contentType: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Injects a caption <text> element below a barcode SVG and expands its height/viewBox.
|
||||||
|
// Used so the "scanner app" hint is baked into the downloaded SVG.
|
||||||
|
function addBarcodeCaptionToSvg(svgElement: SVGElement, caption: string): string {
|
||||||
|
const cloned = svgElement.cloneNode(true) as SVGElement;
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
|
||||||
|
const widthAttr = cloned.getAttribute('width');
|
||||||
|
const heightAttr = cloned.getAttribute('height');
|
||||||
|
const width = widthAttr ? parseFloat(widthAttr) : 200;
|
||||||
|
const height = heightAttr ? parseFloat(heightAttr) : 100;
|
||||||
|
const extraHeight = 18;
|
||||||
|
|
||||||
|
cloned.setAttribute('height', String(height + extraHeight));
|
||||||
|
const viewBox = cloned.getAttribute('viewBox');
|
||||||
|
if (viewBox) {
|
||||||
|
const parts = viewBox.split(/\s+/);
|
||||||
|
if (parts.length === 4) {
|
||||||
|
cloned.setAttribute(
|
||||||
|
'viewBox',
|
||||||
|
`${parts[0]} ${parts[1]} ${parts[2]} ${parseFloat(parts[3]) + extraHeight}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = document.createElementNS(NS, 'text');
|
||||||
|
text.setAttribute('x', String(width / 2));
|
||||||
|
text.setAttribute('y', String(height + 12));
|
||||||
|
text.setAttribute('text-anchor', 'middle');
|
||||||
|
text.setAttribute('font-size', '9');
|
||||||
|
text.setAttribute('font-family', 'Arial, Helvetica, sans-serif');
|
||||||
|
text.setAttribute('fill', '#666666');
|
||||||
|
text.textContent = caption;
|
||||||
|
cloned.appendChild(text);
|
||||||
|
|
||||||
|
return new XMLSerializer().serializeToString(cloned);
|
||||||
|
}
|
||||||
|
|
||||||
export default function CreatePage() {
|
export default function CreatePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { fetchWithCsrf } = useCsrf();
|
const { fetchWithCsrf } = useCsrf();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -109,6 +154,15 @@ export default function CreatePage() {
|
|||||||
// QR preview
|
// QR preview
|
||||||
const [qrDataUrl, setQrDataUrl] = useState('');
|
const [qrDataUrl, setQrDataUrl] = useState('');
|
||||||
|
|
||||||
|
const markDownloadComplete = () => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY, '1');
|
||||||
|
window.dispatchEvent(new CustomEvent(ONBOARDING_DOWNLOAD_COMPLETE_EVENT));
|
||||||
|
};
|
||||||
|
|
||||||
// Check if user can customize colors (PRO+ only)
|
// Check if user can customize colors (PRO+ only)
|
||||||
const canCustomizeColors = userPlan === 'PRO' || userPlan === 'BUSINESS';
|
const canCustomizeColors = userPlan === 'PRO' || userPlan === 'BUSINESS';
|
||||||
|
|
||||||
@@ -128,6 +182,42 @@ export default function CreatePage() {
|
|||||||
fetchUserPlan();
|
fetchUserPlan();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const queryContentType = searchParams.get('contentType');
|
||||||
|
const useCase = searchParams.get('useCase');
|
||||||
|
const titleParam = searchParams.get('title');
|
||||||
|
const isDynamicParam = searchParams.get('dynamic');
|
||||||
|
|
||||||
|
if (queryContentType) {
|
||||||
|
setContentType(queryContentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (titleParam) {
|
||||||
|
setTitle(titleParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDynamicParam) {
|
||||||
|
setIsDynamic(isDynamicParam === '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useCase === 'menu_pdf') {
|
||||||
|
setContent((prev: any) => ({ ...prev, fileUrl: prev.fileUrl || '' }));
|
||||||
|
} else if (useCase === 'contact_card') {
|
||||||
|
setContent((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
firstName: prev.firstName || '',
|
||||||
|
lastName: prev.lastName || '',
|
||||||
|
}));
|
||||||
|
} else if (useCase === 'barcode') {
|
||||||
|
setContent((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
format: prev.format || 'CODE128',
|
||||||
|
}));
|
||||||
|
} else if (queryContentType === 'URL') {
|
||||||
|
setContent((prev: any) => ({ ...prev, url: prev.url || '' }));
|
||||||
|
}
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
const contrast = calculateContrast(foregroundColor, backgroundColor);
|
const contrast = calculateContrast(foregroundColor, backgroundColor);
|
||||||
const hasGoodContrast = contrast >= 4.5;
|
const hasGoodContrast = contrast >= 4.5;
|
||||||
|
|
||||||
@@ -140,6 +230,7 @@ export default function CreatePage() {
|
|||||||
{ value: 'APP', label: 'App Download', icon: Smartphone },
|
{ value: 'APP', label: 'App Download', icon: Smartphone },
|
||||||
{ value: 'COUPON', label: 'Coupon / Discount', icon: Ticket },
|
{ value: 'COUPON', label: 'Coupon / Discount', icon: Ticket },
|
||||||
{ value: 'FEEDBACK', label: 'Feedback / Review', icon: Star },
|
{ value: 'FEEDBACK', label: 'Feedback / Review', icon: Star },
|
||||||
|
{ value: 'BARCODE', label: 'Barcode', icon: BarcodeIcon },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Get QR content based on content type
|
// Get QR content based on content type
|
||||||
@@ -170,12 +261,15 @@ export default function CreatePage() {
|
|||||||
return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`;
|
return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`;
|
||||||
case 'FEEDBACK':
|
case 'FEEDBACK':
|
||||||
return content.feedbackUrl || 'https://example.com/feedback';
|
return content.feedbackUrl || 'https://example.com/feedback';
|
||||||
|
case 'BARCODE':
|
||||||
|
return isDynamic ? (content.url || '') : (content.value || '');
|
||||||
default:
|
default:
|
||||||
return 'https://example.com';
|
return 'https://example.com';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const qrContent = getQRContent();
|
const qrContent = getQRContent();
|
||||||
|
const previewScale = contentType === 'BARCODE' ? 1 : Math.min(1, 240 / Math.max(size, 1));
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
const frame = frameOptions.find((f: { id: string; label: string }) => f.id === frameType);
|
const frame = frameOptions.find((f: { id: string; label: string }) => f.id === frameType);
|
||||||
@@ -191,6 +285,13 @@ export default function CreatePage() {
|
|||||||
link.download = `qrcode-${title || 'download'}.png`;
|
link.download = `qrcode-${title || 'download'}.png`;
|
||||||
link.href = dataUrl;
|
link.href = dataUrl;
|
||||||
link.click();
|
link.click();
|
||||||
|
markDownloadComplete();
|
||||||
|
trackEvent('qr_code_downloaded', {
|
||||||
|
format: 'png',
|
||||||
|
content_type: contentType,
|
||||||
|
qr_type: isDynamic ? 'dynamic' : 'static',
|
||||||
|
plan: userPlan,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// For SVG, we might still want to use the library or just toPng if SVG export of HTML is not needed
|
// For SVG, we might still want to use the library or just toPng if SVG export of HTML is not needed
|
||||||
// Simplest is to check if we can export the SVG element directly but that misses the frame HTML.
|
// Simplest is to check if we can export the SVG element directly but that misses the frame HTML.
|
||||||
@@ -208,7 +309,9 @@ export default function CreatePage() {
|
|||||||
if (frameType === 'none') {
|
if (frameType === 'none') {
|
||||||
const svgElement = qrRef.current.querySelector('svg');
|
const svgElement = qrRef.current.querySelector('svg');
|
||||||
if (svgElement) {
|
if (svgElement) {
|
||||||
const svgData = new XMLSerializer().serializeToString(svgElement);
|
const svgData = contentType === 'BARCODE'
|
||||||
|
? addBarcodeCaptionToSvg(svgElement, 'Scan: iPhone -> Barcode Scanner App | Android -> Google Lens')
|
||||||
|
: new XMLSerializer().serializeToString(svgElement);
|
||||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -216,6 +319,13 @@ export default function CreatePage() {
|
|||||||
a.download = `qrcode-${title || 'download'}.svg`;
|
a.download = `qrcode-${title || 'download'}.svg`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
markDownloadComplete();
|
||||||
|
trackEvent('qr_code_downloaded', {
|
||||||
|
format: 'svg',
|
||||||
|
content_type: contentType,
|
||||||
|
qr_type: isDynamic ? 'dynamic' : 'static',
|
||||||
|
plan: userPlan,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast('SVG download not available with frames yet. Downloading PNG instead.', 'info');
|
showToast('SVG download not available with frames yet. Downloading PNG instead.', 'info');
|
||||||
@@ -224,6 +334,14 @@ export default function CreatePage() {
|
|||||||
link.download = `qrcode-${title || 'download'}.png`;
|
link.download = `qrcode-${title || 'download'}.png`;
|
||||||
link.href = dataUrl;
|
link.href = dataUrl;
|
||||||
link.click();
|
link.click();
|
||||||
|
markDownloadComplete();
|
||||||
|
trackEvent('qr_code_downloaded', {
|
||||||
|
format: 'png',
|
||||||
|
content_type: contentType,
|
||||||
|
qr_type: isDynamic ? 'dynamic' : 'static',
|
||||||
|
plan: userPlan,
|
||||||
|
fallback_from: 'svg_with_frame',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -312,15 +430,35 @@ export default function CreatePage() {
|
|||||||
console.log('RESPONSE DATA:', responseData);
|
console.log('RESPONSE DATA:', responseData);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
trackEvent('qr_code_created', {
|
||||||
|
content_type: contentType,
|
||||||
|
qr_type: isDynamic ? 'dynamic' : 'static',
|
||||||
|
plan: userPlan,
|
||||||
|
has_logo: Boolean(logoUrl),
|
||||||
|
frame_type: frameType,
|
||||||
|
});
|
||||||
|
|
||||||
showToast(`QR Code "${title}" created successfully!`, 'success');
|
showToast(`QR Code "${title}" created successfully!`, 'success');
|
||||||
|
|
||||||
// Wait a moment so user sees the toast, then redirect
|
// Wait a moment so user sees the toast, then redirect
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
const redirectTarget = sanitizeRedirectPath(searchParams.get('redirect'));
|
||||||
|
if (searchParams.get('onboarding') === '1') {
|
||||||
|
router.push(appendRedirectParam('/onboarding', redirectTarget, { step: '8' }));
|
||||||
|
} else {
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
|
}
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
console.error('Error creating QR code:', responseData);
|
console.error('Error creating QR code:', responseData);
|
||||||
|
|
||||||
|
if (response.status === 403 && responseData.error === 'Limit reached') {
|
||||||
|
showToast(responseData.message || 'You have reached your plan limit.', 'error');
|
||||||
|
router.push('/pricing?reason=limit_reached');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showToast(responseData.error || 'Error creating QR code', 'error');
|
showToast(responseData.error || 'Error creating QR code', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -642,6 +780,73 @@ export default function CreatePage() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
case 'BARCODE':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isDynamic ? (
|
||||||
|
<>
|
||||||
|
<div className="rounded-lg bg-blue-50 border border-blue-200 p-3 text-sm text-blue-800 space-y-2">
|
||||||
|
<p>
|
||||||
|
<strong>How dynamic barcodes work:</strong> The barcode encodes a short redirect URL
|
||||||
|
(e.g. <span className="font-mono text-xs">qrmaster.net/r/…</span>) that you can update anytime.
|
||||||
|
</p>
|
||||||
|
<p className="rounded border border-amber-300 bg-amber-50 p-2 text-xs text-amber-900">
|
||||||
|
<strong>📱 Scanner tip:</strong> Use a <strong>barcode scanner app</strong> on iPhone
|
||||||
|
(iOS Camera doesn't auto-open links from barcodes). Android Google Lens / Camera works
|
||||||
|
out of the box. Print min. 5 cm wide for reliable scanning.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="Destination URL"
|
||||||
|
value={content.url || ''}
|
||||||
|
onChange={(e) => setContent({ ...content, url: e.target.value })}
|
||||||
|
placeholder="https://example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Barcode Format</label>
|
||||||
|
<select
|
||||||
|
value={['CODE128', 'CODE39'].includes(content.format) ? content.format : 'CODE128'}
|
||||||
|
onChange={(e) => setContent({ ...content, format: e.target.value })}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
<option value="CODE128">CODE128 — General purpose (recommended)</option>
|
||||||
|
<option value="CODE39">CODE39 — Industrial / logistics</option>
|
||||||
|
</select>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Only URL-capable formats available. EAN-13, UPC, and ITF-14 encode numbers only and cannot embed a redirect URL.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
label="Barcode Value"
|
||||||
|
value={content.value || ''}
|
||||||
|
onChange={(e) => setContent({ ...content, value: e.target.value })}
|
||||||
|
placeholder="123456789012"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Barcode Format</label>
|
||||||
|
<select
|
||||||
|
value={content.format || 'CODE128'}
|
||||||
|
onChange={(e) => setContent({ ...content, format: e.target.value })}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
<option value="CODE128">CODE128 — General purpose (recommended)</option>
|
||||||
|
<option value="EAN13">EAN-13 — Retail products (international)</option>
|
||||||
|
<option value="UPC">UPC — Retail products (USA/Canada)</option>
|
||||||
|
<option value="CODE39">CODE39 — Industrial / logistics</option>
|
||||||
|
<option value="ITF14">ITF-14 — Shipping containers</option>
|
||||||
|
<option value="MSI">MSI — Shelf labeling / inventory</option>
|
||||||
|
<option value="pharmacode">Pharmacode — Pharmaceutical packaging</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -799,6 +1004,7 @@ export default function CreatePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
@@ -843,6 +1049,7 @@ export default function CreatePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Select
|
<Select
|
||||||
label="Corner Style"
|
label="Corner Style"
|
||||||
@@ -972,14 +1179,13 @@ export default function CreatePage() {
|
|||||||
<CardTitle>{t('create.preview')}</CardTitle>
|
<CardTitle>{t('create.preview')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-center">
|
<CardContent className="text-center">
|
||||||
<div id="create-qr-preview" className="flex justify-center mb-4">
|
<div id="create-qr-preview" className="flex justify-center mb-4 w-full min-w-0 overflow-hidden">
|
||||||
{/* WRAPPER FOR REF AND FRAME */}
|
{/* WRAPPER FOR REF AND FRAME */}
|
||||||
<div
|
<div
|
||||||
ref={qrRef}
|
ref={qrRef}
|
||||||
className="relative bg-white rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300"
|
className="relative flex w-full min-w-0 max-w-full flex-col items-center justify-center rounded-xl bg-white p-3 transition-all duration-300 sm:p-4"
|
||||||
style={{
|
style={{
|
||||||
minWidth: '280px',
|
minHeight: '220px',
|
||||||
minHeight: '280px',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Frame Label */}
|
{/* Frame Label */}
|
||||||
@@ -992,8 +1198,38 @@ export default function CreatePage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{qrContent ? (
|
{contentType === 'BARCODE' ? (
|
||||||
<div className={cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
|
qrContent ? (
|
||||||
|
<div className="p-2 bg-white w-full max-w-full [&_svg]:!w-full [&_svg]:!h-auto [&_svg]:!max-w-full">
|
||||||
|
<Barcode
|
||||||
|
key={`${qrContent}-${content.format}-${foregroundColor}`}
|
||||||
|
value={qrContent}
|
||||||
|
format={content.format || 'CODE128'}
|
||||||
|
lineColor={foregroundColor}
|
||||||
|
background={backgroundColor}
|
||||||
|
width={2}
|
||||||
|
height={80}
|
||||||
|
margin={10}
|
||||||
|
displayValue={true}
|
||||||
|
fontSize={14}
|
||||||
|
/>
|
||||||
|
<p className="mt-2 text-center text-[10px] leading-tight text-gray-600 px-2">
|
||||||
|
Scan: iPhone → Barcode Scanner App · Android → Google Lens / Camera
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-[200px] h-[200px] bg-gray-100 rounded flex items-center justify-center text-gray-500">
|
||||||
|
Enter barcode value
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : qrContent ? (
|
||||||
|
<div
|
||||||
|
className={cornerStyle === 'rounded' ? 'overflow-hidden rounded-lg' : ''}
|
||||||
|
style={{
|
||||||
|
transform: `scale(${previewScale})`,
|
||||||
|
transformOrigin: 'center center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<QRCodeSVG
|
<QRCodeSVG
|
||||||
value={qrContent}
|
value={qrContent}
|
||||||
size={size}
|
size={size}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import { useCsrf } from '@/hooks/useCsrf';
|
|||||||
import { showToast } from '@/components/ui/Toast';
|
import { showToast } from '@/components/ui/Toast';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/Dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/Dialog';
|
||||||
import { QrCode } from 'lucide-react';
|
import { QrCode } from 'lucide-react';
|
||||||
|
import { trackEvent, identifyUser } from '@/components/PostHogProvider';
|
||||||
|
import { FREE_DYNAMIC_QR_LIMIT } from '@/lib/plans';
|
||||||
|
import { OnboardingChecklist } from '@/components/dashboard/OnboardingChecklist';
|
||||||
|
|
||||||
interface QRCodeData {
|
interface QRCodeData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -45,6 +48,7 @@ export default function DashboardPage() {
|
|||||||
uniqueScans: 0,
|
uniqueScans: 0,
|
||||||
});
|
});
|
||||||
const [analyticsData, setAnalyticsData] = useState<any>(null);
|
const [analyticsData, setAnalyticsData] = useState<any>(null);
|
||||||
|
const [onboardingState, setOnboardingState] = useState<any>(null);
|
||||||
|
|
||||||
|
|
||||||
const blogPosts = [
|
const blogPosts = [
|
||||||
@@ -117,7 +121,6 @@ export default function DashboardPage() {
|
|||||||
// Store in localStorage for consistency
|
// Store in localStorage for consistency
|
||||||
localStorage.setItem('user', JSON.stringify(user));
|
localStorage.setItem('user', JSON.stringify(user));
|
||||||
|
|
||||||
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
|
|
||||||
identifyUser(user.id, {
|
identifyUser(user.id, {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
@@ -145,11 +148,17 @@ export default function DashboardPage() {
|
|||||||
// Check for successful payment and verify session
|
// Check for successful payment and verify session
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const success = searchParams.get('success');
|
const success = searchParams.get('success');
|
||||||
if (success === 'true') {
|
const sessionId = searchParams.get('session_id');
|
||||||
|
|
||||||
|
if (success === 'true' && sessionId) {
|
||||||
const verifySession = async () => {
|
const verifySession = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/stripe/verify-session', {
|
const response = await fetch('/api/stripe/verify-session', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ sessionId }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -157,6 +166,10 @@ export default function DashboardPage() {
|
|||||||
setUserPlan(data.plan);
|
setUserPlan(data.plan);
|
||||||
setUpgradedPlan(data.plan);
|
setUpgradedPlan(data.plan);
|
||||||
setShowUpgradeDialog(true);
|
setShowUpgradeDialog(true);
|
||||||
|
trackEvent('upgrade_completed', {
|
||||||
|
plan: data.plan,
|
||||||
|
source: 'stripe_checkout',
|
||||||
|
});
|
||||||
// Remove success parameter from URL
|
// Remove success parameter from URL
|
||||||
router.replace('/dashboard');
|
router.replace('/dashboard');
|
||||||
} else {
|
} else {
|
||||||
@@ -218,6 +231,12 @@ export default function DashboardPage() {
|
|||||||
const analytics = await analyticsResponse.json();
|
const analytics = await analyticsResponse.json();
|
||||||
setAnalyticsData(analytics);
|
setAnalyticsData(analytics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onboardingResponse = await fetch('/api/onboarding');
|
||||||
|
if (onboardingResponse.ok) {
|
||||||
|
const onboardingData = await onboardingResponse.json();
|
||||||
|
setOnboardingState(onboardingData);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching data:', error);
|
console.error('Error fetching data:', error);
|
||||||
setQrCodes([]);
|
setQrCodes([]);
|
||||||
@@ -301,27 +320,11 @@ export default function DashboardPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlanBadgeColor = (plan: string) => {
|
|
||||||
switch (plan) {
|
|
||||||
case 'PRO':
|
|
||||||
return 'info';
|
|
||||||
case 'BUSINESS':
|
|
||||||
return 'warning';
|
|
||||||
default:
|
|
||||||
return 'default';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlanEmoji = (plan: string) => {
|
|
||||||
// No emojis anymore
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
{/* Header with Plan Badge */}
|
{/* Header with Plan Badge */}
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<h1 className="text-3xl font-bold text-gray-900">{t('dashboard.title')}</h1>
|
<h1 className="text-3xl font-bold text-gray-900">{t('dashboard.title')}</h1>
|
||||||
<p className="text-gray-600 mt-2">
|
<p className="text-gray-600 mt-2">
|
||||||
{!loading && qrCodes.length === 0
|
{!loading && qrCodes.length === 0
|
||||||
@@ -329,19 +332,21 @@ export default function DashboardPage() {
|
|||||||
: t('dashboard.subtitle')}
|
: t('dashboard.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<Badge variant={getPlanBadgeColor(userPlan)} className="text-lg px-4 py-2">
|
<Badge className="border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700">
|
||||||
{userPlan} Plan
|
{userPlan} Plan
|
||||||
</Badge>
|
</Badge>
|
||||||
{userPlan === 'FREE' && (
|
{userPlan === 'FREE' && (
|
||||||
<Link href="/pricing">
|
<Link href="/pricing">
|
||||||
<Button variant="primary">Upgrade</Button>
|
<Button className="bg-primary-600 text-white hover:bg-primary-700">Upgrade</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
|
<OnboardingChecklist state={onboardingState} />
|
||||||
|
|
||||||
<StatsGrid
|
<StatsGrid
|
||||||
stats={stats}
|
stats={stats}
|
||||||
trends={{
|
trends={{
|
||||||
@@ -352,9 +357,9 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
{/* Recent QR Codes */}
|
{/* Recent QR Codes */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<h2 className="text-xl font-semibold text-gray-900">{t('dashboard.recent_codes')}</h2>
|
<h2 className="text-xl font-semibold text-gray-900">{t('dashboard.recent_codes')}</h2>
|
||||||
<div className="flex gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{qrCodes.length > 0 && (
|
{qrCodes.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -366,7 +371,7 @@ export default function DashboardPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Link href="/create">
|
<Link href="/create">
|
||||||
<Button>Create New QR Code</Button>
|
<Button className="bg-primary-600 text-white hover:bg-primary-700">Create New QR Code</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -389,14 +394,14 @@ export default function DashboardPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : qrCodes.length === 0 ? (
|
) : qrCodes.length === 0 ? (
|
||||||
<div className="text-center py-16 border-2 border-dashed border-gray-200 rounded-xl">
|
<div className="rounded-[24px] border border-dashed border-gray-200 py-16 text-center">
|
||||||
<QrCode className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
<QrCode className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-semibold text-gray-700 mb-2">Create your first QR code</h3>
|
<h3 className="text-lg font-semibold text-gray-700 mb-2">Create your first QR code</h3>
|
||||||
<p className="text-gray-500 mb-6 max-w-sm mx-auto">
|
<p className="text-gray-500 mb-6 max-w-sm mx-auto">
|
||||||
You have 3 free dynamic QR codes. They redirect wherever you want and track every scan.
|
You have {FREE_DYNAMIC_QR_LIMIT} free dynamic QR codes. They redirect wherever you want and track every scan.
|
||||||
</p>
|
</p>
|
||||||
<Link href="/create">
|
<Link href="/create">
|
||||||
<Button>Create QR Code — it takes 90 seconds</Button>
|
<Button className="bg-primary-600 text-white hover:bg-primary-700">Create QR Code — it takes 90 seconds</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ export default function SettingsPage() {
|
|||||||
return { dynamic: 50, price: '€9', period: 'per month' };
|
return { dynamic: 50, price: '€9', period: 'per month' };
|
||||||
case 'BUSINESS':
|
case 'BUSINESS':
|
||||||
return { dynamic: 500, price: '€29', period: 'per month' };
|
return { dynamic: 500, price: '€29', period: 'per month' };
|
||||||
|
case 'ENTERPRISE':
|
||||||
|
return { dynamic: 99999, price: 'Custom', period: 'per month' };
|
||||||
default:
|
default:
|
||||||
return { dynamic: 3, price: '€0', period: 'forever' };
|
return { dynamic: 3, price: '€0', period: 'forever' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/Input';
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { useTranslation } from '@/hooks/useTranslation';
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
import { useCsrf } from '@/hooks/useCsrf';
|
import { useCsrf } from '@/hooks/useCsrf';
|
||||||
|
import { appendRedirectParam, sanitizeRedirectPath } from '@/lib/auth-flow';
|
||||||
|
|
||||||
type LoginClientProps = {
|
type LoginClientProps = {
|
||||||
showPageHeading?: boolean;
|
showPageHeading?: boolean;
|
||||||
@@ -22,6 +23,8 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const redirectTarget = sanitizeRedirectPath(searchParams.get('redirect'));
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -57,7 +60,9 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for redirect parameter
|
// Check for redirect parameter
|
||||||
const redirectUrl = searchParams.get('redirect') || '/dashboard';
|
const redirectUrl = data.needsOnboarding
|
||||||
|
? appendRedirectParam('/onboarding', redirectTarget)
|
||||||
|
: (redirectTarget || '/dashboard');
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} else {
|
} else {
|
||||||
@@ -72,7 +77,7 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
|
|||||||
|
|
||||||
const handleGoogleSignIn = () => {
|
const handleGoogleSignIn = () => {
|
||||||
// Redirect to Google OAuth API route
|
// Redirect to Google OAuth API route
|
||||||
window.location.href = '/api/auth/google';
|
window.location.href = appendRedirectParam('/api/auth/google', redirectTarget);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -112,14 +117,37 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<div className="space-y-1">
|
||||||
label="Password"
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
|
||||||
type="password"
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
required
|
required
|
||||||
|
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="flex items-center">
|
<label className="flex items-center">
|
||||||
@@ -175,7 +203,7 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
|
|||||||
<div className="mt-6 text-center">
|
<div className="mt-6 text-center">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
Don't have an account?{' '}
|
Don't have an account?{' '}
|
||||||
<Link href="/signup" className="text-primary-600 hover:text-primary-700 font-medium">
|
<Link href={appendRedirectParam('/signup', redirectTarget)} className="text-primary-600 hover:text-primary-700 font-medium">
|
||||||
Sign up
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { useTranslation } from '@/hooks/useTranslation';
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
import { useCsrf } from '@/hooks/useCsrf';
|
import { useCsrf } from '@/hooks/useCsrf';
|
||||||
|
import { appendRedirectParam, sanitizeRedirectPath } from '@/lib/auth-flow';
|
||||||
|
|
||||||
export default function SignupClient() {
|
export default function SignupClient() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { fetchWithCsrf } = useCsrf();
|
const { fetchWithCsrf } = useCsrf();
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
@@ -19,6 +21,9 @@ export default function SignupClient() {
|
|||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
const redirectTarget = sanitizeRedirectPath(searchParams.get('redirect'));
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -66,8 +71,8 @@ export default function SignupClient() {
|
|||||||
console.error('PostHog tracking error:', error);
|
console.error('PostHog tracking error:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to dashboard
|
// Redirect to onboarding
|
||||||
router.push('/dashboard');
|
router.push(appendRedirectParam('/onboarding', redirectTarget));
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} else {
|
} else {
|
||||||
setError(data.error || 'Failed to create account');
|
setError(data.error || 'Failed to create account');
|
||||||
@@ -81,7 +86,7 @@ export default function SignupClient() {
|
|||||||
|
|
||||||
const handleGoogleSignIn = () => {
|
const handleGoogleSignIn = () => {
|
||||||
// Redirect to Google OAuth API route
|
// Redirect to Google OAuth API route
|
||||||
window.location.href = '/api/auth/google';
|
window.location.href = appendRedirectParam('/api/auth/google', redirectTarget);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -126,23 +131,69 @@ export default function SignupClient() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<div className="space-y-1">
|
||||||
label="Password"
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
|
||||||
type="password"
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
required
|
required
|
||||||
|
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Input
|
<div className="space-y-1">
|
||||||
label="Confirm Password"
|
<label htmlFor="confirm-password" className="block text-sm font-medium text-gray-700">Confirm Password</label>
|
||||||
type="password"
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
id="confirm-password"
|
||||||
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
required
|
required
|
||||||
|
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button type="submit" className="w-full" loading={loading}>
|
<Button type="submit" className="w-full" loading={loading}>
|
||||||
Create Account
|
Create Account
|
||||||
@@ -188,7 +239,7 @@ export default function SignupClient() {
|
|||||||
<div className="mt-6 text-center">
|
<div className="mt-6 text-center">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
Already have an account?{' '}
|
Already have an account?{' '}
|
||||||
<Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium">
|
<Link href={appendRedirectParam('/login', redirectTarget)} className="text-primary-600 hover:text-primary-700 font-medium">
|
||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export default function MarketingLayout({
|
|||||||
<li><Link href="/blog">{t.nav.blog}</Link></li>
|
<li><Link href="/blog">{t.nav.blog}</Link></li>
|
||||||
<li><Link href="/learn">{t.nav.learn}</Link></li>
|
<li><Link href="/learn">{t.nav.learn}</Link></li>
|
||||||
<li><Link href="/use-cases">Use Cases</Link></li>
|
<li><Link href="/use-cases">Use Cases</Link></li>
|
||||||
|
<li><Link href="/restaurants">Restaurant Menu QR Codes</Link></li>
|
||||||
<li><Link href="/faq">{t.nav.faq}</Link></li>
|
<li><Link href="/faq">{t.nav.faq}</Link></li>
|
||||||
<li><Link href="/about">{t.nav.about}</Link></li>
|
<li><Link href="/about">{t.nav.about}</Link></li>
|
||||||
<li><Link href="/contact">{t.nav.contact}</Link></li>
|
<li><Link href="/contact">{t.nav.contact}</Link></li>
|
||||||
@@ -146,7 +147,7 @@ export default function MarketingLayout({
|
|||||||
<Link href="/" className="flex items-center space-x-3 group">
|
<Link href="/" className="flex items-center space-x-3 group">
|
||||||
<div className="relative w-16 h-16 overflow-hidden rounded-full shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
|
<div className="relative w-16 h-16 overflow-hidden rounded-full shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
|
||||||
<Image
|
<Image
|
||||||
src="/favicon1.png"
|
src="/logo.svg"
|
||||||
alt="QR Master"
|
alt="QR Master"
|
||||||
fill
|
fill
|
||||||
sizes="64px"
|
sizes="64px"
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import { CheckCircle2, Shield, Users, BarChart3, Globe, Lock } from 'lucide-reac
|
|||||||
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'About QR Master | The Team & Mission Behind the Platform',
|
title: 'About QR Master | Free QR Code Generator for Businesses',
|
||||||
description: 'QR Master is built for measurable campaigns and secure QR code operations. Learn about our mission, values, and why businesses trust us.',
|
description: 'QR Master helps businesses create, track, and manage QR codes at scale — free dynamic QR codes, real analytics, and no hidden limits. Learn who we are.',
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'About QR Master | Dynamic QR Codes & Analytics',
|
title: 'About QR Master | Free Dynamic QR Codes & Analytics',
|
||||||
description: 'We help businesses create, manage, and track QR codes at scale. Transparent pricing, privacy-first, and built for reliability.',
|
description: 'Free dynamic QR codes with scan analytics, custom branding, and no reprint headaches. Learn about the team and mission behind QR Master.',
|
||||||
url: 'https://www.qrmaster.net/about',
|
url: 'https://www.qrmaster.net/about',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
images: ['/og-image.png'],
|
images: ['/og-image.png'],
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const competitor = competitors['bitly'];
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
absolute: 'Bitly QR Code Alternative – Purpose-Built for QR Campaigns | QR Master',
|
absolute: 'QR Master vs Bitly QR Codes | Bitly Alternative',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'Looking for a Bitly alternative for QR codes? Bitly\'s Core plan costs $10/month but only allows 2 QR codes total. QR Master is purpose-built for QR code management — 50 codes at €9/month, bulk creation, GDPR analytics. From €0.',
|
'Looking for a Bitly alternative for QR codes? Bitly\'s Core plan costs $10/month but only allows 2 QR codes total. QR Master is purpose-built for QR code management — 50 codes at €9/month, bulk creation, GDPR analytics. From €0.',
|
||||||
@@ -24,7 +24,7 @@ export const metadata: Metadata = {
|
|||||||
canonical: 'https://www.qrmaster.net/alternatives/bitly',
|
canonical: 'https://www.qrmaster.net/alternatives/bitly',
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Bitly QR Code Alternative – Purpose-Built for QR Campaigns',
|
title: 'QR Master vs Bitly QR Codes | Bitly Alternative',
|
||||||
description:
|
description:
|
||||||
'Bitly\'s Core plan costs $10/month but only gives you 2 QR codes. QR Master gives you 50 dynamic QR codes at €9/month — purpose-built for QR workflows, bulk creation, and GDPR analytics.',
|
'Bitly\'s Core plan costs $10/month but only gives you 2 QR codes. QR Master gives you 50 dynamic QR codes at €9/month — purpose-built for QR workflows, bulk creation, and GDPR analytics.',
|
||||||
url: 'https://www.qrmaster.net/alternatives/bitly',
|
url: 'https://www.qrmaster.net/alternatives/bitly',
|
||||||
@@ -32,7 +32,7 @@ export const metadata: Metadata = {
|
|||||||
images: ['/og-image.png'],
|
images: ['/og-image.png'],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'Bitly QR Code Alternative – Purpose-Built for QR Campaigns',
|
title: 'QR Master vs Bitly QR Codes | Bitly Alternative',
|
||||||
description:
|
description:
|
||||||
'Bitly gives you 2 QR codes for $10/month. QR Master gives you 50 at €9/month — purpose-built for real QR campaigns, not link shortening with QR as an afterthought.',
|
'Bitly gives you 2 QR codes for $10/month. QR Master gives you 50 at €9/month — purpose-built for real QR campaigns, not link shortening with QR as an afterthought.',
|
||||||
},
|
},
|
||||||
@@ -40,6 +40,34 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
const comparisonRows = competitor.features;
|
const comparisonRows = competitor.features;
|
||||||
|
|
||||||
|
const atAGlanceRows = [
|
||||||
|
{
|
||||||
|
useCase: 'A couple of QR codes',
|
||||||
|
bitly: 'Reasonable if you already pay for Bitly and only need 1-2 QR codes.',
|
||||||
|
qrMaster: 'Free plan includes 3 active dynamic QR codes plus unlimited static codes.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
useCase: 'Marketing campaign with many placements',
|
||||||
|
bitly: 'Entry plans hit QR count limits quickly.',
|
||||||
|
qrMaster: 'Pro includes 50 dynamic QR codes; Business includes 500.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
useCase: 'Bulk QR creation',
|
||||||
|
bitly: 'No dedicated bulk QR generator.',
|
||||||
|
qrMaster: 'CSV and Excel upload creates up to 1,000 unique QR codes per batch.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
useCase: 'QR campaign analytics',
|
||||||
|
bitly: 'Strong link analytics, but QR is a secondary workflow.',
|
||||||
|
qrMaster: 'QR-first scan analytics with device, country, time, and UTM context.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
useCase: 'Migration risk',
|
||||||
|
bitly: 'Printed codes depend on Bitly redirect infrastructure.',
|
||||||
|
qrMaster: 'Re-create destinations before canceling Bitly and replace codes on the next print cycle.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const faqItems = [
|
const faqItems = [
|
||||||
{
|
{
|
||||||
question: 'How many QR codes does Bitly allow per plan?',
|
question: 'How many QR codes does Bitly allow per plan?',
|
||||||
@@ -256,6 +284,41 @@ export default function BitlyAlternativePage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="border-b bg-white py-20" style={{ borderColor: '#E4E0D9' }}>
|
||||||
|
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<h2 className="mb-3 text-3xl font-bold tracking-tight sm:text-4xl" style={{ color: '#111110' }}>
|
||||||
|
QR Master vs Bitly at a glance
|
||||||
|
</h2>
|
||||||
|
<p className="mb-10 text-lg" style={{ color: '#71717A' }}>
|
||||||
|
Bitly is strongest as a link management platform. QR Master is stronger when QR codes are the main
|
||||||
|
campaign asset and you need predictable pricing, bulk creation, and QR-specific reporting.
|
||||||
|
</p>
|
||||||
|
<div className="overflow-hidden rounded-xl border" style={{ borderColor: '#E4E0D9' }}>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3" style={{ backgroundColor: '#F8F7F4' }}>
|
||||||
|
{['Use case', 'Bitly', 'QR Master'].map((heading) => (
|
||||||
|
<div key={heading} className="p-4 text-xs font-semibold uppercase tracking-wider" style={{ color: '#71717A' }}>
|
||||||
|
{heading}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{atAGlanceRows.map((row, index) => (
|
||||||
|
<div
|
||||||
|
key={row.useCase}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-3"
|
||||||
|
style={{
|
||||||
|
borderTop: '1px solid #E4E0D9',
|
||||||
|
backgroundColor: index % 2 === 0 ? '#FFFFFF' : '#FAFAF8',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="p-4 text-sm font-semibold" style={{ color: '#18181B' }}>{row.useCase}</div>
|
||||||
|
<div className="p-4 text-sm" style={{ color: '#52525B' }}>{row.bitly}</div>
|
||||||
|
<div className="p-4 text-sm font-medium" style={{ color: '#166534' }}>{row.qrMaster}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Why Bitly is the Wrong Tool */}
|
{/* Why Bitly is the Wrong Tool */}
|
||||||
<section className="py-24" style={{ backgroundColor: '#F8F7F4' }}>
|
<section className="py-24" style={{ backgroundColor: '#F8F7F4' }}>
|
||||||
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const competitor = competitors['flowcode'];
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
absolute: 'Flowcode Alternative – No Forced Branding or Scan Interstitials | QR Master',
|
absolute: 'QR Master vs Flowcode | Flowcode Alternative Without Forced Branding',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'Looking for a Flowcode alternative? QR Master gives you clean, customizable QR codes without Flowcode\'s logo or scan-hijacking interstitial pages — from €0 free, Pro at €9/month.',
|
'Looking for a Flowcode alternative? QR Master gives you clean, customizable QR codes without Flowcode\'s logo or scan-hijacking interstitial pages — from €0 free, Pro at €9/month.',
|
||||||
@@ -24,7 +24,7 @@ export const metadata: Metadata = {
|
|||||||
canonical: 'https://www.qrmaster.net/alternatives/flowcode',
|
canonical: 'https://www.qrmaster.net/alternatives/flowcode',
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Flowcode Alternative – No Forced Branding or Scan Interstitials',
|
title: 'QR Master vs Flowcode | Flowcode Alternative Without Forced Branding',
|
||||||
description:
|
description:
|
||||||
'Flowcode puts its logo on your QR codes and routes scans through its own branded page. QR Master gives you full branding control from the start.',
|
'Flowcode puts its logo on your QR codes and routes scans through its own branded page. QR Master gives you full branding control from the start.',
|
||||||
url: 'https://www.qrmaster.net/alternatives/flowcode',
|
url: 'https://www.qrmaster.net/alternatives/flowcode',
|
||||||
@@ -32,7 +32,7 @@ export const metadata: Metadata = {
|
|||||||
images: ['/og-image.png'],
|
images: ['/og-image.png'],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'Flowcode Alternative – No Forced Branding or Scan Interstitials',
|
title: 'QR Master vs Flowcode | Flowcode Alternative Without Forced Branding',
|
||||||
description:
|
description:
|
||||||
'Flowcode puts its logo on your QR codes and routes scans through its own branded page. QR Master gives you full branding control from the start.',
|
'Flowcode puts its logo on your QR codes and routes scans through its own branded page. QR Master gives you full branding control from the start.',
|
||||||
},
|
},
|
||||||
@@ -40,6 +40,34 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
const comparisonRows = competitor.features;
|
const comparisonRows = competitor.features;
|
||||||
|
|
||||||
|
const atAGlanceRows = [
|
||||||
|
{
|
||||||
|
useCase: 'Free branded QR codes',
|
||||||
|
flowcode: 'Flowcode branding can appear in the code style and scan path on lower tiers.',
|
||||||
|
qrMaster: 'No forced QR Master logo or branded interstitial page.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
useCase: 'White-label brand control',
|
||||||
|
flowcode: 'Meaningful white-label control is typically tied to higher paid tiers.',
|
||||||
|
qrMaster: 'Custom colors and logo support start on Pro at EUR 9/month.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
useCase: 'Direct scan experience',
|
||||||
|
flowcode: 'Lower-tier scans may pass through a Flowcode-branded interstitial.',
|
||||||
|
qrMaster: 'Dynamic codes redirect directly to the destination after scan logging.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
useCase: 'Bulk QR creation',
|
||||||
|
flowcode: 'No built-in CSV or Excel bulk QR generator.',
|
||||||
|
qrMaster: 'Business supports up to 1,000 unique QR codes per bulk upload.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
useCase: 'EU privacy posture',
|
||||||
|
flowcode: 'US platform; EU teams should review data processing terms.',
|
||||||
|
qrMaster: 'Scan analytics hash IPs server-side before storage.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const faqItems = [
|
const faqItems = [
|
||||||
{
|
{
|
||||||
question: 'What is the Flowcode interstitial page and why does it matter?',
|
question: 'What is the Flowcode interstitial page and why does it matter?',
|
||||||
@@ -238,6 +266,41 @@ export default function FlowcodeAlternativePage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="border-b bg-white py-20" style={{ borderColor: '#E4E0D9' }}>
|
||||||
|
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<h2 className="mb-3 text-3xl font-bold tracking-tight sm:text-4xl" style={{ color: '#111110' }}>
|
||||||
|
QR Master vs Flowcode at a glance
|
||||||
|
</h2>
|
||||||
|
<p className="mb-10 text-lg" style={{ color: '#71717A' }}>
|
||||||
|
Flowcode is design-forward. QR Master is the cleaner fit when you need a neutral QR code, direct scan
|
||||||
|
experience, bulk creation, and predictable pricing.
|
||||||
|
</p>
|
||||||
|
<div className="overflow-hidden rounded-xl border" style={{ borderColor: '#E4E0D9' }}>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3" style={{ backgroundColor: '#F8F7F4' }}>
|
||||||
|
{['Use case', 'Flowcode', 'QR Master'].map((heading) => (
|
||||||
|
<div key={heading} className="p-4 text-xs font-semibold uppercase tracking-wider" style={{ color: '#71717A' }}>
|
||||||
|
{heading}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{atAGlanceRows.map((row, index) => (
|
||||||
|
<div
|
||||||
|
key={row.useCase}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-3"
|
||||||
|
style={{
|
||||||
|
borderTop: '1px solid #E4E0D9',
|
||||||
|
backgroundColor: index % 2 === 0 ? '#FFFFFF' : '#FAFAF8',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="p-4 text-sm font-semibold" style={{ color: '#18181B' }}>{row.useCase}</div>
|
||||||
|
<div className="p-4 text-sm" style={{ color: '#52525B' }}>{row.flowcode}</div>
|
||||||
|
<div className="p-4 text-sm font-medium" style={{ color: '#166534' }}>{row.qrMaster}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* The Real Problem with Flowcode */}
|
{/* The Real Problem with Flowcode */}
|
||||||
<section className="py-24" style={{ backgroundColor: '#F8F7F4' }}>
|
<section className="py-24" style={{ backgroundColor: '#F8F7F4' }}>
|
||||||
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import Link from 'next/link';
|
|||||||
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
||||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||||
import { breadcrumbSchema } from '@/lib/schema';
|
import { breadcrumbSchema } from '@/lib/schema';
|
||||||
|
import { FAQSection } from '@/components/aeo/FAQSection';
|
||||||
import { MarketingPageTracker, TrackedCtaLink } from '@/components/marketing/MarketingAnalytics';
|
import { MarketingPageTracker, TrackedCtaLink } from '@/components/marketing/MarketingAnalytics';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
absolute: 'QR Code Platform Alternatives | QR Master',
|
absolute: 'QR Master Alternatives: Compare QR Code Platforms',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'Compare QR Master with QR-Code-Generator.com, Flowcode, Beaconstac / Uniqode, and Bitly. Find the right QR alternative for pricing, branding, analytics, and bulk creation.',
|
'Compare QR Master with QR-Code-Generator.com, Flowcode, Beaconstac / Uniqode, and Bitly. Find the right QR alternative for pricing, branding, analytics, and bulk creation.',
|
||||||
@@ -18,7 +19,7 @@ export const metadata: Metadata = {
|
|||||||
canonical: 'https://www.qrmaster.net/alternatives',
|
canonical: 'https://www.qrmaster.net/alternatives',
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'QR Code Platform Alternatives | QR Master',
|
title: 'QR Master Alternatives: Compare QR Code Platforms',
|
||||||
description:
|
description:
|
||||||
'Clean comparisons for the most common QR code platform alternatives, from pricing traps to branding limits and enterprise overkill.',
|
'Clean comparisons for the most common QR code platform alternatives, from pricing traps to branding limits and enterprise overkill.',
|
||||||
url: 'https://www.qrmaster.net/alternatives',
|
url: 'https://www.qrmaster.net/alternatives',
|
||||||
@@ -26,7 +27,7 @@ export const metadata: Metadata = {
|
|||||||
images: ['/og-image.png'],
|
images: ['/og-image.png'],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'QR Code Platform Alternatives | QR Master',
|
title: 'QR Master Alternatives: Compare QR Code Platforms',
|
||||||
description:
|
description:
|
||||||
'Compare QR Master with Flowcode, Bitly, Beaconstac / Uniqode, and QR-Code-Generator.com.',
|
'Compare QR Master with Flowcode, Bitly, Beaconstac / Uniqode, and QR-Code-Generator.com.',
|
||||||
},
|
},
|
||||||
@@ -46,6 +47,43 @@ const pageSchema = {
|
|||||||
'Comparison pages for QR Master versus major QR code and adjacent link-management platforms.',
|
'Comparison pages for QR Master versus major QR code and adjacent link-management platforms.',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const faqItems = [
|
||||||
|
{
|
||||||
|
question: 'What is the best QR code platform alternative?',
|
||||||
|
answer:
|
||||||
|
'The best alternative depends on the workflow. QR Master is strongest when you need dynamic QR codes, clean branding, bulk creation, and privacy-friendly analytics without enterprise pricing.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'When should I choose QR Master over Bitly?',
|
||||||
|
answer:
|
||||||
|
'Choose QR Master when QR codes are the primary workflow. Bitly is useful for short links, but its QR code limits and link-first dashboard become restrictive for campaigns with many QR codes.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'When should I choose QR Master over Flowcode?',
|
||||||
|
answer:
|
||||||
|
'Choose QR Master when you want a direct scan experience without third-party branding or branded interstitial pages. QR Master keeps the scan flow focused on your destination.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Is Beaconstac / Uniqode better than QR Master?',
|
||||||
|
answer:
|
||||||
|
'Beaconstac / Uniqode can make sense for enterprise teams that need a broad QR platform. QR Master is usually a better fit for smaller teams that want dynamic QR codes, analytics, and bulk creation at a simpler price point.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const faqSchema = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'FAQPage',
|
||||||
|
'@id': 'https://www.qrmaster.net/alternatives#faq',
|
||||||
|
mainEntity: faqItems.map((item) => ({
|
||||||
|
'@type': 'Question',
|
||||||
|
name: item.question,
|
||||||
|
acceptedAnswer: {
|
||||||
|
'@type': 'Answer',
|
||||||
|
text: item.answer,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
const alternativePages = [
|
const alternativePages = [
|
||||||
{
|
{
|
||||||
href: '/alternatives/qr-code-generator',
|
href: '/alternatives/qr-code-generator',
|
||||||
@@ -77,10 +115,61 @@ const alternativePages = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const comparisonRows = [
|
||||||
|
{
|
||||||
|
platform: 'Bitly',
|
||||||
|
href: '/alternatives/bitly',
|
||||||
|
price: 'Link-first paid plans',
|
||||||
|
freePlan: 'Limited QR use',
|
||||||
|
dynamicCodes: 'QR feature inside link management',
|
||||||
|
analytics: 'Link analytics first',
|
||||||
|
gdpr: 'Review DPA for EU use',
|
||||||
|
bulk: 'No QR-first bulk workflow',
|
||||||
|
branding: 'Bitly-branded link context',
|
||||||
|
qrMasterAngle: 'QR-first workflow with 50 dynamic codes on Pro',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: 'Flowcode',
|
||||||
|
href: '/alternatives/flowcode',
|
||||||
|
price: 'Brand and team tiers',
|
||||||
|
freePlan: 'Limited free path',
|
||||||
|
dynamicCodes: 'Strong QR design workflow',
|
||||||
|
analytics: 'Paid campaign analytics',
|
||||||
|
gdpr: 'US vendor; review data terms',
|
||||||
|
bulk: 'Best for larger plans',
|
||||||
|
branding: 'Design-forward, possible platform branding',
|
||||||
|
qrMasterAngle: 'Clean redirects and brand control without a branded scan page',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: 'Beaconstac / Uniqode',
|
||||||
|
href: '/alternatives/beaconstac',
|
||||||
|
price: 'Enterprise-oriented tiers',
|
||||||
|
freePlan: 'Trial or limited evaluation',
|
||||||
|
dynamicCodes: 'Advanced enterprise QR platform',
|
||||||
|
analytics: 'Advanced paid analytics',
|
||||||
|
gdpr: 'DPA and configuration review needed',
|
||||||
|
bulk: 'Strong, often enterprise-oriented',
|
||||||
|
branding: 'Enterprise controls',
|
||||||
|
qrMasterAngle: 'Simpler dynamic QR, analytics, and bulk creation for SMB teams',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: 'QR-Code-Generator.com',
|
||||||
|
href: '/alternatives/qr-code-generator',
|
||||||
|
price: 'Paid dynamic QR after trial',
|
||||||
|
freePlan: 'Static QR path',
|
||||||
|
dynamicCodes: 'Dynamic QR behind paid upgrade',
|
||||||
|
analytics: 'Paid analytics',
|
||||||
|
gdpr: 'Review vendor terms',
|
||||||
|
bulk: 'Paid workflow',
|
||||||
|
branding: 'Template-driven branding',
|
||||||
|
qrMasterAngle: 'Free dynamic codes and transparent paid tiers',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function AlternativesHubPage() {
|
export default function AlternativesHubPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SeoJsonLd data={[pageSchema, breadcrumbSchema(breadcrumbItems)]} />
|
<SeoJsonLd data={[pageSchema, faqSchema, breadcrumbSchema(breadcrumbItems)]} />
|
||||||
<MarketingPageTracker pageType="commercial" cluster="competitor" />
|
<MarketingPageTracker pageType="commercial" cluster="competitor" />
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#F8F7F4', color: '#18181B' }}>
|
<div className="min-h-screen" style={{ backgroundColor: '#F8F7F4', color: '#18181B' }}>
|
||||||
<section className="border-b bg-white" style={{ borderColor: '#E4E0D9' }}>
|
<section className="border-b bg-white" style={{ borderColor: '#E4E0D9' }}>
|
||||||
@@ -175,6 +264,79 @@ export default function AlternativesHubPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="border-y bg-white py-24" style={{ borderColor: '#E4E0D9' }}>
|
||||||
|
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-12 max-w-3xl">
|
||||||
|
<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl" style={{ color: '#111110' }}>
|
||||||
|
QR Master vs Bitly, Flowcode, Beaconstac and other QR platforms
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg" style={{ color: '#71717A' }}>
|
||||||
|
Use this table to pick the comparison page that matches your search intent: pricing, branding, bulk
|
||||||
|
creation, analytics, or migration risk.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto rounded-xl border" style={{ borderColor: '#E4E0D9' }}>
|
||||||
|
<table className="w-full min-w-[980px] text-sm">
|
||||||
|
<thead style={{ backgroundColor: '#F8F7F4' }}>
|
||||||
|
<tr>
|
||||||
|
{[
|
||||||
|
'Platform',
|
||||||
|
'Price',
|
||||||
|
'Free plan',
|
||||||
|
'Dynamic codes',
|
||||||
|
'Analytics',
|
||||||
|
'GDPR / EU fit',
|
||||||
|
'Bulk',
|
||||||
|
'Branding',
|
||||||
|
'Compare',
|
||||||
|
].map((heading) => (
|
||||||
|
<th
|
||||||
|
key={heading}
|
||||||
|
className="p-4 text-left text-xs font-semibold uppercase tracking-wider"
|
||||||
|
style={{ color: '#71717A' }}
|
||||||
|
>
|
||||||
|
{heading}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{comparisonRows.map((row, index) => (
|
||||||
|
<tr
|
||||||
|
key={row.platform}
|
||||||
|
style={{
|
||||||
|
borderTop: '1px solid #E4E0D9',
|
||||||
|
backgroundColor: index % 2 === 0 ? '#FFFFFF' : '#FAFAF8',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td className="p-4 font-semibold" style={{ color: '#18181B' }}>{row.platform}</td>
|
||||||
|
<td className="p-4" style={{ color: '#52525B' }}>{row.price}</td>
|
||||||
|
<td className="p-4" style={{ color: '#52525B' }}>{row.freePlan}</td>
|
||||||
|
<td className="p-4" style={{ color: '#52525B' }}>{row.dynamicCodes}</td>
|
||||||
|
<td className="p-4" style={{ color: '#52525B' }}>{row.analytics}</td>
|
||||||
|
<td className="p-4" style={{ color: '#52525B' }}>{row.gdpr}</td>
|
||||||
|
<td className="p-4" style={{ color: '#52525B' }}>{row.bulk}</td>
|
||||||
|
<td className="p-4" style={{ color: '#52525B' }}>{row.branding}</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<Link href={row.href} className="font-semibold" style={{ color: '#166534' }}>
|
||||||
|
QR Master vs {row.platform}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="py-24" style={{ backgroundColor: '#F8F7F4' }}>
|
||||||
|
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<FAQSection items={faqItems} title="Common questions about QR platform alternatives" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import { MarketingPageTracker } from '@/components/marketing/MarketingAnalytics'
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
absolute: 'Bulk QR Code Generator - Create QR Codes from a Spreadsheet',
|
absolute: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'Generate up to 1,000 static QR codes from CSV or Excel files in the QR Master Business plan. Upload, preview, download as ZIP, or save the batch to your dashboard.',
|
'Generate up to 1,000 QR codes from Excel, CSV, XLSX, or exported Google Sheets data. Upload, preview, batch-create, download ZIP files, or save to your dashboard.',
|
||||||
keywords:
|
keywords:
|
||||||
'bulk qr code generator, qr code from excel, csv qr code generator, bulk qr codes, spreadsheet qr generation',
|
'bulk qr code generator, bulk qr code generator excel, batch qr code generator, qr code from excel, csv qr code generator, bulk qr generator, bulk qr code generator in google sheets, spreadsheet qr generation',
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||||
languages: {
|
languages: {
|
||||||
@@ -27,17 +27,17 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Bulk QR Code Generator - Create QR Codes from a Spreadsheet',
|
title: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
|
||||||
description:
|
description:
|
||||||
'Generate up to 1,000 static QR codes from CSV or Excel files in the QR Master Business plan.',
|
'Generate up to 1,000 QR codes from CSV, Excel, XLSX, or exported Google Sheets data.',
|
||||||
url: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
url: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
images: ['/og-image.png'],
|
images: ['/og-image.png'],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'Bulk QR Code Generator - Create QR Codes from a Spreadsheet',
|
title: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
|
||||||
description:
|
description:
|
||||||
'Generate up to 1,000 static QR codes from CSV or Excel files in the QR Master Business plan.',
|
'Generate up to 1,000 QR codes from CSV, Excel, XLSX, or exported Google Sheets data.',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,7 +45,12 @@ const featureCards = [
|
|||||||
{
|
{
|
||||||
title: 'Spreadsheet upload',
|
title: 'Spreadsheet upload',
|
||||||
description:
|
description:
|
||||||
'Upload CSV, XLS, or XLSX files and map the title and content columns before generating the batch.',
|
'Upload CSV, XLS, or XLSX files from Excel, Numbers, Airtable, or a Google Sheets export and map the title and content columns before generating the batch.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Excel and Google Sheets workflow',
|
||||||
|
description:
|
||||||
|
'Prepare rows in Excel or Google Sheets, export to CSV/XLSX, then generate the full QR code batch from one clean spreadsheet.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Up to 1,000 rows per upload',
|
title: 'Up to 1,000 rows per upload',
|
||||||
@@ -75,6 +80,16 @@ const featureCards = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const inputExamples = [
|
const inputExamples = [
|
||||||
|
{
|
||||||
|
title: 'Google Sheets export',
|
||||||
|
content: 'Name,URL\nMenu QR,https://example.com/menu\nFlyer QR,https://example.com/flyer',
|
||||||
|
note: 'Export a Google Sheet as CSV and upload it as a batch QR code generator input.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Excel product list',
|
||||||
|
content: 'SKU,URL\nSKU-1001,https://example.com/products/1001',
|
||||||
|
note: 'Use Excel or XLSX rows when each product, insert, or label needs its own QR code.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Website URLs',
|
title: 'Website URLs',
|
||||||
content: 'https://example.com/product',
|
content: 'https://example.com/product',
|
||||||
@@ -140,6 +155,16 @@ const faqItems = [
|
|||||||
answer:
|
answer:
|
||||||
'The current flow accepts CSV, XLS, and XLSX files.',
|
'The current flow accepts CSV, XLS, and XLSX files.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
question: 'Can I use Google Sheets as the source?',
|
||||||
|
answer:
|
||||||
|
'Yes. Prepare the batch in Google Sheets, export it as CSV or XLSX, then upload that file to QR Master. The workflow is the same as an Excel upload.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Is this a batch QR code generator?',
|
||||||
|
answer:
|
||||||
|
'Yes. QR Master bulk creation is a batch QR code generator for spreadsheet rows. Each row becomes one QR code, and the finished batch can be downloaded together.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
question: 'Which plan includes bulk QR creation?',
|
question: 'Which plan includes bulk QR creation?',
|
||||||
answer:
|
answer:
|
||||||
@@ -161,9 +186,10 @@ const softwareSchema = {
|
|||||||
availability: 'https://schema.org/InStock',
|
availability: 'https://schema.org/InStock',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'Generate up to 1,000 static QR codes from CSV or Excel files in the QR Master Business plan.',
|
'Generate up to 1,000 static QR codes from CSV, Excel, XLSX, or exported Google Sheets files in the QR Master Business plan.',
|
||||||
featureList: [
|
featureList: [
|
||||||
'CSV, XLS, and XLSX upload',
|
'CSV, XLS, and XLSX upload',
|
||||||
|
'Excel and Google Sheets CSV export workflow',
|
||||||
'Up to 1,000 rows per upload',
|
'Up to 1,000 rows per upload',
|
||||||
'Static QR code generation',
|
'Static QR code generation',
|
||||||
'ZIP download of generated SVG files',
|
'ZIP download of generated SVG files',
|
||||||
@@ -393,6 +419,49 @@ export default function BulkQRCodeGeneratorPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section className="bg-white py-16">
|
||||||
|
<div className="container mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-10 max-w-3xl">
|
||||||
|
<p className="mb-3 text-sm font-semibold uppercase tracking-wider text-green-600">
|
||||||
|
Excel, CSV, and Google Sheets
|
||||||
|
</p>
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900">
|
||||||
|
Batch QR code generation from spreadsheet rows
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-lg leading-relaxed text-gray-600">
|
||||||
|
Use QR Master as a bulk QR code generator when your source data
|
||||||
|
already lives in Excel, a CSV export, or Google Sheets. Keep one
|
||||||
|
row per QR code, map the columns, preview the batch, then export
|
||||||
|
the generated SVG files together.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: '1. Prepare columns',
|
||||||
|
body: 'Use columns such as title, URL, SKU, campaign, or location. The content column is what the QR code encodes.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Export clean data',
|
||||||
|
body: 'Save Excel as XLSX or export Google Sheets to CSV. Avoid merged cells and keep one QR code per row.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3. Generate the batch',
|
||||||
|
body: 'Upload the file, map title and content, preview the rows, and download the finished QR codes as a ZIP.',
|
||||||
|
},
|
||||||
|
].map((step) => (
|
||||||
|
<Card key={step.title} className="p-6">
|
||||||
|
<h3 className="mb-2 text-xl font-semibold text-gray-900">
|
||||||
|
{step.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">{step.body}</p>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="container mx-auto max-w-5xl px-4 pb-8 sm:px-6 lg:px-8">
|
<div className="container mx-auto max-w-5xl px-4 pb-8 sm:px-6 lg:px-8">
|
||||||
<FAQSection items={faqItems} title="Bulk QR questions" />
|
<FAQSection items={faqItems} title="Bulk QR questions" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1393
src/app/(main)/(marketing)/dynamic-barcode-generator/page.tsx
Normal file
@@ -14,9 +14,9 @@ function truncateAtWord(text: string, maxLength: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const title = truncateAtWord('QR Master FAQ: Dynamic, Tracking, Bulk, and Print', 60);
|
const title = truncateAtWord('QR Code FAQ – Common Questions Answered | QR Master', 60);
|
||||||
const description = truncateAtWord(
|
const description = truncateAtWord(
|
||||||
'Answers about dynamic QR codes, scan tracking, privacy, bulk creation, and print setup.',
|
'Quick answers to common QR code questions: Do QR codes expire? Static vs dynamic? Can they be scanned through laminate? Get clear answers.',
|
||||||
160
|
160
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -58,6 +58,41 @@ type FAQItemWithRichText = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const faqs: FAQItemWithRichText[] = [
|
const faqs: FAQItemWithRichText[] = [
|
||||||
|
{
|
||||||
|
question: 'Do QR codes expire?',
|
||||||
|
answer:
|
||||||
|
'Static QR codes never expire — the destination is permanently encoded in the image and works indefinitely. Dynamic QR codes remain active as long as your subscription is active. QR Master keeps static QR codes functional forever, including on the free plan.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Will QR codes become obsolete?',
|
||||||
|
answer:
|
||||||
|
'QR codes are unlikely to become obsolete in the near future. Adoption has accelerated since 2020 — Statista reports that QR code usage grew by over 750% between 2018 and 2023. Every major smartphone camera app now natively reads QR codes without a separate app, removing the main adoption barrier.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Will QR codes replace barcodes?',
|
||||||
|
answer:
|
||||||
|
'QR codes will not fully replace barcodes. Barcodes remain dominant in high-speed retail checkout due to laser scanner compatibility and established infrastructure. QR codes excel for consumer-facing use cases: menus, marketing, payments, and product pages. Both formats coexist across different industries.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Do QR codes work through laminate?',
|
||||||
|
answer:
|
||||||
|
'Yes. QR codes work through standard laminate because the scanner reads the contrast pattern, not the physical surface. Matte laminate is preferable to gloss, which can cause glare under direct lighting. Avoid laminate with metallic or tinted finishes that alter contrast.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Do QR codes work with a cracked phone screen?',
|
||||||
|
answer:
|
||||||
|
'Usually yes, as long as the camera can still capture the QR code image. Minor cracks often do not prevent scanning. A heavily cracked screen that distorts the camera view may cause scanning failures. The QR code itself is not affected — only the device reading it matters.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'When were QR codes invented?',
|
||||||
|
answer:
|
||||||
|
'QR codes were invented in 1994 by Masahiro Hara at Denso Wave, a Toyota subsidiary in Japan. They were originally designed to track automotive parts during manufacturing. QR stands for "Quick Response." The format was made publicly available royalty-free, which enabled widespread global adoption.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Can QR codes run out?',
|
||||||
|
answer:
|
||||||
|
'No — QR codes cannot run out. The QR code standard supports approximately 10^9 unique combinations for a typical URL, far more than could ever be used. Generating a new QR code does not "use up" anything from a shared pool. Each code is generated independently.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
question: 'What is a dynamic QR code?',
|
question: 'What is a dynamic QR code?',
|
||||||
answer:
|
answer:
|
||||||
@@ -210,7 +245,7 @@ export default function FAQPage() {
|
|||||||
<p className="mb-4 text-xl text-gray-600">
|
<p className="mb-4 text-xl text-gray-600">
|
||||||
Answers about dynamic QR codes, scan tracking, privacy, bulk creation, and print setup.
|
Answers about dynamic QR codes, scan tracking, privacy, bulk creation, and print setup.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500">Last updated: March 12, 2026</p>
|
<p className="text-sm text-gray-500">Last updated: April 20, 2026</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ export const metadata: Metadata = {
|
|||||||
: { index: false, follow: false },
|
: { index: false, follow: false },
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{ url: '/favicon1.png', sizes: '2048x2048', type: 'image/png' },
|
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
||||||
|
{ url: '/favicon.ico', sizes: '16x16 32x32', type: 'image/x-icon' },
|
||||||
],
|
],
|
||||||
shortcut: '/favicon1.png',
|
shortcut: '/favicon.ico',
|
||||||
apple: '/favicon1.png',
|
apple: '/logo.svg',
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { pillarMeta } from "@/lib/pillar-data";
|
|||||||
import { getPublishedPosts } from "@/lib/content";
|
import { getPublishedPosts } from "@/lib/content";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Learn QR Code Mastery | QR Master Hub",
|
title: "QR Code Tutorials & Guides – QR Master",
|
||||||
description: "Guides, use cases, tracking deep-dives, and security best practices for dynamic QR codes.",
|
description: "Free step-by-step QR code guides: create, track, and optimize dynamic QR codes for your business. No account needed to start.",
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: "https://www.qrmaster.net/learn",
|
canonical: "https://www.qrmaster.net/learn",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||||
import { organizationSchema, websiteSchema, softwareApplicationSchema, reviewSchema, aggregateRatingSchema } from '@/lib/schema';
|
import {
|
||||||
import { getFeaturedTestimonials, getAggregateRating } from '@/lib/testimonial-data';
|
websiteSchema,
|
||||||
|
softwareApplicationSchema,
|
||||||
|
} from '@/lib/schema';
|
||||||
|
import { getAggregateRating } from '@/lib/testimonial-data';
|
||||||
import HomePageClient from '@/components/marketing/HomePageClient';
|
import HomePageClient from '@/components/marketing/HomePageClient';
|
||||||
|
|
||||||
function truncateAtWord(text: string, maxLength: number): string {
|
function truncateAtWord(text: string, maxLength: number): string {
|
||||||
@@ -13,16 +16,29 @@ function truncateAtWord(text: string, maxLength: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const title = truncateAtWord('QR Master: Dynamic QR Generator', 60);
|
|
||||||
const description = truncateAtWord(
|
const description = truncateAtWord(
|
||||||
'Create dynamic QR codes, track scans, and scale campaigns with secure analytics. Free advanced features, bulk generation, and custom branding available.',
|
'QR Master is a free dynamic QR code generator with tracking, editable destinations, custom branding, and bulk QR creation. Create static QR codes without signup.',
|
||||||
160
|
160
|
||||||
);
|
);
|
||||||
|
const brandTitle = 'QR Master - Free Dynamic QR Code Generator with Tracking';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title: brandTitle,
|
||||||
description,
|
description,
|
||||||
keywords: ['qr generator', 'free qr code generator', 'custom qr code generator', 'qr code maker', 'online qr code generator', 'dynamic qr code', 'qr code with logo'],
|
keywords: [
|
||||||
|
'qr generator',
|
||||||
|
'free qr code generator',
|
||||||
|
'custom qr code generator',
|
||||||
|
'qr code maker',
|
||||||
|
'online qr code generator',
|
||||||
|
'dynamic qr code',
|
||||||
|
'qr code with logo',
|
||||||
|
'barcode generator',
|
||||||
|
'free barcode generator',
|
||||||
|
'qr master',
|
||||||
|
'qrmaster',
|
||||||
|
'qr code master',
|
||||||
|
],
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: 'https://www.qrmaster.net/',
|
canonical: 'https://www.qrmaster.net/',
|
||||||
languages: {
|
languages: {
|
||||||
@@ -32,7 +48,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title,
|
title: brandTitle,
|
||||||
description,
|
description,
|
||||||
url: 'https://www.qrmaster.net/',
|
url: 'https://www.qrmaster.net/',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
@@ -46,44 +62,69 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title,
|
title: brandTitle,
|
||||||
description,
|
description,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const featuredTestimonials = getFeaturedTestimonials();
|
|
||||||
const aggregateRating = getAggregateRating();
|
const aggregateRating = getAggregateRating();
|
||||||
const reviewSchemas = featuredTestimonials.map(t => reviewSchema(t));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SeoJsonLd data={[
|
<SeoJsonLd
|
||||||
|
data={[
|
||||||
websiteSchema(),
|
websiteSchema(),
|
||||||
organizationSchema(),
|
|
||||||
softwareApplicationSchema(aggregateRating),
|
softwareApplicationSchema(aggregateRating),
|
||||||
aggregateRatingSchema(aggregateRating),
|
]}
|
||||||
...reviewSchemas
|
/>
|
||||||
]} />
|
|
||||||
|
|
||||||
{/* Server-rendered SEO content for crawlers */}
|
{/* Server-rendered SEO content for crawlers */}
|
||||||
<div className="sr-only" aria-hidden="false">
|
<div className="sr-only" aria-hidden="false">
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Create professional QR codes for your business with QR Master. Our dynamic QR code generator
|
Create professional QR codes for your business with QR Master. Our
|
||||||
lets you create trackable QR codes, edit destinations anytime, and view detailed analytics.
|
dynamic QR code generator lets you create trackable QR codes, edit
|
||||||
Perfect for restaurants, retail, events, and marketing campaigns.
|
destinations anytime, and view detailed analytics. Perfect for
|
||||||
|
restaurants, retail, events, and marketing campaigns.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Features include: Dynamic QR codes with real-time tracking, bulk QR code generation from Excel/CSV,
|
Features include: Dynamic QR codes with real-time tracking, bulk QR
|
||||||
custom branding with colors and logos, advanced scan analytics showing device types and locations,
|
code generation from Excel/CSV, custom branding with colors and logos,
|
||||||
vCard QR codes for digital business cards, restaurant menu QR codes, and a free{' '}
|
advanced scan analytics showing device types and locations, vCard QR
|
||||||
<a href="/tools/barcode-generator">barcode generator</a> for EAN-13, UPC-A, and Code 128 barcodes.
|
codes for digital business cards, restaurant menu QR codes, and a free{' '}
|
||||||
|
<a href="/tools/barcode-generator">barcode generator</a> for EAN-13,
|
||||||
|
UPC-A, and Code 128 barcodes.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Start free with 3 active dynamic QR codes and unlimited static codes. Upgrade to Pro for 50 codes
|
Popular QR Master workflows include the{' '}
|
||||||
with advanced analytics, or Business for 500 codes with bulk creation and priority support.
|
<a href="/dynamic-qr-code-generator">
|
||||||
|
free dynamic QR code generator
|
||||||
|
</a>
|
||||||
|
, <a href="/qr-code-tracking">QR code tracking</a>, and the{' '}
|
||||||
|
<a href="/custom-qr-code-generator">custom QR code generator</a> for
|
||||||
|
branded print campaigns.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Start free with 3 active dynamic QR codes and unlimited static codes.
|
||||||
|
Upgrade to Pro for 50 codes with advanced analytics, or Business for
|
||||||
|
500 codes with bulk creation and priority support.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Frequently used QR Master tools and industry workflows include the{' '}
|
||||||
|
<a href="/tools/teams-qr-code">Teams QR code generator</a>,{' '}
|
||||||
|
<a href="/tools/wifi-qr-code">WiFi QR code generator</a>,{' '}
|
||||||
|
<a href="/qr-code-erstellen">German QR code generator</a>, and{' '}
|
||||||
|
<a href="/qr-code-for/barbershops">QR codes for barbershops</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
High-intent QR Master pages include the{' '}
|
||||||
|
<a href="/bulk-qr-code-generator">
|
||||||
|
bulk QR code generator for Excel and CSV files
|
||||||
|
</a>
|
||||||
|
, <a href="/alternatives">QR code platform alternatives</a>,{' '}
|
||||||
|
<a href="/alternatives/beaconstac">Beaconstac alternative</a>, and{' '}
|
||||||
|
<a href="/alternatives/bitly">Bitly QR code alternative</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ import { showToast } from '@/components/ui/Toast';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { BillingToggle } from '@/components/ui/BillingToggle';
|
import { BillingToggle } from '@/components/ui/BillingToggle';
|
||||||
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||||
|
import { trackEvent } from '@/components/PostHogProvider';
|
||||||
|
import { FREE_DYNAMIC_QR_LIMIT } from '@/lib/plans';
|
||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState<string | null>(null);
|
const [loading, setLoading] = useState<string | null>(null);
|
||||||
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
|
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
|
||||||
const [currentInterval, setCurrentInterval] = useState<'month' | 'year' | null>(null);
|
const [currentInterval, setCurrentInterval] = useState<
|
||||||
|
'month' | 'year' | null
|
||||||
|
>(null);
|
||||||
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
|
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -38,6 +42,13 @@ export default function PricingPage() {
|
|||||||
setLoading(plan);
|
setLoading(plan);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
trackEvent('upgrade_clicked', {
|
||||||
|
plan,
|
||||||
|
billing_interval: billingPeriod,
|
||||||
|
source: 'pricing_page',
|
||||||
|
current_plan: currentPlan,
|
||||||
|
});
|
||||||
|
|
||||||
const response = await fetch('/api/stripe/create-checkout-session', {
|
const response = await fetch('/api/stripe/create-checkout-session', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -50,14 +61,15 @@ export default function PricingPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to create checkout session');
|
const errorData = await response.json().catch(() => null);
|
||||||
|
throw new Error(errorData?.error || 'Failed to create checkout session');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { url } = await response.json();
|
const { url } = await response.json();
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Error creating checkout session:', error);
|
console.error('Error creating checkout session:', error);
|
||||||
showToast('Failed to start checkout. Please try again.', 'error');
|
showToast(error?.message || 'Failed to start checkout. Please try again.', 'error');
|
||||||
setLoading(null);
|
setLoading(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -95,19 +107,29 @@ export default function PricingPage() {
|
|||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error canceling subscription:', error);
|
console.error('Error canceling subscription:', error);
|
||||||
showToast(error.message || 'Failed to downgrade. Please try again.', 'error');
|
showToast(
|
||||||
|
error.message || 'Failed to downgrade. Please try again.',
|
||||||
|
'error'
|
||||||
|
);
|
||||||
setLoading(null);
|
setLoading(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to check if this is the user's exact current plan (plan + interval)
|
// Helper function to check if this is the user's exact current plan (plan + interval)
|
||||||
const isCurrentPlanWithInterval = (planType: string, interval: 'month' | 'year') => {
|
const isCurrentPlanWithInterval = (
|
||||||
|
planType: string,
|
||||||
|
interval: 'month' | 'year'
|
||||||
|
) => {
|
||||||
return currentPlan === planType && currentInterval === interval;
|
return currentPlan === planType && currentInterval === interval;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to check if user has this plan but different interval
|
// Helper function to check if user has this plan but different interval
|
||||||
const hasPlanDifferentInterval = (planType: string) => {
|
const hasPlanDifferentInterval = (planType: string) => {
|
||||||
return currentPlan === planType && currentInterval && currentInterval !== billingPeriod;
|
return (
|
||||||
|
currentPlan === planType &&
|
||||||
|
currentInterval &&
|
||||||
|
currentInterval !== billingPeriod
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedInterval = billingPeriod === 'month' ? 'month' : 'year';
|
const selectedInterval = billingPeriod === 'month' ? 'month' : 'year';
|
||||||
@@ -120,7 +142,7 @@ export default function PricingPage() {
|
|||||||
period: 'forever',
|
period: 'forever',
|
||||||
showDiscount: false,
|
showDiscount: false,
|
||||||
features: [
|
features: [
|
||||||
'3 active dynamic QR codes (8 types available)',
|
`${FREE_DYNAMIC_QR_LIMIT} active dynamic QR codes (8 types available)`,
|
||||||
'Unlimited static QR codes',
|
'Unlimited static QR codes',
|
||||||
'Basic scan tracking',
|
'Basic scan tracking',
|
||||||
'Standard QR design templates',
|
'Standard QR design templates',
|
||||||
@@ -178,6 +200,24 @@ export default function PricingPage() {
|
|||||||
popular: false,
|
popular: false,
|
||||||
onUpgrade: () => handleUpgrade('BUSINESS'),
|
onUpgrade: () => handleUpgrade('BUSINESS'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'enterprise',
|
||||||
|
name: 'Enterprise',
|
||||||
|
price: 'Custom',
|
||||||
|
period: '',
|
||||||
|
showDiscount: false,
|
||||||
|
features: [
|
||||||
|
'∞ dynamic QR codes',
|
||||||
|
'Unlimited static QR codes',
|
||||||
|
'Everything from Business',
|
||||||
|
'Dedicated Account Manager',
|
||||||
|
],
|
||||||
|
buttonText: 'Contact Us',
|
||||||
|
buttonVariant: 'outline' as const,
|
||||||
|
disabled: false,
|
||||||
|
popular: false,
|
||||||
|
onUpgrade: () => (window.location.href = 'mailto:timo@qrmaster.net'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -195,11 +235,13 @@ export default function PricingPage() {
|
|||||||
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
|
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto">
|
||||||
{plans.map((plan) => (
|
{plans.map((plan) => (
|
||||||
<Card
|
<Card
|
||||||
key={plan.key}
|
key={plan.key}
|
||||||
className={plan.popular ? 'border-primary-500 shadow-xl relative' : ''}
|
className={
|
||||||
|
plan.popular ? 'border-primary-500 shadow-xl relative' : ''
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{plan.popular && (
|
{plan.popular && (
|
||||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
||||||
@@ -210,17 +252,11 @@ export default function PricingPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<CardHeader className="text-center pb-8">
|
<CardHeader className="text-center pb-8">
|
||||||
<CardTitle className="text-2xl mb-4">
|
<CardTitle className="text-2xl mb-4">{plan.name}</CardTitle>
|
||||||
{plan.name}
|
|
||||||
</CardTitle>
|
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="flex items-baseline justify-center">
|
<div className="flex items-baseline justify-center">
|
||||||
<span className="text-4xl font-bold">
|
<span className="text-4xl font-bold">{plan.price}</span>
|
||||||
{plan.price}
|
<span className="text-gray-600 ml-2">{plan.period}</span>
|
||||||
</span>
|
|
||||||
<span className="text-gray-600 ml-2">
|
|
||||||
{plan.period}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{plan.showDiscount && (
|
{plan.showDiscount && (
|
||||||
<Badge variant="success" className="mt-2">
|
<Badge variant="success" className="mt-2">
|
||||||
@@ -234,8 +270,16 @@ export default function PricingPage() {
|
|||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{plan.features.map((feature: string, index: number) => (
|
{plan.features.map((feature: string, index: number) => (
|
||||||
<li key={index} className="flex items-start space-x-3">
|
<li key={index} className="flex items-start space-x-3">
|
||||||
<svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
<svg
|
||||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-gray-700">{feature}</span>
|
<span className="text-gray-700">{feature}</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -247,9 +291,15 @@ export default function PricingPage() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={plan.disabled || loading === plan.key.toUpperCase()}
|
disabled={plan.disabled || loading === plan.key.toUpperCase()}
|
||||||
onClick={plan.key === 'free' ? (plan as any).onDowngrade : (plan as any).onUpgrade}
|
onClick={
|
||||||
|
plan.key === 'free'
|
||||||
|
? (plan as any).onDowngrade
|
||||||
|
: (plan as any).onUpgrade
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{loading === plan.key.toUpperCase() ? 'Processing...' : plan.buttonText}
|
{loading === plan.key.toUpperCase()
|
||||||
|
? 'Processing...'
|
||||||
|
: plan.buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -261,7 +311,13 @@ export default function PricingPage() {
|
|||||||
All plans include unlimited static QR codes and basic customization.
|
All plans include unlimited static QR codes and basic customization.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-600 mt-2">
|
<p className="text-gray-600 mt-2">
|
||||||
Need help choosing? <ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</ObfuscatedMailto>
|
Need help choosing?{' '}
|
||||||
|
<ObfuscatedMailto
|
||||||
|
email="support@qrmaster.net"
|
||||||
|
className="text-primary-600 hover:text-primary-700 underline"
|
||||||
|
>
|
||||||
|
Contact our team
|
||||||
|
</ObfuscatedMailto>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildUseCaseMetadata,
|
buildUseCaseMetadata,
|
||||||
UseCasePageTemplate,
|
UseCasePageTemplate,
|
||||||
} from "@/components/marketing/UseCasePageTemplate";
|
} from '@/components/marketing/UseCasePageTemplate';
|
||||||
import { GrowthLinksSection } from "@/components/marketing/GrowthLinksSection";
|
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
|
||||||
|
|
||||||
export const metadata: Metadata = buildUseCaseMetadata({
|
export const metadata: Metadata = buildUseCaseMetadata({
|
||||||
title: "QR Code Analytics",
|
title: 'QR Code Analytics: Measure Offline Campaigns',
|
||||||
description:
|
description:
|
||||||
"Measure QR code scans by placement, timing, device context, and campaign route so offline workflows become reportable.",
|
'Use QR code analytics to interpret scan data, compare placements, measure offline campaigns, and decide what to update or reprint next.',
|
||||||
canonicalPath: "/qr-code-analytics",
|
canonicalPath: '/qr-code-analytics',
|
||||||
});
|
});
|
||||||
|
|
||||||
const softwareSchema = {
|
const softwareSchema = {
|
||||||
"@context": "https://schema.org",
|
'@context': 'https://schema.org',
|
||||||
"@type": "SoftwareApplication",
|
'@type': 'SoftwareApplication',
|
||||||
"@id": "https://www.qrmaster.net/qr-code-analytics#software",
|
'@id': 'https://www.qrmaster.net/qr-code-analytics#software',
|
||||||
name: "QR Master - QR Code Analytics",
|
name: 'QR Master - QR Code Analytics',
|
||||||
applicationCategory: "BusinessApplication",
|
applicationCategory: 'BusinessApplication',
|
||||||
operatingSystem: "Web Browser",
|
operatingSystem: 'Web Browser',
|
||||||
offers: {
|
offers: {
|
||||||
"@type": "Offer",
|
'@type': 'Offer',
|
||||||
price: "0",
|
price: '0',
|
||||||
priceCurrency: "USD",
|
priceCurrency: 'USD',
|
||||||
availability: "https://schema.org/InStock",
|
availability: 'https://schema.org/InStock',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
"QR analytics software for measuring scans by placement, timing, device context, and offline campaign routing.",
|
'QR analytics software for measuring scans by placement, timing, device context, and offline campaign routing.',
|
||||||
featureList: [
|
featureList: [
|
||||||
"Placement-level scan reporting",
|
'Placement-level scan reporting',
|
||||||
"Device and timing context",
|
'Device and timing context',
|
||||||
"Offline-to-online campaign attribution",
|
'Offline-to-online campaign attribution',
|
||||||
"Scan visibility across print workflows",
|
'Scan visibility across print workflows',
|
||||||
"Destination updates without reprinting",
|
'Destination updates without reprinting',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,102 +41,132 @@ export default function QRCodeAnalyticsPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UseCasePageTemplate
|
<UseCasePageTemplate
|
||||||
title="QR Code Analytics"
|
title="QR Code Analytics: Measure Offline Campaigns"
|
||||||
description="Measure QR code scans by placement, timing, device context, and campaign route so offline workflows become reportable."
|
description="Use QR code analytics to interpret scan data, compare placements, measure offline campaigns, and decide what to update or reprint next."
|
||||||
eyebrow="Analytics"
|
eyebrow="Analytics interpretation"
|
||||||
intro="QR code analytics matters when a scan is not just a click, but evidence that a sign, flyer, package, or service prompt is doing its job in the real world."
|
intro="QR code analytics turns scan events into decisions: which printed placement worked, which destination needs work, and what should be changed before the next print run."
|
||||||
pageType="commercial"
|
pageType="commercial"
|
||||||
cluster="qr-analytics"
|
cluster="qr-analytics"
|
||||||
useCase="qr-analytics"
|
useCase="qr-analytics"
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{ name: "Home", url: "/" },
|
{ name: 'Home', url: '/' },
|
||||||
{ name: "QR Code Analytics", url: "/qr-code-analytics" },
|
{ name: 'QR Code Analytics', url: '/qr-code-analytics' },
|
||||||
]}
|
]}
|
||||||
answer="QR code analytics helps you understand which printed placements, campaigns, and post-scan routes generate useful activity so you can improve what gets reprinted, redistributed, or scaled next."
|
answer="QR code analytics helps teams interpret QR scan data by placement, timing, device context, and campaign route so offline marketing decisions are based on evidence rather than raw scan counts alone."
|
||||||
whenToUse={[
|
whenToUse={[
|
||||||
"You need more than raw scan counts from campaigns, packaging, or offline placements.",
|
'You need more than raw scan counts from campaigns, packaging, or offline placements.',
|
||||||
"You want to compare where scans happen and which printed surfaces actually drive action.",
|
'You want to compare where scans happen and which printed surfaces actually drive action.',
|
||||||
"You need a clearer bridge between QR scans and business outcomes such as signup, offers, or support engagement.",
|
'You need a clearer bridge between QR scans and business outcomes such as signup, offers, or support engagement.',
|
||||||
]}
|
]}
|
||||||
comparisonItems={[
|
comparisonItems={[
|
||||||
{ label: "Placement visibility", text: "Usually blended", value: true },
|
{
|
||||||
{ label: "Post-print reporting", text: "Weak", value: true },
|
label: 'Tracking collects scan events',
|
||||||
{ label: "Campaign comparison", text: "Manual or partial", value: true },
|
text: 'Input data',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Analytics explains performance',
|
||||||
|
text: 'Decision layer',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reprint decisions',
|
||||||
|
text: 'Based on evidence',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
howToSteps={[
|
howToSteps={[
|
||||||
"Create QR flows that map to real placements or workflow contexts instead of one generic code for every use case.",
|
'Create QR flows that map to real placements or workflow contexts instead of one generic code for every use case.',
|
||||||
"Track scans with enough context to compare signs, flyers, inserts, or support surfaces cleanly.",
|
'Track scans with enough context to compare signs, flyers, inserts, or support surfaces cleanly.',
|
||||||
"Use the reporting to decide which destinations, offers, or print placements deserve the next round of investment.",
|
'Use the reporting to decide which destinations, offers, or print placements deserve the next round of investment.',
|
||||||
]}
|
]}
|
||||||
primaryCta={{
|
primaryCta={{
|
||||||
href: "/signup",
|
href: '/signup',
|
||||||
label: "Start measuring QR scans",
|
label: 'Start measuring QR scans',
|
||||||
}}
|
}}
|
||||||
secondaryCta={{
|
secondaryCta={{
|
||||||
href: "/use-cases",
|
href: '/use-cases',
|
||||||
label: "Browse measured workflows",
|
label: 'Browse measured workflows',
|
||||||
}}
|
}}
|
||||||
workflowTitle="What useful QR analytics should help you answer"
|
workflowTitle="Questions QR analytics should answer"
|
||||||
workflowIntro="The point of analytics is not to produce dashboards for their own sake. It is to make better decisions about what to print again, where to place it, and what happens after the scan."
|
workflowIntro="The point of analytics is not to produce dashboards for their own sake. It is to decide what to print again, where to place it, and what should happen after the scan."
|
||||||
workflowCards={[
|
workflowCards={[
|
||||||
{
|
{
|
||||||
title: "Placement comparison",
|
title: 'Which placement worked?',
|
||||||
description:
|
description:
|
||||||
"Separate flyer, packaging, sign, event, or service-surface traffic so you know which printed context actually creates useful scan activity.",
|
'Separate flyer, packaging, sign, event, or service-surface traffic so you know which printed context actually creates useful scan activity.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Post-print flexibility",
|
title: 'What should change next?',
|
||||||
description:
|
description:
|
||||||
"Review performance and then improve the destination, offer, or next action without replacing every physical code already in circulation.",
|
'Review performance and then improve the destination, offer, or next action without replacing every physical code already in circulation.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Operational reporting",
|
title: 'What should be reprinted?',
|
||||||
description:
|
description:
|
||||||
"Give marketing, operations, or support teams a better view of what physical QR programs are doing once they are live in the field.",
|
'Give marketing, operations, or support teams a clearer view of which physical QR programs deserve another batch.',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
checklistTitle="QR analytics checklist"
|
checklistTitle="QR analytics checklist"
|
||||||
checklist={[
|
checklist={[
|
||||||
"Define which placements or workflow surfaces should be compared before launching the QR program.",
|
'Define which placements or workflow surfaces should be compared before launching the QR program.',
|
||||||
"Use naming or routing that lets scans be grouped by real business context, not only by one generic campaign.",
|
'Use naming or routing that lets scans be grouped by real business context, not only by one generic campaign.',
|
||||||
"Make the first post-scan step relevant enough that a scan can become a useful action, not a bounce.",
|
'Make the first post-scan step relevant enough that a scan can become a useful action, not a bounce.',
|
||||||
"Review analytics before reprinting so the next batch reflects real-world performance.",
|
'Review analytics before reprinting so the next batch reflects real-world performance.',
|
||||||
]}
|
]}
|
||||||
supportLinks={[
|
supportLinks={[
|
||||||
{
|
{
|
||||||
href: "/use-cases/packaging-qr-codes",
|
href: '/use-cases/packaging-qr-codes',
|
||||||
title: "Use case: Packaging QR Codes",
|
title: 'Use case: Packaging QR Codes',
|
||||||
description:
|
description:
|
||||||
"See how packaging scans can become a measurable post-purchase signal instead of a blind spot.",
|
'See how packaging scans can become a measurable post-purchase signal instead of a blind spot.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/use-cases/flyer-qr-codes",
|
href: '/qr-code-for-marketing-campaigns',
|
||||||
title: "Use case: Flyer QR Codes",
|
title: 'QR Codes for Marketing Campaigns',
|
||||||
description:
|
description:
|
||||||
"Useful when scan performance needs to be reviewed by distribution point or campaign wave.",
|
'Plan campaign QR workflows around attribution, creative testing, and print distribution.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/blog/trackable-qr-codes",
|
href: '/use-cases/flyer-qr-codes',
|
||||||
title: "Trackable QR Codes",
|
title: 'Use case: Flyer QR Codes',
|
||||||
description:
|
description:
|
||||||
"Support article for understanding what measurable QR setups should capture and why it matters.",
|
'Useful when scan performance needs to be reviewed by distribution point or campaign wave.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/blog/utm-parameter-qr-codes',
|
||||||
|
title: 'UTM Parameters with QR Codes',
|
||||||
|
description:
|
||||||
|
'Use GA4 campaign parameters when QR scan data needs to connect to post-scan conversions.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/blog/trackable-qr-codes',
|
||||||
|
title: 'Trackable QR Codes',
|
||||||
|
description:
|
||||||
|
'Support article for understanding what measurable QR setups should capture and why it matters.',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
faq={[
|
faq={[
|
||||||
{
|
{
|
||||||
question: "What can QR code analytics show?",
|
question: 'What can QR code analytics show?',
|
||||||
answer:
|
answer:
|
||||||
"QR code analytics can show scan activity by placement, time, device context, and campaign route so teams can see which physical programs are actually performing.",
|
'QR code analytics can show scan activity by placement, time, device context, and campaign route so teams can see which physical programs are actually performing.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Why are QR code analytics useful for offline campaigns?",
|
question:
|
||||||
|
'What is the difference between QR tracking and QR analytics?',
|
||||||
answer:
|
answer:
|
||||||
"They help turn offline placements such as flyers, packaging, signs, or event materials into something measurable instead of relying on assumptions about what worked.",
|
'QR tracking collects scan events. QR analytics interprets those events so teams can compare placements, understand campaign performance, and decide what to update or reprint.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Do I need dynamic QR codes for analytics?",
|
question: 'Why are QR code analytics useful for offline campaigns?',
|
||||||
answer:
|
answer:
|
||||||
"In most cases yes, because analytics usually depends on a managed redirect or reporting layer that also lets you update destinations without reprinting.",
|
'They help turn offline placements such as flyers, packaging, signs, or event materials into something measurable instead of relying on assumptions about what worked.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Do I need dynamic QR codes for analytics?',
|
||||||
|
answer:
|
||||||
|
'In most cases yes, because analytics usually depends on a managed redirect or reporting layer that also lets you update destinations without reprinting.',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
schemaData={[softwareSchema]}
|
schemaData={[softwareSchema]}
|
||||||
@@ -149,25 +179,43 @@ export default function QRCodeAnalyticsPage() {
|
|||||||
{
|
{
|
||||||
href: '/qr-code-tracking',
|
href: '/qr-code-tracking',
|
||||||
title: 'QR Code Tracking',
|
title: 'QR Code Tracking',
|
||||||
description: 'See device, time, and location context for every scan. Understand which placements drive real activity.',
|
description:
|
||||||
|
'See device, time, and location context for every scan. Understand which placements drive real activity.',
|
||||||
ctaLabel: 'Track QR code scans',
|
ctaLabel: 'Track QR code scans',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/dynamic-qr-code-generator',
|
href: '/dynamic-qr-code-generator',
|
||||||
title: '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.',
|
description:
|
||||||
|
'Create QR codes with updatable destinations so analytics can inform what to change — without reprinting.',
|
||||||
ctaLabel: 'Create dynamic QR code',
|
ctaLabel: 'Create dynamic QR code',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/reprint-calculator',
|
href: '/reprint-calculator',
|
||||||
title: 'Reprint Cost Calculator',
|
title: 'Reprint Cost Calculator',
|
||||||
description: 'Calculate how much static reprints are costing vs one dynamic QR subscription.',
|
description:
|
||||||
|
'Calculate how much static reprints are costing vs one dynamic QR subscription.',
|
||||||
ctaLabel: 'Calculate reprint savings',
|
ctaLabel: 'Calculate reprint savings',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/qr-code-for-marketing-campaigns',
|
||||||
|
title: 'QR Codes for Marketing Campaigns',
|
||||||
|
description:
|
||||||
|
'Plan offline campaigns where each placement has a measurable QR route and follow-up action.',
|
||||||
|
ctaLabel: 'Measure offline QR campaigns',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/use-cases/qr-codes-for-review-collection',
|
||||||
|
title: 'QR Codes for Review Collection',
|
||||||
|
description:
|
||||||
|
'Compare receipts, table cards, packaging, and counters as measurable review-request placements.',
|
||||||
|
ctaLabel: 'Measure review QR placements',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/pricing',
|
href: '/pricing',
|
||||||
title: 'Compare Plans',
|
title: 'Compare Plans',
|
||||||
description: 'See which plan gives you the scan volume, analytics depth, and QR code count your workflows need.',
|
description:
|
||||||
|
'See which plan gives you the scan volume, analytics depth, and QR code count your workflows need.',
|
||||||
ctaLabel: 'Compare plans',
|
ctaLabel: 'Compare plans',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ const highlightedLinks = [
|
|||||||
{ href: "/qr-code-tracking", title: "QR Code Tracking", description: "Measure scans from flyers, property signage, retail displays, trade shows, and offline campaigns." },
|
{ href: "/qr-code-tracking", title: "QR Code Tracking", description: "Measure scans from flyers, property signage, retail displays, trade shows, and offline campaigns." },
|
||||||
{ href: "/bulk-qr-code-generator", title: "Bulk QR Code Generator", description: "Best fit for packaging, product lines, multi-location rollouts, and large campaigns." },
|
{ href: "/bulk-qr-code-generator", title: "Bulk QR Code Generator", description: "Best fit for packaging, product lines, multi-location rollouts, and large campaigns." },
|
||||||
{ href: "/tools/wifi-qr-code", title: "WiFi QR Code Tool", description: "Popular for hotels, cafes, coworking spaces, clinics, and guest-facing venues." },
|
{ href: "/tools/wifi-qr-code", title: "WiFi QR Code Tool", description: "Popular for hotels, cafes, coworking spaces, clinics, and guest-facing venues." },
|
||||||
|
{ href: "/tools/teams-qr-code", title: "Teams QR Code Tool", description: "Useful for offices, meeting rooms, coworking spaces, and event check-in workflows." },
|
||||||
|
{ href: "/qr-code-for/barbershops", title: "QR Codes for Barbershops", description: "A high-intent local service page for bookings, reviews, WiFi, and social profile QR codes." },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -244,7 +246,7 @@ export default function IndustryOverviewPage() {
|
|||||||
|
|
||||||
<section className="py-16">
|
<section className="py-16">
|
||||||
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<div className="grid gap-6 lg:grid-cols-4">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{highlightedLinks.map((item) => (
|
{highlightedLinks.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
|
|||||||
@@ -8,14 +8,17 @@ import { breadcrumbSchema } from '@/lib/schema';
|
|||||||
import { AnswerFirstBlock } from '@/components/marketing/AnswerFirstBlock';
|
import { AnswerFirstBlock } from '@/components/marketing/AnswerFirstBlock';
|
||||||
import { FAQSection } from '@/components/aeo/FAQSection';
|
import { FAQSection } from '@/components/aeo/FAQSection';
|
||||||
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
|
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
|
||||||
import { MarketingPageTracker, TrackedCtaLink } from '@/components/marketing/MarketingAnalytics';
|
import {
|
||||||
|
MarketingPageTracker,
|
||||||
|
TrackedCtaLink,
|
||||||
|
} from '@/components/marketing/MarketingAnalytics';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
absolute: 'Free QR Code Tracking & Analytics – Track Every Scan',
|
absolute: 'QR Code Tracking: Track QR Code Scans',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'Track QR code scans for free. See time, device, and location data for every scan. Use dynamic QR codes to measure printed campaigns and placements from one dashboard.',
|
'Track QR code scans with dynamic QR tracking. See scan time, device, location context, placements, and privacy-aware analytics for printed campaigns.',
|
||||||
keywords:
|
keywords:
|
||||||
'qr code tracking, qr code analytics, track qr scans, dynamic qr tracking, qr scan analytics',
|
'qr code tracking, qr code analytics, track qr scans, dynamic qr tracking, qr scan analytics',
|
||||||
alternates: {
|
alternates: {
|
||||||
@@ -26,7 +29,7 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Free QR Code Tracking & Analytics – Track Every Scan',
|
title: 'QR Code Tracking: Track QR Code Scans',
|
||||||
description:
|
description:
|
||||||
'Track QR code scans with analytics for time, device, and location context. Use dynamic QR codes to measure placements and campaigns.',
|
'Track QR code scans with analytics for time, device, and location context. Use dynamic QR codes to measure placements and campaigns.',
|
||||||
url: 'https://www.qrmaster.net/qr-code-tracking',
|
url: 'https://www.qrmaster.net/qr-code-tracking',
|
||||||
@@ -78,25 +81,41 @@ const trackingUseCases = [
|
|||||||
title: 'Marketing campaigns',
|
title: 'Marketing campaigns',
|
||||||
description:
|
description:
|
||||||
'Measure how printed placements such as flyers, signs, packaging inserts, or booth materials perform over time.',
|
'Measure how printed placements such as flyers, signs, packaging inserts, or booth materials perform over time.',
|
||||||
benefits: ['Compare placements', 'Review campaign timing', 'See scan context in one dashboard'],
|
benefits: [
|
||||||
|
'Compare placements',
|
||||||
|
'Review campaign timing',
|
||||||
|
'See scan context in one dashboard',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Events',
|
title: 'Events',
|
||||||
description:
|
description:
|
||||||
'Track which event materials drive scans before, during, and after the event instead of treating every print touchpoint the same.',
|
'Track which event materials drive scans before, during, and after the event instead of treating every print touchpoint the same.',
|
||||||
benefits: ['Compare booth and signage scans', 'Watch event-day traffic', 'Keep destinations updateable'],
|
benefits: [
|
||||||
|
'Compare booth and signage scans',
|
||||||
|
'Watch event-day traffic',
|
||||||
|
'Keep destinations updateable',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Product and packaging',
|
title: 'Product and packaging',
|
||||||
description:
|
description:
|
||||||
'Measure which labels, inserts, or support links are getting scans after products leave the warehouse.',
|
'Measure which labels, inserts, or support links are getting scans after products leave the warehouse.',
|
||||||
benefits: ['Track support content usage', 'Compare packaging placements', 'Keep links current'],
|
benefits: [
|
||||||
|
'Track support content usage',
|
||||||
|
'Compare packaging placements',
|
||||||
|
'Keep links current',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Restaurant and in-store surfaces',
|
title: 'Restaurant and in-store surfaces',
|
||||||
description:
|
description:
|
||||||
'Review scans from menus, table cards, windows, or counters and compare when in-store prompts actually get used.',
|
'Review scans from menus, table cards, windows, or counters and compare when in-store prompts actually get used.',
|
||||||
benefits: ['Spot peak scan periods', 'Compare service surfaces', 'Keep menu links current'],
|
benefits: [
|
||||||
|
'Spot peak scan periods',
|
||||||
|
'Compare service surfaces',
|
||||||
|
'Keep menu links current',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -104,7 +123,39 @@ const trackingComparison = [
|
|||||||
{ feature: 'Destination can change later', static: false, dynamic: true },
|
{ feature: 'Destination can change later', static: false, dynamic: true },
|
||||||
{ feature: 'Scan analytics', static: false, dynamic: true },
|
{ feature: 'Scan analytics', static: false, dynamic: true },
|
||||||
{ feature: 'Placement comparison after print', static: false, dynamic: true },
|
{ feature: 'Placement comparison after print', static: false, dynamic: true },
|
||||||
{ feature: 'Reusable for campaigns that evolve', static: false, dynamic: true },
|
{
|
||||||
|
feature: 'Reusable for campaigns that evolve',
|
||||||
|
static: false,
|
||||||
|
dynamic: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const trackingLimits = [
|
||||||
|
{
|
||||||
|
label: 'Scan timestamp',
|
||||||
|
tracked: 'Yes',
|
||||||
|
note: 'Useful for campaign timing, service windows, and event-day patterns.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Device context',
|
||||||
|
tracked: 'Yes',
|
||||||
|
note: 'Useful for checking whether post-scan pages are mostly mobile traffic.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Approximate location context',
|
||||||
|
tracked: 'Yes',
|
||||||
|
note: 'Useful for city, region, or country-level placement review.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Exact personal identity',
|
||||||
|
tracked: 'No',
|
||||||
|
note: 'QR scan analytics should not be framed as individual user identification.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Static QR scan history',
|
||||||
|
tracked: 'No',
|
||||||
|
note: 'A static QR code has no managed redirect layer for QR Master to measure.',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const faqItems = [
|
const faqItems = [
|
||||||
@@ -118,6 +169,16 @@ const faqItems = [
|
|||||||
answer:
|
answer:
|
||||||
'QR Master reports scan activity including time of scan, device type (mobile vs. desktop), and location context (country, city). You can also compare total scans vs. unique scans.',
|
'QR Master reports scan activity including time of scan, device type (mobile vs. desktop), and location context (country, city). You can also compare total scans vs. unique scans.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
question: 'Is QR code tracking GDPR-friendly?',
|
||||||
|
answer:
|
||||||
|
'QR tracking can be used in a privacy-aware way when scan reporting avoids personally identifiable profiles. QR Master is positioned around hashed IP handling, anonymized context, and aggregate placement reporting rather than individual user surveillance.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Can I track review collection QR codes?',
|
||||||
|
answer:
|
||||||
|
'Yes. A trackable review QR setup can show which table cards, receipts, counters, or packaging inserts get scanned. The actual review submission still happens on the destination platform or form.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
question: 'Why does tracking use a dynamic QR code?',
|
question: 'Why does tracking use a dynamic QR code?',
|
||||||
answer:
|
answer:
|
||||||
@@ -170,13 +231,14 @@ const howToSchema = {
|
|||||||
'@id': 'https://www.qrmaster.net/qr-code-tracking#howto',
|
'@id': 'https://www.qrmaster.net/qr-code-tracking#howto',
|
||||||
name: 'How to track QR code scans',
|
name: 'How to track QR code scans',
|
||||||
datePublished: '2024-01-01',
|
datePublished: '2024-01-01',
|
||||||
dateModified: '2025-06-01',
|
dateModified: '2026-05-10',
|
||||||
author: {
|
author: {
|
||||||
'@type': 'Person',
|
'@type': 'Person',
|
||||||
name: 'Timo Knuth',
|
name: 'Timo Knuth',
|
||||||
url: 'https://www.qrmaster.net/authors/timo',
|
url: 'https://www.qrmaster.net/authors/timo',
|
||||||
},
|
},
|
||||||
description: 'Create a dynamic QR code, deploy it, and review scan analytics from the QR Master dashboard.',
|
description:
|
||||||
|
'Create a dynamic QR code, deploy it, and review scan analytics from the QR Master dashboard.',
|
||||||
totalTime: 'PT5M',
|
totalTime: 'PT5M',
|
||||||
step: [
|
step: [
|
||||||
{
|
{
|
||||||
@@ -241,6 +303,13 @@ const relatedUseCaseLinks = [
|
|||||||
'Measure which service surfaces and follow-up prompts actually generate customer responses.',
|
'Measure which service surfaces and follow-up prompts actually generate customer responses.',
|
||||||
ctaLabel: 'Create your feedback QR code',
|
ctaLabel: 'Create your feedback QR code',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/use-cases/qr-codes-for-review-collection',
|
||||||
|
title: 'QR Codes for Review Collection',
|
||||||
|
description:
|
||||||
|
'Compare table cards, receipts, counters, and packaging inserts as measurable review-request placements.',
|
||||||
|
ctaLabel: 'Create a trackable review QR',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/use-cases/coupon-qr-codes',
|
href: '/use-cases/coupon-qr-codes',
|
||||||
title: 'Coupon QR Codes',
|
title: 'Coupon QR Codes',
|
||||||
@@ -255,6 +324,13 @@ const relatedUseCaseLinks = [
|
|||||||
'Go deeper into placement-level reporting and offline campaign attribution.',
|
'Go deeper into placement-level reporting and offline campaign attribution.',
|
||||||
ctaLabel: 'Explore QR analytics',
|
ctaLabel: 'Explore QR analytics',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/blog/utm-parameter-qr-codes',
|
||||||
|
title: 'UTM Parameters with QR Codes',
|
||||||
|
description:
|
||||||
|
'Pair QR scan data with GA4 campaign attribution so offline traffic can be reviewed after the scan.',
|
||||||
|
ctaLabel: 'Use UTMs with QR codes',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/reprint-calculator',
|
href: '/reprint-calculator',
|
||||||
title: 'QR Code Reprint Cost Calculator',
|
title: 'QR Code Reprint Cost Calculator',
|
||||||
@@ -274,7 +350,14 @@ const relatedUseCaseLinks = [
|
|||||||
export default function QRCodeTrackingPage() {
|
export default function QRCodeTrackingPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SeoJsonLd data={[softwareSchema, howToSchema, faqSchema, breadcrumbSchema(breadcrumbItems)]} />
|
<SeoJsonLd
|
||||||
|
data={[
|
||||||
|
softwareSchema,
|
||||||
|
howToSchema,
|
||||||
|
faqSchema,
|
||||||
|
breadcrumbSchema(breadcrumbItems),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<MarketingPageTracker pageType="commercial" cluster="qr-tracking" />
|
<MarketingPageTracker pageType="commercial" cluster="qr-tracking" />
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 py-20">
|
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 py-20">
|
||||||
@@ -288,11 +371,12 @@ export default function QRCodeTrackingPage() {
|
|||||||
|
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<h1 className="text-5xl font-bold leading-tight text-gray-900 lg:text-6xl">
|
<h1 className="text-5xl font-bold leading-tight text-gray-900 lg:text-6xl">
|
||||||
Track every QR code scan with useful context
|
QR Code Tracking for Every Scan
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl leading-relaxed text-gray-600">
|
<p className="text-xl leading-relaxed text-gray-600">
|
||||||
Use dynamic QR codes to measure scans by time, device, and location context,
|
Track QR code scans by time, device, and location context
|
||||||
so printed campaigns and physical placements stop being guesswork.
|
with dynamic QR codes, so printed campaigns and physical
|
||||||
|
placements stop being guesswork.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -305,7 +389,11 @@ export default function QRCodeTrackingPage() {
|
|||||||
].map((feature) => (
|
].map((feature) => (
|
||||||
<div key={feature} className="flex items-center gap-3">
|
<div key={feature} className="flex items-center gap-3">
|
||||||
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-green-500">
|
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-green-500">
|
||||||
<svg className="h-3 w-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
<svg
|
||||||
|
className="h-3 w-3 text-white"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
@@ -326,7 +414,10 @@ export default function QRCodeTrackingPage() {
|
|||||||
pageType="commercial"
|
pageType="commercial"
|
||||||
cluster="qr-tracking"
|
cluster="qr-tracking"
|
||||||
>
|
>
|
||||||
<Button size="lg" className="w-full px-8 py-4 text-lg sm:w-auto">
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="w-full px-8 py-4 text-lg sm:w-auto"
|
||||||
|
>
|
||||||
Start Tracking Free
|
Start Tracking Free
|
||||||
</Button>
|
</Button>
|
||||||
</TrackedCtaLink>
|
</TrackedCtaLink>
|
||||||
@@ -337,7 +428,11 @@ export default function QRCodeTrackingPage() {
|
|||||||
pageType="commercial"
|
pageType="commercial"
|
||||||
cluster="qr-tracking"
|
cluster="qr-tracking"
|
||||||
>
|
>
|
||||||
<Button variant="outline" size="lg" className="w-full px-8 py-4 text-lg sm:w-auto">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
className="w-full px-8 py-4 text-lg sm:w-auto"
|
||||||
|
>
|
||||||
Create Dynamic QR Code
|
Create Dynamic QR Code
|
||||||
</Button>
|
</Button>
|
||||||
</TrackedCtaLink>
|
</TrackedCtaLink>
|
||||||
@@ -346,23 +441,33 @@ export default function QRCodeTrackingPage() {
|
|||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Card className="p-6 shadow-2xl">
|
<Card className="p-6 shadow-2xl">
|
||||||
<h3 className="mb-4 text-lg font-semibold">Analytics overview</h3>
|
<h3 className="mb-4 text-lg font-semibold">
|
||||||
|
Analytics overview
|
||||||
|
</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between border-b pb-3">
|
<div className="flex items-center justify-between border-b pb-3">
|
||||||
<span className="text-gray-600">Placement view</span>
|
<span className="text-gray-600">Placement view</span>
|
||||||
<span className="font-semibold text-primary-600">Flyer, sign, menu, package</span>
|
<span className="font-semibold text-primary-600">
|
||||||
|
Flyer, sign, menu, package
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between border-b pb-3">
|
<div className="flex items-center justify-between border-b pb-3">
|
||||||
<span className="text-gray-600">Time trend</span>
|
<span className="text-gray-600">Time trend</span>
|
||||||
<span className="font-semibold text-primary-600">Hourly and daily patterns</span>
|
<span className="font-semibold text-primary-600">
|
||||||
|
Hourly and daily patterns
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between border-b pb-3">
|
<div className="flex items-center justify-between border-b pb-3">
|
||||||
<span className="text-gray-600">Location context</span>
|
<span className="text-gray-600">Location context</span>
|
||||||
<span className="font-semibold">Country, city, region</span>
|
<span className="font-semibold">
|
||||||
|
Country, city, region
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-gray-600">Device mix</span>
|
<span className="text-gray-600">Device mix</span>
|
||||||
<span className="font-semibold">Phone or desktop traffic</span>
|
<span className="font-semibold">
|
||||||
|
Phone or desktop traffic
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -374,9 +479,72 @@ export default function QRCodeTrackingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* WITH VS WITHOUT TRACKING COMPARISON */}
|
||||||
|
<section className="bg-slate-900 py-16">
|
||||||
|
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<h2 className="text-3xl font-bold text-white mb-3">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-slate-800 rounded-2xl p-6 border border-slate-700">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center">
|
||||||
|
<svg className="w-4 h-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="bg-emerald-900/30 rounded-2xl p-6 border border-emerald-700/50">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-emerald-500/20 flex items-center justify-center">
|
||||||
|
<svg className="w-4 h-4 text-emerald-400" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-white">With QR Master tracking</h3>
|
||||||
|
</div>
|
||||||
|
<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>Update destinations without reprinting; fix errors in seconds from your dashboard</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 rounded-xl bg-slate-800 border border-slate-700 p-5">
|
||||||
|
<p className="text-sm font-semibold text-slate-300 mb-3">Real-world tracking use cases</p>
|
||||||
|
<div className="grid sm:grid-cols-3 gap-4">
|
||||||
|
{[
|
||||||
|
{ icon: '🍽️', label: 'Restaurant', detail: 'Which table scans the menu most? Which day drives the most QR activity?' },
|
||||||
|
{ icon: '📋', label: 'Marketing', detail: 'Which flyer variant performs? Which neighbourhood placement drives the most scans?' },
|
||||||
|
{ icon: '🛒', label: 'Retail', detail: 'Which shelf placement gets scanned? Which product insert drives the most post-purchase visits?' },
|
||||||
|
].map(({ icon, label, detail }) => (
|
||||||
|
<div key={label} className="bg-slate-700/50 rounded-lg p-4">
|
||||||
|
<div className="text-2xl mb-2">{icon}</div>
|
||||||
|
<p className="font-semibold text-white text-sm mb-1">{label}</p>
|
||||||
|
<p className="text-slate-400 text-xs">{detail}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<AnswerFirstBlock
|
<AnswerFirstBlock
|
||||||
whatIsIt="QR code tracking works when a dynamic QR code routes the scan through QR Master before the scanner reaches the final destination. That redirect step is what makes analytics such as time, device, and location context possible."
|
whatIsIt="QR code tracking works when a dynamic QR code routes the scan through QR Master before the scanner reaches the final destination. That redirect step makes QR scan analytics such as time, device, location context, total scans, and unique scans possible."
|
||||||
whenToUse={[
|
whenToUse={[
|
||||||
'You want to compare placements such as flyers, signs, menus, packaging, or event surfaces',
|
'You want to compare placements such as flyers, signs, menus, packaging, or event surfaces',
|
||||||
'You need a measurable record of scans instead of relying on guesswork after print is deployed',
|
'You need a measurable record of scans instead of relying on guesswork after print is deployed',
|
||||||
@@ -386,9 +554,17 @@ export default function QRCodeTrackingPage() {
|
|||||||
leftTitle: 'Static QR only',
|
leftTitle: 'Static QR only',
|
||||||
rightTitle: 'Dynamic QR with tracking',
|
rightTitle: 'Dynamic QR with tracking',
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Destination can change later', value: true, text: 'Not available' },
|
{
|
||||||
|
label: 'Destination can change later',
|
||||||
|
value: true,
|
||||||
|
text: 'Not available',
|
||||||
|
},
|
||||||
{ label: 'Scan analytics', value: true, text: 'Not available' },
|
{ label: 'Scan analytics', value: true, text: 'Not available' },
|
||||||
{ label: 'Placement comparison after print', value: true, text: 'Limited' },
|
{
|
||||||
|
label: 'Placement comparison after print',
|
||||||
|
value: true,
|
||||||
|
text: 'Limited',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
howTo={{
|
howTo={{
|
||||||
@@ -401,6 +577,108 @@ export default function QRCodeTrackingPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section className="bg-slate-50 py-16">
|
||||||
|
<div className="container mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-10 max-w-3xl">
|
||||||
|
<p className="mb-3 text-sm font-semibold uppercase tracking-wider text-blue-600">
|
||||||
|
Privacy-aware scan data
|
||||||
|
</p>
|
||||||
|
<h2 className="mb-4 text-3xl font-bold text-gray-900">
|
||||||
|
What QR Master tracks and what it does not
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg leading-relaxed text-gray-600">
|
||||||
|
QR code tracking should answer placement and campaign questions,
|
||||||
|
not create a personally identifiable profile of every scanner.
|
||||||
|
Treat scan analytics as aggregate context for better print,
|
||||||
|
routing, and campaign decisions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full min-w-[720px]">
|
||||||
|
<thead className="bg-white">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-4 text-left font-semibold text-gray-900">
|
||||||
|
Data point
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 text-left font-semibold text-gray-900">
|
||||||
|
QR Master tracking
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 text-left font-semibold text-gray-900">
|
||||||
|
Practical use
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{trackingLimits.map((row) => (
|
||||||
|
<tr key={row.label}>
|
||||||
|
<td className="px-6 py-4 font-medium text-gray-900">
|
||||||
|
{row.label}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-gray-700">
|
||||||
|
{row.tracked}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-gray-600">{row.note}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-white py-16">
|
||||||
|
<div className="container mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-10 max-w-3xl">
|
||||||
|
<p className="mb-3 text-sm font-semibold uppercase tracking-wider text-blue-600">
|
||||||
|
Scan analytics workflow
|
||||||
|
</p>
|
||||||
|
<h2 className="mb-4 text-3xl font-bold text-gray-900">
|
||||||
|
How to track QR code scans from print campaigns
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg leading-relaxed text-gray-600">
|
||||||
|
Create a dynamic QR code for each placement you want to measure,
|
||||||
|
label it by channel or surface, then compare scan activity from
|
||||||
|
the dashboard. This gives each flyer, sign, menu, package
|
||||||
|
insert, or event material its own measurable scan record.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-2 text-xl font-semibold text-gray-900">
|
||||||
|
1. Create trackable codes
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Use a dynamic QR code for each important placement so scans
|
||||||
|
route through QR Master before visitors reach the final page.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-2 text-xl font-semibold text-gray-900">
|
||||||
|
2. Label each placement
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Name codes by campaign, surface, location, or print batch so
|
||||||
|
the analytics stay useful when traffic starts coming in.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="mb-2 text-xl font-semibold text-gray-900">
|
||||||
|
3. Compare scan results
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Review total scans, unique scans, devices, time patterns, and
|
||||||
|
location context to decide which printed placements deserve
|
||||||
|
more budget.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="container mx-auto max-w-5xl px-4 pb-8 sm:px-6 lg:px-8">
|
<div className="container mx-auto max-w-5xl px-4 pb-8 sm:px-6 lg:px-8">
|
||||||
<FAQSection items={faqItems} title="QR tracking questions" />
|
<FAQSection items={faqItems} title="QR tracking questions" />
|
||||||
</div>
|
</div>
|
||||||
@@ -408,16 +686,24 @@ export default function QRCodeTrackingPage() {
|
|||||||
<section className="bg-gray-50 py-20">
|
<section className="bg-gray-50 py-20">
|
||||||
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<div className="mb-16 text-center">
|
<div className="mb-16 text-center">
|
||||||
<h2 className="mb-4 text-4xl font-bold text-gray-900">What QR tracking helps you measure</h2>
|
<h2 className="mb-4 text-4xl font-bold text-gray-900">
|
||||||
|
What QR tracking helps you measure
|
||||||
|
</h2>
|
||||||
<p className="mx-auto max-w-3xl text-xl text-gray-600">
|
<p className="mx-auto max-w-3xl text-xl text-gray-600">
|
||||||
These are the analytics surfaces the current product experience already supports without inventing extra claims.
|
These are the analytics surfaces the current product experience
|
||||||
|
already supports without inventing extra claims.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{trackingFeatures.map((feature) => (
|
{trackingFeatures.map((feature) => (
|
||||||
<Card key={feature.title} className="p-6 transition-shadow hover:shadow-lg">
|
<Card
|
||||||
<h3 className="mb-2 text-xl font-semibold text-gray-900">{feature.title}</h3>
|
key={feature.title}
|
||||||
|
className="p-6 transition-shadow hover:shadow-lg"
|
||||||
|
>
|
||||||
|
<h3 className="mb-2 text-xl font-semibold text-gray-900">
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600">{feature.description}</p>
|
<p className="text-gray-600">{feature.description}</p>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -428,9 +714,12 @@ export default function QRCodeTrackingPage() {
|
|||||||
<section className="py-20">
|
<section className="py-20">
|
||||||
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||||
<div className="mb-16 text-center">
|
<div className="mb-16 text-center">
|
||||||
<h2 className="mb-4 text-4xl font-bold text-gray-900">Tracking setup comparison</h2>
|
<h2 className="mb-4 text-4xl font-bold text-gray-900">
|
||||||
|
Tracking setup comparison
|
||||||
|
</h2>
|
||||||
<p className="text-xl text-gray-600">
|
<p className="text-xl text-gray-600">
|
||||||
Tracking is a dynamic QR workflow, not a static QR workflow with retroactive reporting.
|
Tracking is a dynamic QR workflow, not a static QR workflow with
|
||||||
|
retroactive reporting.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -438,16 +727,26 @@ export default function QRCodeTrackingPage() {
|
|||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gray-100">
|
<thead className="bg-gray-100">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-4 text-left font-semibold text-gray-900">Capability</th>
|
<th className="px-6 py-4 text-left font-semibold text-gray-900">
|
||||||
<th className="px-6 py-4 text-center font-semibold text-gray-900">Static QR</th>
|
Capability
|
||||||
<th className="px-6 py-4 text-center font-semibold text-primary-600">Dynamic QR tracking</th>
|
</th>
|
||||||
|
<th className="px-6 py-4 text-center font-semibold text-gray-900">
|
||||||
|
Static QR
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 text-center font-semibold text-primary-600">
|
||||||
|
Dynamic QR tracking
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{trackingComparison.map((row) => (
|
{trackingComparison.map((row) => (
|
||||||
<tr key={row.feature}>
|
<tr key={row.feature}>
|
||||||
<td className="px-6 py-4 font-medium text-gray-900">{row.feature}</td>
|
<td className="px-6 py-4 font-medium text-gray-900">
|
||||||
<td className="px-6 py-4 text-center text-gray-600">{row.static ? 'Yes' : 'No'}</td>
|
{row.feature}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-center text-gray-600">
|
||||||
|
{row.static ? 'Yes' : 'No'}
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4 text-center font-semibold text-primary-600">
|
<td className="px-6 py-4 text-center font-semibold text-primary-600">
|
||||||
{row.dynamic ? 'Yes' : 'No'}
|
{row.dynamic ? 'Yes' : 'No'}
|
||||||
</td>
|
</td>
|
||||||
@@ -462,21 +761,30 @@ export default function QRCodeTrackingPage() {
|
|||||||
<section className="bg-gray-50 py-20">
|
<section className="bg-gray-50 py-20">
|
||||||
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<div className="mb-16 text-center">
|
<div className="mb-16 text-center">
|
||||||
<h2 className="mb-4 text-4xl font-bold text-gray-900">Where tracking is most useful</h2>
|
<h2 className="mb-4 text-4xl font-bold text-gray-900">
|
||||||
|
Where tracking is most useful
|
||||||
|
</h2>
|
||||||
<p className="mx-auto max-w-3xl text-xl text-gray-600">
|
<p className="mx-auto max-w-3xl text-xl text-gray-600">
|
||||||
Use tracking where physical surfaces or campaigns need measurable follow-through after they are deployed.
|
Use tracking where physical surfaces or campaigns need
|
||||||
|
measurable follow-through after they are deployed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-8 md:grid-cols-2">
|
<div className="grid gap-8 md:grid-cols-2">
|
||||||
{trackingUseCases.map((useCase) => (
|
{trackingUseCases.map((useCase) => (
|
||||||
<Card key={useCase.title} className="p-8">
|
<Card key={useCase.title} className="p-8">
|
||||||
<h3 className="mb-3 text-2xl font-bold text-gray-900">{useCase.title}</h3>
|
<h3 className="mb-3 text-2xl font-bold text-gray-900">
|
||||||
|
{useCase.title}
|
||||||
|
</h3>
|
||||||
<p className="mb-6 text-gray-600">{useCase.description}</p>
|
<p className="mb-6 text-gray-600">{useCase.description}</p>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{useCase.benefits.map((benefit) => (
|
{useCase.benefits.map((benefit) => (
|
||||||
<li key={benefit} className="flex items-center gap-2">
|
<li key={benefit} className="flex items-center gap-2">
|
||||||
<svg className="h-5 w-5 flex-shrink-0 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
<svg
|
||||||
|
className="h-5 w-5 flex-shrink-0 text-green-500"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
@@ -493,44 +801,135 @@ export default function QRCodeTrackingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* QR INDUSTRY GROWTH STATISTICS */}
|
||||||
|
<section className="bg-slate-50 py-16 border-t border-slate-200">
|
||||||
|
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<p className="text-sm font-semibold uppercase tracking-wider text-blue-600 mb-3">
|
||||||
|
Industry data
|
||||||
|
</p>
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-3">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
|
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
|
||||||
|
<div className="text-4xl font-extrabold text-blue-600 mb-2">+26%</div>
|
||||||
|
<p className="text-gray-800 font-semibold text-sm mb-2">
|
||||||
|
Year-over-year growth in QR code scans (2024)
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 text-xs">
|
||||||
|
QR code scans grew 26% globally in 2024 compared to the prior year, confirming accelerating mainstream adoption across retail, hospitality, and events.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-3 italic">Source: QR Tiger Global QR Code Report 2024</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
|
||||||
|
<div className="text-4xl font-extrabold text-purple-600 mb-2">89%</div>
|
||||||
|
<p className="text-gray-800 font-semibold text-sm mb-2">
|
||||||
|
of all QR code scans happen on mobile devices
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 text-xs">
|
||||||
|
Nearly 9 in 10 scans come from smartphones. Knowing your device breakdown helps you design mobile-first landing pages that convert instead of frustrate.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-3 italic">Source: QR Tiger Global QR Code Report 2024</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
|
||||||
|
<div className="text-4xl font-extrabold text-emerald-600 mb-2">34%</div>
|
||||||
|
<p className="text-gray-800 font-semibold text-sm mb-2">
|
||||||
|
average campaign improvement when tracking is used to optimize placement
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 text-xs">
|
||||||
|
Campaigns that use scan data to move or update QR placements mid-run outperform static print deployments on average by around one-third in engagement.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-3 italic">Source: Industry benchmark average across QR-led print campaigns</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* WHY QR TRACKING MATTERS — STATISTICS */}
|
{/* WHY QR TRACKING MATTERS — STATISTICS */}
|
||||||
<section className="bg-white py-16">
|
<section className="bg-white py-16">
|
||||||
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<svg className="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /></svg>
|
<svg
|
||||||
<span className="text-sm font-semibold text-emerald-600 uppercase tracking-wider">Research-backed impact</span>
|
className="w-5 h-5 text-emerald-500"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm font-semibold text-emerald-600 uppercase tracking-wider">
|
||||||
|
Research-backed impact
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-bold text-gray-900 mb-4">
|
<h2 className="text-3xl font-bold text-gray-900 mb-4">
|
||||||
Why Tracking Makes QR Codes Measurable Marketing Assets
|
Why Tracking Makes QR Codes Measurable Marketing Assets
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 mb-10 max-w-2xl">
|
<p className="text-gray-600 mb-10 max-w-2xl">
|
||||||
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.
|
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>
|
</p>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6 mb-8">
|
<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="bg-emerald-50 border border-emerald-100 rounded-2xl p-6">
|
||||||
<div className="text-4xl font-extrabold text-emerald-600 mb-2">89% vs 33%</div>
|
<div className="text-4xl font-extrabold text-emerald-600 mb-2">
|
||||||
|
89% vs 33%
|
||||||
|
</div>
|
||||||
<p className="text-gray-700 text-sm leading-relaxed mb-3">
|
<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 <strong>89% of their customers</strong>, compared to 33% for businesses without integrated tracking.
|
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>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Source: <a href="https://www.aberdeengroup.com/" target="_blank" rel="noopener noreferrer" className="underline hover:text-gray-700">Aberdeen Group</a> — Omnichannel Customer Engagement Study
|
Source:{' '}
|
||||||
|
<a
|
||||||
|
href="https://www.aberdeengroup.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:text-gray-700"
|
||||||
|
>
|
||||||
|
Aberdeen Group
|
||||||
|
</a>{' '}
|
||||||
|
— Omnichannel Customer Engagement Study
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-6">
|
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-6">
|
||||||
<div className="text-4xl font-extrabold text-blue-600 mb-2">46%</div>
|
<div className="text-4xl font-extrabold text-blue-600 mb-2">
|
||||||
|
46%
|
||||||
|
</div>
|
||||||
<p className="text-gray-700 text-sm leading-relaxed mb-3">
|
<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 what works.
|
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
|
||||||
|
what works.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Source: <a href="https://thedma.org/" target="_blank" rel="noopener noreferrer" className="underline hover:text-gray-700">Data & Marketing Association (DMA)</a>
|
Source:{' '}
|
||||||
|
<a
|
||||||
|
href="https://thedma.org/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:text-gray-700"
|
||||||
|
>
|
||||||
|
Data & Marketing Association (DMA)
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-gray-400 italic">
|
<p className="text-xs text-gray-400 italic">
|
||||||
By Timo Knuth, QR Master · Last updated: June 2025 · Based on independent academic and industry research
|
By Timo Knuth, QR Master - Last updated: May 2026 - Based on
|
||||||
|
independent academic and industry research
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -546,9 +945,12 @@ export default function QRCodeTrackingPage() {
|
|||||||
|
|
||||||
<section className="bg-gradient-to-r from-primary-600 to-purple-600 py-20 text-white">
|
<section className="bg-gradient-to-r from-primary-600 to-purple-600 py-20 text-white">
|
||||||
<div className="container mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
|
<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">Start tracking your QR codes today</h2>
|
<h2 className="mb-6 text-4xl font-bold">
|
||||||
|
Start tracking your QR codes today
|
||||||
|
</h2>
|
||||||
<p className="mb-8 text-xl text-primary-100">
|
<p className="mb-8 text-xl text-primary-100">
|
||||||
Measure scans with enough context to improve the next placement, campaign, or printed workflow.
|
Measure scans with enough context to improve the next placement,
|
||||||
|
campaign, or printed workflow.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col justify-center gap-4 sm:flex-row">
|
<div className="flex flex-col justify-center gap-4 sm:flex-row">
|
||||||
<TrackedCtaLink
|
<TrackedCtaLink
|
||||||
|
|||||||
872
src/app/(main)/(marketing)/restaurants/page.tsx
Normal file
@@ -0,0 +1,872 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
BarChart3,
|
||||||
|
Calculator,
|
||||||
|
Check,
|
||||||
|
Clock3,
|
||||||
|
FileText,
|
||||||
|
Link2,
|
||||||
|
Printer,
|
||||||
|
QrCode,
|
||||||
|
RefreshCw,
|
||||||
|
ShieldCheck,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||||
|
import SeoJsonLd from "@/components/SeoJsonLd";
|
||||||
|
import {
|
||||||
|
MarketingPageTracker,
|
||||||
|
TrackedCtaLink,
|
||||||
|
} from "@/components/marketing/MarketingAnalytics";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import { breadcrumbSchema, faqPageSchema, softwareApplicationSchema } from "@/lib/schema";
|
||||||
|
|
||||||
|
const SITE_URL = "https://www.qrmaster.net";
|
||||||
|
const PAGE_URL = `${SITE_URL}/restaurants`;
|
||||||
|
const HERO_IMAGE = "/restaurants-hero-wide.webp";
|
||||||
|
const OG_IMAGE = "/restaurants-hero-og.jpg";
|
||||||
|
const CAMPAIGN_SIGNUP =
|
||||||
|
"/signup?utm_source=meta&utm_medium=paid_social&utm_campaign=restaurant_menu_landing&utm_content=restaurants_page_cta";
|
||||||
|
const FAQ = [
|
||||||
|
{
|
||||||
|
question: "What is a dynamic QR code for restaurant menus?",
|
||||||
|
answer:
|
||||||
|
"A dynamic QR code lets a restaurant keep the same printed QR code while changing the destination behind it. You can update a menu PDF, menu page, or ordering link without replacing table tents, flyers, or window signs.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Can I change menu prices after the QR code is printed?",
|
||||||
|
answer:
|
||||||
|
"Yes. With QR Master, the printed QR code points to a managed redirect. When prices, dishes, or PDFs change, you update the destination in the dashboard and keep the same printed code.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Is QR Master useful for small restaurants?",
|
||||||
|
answer:
|
||||||
|
"Yes. Small restaurants can start with a free account, create a dynamic menu QR code, and use scan analytics to see whether guests actually use the menu link.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Do I need to reprint my menu QR code every time the PDF changes?",
|
||||||
|
answer:
|
||||||
|
"No. If the code is dynamic, the printed QR code can stay on the table while the destination changes online.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Should a restaurant menu QR code be static or dynamic?",
|
||||||
|
answer:
|
||||||
|
"A static QR code is acceptable only when the destination will never change. Restaurants usually need a dynamic QR code because menus, prices, PDFs, opening hours, and ordering links change after print.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Can I use a QR code for a menu PDF?",
|
||||||
|
answer:
|
||||||
|
"Yes. You can point a dynamic QR code to a menu PDF and replace that PDF later. The QR code on the table can stay the same while the file or destination is updated in QR Master.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Can I track scans from table tents, flyers, and window signs separately?",
|
||||||
|
answer:
|
||||||
|
"Yes. Use separate dynamic QR codes or tagged destination URLs for each placement. That lets you compare tables, flyers, receipts, window signs, and campaign materials instead of treating every scan as the same source.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Does a restaurant menu QR code need a landing page?",
|
||||||
|
answer:
|
||||||
|
"If the QR code is for guests at the table, it can open the menu directly. If the QR code is used in ads or flyers, a focused landing page often works better because it can explain the offer before asking visitors to sign up or order.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Can I add UTM parameters to restaurant QR codes?",
|
||||||
|
answer:
|
||||||
|
"Yes. UTM parameters are useful when you want to measure traffic from different printed placements or Meta ad campaigns. QR Master can point dynamic codes to tagged URLs so analytics tools can separate sources and campaigns.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Is scan analytics privacy-friendly for restaurant guests?",
|
||||||
|
answer:
|
||||||
|
"QR Master is designed for privacy-conscious scan analytics. Restaurant teams can see practical scan context such as timing and device patterns without turning a menu QR code into intrusive guest tracking.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "What should a restaurant put on a QR code table tent?",
|
||||||
|
answer:
|
||||||
|
"Use short, direct wording such as Scan for our menu, View today's menu, or Order from your table. Keep the QR code large enough to scan, leave quiet space around it, and test it from normal table distance before printing.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "How much can a dynamic menu QR code save on reprints?",
|
||||||
|
answer:
|
||||||
|
"It depends on your print volume and how often the menu changes. As a simple example, 30 table tents at EUR 2.50 each reprinted twice per year equals EUR 150 before flyers, window signs, design time, or staff coordination are included.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const relatedResources = [
|
||||||
|
{
|
||||||
|
href: "/reprint-calculator",
|
||||||
|
title: "QR code reprint calculator",
|
||||||
|
text: "Estimate how much print work changes when you stop replacing QR materials.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/dynamic-qr-code-generator",
|
||||||
|
title: "Dynamic QR code generator",
|
||||||
|
text: "Create editable QR codes for links, PDFs, campaigns, and menu updates.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/qr-code-tracking",
|
||||||
|
title: "QR code tracking",
|
||||||
|
text: "Compare scans from table tents, flyers, receipts, and window signs.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/qr-code-print-size-guide",
|
||||||
|
title: "QR code print size guide",
|
||||||
|
text: "Pick a practical size before sending table cards or menus to print.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const schemaData = [
|
||||||
|
breadcrumbSchema([
|
||||||
|
{ name: "Home", url: "/" },
|
||||||
|
{ name: "Restaurants", url: "/restaurants" },
|
||||||
|
]),
|
||||||
|
faqPageSchema(FAQ),
|
||||||
|
softwareApplicationSchema(),
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": `${PAGE_URL}#webpage`,
|
||||||
|
url: PAGE_URL,
|
||||||
|
name: "Restaurant Menu QR Codes",
|
||||||
|
description:
|
||||||
|
"Create a dynamic restaurant menu QR code that can be updated after print, with scan analytics and a free start.",
|
||||||
|
inLanguage: "en",
|
||||||
|
isPartOf: {
|
||||||
|
"@id": `${SITE_URL}/#website`,
|
||||||
|
},
|
||||||
|
about: [
|
||||||
|
"dynamic QR codes",
|
||||||
|
"restaurant menu QR codes",
|
||||||
|
"QR code analytics",
|
||||||
|
"menu PDF updates",
|
||||||
|
"QR code reprint savings",
|
||||||
|
],
|
||||||
|
significantLink: relatedResources.map((resource) => `${SITE_URL}${resource.href}`),
|
||||||
|
primaryImageOfPage: {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
url: `${SITE_URL}${HERO_IMAGE}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "HowTo",
|
||||||
|
name: "How to create a restaurant menu QR code that can be updated",
|
||||||
|
description:
|
||||||
|
"Create one dynamic QR code, print it on your table materials, then update the menu destination whenever prices or dishes change.",
|
||||||
|
step: [
|
||||||
|
{
|
||||||
|
"@type": "HowToStep",
|
||||||
|
position: 1,
|
||||||
|
name: "Create a dynamic QR code",
|
||||||
|
text: "Create a QR Master account and choose a dynamic QR code for your restaurant menu.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "HowToStep",
|
||||||
|
position: 2,
|
||||||
|
name: "Add your menu destination",
|
||||||
|
text: "Link the QR code to a menu PDF, menu page, ordering link, or hosted restaurant menu.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "HowToStep",
|
||||||
|
position: 3,
|
||||||
|
name: "Print the QR code once",
|
||||||
|
text: "Place the same QR code on table tents, printed menus, flyers, window signs, or receipts.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "HowToStep",
|
||||||
|
position: 4,
|
||||||
|
name: "Update the destination later",
|
||||||
|
text: "Change the destination in QR Master when prices, dishes, opening hours, or menu files change.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: {
|
||||||
|
absolute: "Restaurant Menu QR Codes | QR Master",
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"Create one restaurant menu QR code, update the destination after print, and track scans. Built for menus, table tents, flyers, and price changes.",
|
||||||
|
alternates: {
|
||||||
|
canonical: PAGE_URL,
|
||||||
|
languages: {
|
||||||
|
"x-default": PAGE_URL,
|
||||||
|
en: PAGE_URL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: "Restaurant Menu QR Codes | QR Master",
|
||||||
|
description:
|
||||||
|
"Update restaurant menu links, PDFs, and prices without reprinting your QR code.",
|
||||||
|
url: PAGE_URL,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${SITE_URL}${OG_IMAGE}`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: "Restaurant owner using QR Master to update a menu QR code",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: "Restaurant Menu QR Codes | QR Master",
|
||||||
|
description:
|
||||||
|
"Update restaurant menu links, PDFs, and prices without reprinting your QR code.",
|
||||||
|
images: [`${SITE_URL}${OG_IMAGE}`],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const proofPoints = [
|
||||||
|
"Change menu PDFs after print",
|
||||||
|
"Use one QR code across tables and flyers",
|
||||||
|
"Track scans without exposing guest data",
|
||||||
|
];
|
||||||
|
|
||||||
|
const comparisonRows = [
|
||||||
|
{
|
||||||
|
label: "Menu price changes",
|
||||||
|
static: "Reprint table materials",
|
||||||
|
dynamic: "Update the destination online",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Seasonal dishes",
|
||||||
|
static: "Old QR code points to old menu",
|
||||||
|
dynamic: "Same QR code points to the new menu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Scan analytics",
|
||||||
|
static: "No reliable usage data",
|
||||||
|
dynamic: "See scans, device types, and timing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Campaign tracking",
|
||||||
|
static: "Hard to compare placements",
|
||||||
|
dynamic: "Use tagged links for tables, flyers, and windows",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
icon: QrCode,
|
||||||
|
title: "Create one dynamic code",
|
||||||
|
text: "Start with a menu QR code that points through QR Master, not directly to a file you cannot change later.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: FileText,
|
||||||
|
title: "Connect your menu",
|
||||||
|
text: "Use a PDF, website menu, ordering page, or any link your guests should open from the table.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RefreshCw,
|
||||||
|
title: "Update when prices change",
|
||||||
|
text: "Switch the destination from your dashboard while the printed QR code stays where it is.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const useCases = [
|
||||||
|
"Table tents and counter displays",
|
||||||
|
"Printed menus with a digital backup",
|
||||||
|
"Window signs for after-hours browsing",
|
||||||
|
"Flyers for lunch specials or events",
|
||||||
|
"Receipts with feedback and review links",
|
||||||
|
"Seasonal menu PDFs",
|
||||||
|
];
|
||||||
|
|
||||||
|
const reprintExampleRows = [
|
||||||
|
{
|
||||||
|
label: "Table tents",
|
||||||
|
math: "30 pieces x EUR 2.50 x 2 menu changes",
|
||||||
|
value: "EUR 150",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Lunch flyers",
|
||||||
|
math: "500 pieces x EUR 0.20 x 1 outdated link",
|
||||||
|
value: "EUR 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Window and counter signs",
|
||||||
|
math: "6 pieces x EUR 8.00 x 1 update",
|
||||||
|
value: "EUR 48",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const restaurantScenarios = [
|
||||||
|
{
|
||||||
|
type: "Small cafe",
|
||||||
|
profile: "12 tables, seasonal drinks, one counter sign, and a simple PDF menu.",
|
||||||
|
printRisk:
|
||||||
|
"A coffee price update or new brunch menu can make table cards outdated before the next print run.",
|
||||||
|
qrMasterFit:
|
||||||
|
"Use one dynamic menu QR code for table cards and update the PDF when prices or specials change.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Independent restaurant",
|
||||||
|
profile: "30 tables, lunch flyers, takeaway inserts, and a menu that changes several times per year.",
|
||||||
|
printRisk:
|
||||||
|
"Printed flyers and table tents can send guests to old menu files, old prices, or inactive ordering links.",
|
||||||
|
qrMasterFit:
|
||||||
|
"Keep separate dynamic QR codes for tables, flyers, and takeaway materials so scan analytics show which placement works.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Multi-location group",
|
||||||
|
profile: "Several locations, local menus, shared brand materials, and recurring print coordination.",
|
||||||
|
printRisk:
|
||||||
|
"One changed PDF or location-specific menu can trigger edits across many printed assets.",
|
||||||
|
qrMasterFit:
|
||||||
|
"Manage destinations centrally while each location keeps its printed QR materials stable.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const whyItWorks = [
|
||||||
|
{
|
||||||
|
title: "Editable destinations reduce print dependency",
|
||||||
|
text: "A dynamic QR code separates the printed code from the menu destination. That is why a PDF, menu page, or ordering link can change without replacing the physical table card.",
|
||||||
|
href: "/dynamic-qr-code-generator",
|
||||||
|
linkLabel: "Dynamic QR codes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tracking separates placements",
|
||||||
|
text: "Restaurants can use different dynamic codes or tagged URLs for tables, flyers, receipts, and window signs. That makes scan data useful for decisions instead of blending every scan into one number.",
|
||||||
|
href: "/qr-code-tracking",
|
||||||
|
linkLabel: "QR code tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Print sizing protects the guest experience",
|
||||||
|
text: "The code still has to scan reliably from the table. Correct print size, quiet space, contrast, and test scans matter before sending table tents or menus to print.",
|
||||||
|
href: "/qr-code-print-size-guide",
|
||||||
|
linkLabel: "Print size guide",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function SectionHeading({
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
inverse = false,
|
||||||
|
}: {
|
||||||
|
eyebrow: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
inverse?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<p
|
||||||
|
className={`mb-3 text-xs font-semibold uppercase tracking-[0.22em] ${
|
||||||
|
inverse ? "text-blue-300" : "text-blue-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{eyebrow}
|
||||||
|
</p>
|
||||||
|
<h2
|
||||||
|
className={`text-3xl font-semibold tracking-tight sm:text-4xl ${
|
||||||
|
inverse ? "text-white" : "text-slate-950"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
className={`mt-4 text-base leading-7 sm:text-lg ${
|
||||||
|
inverse ? "text-slate-300" : "text-slate-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RestaurantsPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SeoJsonLd data={schemaData} />
|
||||||
|
<MarketingPageTracker
|
||||||
|
pageType="commercial"
|
||||||
|
cluster="restaurants"
|
||||||
|
useCase="restaurant-menu"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<main className="bg-[#fbfcff] text-slate-950">
|
||||||
|
<section className="overflow-hidden border-b border-slate-200 bg-[linear-gradient(135deg,#f8fbff_0%,#edf4ff_48%,#fff7ed_100%)]">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-4 px-4 pb-8 pt-5 sm:px-6 lg:grid-cols-[minmax(0,0.92fr)_minmax(500px,1.08fr)] lg:items-start lg:px-8">
|
||||||
|
<Breadcrumbs
|
||||||
|
items={[
|
||||||
|
{ name: "Home", url: "/" },
|
||||||
|
{ name: "Restaurants", url: "/restaurants" },
|
||||||
|
]}
|
||||||
|
className="lg:col-span-2 [&_a]:text-slate-500 [&_a:hover]:text-slate-950 [&_span]:text-slate-500 [&_[aria-current=page]]:text-slate-950"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="max-w-3xl pt-0 lg:pt-1">
|
||||||
|
<p className="mb-4 inline-flex rounded-md border border-blue-200 bg-white/80 px-3 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-blue-700 shadow-sm">
|
||||||
|
Dynamic menu QR codes for restaurants
|
||||||
|
</p>
|
||||||
|
<h1 className="max-w-3xl text-4xl font-semibold leading-[1.02] tracking-tight text-slate-950 sm:text-5xl lg:text-[4.15rem] xl:text-[4.65rem]">
|
||||||
|
Update your menu QR code without reprinting.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-3 max-w-2xl text-base leading-7 text-slate-700 sm:text-lg">
|
||||||
|
Keep one printed QR code on the table. Change prices, menu PDFs,
|
||||||
|
and ordering links from QR Master when your restaurant changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-col gap-3 sm:flex-row">
|
||||||
|
<TrackedCtaLink
|
||||||
|
href={CAMPAIGN_SIGNUP}
|
||||||
|
ctaLabel="Create free menu QR code"
|
||||||
|
ctaLocation="restaurants_hero_primary"
|
||||||
|
pageType="commercial"
|
||||||
|
cluster="restaurants"
|
||||||
|
useCase="restaurant-menu"
|
||||||
|
>
|
||||||
|
<Button className="w-full rounded-md bg-blue-600 px-6 py-3 text-base font-semibold text-white hover:bg-blue-700 sm:w-auto">
|
||||||
|
Create free menu QR code
|
||||||
|
</Button>
|
||||||
|
</TrackedCtaLink>
|
||||||
|
<TrackedCtaLink
|
||||||
|
href="/reprint-calculator"
|
||||||
|
ctaLabel="Calculate reprint savings"
|
||||||
|
ctaLocation="restaurants_hero_secondary"
|
||||||
|
pageType="commercial"
|
||||||
|
cluster="restaurants"
|
||||||
|
useCase="restaurant-menu"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full rounded-md border-blue-200 bg-white/80 px-6 py-3 text-base text-blue-700 hover:bg-white sm:w-auto"
|
||||||
|
>
|
||||||
|
Calculate reprint savings
|
||||||
|
</Button>
|
||||||
|
</TrackedCtaLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative pt-0 lg:pt-4">
|
||||||
|
<div className="absolute -inset-4 rounded-[2rem] bg-blue-200/25 blur-2xl" />
|
||||||
|
<div className="relative overflow-hidden rounded-xl border border-white/80 bg-white p-2 shadow-[0_28px_80px_-45px_rgba(15,23,42,0.45)]">
|
||||||
|
<Image
|
||||||
|
src={HERO_IMAGE}
|
||||||
|
alt="Restaurant owner reviewing menu updates with QR Master dashboard"
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
priority
|
||||||
|
sizes="(min-width: 1024px) 52vw, 100vw"
|
||||||
|
className="aspect-video w-full rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative mt-4 grid gap-2 text-sm text-slate-700 sm:grid-cols-3">
|
||||||
|
{proofPoints.map((point) => (
|
||||||
|
<div
|
||||||
|
key={point}
|
||||||
|
className="flex items-start gap-2 rounded-md border border-blue-100 bg-white/82 px-3 py-2 shadow-sm"
|
||||||
|
>
|
||||||
|
<Check className="mt-0.5 h-4 w-4 shrink-0 text-blue-700" />
|
||||||
|
<span>{point}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-b border-slate-200 bg-white">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-8 px-4 py-12 sm:px-6 lg:grid-cols-[0.72fr_1fr] lg:px-8">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-blue-700">
|
||||||
|
Direct answer
|
||||||
|
</p>
|
||||||
|
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-slate-950">
|
||||||
|
What is a restaurant menu QR code?
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="max-w-3xl text-xl leading-9 text-slate-700">
|
||||||
|
A restaurant menu QR code lets guests open your digital menu from
|
||||||
|
a table, flyer, or window sign. A dynamic menu QR code is better
|
||||||
|
for restaurants because the printed code stays the same while the
|
||||||
|
menu PDF, menu page, or ordering link can change later.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Why dynamic matters"
|
||||||
|
title="Menus change faster than printed materials."
|
||||||
|
text="Static QR codes are fine for links that never move. Restaurants need more room to adapt: prices change, dishes sell out, PDFs are replaced, and delivery links move."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-10 overflow-hidden rounded-lg border border-slate-200 bg-white shadow-[0_24px_54px_-44px_rgba(15,23,42,0.45)]">
|
||||||
|
<div className="hidden grid-cols-[1fr_1fr_1fr] border-b border-slate-200 bg-slate-50 text-sm font-semibold text-slate-700 md:grid">
|
||||||
|
<div className="p-4">Restaurant change</div>
|
||||||
|
<div className="border-l border-slate-200 p-4">Static QR code</div>
|
||||||
|
<div className="border-l border-slate-200 p-4">QR Master dynamic code</div>
|
||||||
|
</div>
|
||||||
|
{comparisonRows.map((row) => (
|
||||||
|
<div
|
||||||
|
key={row.label}
|
||||||
|
className="grid grid-cols-1 border-b border-slate-200 last:border-b-0 md:grid-cols-[1fr_1fr_1fr]"
|
||||||
|
>
|
||||||
|
<div className="bg-white p-4 font-medium text-slate-950">
|
||||||
|
{row.label}
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-slate-200 p-4 text-slate-600 md:border-l md:border-t-0">
|
||||||
|
{row.static}
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-slate-200 p-4 text-slate-950 md:border-l md:border-t-0">
|
||||||
|
<span className="inline-flex items-start gap-2">
|
||||||
|
<Check className="mt-1 h-4 w-4 shrink-0 text-green-600" />
|
||||||
|
{row.dynamic}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-y border-blue-100 bg-white">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-16 sm:px-6 lg:grid-cols-[0.85fr_1.15fr] lg:px-8">
|
||||||
|
<div>
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Example reprint math"
|
||||||
|
title="A few small menu changes can become a real print bill."
|
||||||
|
text="This is an illustrative restaurant scenario, not a guaranteed saving. The useful point is simple: every QR code destination change that stays digital removes one print-dependent fix."
|
||||||
|
/>
|
||||||
|
<TrackedCtaLink
|
||||||
|
href="/reprint-calculator"
|
||||||
|
ctaLabel="Open reprint calculator"
|
||||||
|
ctaLocation="restaurants_reprint_example"
|
||||||
|
pageType="commercial"
|
||||||
|
cluster="restaurants"
|
||||||
|
useCase="restaurant-menu"
|
||||||
|
>
|
||||||
|
<Button className="mt-7 rounded-md bg-blue-600 px-6 py-3 text-white hover:bg-blue-700">
|
||||||
|
Open reprint calculator
|
||||||
|
</Button>
|
||||||
|
</TrackedCtaLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-slate-200 bg-slate-50 p-5 shadow-[0_22px_58px_-44px_rgba(15,23,42,0.45)]">
|
||||||
|
<div className="flex items-center gap-3 border-b border-slate-200 pb-4">
|
||||||
|
<div className="flex h-11 w-11 items-center justify-center rounded-md bg-blue-100 text-blue-700">
|
||||||
|
<Calculator className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-slate-950">
|
||||||
|
Sample yearly avoidable reprint cost
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-600">
|
||||||
|
Restaurant with table tents, flyers, and a few fixed signs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divide-y divide-slate-200">
|
||||||
|
{reprintExampleRows.map((row) => (
|
||||||
|
<div
|
||||||
|
key={row.label}
|
||||||
|
className="grid gap-2 py-4 sm:grid-cols-[0.8fr_1.35fr_0.5fr] sm:items-center"
|
||||||
|
>
|
||||||
|
<div className="font-medium text-slate-950">{row.label}</div>
|
||||||
|
<div className="text-sm text-slate-600">{row.math}</div>
|
||||||
|
<div className="font-semibold text-slate-950 sm:text-right">
|
||||||
|
{row.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 rounded-md border border-blue-200 bg-white p-4">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<span className="font-semibold text-slate-950">
|
||||||
|
Example total
|
||||||
|
</span>
|
||||||
|
<span className="text-2xl font-semibold text-blue-700">
|
||||||
|
EUR 298/year
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-sm leading-6 text-slate-600">
|
||||||
|
A dynamic restaurant QR code does not remove printing
|
||||||
|
entirely. It prevents a menu PDF, price change, or ordering
|
||||||
|
link change from forcing another print run.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-10">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Restaurant scenarios"
|
||||||
|
title="The same QR code workflow scales from cafe to multi-location group."
|
||||||
|
text="These examples show where dynamic menu QR codes usually create the most practical value: menu updates, placement tracking, and less coordination around printed materials."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-5 lg:grid-cols-3">
|
||||||
|
{restaurantScenarios.map((scenario) => (
|
||||||
|
<article
|
||||||
|
key={scenario.type}
|
||||||
|
className="rounded-lg border border-slate-200 bg-white p-6 shadow-[0_20px_54px_-42px_rgba(15,23,42,0.45)]"
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-semibold text-slate-950">
|
||||||
|
{scenario.type}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 text-sm leading-6 text-slate-600">
|
||||||
|
{scenario.profile}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6 space-y-5">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">
|
||||||
|
Print risk
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 leading-7 text-slate-700">
|
||||||
|
{scenario.printRisk}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">
|
||||||
|
QR Master fit
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 leading-7 text-slate-700">
|
||||||
|
{scenario.qrMasterFit}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-y border-blue-100 bg-[linear-gradient(135deg,#eef5ff_0%,#ffffff_48%,#fff4e6_100%)] py-20">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Workflow"
|
||||||
|
title="Print once. Update whenever the menu changes."
|
||||||
|
text="The QR code on the table should not be the fragile part of your restaurant workflow. Keep print stable and move the flexible work into the dashboard."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-12 grid gap-5 md:grid-cols-3">
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<div
|
||||||
|
key={step.title}
|
||||||
|
className="rounded-lg border border-blue-100 bg-white/86 p-6 shadow-[0_18px_48px_-36px_rgba(37,99,235,0.55)]"
|
||||||
|
>
|
||||||
|
<div className="mb-8 flex items-center justify-between">
|
||||||
|
<div className="flex h-11 w-11 items-center justify-center rounded-md bg-blue-50 text-blue-700">
|
||||||
|
<step.icon className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-sm text-blue-500">
|
||||||
|
0{index + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-slate-950">
|
||||||
|
{step.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-4 leading-7 text-slate-600">{step.text}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mx-auto grid max-w-7xl gap-12 px-4 py-20 sm:px-6 lg:grid-cols-[0.9fr_1.1fr] lg:px-8">
|
||||||
|
<div>
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="For restaurant operators"
|
||||||
|
title="Built for the places where QR codes actually live."
|
||||||
|
text="A restaurant QR code is not only a link. It is part of the table, the menu, the receipt, and the guest experience."
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||||
|
<TrackedCtaLink
|
||||||
|
href={CAMPAIGN_SIGNUP}
|
||||||
|
ctaLabel="Start with a free dynamic code"
|
||||||
|
ctaLocation="restaurants_mid_primary"
|
||||||
|
pageType="commercial"
|
||||||
|
cluster="restaurants"
|
||||||
|
useCase="restaurant-menu"
|
||||||
|
>
|
||||||
|
<Button className="rounded-md bg-blue-600 px-6 py-3 text-white hover:bg-blue-700">
|
||||||
|
Start with a free dynamic code
|
||||||
|
</Button>
|
||||||
|
</TrackedCtaLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
{useCases.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item}
|
||||||
|
className="flex items-start gap-3 rounded-lg border border-slate-200 bg-white p-4"
|
||||||
|
>
|
||||||
|
<QrCode className="mt-1 h-5 w-5 shrink-0 text-blue-700" />
|
||||||
|
<span className="leading-7 text-slate-700">{item}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-y border-slate-200 bg-white">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-16 sm:px-6 lg:grid-cols-4 lg:px-8">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
icon: Printer,
|
||||||
|
label: "Reprint control",
|
||||||
|
text: "Avoid replacing table materials just because the destination changed.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Link2,
|
||||||
|
label: "Editable destination",
|
||||||
|
text: "Swap PDFs, menu pages, ordering links, and campaign URLs.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: BarChart3,
|
||||||
|
label: "Scan analytics",
|
||||||
|
text: "See whether guests scan, when they scan, and which devices they use.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ShieldCheck,
|
||||||
|
label: "Privacy aware",
|
||||||
|
text: "QR Master is built around privacy-conscious analytics for business use.",
|
||||||
|
},
|
||||||
|
].map((item) => (
|
||||||
|
<div key={item.label}>
|
||||||
|
<item.icon className="h-7 w-7 text-blue-700" />
|
||||||
|
<h3 className="mt-5 text-lg font-semibold text-slate-950">
|
||||||
|
{item.label}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 text-sm leading-6 text-slate-600">
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-10 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Related workflows"
|
||||||
|
title="Go deeper before you print."
|
||||||
|
text="These QR Master guides connect the restaurant page to the practical jobs behind it: editable codes, tracking, print sizing, and reprint planning."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{relatedResources.map((resource) => (
|
||||||
|
<Link
|
||||||
|
key={resource.href}
|
||||||
|
href={resource.href}
|
||||||
|
className="group rounded-lg border border-slate-200 bg-white p-5 shadow-sm transition hover:-translate-y-0.5 hover:border-blue-200 hover:shadow-[0_18px_40px_-34px_rgba(37,99,235,0.65)]"
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-between gap-4 text-base font-semibold text-slate-950">
|
||||||
|
{resource.title}
|
||||||
|
<ArrowRight className="h-4 w-4 shrink-0 text-slate-400 transition group-hover:translate-x-1 group-hover:text-blue-700" />
|
||||||
|
</span>
|
||||||
|
<span className="mt-3 block text-sm leading-6 text-slate-600">
|
||||||
|
{resource.text}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-y border-blue-100 bg-[linear-gradient(135deg,#f7fbff_0%,#ffffff_52%,#fff7ed_100%)]">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-8 px-4 py-16 sm:px-6 lg:grid-cols-[0.7fr_1.3fr] lg:px-8">
|
||||||
|
<div>
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Why this works"
|
||||||
|
title="Dynamic QR codes turn a print problem into a dashboard update."
|
||||||
|
text="For restaurants, the QR code is usually printed before the menu stops changing. The strongest workflow keeps the physical code stable and moves updates, tracking, and testing into QR Master."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{whyItWorks.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.title}
|
||||||
|
className="rounded-lg border border-blue-100 bg-white/88 p-5 shadow-sm"
|
||||||
|
>
|
||||||
|
<h3 className="text-lg font-semibold text-slate-950">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 leading-7 text-slate-600">{item.text}</p>
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-blue-700 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
{item.linkLabel}
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mx-auto max-w-5xl px-4 py-20 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-10 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Questions"
|
||||||
|
title="Restaurant menu QR code FAQ"
|
||||||
|
text="Short answers for the decisions restaurant owners usually need to make before printing QR materials."
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||||
|
<Clock3 className="h-4 w-4" />
|
||||||
|
<span>Last updated April 30, 2026</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divide-y divide-slate-200 rounded-lg border border-slate-200 bg-white">
|
||||||
|
{FAQ.map((item) => (
|
||||||
|
<details key={item.question} className="group p-5 open:bg-slate-50">
|
||||||
|
<summary className="flex cursor-pointer list-none items-center justify-between gap-6 text-base font-semibold text-slate-950">
|
||||||
|
{item.question}
|
||||||
|
<ArrowRight className="h-4 w-4 shrink-0 text-slate-400 transition-transform group-open:rotate-90" />
|
||||||
|
</summary>
|
||||||
|
<p className="mt-4 max-w-3xl leading-7 text-slate-600">
|
||||||
|
{item.answer}
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-y border-blue-100 bg-[linear-gradient(135deg,#f7fbff_0%,#ffffff_55%,#fff7ed_100%)] py-16">
|
||||||
|
<div className="mx-auto flex max-w-7xl flex-col gap-8 px-4 sm:px-6 lg:flex-row lg:items-end lg:justify-between lg:px-8">
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-blue-700">
|
||||||
|
Ready for the next menu change
|
||||||
|
</p>
|
||||||
|
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-slate-950 sm:text-5xl">
|
||||||
|
Keep the QR code. Change the menu.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-5 text-lg leading-8 text-slate-600">
|
||||||
|
Create a free QR Master account and test the restaurant menu
|
||||||
|
workflow before your next print run.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<TrackedCtaLink
|
||||||
|
href={CAMPAIGN_SIGNUP}
|
||||||
|
ctaLabel="Create free menu QR code"
|
||||||
|
ctaLocation="restaurants_footer_primary"
|
||||||
|
pageType="commercial"
|
||||||
|
cluster="restaurants"
|
||||||
|
useCase="restaurant-menu"
|
||||||
|
>
|
||||||
|
<Button className="w-full rounded-md bg-blue-600 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-blue-200 hover:bg-blue-700 sm:w-auto">
|
||||||
|
Create free menu QR code
|
||||||
|
</Button>
|
||||||
|
</TrackedCtaLink>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { showToast } from '@/components/ui/Toast';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { toPng, toSvg, toBlob } from 'html-to-image';
|
import { toPng, toSvg, toBlob } from 'html-to-image';
|
||||||
import { trackEvent } from '@/components/PostHogProvider';
|
import { trackEvent } from '@/components/PostHogProvider';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -54,6 +55,7 @@ export default function BarcodeGeneratorClient() {
|
|||||||
const [lineColor, setLineColor] = useState('#000000');
|
const [lineColor, setLineColor] = useState('#000000');
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const barcodeRef = useRef<HTMLDivElement>(null);
|
const barcodeRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -109,6 +111,7 @@ export default function BarcodeGeneratorClient() {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
|
|
||||||
showToast(`Barcode downloaded as ${extension.toUpperCase()}`, 'success');
|
showToast(`Barcode downloaded as ${extension.toUpperCase()}`, 'success');
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
trackEvent('barcode_downloaded', {
|
trackEvent('barcode_downloaded', {
|
||||||
format: format,
|
format: format,
|
||||||
extension: extension,
|
extension: extension,
|
||||||
@@ -160,6 +163,8 @@ export default function BarcodeGeneratorClient() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -453,5 +458,6 @@ export default function BarcodeGeneratorClient() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function BarcodeGuide() {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-400 mb-8 not-prose">
|
<p className="text-xs text-slate-400 mb-8 not-prose">
|
||||||
By <strong className="text-slate-600">Timo Knuth</strong>, QR Master · Last updated: June 2025 · GS1-verified content
|
By <strong className="text-slate-600">Timo Knuth</strong>, QR Master · Last updated: May 2026 · GS1-verified content
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="lead text-xl text-slate-600">
|
<p className="lead text-xl text-slate-600">
|
||||||
|
|||||||
@@ -1,20 +1,46 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import BarcodeGeneratorClient from './BarcodeGeneratorClient';
|
import BarcodeGeneratorClient from './BarcodeGeneratorClient';
|
||||||
import { BarcodeGuide } from './BarcodeGuide';
|
import { BarcodeGuide } from './BarcodeGuide';
|
||||||
import { Barcode as BarcodeIcon, Shield, Zap, Printer, Download, Share2, Sparkles, Sliders, Check } from 'lucide-react';
|
import {
|
||||||
|
Barcode as BarcodeIcon,
|
||||||
|
Shield,
|
||||||
|
Zap,
|
||||||
|
Printer,
|
||||||
|
Download,
|
||||||
|
Share2,
|
||||||
|
Sparkles,
|
||||||
|
Sliders,
|
||||||
|
Check,
|
||||||
|
} from 'lucide-react';
|
||||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||||
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||||
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
import {
|
||||||
|
generateSoftwareAppSchema,
|
||||||
|
generateFaqSchema,
|
||||||
|
} from '@/lib/schema-utils';
|
||||||
|
|
||||||
// SEO Optimized Metadata
|
// SEO Optimized Metadata
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
absolute: 'Free Barcode Generator Online – EAN, UPC, Code 128',
|
absolute: 'Free Custom Barcode Generator - EAN, UPC, Code 128',
|
||||||
},
|
},
|
||||||
description: 'Free online barcode generator for EAN-13, UPC-A, and Code 128 barcodes. Create scannable labels for retail, inventory, and logistics instantly. Download PNG or SVG — no signup required.',
|
description:
|
||||||
keywords: ['barcode generator', 'online barcode maker', 'create barcode free', 'ean-13 generator', 'upc-a generator', 'code 128 generator', 'barcode creator', 'printable barcodes'],
|
'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.',
|
||||||
|
keywords: [
|
||||||
|
'barcode generator',
|
||||||
|
'custom barcode generator',
|
||||||
|
'online barcode generator',
|
||||||
|
'free online barcode generator',
|
||||||
|
'barcode maker',
|
||||||
|
'upc barcode generator',
|
||||||
|
'ean-13 generator',
|
||||||
|
'upc-a generator',
|
||||||
|
'code 128 generator',
|
||||||
|
'barcode creator',
|
||||||
|
'create barcode free',
|
||||||
|
'printable barcodes',
|
||||||
|
],
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: 'https://www.qrmaster.net/tools/barcode-generator',
|
canonical: 'https://www.qrmaster.net/tools/barcode-generator',
|
||||||
languages: {
|
languages: {
|
||||||
@@ -23,18 +49,22 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Barcode Generator: Create EAN, UPC & Code 128',
|
title: 'Free Custom Barcode Generator - EAN, UPC & Code 128',
|
||||||
description: 'Barcode Generator: Create professional labels instantly. Free & Secured.',
|
description:
|
||||||
|
'Free online barcode maker for EAN-13, UPC-A, and Code 128. Create scannable custom barcodes in seconds and download PNG or SVG.',
|
||||||
url: 'https://www.qrmaster.net/tools/barcode-generator',
|
url: 'https://www.qrmaster.net/tools/barcode-generator',
|
||||||
siteName: 'QR Master',
|
siteName: 'QR Master',
|
||||||
locale: 'en_US',
|
locale: 'en_US',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
images: [{ url: '/barcode-generator-preview.png', width: 1200, height: 630 }],
|
images: [
|
||||||
|
{ url: '/barcode-generator-preview.png', width: 1200, height: 630 },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
title: 'Free Barcode Generator',
|
title: 'Free Barcode Generator',
|
||||||
description: 'Create custom barcodes in seconds. Download high-quality PNG/SVG.',
|
description:
|
||||||
|
'Create custom barcodes in seconds. Download high-quality PNG/SVG.',
|
||||||
},
|
},
|
||||||
robots: {
|
robots: {
|
||||||
index: true,
|
index: true,
|
||||||
@@ -52,11 +82,28 @@ const jsonLd = {
|
|||||||
'/barcode-generator-preview.png',
|
'/barcode-generator-preview.png',
|
||||||
'UtilitiesApplication'
|
'UtilitiesApplication'
|
||||||
),
|
),
|
||||||
|
{
|
||||||
|
'@type': 'WebPage',
|
||||||
|
'@id': 'https://www.qrmaster.net/tools/barcode-generator',
|
||||||
|
name: 'Free Custom Barcode Generator Online - EAN, UPC, Code 128',
|
||||||
|
description:
|
||||||
|
'A barcode generator converts any number or text into a scannable barcode image for retail labels, inventory, and product packaging. Supports EAN-13, UPC-A, and Code 128.',
|
||||||
|
speakable: {
|
||||||
|
'@type': 'SpeakableSpecification',
|
||||||
|
cssSelector: ['.bg-blue-50', 'h1'],
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
'@type': 'Person',
|
||||||
|
name: 'Timo Knuth',
|
||||||
|
url: 'https://www.qrmaster.net/authors/timo',
|
||||||
|
},
|
||||||
|
dateModified: '2026-05-10',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'@type': 'HowTo',
|
'@type': 'HowTo',
|
||||||
name: 'How to Create a Barcode',
|
name: 'How to Create a Barcode',
|
||||||
datePublished: '2024-06-01',
|
datePublished: '2024-06-01',
|
||||||
dateModified: '2025-06-01',
|
dateModified: '2026-05-10',
|
||||||
author: {
|
author: {
|
||||||
'@type': 'Person',
|
'@type': 'Person',
|
||||||
name: 'Timo Knuth',
|
name: 'Timo Knuth',
|
||||||
@@ -100,35 +147,63 @@ const jsonLd = {
|
|||||||
generateFaqSchema({
|
generateFaqSchema({
|
||||||
'What is a Barcode Generator?': {
|
'What is a Barcode Generator?': {
|
||||||
question: 'What is a Barcode Generator?',
|
question: 'What is a Barcode Generator?',
|
||||||
answer: 'A Barcode Generator is an online tool that converts numbers or text into scannable barcode images that can be used for products, labels, and inventory systems.',
|
answer:
|
||||||
|
'A Barcode Generator is an online tool that converts numbers or text into scannable barcode images that can be used for products, labels, and inventory systems.',
|
||||||
},
|
},
|
||||||
'Is this barcode generator free to use?': {
|
'Is this barcode generator free to use?': {
|
||||||
question: 'Is this barcode generator free to use?',
|
question: 'Is this barcode generator free to use?',
|
||||||
answer: 'Yes, our online barcode generator is completely free to use with no hidden costs or sign-ups required. You can generate, download, and print barcodes instantly.',
|
answer:
|
||||||
|
'Yes, our online barcode generator is completely free to use with no hidden costs or sign-ups required. You can generate, download, and print barcodes instantly.',
|
||||||
},
|
},
|
||||||
'Which barcode format should I use?': {
|
'Which barcode format should I use?': {
|
||||||
question: 'Which barcode format should I use?',
|
question: 'Which barcode format should I use?',
|
||||||
answer: 'EAN-13 is standard for retail in Europe/Global. UPC-A is standard for retail in USA/Canada. Code 128 is best for logistics and internal tracking as it supports letters and numbers.',
|
answer:
|
||||||
|
'EAN-13 is standard for retail in Europe/Global. UPC-A is standard for retail in USA/Canada. Code 128 is best for logistics and internal tracking as it supports letters and numbers.',
|
||||||
},
|
},
|
||||||
'Can I download barcodes in vector format (SVG)?': {
|
'Can I download barcodes in vector format (SVG)?': {
|
||||||
question: '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.',
|
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.',
|
||||||
},
|
},
|
||||||
'How do I generate a barcode online?': {
|
'How do I generate a barcode online?': {
|
||||||
question: 'How do I generate a barcode online?',
|
question: 'How do I generate a barcode online?',
|
||||||
answer: 'To generate a barcode online, enter your product number or text, select the desired barcode format (such as EAN-13 or Code 128), and click the generate button. The barcode will be created instantly.',
|
answer:
|
||||||
|
'To generate a barcode online, enter your product number or text, select the desired barcode format (such as EAN-13 or Code 128), and click the generate button. The barcode will be created instantly.',
|
||||||
},
|
},
|
||||||
'Are generated barcodes scannable?': {
|
'Are generated barcodes scannable?': {
|
||||||
question: 'Are generated barcodes scannable?',
|
question: 'Are generated barcodes scannable?',
|
||||||
answer: 'Yes, barcodes generated with a proper barcode generator are fully scannable. We generate standard-compliant barcodes readable by any standard optical or laser barcode scanner.',
|
answer:
|
||||||
|
'Yes, barcodes generated with a proper barcode generator are fully scannable. We generate standard-compliant barcodes readable by any standard optical or laser barcode scanner.',
|
||||||
},
|
},
|
||||||
'Can I use these barcodes for Amazon (EAN/UPC)?': {
|
'Can I use these barcodes for Amazon (EAN/UPC)?': {
|
||||||
question: '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.',
|
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.',
|
||||||
},
|
},
|
||||||
'What is the difference between a barcode and a QR code?': {
|
'What is the difference between a barcode and a QR code?': {
|
||||||
question: 'What is the difference between a barcode and a QR code?',
|
question: 'What is the difference between a barcode and a QR code?',
|
||||||
answer: 'A barcode stores data horizontally (1D) and is mainly used for product IDs. A QR code stores data in 2D (matrix) and can hold much more information, such as URLs, vCards, or WiFi credentials.',
|
answer:
|
||||||
|
'A barcode stores data horizontally (1D) and is mainly used for product IDs. A QR code stores data in 2D (matrix) and can hold much more information, such as URLs, vCards, or WiFi credentials.',
|
||||||
|
},
|
||||||
|
'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.',
|
||||||
|
},
|
||||||
|
'What is the minimum print size for a scannable barcode?': {
|
||||||
|
question: 'What is the minimum print size for a scannable barcode?',
|
||||||
|
answer:
|
||||||
|
'The GS1 standard recommends a minimum width of 25.9mm (1 inch) for EAN-13 barcodes on retail packaging. Smaller sizes increase scan failure rates. For internal inventory labels, Code 128 can be printed as narrow as 15mm wide while remaining reliably scannable with modern handheld scanners.',
|
||||||
|
},
|
||||||
|
'Can I use Code 128 for inventory management?': {
|
||||||
|
question: 'Can I use Code 128 for inventory management?',
|
||||||
|
answer:
|
||||||
|
'Yes. Code 128 is the most widely used barcode format for inventory, logistics, and warehousing because it supports both letters and numbers, has high data density, and is readable by virtually all laser and 2D scanners. It is the recommended format for internal SKU systems, warehouse bin labels, and shipping labels.',
|
||||||
|
},
|
||||||
|
'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.',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -141,18 +216,35 @@ export default function BarcodeGeneratorPage() {
|
|||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
/>
|
/>
|
||||||
<ToolBreadcrumb toolName="Barcode Generator" toolSlug="barcode-generator" />
|
<ToolBreadcrumb
|
||||||
|
toolName="Barcode Generator"
|
||||||
|
toolSlug="barcode-generator"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
|
|
||||||
{/* HERO SECTION */}
|
{/* HERO SECTION */}
|
||||||
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-slate-900">
|
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-slate-900">
|
||||||
<div className="absolute inset-0 opacity-10">
|
<div className="absolute inset-0 opacity-10">
|
||||||
{/* Barcode Pattern */}
|
{/* Barcode Pattern */}
|
||||||
<svg className="w-full h-full" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
className="w-full h-full"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="barcode_pattern" width="60" height="60" patternUnits="userSpaceOnUse">
|
<pattern
|
||||||
<path d="M5 0 V 60 M15 0 V 60 M20 0 V 60 M35 0 V 60 M40 0 V 60 M55 0 V 60" stroke="white" strokeWidth="2" strokeOpacity="0.5" />
|
id="barcode_pattern"
|
||||||
|
width="60"
|
||||||
|
height="60"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5 0 V 60 M15 0 V 60 M20 0 V 60 M35 0 V 60 M40 0 V 60 M55 0 V 60"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeOpacity="0.5"
|
||||||
|
/>
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill="url(#barcode_pattern)" />
|
<rect width="100%" height="100%" fill="url(#barcode_pattern)" />
|
||||||
@@ -170,12 +262,19 @@ export default function BarcodeGeneratorPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||||
Free Online <span className="text-blue-400">Barcode Generator</span>
|
Free Custom{' '}
|
||||||
|
<span className="text-blue-400">Barcode Generator</span>
|
||||||
|
<span className="block text-2xl md:text-3xl font-semibold text-slate-300 mt-2">
|
||||||
|
Barcode Maker for EAN, UPC & Code 128
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-lg md:text-xl text-slate-400 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
<p className="text-lg md:text-xl text-slate-400 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||||
A <strong>barcode generator</strong> converts any number or text into a scannable barcode image — ready to download, print, and use on products, labels, or inventory systems.
|
A <strong>free custom barcode generator</strong> and{' '}
|
||||||
<span className="text-white block sm:inline mt-2 sm:mt-0"> Supports EAN-13, UPC-A, and Code 128. No signup required.</span>
|
<strong>barcode maker</strong> for EAN-13, UPC-A, UPC barcodes,
|
||||||
|
and Code 128. Convert any number or text into a scannable
|
||||||
|
barcode, download PNG or SVG, print instantly, no signup
|
||||||
|
required.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
||||||
@@ -204,19 +303,29 @@ export default function BarcodeGeneratorPage() {
|
|||||||
<div className="w-full bg-gradient-to-br from-blue-400 to-indigo-600 rounded-xl shadow-lg p-5 mb-6 relative overflow-hidden text-white">
|
<div className="w-full bg-gradient-to-br from-blue-400 to-indigo-600 rounded-xl shadow-lg p-5 mb-6 relative overflow-hidden text-white">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<BarcodeIcon className="w-8 h-8 opacity-80" />
|
<BarcodeIcon className="w-8 h-8 opacity-80" />
|
||||||
<div className="bg-white/20 px-2 py-1 rounded text-xs font-bold uppercase tracking-wider">Label</div>
|
<div className="bg-white/20 px-2 py-1 rounded text-xs font-bold uppercase tracking-wider">
|
||||||
|
Label
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold tracking-wider mb-1">
|
||||||
|
PROD-98234
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold tracking-wider mb-1">PROD-98234</div>
|
|
||||||
<div className="text-xs opacity-70">Inventory ID</div>
|
<div className="text-xs opacity-70">Inventory ID</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-48 h-32 bg-white rounded-xl p-4 shadow-inner relative overflow-hidden flex flex-col items-center justify-center">
|
<div className="w-48 h-32 bg-white rounded-xl p-4 shadow-inner relative overflow-hidden flex flex-col items-center justify-center">
|
||||||
<div className="w-full h-16 bg-black flex gap-1 mb-2">
|
<div className="w-full h-16 bg-black flex gap-1 mb-2">
|
||||||
{[2, 4, 1, 3, 2, 1, 4, 2, 1, 3].map((w, i) => (
|
{[2, 4, 1, 3, 2, 1, 4, 2, 1, 3].map((w, i) => (
|
||||||
<div key={i} className="bg-black flex-1" style={{ flex: w }} />
|
<div
|
||||||
|
key={i}
|
||||||
|
className="bg-black flex-1"
|
||||||
|
style={{ flex: w }}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] font-mono font-bold tracking-widest uppercase">98234001A</div>
|
<div className="text-[10px] font-mono font-bold tracking-widest uppercase">
|
||||||
|
98234001A
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating Badge */}
|
{/* Floating Badge */}
|
||||||
@@ -225,8 +334,12 @@ export default function BarcodeGeneratorPage() {
|
|||||||
<Printer className="w-5 h-5 text-blue-500" />
|
<Printer className="w-5 h-5 text-blue-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Ready</div>
|
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">
|
||||||
<div className="text-sm font-bold text-white">Print Instantly</div>
|
Ready
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-bold text-white">
|
||||||
|
Print Instantly
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,6 +352,89 @@ export default function BarcodeGeneratorPage() {
|
|||||||
<BarcodeGeneratorClient />
|
<BarcodeGeneratorClient />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* AI-EXTRACTABLE DEFINITION + STATS BLOCK */}
|
||||||
|
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-white border-b border-slate-100">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="bg-blue-50 border-l-4 border-blue-500 rounded-xl p-6 mb-8">
|
||||||
|
<h2 className="text-xl font-bold text-slate-900 mb-2">
|
||||||
|
What is a Barcode Generator?
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-700 leading-relaxed">
|
||||||
|
A <strong>custom barcode generator</strong> is an online tool
|
||||||
|
that converts a number or text string into a scannable barcode
|
||||||
|
image (EAN-13, UPC-A, or Code 128). The generated barcode can be
|
||||||
|
downloaded as PNG or SVG and printed on product labels,
|
||||||
|
packaging, or inventory stickers for use with any standard
|
||||||
|
barcode scanner.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-3 gap-6 mb-8">
|
||||||
|
<div className="text-center p-5 rounded-xl bg-slate-50 border border-slate-200">
|
||||||
|
<div className="text-3xl font-extrabold text-blue-600 mb-1">
|
||||||
|
EAN-13
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-semibold text-slate-700 mb-1">
|
||||||
|
Global Retail Standard
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500">
|
||||||
|
Used on over 5 billion product labels worldwide (GS1, 2024)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-5 rounded-xl bg-slate-50 border border-slate-200">
|
||||||
|
<div className="text-3xl font-extrabold text-blue-600 mb-1">
|
||||||
|
UPC-A
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-semibold text-slate-700 mb-1">
|
||||||
|
North America Standard
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500">
|
||||||
|
Required by Amazon, Walmart, Target for product listings
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-5 rounded-xl bg-slate-50 border border-slate-200">
|
||||||
|
<div className="text-3xl font-extrabold text-blue-600 mb-1">
|
||||||
|
Code 128
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-semibold text-slate-700 mb-1">
|
||||||
|
Inventory & Logistics
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500">
|
||||||
|
Supports letters + numbers — best for internal SKU systems
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-xl p-5">
|
||||||
|
<h3 className="font-bold text-slate-900 mb-2">
|
||||||
|
Barcode vs. QR Code — When to Use Which
|
||||||
|
</h3>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4 text-sm text-slate-700">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-slate-800 mb-1">
|
||||||
|
✓ Use a barcode for:
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1 list-disc list-inside text-slate-600">
|
||||||
|
<li>Product SKUs and retail checkout</li>
|
||||||
|
<li>Warehouse shelf and bin labels</li>
|
||||||
|
<li>Inventory counting and stock control</li>
|
||||||
|
<li>Order fulfillment and packing verification</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-slate-800 mb-1">
|
||||||
|
✓ Use a QR code for:
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1 list-disc list-inside text-slate-600">
|
||||||
|
<li>Restaurant menus and digital content</li>
|
||||||
|
<li>Marketing campaigns and landing pages</li>
|
||||||
|
<li>Review collection and customer feedback</li>
|
||||||
|
<li>Product setup guides and support pages</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* HOW IT WORKS */}
|
{/* HOW IT WORKS */}
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
@@ -300,15 +496,78 @@ export default function BarcodeGeneratorPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-slate-50 border-y border-slate-200">
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
<div className="max-w-3xl mb-10">
|
||||||
|
<p className="text-sm font-semibold text-blue-600 uppercase tracking-wider mb-3">
|
||||||
|
Custom barcode generator
|
||||||
|
</p>
|
||||||
|
<h2 className="text-3xl font-bold text-slate-900 mb-4">
|
||||||
|
Create custom barcodes for labels, inventory, and product
|
||||||
|
workflows
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-slate-600 leading-relaxed">
|
||||||
|
Use the barcode maker when you already have a product number,
|
||||||
|
SKU, inventory ID, or internal reference and need a printable
|
||||||
|
barcode image. Choose the format, adjust the dimensions, show or
|
||||||
|
hide the text value, then export a clean PNG or SVG for labels.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
|
<article className="rounded-xl border border-slate-200 bg-white p-6">
|
||||||
|
<h3 className="font-bold text-slate-900 mb-2">Retail labels</h3>
|
||||||
|
<p className="text-slate-600 text-sm leading-relaxed">
|
||||||
|
Generate EAN-13 or UPC-A barcode images for packaging and
|
||||||
|
shelf labels when you already have valid GS1-issued numbers.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article className="rounded-xl border border-slate-200 bg-white p-6">
|
||||||
|
<h3 className="font-bold text-slate-900 mb-2">
|
||||||
|
Inventory codes
|
||||||
|
</h3>
|
||||||
|
<p className="text-slate-600 text-sm leading-relaxed">
|
||||||
|
Use Code 128 for alphanumeric SKUs, warehouse bins, assets,
|
||||||
|
and internal tracking labels that need scanner-friendly codes.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article className="rounded-xl border border-slate-200 bg-white p-6">
|
||||||
|
<h3 className="font-bold text-slate-900 mb-2">
|
||||||
|
Printable exports
|
||||||
|
</h3>
|
||||||
|
<p className="text-slate-600 text-sm leading-relaxed">
|
||||||
|
Download SVG for crisp print layouts or PNG for fast use in
|
||||||
|
docs, product sheets, and simple label workflows.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-8 text-sm text-slate-600">
|
||||||
|
Need a code for a web page instead of a retail scanner? Use the{' '}
|
||||||
|
<a
|
||||||
|
href="/dynamic-qr-code-generator"
|
||||||
|
className="font-semibold text-blue-700 underline"
|
||||||
|
>
|
||||||
|
dynamic QR code generator
|
||||||
|
</a>{' '}
|
||||||
|
if the destination may change later, or the{' '}
|
||||||
|
<a
|
||||||
|
href="/qr-code-tracking"
|
||||||
|
className="font-semibold text-blue-700 underline"
|
||||||
|
>
|
||||||
|
QR code tracking
|
||||||
|
</a>{' '}
|
||||||
|
workflow when you need scan analytics.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* RELATED TOOLS */}
|
{/* RELATED TOOLS */}
|
||||||
<RelatedTools />
|
<RelatedTools />
|
||||||
|
|
||||||
{/* SEO GUIDE */}
|
{/* SEO GUIDE */}
|
||||||
<BarcodeGuide />
|
<BarcodeGuide />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -45,6 +46,7 @@ export default function PhoneGenerator() {
|
|||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
const [qrColor, setQrColor] = useState(BRAND.richBlue);
|
const [qrColor, setQrColor] = useState(BRAND.richBlue);
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -74,6 +76,7 @@ export default function PhoneGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
@@ -82,6 +85,8 @@ export default function PhoneGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -243,5 +248,6 @@ export default function PhoneGenerator() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Input } from '@/components/ui/Input';
|
|||||||
import { Select } from '@/components/ui/Select';
|
import { Select } from '@/components/ui/Select';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import AdBanner from '@/components/ads/AdBanner';
|
import AdBanner from '@/components/ads/AdBanner';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -58,6 +59,7 @@ export default function CryptoGenerator() {
|
|||||||
const [qrMode, setQrMode] = useState<'universal' | 'wallet'>('universal');
|
const [qrMode, setQrMode] = useState<'universal' | 'wallet'>('universal');
|
||||||
const [qrColor, setQrColor] = useState('#F7931A');
|
const [qrColor, setQrColor] = useState('#F7931A');
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -123,6 +125,7 @@ export default function CryptoGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
@@ -131,6 +134,8 @@ export default function CryptoGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -370,5 +375,6 @@ export default function CryptoGenerator() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -47,6 +48,7 @@ export default function EmailGenerator() {
|
|||||||
subject: '',
|
subject: '',
|
||||||
body: ''
|
body: ''
|
||||||
});
|
});
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const [qrColor, setQrColor] = useState('#dc2626');
|
const [qrColor, setQrColor] = useState('#dc2626');
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
@@ -88,6 +90,7 @@ export default function EmailGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
@@ -100,6 +103,8 @@ export default function EmailGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -292,5 +297,6 @@ export default function EmailGenerator() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -51,6 +52,7 @@ export default function EventGenerator() {
|
|||||||
|
|
||||||
const [qrColor, setQrColor] = useState(BRAND.primary);
|
const [qrColor, setQrColor] = useState(BRAND.primary);
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -102,6 +104,7 @@ export default function EventGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
@@ -110,6 +113,8 @@ export default function EventGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -326,5 +331,6 @@ export default function EventGenerator() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -46,6 +47,7 @@ export default function FacebookGenerator() {
|
|||||||
const [url, setUrl] = useState('');
|
const [url, setUrl] = useState('');
|
||||||
const [qrColor, setQrColor] = useState('#1877F2'); // Default to FB Blue
|
const [qrColor, setQrColor] = useState('#1877F2'); // Default to FB Blue
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -73,6 +75,7 @@ export default function FacebookGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
@@ -81,6 +84,8 @@ export default function FacebookGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -243,5 +248,6 @@ export default function FacebookGenerator() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -46,6 +47,7 @@ export default function GeolocationGenerator() {
|
|||||||
const [longitude, setLongitude] = useState('');
|
const [longitude, setLongitude] = useState('');
|
||||||
const [qrColor, setQrColor] = useState(BRAND.primary);
|
const [qrColor, setQrColor] = useState(BRAND.primary);
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ export default function GeolocationGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
@@ -101,6 +104,8 @@ export default function GeolocationGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -288,5 +293,6 @@ export default function GeolocationGenerator() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { toPng } from 'html-to-image';
|
|||||||
import { Star, Download, AlertCircle } from 'lucide-react';
|
import { Star, Download, AlertCircle } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
const QR_COLORS = [
|
const QR_COLORS = [
|
||||||
{ name: 'Google Blue', value: '#1A73E8' },
|
{ name: 'Google Blue', value: '#1A73E8' },
|
||||||
@@ -41,6 +42,7 @@ export default function GoogleReviewGenerator() {
|
|||||||
const [qrColor, setQrColor] = useState('#1A73E8');
|
const [qrColor, setQrColor] = useState('#1A73E8');
|
||||||
const [frameType, setFrameType] = useState('review');
|
const [frameType, setFrameType] = useState('review');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -76,12 +78,15 @@ export default function GoogleReviewGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const frameLabel = FRAME_OPTIONS.find(f => f.id === frameType && f.id !== 'none')?.label ?? null;
|
const frameLabel = FRAME_OPTIONS.find(f => f.id === frameType && f.id !== 'none')?.label ?? null;
|
||||||
const isReady = reviewUrl && !error && isValidGoogleReviewLink(reviewUrl);
|
const isReady = reviewUrl && !error && isValidGoogleReviewLink(reviewUrl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
||||||
<div className="grid lg:grid-cols-2">
|
<div className="grid lg:grid-cols-2">
|
||||||
@@ -209,5 +214,6 @@ export default function GoogleReviewGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,15 @@ export const metadata: Metadata = {
|
|||||||
title: {
|
title: {
|
||||||
absolute: 'Google Review QR Code Generator — Free | QR Master',
|
absolute: 'Google Review QR Code Generator — Free | QR Master',
|
||||||
},
|
},
|
||||||
description: 'Create a QR code for your Google Reviews in seconds. Customers scan once and land directly on your review form. Free, no signup required.',
|
description:
|
||||||
keywords: ['qr code for google reviews', 'qr code generator for google reviews', 'google review qr code', 'google maps review qr code', 'get more google reviews'],
|
'Create a QR code for your Google Reviews in seconds. Customers scan once and land directly on your review form. Free, no signup required.',
|
||||||
|
keywords: [
|
||||||
|
'qr code for google reviews',
|
||||||
|
'qr code generator for google reviews',
|
||||||
|
'google review qr code',
|
||||||
|
'google maps review qr code',
|
||||||
|
'get more google reviews',
|
||||||
|
],
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: 'https://www.qrmaster.net/tools/google-review-qr-code',
|
canonical: 'https://www.qrmaster.net/tools/google-review-qr-code',
|
||||||
languages: {
|
languages: {
|
||||||
@@ -23,14 +30,16 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Google Review QR Code Generator — Free | QR Master',
|
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.',
|
description:
|
||||||
|
'Create a QR code that takes customers directly to your Google review form. More reviews, less friction.',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
url: 'https://www.qrmaster.net/tools/google-review-qr-code',
|
url: 'https://www.qrmaster.net/tools/google-review-qr-code',
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
title: 'Google Review QR Code Generator — Free',
|
title: 'Google Review QR Code Generator — Free',
|
||||||
description: 'Create a QR code that takes customers directly to your Google review form.',
|
description:
|
||||||
|
'Create a QR code that takes customers directly to your Google review form.',
|
||||||
},
|
},
|
||||||
robots: {
|
robots: {
|
||||||
index: true,
|
index: true,
|
||||||
@@ -50,13 +59,14 @@ const jsonLd = {
|
|||||||
'@type': 'HowTo',
|
'@type': 'HowTo',
|
||||||
name: 'How to Create a Google Review QR Code',
|
name: 'How to Create a Google Review QR Code',
|
||||||
datePublished: '2024-01-01',
|
datePublished: '2024-01-01',
|
||||||
dateModified: '2025-06-01',
|
dateModified: '2026-04-27',
|
||||||
author: {
|
author: {
|
||||||
'@type': 'Person',
|
'@type': 'Person',
|
||||||
name: 'Timo Knuth',
|
name: 'Timo Knuth',
|
||||||
url: 'https://www.qrmaster.net/authors/timo',
|
url: 'https://www.qrmaster.net/authors/timo',
|
||||||
},
|
},
|
||||||
description: 'Generate a QR code that sends customers directly to your Google review form.',
|
description:
|
||||||
|
'Generate a QR code that sends customers directly to your Google review form.',
|
||||||
step: [
|
step: [
|
||||||
{
|
{
|
||||||
'@type': 'HowToStep',
|
'@type': 'HowToStep',
|
||||||
@@ -133,9 +143,24 @@ const jsonLd = {
|
|||||||
{
|
{
|
||||||
'@type': 'BreadcrumbList',
|
'@type': 'BreadcrumbList',
|
||||||
itemListElement: [
|
itemListElement: [
|
||||||
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://www.qrmaster.net' },
|
{
|
||||||
{ '@type': 'ListItem', position: 2, name: 'QR Code Tools', item: 'https://www.qrmaster.net/tools' },
|
'@type': 'ListItem',
|
||||||
{ '@type': 'ListItem', position: 3, name: 'Google Review QR Code Generator', item: 'https://www.qrmaster.net/tools/google-review-qr-code' },
|
position: 1,
|
||||||
|
name: 'Home',
|
||||||
|
item: 'https://www.qrmaster.net',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 2,
|
||||||
|
name: 'QR Code Tools',
|
||||||
|
item: 'https://www.qrmaster.net/tools',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 3,
|
||||||
|
name: 'Google Review QR Code Generator',
|
||||||
|
item: 'https://www.qrmaster.net/tools/google-review-qr-code',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -148,10 +173,12 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
/>
|
/>
|
||||||
<ToolBreadcrumb toolName="Google Review QR Code Generator" toolSlug="google-review-qr-code" />
|
<ToolBreadcrumb
|
||||||
|
toolName="Google Review QR Code Generator"
|
||||||
|
toolSlug="google-review-qr-code"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
|
|
||||||
{/* HERO */}
|
{/* HERO */}
|
||||||
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-[#1A1265]">
|
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-[#1A1265]">
|
||||||
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
||||||
@@ -166,12 +193,18 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
|
|
||||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||||
Google Review QR Code <br className="hidden lg:block" />
|
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</span>
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-orange-400">
|
||||||
|
Generator — Free
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||||
Customers scan once and land directly on your Google review form.
|
Customers scan once and land directly on your Google review
|
||||||
<strong className="text-white block sm:inline mt-2 sm:mt-0"> More reviews, less friction.</strong>
|
form.
|
||||||
|
<strong className="text-white block sm:inline mt-2 sm:mt-0">
|
||||||
|
{' '}
|
||||||
|
More reviews, less friction.
|
||||||
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
||||||
@@ -192,7 +225,12 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform -rotate-3 hover:rotate-0 transition-all duration-700 group">
|
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform -rotate-3 hover:rotate-0 transition-all duration-700 group">
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
||||||
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner mb-6 relative overflow-hidden flex items-center justify-center">
|
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner mb-6 relative overflow-hidden flex items-center justify-center">
|
||||||
<QRCodeSVG value="https://www.qrmaster.net/tools/google-review-qr-code" size={170} fgColor="#1A73E8" level="Q" />
|
<QRCodeSVG
|
||||||
|
value="https://www.qrmaster.net/tools/google-review-qr-code"
|
||||||
|
size={170}
|
||||||
|
fgColor="#1A73E8"
|
||||||
|
level="Q"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-white/10 rounded-xl p-4 backdrop-blur-sm border border-white/10">
|
<div className="w-full bg-white/10 rounded-xl p-4 backdrop-blur-sm border border-white/10">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -222,7 +260,8 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
How to Find Your Google Review Link
|
How to Find Your Google Review Link
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-slate-600 text-center mb-12 max-w-2xl mx-auto">
|
<p className="text-slate-600 text-center mb-12 max-w-2xl mx-auto">
|
||||||
You need your Google Business review URL before creating the QR code. Here are two ways to get it.
|
You need your Google Business review URL before creating the QR
|
||||||
|
code. Here are two ways to get it.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-8">
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
@@ -230,13 +269,38 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center mb-4">
|
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center mb-4">
|
||||||
<Search className="w-6 h-6 text-blue-600" />
|
<Search className="w-6 h-6 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-slate-900 mb-3">Method 1: Google Maps</h3>
|
<h3 className="font-bold text-slate-900 mb-3">
|
||||||
|
Method 1: Google Maps
|
||||||
|
</h3>
|
||||||
<ol className="space-y-2 text-sm text-slate-600 list-none">
|
<ol className="space-y-2 text-sm text-slate-600 list-none">
|
||||||
<li><span className="font-semibold text-slate-800">1.</span> Open Google Maps</li>
|
<li>
|
||||||
<li><span className="font-semibold text-slate-800">2.</span> Search for your business name</li>
|
<span className="font-semibold text-slate-800">1.</span>{' '}
|
||||||
<li><span className="font-semibold text-slate-800">3.</span> Click the <strong>Share</strong> button</li>
|
Open Google Maps
|
||||||
<li><span className="font-semibold text-slate-800">4.</span> Copy the short link (starts with <code className="bg-white px-1 rounded text-xs">maps.app.goo.gl</code> or <code className="bg-white px-1 rounded text-xs">g.page</code>)</li>
|
</li>
|
||||||
<li><span className="font-semibold text-slate-800">5.</span> Paste into the generator above</li>
|
<li>
|
||||||
|
<span className="font-semibold text-slate-800">2.</span>{' '}
|
||||||
|
Search for your business name
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="font-semibold text-slate-800">3.</span>{' '}
|
||||||
|
Click the <strong>Share</strong> button
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="font-semibold text-slate-800">4.</span>{' '}
|
||||||
|
Copy the short link (starts with{' '}
|
||||||
|
<code className="bg-white px-1 rounded text-xs">
|
||||||
|
maps.app.goo.gl
|
||||||
|
</code>{' '}
|
||||||
|
or{' '}
|
||||||
|
<code className="bg-white px-1 rounded text-xs">
|
||||||
|
g.page
|
||||||
|
</code>
|
||||||
|
)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="font-semibold text-slate-800">5.</span>{' '}
|
||||||
|
Paste into the generator above
|
||||||
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
@@ -244,13 +308,30 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
<div className="w-12 h-12 rounded-xl bg-yellow-100 flex items-center justify-center mb-4">
|
<div className="w-12 h-12 rounded-xl bg-yellow-100 flex items-center justify-center mb-4">
|
||||||
<Share2 className="w-6 h-6 text-yellow-600" />
|
<Share2 className="w-6 h-6 text-yellow-600" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-slate-900 mb-3">Method 2: Google Business Profile</h3>
|
<h3 className="font-bold text-slate-900 mb-3">
|
||||||
|
Method 2: Google Business Profile
|
||||||
|
</h3>
|
||||||
<ol className="space-y-2 text-sm text-slate-600 list-none">
|
<ol className="space-y-2 text-sm text-slate-600 list-none">
|
||||||
<li><span className="font-semibold text-slate-800">1.</span> Sign in to <strong>business.google.com</strong></li>
|
<li>
|
||||||
<li><span className="font-semibold text-slate-800">2.</span> Select your business location</li>
|
<span className="font-semibold text-slate-800">1.</span>{' '}
|
||||||
<li><span className="font-semibold text-slate-800">3.</span> Click <strong>"Get more reviews"</strong></li>
|
Sign in to <strong>business.google.com</strong>
|
||||||
<li><span className="font-semibold text-slate-800">4.</span> Copy the review shortlink provided</li>
|
</li>
|
||||||
<li><span className="font-semibold text-slate-800">5.</span> Paste into the generator above</li>
|
<li>
|
||||||
|
<span className="font-semibold text-slate-800">2.</span>{' '}
|
||||||
|
Select your business location
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="font-semibold text-slate-800">3.</span>{' '}
|
||||||
|
Click <strong>"Get more reviews"</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="font-semibold text-slate-800">4.</span>{' '}
|
||||||
|
Copy the review shortlink provided
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="font-semibold text-slate-800">5.</span>{' '}
|
||||||
|
Paste into the generator above
|
||||||
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,7 +339,10 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* USE CASES */}
|
{/* USE CASES */}
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
<section
|
||||||
|
className="py-16 px-4 sm:px-6 lg:px-8"
|
||||||
|
style={{ backgroundColor: '#EBEBDF' }}
|
||||||
|
>
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
||||||
Who Uses Google Review QR Codes?
|
Who Uses Google Review QR Codes?
|
||||||
@@ -269,15 +353,38 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
|
|
||||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
{[
|
{[
|
||||||
{ icon: '🍽️', title: 'Restaurants & Cafés', text: 'Print on receipts or table cards. Ask after the meal when the experience is fresh.' },
|
{
|
||||||
{ icon: '🏨', title: 'Hotels & Guesthouses', text: 'Place on checkout envelopes or in-room cards. Capture reviews at checkout.' },
|
icon: '🍽️',
|
||||||
{ icon: '🏥', title: 'Clinics & Salons', text: 'Display at reception. Patients and clients who had a great experience can review in seconds.' },
|
title: 'Restaurants & Cafés',
|
||||||
{ icon: '🛍️', title: 'Retail & Shops', text: 'Include in packaging or display at checkout. Turn happy shoppers into reviewers.' },
|
text: 'Print on receipts or table cards. Ask after the meal when the experience is fresh.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '🏨',
|
||||||
|
title: 'Hotels & Guesthouses',
|
||||||
|
text: 'Place on checkout envelopes or in-room cards. Capture reviews at checkout.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '🏥',
|
||||||
|
title: 'Clinics & Salons',
|
||||||
|
text: 'Display at reception. Patients and clients who had a great experience can review in seconds.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '🛍️',
|
||||||
|
title: 'Retail & Shops',
|
||||||
|
text: 'Include in packaging or display at checkout. Turn happy shoppers into reviewers.',
|
||||||
|
},
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<article key={item.title} className="p-6 bg-white rounded-2xl border border-slate-100 shadow-sm">
|
<article
|
||||||
|
key={item.title}
|
||||||
|
className="p-6 bg-white rounded-2xl border border-slate-100 shadow-sm"
|
||||||
|
>
|
||||||
<div className="text-3xl mb-3">{item.icon}</div>
|
<div className="text-3xl mb-3">{item.icon}</div>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">{item.title}</h3>
|
<h3 className="font-bold text-slate-900 mb-2">
|
||||||
<p className="text-sm text-slate-600 leading-relaxed">{item.text}</p>
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-600 leading-relaxed">
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -289,43 +396,91 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<TrendingUp className="w-5 h-5 text-yellow-500" />
|
<TrendingUp className="w-5 h-5 text-yellow-500" />
|
||||||
<span className="text-sm font-semibold text-yellow-600 uppercase tracking-wider">Research-backed impact</span>
|
<span className="text-sm font-semibold text-yellow-600 uppercase tracking-wider">
|
||||||
|
Research-backed impact
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-bold text-slate-900 mb-4">
|
<h2 className="text-3xl font-bold text-slate-900 mb-4">
|
||||||
Why Google Reviews Matter for Your Business
|
Why Google Reviews Matter for Your Business
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-slate-600 mb-10 max-w-2xl">
|
<p className="text-slate-600 mb-10 max-w-2xl">
|
||||||
A <strong>Google Review QR code</strong> reduces the friction between a satisfied customer and a published review — the single biggest barrier to getting more reviews.
|
A <strong>Google Review QR code</strong> reduces the friction
|
||||||
|
between a satisfied customer and a published review — the single
|
||||||
|
biggest barrier to getting more reviews.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6 mb-8">
|
<div className="grid md:grid-cols-2 gap-6 mb-8">
|
||||||
<div className="bg-yellow-50 border border-yellow-100 rounded-2xl p-6">
|
<div className="bg-yellow-50 border border-yellow-100 rounded-2xl p-6">
|
||||||
<div className="text-4xl font-extrabold text-yellow-600 mb-2">70%</div>
|
<div className="text-4xl font-extrabold text-yellow-600 mb-2">
|
||||||
|
70%
|
||||||
|
</div>
|
||||||
<p className="text-slate-700 text-sm leading-relaxed mb-3">
|
<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%.
|
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%.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-500">
|
||||||
Source: <a href="https://brightlocal.com/research/local-consumer-review-survey/" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">BrightLocal Local Consumer Review Survey</a>
|
Source:{' '}
|
||||||
|
<a
|
||||||
|
href="https://brightlocal.com/research/local-consumer-review-survey/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:text-slate-700"
|
||||||
|
>
|
||||||
|
BrightLocal Local Consumer Review Survey
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-6">
|
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-6">
|
||||||
<div className="text-4xl font-extrabold text-blue-600 mb-2">+270%</div>
|
<div className="text-4xl font-extrabold text-blue-600 mb-2">
|
||||||
|
+270%
|
||||||
|
</div>
|
||||||
<p className="text-slate-700 text-sm leading-relaxed mb-3">
|
<p className="text-slate-700 text-sm leading-relaxed mb-3">
|
||||||
increase in conversion rates for products and businesses with reviews compared to those without. Capturing reviews at the point of sale — where satisfaction is highest — maximizes this effect.
|
increase in conversion rates for products and businesses with
|
||||||
|
reviews compared to those without. Capturing reviews at the
|
||||||
|
point of sale — where satisfaction is highest — maximizes this
|
||||||
|
effect.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-500">
|
||||||
Source: <a href="https://spiegel.medill.northwestern.edu/online-reviews/" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">Spiegel Research Center, Northwestern University</a>
|
Source:{' '}
|
||||||
|
<a
|
||||||
|
href="https://spiegel.medill.northwestern.edu/online-reviews/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:text-slate-700"
|
||||||
|
>
|
||||||
|
Spiegel Research Center, Northwestern University
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-slate-400 italic">
|
<p className="text-xs text-slate-400 italic">
|
||||||
By Timo Knuth, QR Master · Last updated: June 2025 · Based on independent academic and industry research
|
By Timo Knuth, QR Master · Last updated: June 2025 · Based on
|
||||||
|
independent academic and industry research
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* SEO Content Block */}
|
||||||
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white border-t border-slate-100">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-slate-900 mb-8">
|
||||||
|
Why Google Review QR Codes Work Better Than Asking Verbally
|
||||||
|
</h2>
|
||||||
|
<div className="prose prose-slate max-w-none">
|
||||||
|
<p className="text-lg text-slate-600 mb-6">Verbally asking for a review creates a promise customers intend to keep but rarely fulfill. The moment they leave your business, the intention fades. A Google Review QR code shortens the gap between the moment of satisfaction and the act of leaving a review to a single scan — while the experience is still fresh and the customer is still engaged.</p>
|
||||||
|
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">The Best Placement for Google Review QR Codes</h3>
|
||||||
|
<p className="text-slate-600 mb-4">Placement is everything. The highest-performing locations are those where customers are already pausing: on printed receipts so they see it while reviewing the bill, on table tent cards at restaurants between ordering and paying, on the front door or exit so it is the last thing they see when leaving satisfied, and on packaging inserts inside product boxes that customers open at home after a purchase. Display the QR code at roughly A5 size with a clear label such as "Happy with your visit? Leave us a Google Review" — customers do not need instructions beyond that. Checkout counters and front desk areas work especially well because staff can gesture toward the code while the customer is already in a positive frame of mind.</p>
|
||||||
|
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">How Many More Reviews Will You Get?</h3>
|
||||||
|
<p className="text-slate-600 mb-4">Research consistently shows that reducing friction is the primary lever for increasing review volume. Businesses that deploy Google Review QR codes at the point of sale typically see 3 to 5 times more reviews compared to relying on email follow-ups alone, where completion rates often fall below 2%. The reason is timing: a QR code captures the customer at peak satisfaction, requiring no extra steps beyond scanning and tapping the star rating. Email review requests, by contrast, arrive hours or days later when the emotional high has passed and competing priorities fill the inbox. Even a modest increase from 5 to 20 reviews per month compounds over a year into a significantly stronger local search presence, since Google's ranking algorithm weighs both review count and recency.</p>
|
||||||
|
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Responding to Reviews: What to Do After You Collect Them</h3>
|
||||||
|
<p className="text-slate-600 mb-4">Collecting reviews is only half the strategy. Responding to every review — positive and negative — signals to Google that your business is active and engaged, which supports local ranking. For positive reviews, a brief personalised thank-you (mentioning a specific detail if possible) reinforces the relationship. For critical reviews, acknowledge the issue, apologise where appropriate, and invite further contact offline. Google surfaces response rate and speed in its quality signals, so even a short reply within 24 hours outperforms silence. To understand which QR code placements are driving the most scans before reviewers land on Google, use <a href="/qr-code-tracking" className="text-blue-600 underline hover:text-blue-800">QR code scan tracking</a> to measure volume by location and time of day.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<GrowthLinksSection
|
<GrowthLinksSection
|
||||||
eyebrow="Level up your local marketing"
|
eyebrow="Level up your local marketing"
|
||||||
title="More QR workflows for local businesses"
|
title="More QR workflows for local businesses"
|
||||||
@@ -334,25 +489,36 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
{
|
{
|
||||||
href: '/qr-code-for/restaurants',
|
href: '/qr-code-for/restaurants',
|
||||||
title: 'QR Codes for Restaurants',
|
title: 'QR Codes for Restaurants',
|
||||||
description: 'Menu, ordering, and review QR workflows built for food service businesses.',
|
description:
|
||||||
|
'Menu, ordering, and review QR workflows built for food service businesses.',
|
||||||
ctaLabel: 'Restaurant QR workflows',
|
ctaLabel: 'Restaurant QR workflows',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/qr-code-for/hotels',
|
href: '/qr-code-for/hotels',
|
||||||
title: 'QR Codes for Hotels',
|
title: 'QR Codes for Hotels',
|
||||||
description: 'Check-in, room service, and review QR setups for hospitality.',
|
description:
|
||||||
|
'Check-in, room service, and review QR setups for hospitality.',
|
||||||
ctaLabel: 'Hotel QR workflows',
|
ctaLabel: 'Hotel QR workflows',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/use-cases/qr-codes-for-review-collection',
|
||||||
|
title: 'QR Codes for Review Collection',
|
||||||
|
description:
|
||||||
|
'Plan receipts, table cards, packaging, and counters as measurable review-request placements.',
|
||||||
|
ctaLabel: 'Build review collection workflow',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/dynamic-qr-code-generator',
|
href: '/dynamic-qr-code-generator',
|
||||||
title: 'Dynamic QR Code Generator',
|
title: 'Dynamic QR Code Generator',
|
||||||
description: 'Update your review link or redirect to a different page anytime — no reprint needed.',
|
description:
|
||||||
|
'Update your review link or redirect to a different page anytime — no reprint needed.',
|
||||||
ctaLabel: 'Create dynamic QR code',
|
ctaLabel: 'Create dynamic QR code',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/qr-code-tracking',
|
href: '/qr-code-tracking',
|
||||||
title: 'QR Code Tracking',
|
title: 'QR Code Tracking',
|
||||||
description: 'See exactly how many people scanned your review QR code and from which location.',
|
description:
|
||||||
|
'See exactly how many people scanned your review QR code and from which location.',
|
||||||
ctaLabel: 'Track QR code scans',
|
ctaLabel: 'Track QR code scans',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -363,7 +529,10 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
<RelatedTools />
|
<RelatedTools />
|
||||||
|
|
||||||
{/* FAQ */}
|
{/* FAQ */}
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
<section
|
||||||
|
className="py-16 px-4 sm:px-6 lg:px-8"
|
||||||
|
style={{ backgroundColor: '#EBEBDF' }}
|
||||||
|
>
|
||||||
<div className="max-w-3xl mx-auto">
|
<div className="max-w-3xl mx-auto">
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
||||||
Frequently Asked Questions
|
Frequently Asked Questions
|
||||||
@@ -376,30 +545,47 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
question: 'How do I find my Google Review link?',
|
question: 'How do I find my Google Review link?',
|
||||||
answer: 'Open Google Maps → search your business → click Share → Copy link. Or go to Google Business Profile → "Get more reviews" for a direct shortlink.',
|
answer:
|
||||||
|
'Open Google Maps → search your business → click Share → Copy link. Or go to Google Business Profile → "Get more reviews" for a direct shortlink.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: 'Does the Google Review QR code expire?',
|
question: 'Does the Google Review QR code expire?',
|
||||||
answer: 'No. This is a static QR code that encodes your Google review URL directly. It works indefinitely as long as your Google Business Profile is active.',
|
answer:
|
||||||
|
'No. This is a static QR code that encodes your Google review URL directly. It works indefinitely as long as your Google Business Profile is active.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: 'Can I track how many people scanned it?',
|
question: 'Can I track how many people scanned it?',
|
||||||
answer: 'Not with a static QR code. For scan analytics (device, location, time), create a dynamic QR code with tracking through QR Master.',
|
answer:
|
||||||
|
'Not with a static QR code. For scan analytics (device, location, time), create a dynamic QR code with tracking through QR Master.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: 'What happens when a customer scans the QR code?',
|
question: 'What happens when a customer scans the QR code?',
|
||||||
answer: 'They are taken directly to your Google review form. If they are logged into a Google account, they can leave a star rating and review immediately.',
|
answer:
|
||||||
|
'They are taken directly to your Google review form. If they are logged into a Google account, they can leave a star rating and review immediately.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: 'Where should I put the Google Review QR code?',
|
question: 'Where should I put the Google Review QR code?',
|
||||||
answer: 'Best placements: receipts, table cards, checkout counters, packaging inserts, and your shop window. Ask for reviews right after the positive experience.',
|
answer:
|
||||||
|
'Best placements: receipts, table cards, checkout counters, packaging inserts, and your shop window. Ask for reviews right after the positive experience.',
|
||||||
},
|
},
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<details key={item.question} className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
<details
|
||||||
|
key={item.question}
|
||||||
|
className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden"
|
||||||
|
>
|
||||||
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
||||||
{item.question}
|
{item.question}
|
||||||
<span className="transition group-open:rotate-180 text-slate-400">
|
<span className="transition group-open:rotate-180 text-slate-400">
|
||||||
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
>
|
||||||
<path d="M6 9l6 6 6-6" />
|
<path d="M6 9l6 6 6-6" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
@@ -412,7 +598,6 @@ export default function GoogleReviewQRCodePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -45,6 +46,7 @@ export default function InstagramGenerator() {
|
|||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [qrColor, setQrColor] = useState('#E1306C');
|
const [qrColor, setQrColor] = useState('#E1306C');
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -78,6 +80,7 @@ export default function InstagramGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
@@ -86,6 +89,8 @@ export default function InstagramGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -248,5 +253,6 @@ export default function InstagramGenerator() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const jsonLd = {
|
|||||||
name: 'How to Create an Instagram QR Code',
|
name: 'How to Create an Instagram QR Code',
|
||||||
description: 'Create a QR code that opens an Instagram profile.',
|
description: 'Create a QR code that opens an Instagram profile.',
|
||||||
datePublished: '2024-01-01',
|
datePublished: '2024-01-01',
|
||||||
dateModified: '2025-06-01',
|
dateModified: '2026-04-27',
|
||||||
author: {
|
author: {
|
||||||
'@type': 'Person',
|
'@type': 'Person',
|
||||||
name: 'Timo Knuth',
|
name: 'Timo Knuth',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Button } from '@/components/ui/Button';
|
|||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Select } from '@/components/ui/Select';
|
import { Select } from '@/components/ui/Select';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors - PayPal Blue
|
// Brand Colors - PayPal Blue
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -64,6 +65,7 @@ export default function PayPalGenerator() {
|
|||||||
const [currency, setCurrency] = useState('EUR');
|
const [currency, setCurrency] = useState('EUR');
|
||||||
const [qrColor, setQrColor] = useState(BRAND.primary);
|
const [qrColor, setQrColor] = useState(BRAND.primary);
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -114,6 +116,7 @@ export default function PayPalGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
@@ -122,6 +125,8 @@ export default function PayPalGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -338,5 +343,6 @@ export default function PayPalGenerator() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -46,6 +47,7 @@ export default function SMSGenerator() {
|
|||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [qrColor, setQrColor] = useState(BRAND.primary);
|
const [qrColor, setQrColor] = useState(BRAND.primary);
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ export default function SMSGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
@@ -84,6 +87,8 @@ export default function SMSGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -262,5 +267,6 @@ export default function SMSGenerator() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors - Microsoft Teams Purple
|
// Brand Colors - Microsoft Teams Purple
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -53,6 +54,7 @@ export default function TeamsGenerator() {
|
|||||||
const [userEmail, setUserEmail] = useState('');
|
const [userEmail, setUserEmail] = useState('');
|
||||||
const [qrColor, setQrColor] = useState(BRAND.primary);
|
const [qrColor, setQrColor] = useState(BRAND.primary);
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -96,6 +98,7 @@ export default function TeamsGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
@@ -104,6 +107,8 @@ export default function TeamsGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -314,5 +319,6 @@ export default function TeamsGenerator() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
@@ -45,6 +46,7 @@ export default function TextGenerator() {
|
|||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
const [qrColor, setQrColor] = useState(BRAND.richBlue);
|
const [qrColor, setQrColor] = useState(BRAND.richBlue);
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ export default function TextGenerator() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
|
if (shouldShowDownloadPopup()) setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
@@ -80,6 +83,8 @@ export default function TextGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
@@ -241,5 +246,6 @@ export default function TextGenerator() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||