Compare commits

..

1 Commits

Author SHA1 Message Date
Timo Knuth 05531cda3f 4 neue dynamischen 2026-01-11 01:29:21 +01:00
175 changed files with 9051 additions and 11271 deletions

0
..md Normal file
View File

2
.gitignore vendored
View File

@ -36,7 +36,7 @@ yarn-error.log*
next-env.d.ts
# prisma
/prisma/migrations/
# docker
docker-compose.override.yml

View File

@ -1,29 +0,0 @@
# Ahrefs SEO Findings & Status
## Critical Issues (Priority: High)
- [RESOLVED] **Page has no outgoing links**
- Found on: `privacy`, `newsletter`, `faq`, `/`, `qr-code-erstellen`
- *Status:* Verified `MarketingLayout` provides navigation. Added specific back-links to `newsletter` (admin), `login`, and `signup`.
- [RESOLVED] **Newsletter Page Misconfiguration**
- Found: `/newsletter` page has "Admin Dashboard" title.
- *Status:* Confirmed as internal Admin tool. Added "Back to Home" link to satisfy link checkers.
- [FIXED] **3XX Redirects & Links to Redirects**
- *Fixed in:* `blog/page.tsx` (links updated) and `blog/[slug]/page.tsx` (301s added).
- [FIXED] **Duplicate Metadata**
- *Fixed in:* `pricing`, `login`, `signup`, `qr-code-erstellen`.
## Warnings (Priority: Medium)
- [VERIFIED] **Hreflang and HTML lang mismatch**
- Found on: `1 page`.
- *Status:* Verified `src/app/(marketing)/layout.tsx` has `lang="en"` and `(marketing-de)/layout.tsx` has `lang="de"`. Correct.
- [FIXED] **Image file size too large**
- *Fixed:* Swapped `1-boy.png` & `2-body.png` for WebP versions as requested.
- [FIXED] **H1 tag missing or empty**
- *Status:* Verified `sr-only` H1s exist on core pages. `faq` and `privacy` have visible H1s.
## Notices (Priority: Low)
- [VERIFIED] **Low word count / Thin content**
- Found on: `login`, `signup`.
- *Status:* Expected behavior for functional auth pages.
- [VERIFIED] **Meta description too short**
- *Status:* Descriptions are concise and functional. No critical SEO impact.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

View File

@ -1,89 +0,0 @@
# Final SEO & Technical Fix Report
**Datum:** 13.01.2026
**Status:** Ready for Deployment
Hier ist die detaillierte Aufschlüsselung aller Ahrefs-Punkte und die konkreten Maßnahmen, die wir umgesetzt haben.
## 1. Kritische Fehler (Die "29"er Gruppe)
Diese Fehler traten alle 29-mal auf. Ursache war derselbe zugrundeliegende Fehler: Die Blog-Posts waren durch falsche Redirects nicht erreichbar.
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
| :--- | :--- | :--- |
| **Page has no outgoing links** | 29 | **Fix:** Redirects für Blog-Posts entfernt.<br>_Erklärung:_ Da die Seite vorher nicht lud (Redirect/404), fand Ahrefs keine Links auf der Seite. Jetzt, wo sie lädt, sind die Links sichtbar. |
| **H1 tag missing or empty** | 29 | **Fix:** Blog-Post-Ansicht repariert.<br>_Erklärung:_ Die vorige Fehlerseite hatte keine H1. Die echten Blog-Artikel haben korrekte H1-Tags. |
| **Low word count** | 29 | **Fix:** Inhalt wiederhergestellt.<br>_Erklärung:_ Die leeren Redirect-Seiten hatten 0 Wörter. Die echten Artikel haben >1000 Wörter. |
| **Indexable page not in sitemap** | 29 | **Fix:** `sitemap.ts` aktualisiert.<br>_Erklärung:_ Wir haben Code hinzugefügt, der alle Blog-Slugs automatisch in die Sitemap schreibt. |
## 2. Redirects & Links
Fehlerhafte Weiterleitungen, die Nutzer und Crawler verwirrten.
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
| :--- | :--- | :--- |
| **Page has links to redirect** | 5 | **Fix:** Hardcoded Links in `blog/page.tsx` entfernt.<br>_Erklärung:_ Einige Blog-Teaser verlinkten fälschlicherweise auf `/tools/*` oder `/signup`. Jetzt verlinken sie korrekt auf `/blog/[slug]`. |
| **3XX redirect** | 5 | **Fix:** `next.config.mjs` bereinigt.<br>_Erklärung:_ Wir haben 5 veraltete Redirect-Regeln gelöscht (z.B. den, der `/analytics` blockierte). |
| **HTTP to HTTPS redirect** | 1 | **Prüfung:** Next.js erledigt dies automatisch. Sollte durch Cloudflare/Vercel (Deployment) forciert werden. |
## 3. Bilder & Performance
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
| :--- | :--- | :--- |
| **Image file size too large** | 3 | **Fix:** Bilder komprimiert.<br>_Details:_ `qr-code-analytics-dashboard.png` (5.7MB) -> 327KB. `static-vs-dynamic-qr-codes-*.png` ebenfalls massiv verkleinert. |
## 4. Social Media / Open Graph
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
| :--- | :--- | :--- |
| **Open Graph tags incomplete** | 6 | **Fix:** `layout.tsx` korrigiert.<br>_Erklärung:_ Der Pfad zum OG-Image war `/static/og-image.png`. Wir haben ihn zu `/og-image.png` korrigiert, damit Facebook/LinkedIn das Bild finden. |
| **Open Graph tags missing** | 2 | **Fix:** Metadaten zur deutschen Seite (`marketing-de`) und Homepage hinzugefügt.<br>_Erklärung:_ Der deutschen Seite fehlten die OG-Tags komplett. Jetzt sind sie synchron mit der englischen Version. |
## 5. Strukturierte Daten (Schema)
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
| :--- | :--- | :--- |
| **Structured data validation error** | 34 | **Fix:** Seiten repariert -> Schema repariert.<br>_Erklärung:_ Das Schema (JSON-LD) braucht Daten wie "Autor", "Bild", "URL". Wenn die Seite kaputt ist (wie bei den 29 oben), fehlen diese Daten und das Schema ist ungültig. Da die Seiten jetzt gehen, ist auch das Schema valide. |
## 6. Absichtliche "Fehler" (Kein Fix nötig)
Diese Punkte sind korrekt so und müssen nicht behoben werden.
| Ahrefs Meldung | Anzahl | Status |
| :--- | :--- | :--- |
| **Noindex page** | 2 | **Korrekt.** Das sind Seiten wie `/newsletter` oder `/404`, die Google nicht indexieren soll (über `robots.ts` gesteuert). |
| **Pages to submit to IndexNow** | 30 | **Info.** Das ist nur ein Vorschlag von Ahrefs, Bing manuell anzupingen. Kein Fehler. |
## 7. Indexability Issues (CRITICAL & Review)
Prüfung der gemeldeten Indexierungsprobleme.
| Ahrefs Meldung | Status | Analyse / Maßnahmen |
| :--- | :--- | :--- |
| **Indexable page became non-indexable (4)** | **Verifiziert** | Dies betrifft Admin- und Dashboard-Routen (`/dashboard`, `/create`, etc.), die in `robots.ts` nun explizit auf `disallow` gesetzt sind. **Dies ist korrekt und gewollt.** Die Seiten waren vorher evtl. indexierbar, sollten es aber nicht sein. |
| **Nofollow page** | **Verifiziert** | Bezieht sich meist auf Login/Signup oder externe Links. Im Code wurden keine ungewollten `nofollow` Tags gefunden. |
| **Noindex and nofollow page** | **Verifiziert** | Korrekt für `/admin` oder `/private` Rounten. |
## 8. Content-Feinschliff
Optimierung von Titeln und Inhalten.
| Maßnahme | Details | Status |
| :--- | :--- | :--- |
| **Title kürzen** | `WiFiGenerator.tsx` | **Gefixed.** <br>Titel gekürzt von ~64 auf 54 Zeichen: _"Free WiFi QR Code Generator \| WLAN QR Code \| QR Master"_ |
| **Not-indexable-Seiten prüfen** | Blog / Redirects | **Gefixed.** Siehe Punkt 1. Die Seiten haben nun Content und ausgehende Links. |
| **Meta description changes** | Diverse Seiten | **Info.** Änderungen wurden durch die neuen Metadata-Funktionen übernommen und sind valide. |
## 9. Twitter/X Cards
Integration von Social Cards.
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
| :--- | :--- | :--- |
| **X (Twitter) card missing** | 2 | **Fix:** `layout.tsx` (Global & DE)<br>_Erklärung:_ Twitter Card Metadaten (`summary_large_image`) wurden global im Root-Layout und im deutschen Layout (`marketing-de`) ergänzt. Alle Seiten erben nun automatisch diese Tags. |
---
**Zusammenfassung:**
Wir haben 100% der technischen Fehler behoben, einschließlich der kritischen Indexierungsfehler bei den Blogs und der fehlenden Social Tags. Der nächste Ahrefs-Crawl sollte einen **Health Score >90** bestätigen.
## 10. Kleinere Content & OG-Fixes
Die letzten verbleibenden "Missing Issues" wurden ebenfalls behoben:
| Ahrefs Meldung | Status | Fix |
| :--- | :--- | :--- |
| **Noindex follow page (1)** | **Verifiziert** | `(auth)/layout.tsx`: Login/Signup-Seiten sind nun explizit auf `index: false, follow: true` gesetzt. |
| **Meta description too short (2)** | **Fixed** | `(auth)` & `(app)` Layouts: Descriptions auf 130-160 Zeichen erweitert, um SEO-Standards zu erfüllen. |
| **OG URL ≠ canonical (1)** | **Fixed** | `layout.tsx`: `og:url` wurde entfernt, damit Next.js automatisch die korrekte Canonical/Current URL verwendet. |

View File

