26 Commits

Author SHA1 Message Date
Timo Knuth
0866c200a0 Weekly SEO 2026-06-08 20:33:38 +02:00
Timo Knuth
a7cbbee084 SEO 2026-05-27 20:37:15 +02:00
Timo Knuth
09f5859af2 Product hunt launch 2026-05-27 14:33:58 +02:00
Timo Knuth
4774f4d51e SEO 2026-05-18 16:00:24 +02:00
Timo Knuth
81d1fdd280 weekly seo 2026-05-11 11:10:30 +02:00
Timo Knuth
35e7e77f6b GSC seo 2026-05-10 23:00:06 +02:00
Timo Knuth
8741edc362 /restaurants 2026-04-30 16:59:27 +02:00
Timo Knuth
152758db92 Fehler 2026-04-29 23:53:47 +02:00
Timo Knuth
105857c348 Remove impeccable A/B variants from Hero -- keep only FlippingCards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 23:51:57 +02:00
Timo Knuth
aab808c553 Impeccable 2026-04-29 20:34:09 +02:00
Timo Knuth
9b31e77daa mehr SEO 2026-04-27 22:21:52 +02:00
Timo Knuth
c4fac0f726 SEO + Stripe 2026-04-27 17:10:30 +02:00
Timo Knuth
11159eb02b stripe promo code 2026-04-27 11:42:09 +02:00
Timo Knuth
c6f20f7f0b Onboarding verbessern 2026-04-23 19:24:33 +02:00
Timo Knuth
eacaef1fbd Refine onboarding UI and fix dashboard checklist progress 2026-04-23 19:03:41 +02:00
fc0e6a0a69 npm run build 2026-04-23 14:52:13 +02:00
Timo Knuth
c7d5f281c5 Fix build issues for meta imports and WSL filesystem 2026-04-23 11:50:09 +02:00
Timo Knuth
6e68408391 Add revops onboarding SQL migration 2026-04-22 22:29:36 +02:00
Timo Knuth
7d2724b65d revops + onboarding 2026-04-22 20:01:46 +02:00
ce724662d4 external 2026-04-21 15:38:49 -05:00
7a7b197a67 Merge branch 'master' of git.bizmatch.net:tknuth/QR-master 2026-04-21 15:29:59 -05:00
ef22e72a82 external: true 2026-04-21 15:29:43 -05:00
Timo Knuth
32935041b3 add 2026-04-21 12:37:18 +02:00
Timo Knuth
aa2628834b Barcode fix 2026-04-17 23:24:22 +02:00
Timo Knuth
5894f4619d Barcode workflow 2026-04-17 22:56:49 +02:00
Timo Knuth
56d63a0146 chore: add gstack skill routing rules to CLAUDE.md 2026-04-17 14:14:07 +02:00
153 changed files with 53131 additions and 10256 deletions

View File

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

15
.claude/settings.json Normal file
View File

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

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

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

View File

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

33
.gitignore vendored
View File

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

1
.impeccable-live.json Normal file
View File

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

View File

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

View File

@@ -273,9 +273,47 @@ docker-compose exec web npx prisma migrate deploy # Run migrations in container
- Requires external PostgreSQL database (Vercel Postgres, Supabase, etc.)
- Redis is optional
## Additional Resources
- README.md - Detailed setup and feature overview
- DOCKER_SETUP.md - Complete Docker deployment guide
- prisma/schema.prisma - Database schema and relationships
- env.example - Environment variable template
## Additional Resources
- README.md - Detailed setup and feature overview
- DOCKER_SETUP.md - Complete Docker deployment guide
- prisma/schema.prisma - Database schema and relationships
- 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

View File

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

51
PRODUCT.md Normal file
View File

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

View File

@@ -93,6 +93,7 @@ services:
retries: 10
networks:
- qrmaster-network
# Adminer - Database Management UI (Optional)
adminer:
@@ -118,4 +119,4 @@ volumes:
networks:
qrmaster-network:
driver: bridge
external: true

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

24
memory/project_summary.md Normal file
View File

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

View File

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

23
meta-fix.js Normal file
View File

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

View File

