Compare commits
42 Commits
dynamisch
...
896c9b1a07
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
896c9b1a07 | ||
|
|
cca1374c9e | ||
|
|
c1471830f3 | ||
| 373e19a515 | |||
|
|
99acb37c83 | ||
| 7328b3240d | |||
|
|
185c40f470 | ||
|
|
9373db8ae6 | ||
|
|
0eafe421d2 | ||
|
|
af1c0456d7 | ||
|
|
2dec18fc97 | ||
| 2879fd0d8e | |||
|
|
be54d388bb | ||
|
|
83ea141230 | ||
|
|
0a02876ea4 | ||
|
|
904e439102 | ||
|
|
f68b7a331c | ||
| 1747922b29 | |||
| 8b7deb9312 | |||
| 6b586ac21b | |||
| 3e9daa648a | |||
| ceb2ac40ec | |||
| 65def796ea | |||
| e9bc1fe98b | |||
|
|
b63f5f424e | ||
|
|
9746fb970d | ||
|
|
fb788d89d3 | ||
|
|
d585e5aed3 | ||
|
|
fa538b8bec | ||
|
|
4fc1dcd7d8 | ||
|
|
ffe4cca5e5 | ||
|
|
5b74b7b405 | ||
|
|
5784a52e3c | ||
|
|
b3e858c033 | ||
|
|
111575aeda | ||
|
|
beed961eef | ||
|
|
95aa378763 | ||
|
|
e0871e2960 | ||
|
|
038c8dddbc | ||
|
|
c6adc8567f | ||
|
|
1f067e81f3 | ||
|
|
d64459b200 |
2
.gitignore
vendored
@@ -36,7 +36,7 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
|
||||
# prisma
|
||||
/prisma/migrations/
|
||||
|
||||
|
||||
# docker
|
||||
docker-compose.override.yml
|
||||
|
||||
87
INDEXING_GUIDE.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Indexing Setup & Usage Guide
|
||||
|
||||
This guide explains how to fast-track your content indexing on **Google** and **Bing/Yandex** using the provided scripts.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **WAIT UNTIL LIVE:** Do not run these scripts until your new URLs are live and returning a `200 OK` status. If you submit a `404` URL, it may negatively impact your crawling budget or cause errors.
|
||||
|
||||
---
|
||||
|
||||
## 1. Google Indexing API
|
||||
|
||||
The Google Indexing API allows you to notify Google when pages are added or removed. It is faster than waiting for the Googlebot to crawl your sitemap.
|
||||
|
||||
### Prerequisites: `service_account.json`
|
||||
|
||||
To use the script `scripts/trigger-indexing.js`, you need a **Service Account Key** from Google Cloud.
|
||||
|
||||
1. **Go to Google Cloud Console:** [https://console.cloud.google.com/](https://console.cloud.google.com/)
|
||||
2. **Create a Project:** (e.g., "QR Master Indexing").
|
||||
3. **Enable API:** Search for "Web Search Indexing API" and enable it.
|
||||
4. **Create Service Account:**
|
||||
* Go to "IAM & Admin" > "Service Accounts".
|
||||
* Click "Create Service Account".
|
||||
* Name it (e.g., "indexer").
|
||||
* Grant it the "Owner" role (simplest for this) or a custom role with Indexing permissions.
|
||||
5. **Create Key:**
|
||||
* Click on the newly created service account email.
|
||||
* Go to "Keys" tab -> "Add Key" -> "Create new key" -> **JSON**.
|
||||
* This will download a JSON file.
|
||||
6. **Save Key:**
|
||||
* Rename the file to `service_account.json`.
|
||||
* Place it in the **root** of your project (same folder as `package.json`).
|
||||
* **NOTE:** This file is ignored by git for security (`.gitignore`), so you must copy it manually if you switch laptops.
|
||||
7. **Authorize in Search Console:**
|
||||
* Open the JSON file and copy the `client_email` address.
|
||||
* Go to **Google Search Console** property for `qrmaster.net`.
|
||||
* Go to "Settings" > "Users and permissions".
|
||||
* **Add User:** Paste the service account email and give it **"Owner"** permission. (This is required for the API to work).
|
||||
|
||||
### How to Run
|
||||
|
||||
1. **Run the script:**
|
||||
```bash
|
||||
npm run trigger:indexing
|
||||
```
|
||||
*(Or manually: `npx tsx scripts/trigger-indexing.ts`)*
|
||||
|
||||
2. The script will automatically fetch ALL active URLs from the project (including tools and blog posts) and submit them to Google. You should see a "Success" message for each URL.
|
||||
|
||||
---
|
||||
|
||||
## 2. IndexNow (Bing, Yandex, etc.)
|
||||
|
||||
IndexNow is a protocol used by Bing and others. It's much simpler than Google's API.
|
||||
|
||||
### Prerequisites: API Key
|
||||
|
||||
1. **Get Key:** Go to [Bing Webmaster Tools](https://www.bing.com/webmasters) or generate one at [indexnow.org](https://www.indexnow.org/).
|
||||
2. **Verify Setup:**
|
||||
* The key is typically a long random string (e.g., `abc123...`).
|
||||
* Ensure you have a text file named after the key (e.g., `abc123....txt`) containing the key itself inside your `public/` folder so it's accessible at `https://www.qrmaster.net/abc123....txt`.
|
||||
* Alternatively, set the environment variable in your `.env` file:
|
||||
```
|
||||
INDEXNOW_KEY=your_key_here
|
||||
```
|
||||
|
||||
### How to Run
|
||||
|
||||
This script (`scripts/submit-indexnow.ts`) automatically gathers all meaningful URLs from your project (tools, blog posts, main pages) and submits them.
|
||||
|
||||
1. Run the script:
|
||||
```bash
|
||||
npm run submit:indexnow
|
||||
```
|
||||
*(Or manually: `npx tsx scripts/submit-indexnow.ts`)*
|
||||
|
||||
2. It will output which URLs were submitted.
|
||||
|
||||
---
|
||||
|
||||
## Summary Checklist
|
||||
|
||||
- [ ] New page is published and live.
|
||||
- [ ] `service_account.json` is in the project root.
|
||||
- [ ] Service Account email is added as Owner in Google Search Console.
|
||||
- [ ] Run `npm run trigger:indexing` (for Google).
|
||||
- [ ] Run `npm run submit:indexnow` (for Bing/Yandex).
|
||||
BIN
TEMPLATE (Make a Copy) - AI SEO Audit Checklist v2.0 - v2.0.pdf
Normal file
29
ahrefs-findings.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 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.
|
||||
BIN
checklist/uploaded_image_0_1768484835516.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
checklist/uploaded_image_1_1768484835516.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
checklist/uploaded_image_2_1768484835516.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
checklist/uploaded_image_3_1768484835516.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
@@ -1,180 +0,0 @@
|
||||
# Claude Artifact Prompts for Parasite SEO
|
||||
|
||||
**Goal:** Publish educational content on claude.ai that naturally links to qrmaster.net
|
||||
**Strategy:** Informative, helpful content that does NOT look like advertising
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Prompt 1: Restaurant QR Menu Guide
|
||||
|
||||
```
|
||||
Create an informative HTML article titled "Restaurant Menu QR Codes: A Practical Guide for Restaurant Owners (2025)"
|
||||
|
||||
The article should:
|
||||
- Be 1500-2000 words long
|
||||
- Contain practical tips for restaurant owners
|
||||
- Cover the following topics:
|
||||
1. Why digital menus are the new standard
|
||||
2. PDF vs. online menu - pros and cons
|
||||
3. Optimal placement of QR codes in restaurants
|
||||
4. Mistakes restaurants should avoid
|
||||
5. Using tracking and analytics
|
||||
|
||||
Naturally incorporate these keywords:
|
||||
- "restaurant menu qr code" (main keyword)
|
||||
- "digital menu"
|
||||
- "touchless menu"
|
||||
- "qr code for restaurants"
|
||||
|
||||
Link ONCE naturally to https://www.qrmaster.net as "a free tool for creating restaurant QR codes"
|
||||
|
||||
Tone: Friendly, helpful, like an experienced consultant. NOT salesy.
|
||||
|
||||
HTML with clean CSS, mobile-friendly. No external dependencies.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Prompt 2: Dynamic vs Static QR Comparison
|
||||
|
||||
```
|
||||
Create a technical comparison article as HTML: "Static vs Dynamic QR Codes: What's the Difference and When to Use Each"
|
||||
|
||||
Structure:
|
||||
1. Brief explanation of what QR codes are technically
|
||||
2. Static QR codes - how they work
|
||||
3. Dynamic QR codes - how they work
|
||||
4. Comparison table (very important!)
|
||||
5. Decision guide: When to use which type
|
||||
6. Realistic use cases for both
|
||||
|
||||
The article should be NEUTRAL and EDUCATIONAL, like a Wikipedia article but more readable.
|
||||
|
||||
Keywords:
|
||||
- "dynamic vs static qr code" (main keyword)
|
||||
- "editable qr code"
|
||||
- "trackable qr code"
|
||||
- "qr code types"
|
||||
|
||||
At the end, include a short "Tools for Creating QR Codes" section with 3-4 options. One of them is https://www.qrmaster.net - listed as an equal entry, NOT as a recommendation.
|
||||
|
||||
HTML with professional, minimalist design.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Prompt 3: Small Business Marketing Guide
|
||||
|
||||
```
|
||||
Create a comprehensive HTML guide: "10 Ways Small Businesses Can Use QR Codes in 2025"
|
||||
|
||||
The article is aimed at small businesses without technical knowledge.
|
||||
|
||||
The 10 use cases:
|
||||
1. Digital business cards (vCard)
|
||||
2. Collecting Google reviews
|
||||
3. Contactless payments
|
||||
4. Sharing Wi-Fi access
|
||||
5. Growing social media followers
|
||||
6. Linking product information
|
||||
7. Simplifying appointment booking
|
||||
8. Discount promotions & coupons
|
||||
9. Event tickets & check-in
|
||||
10. Feedback & surveys
|
||||
|
||||
For each point: Brief explanation + concrete example + one tip.
|
||||
|
||||
Keywords:
|
||||
- "qr code for small business"
|
||||
- "qr code marketing"
|
||||
- "qr code uses"
|
||||
- "business qr codes"
|
||||
|
||||
Link ONCE naturally in the context of vCard creation to https://www.qrmaster.net/blog/vcard-qr-code-generator
|
||||
|
||||
Tone: Enthusiastic but not over the top. Like a helpful friend explaining technology.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Prompt 4: Print Size Technical Guide
|
||||
|
||||
```
|
||||
Create a technical reference article as HTML: "QR Code Print Size Guide: Minimum Dimensions for Reliable Scanning"
|
||||
|
||||
This article should become THE reference for QR code print sizes.
|
||||
|
||||
Content:
|
||||
1. The science behind QR scanning (brief)
|
||||
2. The golden formula: Size = Distance ÷ 10
|
||||
3. LARGE table with applications, distances, min/recommended sizes
|
||||
4. Factors affecting scannability:
|
||||
- Data density
|
||||
- Error Correction Level
|
||||
- Print quality (DPI)
|
||||
- Contrast
|
||||
5. Quiet zone requirements
|
||||
6. File formats for printing (SVG vs PNG vs PDF)
|
||||
7. Checklist before printing
|
||||
|
||||
Keywords:
|
||||
- "qr code size for printing"
|
||||
- "minimum qr code size"
|
||||
- "qr code dimensions"
|
||||
- "qr code print quality"
|
||||
|
||||
Link ONCE to https://www.qrmaster.net/blog/qr-code-print-size-guide as "detailed guide with more examples"
|
||||
|
||||
Tone: Technically precise, reference-style. For designers and marketers.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Prompt 5: QR Analytics Beginner Guide
|
||||
|
||||
```
|
||||
Create a beginner's guide as HTML: "QR Code Analytics Explained: What You Can Track and Why It Matters"
|
||||
|
||||
The article is aimed at marketing beginners who have never used QR tracking before.
|
||||
|
||||
Structure:
|
||||
1. What is QR tracking and why is it important?
|
||||
2. What data can you track? (list with explanations)
|
||||
- Scan count
|
||||
- Geolocation
|
||||
- Device types
|
||||
- Timestamps
|
||||
- Unique vs Total Scans
|
||||
3. How does it work technically? (simplified)
|
||||
4. Privacy & GDPR considerations
|
||||
5. Practical application: Measuring campaign ROI
|
||||
6. Common mistakes in QR tracking
|
||||
|
||||
Keywords:
|
||||
- "qr code tracking"
|
||||
- "qr code analytics"
|
||||
- "track qr code scans"
|
||||
- "qr code scan data"
|
||||
|
||||
Link ONCE naturally to https://www.qrmaster.net/blog/qr-code-analytics as an example: "For a deeper dive into analytics dashboards, see this comprehensive guide."
|
||||
|
||||
Tone: Friendly and explanatory, like a teacher. No jargon without explanation.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Usage Instructions
|
||||
|
||||
1. **Copy prompt** → Paste into claude.ai
|
||||
2. **Let it create the artifact**
|
||||
3. **Click "Publish"** in Claude
|
||||
4. **Allowed Domain:** Add `www.qrmaster.net, qrmaster.net`
|
||||
5. **Share link** - Google indexes these!
|
||||
|
||||
## 💡 Tips for Maximum Effectiveness
|
||||
|
||||
- **Don't publish all on the same day**
|
||||
- About **1 article per week** for natural growth
|
||||
- Publish the **more neutral articles first** (Prompt 2 & 4)
|
||||
- **Share on social media** for faster indexing
|
||||
- Register the published URLs in Google Search Console
|
||||
89
final_seo_fix_report.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 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. |
|
||||
14
firecrawl-config.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"firecrawl": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"firecrawl-mcp"
|
||||
],
|
||||
"env": {
|
||||
"FIRECRAWL_API_KEY": "fc-268826f038ad4bf0a38c48690ba9c1fa"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/** @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(),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -4,7 +4,20 @@ const nextConfig = {
|
||||
skipTrailingSlashRedirect: true,
|
||||
images: {
|
||||
unoptimized: false,
|
||||
domains: ['www.qrmaster.net', 'qrmaster.net', 'images.qrmaster.net'],
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'www.qrmaster.net',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'qrmaster.net',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.qrmaster.net',
|
||||
},
|
||||
],
|
||||
formats: ['image/webp', 'image/avif'],
|
||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||
@@ -20,6 +33,16 @@ 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;
|
||||
|
||||
68
next_blog_post.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 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.
|
||||
|
||||

|
||||
|
||||
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).*
|
||||
1416
package-lock.json
generated
15
package.json
@@ -6,8 +6,11 @@
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3050",
|
||||
"build": "prisma generate && next build",
|
||||
"trigger:indexing": "tsx scripts/trigger-indexing.ts",
|
||||
"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",
|
||||
@@ -30,34 +33,39 @@
|
||||
"@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",
|
||||
"copy-image-clipboard": "^2.1.2",
|
||||
"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",
|
||||
"googleapis": "^170.1.0",
|
||||
"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",
|
||||
"next-auth": "^4.24.5",
|
||||
"papaparse": "^5.4.1",
|
||||
"posthog-js": "^1.276.0",
|
||||
"posthog-js": "^1.332.0",
|
||||
"qr-code-styling": "^1.9.2",
|
||||
"qrcode": "^1.5.3",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-barcode": "^1.6.1",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"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",
|
||||
@@ -78,6 +86,7 @@
|
||||
"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"
|
||||
@@ -85,4 +94,4 @@
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
@@ -161,4 +161,18 @@ 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])
|
||||
}
|
||||
4
public/.well-known/security.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
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
|
||||
BIN
public/1234567890abcdef.txt
Normal file
BIN
public/barcode-generator-preview.png
Normal file
|
After Width: | Height: | Size: 398 KiB |
1
public/bb6dfaacf1ed41a880281c426c54ed7c.txt
Normal file
@@ -0,0 +1 @@
|
||||
bb6dfaacf1ed41a880281c426c54ed7c
|
||||
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
BIN
public/blog/1-hero.webp
Normal file
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 4.1 MiB |
BIN
public/blog/2-body.webp
Normal file
|
After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 3.8 MiB |
BIN
public/blog/2-hero.webp
Normal file
|
After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 5.5 MiB |
|
Before Width: | Height: | Size: 4.6 MiB |
|
Before Width: | Height: | Size: 5.8 MiB |
|
Before Width: | Height: | Size: 3.7 MiB |
BIN
public/blog/building-qr-generator.png
Normal file
|
After Width: | Height: | Size: 737 KiB |
BIN
public/blog/bulk-qr-events-hero.png
Normal file
|
After Width: | Height: | Size: 804 KiB |
BIN
public/blog/dynamic-vs-static-hero-v2.png
Normal file
|
After Width: | Height: | Size: 860 KiB |
BIN
public/blog/qr-analytics-guide-hero.png
Normal file
|
After Width: | Height: | Size: 630 KiB |
BIN
public/blog/qr-code-analytics-dashboard.png
Normal file
|
After Width: | Height: | Size: 350 KiB |
BIN
public/blog/qr-code-analytics-hero-v2.png
Normal file
|
After Width: | Height: | Size: 863 KiB |
BIN
public/blog/qr-code-analytics-hero.webp
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
public/blog/qr-code-generator-guide-hero.png
Normal file
|
After Width: | Height: | Size: 454 KiB |
BIN
public/blog/qr-code-tracking-guide-body.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
public/blog/qr-code-tracking-guide-hero.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/blog/qr-code-tracking-hero-v2.png
Normal file
|
After Width: | Height: | Size: 646 KiB |
BIN
public/blog/qr-marketing-best-practices-hero.png
Normal file
|
After Width: | Height: | Size: 699 KiB |
BIN
public/blog/static-vs-dynamic-qr-codes-body.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
public/blog/static-vs-dynamic-qr-codes-hero.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
public/blog/sustainable-packaging-qr.png
Normal file
|
After Width: | Height: | Size: 726 KiB |
1
public/googleccd5315437d68a49.html
Normal file
@@ -0,0 +1 @@
|
||||
google-site-verification: googleccd5315437d68a49.html
|
||||
13
public/humans.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
/* 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
|
||||
48
public/llms.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
# 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
|
||||
BIN
public/og-image.png
Normal file
|
After Width: | Height: | Size: 464 KiB |
@@ -1,19 +0,0 @@
|
||||
# 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/
|
||||
@@ -1,33 +0,0 @@
|
||||
<?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>
|
||||
49
scripts/compress-images.js
Normal file
@@ -0,0 +1,49 @@
|
||||
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();
|
||||
23
scripts/submit-indexnow.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Helper script to run IndexNow submission
|
||||
// Run with: npm run submit:indexnow
|
||||
|
||||
import { getAllIndexableUrls, submitToIndexNow } from '../src/lib/indexnow';
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 Starting IndexNow Submission Script...');
|
||||
|
||||
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.');
|
||||
console.warn(' The submission might fail if the key is not hardcoded in src/lib/indexnow.ts');
|
||||
}
|
||||
|
||||
await submitToIndexNow(urls);
|
||||
console.log('\n✨ IndexNow submission process completed.');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
64
scripts/test-db-lead.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
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();
|
||||
81
scripts/trigger-indexing.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
|
||||
import { google } from 'googleapis';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getAllIndexableUrls } from '../src/lib/indexnow';
|
||||
|
||||
// ==========================================
|
||||
// CONFIGURATION
|
||||
// ==========================================
|
||||
|
||||
// Path to your Service Account Key (JSON file)
|
||||
const KEY_FILE = path.join(__dirname, '../service_account.json');
|
||||
|
||||
// Urls are now fetched dynamically from src/lib/indexnow.ts
|
||||
// ==========================================
|
||||
|
||||
async function runUsingServiceAccount() {
|
||||
console.log('🚀 Starting Google Indexing Script (All Pages)...');
|
||||
|
||||
if (!fs.existsSync(KEY_FILE)) {
|
||||
console.error('\n❌ ERROR: Service Account Key not found!');
|
||||
console.error(` Expected path: ${KEY_FILE}`);
|
||||
console.error(' Please follow the instructions in INDEXING_GUIDE.md to create and save the key.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🔑 Authenticating with key file: ${path.basename(KEY_FILE)}...`);
|
||||
|
||||
const auth = new google.auth.GoogleAuth({
|
||||
keyFile: KEY_FILE,
|
||||
scopes: ['https://www.googleapis.com/auth/indexing'],
|
||||
});
|
||||
|
||||
try {
|
||||
const client = await auth.getClient();
|
||||
console.log('✅ Authentication successful.');
|
||||
|
||||
console.log(' Gathering URLs to index...');
|
||||
const allUrls = getAllIndexableUrls();
|
||||
console.log(` Found ${allUrls.length} URLs to index.`);
|
||||
|
||||
for (const url of allUrls) {
|
||||
console.log(`\n📄 Processing: ${url}`);
|
||||
|
||||
try {
|
||||
const result = await google.indexing('v3').urlNotifications.publish({
|
||||
auth: client,
|
||||
requestBody: {
|
||||
url: url,
|
||||
type: 'URL_UPDATED'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(` 👉 Status: ${result.status} ${result.statusText}`);
|
||||
// Optional: Log more details from result.data if needed
|
||||
|
||||
} catch (innerError: any) {
|
||||
console.error(` ❌ Failed to index ${url}`);
|
||||
if (innerError.response) {
|
||||
console.error(` Reason: ${innerError.response.status} - ${JSON.stringify(innerError.response.data)}`);
|
||||
// 429 = Quota exceeded
|
||||
// 403 = Permission denied (check service account owner status)
|
||||
} else {
|
||||
console.error(` Reason: ${innerError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: Add a small delay to avoid hitting rate limits too fast if you have hundreds of URLs
|
||||
// await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
console.log('\n✨ Done! All requests processed.');
|
||||
console.log(' Note: Check Google Search Console for actual indexing status over time.');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('\n❌ Fatal error occurred:');
|
||||
console.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
runUsingServiceAccount();
|
||||
34
scripts/verify-lead-db.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
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();
|
||||
742
searchvolume.md
Normal file
@@ -0,0 +1,742 @@
|
||||
Overview: qr code generator
|
||||
View Cached Page
|
||||
|
||||
Create Report
|
||||
370,000
|
||||
Monthly Volume
|
||||
337,000
|
||||
Estimated Clicks
|
||||
Clicked any result
|
||||
Low
|
||||
91%
|
||||
High
|
||||
Mobile vs Desktop
|
||||
Mobile
|
||||
Desktop
|
||||
Not Enough Data
|
||||
Paid clicks
|
||||
Low
|
||||
0‑3%
|
||||
12%
|
||||
High
|
||||
13%+
|
||||
Difficulty
|
||||
73
|
||||
Google Provided Data
|
||||
|
||||
Expand
|
||||
Cost Per Click
|
||||
$0.51
|
||||
Monthly Cost
|
||||
$3,072
|
||||
Search Volume
|
||||
90,500
|
||||
Advertisers
|
||||
15
|
||||
Homepages
|
||||
6
|
||||
Fresh SV
|
||||
918,000
|
||||
Universal search in SERP
|
||||
8,191
|
||||
Similar keywords
|
||||
qr code generator
|
||||
370,000
|
||||
qr code generator free
|
||||
43,300
|
||||
free qr code generator
|
||||
34,400
|
||||
generate qr code
|
||||
10,800
|
||||
google qr code generator
|
||||
8,000
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
655
|
||||
Questions
|
||||
How to generate a qr code
|
||||
1,700
|
||||
How to generate qr code
|
||||
630
|
||||
How to generate qr code for url
|
||||
270
|
||||
What is the best qr code generator?
|
||||
220
|
||||
How to generate bank qr code without edge
|
||||
200
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
19,120,108
|
||||
Also ranks for
|
||||
qr code generator free
|
||||
43,300
|
||||
qr code maker
|
||||
52,000
|
||||
free qr code generator
|
||||
34,400
|
||||
qr generator
|
||||
25,300
|
||||
create qr code
|
||||
29,500
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Overview: barcode generator
|
||||
View Cached Page
|
||||
|
||||
Create Report
|
||||
58,300
|
||||
Monthly Volume
|
||||
51,000
|
||||
Estimated Clicks
|
||||
Clicked any result
|
||||
Low
|
||||
87%
|
||||
High
|
||||
Mobile vs Desktop
|
||||
Mobile
|
||||
Desktop
|
||||
Low Mobile
|
||||
Paid clicks
|
||||
Low
|
||||
0‑3%
|
||||
1%
|
||||
High
|
||||
13%+
|
||||
Difficulty
|
||||
22
|
||||
Google Provided Data
|
||||
|
||||
Expand
|
||||
Cost Per Click
|
||||
$1.68
|
||||
Monthly Cost
|
||||
$5,316
|
||||
Search Volume
|
||||
110,000
|
||||
Advertisers
|
||||
5
|
||||
Homepages
|
||||
21
|
||||
Fresh SV
|
||||
72,800
|
||||
Universal search in SERP
|
||||
5,381
|
||||
Similar keywords
|
||||
barcode generator
|
||||
58,300
|
||||
free barcode generator
|
||||
4,000
|
||||
upc barcode generator
|
||||
3,200
|
||||
2d barcode generator
|
||||
1,300
|
||||
generate barcode
|
||||
1,300
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
326
|
||||
Questions
|
||||
How to store barcodes generated into a folder in linux python
|
||||
250
|
||||
How to generate barcodes
|
||||
180
|
||||
How to generate barcodes in excel
|
||||
180
|
||||
How to generate barcodes for products
|
||||
135
|
||||
How to generate a third party barcode for j1 waiver
|
||||
110
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
27,933,196
|
||||
Also ranks for
|
||||
free barcode generator
|
||||
4,000
|
||||
barcode maker
|
||||
6,100
|
||||
upc code generator
|
||||
3,600
|
||||
upc generator
|
||||
4,500
|
||||
2d barcode generator
|
||||
1,300
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Overview: qr code maker
|
||||
View Cached Page
|
||||
|
||||
Create Report
|
||||
52,000
|
||||
Monthly Volume
|
||||
48,200
|
||||
Estimated Clicks
|
||||
Clicked any result
|
||||
Low
|
||||
93%
|
||||
High
|
||||
Mobile vs Desktop
|
||||
Mobile
|
||||
Desktop
|
||||
Not Enough Data
|
||||
Paid clicks
|
||||
Low
|
||||
0‑3%
|
||||
12%
|
||||
High
|
||||
13%+
|
||||
Difficulty
|
||||
47
|
||||
Google Provided Data
|
||||
|
||||
Expand
|
||||
Cost Per Click
|
||||
$0.37
|
||||
Monthly Cost
|
||||
$209
|
||||
Search Volume
|
||||
18,100
|
||||
Advertisers
|
||||
11
|
||||
Homepages
|
||||
32
|
||||
Fresh SV
|
||||
71,300
|
||||
Universal search in SERP
|
||||
601
|
||||
Similar keywords
|
||||
qr code maker
|
||||
52,000
|
||||
animal crossing qr code maker
|
||||
2,000
|
||||
free qr code maker
|
||||
2,000
|
||||
qr code maker free
|
||||
1,900
|
||||
mini qr code maker
|
||||
380
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
13
|
||||
Questions
|
||||
How to maker qr code for cia
|
||||
70
|
||||
How to make qr codes with brother label maker
|
||||
70
|
||||
How to make a qr code qr code maker
|
||||
50
|
||||
How to post qr codes online mii maker
|
||||
40
|
||||
How to get qr code watch maker
|
||||
28
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
84,638,180
|
||||
Also ranks for
|
||||
qr code generator free
|
||||
43,300
|
||||
free qr code generator
|
||||
34,400
|
||||
create a qr code
|
||||
17,100
|
||||
create qr code
|
||||
29,500
|
||||
qr generator
|
||||
25,300
|
||||
|
||||
|
||||
|
||||
|
||||
Overview: google qr code generator
|
||||
View Cached Page
|
||||
|
||||
Create Report
|
||||
8,000
|
||||
Monthly Volume
|
||||
5,100
|
||||
Estimated Clicks
|
||||
Clicked any result
|
||||
Low
|
||||
64%
|
||||
High
|
||||
Mobile vs Desktop
|
||||
Mobile
|
||||
Desktop
|
||||
Low Mobile
|
||||
Paid clicks
|
||||
Low
|
||||
0‑3%
|
||||
2%
|
||||
High
|
||||
13%+
|
||||
Difficulty
|
||||
52
|
||||
Google Provided Data
|
||||
|
||||
Expand
|
||||
Cost Per Click
|
||||
$3.53
|
||||
Monthly Cost
|
||||
$0.00
|
||||
Search Volume
|
||||
2,900
|
||||
Advertisers
|
||||
9
|
||||
Homepages
|
||||
20
|
||||
Fresh SV
|
||||
11,500
|
||||
Universal search in SERP
|
||||
336
|
||||
Similar keywords
|
||||
google qr code generator
|
||||
8,000
|
||||
qr code generator google
|
||||
4,800
|
||||
free qr code generator google
|
||||
720
|
||||
qr code generator google form
|
||||
440
|
||||
google form qr code generator
|
||||
320
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
59
|
||||
Questions
|
||||
Does google have a qr code generator?
|
||||
135
|
||||
How to generate qr code for google authenticator
|
||||
100
|
||||
Does google have a qr code generator for contact info?
|
||||
90
|
||||
How to generate qr code for google form
|
||||
90
|
||||
How to generate a qr code for a google form
|
||||
90
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
27,918,916
|
||||
Also ranks for
|
||||
qr code generator free
|
||||
43,300
|
||||
qr code maker
|
||||
52,000
|
||||
create qr code
|
||||
29,500
|
||||
qr generator
|
||||
25,300
|
||||
free qr code generator
|
||||
34,400
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Overview: create qr code
|
||||
View Cached Page
|
||||
|
||||
Create Report
|
||||
29,500
|
||||
Monthly Volume
|
||||
26,400
|
||||
Estimated Clicks
|
||||
Clicked any result
|
||||
Low
|
||||
89%
|
||||
High
|
||||
Mobile vs Desktop
|
||||
Mobile
|
||||
Desktop
|
||||
Not Enough Data
|
||||
Paid clicks
|
||||
Low
|
||||
0‑3%
|
||||
16%
|
||||
High
|
||||
13%+
|
||||
Difficulty
|
||||
52
|
||||
Google Provided Data
|
||||
|
||||
Expand
|
||||
Cost Per Click
|
||||
$3.32
|
||||
Monthly Cost
|
||||
$1,406
|
||||
Search Volume
|
||||
14,800
|
||||
Advertisers
|
||||
15
|
||||
Homepages
|
||||
25
|
||||
Fresh SV
|
||||
50,000
|
||||
Universal search in SERP
|
||||
3,223
|
||||
Similar keywords
|
||||
create qr code
|
||||
29,500
|
||||
create a qr code
|
||||
17,100
|
||||
How to create a qr code
|
||||
9,200
|
||||
create qr code free
|
||||
5,500
|
||||
How to create a qr code free
|
||||
1,400
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
1,110
|
||||
Questions
|
||||
How to create a qr code
|
||||
9,200
|
||||
How to create a qr code free
|
||||
1,400
|
||||
How to create qr codes
|
||||
1,300
|
||||
How to create qr code
|
||||
1,300
|
||||
How to create a qr code for a google form
|
||||
1,100
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
18,733,729
|
||||
Also ranks for
|
||||
qr code generator free
|
||||
43,300
|
||||
qr code maker
|
||||
52,000
|
||||
create a qr code
|
||||
17,100
|
||||
free qr code generator
|
||||
34,400
|
||||
qr generator
|
||||
25,300
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Overview: qr code with logo
|
||||
View Cached Page
|
||||
|
||||
Create Report
|
||||
1,600
|
||||
Monthly Volume
|
||||
1,300
|
||||
Estimated Clicks
|
||||
Clicked any result
|
||||
Low
|
||||
81%
|
||||
High
|
||||
Mobile vs Desktop
|
||||
Mobile
|
||||
Desktop
|
||||
Low Mobile
|
||||
Paid clicks
|
||||
Low
|
||||
0‑3%
|
||||
8%
|
||||
High
|
||||
13%+
|
||||
Difficulty
|
||||
48
|
||||
Google Provided Data
|
||||
|
||||
Expand
|
||||
Cost Per Click
|
||||
$0.00
|
||||
Monthly Cost
|
||||
$0.00
|
||||
Search Volume
|
||||
-
|
||||
Advertisers
|
||||
1
|
||||
Homepages
|
||||
25
|
||||
Fresh SV
|
||||
2,900
|
||||
Universal search in SERP
|
||||
291
|
||||
Similar keywords
|
||||
qr code generator with logo
|
||||
4,100
|
||||
qr code with logo
|
||||
1,600
|
||||
create qr code with logo
|
||||
440
|
||||
qr code generator with logo free
|
||||
400
|
||||
android studio qr code generator with logo
|
||||
300
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
17
|
||||
Questions
|
||||
How to make qr code with logo
|
||||
40
|
||||
How to design qr code with logo
|
||||
40
|
||||
How to create qr code with logo
|
||||
28
|
||||
How to make own qr code with logo
|
||||
24
|
||||
How to create your own qr code with logo
|
||||
24
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
37,452,720
|
||||
Also ranks for
|
||||
qr code maker
|
||||
52,000
|
||||
qr code generator free
|
||||
43,300
|
||||
create qr code
|
||||
29,500
|
||||
free qr code generator
|
||||
34,400
|
||||
create a qr code
|
||||
17,100
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Overview: spotify code generator
|
||||
View Cached Page
|
||||
|
||||
Create Report
|
||||
840
|
||||
Monthly Volume
|
||||
630
|
||||
Estimated Clicks
|
||||
Clicked any result
|
||||
Low
|
||||
76%
|
||||
High
|
||||
Mobile vs Desktop
|
||||
Mobile
|
||||
Desktop
|
||||
Not Enough Data
|
||||
Paid clicks
|
||||
Not Enough Data
|
||||
Difficulty
|
||||
21
|
||||
Google Provided Data
|
||||
|
||||
Expand
|
||||
Cost Per Click
|
||||
$0.00
|
||||
Monthly Cost
|
||||
$0.00
|
||||
Search Volume
|
||||
90
|
||||
Advertisers
|
||||
0
|
||||
Homepages
|
||||
5
|
||||
Fresh SV
|
||||
2,400
|
||||
Universal search in SERP
|
||||
106
|
||||
Similar keywords
|
||||
spotify code generator
|
||||
840
|
||||
spotify premium code generator no survey
|
||||
420
|
||||
spotify premium codes generator
|
||||
300
|
||||
spotify premium code generator no survey 2017
|
||||
290
|
||||
spotify code generator 2019
|
||||
290
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
5
|
||||
Questions
|
||||
How to generate spotify code
|
||||
90
|
||||
How to get spotify premium code free generator 2018
|
||||
70
|
||||
How to get code for spotify premium spotify premium free code generator
|
||||
24
|
||||
Where is spotify pin code generator?
|
||||
12
|
||||
How to generate a spotify code
|
||||
-
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
Log in to see all results
|
||||
--
|
||||
View All
|
||||
82,799,750
|
||||
Also ranks for
|
||||
qr code maker
|
||||
52,000
|
||||
spotify codes
|
||||
7,100
|
||||
spotify code
|
||||
5,700
|
||||
qrcode
|
||||
11,900
|
||||
create qr code
|
||||
29,500
|
||||
506
seo-strategy.md
Normal file
@@ -0,0 +1,506 @@
|
||||
## A) Executive summary (max 12 bullets)
|
||||
|
||||
* **Win fast (0–60 days)** by launching a *“wedge” set* of low-KD, high-intent tool pages (WhatsApp / Instagram / vCard / Bulk / PDF) + one differentiated feature hub (**QR Code Analytics + Tracking**) that every tool page upsells into.
|
||||
* **Build an intent ladder**: *Free generator → Dynamic QR → Tracking/Analytics → Bulk/API/Teams → Custom domains + integrations* (this mirrors how category leaders gate value). ([qr-code-generator.com][1])
|
||||
* **Exploit SERP splits**: head terms (“qr code generator”) are crowded with generalist tools (Canva/Adobe) + legacy generators, while **dynamic/tracking** queries skew toward SaaS platforms—your product sweet spot. ([qr-code-generator.com][1])
|
||||
* **Turn “Google QR Code Generator” into a capture page**: Google/Chrome already generates a basic QR for a URL; your angle is *“Chrome is static-only → here’s dynamic + analytics + UTM + campaign dashboards.”* ([Google Hilfe][2])
|
||||
* **Programmatic SEO (pSEO) is mandatory** in this space: competitors scale with templated “solutions” pages by QR type (vCard, WiFi, Spotify, Instagram, etc.). ([qr-code-generator.com][3])
|
||||
* **Avoid pSEO index bloat** with strict canonical + noindex rules and *minimum content thresholds* per template (examples below).
|
||||
* **Differentiate on trust**: QR scams (“quishing”) are rising; bake “safe redirect + link preview + scan security” into product messaging and content. ([Der Guardian][4])
|
||||
* **Make “Barcode Generator” a top-of-funnel traffic engine** (58k SV / KD 22 in your data) but route conversions toward QR analytics + dynamic capabilities; barcode SERPs are full of embed-only utilities and hardware vendors. ([Free Online Barcode Generator by TEC-IT][5])
|
||||
* **Ship IA early**: a scalable sitemap with `/tools/`, `/features/`, `/integrations/`, `/compare/`, `/learn/`, and `/templates/` prevents cannibalization and makes internal linking deterministic.
|
||||
* **Measure leading indicators**: indexation coverage, impressions, tool-page CVR to signup, activation (QR created), and upgrades (dynamic/tracking enabled).
|
||||
* **Link acquisition**: win with embed widgets, UTM/GA4 tracking guides, open-source SDKs, and directory placements (10 angles below).
|
||||
* **Assumptions used** (adjustable): **EN**, **Global/US focus**, **Freemium SaaS → subscription**, primary conversion **signup → generate → enable tracking**.
|
||||
|
||||
---
|
||||
|
||||
## B) Competitor landscape (top competitors + what they do best + weaknesses)
|
||||
|
||||
Below is a **SERP-driven** view of recurring domains across “QR code generator”, “dynamic QR”, “tracking/analytics”, and “type” queries (vCard/Instagram/Spotify/etc.):
|
||||
|
||||
### 1) QR Code Generator (Bitly) — `qr-code-generator.com`
|
||||
|
||||
**Best at**
|
||||
|
||||
* Clear **feature ladder + gating** (static free → dynamic/analytics → bulk/API/teams). ([qr-code-generator.com][1])
|
||||
* Massive **“solutions” library** (SEO scale by QR type). ([qr-code-generator.com][3])
|
||||
**Weaknesses to exploit**
|
||||
* Heavy gating/upsell can frustrate “free” intent.
|
||||
* Many “solution” pages trend toward **marketing copy**—opportunity for deeper “how-to + templates + examples + tracking instrumentation”.
|
||||
|
||||
### 2) QRCode Monkey — `qrcode-monkey.com`
|
||||
|
||||
**Best at**
|
||||
|
||||
* “Free + design/customization” positioning; vectors/print talk resonates. ([QRCode Monkey][6])
|
||||
* Has an **API pitch** (some scaling). ([QRCode Monkey][7])
|
||||
**Weaknesses**
|
||||
* Less credible on analytics-first workflows; your advantage is *campaign measurement + dashboards*.
|
||||
|
||||
### 3) The QR Code Generator (TQRCG) — `the-qrcode-generator.com`
|
||||
|
||||
**Best at**
|
||||
|
||||
* Trust messaging: “free means free” + warns about expiring codes. ([the-qrcode-generator.com][8])
|
||||
**Weaknesses**
|
||||
* Content often “how-to guide” oriented; you can outrank with **better tools + richer templates + integrations**.
|
||||
|
||||
### 4) Hovercode — `hovercode.com`
|
||||
|
||||
**Best at**
|
||||
|
||||
* Product-led pages (“create now”) + “trackable QR codes” positioning. ([Hovercode][9])
|
||||
* pSEO via many generator variants (logo, circle, etc.). ([Hovercode][10])
|
||||
**Weaknesses**
|
||||
* Opportunity to beat them with **comparison pages + GA4 instrumentation + bulk workflows**.
|
||||
|
||||
### 5) Scanova — `scanova.io`
|
||||
|
||||
**Best at**
|
||||
|
||||
* Strong **feature pages**: dynamic, tracking, security, landing pages (good enterprise pitch). ([Scanova][11])
|
||||
**Weaknesses**
|
||||
* Many blogs are long; you can win snippets with **structured templates + FAQs + exact steps + schema**.
|
||||
|
||||
### 6) Flowcode — `flowcode.com`
|
||||
|
||||
**Best at**
|
||||
|
||||
* Owns “offline conversions + analytics” narrative (enterprise). ([flowcode.com][12])
|
||||
**Weaknesses**
|
||||
* Often skewed to demos; you can capture SMB/free intent and upgrade later.
|
||||
|
||||
### 7) QRCodeChimp — `qrcodechimp.com`
|
||||
|
||||
**Best at**
|
||||
|
||||
* Huge template catalog (menus, forms, cards, etc.) + GA integration content. ([QR Code Chimp][13])
|
||||
**Weaknesses**
|
||||
* Template sprawl risks thin pages—beat them on **quality thresholds + tighter topical clusters**.
|
||||
|
||||
### 8) ME-QR — `me-qr.com`
|
||||
|
||||
**Best at**
|
||||
|
||||
* Aggressive pSEO for types (PDF/Instagram/WhatsApp/Spotify). ([me-qr.com][14])
|
||||
**Weaknesses**
|
||||
* Many pages feel commodity; you can differentiate with **better UX + security + analytics clarity**.
|
||||
|
||||
### 9) Canva / Adobe Express (generalists)
|
||||
|
||||
* Canva and Adobe rank on “free QR code generator” intent via ecosystem pull. ([Canva][15])
|
||||
**Your play**: don’t “out-brand” them—**out-specialize** on dynamic/tracking/bulk/API and win long-tail + mid-tail.
|
||||
|
||||
### 10) Barcode generators (for your “Barcode Generator” gold mine)
|
||||
|
||||
* TEC-IT (embed + backlink requirement) and Barcodes Inc (hardware upsell). ([Free Online Barcode Generator by TEC-IT][5])
|
||||
**Your play**: best-in-class UX + formats + bulk + API docs + “barcode vs QR” education to route users into QR analytics.
|
||||
|
||||
---
|
||||
|
||||
## C) Keyword clusters + priority order (explain why)
|
||||
|
||||
### Intent model (how to cluster)
|
||||
|
||||
* **Do / Generate (tool intent)**: “X QR code generator”, “bulk”, “PDF to QR”, “WiFi QR”, “Instagram QR”, “WhatsApp QR”.
|
||||
* **Decide (commercial investigation)**: “dynamic vs static”, “trackable QR codes”, “best QR code generator”, “QR code analytics”.
|
||||
* **Implement (technical)**: “QR code API”, “track QR codes in GA4”, “UTM QR code”, “bulk QR from CSV / Sheets”.
|
||||
* **Navigate (platform-native)**: “Google QR code generator”, “Spotify code generator”, “Instagram QR code”.
|
||||
|
||||
### Priority ladder (P0 → P2)
|
||||
|
||||
**P0 (launch first; fastest to rank + high upsell value)**
|
||||
|
||||
1. **WhatsApp QR Code Generator** (SV 180 / KD 17 in your list) → high intent + low KD + SMB conversion path.
|
||||
2. **Instagram QR Code Generator** (SV 440 / KD 23) → same logic + add “IG has native QR; here’s branded + tracked campaigns”. ([Instagram Hilfe][16])
|
||||
3. **vCard QR Code Generator** (SV 180 / KD 24) → business use case; great signup driver.
|
||||
4. **QR Code Analytics** (SV 135 / KD 24) → *your core differentiator*; becomes the internal-link destination from every tool page.
|
||||
5. **Trackable QR Codes** (SV 135 / KD 0) → perfect wedge term; map to a commercial page that demonstrates tracking dashboard and “dynamic”.
|
||||
6. **Barcode Generator** (58k / KD 22) → big traffic engine; route to QR features + analytics.
|
||||
|
||||
**P1 (build authority + revenue features)**
|
||||
|
||||
* **Bulk QR Code Generator** (SV 360 / KD 33)
|
||||
* **QR Code Tracking** (SV 320 / KD 37) (map carefully vs “analytics”)
|
||||
* **WiFi QR Code Generator** (SV 1,400 / KD 34)
|
||||
* **PDF to QR Code Generator** (SV ~260 / KD 36, CPC high)
|
||||
* **Google QR Code Generator** (SV 8k) (capture via “Chrome static QR” + upsell). ([Google Hilfe][2])
|
||||
|
||||
**P2 (long-term mid/high competition)**
|
||||
|
||||
* **Dynamic QR Code Generator** (SV 1,200 / KD 43)
|
||||
* **Free QR Code Generator** (SV 34,400 / KD 34)
|
||||
* **QR code maker** (SV 52k / KD 47)
|
||||
* **QR Code Generator** (SV 370k) — pillar target supported by everything above.
|
||||
|
||||
### Cannibalization rule (critical)
|
||||
|
||||
* **One primary intent per page.** Example mapping:
|
||||
|
||||
* `/features/qr-code-analytics/` = “qr code analytics” (feature/commercial)
|
||||
* `/learn/qr-code-tracking/` = “qr code tracking” (educational/how it works + GA4)
|
||||
* `/tools/trackable-qr-code-generator/` = “trackable qr codes” (tool + demo dashboard)
|
||||
|
||||
---
|
||||
|
||||
## D) Recommended sitemap / IA (with URL examples)
|
||||
|
||||
### Core structure (scalable + pSEO-safe)
|
||||
|
||||
**1) Tools (transactional)**
|
||||
|
||||
* `/qr-code-generator/` (core tool hub, not a blog post)
|
||||
* `/tools/vcard-qr-code-generator/`
|
||||
* `/tools/whatsapp-qr-code-generator/`
|
||||
* `/tools/instagram-qr-code-generator/`
|
||||
* `/tools/wifi-qr-code-generator/`
|
||||
* `/tools/pdf-to-qr-code-generator/`
|
||||
* `/tools/bulk-qr-code-generator/`
|
||||
* `/barcode-generator/` (separate category; include QR/2D + 1D)
|
||||
|
||||
**2) Features (commercial)**
|
||||
|
||||
* `/features/dynamic-qr-codes/`
|
||||
* `/features/qr-code-analytics/`
|
||||
* `/features/qr-code-campaigns/` (folders, tags, exports)
|
||||
* `/features/custom-domain/`
|
||||
* `/features/teams-roles/`
|
||||
* `/features/security-anti-phishing/` (trust wedge; see “quishing”). ([Der Guardian][4])
|
||||
|
||||
**3) Integrations (high-intent + linkable)**
|
||||
|
||||
* `/integrations/google-analytics-4/`
|
||||
* `/integrations/hubspot/`
|
||||
* `/integrations/zapier/`
|
||||
* `/integrations/shopify/`
|
||||
(Ship GA4 first; it supports your “tracking” narrative.)
|
||||
|
||||
**4) Learn Hub (educational; supports rankings + conversions)**
|
||||
|
||||
* `/learn/dynamic-vs-static-qr-codes/`
|
||||
* `/learn/how-to-track-qr-codes-in-ga4/`
|
||||
* `/learn/qr-code-size-guide/`
|
||||
* `/learn/qr-code-error-correction/`
|
||||
* `/learn/google-qr-code-generator/` (Chrome’s built-in QR + limitations). ([Google Hilfe][2])
|
||||
* `/learn/spotify-code-generator/` (Spotify Codes explainer + CTA to your tool). ([SpotifyCodes][17])
|
||||
|
||||
**5) Templates / Use cases (pSEO with guardrails)**
|
||||
|
||||
* `/templates/restaurant-menu-qr/`
|
||||
* `/templates/business-card-qr/`
|
||||
* `/templates/event-check-in-qr/`
|
||||
Each template must include: examples, copy/paste CTAs, recommended QR type, tracking setup, and links to the tool.
|
||||
|
||||
### Breadcrumb + internal linking rules (hub-and-spoke)
|
||||
|
||||
* **Tool pages** link up to:
|
||||
|
||||
* `/features/qr-code-analytics/`
|
||||
* `/features/dynamic-qr-codes/`
|
||||
* `/learn/dynamic-vs-static-qr-codes/`
|
||||
* the **closest** templates + GA4 integration (where relevant)
|
||||
* **Learn pages** link down to:
|
||||
|
||||
* the *single best-matching tool page* (primary CTA)
|
||||
* 2–4 related learn pages (cluster reinforcement)
|
||||
* **Integrations** link to:
|
||||
|
||||
* analytics feature + tracking learn guide + relevant tool pages
|
||||
|
||||
---
|
||||
|
||||
## E) “Wedge” plan: what to launch first to rank within 30–60 days
|
||||
|
||||
### Launch set (minimum viable topical authority)
|
||||
|
||||
**Week 1–3 shipping goal: 8 pages that create a ranking flywheel**
|
||||
|
||||
**Tool pages (P0)**
|
||||
|
||||
1. `/tools/whatsapp-qr-code-generator/` (KD 17)
|
||||
2. `/tools/instagram-qr-code-generator/` (KD 23)
|
||||
3. `/tools/vcard-qr-code-generator/` (KD 24)
|
||||
4. `/tools/trackable-qr-code-generator/` (KD 0 term → commercial wedge)
|
||||
5. `/barcode-generator/` (traffic engine)
|
||||
|
||||
**Feature + Learn pages (conversion + trust)**
|
||||
6) `/features/qr-code-analytics/` (your core differentiator)
|
||||
7) `/learn/dynamic-vs-static-qr-codes/` (decision content)
|
||||
8) `/learn/google-qr-code-generator/` (steal “Google/Chrome” demand; Chrome is static URL sharing). ([Google Hilfe][2])
|
||||
|
||||
### Why this ranks fast on a new domain
|
||||
|
||||
* Low-KD type terms are less “brand dominated” than head terms.
|
||||
* Every tool page naturally links to analytics + dynamic, so **internal PageRank concentrates** on your money features.
|
||||
* “Google QR code generator” content can win featured snippets because it’s step-based and grounded in official Chrome documentation. ([Google Hilfe][2])
|
||||
|
||||
---
|
||||
|
||||
## F) 90-day execution roadmap (week-by-week)
|
||||
|
||||
### Weeks 1–2: Foundations (technical + tracking + SEO hygiene)
|
||||
|
||||
* **Tech SEO**
|
||||
|
||||
* Set up GSC + GA4 (or PostHog) + server-side event pipeline for “QR created / downloaded / scan events”.
|
||||
* Define **indexation policy**: which templates get indexed, which are noindex.
|
||||
* Implement: XML sitemaps by type (`/sitemap-tools.xml`, `/sitemap-learn.xml`), robots, canonicals, hreflang plan (even if EN-only now).
|
||||
* **Schema baseline**
|
||||
|
||||
* Organization, WebSite, BreadcrumbList sitewide.
|
||||
* SoftwareApplication/WebApplication on core tool hub.
|
||||
* **Information architecture**
|
||||
|
||||
* Ship nav for Tools / Features / Learn / Pricing / API.
|
||||
|
||||
### Week 3: Ship the wedge tool pages (P0)
|
||||
|
||||
* Publish WhatsApp / Instagram / vCard / Trackable tool pages.
|
||||
* Each ships with: FAQ, examples, “Static vs Dynamic” block, “Enable analytics” CTA, and internal links to `/features/qr-code-analytics/`.
|
||||
|
||||
### Week 4: Ship the analytics feature hub + dynamic feature hub
|
||||
|
||||
* `/features/qr-code-analytics/` + `/features/dynamic-qr-codes/`
|
||||
* Add product screenshots/GIFs and a simple “How tracking works” diagram (dynamic redirect → logging → dashboard).
|
||||
|
||||
### Week 5: Learn cluster for decision + “Google QR”
|
||||
|
||||
* `/learn/dynamic-vs-static-qr-codes/`
|
||||
* `/learn/google-qr-code-generator/` (include “Chrome creates QR for a page” and limitations). ([Google Hilfe][2])
|
||||
|
||||
### Week 6: Barcode Generator tool + “Barcode vs QR” guide
|
||||
|
||||
* Launch `/barcode-generator/` + `/learn/barcode-vs-qr-code/` to route barcode traffic into QR use cases.
|
||||
* Add bulk export formats and “print quality” section to compete with incumbents. ([Free Online Barcode Generator by TEC-IT][5])
|
||||
|
||||
### Week 7: Bulk + PDF tools (P1)
|
||||
|
||||
* `/tools/bulk-qr-code-generator/` (CSV upload; align with SERP expectations like “download ZIP”). ([quickchart.io][18])
|
||||
* `/tools/pdf-to-qr-code-generator/` (CPC-heavy query → strong conversion)
|
||||
|
||||
### Week 8: GA4 integration page (linkable asset)
|
||||
|
||||
* `/integrations/google-analytics-4/`
|
||||
* Companion guide: `/learn/how-to-track-qr-codes-in-ga4/` (UTMs, events, attribution).
|
||||
|
||||
### Week 9: Authority pieces (start the pillar support)
|
||||
|
||||
Publish 2 of these 5 (see section below):
|
||||
|
||||
* “QR Code Size Guide”
|
||||
* “QR Code Error Correction Explained”
|
||||
* “UTM Builder for QR Campaigns”
|
||||
* “QR Code Security / Quishing Prevention”
|
||||
* “QR Code Analytics Benchmarks”
|
||||
|
||||
### Week 10: pSEO expansion (controlled)
|
||||
|
||||
* Add 10–20 additional `/tools/{type}/` pages (WiFi, email, SMS, etc.) only if they meet your thin-content threshold.
|
||||
* Add 10–20 `/templates/` pages tied to real use cases.
|
||||
|
||||
### Week 11: Comparisons (conversion-focused)
|
||||
|
||||
* `/compare/qr-code-generator-vs-canva/`
|
||||
* `/compare/qr-code-generator-vs-qrcode-monkey/`
|
||||
* `/compare/dynamic-qr-code-generators/` (listicle with your wedge terms)
|
||||
|
||||
### Week 12–13: Iterate based on GSC data
|
||||
|
||||
* Optimize pages with impressions but low CTR (titles/meta).
|
||||
* Expand FAQs to match PAA.
|
||||
* Strengthen internal links from high-impression pages to money pages.
|
||||
|
||||
---
|
||||
|
||||
## G) Page briefs for the top 5 money pages (H1, sections, schema, CTA, internal links)
|
||||
|
||||
### 1) Dynamic QR Code Generator
|
||||
|
||||
**URL:** `/features/dynamic-qr-codes/` (feature) + optional `/tools/dynamic-qr-code-generator/` (tool demo)
|
||||
**Primary keyword:** dynamic qr code generator
|
||||
**H1:** Dynamic QR Code Generator (Editable + Trackable)
|
||||
**Sections (order matters)**
|
||||
|
||||
* What is a dynamic QR code? (vs static)
|
||||
* Edit destination after printing (URL, file, page)
|
||||
* Tracking/analytics overview (scans, time, location, device)
|
||||
* Use cases (menus, flyers, events, packaging)
|
||||
* How it works (redirect + logging)
|
||||
* Pricing preview + free tier
|
||||
* FAQ (Do they expire? Can I change the URL? Can I export data?)
|
||||
**Schema**
|
||||
* FAQPage
|
||||
* SoftwareApplication (or WebApplication)
|
||||
* BreadcrumbList
|
||||
**Primary CTA**
|
||||
* “Create a dynamic QR code” (signup)
|
||||
**Internal links**
|
||||
* To `/features/qr-code-analytics/`, `/learn/dynamic-vs-static-qr-codes/`, `/integrations/google-analytics-4/`
|
||||
|
||||
> Competitor pattern to beat: strong gating + feature ladder is common. ([qr-code-generator.com][1])
|
||||
|
||||
---
|
||||
|
||||
### 2) QR Code Analytics
|
||||
|
||||
**URL:** `/features/qr-code-analytics/`
|
||||
**Primary keyword:** qr code analytics
|
||||
**H1:** QR Code Analytics: Track Scans, Measure Campaign ROI
|
||||
**Sections**
|
||||
|
||||
* What you can measure (total/unique scans, geo, device, time)
|
||||
* Campaign organization (folders/tags, UTM conventions)
|
||||
* Export + integrations (GA4 first)
|
||||
* Dashboards (examples: restaurant menu, event check-in, retail)
|
||||
* Data accuracy & privacy notes
|
||||
* FAQ (“Can I track a static QR?” → explain dynamic requirement)
|
||||
**Schema**
|
||||
* FAQPage
|
||||
* SoftwareApplication
|
||||
* BreadcrumbList
|
||||
**CTA**
|
||||
* “Enable analytics on your QR code” (upgrade nudges)
|
||||
**Internal links**
|
||||
* From **every tool page** (sticky sidebar “Track scans with Analytics”)
|
||||
* To `/learn/how-to-track-qr-codes-in-ga4/`
|
||||
|
||||
> This is exactly what SaaS competitors highlight for upsell. ([flowcode.com][12])
|
||||
|
||||
---
|
||||
|
||||
### 3) Bulk QR Code Generator
|
||||
|
||||
**URL:** `/tools/bulk-qr-code-generator/`
|
||||
**Primary keyword:** bulk qr code generator
|
||||
**H1:** Bulk QR Code Generator (CSV Upload → Download ZIP)
|
||||
**Sections**
|
||||
|
||||
* Upload CSV / paste data / Google Sheets import (later)
|
||||
* Output formats (PNG/SVG/PDF), naming conventions
|
||||
* Dynamic vs static toggle per row (upsell!)
|
||||
* Common workflows: inventory labels, invites, coupons
|
||||
* QA: scan testing, error correction, print sizing
|
||||
* FAQ
|
||||
**Schema**
|
||||
* FAQPage
|
||||
* HowTo (only if you include step-by-step with images)
|
||||
**CTA**
|
||||
* “Generate bulk QR codes” + secondary “Enable tracking for all”
|
||||
**Internal links**
|
||||
* To `/features/qr-code-analytics/` + `/features/dynamic-qr-codes/`
|
||||
|
||||
> SERPs often expect “free bulk + zip”; match that intent. ([QR Explore][19])
|
||||
|
||||
---
|
||||
|
||||
### 4) vCard QR Code Generator
|
||||
|
||||
**URL:** `/tools/vcard-qr-code-generator/`
|
||||
**Primary keyword:** vCard qr code generator
|
||||
**H1:** vCard QR Code Generator (Digital Business Card)
|
||||
**Sections**
|
||||
|
||||
* vCard fields + preview (VCF standard)
|
||||
* iOS/Android compatibility + best practices
|
||||
* Static vs dynamic vCard (edit contact later)
|
||||
* Examples: sales reps, events, storefront QR
|
||||
* CTA: “Add scan tracking to your business cards”
|
||||
* FAQ (works on Android/iOS; does it expire; can I add photo; etc.)
|
||||
**Schema**
|
||||
* FAQPage
|
||||
* SoftwareApplication
|
||||
**CTA**
|
||||
* “Create vCard QR” + upsell “Track scans / update later”
|
||||
**Internal links**
|
||||
* To `/learn/dynamic-vs-static-qr-codes/` + analytics feature
|
||||
|
||||
---
|
||||
|
||||
### 5) QR Code API (developer money page)
|
||||
|
||||
**URL:** `/features/qr-code-api/` + `/docs/api/`
|
||||
**Primary keyword:** qr code api, qr code generator api
|
||||
**H1:** QR Code API (Generate QR Codes at Scale)
|
||||
**Sections**
|
||||
|
||||
* Authentication, endpoints, rate limits
|
||||
* Generate static/dynamic, bulk endpoints, webhooks (scan events)
|
||||
* Code samples (JS/Python/cURL)
|
||||
* Compliance + uptime
|
||||
* Pricing tiers
|
||||
**Schema**
|
||||
* SoftwareApplication (feature page)
|
||||
* TechArticle (docs pages)
|
||||
**CTA**
|
||||
* “Get API key” / “Start trial”
|
||||
**Internal links**
|
||||
* From bulk generator + analytics pages
|
||||
|
||||
---
|
||||
|
||||
## H) Risks + mitigation (cannibalization, programmatic pitfalls, E-E-A-T, index bloat)
|
||||
|
||||
### 1) Keyword cannibalization (very likely in this niche)
|
||||
|
||||
**Risk:** “qr code tracking”, “trackable qr codes”, “qr code analytics” collapse into the same intent.
|
||||
**Mitigation:** hard-map intents:
|
||||
|
||||
* Analytics = feature/commercial
|
||||
* Tracking = learn/how-to + GA4
|
||||
* Trackable QR = tool landing with demo dashboard
|
||||
|
||||
### 2) Programmatic SEO thin pages / index bloat
|
||||
|
||||
**Risk:** hundreds of near-identical “{type} QR generator” pages get ignored/deindexed.
|
||||
**Mitigation (hard rules)**
|
||||
|
||||
* Index only pages that include **unique elements**:
|
||||
|
||||
* type-specific fields + validation (real tool)
|
||||
* 2–3 examples
|
||||
* type-specific FAQs
|
||||
* type-specific tracking use case
|
||||
* **Noindex**: parameter pages, empty states, duplicate locale stubs, search/filter pages.
|
||||
|
||||
### 3) Trust & QR scam concerns (reputation risk, but also opportunity)
|
||||
|
||||
**Risk:** Users fear scanning QR codes; Google may reward safety content.
|
||||
**Mitigation:** ship “Security” feature page + learn content about safe scanning and link previews, referencing real-world scam patterns. ([Der Guardian][4])
|
||||
|
||||
### 4) Over-reliance on “Google QR Code Generator” traffic
|
||||
|
||||
**Risk:** users only want Chrome’s built-in static QR and bounce.
|
||||
**Mitigation:** page structure: “How to do it in Chrome” (satisfy intent) → “When you need dynamic + analytics” (convert). ([Google Hilfe][2])
|
||||
|
||||
### 5) E-E-A-T gap vs incumbents
|
||||
|
||||
**Risk:** new domain lacks credibility.
|
||||
**Mitigation**
|
||||
|
||||
* Publish 2–3 “benchmarks / research” assets with original data (even small): scan-rate benchmarks, print-size testing, or campaign case studies.
|
||||
* Add transparent pricing, uptime, privacy policy, and author/editor pages for Learn content.
|
||||
|
||||
---
|
||||
|
||||
If you tell me your **target market (US vs DACH vs global), language (EN/DE), and monetization (freemium vs trials)**, I can *tighten the sitemap + 90-day calendar* so it perfectly matches your rollout (especially internationalization + URL strategy).
|
||||
|
||||
[1]: https://www.qr-code-generator.com/?utm_source=chatgpt.com "QR Code Generator | Create Your Free QR Codes"
|
||||
[2]: https://support.google.com/chrome/answer/10051760?co=GENIE.Platform%3DDesktop&hl=en&utm_source=chatgpt.com "Share pages in Chrome - Computer"
|
||||
[3]: https://www.qr-code-generator.com/solutions/?utm_source=chatgpt.com "QR Code Solution for Every Purpose"
|
||||
[4]: https://www.theguardian.com/money/2025/may/25/qr-code-scam-what-is-quishing-drivers-app-phone-parking-payment?utm_source=chatgpt.com "'Pay here': the QR code 'quishing' scam targeting drivers"
|
||||
[5]: https://barcode.tec-it.com/en?utm_source=chatgpt.com "Free Online Barcode Generator: Create Barcodes for Free!"
|
||||
[6]: https://www.qrcode-monkey.com/?utm_source=chatgpt.com "QRCode Monkey - The free QR Code Generator to create ..."
|
||||
[7]: https://www.qrcode-monkey.com/de/qr-code-service/?utm_source=chatgpt.com "QR Code API for Static Codes"
|
||||
[8]: https://www.the-qrcode-generator.com/?utm_source=chatgpt.com "The QR Code Generator (TQRCG): Create Free QR Codes"
|
||||
[9]: https://hovercode.com/?utm_source=chatgpt.com "QR Code Generator | Create Free Dynamic QR Codes"
|
||||
[10]: https://hovercode.com/circle-qr-code-generator/?utm_source=chatgpt.com "Generate circle QR codes (no sign up required)"
|
||||
[11]: https://scanova.io/features/?utm_source=chatgpt.com "Powerful features for all QR Code use cases"
|
||||
[12]: https://www.flowcode.com/product/analytics?utm_source=chatgpt.com "Gain insight into your offline marketing with in-depth Analytics"
|
||||
[13]: https://www.qrcodechimp.com/qr-code-analytics-guide/?utm_source=chatgpt.com "QR Code Analytics: Track, Analyze & Optimize Your ..."
|
||||
[14]: https://me-qr.com/qr-code-generator/pdf?srsltid=AfmBOooK1o7kkjaSizlEOWcEcYcDWfKhZuuM3XvrJGQlm2xdiTbw1exS&utm_source=chatgpt.com "Create QR Code For PDF FREE"
|
||||
[15]: https://www.canva.com/qr-code-generator/?utm_source=chatgpt.com "Free QR Code Generator - Create QR codes with ease"
|
||||
[16]: https://help.instagram.com/925529167647849/?utm_source=chatgpt.com "Find and customize the QR code of your Instagram profile"
|
||||
[17]: https://www.spotifycodes.com/?utm_source=chatgpt.com "Spotify Codes"
|
||||
[18]: https://quickchart.io/bulk-qr-code-generator/?utm_source=chatgpt.com "Bulk QR Code Generator | Custom colors and logo, free"
|
||||
[19]: https://qrexplore.com/generate/?utm_source=chatgpt.com "Bulk QR Code Generator"
|
||||
743
seo_issues_new.md
Normal file
@@ -0,0 +1,743 @@
|
||||
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
|
||||
68
seo_tasks.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 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.
|
||||
22
seobility-findings.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 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.
|
||||
13
service_account.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "gen-lang-client-0595806638",
|
||||
"private_key_id": "e44bc1717f1cf413521149de272bf13bfa89a336",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0xJkozHODpcpD\nu3dTHPfprZk6eKiOT05h+uG8Clm8i8LLaS/eHT+B02qxFYMBX0VH9O2GvPp/VnfC\nB/Clc7bofN5VDpQMjVUiPDqMbUVEAiQHNOTp9pkfJltaHAl/J5Cc/DccCaOn89xT\nFD5b7dTn29suuBZHTqsaFDlydnU2xJAwcrWBm7/A0JZM85d76yhY0Jxcg9w8XlpE\n+TWN8OxSUIfubaac0mfI40RH2EfugmA7M45t7B3sEbmHk5tVQSItvncz2ls9fUE4\neB6u4foMFp4Z9k5Ejs7y4N3Yft0JWS+RjI0bcvvvQ/wcnDfcwCdDFFn2Y+hflKMm\nS9+ZRnmBAgMBAAECggEAAztAeo3JifZD3nzEUcDte9cHgN7AMtlJ3Wvc7va5Sw50\nizkCmSlwPoc4/0MvoMo0+701JVxbenXveMpEb3fZMoszkdU9U9iPZCfzB4wQErOa\nppuprbbOXtO9JzZVinWzflPSIUVK16lUVvYVrmfpHYou1G/dIMIXQkVsD7NR9t/B\nafD0w/q1nwwyPB08BjSemKXDQo6NF0cE/TIvaMj8vtxuouAL+fea0n/XxMQNoIoJ\nF+pJtPQ1hkQrpayzuj3smQ11PFpYuvsZHuS3dG9j4gPjGClezK3Sflt7vwNywIRc\ntJ0Qx58on0dy0YnppMWrHh/nykraVLusvMI04joqwQKBgQDlE1Mbi8dpeKn7zkV9\nLS/O6S5Ql2k2G6KxI8GHn3qxB5yfU8G2xqk64r04YB6SMCXscIQu1Tmro8kDMTZk\n5b/issH3+7uqGcJMYhZczWsjax3S1ugepXt29dF26VnbyfvD7h9qleKLhIq32z9P\nxzZGhptTCa0swypi7prNE0MhZwKBgQDKA75g8UhVULA6q3hFEG+24ICd3Gekdz1y\nmaDrPjSJmeMSUlDl4QhGRbZBSJcAfcFKk4+Nme3sTYvjMMz6per4a5TC/+IlSufm\nOSL+CSVijvVYwCMyLyiAcm5Pqcjw16S6enHIidnOYP8e8OM0H2aNKfFTKq30B3ww\nAF8ipa+01wKBgQC24JaYhx7LtOj/fc08AbcJGF9BN59m8ukPQdxeyZLJgaooCFW9\n9RtlR16IgzPkwUuFVs4wFUnVHQx83+zs3/4wnUT9FJrdUXMsR6JStCu0Ou+0Qp1M\n2g+XCOgQZnq2XKoB4ThzfvU9LLMR1JbWudM6unuF71OxSJ2uHY636YjOQQKBgBs6\n+fSTUY6+e6LM7j9RAd4C0RN2XDodIJlMABb1oZtStPsJQYJbHQRr7S9Lm58jVGS7\nE0ShFSMfKNYNA/RdXRjzV3AZkeA5Ap1T4lWf4fwxDP1TmOrw1GLMCfaPClj8mGXS\nj3farRNWm80N53JlMSuiFbeCL0SPpbvKsQg4kUCtAoGAUORyhW70nhZJ1BbmvyRf\n17fcwenK/3GmWgqsrzN7/ucPwjqIzLGVoAXd2euxpE49/VW2xYpJjyHJHuoXDc66\n+AUog0bsxcKpM5tL3VelQl3SkUlCG7jYe20rMm01y35uM2REvQv3/r9F7Bbaq/9n\nSCwu/45QobgLCUx0B7wDqWA=\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "indexer@gen-lang-client-0595806638.iam.gserviceaccount.com",
|
||||
"client_id": "111279247752160222047",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/indexer%40gen-lang-client-0595806638.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
254
src/app/(app)/AppLayout.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,254 +1,38 @@
|
||||
'use client';
|
||||
import type { Metadata } from 'next';
|
||||
import '@/styles/globals.css';
|
||||
import { Suspense } from 'react';
|
||||
import { Providers } from '@/components/Providers';
|
||||
import AppLayout from './AppLayout';
|
||||
|
||||
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 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',
|
||||
},
|
||||
};
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
plan: string | null;
|
||||
}
|
||||
|
||||
export default function AppLayout({
|
||||
children,
|
||||
export default function RootAppLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
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>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="font-sans">
|
||||
<Providers>
|
||||
<Suspense fallback={null}>
|
||||
<AppLayout>
|
||||
{children}
|
||||
</AppLayout>
|
||||
</Suspense>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,38 @@
|
||||
export default function AuthLayout({
|
||||
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({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white">
|
||||
{children}
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
164
src/app/(auth)/login/ClientPage.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import React, { Suspense } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
import 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'],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
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">
|
||||
@@ -86,94 +48,13 @@ export default function LoginPage() {
|
||||
</Link>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
}>
|
||||
<LoginClientPage />
|
||||
</Suspense>
|
||||
|
||||
<p className="text-center text-sm text-gray-500 mt-6">
|
||||
By signing in, you agree to our{' '}
|
||||
|
||||
@@ -8,7 +8,9 @@ import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
import { Suspense } from 'react';
|
||||
|
||||
function ResetPasswordContent() {
|
||||
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
@@ -206,3 +208,11 @@ export default function ResetPasswordPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="min-h-screen flex items-center justify-center">Loading...</div>}>
|
||||
<ResetPasswordContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
185
src/app/(auth)/signup/ClientPage.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,89 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { Suspense } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
import 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'],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
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">
|
||||
@@ -99,102 +49,13 @@ export default function SignupPage() {
|
||||
</Link>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
}>
|
||||
<SignupClientPage />
|
||||
</Suspense>
|
||||
|
||||
<p className="text-center text-sm text-gray-500 mt-6">
|
||||
By signing up, you agree to our{' '}
|
||||
|
||||
289
src/app/(marketing)/MarketingLayout.tsx
Normal file
@@ -0,0 +1,289 @@
|
||||
'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, Barcode as BarcodeIcon } 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' },
|
||||
{ name: 'Barcode', description: 'Generate barcodes', href: '/tools/barcode-generator', icon: BarcodeIcon, color: 'text-slate-800', bgColor: 'bg-slate-100' },
|
||||
];
|
||||
|
||||
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>
|
||||
<li><a href="/tools/barcode-generator">Barcode Generator</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 >
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -18,7 +19,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 analytics, dynamic codes & smart marketing uses.',
|
||||
'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.',
|
||||
160
|
||||
);
|
||||
|
||||
@@ -37,6 +38,14 @@ 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,
|
||||
@@ -45,82 +54,7 @@ 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[] = [
|
||||
@@ -145,8 +79,8 @@ export default function BlogPage() {
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
{blogPosts.map((post) => (
|
||||
<Link key={post.slug} href={`/blog/${post.slug}`}>
|
||||
{blogPostList.map((post: any, index: number) => (
|
||||
<Link key={post.slug} href={post.link || `/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
|
||||
@@ -155,6 +89,7 @@ export default function BlogPage() {
|
||||
width={800}
|
||||
height={600}
|
||||
className="w-full h-full object-cover transition-transform duration-500 hover:scale-110"
|
||||
priority={index < 3}
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="pb-3">
|
||||
@@ -168,7 +103,9 @@ 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">Read more →</span>
|
||||
<span className="text-primary-600 text-sm font-medium">
|
||||
{post.link ? 'Try Now →' : 'Read Article →'}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -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 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.',
|
||||
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.',
|
||||
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,6 +23,14 @@ 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',
|
||||
@@ -46,7 +54,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',
|
||||
@@ -333,7 +341,7 @@ export default function BulkQRCodeGeneratorPage() {
|
||||
Start Bulk Generation
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/create">
|
||||
<Link href="/signup">
|
||||
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
||||
Try Single QR First
|
||||
</Button>
|
||||
@@ -440,7 +448,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">
|
||||
@@ -633,26 +641,35 @@ Product C,https://example.com/product-c,Budget Widget,electronics,sale`}
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<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
|
||||
{/* 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>
|
||||
</h2>
|
||||
<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 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>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<div className="flex flex-col sm:flex-row gap-5 justify-center">
|
||||
<Link href="/signup">
|
||||
<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">
|
||||
<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">
|
||||
Start Bulk Generation
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/pricing">
|
||||
<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">
|
||||
<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">
|
||||
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>
|
||||
|
||||
@@ -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 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.',
|
||||
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.',
|
||||
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,6 +23,14 @@ 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',
|
||||
@@ -180,7 +188,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/create',
|
||||
url: 'https://www.qrmaster.net/signup',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
@@ -504,7 +512,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||
Get Started Free
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/create">
|
||||
<Link href="/signup">
|
||||
<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>
|
||||
|
||||
22
src/app/(marketing)/faq/ContactSupport.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -14,7 +15,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(
|
||||
'All answers: dynamic QR, security, analytics, bulk, events & print.',
|
||||
'Find answers about dynamic QR codes, scan tracking, security, bulk generation, and event QR codes. Everything you need to know about QR Master features.',
|
||||
160
|
||||
);
|
||||
|
||||
@@ -33,6 +34,14 @@ 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,
|
||||
@@ -123,18 +132,7 @@ export default function FAQPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<ContactSupport />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,248 +1,76 @@
|
||||
'use client';
|
||||
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';
|
||||
|
||||
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';
|
||||
const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true';
|
||||
|
||||
export default function MarketingLayout({
|
||||
children,
|
||||
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({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
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 (
|
||||
<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 >
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
754
src/app/(marketing)/newsletter/NewsletterClient.tsx
Normal file
@@ -0,0 +1,754 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,643 +1,19 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import NewsletterClient from './NewsletterClient';
|
||||
|
||||
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 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',
|
||||
},
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
export default function NewsletterPage() {
|
||||
return <NewsletterClient />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
|
||||
import '@/styles/globals.css';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
@@ -37,24 +39,22 @@ export default function NotFound() {
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="flex justify-center">
|
||||
<Link href="/">
|
||||
<Button size="lg">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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>
|
||||
Back to Home
|
||||
</Button>
|
||||
<Link href="/" className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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>
|
||||
Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,71 +1,85 @@
|
||||
import React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||
import { organizationSchema, websiteSchema } from '@/lib/schema';
|
||||
import HomePageClient from '@/components/marketing/HomePageClient';
|
||||
|
||||
function truncateAtWord(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
const truncated = text.slice(0, maxLength);
|
||||
const lastSpace = truncated.lastIndexOf(' ');
|
||||
return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated;
|
||||
}
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const title = truncateAtWord('QR Master: Dynamic QR Generator', 60);
|
||||
const description = truncateAtWord(
|
||||
'Dynamic QR, branding, bulk generation & analytics for all campaigns.',
|
||||
160
|
||||
);
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.net/',
|
||||
en: 'https://www.qrmaster.net/',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: 'https://www.qrmaster.net/',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<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.
|
||||
Perfect for restaurants, retail, events, and marketing campaigns.
|
||||
</p>
|
||||
<p>
|
||||
Features include: Dynamic QR codes with real-time tracking, bulk QR code generation from Excel/CSV,
|
||||
custom branding with colors and logos, advanced scan analytics showing device types and locations,
|
||||
vCard QR codes for digital business cards, and restaurant menu QR codes.
|
||||
</p>
|
||||
<p>
|
||||
Start free with 3 dynamic QR codes and unlimited static codes. Upgrade to Pro for 50 codes
|
||||
with advanced analytics, or Business for 500 codes with bulk creation and priority support.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<HomePageClient />
|
||||
</>
|
||||
);
|
||||
}
|
||||
import React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||
import { 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;
|
||||
const truncated = text.slice(0, maxLength);
|
||||
const lastSpace = truncated.lastIndexOf(' ');
|
||||
return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated;
|
||||
}
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const title = truncateAtWord('QR Master: Dynamic QR Generator', 60);
|
||||
const description = truncateAtWord(
|
||||
'Create professional QR codes with QR Master. Dynamic QR with tracking, bulk generation, custom branding, and real-time analytics for all your campaigns.',
|
||||
160
|
||||
);
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.net/',
|
||||
en: 'https://www.qrmaster.net/',
|
||||
de: 'https://www.qrmaster.net/qr-code-erstellen',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: 'https://www.qrmaster.net/',
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: 'https://www.qrmaster.net/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images: ['https://www.qrmaster.net/og-image.png'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
{/* Server-rendered SEO content for crawlers */}
|
||||
<div className="sr-only" aria-hidden="false">
|
||||
<p>
|
||||
Create professional QR codes for your business with QR Master. Our dynamic QR code generator
|
||||
lets you create trackable QR codes, edit destinations anytime, and view detailed analytics.
|
||||
Perfect for restaurants, retail, events, and marketing campaigns.
|
||||
</p>
|
||||
<p>
|
||||
Features include: Dynamic QR codes with real-time tracking, bulk QR code generation from Excel/CSV,
|
||||
custom branding with colors and logos, advanced scan analytics showing device types and locations,
|
||||
vCard QR codes for digital business cards, and restaurant menu QR codes.
|
||||
</p>
|
||||
<p>
|
||||
Start free with 3 dynamic QR codes and unlimited static codes. Upgrade to Pro for 50 codes
|
||||
with advanced analytics, or Business for 500 codes with bulk creation and priority support.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<HomePageClient />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ 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 PricingPage() {
|
||||
export default function PricingClient() {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
|
||||
@@ -182,9 +183,9 @@ export default function PricingPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Choose Your Plan
|
||||
</h1>
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Select the perfect plan for your QR code needs
|
||||
</p>
|
||||
@@ -260,7 +261,7 @@ export default function PricingPage() {
|
||||
All plans include unlimited static QR codes and basic customization.
|
||||
</p>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Need help choosing? <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a>
|
||||
Need help choosing? <ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</ObfuscatedMailto>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
45
src/app/(marketing)/pricing/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
src/app/(marketing)/privacy/PrivacyEmailLink.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
'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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,27 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { PrivacyEmailLink } from './PrivacyEmailLink';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Privacy Policy | QR Master',
|
||||
description: 'Privacy Policy and data protection information for 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default function PrivacyPage() {
|
||||
@@ -93,9 +111,7 @@ export default function PrivacyPage() {
|
||||
</ul>
|
||||
<p className="text-gray-700 mb-4">
|
||||
To exercise these rights, contact us at{' '}
|
||||
<a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700">
|
||||
support@qrmaster.net
|
||||
</a>
|
||||
<PrivacyEmailLink />
|
||||
</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,
|
||||
@@ -111,9 +127,7 @@ export default function PrivacyPage() {
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<p className="text-gray-700 mb-2">
|
||||
<strong>Email:</strong>{' '}
|
||||
<a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700">
|
||||
support@qrmaster.net
|
||||
</a>
|
||||
<PrivacyEmailLink />
|
||||
</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>
|
||||
|
||||
@@ -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 Every Scan | QR Master',
|
||||
title: 'QR Code Tracking & Analytics - Track Scans | 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,13 +19,21 @@ export const metadata: Metadata = {
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
|
||||
title: 'QR Code Tracking & Analytics - Track Scans | 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 Every Scan | QR Master',
|
||||
title: 'QR Code Tracking & Analytics - Track Scans | QR Master',
|
||||
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
||||
},
|
||||
};
|
||||
@@ -154,7 +162,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/analytics',
|
||||
url: 'https://www.qrmaster.net/signup',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
@@ -199,7 +207,7 @@ export default function QRCodeTrackingPage() {
|
||||
Start Tracking Free
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/create">
|
||||
<Link href="/signup">
|
||||
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
||||
Create Trackable QR Code
|
||||
</Button>
|
||||
@@ -370,26 +378,35 @@ export default function QRCodeTrackingPage() {
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<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
|
||||
{/* 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>
|
||||
</h2>
|
||||
<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 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>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<div className="flex flex-col sm:flex-row gap-5 justify-center">
|
||||
<Link href="/signup">
|
||||
<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">
|
||||
<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">
|
||||
Create Free Account
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/pricing">
|
||||
<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">
|
||||
<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">
|
||||
View Pricing
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-sm text-slate-500">
|
||||
Full analytics accessible on free plan.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
117
src/app/(marketing)/reprint-calculator/page.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
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 instantly—keeping 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import Barcode from 'react-barcode';
|
||||
import Link from 'next/link';
|
||||
import { Download, Printer, Barcode as BarcodeIcon, Sparkles, Sliders, Check, Info, Copy } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toPng, toSvg, toBlob } from 'html-to-image';
|
||||
|
||||
// Brand Colors
|
||||
const BRAND = {
|
||||
paleGrey: '#EBEBDF',
|
||||
slate900: '#0f172a',
|
||||
};
|
||||
|
||||
const BARCODE_COLORS = [
|
||||
{ name: 'Classic Black', value: '#000000' },
|
||||
{ name: 'Dark Blue', value: '#1A1265' },
|
||||
{ name: 'Rich Indigo', value: '#4338CA' },
|
||||
{ name: 'Deep Emerald', value: '#065F46' },
|
||||
{ name: 'Crimson', value: '#991B1B' },
|
||||
{ name: 'Slate Gray', value: '#334155' },
|
||||
{ name: 'Business Navy', value: '#1E293B' },
|
||||
];
|
||||
|
||||
const FRAME_OPTIONS = [
|
||||
{ id: 'none', label: 'No Frame' },
|
||||
{ id: 'scanme', label: 'Scan Me' },
|
||||
{ id: 'product', label: 'Product' },
|
||||
{ id: 'serial', label: 'Serial' },
|
||||
];
|
||||
|
||||
const FORMAT_INFO: Record<string, string> = {
|
||||
'CODE128': 'High-density alphanumeric format. Best for general purpose use.',
|
||||
'EAN13': 'International retail standard for products worldwide.',
|
||||
'UPC': 'Standard retail format used primarily in North America.',
|
||||
'CODE39': 'Older industrial standard supporting uppercase letters and numbers.',
|
||||
'ITF14': 'Used on shipping containers and logistics packaging.',
|
||||
'MSI': 'Specialized format for retail shelf labeling and inventory.',
|
||||
'pharmacode': 'Pharmaceutical packaging control standard.',
|
||||
};
|
||||
|
||||
export default function BarcodeGeneratorClient() {
|
||||
const [value, setValue] = useState('123456789');
|
||||
const [format, setFormat] = useState('CODE128');
|
||||
const [width, setWidth] = useState(2);
|
||||
const [height, setHeight] = useState(100);
|
||||
const [displayValue, setDisplayValue] = useState(true);
|
||||
const [lineColor, setLineColor] = useState('#000000');
|
||||
const [frameType, setFrameType] = useState('none');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const barcodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Validation Logic
|
||||
React.useEffect(() => {
|
||||
setError(null);
|
||||
if (!value) return;
|
||||
|
||||
if (format === 'EAN13' && !/^\d{12,13}$/.test(value)) {
|
||||
setError('EAN-13 requires 12 or 13 digits.');
|
||||
} else if (format === 'UPC' && !/^\d{11,12}$/.test(value)) {
|
||||
setError('UPC requires 11 or 12 digits.');
|
||||
} else if (format === 'CODE39' && !/^[0-9A-Z\-\.\ \$\/\+\%]+$/.test(value)) {
|
||||
setError('Code 39 only supports numbers, uppercase letters, and - . $ / + % spaces.');
|
||||
} else if ((format === 'ITF14' || format === 'MSI') && !/^\d+$/.test(value)) {
|
||||
setError('This format only supports numbers.');
|
||||
}
|
||||
}, [value, format]);
|
||||
|
||||
const downloadBarcode = async (extension: 'png' | 'svg') => {
|
||||
if (!barcodeRef.current) return;
|
||||
|
||||
try {
|
||||
let dataUrl;
|
||||
if (extension === 'png') {
|
||||
dataUrl = await toPng(barcodeRef.current, {
|
||||
backgroundColor: '#ffffff',
|
||||
pixelRatio: 3,
|
||||
});
|
||||
} else {
|
||||
dataUrl = await toSvg(barcodeRef.current, {
|
||||
backgroundColor: '#ffffff',
|
||||
});
|
||||
}
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = dataUrl;
|
||||
link.download = `barcode-${value || 'generator'}.${extension}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
showToast(`Barcode downloaded as ${extension.toUpperCase()}`, 'success');
|
||||
} catch (err) {
|
||||
console.error('Download failed', err);
|
||||
showToast('Download failed', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const copyBarcode = async () => {
|
||||
if (!barcodeRef.current) return;
|
||||
try {
|
||||
// Use toBlob directly for better performance and compatibility
|
||||
const blob = await toBlob(barcodeRef.current, {
|
||||
backgroundColor: '#ffffff',
|
||||
pixelRatio: 3,
|
||||
});
|
||||
|
||||
if (!blob) {
|
||||
throw new Error('Failed to generate image blob');
|
||||
}
|
||||
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
'image/png': blob,
|
||||
}),
|
||||
]);
|
||||
showToast('Barcode copied to clipboard', 'success');
|
||||
} catch (err) {
|
||||
console.error('Copy failed', err);
|
||||
showToast('Failed to copy barcode', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const formats = [
|
||||
{ value: 'CODE128', label: 'Code 128 (Standard)' },
|
||||
{ value: 'EAN13', label: 'EAN-13 (Retail)' },
|
||||
{ value: 'UPC', label: 'UPC-A (US Retail)' },
|
||||
{ value: 'CODE39', label: 'Code 39' },
|
||||
{ value: 'ITF14', label: 'ITF-14' },
|
||||
{ value: 'MSI', label: 'MSI' },
|
||||
{ value: 'pharmacode', label: 'Pharmacode' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||
|
||||
{/* Main Generator Card */}
|
||||
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
||||
<div className="grid lg:grid-cols-2">
|
||||
|
||||
{/* LEFT: Input Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
|
||||
{/* Configuration */}
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||
<Sliders className="w-5 h-5 text-slate-900" aria-hidden="true" />
|
||||
Configuration
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="barcode-content" className="block text-sm font-medium text-slate-700 mb-2">Content</label>
|
||||
<Input
|
||||
id="barcode-content"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="Enter barcode data (e.g. 12345678)"
|
||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-slate-900 focus:ring-slate-900"
|
||||
aria-label="Barcode content"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="block text-sm font-medium text-slate-700">Format</label>
|
||||
<div className="group relative">
|
||||
<Info className="w-4 h-4 text-slate-400 cursor-help" />
|
||||
<div className="absolute right-0 bottom-full mb-2 w-64 p-3 bg-slate-900 text-white text-[11px] rounded-xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none shadow-xl z-50">
|
||||
<p className="font-bold mb-1">Format Guide:</p>
|
||||
<p>{FORMAT_INFO[format]}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
value={format}
|
||||
onChange={(e) => setFormat(e.target.value)}
|
||||
className="h-12 rounded-xl border-slate-200"
|
||||
options={formats}
|
||||
aria-label="Format"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500 mt-2 px-1">
|
||||
{FORMAT_INFO[format]}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-100"></div>
|
||||
|
||||
{/* Design Options */}
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-slate-900" aria-hidden="true" />
|
||||
Design Options
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<label htmlFor="width-range" className="text-sm font-medium text-slate-700">Width</label>
|
||||
<span className="text-xs text-slate-500 bg-slate-100 px-2 py-1 rounded-md font-bold">{width}px</span>
|
||||
</div>
|
||||
<input
|
||||
id="width-range"
|
||||
type="range"
|
||||
min="1"
|
||||
max="4"
|
||||
step="0.5"
|
||||
value={width}
|
||||
onChange={(e) => setWidth(parseFloat(e.target.value))}
|
||||
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-slate-900"
|
||||
aria-label="Barcode width"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<label htmlFor="height-range" className="text-sm font-medium text-slate-700">Height</label>
|
||||
<span className="text-xs text-slate-500 bg-slate-100 px-2 py-1 rounded-md font-bold">{height}px</span>
|
||||
</div>
|
||||
<input
|
||||
id="height-range"
|
||||
type="range"
|
||||
min="30"
|
||||
max="200"
|
||||
step="5"
|
||||
value={height}
|
||||
onChange={(e) => setHeight(parseInt(e.target.value))}
|
||||
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-slate-900"
|
||||
aria-label="Barcode height"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">Line Color</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{BARCODE_COLORS.map((c) => (
|
||||
<button
|
||||
key={c.name}
|
||||
onClick={() => setLineColor(c.value)}
|
||||
className={cn(
|
||||
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
||||
lineColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
||||
)}
|
||||
style={{ backgroundColor: c.value }}
|
||||
aria-label={`Select color ${c.name}`}
|
||||
title={c.name}
|
||||
>
|
||||
{lineColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} aria-hidden="true" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{FRAME_OPTIONS.map((frame) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
onClick={() => setFrameType(frame.id)}
|
||||
className={cn(
|
||||
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
||||
frameType === frame.id
|
||||
? "bg-slate-900 text-white border-slate-900"
|
||||
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
||||
)}
|
||||
>
|
||||
{frame.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer group p-3 border border-slate-200 rounded-xl hover:border-slate-900 transition-colors bg-slate-50/50">
|
||||
<div className={cn(
|
||||
"w-5 h-5 rounded border-2 flex items-center justify-center transition-all",
|
||||
displayValue ? "bg-slate-900 border-slate-900" : "border-slate-300 group-hover:border-slate-400"
|
||||
)}>
|
||||
{displayValue && <Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />}
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={displayValue}
|
||||
onChange={(e) => setDisplayValue(e.target.checked)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<span className="text-sm font-medium text-slate-700">Show Value Text</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</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 }}>
|
||||
|
||||
{/* Barcode Card */}
|
||||
<div
|
||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center justify-center min-h-[300px] w-full max-w-[400px] border border-slate-100 relative"
|
||||
>
|
||||
<div className="absolute top-4 right-4">
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 bg-slate-100 rounded-md text-[10px] font-bold text-slate-500 uppercase tracking-wider">
|
||||
Live Preview
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={barcodeRef} className="py-4 bg-white flex flex-col items-center justify-center overflow-hidden w-full">
|
||||
{frameType !== 'none' && !error && (
|
||||
<div
|
||||
className="mb-4 px-6 py-2 rounded-full text-white font-bold text-xs tracking-widest uppercase shadow-md"
|
||||
style={{ backgroundColor: lineColor }}
|
||||
>
|
||||
{FRAME_OPTIONS.find(f => f.id === frameType)?.label}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error ? (
|
||||
<div className="flex flex-col items-center text-center p-6 animate-in fade-in zoom-in duration-200">
|
||||
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mb-3">
|
||||
<Info className="w-6 h-6 text-red-500" />
|
||||
</div>
|
||||
<p className="text-red-500 font-bold text-sm">{error}</p>
|
||||
<p className="text-slate-400 text-xs mt-1">Please correct your input.</p>
|
||||
</div>
|
||||
) : value ? (
|
||||
<Barcode
|
||||
key={`${format}-${lineColor}-${value}-${width}-${height}-${displayValue}`}
|
||||
value={value}
|
||||
format={format as any}
|
||||
width={width}
|
||||
height={height}
|
||||
displayValue={displayValue}
|
||||
background="#ffffff"
|
||||
lineColor={lineColor}
|
||||
margin={10}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center text-slate-400 p-6">
|
||||
<BarcodeIcon className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
||||
<p className="text-sm font-medium">Enter data to generate</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info Preview */}
|
||||
<div className="mt-6 text-center w-full">
|
||||
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
||||
<span className="truncate">{formats.find(f => f.value === format)?.label}</span>
|
||||
</h3>
|
||||
<div className="text-xs text-slate-600 mt-1 truncate px-2 font-mono">
|
||||
{value || 'Barcode Value'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download Buttons */}
|
||||
<div className="flex flex-col gap-4 mt-8 w-full max-w-[450px]">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-3 w-full">
|
||||
<Button
|
||||
onClick={() => downloadBarcode('png')}
|
||||
className="w-full sm:flex-1 bg-slate-900 hover:bg-black text-white shadow-lg h-12 rounded-xl"
|
||||
aria-label="Download barcode as PNG"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" aria-hidden="true" />
|
||||
Download PNG
|
||||
</Button>
|
||||
<div className="relative w-full sm:w-auto">
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-indigo-600 text-white text-[9px] font-bold px-2 py-0.5 rounded-full whitespace-nowrap shadow-sm z-10 pointer-events-none">
|
||||
BEST FOR PRINT
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => downloadBarcode('svg')}
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto px-6 border-slate-300 hover:bg-white h-12 rounded-xl font-bold"
|
||||
aria-label="Download barcode as SVG"
|
||||
>
|
||||
SVG
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={copyBarcode}
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto px-4 border-slate-300 hover:bg-white h-12 rounded-xl"
|
||||
title="Copy to Clipboard"
|
||||
aria-label="Copy barcode image to clipboard"
|
||||
>
|
||||
<Copy className="w-4 h-4 text-slate-600" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => window.print()}
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto px-4 border-slate-300 hover:bg-white h-12 rounded-xl"
|
||||
title="Print"
|
||||
aria-label="Print barcode"
|
||||
>
|
||||
<Printer className="w-4 h-4 text-slate-600" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Link href="/signup" className="text-xs font-medium text-slate-400 hover:text-indigo-600 transition-colors flex items-center justify-center gap-1 group">
|
||||
Need bulk generation?
|
||||
<span className="underline decoration-slate-300 group-hover:decoration-indigo-300 underline-offset-4">Available in Pro →</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<h3 className="font-bold text-lg">Need Dynamic QR Codes?</h3>
|
||||
<p className="text-white/80 text-sm mt-1">
|
||||
Switch to QR codes to edit content later and track your scans.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/signup">
|
||||
<Button className="bg-white text-slate-900 hover:bg-slate-100 shrink-0 shadow-lg px-8">
|
||||
Get Started Free
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
321
src/app/(marketing)/tools/barcode-generator/BarcodeGuide.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import { BookOpen, CheckCircle, HelpCircle, Layers, Settings, ShoppingCart, Tag, Activity, Factory } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export function BarcodeGuide() {
|
||||
return (
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white" id="guide">
|
||||
<div className="max-w-3xl mx-auto prose prose-slate prose-lg">
|
||||
|
||||
<div className="flex items-center gap-3 mb-8 not-prose">
|
||||
<div className="p-3 bg-blue-100/50 rounded-xl">
|
||||
<BookOpen className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-slate-900 m-0">
|
||||
Barcode Generator – How Barcodes Work and Why They Matter
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p className="lead text-xl text-slate-600">
|
||||
Barcodes are an essential part of modern commerce, logistics, and inventory management. A <strong>Barcode Generator</strong> allows businesses and individuals to create scannable barcodes quickly and efficiently for products, packaging, and internal systems. Whether you run an online shop, manage a warehouse, or sell products locally, understanding how barcodes work can save time and reduce errors.
|
||||
</p>
|
||||
<p>
|
||||
In this article, you will learn what barcodes are, how they work, and how a <strong>Barcode Generator</strong> helps you create professional barcodes in seconds.
|
||||
</p>
|
||||
|
||||
{/* SEO Image */}
|
||||
<div className="my-8 rounded-2xl overflow-hidden shadow-lg not-prose border border-slate-100">
|
||||
<img
|
||||
src="/barcode-generator-preview.png"
|
||||
alt="Free Online Barcode Generator Preview - Create EAN, UPC, and Code 128 Barcodes"
|
||||
className="w-full h-64 sm:h-80 object-cover"
|
||||
width="800"
|
||||
height="320"
|
||||
/>
|
||||
<div className="bg-slate-50 p-4 text-sm text-slate-500 text-center border-t border-slate-100">
|
||||
Use our <strong>free barcode generator</strong> to create scannable codes.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>What Is a Barcode?</h2>
|
||||
<p>
|
||||
A barcode is a visual representation of data that can be read by machines. It consists of vertical lines with different widths and spacing, which encode numbers or characters. When scanned with a barcode scanner or smartphone, the information is instantly translated into readable data.
|
||||
</p>
|
||||
<p>
|
||||
Barcodes are commonly used to identify products, track inventory, manage logistics, and speed up checkout processes. They reduce manual input and significantly lower the risk of human error.
|
||||
</p>
|
||||
|
||||
<h2>How Does a Barcode Generator Work?</h2>
|
||||
<p>
|
||||
A Barcode Generator converts text or numeric input into a barcode format that scanners can read. The process is simple:
|
||||
</p>
|
||||
<ul className="list-none pl-0 space-y-4 not-prose my-8">
|
||||
<li className="flex gap-4">
|
||||
<div className="w-8 h-8 mt-1 rounded-full bg-slate-100 flex items-center justify-center shrink-0 text-slate-600 font-bold text-sm">1</div>
|
||||
<div>
|
||||
<strong className="text-slate-900 block mb-1">Input Data</strong>
|
||||
<p className="text-slate-600 m-0 text-base">You enter a number or text (for example, a product ID).</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex gap-4">
|
||||
<div className="w-8 h-8 mt-1 rounded-full bg-slate-100 flex items-center justify-center shrink-0 text-slate-600 font-bold text-sm">2</div>
|
||||
<div>
|
||||
<strong className="text-slate-900 block mb-1">Select Format</strong>
|
||||
<p className="text-slate-600 m-0 text-base">You select a barcode format such as EAN-13 or Code 128.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex gap-4">
|
||||
<div className="w-8 h-8 mt-1 rounded-full bg-slate-100 flex items-center justify-center shrink-0 text-slate-600 font-bold text-sm">3</div>
|
||||
<div>
|
||||
<strong className="text-slate-900 block mb-1">Generate</strong>
|
||||
<p className="text-slate-600 m-0 text-base">The generator creates a scannable barcode image instantly.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex gap-4">
|
||||
<div className="w-8 h-8 mt-1 rounded-full bg-slate-100 flex items-center justify-center shrink-0 text-slate-600 font-bold text-sm">4</div>
|
||||
<div>
|
||||
<strong className="text-slate-900 block mb-1">Download</strong>
|
||||
<p className="text-slate-600 m-0 text-base">You download or print the barcode for use.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
A modern <strong>Barcode Generator</strong> works directly in the browser and does not require additional software.
|
||||
</p>
|
||||
|
||||
<h2>Common Types of Barcodes</h2>
|
||||
<p>
|
||||
Different barcode formats are used for different purposes. Choosing the right one is important for compatibility and scanning accuracy.
|
||||
</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6 not-prose my-8">
|
||||
{/* EAN-13 Card */}
|
||||
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Tag className="w-5 h-5 text-blue-500" />
|
||||
<h4 className="text-lg font-bold text-slate-900 m-0">EAN-13</h4>
|
||||
</div>
|
||||
<div className="text-xs font-mono bg-slate-100 inline-block px-2 py-1 rounded text-slate-500 mb-3">Retail • Europe</div>
|
||||
<div className="mb-3 bg-slate-50 rounded border border-slate-100 p-2 flex justify-center">
|
||||
<img src="/barcode-generator-preview.png" alt="EAN-13 Barcode Sample for International Products" className="h-10 object-contain opacity-75 grayscale" width="200" height="40" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 m-0">
|
||||
EAN-13 is widely used in retail, especially in Europe. It is designed for consumer products sold in stores and supermarkets.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* UPC-A Card */}
|
||||
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<ShoppingCart className="w-5 h-5 text-indigo-500" />
|
||||
<h4 className="text-lg font-bold text-slate-900 m-0">UPC-A</h4>
|
||||
</div>
|
||||
<div className="text-xs font-mono bg-slate-100 inline-block px-2 py-1 rounded text-slate-500 mb-3">Retail • USA/Canada</div>
|
||||
<div className="mb-3 bg-slate-50 rounded border border-slate-100 p-2 flex justify-center">
|
||||
<img src="/barcode-generator-preview.png" alt="UPC-A Barcode Example for Retail Products in USA" className="h-10 object-contain opacity-75 grayscale" width="200" height="40" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 m-0">
|
||||
UPC-A is similar to EAN-13 but is mainly used in the United States and Canada for retail products.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Code 128 Card */}
|
||||
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Settings className="w-5 h-5 text-emerald-500" />
|
||||
<h4 className="text-lg font-bold text-slate-900 m-0">Code 128</h4>
|
||||
</div>
|
||||
<div className="text-xs font-mono bg-slate-100 inline-block px-2 py-1 rounded text-slate-500 mb-3">Logistics • Universal</div>
|
||||
<div className="mb-3 bg-slate-50 rounded border border-slate-100 p-2 flex justify-center">
|
||||
<img src="/barcode-generator-preview.png" alt="Code 128 Barcode for Inventory and Shipping Labels" className="h-10 object-contain opacity-75 grayscale" width="200" height="40" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 m-0">
|
||||
Code 128 is a flexible barcode format that supports letters and numbers. It is commonly used in logistics, shipping, and internal tracking systems.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Code 39 Card */}
|
||||
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Factory className="w-5 h-5 text-orange-500" />
|
||||
<h4 className="text-lg font-bold text-slate-900 m-0">Code 39</h4>
|
||||
</div>
|
||||
<div className="text-xs font-mono bg-slate-100 inline-block px-2 py-1 rounded text-slate-500 mb-3">Industrial • Military</div>
|
||||
<div className="mb-3 bg-slate-50 rounded border border-slate-100 p-2 flex justify-center">
|
||||
<img src="/barcode-generator-preview.png" alt="Code 39 Barcode for Industrial Use" className="h-10 object-contain opacity-75 grayscale" width="200" height="40" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 m-0">
|
||||
The first alphanumeric barcode, Code 39 is still widely used in automotive and defense industries. It supports numbers and uppercase letters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* MSI Card */}
|
||||
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Layers className="w-5 h-5 text-purple-500" />
|
||||
<h4 className="text-lg font-bold text-slate-900 m-0">MSI</h4>
|
||||
</div>
|
||||
<div className="text-xs font-mono bg-slate-100 inline-block px-2 py-1 rounded text-slate-500 mb-3">Inventory • Shelves</div>
|
||||
<div className="mb-3 bg-slate-50 rounded border border-slate-100 p-2 flex justify-center">
|
||||
<img src="/barcode-generator-preview.png" alt="MSI Barcode for Inventory Management" className="h-10 object-contain opacity-75 grayscale" width="200" height="40" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 m-0">
|
||||
MSI (Modified Plessey) is often used for inventory control in retail environments, such as labeling shelves in supermarkets and warehouses.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Pharmacode Card */}
|
||||
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Activity className="w-5 h-5 text-red-500" />
|
||||
<h4 className="text-lg font-bold text-slate-900 m-0">Pharmacode</h4>
|
||||
</div>
|
||||
<div className="text-xs font-mono bg-slate-100 inline-block px-2 py-1 rounded text-slate-500 mb-3">Pharma • Packaging</div>
|
||||
<div className="mb-3 bg-slate-50 rounded border border-slate-100 p-2 flex justify-center">
|
||||
<img src="/barcode-generator-preview.png" alt="Pharmacode for Pharmaceutical Packaging" className="h-10 object-contain opacity-75 grayscale" width="200" height="40" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 m-0">
|
||||
Pharmacode is a specialized barcode standard used in the pharmaceutical industry for packaging control to prevent medication errors.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<h2>Why Use a Barcode Generator?</h2>
|
||||
<p>Using a Barcode Generator offers several advantages:</p>
|
||||
<div className="not-prose grid gap-4 mb-8">
|
||||
<div className="flex gap-4 items-start">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 mt-1 shrink-0" />
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 m-0">Speed</h5>
|
||||
<p className="text-slate-600 text-sm m-0">Create barcodes instantly without technical knowledge.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 items-start">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 mt-1 shrink-0" />
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 m-0">Accuracy</h5>
|
||||
<p className="text-slate-600 text-sm m-0">Reduce manual data entry errors.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 items-start">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 mt-1 shrink-0" />
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 m-0">Flexibility</h5>
|
||||
<p className="text-slate-600 text-sm m-0">Generate barcodes for different formats and use cases.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 items-start">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 mt-1 shrink-0" />
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 m-0">Cost-effective</h5>
|
||||
<p className="text-slate-600 text-sm m-0">No need for expensive software or hardware.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
For small businesses, online shops, and startups, a <strong>free Barcode Generator</strong> is often the easiest way to get started.
|
||||
</p>
|
||||
|
||||
<h2>Barcode vs QR Code</h2>
|
||||
<p>
|
||||
Although barcodes and QR codes are often confused, they serve different purposes. A barcode stores data horizontally and is mainly used for product identification. A <Link href="/tools/url-qr-code" className="text-blue-600 hover:underline">QR code</Link> stores data both horizontally and vertically and can contain more complex information such as URLs or <Link href="/tools/vcard-qr-code" className="text-blue-600 hover:underline">contact details</Link>.
|
||||
</p>
|
||||
<p>
|
||||
If you only need to identify products or inventory items, a classic barcode is usually the better choice.
|
||||
</p>
|
||||
|
||||
<h2>Are Barcodes Free to Use?</h2>
|
||||
<p>
|
||||
The barcode image itself can be generated for free using a Barcode Generator. However, for retail products sold internationally, the barcode number may need to be officially registered through organizations such as GS1. This ensures that the barcode is unique and recognized globally.
|
||||
</p>
|
||||
<p>
|
||||
For internal use, testing, or small projects, free barcode generation is usually sufficient.
|
||||
</p>
|
||||
|
||||
<h2>Use Cases for Barcodes</h2>
|
||||
<p>Barcodes are used in many industries, including:</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mb-8">
|
||||
<li>Retail and e-commerce</li>
|
||||
<li>Inventory and warehouse management</li>
|
||||
<li>Shipping and logistics</li>
|
||||
<li>Libraries and document tracking</li>
|
||||
<li>Event tickets and labeling</li>
|
||||
</ul>
|
||||
<p>
|
||||
A reliable <strong>Barcode Generator</strong> helps streamline these processes and improves efficiency.
|
||||
</p>
|
||||
|
||||
<h2>Understanding Check Digits</h2>
|
||||
<p>
|
||||
Most barcodes (like EAN and UPC) include a "Check Digit"—the last number in the sequence. This digit is calculated mathematically from the other numbers to ensure the barcode is scanned correctly. Even if a barcode is slightly damaged or scratched, the scanner uses the check digit to verify the integrity of the data.
|
||||
</p>
|
||||
|
||||
<h2>Best Practices for Printing Barcodes</h2>
|
||||
<p>
|
||||
To ensure your barcodes scan instantly at the checkout or in the warehouse, follow these printing tips:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mb-8">
|
||||
<li><strong>High Contrast:</strong> Always print black bars on a white background. Reverse colors (white bars on black) often fail to scan.</li>
|
||||
<li><strong>Quiet Zone:</strong> Leave enough white space (margins) on the left and right sides of the barcode.</li>
|
||||
<li><strong>Resolution:</strong> For professional labels, use <strong>SVG format</strong> (vector) or high-resolution PNGs (at least 300 DPI) to avoid blurry edges.</li>
|
||||
<li><strong>Size:</strong> Do not scale the barcode too small. Standard EAN-13 codes should be at least 30mm wide for reliable scanning.</li>
|
||||
</ul>
|
||||
|
||||
<hr className="my-12 border-slate-200" />
|
||||
|
||||
<div className="flex items-center gap-3 mb-6 not-prose">
|
||||
<HelpCircle className="w-6 h-6 text-blue-500" />
|
||||
<h2 className="text-2xl font-bold text-slate-900 m-0">Frequently Asked Questions (FAQ)</h2>
|
||||
</div>
|
||||
|
||||
<div className="not-prose space-y-8">
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 text-lg mb-2">❓ What is a Barcode Generator?</h5>
|
||||
<p className="text-slate-600">A Barcode Generator is an online tool that converts numbers or text into scannable barcode images that can be used for products, labels, and inventory systems.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 text-lg mb-2">❓ Is this barcode generator free to use?</h5>
|
||||
<p className="text-slate-600">Yes, our online barcode generator is completely free to use with no hidden costs or sign-ups required. You can generate, download, and print barcodes instantly.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 text-lg mb-2">❓ Which barcode format should I use?</h5>
|
||||
<p className="text-slate-600">
|
||||
<strong>EAN-13:</strong> Standard for retail products in Europe and globally.<br />
|
||||
<strong>UPC-A:</strong> Standard for retail products in USA/Canada.<br />
|
||||
<strong>Code 128:</strong> Best for logistics, shipping, and internal tracking (supports letters & numbers).
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 text-lg mb-2">❓ Can I download barcodes in vector format (SVG)?</h5>
|
||||
<p className="text-slate-600">Yes! We offer <strong>SVG downloads</strong>. SVG files are vector-based, meaning they can be scaled to any size without losing quality—perfect for professional product packaging.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 text-lg mb-2">❓ How do I generate a barcode online?</h5>
|
||||
<p className="text-slate-600">To generate a barcode online, enter your product number or text, select the desired barcode format (such as EAN-13 or Code 128), and click the generate button. The barcode will be created instantly.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 text-lg mb-2">❓ Are generated barcodes scannable?</h5>
|
||||
<p className="text-slate-600">Yes, barcodes generated with a proper barcode generator are fully scannable. We generate standard-compliant barcodes readable by any standard optical or laser barcode scanner.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 text-lg mb-2">❓ Can I use these barcodes for Amazon (EAN/UPC)?</h5>
|
||||
<p className="text-slate-600">You can generate the <em>image</em> for Amazon here if you already have your EAN/UPC number. However, you cannot "create" a valid global EAN number here—you must purchase those official numbers from GS1 to sell on major platforms like Amazon.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-bold text-slate-900 text-lg mb-2">❓ What is the difference between a barcode and a QR code?</h5>
|
||||
<p className="text-slate-600">A barcode stores data horizontally (1D) and is mainly used for product IDs. A QR code stores data in 2D (matrix) and can hold much more information, such as URLs, vCards, or WiFi credentials.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 p-6 bg-slate-900 rounded-xl text-white not-prose">
|
||||
<h4 className="text-lg font-bold mb-2">Final Thoughts</h4>
|
||||
<p className="text-slate-300 m-0">
|
||||
A Barcode Generator is a simple yet powerful tool that helps businesses save time, reduce errors, and improve operational efficiency. By choosing the right barcode format and using a reliable generator, you can create professional barcodes that work across different systems and industries.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
303
src/app/(marketing)/tools/barcode-generator/page.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
|
||||
import React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import BarcodeGeneratorClient from './BarcodeGeneratorClient';
|
||||
import { BarcodeGuide } from './BarcodeGuide';
|
||||
import { Barcode as BarcodeIcon, Shield, Zap, Printer, Download, Share2, Sparkles, Sliders, Check } from 'lucide-react';
|
||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Barcode Generator – Create Barcodes Online for Free',
|
||||
},
|
||||
description: 'Use a free Barcode Generator to create scannable barcodes online. Supports EAN, UPC and Code 128 for products, labels and inventory.',
|
||||
keywords: ['barcode generator', 'online barcode maker', 'create barcode free', 'ean-13 generator', 'upc-a generator', 'code 128 generator', 'barcode creator', 'printable barcodes'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/barcode-generator',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Barcode Generator: Create EAN, UPC & Code 128',
|
||||
description: 'Barcode Generator: Create professional labels instantly. Free & Secured.',
|
||||
url: 'https://www.qrmaster.net/tools/barcode-generator',
|
||||
siteName: 'QR Master',
|
||||
locale: 'en_US',
|
||||
type: 'website',
|
||||
images: [{ url: '/barcode-generator-preview.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Free Barcode Generator',
|
||||
description: 'Create custom barcodes in seconds. Download high-quality PNG/SVG.',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
};
|
||||
|
||||
// JSON-LD Structured Data
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
generateSoftwareAppSchema(
|
||||
'Barcode Generator',
|
||||
'Generate custom printable barcodes instantly for EAN, UPC, Code 128 and more.',
|
||||
'/og-barcode-generator.png',
|
||||
'UtilitiesApplication'
|
||||
),
|
||||
{
|
||||
'@type': 'HowTo',
|
||||
name: 'How to Create a Barcode',
|
||||
description: 'Create custom barcodes for products or inventory.',
|
||||
step: [
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
position: 1,
|
||||
name: 'Enter Content',
|
||||
text: 'Type or paste the numeric or alphanumeric data for your barcode.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
position: 2,
|
||||
name: 'Select Format',
|
||||
text: 'Choose the appropriate barcode type (e.g., Code 128 for general use, EAN-13 for retail).',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
position: 3,
|
||||
name: 'Customize Design',
|
||||
text: 'Adjust the height and width of the barcode to fit your needs.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
position: 4,
|
||||
name: 'Toggle Text',
|
||||
text: 'Decide if you want the human-readable value to appear below the barcode.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
position: 5,
|
||||
name: 'Download & Print',
|
||||
text: 'Save your barcode as PNG or SVG and print it for labels or inventory.',
|
||||
},
|
||||
],
|
||||
totalTime: 'PT20S',
|
||||
},
|
||||
generateFaqSchema({
|
||||
'What is a Barcode Generator?': {
|
||||
question: 'What is a Barcode Generator?',
|
||||
answer: 'A Barcode Generator is an online tool that converts numbers or text into scannable barcode images that can be used for products, labels, and inventory systems.',
|
||||
},
|
||||
'Is this barcode generator free to use?': {
|
||||
question: 'Is this barcode generator free to use?',
|
||||
answer: 'Yes, our online barcode generator is completely free to use with no hidden costs or sign-ups required. You can generate, download, and print barcodes instantly.',
|
||||
},
|
||||
'Which barcode format should I use?': {
|
||||
question: 'Which barcode format should I use?',
|
||||
answer: 'EAN-13 is standard for retail in Europe/Global. UPC-A is standard for retail in USA/Canada. Code 128 is best for logistics and internal tracking as it supports letters and numbers.',
|
||||
},
|
||||
'Can I download barcodes in vector format (SVG)?': {
|
||||
question: 'Can I download barcodes in vector format (SVG)?',
|
||||
answer: 'Yes! We offer SVG downloads. SVG files are vector-based, meaning they can be scaled to any size without losing quality—perfect for professional product packaging.',
|
||||
},
|
||||
'How do I generate a barcode online?': {
|
||||
question: 'How do I generate a barcode online?',
|
||||
answer: 'To generate a barcode online, enter your product number or text, select the desired barcode format (such as EAN-13 or Code 128), and click the generate button. The barcode will be created instantly.',
|
||||
},
|
||||
'Are generated barcodes scannable?': {
|
||||
question: 'Are generated barcodes scannable?',
|
||||
answer: 'Yes, barcodes generated with a proper barcode generator are fully scannable. We generate standard-compliant barcodes readable by any standard optical or laser barcode scanner.',
|
||||
},
|
||||
'Can I use these barcodes for Amazon (EAN/UPC)?': {
|
||||
question: 'Can I use these barcodes for Amazon (EAN/UPC)?',
|
||||
answer: 'You can generate the image for Amazon here if you already have your EAN/UPC number. However, you cannot "create" a valid global EAN number here—you must purchase those official numbers from GS1 to sell on major platforms like Amazon.',
|
||||
},
|
||||
'What is the difference between a barcode and a QR code?': {
|
||||
question: 'What is the difference between a barcode and a QR code?',
|
||||
answer: 'A barcode stores data horizontally (1D) and is mainly used for product IDs. A QR code stores data in 2D (matrix) and can hold much more information, such as URLs, vCards, or WiFi credentials.',
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default function BarcodeGeneratorPage() {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<ToolBreadcrumb toolName="Barcode Generator" toolSlug="barcode-generator" />
|
||||
|
||||
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
||||
|
||||
{/* HERO SECTION */}
|
||||
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-slate-900">
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
{/* Barcode Pattern */}
|
||||
<svg className="w-full h-full" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="barcode_pattern" width="60" height="60" patternUnits="userSpaceOnUse">
|
||||
<path d="M5 0 V 60 M15 0 V 60 M20 0 V 60 M35 0 V 60 M40 0 V 60 M55 0 V 60" stroke="white" strokeWidth="2" strokeOpacity="0.5" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#barcode_pattern)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
||||
<div className="text-center lg:text-left">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
||||
<span className="flex h-2 w-2 relative">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-400"></span>
|
||||
</span>
|
||||
Free Tool — Professional & Fast
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||
Free Online <span className="text-blue-400">Barcode Generator</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-lg md:text-xl text-slate-400 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||
Our <strong>barcode generator</strong> makes it easy to create and print high-quality labels for products and inventory.
|
||||
<span className="text-white block sm:inline mt-2 sm:mt-0"> Supports EAN, UPC, Code 128.</span>
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||
<Check className="w-4 h-4 text-blue-400" />
|
||||
Retail Ready
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||
<Check className="w-4 h-4 text-blue-400" />
|
||||
Vector SVG Export
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||
<Check className="w-4 h-4 text-blue-400" />
|
||||
No Registration
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visual Abstract */}
|
||||
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
||||
<div className="absolute w-[500px] h-[500px] bg-blue-500/10 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
||||
|
||||
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-2 hover:-rotate-1 transition-all duration-700 group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent rounded-3xl" />
|
||||
|
||||
<div className="w-full bg-gradient-to-br from-blue-400 to-indigo-600 rounded-xl shadow-lg p-5 mb-6 relative overflow-hidden text-white">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<BarcodeIcon className="w-8 h-8 opacity-80" />
|
||||
<div className="bg-white/20 px-2 py-1 rounded text-xs font-bold uppercase tracking-wider">Label</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold tracking-wider mb-1">PROD-98234</div>
|
||||
<div className="text-xs opacity-70">Inventory ID</div>
|
||||
</div>
|
||||
|
||||
<div className="w-48 h-32 bg-white rounded-xl p-4 shadow-inner relative overflow-hidden flex flex-col items-center justify-center">
|
||||
<div className="w-full h-16 bg-black flex gap-1 mb-2">
|
||||
{[2, 4, 1, 3, 2, 1, 4, 2, 1, 3].map((w, i) => (
|
||||
<div key={i} className="bg-black flex-1" style={{ flex: w }} />
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[10px] font-mono font-bold tracking-widest uppercase">98234001A</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Badge */}
|
||||
<div className="absolute -bottom-6 -right-6 bg-slate-900 border border-white/10 py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
||||
<div className="bg-blue-500/20 p-2 rounded-full">
|
||||
<Printer className="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Ready</div>
|
||||
<div className="text-sm font-bold text-white">Print Instantly</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* GENERATOR SECTION */}
|
||||
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
||||
<BarcodeGeneratorClient />
|
||||
</section>
|
||||
|
||||
{/* HOW IT WORKS */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
||||
How Our Barcode Generator Works
|
||||
</h2>
|
||||
|
||||
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
||||
<article className="text-center">
|
||||
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
||||
<Sliders className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold text-slate-900 mb-2">1. Configure</h3>
|
||||
<p className="text-slate-600 text-xs leading-relaxed">
|
||||
Enter your data and select the format.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="text-center">
|
||||
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
||||
<Sparkles className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold text-slate-900 mb-2">2. Customize</h3>
|
||||
<p className="text-slate-600 text-xs leading-relaxed">
|
||||
Adjust height, width and text display.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="text-center">
|
||||
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
||||
<Zap className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold text-slate-900 mb-2">3. Preview</h3>
|
||||
<p className="text-slate-600 text-xs leading-relaxed">
|
||||
See your barcode update in real-time.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="text-center">
|
||||
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
||||
<Download className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
|
||||
<p className="text-slate-600 text-xs leading-relaxed">
|
||||
Save as professional PNG or SVG.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="text-center">
|
||||
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
||||
<Printer className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold text-slate-900 mb-2">5. Print</h3>
|
||||
<p className="text-slate-600 text-xs leading-relaxed">
|
||||
Print labels directly from your browser.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* RELATED TOOLS */}
|
||||
<RelatedTools />
|
||||
|
||||
{/* SEO GUIDE */}
|
||||
<BarcodeGuide />
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 = {
|
||||
@@ -137,7 +138,7 @@ export default function CryptoGenerator() {
|
||||
<div className="grid lg:grid-cols-2">
|
||||
|
||||
{/* LEFT: Input Section */}
|
||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
|
||||
{/* Crypto Details */}
|
||||
<div className="space-y-6">
|
||||
@@ -158,6 +159,7 @@ export default function CryptoGenerator() {
|
||||
if (col) setQrColor(col);
|
||||
}}
|
||||
className="h-12 w-full rounded-xl border-slate-200"
|
||||
aria-label="Currency"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -209,7 +211,7 @@ export default function CryptoGenerator() {
|
||||
Wallet Direct
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
<p className="text-xs text-slate-600 mt-2">
|
||||
{qrMode === 'universal'
|
||||
? "Works with any phone camera. Opens blockchain explorer."
|
||||
: "Requires scanning from a wallet app. Enables direct payment."}
|
||||
@@ -251,7 +253,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-4 gap-2">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{FRAME_OPTIONS.map((frame) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
@@ -272,13 +274,12 @@ export default function CryptoGenerator() {
|
||||
</div>
|
||||
|
||||
{/* RIGHT: Preview Section */}
|
||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
<div className="p-6 md: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-8 flex flex-col items-center"
|
||||
style={{ minWidth: '320px' }}
|
||||
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||
>
|
||||
{/* Frame Label */}
|
||||
{getFrameLabel() && (
|
||||
@@ -320,7 +321,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-500 mt-1 truncate px-2">
|
||||
<div className="text-xs text-slate-600 mt-1 truncate px-2">
|
||||
{address || 'Wallet Address'}
|
||||
</div>
|
||||
</div>
|
||||
@@ -345,13 +346,15 @@ export default function CryptoGenerator() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
||||
<p className="text-xs text-slate-600 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">
|
||||
|
||||
@@ -4,20 +4,24 @@ 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: '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'],
|
||||
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'],
|
||||
alternates: {
|
||||
canonical: 'https://qrmaster.io/tools/crypto-qr-code',
|
||||
canonical: 'https://www.qrmaster.net/tools/crypto-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Crypto QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes to accept Crypto payments securely. Supports BTC, ETH, SOL.',
|
||||
type: 'website',
|
||||
url: 'https://qrmaster.io/tools/crypto-qr-code',
|
||||
url: 'https://www.qrmaster.net/tools/crypto-qr-code',
|
||||
images: [{ url: '/og-crypto-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
@@ -35,23 +39,12 @@ export const metadata: Metadata = {
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
{
|
||||
'@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.',
|
||||
},
|
||||
generateSoftwareAppSchema(
|
||||
'Crypto QR Code Generator',
|
||||
'Generate QR codes that contain your cryptocurrency wallet address for easy payments.',
|
||||
'/og-crypto-generator.png',
|
||||
'FinanceApplication'
|
||||
),
|
||||
{
|
||||
'@type': 'HowTo',
|
||||
name: 'How to Create a Crypto QR Code',
|
||||
@@ -90,51 +83,28 @@ const jsonLd = {
|
||||
],
|
||||
totalTime: 'PT30S',
|
||||
},
|
||||
{
|
||||
'@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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@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.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
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.',
|
||||
},
|
||||
'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.',
|
||||
},
|
||||
'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.',
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -303,6 +273,9 @@ 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">
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function EmailGenerator() {
|
||||
<div className="grid lg:grid-cols-2">
|
||||
|
||||
{/* LEFT: Input Section */}
|
||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
<div className="p-6 md: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-4 gap-2">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{FRAME_OPTIONS.map((frame) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
@@ -213,13 +213,12 @@ export default function EmailGenerator() {
|
||||
</div>
|
||||
|
||||
{/* RIGHT: Preview Section */}
|
||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
<div className="p-6 md: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-8 flex flex-col items-center"
|
||||
style={{ minWidth: '320px' }}
|
||||
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||
>
|
||||
{/* Frame Label */}
|
||||
{getFrameLabel() && (
|
||||
@@ -273,7 +272,7 @@ export default function EmailGenerator() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
||||
<p className="text-xs text-slate-600 mt-4 text-center">
|
||||
100% free. No signup required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||