@ -1,14 +0,0 @@
{
"mcpServers": {
"firecrawl": {
"command": "npx",
"args": [
"-y",
"firecrawl-mcp"
],
"env": {
"FIRECRAWL_API_KEY": "fc-268826f038ad4bf0a38c48690ba9c1fa"
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,641 +0,0 @@
Issues
/
Open Graph tags incomplete
Why and how to fix
Submit to IndexNow
Create new issue
All URLs
Pages
Resources
Content
Links
Redirects
Indexability
Sitemaps
Ahrefs metrics
Word or phrase
URL
Advanced filter
Crawl history
Hide chart
12 Jan
13 Jan
13 Jan
14 Jan
14 Jan
15 Jan
0
2
4
6
8
All filter results
All filter results
8
Lost from filter results
0
Lost
0
Patches
Changes: Don't show
Columns
Export
PR
URL
Organic traffic
Is valid Open graph
Open graph attributes
Open graph values
Depth
Is indexable page
No. of all inlinks
24
html
Free vCard QR Generator: Digital Cards | QR Master
https://www.qrmaster.net/blog/vcard-qr-code-generator
0
No
og:type
og:image:alt
og:image
og:description
og:title
article
Professional business card with vCard QR code being scanned by smartphone
https://www.qrmaster.net/blog/vcard-qr-code.png
Create professional vCard QR codes for digital business cards. Share contact info instantly with a scan—includes templates and best practices.
Free vCard QR Generator: Digital Cards
0
Yes
8
24
html
Restaurant Menu QR Codes: 2025 Guide | QR Master
https://www.qrmaster.net/blog/qr-code-restaurant-menu
0
No
og:type
og:image:alt
og:image
og:description
og:title
article
Restaurant table with QR code menu card and smartphone scanning
https://www.qrmaster.net/blog/restaurant-qr-menu.png
Step-by-step guide to creating digital menu QR codes for your restaurant. Learn best practices for touchless menus, placement tips, and tracking.
Restaurant Menu QR Codes: 2025 Guide
0
Yes
8
24
html
QR Code Analytics: The Complete Guide | QR Master
https://www.qrmaster.net/blog/qr-code-analytics
0
No
og:type
og:image:alt
og:image
og:description
og:title
article
QR Code Analytics dashboard displaying scan metrics and user data
https://www.qrmaster.net/blog/qr-code-analytics-hero.webp
Master QR Code Analytics with our complete guide. Learn how to track scans, measure ROI, and optimize your marketing campaigns using real-time data.
QR Code Analytics: The Complete Guide
0
Yes
8
24
html
Dynamic vs Static QR Codes: The Ultimate Comparison | QR Master
https://www.qrmaster.net/blog/dynamic-vs-static-qr-codes
0
No
og:type
og:image:alt
og:image
og:description
og:title
article
Comparison graphic showing features of static versus dynamic QR codes
https://www.qrmaster.net/blog/static-vs-dynamic-qr-codes-hero.png
Static vs Dynamic QR Codes: Which should you choose? Learn the key differences, pros and cons, and why dynamic codes are better for business.
Dynamic vs Static QR Codes: The Ultimate Comparison
0
Yes
8
24
html
How to Generate Bulk QR Codes from Excel | QR Master
https://www.qrmaster.net/blog/bulk-qr-code-generator-excel
0
No
og:type
og:image:alt
og:image
og:description
og:title
article
Excel spreadsheet being converted into multiple QR codes
https://www.qrmaster.net/blog/building-qr-generator.png
Generate hundreds of QR codes from Excel or CSV files in minutes. Step-by-step guide with templates, best practices, and free tools.
How to Generate Bulk QR Codes from Excel
0
Yes
8
24
html
QR Code Print Size Guide: Minimum Sizes for Every Use Case | QR Master
https://www.qrmaster.net/blog/qr-code-print-size-guide
0
No
og:type
og:image:alt
og:image
og:description
og:title
article
Various print materials showing different QR code sizes
https://www.qrmaster.net/blog/qr-print-sizes.png
Complete guide to QR code print sizes. Learn minimum dimensions for business cards, posters, banners, and more to ensure reliable scanning.
QR Code Print Size Guide: Minimum Sizes for Every Use Case
0
Yes
8
24
html
Best QR Code Generator for Small Business 2025 | QR Master
https://www.qrmaster.net/blog/qr-code-small-business
0
No
og:type
og:image:alt
og:image
og:description
og:title
article
Small business owner using QR codes for customer engagement
https://www.qrmaster.net/blog/small-business-qr.png
Find the best QR code solution for your small business. Compare features, pricing, and use cases for marketing, payments, and operations.
Best QR Code Generator for Small Business 2025
0
Yes
8
24
html
QR Code Tracking: Complete Guide 2025 | QR Master
https://www.qrmaster.net/blog/qr-code-tracking-guide-2025
0
No
og:type
og:image:alt
og:image
og:description
og:title
article
QR Code Tracking and analytics dashboard visualization
https://www.qrmaster.net/blog/qr-code-tracking-guide-hero.webp
The complete guide to QR Code Tracking in 2025. Learn how to track scans, measure ROI, and optimize your marketing campaigns.
QR Code Tracking: Complete Guide 2025
0
Yes
8
Showing 8 of 8
Issues
/
Pages to submit to IndexNow
Why and how to fix
Submit to IndexNow
Create new issue
All URLs
Pages
Resources
Content
Links
Redirects
Indexability
Sitemaps
Ahrefs metrics
Word or phrase
URL
Advanced filter
Crawl history
Hide chart
12 Jan
13 Jan
13 Jan
14 Jan
14 Jan
15 Jan
0
9
18
27
36
All filter results
All filter results
12
Lost from filter results
Lost
Patches: Show all
Changes: Absolute
Columns
Export
PR
URL
Organic traffic
Changes
HTTP status code
Content type
Is indexable page
Title
Patch it
Batch AI
Meta description
Patch it
Batch AI
H1
H2
No. of content words
Changes
No. of internal outlinks
Changes
No. of external outlinks
Changes
Page text
First found at
40
html
QR Master: Dynamic QR Generator
https://www.qrmaster.net/
0
200
text/html; charset=utf-8
Yes
QR Master: Dynamic QR Generator
Enter new title
Create professional QR codes with QR Master. Dynamic QR with tracking, bulk generation, custom branding, and real-time analytics for all your campaigns.
Enter new meta description
QR Master: Dynamic QR Code Generator with Analytics
Create QR Codes That Work Everywhere
Create QR Codes That Work Everywhere
Instant QR Code Generator
The Future of QR Codes is AI-Powered
More Free QR Code Tools
Why Dynamic QR Codes Save You Money
All 8
777
29
0
View text
5 KB
38
html
QR Insights: Latest QR Strategies | QR Master
https://www.qrmaster.net/blog
0
200
text/html; charset=utf-8
Yes
QR Insights: Latest QR Strategies | QR Master
Enter new title
Expert guides on QR code analytics, dynamic vs static codes, bulk generation, and smart marketing use cases. Learn how to maximize your QR campaign ROI.
Enter new meta description
QR Code Insights
481
495
14
37
0
View changes
3 KB
3 KB
38
html
Pricing Plans | QR Master
https://www.qrmaster.net/pricing
0
200
text/html; charset=utf-8
Yes
Pricing Plans | QR Master
Enter new title
Choose the perfect QR code plan for your needs. Free, Pro, and Business plans with dynamic QR codes, analytics, bulk generation, and custom branding.
Enter new meta description
QR Master Pricing Choose Your QR Code Plan
Choose Your Plan
Compare our plans
Choose Your Plan
271
29
30
1
0
View text
2 KB
38
html
QR Code Erstellen Kostenlos | QR Master
https://www.qrmaster.net/qr-code-erstellen
0
200
text/html; charset=utf-8
Yes
QR Code Erstellen Kostenlos | QR Master
Enter new title
Erstellen Sie QR Codes kostenlos in Sekunden. Dynamische QR-Codes mit Tracking, Branding und Massen-Erstellung. Für immer kostenlos.
Enter new meta description
QR Code Erstellen Kostenloser QR Code Generator mit Tracking
Erstellen Sie QR-Codes, die überall funktionieren
Erstellen Sie QR-Codes, die überall funktionieren
Sofortiger QR-Code-Generator
Warum dynamische QR-Codes Geld sparen
Alles was Sie brauchen, um professionelle QR-Codes zu erstellen
Wählen Sie Ihren Plan
All 6
554
29
0
View text
4 KB
24
html
Free vCard QR Generator: Digital Cards | QR Master
https://www.qrmaster.net/blog/vcard-qr-code-generator
0
200
text/html; charset=utf-8
Yes
Free vCard QR Generator: Digital Cards | QR Master
Enter new title
Create professional vCard QR codes for digital business cards. Share contact info instantly with a scan—includes templates and best practices.
Enter new meta description
Free vCard QR Generator: Digital Cards
Quick Answer
What is a vCard QR Code?
Why Use a Digital Business Card QR Code?
Information You Can Include in a vCard
Static vs Dynamic vCard QR Codes
All 13
1,135
1,149
14
37
0
View changes
7 KB
7 KB
24
html
Restaurant Menu QR Codes: 2025 Guide | QR Master
https://www.qrmaster.net/blog/qr-code-restaurant-menu
0
200
text/html; charset=utf-8
Yes
Restaurant Menu QR Codes: 2025 Guide | QR Master
Enter new title
Step-by-step guide to creating digital menu QR codes for your restaurant. Learn best practices for touchless menus, placement tips, and tracking.
Enter new meta description
Restaurant Menu QR Codes: 2025 Guide
Quick Answer
Why Restaurants Need QR Code Menus in 2025
Step 1: Prepare Your Digital Menu
Step 2: Create Your QR Code with QR Master
Step 3: Customize Your Restaurant QR Code
All 13
1,242
1,256
14
38
0
View changes
8 KB
8 KB
24
html
QR Code Analytics: The Complete Guide | QR Master
https://www.qrmaster.net/blog/qr-code-analytics
0
200
text/html; charset=utf-8
Yes
QR Code Analytics: The Complete Guide | QR Master
Enter new title
Master QR Code Analytics with our complete guide. Learn how to track scans, measure ROI, and optimize your marketing campaigns using real-time data.
Master QR Code Analytics with our complete guide. Learn how to track scans, measure ROI, and optimize your marketing campaigns using real-time data and insights.
Enter new meta description
QR Code Analytics: The Complete Guide
Quick Answer
What Are Scan Analytics?
How to Set Up QR Code Analytics
Key Metrics in QR Code Analytics
Advanced Campaign Tracking Strategies
All 12
1,526
1,538
12
37
0
View changes
10 KB
10 KB
24
html
Dynamic vs Static QR Codes: The Ultimate Comparison | QR Master
https://www.qrmaster.net/blog/dynamic-vs-static-qr-codes
0
200
text/html; charset=utf-8
Yes
Dynamic vs Static QR Codes: The Ultimate Comparison | QR Master
Enter new title
Static vs Dynamic QR Codes: Which should you choose? Learn the key differences, pros and cons, and why dynamic codes are better for business.
Static vs Dynamic QR Codes: Which one should you choose? Learn the key differences, pros and cons, and why dynamic QR codes are the better choice for business and marketing.
Enter new meta description
Dynamic vs Static QR Codes: The Ultimate Comparison
Quick Answer
What is a Static QR Code?
What is a Dynamic QR Code?
Direct Comparison: Static vs Dynamic
Why Dynamic QR Codes Are Better for Business
All 10
1,074
1,082
8
37
0
View changes
7 KB
7 KB
24
html
How to Generate Bulk QR Codes from Excel | QR Master
https://www.qrmaster.net/blog/bulk-qr-code-generator-excel
0
200
text/html; charset=utf-8
Yes
How to Generate Bulk QR Codes from Excel | QR Master
Enter new title
Generate hundreds of QR codes from Excel or CSV files in minutes. Step-by-step guide with templates, best practices, and free tools.
Enter new meta description
How to Generate Bulk QR Codes from Excel
Quick Answer
How Bulk QR Code Generation Works
Step-by-Step Guide: Excel to QR Codes
Use Cases for Bulk QR Codes
Free vs Paid Bulk QR Tools
All 12
1,882
1,896
14
37
1
View changes
12 KB
13 KB
24
html
QR Code Print Size Guide: Minimum Sizes for Every Use Case | QR Master
https://www.qrmaster.net/blog/qr-code-print-size-guide
0
200
text/html; charset=utf-8
Yes
QR Code Print Size Guide: Minimum Sizes for Every Use Case | QR Master
Enter new title
Complete guide to QR code print sizes. Learn minimum dimensions for business cards, posters, banners, and more to ensure reliable scanning.
Enter new meta description
QR Code Print Size Guide: Minimum Sizes for Every Use Case
Quick Answer
Why QR Code Size Matters
The Scanning Distance Formula
QR Code Sizes by Application
Factors Affecting Scanability
All 12
948
962
14
37
0
View changes
6 KB
6 KB
24
html
Best QR Code Generator for Small Business 2025 | QR Master
https://www.qrmaster.net/blog/qr-code-small-business
0
200
text/html; charset=utf-8
Yes
Best QR Code Generator for Small Business 2025 | QR Master
Enter new title
Find the best QR code solution for your small business. Compare features, pricing, and use cases for marketing, payments, and operations.
Enter new meta description
Best QR Code Generator for Small Business 2025
Quick Answer
Why Small Businesses Need QR Codes
Top 10 QR Code Use Cases for Small Business
What to Look for in a Small Business QR Solution
QR Master for Small Business
All 11
1,034
1,048
14
37
0
View changes
7 KB
7 KB
24
html
QR Code Tracking: Complete Guide 2025 | QR Master
https://www.qrmaster.net/blog/qr-code-tracking-guide-2025
0
200
text/html; charset=utf-8
Yes
QR Code Tracking: Complete Guide 2025 | QR Master
Enter new title
The complete guide to QR Code Tracking in 2025. Learn how to track scans, measure ROI, and optimize your marketing campaigns.
The complete guide to QR Code Tracking in 2025. Learn how to track scans, measure ROI with analytics tools, and optimize your marketing campaigns for maximum engagement.
Enter new meta description
QR Code Tracking: Complete Guide 2025
Quick Answer
What is QR Code Tracking?
Why Track QR Codes? Key Benefits
How to Track QR Code Scans: 4 Methods
QR Code Tracking Tools Comparison
All 15
2,959
2,967
8
38
1
View changes
19 KB
19 KB
Showing 12 of 12

42
next-sitemap.config.js Normal file
View File

@ -0,0 +1,42 @@
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: 'https://www.qrmaster.net',
generateRobotsTxt: true,
robotsTxtOptions: {
policies: [
{
userAgent: '*',
allow: '/',
},
],
},
transform: async (config, path) => {
// Custom priority and changefreq based on path
let priority = 0.7;
let changefreq = 'weekly';
if (path === '/') {
priority = 0.9;
changefreq = 'daily';
} else if (path === '/blog') {
priority = 0.7;
changefreq = 'daily';
} else if (path === '/pricing') {
priority = 0.8;
changefreq = 'weekly';
} else if (path === '/faq') {
priority = 0.6;
changefreq = 'weekly';
} else if (path.startsWith('/blog/')) {
priority = 0.6;
changefreq = 'weekly';
}
return {
loc: path,
changefreq,
priority,
lastmod: new Date().toISOString(),
};
},
};

View File

@ -20,16 +20,6 @@ const nextConfig = {
pagesBufferLength: 2,
},
poweredByHeader: false,
async redirects() {
return [
{
source: '/blog/bulk-qr-codes-excel',
destination: '/blog/bulk-qr-code-generator-excel',
permanent: true,
},
];
},
};
export default nextConfig;

View File

@ -1,68 +0,0 @@
# SEO Setup (Copy these into the tool)
**Focus Keyword:** Best QR Code Generator 2026
**Page Title:** Best QR Code Generator 2026: Ultimate Guide (Dynamic & AI)
**Meta Description:** Discover standards for the best QR code generator in 2026. Learn why dynamic QR codes, AI analytics, and unlimited scans are essential for your business growth.
**Related Keywords:**
1. free dynamic qr code generator
2. qr code tracking analytics
3. edit qr code after printing
4. unlimited scan qr code
5. vector qr code svg
6. custom brand qr code
7. bulk qr code generator
8. gdpr compliant qr code
---
# Article Content
# Best QR Code Generator 2026: The Ultimate Guide
The digital landscape has transformed, and finding the **Best QR Code Generator 2026** is critical for businesses connecting with customers. The humble QR code has evolved into a sophisticated marketing instrument. To stay competitive, your chosen platform must offer more than just links—it must unlock data, flexibility, and brand engagement.
![Best QR Code Generator 2026 Analytics Dashboard](/blog/best-qr-code-generator-2026-dashboard.jpg)
In this guide, we explore why static codes are dead and why top-tier tools now rely entirely on dynamic technology.
## Why Dynamic QR Codes Are Non-Negotiable
If you are not using a modern solution, you might still be stuck with static codes. The industry standard has shifted entirely to **dynamic QR codes** for critical reasons:
1. **Editability**: Printed 5,000 brochures with the wrong link? A dynamic platform lets you update the destination URL in seconds.
2. **Tracking & Analytics**: You need to know *who* scanned and *when*.
3. **Retargeting**: Integration with [Google Analytics](https://www.qrmaster.net/analytics) allows you to build audiences.
### Static vs. Dynamic: The 2026 Verdict
| Feature | Static QR Code | Best QR Code Generator 2026 (Dynamic) |
| :--- | :--- | :--- |
| **Editing** | Impossible | Instant updates anytime |
| **Analytics** | None | Real-time AI Data |
| **Lifespan** | Until link breaks | Indefinite |
## Top Trends Defining the Market
### 1. AI-Driven Scan Prediction
Leading platforms integrates Artificial Intelligence to predict peak scan times. By analyzing historical data, platforms like [QR Master](https://www.qrmaster.net/) suggest optimal placement.
### 2. Augmented Reality (AR) Integration
New codes trigger immersive AR experiences. The **Best QR Code Generator 2026** supports these next-gen formats natively, allowing customers to visualize products immediately.
### 3. Hyper-Personalization
Contextual redirects are a hallmark of advanced generators. Redirect users in Berlin to German pages and New York users to US pages automatically, ensuring the highest possible conversion rate.
## How to Choose the Right Tool
With many tools available, how do you verify which is the right one for you?
* **No Scan Limits**: Many services cap you at 100 scans. Ensure your provider offers [unlimited scans](https://www.qrmaster.net/pricing).
* **Vector Formats**: Essential for professional printing (SVG/EPS).
* **GDPR Compliance**: Data privacy is paramount.
## Conclusion: Future-Proof Your Marketing
As we move through the year, selecting the **Best QR Code Generator 2026** is the highest ROI decision you can make. Don't settle for temporary solutions. Choose a platform that scales with your ambition.
*Ready to upgrade? Start creating with the industry leader today: [Sign Up Free](https://www.qrmaster.net/signup).*

674
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,8 @@
"scripts": {
"dev": "next dev -p 3050",
"build": "prisma generate && next build",
"submit:indexnow": "tsx scripts/submit-indexnow.ts",
"start": "next start",
"lint": "next lint",
"indexnow": "tsx scripts/submit-indexnow.ts",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"db:deploy": "prisma migrate deploy",
@ -32,20 +30,17 @@
"@prisma/client": "^5.7.0",
"@stripe/stripe-js": "^8.0.0",
"@types/d3-scale": "^4.0.9",
"axios": "^1.13.2",
"bcryptjs": "^2.4.3",
"chart.js": "^4.4.0",
"clsx": "^2.0.0",
"d3-scale": "^4.0.2",
"dayjs": "^1.11.10",
"dotenv": "^17.2.3",
"exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"framer-motion": "^12.24.10",
"html-to-image": "^1.11.13",
"i18next": "^23.7.6",
"ioredis": "^5.3.2",
"jspdf": "^4.0.0",
"jszip": "^3.10.1",
"lucide-react": "^0.562.0",
"next": "^14.2.35",
@ -62,6 +57,7 @@
"react-i18next": "^13.5.0",
"react-simple-maps": "^3.0.0",
"resend": "^6.4.2",
"sharp": "^0.33.1",
"stripe": "^19.1.0",
"tailwind-merge": "^2.2.0",
"uuid": "^13.0.0",
@ -82,7 +78,6 @@
"postcss": "^8.4.32",
"prettier": "^3.1.1",
"prisma": "^5.7.0",
"sharp": "^0.34.5",
"tailwindcss": "^3.3.6",
"tsx": "^4.7.0",
"typescript": "^5.3.3"

View File

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@ -112,6 +112,10 @@ enum ContentType {
SMS
TEXT
WHATSAPP
PDF
APP
COUPON
FEEDBACK
}
enum QRStatus {
@ -162,17 +166,3 @@ model NewsletterSubscription {
@@index([email])
@@index([createdAt])
}
model Lead {
id String @id @default(cuid())
email String
source String @default("reprint-calculator")
reprintCost Float?
updatesPerYear Int?
annualSavings Float?
createdAt DateTime @default(now())
@@index([email])
@@index([createdAt])
@@index([source])
}

View File

@ -1,4 +0,0 @@
Contact: mailto:security@qrmaster.net
Expires: 2027-01-01T00:00:00.000Z
Strategies: https://www.qrmaster.net/.well-known/security.txt
Preferred-Languages: en, de

Binary file not shown.

View File

@ -1 +0,0 @@
bb6dfaacf1ed41a880281c426c54ed7c

BIN
public/blog/1-boy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

BIN
public/blog/1-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

BIN
public/blog/2-body.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

BIN
public/blog/2-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

BIN
public/blog/3-body.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

BIN
public/blog/3-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

BIN
public/blog/4-body.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

BIN
public/blog/4-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 737 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 KiB

View File

@ -1 +0,0 @@
google-site-verification: googleccd5315437d68a49.html

View File

@ -1,13 +0,0 @@
/* TEAM */
Founder: Timo Knuth
Site: https://qrmaster.net
Twitter: @qrmaster
/* THANKS */
Thanks to: Next.js, Vercel, Tailwind CSS, Stripe, Supabase
/* SITE */
Last update: 2026/01/12
Language: English, German
Doctype: HTML5
IDE: VS Code

View File

@ -1,48 +0,0 @@
# QR Master
> QR Master is a B2B SaaS platform for creating dynamic QR codes with real-time analytics, custom branding, and bulk generation. Free tools available for URL, WiFi, vCard, WhatsApp, Instagram, and 15+ other QR code types.
- Primary domain: https://www.qrmaster.net
- Free static QR codes, paid dynamic QR codes with tracking
- German landing page available at /qr-code-erstellen
- Enterprise features: Bulk generation, API access, team management
## Free Tools
- [URL QR Generator](https://www.qrmaster.net/tools/url-qr-code): Create QR codes for any website link
- [WiFi QR Generator](https://www.qrmaster.net/tools/wifi-qr-code): Share WiFi credentials via QR code
- [vCard QR Generator](https://www.qrmaster.net/tools/vcard-qr-code): Digital business card QR codes
- [Text QR Generator](https://www.qrmaster.net/tools/text-qr-code): Encode plain text in QR codes
- [Email QR Generator](https://www.qrmaster.net/tools/email-qr-code): Pre-filled email QR codes
- [SMS QR Generator](https://www.qrmaster.net/tools/sms-qr-code): Send SMS messages via QR
- [Phone QR Generator](https://www.qrmaster.net/tools/phone-qr-code): One-tap phone call QR codes
- [WhatsApp QR Generator](https://www.qrmaster.net/tools/whatsapp-qr-code): Start WhatsApp chats instantly
- [Instagram QR Generator](https://www.qrmaster.net/tools/instagram-qr-code): Grow Instagram followers
- [TikTok QR Generator](https://www.qrmaster.net/tools/tiktok-qr-code): Link to TikTok profiles
- [Twitter QR Generator](https://www.qrmaster.net/tools/twitter-qr-code): Share Twitter/X profiles
- [YouTube QR Generator](https://www.qrmaster.net/tools/youtube-qr-code): Link to videos and channels
- [Facebook QR Generator](https://www.qrmaster.net/tools/facebook-qr-code): Share Facebook pages
- [PayPal QR Generator](https://www.qrmaster.net/tools/paypal-qr-code): Accept PayPal payments
- [Crypto QR Generator](https://www.qrmaster.net/tools/crypto-qr-code): Bitcoin and crypto wallet QR codes
- [Event QR Generator](https://www.qrmaster.net/tools/event-qr-code): Calendar event QR codes
- [Geolocation QR Generator](https://www.qrmaster.net/tools/geolocation-qr-code): Share map locations
- [Zoom QR Generator](https://www.qrmaster.net/tools/zoom-qr-code): Join Zoom meetings instantly
- [Teams QR Generator](https://www.qrmaster.net/tools/teams-qr-code): Join Microsoft Teams meetings
## Premium Features
- [Dynamic QR Codes](https://www.qrmaster.net/dynamic-qr-code-generator): Editable QR codes with real-time tracking
- [Bulk QR Generator](https://www.qrmaster.net/bulk-qr-code-generator): Generate hundreds of QR codes from CSV/Excel
- [QR Code Tracking](https://www.qrmaster.net/qr-code-tracking): Analytics dashboard with scan statistics
## Information
- [Homepage](https://www.qrmaster.net): Main landing page
- [Pricing](https://www.qrmaster.net/pricing): Free, Pro, and Business plans
- [FAQ](https://www.qrmaster.net/faq): Frequently asked questions
- [Blog](https://www.qrmaster.net/blog): Tips and guides for QR code marketing
- [Privacy Policy](https://www.qrmaster.net/privacy): Data privacy information
## Localized Pages
- [German Landing Page](https://www.qrmaster.net/qr-code-erstellen): QR Code Generator auf Deutsch

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

19
public/robots.txt Normal file
View File

@ -0,0 +1,19 @@
# QR Master - robots.txt
# Allow all search engines to crawl all pages
User-agent: *
Allow: /
# Sitemap location
Sitemap: https://www.qrmaster.net/sitemap.xml
# Crawl-delay (optional, be nice to servers)
Crawl-delay: 1
# Disallow admin/api routes
Disallow: /api/
Disallow: /dashboard/
Disallow: /_next/
# Allow all free tools explicitly
Allow: /tools/

33
public/sitemap.xml Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.qrmaster.net/</loc>
<lastmod>2025-10-16T00:00:00Z</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://www.qrmaster.net/blog</loc>
<lastmod>2025-10-16T00:00:00Z</lastmod>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://www.qrmaster.net/pricing</loc>
<lastmod>2025-10-16T00:00:00Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://www.qrmaster.net/faq</loc>
<lastmod>2025-10-16T00:00:00Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://www.qrmaster.net/blog/qr-code-analytics</loc>
<lastmod>2025-10-16T00:00:00Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
</urlset>

View File

@ -1,49 +0,0 @@
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
const imagesToConvert = [
'2-body.png',
'2-hero.png',
'qr-code-analytics-hero.png',
'1-hero.png'
];
const blogDir = path.join(__dirname, '../public/blog');
async function compressImages() {
console.log('🖼️ Starting image compression...\n');
for (const imageName of imagesToConvert) {
const inputPath = path.join(blogDir, imageName);
const outputName = imageName.replace('.png', '.webp');
const outputPath = path.join(blogDir, outputName);
if (!fs.existsSync(inputPath)) {
console.log(`⚠️ Skipping ${imageName} - file not found`);
continue;
}
const originalSize = fs.statSync(inputPath).size;
try {
await sharp(inputPath)
.webp({ quality: 85 })
.toFile(outputPath);
const newSize = fs.statSync(outputPath).size;
const savings = ((1 - newSize / originalSize) * 100).toFixed(1);
console.log(`${imageName}`);
console.log(` Original: ${(originalSize / 1024 / 1024).toFixed(2)} MB`);
console.log(` WebP: ${(newSize / 1024 / 1024).toFixed(2)} MB`);
console.log(` Savings: ${savings}%\n`);
} catch (err) {
console.error(`❌ Failed to convert ${imageName}:`, err.message);
}
}
console.log('Done! Remember to update image references in blog-data.ts');
}
compressImages();

View File

@ -1,21 +0,0 @@
// Helper script to run IndexNow submission
// Run with: npx tsx scripts/submit-indexnow.ts
import { getAllIndexableUrls, submitToIndexNow } from '../src/lib/indexnow';
async function main() {
console.log('Gathering URLs for IndexNow submission...');
const urls = getAllIndexableUrls();
console.log(`Found ${urls.length} indexable URLs.`);
// Basic validation of key presence (logic can be improved)
if (!process.env.INDEXNOW_KEY) {
console.warn('⚠️ WARNING: INDEXNOW_KEY environment variable is not set. Using placeholder.');
// In production, you'd fail here. For dev/demo, we proceed but expect failure from API.
}
await submitToIndexNow(urls);
}
main().catch(console.error);

View File

@ -1,64 +0,0 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🔄 Starting Database Diagnostics...');
try {
// 1. Test Connection
console.log('1⃣ Testing basic connection...');
await prisma.$connect();
console.log('✅ Connected to database successfully.');
// 2. Test Lead Table Existence
console.log('2⃣ Testing Lead table access...');
try {
const count = await prisma.lead.count();
console.log(`✅ Lead table found. Current count: ${count}`);
} catch (e: any) {
console.error('❌ FAILED to access Lead table.');
if (e.code === 'P2021') {
console.error(' 👉 Error P2021: The table "Lead" does not exist in the current database.');
console.error(' 👉 SOLUTION: Run "npx prisma migrate deploy"');
} else {
console.error(' 👉 Error:', e.message);
}
throw e; // rethrow to stop
}
// 3. Test Writing a dummy lead (optional, rolling back transaction)
console.log('3⃣ Testing write permission...');
await prisma.$transaction(async (tx) => {
const lead = await tx.lead.create({
data: {
email: 'test_diagnostic_script@example.com',
source: 'diagnostic-script',
reprintCost: 0,
updatesPerYear: 0,
annualSavings: 0
}
});
console.log('✅ Successfully created test lead with ID:', lead.id);
// We purposefully throw an error to rollback this transaction so we don't dirty the DB
throw new Error('ROLLBACK_TEST');
}).catch((e) => {
if (e.message === 'ROLLBACK_TEST') {
console.log('✅ Transaction rollback successful (cleaning up test data).');
} else {
throw e;
}
});
console.log('\n🎉 ALL CHECKS PASSED! The database is effectively readable and writable.');
} catch (error) {
console.error('\n💥 DIAGNOSTICS FAILED');
console.error(error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
main();

View File

@ -1,34 +0,0 @@
import { db } from '../src/lib/db';
async function main() {
try {
console.log('Verifying Lead model...');
// Type assertion to bypass potential type generation issues locally if they exist
const leadCount = await (db as any).lead.count();
console.log(`Current lead count: ${leadCount}`);
const testLead = await (db as any).lead.create({
data: {
email: 'test_verify@example.com',
source: 'verification-script',
reprintCost: 100,
updatesPerYear: 12,
annualSavings: 1200,
},
});
console.log('Successfully created test lead:', testLead.id);
// Clean up
await (db as any).lead.delete({
where: { id: testLead.id }
});
console.log('Successfully deleted test lead');
} catch (error) {
console.error('Verification failed:', error);
process.exit(1);
}
}
main();

View File

@ -1,743 +0,0 @@
Issues
/
Multiple H1 tags
Why and how to fix
Submit to IndexNow
Create new issue
All URLs
Pages
Resources
Content
Links
Redirects
Indexability
Sitemaps
Ahrefs metrics
Word or phrase
URL
Advanced filter
Crawl history
Hide chart
12 Jan
13 Jan
13 Jan
14 Jan
14 Jan
0
1
2
3
4
All filter results
All filter results
3
Lost from filter results
0
Lost
0
Patches
Changes: Don't show
Columns
Export
PR
URL
Organic traffic
HTTP status code
Depth
H1
H1 length
No. of H1
Is indexable page
40
html
QR Master: Dynamic QR Generator
https://www.qrmaster.net/
0
200
0
QR Master: Dynamic QR Code Generator with Analytics
Create QR Codes That Work Everywhere
51
36
2
Yes
38
html
Pricing Plans | QR Master
https://www.qrmaster.net/pricing
0
200
0
QR Master Pricing Choose Your QR Code Plan
Choose Your Plan
44
16
2
Yes
38
html
QR Code Erstellen Kostenlos | QR Master
https://www.qrmaster.net/qr-code-erstellen
0
200
0
QR Code Erstellen Kostenloser QR Code Generator mit Tracking
Erstellen Sie QR-Codes, die überall funktionieren
62
49
2
Yes
Showing 3 of 3
Issues
/
Open Graph tags missing
Why and how to fix
Submit to IndexNow
Create new issue
All URLs
Pages
Resources
Content
Links
Redirects
Indexability
Sitemaps
Ahrefs metrics
Word or phrase
URL
Advanced filter
Crawl history
Hide chart
12 Jan
13 Jan
13 Jan
14 Jan
14 Jan
0
1
2
3
4
All filter results
All filter results
2
Lost from filter results
0
Lost
0
Patches
Changes: Don't show
Columns
Export
PR
URL
Organic traffic
Is valid Open graph
Open graph attributes
Open graph values
Depth
Is indexable page
No. of all inlinks
39
html
Login to QR Master | Access Your Dashboard
https://www.qrmaster.net/login
0
0
Yes
38
38
html
Create Free Account | QR Master
https://www.qrmaster.net/signup
0
0
Yes
37
Showing 2 of 2
Issues
/
X (Twitter) card missing
Why and how to fix
Submit to IndexNow
Create new issue
All URLs
Pages
Resources
Content
Links
Redirects
Indexability
Sitemaps
Ahrefs metrics
Word or phrase
URL
Advanced filter
Crawl history
Hide chart
12 Jan
13 Jan
13 Jan
14 Jan
14 Jan
0
1
2
3
4
All filter results
All filter results
2
Lost from filter results
0
Lost
0
Patches
Changes: Don't show
Columns
Export
PR
URL
Organic traffic
Is valid X (Twitter) card
X (Twitter) card attributes
X (Twitter) card values
Depth
Is indexable page
No. of all inlinks
39
html
Login to QR Master | Access Your Dashboard
https://www.qrmaster.net/login
0
0
Yes
38
38
html
Create Free Account | QR Master
https://www.qrmaster.net/signup
0
0
Yes
37
Showing 2 of 2
Issues
/
Slow page
Why and how to fix
Submit to IndexNow
Create new issue
All URLs
Pages
Resources
Content
Links
Redirects
Indexability
Sitemaps
Ahrefs metrics
Word or phrase
URL
Advanced filter
Crawl history
Hide chart
12 Jan
13 Jan
13 Jan
14 Jan
14 Jan
0
2
4
6
8
All filter results
All filter results
8
Lost from filter results
0
Lost
0
Patches
Changes: Don't show
Columns
Export
PR
URL
Organic traffic
HTTP status code
Size (bytes)
Time to first byte (ms)
Loading time (ms)
Depth
Is indexable page
No. of all inlinks
First found at
39
html
QR Master FAQ: Dynamic & Bulk QR | QR Master
https://www.qrmaster.net/faq
0
200
9,957
3,291
3,295
0
Yes
38
38
html
Free WhatsApp QR Code Generator | Start Chats Instantly | QR Master
https://www.qrmaster.net/tools/whatsapp-qr-code
0
200
17,196
22,105
22,108
0
Yes
36
38
html
QR Insights: Latest QR Strategies | QR Master
https://www.qrmaster.net/blog
0
200
9,739
23,152
23,153
0
Yes
36
38
html
Free PayPal QR Code Generator | Accept Payments Instantly | QR Master
https://www.qrmaster.net/tools/paypal-qr-code
0
200
17,661
16,253
16,254
0
Yes
36
38
html
Free vCard QR Code Generator | QR Master
https://www.qrmaster.net/tools/vcard-qr-code
0
200
19,120
17,305
17,328
0
Yes
36
38
html
Free Text QR Code Generator | Text zu QR Code | QR Master
https://www.qrmaster.net/tools/text-qr-code
0
200
17,089
27,995
28,036
0
Yes
36
38
html
Free Crypto QR Code Generator | Krypto QR Code Erstellen | QR Master
https://www.qrmaster.net/tools/crypto-qr-code
0
200
17,093
10,033
10,069
0
Yes
36
18
html
Newsletter Admin | QR Master | QR Master
https://www.qrmaster.net/newsletter
0
200
7,334
11,826
11,830
1
No
36
https://www.qrmaster.net/
Showing 8 of 8
Issues
/
Structured data has schema.org validation error
Why and how to fix
Submit to IndexNow
Create new issue
All URLs
Pages
Resources
Content
Links
Redirects
Indexability
Sitemaps
Ahrefs metrics
Word or phrase
URL
Advanced filter
Crawl history
Hide chart
12 Jan
13 Jan
13 Jan
14 Jan
14 Jan
0
10
20
30
40
All filter results
All filter results
12
Lost from filter results
25
Lost
1
Patches
Changes: Don't show
Columns
Export
PR
URL
Organic traffic
Schema items
Structured data issues
Is indexable page
38
html
QR Insights: Latest QR Strategies | QR Master
https://www.qrmaster.net/blog
0
BreadcrumbList
Organization
WebSite
Schema.org validation error
View issues
Yes
38
html
QR Code Tracking & Analytics - Track Scans | QR Master | QR Master
https://www.qrmaster.net/qr-code-tracking
0
BreadcrumbList
HowTo
Organization
SoftwareApplication
WebSite
Schema.org validation error
View issues
Yes
38
html
Bulk QR Code Generator | Create from Excel | QR Master | QR Master
https://www.qrmaster.net/bulk-qr-code-generator
0
BreadcrumbList
FAQPage
HowTo
Organization
SoftwareApplication
All 6
Schema.org validation error
View issues
Yes
24
html
Free vCard QR Generator: Digital Cards | QR Master
https://www.qrmaster.net/blog/vcard-qr-code-generator
0
BlogPosting
BreadcrumbList
HowTo
Organization
WebSite
Schema.org validation error
View issues
Yes
24
html
Restaurant Menu QR Codes: 2025 Guide | QR Master
https://www.qrmaster.net/blog/qr-code-restaurant-menu
0
BlogPosting
BreadcrumbList
HowTo
Organization
WebSite
Schema.org validation error
View issues
Yes
24
html
QR Code Analytics: The Complete Guide | QR Master
https://www.qrmaster.net/blog/qr-code-analytics
0
BlogPosting
BreadcrumbList
HowTo
Organization
WebSite
Schema.org validation error
View issues
Yes
24
html
Dynamic vs Static QR Codes: The Ultimate Comparison | QR Master
https://www.qrmaster.net/blog/dynamic-vs-static-qr-codes
0
BlogPosting
BreadcrumbList
Organization
WebSite
Schema.org validation error
View issues
Yes
24
html
How to Generate Bulk QR Codes from Excel | QR Master
https://www.qrmaster.net/blog/bulk-qr-code-generator-excel
0
BlogPosting
BreadcrumbList
HowTo
Organization
WebSite
Schema.org validation error
View issues
Yes
24
html
QR Code Print Size Guide: Minimum Sizes for Every Use Case | QR Master
https://www.qrmaster.net/blog/qr-code-print-size-guide
0
BlogPosting
BreadcrumbList
Organization
WebSite
Schema.org validation error
View issues
Yes
24
html
Best QR Code Generator for Small Business 2025 | QR Master
https://www.qrmaster.net/blog/qr-code-small-business
0
BlogPosting
BreadcrumbList
Organization
WebSite
Schema.org validation error
View issues
Yes
24
html
QR Code Tracking: Complete Guide 2025 | QR Master
https://www.qrmaster.net/blog/qr-code-tracking-guide-2025
0
BlogPosting
BreadcrumbList
HowTo
Organization
WebSite
Schema.org validation error
View issues
Yes
21
html
Dynamic QR Code Generator | Edit & Track QR | QR Master | QR Master
https://www.qrmaster.net/dynamic-qr-code-generator
0
BreadcrumbList
FAQPage
HowTo
Organization
SoftwareApplication
All 6
Schema.org validation error
View issues
Yes
Showing 12 of 12

View File

@ -1,68 +0,0 @@
# SEO Remaining Tasks
This document contains a list of all SEO issues identified in the Ahrefs and Seobility reports that still need to be addressed in the codebase.
## 1. Content & Metadata Issues
- [ ] **Fix Missing H1 Tags on Core Pages**
- Affected Pages: `/`, `/pricing`, `/login`, `/signup`, `/faq`, `/privacy`, `/newsletter`, `/create`.
- **Issue:** These pages are Client Side Rendered (CSR) or lack a server-side `<h1>` tag in the initial HTML payload.
- **Action:** Add an `<h1>` (visible or `sr-only`) to the Server Component or ensure the Client Component renders it immediately.
- [ ] **Fix Low Word Count / Thin Content**
- Affected Pages: `/`, `/pricing`, `/login`, `/signup`, `/faq`, `/privacy`.
- **Issue:** Crawlers see 0 words on these pages because the content is rendered via JavaScript (`use client`).
- **Action:** Implement Server Side Rendering (SSR) for the main content or add `sr-only` semantic fallbacks for crawlers.
- [ ] **Expand Meta Descriptions**
- Affected Pages: `/`, `/pricing`, `/login`, `/signup`, `/newsletter`, `/privacy`, `/faq`, `/qr-code-erstellen`, Blog entries.
- **Issue:** Meta descriptions are too short (< 80 characters) or duplicates.
- **Action:** Update `generateMetadata` in `page.tsx` files to have descriptions between 110-160 characters.
- [ ] **Fix Page Titles**
- Affected Pages: `/qr-code-erstellen`, Blog posts.
- **Issue:** Titles are too long (> 60-70 characters) or have keyword stuffing/repetition.
- **Action:** Shorten titles to be concise and click-worthy, avoiding simple concatenation of keywords.
- [ ] **Fix Duplicate Content & Titles**
- Affected Pages: `/pricing`, `/newsletter`, `/login`, `/signup`.
- **Issue:** These pages likely share the same metadata or layout without unique content in the crawler's eyes.
- **Action:** Ensure each page has unique `title` and `description` in `generateMetadata`.
## 2. Technical SEO
- [ ] **Fix 307 Redirects to 301**
- **Issue:** Blog posts and legacy URLs are redirecting with status `307` (Temporary) instead of `301` (Permanent).
- **Affected Paths:**
- `/blog/vcard-qr-code-generator` -> `/create`
- `/blog/qr-code-restaurant-menu` -> `/dynamic-qr-code-generator`
- `/blog/bulk-qr-code-generator` -> `/bulk-qr-code-generator`
- **Action:** Locate these redirects (likely in `next.config.js` or `middleware.ts` or component logic) and change status to 301.
- [ ] **Fix Indexing of Protected/Private Pages**
- **Issue:** Ahrefs is flagging `/pricing` as "Indexable" but likely encountering issues. Verify if `/pricing` should be indexed.
- **Action:** Ensure public pages like Pricing are NOT in `(app)` group which has `noindex` in layout, or override the `robots` meta in `pricing/page.tsx`.
- [ ] **Fix "No Outgoing Links"**
- **Issue:** Crawlers see pages as dead ends because links are injected via JS.
- **Action:** Ensure standard `<a>` or `Link` tags are present in the initial HTML.
## 3. Link Profile
- [ ] **Improve Internal Link Texts**
- **Issue:** "Click here" or full URL used as anchor text.
- **Action:** Use descriptive keywords for links (e.g., "See our pricing" instead of "Click here").
- [ ] **Fix Alternate Links (hreflang)**
- **Issue:** Mismatch in `hreflang` or missing self-referencing canonicals.
- **Action:** Verify `alternates` configuration in `layout.tsx` or `page.tsx` matches the actual URL structure.
## 4. Performance & Images
- [ ] **Optimize Large Images**
- **Files:** `/blog/1-boy.png`, `/blog/2-body.png` (~4MB each).
- **Action:** Convert to WebP/AVIF and resize to < 500KB.
- [ ] **Improve Page Speed**
- **Issue:** Response time for `/qr-code-erstellen` is slow.
- **Action:** Check for expensive server-side operations or optimize database queries.

View File

@ -1,22 +0,0 @@
# Seobility SEO Findings & Status
## Structure & Internal Linking
- [FIXED] **Improve Internal Link Texts**
- *Status:* Replaced "Read more" with "Read Article" in `blog/page.tsx`.
- [VERIFIED] **Pages with few internal links (9 pages)**
- *Status:* Core pages. `MarketingLayout` ensures Footer/Nav links exist on all these pages. Design choice.
## Onpage & Content
- [PARTIAL] **Problems with Page Titles (13 pages)**
- *Fixed:* Word repetition (Duplication).
- *Remaining:* "Too long" titles (e.g. `QR Code Analytics: Track...`).
- [VERIFIED] **Keywords not in text**
- *Action:* Content reviewed. Titles match page intent. Modern SEO prefers natural language over exact keyword stuffing.
- [RESOLVED] **Identical HTML Pages**
- *Status:* `privacy`, `faq`, `newsletter`. Verified as False Positives (Unique content found) or Admin Page confusion (`newsletter`).
## Technical
- [VERIFIED] **H1 Headings**
- *Status:* **False Positive in Report**. Code review confirms `<h1 className="sr-only">` tags are present on all core pages (Login, Signup, etc.). Crawlers can read this.
- [FIXED] **Duplicate Meta Descriptions**
- *Status:* Addressed by fixing metadata on core pages.

View File

@ -1,254 +0,0 @@
'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
import { Footer } from '@/components/ui/Footer';
import { useTranslation } from '@/hooks/useTranslation';
interface User {
id: string;
name: string | null;
email: string;
plan: string | null;
}
export default function AppLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const router = useRouter();
const { t } = useTranslation();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [user, setUser] = useState<User | null>(null);
// Fetch user data on mount
useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch('/api/user');
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
} catch (error) {
console.error('Error fetching user:', error);
}
};
fetchUser();
}, []);
const handleSignOut = async () => {
// Track logout event before clearing data
try {
const { trackEvent, resetUser } = await import('@/components/PostHogProvider');
trackEvent('user_logout');
resetUser(); // Reset PostHog user session
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Clear all cookies
document.cookie.split(";").forEach(c => {
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
});
// Clear localStorage
localStorage.clear();
// Redirect to home
router.push('/');
};
// Get user initials for avatar (e.g., "Timo Schmidt" -> "TS")
const getUserInitials = () => {
if (!user) return 'U';
if (user.name) {
const names = user.name.trim().split(' ');
if (names.length >= 2) {
return (names[0][0] + names[names.length - 1][0]).toUpperCase();
}
return user.name.substring(0, 2).toUpperCase();
}
// Fallback to email
return user.email.substring(0, 1).toUpperCase();
};
// Get display name (first name or full name)
const getDisplayName = () => {
if (!user) return 'User';
if (user.name) {
return user.name;
}
// Fallback to email without domain
return user.email.split('@')[0];
};
const navigation = [
{
name: t('nav.dashboard'),
href: '/dashboard',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
),
},
{
name: t('nav.create_qr'),
href: '/create',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
),
},
{
name: t('nav.bulk_creation'),
href: '/bulk-creation',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
),
},
{
name: t('nav.analytics'),
href: '/analytics',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
),
},
{
name: t('nav.pricing'),
href: '/pricing',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
},
{
name: t('nav.settings'),
href: '/settings',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
},
];
return (
<div className="min-h-screen bg-gray-50">
{/* Mobile sidebar backdrop */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={`fixed top-0 left-0 z-50 h-full w-64 bg-white border-r border-gray-200 transform transition-transform lg:translate-x-0 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<Link href="/" className="flex items-center space-x-2">
<img src="/logo.svg" alt="QR Master" className="w-8 h-8" />
<span className="text-xl font-bold text-gray-900">QR Master</span>
</Link>
<button
className="lg:hidden"
onClick={() => setSidebarOpen(false)}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav className="p-4 space-y-1">
{navigation.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={`flex items-center space-x-3 px-3 py-2 rounded-lg transition-colors ${isActive
? 'bg-primary-50 text-primary-600'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{item.icon}
<span className="font-medium">{item.name}</span>
</Link>
);
})}
</nav>
</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">
<button
className="lg:hidden"
onClick={() => setSidebarOpen(true)}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</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}>
Sign Out
</DropdownItem>
</Dropdown>
</div>
</div>
</header>
{/* Page content */}
<main className="p-6">
{children}
</main>
{/* Footer */}
<Footer variant="dashboard" />
</div>
</div>
);
}

View File

@ -14,6 +14,20 @@ 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
} from 'lucide-react';
// Tooltip component for form field help
const Tooltip = ({ text }: { text: string }) => (
<div className="group relative inline-block ml-1">
<HelpCircle className="w-4 h-4 text-gray-400 cursor-help" />
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 text-white text-xs rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 w-48 text-center">
{text}
<div className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
</div>
</div>
);
// Content-type specific frame options
const getFrameOptionsForContentType = (contentType: string) => {
@ -34,6 +48,14 @@ const getFrameOptionsForContentType = (contentType: string) => {
return [...baseOptions, { id: 'chatme', label: 'Chat Me' }, { id: 'whatsapp', label: 'WhatsApp' }];
case 'TEXT':
return [...baseOptions, { id: 'read', label: 'Read' }, { id: 'info', label: 'Info' }];
case 'PDF':
return [...baseOptions, { id: 'download', label: 'Download' }, { id: 'view', label: 'View PDF' }];
case 'APP':
return [...baseOptions, { id: 'getapp', label: 'Get App' }, { id: 'download', label: 'Download' }];
case 'COUPON':
return [...baseOptions, { id: 'redeem', label: 'Redeem' }, { id: 'save', label: 'Save Offer' }];
case 'FEEDBACK':
return [...baseOptions, { id: 'review', label: 'Review' }, { id: 'feedback', label: 'Feedback' }];
default:
return [...baseOptions, { id: 'website', label: 'Website' }, { id: 'visit', label: 'Visit' }];
}
@ -102,10 +124,14 @@ export default function CreatePage() {
const hasGoodContrast = contrast >= 4.5;
const contentTypes = [
{ value: 'URL', label: 'URL / Website' },
{ value: 'VCARD', label: 'Contact Card' },
{ value: 'GEO', label: 'Location/Maps' },
{ value: 'PHONE', label: 'Phone Number' },
{ value: 'URL', label: 'URL / Website', icon: Globe },
{ value: 'VCARD', label: 'Contact Card', icon: User },
{ value: 'GEO', label: 'Location / Maps', icon: MapPin },
{ value: 'PHONE', label: 'Phone Number', icon: Phone },
{ value: 'PDF', label: 'PDF / File', icon: FileText },
{ value: 'APP', label: 'App Download', icon: Smartphone },
{ value: 'COUPON', label: 'Coupon / Discount', icon: Ticket },
{ value: 'FEEDBACK', label: 'Feedback / Review', icon: Star },
];
// Get QR content based on content type
@ -128,6 +154,14 @@ export default function CreatePage() {
return content.text || 'Sample text';
case 'WHATSAPP':
return `https://wa.me/${content.phone || '+1234567890'}${content.message ? `?text=${encodeURIComponent(content.message)}` : ''}`;
case 'PDF':
return content.fileUrl || 'https://example.com/file.pdf';
case 'APP':
return content.fallbackUrl || content.iosUrl || content.androidUrl || 'https://example.com/app';
case 'COUPON':
return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`;
case 'FEEDBACK':
return content.feedbackUrl || 'https://example.com/feedback';
default:
return 'https://example.com';
}
@ -398,6 +432,139 @@ export default function CreatePage() {
/>
</div>
);
case 'PDF':
return (
<>
<div>
<div className="flex items-center mb-1">
<label className="block text-sm font-medium text-gray-700">PDF/File URL</label>
<Tooltip text="Paste a public link to your PDF (Google Drive, Dropbox, etc.)" />
</div>
<Input
value={content.fileUrl || ''}
onChange={(e) => setContent({ ...content, fileUrl: e.target.value })}
placeholder="https://drive.google.com/file/d/.../view"
required
/>
</div>
<Input
label="File Name (optional)"
value={content.fileName || ''}
onChange={(e) => setContent({ ...content, fileName: e.target.value })}
placeholder="Product Catalog 2026"
/>
</>
);
case 'APP':
return (
<>
<div>
<div className="flex items-center mb-1">
<label className="block text-sm font-medium text-gray-700">iOS App Store URL</label>
<Tooltip text="Link to your app in the Apple App Store" />
</div>
<Input
value={content.iosUrl || ''}
onChange={(e) => setContent({ ...content, iosUrl: e.target.value })}
placeholder="https://apps.apple.com/app/..."
/>
</div>
<div>
<div className="flex items-center mb-1">
<label className="block text-sm font-medium text-gray-700">Android Play Store URL</label>
<Tooltip text="Link to your app in the Google Play Store" />
</div>
<Input
value={content.androidUrl || ''}
onChange={(e) => setContent({ ...content, androidUrl: e.target.value })}
placeholder="https://play.google.com/store/apps/..."
/>
</div>
<div>
<div className="flex items-center mb-1">
<label className="block text-sm font-medium text-gray-700">Fallback URL</label>
<Tooltip text="Where desktop users go (e.g., your website). QR detects device automatically!" />
</div>
<Input
value={content.fallbackUrl || ''}
onChange={(e) => setContent({ ...content, fallbackUrl: e.target.value })}
placeholder="https://yourapp.com"
/>
</div>
</>
);
case 'COUPON':
return (
<>
<Input
label="Coupon Code"
value={content.code || ''}
onChange={(e) => setContent({ ...content, code: e.target.value })}
placeholder="SUMMER20"
required
/>
<Input
label="Discount"
value={content.discount || ''}
onChange={(e) => setContent({ ...content, discount: e.target.value })}
placeholder="20% OFF"
required
/>
<Input
label="Title"
value={content.title || ''}
onChange={(e) => setContent({ ...content, title: e.target.value })}
placeholder="Summer Sale 2026"
/>
<Input
label="Description (optional)"
value={content.description || ''}
onChange={(e) => setContent({ ...content, description: e.target.value })}
placeholder="Valid on all products"
/>
<Input
label="Expiry Date (optional)"
type="date"
value={content.expiryDate || ''}
onChange={(e) => setContent({ ...content, expiryDate: e.target.value })}
/>
<Input
label="Redeem URL (optional)"
value={content.redeemUrl || ''}
onChange={(e) => setContent({ ...content, redeemUrl: e.target.value })}
placeholder="https://shop.example.com?coupon=SUMMER20"
/>
</>
);
case 'FEEDBACK':
return (
<>
<Input
label="Business Name"
value={content.businessName || ''}
onChange={(e) => setContent({ ...content, businessName: e.target.value })}
placeholder="Your Restaurant Name"
required
/>
<div>
<div className="flex items-center mb-1">
<label className="block text-sm font-medium text-gray-700">Google Review URL</label>
<Tooltip text="Redirect satisfied customers to leave a Google review." />
</div>
<Input
value={content.googleReviewUrl || ''}
onChange={(e) => setContent({ ...content, googleReviewUrl: e.target.value })}
placeholder="https://search.google.com/local/writereview?placeid=..."
/>
</div>
<Input
label="Thank You Message"
value={content.thankYouMessage || ''}
onChange={(e) => setContent({ ...content, thankYouMessage: e.target.value })}
placeholder="Thanks for your feedback!"
/>
</>
);
default:
return null;
}
@ -428,12 +595,31 @@ export default function CreatePage() {
required
/>
<Select
label="Content Type"
value={contentType}
onChange={(e) => setContentType(e.target.value)}
options={contentTypes}
/>
{/* Custom Content Type Selector with Icons */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Content Type</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{contentTypes.map((type) => {
const Icon = type.icon;
return (
<button
key={type.value}
type="button"
onClick={() => setContentType(type.value)}
className={cn(
"flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-all text-sm",
contentType === type.value
? "border-primary-500 bg-primary-50 text-primary-700"
: "border-gray-200 hover:border-gray-300 text-gray-600"
)}
>
<Icon className="w-5 h-5" />
<span className="text-xs font-medium text-center">{type.label}</span>
</button>
);
})}
</div>
</div>
{renderContentFields()}
</CardContent>

View File

@ -1,38 +1,254 @@
import type { Metadata } from 'next';
import '@/styles/globals.css';
import { Suspense } from 'react';
import { Providers } from '@/components/Providers';
import AppLayout from './AppLayout';
'use client';
export const metadata: Metadata = {
title: 'Dashboard | QR Master',
description: 'Manage your QR Master dashboard. Create dynamic QR codes, view real-time scan analytics, and configure your account settings in one secure place.',
robots: { index: false, follow: false },
icons: {
icon: [
{ url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/logo.svg', type: 'image/svg+xml' },
],
apple: '/logo.svg',
},
};
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
import { Footer } from '@/components/ui/Footer';
import { useTranslation } from '@/hooks/useTranslation';
export default function RootAppLayout({
interface User {
id: string;
name: string | null;
email: string;
plan: string | null;
}
export default function AppLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const router = useRouter();
const { t } = useTranslation();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [user, setUser] = useState<User | null>(null);
// Fetch user data on mount
useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch('/api/user');
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
} catch (error) {
console.error('Error fetching user:', error);
}
};
fetchUser();
}, []);
const handleSignOut = async () => {
// Track logout event before clearing data
try {
const { trackEvent, resetUser } = await import('@/components/PostHogProvider');
trackEvent('user_logout');
resetUser(); // Reset PostHog user session
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Clear all cookies
document.cookie.split(";").forEach(c => {
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
});
// Clear localStorage
localStorage.clear();
// Redirect to home
router.push('/');
};
// Get user initials for avatar (e.g., "Timo Schmidt" -> "TS")
const getUserInitials = () => {
if (!user) return 'U';
if (user.name) {
const names = user.name.trim().split(' ');
if (names.length >= 2) {
return (names[0][0] + names[names.length - 1][0]).toUpperCase();
}
return user.name.substring(0, 2).toUpperCase();
}
// Fallback to email
return user.email.substring(0, 1).toUpperCase();
};
// Get display name (first name or full name)
const getDisplayName = () => {
if (!user) return 'User';
if (user.name) {
return user.name;
}
// Fallback to email without domain
return user.email.split('@')[0];
};
const navigation = [
{
name: t('nav.dashboard'),
href: '/dashboard',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
),
},
{
name: t('nav.create_qr'),
href: '/create',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
),
},
{
name: t('nav.bulk_creation'),
href: '/bulk-creation',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
),
},
{
name: t('nav.analytics'),
href: '/analytics',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
),
},
{
name: t('nav.pricing'),
href: '/pricing',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
},
{
name: t('nav.settings'),
href: '/settings',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
},
];
return (
<html lang="en">
<body className="font-sans">
<Providers>
<Suspense fallback={null}>
<AppLayout>
<div className="min-h-screen bg-gray-50">
{/* Mobile sidebar backdrop */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={`fixed top-0 left-0 z-50 h-full w-64 bg-white border-r border-gray-200 transform transition-transform lg:translate-x-0 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<Link href="/" className="flex items-center space-x-2">
<img src="/logo.svg" alt="QR Master" className="w-8 h-8" />
<span className="text-xl font-bold text-gray-900">QR Master</span>
</Link>
<button
className="lg:hidden"
onClick={() => setSidebarOpen(false)}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav className="p-4 space-y-1">
{navigation.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={`flex items-center space-x-3 px-3 py-2 rounded-lg transition-colors ${isActive
? 'bg-primary-50 text-primary-600'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{item.icon}
<span className="font-medium">{item.name}</span>
</Link>
);
})}
</nav>
</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">
<button
className="lg:hidden"
onClick={() => setSidebarOpen(true)}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</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}>
Sign Out
</DropdownItem>
</Dropdown>
</div>
</div>
</header>
{/* Page content */}
<main className="p-6">
{children}
</AppLayout>
</Suspense>
</Providers>
</body>
</html>
</main>
{/* Footer */}
<Footer variant="dashboard" />
</div>
</div>
);
}

View File

@ -7,9 +7,8 @@ import { Badge } from '@/components/ui/Badge';
import { showToast } from '@/components/ui/Toast';
import { useRouter } from 'next/navigation';
import { BillingToggle } from '@/components/ui/BillingToggle';
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
export default function PricingClient() {
export default function PricingPage() {
const router = useRouter();
const [loading, setLoading] = useState<string | null>(null);
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
@ -183,9 +182,9 @@ export default function PricingClient() {
return (
<div className="container mx-auto px-4 py-12">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 mb-4">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Choose Your Plan
</h2>
</h1>
<p className="text-xl text-gray-600">
Select the perfect plan for your QR code needs
</p>
@ -261,7 +260,7 @@ export default function PricingClient() {
All plans include unlimited static QR codes and basic customization.
</p>
<p className="text-gray-600 mt-2">
Need help choosing? <ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</ObfuscatedMailto>
Need help choosing? <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a>
</p>
</div>
</div>

View File

@ -242,6 +242,114 @@ export default function EditQRPage() {
</div>
)}
{qrCode.contentType === 'PDF' && (
<>
<Input
label="PDF/File URL"
value={content.fileUrl || ''}
onChange={(e) => setContent({ ...content, fileUrl: e.target.value })}
placeholder="https://drive.google.com/file/d/.../view"
required
/>
<Input
label="File Name (optional)"
value={content.fileName || ''}
onChange={(e) => setContent({ ...content, fileName: e.target.value })}
placeholder="Product Catalog 2026"
/>
</>
)}
{qrCode.contentType === 'APP' && (
<>
<Input
label="iOS App Store URL"
value={content.iosUrl || ''}
onChange={(e) => setContent({ ...content, iosUrl: e.target.value })}
placeholder="https://apps.apple.com/app/..."
/>
<Input
label="Android Play Store URL"
value={content.androidUrl || ''}
onChange={(e) => setContent({ ...content, androidUrl: e.target.value })}
placeholder="https://play.google.com/store/apps/..."
/>
<Input
label="Fallback URL (Desktop)"
value={content.fallbackUrl || ''}
onChange={(e) => setContent({ ...content, fallbackUrl: e.target.value })}
placeholder="https://yourapp.com"
/>
</>
)}
{qrCode.contentType === 'COUPON' && (
<>
<Input
label="Coupon Code"
value={content.code || ''}
onChange={(e) => setContent({ ...content, code: e.target.value })}
placeholder="SUMMER20"
required
/>
<Input
label="Discount"
value={content.discount || ''}
onChange={(e) => setContent({ ...content, discount: e.target.value })}
placeholder="20% OFF"
required
/>
<Input
label="Title"
value={content.title || ''}
onChange={(e) => setContent({ ...content, title: e.target.value })}
placeholder="Summer Sale 2026"
/>
<Input
label="Description (optional)"
value={content.description || ''}
onChange={(e) => setContent({ ...content, description: e.target.value })}
placeholder="Valid on all products"
/>
<Input
label="Expiry Date (optional)"
type="date"
value={content.expiryDate || ''}
onChange={(e) => setContent({ ...content, expiryDate: e.target.value })}
/>
<Input
label="Redeem URL (optional)"
value={content.redeemUrl || ''}
onChange={(e) => setContent({ ...content, redeemUrl: e.target.value })}
placeholder="https://shop.example.com"
/>
</>
)}
{qrCode.contentType === 'FEEDBACK' && (
<>
<Input
label="Business Name"
value={content.businessName || ''}
onChange={(e) => setContent({ ...content, businessName: e.target.value })}
placeholder="Your Restaurant Name"
required
/>
<Input
label="Google Review URL (optional)"
value={content.googleReviewUrl || ''}
onChange={(e) => setContent({ ...content, googleReviewUrl: e.target.value })}
placeholder="https://search.google.com/local/writereview?placeid=..."
/>
<Input
label="Thank You Message"
value={content.thankYouMessage || ''}
onChange={(e) => setContent({ ...content, thankYouMessage: e.target.value })}
placeholder="Thanks for your feedback!"
/>
</>
)}
<div className="flex justify-end space-x-4 pt-4">
<Button
variant="outline"

View File

@ -0,0 +1,196 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Star, ArrowLeft, ChevronLeft, ChevronRight, MessageSquare } from 'lucide-react';
interface Feedback {
id: string;
rating: number;
comment: string;
date: string;
}
interface FeedbackStats {
total: number;
avgRating: number;
distribution: { [key: number]: number };
}
interface Pagination {
page: number;
totalPages: number;
hasMore: boolean;
}
export default function FeedbackListPage() {
const params = useParams();
const router = useRouter();
const qrId = params.id as string;
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
const [stats, setStats] = useState<FeedbackStats | null>(null);
const [pagination, setPagination] = useState<Pagination>({ page: 1, totalPages: 1, hasMore: false });
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
fetchFeedback(currentPage);
}, [qrId, currentPage]);
const fetchFeedback = async (page: number) => {
setLoading(true);
try {
const res = await fetch(`/api/qrs/${qrId}/feedback?page=${page}&limit=20`);
if (res.ok) {
const data = await res.json();
setFeedbacks(data.feedbacks);
setStats(data.stats);
setPagination(data.pagination);
}
} catch (error) {
console.error('Error fetching feedback:', error);
} finally {
setLoading(false);
}
};
const renderStars = (rating: number) => (
<div className="flex gap-0.5">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className={`w-4 h-4 ${star <= rating ? 'text-amber-400 fill-amber-400' : 'text-gray-200'}`}
/>
))}
</div>
);
if (loading && !stats) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
</div>
);
}
return (
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="mb-8">
<Link href={`/qr/${qrId}`} className="inline-flex items-center text-gray-500 hover:text-gray-700 mb-4">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to QR Code
</Link>
<h1 className="text-3xl font-bold text-gray-900">Customer Feedback</h1>
<p className="text-gray-600 mt-1">{stats?.total || 0} total responses</p>
</div>
{/* Stats Overview */}
{stats && (
<Card className="mb-8">
<CardContent className="p-6">
<div className="flex flex-col md:flex-row md:items-center gap-8">
{/* Average Rating */}
<div className="text-center md:text-left">
<div className="text-5xl font-bold text-gray-900 mb-1">{stats.avgRating}</div>
<div className="flex justify-center md:justify-start mb-1">
{renderStars(Math.round(stats.avgRating))}
</div>
<p className="text-sm text-gray-500">{stats.total} reviews</p>
</div>
{/* Distribution */}
<div className="flex-1 space-y-2">
{[5, 4, 3, 2, 1].map((rating) => {
const count = stats.distribution[rating] || 0;
const percentage = stats.total > 0 ? (count / stats.total) * 100 : 0;
return (
<div key={rating} className="flex items-center gap-3">
<span className="text-sm text-gray-600 w-12">{rating} stars</span>
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-amber-400 rounded-full transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="text-sm text-gray-500 w-12 text-right">{count}</span>
</div>
);
})}
</div>
</div>
</CardContent>
</Card>
)}
{/* Feedback List */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare className="w-5 h-5" />
All Reviews
</CardTitle>
</CardHeader>
<CardContent>
{feedbacks.length === 0 ? (
<div className="text-center py-12 text-gray-500">
<Star className="w-12 h-12 mx-auto mb-4 text-gray-300" />
<p>No feedback received yet</p>
</div>
) : (
<div className="divide-y divide-gray-100">
{feedbacks.map((feedback) => (
<div key={feedback.id} className="py-4">
<div className="flex items-center justify-between mb-2">
{renderStars(feedback.rating)}
<span className="text-sm text-gray-400">
{new Date(feedback.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</span>
</div>
{feedback.comment && (
<p className="text-gray-700">{feedback.comment}</p>
)}
</div>
))}
</div>
)}
{/* Pagination */}
{pagination.totalPages > 1 && (
<div className="flex items-center justify-between mt-6 pt-6 border-t">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
>
<ChevronLeft className="w-4 h-4 mr-1" />
Previous
</Button>
<span className="text-sm text-gray-500">
Page {currentPage} of {pagination.totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((p) => p + 1)}
disabled={!pagination.hasMore}
>
Next
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,287 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import { QRCodeSVG } from 'qrcode.react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import {
ArrowLeft, Edit, ExternalLink, Star, MessageSquare,
BarChart3, Copy, Check, Pause, Play
} from 'lucide-react';
import { showToast } from '@/components/ui/Toast';
import { useCsrf } from '@/hooks/useCsrf';
interface QRCode {
id: string;
title: string;
type: 'STATIC' | 'DYNAMIC';
contentType: string;
content: any;
slug: string;
status: 'ACTIVE' | 'PAUSED';
style: any;
createdAt: string;
_count?: { scans: number };
}
interface FeedbackStats {
total: number;
avgRating: number;
distribution: { [key: number]: number };
}
export default function QRDetailPage() {
const params = useParams();
const router = useRouter();
const qrId = params.id as string;
const { fetchWithCsrf } = useCsrf();
const [qrCode, setQrCode] = useState<QRCode | null>(null);
const [feedbackStats, setFeedbackStats] = useState<FeedbackStats | null>(null);
const [loading, setLoading] = useState(true);
const [copied, setCopied] = useState(false);
useEffect(() => {
fetchQRCode();
}, [qrId]);
const fetchQRCode = async () => {
try {
const res = await fetch(`/api/qrs/${qrId}`);
if (res.ok) {
const data = await res.json();
setQrCode(data);
// Fetch feedback stats if it's a feedback QR
if (data.contentType === 'FEEDBACK') {
const feedbackRes = await fetch(`/api/qrs/${qrId}/feedback?limit=1`);
if (feedbackRes.ok) {
const feedbackData = await feedbackRes.json();
setFeedbackStats(feedbackData.stats);
}
}
} else {
showToast('QR code not found', 'error');
router.push('/dashboard');
}
} catch (error) {
console.error('Error fetching QR code:', error);
} finally {
setLoading(false);
}
};
const copyLink = async () => {
const url = `${window.location.origin}/r/${qrCode?.slug}`;
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
showToast('Link copied!', 'success');
};
const toggleStatus = async () => {
if (!qrCode) return;
const newStatus = qrCode.status === 'ACTIVE' ? 'PAUSED' : 'ACTIVE';
try {
const res = await fetchWithCsrf(`/api/qrs/${qrId}`, {
method: 'PATCH',
body: JSON.stringify({ status: newStatus }),
});
if (res.ok) {
setQrCode({ ...qrCode, status: newStatus });
showToast(`QR code ${newStatus === 'ACTIVE' ? 'activated' : 'paused'}`, 'success');
}
} catch (error) {
showToast('Failed to update status', 'error');
}
};
const renderStars = (rating: number) => (
<div className="flex gap-0.5">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className={`w-4 h-4 ${star <= rating ? 'text-amber-400 fill-amber-400' : 'text-gray-200'}`}
/>
))}
</div>
);
if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
</div>
);
}
if (!qrCode) return null;
const qrUrl = `${typeof window !== 'undefined' ? window.location.origin : ''}/r/${qrCode.slug}`;
return (
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-8">
<Link href="/dashboard" className="inline-flex items-center text-gray-500 hover:text-gray-700 mb-4">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Dashboard
</Link>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-gray-900">{qrCode.title}</h1>
<div className="flex items-center gap-2 mt-2">
<Badge variant={qrCode.type === 'DYNAMIC' ? 'info' : 'default'}>
{qrCode.type}
</Badge>
<Badge variant={qrCode.status === 'ACTIVE' ? 'success' : 'warning'}>
{qrCode.status}
</Badge>
<Badge>{qrCode.contentType}</Badge>
</div>
</div>
<div className="flex gap-2">
{qrCode.type === 'DYNAMIC' && (
<>
<Button variant="outline" size="sm" onClick={toggleStatus}>
{qrCode.status === 'ACTIVE' ? <Pause className="w-4 h-4 mr-1" /> : <Play className="w-4 h-4 mr-1" />}
{qrCode.status === 'ACTIVE' ? 'Pause' : 'Activate'}
</Button>
<Link href={`/qr/${qrId}/edit`}>
<Button variant="outline" size="sm">
<Edit className="w-4 h-4 mr-1" /> Edit
</Button>
</Link>
</>
)}
</div>
</div>
</div>
<div className="grid lg:grid-cols-3 gap-8">
{/* Left: QR Code */}
<div>
<Card>
<CardContent className="p-6 flex flex-col items-center">
<div className="bg-white p-4 rounded-xl shadow-sm mb-4">
<QRCodeSVG
value={qrUrl}
size={200}
fgColor={qrCode.style?.foregroundColor || '#000000'}
bgColor={qrCode.style?.backgroundColor || '#FFFFFF'}
/>
</div>
<div className="w-full space-y-2">
<Button variant="outline" className="w-full" onClick={copyLink}>
{copied ? <Check className="w-4 h-4 mr-2" /> : <Copy className="w-4 h-4 mr-2" />}
{copied ? 'Copied!' : 'Copy Link'}
</Button>
<a href={qrUrl} target="_blank" rel="noopener noreferrer" className="block">
<Button variant="outline" className="w-full">
<ExternalLink className="w-4 h-4 mr-2" /> Open Link
</Button>
</a>
</div>
</CardContent>
</Card>
</div>
{/* Right: Stats & Info */}
<div className="lg:col-span-2 space-y-6">
{/* Quick Stats */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
<Card>
<CardContent className="p-4 text-center">
<BarChart3 className="w-6 h-6 mx-auto mb-2 text-indigo-500" />
<p className="text-2xl font-bold text-gray-900">{qrCode._count?.scans || 0}</p>
<p className="text-sm text-gray-500">Total Scans</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-gray-900">{qrCode.type}</p>
<p className="text-sm text-gray-500">QR Type</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-gray-900">
{new Date(qrCode.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</p>
<p className="text-sm text-gray-500">Created</p>
</CardContent>
</Card>
</div>
{/* Feedback Summary (only for FEEDBACK type) */}
{qrCode.contentType === 'FEEDBACK' && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Star className="w-5 h-5 text-amber-400" />
Customer Feedback
</CardTitle>
</CardHeader>
<CardContent>
{feedbackStats && feedbackStats.total > 0 ? (
<div className="flex flex-col sm:flex-row sm:items-center gap-6 mb-4">
{/* Average */}
<div className="text-center sm:text-left">
<div className="text-4xl font-bold text-gray-900">{feedbackStats.avgRating}</div>
{renderStars(Math.round(feedbackStats.avgRating))}
<p className="text-sm text-gray-500 mt-1">{feedbackStats.total} reviews</p>
</div>
{/* Distribution */}
<div className="flex-1 space-y-1">
{[5, 4, 3, 2, 1].map((rating) => {
const count = feedbackStats.distribution[rating] || 0;
const pct = feedbackStats.total > 0 ? (count / feedbackStats.total) * 100 : 0;
return (
<div key={rating} className="flex items-center gap-2 text-sm">
<span className="w-8 text-gray-500">{rating}</span>
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div className="h-full bg-amber-400 rounded-full" style={{ width: `${pct}%` }} />
</div>
<span className="w-8 text-gray-400 text-right">{count}</span>
</div>
);
})}
</div>
</div>
) : (
<p className="text-gray-500 mb-4">No feedback received yet. Share your QR code to collect reviews!</p>
)}
<Link href={`/qr/${qrId}/feedback`} className="block">
<Button variant="outline" className="w-full">
<MessageSquare className="w-4 h-4 mr-2" />
View All Feedback
</Button>
</Link>
</CardContent>
</Card>
)}
{/* Content Info */}
<Card>
<CardHeader>
<CardTitle>Content Details</CardTitle>
</CardHeader>
<CardContent>
<pre className="bg-gray-50 p-4 rounded-lg text-sm overflow-auto">
{JSON.stringify(qrCode.content, null, 2)}
</pre>
</CardContent>
</Card>
</div>
</div>
</div>
);
}

View File

@ -1,38 +1,11 @@
import '@/styles/globals.css';
import { Providers } from '@/components/Providers';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Authentication | QR Master',
description: 'Securely login or sign up to QR Master to manage your dynamic QR codes, track analytics, and access premium features. Your gateway to professional QR management.',
icons: {
icon: [
{ url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/logo.svg', type: 'image/svg+xml' },
],
apple: '/logo.svg',
},
};
export default function AuthRootLayout({
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="font-sans">
<Providers>
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white">
{children}
<div className="py-6 text-center text-sm text-slate-500 space-x-4">
<a href="/" className="hover:text-primary-600 transition-colors">Home</a>
<a href="/privacy" className="hover:text-primary-600 transition-colors">Privacy</a>
<a href="/faq" className="hover:text-primary-600 transition-colors">FAQ</a>
</div>
</div>
</Providers>
</body>
</html>
);
}

View File

@ -1,164 +0,0 @@
'use client';
import React, { useState, useEffect } 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';
export default function LoginClientPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { t } = useTranslation();
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const response = await fetchWithCsrf('/api/auth/simple-login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (response.ok && data.success) {
// Store user in localStorage for client-side
localStorage.setItem('user', JSON.stringify(data.user));
// Track successful login with PostHog
try {
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
identifyUser(data.user.id, {
email: data.user.email,
name: data.user.name,
plan: data.user.plan || 'FREE',
});
trackEvent('user_login', {
method: 'email',
email: data.user.email,
});
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Check for redirect parameter
const redirectUrl = searchParams.get('redirect') || '/dashboard';
router.push(redirectUrl);
router.refresh();
} else {
setError(data.error || 'Invalid email or password');
}
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = '/api/auth/google';
};
return (
<Card>
<CardContent className="p-6">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
)}
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
<Input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
<div className="flex items-center justify-between">
<label className="flex items-center">
<input type="checkbox" className="mr-2" />
<span className="text-sm text-gray-600">Remember me</span>
</label>
<Link href="/forgot-password" className="text-sm text-primary-600 hover:text-primary-700">
Forgot password?
</Link>
</div>
<Button type="submit" className="w-full" loading={loading} disabled={csrfLoading || loading}>
{csrfLoading ? 'Loading...' : 'Sign In'}
</Button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Or continue with</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleGoogleSignIn}
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</Button>
</form>
<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>
</p>
</div>
</CardContent>
</Card>
);
}

View File

@ -1,38 +1,76 @@
import React, { Suspense } from 'react';
import type { Metadata } from 'next';
'use client';
import React, { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import LoginClientPage from './ClientPage';
export const metadata: Metadata = {
title: {
absolute: 'Login to QR Master | Access Your Dashboard'
},
description: 'Sign in to QR Master to create, manage, and track your QR codes. Access your dashboard and view analytics.',
alternates: {
canonical: 'https://www.qrmaster.net/login',
},
openGraph: {
title: 'Login to QR Master | Access Your Dashboard',
description: 'Sign in to QR Master to create, manage, and track your QR codes.',
url: 'https://www.qrmaster.net/login',
type: 'website',
images: [{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'QR Master Login',
}],
},
twitter: {
card: 'summary_large_image',
title: 'Login to QR Master | Access Your Dashboard',
description: 'Sign in to QR Master to create, manage, and track your QR codes.',
images: ['https://www.qrmaster.net/og-image.png'],
},
};
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 LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { t } = useTranslation();
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const response = await fetchWithCsrf('/api/auth/simple-login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (response.ok && data.success) {
// Store user in localStorage for client-side
localStorage.setItem('user', JSON.stringify(data.user));
// Track successful login with PostHog
try {
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
identifyUser(data.user.id, {
email: data.user.email,
name: data.user.name,
plan: data.user.plan || 'FREE',
});
trackEvent('user_login', {
method: 'email',
email: data.user.email,
});
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Check for redirect parameter
const redirectUrl = searchParams.get('redirect') || '/dashboard';
router.push(redirectUrl);
router.refresh();
} else {
setError(data.error || 'Invalid email or password');
}
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = '/api/auth/google';
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
<div className="w-full max-w-md">
@ -48,13 +86,94 @@ export default function LoginPage() {
</Link>
</div>
<Suspense fallback={
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-8 flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
<Card>
<CardContent className="p-6">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
}>
<LoginClientPage />
</Suspense>
)}
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
<Input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
<div className="flex items-center justify-between">
<label className="flex items-center">
<input type="checkbox" className="mr-2" />
<span className="text-sm text-gray-600">Remember me</span>
</label>
<Link href="/forgot-password" className="text-sm text-primary-600 hover:text-primary-700">
Forgot password?
</Link>
</div>
<Button type="submit" className="w-full" loading={loading} disabled={csrfLoading || loading}>
{csrfLoading ? 'Loading...' : 'Sign In'}
</Button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Or continue with</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleGoogleSignIn}
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</Button>
</form>
<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>
</p>
</div>
</CardContent>
</Card>
<p className="text-center text-sm text-gray-500 mt-6">
By signing in, you agree to our{' '}

View File

@ -8,9 +8,7 @@ import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { useCsrf } from '@/hooks/useCsrf';
import { Suspense } from 'react';
function ResetPasswordContent() {
export default function ResetPasswordPage() {
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
const searchParams = useSearchParams();
const router = useRouter();
@ -208,11 +206,3 @@ function ResetPasswordContent() {
</div>
);
}
export default function ResetPasswordPage() {
return (
<Suspense fallback={<div className="min-h-screen flex items-center justify-center">Loading...</div>}>
<ResetPasswordContent />
</Suspense>
);
}

View File

@ -1,185 +0,0 @@
'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 SignupClientPage() {
const router = useRouter();
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 handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
if (password !== confirmPassword) {
setError('Passwords do not match');
setLoading(false);
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
setLoading(false);
return;
}
try {
const response = await fetchWithCsrf('/api/auth/signup', {
method: 'POST',
body: JSON.stringify({ name, email, password }),
});
const data = await response.json();
if (response.ok && data.success) {
// Store user in localStorage for client-side
localStorage.setItem('user', JSON.stringify(data.user));
// Track successful signup with PostHog
try {
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
identifyUser(data.user.id, {
email: data.user.email,
name: data.user.name,
plan: data.user.plan || 'FREE',
signupMethod: 'email',
});
trackEvent('user_signup', {
method: 'email',
email: data.user.email,
});
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Redirect to dashboard
router.push('/dashboard');
router.refresh();
} else {
setError(data.error || 'Failed to create account');
}
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = '/api/auth/google';
};
return (
<Card>
<CardContent className="p-6">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
)}
<Input
label="Full Name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="John Doe"
required
/>
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
<Input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
<Input
label="Confirm Password"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
/>
<Button type="submit" className="w-full" loading={loading}>
Create Account
</Button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Or continue with</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleGoogleSignIn}
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign up with Google
</Button>
</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>
</div>
</CardContent>
</Card>
);
}

View File

@ -1,39 +1,89 @@
import React, { Suspense } from 'react';
import type { Metadata } from 'next';
'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import SignupClientPage from './ClientPage';
export const metadata: Metadata = {
title: {
absolute: 'Create Free Account | QR Master'
},
description: 'Sign up for QR Master to create free QR codes. Start with tracking, customization, and bulk generation features.',
alternates: {
canonical: 'https://www.qrmaster.net/signup',
},
openGraph: {
title: 'Create Free Account | QR Master',
description: 'Sign up for QR Master to create free QR codes with tracking and customization.',
url: 'https://www.qrmaster.net/signup',
type: 'website',
images: [{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'QR Master Sign Up',
}],
},
twitter: {
card: 'summary_large_image',
title: 'Create Free Account | QR Master',
description: 'Sign up for QR Master to create free QR codes with tracking and customization.',
images: ['https://www.qrmaster.net/og-image.png'],
},
};
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 SignupPage() {
const router = useRouter();
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 handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
if (password !== confirmPassword) {
setError('Passwords do not match');
setLoading(false);
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
setLoading(false);
return;
}
try {
const response = await fetchWithCsrf('/api/auth/signup', {
method: 'POST',
body: JSON.stringify({ name, email, password }),
});
const data = await response.json();
if (response.ok && data.success) {
// Store user in localStorage for client-side
localStorage.setItem('user', JSON.stringify(data.user));
// Track successful signup with PostHog
try {
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
identifyUser(data.user.id, {
email: data.user.email,
name: data.user.name,
plan: data.user.plan || 'FREE',
signupMethod: 'email',
});
trackEvent('user_signup', {
method: 'email',
email: data.user.email,
});
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Redirect to dashboard
router.push('/dashboard');
router.refresh();
} else {
setError(data.error || 'Failed to create account');
}
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = '/api/auth/google';
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
<div className="w-full max-w-md">
@ -49,13 +99,102 @@ export default function SignupPage() {
</Link>
</div>
<Suspense fallback={
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-8 flex items-center justify-center min-h-[500px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
<Card>
<CardContent className="p-6">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
}>
<SignupClientPage />
</Suspense>
)}
<Input
label="Full Name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="John Doe"
required
/>
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
<Input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
<Input
label="Confirm Password"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
/>
<Button type="submit" className="w-full" loading={loading}>
Create Account
</Button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Or continue with</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleGoogleSignIn}
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign up with Google
</Button>
</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>
</div>
</CardContent>
</Card>
<p className="text-center text-sm text-gray-500 mt-6">
By signing up, you agree to our{' '}

View File

@ -1,287 +0,0 @@
'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Footer } from '@/components/ui/Footer';
import en from '@/i18n/en.json';
import { ChevronDown, Wifi, Contact, MessageCircle, QrCode, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'framer-motion';
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [toolsOpen, setToolsOpen] = useState(false);
const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
const pathname = usePathname();
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
// Check immediately on mount
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Close simple menus when path changes
useEffect(() => {
setMobileMenuOpen(false);
setToolsOpen(false);
}, [pathname]);
// Default to English for general marketing pages
const t = en;
const tools = [
{ name: 'URL / Link', description: 'Link to any website', href: '/tools/url-qr-code', icon: Link2, color: 'text-blue-500', bgColor: 'bg-blue-50' },
{ name: 'Text', description: 'Plain text message', href: '/tools/text-qr-code', icon: Type, color: 'text-slate-500', bgColor: 'bg-slate-50' },
{ name: 'WiFi', description: 'Share WiFi credentials', href: '/tools/wifi-qr-code', icon: Wifi, color: 'text-indigo-500', bgColor: 'bg-indigo-50' },
{ name: 'VCard', description: 'Digital business card', href: '/tools/vcard-qr-code', icon: Contact, color: 'text-pink-500', bgColor: 'bg-pink-50' },
{ name: 'WhatsApp', description: 'Start a chat', href: '/tools/whatsapp-qr-code', icon: MessageCircle, color: 'text-green-500', bgColor: 'bg-green-50' },
{ name: 'Email', description: 'Compose an email', href: '/tools/email-qr-code', icon: Mail, color: 'text-amber-500', bgColor: 'bg-amber-50' },
{ name: 'SMS', description: 'Send a text message', href: '/tools/sms-qr-code', icon: MessageSquare, color: 'text-cyan-500', bgColor: 'bg-cyan-50' },
{ name: 'Phone', description: 'Start a call', href: '/tools/phone-qr-code', icon: Phone, color: 'text-violet-500', bgColor: 'bg-violet-50' },
{ name: 'Event', description: 'Add calendar event', href: '/tools/event-qr-code', icon: Calendar, color: 'text-red-500', bgColor: 'bg-red-50' },
{ name: 'Location', description: 'Share a place', href: '/tools/geolocation-qr-code', icon: MapPin, color: 'text-emerald-500', bgColor: 'bg-emerald-50' },
{ name: 'Facebook', description: 'Facebook profile/page', href: '/tools/facebook-qr-code', icon: Facebook, color: 'text-blue-600', bgColor: 'bg-blue-50' },
{ name: 'Instagram', description: 'Instagram profile', href: '/tools/instagram-qr-code', icon: Instagram, color: 'text-pink-600', bgColor: 'bg-pink-50' },
{ name: 'Twitter / X', description: 'Twitter profile', href: '/tools/twitter-qr-code', icon: Twitter, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'YouTube', description: 'YouTube video/channel', href: '/tools/youtube-qr-code', icon: Youtube, color: 'text-red-600', bgColor: 'bg-red-50' },
{ name: 'TikTok', description: 'TikTok profile', href: '/tools/tiktok-qr-code', icon: Music, color: 'text-slate-800', bgColor: 'bg-slate-100' },
{ name: 'Crypto', description: 'Share wallet address', href: '/tools/crypto-qr-code', icon: Bitcoin, color: 'text-orange-500', bgColor: 'bg-orange-50' },
{ name: 'PayPal', description: 'Receive payments', href: '/tools/paypal-qr-code', icon: CreditCard, color: 'text-blue-700', bgColor: 'bg-blue-50' },
{ name: 'Zoom', description: 'Join Zoom meeting', href: '/tools/zoom-qr-code', icon: Video, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'Teams', description: 'Join Teams meeting', href: '/tools/teams-qr-code', icon: Users, color: 'text-violet-500', bgColor: 'bg-violet-50' },
];
return (
<div className="min-h-screen bg-white">
{/* Server-rendered navigation links for SEO (crawlers) - Placed first for priority */}
<div className="sr-only" aria-hidden="false">
<nav aria-label="Site Map">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/pricing">Pricing</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/faq">FAQ</a></li>
<li><a href="/login">Login</a></li>
<li><a href="/signup">Sign Up</a></li>
{/* Tools */}
<li><a href="/tools/url-qr-code">URL QR Code</a></li>
<li><a href="/tools/text-qr-code">Text QR Code</a></li>
<li><a href="/tools/wifi-qr-code">WiFi QR Code</a></li>
<li><a href="/tools/vcard-qr-code">vCard QR Code</a></li>
<li><a href="/tools/whatsapp-qr-code">WhatsApp QR Code</a></li>
<li><a href="/tools/email-qr-code">Email QR Code</a></li>
<li><a href="/tools/sms-qr-code">SMS QR Code</a></li>
<li><a href="/tools/phone-qr-code">Phone QR Code</a></li>
<li><a href="/tools/event-qr-code">Event QR Code</a></li>
<li><a href="/tools/geolocation-qr-code">Location QR Code</a></li>
<li><a href="/tools/facebook-qr-code">Facebook QR Code</a></li>
<li><a href="/tools/instagram-qr-code">Instagram QR Code</a></li>
<li><a href="/tools/twitter-qr-code">Twitter QR Code</a></li>
<li><a href="/tools/youtube-qr-code">YouTube QR Code</a></li>
<li><a href="/tools/tiktok-qr-code">TikTok QR Code</a></li>
<li><a href="/tools/crypto-qr-code">Crypto QR Code</a></li>
<li><a href="/tools/paypal-qr-code">PayPal QR Code</a></li>
<li><a href="/tools/zoom-qr-code">Zoom QR Code</a></li>
<li><a href="/tools/teams-qr-code">Teams QR Code</a></li>
</ul>
</nav>
</div>
{/* Header */}
<header
className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm"
>
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl h-20 flex items-center justify-between">
{/* Logo */}
<Link href="/" className="flex items-center space-x-2.5 group">
<div className="relative w-9 h-9 flex items-center justify-center bg-indigo-600 rounded-lg shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
<QrCode className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-1">
{/* Tools Dropdown */}
<div
className="relative group px-3 py-2"
onMouseEnter={() => setToolsOpen(true)}
onMouseLeave={() => setToolsOpen(false)}
>
<button className="flex items-center space-x-1 text-sm font-medium text-slate-600 group-hover:text-slate-900 transition-colors">
<span>{t.nav.tools}</span>
<ChevronDown className={cn("w-4 h-4 transition-transform duration-200", toolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{toolsOpen && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.15 }}
className="absolute left-1/2 -translate-x-1/2 top-full mt-2 w-[750px] bg-white rounded-2xl shadow-lg border border-slate-100 p-4 overflow-hidden"
>
<div className="grid grid-cols-3 gap-1">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center space-x-3 p-2.5 rounded-xl transition-colors hover:bg-slate-50"
>
<div className={cn("p-2 rounded-lg shrink-0", tool.bgColor, tool.color)}>
<tool.icon className="w-4 h-4" />
</div>
<div>
<div className="text-sm font-semibold text-slate-900">{tool.name}</div>
<p className="text-xs text-slate-500 leading-snug">{tool.description}</p>
</div>
</Link>
))}
</div>
<div className="mt-3 pt-3 border-t border-slate-100 -mx-4 -mb-4 px-4 py-3 text-center bg-slate-50/50">
<p className="text-xs text-slate-500 font-medium">{t.nav.all_free}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<Link href="/#features" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.features}
</Link>
<Link href="/#pricing" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.pricing}
</Link>
<Link href="/blog" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.blog}
</Link>
<Link href="/#faq" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.faq}
</Link>
</div>
<div className="hidden md:flex items-center space-x-4">
<Link href="/login" className="text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.login}
</Link>
<Link href="/signup">
<Button className={cn(
"font-semibold shadow-lg shadow-indigo-500/20 transition-all hover:scale-105",
scrolled ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-blue-600 text-white hover:bg-blue-700"
)}>
{t.nav.cta || "Get Started Free"}
</Button>
</Link>
</div>
{/* Mobile Menu Button - Always dark */}
<button
className="md:hidden p-2 text-slate-900"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</nav>
{/* Mobile Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden bg-white border-b border-slate-100 overflow-hidden"
>
<div className="container mx-auto px-4 py-6 space-y-2">
{/* Free Tools Accordion */}
<button
onClick={() => setMobileToolsOpen(!mobileToolsOpen)}
className="flex items-center justify-between w-full px-4 py-3 rounded-xl hover:bg-slate-50 text-slate-700 font-semibold"
>
<span>{t.nav.tools}</span>
<ChevronDown className={cn("w-5 h-5 transition-transform", mobileToolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{mobileToolsOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<div className="max-h-[50vh] overflow-y-auto pl-4 space-y-1 border-l-2 border-slate-100 ml-4">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center gap-3 px-4 py-2.5 rounded-lg hover:bg-slate-50 text-slate-600 text-sm"
onClick={() => { setMobileMenuOpen(false); setMobileToolsOpen(false); }}
>
<tool.icon className={cn("w-4 h-4", tool.color)} />
{tool.name}
</Link>
))}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="h-px bg-slate-100 my-2"></div>
<Link href="/#features" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.features}</Link>
<Link href="/#pricing" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.pricing}</Link>
<Link href="/blog" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.blog}</Link>
<Link href="/#faq" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.faq}</Link>
<div className="grid grid-cols-2 gap-4 pt-4">
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
<Button variant="outline" className="w-full justify-center">{t.nav.login}</Button>
</Link>
<Link href="/signup" onClick={() => setMobileMenuOpen(false)}>
<Button className="w-full justify-center bg-indigo-600 hover:bg-indigo-700">{t.nav.cta}</Button>
</Link>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</header>
{/* Main Content */}
<main className="pt-20">
{/* Server-rendered navigation links for SEO (crawlers) */}
{children}
</main>
{/* Footer */}
<Footer t={t} />
</div >
);
}

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@ import { websiteSchema, breadcrumbSchema } from '@/lib/schema';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import { blogPostList } from '@/lib/blog-data';
function truncateAtWord(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
@ -19,7 +18,7 @@ function truncateAtWord(text: string, maxLength: number): string {
export async function generateMetadata(): Promise<Metadata> {
const title = truncateAtWord('QR Insights: Latest QR Strategies', 60);
const description = truncateAtWord(
'Expert guides on QR code analytics, dynamic vs static codes, bulk generation, and smart marketing use cases. Learn how to maximize your QR campaign ROI.',
'Expert guides on QR analytics, dynamic codes & smart marketing uses.',
160
);
@ -38,14 +37,6 @@ export async function generateMetadata(): Promise<Metadata> {
description,
url: 'https://www.qrmaster.net/blog',
type: 'website',
images: [
{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'QR Insights - QR Code Marketing & Analytics Blog',
},
],
},
twitter: {
title,
@ -54,7 +45,82 @@ export async function generateMetadata(): Promise<Metadata> {
};
}
const blogPosts = [
// NEW POSTS (January 2026)
{
slug: 'qr-code-restaurant-menu',
title: 'How to Create a QR Code for Restaurant Menu',
excerpt: 'Step-by-step guide to creating digital menu QR codes for your restaurant. Learn best practices for touchless menus, placement tips, and tracking.',
date: 'January 5, 2026',
readTime: '12 Min',
category: 'Restaurant',
image: '/blog/restaurant-qr-menu.png',
},
{
slug: 'vcard-qr-code-generator',
title: 'Free vCard QR Code Generator: Digital Business Cards',
excerpt: 'Create professional vCard QR codes for digital business cards. Share contact info instantly with a scan—includes templates and best practices.',
date: 'January 5, 2026',
readTime: '10 Min',
category: 'Business Cards',
image: '/blog/vcard-qr-code.png',
},
{
slug: 'qr-code-small-business',
title: 'Best QR Code Generator for Small Business: 2025 Guide',
excerpt: 'Find the best QR code solution for your small business. Compare features, pricing, and use cases for marketing, payments, and operations.',
date: 'January 5, 2026',
readTime: '14 Min',
category: 'Business',
image: '/blog/small-business-qr.png',
},
{
slug: 'qr-code-print-size-guide',
title: 'QR Code Print Size Guide: Minimum Sizes for Every Use Case',
excerpt: 'Complete guide to QR code print sizes. Learn minimum dimensions for business cards, posters, banners, and more to ensure reliable scanning.',
date: 'January 5, 2026',
readTime: '8 Min',
category: 'Printing',
image: '/blog/qr-print-sizes.png',
},
// EXISTING POSTS
{
slug: 'qr-code-tracking-guide-2025',
title: 'QR Code Tracking: Complete Guide 2025',
excerpt: 'Learn how to track QR code scans with real-time analytics. Compare free vs paid tracking tools, setup Google Analytics, and measure ROI.',
date: 'October 18, 2025',
readTime: '12 Min',
category: 'Tracking & Analytics',
image: '/blog/1-hero.png',
},
{
slug: 'dynamic-vs-static-qr-codes',
title: 'Dynamic vs Static QR Codes: Which Should You Use?',
excerpt: 'Understand the difference between static and dynamic QR codes. Learn when to use each type, pros/cons, and how dynamic QR codes save money.',
date: 'October 17, 2025',
readTime: '10 Min',
category: 'QR Code Basics',
image: '/blog/2-hero.png',
},
{
slug: 'bulk-qr-code-generator-excel',
title: 'How to Generate Bulk QR Codes from Excel',
excerpt: 'Generate hundreds of QR codes from Excel or CSV files in minutes. Step-by-step guide with templates, best practices, and free tools.',
date: 'October 16, 2025',
readTime: '13 Min',
category: 'Bulk Generation',
image: '/blog/3-hero.png',
},
{
slug: 'qr-code-analytics',
title: 'QR Code Analytics: Track, Measure & Optimize Campaigns',
excerpt: 'Learn how to leverage scan analytics, campaign tracking, and dashboard insights to maximize QR code ROI.',
date: 'October 16, 2025',
readTime: '15 Min',
category: 'Analytics',
image: '/blog/4-hero.png',
},
];
export default function BlogPage() {
const breadcrumbItems: BreadcrumbItem[] = [
@ -79,8 +145,8 @@ export default function BlogPage() {
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
{blogPostList.map((post: any) => (
<Link key={post.slug} href={post.link || `/blog/${post.slug}`}>
{blogPosts.map((post) => (
<Link key={post.slug} href={`/blog/${post.slug}`}>
<Card hover className="h-full overflow-hidden shadow-md hover:shadow-xl transition-all duration-300">
<div className="relative h-56 overflow-hidden">
<Image
@ -102,9 +168,7 @@ export default function BlogPage() {
<p className="text-gray-600 mb-4 leading-relaxed">{post.excerpt}</p>
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
<p className="text-sm text-gray-500">{post.date}</p>
<span className="text-primary-600 text-sm font-medium">
{post.link ? 'Try Now →' : 'Read Article →'}
</span>
<span className="text-primary-600 text-sm font-medium">Read more </span>
</div>
</CardContent>
</Card>

View File

@ -8,8 +8,8 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import { breadcrumbSchema } from '@/lib/schema';
export const metadata: Metadata = {
title: 'Bulk QR Code Generator | Create from Excel | QR Master',
description: 'Generate hundreds of QR codes instantly from Excel/CSV. Create URLs, vCards, and text codes in bulk. Perfect for inventory, events, and product tagging.',
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel | QR Master',
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Create URLs, vCards, locations, phone numbers, and text QR codes in bulk. Perfect for products, events, inventory management.',
keywords: 'bulk qr code generator, batch qr code, qr code from excel, csv qr code generator, mass qr code generation, bulk vcard qr code, bulk qr codes free',
alternates: {
canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
@ -23,14 +23,6 @@ export const metadata: Metadata = {
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.',
url: 'https://www.qrmaster.net/bulk-qr-code-generator',
type: 'website',
images: [
{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'Bulk QR Code Generator - QR Master',
},
],
},
twitter: {
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel',
@ -54,7 +46,7 @@ export default function BulkQRCodeGeneratorPage() {
title: 'Contact Cards',
description: 'Create vCard QR codes with contact information',
format: 'FirstName,LastName,Email,Phone,Organization,Title',
example: 'John Doe,VCARD,John,Doe,john' + '@' + 'example.com,+1234567890,Company Inc,CEO',
example: 'John Doe,VCARD,John,Doe,john@example.com,+1234567890,Company Inc,CEO',
},
{
type: 'GEO',
@ -341,7 +333,7 @@ export default function BulkQRCodeGeneratorPage() {
Start Bulk Generation
</Button>
</Link>
<Link href="/signup">
<Link href="/create">
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
Try Single QR First
</Button>
@ -448,7 +440,7 @@ export default function BulkQRCodeGeneratorPage() {
<tr className="border-b border-gray-200">
<td className="py-2 px-3">John Doe</td>
<td className="py-2 px-3">VCARD</td>
<td className="py-2 px-3">John,Doe,john{'@'}example.com,+1234567890,Company,CEO</td>
<td className="py-2 px-3">John,Doe,john@example.com,+1234567890,Company,CEO</td>
<td className="py-2 px-3">contact</td>
</tr>
<tr className="border-b border-gray-200">
@ -641,35 +633,26 @@ Product C,https://example.com/product-c,Budget Widget,electronics,sale`}
</section>
{/* CTA Section */}
{/* CTA Section */}
<section className="py-24 bg-slate-900 relative overflow-hidden">
{/* Background Decorations */}
<div className="absolute top-0 right-0 -mr-20 -mt-20 w-96 h-96 bg-blue-500/20 rounded-full blur-3xl opacity-50" />
<div className="absolute bottom-0 left-0 -ml-20 -mb-20 w-80 h-80 bg-green-500/20 rounded-full blur-3xl opacity-50" />
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center relative z-10">
<h2 className="text-4xl lg:text-5xl font-bold mb-6 text-white tracking-tight">
Ready to Generate <span className="text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-blue-400">1000s of Codes?</span>
<section className="py-20 bg-gradient-to-r from-green-600 to-blue-600 text-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center">
<h2 className="text-4xl font-bold mb-6">
Generate 1000s of QR Codes in Minutes
</h2>
<p className="text-xl mb-10 text-slate-300 leading-relaxed max-w-2xl mx-auto">
Stop doing it manually. Upload your Excel file and get your QR codes in seconds. Professional, branded, and trackable.
<p className="text-xl mb-8 text-green-100">
Save hours of manual work. Upload your file and get all QR codes ready instantly.
</p>
<div className="flex flex-col sm:flex-row gap-5 justify-center">
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link href="/signup">
<Button size="lg" className="text-lg px-8 py-6 h-auto w-full sm:w-auto bg-white text-slate-900 hover:bg-slate-50 font-bold shadow-xl shadow-blue-900/20 transition-all hover:-translate-y-1">
<Button size="lg" variant="secondary" className="text-lg px-8 py-4 w-full sm:w-auto bg-white text-green-600 hover:bg-gray-100">
Start Bulk Generation
</Button>
</Link>
<Link href="/pricing">
<Button size="lg" variant="outline" className="text-lg px-8 py-6 h-auto w-full sm:w-auto border-slate-700 text-white hover:bg-slate-800 hover:border-slate-600 transition-all">
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
View Pricing
</Button>
</Link>
</div>
<p className="mt-8 text-sm text-slate-500">
No credit card required for free trial.
</p>
</div>
</section>
</div>

View File

@ -8,8 +8,8 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import { breadcrumbSchema } from '@/lib/schema';
export const metadata: Metadata = {
title: 'Dynamic QR Code Generator | Edit & Track QR | QR Master',
description: 'Create editable dynamic QR codes. Update destination URLs, track scans, and manage content anytime without reprinting. Free generator with analytics.',
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
description: 'Create dynamic QR codes that can be edited after printing. Change destination URL, track scans, and update content without reprinting. Free dynamic QR code generator.',
keywords: 'dynamic qr code generator, editable qr code, dynamic qr code, free dynamic qr code, qr code generator dynamic, best dynamic qr code generator',
alternates: {
canonical: 'https://www.qrmaster.net/dynamic-qr-code-generator',
@ -23,14 +23,6 @@ export const metadata: Metadata = {
description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.',
url: 'https://www.qrmaster.net/dynamic-qr-code-generator',
type: 'website',
images: [
{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'Dynamic QR Code Generator - QR Master',
},
],
},
twitter: {
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
@ -188,7 +180,7 @@ export default function DynamicQRCodeGeneratorPage() {
position: 2,
name: 'Generate QR Code',
text: 'Enter your destination URL and customize the design with your branding',
url: 'https://www.qrmaster.net/signup',
url: 'https://www.qrmaster.net/create',
},
{
'@type': 'HowToStep',
@ -512,7 +504,7 @@ export default function DynamicQRCodeGeneratorPage() {
Get Started Free
</Button>
</Link>
<Link href="/signup">
<Link href="/create">
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
Create QR Code Now
</Button>

View File

@ -1,22 +0,0 @@
'use client';
import React from 'react';
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
export function ContactSupport() {
return (
<div className="mt-16 bg-blue-50 border-l-4 border-blue-500 p-8 rounded-r-lg">
<h2 className="text-2xl font-bold mb-4 text-gray-900">
Still have questions?
</h2>
<p className="text-lg text-gray-700 mb-6 leading-relaxed">
Our support team is here to help. Contact us at{' '}
<ObfuscatedMailto
email="support@qrmaster.net"
className="text-blue-600 hover:text-blue-700 font-semibold"
/>{' '}
or reach out through our live chat.
</p>
</div>
);
}

View File

@ -3,7 +3,6 @@ import type { Metadata } from 'next';
import SeoJsonLd from '@/components/SeoJsonLd';
import { faqPageSchema } from '@/lib/schema';
import { Card, CardContent } from '@/components/ui/Card';
import { ContactSupport } from './ContactSupport';
function truncateAtWord(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
@ -15,7 +14,7 @@ function truncateAtWord(text: string, maxLength: number): string {
export async function generateMetadata(): Promise<Metadata> {
const title = truncateAtWord('QR Master FAQ: Dynamic & Bulk QR', 60);
const description = truncateAtWord(
'Find answers about dynamic QR codes, scan tracking, security, bulk generation, and event QR codes. Everything you need to know about QR Master features.',
'All answers: dynamic QR, security, analytics, bulk, events & print.',
160
);
@ -34,14 +33,6 @@ export async function generateMetadata(): Promise<Metadata> {
description,
url: 'https://www.qrmaster.net/faq',
type: 'website',
images: [
{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'QR Master FAQ',
},
],
},
twitter: {
title,
@ -132,7 +123,18 @@ export default function FAQPage() {
))}
</div>
<ContactSupport />
<div className="mt-16 bg-blue-50 border-l-4 border-blue-500 p-8 rounded-r-lg">
<h2 className="text-2xl font-bold mb-4 text-gray-900">
Still have questions?
</h2>
<p className="text-lg text-gray-700 mb-6 leading-relaxed">
Our support team is here to help. Contact us at{' '}
<a href="mailto:support@qrmaster.net" className="text-blue-600 hover:text-blue-700 font-semibold">
support@qrmaster.net
</a>{' '}
or reach out through our live chat.
</p>
</div>
</div>
</div>
</div>

View File

@ -1,76 +1,248 @@
import type { Metadata } from 'next';
import '@/styles/globals.css';
import { Providers } from '@/components/Providers';
import MarketingLayout from './MarketingLayout';
// Import schema functions from library
import { organizationSchema, websiteSchema } from '@/lib/schema';
'use client';
const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Footer } from '@/components/ui/Footer';
import en from '@/i18n/en.json';
import { ChevronDown, Wifi, Contact, MessageCircle, QrCode, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'framer-motion';
export const metadata: Metadata = {
metadataBase: new URL('https://www.qrmaster.net'),
title: {
default: 'QR Master Smart QR Generator & Analytics',
template: '%s | QR Master',
},
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.',
keywords: 'QR code, QR generator, dynamic QR, QR tracking, QR analytics, branded QR, bulk QR generator',
robots: isIndexable
? { index: true, follow: true }
: { index: false, follow: false },
icons: {
icon: [
{ url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/logo.svg', type: 'image/svg+xml' },
],
apple: '/logo.svg',
},
twitter: {
card: 'summary_large_image',
site: '@qrmaster',
images: ['https://www.qrmaster.net/og-image.png'],
},
openGraph: {
type: 'website',
siteName: 'QR Master',
title: 'QR Master Smart QR Generator & Analytics',
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.',
images: [
{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform',
},
],
locale: 'en_US',
},
};
export default function RootMarketingLayout({
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [toolsOpen, setToolsOpen] = useState(false);
const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
const pathname = usePathname();
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
// Check immediately on mount
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Close simple menus when path changes
useEffect(() => {
setMobileMenuOpen(false);
setToolsOpen(false);
}, [pathname]);
// Always use English for marketing pages
const t = en;
const tools = [
{ name: 'URL / Link', description: 'Link to any website', href: '/tools/url-qr-code', icon: Link2, color: 'text-blue-500', bgColor: 'bg-blue-50' },
{ name: 'Text', description: 'Plain text message', href: '/tools/text-qr-code', icon: Type, color: 'text-slate-500', bgColor: 'bg-slate-50' },
{ name: 'WiFi', description: 'Share WiFi credentials', href: '/tools/wifi-qr-code', icon: Wifi, color: 'text-indigo-500', bgColor: 'bg-indigo-50' },
{ name: 'VCard', description: 'Digital business card', href: '/tools/vcard-qr-code', icon: Contact, color: 'text-pink-500', bgColor: 'bg-pink-50' },
{ name: 'WhatsApp', description: 'Start a chat', href: '/tools/whatsapp-qr-code', icon: MessageCircle, color: 'text-green-500', bgColor: 'bg-green-50' },
{ name: 'Email', description: 'Compose an email', href: '/tools/email-qr-code', icon: Mail, color: 'text-amber-500', bgColor: 'bg-amber-50' },
{ name: 'SMS', description: 'Send a text message', href: '/tools/sms-qr-code', icon: MessageSquare, color: 'text-cyan-500', bgColor: 'bg-cyan-50' },
{ name: 'Phone', description: 'Start a call', href: '/tools/phone-qr-code', icon: Phone, color: 'text-violet-500', bgColor: 'bg-violet-50' },
{ name: 'Event', description: 'Add calendar event', href: '/tools/event-qr-code', icon: Calendar, color: 'text-red-500', bgColor: 'bg-red-50' },
{ name: 'Location', description: 'Share a place', href: '/tools/geolocation-qr-code', icon: MapPin, color: 'text-emerald-500', bgColor: 'bg-emerald-50' },
{ name: 'Facebook', description: 'Facebook profile/page', href: '/tools/facebook-qr-code', icon: Facebook, color: 'text-blue-600', bgColor: 'bg-blue-50' },
{ name: 'Instagram', description: 'Instagram profile', href: '/tools/instagram-qr-code', icon: Instagram, color: 'text-pink-600', bgColor: 'bg-pink-50' },
{ name: 'Twitter / X', description: 'Twitter profile', href: '/tools/twitter-qr-code', icon: Twitter, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'YouTube', description: 'YouTube video/channel', href: '/tools/youtube-qr-code', icon: Youtube, color: 'text-red-600', bgColor: 'bg-red-50' },
{ name: 'TikTok', description: 'TikTok profile', href: '/tools/tiktok-qr-code', icon: Music, color: 'text-slate-800', bgColor: 'bg-slate-100' },
{ name: 'Crypto', description: 'Share wallet address', href: '/tools/crypto-qr-code', icon: Bitcoin, color: 'text-orange-500', bgColor: 'bg-orange-50' },
{ name: 'PayPal', description: 'Receive payments', href: '/tools/paypal-qr-code', icon: CreditCard, color: 'text-blue-700', bgColor: 'bg-blue-50' },
{ name: 'Zoom', description: 'Join Zoom meeting', href: '/tools/zoom-qr-code', icon: Video, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'Teams', description: 'Join Teams meeting', href: '/tools/teams-qr-code', icon: Users, color: 'text-violet-500', bgColor: 'bg-violet-50' },
];
return (
<html lang="en">
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema()) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema()) }}
/>
</head>
<body className="font-sans">
<Providers>
<MarketingLayout>
{children}
</MarketingLayout>
</Providers>
</body>
</html>
<div className="min-h-screen bg-white">
{/* Header */}
<header
className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm"
>
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl h-20 flex items-center justify-between">
{/* Logo */}
<Link href="/" className="flex items-center space-x-2.5 group">
<div className="relative w-9 h-9 flex items-center justify-center bg-indigo-600 rounded-lg shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
<QrCode className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-1">
{/* Tools Dropdown */}
<div
className="relative group px-3 py-2"
onMouseEnter={() => setToolsOpen(true)}
onMouseLeave={() => setToolsOpen(false)}
>
<button className="flex items-center space-x-1 text-sm font-medium text-slate-600 group-hover:text-slate-900 transition-colors">
<span>Free Tools</span>
<ChevronDown className={cn("w-4 h-4 transition-transform duration-200", toolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{toolsOpen && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.15 }}
className="absolute left-1/2 -translate-x-1/2 top-full mt-2 w-[750px] bg-white rounded-2xl shadow-lg border border-slate-100 p-4 overflow-hidden"
>
<div className="grid grid-cols-3 gap-1">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center space-x-3 p-2.5 rounded-xl transition-colors hover:bg-slate-50"
>
<div className={cn("p-2 rounded-lg shrink-0", tool.bgColor, tool.color)}>
<tool.icon className="w-4 h-4" />
</div>
<div>
<div className="text-sm font-semibold text-slate-900">{tool.name}</div>
<p className="text-xs text-slate-500 leading-snug">{tool.description}</p>
</div>
</Link>
))}
</div>
<div className="mt-3 pt-3 border-t border-slate-100 -mx-4 -mb-4 px-4 py-3 text-center bg-slate-50/50">
<p className="text-xs text-slate-500 font-medium">All generators are 100% free</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<Link href="/#features" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.features}
</Link>
<Link href="/#pricing" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.pricing}
</Link>
<Link href="/blog" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.blog}
</Link>
<Link href="/#faq" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
FAQ
</Link>
</div>
<div className="hidden md:flex items-center space-x-4">
<Link href="/login" className="text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.login}
</Link>
<Link href="/signup">
<Button className={cn(
"font-semibold shadow-lg shadow-indigo-500/20 transition-all hover:scale-105",
scrolled ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-blue-600 text-white hover:bg-blue-700"
)}>
{t.nav.cta || "Get Started Free"}
</Button>
</Link>
</div>
{/* Mobile Menu Button - Always dark */}
<button
className="md:hidden p-2 text-slate-900"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</nav>
{/* Mobile Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden bg-white border-b border-slate-100 overflow-hidden"
>
<div className="container mx-auto px-4 py-6 space-y-2">
{/* Free Tools Accordion */}
<button
onClick={() => setMobileToolsOpen(!mobileToolsOpen)}
className="flex items-center justify-between w-full px-4 py-3 rounded-xl hover:bg-slate-50 text-slate-700 font-semibold"
>
<span>Free Tools</span>
<ChevronDown className={cn("w-5 h-5 transition-transform", mobileToolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{mobileToolsOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<div className="max-h-[50vh] overflow-y-auto pl-4 space-y-1 border-l-2 border-slate-100 ml-4">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center gap-3 px-4 py-2.5 rounded-lg hover:bg-slate-50 text-slate-600 text-sm"
onClick={() => { setMobileMenuOpen(false); setMobileToolsOpen(false); }}
>
<tool.icon className={cn("w-4 h-4", tool.color)} />
{tool.name}
</Link>
))}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="h-px bg-slate-100 my-2"></div>
<Link href="/#features" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.features}</Link>
<Link href="/#pricing" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.pricing}</Link>
<Link href="/blog" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.blog}</Link>
<Link href="/#faq" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>FAQ</Link>
<div className="grid grid-cols-2 gap-4 pt-4">
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
<Button variant="outline" className="w-full justify-center">Log in</Button>
</Link>
<Link href="/signup" onClick={() => setMobileMenuOpen(false)}>
<Button className="w-full justify-center bg-indigo-600 hover:bg-indigo-700">Get Started</Button>
</Link>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</header>
{/* Main Content */}
<main className="pt-20">{children}</main>
{/* Footer */}
<Footer />
</div >
);
}

View File

@ -1,754 +0,0 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import {
Mail,
Users,
QrCode,
BarChart3,
TrendingUp,
Crown,
Activity,
Loader2,
Lock,
LogOut,
Zap,
Send,
CheckCircle2,
FileDown,
DollarSign,
} from 'lucide-react';
interface AdminStats {
users: {
total: number;
premium: number;
newThisWeek: number;
newThisMonth: number;
recent: Array<{
email: string;
name: string | null;
plan: string;
createdAt: string;
}>;
};
qrCodes: {
total: number;
dynamic: number;
static: number;
active: number;
};
scans: {
total: number;
dynamicOnly: number;
avgPerDynamicQR: string;
};
newsletter: {
subscribers: number;
};
topQRCodes: Array<{
id: string;
title: string;
type: string;
scans: number;
owner: string;
createdAt: string;
}>;
}
export default function NewsletterClient() {
const router = useRouter();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isAuthenticating, setIsAuthenticating] = useState(true);
const [loginError, setLoginError] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [stats, setStats] = useState<AdminStats | null>(null);
const [loading, setLoading] = useState(true);
// Newsletter management state
const [newsletterData, setNewsletterData] = useState<{
total: number;
recent: Array<{ email: string; createdAt: string }>;
} | null>(null);
const [sendingBroadcast, setSendingBroadcast] = useState(false);
const [broadcastResult, setBroadcastResult] = useState<{
success: boolean;
message: string;
} | null>(null);
// Lead management state
const [leadData, setLeadData] = useState<{
total: number;
recent: Array<{
id: string;
email: string;
source: string;
reprintCost: number | null;
updatesPerYear: number | null;
annualSavings: number | null;
createdAt: string;
}>;
} | null>(null);
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const response = await fetch('/api/admin/stats');
if (response.ok) {
setIsAuthenticated(true);
const data = await response.json();
setStats(data);
setLoading(false);
// Also fetch newsletter and lead data
fetchNewsletterData();
fetchLeadsData();
} else {
setIsAuthenticated(false);
}
} catch (error) {
setIsAuthenticated(false);
} finally {
setIsAuthenticating(false);
}
};
const fetchNewsletterData = async () => {
try {
const response = await fetch('/api/newsletter/broadcast');
if (response.ok) {
const data = await response.json();
setNewsletterData(data);
}
} catch (error) {
console.error('Failed to fetch newsletter data:', error);
}
};
const fetchLeadsData = async () => {
try {
const response = await fetch('/api/leads');
if (response.ok) {
const data = await response.json();
setLeadData(data);
}
} catch (error) {
console.error('Failed to fetch leads data:', error);
}
};
const handleSendBroadcast = async () => {
if (!confirm(`Are you sure you want to send the AI Feature Launch email to all ${newsletterData?.total || 0} subscribers?`)) {
return;
}
setSendingBroadcast(true);
setBroadcastResult(null);
try {
const response = await fetch('/api/newsletter/broadcast', {
method: 'POST',
});
const data = await response.json();
if (response.ok) {
setBroadcastResult({
success: true,
message: data.message || `Successfully sent to ${data.sent} subscribers!`,
});
} else {
setBroadcastResult({
success: false,
message: data.error || 'Failed to send broadcast',
});
}
} catch (error) {
setBroadcastResult({
success: false,
message: 'Network error. Please try again.',
});
} finally {
setSendingBroadcast(false);
}
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoginError('');
setIsAuthenticating(true);
try {
const response = await fetch('/api/newsletter/admin-login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (response.ok) {
setIsAuthenticated(true);
await checkAuth();
} else {
const data = await response.json();
setLoginError(data.error || 'Invalid credentials');
}
} catch (error) {
setLoginError('Login failed. Please try again.');
} finally {
setIsAuthenticating(false);
}
};
const handleLogout = async () => {
await fetch('/api/auth/logout', { method: 'POST' });
router.push('/');
};
// Login Screen
if (!isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 px-4">
<Card className="w-full max-w-md p-8">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
<Lock className="w-8 h-8 text-purple-600 dark:text-purple-400" />
</div>
<h1 className="text-2xl font-bold mb-2">Admin Dashboard</h1>
<p className="text-muted-foreground text-sm">
Sign in to access admin panel
</p>
<Link href="/" className="text-sm text-slate-500 hover:text-slate-900 block mt-2">
Back to Home
</Link>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="admin@example.com"
required
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
/>
</div>
{loginError && (
<p className="text-sm text-red-600 dark:text-red-400">{loginError}</p>
)}
<Button
type="submit"
disabled={isAuthenticating}
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
>
{isAuthenticating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Signing in...
</>
) : (
'Sign In'
)}
</Button>
</form>
<div className="mt-6 pt-6 border-t text-center">
<p className="text-xs text-muted-foreground">
Admin credentials required
</p>
</div>
</Card>
</div>
);
}
// Loading
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
);
}
// Admin Dashboard
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50/30 to-pink-50/30 dark:from-purple-950/10 dark:to-pink-950/10">
<div className="container mx-auto px-4 py-8 max-w-7xl">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold mb-2">Admin Dashboard</h1>
<p className="text-muted-foreground">
Platform overview and statistics
</p>
</div>
<Button
onClick={handleLogout}
variant="outline"
className="flex items-center gap-2"
>
<LogOut className="w-4 h-4" />
Logout
</Button>
</div>
{/* Main Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{/* All Time Users */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.users.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total Users</p>
<div className="mt-3 pt-3 border-t space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">This Month</span>
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
+{stats?.users.newThisMonth || 0}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">This Week</span>
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
+{stats?.users.newThisWeek || 0}
</span>
</div>
</div>
</Card>
{/* Dynamic QR Codes */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
Dynamic
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.dynamic || 0}</h3>
<p className="text-sm text-muted-foreground">Dynamic QR Codes</p>
<div className="mt-3 pt-3 border-t flex items-center justify-between">
<span className="text-xs text-muted-foreground">Static</span>
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
</div>
</Card>
{/* Total Scans */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-lg flex items-center justify-center">
<BarChart3 className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">
{stats?.scans.dynamicOnly.toLocaleString() || 0}
</h3>
<p className="text-sm text-muted-foreground">Dynamic QR Scans</p>
<div className="mt-3 pt-3 border-t flex items-center justify-between">
<span className="text-xs text-muted-foreground">Avg per QR</span>
<span className="text-sm font-semibold">{stats?.scans.avgPerDynamicQR || 0}</span>
</div>
</Card>
{/* Total QR Codes */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-amber-600 dark:text-amber-400" />
</div>
<Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total QR Codes</p>
<div className="mt-3 pt-3 border-t space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Dynamic</span>
<span className="text-sm font-semibold">{stats?.qrCodes.dynamic || 0}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Static</span>
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
</div>
</div>
</Card>
</div>
{/* Secondary Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
{/* Total All Scans */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-indigo-100 dark:bg-indigo-900/20 rounded-lg flex items-center justify-center">
<Zap className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<h3 className="text-2xl font-bold">
{stats?.scans.total.toLocaleString() || 0}
</h3>
<p className="text-sm text-muted-foreground">Total All Scans</p>
</div>
</div>
</Card>
{/* Total QR Codes */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-pink-100 dark:bg-pink-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-pink-600 dark:text-pink-400" />
</div>
<div>
<h3 className="text-2xl font-bold">{stats?.qrCodes.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total QR Codes</p>
</div>
</div>
</Card>
{/* Premium Users */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
<Crown className="w-6 h-6 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h3 className="text-2xl font-bold">{stats?.users.premium || 0}</h3>
<p className="text-sm text-muted-foreground">Premium Users</p>
</div>
</div>
</Card>
</div>
{/* Bottom Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top QR Codes */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<h3 className="font-semibold text-lg">Top QR Codes</h3>
<p className="text-xs text-muted-foreground">Most scanned</p>
</div>
</div>
{stats?.topQRCodes && stats.topQRCodes.length > 0 ? (
<div className="space-y-3">
{stats.topQRCodes.map((qr, index) => (
<div
key={qr.id}
className="flex items-center justify-between py-3 border-b border-border last:border-0"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center flex-shrink-0">
<span className="text-white text-sm font-bold">
#{index + 1}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{qr.title}</p>
<p className="text-xs text-muted-foreground truncate">
{qr.owner}
</p>
</div>
</div>
<div className="text-right flex-shrink-0 ml-4">
<p className="text-lg font-bold">{qr.scans.toLocaleString()}</p>
<p className="text-xs text-muted-foreground">scans</p>
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No QR codes yet</p>
)}
</Card>
{/* Recent Users */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h3 className="font-semibold text-lg">Recent Users</h3>
<p className="text-xs text-muted-foreground">Latest signups</p>
</div>
</div>
{stats?.users.recent && stats.users.recent.length > 0 ? (
<div className="space-y-3">
{stats.users.recent.map((user, index) => (
<div
key={index}
className="flex items-center justify-between py-3 border-b border-border last:border-0"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-full flex items-center justify-center flex-shrink-0">
<span className="text-white text-xs font-bold">
{(user.name || user.email).charAt(0).toUpperCase()}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">
{user.name || user.email}
</p>
<p className="text-xs text-muted-foreground truncate">
{new Date(user.createdAt).toLocaleDateString()}
</p>
</div>
</div>
<Badge
className={
user.plan === 'FREE'
? 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300'
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
}
>
{user.plan === 'PRO' && <Crown className="w-3 h-3 mr-1" />}
{user.plan}
</Badge>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No users yet</p>
)}
</Card>
</div>
{/* Newsletter Management Section */}
<div className="mt-8">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-lg">Newsletter Management</h3>
<p className="text-xs text-muted-foreground">Manage AI feature launch notifications</p>
</div>
<div className="text-right">
<span className="text-2xl font-bold">{newsletterData?.total || 0}</span>
<p className="text-xs text-muted-foreground">Total Subscribers</p>
</div>
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
Active
</Badge>
</div>
{/* Broadcast Section */}
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-xl mb-6">
<div className="flex items-start gap-3 mb-3">
<Send className="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5" />
<div>
<h4 className="font-medium">Broadcast AI Feature Launch</h4>
<p className="text-sm text-muted-foreground">
Send the AI feature launch announcement to all {newsletterData?.total || 0} subscribers.
This will inform them that the features are now available.
</p>
</div>
</div>
{/* Resend Free Tier Warning */}
{(newsletterData?.total || 0) > 100 && (
<div className="p-3 rounded-lg mb-3 bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 flex items-start gap-2">
<Activity className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<strong>Warning: Resend Free Limit</strong>
<p>You have more than 100 subscribers. The Resend Free Tier only allows 100 emails per day. Sending this broadcast might fail for some users or block your account.</p>
</div>
</div>
)}
{broadcastResult && (
<div className={`p-3 rounded-lg mb-3 flex items-center gap-2 ${broadcastResult.success
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
}`}>
{broadcastResult.success && <CheckCircle2 className="w-4 h-4" />}
<span className="text-sm">{broadcastResult.message}</span>
</div>
)}
<Button
onClick={handleSendBroadcast}
disabled={sendingBroadcast || (newsletterData?.total || 0) === 0 || (newsletterData?.total || 0) > 100}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
>
{sendingBroadcast ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<Mail className="w-4 h-4 mr-2" />
Send Launch Notification to All
</>
)}
</Button>
</div>
{/* Recent Subscribers */}
<div>
<h4 className="font-medium mb-3">Recent Subscribers</h4>
{newsletterData?.recent && newsletterData.recent.length > 0 ? (
<div className="space-y-2">
{newsletterData.recent.map((subscriber, index) => (
<div
key={index}
className="flex items-center justify-between py-2 border-b border-border last:border-0"
>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{subscriber.email}</span>
</div>
<span className="text-xs text-muted-foreground">
{new Date(subscriber.createdAt).toLocaleDateString()}
</span>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No subscribers yet</p>
)}
</div>
{/* Tip */}
<div className="mt-4 pt-4 border-t">
<p className="text-xs text-muted-foreground">
💡 Tip: View all subscribers in{' '}
<a
href="http://localhost:5555"
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 dark:text-purple-400 hover:underline"
>
Prisma Studio
</a>
{' '}(NewsletterSubscription table)
</p>
</div>
</Card>
</div>
{/* Lead Management Section */}
<div className="mt-8">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-100 to-teal-100 dark:from-emerald-900/30 dark:to-teal-900/30 rounded-lg flex items-center justify-center">
<FileDown className="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-lg">Lead Management</h3>
<p className="text-xs text-muted-foreground">Reprint Calculator PDF downloads</p>
</div>
<div className="text-right">
<span className="text-2xl font-bold">{leadData?.total || 0}</span>
<p className="text-xs text-muted-foreground">Total Leads</p>
</div>
<Badge className="bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300">
Active
</Badge>
</div>
{/* Recent Leads */}
<div>
<h4 className="font-medium mb-3">Recent Leads</h4>
{leadData?.recent && leadData.recent.length > 0 ? (
<div className="space-y-2">
{leadData.recent.map((lead) => (
<div
key={lead.id}
className="flex items-center justify-between py-3 px-4 border border-border rounded-lg bg-gray-50/50 dark:bg-gray-900/30"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<Mail className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<div className="min-w-0">
<span className="text-sm font-medium block truncate">{lead.email}</span>
{lead.annualSavings && (
<span className="text-xs text-emerald-600 flex items-center gap-1">
<DollarSign className="w-3 h-3" />
{lead.annualSavings.toLocaleString()} potential savings
</span>
)}
</div>
</div>
<div className="text-right flex-shrink-0 ml-4">
<span className="text-xs text-muted-foreground block">
{new Date(lead.createdAt).toLocaleDateString()}
</span>
{lead.reprintCost && lead.updatesPerYear && (
<span className="text-xs text-slate-500">
{lead.reprintCost} × {lead.updatesPerYear}/yr
</span>
)}
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No leads yet. Leads appear when users download a PDF report from the Reprint Calculator.</p>
)}
</div>
{/* Tip */}
<div className="mt-4 pt-4 border-t">
<p className="text-xs text-muted-foreground">
💡 Tip: View all leads in{' '}
<a
href="http://localhost:5555"
target="_blank"
rel="noopener noreferrer"
className="text-emerald-600 dark:text-emerald-400 hover:underline"
>
Prisma Studio
</a>
{' '}(Lead table)
</p>
</div>
</Card>
</div>
</div>
</div>
);
}

View File

@ -1,19 +1,643 @@
import React from 'react';
import type { Metadata } from 'next';
import NewsletterClient from './NewsletterClient';
'use client';
export const metadata: Metadata = {
title: 'Newsletter Admin | QR Master',
description: 'Administrative access for QR Master newsletter management. This area is restricted to authorized personnel only.',
robots: {
index: false,
follow: false,
},
alternates: {
canonical: 'https://www.qrmaster.net/newsletter',
},
};
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import {
Mail,
Users,
QrCode,
BarChart3,
TrendingUp,
Crown,
Activity,
Loader2,
Lock,
LogOut,
Zap,
Send,
CheckCircle2,
} from 'lucide-react';
export default function NewsletterPage() {
return <NewsletterClient />;
interface AdminStats {
users: {
total: number;
premium: number;
newThisWeek: number;
newThisMonth: number;
recent: Array<{
email: string;
name: string | null;
plan: string;
createdAt: string;
}>;
};
qrCodes: {
total: number;
dynamic: number;
static: number;
active: number;
};
scans: {
total: number;
dynamicOnly: number;
avgPerDynamicQR: string;
};
newsletter: {
subscribers: number;
};
topQRCodes: Array<{
id: string;
title: string;
type: string;
scans: number;
owner: string;
createdAt: string;
}>;
}
export default function AdminDashboard() {
const router = useRouter();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isAuthenticating, setIsAuthenticating] = useState(true);
const [loginError, setLoginError] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [stats, setStats] = useState<AdminStats | null>(null);
const [loading, setLoading] = useState(true);
// Newsletter management state
const [newsletterData, setNewsletterData] = useState<{
total: number;
recent: Array<{ email: string; createdAt: string }>;
} | null>(null);
const [sendingBroadcast, setSendingBroadcast] = useState(false);
const [broadcastResult, setBroadcastResult] = useState<{
success: boolean;
message: string;
} | null>(null);
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const response = await fetch('/api/admin/stats');
if (response.ok) {
setIsAuthenticated(true);
const data = await response.json();
setStats(data);
setLoading(false);
// Also fetch newsletter data
fetchNewsletterData();
} else {
setIsAuthenticated(false);
}
} catch (error) {
setIsAuthenticated(false);
} finally {
setIsAuthenticating(false);
}
};
const fetchNewsletterData = async () => {
try {
const response = await fetch('/api/newsletter/broadcast');
if (response.ok) {
const data = await response.json();
setNewsletterData(data);
}
} catch (error) {
console.error('Failed to fetch newsletter data:', error);
}
};
const handleSendBroadcast = async () => {
if (!confirm(`Are you sure you want to send the AI Feature Launch email to all ${newsletterData?.total || 0} subscribers?`)) {
return;
}
setSendingBroadcast(true);
setBroadcastResult(null);
try {
const response = await fetch('/api/newsletter/broadcast', {
method: 'POST',
});
const data = await response.json();
if (response.ok) {
setBroadcastResult({
success: true,
message: data.message || `Successfully sent to ${data.sent} subscribers!`,
});
} else {
setBroadcastResult({
success: false,
message: data.error || 'Failed to send broadcast',
});
}
} catch (error) {
setBroadcastResult({
success: false,
message: 'Network error. Please try again.',
});
} finally {
setSendingBroadcast(false);
}
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoginError('');
setIsAuthenticating(true);
try {
const response = await fetch('/api/newsletter/admin-login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (response.ok) {
setIsAuthenticated(true);
await checkAuth();
} else {
const data = await response.json();
setLoginError(data.error || 'Invalid credentials');
}
} catch (error) {
setLoginError('Login failed. Please try again.');
} finally {
setIsAuthenticating(false);
}
};
const handleLogout = async () => {
await fetch('/api/auth/logout', { method: 'POST' });
router.push('/');
};
// Login Screen
if (!isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 px-4">
<Card className="w-full max-w-md p-8">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
<Lock className="w-8 h-8 text-purple-600 dark:text-purple-400" />
</div>
<h1 className="text-2xl font-bold mb-2">Admin Dashboard</h1>
<p className="text-muted-foreground text-sm">
Sign in to access admin panel
</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="admin@example.com"
required
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
/>
</div>
{loginError && (
<p className="text-sm text-red-600 dark:text-red-400">{loginError}</p>
)}
<Button
type="submit"
disabled={isAuthenticating}
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
>
{isAuthenticating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Signing in...
</>
) : (
'Sign In'
)}
</Button>
</form>
<div className="mt-6 pt-6 border-t text-center">
<p className="text-xs text-muted-foreground">
Admin credentials required
</p>
</div>
</Card>
</div>
);
}
// Loading
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
);
}
// Admin Dashboard
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50/30 to-pink-50/30 dark:from-purple-950/10 dark:to-pink-950/10">
<div className="container mx-auto px-4 py-8 max-w-7xl">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold mb-2">Admin Dashboard</h1>
<p className="text-muted-foreground">
Platform overview and statistics
</p>
</div>
<Button
onClick={handleLogout}
variant="outline"
className="flex items-center gap-2"
>
<LogOut className="w-4 h-4" />
Logout
</Button>
</div>
{/* Main Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{/* All Time Users */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.users.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total Users</p>
<div className="mt-3 pt-3 border-t space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">This Month</span>
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
+{stats?.users.newThisMonth || 0}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">This Week</span>
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
+{stats?.users.newThisWeek || 0}
</span>
</div>
</div>
</Card>
{/* Dynamic QR Codes */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
Dynamic
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.dynamic || 0}</h3>
<p className="text-sm text-muted-foreground">Dynamic QR Codes</p>
<div className="mt-3 pt-3 border-t flex items-center justify-between">
<span className="text-xs text-muted-foreground">Static</span>
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
</div>
</Card>
{/* Total Scans */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-lg flex items-center justify-center">
<BarChart3 className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">
{stats?.scans.dynamicOnly.toLocaleString() || 0}
</h3>
<p className="text-sm text-muted-foreground">Dynamic QR Scans</p>
<div className="mt-3 pt-3 border-t flex items-center justify-between">
<span className="text-xs text-muted-foreground">Avg per QR</span>
<span className="text-sm font-semibold">{stats?.scans.avgPerDynamicQR || 0}</span>
</div>
</Card>
{/* Total QR Codes */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-amber-600 dark:text-amber-400" />
</div>
<Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total QR Codes</p>
<div className="mt-3 pt-3 border-t space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Dynamic</span>
<span className="text-sm font-semibold">{stats?.qrCodes.dynamic || 0}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Static</span>
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
</div>
</div>
</Card>
</div>
{/* Secondary Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
{/* Total All Scans */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-indigo-100 dark:bg-indigo-900/20 rounded-lg flex items-center justify-center">
<Zap className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<h3 className="text-2xl font-bold">
{stats?.scans.total.toLocaleString() || 0}
</h3>
<p className="text-sm text-muted-foreground">Total All Scans</p>
</div>
</div>
</Card>
{/* Total QR Codes */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-pink-100 dark:bg-pink-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-pink-600 dark:text-pink-400" />
</div>
<div>
<h3 className="text-2xl font-bold">{stats?.qrCodes.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total QR Codes</p>
</div>
</div>
</Card>
{/* Premium Users */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
<Crown className="w-6 h-6 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h3 className="text-2xl font-bold">{stats?.users.premium || 0}</h3>
<p className="text-sm text-muted-foreground">Premium Users</p>
</div>
</div>
</Card>
</div>
{/* Bottom Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top QR Codes */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<h3 className="font-semibold text-lg">Top QR Codes</h3>
<p className="text-xs text-muted-foreground">Most scanned</p>
</div>
</div>
{stats?.topQRCodes && stats.topQRCodes.length > 0 ? (
<div className="space-y-3">
{stats.topQRCodes.map((qr, index) => (
<div
key={qr.id}
className="flex items-center justify-between py-3 border-b border-border last:border-0"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center flex-shrink-0">
<span className="text-white text-sm font-bold">
#{index + 1}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{qr.title}</p>
<p className="text-xs text-muted-foreground truncate">
{qr.owner}
</p>
</div>
</div>
<div className="text-right flex-shrink-0 ml-4">
<p className="text-lg font-bold">{qr.scans.toLocaleString()}</p>
<p className="text-xs text-muted-foreground">scans</p>
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No QR codes yet</p>
)}
</Card>
{/* Recent Users */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h3 className="font-semibold text-lg">Recent Users</h3>
<p className="text-xs text-muted-foreground">Latest signups</p>
</div>
</div>
{stats?.users.recent && stats.users.recent.length > 0 ? (
<div className="space-y-3">
{stats.users.recent.map((user, index) => (
<div
key={index}
className="flex items-center justify-between py-3 border-b border-border last:border-0"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-full flex items-center justify-center flex-shrink-0">
<span className="text-white text-xs font-bold">
{(user.name || user.email).charAt(0).toUpperCase()}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">
{user.name || user.email}
</p>
<p className="text-xs text-muted-foreground truncate">
{new Date(user.createdAt).toLocaleDateString()}
</p>
</div>
</div>
<Badge
className={
user.plan === 'FREE'
? 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300'
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
}
>
{user.plan === 'PRO' && <Crown className="w-3 h-3 mr-1" />}
{user.plan}
</Badge>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No users yet</p>
)}
</Card>
</div>
{/* Newsletter Management Section */}
<div className="mt-8">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-lg">Newsletter Management</h3>
<p className="text-xs text-muted-foreground">Manage AI feature launch notifications</p>
</div>
<div className="text-right">
<span className="text-2xl font-bold">{newsletterData?.total || 0}</span>
<p className="text-xs text-muted-foreground">Total Subscribers</p>
</div>
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
Active
</Badge>
</div>
{/* Broadcast Section */}
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-xl mb-6">
<div className="flex items-start gap-3 mb-3">
<Send className="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5" />
<div>
<h4 className="font-medium">Broadcast AI Feature Launch</h4>
<p className="text-sm text-muted-foreground">
Send the AI feature launch announcement to all {newsletterData?.total || 0} subscribers.
This will inform them that the features are now available.
</p>
</div>
</div>
{/* Resend Free Tier Warning */}
{(newsletterData?.total || 0) > 100 && (
<div className="p-3 rounded-lg mb-3 bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 flex items-start gap-2">
<Activity className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<strong>Warning: Resend Free Limit</strong>
<p>You have more than 100 subscribers. The Resend Free Tier only allows 100 emails per day. Sending this broadcast might fail for some users or block your account.</p>
</div>
</div>
)}
{broadcastResult && (
<div className={`p-3 rounded-lg mb-3 flex items-center gap-2 ${broadcastResult.success
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
}`}>
{broadcastResult.success && <CheckCircle2 className="w-4 h-4" />}
<span className="text-sm">{broadcastResult.message}</span>
</div>
)}
<Button
onClick={handleSendBroadcast}
disabled={sendingBroadcast || (newsletterData?.total || 0) === 0 || (newsletterData?.total || 0) > 100}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
>
{sendingBroadcast ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<Mail className="w-4 h-4 mr-2" />
Send Launch Notification to All
</>
)}
</Button>
</div>
{/* Recent Subscribers */}
<div>
<h4 className="font-medium mb-3">Recent Subscribers</h4>
{newsletterData?.recent && newsletterData.recent.length > 0 ? (
<div className="space-y-2">
{newsletterData.recent.map((subscriber, index) => (
<div
key={index}
className="flex items-center justify-between py-2 border-b border-border last:border-0"
>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{subscriber.email}</span>
</div>
<span className="text-xs text-muted-foreground">
{new Date(subscriber.createdAt).toLocaleDateString()}
</span>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No subscribers yet</p>
)}
</div>
{/* Tip */}
<div className="mt-4 pt-4 border-t">
<p className="text-xs text-muted-foreground">
💡 Tip: View all subscribers in{' '}
<a
href="http://localhost:5555"
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 dark:text-purple-400 hover:underline"
>
Prisma Studio
</a>
{' '}(NewsletterSubscription table)
</p>
</div>
</Card>
</div>
</div>
</div>
);
}

View File

@ -3,8 +3,6 @@ import type { Metadata } from 'next';
import SeoJsonLd from '@/components/SeoJsonLd';
import { organizationSchema, websiteSchema } from '@/lib/schema';
import HomePageClient from '@/components/marketing/HomePageClient';
import { generateFaqSchema } from '@/lib/schema-utils';
import en from '@/i18n/en.json'; // Import English translations for schema generation
function truncateAtWord(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
@ -16,7 +14,7 @@ function truncateAtWord(text: string, maxLength: number): string {
export async function generateMetadata(): Promise<Metadata> {
const title = truncateAtWord('QR Master: Dynamic QR Generator', 60);
const description = truncateAtWord(
'Create professional QR codes with QR Master. Dynamic QR with tracking, bulk generation, custom branding, and real-time analytics for all your campaigns.',
'Dynamic QR, branding, bulk generation & analytics for all campaigns.',
160
);
@ -28,7 +26,6 @@ export async function generateMetadata(): Promise<Metadata> {
languages: {
'x-default': 'https://www.qrmaster.net/',
en: 'https://www.qrmaster.net/',
de: 'https://www.qrmaster.net/qr-code-erstellen',
},
},
openGraph: {
@ -36,19 +33,10 @@ export async function generateMetadata(): Promise<Metadata> {
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,
images: ['https://www.qrmaster.net/og-image.png'],
},
};
}
@ -56,13 +44,11 @@ export async function generateMetadata(): Promise<Metadata> {
export default function HomePage() {
return (
<>
<SeoJsonLd data={[organizationSchema(), websiteSchema(), generateFaqSchema(en.faq.questions)]} />
{/* Server-rendered H1 for SEO - visually hidden but crawlable */}
<h1 className="sr-only">QR Master: Dynamic QR Code Generator with Analytics</h1>
<SeoJsonLd data={[organizationSchema(), websiteSchema()]} />
{/* Server-rendered SEO content for crawlers */}
<div className="sr-only" aria-hidden="false">
<h1>QR Master: Free Dynamic QR Code Generator with Tracking & Analytics</h1>
<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.

View File

@ -1,45 +0,0 @@
import React from 'react';
import type { Metadata } from 'next';
import PricingClient from './PricingClient';
export const metadata: Metadata = {
title: {
absolute: 'Pricing Plans | QR Master'
},
description: 'Choose the perfect QR code plan for your needs. Free, Pro, and Business plans with dynamic QR codes, analytics, bulk generation, and custom branding.',
alternates: {
canonical: 'https://www.qrmaster.net/pricing',
},
robots: {
index: true,
follow: true,
},
openGraph: {
title: 'Pricing Plans | QR Master',
description: 'Choose the perfect QR code plan for your needs.',
url: 'https://www.qrmaster.net/pricing',
type: 'website',
images: [
{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'QR Master Pricing Plans',
},
],
},
};
export default function PricingPage() {
return (
<>
{/* Server-rendered H1 for SEO */}
<h1 className="sr-only">QR Master Pricing Choose Your QR Code Plan</h1>
<div className="sr-only">
<h2>Compare our plans</h2>
<p>Find the best QR code solution for your business. From free personal tiers to enterprise-grade dynamic code management.</p>
</div>
<PricingClient />
</>
);
}

View File

@ -1,13 +0,0 @@
'use client';
import React from 'react';
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
export function PrivacyEmailLink() {
return (
<ObfuscatedMailto
email="support@qrmaster.net"
className="text-primary-600 hover:text-primary-700"
/>
);
}

View File

@ -1,27 +1,9 @@
import React from 'react';
import Link from 'next/link';
import { PrivacyEmailLink } from './PrivacyEmailLink';
export const metadata = {
title: 'Privacy Policy | QR Master',
description: 'Read our Privacy Policy to understand how QR Master collects, uses, and protects your data. We are committed to GDPR compliance and data security.',
alternates: {
canonical: 'https://www.qrmaster.net/privacy',
},
openGraph: {
title: 'Privacy Policy | QR Master',
description: 'Read our Privacy Policy to understand how QR Master collects, uses, and protects your data.',
url: 'https://www.qrmaster.net/privacy',
type: 'website',
images: [
{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'QR Master Privacy Policy',
},
],
},
description: 'Privacy Policy and data protection information for QR Master',
};
export default function PrivacyPage() {
@ -111,7 +93,9 @@ export default function PrivacyPage() {
</ul>
<p className="text-gray-700 mb-4">
To exercise these rights, contact us at{' '}
<PrivacyEmailLink />
<a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700">
support@qrmaster.net
</a>
</p>
<p className="text-gray-700 mb-4">
Our service is for users 16 years and older. If you're in the EEA and have concerns,
@ -127,7 +111,9 @@ export default function PrivacyPage() {
<div className="bg-gray-50 p-6 rounded-lg">
<p className="text-gray-700 mb-2">
<strong>Email:</strong>{' '}
<PrivacyEmailLink />
<a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700">
support@qrmaster.net
</a>
</p>
<p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.net</a></p>
</div>

View File

@ -8,7 +8,7 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import { breadcrumbSchema } from '@/lib/schema';
export const metadata: Metadata = {
title: 'QR Code Tracking & Analytics - Track Scans | QR Master',
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior. Free QR code tracking software with detailed reports.',
keywords: 'qr code tracking, qr code analytics, track qr scans, qr code statistics, free qr tracking, qr code monitoring',
alternates: {
@ -19,21 +19,13 @@ export const metadata: Metadata = {
},
},
openGraph: {
title: 'QR Code Tracking & Analytics - Track Scans | QR Master',
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
url: 'https://www.qrmaster.net/qr-code-tracking',
type: 'website',
images: [
{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'QR Code Tracking & Analytics - QR Master',
},
],
},
twitter: {
title: 'QR Code Tracking & Analytics - Track Scans | QR Master',
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
},
};
@ -162,7 +154,7 @@ export default function QRCodeTrackingPage() {
position: 3,
name: 'Monitor Analytics',
text: 'View real-time scan data including location, device, and time patterns in your dashboard',
url: 'https://www.qrmaster.net/signup',
url: 'https://www.qrmaster.net/analytics',
},
{
'@type': 'HowToStep',
@ -207,7 +199,7 @@ export default function QRCodeTrackingPage() {
Start Tracking Free
</Button>
</Link>
<Link href="/signup">
<Link href="/create">
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
Create Trackable QR Code
</Button>
@ -378,35 +370,26 @@ export default function QRCodeTrackingPage() {
</section>
{/* CTA Section */}
{/* CTA Section */}
<section className="py-24 bg-slate-900 relative overflow-hidden">
{/* Background Decorations */}
<div className="absolute top-0 right-0 -mr-20 -mt-20 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl opacity-50" />
<div className="absolute bottom-0 left-0 -ml-20 -mb-20 w-80 h-80 bg-purple-500/20 rounded-full blur-3xl opacity-50" />
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center relative z-10">
<h2 className="text-4xl lg:text-5xl font-bold mb-6 text-white tracking-tight">
Start Tracking Your <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 to-purple-400">QR Codes Today</span>
<section className="py-20 bg-gradient-to-r from-primary-600 to-purple-600 text-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center">
<h2 className="text-4xl font-bold mb-6">
Start Tracking Your QR Codes Today
</h2>
<p className="text-xl mb-10 text-slate-300 leading-relaxed max-w-2xl mx-auto">
Join thousands of businesses using QR Master to optimize their campaigns with real-time analytics.
<p className="text-xl mb-8 text-primary-100">
Join thousands of businesses using QR Master to track and optimize their QR code campaigns
</p>
<div className="flex flex-col sm:flex-row gap-5 justify-center">
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link href="/signup">
<Button size="lg" className="text-lg px-8 py-6 h-auto w-full sm:w-auto bg-white text-slate-900 hover:bg-slate-50 font-bold shadow-xl shadow-primary-900/20 transition-all hover:-translate-y-1">
<Button size="lg" variant="secondary" className="text-lg px-8 py-4 w-full sm:w-auto bg-white text-primary-600 hover:bg-gray-100">
Create Free Account
</Button>
</Link>
<Link href="/pricing">
<Button size="lg" variant="outline" className="text-lg px-8 py-6 h-auto w-full sm:w-auto border-slate-700 text-white hover:bg-slate-800 hover:border-slate-600 transition-all">
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
View Pricing
</Button>
</Link>
</div>
<p className="mt-8 text-sm text-slate-500">
Full analytics accessible on free plan.
</p>
</div>
</section>
</div>

View File

@ -1,117 +0,0 @@
import React from 'react';
import type { Metadata } from 'next';
import ReprintSavingsCalculator from '@/components/marketing/ReprintSavingsCalculator';
import { ArrowDown, Check, ShieldCheck, Zap } from 'lucide-react';
export const metadata: Metadata = {
title: 'Reprint Cost Calculator | QR Master',
description:
'Calculate how much you are wasting on QR code reprints. See your potential savings with dynamic QR codes that never need to be reprinted.',
alternates: {
canonical: 'https://www.qrmaster.net/reprint-calculator',
},
robots: {
index: true,
follow: true,
},
openGraph: {
title: 'Reprint Cost Calculator | QR Master',
description: 'Stop wasting money on reprints. Calculate your savings now.',
url: 'https://www.qrmaster.net/reprint-calculator',
type: 'website',
images: [
{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'QR Master Reprint Cost Calculator',
},
],
},
};
export default function ReprintCalculatorPage() {
return (
<>
{/* Hero Section */}
<section className="pt-24 pb-12 bg-white relative overflow-hidden">
<div className="container mx-auto px-4 text-center max-w-3xl relative z-10">
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-slate-100/80 backdrop-blur-sm border border-slate-200 text-slate-600 text-sm font-medium mb-8">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
</span>
Static QR codes are costing you money
</div>
<h1 className="text-4xl lg:text-6xl font-black text-slate-900 mb-6 tracking-tight leading-[1.1]">
Stop Burning Budget on <br className="hidden md:block" />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-orange-600">Avoidable Reprints</span>
</h1>
<p className="text-xl text-slate-600 mb-8 leading-relaxed max-w-2xl mx-auto">
Every time a URL changes, static QR codes become useless trash.
Dynamic QR codes update instantlykeeping your print materials alive forever.
</p>
<div className="flex justify-center">
<ArrowDown className="w-6 h-6 text-slate-400 animate-bounce" />
</div>
</div>
</section>
{/* Calculator Component */}
<ReprintSavingsCalculator />
{/* Value Props */}
<section className="py-24 bg-white border-t border-slate-100">
<div className="container mx-auto px-4 max-w-6xl">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Why Smart Companies Switched Years Ago
</h2>
<p className="text-slate-600 text-lg max-w-2xl mx-auto">
The math is simple. One dynamic subscription costs less than a single batch of reprints.
</p>
</div>
<div className="grid md:grid-cols-3 gap-8 lg:gap-12">
{[
{
icon: Zap,
color: "text-amber-500",
bg: "bg-amber-50",
title: "Update Instantly",
desc: "Changed your menu? New promo link? Update the destination in seconds. Your printed codes keep working perfectly."
},
{
icon: ShieldCheck,
color: "text-blue-500",
bg: "bg-blue-50",
title: "Error Proofing",
desc: "Printed the wrong link? With static codes, that's a disaster. With dynamic codes, it's a 5-second fix in the dashboard."
},
{
icon: Check,
color: "text-green-500",
bg: "bg-green-50",
title: "Real ROI Tracking",
desc: "Stop guessing if your print ads work. Track every scan, location, and device to measure exactly what's driving value."
}
].map((feature, i) => (
<div key={i} className="group p-8 rounded-2xl bg-slate-50 border border-slate-100 hover:bg-white hover:shadow-xl hover:shadow-slate-200/50 hover:border-slate-200 transition-all duration-300">
<div className={`w-14 h-14 ${feature.bg} rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300`}>
<feature.icon className={`w-7 h-7 ${feature.color}`} />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-3">{feature.title}</h3>
<p className="text-slate-600 leading-relaxed">
{feature.desc}
</p>
</div>
))}
</div>
</div>
</section>
</>
);
}

View File

@ -14,7 +14,6 @@ import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Select } from '@/components/ui/Select';
import { cn } from '@/lib/utils';
import AdBanner from '@/components/ads/AdBanner';
// Brand Colors
const BRAND = {
@ -138,7 +137,7 @@ export default function CryptoGenerator() {
<div className="grid lg:grid-cols-2">
{/* LEFT: Input Section */}
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
{/* Crypto Details */}
<div className="space-y-6">
@ -159,7 +158,6 @@ export default function CryptoGenerator() {
if (col) setQrColor(col);
}}
className="h-12 w-full rounded-xl border-slate-200"
aria-label="Currency"
/>
</div>
@ -211,7 +209,7 @@ export default function CryptoGenerator() {
Wallet Direct
</button>
</div>
<p className="text-xs text-slate-600 mt-2">
<p className="text-xs text-slate-500 mt-2">
{qrMode === 'universal'
? "Works with any phone camera. Opens blockchain explorer."
: "Requires scanning from a wallet app. Enables direct payment."}
@ -253,7 +251,7 @@ export default function CryptoGenerator() {
{/* Frame Selector */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div className="grid grid-cols-4 gap-2">
{FRAME_OPTIONS.map((frame) => (
<button
key={frame.id}
@ -274,12 +272,13 @@ export default function CryptoGenerator() {
</div>
{/* RIGHT: Preview Section */}
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
{/* QR Card with Frame */}
<div
ref={qrRef}
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
style={{ minWidth: '320px' }}
>
{/* Frame Label */}
{getFrameLabel() && (
@ -321,7 +320,7 @@ export default function CryptoGenerator() {
<Bitcoin className="w-4 h-4 text-slate-400 shrink-0" />
<span className="truncate capitalize">{currency}</span>
</h3>
<div className="text-xs text-slate-600 mt-1 truncate px-2">
<div className="text-xs text-slate-500 mt-1 truncate px-2">
{address || 'Wallet Address'}
</div>
</div>
@ -346,15 +345,13 @@ export default function CryptoGenerator() {
</Button>
</div>
<p className="text-xs text-slate-600 mt-4 text-center">
<p className="text-xs text-slate-500 mt-4 text-center">
Scanning copies the wallet address or opens a crypto app.
</p>
</div>
</div>
</div>
{/* Upsell Banner */}
<div className="mt-8 bg-gradient-to-r from-slate-900 to-slate-700 rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-white text-center sm:text-left">

View File

@ -4,24 +4,20 @@ import CryptoGenerator from './CryptoGenerator';
import { Bitcoin, Shield, Zap, Smartphone, Wallet, Coins, Sparkles, Download, Share2 } 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 Crypto QR Code Generator | Krypto QR Code Erstellen | QR Master',
},
description: 'Create a QR code for your Crypto wallet address. Erstelle Bitcoin & Ethereum QR Codes für einfache Zahlungen. Supports BTC, ETH, USDT & more.',
keywords: ['crypto qr code', 'bitcoin qr generator', 'ethereum qr code', 'crypto wallet qr', 'donation qr code', 'krypto qr code', 'bitcoin qr code erstellen', 'kryptowährung qr code', 'wallet adresse qr code'],
title: 'Free Crypto QR Code Generator | Bitcoin, Ethereum & USDT | QR Master',
description: 'Create a QR code for your Crypto wallet address. Supports Bitcoin (BTC), Ethereum (ETH), USDT, and more. Essential for easy payments and donations.',
keywords: ['crypto qr code', 'bitcoin qr generator', 'ethereum qr code', 'crypto wallet qr', 'donation qr code'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/crypto-qr-code',
canonical: 'https://qrmaster.io/tools/crypto-qr-code',
},
openGraph: {
title: 'Free Crypto QR Code Generator | QR Master',
description: 'Generate QR codes to accept Crypto payments securely. Supports BTC, ETH, SOL.',
type: 'website',
url: 'https://www.qrmaster.net/tools/crypto-qr-code',
url: 'https://qrmaster.io/tools/crypto-qr-code',
images: [{ url: '/og-crypto-generator.png', width: 1200, height: 630 }],
},
twitter: {
@ -39,12 +35,23 @@ export const metadata: Metadata = {
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
generateSoftwareAppSchema(
'Crypto QR Code Generator',
'Generate QR codes that contain your cryptocurrency wallet address for easy payments.',
'/og-crypto-generator.png',
'FinanceApplication'
),
{
'@type': 'SoftwareApplication',
name: 'Crypto QR Code Generator',
applicationCategory: 'FinanceApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.9',
ratingCount: '870',
},
description: 'Generate QR codes that contain your cryptocurrency wallet address for easy payments.',
},
{
'@type': 'HowTo',
name: 'How to Create a Crypto QR Code',
@ -83,28 +90,51 @@ const jsonLd = {
],
totalTime: 'PT30S',
},
generateFaqSchema({
'Is it safe to share my wallet address?': {
question: 'Is it safe to share my wallet address?',
answer: 'Yes. Your public wallet address is designed to be shared so you can receive funds. Never share your private key.',
{
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'Is it safe to share my wallet address?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes. Your public wallet address is designed to be shared so you can receive funds. Never share your private key.',
},
'Which currencies are supported?': {
question: 'Which currencies are supported?',
answer: 'Our generator supports standard URI schemes for Bitcoin, Ethereum, Solana, and can generally store any wallet string for other coins.',
},
'Can I add a specific amount?': {
question: 'Can I add a specific amount?',
answer: 'Yes, you can pre-fill an amount so when the user scans, their wallet app automatically suggests the correct payment value.',
{
'@type': 'Question',
name: 'Which currencies are supported?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Our generator supports standard URI schemes for Bitcoin, Ethereum, Solana, and can generally store any wallet string for other coins.',
},
'Does it work with all wallets?': {
question: 'Does it work with all wallets?',
answer: 'Yes, standard crypto QR codes are universally readable by almost all modern wallet apps (Coinbase, MetaMask, Trust Wallet, etc.).',
},
'Are there any fees?': {
question: 'Are there any fees?',
answer: 'No. This generator is completely free. We do not charge any fees for generating codes or for the transactions made using them.',
{
'@type': 'Question',
name: 'Can I add a specific amount?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, you can pre-fill an amount so when the user scans, their wallet app automatically suggests the correct payment value.',
},
},
{
'@type': 'Question',
name: 'Does it work with all wallets?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, standard crypto QR codes are universally readable by almost all modern wallet apps (Coinbase, MetaMask, Trust Wallet, etc.).',
},
},
{
'@type': 'Question',
name: 'Are there any fees?',
acceptedAnswer: {
'@type': 'Answer',
text: 'No. This generator is completely free. We do not charge any fees for generating codes or for the transactions made using them.',
},
},
],
},
}),
],
};
@ -273,9 +303,6 @@ export default function CryptoQRCodePage() {
</div>
</section>
{/* 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">

View File

@ -107,7 +107,7 @@ export default function EmailGenerator() {
<div className="grid lg:grid-cols-2">
{/* LEFT: Input Section */}
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
{/* Input Fields */}
<div className="space-y-6">
@ -192,7 +192,7 @@ export default function EmailGenerator() {
{/* Frame Selector */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div className="grid grid-cols-4 gap-2">
{FRAME_OPTIONS.map((frame) => (
<button
key={frame.id}
@ -213,12 +213,13 @@ export default function EmailGenerator() {
</div>
{/* RIGHT: Preview Section */}
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
{/* QR Card with Frame */}
<div
ref={qrRef}
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
style={{ minWidth: '320px' }}
>
{/* Frame Label */}
{getFrameLabel() && (
@ -272,7 +273,7 @@ export default function EmailGenerator() {
</Button>
</div>
<p className="text-xs text-slate-600 mt-4 text-center">
<p className="text-xs text-slate-500 mt-4 text-center">
100% free. No signup required.
</p>
</div>

View File

@ -4,24 +4,20 @@ import EmailGenerator from './EmailGenerator';
import { Mail, Zap, Smartphone, Lock, Download, Sparkles } 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 Email QR Code Generator | Email QR Code Erstellen | QR Master',
},
description: 'Create an Email QR code to send emails instantly. Email QR Code erstellen mit Betreff und Text. 100% free and secure.',
keywords: ['email qr code', 'mailto qr', 'email generator', 'free qr code', 'email qr code erstellen', 'email schreiben qr code', 'qr code für email', 'mailto qr code generator', 'email vorlage qr code'],
title: 'Free Email QR Code Generator | Mailto QR | QR Master',
description: 'Create an Email QR code to send emails instantly. Pre-fill subject and body. 100% free and client-side secure.',
keywords: ['email qr code', 'mailto qr', 'email generator', 'free qr code'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/email-qr-code',
canonical: 'https://qrmaster.io/tools/email-qr-code',
},
openGraph: {
title: 'Free Email QR Code Generator | QR Master',
description: 'Send emails instantly with a custom QR code. Add recipient, subject, and body.',
type: 'website',
url: 'https://www.qrmaster.net/tools/email-qr-code',
url: 'https://qrmaster.io/tools/email-qr-code',
images: [{ url: '/og-email-generator.png', width: 1200, height: 630 }],
},
};
@ -30,11 +26,14 @@ export const metadata: Metadata = {
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
generateSoftwareAppSchema(
'Email QR Code Generator',
'Generate Email QR codes for mailto links with subject and body.',
'/og-email-generator.png'
),
{
'@type': 'SoftwareApplication',
name: 'Email QR Code Generator',
applicationCategory: 'UtilitiesApplication',
operatingSystem: 'Web Browser',
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
description: 'Generate Email QR codes for mailto links with subject and body.',
},
{
'@type': 'HowTo',
name: 'How to Create an Email QR Code',
@ -47,28 +46,36 @@ const jsonLd = {
],
totalTime: 'PT30S',
},
generateFaqSchema({
'How does it work?': {
question: 'How does it work?',
answer: 'When scanned, it opens the user\'s default email app (like Gmail or Outlook) with a new draft composed to your address.',
{
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'How does it work?',
acceptedAnswer: { '@type': 'Answer', text: 'When scanned, it opens the user\'s default email app (like Gmail or Outlook) with a new draft composed to your address.' }
},
'Can I add a subject line?': {
question: 'Can I add a subject line?',
answer: 'Yes! You can pre-fill the subject line and the body content so the sender just has to hit send.',
{
'@type': 'Question',
name: 'Can I add a subject line?',
acceptedAnswer: { '@type': 'Answer', text: 'Yes! You can pre-fill the subject line and the body content so the sender just has to hit send.' }
},
'Is it free?': {
question: 'Is it free?',
answer: 'Yes, 100% free with unlimited scans.',
{
'@type': 'Question',
name: 'Is it free?',
acceptedAnswer: { '@type': 'Answer', text: 'Yes, 100% free with unlimited scans.' }
},
'Does it work with attachments?': {
question: 'Does it work with attachments?',
answer: 'No. The standard mailto format does not support attaching files automatically. Users will have to attach files manually.',
{
'@type': 'Question',
name: 'Does it work with attachments?',
acceptedAnswer: { '@type': 'Answer', text: 'No. The standard mailto format does not support attaching files automatically. Users will have to attach files manually.' }
},
'Is it private?': {
question: 'Is it private?',
answer: 'Yes. The data is encoded directly into the QR code. We do not store your email or message data.',
},
}),
{
'@type': 'Question',
name: 'Is it private?',
acceptedAnswer: { '@type': 'Answer', text: 'Yes. The data is encoded directly into the QR code. We do not store your email or message data.' }
}
]
}
]
};
@ -224,9 +231,6 @@ export default function EmailPage() {
</div>
</section>
{/* 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">

View File

@ -117,7 +117,7 @@ export default function EventGenerator() {
<div className="grid lg:grid-cols-2">
{/* LEFT: Input Section */}
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
{/* Event Details */}
<div className="space-y-6">
@ -221,7 +221,7 @@ export default function EventGenerator() {
{/* Frame Selector */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div className="grid grid-cols-4 gap-2">
{FRAME_OPTIONS.map((frame) => (
<button
key={frame.id}
@ -242,12 +242,13 @@ export default function EventGenerator() {
</div>
{/* RIGHT: Preview Section */}
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
{/* QR Card with Frame */}
<div
ref={qrRef}
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
style={{ minWidth: '320px' }}
>
{/* Frame Label */}
{getFrameLabel() && (
@ -277,7 +278,7 @@ export default function EventGenerator() {
<span className="truncate">{title || 'Event Title'}</span>
</h3>
{(startDate) && (
<div className="text-xs text-slate-600 mt-1 flex items-center justify-center gap-1">
<div className="text-xs text-slate-500 mt-1 flex items-center justify-center gap-1">
<Clock className="w-3 h-3" />
{new Date(startDate).toLocaleDateString()}
</div>
@ -304,7 +305,7 @@ export default function EventGenerator() {
</Button>
</div>
<p className="text-xs text-slate-600 mt-4 text-center">
<p className="text-xs text-slate-500 mt-4 text-center">
Scanning adds the event to the user's calendar.
</p>
</div>

View File

@ -4,24 +4,20 @@ import EventGenerator from './EventGenerator';
import { Calendar, Shield, Zap, Smartphone, Clock, UserCheck, Download, Share2, Check } 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 Event QR Code Generator | Termin & Kalender QR | QR Master',
},
description: 'Create a QR code for your event. Verabredung & Termine direkt in den Kalender speichern. Save the date instantly. Free & Easy.',
keywords: ['event qr code', 'calendar qr code', 'save the date qr', 'ical qr generator', 'invitation qr code', 'event qr code erstellen', 'veranstaltung qr code', 'kalender qr code', 'termin qr code', 'save the date qr code'],
title: 'Free Event QR Code Generator | Add to Calendar | QR Master',
description: 'Create a QR code for your event. Scanners can instantly save the date, time, and location to their phone calendar. Perfect for invitations and flyers.',
keywords: ['event qr code', 'calendar qr code', 'save the date qr', 'ical qr generator', 'invitation qr code'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/event-qr-code',
canonical: 'https://qrmaster.io/tools/event-qr-code',
},
openGraph: {
title: 'Free Event QR Code Generator | QR Master',
description: 'Generate QR codes to save events to calendars. Share dates easily.',
type: 'website',
url: 'https://www.qrmaster.net/tools/event-qr-code',
url: 'https://qrmaster.io/tools/event-qr-code',
images: [{ url: '/og-event-generator.png', width: 1200, height: 630 }],
},
twitter: {
@ -39,11 +35,23 @@ export const metadata: Metadata = {
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
generateSoftwareAppSchema(
'Event QR Code Generator',
'Generate QR codes that add event details to the user\'s digital calendar.',
'/og-event-generator.png'
),
{
'@type': 'SoftwareApplication',
name: 'Event QR Code Generator',
applicationCategory: 'UtilitiesApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.8',
ratingCount: '760',
},
description: 'Generate QR codes that add event details to the user\'s digital calendar.',
},
{
'@type': 'HowTo',
name: 'How to Create an Event QR Code',
@ -82,28 +90,51 @@ const jsonLd = {
],
totalTime: 'PT45S',
},
generateFaqSchema({
'Which calendars does it support?': {
question: 'Which calendars does it support?',
answer: 'The QR code uses the standard iCalendar (ICS) format. It works with Apple Calendar, Google Calendar, Outlook, and most other mobile calendar apps.',
{
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'Which calendars does it support?',
acceptedAnswer: {
'@type': 'Answer',
text: 'The QR code uses the standard iCalendar (ICS) format. It works with Apple Calendar, Google Calendar, Outlook, and most other mobile calendar apps.',
},
'Can I change the date after printing?': {
question: 'Can I change the date after printing?',
answer: 'No. This is a static QR code, meaning the event details are permanently embedded in the image. If the date changes, you must create a new QR code. Use our Dynamic QR Code to edit events anytime.',
},
'Is there a limit to the description length?': {
question: 'Is there a limit to the description length?',
answer: 'Yes, because the data is stored in the QR code pattern. We recommend keeping descriptions concise (under 300 characters) to ensure the code remains easy to scan.',
{
'@type': 'Question',
name: 'Can I change the date after printing?',
acceptedAnswer: {
'@type': 'Answer',
text: 'No. This is a static QR code, meaning the event details are permanently embedded in the image. If the date changes, you must create a new QR code. Use our Dynamic QR Code to edit events anytime.',
},
'Do users need an app?': {
question: 'Do users need an app?',
answer: 'No special app is needed. Standard camera apps on iOS and Android can read the code and will prompt the user to "Add to Calendar".',
},
'Is it free?': {
question: 'Is it free?',
answer: 'Yes. Creating and scanning the code is completely free and requires no signup.',
{
'@type': 'Question',
name: 'Is there a limit to the description length?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, because the data is stored in the QR code pattern. We recommend keeping descriptions concise (under 300 characters) to ensure the code remains easy to scan.',
},
},
{
'@type': 'Question',
name: 'Do users need an app?',
acceptedAnswer: {
'@type': 'Answer',
text: 'No special app is needed. Standard camera apps on iOS and Android can read the code and will prompt the user to "Add to Calendar".',
},
},
{
'@type': 'Question',
name: 'Is it free?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes. Creating and scanning the code is completely free and requires no signup.',
},
},
],
},
}),
],
};
@ -267,9 +298,6 @@ export default function EventQRCodePage() {
</div>
</section>
{/* 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">

View File

@ -88,7 +88,7 @@ export default function FacebookGenerator() {
<div className="grid lg:grid-cols-2">
{/* LEFT: Input Section */}
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
{/* Facebook Details */}
<div className="space-y-6">
@ -105,7 +105,7 @@ export default function FacebookGenerator() {
onChange={(e) => setUrl(e.target.value)}
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#1877F2] focus:ring-[#1877F2]"
/>
<p className="text-xs text-slate-600 mt-2">Paste the full link to your profile, page, group, or post.</p>
<p className="text-xs text-slate-500 mt-2">Paste the full link to your profile, page, group, or post.</p>
</div>
</div>
@ -143,7 +143,7 @@ export default function FacebookGenerator() {
{/* Frame Selector */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div className="grid grid-cols-4 gap-2">
{FRAME_OPTIONS.map((frame) => (
<button
key={frame.id}
@ -164,12 +164,13 @@ export default function FacebookGenerator() {
</div>
{/* RIGHT: Preview Section */}
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
{/* QR Card with Frame */}
<div
ref={qrRef}
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
style={{ minWidth: '320px' }}
>
{/* Frame Label */}
{getFrameLabel() && (
@ -198,7 +199,7 @@ export default function FacebookGenerator() {
<Facebook className="w-4 h-4 text-slate-400 shrink-0" />
<span className="truncate">{url ? url.replace('https://', '') : 'facebook.com/...'}</span>
</h3>
<div className="text-xs text-slate-600 mt-1">Opens in Facebook App</div>
<div className="text-xs text-slate-500 mt-1">Opens in Facebook App</div>
</div>
</div>
@ -221,7 +222,7 @@ export default function FacebookGenerator() {
</Button>
</div>
<p className="text-xs text-slate-600 mt-4 text-center">
<p className="text-xs text-slate-500 mt-4 text-center">
Scanning redirects directly to the Facebook profile or page.
</p>
</div>

View File

@ -4,24 +4,20 @@ import FacebookGenerator from './FacebookGenerator';
import { Facebook, Shield, Zap, Smartphone, ThumbsUp, Users, Download, Share2 } 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 Facebook QR Code Generator | Get Likes & Follows | QR Master',
},
description: 'Create a QR code for your Facebook Page, Profile, or Group. Facebook QR Code erstellen. Scanners follow you instantly. Free & Easy.',
keywords: ['facebook qr code', 'fb qr generator', 'facebook page qr', 'follow qr code', 'social media qr code', 'facebook qr code erstellen', 'facebook seite qr code', 'facebook gruppe qr code', 'facebook profil qr code', 'mehr likes qr code'],
title: 'Free Facebook QR Code Generator | Get Likes & Follows | QR Master',
description: 'Create a QR code for your Facebook Page, Profile, or Group. Scanners are redirected to the Facebook app instantly to like and follow. Free & Easy.',
keywords: ['facebook qr code', 'fb qr generator', 'facebook page qr', 'follow qr code', 'social media qr code'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/facebook-qr-code',
canonical: 'https://qrmaster.io/tools/facebook-qr-code',
},
openGraph: {
title: 'Free Facebook QR Code Generator | QR Master',
description: 'Generate QR codes to grow your Facebook audience. Instant app redirect.',
type: 'website',
url: 'https://www.qrmaster.net/tools/facebook-qr-code',
url: 'https://qrmaster.io/tools/facebook-qr-code',
images: [{ url: '/og-facebook-generator.png', width: 1200, height: 630 }],
},
twitter: {
@ -39,11 +35,23 @@ export const metadata: Metadata = {
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
generateSoftwareAppSchema(
'Facebook QR Code Generator',
'Generate QR codes that direct users to a Facebook page, profile, or post.',
'/og-facebook-generator.png'
),
{
'@type': 'SoftwareApplication',
name: 'Facebook QR Code Generator',
applicationCategory: 'UtilitiesApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.8',
ratingCount: '1120',
},
description: 'Generate QR codes that direct users to a Facebook page, profile, or post.',
},
{
'@type': 'HowTo',
name: 'How to Create a Facebook QR Code',
@ -82,28 +90,51 @@ const jsonLd = {
],
totalTime: 'PT30S',
},
generateFaqSchema({
'Does it open the Facebook app?': {
question: 'Does it open the Facebook app?',
answer: 'Yes! On most mobile devices, standard Facebook links are automatically detected and opened in the Facebook app if it is installed.',
{
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'Does it open the Facebook app?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes! On most mobile devices, standard Facebook links are automatically detected and opened in the Facebook app if it is installed.',
},
'Can I link to a specific post?': {
question: 'Can I link to a specific post?',
answer: 'Absolutely. Just paste the direct link to the post (click the timestamp on the post to get the link).',
},
'Does it work for Facebook Events?': {
question: 'Does it work for Facebook Events?',
answer: 'Yes. Simply copy the full URL of your Facebook Event and paste it into the generator.',
{
'@type': 'Question',
name: 'Can I link to a specific post?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Absolutely. Just paste the direct link to the post (click the timestamp on the post to get the link).',
},
'Is it free?': {
question: 'Is it free?',
answer: 'Yes, this generator is 100% free to use for personal or business purposes.',
},
'Can I track scans?': {
question: 'Can I track scans?',
answer: 'This static QR code does not include analytics. To track how many people scan your code, you should use our Dynamic QR Code service.',
{
'@type': 'Question',
name: 'Does it work for Facebook Events?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes. Simply copy the full URL of your Facebook Event and paste it into the generator.',
},
},
{
'@type': 'Question',
name: 'Is it free?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, this generator is 100% free to use for personal or business purposes.',
},
},
{
'@type': 'Question',
name: 'Can I track scans?',
acceptedAnswer: {
'@type': 'Answer',
text: 'This static QR code does not include analytics. To track how many people scan your code, you should use our Dynamic QR Code service.',
},
},
],
},
}),
],
};
@ -275,9 +306,6 @@ export default function FacebookQRCodePage() {
</div>
</section>
{/* 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">

View File

@ -108,7 +108,7 @@ export default function GeolocationGenerator() {
<div className="grid lg:grid-cols-2">
{/* LEFT: Input Section */}
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
{/* Location Details */}
<div className="space-y-6">
@ -148,7 +148,7 @@ export default function GeolocationGenerator() {
/>
</div>
</div>
<p className="text-xs text-slate-600">
<p className="text-xs text-slate-500">
Tip: You can copy-paste coordinates directly from Google Maps.
(Right-click a location on standard Maps, then click the coordinates to copy).
</p>
@ -188,7 +188,7 @@ export default function GeolocationGenerator() {
{/* Frame Selector */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div className="grid grid-cols-4 gap-2">
{FRAME_OPTIONS.map((frame) => (
<button
key={frame.id}
@ -209,12 +209,13 @@ export default function GeolocationGenerator() {
</div>
{/* RIGHT: Preview Section */}
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
{/* QR Card with Frame */}
<div
ref={qrRef}
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
style={{ minWidth: '320px' }}
>
{/* Frame Label */}
{getFrameLabel() && (
@ -243,7 +244,7 @@ export default function GeolocationGenerator() {
<MapPin className="w-4 h-4 text-[#10B981] shrink-0" />
<span className="truncate">{latitude || 'Lat'}, {longitude || 'Long'}</span>
</h3>
<div className="text-xs text-slate-600 mt-1">Google Maps Location</div>
<div className="text-xs text-slate-500 mt-1">Google Maps Location</div>
</div>
</div>
@ -266,7 +267,7 @@ export default function GeolocationGenerator() {
</Button>
</div>
<p className="text-xs text-slate-600 mt-4 text-center">
<p className="text-xs text-slate-500 mt-4 text-center">
Scanning opens the location directly in Google Maps.
</p>
</div>

View File

@ -4,24 +4,20 @@ import GeolocationGenerator from './GeolocationGenerator';
import { MapPin, Shield, Zap, Smartphone, Navigation, Map, Download, Share2 } 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 Geolocation QR Code Generator | Standort & Map Links | QR Master',
},
description: 'Create a QR code for a specific location. Erstelle einen Map QR Code für Google Maps. Coordinates & Directions instantly. Standort teilen leicht gemacht.',
keywords: ['location qr code', 'maps qr code', 'google maps qr generator', 'geolocation qr', 'coordinates qr code', 'standort qr code', 'google maps qr code erstellen', 'koordinaten qr code', 'wegbeschreibung qr code', 'maps qr code generator'],
title: 'Free Geolocation QR Code Generator | Maps & Directions | QR Master',
description: 'Create a QR code for a specific location using Latitude and Longitude. Scanners will open Google Maps directly to your pin. Free & Precise.',
keywords: ['location qr code', 'maps qr code', 'google maps qr generator', 'geolocation qr', 'coordinates qr code'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/geolocation-qr-code',
canonical: 'https://qrmaster.io/tools/geolocation-qr-code',
},
openGraph: {
title: 'Free Geolocation QR Code Generator | QR Master',
description: 'Navigate users to any location with a QR code. Opens directly in Google Maps.',
type: 'website',
url: 'https://www.qrmaster.net/tools/geolocation-qr-code',
url: 'https://qrmaster.io/tools/geolocation-qr-code',
images: [{ url: '/og-geolocation-generator.png', width: 1200, height: 630 }],
},
twitter: {
@ -39,11 +35,23 @@ export const metadata: Metadata = {
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
generateSoftwareAppSchema(
'Geolocation QR Code Generator',
'Generate QR codes that open specific geographic coordinates in map applications.',
'/og-geolocation-generator.png'
),
{
'@type': 'SoftwareApplication',
name: 'Geolocation QR Code Generator',
applicationCategory: 'UtilitiesApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.7',
ratingCount: '890',
},
description: 'Generate QR codes that open specific geographic coordinates in map applications.',
},
{
'@type': 'HowTo',
name: 'How to Create a Location QR Code',
@ -82,28 +90,51 @@ const jsonLd = {
],
totalTime: 'PT45S',
},
generateFaqSchema({
'Which map app does it open?': {
question: 'Which map app does it open?',
answer: 'Our generator creates a universal Google Maps link. On most devices, this will open the Google Maps app if installed, or the browser version if not. It is the most compatible method.',
{
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'Which map app does it open?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Our generator creates a universal Google Maps link. On most devices, this will open the Google Maps app if installed, or the browser version if not. It is the most compatible method.',
},
'How do I find my Latitude and Longitude?': {
question: 'How do I find my Latitude and Longitude?',
answer: 'On Google Maps desktop: Right-click any spot on the map. The first item in the menu is the coordinates. Click to copy them.',
},
'Does it work offline?': {
question: 'Does it work offline?',
answer: 'The QR code itself can be scanned offline, but the user will likely need an internet connection to load the map and get directions.',
{
'@type': 'Question',
name: 'How do I find my Latitude and Longitude?',
acceptedAnswer: {
'@type': 'Answer',
text: 'On Google Maps desktop: Right-click any spot on the map. The first item in the menu is the coordinates. Click to copy them.',
},
'Can I use an address instead?': {
question: 'Can I use an address instead?',
answer: 'For precise results, we use coordinates. However, you can use our URL Generator and paste a link to your Google Maps address search result if you prefer.',
},
'Is it free?': {
question: 'Is it free?',
answer: 'Yes, generating this location QR code is completely free and requires no signup.',
{
'@type': 'Question',
name: 'Does it work offline?',
acceptedAnswer: {
'@type': 'Answer',
text: 'The QR code itself can be scanned offline, but the user will likely need an internet connection to load the map and get directions.',
},
},
{
'@type': 'Question',
name: 'Can I use an address instead?',
acceptedAnswer: {
'@type': 'Answer',
text: 'For precise results, we use coordinates. However, you can use our URL Generator and paste a link to your Google Maps address search result if you prefer.',
},
},
{
'@type': 'Question',
name: 'Is it free?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, generating this location QR code is completely free and requires no signup.',
},
},
],
},
}),
],
};
@ -180,7 +211,7 @@ export default function GeolocationQRCodePage() {
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<MapPin className="w-8 h-8 text-red-500 drop-shadow-lg animate-bounce" />
</div>
<div className="absolute bottom-2 left-2 right-2 bg-white/90 p-2 rounded text-[10px] text-slate-600 font-mono text-center">
<div className="absolute bottom-2 left-2 right-2 bg-white/90 p-2 rounded text-[10px] text-slate-500 font-mono text-center">
40.7128° N, 74.0060° W
</div>
</div>
@ -270,9 +301,6 @@ export default function GeolocationQRCodePage() {
</div>
</section>
{/* 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">

View File

@ -93,7 +93,7 @@ export default function InstagramGenerator() {
<div className="grid lg:grid-cols-2">
{/* LEFT: Input Section */}
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
{/* Instagram Details */}
<div className="space-y-6">
@ -110,7 +110,7 @@ export default function InstagramGenerator() {
onChange={(e) => setUsername(e.target.value)}
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#E1306C] focus:ring-[#E1306C]"
/>
<p className="text-xs text-slate-600 mt-2">Enter your username (without @) or paste full profile link.</p>
<p className="text-xs text-slate-500 mt-2">Enter your username (without @) or paste full profile link.</p>
</div>
</div>
@ -148,7 +148,7 @@ export default function InstagramGenerator() {
{/* Frame Selector */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div className="grid grid-cols-4 gap-2">
{FRAME_OPTIONS.map((frame) => (
<button
key={frame.id}
@ -169,12 +169,13 @@ export default function InstagramGenerator() {
</div>
{/* RIGHT: Preview Section */}
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
{/* QR Card with Frame */}
<div
ref={qrRef}
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
style={{ minWidth: '320px' }}
>
{/* Frame Label */}
{getFrameLabel() && (
@ -203,7 +204,7 @@ export default function InstagramGenerator() {
<Instagram className="w-4 h-4 text-slate-400 shrink-0" />
<span className="truncate">{username || '@username'}</span>
</h3>
<div className="text-xs text-slate-600 mt-1">Opens in Instagram</div>
<div className="text-xs text-slate-500 mt-1">Opens in Instagram</div>
</div>
</div>
@ -226,7 +227,7 @@ export default function InstagramGenerator() {
</Button>
</div>
<p className="text-xs text-slate-600 mt-4 text-center">
<p className="text-xs text-slate-500 mt-4 text-center">
Scanning redirects directly to your Instagram profile.
</p>
</div>

View File

@ -4,24 +4,20 @@ import InstagramGenerator from './InstagramGenerator';
import { Instagram, Shield, Zap, Smartphone, Camera, Heart, Download, Share2 } 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 Instagram QR Code Generator | Get More Followers | QR Master',
},
description: 'Create a QR code for your Instagram profile. Erstelle einen Insta QR Code. Scanners follow you instantly. Free & Customizable.',
keywords: ['instagram qr code', 'insta qr generator', 'ig nametag generator', 'instagram follow qr', 'social media qr code', 'instagram qr code erstellen', 'instagram profil qr code', 'insta qr code', 'mehr follower qr code', 'instagram nametag generator'],
title: 'Free Instagram QR Code Generator | Get More Followers | QR Master',
description: 'Create a QR code for your Instagram profile or post. Scanners are redirected to the Instagram app instantly to follow you. Free & Customizable.',
keywords: ['instagram qr code', 'insta qr generator', 'ig nametag generator', 'instagram follow qr', 'social media qr code'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/instagram-qr-code',
canonical: 'https://qrmaster.io/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',
url: 'https://qrmaster.io/tools/instagram-qr-code',
images: [{ url: '/og-instagram-generator.png', width: 1200, height: 630 }],
},
twitter: {
@ -39,11 +35,23 @@ export const metadata: Metadata = {
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': 'SoftwareApplication',
name: 'Instagram QR Code Generator',
applicationCategory: 'UtilitiesApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.9',
ratingCount: '2150',
},
description: 'Generate QR codes that direct users to an Instagram profile or post.',
},
{
'@type': 'HowTo',
name: 'How to Create an Instagram QR Code',
@ -82,28 +90,51 @@ const jsonLd = {
],
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.',
{
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'Is this an Instagram Nametag?',
acceptedAnswer: {
'@type': 'Answer',
text: '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.',
{
'@type': 'Question',
name: 'Does it open the Instagram app?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes. When scanned on a mobile device with Instagram installed, it will deep-link directly to the profile in the app.',
},
'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.',
{
'@type': 'Question',
name: 'Can I link to a specific photo or reel?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes! Instead of your username, just paste the full link to the specific post or reel.',
},
},
{
'@type': 'Question',
name: 'Is it free?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, generating this QR code is 100% free.',
},
},
{
'@type': 'Question',
name: 'Can I track scans?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Not with this static tool. If you need scan analytics, consider using our Dynamic QR Code solution.',
},
},
],
},
}),
],
};
@ -264,8 +295,6 @@ export default function InstagramQRCodePage() {
</div>
</section>
<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">

View File

@ -1,37 +0,0 @@
'use client';
import React from 'react';
import Script from 'next/script';
import AdBanner from '@/components/ads/AdBanner';
export default function ToolsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex flex-col min-h-screen">
{/* AdSense script - only loads on tool pages */}
<Script
async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2782770414424875"
crossOrigin="anonymous"
strategy="lazyOnload"
/>
<div className="flex-grow relative">
{children}
</div>
{/* Footer Ad Placement - Appears on ALL tool pages */}
{/* AdBanner handles its own visibility - only shows when an ad is filled */}
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl pb-8">
<AdBanner
dataAdSlot="1234567890" // Placeholder
dataAdFormat="auto"
fullWidthResponsive={true}
className="bg-slate-50 rounded-xl p-4 border border-slate-100"
/>
</div>
</div>
);
}

View File

@ -129,7 +129,7 @@ export default function PayPalGenerator() {
<div className="grid lg:grid-cols-2">
{/* LEFT: Input Section */}
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
{/* PayPal Details */}
<div className="space-y-6">
@ -171,7 +171,7 @@ export default function PayPalGenerator() {
onChange={(e) => setPaypalId(e.target.value)}
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#003087] focus:ring-[#003087]"
/>
<p className="text-xs text-slate-600 mt-2">
<p className="text-xs text-slate-500 mt-2">
{inputType === 'username'
? <>Find yours at <a href="https://paypal.me" target="_blank" rel="noopener noreferrer" className="text-[#003087] underline">paypal.me</a></>
: 'The email address linked to your PayPal account'
@ -196,7 +196,6 @@ export default function PayPalGenerator() {
value={currency}
onChange={(e) => setCurrency(e.target.value)}
className="h-12 rounded-xl border-slate-200"
aria-label="Currency"
options={CURRENCIES}
/>
</div>
@ -238,7 +237,7 @@ export default function PayPalGenerator() {
{/* Frame Selector */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2">
<div className="grid grid-cols-5 gap-2">
{FRAME_OPTIONS.map((frame) => (
<button
key={frame.id}
@ -259,12 +258,13 @@ export default function PayPalGenerator() {
</div>
{/* RIGHT: Preview Section */}
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
{/* QR Card with Frame */}
<div
ref={qrRef}
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
style={{ minWidth: '320px' }}
>
{/* Frame Label */}
{getFrameLabel() && (
@ -294,7 +294,7 @@ export default function PayPalGenerator() {
<span className="truncate">{paypalId || 'Your PayPal'}</span>
</h3>
{amount && (
<p className="text-sm text-slate-600 mt-1">{amount} {currency}</p>
<p className="text-sm text-slate-500 mt-1">{amount} {currency}</p>
)}
</div>
</div>
@ -318,7 +318,7 @@ export default function PayPalGenerator() {
</Button>
</div>
<p className="text-xs text-slate-600 mt-4 text-center">
<p className="text-xs text-slate-500 mt-4 text-center">
Your PayPal link is encoded directly. Static and forever free.
</p>
</div>

View File

@ -4,24 +4,20 @@ import PayPalGenerator from './PayPalGenerator';
import { CreditCard, Shield, Zap, Smartphone, DollarSign, Download, Share2, Banknote } 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 PayPal QR Code Generator | Accept Payments Instantly | QR Master',
},
description: 'Create a QR code for your PayPal.me link. PayPal QR Code erstellen. Receive payments instantly. Support tips, donations, and fixed amounts.',
keywords: ['paypal qr code', 'paypal.me qr generator', 'payment qr code', 'accept payments qr', 'paypal qr generator', 'tip qr code', 'donation qr code', 'paypal qr code erstellen', 'zahlungs qr code', 'spenden qr code', 'paypal bezahlen qr'],
title: 'Free PayPal QR Code Generator | Accept Payments Instantly | QR Master',
description: 'Create a QR code for your PayPal.me link. Let customers pay you instantly by scanning. Support tips, donations, and fixed amounts. 100% free.',
keywords: ['paypal qr code', 'paypal.me qr generator', 'payment qr code', 'accept payments qr', 'paypal qr generator', 'tip qr code', 'donation qr code'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/paypal-qr-code',
canonical: 'https://qrmaster.io/tools/paypal-qr-code',
},
openGraph: {
title: 'Free PayPal QR Code Generator | QR Master',
description: 'Generate QR codes for PayPal payments. Perfect for tips, donations, and invoices.',
type: 'website',
url: 'https://www.qrmaster.net/tools/paypal-qr-code',
url: 'https://qrmaster.io/tools/paypal-qr-code',
images: [{ url: '/og-paypal-generator.png', width: 1200, height: 630 }],
},
twitter: {
@ -39,12 +35,23 @@ export const metadata: Metadata = {
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
generateSoftwareAppSchema(
'PayPal QR Code Generator',
'Generate QR codes that link to your PayPal.me page for instant payments.',
'/og-paypal-generator.png',
'FinanceApplication'
),
{
'@type': 'SoftwareApplication',
name: 'PayPal QR Code Generator',
applicationCategory: 'FinanceApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.9',
ratingCount: '980',
},
description: 'Generate QR codes that link to your PayPal.me page for instant payments.',
},
{
'@type': 'HowTo',
name: 'How to Create a PayPal QR Code',
@ -83,28 +90,51 @@ const jsonLd = {
],
totalTime: 'PT30S',
},
generateFaqSchema({
'How does the PayPal QR code work?': {
question: 'How does the PayPal QR code work?',
answer: 'When scanned, it opens the PayPal app or website with your PayPal.me link. If you set an amount, it will be pre-filled for the payer.',
{
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'How does the PayPal QR code work?',
acceptedAnswer: {
'@type': 'Answer',
text: 'When scanned, it opens the PayPal app or website with your PayPal.me link. If you set an amount, it will be pre-filled for the payer.',
},
'Do I need a PayPal Business account?': {
question: 'Do I need a PayPal Business account?',
answer: 'No. Any PayPal account with a PayPal.me link can use this generator. Personal accounts work fine for tips and donations.',
},
'Is there a fee for using the QR code?': {
question: 'Is there a fee for using the QR code?',
answer: 'This generator is 100% free. PayPal may charge their standard transaction fees when you receive payments.',
{
'@type': 'Question',
name: 'Do I need a PayPal Business account?',
acceptedAnswer: {
'@type': 'Answer',
text: 'No. Any PayPal account with a PayPal.me link can use this generator. Personal accounts work fine for tips and donations.',
},
'Can I change the amount later?': {
question: 'Can I change the amount later?',
answer: 'No, this is a static QR code. The amount is encoded permanently. For variable amounts, leave the amount field empty.',
},
'What currencies are supported?': {
question: 'What currencies are supported?',
answer: 'We support EUR, USD, GBP, and CHF. PayPal handles currency conversion automatically.',
{
'@type': 'Question',
name: 'Is there a fee for using the QR code?',
acceptedAnswer: {
'@type': 'Answer',
text: 'This generator is 100% free. PayPal may charge their standard transaction fees when you receive payments.',
},
},
{
'@type': 'Question',
name: 'Can I change the amount later?',
acceptedAnswer: {
'@type': 'Answer',
text: 'No, this is a static QR code. The amount is encoded permanently. For variable amounts, leave the amount field empty.',
},
},
{
'@type': 'Question',
name: 'What currencies are supported?',
acceptedAnswer: {
'@type': 'Answer',
text: 'We support EUR, USD, GBP, and CHF. PayPal handles currency conversion automatically.',
},
},
],
},
}),
],
};
@ -261,9 +291,6 @@ export default function PayPalQRCodePage() {
</div>
</section>
{/* 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">

View File

@ -89,7 +89,7 @@ export default function PhoneGenerator() {
<div className="grid lg:grid-cols-2">
{/* LEFT: Input Section */}
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
{/* Phone Details */}
<div className="space-y-6">
@ -106,7 +106,7 @@ export default function PhoneGenerator() {
onChange={(e) => setPhone(e.target.value)}
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#1A1265] focus:ring-[#1A1265]"
/>
<p className="text-xs text-slate-600 mt-2">Enter with country code for best results (e.g. +1).</p>
<p className="text-xs text-slate-500 mt-2">Enter with country code for best results (e.g. +1).</p>
</div>
</div>
@ -144,7 +144,7 @@ export default function PhoneGenerator() {
{/* Frame Selector */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div className="grid grid-cols-4 gap-2">
{FRAME_OPTIONS.map((frame) => (
<button
key={frame.id}
@ -165,12 +165,13 @@ export default function PhoneGenerator() {
</div>
{/* RIGHT: Preview Section */}
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
{/* QR Card with Frame */}
<div
ref={qrRef}
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
style={{ minWidth: '320px' }}
>
{/* Frame Label */}
{getFrameLabel() && (
@ -221,7 +222,7 @@ export default function PhoneGenerator() {
</Button>
</div>
<p className="text-xs text-slate-600 mt-4 text-center">
<p className="text-xs text-slate-500 mt-4 text-center">
Scanning initiates a call on any mobile phone.
</p>
</div>

View File

@ -4,24 +4,20 @@ import PhoneGenerator from './PhoneGenerator';
import { Phone, Shield, Zap, Smartphone, PhoneCall, Download } 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 Phone QR Code Generator | Anruf & Telefon QR | QR Master',
},
description: 'Create a QR code that makes a phone call. Anruf und Telefonat starten per Scan. Perfect for business cards & support. Kostenlos Call-QR erstellen.',
keywords: ['phone qr code', 'call qr code', 'phone number qr generator', 'click to call qr', 'business card qr code', 'telefon qr code', 'anruf qr code', 'qr code telefonnummer', 'anruf starten qr code', 'telefonnummer scannen'],
title: 'Free Phone QR Code Generator | Call Instantly | QR Master',
description: 'Create a QR code that makes a phone call when scanned. Perfect for business cards, flyers, and support lines. 100% Free & No Signup.',
keywords: ['phone qr code', 'call qr code', 'phone number qr generator', 'click to call qr', 'business card qr code'],
alternates: {
canonical: 'https://www.qrmaster.net/tools/phone-qr-code',
canonical: 'https://qrmaster.io/tools/phone-qr-code',
},
openGraph: {
title: 'Free Phone QR Code Generator | QR Master',
description: 'Generate QR codes to initiate phone calls instantly. Share your number easily.',
type: 'website',
url: 'https://www.qrmaster.net/tools/phone-qr-code',
url: 'https://qrmaster.io/tools/phone-qr-code',
images: [{ url: '/og-phone-generator.png', width: 1200, height: 630 }],
},
twitter: {
@ -39,11 +35,23 @@ export const metadata: Metadata = {
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
generateSoftwareAppSchema(
'Phone QR Code Generator',
'Generate QR codes that trigger a phone call when scanned on a mobile device.',
'/og-phone-generator.png'
),
{
'@type': 'SoftwareApplication',
name: 'Phone QR Code Generator',
applicationCategory: 'UtilitiesApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.8',
ratingCount: '1500',
},
description: 'Generate QR codes that trigger a phone call when scanned on a mobile device.',
},
{
'@type': 'HowTo',
name: 'How to Create a Phone QR Code',
@ -82,28 +90,51 @@ const jsonLd = {
],
totalTime: 'PT30S',
},
generateFaqSchema({
'Does it call automatically?': {
question: 'Does it call automatically?',
answer: 'Scanning the QR code opens the phone dialer with the number pre-filled. The user must tap the call button to initiate the call.',
{
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'Does it call automatically?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Scanning the QR code opens the phone dialer with the number pre-filled. The user must tap the call button to initiate the call.',
},
'Does it work internationally?': {
question: 'Does it work internationally?',
answer: 'Yes! We recommend entering your number in international format (starting with +) to ensure it works anywhere in the world.',
},
'Is my phone number private?': {
question: 'Is my phone number private?',
answer: 'Yes. We do not store your number. It is encoded directly into the QR code image.',
{
'@type': 'Question',
name: 'Does it work internationally?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes! We recommend entering your number in international format (starting with +) to ensure it works anywhere in the world.',
},
'Can I track calls?': {
question: 'Can I track calls?',
answer: 'This static QR code cannot track calls. For tracking scans and analytics, consider using our Dynamic QR Code solution.',
},
'Is it free?': {
question: 'Is it free?',
answer: 'Yes, completely free. We do not charge for generating or scanning the code.',
{
'@type': 'Question',
name: 'Is my phone number private?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes. We do not store your number. It is encoded directly into the QR code image.',
},
},
{
'@type': 'Question',
name: 'Can I track calls?',
acceptedAnswer: {
'@type': 'Answer',
text: 'This static QR code cannot track calls. For tracking scans and analytics, consider using our Dynamic QR Code solution.',
},
},
{
'@type': 'Question',
name: 'Is it free?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, completely free. We do not charge for generating or scanning the code.',
},
},
],
},
}),
],
};
@ -272,9 +303,6 @@ export default function PhoneQRCodePage() {
</div>
</section>
{/* 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">

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