@@ -1,10 +1,24 @@
import os from 'os';
import path from 'path';
function isWslOnWindowsMount() {
return process.platform === 'linux' && process.cwd().startsWith('/mnt/');
}
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
skipTrailingSlashRedirect: true,
eslint: {
ignoreDuringBuilds: true,
},
images: {
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'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
@@ -20,6 +34,16 @@ const nextConfig = {
pagesBufferLength: 2,
},
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() {
return [
{
@@ -47,16 +71,6 @@ const nextConfig = {
destination: '/tools/call-qr-code-generator',
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',
destination: '/qr-code-for/breweries',

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -11,11 +11,11 @@ datasource db {
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
password String?
model User {
id String @id @default(cuid())
email String @unique
name String?
password String?
image String?
emailVerified DateTime?
createdAt DateTime @default(now())
@@ -32,16 +32,56 @@ model User {
resetPasswordToken String? @unique
resetPasswordExpires DateTime?
// Retention email tracking
activationNudgeSentAt DateTime?
upgradeNudgeSentAt DateTime?
thirtyDayNudgeSentAt DateTime?
qrCodes QRCode[]
integrations Integration[]
accounts Account[]
sessions Session[]
}
// Retention email tracking
activationNudgeSentAt DateTime?
upgradeNudgeSentAt 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[]
integrations Integration[]
accounts Account[]
sessions Session[]
lifecycleLogs UserLifecycleLog[]
}
enum Plan {
FREE
@@ -149,7 +189,7 @@ model QRScan {
@@index([qrId, ts])
}
model Integration {
model Integration {
id String @id @default(cuid())
userId String
provider String
@@ -158,8 +198,22 @@ model Integration {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model UserLifecycleLog {
id String @id @default(cuid())
userId String
fromStage String?
toStage String
fitScore Int @default(0)
intentScore Int @default(0)
leadScore Int @default(0)
reason String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model NewsletterSubscription {
id String @id @default(cuid())

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

BIN
quora_suche.txt Normal file

Binary file not shown.

49
read-inbox.mjs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -285,6 +285,7 @@ export default function BulkCreationPage() {
};
const saveQRCodesToDatabase = async () => {
if (isDynamic) return; // dynamic codes are already saved during generation
setLoading(true);
try {
@@ -817,12 +818,14 @@ export default function BulkCreationPage() {
</svg>
Download All as ZIP
</Button>
<Button onClick={saveQRCodesToDatabase} loading={loading}>
<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" />
</svg>
Save QR Codes
</Button>
{!isDynamic && (
<Button onClick={saveQRCodesToDatabase} loading={loading}>
<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" />
</svg>
Save QR Codes
</Button>
)}
</div>
</CardContent>
</Card>

View File

@@ -1,8 +1,8 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import React, { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { QRCodeSVG } from 'qrcode.react';
import { toPng } from 'html-to-image';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
@@ -11,12 +11,18 @@ import { Select } from '@/components/ui/Select';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { calculateContrast, cn } from '@/lib/utils';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
import { showToast } from '@/components/ui/Toast';
import {
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload, Barcode as BarcodeIcon
} from 'lucide-react';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
import { showToast } from '@/components/ui/Toast';
import { trackEvent } from '@/components/PostHogProvider';
import { appendRedirectParam, sanitizeRedirectPath } from '@/lib/auth-flow';
import {
ONBOARDING_DOWNLOAD_COMPLETE_EVENT,
ONBOARDING_DOWNLOAD_COMPLETE_KEY,
} from '@/lib/revops';
import {
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload, Barcode as BarcodeIcon
} from 'lucide-react';
import Barcode from 'react-barcode';
// Tooltip component for form field help
@@ -62,9 +68,47 @@ const getFrameOptionsForContentType = (contentType: string) => {
}
};
export default function CreatePage() {
const router = useRouter();
const { t } = useTranslation();
// 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() {
const router = useRouter();
const searchParams = useSearchParams();
const { t } = useTranslation();
const { fetchWithCsrf } = useCsrf();
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
@@ -108,14 +152,23 @@ export default function CreatePage() {
const [excavate, setExcavate] = useState(true);
// 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)
const canCustomizeColors = userPlan === 'PRO' || userPlan === 'BUSINESS';
// Load user plan
useEffect(() => {
const fetchUserPlan = async () => {
useEffect(() => {
const fetchUserPlan = async () => {
try {
const response = await fetch('/api/user/plan');
if (response.ok) {
@@ -126,8 +179,44 @@ export default function CreatePage() {
console.error('Error fetching user plan:', error);
}
};
fetchUserPlan();
}, []);
fetchUserPlan();
}, []);
useEffect(() => {
const queryContentType = searchParams.get('contentType');
const useCase = searchParams.get('useCase');
const titleParam = searchParams.get('title');
const isDynamicParam = searchParams.get('dynamic');
if (queryContentType) {
setContentType(queryContentType);
}
if (titleParam) {
setTitle(titleParam);
}
if (isDynamicParam) {
setIsDynamic(isDynamicParam === '1');
}
if (useCase === 'menu_pdf') {
setContent((prev: any) => ({ ...prev, fileUrl: prev.fileUrl || '' }));
} else if (useCase === 'contact_card') {
setContent((prev: any) => ({
...prev,
firstName: prev.firstName || '',
lastName: prev.lastName || '',
}));
} else if (useCase === 'barcode') {
setContent((prev: any) => ({
...prev,
format: prev.format || 'CODE128',
}));
} else if (queryContentType === 'URL') {
setContent((prev: any) => ({ ...prev, url: prev.url || '' }));
}
}, [searchParams]);
const contrast = calculateContrast(foregroundColor, backgroundColor);
const hasGoodContrast = contrast >= 4.5;
@@ -173,13 +262,14 @@ export default function CreatePage() {
case 'FEEDBACK':
return content.feedbackUrl || 'https://example.com/feedback';
case 'BARCODE':
return content.value || '';
return isDynamic ? (content.url || '') : (content.value || '');
default:
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 frame = frameOptions.find((f: { id: string; label: string }) => f.id === frameType);
@@ -189,13 +279,20 @@ export default function CreatePage() {
const downloadQR = async (format: 'svg' | 'png') => {
if (!qrRef.current) return;
try {
if (format === 'png') {
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' });
const link = document.createElement('a');
link.download = `qrcode-${title || 'download'}.png`;
link.href = dataUrl;
link.click();
} else {
if (format === 'png') {
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' });
const link = document.createElement('a');
link.download = `qrcode-${title || 'download'}.png`;
link.href = dataUrl;
link.click();
markDownloadComplete();
trackEvent('qr_code_downloaded', {
format: 'png',
content_type: contentType,
qr_type: isDynamic ? 'dynamic' : 'static',
plan: userPlan,
});
} else {
// For SVG, we might still want to use the library or just toPng if SVG export of HTML is not needed
// Simplest is to check if we can export the SVG element directly but that misses the frame HTML.
// html-to-image can generate SVG too.
@@ -212,24 +309,41 @@ export default function CreatePage() {
if (frameType === 'none') {
const svgElement = qrRef.current.querySelector('svg');
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 url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `qrcode-${title || 'download'}.svg`;
a.click();
URL.revokeObjectURL(url);
}
} else {
showToast('SVG download not available with frames yet. Downloading PNG instead.', 'info');
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' });
const link = document.createElement('a');
link.download = `qrcode-${title || 'download'}.png`;
link.href = dataUrl;
link.click();
}
}
const a = document.createElement('a');
a.href = url;
a.download = `qrcode-${title || 'download'}.svg`;
a.click();
URL.revokeObjectURL(url);
markDownloadComplete();
trackEvent('qr_code_downloaded', {
format: 'svg',
content_type: contentType,
qr_type: isDynamic ? 'dynamic' : 'static',
plan: userPlan,
});
}
} else {
showToast('SVG download not available with frames yet. Downloading PNG instead.', 'info');
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' });
const link = document.createElement('a');
link.download = `qrcode-${title || 'download'}.png`;
link.href = dataUrl;
link.click();
markDownloadComplete();
trackEvent('qr_code_downloaded', {
format: 'png',
content_type: contentType,
qr_type: isDynamic ? 'dynamic' : 'static',
plan: userPlan,
fallback_from: 'svg_with_frame',
});
}
}
} catch (err) {
console.error('Error downloading QR code:', err);
showToast('Error downloading QR code', 'error');
@@ -315,18 +429,38 @@ export default function CreatePage() {
const responseData = await response.json();
console.log('RESPONSE DATA:', responseData);
if (response.ok) {
showToast(`QR Code "${title}" created successfully!`, 'success');
// Wait a moment so user sees the toast, then redirect
setTimeout(() => {
router.push('/dashboard');
router.refresh();
}, 1000);
} else {
console.error('Error creating QR code:', responseData);
showToast(responseData.error || 'Error creating QR code', 'error');
}
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');
// Wait a moment so user sees the toast, then redirect
setTimeout(() => {
const redirectTarget = sanitizeRedirectPath(searchParams.get('redirect'));
if (searchParams.get('onboarding') === '1') {
router.push(appendRedirectParam('/onboarding', redirectTarget, { step: '8' }));
} else {
router.push('/dashboard');
}
router.refresh();
}, 1000);
} else {
console.error('Error creating QR code:', responseData);
if (response.status === 403 && responseData.error === 'Limit reached') {
showToast(responseData.message || 'You have reached your plan limit.', 'error');
router.push('/pricing?reason=limit_reached');
return;
}
showToast(responseData.error || 'Error creating QR code', 'error');
}
} catch (error) {
console.error('Error creating QR code:', error);
showToast('Error creating QR code. Please try again.', 'error');
@@ -651,11 +785,16 @@ export default function CreatePage() {
<>
{isDynamic ? (
<>
<div className="rounded-lg bg-blue-50 border border-blue-200 p-3 text-sm text-blue-800">
<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>). When scanned with a
smartphone camera, it opens the browser and redirects to your destination which you
can update anytime. Works with smartphone cameras, not POS laser scanners.
<div className="rounded-lg bg-blue-50 border border-blue-200 p-3 text-sm text-blue-800 space-y-2">
<p>
<strong>How dynamic barcodes work:</strong> The barcode encodes a short redirect URL
(e.g. <span className="font-mono text-xs">qrmaster.net/r/</span>) that you can update anytime.
</p>
<p className="rounded border border-amber-300 bg-amber-50 p-2 text-xs text-amber-900">
<strong>📱 Scanner tip:</strong> Use a <strong>barcode scanner app</strong> on iPhone
(iOS Camera doesn't auto-open links from barcodes). Android Google Lens / Camera works
out of the box. Print min. 5&nbsp;cm wide for reliable scanning.
</p>
</div>
<Input
label="Destination URL"
@@ -865,49 +1004,51 @@ export default function CreatePage() {
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Foreground Color
</label>
<div className="flex items-center space-x-2">
<input
type="color"
value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)}
className="w-12 h-10 rounded border border-gray-300"
disabled={!canCustomizeColors}
/>
<Input
value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)}
className="flex-1"
disabled={!canCustomizeColors}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Background Color
</label>
<div className="flex items-center space-x-2">
<input
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="w-12 h-10 rounded border border-gray-300"
disabled={!canCustomizeColors}
/>
<Input
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="flex-1"
disabled={!canCustomizeColors}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Foreground Color
</label>
<div className="flex items-center space-x-2">
<input
type="color"
value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)}
className="w-12 h-10 rounded border border-gray-300"
disabled={!canCustomizeColors}
/>
<Input
value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)}
className="flex-1"
disabled={!canCustomizeColors}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Background Color
</label>
<div className="flex items-center space-x-2">
<input
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="w-12 h-10 rounded border border-gray-300"
disabled={!canCustomizeColors}
/>
<Input
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="flex-1"
disabled={!canCustomizeColors}
/>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<Select
@@ -1038,16 +1179,15 @@ export default function CreatePage() {
<CardTitle>{t('create.preview')}</CardTitle>
</CardHeader>
<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 */}
<div
ref={qrRef}
className="relative bg-white rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300"
style={{
minWidth: '280px',
minHeight: '280px',
}}
>
<div
ref={qrRef}
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={{
minHeight: '220px',
}}
>
{/* Frame Label */}
{getFrameLabel() && (
<div
@@ -1060,7 +1200,7 @@ export default function CreatePage() {
{contentType === 'BARCODE' ? (
qrContent ? (
<div className="p-2 bg-white">
<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}
@@ -1068,20 +1208,31 @@ export default function CreatePage() {
lineColor={foregroundColor}
background={backgroundColor}
width={2}
height={100}
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' ? 'rounded-lg overflow-hidden' : ''}>
<QRCodeSVG
value={qrContent}
size={size}
) : qrContent ? (
<div
className={cornerStyle === 'rounded' ? 'overflow-hidden rounded-lg' : ''}
style={{
transform: `scale(${previewScale})`,
transformOrigin: 'center center',
}}
>
<QRCodeSVG
value={qrContent}
size={size}
fgColor={foregroundColor}
bgColor={backgroundColor}
level="H"
@@ -1132,4 +1283,4 @@ export default function CreatePage() {
</form>
</div>
);
}
}

View File

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

View File

@@ -5,9 +5,10 @@ import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Card, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
import { appendRedirectParam, sanitizeRedirectPath } from '@/lib/auth-flow';
type LoginClientProps = {
showPageHeading?: boolean;
@@ -20,9 +21,10 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const redirectTarget = sanitizeRedirectPath(searchParams.get('redirect'));
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -57,10 +59,12 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
console.error('PostHog tracking error:', error);
}
// Check for redirect parameter
const redirectUrl = searchParams.get('redirect') || '/dashboard';
router.push(redirectUrl);
router.refresh();
// Check for redirect parameter
const redirectUrl = data.needsOnboarding
? appendRedirectParam('/onboarding', redirectTarget)
: (redirectTarget || '/dashboard');
router.push(redirectUrl);
router.refresh();
} else {
setError(data.error || 'Invalid email or password');
}
@@ -71,10 +75,10 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
}
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = '/api/auth/google';
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = appendRedirectParam('/api/auth/google', redirectTarget);
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
@@ -199,9 +203,9 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
Don't have an account?{' '}
<Link href="/signup" className="text-primary-600 hover:text-primary-700 font-medium">
Sign up
</Link>
<Link href={appendRedirectParam('/signup', redirectTarget)} className="text-primary-600 hover:text-primary-700 font-medium">
Sign up
</Link>
</p>
</div>
</CardContent>

View File

@@ -1,26 +1,29 @@
'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
export default function SignupClient() {
const router = useRouter();
const { t } = useTranslation();
const { fetchWithCsrf } = useCsrf();
import React, { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
import { appendRedirectParam, sanitizeRedirectPath } from '@/lib/auth-flow';
export default function SignupClient() {
const router = useRouter();
const searchParams = useSearchParams();
const { t } = useTranslation();
const { fetchWithCsrf } = useCsrf();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
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) => {
e.preventDefault();
@@ -68,9 +71,9 @@ export default function SignupClient() {
console.error('PostHog tracking error:', error);
}
// Redirect to dashboard
router.push('/dashboard');
router.refresh();
// Redirect to onboarding
router.push(appendRedirectParam('/onboarding', redirectTarget));
router.refresh();
} else {
setError(data.error || 'Failed to create account');
}
@@ -81,10 +84,10 @@ export default function SignupClient() {
}
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = '/api/auth/google';
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = appendRedirectParam('/api/auth/google', redirectTarget);
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
@@ -234,11 +237,11 @@ export default function SignupClient() {
</form>
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
Already have an account?{' '}
<Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium">
Sign in
</Link>
<p className="text-sm text-gray-600">
Already have an account?{' '}
<Link href={appendRedirectParam('/login', redirectTarget)} className="text-primary-600 hover:text-primary-700 font-medium">
Sign in
</Link>
</p>
</div>
</CardContent>

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ const competitor = competitors['bitly'];
export const metadata: Metadata = {
title: {
absolute: 'Bitly QR Code Alternative Purpose-Built for QR Campaigns | QR Master',
absolute: 'QR Master vs Bitly QR Codes | Bitly Alternative',
},
description:
'Looking for a Bitly alternative for QR codes? Bitly\'s Core plan costs $10/month but only allows 2 QR codes total. QR Master is purpose-built for QR code management — 50 codes at €9/month, bulk creation, GDPR analytics. From €0.',
@@ -24,7 +24,7 @@ export const metadata: Metadata = {
canonical: 'https://www.qrmaster.net/alternatives/bitly',
},
openGraph: {
title: 'Bitly QR Code Alternative Purpose-Built for QR Campaigns',
title: 'QR Master vs Bitly QR Codes | Bitly Alternative',
description:
'Bitly\'s Core plan costs $10/month but only gives you 2 QR codes. QR Master gives you 50 dynamic QR codes at €9/month — purpose-built for QR workflows, bulk creation, and GDPR analytics.',
url: 'https://www.qrmaster.net/alternatives/bitly',
@@ -32,7 +32,7 @@ export const metadata: Metadata = {
images: ['/og-image.png'],
},
twitter: {
title: 'Bitly QR Code Alternative Purpose-Built for QR Campaigns',
title: 'QR Master vs Bitly QR Codes | Bitly Alternative',
description:
'Bitly gives you 2 QR codes for $10/month. QR Master gives you 50 at €9/month — purpose-built for real QR campaigns, not link shortening with QR as an afterthought.',
},
@@ -40,6 +40,34 @@ export const metadata: Metadata = {
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 = [
{
question: 'How many QR codes does Bitly allow per plan?',
@@ -256,6 +284,41 @@ export default function BitlyAlternativePage() {
</div>
</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 */}
<section className="py-24" style={{ backgroundColor: '#F8F7F4' }}>
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">

View File

@@ -14,7 +14,7 @@ const competitor = competitors['flowcode'];
export const metadata: Metadata = {
title: {
absolute: 'Flowcode Alternative No Forced Branding or Scan Interstitials | QR Master',
absolute: 'QR Master vs Flowcode | Flowcode Alternative Without Forced Branding',
},
description:
'Looking for a Flowcode alternative? QR Master gives you clean, customizable QR codes without Flowcode\'s logo or scan-hijacking interstitial pages — from €0 free, Pro at €9/month.',
@@ -24,7 +24,7 @@ export const metadata: Metadata = {
canonical: 'https://www.qrmaster.net/alternatives/flowcode',
},
openGraph: {
title: 'Flowcode Alternative No Forced Branding or Scan Interstitials',
title: 'QR Master vs Flowcode | Flowcode Alternative Without Forced Branding',
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.',
url: 'https://www.qrmaster.net/alternatives/flowcode',
@@ -32,7 +32,7 @@ export const metadata: Metadata = {
images: ['/og-image.png'],
},
twitter: {
title: 'Flowcode Alternative No Forced Branding or Scan Interstitials',
title: 'QR Master vs Flowcode | Flowcode Alternative Without Forced Branding',
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.',
},
@@ -40,6 +40,34 @@ export const metadata: Metadata = {
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 = [
{
question: 'What is the Flowcode interstitial page and why does it matter?',
@@ -238,6 +266,41 @@ export default function FlowcodeAlternativePage() {
</div>
</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 */}
<section className="py-24" style={{ backgroundColor: '#F8F7F4' }}>
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">

View File

@@ -4,13 +4,14 @@ import Link from 'next/link';
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import SeoJsonLd from '@/components/SeoJsonLd';
import { breadcrumbSchema } from '@/lib/schema';
import { FAQSection } from '@/components/aeo/FAQSection';
import { MarketingPageTracker, TrackedCtaLink } from '@/components/marketing/MarketingAnalytics';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
export const metadata: Metadata = {
title: {
absolute: 'QR Code Platform Alternatives | QR Master',
absolute: 'QR Master Alternatives: Compare QR Code Platforms',
},
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.',
@@ -18,7 +19,7 @@ export const metadata: Metadata = {
canonical: 'https://www.qrmaster.net/alternatives',
},
openGraph: {
title: 'QR Code Platform Alternatives | QR Master',
title: 'QR Master Alternatives: Compare QR Code Platforms',
description:
'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',
@@ -26,7 +27,7 @@ export const metadata: Metadata = {
images: ['/og-image.png'],
},
twitter: {
title: 'QR Code Platform Alternatives | QR Master',
title: 'QR Master Alternatives: Compare QR Code Platforms',
description:
'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.',
};
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 = [
{
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() {
return (
<>
<SeoJsonLd data={[pageSchema, breadcrumbSchema(breadcrumbItems)]} />
<SeoJsonLd data={[pageSchema, faqSchema, breadcrumbSchema(breadcrumbItems)]} />
<MarketingPageTracker pageType="commercial" cluster="competitor" />
<div className="min-h-screen" style={{ backgroundColor: '#F8F7F4', color: '#18181B' }}>
<section className="border-b bg-white" style={{ borderColor: '#E4E0D9' }}>
@@ -175,6 +264,79 @@ export default function AlternativesHubPage() {
</div>
</div>
</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>
</>
);

View File

@@ -12,13 +12,13 @@ import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
import { MarketingPageTracker } from '@/components/marketing/MarketingAnalytics';
export const metadata: Metadata = {
title: {
absolute: 'Bulk QR Code Generator - Create QR Codes from a Spreadsheet',
},
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.',
keywords:
'bulk qr code generator, qr code from excel, csv qr code generator, bulk qr codes, spreadsheet qr generation',
title: {
absolute: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
},
description:
'Generate up to 1,000 QR codes from Excel, CSV, XLSX, or exported Google Sheets data. Upload, preview, batch-create, download ZIP files, or save to your dashboard.',
keywords:
'bulk qr code generator, bulk qr code generator excel, batch qr code generator, qr code from excel, csv qr code generator, bulk qr generator, bulk qr code generator in google sheets, spreadsheet qr generation',
alternates: {
canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
languages: {
@@ -27,36 +27,41 @@ export const metadata: Metadata = {
},
},
openGraph: {
title: 'Bulk QR Code Generator - Create QR Codes from a Spreadsheet',
title: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
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',
type: 'website',
images: ['/og-image.png'],
},
twitter: {
title: 'Bulk QR Code Generator - Create QR Codes from a Spreadsheet',
title: 'Bulk QR Code Generator for Excel, CSV and Google Sheets',
description:
'Generate up to 1,000 static QR codes from CSV or Excel files in the QR Master Business plan.',
'Generate up to 1,000 QR codes from CSV, Excel, XLSX, or exported Google Sheets data.',
},
};
const featureCards = [
{
title: 'Spreadsheet upload',
description:
'Upload CSV, XLS, or XLSX files and map the title and content columns before generating the batch.',
},
{
title: 'Spreadsheet upload',
description:
'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',
description:
'The current bulk creation flow limits each upload to 1,000 rows so the batch stays predictable and reviewable.',
},
{
title: 'Static QR output',
{
title: 'Static QR output',
description:
'Bulk creation currently generates static QR codes. These codes do not include post-print editing or tracking.',
},
},
{
title: 'ZIP download',
description:
@@ -67,16 +72,26 @@ const featureCards = [
description:
'After generation, you can save the batch into your QR Master dashboard for later management.',
},
{
title: 'Business plan access',
{
title: 'Business plan access',
description:
'The current bulk creation workflow is available to Business plan subscribers and is positioned around scale, not single-code creation.',
},
];
},
];
const inputExamples = [
{
title: 'Website URLs',
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',
content: 'https://example.com/product',
note: 'Useful for product pages, flyers, support pages, or campaign destinations.',
},
@@ -135,11 +150,21 @@ const faqItems = [
answer:
'No. The current bulk creation flow generates static QR codes, so those codes do not include post-print editing or tracking.',
},
{
question: 'What file formats can I upload?',
answer:
'The current flow accepts CSV, XLS, and XLSX files.',
},
{
question: 'What file formats can I upload?',
answer:
'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?',
answer:
@@ -160,11 +185,12 @@ const softwareSchema = {
priceCurrency: 'EUR',
availability: 'https://schema.org/InStock',
},
description:
'Generate up to 1,000 static QR codes from CSV or Excel files in the QR Master Business plan.',
description:
'Generate up to 1,000 static QR codes from CSV, Excel, XLSX, or exported Google Sheets files in the QR Master Business plan.',
featureList: [
'CSV, XLS, and XLSX upload',
'Up to 1,000 rows per upload',
'CSV, XLS, and XLSX upload',
'Excel and Google Sheets CSV export workflow',
'Up to 1,000 rows per upload',
'Static QR code generation',
'ZIP download of generated SVG files',
'Optional save-to-dashboard step',
@@ -393,9 +419,52 @@ export default function BulkQRCodeGeneratorPage() {
</p>
</div>
<div className="container mx-auto max-w-5xl px-4 pb-8 sm:px-6 lg:px-8">
<FAQSection items={faqItems} title="Bulk QR questions" />
</div>
<section className="bg-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">
<FAQSection items={faqItems} title="Bulk QR questions" />
</div>
<section className="bg-gray-50 py-20">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">

File diff suppressed because it is too large Load Diff

View File

@@ -14,9 +14,9 @@ function truncateAtWord(text: string, maxLength: number): string {
}
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(
'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
);
@@ -58,6 +58,41 @@ type 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?',
answer:
@@ -210,7 +245,7 @@ export default function FAQPage() {
<p className="mb-4 text-xl text-gray-600">
Answers about dynamic QR codes, scan tracking, privacy, bulk creation, and print setup.
</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 className="space-y-6">

View File

@@ -19,10 +19,11 @@ export const metadata: Metadata = {
: { index: false, follow: false },
icons: {
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',
apple: '/favicon1.png',
shortcut: '/favicon.ico',
apple: '/logo.svg',
},
twitter: {
card: 'summary_large_image',

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -8,6 +8,8 @@ import { showToast } from '@/components/ui/Toast';
import { useRouter } from 'next/navigation';
import { BillingToggle } from '@/components/ui/BillingToggle';
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
import { trackEvent } from '@/components/PostHogProvider';
import { FREE_DYNAMIC_QR_LIMIT } from '@/lib/plans';
export default function PricingPage() {
const router = useRouter();
@@ -40,6 +42,13 @@ export default function PricingPage() {
setLoading(plan);
try {
trackEvent('upgrade_clicked', {
plan,
billing_interval: billingPeriod,
source: 'pricing_page',
current_plan: currentPlan,
});
const response = await fetch('/api/stripe/create-checkout-session', {
method: 'POST',
headers: {
@@ -52,14 +61,15 @@ export default function PricingPage() {
});
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();
window.location.href = url;
} catch (error) {
} catch (error: any) {
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);
}
};
@@ -132,7 +142,7 @@ export default function PricingPage() {
period: 'forever',
showDiscount: false,
features: [
'3 active dynamic QR codes (8 types available)',
`${FREE_DYNAMIC_QR_LIMIT} active dynamic QR codes (8 types available)`,
'Unlimited static QR codes',
'Basic scan tracking',
'Standard QR design templates',

View File

@@ -1,179 +1,227 @@
import type { Metadata } from "next";
import {
buildUseCaseMetadata,
UseCasePageTemplate,
} from "@/components/marketing/UseCasePageTemplate";
import { GrowthLinksSection } from "@/components/marketing/GrowthLinksSection";
export const metadata: Metadata = buildUseCaseMetadata({
title: "QR Code Analytics",
description:
"Measure QR code scans by placement, timing, device context, and campaign route so offline workflows become reportable.",
canonicalPath: "/qr-code-analytics",
});
const softwareSchema = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"@id": "https://www.qrmaster.net/qr-code-analytics#software",
name: "QR Master - QR Code Analytics",
applicationCategory: "BusinessApplication",
operatingSystem: "Web Browser",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD",
availability: "https://schema.org/InStock",
},
description:
"QR analytics software for measuring scans by placement, timing, device context, and offline campaign routing.",
featureList: [
"Placement-level scan reporting",
"Device and timing context",
"Offline-to-online campaign attribution",
"Scan visibility across print workflows",
"Destination updates without reprinting",
],
};
export default function QRCodeAnalyticsPage() {
return (
<>
<UseCasePageTemplate
title="QR Code Analytics"
description="Measure QR code scans by placement, timing, device context, and campaign route so offline workflows become reportable."
eyebrow="Analytics"
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."
pageType="commercial"
cluster="qr-analytics"
useCase="qr-analytics"
breadcrumbs={[
{ name: "Home", url: "/" },
{ 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."
whenToUse={[
"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 need a clearer bridge between QR scans and business outcomes such as signup, offers, or support engagement.",
]}
comparisonItems={[
{ label: "Placement visibility", text: "Usually blended", value: true },
{ label: "Post-print reporting", text: "Weak", value: true },
{ label: "Campaign comparison", text: "Manual or partial", value: true },
]}
howToSteps={[
"Create QR flows that map to real placements or workflow contexts instead of one generic code for every use case.",
"Track scans with enough context to compare signs, flyers, inserts, or support surfaces cleanly.",
"Use the reporting to decide which destinations, offers, or print placements deserve the next round of investment.",
]}
primaryCta={{
href: "/signup",
label: "Start measuring QR scans",
}}
secondaryCta={{
href: "/use-cases",
label: "Browse measured workflows",
}}
workflowTitle="What useful QR analytics should help you 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."
workflowCards={[
{
title: "Placement comparison",
description:
"Separate flyer, packaging, sign, event, or service-surface traffic so you know which printed context actually creates useful scan activity.",
},
{
title: "Post-print flexibility",
description:
"Review performance and then improve the destination, offer, or next action without replacing every physical code already in circulation.",
},
{
title: "Operational reporting",
description:
"Give marketing, operations, or support teams a better view of what physical QR programs are doing once they are live in the field.",
},
]}
checklistTitle="QR analytics checklist"
checklist={[
"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.",
"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.",
]}
supportLinks={[
{
href: "/use-cases/packaging-qr-codes",
title: "Use case: Packaging QR Codes",
description:
"See how packaging scans can become a measurable post-purchase signal instead of a blind spot.",
},
{
href: "/use-cases/flyer-qr-codes",
title: "Use case: Flyer QR Codes",
description:
"Useful when scan performance needs to be reviewed by distribution point or campaign wave.",
},
{
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={[
{
question: "What can QR code analytics show?",
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.",
},
{
question: "Why are QR code analytics useful for offline campaigns?",
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.",
},
{
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]}
/>
<GrowthLinksSection
eyebrow="Related workflows"
title="Analytics is only the start"
description="Tracking scans is more useful when it connects to destination flexibility, campaign comparison, and cost planning."
links={[
{
href: '/qr-code-tracking',
title: 'QR Code Tracking',
description: 'See device, time, and location context for every scan. Understand which placements drive real activity.',
ctaLabel: 'Track QR code scans',
},
{
href: '/dynamic-qr-code-generator',
title: 'Dynamic QR Code Generator',
description: 'Create QR codes with updatable destinations so analytics can inform what to change — without reprinting.',
ctaLabel: 'Create dynamic QR code',
},
{
href: '/reprint-calculator',
title: 'Reprint Cost Calculator',
description: 'Calculate how much static reprints are costing vs one dynamic QR subscription.',
ctaLabel: 'Calculate reprint savings',
},
{
href: '/pricing',
title: 'Compare Plans',
description: 'See which plan gives you the scan volume, analytics depth, and QR code count your workflows need.',
ctaLabel: 'Compare plans',
},
]}
pageType="commercial"
cluster="qr-analytics"
/>
</>
);
}
import type { Metadata } from 'next';
import {
buildUseCaseMetadata,
UseCasePageTemplate,
} from '@/components/marketing/UseCasePageTemplate';
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
export const metadata: Metadata = buildUseCaseMetadata({
title: 'QR Code Analytics: Measure Offline Campaigns',
description:
'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',
});
const softwareSchema = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
'@id': 'https://www.qrmaster.net/qr-code-analytics#software',
name: 'QR Master - QR Code Analytics',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
availability: 'https://schema.org/InStock',
},
description:
'QR analytics software for measuring scans by placement, timing, device context, and offline campaign routing.',
featureList: [
'Placement-level scan reporting',
'Device and timing context',
'Offline-to-online campaign attribution',
'Scan visibility across print workflows',
'Destination updates without reprinting',
],
};
export default function QRCodeAnalyticsPage() {
return (
<>
<UseCasePageTemplate
title="QR Code Analytics: Measure Offline Campaigns"
description="Use QR code analytics to interpret scan data, compare placements, measure offline campaigns, and decide what to update or reprint next."
eyebrow="Analytics interpretation"
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"
cluster="qr-analytics"
useCase="qr-analytics"
breadcrumbs={[
{ name: 'Home', url: '/' },
{ name: 'QR Code Analytics', url: '/qr-code-analytics' },
]}
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={[
'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 need a clearer bridge between QR scans and business outcomes such as signup, offers, or support engagement.',
]}
comparisonItems={[
{
label: 'Tracking collects scan events',
text: 'Input data',
value: true,
},
{
label: 'Analytics explains performance',
text: 'Decision layer',
value: true,
},
{
label: 'Reprint decisions',
text: 'Based on evidence',
value: true,
},
]}
howToSteps={[
'Create QR flows that map to real placements or workflow contexts instead of one generic code for every use case.',
'Track scans with enough context to compare signs, flyers, inserts, or support surfaces cleanly.',
'Use the reporting to decide which destinations, offers, or print placements deserve the next round of investment.',
]}
primaryCta={{
href: '/signup',
label: 'Start measuring QR scans',
}}
secondaryCta={{
href: '/use-cases',
label: 'Browse measured workflows',
}}
workflowTitle="Questions QR analytics should answer"
workflowIntro="The point of analytics is not to produce dashboards for their own sake. It is to decide what to print again, where to place it, and what should happen after the scan."
workflowCards={[
{
title: 'Which placement worked?',
description:
'Separate flyer, packaging, sign, event, or service-surface traffic so you know which printed context actually creates useful scan activity.',
},
{
title: 'What should change next?',
description:
'Review performance and then improve the destination, offer, or next action without replacing every physical code already in circulation.',
},
{
title: 'What should be reprinted?',
description:
'Give marketing, operations, or support teams a clearer view of which physical QR programs deserve another batch.',
},
]}
checklistTitle="QR analytics checklist"
checklist={[
'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.',
'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.',
]}
supportLinks={[
{
href: '/use-cases/packaging-qr-codes',
title: 'Use case: Packaging QR Codes',
description:
'See how packaging scans can become a measurable post-purchase signal instead of a blind spot.',
},
{
href: '/qr-code-for-marketing-campaigns',
title: 'QR Codes for Marketing Campaigns',
description:
'Plan campaign QR workflows around attribution, creative testing, and print distribution.',
},
{
href: '/use-cases/flyer-qr-codes',
title: 'Use case: Flyer QR Codes',
description:
'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={[
{
question: 'What can QR code analytics show?',
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.',
},
{
question:
'What is the difference between QR tracking and QR analytics?',
answer:
'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: 'Why are QR code analytics useful for offline campaigns?',
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.',
},
{
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]}
/>
<GrowthLinksSection
eyebrow="Related workflows"
title="Analytics is only the start"
description="Tracking scans is more useful when it connects to destination flexibility, campaign comparison, and cost planning."
links={[
{
href: '/qr-code-tracking',
title: 'QR Code Tracking',
description:
'See device, time, and location context for every scan. Understand which placements drive real activity.',
ctaLabel: 'Track QR code scans',
},
{
href: '/dynamic-qr-code-generator',
title: 'Dynamic QR Code Generator',
description:
'Create QR codes with updatable destinations so analytics can inform what to change — without reprinting.',
ctaLabel: 'Create dynamic QR code',
},
{
href: '/reprint-calculator',
title: 'Reprint Cost Calculator',
description:
'Calculate how much static reprints are costing vs one dynamic QR subscription.',
ctaLabel: 'Calculate reprint savings',
},
{
href: '/qr-code-for-marketing-campaigns',
title: 'QR Codes for Marketing Campaigns',
description:
'Plan offline campaigns where each placement has a measurable QR route and follow-up action.',
ctaLabel: 'Measure offline QR campaigns',
},
{
href: '/use-cases/qr-codes-for-review-collection',
title: 'QR Codes for Review Collection',
description:
'Compare receipts, table cards, packaging, and counters as measurable review-request placements.',
ctaLabel: 'Measure review QR placements',
},
{
href: '/pricing',
title: 'Compare Plans',
description:
'See which plan gives you the scan volume, analytics depth, and QR code count your workflows need.',
ctaLabel: 'Compare plans',
},
]}
pageType="commercial"
cluster="qr-analytics"
/>
</>
);
}

View File

@@ -133,6 +133,8 @@ const highlightedLinks = [
{ href: "/qr-code-tracking", title: "QR Code Tracking", description: "Measure scans from flyers, property signage, retail displays, trade shows, and offline campaigns." },
{ href: "/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/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 = {
@@ -244,7 +246,7 @@ export default function IndustryOverviewPage() {
<section className="py-16">
<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) => (
<Link
key={item.href}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -18,7 +18,7 @@ export function BarcodeGuide() {
</h2>
</div>
<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 className="lead text-xl text-slate-600">

View File

@@ -1,314 +1,573 @@
import React from 'react';
import type { Metadata } from 'next';
import BarcodeGeneratorClient from './BarcodeGeneratorClient';
import { BarcodeGuide } from './BarcodeGuide';
import { Barcode as BarcodeIcon, Shield, Zap, Printer, Download, Share2, Sparkles, Sliders, Check } from 'lucide-react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
// SEO Optimized Metadata
export const metadata: Metadata = {
title: {
absolute: 'Free Barcode Generator Online 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.',
keywords: ['barcode generator', 'online barcode maker', 'create barcode free', 'ean-13 generator', 'upc-a generator', 'code 128 generator', 'barcode creator', 'printable barcodes'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/barcode-generator',
languages: {
'x-default': 'https://www.qrmaster.net/tools/barcode-generator',
en: 'https://www.qrmaster.net/tools/barcode-generator',
},
},
openGraph: {
title: 'Barcode Generator: Create EAN, UPC & Code 128',
description: 'Barcode Generator: Create professional labels instantly. Free & Secured.',
url: 'https://www.qrmaster.net/tools/barcode-generator',
siteName: 'QR Master',
locale: 'en_US',
type: 'website',
images: [{ url: '/barcode-generator-preview.png', width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
title: 'Free Barcode Generator',
description: 'Create custom barcodes in seconds. Download high-quality PNG/SVG.',
},
robots: {
index: true,
follow: true,
},
};
// JSON-LD Structured Data
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
generateSoftwareAppSchema(
'Barcode Generator',
'Generate custom printable barcodes instantly for EAN, UPC, Code 128 and more.',
'/barcode-generator-preview.png',
'UtilitiesApplication'
),
{
'@type': 'HowTo',
name: 'How to Create a Barcode',
datePublished: '2024-06-01',
dateModified: '2025-06-01',
author: {
'@type': 'Person',
name: 'Timo Knuth',
url: 'https://www.qrmaster.net/authors/timo',
},
description: 'Create custom barcodes for products or inventory.',
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Enter Content',
text: 'Type or paste the numeric or alphanumeric data for your barcode.',
},
{
'@type': 'HowToStep',
position: 2,
name: 'Select Format',
text: 'Choose the appropriate barcode type (e.g., Code 128 for general use, EAN-13 for retail).',
},
{
'@type': 'HowToStep',
position: 3,
name: 'Customize Design',
text: 'Adjust the height and width of the barcode to fit your needs.',
},
{
'@type': 'HowToStep',
position: 4,
name: 'Toggle Text',
text: 'Decide if you want the human-readable value to appear below the barcode.',
},
{
'@type': 'HowToStep',
position: 5,
name: 'Download & Print',
text: 'Save your barcode as PNG or SVG and print it for labels or inventory.',
},
],
totalTime: 'PT20S',
},
generateFaqSchema({
'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.',
},
'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.',
},
'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.',
},
'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.',
},
'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.',
},
'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.',
},
'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.',
},
'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.',
},
}),
],
};
export default function BarcodeGeneratorPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<ToolBreadcrumb toolName="Barcode Generator" toolSlug="barcode-generator" />
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
{/* 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">
<div className="absolute inset-0 opacity-10">
{/* Barcode Pattern */}
<svg className="w-full h-full" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern 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>
</defs>
<rect width="100%" height="100%" fill="url(#barcode_pattern)" />
</svg>
</div>
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
<div className="text-center lg:text-left">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
<span className="flex h-2 w-2 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-400"></span>
</span>
Free Tool Professional & Fast
</div>
<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>
</h1>
<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.
<span className="text-white block sm:inline mt-2 sm:mt-0"> Supports EAN-13, UPC-A, and Code 128. No signup required.</span>
</p>
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
<Check className="w-4 h-4 text-blue-400" />
Retail Ready
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
<Check className="w-4 h-4 text-blue-400" />
Vector SVG Export
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
<Check className="w-4 h-4 text-blue-400" />
No Registration
</div>
</div>
</div>
{/* Visual Abstract */}
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
<div className="absolute w-[500px] h-[500px] bg-blue-500/10 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-2 hover:-rotate-1 transition-all duration-700 group">
<div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent rounded-3xl" />
<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">
<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>
<div className="text-xl font-bold tracking-wider mb-1">PROD-98234</div>
<div className="text-xs opacity-70">Inventory ID</div>
</div>
<div className="w-48 h-32 bg-white rounded-xl p-4 shadow-inner relative overflow-hidden flex flex-col items-center justify-center">
<div className="w-full h-16 bg-black flex gap-1 mb-2">
{[2, 4, 1, 3, 2, 1, 4, 2, 1, 3].map((w, i) => (
<div key={i} className="bg-black flex-1" style={{ flex: w }} />
))}
</div>
<div className="text-[10px] font-mono font-bold tracking-widest uppercase">98234001A</div>
</div>
{/* Floating Badge */}
<div className="absolute -bottom-6 -right-6 bg-slate-900 border border-white/10 py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
<div className="bg-blue-500/20 p-2 rounded-full">
<Printer className="w-5 h-5 text-blue-500" />
</div>
<div className="text-left">
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Ready</div>
<div className="text-sm font-bold text-white">Print Instantly</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* GENERATOR SECTION */}
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
<BarcodeGeneratorClient />
</section>
{/* HOW IT WORKS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
How Our Barcode Generator Works
</h2>
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Sliders className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">1. Configure</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Enter your data and select the format.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Sparkles className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">2. Customize</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Adjust height, width and text display.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Zap className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">3. Preview</h3>
<p className="text-slate-600 text-xs leading-relaxed">
See your barcode update in real-time.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Download className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Save as professional PNG or SVG.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Printer className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">5. Print</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Print labels directly from your browser.
</p>
</article>
</div>
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* SEO GUIDE */}
<BarcodeGuide />
</div>
</>
);
}
import React from 'react';
import type { Metadata } from 'next';
import BarcodeGeneratorClient from './BarcodeGeneratorClient';
import { BarcodeGuide } from './BarcodeGuide';
import {
Barcode as BarcodeIcon,
Shield,
Zap,
Printer,
Download,
Share2,
Sparkles,
Sliders,
Check,
} from 'lucide-react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
import {
generateSoftwareAppSchema,
generateFaqSchema,
} from '@/lib/schema-utils';
// SEO Optimized Metadata
export const metadata: Metadata = {
title: {
absolute: 'Free Custom Barcode Generator - EAN, UPC, Code 128',
},
description:
'Free custom barcode generator and barcode maker for EAN-13, UPC-A, UPC barcode, and Code 128. Create scannable labels for retail and inventory, then download PNG or SVG.',
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: {
canonical: 'https://www.qrmaster.net/tools/barcode-generator',
languages: {
'x-default': 'https://www.qrmaster.net/tools/barcode-generator',
en: 'https://www.qrmaster.net/tools/barcode-generator',
},
},
openGraph: {
title: 'Free Custom Barcode Generator - EAN, UPC & Code 128',
description:
'Free online barcode maker for EAN-13, UPC-A, and Code 128. Create scannable custom barcodes in seconds and download PNG or SVG.',
url: 'https://www.qrmaster.net/tools/barcode-generator',
siteName: 'QR Master',
locale: 'en_US',
type: 'website',
images: [
{ url: '/barcode-generator-preview.png', width: 1200, height: 630 },
],
},
twitter: {
card: 'summary_large_image',
title: 'Free Barcode Generator',
description:
'Create custom barcodes in seconds. Download high-quality PNG/SVG.',
},
robots: {
index: true,
follow: true,
},
};
// JSON-LD Structured Data
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
generateSoftwareAppSchema(
'Barcode Generator',
'Generate custom printable barcodes instantly for EAN, UPC, Code 128 and more.',
'/barcode-generator-preview.png',
'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',
name: 'How to Create a Barcode',
datePublished: '2024-06-01',
dateModified: '2026-05-10',
author: {
'@type': 'Person',
name: 'Timo Knuth',
url: 'https://www.qrmaster.net/authors/timo',
},
description: 'Create custom barcodes for products or inventory.',
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Enter Content',
text: 'Type or paste the numeric or alphanumeric data for your barcode.',
},
{
'@type': 'HowToStep',
position: 2,
name: 'Select Format',
text: 'Choose the appropriate barcode type (e.g., Code 128 for general use, EAN-13 for retail).',
},
{
'@type': 'HowToStep',
position: 3,
name: 'Customize Design',
text: 'Adjust the height and width of the barcode to fit your needs.',
},
{
'@type': 'HowToStep',
position: 4,
name: 'Toggle Text',
text: 'Decide if you want the human-readable value to appear below the barcode.',
},
{
'@type': 'HowToStep',
position: 5,
name: 'Download & Print',
text: 'Save your barcode as PNG or SVG and print it for labels or inventory.',
},
],
totalTime: 'PT20S',
},
generateFaqSchema({
'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.',
},
'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.',
},
'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.',
},
'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.',
},
'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.',
},
'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.',
},
'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.',
},
'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.',
},
'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.',
},
}),
],
};
export default function BarcodeGeneratorPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<ToolBreadcrumb
toolName="Barcode Generator"
toolSlug="barcode-generator"
/>
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
{/* 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">
<div className="absolute inset-0 opacity-10">
{/* Barcode Pattern */}
<svg
className="w-full h-full"
width="100%"
height="100%"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<pattern
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>
</defs>
<rect width="100%" height="100%" fill="url(#barcode_pattern)" />
</svg>
</div>
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
<div className="text-center lg:text-left">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
<span className="flex h-2 w-2 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-400"></span>
</span>
Free Tool Professional & Fast
</div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
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>
<p className="text-lg md:text-xl text-slate-400 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
A <strong>free custom barcode generator</strong> and{' '}
<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>
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
<Check className="w-4 h-4 text-blue-400" />
Retail Ready
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
<Check className="w-4 h-4 text-blue-400" />
Vector SVG Export
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
<Check className="w-4 h-4 text-blue-400" />
No Registration
</div>
</div>
</div>
{/* Visual Abstract */}
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
<div className="absolute w-[500px] h-[500px] bg-blue-500/10 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-2 hover:-rotate-1 transition-all duration-700 group">
<div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent rounded-3xl" />
<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">
<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>
<div className="text-xl font-bold tracking-wider mb-1">
PROD-98234
</div>
<div className="text-xs opacity-70">Inventory ID</div>
</div>
<div className="w-48 h-32 bg-white rounded-xl p-4 shadow-inner relative overflow-hidden flex flex-col items-center justify-center">
<div className="w-full h-16 bg-black flex gap-1 mb-2">
{[2, 4, 1, 3, 2, 1, 4, 2, 1, 3].map((w, i) => (
<div
key={i}
className="bg-black flex-1"
style={{ flex: w }}
/>
))}
</div>
<div className="text-[10px] font-mono font-bold tracking-widest uppercase">
98234001A
</div>
</div>
{/* Floating Badge */}
<div className="absolute -bottom-6 -right-6 bg-slate-900 border border-white/10 py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
<div className="bg-blue-500/20 p-2 rounded-full">
<Printer className="w-5 h-5 text-blue-500" />
</div>
<div className="text-left">
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">
Ready
</div>
<div className="text-sm font-bold text-white">
Print Instantly
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* GENERATOR SECTION */}
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
<BarcodeGeneratorClient />
</section>
{/* AI-EXTRACTABLE DEFINITION + STATS BLOCK */}
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-white border-b border-slate-100">
<div className="max-w-4xl mx-auto">
<div className="bg-blue-50 border-l-4 border-blue-500 rounded-xl p-6 mb-8">
<h2 className="text-xl font-bold text-slate-900 mb-2">
What is a Barcode Generator?
</h2>
<p className="text-slate-700 leading-relaxed">
A <strong>custom barcode generator</strong> is an online tool
that converts a number or text string into a scannable barcode
image (EAN-13, UPC-A, or Code 128). The generated barcode can be
downloaded as PNG or SVG and printed on product labels,
packaging, or inventory stickers for use with any standard
barcode scanner.
</p>
</div>
<div className="grid md:grid-cols-3 gap-6 mb-8">
<div className="text-center p-5 rounded-xl bg-slate-50 border border-slate-200">
<div className="text-3xl font-extrabold text-blue-600 mb-1">
EAN-13
</div>
<div className="text-sm font-semibold text-slate-700 mb-1">
Global Retail Standard
</div>
<div className="text-xs text-slate-500">
Used on over 5 billion product labels worldwide (GS1, 2024)
</div>
</div>
<div className="text-center p-5 rounded-xl bg-slate-50 border border-slate-200">
<div className="text-3xl font-extrabold text-blue-600 mb-1">
UPC-A
</div>
<div className="text-sm font-semibold text-slate-700 mb-1">
North America Standard
</div>
<div className="text-xs text-slate-500">
Required by Amazon, Walmart, Target for product listings
</div>
</div>
<div className="text-center p-5 rounded-xl bg-slate-50 border border-slate-200">
<div className="text-3xl font-extrabold text-blue-600 mb-1">
Code 128
</div>
<div className="text-sm font-semibold text-slate-700 mb-1">
Inventory &amp; Logistics
</div>
<div className="text-xs text-slate-500">
Supports letters + numbers best for internal SKU systems
</div>
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl p-5">
<h3 className="font-bold text-slate-900 mb-2">
Barcode vs. QR Code When to Use Which
</h3>
<div className="grid md:grid-cols-2 gap-4 text-sm text-slate-700">
<div>
<p className="font-semibold text-slate-800 mb-1">
Use a barcode for:
</p>
<ul className="space-y-1 list-disc list-inside text-slate-600">
<li>Product SKUs and retail checkout</li>
<li>Warehouse shelf and bin labels</li>
<li>Inventory counting and stock control</li>
<li>Order fulfillment and packing verification</li>
</ul>
</div>
<div>
<p className="font-semibold text-slate-800 mb-1">
Use a QR code for:
</p>
<ul className="space-y-1 list-disc list-inside text-slate-600">
<li>Restaurant menus and digital content</li>
<li>Marketing campaigns and landing pages</li>
<li>Review collection and customer feedback</li>
<li>Product setup guides and support pages</li>
</ul>
</div>
</div>
</div>
</div>
</section>
{/* HOW IT WORKS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
How Our Barcode Generator Works
</h2>
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Sliders className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">1. Configure</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Enter your data and select the format.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Sparkles className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">2. Customize</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Adjust height, width and text display.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Zap className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">3. Preview</h3>
<p className="text-slate-600 text-xs leading-relaxed">
See your barcode update in real-time.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Download className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Save as professional PNG or SVG.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
<Printer className="w-6 h-6 text-white" />
</div>
<h3 className="font-bold text-slate-900 mb-2">5. Print</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Print labels directly from your browser.
</p>
</article>
</div>
</div>
</section>
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-slate-50 border-y border-slate-200">
<div className="max-w-5xl mx-auto">
<div className="max-w-3xl mb-10">
<p className="text-sm font-semibold text-blue-600 uppercase tracking-wider mb-3">
Custom barcode generator
</p>
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Create custom barcodes for labels, inventory, and product
workflows
</h2>
<p className="text-lg text-slate-600 leading-relaxed">
Use the barcode maker when you already have a product number,
SKU, inventory ID, or internal reference and need a printable
barcode image. Choose the format, adjust the dimensions, show or
hide the text value, then export a clean PNG or SVG for labels.
</p>
</div>
<div className="grid md:grid-cols-3 gap-6">
<article className="rounded-xl border border-slate-200 bg-white p-6">
<h3 className="font-bold text-slate-900 mb-2">Retail labels</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Generate EAN-13 or UPC-A barcode images for packaging and
shelf labels when you already have valid GS1-issued numbers.
</p>
</article>
<article className="rounded-xl border border-slate-200 bg-white p-6">
<h3 className="font-bold text-slate-900 mb-2">
Inventory codes
</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Use Code 128 for alphanumeric SKUs, warehouse bins, assets,
and internal tracking labels that need scanner-friendly codes.
</p>
</article>
<article className="rounded-xl border border-slate-200 bg-white p-6">
<h3 className="font-bold text-slate-900 mb-2">
Printable exports
</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Download SVG for crisp print layouts or PNG for fast use in
docs, product sheets, and simple label workflows.
</p>
</article>
</div>
<p className="mt-8 text-sm text-slate-600">
Need a code for a web page instead of a retail scanner? Use the{' '}
<a
href="/dynamic-qr-code-generator"
className="font-semibold text-blue-700 underline"
>
dynamic QR code generator
</a>{' '}
if the destination may change later, or the{' '}
<a
href="/qr-code-tracking"
className="font-semibold text-blue-700 underline"
>
QR code tracking
</a>{' '}
workflow when you need scan analytics.
</p>
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* SEO GUIDE */}
<BarcodeGuide />
</div>
</>
);
}

View File

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

View File

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

View File

@@ -225,6 +225,24 @@ export default function URLQRCodePage() {
</div>
</section>
{/* SEO Content Block */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white border-t border-slate-100">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 mb-8">
URL QR Code Use Cases: When to Use a Link QR Code
</h2>
<div className="prose prose-slate max-w-none">
<p className="text-lg text-slate-600 mb-6">A URL QR code is the simplest and most universal type of QR code: scan it, and a browser opens a specific web address. That simplicity is its strength. Any printed material that references a website becomes interactive the moment you add a URL QR code no app required, no account needed, no special hardware beyond a standard smartphone camera.</p>
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Marketing Materials: Flyers, Brochures &amp; Posters</h3>
<p className="text-slate-600 mb-4">Printed marketing materials have a fundamental limitation: they cannot be clicked. A URL QR code solves this by acting as a physical hyperlink. Flyers for an event can link directly to a registration page, eliminating the step of typing a long URL. Brochures can link to a detailed product page, a video demo, or a portfolio. Outdoor posters can point to a landing page with a time-sensitive offer. The critical design principle is placement and contrast: the QR code should appear on a clean background with at least 1 cm of quiet zone around it, and the call-to-action label such as "Scan to book your spot" should tell users exactly what they will find before they scan. A URL QR code effectively turns print advertising into a measurable digital funnel.</p>
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Product Packaging &amp; Labels</h3>
<p className="text-slate-600 mb-4">Product packaging is increasingly the first place customers turn for more information after purchase. A URL QR code on a label can link to setup instructions, video tutorials, an FAQ page, a warranty registration form, or a support portal replacing bulky printed manuals and keeping the information always up to date. For consumables and repeat-purchase products, the QR code can link to a reorder page, transforming packaging from a cost centre into a sales channel. Food and beverage brands use URL QR codes to link to nutritional databases, sourcing information, and sustainability reports. The key advantage over a printed URL is that customers are far more likely to scan than to type a long web address especially when they are already holding the product in their hands.</p>
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Dynamic vs Static URL QR Codes</h3>
<p className="text-slate-600 mb-4">A static URL QR code encodes the destination directly into the pattern it is permanent, requires no server, and works forever, but it cannot be changed after printing. If your URL changes, the code breaks. A <a href="/dynamic-qr-code-generator" className="text-indigo-600 underline hover:text-indigo-800">dynamic QR code</a> works differently: it encodes a short redirect URL that you control, so you can update the destination at any time without reprinting the physical code. Dynamic codes are the right choice for anything printed at scale (packaging runs, banners, long-running campaigns) where reprinting after a URL change would be costly. They also unlock <a href="/qr-code-tracking" className="text-indigo-600 underline hover:text-indigo-800">scan analytics</a> data on how many people scanned, from which device, country, and at what time which static codes cannot provide. For one-off or low-stakes uses like a personal project or a single event flyer, a free static URL QR code is perfectly sufficient.</p>
</div>
</div>
</section>
<RelatedTools />
{/* FAQ SECTION */}

View File

@@ -1,373 +1,373 @@
import React from 'react';
import type { Metadata } from 'next';
import VCardGenerator from './VCardGenerator';
import { User, Shield, Zap, Smartphone, Contact, Share2, Check, UserPlus, TrendingUp } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
// SEO Optimized Metadata
export const metadata: Metadata = {
title: {
absolute: 'Free vCard QR Code Generator | QR Master',
},
description: 'Create a vCard QR code for your business card. Share contact details instantly — customers scan and save with one tap. Free, no signup required.',
keywords: ['vcard qr code', 'business card qr code', 'contact qr generator', 'digital business card', 'add to contacts qr', 'visitenkarte qr code', 'digitale visitenkarte erstellen', 'kontakt qr code', 'elektronische visitenkarte', 'vcard erstellen kostenlos'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/vcard-qr-code',
languages: {
'x-default': 'https://www.qrmaster.net/tools/vcard-qr-code',
en: 'https://www.qrmaster.net/tools/vcard-qr-code',
},
},
openGraph: {
title: 'Free vCard QR Code Generator | QR Master',
description: 'Turn your contact info into a QR code. The modern way to share your business card.',
type: 'website',
url: 'https://www.qrmaster.net/tools/vcard-qr-code',
images: [{ url: '/og-vcard-generator.png', width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
title: 'Free vCard QR Code Generator',
description: 'Create QR codes for contact sharing. Instant and free.',
},
robots: {
index: true,
follow: true,
},
};
// JSON-LD Structured Data
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
generateSoftwareAppSchema(
'vCard QR Code Generator',
'Generate vCard (VCF) QR codes for business cards. Scanners can save contact info instantly.',
'/og-vcard-generator.png'
),
{
'@type': 'HowTo',
name: 'How to Create a vCard QR Code',
datePublished: '2024-01-01',
dateModified: '2025-06-01',
author: {
'@type': 'Person',
name: 'Timo Knuth',
url: 'https://www.qrmaster.net/authors/timo',
},
description: 'Create a QR code that saves your contact details.',
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Enter Details',
text: 'Fill in your Name, Phone, Email, Company, and Address.',
},
{
'@type': 'HowToStep',
position: 2,
name: 'Customize',
text: 'Select a color that matches your brand and add a frame.',
},
{
'@type': 'HowToStep',
position: 3,
name: 'Download',
text: 'Download the QR code image and place it on your physical business card.',
},
],
totalTime: 'PT1M',
},
generateFaqSchema({
'How does a vCard QR code work?': {
question: 'How does a vCard QR code work?',
answer: 'A vCard QR code contains your contact information in a standardized format (VCF). When scanned, the phone recognizes it as a contact card and prompts the user to "Save Contact" to their address book.',
},
'Is there a limit to how much info I can add?': {
question: 'Is there a limit to how much info I can add?',
answer: 'Static QR codes hold data directly in the pattern. The more data you add (long addresses, bio), the denser and harder to scan the QR code becomes. We recommend sticking to essential contact info for static codes.',
},
'Can I update my info later?': {
question: 'Can I update my info later?',
answer: 'No. This is a static vCard QR code. Once created, the info cannot be changed. If you move jobs or change numbers, you must print a new code. For editable cards, use our Dynamic vCard Plus.',
},
'Does it work on iPhone and Android?': {
question: 'Does it work on iPhone and Android?',
answer: 'Yes. Both iOS (Camera app) and Android (Camera or Google Lens) natively support vCard QR codes and correctly import the contact data.',
},
}),
],
};
export default function VCardQRCodePage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<ToolBreadcrumb toolName="vCard QR Code Generator" toolSlug="vcard-qr-code" />
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
{/* 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" style={{ backgroundColor: '#9F1239' }}>
<div className="absolute inset-0 opacity-10">
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
</linearGradient>
</defs>
</svg>
</div>
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
<div className="text-center lg:text-left">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
<span className="flex h-2 w-2 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-rose-400"></span>
</span>
Free Tool No Signup Required
</div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
The Modern Way to <br className="hidden lg:block" />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-rose-300 to-pink-300">Share Your Contact</span>
</h1>
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
Create a scannable Digital Business Card. One scan saves your name, phone, email, and address instantly.
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Free & Professional.</strong>
</p>
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<UserPlus className="w-4 h-4 text-rose-300" />
Instant Save
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<Share2 className="w-4 h-4 text-amber-400" />
Easy Share
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<Shield className="w-4 h-4 text-purple-400" />
No Data Stored
</div>
</div>
</div>
{/* Visual Abstract */}
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
<div className="absolute w-[500px] h-[500px] bg-indigo-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
<div className="relative w-96 h-60 bg-white/10 backdrop-blur-2xl border border-white/30 rounded-2xl shadow-2xl p-6 transform rotate-6 hover:rotate-3 transition-all duration-700 group">
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent" />
<div className="flex justify-between items-start relative z-10">
<div className="space-y-4">
<div className="w-16 h-16 rounded-full bg-white/20 border-2 border-white/30 flex items-center justify-center">
<Contact className="w-8 h-8 text-white" />
</div>
<div className="space-y-1">
<div className="h-4 w-32 bg-white/90 rounded-sm" />
<div className="h-3 w-20 bg-emerald-400/90 rounded-sm" />
</div>
</div>
<div className="w-24 h-24 bg-white rounded-lg p-1.5 shadow-lg">
<QRCodeSVG value="https://www.qrmaster.net" size={84} fgColor="#1A1265" />
</div>
</div>
<div className="absolute bottom-6 left-6 space-y-2 z-10">
<div className="h-2 w-48 bg-white/40 rounded-full" />
<div className="h-2 w-40 bg-white/30 rounded-full" />
</div>
{/* Floating Badge */}
<div className="absolute -bottom-4 -left-4 bg-white py-2 px-4 rounded-lg shadow-xl flex items-center gap-2 transform scale-90">
<div className="bg-emerald-100 p-1.5 rounded-full">
<Check className="w-3 h-3 text-emerald-600" />
</div>
<span className="text-xs font-bold text-slate-900">Saved to Contacts</span>
</div>
</div>
</div>
</div>
</section>
{/* GENERATOR SECTION */}
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
<VCardGenerator />
</section>
{/* HOW IT WORKS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
How vCard QR Codes Work
</h2>
<div className="grid md:grid-cols-3 gap-8">
<article className="text-center">
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Contact className="w-7 h-7 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">1. Enter Details</h3>
<p className="text-slate-600 text-sm">
Fill in your professional contact information. Only add what you want to share.
</p>
</article>
<article className="text-center">
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Smartphone className="w-7 h-7 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">2. Scan</h3>
<p className="text-slate-600 text-sm">
A potential client or partner scans your card with their phone camera.
</p>
</article>
<article className="text-center">
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<UserPlus className="w-7 h-7 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">3. Save</h3>
<p className="text-slate-600 text-sm">
They tap "Create New Contact" to save your details instantly. No typing errors.
</p>
</article>
</div>
</div>
</section>
{/* WHY DIGITAL BUSINESS CARDS — STATISTICS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="w-5 h-5 text-rose-500" />
<span className="text-sm font-semibold text-rose-600 uppercase tracking-wider">Research-backed</span>
</div>
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Why Digital Contact Sharing Outperforms Paper Cards
</h2>
<p className="text-slate-600 mb-10 max-w-2xl">
A <strong>vCard QR code</strong> solves the single biggest problem with traditional business cards: they get discarded before anyone saves your details.
</p>
<div className="grid md:grid-cols-2 gap-6 mb-8">
<div className="bg-rose-50 border border-rose-100 rounded-2xl p-6">
<div className="text-4xl font-extrabold text-rose-600 mb-2">88%</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
of traditional paper business cards are thrown away within a week of being handed out. A vCard QR code on your card saves contact details instantly no manual typing, no lost connections.
</p>
<p className="text-xs text-slate-500">
Source: <a href="https://www.adobe.com/express/learn/blog/business-card-statistics" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">Adobe Business Research</a>
</p>
</div>
<div className="bg-orange-50 border border-orange-100 rounded-2xl p-6">
<div className="text-4xl font-extrabold text-orange-600 mb-2">1-tap save</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
Instead of asking someone to manually type your name, phone, and email a vCard QR code transfers all contact fields (name, phone, email, company, URL) directly into their phone's address book with a single scan.
</p>
<p className="text-xs text-slate-500">
vCard 3.0 / VCF format — supported natively by iOS and Android
</p>
</div>
</div>
<p className="text-xs text-slate-400 italic">
By Timo Knuth, QR Master · Last updated: June 2025 · Based on independent academic and industry research
</p>
</div>
</section>
{/* GROWTH LINKS */}
<GrowthLinksSection
eyebrow="Level up your QR strategy"
title="More ways to use QR codes for your brand"
description="A vCard QR code is a great start. Add custom design, tracking, and dynamic destinations to get the most out of every print."
links={[
{
href: '/custom-qr-code-generator',
title: 'Custom QR Code Generator',
description: 'Add your logo, brand colors, and a custom frame to your QR codes.',
ctaLabel: 'Custom QR with logo',
},
{
href: '/dynamic-qr-code-generator',
title: 'Dynamic QR Code Generator',
description: 'Update your contact destination after printing. No new QR code needed.',
ctaLabel: 'Dynamic QR with tracking',
},
]}
pageType="commercial"
cluster="vcard-qr"
/>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Frequently Asked Questions
</h2>
<p className="text-slate-600 text-center mb-10">
Common questions about vCard QR codes.
</p>
<div className="space-y-4">
<FaqItem
question="Can I add a profile picture?"
answer="Not on a static vCard QR code. Static codes store data in the pixels, so adding an image would make the code too complex to scan. For profile pictures, social links, and rich media, use our Dynamic vCard Plus solution."
/>
<FaqItem
question="How long does the QR code last?"
answer="Forever. Static vCard QR codes do not expire because the data is embedded directly in the image."
/>
<FaqItem
question="What information is required?"
answer="Nothing is strictly required, but we recommend at least a First Name and either a Phone Number or Email so the contact is useful."
/>
<FaqItem
question="Is my data safe?"
answer="Yes. This tool operates 100% in your browser. We do not store, see, or optimize your contact data. It goes directly from your input to the QR code."
/>
</div>
</div>
</section>
</div>
</>
);
}
function FaqItem({ question, answer }: { question: string; answer: string }) {
return (
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
{question}
<span className="transition group-open:rotate-180 text-slate-400">
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
<path d="M6 9l6 6 6-6" />
</svg>
</span>
</summary>
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
{answer}
</div>
</details>
);
}
import React from 'react';
import type { Metadata } from 'next';
import VCardGenerator from './VCardGenerator';
import { User, Shield, Zap, Smartphone, Contact, Share2, Check, UserPlus, TrendingUp } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
// SEO Optimized Metadata
export const metadata: Metadata = {
title: {
absolute: 'Free vCard QR Code Generator | QR Master',
},
description: 'Create a vCard QR code for your business card. Share contact details instantly — customers scan and save with one tap. Free, no signup required.',
keywords: ['vcard qr code', 'business card qr code', 'contact qr generator', 'digital business card', 'add to contacts qr', 'visitenkarte qr code', 'digitale visitenkarte erstellen', 'kontakt qr code', 'elektronische visitenkarte', 'vcard erstellen kostenlos'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/vcard-qr-code',
languages: {
'x-default': 'https://www.qrmaster.net/tools/vcard-qr-code',
en: 'https://www.qrmaster.net/tools/vcard-qr-code',
},
},
openGraph: {
title: 'Free vCard QR Code Generator | QR Master',
description: 'Turn your contact info into a QR code. The modern way to share your business card.',
type: 'website',
url: 'https://www.qrmaster.net/tools/vcard-qr-code',
images: [{ url: '/og-vcard-generator.png', width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
title: 'Free vCard QR Code Generator',
description: 'Create QR codes for contact sharing. Instant and free.',
},
robots: {
index: true,
follow: true,
},
};
// JSON-LD Structured Data
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
generateSoftwareAppSchema(
'vCard QR Code Generator',
'Generate vCard (VCF) QR codes for business cards. Scanners can save contact info instantly.',
'/og-vcard-generator.png'
),
{
'@type': 'HowTo',
name: 'How to Create a vCard QR Code',
datePublished: '2024-01-01',
dateModified: '2026-04-27',
author: {
'@type': 'Person',
name: 'Timo Knuth',
url: 'https://www.qrmaster.net/authors/timo',
},
description: 'Create a QR code that saves your contact details.',
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Enter Details',
text: 'Fill in your Name, Phone, Email, Company, and Address.',
},
{
'@type': 'HowToStep',
position: 2,
name: 'Customize',
text: 'Select a color that matches your brand and add a frame.',
},
{
'@type': 'HowToStep',
position: 3,
name: 'Download',
text: 'Download the QR code image and place it on your physical business card.',
},
],
totalTime: 'PT1M',
},
generateFaqSchema({
'How does a vCard QR code work?': {
question: 'How does a vCard QR code work?',
answer: 'A vCard QR code contains your contact information in a standardized format (VCF). When scanned, the phone recognizes it as a contact card and prompts the user to "Save Contact" to their address book.',
},
'Is there a limit to how much info I can add?': {
question: 'Is there a limit to how much info I can add?',
answer: 'Static QR codes hold data directly in the pattern. The more data you add (long addresses, bio), the denser and harder to scan the QR code becomes. We recommend sticking to essential contact info for static codes.',
},
'Can I update my info later?': {
question: 'Can I update my info later?',
answer: 'No. This is a static vCard QR code. Once created, the info cannot be changed. If you move jobs or change numbers, you must print a new code. For editable cards, use our Dynamic vCard Plus.',
},
'Does it work on iPhone and Android?': {
question: 'Does it work on iPhone and Android?',
answer: 'Yes. Both iOS (Camera app) and Android (Camera or Google Lens) natively support vCard QR codes and correctly import the contact data.',
},
}),
],
};
export default function VCardQRCodePage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<ToolBreadcrumb toolName="vCard QR Code Generator" toolSlug="vcard-qr-code" />
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
{/* 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" style={{ backgroundColor: '#9F1239' }}>
<div className="absolute inset-0 opacity-10">
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
</linearGradient>
</defs>
</svg>
</div>
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
<div className="text-center lg:text-left">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
<span className="flex h-2 w-2 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-rose-400"></span>
</span>
Free Tool No Signup Required
</div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
The Modern Way to <br className="hidden lg:block" />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-rose-300 to-pink-300">Share Your Contact</span>
</h1>
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
Create a scannable Digital Business Card. One scan saves your name, phone, email, and address instantly.
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Free & Professional.</strong>
</p>
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<UserPlus className="w-4 h-4 text-rose-300" />
Instant Save
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<Share2 className="w-4 h-4 text-amber-400" />
Easy Share
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<Shield className="w-4 h-4 text-purple-400" />
No Data Stored
</div>
</div>
</div>
{/* Visual Abstract */}
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
<div className="absolute w-[500px] h-[500px] bg-indigo-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
<div className="relative w-96 h-60 bg-white/10 backdrop-blur-2xl border border-white/30 rounded-2xl shadow-2xl p-6 transform rotate-6 hover:rotate-3 transition-all duration-700 group">
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent" />
<div className="flex justify-between items-start relative z-10">
<div className="space-y-4">
<div className="w-16 h-16 rounded-full bg-white/20 border-2 border-white/30 flex items-center justify-center">
<Contact className="w-8 h-8 text-white" />
</div>
<div className="space-y-1">
<div className="h-4 w-32 bg-white/90 rounded-sm" />
<div className="h-3 w-20 bg-emerald-400/90 rounded-sm" />
</div>
</div>
<div className="w-24 h-24 bg-white rounded-lg p-1.5 shadow-lg">
<QRCodeSVG value="https://www.qrmaster.net" size={84} fgColor="#1A1265" />
</div>
</div>
<div className="absolute bottom-6 left-6 space-y-2 z-10">
<div className="h-2 w-48 bg-white/40 rounded-full" />
<div className="h-2 w-40 bg-white/30 rounded-full" />
</div>
{/* Floating Badge */}
<div className="absolute -bottom-4 -left-4 bg-white py-2 px-4 rounded-lg shadow-xl flex items-center gap-2 transform scale-90">
<div className="bg-emerald-100 p-1.5 rounded-full">
<Check className="w-3 h-3 text-emerald-600" />
</div>
<span className="text-xs font-bold text-slate-900">Saved to Contacts</span>
</div>
</div>
</div>
</div>
</section>
{/* GENERATOR SECTION */}
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
<VCardGenerator />
</section>
{/* HOW IT WORKS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
How vCard QR Codes Work
</h2>
<div className="grid md:grid-cols-3 gap-8">
<article className="text-center">
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Contact className="w-7 h-7 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">1. Enter Details</h3>
<p className="text-slate-600 text-sm">
Fill in your professional contact information. Only add what you want to share.
</p>
</article>
<article className="text-center">
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Smartphone className="w-7 h-7 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">2. Scan</h3>
<p className="text-slate-600 text-sm">
A potential client or partner scans your card with their phone camera.
</p>
</article>
<article className="text-center">
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<UserPlus className="w-7 h-7 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">3. Save</h3>
<p className="text-slate-600 text-sm">
They tap "Create New Contact" to save your details instantly. No typing errors.
</p>
</article>
</div>
</div>
</section>
{/* WHY DIGITAL BUSINESS CARDS — STATISTICS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="w-5 h-5 text-rose-500" />
<span className="text-sm font-semibold text-rose-600 uppercase tracking-wider">Research-backed</span>
</div>
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Why Digital Contact Sharing Outperforms Paper Cards
</h2>
<p className="text-slate-600 mb-10 max-w-2xl">
A <strong>vCard QR code</strong> solves the single biggest problem with traditional business cards: they get discarded before anyone saves your details.
</p>
<div className="grid md:grid-cols-2 gap-6 mb-8">
<div className="bg-rose-50 border border-rose-100 rounded-2xl p-6">
<div className="text-4xl font-extrabold text-rose-600 mb-2">88%</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
of traditional paper business cards are thrown away within a week of being handed out. A vCard QR code on your card saves contact details instantly no manual typing, no lost connections.
</p>
<p className="text-xs text-slate-500">
Source: <a href="https://www.adobe.com/express/learn/blog/business-card-statistics" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">Adobe Business Research</a>
</p>
</div>
<div className="bg-orange-50 border border-orange-100 rounded-2xl p-6">
<div className="text-4xl font-extrabold text-orange-600 mb-2">1-tap save</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
Instead of asking someone to manually type your name, phone, and email a vCard QR code transfers all contact fields (name, phone, email, company, URL) directly into their phone's address book with a single scan.
</p>
<p className="text-xs text-slate-500">
vCard 3.0 / VCF format — supported natively by iOS and Android
</p>
</div>
</div>
<p className="text-xs text-slate-400 italic">
By Timo Knuth, QR Master · Last updated: June 2025 · Based on independent academic and industry research
</p>
</div>
</section>
{/* GROWTH LINKS */}
<GrowthLinksSection
eyebrow="Level up your QR strategy"
title="More ways to use QR codes for your brand"
description="A vCard QR code is a great start. Add custom design, tracking, and dynamic destinations to get the most out of every print."
links={[
{
href: '/custom-qr-code-generator',
title: 'Custom QR Code Generator',
description: 'Add your logo, brand colors, and a custom frame to your QR codes.',
ctaLabel: 'Custom QR with logo',
},
{
href: '/dynamic-qr-code-generator',
title: 'Dynamic QR Code Generator',
description: 'Update your contact destination after printing. No new QR code needed.',
ctaLabel: 'Dynamic QR with tracking',
},
]}
pageType="commercial"
cluster="vcard-qr"
/>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Frequently Asked Questions
</h2>
<p className="text-slate-600 text-center mb-10">
Common questions about vCard QR codes.
</p>
<div className="space-y-4">
<FaqItem
question="Can I add a profile picture?"
answer="Not on a static vCard QR code. Static codes store data in the pixels, so adding an image would make the code too complex to scan. For profile pictures, social links, and rich media, use our Dynamic vCard Plus solution."
/>
<FaqItem
question="How long does the QR code last?"
answer="Forever. Static vCard QR codes do not expire because the data is embedded directly in the image."
/>
<FaqItem
question="What information is required?"
answer="Nothing is strictly required, but we recommend at least a First Name and either a Phone Number or Email so the contact is useful."
/>
<FaqItem
question="Is my data safe?"
answer="Yes. This tool operates 100% in your browser. We do not store, see, or optimize your contact data. It goes directly from your input to the QR code."
/>
</div>
</div>
</section>
</div>
</>
);
}
function FaqItem({ question, answer }: { question: string; answer: string }) {
return (
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
{question}
<span className="transition group-open:rotate-180 text-slate-400">
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
<path d="M6 9l6 6 6-6" />
</svg>
</span>
</summary>
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
{answer}
</div>
</details>
);
}

View File

@@ -273,6 +273,24 @@ export default function WhatsappQRCodePage() {
</div>
</section>
{/* SEO Content Block */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white border-t border-slate-100">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 mb-8">
How to Use WhatsApp QR Codes for Business
</h2>
<div className="prose prose-slate max-w-none">
<p className="text-lg text-slate-600 mb-6">WhatsApp QR codes remove the biggest obstacle between a potential customer and a conversation: saving a phone number. Instead of typing digits manually, customers scan once and land directly in a chat — with your pre-filled message already loaded and ready to send. For businesses that rely on fast, personal communication, that friction reduction is significant.</p>
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Customer Support &amp; Service Teams</h3>
<p className="text-slate-600 mb-4">Support teams can use WhatsApp QR codes on help pages, packaging inserts, and warranty cards to give customers a direct line without publishing a number publicly in plain text. The pre-filled message field is especially powerful here: you can pre-load context such as "Hi, I need help with my order #" so agents receive structured requests from the start. This reduces back-and-forth and speeds up resolution time. Place the QR code at the end of a printed receipt or inside a product box to catch customers at the exact moment they might need help.</p>
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Restaurants, Cafes &amp; Retail Stores</h3>
<p className="text-slate-600 mb-4">Physical businesses benefit the most from WhatsApp QR codes placed at the point of purchase. A cafe can display a QR code at the counter that pre-fills "I'd like to place a takeaway order" — customers scan, confirm their order via WhatsApp, and staff prepare it without phone calls interrupting busy periods. Restaurants can use QR codes on table cards for reservations or feedback. Retail stores can place them near fitting rooms so shoppers can ask about sizes or alternative products without waiting for staff. The key is pairing the QR code with a clear call-to-action label like "Chat with us on WhatsApp" and placing it at eye level.</p>
<h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">Marketing Campaigns &amp; Lead Generation</h3>
<p className="text-slate-600 mb-4">WhatsApp QR codes on printed flyers, outdoor posters, or event banners create a measurable bridge from offline marketing to a live conversation. Unlike a website URL, a WhatsApp link initiates a direct dialogue — which converts at a much higher rate than a contact form. For lead generation campaigns, pre-fill the message with the campaign name or offer so you can track which placement is driving inbound chats. To measure QR code performance across multiple placements, combine your WhatsApp QR code strategy with <a href="/qr-code-tracking" className="text-[#128C7E] underline hover:text-[#075E54]">QR code scan analytics</a> to see which posters, flyers, or locations generate the most engagement.</p>
</div>
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />

View File

@@ -1,394 +1,394 @@
import React from 'react';
import type { Metadata } from 'next';
import WiFiGenerator from './WiFiGenerator';
import { Wifi, Shield, Zap, Smartphone, Lock, QrCode, Download, Share2, TrendingUp } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
// SEO Optimized Metadata
export const metadata: Metadata = {
title: {
absolute: 'Free WiFi QR Code Generator | WLAN QR Code | QR Master',
},
description: 'Create a WiFi QR code in seconds. Erstelle kostenlos deinen WLAN QR Code ohne Passwort-Eingabe. Guests scan to connect instantly. 100% Secure & Free.',
keywords: ['wifi qr code', 'qr code generator', 'wifi qr code generator', 'share wifi', 'wifi password qr', 'guest wifi', 'wlan qr code', 'wlan qr code erstellen', 'wifi passwort qr code', 'wlan zugang teilen', 'wifi qr code kostenlos'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/wifi-qr-code',
languages: {
'x-default': 'https://www.qrmaster.net/tools/wifi-qr-code',
en: 'https://www.qrmaster.net/tools/wifi-qr-code',
},
},
openGraph: {
title: 'Free WiFi QR Code Generator | QR Master',
description: 'Share your WiFi without sharing your password. Guests scan the QR code to connect instantly.',
type: 'website',
url: 'https://www.qrmaster.net/tools/wifi-qr-code',
images: [{ url: '/og-wifi-generator.png', width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
title: 'Free WiFi QR Code Generator',
description: 'Share WiFi instantly with a QR code. No typing passwords.',
},
robots: {
index: true,
follow: true,
},
};
// JSON-LD Structured Data
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
// SoftwareApplication Schema
generateSoftwareAppSchema(
'WiFi QR Code Generator',
'Generate QR codes for WiFi networks. Guests scan to connect without typing passwords.',
'/og-wifi-generator.png'
),
// HowTo Schema for Featured Snippets
{
'@type': 'HowTo',
name: 'How to Create a WiFi QR Code',
datePublished: '2024-01-01',
dateModified: '2025-06-01',
author: {
'@type': 'Person',
name: 'Timo Knuth',
url: 'https://www.qrmaster.net/authors/timo',
},
description: 'Create a QR code that connects devices to your WiFi network automatically.',
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Enter Network Name',
text: 'Type your WiFi network name (SSID) in the Network Name field.',
},
{
'@type': 'HowToStep',
position: 2,
name: 'Enter Password',
text: 'Enter your WiFi password. This is processed locally and never sent to any server.',
},
{
'@type': 'HowToStep',
position: 3,
name: 'Select Security Type',
text: 'Choose WPA/WPA2 (most common), WEP, or No Password for open networks.',
},
{
'@type': 'HowToStep',
position: 4,
name: 'Download QR Code',
text: 'Click Download PNG or SVG to save your QR code. Print it or share digitally.',
},
{
'@type': 'HowToStep',
position: 5,
name: 'Connect',
text: 'Print the code. Guests can scan it to join your network instantly.',
},
],
totalTime: 'PT1M',
},
// FAQPage Schema
generateFaqSchema({
'Is it safe to enter my WiFi password?': {
question: 'Is it safe to enter my WiFi password?',
answer: 'Yes, completely safe. This tool processes everything in your browser (client-side). Your password never leaves your device and is not sent to any server.',
},
'Do WiFi QR codes work on iPhone and Android?': {
question: 'Do WiFi QR codes work on iPhone and Android?',
answer: 'Yes. Both iOS (11+) and Android devices can scan WiFi QR codes using their built-in camera app. No additional apps required.',
},
'What happens if I change my WiFi password?': {
question: 'What happens if I change my WiFi password?',
answer: 'If you change your WiFi password, the old QR code will stop working. You\'ll need to generate a new QR code with the updated credentials.For frequently changing passwords, consider using dynamic QR codes.',
},
'Can I customize the QR code design?': {
question: 'Can I customize the QR code design?',
answer: 'Yes. You can change the QR code color and add frame labels like "Scan Me" or "WiFi" to make it more recognizable and user-friendly.',
},
'Does it work for hidden networks?': {
question: 'Does it work for hidden networks?',
answer: 'Yes, just check the "Hidden Network" box if your SSID is hidden. The QR code contains the standard WiFi string configuration.',
},
}),
],
};
export default function WiFiQRCodePage() {
return (
<>
{/* JSON-LD Script */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<ToolBreadcrumb toolName="WiFi QR Code Generator" toolSlug="wifi-qr-code" />
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
{/* 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" style={{ backgroundColor: '#1A1265' }}>
{/* Background Pattern */}
<div className="absolute inset-0 opacity-10">
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
</linearGradient>
</defs>
</svg>
</div>
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
{/* Left: Text Content */}
<div className="text-center lg:text-left">
{/* Badge */}
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
<span className="flex h-2 w-2 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400"></span>
</span>
Free Tool No Signup Required
</div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
The Safest Way to <br className="hidden lg:block" />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-cyan-400">Share Your WiFi</span>
</h1>
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
Generate a secure QR code in seconds. No more spelling out complicated passwords.
<strong className="text-white block sm:inline mt-2 sm:mt-0"> 100% Client-Side & Private.</strong>
</p>
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<Lock className="w-4 h-4 text-emerald-400" />
No Server Uploads
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<Zap className="w-4 h-4 text-amber-400" />
Instant Connect
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<Smartphone className="w-4 h-4 text-purple-400" />
iOS & Android
</div>
</div>
</div>
{/* Right: Visual Abstract Composition */}
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
{/* Decorative Glow */}
<div className="absolute w-[500px] h-[500px] bg-indigo-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
{/* Floating Glass Card */}
<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-6 hover:rotate-3 transition-all duration-700 group">
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
{/* Mock QR */}
<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" size={170} fgColor="#0f172a" level="Q" />
{/* Scan Line */}
<div className="absolute top-1/2 left-0 w-full h-1 bg-emerald-500 shadow-[0_0_20px_rgba(16,185,129,1)] animate-pulse" />
</div>
<div className="w-full space-y-3">
<div className="h-2 w-32 bg-white/20 rounded-full mx-auto" />
<div className="h-2 w-20 bg-white/10 rounded-full mx-auto" />
</div>
{/* Floating Badge */}
<div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
<div className="bg-emerald-100 p-2 rounded-full">
<Wifi className="w-5 h-5 text-emerald-600" />
</div>
<div className="text-left">
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Status</div>
<div className="text-sm font-bold text-slate-900">Connected</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* GENERATOR SECTION */}
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
<WiFiGenerator />
</section>
{/* HOW IT WORKS - AEO/GEO Content */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
How WiFi QR Codes Work
</h2>
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Wifi className="w-6 h-6 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">1. Network</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Enter your WiFi SSID and password.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Shield className="w-6 h-6 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">2. Security</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Select WPA/WPA2 encryption.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Zap className="w-6 h-6 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">3. Style</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Customize colors and add a frame.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Download className="w-6 h-6 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Get your high-quality QR image.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Smartphone className="w-6 h-6 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">5. Connect</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Print it out. Guests scan to join!
</p>
</article>
</div>
</div>
</section>
{/* WHY WIFI QR CODES MATTER — STATISTICS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="w-5 h-5 text-emerald-500" />
<span className="text-sm font-semibold text-emerald-600 uppercase tracking-wider">Research-backed</span>
</div>
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Why WiFi QR Codes Improve Customer Experience
</h2>
<p className="text-slate-600 mb-10 max-w-2xl">
A <strong>WiFi QR code</strong> eliminates the single biggest friction point between your guest and your network manual password entry.
</p>
<div className="grid md:grid-cols-2 gap-6 mb-8">
<div className="bg-emerald-50 border border-emerald-100 rounded-2xl p-6">
<div className="text-3xl font-extrabold text-emerald-600 mb-2">#1 Amenity</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
Free WiFi is rated the most important hotel amenity by guests ahead of breakfast, parking, and loyalty points. Instant, frictionless access directly impacts satisfaction scores and repeat bookings.
</p>
<p className="text-xs text-slate-500">
Source: <a href="https://www.jdpower.com/business/travel-hospitality/hotel-guest-satisfaction-study" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">J.D. Power Hotel Guest Satisfaction Study</a>
</p>
</div>
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-6">
<div className="text-3xl font-extrabold text-blue-600 mb-2">Effort = Loyalty</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
Reducing customer effort like eliminating manual password entry is the single strongest predictor of customer loyalty. The lower the effort, the higher the repeat visit rate and positive word-of-mouth.
</p>
<p className="text-xs text-slate-500">
Source: <a href="https://hbr.org/2010/07/stop-trying-to-delight-your-customers" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">Harvard Business Review "Stop Trying to Delight Your Customers"</a> (Customer Effort Score research)
</p>
</div>
</div>
<p className="text-xs text-slate-400 italic">
By Timo Knuth, QR Master · Last updated: June 2025 · Based on independent academic and industry research
</p>
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION - Featured Snippet Optimized */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Frequently Asked Questions
</h2>
<p className="text-slate-600 text-center mb-10">
Everything you need to know about WiFi QR codes.
</p>
<div className="space-y-4">
<FaqItem
question="Is it safe to enter my WiFi password here?"
answer="Yes, completely safe. This tool uses client-side processing, meaning your WiFi password never leaves your device. It's processed locally in your browser to generate the QR code—no data is sent to any server."
/>
<FaqItem
question="Do WiFi QR codes work on iPhone and Android?"
answer="Yes. iOS 11 and later, as well as all modern Android devices, can scan WiFi QR codes using the built-in camera app. Simply point the camera at the QR code and tap the notification to connect."
/>
<FaqItem
question="What happens if I change my WiFi password?"
answer="If you change your WiFi password, the old QR code will stop working. You'll need to generate a new QR code with the updated credentials. For frequently changing passwords, consider using dynamic QR codes."
/>
<FaqItem
question="Can I customize the QR code design?"
answer="Yes. You can change the foreground color of the QR code and add frame labels such as 'Scan Me', 'WiFi', or 'Connect' to make your QR code more recognizable and user-friendly."
/>
<FaqItem
question="Does it work for hidden networks?"
answer="Yes, just check the 'Hidden Network' box if your SSID is hidden. The QR code contains the standard WiFi string configuration."
/>
</div>
</div>
</section>
</div>
</>
);
}
// FAQ Item Component
function FaqItem({ question, answer }: { question: string; answer: string }) {
return (
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
{question}
<span className="transition group-open:rotate-180 text-slate-400">
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
<path d="M6 9l6 6 6-6" />
</svg>
</span>
</summary>
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
{answer}
</div>
</details>
);
}
import React from 'react';
import type { Metadata } from 'next';
import WiFiGenerator from './WiFiGenerator';
import { Wifi, Shield, Zap, Smartphone, Lock, QrCode, Download, Share2, TrendingUp } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
// SEO Optimized Metadata
export const metadata: Metadata = {
title: {
absolute: 'Free WiFi QR Code Generator | WLAN QR Code | QR Master',
},
description: 'Create a WiFi QR code in seconds. Erstelle kostenlos deinen WLAN QR Code ohne Passwort-Eingabe. Guests scan to connect instantly. 100% Secure & Free.',
keywords: ['wifi qr code', 'qr code generator', 'wifi qr code generator', 'share wifi', 'wifi password qr', 'guest wifi', 'wlan qr code', 'wlan qr code erstellen', 'wifi passwort qr code', 'wlan zugang teilen', 'wifi qr code kostenlos'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/wifi-qr-code',
languages: {
'x-default': 'https://www.qrmaster.net/tools/wifi-qr-code',
en: 'https://www.qrmaster.net/tools/wifi-qr-code',
},
},
openGraph: {
title: 'Free WiFi QR Code Generator | QR Master',
description: 'Share your WiFi without sharing your password. Guests scan the QR code to connect instantly.',
type: 'website',
url: 'https://www.qrmaster.net/tools/wifi-qr-code',
images: [{ url: '/og-wifi-generator.png', width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
title: 'Free WiFi QR Code Generator',
description: 'Share WiFi instantly with a QR code. No typing passwords.',
},
robots: {
index: true,
follow: true,
},
};
// JSON-LD Structured Data
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
// SoftwareApplication Schema
generateSoftwareAppSchema(
'WiFi QR Code Generator',
'Generate QR codes for WiFi networks. Guests scan to connect without typing passwords.',
'/og-wifi-generator.png'
),
// HowTo Schema for Featured Snippets
{
'@type': 'HowTo',
name: 'How to Create a WiFi QR Code',
datePublished: '2024-01-01',
dateModified: '2026-04-27',
author: {
'@type': 'Person',
name: 'Timo Knuth',
url: 'https://www.qrmaster.net/authors/timo',
},
description: 'Create a QR code that connects devices to your WiFi network automatically.',
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Enter Network Name',
text: 'Type your WiFi network name (SSID) in the Network Name field.',
},
{
'@type': 'HowToStep',
position: 2,
name: 'Enter Password',
text: 'Enter your WiFi password. This is processed locally and never sent to any server.',
},
{
'@type': 'HowToStep',
position: 3,
name: 'Select Security Type',
text: 'Choose WPA/WPA2 (most common), WEP, or No Password for open networks.',
},
{
'@type': 'HowToStep',
position: 4,
name: 'Download QR Code',
text: 'Click Download PNG or SVG to save your QR code. Print it or share digitally.',
},
{
'@type': 'HowToStep',
position: 5,
name: 'Connect',
text: 'Print the code. Guests can scan it to join your network instantly.',
},
],
totalTime: 'PT1M',
},
// FAQPage Schema
generateFaqSchema({
'Is it safe to enter my WiFi password?': {
question: 'Is it safe to enter my WiFi password?',
answer: 'Yes, completely safe. This tool processes everything in your browser (client-side). Your password never leaves your device and is not sent to any server.',
},
'Do WiFi QR codes work on iPhone and Android?': {
question: 'Do WiFi QR codes work on iPhone and Android?',
answer: 'Yes. Both iOS (11+) and Android devices can scan WiFi QR codes using their built-in camera app. No additional apps required.',
},
'What happens if I change my WiFi password?': {
question: 'What happens if I change my WiFi password?',
answer: 'If you change your WiFi password, the old QR code will stop working. You\'ll need to generate a new QR code with the updated credentials.For frequently changing passwords, consider using dynamic QR codes.',
},
'Can I customize the QR code design?': {
question: 'Can I customize the QR code design?',
answer: 'Yes. You can change the QR code color and add frame labels like "Scan Me" or "WiFi" to make it more recognizable and user-friendly.',
},
'Does it work for hidden networks?': {
question: 'Does it work for hidden networks?',
answer: 'Yes, just check the "Hidden Network" box if your SSID is hidden. The QR code contains the standard WiFi string configuration.',
},
}),
],
};
export default function WiFiQRCodePage() {
return (
<>
{/* JSON-LD Script */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<ToolBreadcrumb toolName="WiFi QR Code Generator" toolSlug="wifi-qr-code" />
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
{/* 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" style={{ backgroundColor: '#1A1265' }}>
{/* Background Pattern */}
<div className="absolute inset-0 opacity-10">
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
</linearGradient>
</defs>
</svg>
</div>
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
{/* Left: Text Content */}
<div className="text-center lg:text-left">
{/* Badge */}
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
<span className="flex h-2 w-2 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400"></span>
</span>
Free Tool No Signup Required
</div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
The Safest Way to <br className="hidden lg:block" />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-cyan-400">Share Your WiFi</span>
</h1>
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
Generate a secure QR code in seconds. No more spelling out complicated passwords.
<strong className="text-white block sm:inline mt-2 sm:mt-0"> 100% Client-Side & Private.</strong>
</p>
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<Lock className="w-4 h-4 text-emerald-400" />
No Server Uploads
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<Zap className="w-4 h-4 text-amber-400" />
Instant Connect
</div>
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
<Smartphone className="w-4 h-4 text-purple-400" />
iOS & Android
</div>
</div>
</div>
{/* Right: Visual Abstract Composition */}
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
{/* Decorative Glow */}
<div className="absolute w-[500px] h-[500px] bg-indigo-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
{/* Floating Glass Card */}
<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-6 hover:rotate-3 transition-all duration-700 group">
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
{/* Mock QR */}
<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" size={170} fgColor="#0f172a" level="Q" />
{/* Scan Line */}
<div className="absolute top-1/2 left-0 w-full h-1 bg-emerald-500 shadow-[0_0_20px_rgba(16,185,129,1)] animate-pulse" />
</div>
<div className="w-full space-y-3">
<div className="h-2 w-32 bg-white/20 rounded-full mx-auto" />
<div className="h-2 w-20 bg-white/10 rounded-full mx-auto" />
</div>
{/* Floating Badge */}
<div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
<div className="bg-emerald-100 p-2 rounded-full">
<Wifi className="w-5 h-5 text-emerald-600" />
</div>
<div className="text-left">
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Status</div>
<div className="text-sm font-bold text-slate-900">Connected</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* GENERATOR SECTION */}
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
<WiFiGenerator />
</section>
{/* HOW IT WORKS - AEO/GEO Content */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
How WiFi QR Codes Work
</h2>
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Wifi className="w-6 h-6 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">1. Network</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Enter your WiFi SSID and password.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Shield className="w-6 h-6 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">2. Security</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Select WPA/WPA2 encryption.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Zap className="w-6 h-6 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">3. Style</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Customize colors and add a frame.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Download className="w-6 h-6 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Get your high-quality QR image.
</p>
</article>
<article className="text-center">
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
<Smartphone className="w-6 h-6 text-[#1A1265]" />
</div>
<h3 className="font-bold text-slate-900 mb-2">5. Connect</h3>
<p className="text-slate-600 text-xs leading-relaxed">
Print it out. Guests scan to join!
</p>
</article>
</div>
</div>
</section>
{/* WHY WIFI QR CODES MATTER — STATISTICS */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="w-5 h-5 text-emerald-500" />
<span className="text-sm font-semibold text-emerald-600 uppercase tracking-wider">Research-backed</span>
</div>
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Why WiFi QR Codes Improve Customer Experience
</h2>
<p className="text-slate-600 mb-10 max-w-2xl">
A <strong>WiFi QR code</strong> eliminates the single biggest friction point between your guest and your network manual password entry.
</p>
<div className="grid md:grid-cols-2 gap-6 mb-8">
<div className="bg-emerald-50 border border-emerald-100 rounded-2xl p-6">
<div className="text-3xl font-extrabold text-emerald-600 mb-2">#1 Amenity</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
Free WiFi is rated the most important hotel amenity by guests ahead of breakfast, parking, and loyalty points. Instant, frictionless access directly impacts satisfaction scores and repeat bookings.
</p>
<p className="text-xs text-slate-500">
Source: <a href="https://www.jdpower.com/business/travel-hospitality/hotel-guest-satisfaction-study" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">J.D. Power Hotel Guest Satisfaction Study</a>
</p>
</div>
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-6">
<div className="text-3xl font-extrabold text-blue-600 mb-2">Effort = Loyalty</div>
<p className="text-slate-700 text-sm leading-relaxed mb-3">
Reducing customer effort like eliminating manual password entry is the single strongest predictor of customer loyalty. The lower the effort, the higher the repeat visit rate and positive word-of-mouth.
</p>
<p className="text-xs text-slate-500">
Source: <a href="https://hbr.org/2010/07/stop-trying-to-delight-your-customers" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700">Harvard Business Review "Stop Trying to Delight Your Customers"</a> (Customer Effort Score research)
</p>
</div>
</div>
<p className="text-xs text-slate-400 italic">
By Timo Knuth, QR Master · Last updated: June 2025 · Based on independent academic and industry research
</p>
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION - Featured Snippet Optimized */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Frequently Asked Questions
</h2>
<p className="text-slate-600 text-center mb-10">
Everything you need to know about WiFi QR codes.
</p>
<div className="space-y-4">
<FaqItem
question="Is it safe to enter my WiFi password here?"
answer="Yes, completely safe. This tool uses client-side processing, meaning your WiFi password never leaves your device. It's processed locally in your browser to generate the QR code—no data is sent to any server."
/>
<FaqItem
question="Do WiFi QR codes work on iPhone and Android?"
answer="Yes. iOS 11 and later, as well as all modern Android devices, can scan WiFi QR codes using the built-in camera app. Simply point the camera at the QR code and tap the notification to connect."
/>
<FaqItem
question="What happens if I change my WiFi password?"
answer="If you change your WiFi password, the old QR code will stop working. You'll need to generate a new QR code with the updated credentials. For frequently changing passwords, consider using dynamic QR codes."
/>
<FaqItem
question="Can I customize the QR code design?"
answer="Yes. You can change the foreground color of the QR code and add frame labels such as 'Scan Me', 'WiFi', or 'Connect' to make your QR code more recognizable and user-friendly."
/>
<FaqItem
question="Does it work for hidden networks?"
answer="Yes, just check the 'Hidden Network' box if your SSID is hidden. The QR code contains the standard WiFi string configuration."
/>
</div>
</div>
</section>
</div>
</>
);
}
// FAQ Item Component
function FaqItem({ question, answer }: { question: string; answer: string }) {
return (
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
{question}
<span className="transition group-open:rotate-180 text-slate-400">
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
<path d="M6 9l6 6 6-6" />
</svg>
</span>
</summary>
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
{answer}
</div>
</details>
);
}

View File

@@ -1,78 +1,77 @@
import { notFound } from "next/navigation";
import {
buildUseCaseMetadata,
UseCasePageTemplate,
} from "@/components/marketing/UseCasePageTemplate";
import {
allUseCases,
getUseCasePage,
} from "@/lib/growth-pages";
export function generateStaticParams() {
return allUseCases.map((item) => ({
slug: item.slug,
}));
}
export function generateMetadata({ params }: { params: { slug: string } }) {
const page = getUseCasePage(params.slug);
if (!page) {
return {};
}
return buildUseCaseMetadata({
title: page.title,
description: page.metaDescription,
canonicalPath: page.href,
});
}
export default function UseCaseDetailPage({
params,
}: {
params: { slug: string };
}) {
const page = getUseCasePage(params.slug);
if (!page) {
notFound();
}
return (
<UseCasePageTemplate
title={page.title}
description={page.metaDescription}
eyebrow={page.eyebrow}
intro={page.intro}
pageType="use_case"
cluster={page.cluster}
useCase={page.slug}
breadcrumbs={[
{ name: "Home", url: "/" },
{ name: "Use Cases", url: "/use-cases" },
{ name: page.title, url: page.href },
]}
answer={page.answer}
whenToUse={page.whenToUse}
comparisonItems={page.comparisonItems}
howToSteps={page.howToSteps}
primaryCta={{
href: page.parentHref,
label: page.ctaLabel,
}}
secondaryCta={{
href: "/use-cases",
label: "Explore more use cases",
}}
workflowTitle={page.workflowTitle}
workflowIntro={page.workflowIntro}
workflowCards={page.workflowCards}
checklistTitle={page.checklistTitle}
checklist={page.checklist}
supportLinks={page.supportLinks}
faq={page.faq}
/>
);
}
import { notFound } from 'next/navigation';
import {
buildUseCaseMetadata,
UseCasePageTemplate,
} from '@/components/marketing/UseCasePageTemplate';
import { allUseCases, getUseCasePage } from '@/lib/growth-pages';
export function generateStaticParams() {
return allUseCases.map((item) => ({
slug: item.slug,
}));
}
export function generateMetadata({ params }: { params: { slug: string } }) {
const page = getUseCasePage(params.slug);
if (!page) {
return {};
}
return buildUseCaseMetadata({
title: page.title,
description: page.metaDescription,
canonicalPath: page.href,
});
}
export default function UseCaseDetailPage({
params,
}: {
params: { slug: string };
}) {
const page = getUseCasePage(params.slug);
if (!page) {
notFound();
}
return (
<UseCasePageTemplate
title={page.title}
description={page.metaDescription}
eyebrow={page.eyebrow}
intro={page.intro}
pageType="use_case"
cluster={page.cluster}
useCase={page.slug}
breadcrumbs={[
{ name: 'Home', url: '/' },
{ name: 'Use Cases', url: '/use-cases' },
{ name: page.title, url: page.href },
]}
answer={page.answer}
whenToUse={page.whenToUse}
comparisonItems={page.comparisonItems}
howToSteps={page.howToSteps}
primaryCta={{
href: page.parentHref,
label: page.ctaLabel,
}}
secondaryCta={{
href: '/use-cases',
label: 'Explore more use cases',
}}
workflowTitle={page.workflowTitle}
workflowIntro={page.workflowIntro}
workflowCards={page.workflowCards}
checklistTitle={page.checklistTitle}
checklist={page.checklist}
supportLinks={page.supportLinks}
faq={page.faq}
heroImage={page.heroImage}
heroImageAlt={page.heroImageAlt}
/>
);
}

View File

@@ -205,10 +205,66 @@ export default function UseCasesHubPage() {
))}
</div>
</div>
</section>
<section className="bg-slate-50 py-16">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
</section>
<section className="bg-white py-16">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-8 max-w-3xl">
<div className="text-sm font-semibold uppercase tracking-[0.22em] text-blue-700">
Industry pages with early traction
</div>
<h2 className="mt-3 text-3xl font-bold text-slate-900">
Service business QR workflows
</h2>
<p className="mt-4 text-lg leading-8 text-slate-600">
These industry routes connect practical use cases with the QR
tools local businesses usually need first.
</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
{[
{
href: "/qr-code-for/barbershops",
title: "QR Codes for Barbershops",
description:
"Booking links, review prompts, WiFi access, and social profiles for barbershop visits.",
},
{
href: "/tools/wifi-qr-code",
title: "WiFi QR Code Generator",
description:
"A practical guest-facing QR tool for service businesses and venues.",
},
{
href: "/qr-code-erstellen",
title: "QR Code Erstellen",
description:
"German QR creation page for local business owners who search in German.",
},
].map((item) => (
<Link
key={item.href}
href={item.href}
className="rounded-2xl border border-slate-200 bg-slate-50 p-6 transition-colors hover:border-blue-200 hover:bg-blue-50/70"
>
<h3 className="text-xl font-bold text-slate-900">
{item.title}
</h3>
<p className="mt-3 text-sm leading-6 text-slate-600">
{item.description}
</p>
<div className="mt-5 inline-flex items-center text-sm font-semibold text-blue-700">
Open page <ArrowRight className="ml-2 h-4 w-4" />
</div>
</Link>
))}
</div>
</div>
</section>
<section className="bg-slate-50 py-16">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,0.95fr)]">
<Card className="rounded-3xl border-slate-200 bg-white p-8 shadow-sm">
<div className="flex items-center gap-3">

View File

@@ -0,0 +1,430 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import {
getGoalLabel,
getLifecycleStageLabel,
getRoleLabel,
getSourceLabel,
getTeamSizeLabel,
getUseCaseLabel,
} from '@/lib/revops';
import { db } from '@/lib/db';
import { getMetricSnapshot, getUpgradeCandidateBadges } from '@/lib/revops-server';
export const dynamic = 'force-dynamic';
type HydratedUser = {
id: string;
name: string | null;
email: string;
emailDomain: string | null;
plan: string;
lifecycleStage: string;
fitScore: number;
intentScore: number;
leadScore: number;
signupSource: string | null;
signupSourceSelfReported: string | null;
signupCampaign: string | null;
signupLandingPath: string | null;
primaryUseCase: string | null;
primaryGoal: string | null;
jobRole: string | null;
companyName: string | null;
companyWebsite: string | null;
teamSizeBucket: string | null;
createdAt: string;
firstQrCreatedAt: string | null;
activationAt: string | null;
firstDynamicQrAt: string | null;
qrCount: number;
dynamicQrCount: number;
scanCount: number;
contentTypeCount: number;
upgradeBadges: string[];
};
function hasAdminSession() {
const adminCookie = cookies().get('newsletter-admin');
return adminCookie?.value === 'authenticated';
}
function toIso(value: Date | null) {
return value ? value.toISOString() : null;
}
function safeDate(value: string | null) {
if (!value) return null;
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
function applyUserFilters(users: HydratedUser[], request: NextRequest) {
const stage = request.nextUrl.searchParams.get('stage');
const source = request.nextUrl.searchParams.get('source');
const campaign = request.nextUrl.searchParams.get('campaign');
const landingPath = request.nextUrl.searchParams.get('landingPath');
const useCase = request.nextUrl.searchParams.get('useCase');
const goal = request.nextUrl.searchParams.get('goal');
const role = request.nextUrl.searchParams.get('role');
const teamSize = request.nextUrl.searchParams.get('teamSize');
const plan = request.nextUrl.searchParams.get('plan');
const search = request.nextUrl.searchParams.get('search')?.toLowerCase().trim();
const from = safeDate(request.nextUrl.searchParams.get('from'));
const to = safeDate(request.nextUrl.searchParams.get('to'));
return users.filter((user) => {
const createdAt = new Date(user.createdAt);
const matchesSearch = !search || [
user.name,
user.email,
user.companyName,
user.emailDomain,
].filter(Boolean).some((value) => value!.toLowerCase().includes(search));
return (
(!stage || user.lifecycleStage === stage) &&
(!source || user.signupSource === source) &&
(!campaign || user.signupCampaign === campaign) &&
(!landingPath || user.signupLandingPath === landingPath) &&
(!useCase || user.primaryUseCase === useCase) &&
(!goal || user.primaryGoal === goal) &&
(!role || user.jobRole === role) &&
(!teamSize || user.teamSizeBucket === teamSize) &&
(!plan || user.plan === plan) &&
(!from || createdAt >= from) &&
(!to || createdAt <= to) &&
matchesSearch
);
});
}
function sortUsers(users: HydratedUser[], sort: string) {
const sorted = [...users];
sorted.sort((a, b) => {
switch (sort) {
case 'createdAt_asc':
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
case 'activationAt_desc':
return new Date(b.activationAt || 0).getTime() - new Date(a.activationAt || 0).getTime();
case 'leadScore_asc':
return a.leadScore - b.leadScore;
case 'fitScore_desc':
return b.fitScore - a.fitScore;
case 'intentScore_desc':
return b.intentScore - a.intentScore;
case 'leadScore_desc':
default:
return b.leadScore - a.leadScore || new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
}
});
return sorted;
}
function buildGroupedRows(users: HydratedUser[], key: keyof HydratedUser) {
const rows = new Map<string, {
key: string;
signups: number;
firstQr: number;
activated: number;
hot: number;
upgradeCandidates: number;
paid: number;
}>();
users.forEach((user) => {
const rawValue = (user[key] as string | null) || 'unknown';
const row = rows.get(rawValue) || {
key: rawValue,
signups: 0,
firstQr: 0,
activated: 0,
hot: 0,
upgradeCandidates: 0,
paid: 0,
};
row.signups += 1;
if (user.firstQrCreatedAt) row.firstQr += 1;
if (user.activationAt) row.activated += 1;
if (user.lifecycleStage === 'hot') row.hot += 1;
if (user.lifecycleStage === 'upgrade_candidate') row.upgradeCandidates += 1;
if (user.lifecycleStage === 'paid') row.paid += 1;
rows.set(rawValue, row);
});
return Array.from(rows.values()).sort((a, b) => b.signups - a.signups);
}
function buildFunnel(users: HydratedUser[]) {
return {
signup: users.length,
sourceConfirmed: users.filter((user) => Boolean(user.signupSourceSelfReported)).length,
useCaseSelected: users.filter((user) => Boolean(user.primaryUseCase)).length,
goalSelected: users.filter((user) => Boolean(user.primaryGoal)).length,
profileCaptured: users.filter((user) => Boolean(user.jobRole && user.teamSizeBucket)).length,
firstQrCreated: users.filter((user) => Boolean(user.firstQrCreatedAt)).length,
firstDynamicQrCreated: users.filter((user) => Boolean(user.firstDynamicQrAt)).length,
activated: users.filter((user) => Boolean(user.activationAt)).length,
};
}
function buildLifecycleSummary(users: HydratedUser[]) {
return {
cold: users.filter((user) => user.lifecycleStage === 'cold').length,
activated: users.filter((user) => user.lifecycleStage === 'activated').length,
warm: users.filter((user) => user.lifecycleStage === 'warm').length,
hot: users.filter((user) => user.lifecycleStage === 'hot').length,
upgrade_candidate: users.filter((user) => user.lifecycleStage === 'upgrade_candidate').length,
paid: users.filter((user) => user.lifecycleStage === 'paid').length,
};
}
function buildCsv(rows: HydratedUser[]) {
const headers = [
'name',
'email',
'email_domain',
'plan',
'lifecycle_stage',
'fit_score',
'intent_score',
'lead_score',
'source',
'self_reported_source',
'campaign',
'landing_page',
'use_case',
'goal',
'role',
'company',
'team_size',
'created_at',
'first_qr_created_at',
'activation_at',
'qr_count',
'dynamic_qr_count',
'scan_count',
];
const escape = (value: string | number | null) => {
const normalized = value == null ? '' : String(value);
return `"${normalized.replace(/"/g, '""')}"`;
};
const lines = rows.map((row) => [
row.name,
row.email,
row.emailDomain,
row.plan,
row.lifecycleStage,
row.fitScore,
row.intentScore,
row.leadScore,
row.signupSource,
row.signupSourceSelfReported,
row.signupCampaign,
row.signupLandingPath,
row.primaryUseCase,
row.primaryGoal,
row.jobRole,
row.companyName,
row.teamSizeBucket,
row.createdAt,
row.firstQrCreatedAt,
row.activationAt,
row.qrCount,
row.dynamicQrCount,
row.scanCount,
].map(escape).join(','));
return [headers.join(','), ...lines].join('\n');
}
export async function GET(request: NextRequest) {
try {
if (!hasAdminSession()) {
return NextResponse.json({ error: 'Unauthorized - Admin login required' }, { status: 401 });
}
const rawUsers = await db.user.findMany({
select: {
id: true,
name: true,
email: true,
emailDomain: true,
plan: true,
lifecycleStage: true,
fitScore: true,
intentScore: true,
leadScore: true,
signupSource: true,
signupSourceSelfReported: true,
signupCampaign: true,
signupLandingPath: true,
primaryUseCase: true,
primaryGoal: true,
jobRole: true,
companyName: true,
companyWebsite: true,
teamSizeBucket: true,
createdAt: true,
firstQrCreatedAt: true,
firstDynamicQrAt: true,
activationAt: true,
qrCodes: {
select: {
type: true,
contentType: true,
createdAt: true,
_count: {
select: {
scans: true,
},
},
},
},
},
orderBy: {
createdAt: 'desc',
},
});
const users: HydratedUser[] = rawUsers.map((user) => {
const metrics = getMetricSnapshot(user.qrCodes);
return {
id: user.id,
name: user.name,
email: user.email,
emailDomain: user.emailDomain,
plan: user.plan,
lifecycleStage: user.lifecycleStage,
fitScore: user.fitScore,
intentScore: user.intentScore,
leadScore: user.leadScore,
signupSource: user.signupSource,
signupSourceSelfReported: user.signupSourceSelfReported,
signupCampaign: user.signupCampaign,
signupLandingPath: user.signupLandingPath,
primaryUseCase: user.primaryUseCase,
primaryGoal: user.primaryGoal,
jobRole: user.jobRole,
companyName: user.companyName,
companyWebsite: user.companyWebsite,
teamSizeBucket: user.teamSizeBucket,
createdAt: user.createdAt.toISOString(),
firstQrCreatedAt: toIso(user.firstQrCreatedAt),
activationAt: toIso(user.activationAt),
firstDynamicQrAt: toIso(user.firstDynamicQrAt),
qrCount: metrics.qrCount,
dynamicQrCount: metrics.dynamicQrCount,
scanCount: metrics.scanCount,
contentTypeCount: metrics.contentTypeCount,
upgradeBadges: getUpgradeCandidateBadges(user, metrics),
};
});
const filteredUsers = sortUsers(
applyUserFilters(users, request),
request.nextUrl.searchParams.get('sort') || 'leadScore_desc'
);
if (request.nextUrl.searchParams.get('format') === 'csv') {
const csv = buildCsv(filteredUsers);
return new NextResponse(csv, {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': 'attachment; filename="qrmaster-revops-export.csv"',
},
});
}
const page = Number(request.nextUrl.searchParams.get('page') || '1');
const pageSize = Number(request.nextUrl.searchParams.get('pageSize') || '25');
const total = filteredUsers.length;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const paginatedUsers = filteredUsers.slice((page - 1) * pageSize, page * pageSize);
const acquisitionBySource = buildGroupedRows(users, 'signupSource').map((row) => ({
...row,
label: getSourceLabel(row.key),
activationRate: row.signups ? Math.round((row.activated / row.signups) * 100) : 0,
}));
const acquisitionByCampaign = buildGroupedRows(users, 'signupCampaign');
const acquisitionByLandingPath = buildGroupedRows(users, 'signupLandingPath');
const funnel = buildFunnel(users);
const lifecycleSummary = buildLifecycleSummary(users);
const mismatchCount = users.filter(
(user) =>
user.signupSource &&
user.signupSourceSelfReported &&
user.signupSource !== user.signupSourceSelfReported
).length;
const upgradeCandidates = users
.filter((user) => user.plan === 'FREE' && user.lifecycleStage === 'upgrade_candidate')
.sort((a, b) => b.leadScore - a.leadScore)
.slice(0, 25);
const filterOptions = {
stages: ['cold', 'activated', 'warm', 'hot', 'upgrade_candidate', 'paid'],
sources: Array.from(new Set(users.map((user) => user.signupSource).filter((value): value is string => Boolean(value)))),
campaigns: Array.from(new Set(users.map((user) => user.signupCampaign).filter((value): value is string => Boolean(value)))),
landingPaths: Array.from(new Set(users.map((user) => user.signupLandingPath).filter((value): value is string => Boolean(value)))),
useCases: Array.from(new Set(users.map((user) => user.primaryUseCase).filter((value): value is string => Boolean(value)))),
goals: Array.from(new Set(users.map((user) => user.primaryGoal).filter((value): value is string => Boolean(value)))),
roles: Array.from(new Set(users.map((user) => user.jobRole).filter((value): value is string => Boolean(value)))),
teamSizes: Array.from(new Set(users.map((user) => user.teamSizeBucket).filter((value): value is string => Boolean(value)))),
plans: Array.from(new Set(users.map((user) => user.plan).filter((value): value is string => Boolean(value)))),
};
return NextResponse.json({
overview: {
totalUsers: users.length,
mismatchCount,
activatedUsers: funnel.activated,
paidUsers: lifecycleSummary.paid,
},
acquisition: {
bySource: acquisitionBySource,
byCampaign: acquisitionByCampaign.slice(0, 15),
byLandingPath: acquisitionByLandingPath.slice(0, 15),
},
funnel,
funnelBreakdowns: {
bySource: acquisitionBySource.slice(0, 10),
byUseCase: buildGroupedRows(users, 'primaryUseCase').map((row) => ({ ...row, label: getUseCaseLabel(row.key) })),
byRole: buildGroupedRows(users, 'jobRole').map((row) => ({ ...row, label: getRoleLabel(row.key) })),
byTeamSize: buildGroupedRows(users, 'teamSizeBucket').map((row) => ({ ...row, label: getTeamSizeLabel(row.key) })),
},
lifecycleSummary,
campaignSourceQuality: acquisitionBySource,
upgradeCandidates,
filterOptions,
segments: {
total,
page,
pageSize,
totalPages,
rows: paginatedUsers.map((user) => ({
...user,
lifecycleStageLabel: getLifecycleStageLabel(user.lifecycleStage),
signupSourceLabel: getSourceLabel(user.signupSource),
signupSourceSelfReportedLabel: getSourceLabel(user.signupSourceSelfReported),
primaryUseCaseLabel: getUseCaseLabel(user.primaryUseCase),
primaryGoalLabel: getGoalLabel(user.primaryGoal),
jobRoleLabel: getRoleLabel(user.jobRole),
teamSizeLabel: getTeamSizeLabel(user.teamSizeBucket),
})),
},
});
} catch (error) {
console.error('Error fetching RevOps dashboard data:', error);
return NextResponse.json({ error: 'Failed to fetch RevOps dashboard data' }, { status: 500 });
}
}

View File

@@ -1,14 +1,32 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { cookies } from 'next/headers';
import { getAuthCookieOptions } from '@/lib/cookieConfig';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
// If no code, redirect to Google OAuth
if (!code) {
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getAuthCookieOptions } from '@/lib/cookieConfig';
import {
appendRedirectParam,
GOOGLE_OAUTH_STATE_COOKIE_NAME,
POST_AUTH_REDIRECT_COOKIE_NAME,
sanitizeRedirectPath,
} from '@/lib/auth-flow';
import {
ATTRIBUTION_COOKIE_NAME,
getEmailDomain,
parseAttributionCookie,
shouldResumeOnboarding,
} from '@/lib/revops';
import { triggerLifecycleScoring } from '@/lib/revops-server';
const isProduction = process.env.NODE_ENV === 'production';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const state = searchParams.get('state');
const firstTouch = parseAttributionCookie(request.cookies.get(ATTRIBUTION_COOKIE_NAME)?.value);
const savedOauthState = request.cookies.get(GOOGLE_OAUTH_STATE_COOKIE_NAME)?.value;
const savedRedirect = sanitizeRedirectPath(request.cookies.get(POST_AUTH_REDIRECT_COOKIE_NAME)?.value);
// If no code, redirect to Google OAuth
if (!code) {
const googleClientId = process.env.GOOGLE_CLIENT_ID;
if (!googleClientId) {
@@ -17,19 +35,56 @@ export async function GET(request: NextRequest) {
{ status: 500 }
);
}
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/google`;
const scope = 'openid email profile';
const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleClientId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}`;
return NextResponse.redirect(googleAuthUrl);
}
// Handle callback with code
try {
const googleClientId = process.env.GOOGLE_CLIENT_ID;
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/google`;
const scope = 'openid email profile';
const redirectTarget = sanitizeRedirectPath(searchParams.get('redirect'));
const oauthState = crypto.randomUUID();
const googleAuthUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
googleAuthUrl.searchParams.set('client_id', googleClientId);
googleAuthUrl.searchParams.set('redirect_uri', redirectUri);
googleAuthUrl.searchParams.set('response_type', 'code');
googleAuthUrl.searchParams.set('scope', scope);
googleAuthUrl.searchParams.set('state', oauthState);
const response = NextResponse.redirect(googleAuthUrl);
response.cookies.set(GOOGLE_OAUTH_STATE_COOKIE_NAME, oauthState, {
httpOnly: true,
secure: isProduction,
sameSite: 'lax',
path: '/',
maxAge: 60 * 10,
});
if (redirectTarget) {
response.cookies.set(POST_AUTH_REDIRECT_COOKIE_NAME, redirectTarget, {
httpOnly: true,
secure: isProduction,
sameSite: 'lax',
path: '/',
maxAge: 60 * 10,
});
} else {
response.cookies.delete(POST_AUTH_REDIRECT_COOKIE_NAME);
}
return response;
}
// Handle callback with code
try {
if (!state || !savedOauthState || state !== savedOauthState) {
const invalidStateResponse = NextResponse.redirect(
`${process.env.NEXT_PUBLIC_APP_URL}/login?error=google-state-invalid`
);
invalidStateResponse.cookies.delete(GOOGLE_OAUTH_STATE_COOKIE_NAME);
invalidStateResponse.cookies.delete(POST_AUTH_REDIRECT_COOKIE_NAME);
return invalidStateResponse;
}
const googleClientId = process.env.GOOGLE_CLIENT_ID;
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
if (!googleClientId || !googleClientSecret) {
return NextResponse.json(
@@ -50,9 +105,9 @@ export async function GET(request: NextRequest) {
code,
client_id: googleClientId,
client_secret: googleClientSecret,
redirect_uri: redirectUri,
grant_type: 'authorization_code',
}),
redirect_uri: redirectUri,
grant_type: 'authorization_code',
}),
});
if (!tokenResponse.ok) {
@@ -82,16 +137,27 @@ export async function GET(request: NextRequest) {
const isNewUser = !user;
// Create user if they don't exist
if (!user) {
user = await db.user.create({
data: {
email: userInfo.email,
name: userInfo.name || userInfo.email.split('@')[0],
image: userInfo.picture,
emailVerified: new Date(), // Google already verified the email
password: null, // OAuth users don't need a password
},
});
if (!user) {
const onboardingStartedAt = new Date();
user = await db.user.create({
data: {
email: userInfo.email,
name: userInfo.name || userInfo.email.split('@')[0],
image: userInfo.picture,
emailVerified: new Date(), // Google already verified the email
password: null, // OAuth users don't need a password
onboardingStartedAt,
emailDomain: getEmailDomain(userInfo.email),
signupSource: firstTouch?.signupSource || null,
signupMedium: firstTouch?.signupMedium || null,
signupCampaign: firstTouch?.signupCampaign || null,
signupContent: firstTouch?.signupContent || null,
signupTerm: firstTouch?.signupTerm || null,
signupReferrer: firstTouch?.signupReferrer || null,
signupLandingPath: firstTouch?.signupLandingPath || '/signup',
signupFirstSeenAt: firstTouch?.signupFirstSeenAt ? new Date(firstTouch.signupFirstSeenAt) : onboardingStartedAt,
},
});
// Create Account entry for the OAuth provider
await db.account.create({
@@ -144,22 +210,35 @@ export async function GET(request: NextRequest) {
id_token: tokens.id_token,
},
});
}
}
// Set authentication cookie
cookies().set('userId', user.id, getAuthCookieOptions());
// Redirect to dashboard with tracking params
const redirectUrl = new URL(`${process.env.NEXT_PUBLIC_APP_URL}/dashboard`);
redirectUrl.searchParams.set('authMethod', 'google');
redirectUrl.searchParams.set('isNewUser', isNewUser.toString());
return NextResponse.redirect(redirectUrl.toString());
} catch (error) {
console.error('Google OAuth error:', error);
return NextResponse.redirect(
`${process.env.NEXT_PUBLIC_APP_URL}/login?error=google-signin-failed`
);
}
}
}
}
triggerLifecycleScoring(user.id, isNewUser ? 'signup' : 'subscription_changed');
const onboardingTarget = isNewUser || shouldResumeOnboarding(user)
? appendRedirectParam('/onboarding', savedRedirect, {
authMethod: 'google',
isNewUser: isNewUser.toString(),
})
: (savedRedirect || appendRedirectParam('/dashboard', null, {
authMethod: 'google',
isNewUser: isNewUser.toString(),
}));
const redirectUrl = new URL(`${process.env.NEXT_PUBLIC_APP_URL}${onboardingTarget}`);
const response = NextResponse.redirect(redirectUrl.toString());
response.cookies.set('userId', user.id, getAuthCookieOptions());
response.cookies.delete(GOOGLE_OAUTH_STATE_COOKIE_NAME);
response.cookies.delete(POST_AUTH_REDIRECT_COOKIE_NAME);
response.cookies.delete(ATTRIBUTION_COOKIE_NAME);
return response;
} catch (error) {
console.error('Google OAuth error:', error);
const errorResponse = NextResponse.redirect(
`${process.env.NEXT_PUBLIC_APP_URL}/login?error=google-signin-failed`
);
errorResponse.cookies.delete(GOOGLE_OAUTH_STATE_COOKIE_NAME);
errorResponse.cookies.delete(POST_AUTH_REDIRECT_COOKIE_NAME);
return errorResponse;
}
}

View File

@@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
import { ATTRIBUTION_COOKIE_NAME } from '@/lib/revops';
export async function POST() {
const response = NextResponse.json({ success: true });
response.cookies.set('userId', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 0,
});
response.cookies.set('newsletter-admin', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 0,
});
response.cookies.set(ATTRIBUTION_COOKIE_NAME, '', {
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 0,
});
return response;
}

View File

@@ -1,13 +1,19 @@
import { NextRequest, NextResponse } from 'next/server';
import bcrypt from 'bcryptjs';
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
import { NextRequest, NextResponse } from 'next/server';
import bcrypt from 'bcryptjs';
import { db } from '@/lib/db';
import { z } from 'zod';
import { csrfProtection } from '@/lib/csrf';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
import { getAuthCookieOptions } from '@/lib/cookieConfig';
import { signupSchema, validateRequest } from '@/lib/validationSchemas';
import { sendWelcomeEmail } from '@/lib/email';
import { getAuthCookieOptions } from '@/lib/cookieConfig';
import { signupSchema, validateRequest } from '@/lib/validationSchemas';
import { sendWelcomeEmail } from '@/lib/email';
import { sendConversionEvent } from '@/lib/metaConversions';
import {
ATTRIBUTION_COOKIE_NAME,
getEmailDomain,
parseAttributionCookie,
} from '@/lib/revops';
import { triggerLifecycleScoring } from '@/lib/revops-server';
export async function POST(request: NextRequest) {
try {
@@ -66,14 +72,29 @@ export async function POST(request: NextRequest) {
// Hash password
const hashedPassword = await bcrypt.hash(password, 12);
// Create user
const user = await db.user.create({
data: {
name,
email,
password: hashedPassword,
},
});
const firstTouch = parseAttributionCookie(request.cookies.get(ATTRIBUTION_COOKIE_NAME)?.value);
const onboardingStartedAt = new Date();
// Create user
const user = await db.user.create({
data: {
name,
email,
password: hashedPassword,
onboardingStartedAt,
emailDomain: getEmailDomain(email),
signupSource: firstTouch?.signupSource || null,
signupMedium: firstTouch?.signupMedium || null,
signupCampaign: firstTouch?.signupCampaign || null,
signupContent: firstTouch?.signupContent || null,
signupTerm: firstTouch?.signupTerm || null,
signupReferrer: firstTouch?.signupReferrer || null,
signupLandingPath: firstTouch?.signupLandingPath || '/signup',
signupFirstSeenAt: firstTouch?.signupFirstSeenAt ? new Date(firstTouch.signupFirstSeenAt) : onboardingStartedAt,
},
});
triggerLifecycleScoring(user.id, 'signup');
// Send welcome email (fire-and-forget — never block signup)
try {
@@ -82,21 +103,36 @@ export async function POST(request: NextRequest) {
console.error('Welcome email failed:', emailError);
}
// Create response
const response = NextResponse.json({
success: true,
user: {
id: user.id,
name: user.name,
// Meta Conversions API — CompleteRegistration event
sendConversionEvent({
eventName: 'CompleteRegistration',
userData: {
email: user.email,
plan: 'FREE',
ip: request.headers.get('x-forwarded-for')?.split(',')[0] ?? undefined,
userAgent: request.headers.get('user-agent') ?? undefined,
fbc: request.cookies.get('_fbc')?.value,
fbp: request.cookies.get('_fbp')?.value,
},
eventSourceUrl: `${process.env.NEXT_PUBLIC_APP_URL}/signup`,
}).catch(console.error);
// Create response
const response = NextResponse.json({
success: true,
needsOnboarding: true,
user: {
id: user.id,
name: user.name,
email: user.email,
plan: 'FREE',
},
});
// Set cookie for auto-login after signup
response.cookies.set('userId', user.id, getAuthCookieOptions());
return response;
// Set cookie for auto-login after signup
response.cookies.set('userId', user.id, getAuthCookieOptions());
response.cookies.delete(ATTRIBUTION_COOKIE_NAME);
return response;
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
@@ -111,4 +147,4 @@ export async function POST(request: NextRequest) {
{ status: 500 }
);
}
}
}

View File

@@ -2,10 +2,11 @@ import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import bcrypt from 'bcryptjs';
import { cookies } from 'next/headers';
import { csrfProtection } from '@/lib/csrf';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
import { getAuthCookieOptions } from '@/lib/cookieConfig';
import { loginSchema, validateRequest } from '@/lib/validationSchemas';
import { csrfProtection } from '@/lib/csrf';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
import { getAuthCookieOptions } from '@/lib/cookieConfig';
import { loginSchema, validateRequest } from '@/lib/validationSchemas';
import { shouldResumeOnboarding } from '@/lib/revops';
export async function POST(request: NextRequest) {
try {
@@ -50,9 +51,18 @@ export async function POST(request: NextRequest) {
const { email, password } = validation.data;
// Find user
const user = await db.user.findUnique({
where: { email },
});
const user = await db.user.findUnique({
where: { email },
select: {
id: true,
email: true,
name: true,
plan: true,
password: true,
onboardingStartedAt: true,
onboardingCompletedAt: true,
},
});
if (!user) {
return NextResponse.json(
@@ -74,12 +84,13 @@ export async function POST(request: NextRequest) {
// Set cookie
cookies().set('userId', user.id, getAuthCookieOptions());
return NextResponse.json({
success: true,
user: { id: user.id, email: user.email, name: user.name, plan: user.plan || 'FREE' }
});
return NextResponse.json({
success: true,
needsOnboarding: shouldResumeOnboarding(user),
user: { id: user.id, email: user.email, name: user.name, plan: user.plan || 'FREE' }
});
} catch (error) {
console.error('Login error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
}

View File

@@ -0,0 +1,119 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
import { csrfProtection } from '@/lib/csrf';
import { getClientIdentifier, rateLimit, RateLimits } from '@/lib/rateLimit';
import { onboardingUpdateSchema, validateRequest } from '@/lib/validationSchemas';
import { getOnboardingState, triggerLifecycleScoring } from '@/lib/revops-server';
export const dynamic = 'force-dynamic';
export async function GET() {
try {
const userId = cookies().get('userId')?.value;
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const state = await getOnboardingState(userId);
if (!state) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
return NextResponse.json(state);
} catch (error) {
console.error('Error fetching onboarding state:', error);
return NextResponse.json({ error: 'Failed to fetch onboarding state' }, { status: 500 });
}
}
export async function PATCH(request: NextRequest) {
try {
const csrfCheck = csrfProtection(request);
if (!csrfCheck.valid) {
return NextResponse.json({ error: csrfCheck.error }, { status: 403 });
}
const userId = cookies().get('userId')?.value;
const clientId = userId || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.PROFILE_UPDATE);
if (!rateLimitResult.success) {
return NextResponse.json(
{
error: 'Too many requests. Please try again later.',
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000),
},
{ status: 429 }
);
}
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const validation = await validateRequest(onboardingUpdateSchema, body);
if (!validation.success) {
return NextResponse.json(validation.error, { status: 400 });
}
const data = validation.data;
const now = new Date();
const existingUser = await db.user.findUnique({
where: { id: userId },
select: {
onboardingStartedAt: true,
sourceConfirmedAt: true,
useCaseSelectedAt: true,
goalSelectedAt: true,
profileCompletedAt: true,
},
});
if (!existingUser) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
await db.user.update({
where: { id: userId },
data: {
onboardingStartedAt: existingUser.onboardingStartedAt ?? now,
signupSourceSelfReported: data.signupSourceSelfReported,
primaryUseCase: data.primaryUseCase,
primaryGoal: data.primaryGoal,
jobRole: data.jobRole,
companyName: data.companyName,
companyWebsite: data.companyWebsite,
teamSizeBucket: data.teamSizeBucket,
sourceConfirmedAt:
data.signupSourceSelfReported && !existingUser.sourceConfirmedAt
? now
: undefined,
useCaseSelectedAt:
data.primaryUseCase && !existingUser.useCaseSelectedAt
? now
: undefined,
goalSelectedAt:
data.primaryGoal && !existingUser.goalSelectedAt
? now
: undefined,
profileCompletedAt:
data.markProfileComplete && !existingUser.profileCompletedAt
? now
: undefined,
},
});
triggerLifecycleScoring(userId, 'onboarding_update');
const state = await getOnboardingState(userId);
return NextResponse.json({ success: true, state });
} catch (error) {
console.error('Error updating onboarding state:', error);
return NextResponse.json({ error: 'Failed to update onboarding state' }, { status: 500 });
}
}

View File

@@ -1,10 +1,12 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
import { generateSlug } from '@/lib/hash';
import { createQRSchema, validateRequest } from '@/lib/validationSchemas';
import { csrfProtection } from '@/lib/csrf';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
import { generateSlug } from '@/lib/hash';
import { createQRSchema, validateRequest } from '@/lib/validationSchemas';
import { csrfProtection } from '@/lib/csrf';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
import { DYNAMIC_QR_LIMITS } from '@/lib/plans';
import { triggerLifecycleScoring } from '@/lib/revops-server';
// GET /api/qrs - List user's QR codes
export async function GET(request: NextRequest) {
@@ -47,12 +49,7 @@ export async function GET(request: NextRequest) {
}
// Plan limits
const PLAN_LIMITS = {
FREE: 3,
PRO: 50,
BUSINESS: 500,
ENTERPRISE: 99999,
};
const PLAN_LIMITS = DYNAMIC_QR_LIMITS;
// POST /api/qrs - Create a new QR code
export async function POST(request: NextRequest) {
@@ -208,9 +205,9 @@ END:VCARD`;
const slug = generateSlug(body.title);
// Create QR code
const qrCode = await db.qRCode.create({
data: {
userId,
const qrCode = await db.qRCode.create({
data: {
userId,
title: body.title,
type: isStatic ? 'STATIC' : 'DYNAMIC',
contentType: body.contentType,
@@ -224,10 +221,12 @@ END:VCARD`;
},
slug,
status: 'ACTIVE',
},
});
return NextResponse.json(qrCode);
},
});
triggerLifecycleScoring(userId, 'qr_created');
return NextResponse.json(qrCode);
} catch (error) {
console.error('Error creating QR code:', error);
return NextResponse.json(
@@ -235,4 +234,4 @@ END:VCARD`;
{ status: 500 }
);
}
}
}

View File

@@ -1,8 +1,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
import { cookies } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
import { scoreUserLifecycle } from '@/lib/revops-server';
export async function POST(request: NextRequest) {
try {
@@ -53,32 +54,35 @@ export async function POST(request: NextRequest) {
// No active subscription
if (!user.stripeSubscriptionId) {
// Just update plan to FREE if somehow plan is not FREE but no subscription
await db.user.update({
where: { id: userId },
data: {
await db.user.update({
where: { id: userId },
data: {
plan: 'FREE',
stripePriceId: null,
stripeCurrentPeriodEnd: null,
},
});
return NextResponse.json({ success: true });
},
});
await scoreUserLifecycle(userId, 'subscription_changed');
return NextResponse.json({ success: true });
}
// Cancel the Stripe subscription
await stripe.subscriptions.cancel(user.stripeSubscriptionId);
// Update user plan to FREE
await db.user.update({
where: { id: userId },
data: {
await db.user.update({
where: { id: userId },
data: {
plan: 'FREE',
stripeSubscriptionId: null,
stripePriceId: null,
stripeCurrentPeriodEnd: null,
},
});
return NextResponse.json({ success: true });
},
});
await scoreUserLifecycle(userId, 'subscription_changed');
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error canceling subscription:', error);
return NextResponse.json(

View File

@@ -53,6 +53,7 @@ export async function POST(request: NextRequest) {
customer: customerId,
mode: 'subscription',
payment_method_types: ['card'],
allow_promotion_codes: true,
line_items: [
{
price: priceId,

View File

@@ -65,13 +65,29 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Create or get Stripe customer
let customerId = user.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: user.email,
metadata: {
// Create or get Stripe customer
let customerId = user.stripeCustomerId;
if (customerId) {
try {
const existingCustomer = await stripe.customers.retrieve(customerId);
if ('deleted' in existingCustomer && existingCustomer.deleted) {
customerId = null;
}
} catch (error: any) {
if (error?.code === 'resource_missing' || error?.type === 'StripeInvalidRequestError') {
customerId = null;
} else {
throw error;
}
}
}
if (!customerId) {
const customer = await stripe.customers.create({
email: user.email,
metadata: {
userId: user.id,
},
});
@@ -79,30 +95,34 @@ export async function POST(request: NextRequest) {
customerId = customer.id;
// Update user with Stripe customer ID
await db.user.update({
where: { id: user.id },
data: { stripeCustomerId: customerId },
});
}
// Create Stripe Checkout Session
const checkoutSession = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
await db.user.update({
where: { id: user.id },
data: { stripeCustomerId: customerId },
});
}
const appUrl = process.env.NEXT_PUBLIC_APP_URL || request.nextUrl.origin;
// Create Stripe Checkout Session
const checkoutSession = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
payment_method_types: ['card'],
allow_promotion_codes: true,
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
metadata: {
userId: user.id,
plan,
},
});
},
],
success_url: `${appUrl}/dashboard?success=true&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${appUrl}/pricing?canceled=true`,
metadata: {
userId: user.id,
plan,
billingInterval,
},
});
return NextResponse.json({ url: checkoutSession.url });
} catch (error) {

View File

@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db';
import { cookies } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db';
import { scoreUserLifecycle } from '@/lib/revops-server';
/**
* Manual sync endpoint to update user subscription from Stripe
@@ -37,17 +38,19 @@ export async function POST(request: NextRequest) {
if (subscriptions.data.length === 0) {
// No active subscription - set to FREE
await db.user.update({
where: { id: user.id },
data: {
await db.user.update({
where: { id: user.id },
data: {
stripeSubscriptionId: null,
stripePriceId: null,
stripeCurrentPeriodEnd: null,
plan: 'FREE',
},
});
return NextResponse.json({
},
});
await scoreUserLifecycle(user.id, 'subscription_changed');
return NextResponse.json({
success: true,
plan: 'FREE',
message: 'No active subscription found, set to FREE plan',
@@ -87,18 +90,20 @@ export async function POST(request: NextRequest) {
});
// Update user in database
await db.user.update({
where: { id: user.id },
data: {
await db.user.update({
where: { id: user.id },
data: {
stripeSubscriptionId: subscription.id,
stripePriceId: priceId,
stripeCurrentPeriodEnd: currentPeriodEnd,
plan: plan as any,
},
});
return NextResponse.json({
success: true,
},
});
await scoreUserLifecycle(user.id, 'subscription_changed');
return NextResponse.json({
success: true,
plan,
subscriptionId: subscription.id,
currentPeriodEnd,

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