SEO/AEO
This commit is contained in:
parent
cd3ee5fc8f
commit
254e6490b8
|
|
@ -15,7 +15,10 @@
|
|||
"Read(//c/Users/a931627/.ssh/**)",
|
||||
"Bash(ssh-keygen:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(git remote add:*)"
|
||||
"Bash(git remote add:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git remote set-url:*)",
|
||||
"Bash(npm install:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/** @type {import('next-sitemap').IConfig} */
|
||||
module.exports = {
|
||||
siteUrl: 'https://www.qrmaster.com',
|
||||
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(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
images: {
|
||||
unoptimized: true
|
||||
images: {
|
||||
unoptimized: true,
|
||||
domains: ['www.qrmaster.com', 'qrmaster.com', 'images.qrmaster.com']
|
||||
},
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs']
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "14.2.18",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.1.1",
|
||||
"prisma": "^5.7.0",
|
||||
|
|
@ -120,6 +121,13 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@corex/deepmerge": {
|
||||
"version": "4.0.43",
|
||||
"resolved": "https://registry.npmjs.org/@corex/deepmerge/-/deepmerge-4.0.43.tgz",
|
||||
"integrity": "sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
|
||||
|
|
@ -5949,6 +5957,41 @@
|
|||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/next-sitemap": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.2.3.tgz",
|
||||
"integrity": "sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/iamvishnusankar/next-sitemap.git"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@corex/deepmerge": "^4.0.43",
|
||||
"@next/env": "^13.4.3",
|
||||
"fast-glob": "^3.2.12",
|
||||
"minimist": "^1.2.8"
|
||||
},
|
||||
"bin": {
|
||||
"next-sitemap": "bin/next-sitemap.mjs",
|
||||
"next-sitemap-cjs": "bin/next-sitemap.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/next-sitemap/node_modules/@next/env": {
|
||||
"version": "13.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.11.tgz",
|
||||
"integrity": "sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "14.2.18",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.1.1",
|
||||
"prisma": "^5.7.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
Sitemap: https://www.qrmaster.com/sitemap.xml
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://www.qrmaster.com/</loc>
|
||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.qrmaster.com/blog</loc>
|
||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.qrmaster.com/pricing</loc>
|
||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.qrmaster.com/faq</loc>
|
||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.qrmaster.com/blog/qr-code-analytics</loc>
|
||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# Placeholder for og-image.png
|
||||
# Replace public/static/og-image.png with actual 1200×630 branded image before production
|
||||
|
|
@ -60,12 +60,8 @@ export default function CreatePage() {
|
|||
const contentTypes = [
|
||||
{ value: 'URL', label: 'URL / Website' },
|
||||
{ value: 'WIFI', label: 'WiFi Network' },
|
||||
{ value: 'VCARD', label: 'Contact Card' },
|
||||
{ value: 'PHONE', label: 'Phone Number' },
|
||||
{ value: 'EMAIL', label: 'Email' },
|
||||
{ value: 'SMS', label: 'SMS' },
|
||||
{ value: 'TEXT', label: 'Plain Text' },
|
||||
{ value: 'WHATSAPP', label: 'WhatsApp' },
|
||||
{ value: 'PHONE', label: 'Phone Number' },
|
||||
];
|
||||
|
||||
// Get QR content based on content type
|
||||
|
|
@ -159,7 +155,7 @@ export default function CreatePage() {
|
|||
title,
|
||||
contentType,
|
||||
content,
|
||||
isStatic: !isDynamic, // Add this flag
|
||||
isStatic: !isDynamic,
|
||||
tags: tags.split(',').map(t => t.trim()).filter(Boolean),
|
||||
style: {
|
||||
// FREE users can only use black/white
|
||||
|
|
@ -291,7 +287,7 @@ export default function CreatePage() {
|
|||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('create.title')}</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Create QR Code</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
|
|
@ -333,7 +329,7 @@ export default function CreatePage() {
|
|||
{/* QR Type Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('create.type')}</CardTitle>
|
||||
<CardTitle>QR Code Type</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center space-x-4">
|
||||
|
|
@ -354,22 +350,14 @@ export default function CreatePage() {
|
|||
onChange={() => setIsDynamic(false)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="font-medium">Static (Direct URL)</span>
|
||||
<span className="font-medium">Static</span>
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
{isDynamic
|
||||
{isDynamic
|
||||
? '✅ Dynamic: Track scans, edit URL later, view analytics. QR contains tracking link.'
|
||||
: '⚡ Static: Direct to URL, no tracking, cannot edit. QR contains actual URL.'}
|
||||
: '⚡ Static: Direct to content, no tracking, cannot edit. QR contains actual content.'}
|
||||
</p>
|
||||
{isDynamic && (
|
||||
<div className="mt-3 p-3 bg-blue-50 rounded-lg">
|
||||
<p className="text-sm text-blue-900">
|
||||
<strong>Note:</strong> Dynamic QR codes route through your server for tracking.
|
||||
In production, deploy your app to get a public URL instead of localhost.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { signOut } from 'next-auth/react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
|
@ -14,9 +13,21 @@ export default function AppLayout({
|
|||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const { t, locale, setLocale } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const handleSignOut = () => {
|
||||
// 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('/');
|
||||
};
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
|
|
@ -54,6 +65,16 @@ export default function AppLayout({
|
|||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '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 (
|
||||
|
|
@ -123,14 +144,6 @@ export default function AppLayout({
|
|||
</button>
|
||||
|
||||
<div className="flex items-center space-x-4 ml-auto">
|
||||
{/* Language Switcher */}
|
||||
<button
|
||||
onClick={() => setLocale(locale === 'en' ? 'de' : 'en')}
|
||||
className="text-gray-600 hover:text-gray-900 font-medium"
|
||||
>
|
||||
{locale === 'en' ? '🇩🇪 DE' : '🇬🇧 EN'}
|
||||
</button>
|
||||
|
||||
{/* User Menu */}
|
||||
<Dropdown
|
||||
align="right"
|
||||
|
|
@ -150,7 +163,7 @@ export default function AppLayout({
|
|||
</button>
|
||||
}
|
||||
>
|
||||
<DropdownItem onClick={() => signOut()}>
|
||||
<DropdownItem onClick={handleSignOut}>
|
||||
Sign Out
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ export default function PricingPage() {
|
|||
description: 'Privatnutzer & Testkunden',
|
||||
features: [
|
||||
'3 dynamische QR-Codes',
|
||||
'Basis-Tracking (Scans + Standort)',
|
||||
'Einfache Designs',
|
||||
'Unbegrenzte statische QR-Codes',
|
||||
'Basis-Scan-Tracking',
|
||||
'Standard QR-Design-Vorlagen',
|
||||
],
|
||||
cta: 'Get Started',
|
||||
popular: false,
|
||||
|
|
@ -50,11 +50,11 @@ export default function PricingPage() {
|
|||
priceYearly: 90,
|
||||
description: 'Selbstständige / kleine Firmen',
|
||||
features: [
|
||||
'50 QR-Codes',
|
||||
'Branding (Logo, Farben)',
|
||||
'Detaillierte Analytics',
|
||||
'CSV-Export',
|
||||
'Passwortschutz',
|
||||
'50 dynamische QR-Codes',
|
||||
'Unbegrenzte statische QR-Codes',
|
||||
'Erweiterte Analytik (Scans, Geräte, Standorte)',
|
||||
'Individuelles Branding (Farben & Logo)',
|
||||
'Download als SVG/PNG',
|
||||
],
|
||||
cta: 'Upgrade to Pro',
|
||||
popular: true,
|
||||
|
|
@ -69,11 +69,11 @@ export default function PricingPage() {
|
|||
priceYearly: 290,
|
||||
description: 'Agenturen / Startups',
|
||||
features: [
|
||||
'500 QR-Codes',
|
||||
'Team-Zugänge (bis 3 User)',
|
||||
'Benutzerdefinierte Domains',
|
||||
'White-Label',
|
||||
'Prioritäts-Support',
|
||||
'500 dynamische QR-Codes',
|
||||
'Unbegrenzte statische QR-Codes',
|
||||
'Alles aus Pro',
|
||||
'Prioritäts-E-Mail-Support',
|
||||
'Erweiterte Tracking & Insights',
|
||||
],
|
||||
cta: 'Upgrade to Business',
|
||||
popular: false,
|
||||
|
|
|
|||
|
|
@ -1,453 +1,146 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('profile');
|
||||
const { t, setLanguage, language } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Form states
|
||||
const [profile, setProfile] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
phone: '',
|
||||
});
|
||||
|
||||
// Load user data from localStorage
|
||||
React.useEffect(() => {
|
||||
// Form states
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(language || 'en');
|
||||
|
||||
// Load user data and language from localStorage
|
||||
useEffect(() => {
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
setProfile({
|
||||
name: user.name || '',
|
||||
email: user.email || '',
|
||||
company: user.company || '',
|
||||
phone: user.phone || '',
|
||||
});
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
setName(user.name || '');
|
||||
setEmail(user.email || '');
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user data:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved language preference
|
||||
const savedLocale = localStorage.getItem('locale');
|
||||
if (savedLocale && (savedLocale === 'en' || savedLocale === 'de')) {
|
||||
setSelectedLanguage(savedLocale);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
|
||||
const tabs = [
|
||||
{ id: 'profile', label: 'Profile', icon: '👤' },
|
||||
{ id: 'billing', label: 'Billing', icon: '💳' },
|
||||
{ id: 'team', label: 'Team & Roles', icon: '👥' },
|
||||
{ id: 'api', label: 'API Keys', icon: '🔑' },
|
||||
{ id: 'workspace', label: 'Workspace', icon: '🏢' },
|
||||
];
|
||||
|
||||
const generateApiKey = () => {
|
||||
const key = 'qrm_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
setApiKey(key);
|
||||
setShowApiKey(true);
|
||||
};
|
||||
|
||||
const handleSaveProfile = async () => {
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setLoading(false);
|
||||
|
||||
try {
|
||||
// Update language
|
||||
setLanguage(selectedLanguage);
|
||||
|
||||
// Update user data in localStorage
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
user.name = name;
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
showToast('Settings saved successfully!', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
showToast('Failed to save settings', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('settings.title')}</h1>
|
||||
<p className="text-gray-600 mt-2">{t('settings.subtitle')}</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Settings</h1>
|
||||
<p className="text-gray-600 mt-2">Manage your account settings and preferences</p>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-4 gap-8">
|
||||
{/* Sidebar */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card>
|
||||
<CardContent className="p-2">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full text-left px-4 py-3 rounded-lg flex items-center space-x-3 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-primary-50 text-primary-600'
|
||||
: 'hover:bg-gray-50 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="text-xl">{tab.icon}</span>
|
||||
<span className="font-medium">{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="lg:col-span-3">
|
||||
{/* Profile Tab */}
|
||||
{activeTab === 'profile' && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profile Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-20 h-20 bg-primary-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-2xl font-bold text-primary-600">JD</span>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="outline" size="sm">Change Photo</Button>
|
||||
<p className="text-sm text-gray-500 mt-1">JPG, PNG or GIF. Max 2MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Full Name"
|
||||
value={profile.name}
|
||||
onChange={(e) => setProfile({ ...profile, name: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={profile.email}
|
||||
onChange={(e) => setProfile({ ...profile, email: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label="Company"
|
||||
value={profile.company}
|
||||
onChange={(e) => setProfile({ ...profile, company: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label="Phone"
|
||||
value={profile.phone}
|
||||
onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSaveProfile} loading={loading}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Billing Tab */}
|
||||
{activeTab === 'billing' && (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Current Plan</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="text-2xl font-bold text-gray-900">Pro Plan</h3>
|
||||
<Badge variant="success">Active</Badge>
|
||||
</div>
|
||||
<p className="text-gray-600 mt-1">€9/month • Renews on Jan 1, 2025</p>
|
||||
</div>
|
||||
<Button variant="outline">Change Plan</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-600 mb-1">QR Codes</p>
|
||||
<p className="text-xl font-bold text-gray-900">234 / 500</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
||||
<div className="bg-primary-600 h-2 rounded-full" style={{ width: '46.8%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-600 mb-1">Scans</p>
|
||||
<p className="text-xl font-bold text-gray-900">45,678 / 100,000</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
||||
<div className="bg-primary-600 h-2 rounded-full" style={{ width: '45.7%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-600 mb-1">API Calls</p>
|
||||
<p className="text-xl font-bold text-gray-900">12,345 / 50,000</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
||||
<div className="bg-primary-600 h-2 rounded-full" style={{ width: '24.7%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Payment Method</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-8 bg-gradient-to-r from-blue-600 to-blue-400 rounded flex items-center justify-center">
|
||||
<span className="text-white text-xs font-bold">VISA</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">•••• •••• •••• 4242</p>
|
||||
<p className="text-sm text-gray-500">Expires 12/25</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Update</Button>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full mt-4">
|
||||
Add Payment Method
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Billing History</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ date: 'Dec 1, 2024', amount: '€9.00', status: 'Paid' },
|
||||
{ date: 'Nov 1, 2024', amount: '€9.00', status: 'Paid' },
|
||||
{ date: 'Oct 1, 2024', amount: '€9.00', status: 'Paid' },
|
||||
].map((invoice, index) => (
|
||||
<div key={index} className="flex items-center justify-between py-3 border-b last:border-0">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{invoice.date}</p>
|
||||
<p className="text-sm text-gray-500">Pro Plan Monthly</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Badge variant="success">{invoice.status}</Badge>
|
||||
<span className="font-medium text-gray-900">{invoice.amount}</span>
|
||||
<Button variant="outline" size="sm">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-6">
|
||||
{/* Profile Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profile Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Enter your name"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Team Tab */}
|
||||
{activeTab === 'team' && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Team Members</CardTitle>
|
||||
<Button size="sm">Invite Member</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ name: 'John Doe', email: 'john@example.com', role: 'Owner', status: 'Active' },
|
||||
{ name: 'Jane Smith', email: 'jane@example.com', role: 'Admin', status: 'Active' },
|
||||
{ name: 'Bob Johnson', email: 'bob@example.com', role: 'Editor', status: 'Active' },
|
||||
{ name: 'Alice Brown', email: 'alice@example.com', role: 'Viewer', status: 'Pending' },
|
||||
].map((member, index) => (
|
||||
<div key={index} className="flex items-center justify-between py-3 border-b last:border-0">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center">
|
||||
<span className="text-sm font-medium text-gray-600">
|
||||
{member.name.split(' ').map(n => n[0]).join('')}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{member.name}</p>
|
||||
<p className="text-sm text-gray-500">{member.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Badge variant={member.status === 'Active' ? 'success' : 'warning'}>
|
||||
{member.status}
|
||||
</Badge>
|
||||
<select className="px-3 py-1 border rounded-lg text-sm">
|
||||
<option value="owner" selected={member.role === 'Owner'}>Owner</option>
|
||||
<option value="admin" selected={member.role === 'Admin'}>Admin</option>
|
||||
<option value="editor" selected={member.role === 'Editor'}>Editor</option>
|
||||
<option value="viewer" selected={member.role === 'Viewer'}>Viewer</option>
|
||||
</select>
|
||||
{member.role !== 'Owner' && (
|
||||
<Button variant="outline" size="sm">Remove</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-info-50 rounded-lg">
|
||||
<p className="text-sm text-info-900">
|
||||
<strong>Team Seats:</strong> 4 of 5 used
|
||||
</p>
|
||||
<p className="text-sm text-info-700 mt-1">
|
||||
Upgrade to Business plan for unlimited team members
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* API Keys Tab */}
|
||||
{activeTab === 'api' && (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API Access</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-600 mb-4">
|
||||
Use API keys to integrate QR Master with your applications. Keep your keys secure and never share them publicly.
|
||||
</p>
|
||||
<div className="p-4 bg-warning-50 rounded-lg">
|
||||
<p className="text-sm text-warning-900">
|
||||
<strong>⚠️ Warning:</strong> API keys provide full access to your account. Treat them like passwords.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!apiKey ? (
|
||||
<Button onClick={generateApiKey}>Generate New API Key</Button>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Your API Key</label>
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
value={apiKey}
|
||||
readOnly
|
||||
className="font-mono"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowApiKey(!showApiKey)}
|
||||
>
|
||||
{showApiKey ? 'Hide' : 'Show'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigator.clipboard.writeText(apiKey)}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
This key will only be shown once. Store it securely.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API Documentation</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-2">Base URL</h4>
|
||||
<code className="block p-3 bg-gray-100 rounded text-sm">
|
||||
https://api.qrmaster.com/v1
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-2">Authentication</h4>
|
||||
<code className="block p-3 bg-gray-100 rounded text-sm">
|
||||
Authorization: Bearer YOUR_API_KEY
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-2">Example Request</h4>
|
||||
<pre className="p-3 bg-gray-100 rounded text-sm overflow-x-auto">
|
||||
{`curl -X POST https://api.qrmaster.com/v1/qr-codes \\
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"title":"My QR","content":"https://example.com"}'`}
|
||||
</pre>
|
||||
</div>
|
||||
<Button variant="outline">View Full Documentation</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
disabled
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500 cursor-not-allowed"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Email cannot be changed
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Workspace Tab */}
|
||||
{activeTab === 'workspace' && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Workspace Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Workspace Name</label>
|
||||
<Input value="Acme Corp" />
|
||||
</div>
|
||||
{/* Language Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Language Preferences</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Display Language
|
||||
</label>
|
||||
<select
|
||||
value={selectedLanguage}
|
||||
onChange={(e) => setSelectedLanguage(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option value="en">English</option>
|
||||
<option value="de">Deutsch (German)</option>
|
||||
</select>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Choose your preferred language for the interface
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Workspace URL</label>
|
||||
<div className="flex">
|
||||
<span className="inline-flex items-center px-3 rounded-l-lg border border-r-0 border-gray-300 bg-gray-50 text-gray-500 text-sm">
|
||||
qrmaster.com/
|
||||
</span>
|
||||
<Input value="acme-corp" className="rounded-l-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Default QR Settings</label>
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center">
|
||||
<input type="checkbox" className="mr-2" defaultChecked />
|
||||
<span className="text-sm text-gray-700">Auto-generate slugs for dynamic QR codes</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input type="checkbox" className="mr-2" defaultChecked />
|
||||
<span className="text-sm text-gray-700">Track scan analytics by default</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input type="checkbox" className="mr-2" />
|
||||
<span className="text-sm text-gray-700">Require approval for new QR codes</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t">
|
||||
<h4 className="text-lg font-medium text-gray-900 mb-4">Danger Zone</h4>
|
||||
<div className="p-4 border border-red-200 rounded-lg bg-red-50">
|
||||
<p className="text-sm text-red-900 mb-3">
|
||||
Deleting your workspace will permanently remove all QR codes, analytics data, and team members.
|
||||
</p>
|
||||
<Button variant="outline" className="border-red-300 text-red-600 hover:bg-red-100">
|
||||
Delete Workspace
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button>Save Changes</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
size="lg"
|
||||
variant="primary"
|
||||
>
|
||||
{loading ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,157 +1,324 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { notFound } from 'next/navigation';
|
||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
||||
import { blogPostingSchema, breadcrumbSchema, howToSchema } from '@/lib/schema';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
|
||||
const blogContent = {
|
||||
'qr-codes-im-restaurant': {
|
||||
title: 'QR-Codes im Restaurant: Die digitale Revolution der Speisekarte',
|
||||
date: '2024-01-15',
|
||||
readTime: '5 Min',
|
||||
category: 'Gastronomie',
|
||||
content: `
|
||||
<p>Die Gastronomie hat sich in den letzten Jahren stark digitalisiert, und QR-Codes spielen dabei eine zentrale Rolle. Von kontaktlosen Speisekarten bis hin zu digitalen Zahlungssystemen – QR-Codes revolutionieren die Art und Weise, wie Restaurants mit ihren Gästen interagieren.</p>
|
||||
|
||||
<h2>Vorteile für Restaurants</h2>
|
||||
<ul>
|
||||
<li>Kostenersparnis durch digitale Speisekarten</li>
|
||||
<li>Einfache Aktualisierung von Preisen und Angeboten</li>
|
||||
<li>Hygienische, kontaktlose Lösung</li>
|
||||
<li>Mehrsprachige Menüs ohne zusätzliche Druckkosten</li>
|
||||
</ul>
|
||||
|
||||
<h2>Vorteile für Gäste</h2>
|
||||
<ul>
|
||||
<li>Schneller Zugriff auf aktuelle Informationen</li>
|
||||
<li>Detaillierte Produktbeschreibungen und Allergeninformationen</li>
|
||||
<li>Einfache Bestellung und Bezahlung</li>
|
||||
<li>Personalisierte Empfehlungen</li>
|
||||
</ul>
|
||||
|
||||
<p>Die Implementierung von QR-Codes in Ihrem Restaurant ist einfacher als Sie denken. Mit QR Master können Sie in wenigen Minuten professionelle QR-Codes erstellen, die perfekt zu Ihrem Branding passen.</p>
|
||||
`,
|
||||
},
|
||||
'dynamische-vs-statische-qr-codes': {
|
||||
title: 'Dynamische vs. Statische QR-Codes: Was ist der Unterschied?',
|
||||
date: '2024-01-10',
|
||||
readTime: '3 Min',
|
||||
category: 'Grundlagen',
|
||||
content: `
|
||||
<p>Bei der Erstellung von QR-Codes stehen Sie vor der Wahl zwischen statischen und dynamischen Codes. Beide haben ihre Vor- und Nachteile, und die richtige Wahl hängt von Ihrem spezifischen Anwendungsfall ab.</p>
|
||||
|
||||
<h2>Statische QR-Codes</h2>
|
||||
<p>Statische QR-Codes enthalten die Informationen direkt im Code selbst. Einmal erstellt, können sie nicht mehr geändert werden.</p>
|
||||
<ul>
|
||||
<li>Kostenlos und unbegrenzt nutzbar</li>
|
||||
<li>Funktionieren für immer ohne Server</li>
|
||||
<li>Ideal für permanente Informationen</li>
|
||||
<li>Keine Tracking-Möglichkeiten</li>
|
||||
</ul>
|
||||
|
||||
<h2>Dynamische QR-Codes</h2>
|
||||
<p>Dynamische QR-Codes verweisen auf eine URL, die Sie jederzeit ändern können.</p>
|
||||
<ul>
|
||||
<li>Inhalt kann nachträglich geändert werden</li>
|
||||
<li>Detaillierte Scan-Statistiken</li>
|
||||
<li>Kürzere, sauberere QR-Codes</li>
|
||||
<li>Perfekt für Marketing-Kampagnen</li>
|
||||
</ul>
|
||||
|
||||
<p>Mit QR Master können Sie beide Arten von QR-Codes erstellen und verwalten. Unsere Plattform bietet Ihnen die Flexibilität, die Sie für Ihre Projekte benötigen.</p>
|
||||
`,
|
||||
},
|
||||
'qr-code-marketing-strategien': {
|
||||
title: 'QR-Code Marketing-Strategien für 2024',
|
||||
date: '2024-01-05',
|
||||
readTime: '7 Min',
|
||||
category: 'Marketing',
|
||||
content: `
|
||||
<p>QR-Codes sind zu einem unverzichtbaren Werkzeug im modernen Marketing geworden. Hier sind die effektivsten Strategien für 2024.</p>
|
||||
|
||||
<h2>1. Personalisierte Kundenerlebnisse</h2>
|
||||
<p>Nutzen Sie dynamische QR-Codes, um personalisierte Landingpages basierend auf Standort, Zeit oder Kundenverhalten zu erstellen.</p>
|
||||
|
||||
<h2>2. Social Media Integration</h2>
|
||||
<p>Verbinden Sie QR-Codes mit Ihren Social-Media-Kampagnen für nahtlose Cross-Channel-Erlebnisse.</p>
|
||||
|
||||
<h2>3. Event-Marketing</h2>
|
||||
<p>Von Tickets bis zu Networking – QR-Codes machen Events interaktiver und messbar.</p>
|
||||
|
||||
<h2>4. Loyalty-Programme</h2>
|
||||
<p>Digitale Treuekarten und Rabattaktionen lassen sich perfekt mit QR-Codes umsetzen.</p>
|
||||
|
||||
<h2>5. Analytics und Optimierung</h2>
|
||||
<p>Nutzen Sie die Tracking-Funktionen, um Ihre Kampagnen kontinuierlich zu verbessern.</p>
|
||||
|
||||
<p>Mit QR Master haben Sie alle Tools, die Sie für erfolgreiches QR-Code-Marketing benötigen. Starten Sie noch heute mit Ihrer ersten Kampagne!</p>
|
||||
`,
|
||||
interface BlogPostData {
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
date: string;
|
||||
datePublished: string;
|
||||
dateModified: string;
|
||||
readTime: string;
|
||||
category: string;
|
||||
image: string;
|
||||
imageAlt: string;
|
||||
author: string;
|
||||
authorUrl: string;
|
||||
answer?: string;
|
||||
howTo?: any;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const blogPosts: Record<string, BlogPostData> = {
|
||||
'qr-code-analytics': {
|
||||
slug: 'qr-code-analytics',
|
||||
title: 'QR Code Analytics: Track, Measure & Optimize',
|
||||
excerpt: 'Master scan analytics, campaign tracking & dashboard insights to maximize QR ROI with dynamic codes.',
|
||||
date: 'October 16, 2025',
|
||||
datePublished: '2025-10-16T09:00:00Z',
|
||||
dateModified: '2025-10-16T09:00:00Z',
|
||||
readTime: '8 Min',
|
||||
category: 'Analytics',
|
||||
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=1200&q=80',
|
||||
imageAlt: 'QR code analytics dashboard showing real-time scan metrics and campaign performance data',
|
||||
author: 'QR Master Team',
|
||||
authorUrl: 'https://www.qrmaster.com/about',
|
||||
answer: 'QR code analytics empowers marketers to track scan rates, user behavior, and campaign ROI through real-time dashboards, enabling data-driven optimization of dynamic QR codes and branded marketing campaigns.',
|
||||
howTo: {
|
||||
name: 'How to Track QR Code Scans',
|
||||
description: 'Step-by-step guide to setting up and monitoring QR code analytics',
|
||||
totalTime: 'PT10M',
|
||||
steps: [
|
||||
{
|
||||
name: 'Create a Dynamic QR Code',
|
||||
text: 'Log into your QR Master dashboard and select "Create Dynamic QR Code". Enter your destination URL and customize design options.',
|
||||
url: 'https://www.qrmaster.com/create',
|
||||
},
|
||||
{
|
||||
name: 'Enable UTM Tracking',
|
||||
text: 'Add UTM parameters (source, medium, campaign) to track the QR code in Google Analytics and marketing platforms.',
|
||||
},
|
||||
{
|
||||
name: 'Access Analytics Dashboard',
|
||||
text: 'Navigate to Dashboard → Analytics to view real-time scan data, geographic distribution, and device breakdowns.',
|
||||
url: 'https://www.qrmaster.com/analytics',
|
||||
},
|
||||
],
|
||||
},
|
||||
content: `<div class="blog-content">
|
||||
<h2>What Are Scan Analytics?</h2>
|
||||
<p>Scan analytics provide comprehensive insights into how users interact with your QR codes. Our advanced dashboard tracks scan analytics including geographic location, device types, scan timestamps, and user engagement patterns. For marketers running dynamic QR code campaigns, these insights are essential for understanding campaign tracking performance and optimizing conversion rates.</p>
|
||||
<p>With branded QR codes deployed across print materials, event tickets, and business cards, scan analytics reveal which channels drive the highest engagement. Security features ensure all data collection is GDPR-compliant, protecting user privacy while delivering actionable campaign tracking insights.</p>
|
||||
|
||||
<h2>How to Set Up QR Code Analytics</h2>
|
||||
<h3>Step 1: Create a Dynamic QR Code</h3>
|
||||
<p>Start by generating a dynamic QR code in your QR Master dashboard. Unlike static codes, dynamic QR codes allow you to update destination URLs and track every scan through our analytics platform.</p>
|
||||
|
||||
<h3>Step 2: Enable Campaign Tracking</h3>
|
||||
<p>Configure UTM parameters for your QR codes to integrate with Google Analytics and marketing automation platforms. UTM tracking allows you to attribute conversions, measure ROI, and segment campaign performance by source, medium, and campaign name.</p>
|
||||
|
||||
<h3>Step 3: Access Your Analytics Dashboard</h3>
|
||||
<p>Navigate to the scan analytics dashboard to view real-time reports. Monitor scan rates, geographic distribution, device breakdowns, and time-series data. Set up automated reports to track campaign tracking metrics over time.</p>
|
||||
|
||||
<h3>Step 4: Optimize Based on Insights</h3>
|
||||
<p>Use scan analytics to identify high-performing campaigns and optimize underperforming ones. A/B test different branded QR designs, placement strategies, and call-to-action messaging to maximize engagement and conversion rates.</p>
|
||||
|
||||
<h2>Key Metrics in QR Code Analytics</h2>
|
||||
<h3>Scan Rates and Volume</h3>
|
||||
<p>Track total scans, unique scans, and scan velocity. Scan rates reveal campaign momentum and help identify viral growth patterns. Compare scan volumes across different branded QR variations to determine which designs perform best.</p>
|
||||
|
||||
<h3>Geographic Distribution</h3>
|
||||
<p>Understand where your audience is scanning from. Geographic analytics support localized marketing strategies and event tracking for conferences, trade shows, and retail activations.</p>
|
||||
|
||||
<h3>Device and Browser Analytics</h3>
|
||||
<p>Know whether users scan from iOS or Android devices, which browsers they use, and screen resolutions. This data informs mobile optimization strategies and ensures your landing pages deliver seamless experiences across all devices.</p>
|
||||
|
||||
<h3>Time-Based Patterns</h3>
|
||||
<p>Identify peak scanning hours, days of the week, and seasonal trends. Time-based analytics optimize campaign timing for email blasts, social media posts, and print QR deployments.</p>
|
||||
|
||||
<h3>Conversion Tracking</h3>
|
||||
<p>Measure downstream actions after the scan—form submissions, purchases, app downloads, or content engagement. Integrate with your CRM and marketing stack to attribute revenue to specific QR campaigns.</p>
|
||||
|
||||
<h2>Advanced Campaign Tracking Strategies</h2>
|
||||
<h3>UTM Tracking Integration</h3>
|
||||
<p>Append UTM parameters to your dynamic QR URLs for granular campaign attribution. Use consistent naming conventions across campaigns to compare performance in Google Analytics. UTM tracking bridges offline and online marketing, providing a unified view of customer journeys.</p>
|
||||
|
||||
<h3>Multi-Channel Attribution</h3>
|
||||
<p>Deploy branded QR codes across print ads, packaging, event tickets, business cards, and signage. Use unique QR codes for each channel to measure which touchpoints drive the highest ROI. Multi-channel attribution reveals the true value of integrated marketing campaigns.</p>
|
||||
|
||||
<h3>A/B Testing QR Designs</h3>
|
||||
<p>Test different branded QR styles—color schemes, logo placements, and call-to-action text—to optimize scan rates. Our analytics dashboard makes it easy to compare performance and roll out winning variations at scale.</p>
|
||||
|
||||
<h3>Retargeting and Remarketing</h3>
|
||||
<p>Leverage scan analytics to build retargeting audiences. Users who scan but don't convert can be re-engaged with display ads, email campaigns, and social media retargeting, boosting overall campaign ROI.</p>
|
||||
|
||||
<h2>Security and Compliance in QR Analytics</h2>
|
||||
<p>All QR Master scan analytics are GDPR-compliant, ensuring user data is collected, stored, and processed securely. We employ enterprise-grade security protocols to protect sensitive campaign data, making our platform ideal for bulk QR generation workflows in regulated industries.</p>
|
||||
<p>Secure QR codes prevent unauthorized access and malicious redirects. Our platform includes link validation, SSL encryption, and fraud detection to maintain trust and protect your brand reputation.</p>
|
||||
|
||||
<h2>Use Cases for QR Code Analytics</h2>
|
||||
<h3>Event Tracking</h3>
|
||||
<p>Deploy QR codes on event tickets, badges, and signage to track attendee engagement. Scan analytics reveal which sessions attract the most interest, optimize check-in flows, and measure event ROI.</p>
|
||||
|
||||
<h3>Print Marketing Campaigns</h3>
|
||||
<p>Use QR codes in magazine ads, direct mail, and packaging to bridge offline and online channels. Campaign tracking quantifies print campaign performance and justifies marketing spend.</p>
|
||||
|
||||
<h3>Business Card Analytics</h3>
|
||||
<p>Add dynamic QR codes to business cards to track networking effectiveness. Scan analytics show how many contacts engage, when they scan, and which follow-up actions they take.</p>
|
||||
|
||||
<h3>Bulk QR Generation for Retail</h3>
|
||||
<p>Generate thousands of product QR codes with our bulk QR tool. Track scan analytics at the SKU level to understand customer interest, optimize inventory, and personalize marketing.</p>
|
||||
|
||||
<h3>API-Driven Automation</h3>
|
||||
<p>Integrate QR code generation and analytics into your marketing automation platform via our API. Automate bulk QR creation, dynamic URL updates, and reporting workflows for enterprise-scale campaigns.</p>
|
||||
|
||||
<h2>Maximizing ROI with Scan Analytics</h2>
|
||||
<p>To maximize QR code ROI, continuously monitor scan analytics and iterate on campaign strategies. Test different branded QR designs, optimize UTM parameters, and leverage multi-channel attribution to understand the full customer journey.</p>
|
||||
<p>Combine scan analytics with customer data platforms (CDPs) and CRMs to personalize follow-up communications. Segment audiences based on scan behavior and deliver targeted offers that drive conversions.</p>
|
||||
<p>For bulk QR campaigns, use our analytics dashboard to identify trends across thousands of codes. Aggregate data reveals macro patterns while code-level metrics enable micro-optimizations.</p>
|
||||
|
||||
<h2>Conclusion</h2>
|
||||
<p>QR code analytics transforms QR codes from simple links into powerful marketing instruments. By tracking scan rates, user behavior, and campaign performance through advanced dashboards, marketers gain the insights needed to optimize dynamic QR campaigns, enhance branded experiences, and achieve measurable ROI.</p>
|
||||
<p>Whether you're deploying QR codes for event tracking, print marketing, bulk generation, or API-driven automation, scan analytics provides the data foundation for smarter, more effective campaigns. Start leveraging QR analytics today to unlock the full potential of your QR marketing strategy.</p>
|
||||
</div>`,
|
||||
},
|
||||
};
|
||||
|
||||
export default function BlogPostPage() {
|
||||
const params = useParams();
|
||||
const slug = params?.slug as string;
|
||||
const post = blogContent[slug as keyof typeof blogContent];
|
||||
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({ params }: { params: { slug: string } }): Promise<Metadata> {
|
||||
const post = blogPosts[params.slug];
|
||||
|
||||
if (!post) {
|
||||
return (
|
||||
<div className="py-20">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">Post not found</h1>
|
||||
<p className="text-xl text-gray-600 mb-8">The blog post you're looking for doesn't exist.</p>
|
||||
<Link href="/blog">
|
||||
<Button>Back to Blog</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return {
|
||||
title: 'Post Not Found',
|
||||
};
|
||||
}
|
||||
|
||||
const title = truncateAtWord(`${post.title} - QR Analytics Tips`, 60);
|
||||
const description = truncateAtWord(post.excerpt, 160);
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: `https://www.qrmaster.com/blog/${params.slug}`,
|
||||
languages: {
|
||||
'x-default': `https://www.qrmaster.com/blog/${params.slug}`,
|
||||
en: `https://www.qrmaster.com/blog/${params.slug}`,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: `https://www.qrmaster.com/blog/${params.slug}`,
|
||||
type: 'article',
|
||||
publishedTime: post.datePublished,
|
||||
modifiedTime: post.dateModified,
|
||||
authors: [post.author],
|
||||
images: [
|
||||
{
|
||||
url: post.image,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: post.imageAlt,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
card: 'summary_large_image',
|
||||
images: [post.image],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function BlogPostPage({ params }: { params: { slug: string } }) {
|
||||
const post = blogPosts[params.slug];
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Blog', url: '/blog' },
|
||||
{ name: post.title, url: `/blog/${post.slug}` },
|
||||
];
|
||||
|
||||
const schemas = [
|
||||
blogPostingSchema({
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
slug: post.slug,
|
||||
author: post.author,
|
||||
authorUrl: post.authorUrl,
|
||||
datePublished: post.datePublished,
|
||||
dateModified: post.dateModified,
|
||||
image: post.image,
|
||||
}),
|
||||
breadcrumbSchema(breadcrumbItems),
|
||||
];
|
||||
|
||||
if (post.howTo) {
|
||||
schemas.push(howToSchema(post.howTo));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Link href="/blog" className="inline-flex items-center text-primary-600 hover:text-primary-700 mb-8">
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Blog
|
||||
</Link>
|
||||
<>
|
||||
<SeoJsonLd data={schemas} />
|
||||
<div className="py-20 bg-gradient-to-b from-gray-50 to-white">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Breadcrumbs items={breadcrumbItems} />
|
||||
|
||||
<article>
|
||||
<header className="mb-8">
|
||||
<div className="flex items-center space-x-4 mb-4">
|
||||
<Badge variant="info">{post.category}</Badge>
|
||||
<span className="text-gray-500">{post.readTime}</span>
|
||||
<span className="text-gray-500">{post.date}</span>
|
||||
<article className="bg-white rounded-2xl shadow-sm p-8 md:p-12">
|
||||
<header className="mb-10">
|
||||
<div className="flex items-center flex-wrap gap-3 mb-6">
|
||||
<Badge variant="info">{post.category}</Badge>
|
||||
<span className="text-gray-500 flex items-center">
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{post.readTime} read
|
||||
</span>
|
||||
<span className="text-gray-500">By {post.author}</span>
|
||||
<span className="text-gray-500">{post.date}</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6">
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
{post.answer && (
|
||||
<div className="bg-blue-50 border-l-4 border-blue-500 p-6 mb-8 rounded-r-lg">
|
||||
<h2 className="text-xl font-semibold mb-2 text-gray-900">Quick Answer</h2>
|
||||
<p className="text-lg text-gray-800 leading-relaxed">{post.answer}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative w-full h-96 rounded-2xl overflow-hidden shadow-lg mb-8">
|
||||
<Image
|
||||
src={post.image}
|
||||
alt={post.imageAlt}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
className="prose prose-lg max-w-none
|
||||
prose-headings:font-bold prose-headings:text-gray-900
|
||||
prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-6
|
||||
prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4
|
||||
prose-p:text-gray-700 prose-p:leading-relaxed prose-p:mb-6 prose-p:text-lg
|
||||
prose-ul:my-6 prose-ul:space-y-2
|
||||
prose-li:text-gray-700 prose-li:leading-relaxed
|
||||
prose-strong:text-gray-900 prose-strong:font-semibold"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
/>
|
||||
|
||||
{post.howTo && (
|
||||
<div className="mt-12 bg-gradient-to-br from-blue-50 to-indigo-50 p-8 rounded-2xl border border-blue-200">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-6">{post.howTo.name}</h2>
|
||||
<p className="text-lg text-gray-700 mb-6 leading-relaxed">{post.howTo.description}</p>
|
||||
<ol className="space-y-6">
|
||||
{post.howTo.steps.map((step: any, index: number) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<span className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold text-lg mr-4">
|
||||
{index + 1}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-xl mb-2 text-gray-900">{step.name}</h3>
|
||||
<p className="text-gray-700 leading-relaxed">{step.text}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-16 p-10 bg-gradient-to-br from-primary-50 to-primary-100 rounded-2xl text-center border border-primary-200">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-4">
|
||||
Ready to Track Your QR Campaigns?
|
||||
</h2>
|
||||
<p className="text-lg text-gray-700 mb-8 max-w-2xl mx-auto leading-relaxed">
|
||||
Start creating professional dynamic QR codes with advanced scan analytics, campaign tracking, and real-time insights.
|
||||
</p>
|
||||
<Link href="/create">
|
||||
<Button size="lg">Create QR Code Free</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-4">
|
||||
{post.title}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div
|
||||
className="prose prose-lg max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
/>
|
||||
|
||||
<div className="mt-12 p-8 bg-primary-50 rounded-xl text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
Ready to create your QR codes?
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Start creating professional QR codes with advanced tracking and analytics.
|
||||
</p>
|
||||
<Link href="/dashboard">
|
||||
<Button size="lg">Get Started Free</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,76 +1,128 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||
import { websiteSchema } from '@/lib/schema';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
|
||||
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 Insights: Latest QR Strategies', 60);
|
||||
const description = truncateAtWord(
|
||||
'Expert guides on QR analytics, dynamic codes & smart marketing uses.',
|
||||
160
|
||||
);
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.com/blog',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.com/blog',
|
||||
en: 'https://www.qrmaster.com/blog',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: 'https://www.qrmaster.com/blog',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const blogPosts = [
|
||||
{
|
||||
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: '8 Min',
|
||||
category: 'Analytics',
|
||||
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&q=80',
|
||||
},
|
||||
{
|
||||
slug: 'qr-codes-im-restaurant',
|
||||
title: 'QR-Codes im Restaurant: Die digitale Revolution der Speisekarte',
|
||||
excerpt: 'Erfahren Sie, wie QR-Codes die Gastronomie revolutionieren und welche Vorteile sie für Restaurants und Gäste bieten.',
|
||||
date: '2024-01-15',
|
||||
title: 'QR Codes in Restaurants: The Digital Menu Revolution',
|
||||
excerpt: 'Discover how QR codes are revolutionizing the restaurant industry and what benefits they offer for restaurants and guests.',
|
||||
date: 'January 15, 2024',
|
||||
readTime: '5 Min',
|
||||
category: 'Gastronomie',
|
||||
image: '🍽️',
|
||||
category: 'Restaurant',
|
||||
image: 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&q=80',
|
||||
},
|
||||
{
|
||||
slug: 'dynamische-vs-statische-qr-codes',
|
||||
title: 'Dynamische vs. Statische QR-Codes: Was ist der Unterschied?',
|
||||
excerpt: 'Ein umfassender Vergleich zwischen dynamischen und statischen QR-Codes und wann Sie welchen Typ verwenden sollten.',
|
||||
date: '2024-01-10',
|
||||
title: 'Dynamic vs Static QR Codes: What\'s the Difference?',
|
||||
excerpt: 'A comprehensive comparison between dynamic and static QR codes and when you should use each type.',
|
||||
date: 'January 10, 2024',
|
||||
readTime: '3 Min',
|
||||
category: 'Grundlagen',
|
||||
image: '📊',
|
||||
},
|
||||
{
|
||||
slug: 'qr-code-marketing-strategien',
|
||||
title: 'QR-Code Marketing-Strategien für 2024',
|
||||
excerpt: 'Die besten Marketing-Strategien mit QR-Codes für Ihr Unternehmen im Jahr 2024.',
|
||||
date: '2024-01-05',
|
||||
readTime: '7 Min',
|
||||
category: 'Marketing',
|
||||
image: '📈',
|
||||
category: 'Basics',
|
||||
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&q=80',
|
||||
},
|
||||
];
|
||||
|
||||
export default function BlogPage() {
|
||||
return (
|
||||
<div className="py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-4">
|
||||
Blog & Resources
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
Learn about QR codes, best practices, and industry insights
|
||||
</p>
|
||||
</div>
|
||||
<>
|
||||
<SeoJsonLd data={websiteSchema()} />
|
||||
<div className="py-20 bg-gradient-to-b from-gray-50 to-white">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6">
|
||||
QR Code Insights
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||
Expert guides on dynamic QR codes, campaign tracking, UTM analytics, and smart marketing use cases.
|
||||
Discover how-to tutorials and best practices for QR code analytics.
|
||||
</p>
|
||||
</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}`}>
|
||||
<Card hover className="h-full">
|
||||
<CardHeader>
|
||||
<div className="text-4xl mb-4 text-center bg-gray-100 rounded-lg py-8">
|
||||
{post.image}
|
||||
<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}`}>
|
||||
<Card hover className="h-full overflow-hidden shadow-md hover:shadow-xl transition-all duration-300">
|
||||
<div className="relative h-56 overflow-hidden">
|
||||
<Image
|
||||
src={post.image}
|
||||
alt={`${post.title} - QR code guide showing ${post.category.toLowerCase()} strategies`}
|
||||
width={800}
|
||||
height={600}
|
||||
className="w-full h-full object-cover transition-transform duration-500 hover:scale-110"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Badge variant="info">{post.category}</Badge>
|
||||
<span className="text-sm text-gray-500">{post.readTime}</span>
|
||||
</div>
|
||||
<CardTitle className="text-xl">{post.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-600 mb-4">{post.excerpt}</p>
|
||||
<p className="text-sm text-gray-500">{post.date}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Badge variant="info">{post.category}</Badge>
|
||||
<span className="text-sm text-gray-500 font-medium">{post.readTime} read</span>
|
||||
</div>
|
||||
<CardTitle className="text-xl leading-tight mb-3">{post.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,143 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { FAQ } from '@/components/marketing/FAQ';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import type { Metadata } from 'next';
|
||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||
import { faqPageSchema } from '@/lib/schema';
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
|
||||
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 FAQ: Dynamic & Bulk QR', 60);
|
||||
const description = truncateAtWord(
|
||||
'All answers: dynamic QR, security, analytics, bulk, events & print.',
|
||||
160
|
||||
);
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.com/faq',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.com/faq',
|
||||
en: 'https://www.qrmaster.com/faq',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: 'https://www.qrmaster.com/faq',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: 'What is a dynamic QR code?',
|
||||
answer: 'A dynamic QR code allows you to change the destination URL after the code has been created and printed. Unlike static QR codes, dynamic codes redirect through a short URL that you control, enabling real-time updates, scan analytics, and campaign tracking without reprinting the code.',
|
||||
},
|
||||
{
|
||||
question: 'How do I track QR scans?',
|
||||
answer: 'QR Master provides a comprehensive analytics dashboard that tracks every scan in real-time. You can monitor scan rates, geographic locations, device types, timestamps, and user behavior. Enable UTM parameters to integrate with Google Analytics for advanced campaign tracking and conversion attribution.',
|
||||
},
|
||||
{
|
||||
question: 'What security features does QR Master offer?',
|
||||
answer: 'QR Master employs enterprise-grade security including SSL encryption, link validation to prevent malicious redirects, fraud detection, and GDPR-compliant data handling. All scan analytics are stored securely and access is protected with multi-factor authentication for business accounts.',
|
||||
},
|
||||
{
|
||||
question: 'Can I generate bulk QR codes for print?',
|
||||
answer: 'Yes. Our bulk QR generation tool allows you to create thousands of QR codes at once by uploading a CSV file. Each code can be customized with unique URLs, UTM parameters, and branding. Download print-ready files in SVG, PNG, or PDF formats optimized for high-resolution printing.',
|
||||
},
|
||||
{
|
||||
question: 'How do I brand my QR codes?',
|
||||
answer: 'QR Master offers full customization options including custom colors, logo embedding, rounded corners, and pattern styles. Branded QR codes maintain scannability while matching your brand identity. Upload your logo, choose your color palette, and preview designs before downloading.',
|
||||
},
|
||||
{
|
||||
question: 'Is scan analytics GDPR compliant?',
|
||||
answer: 'Yes. All QR Master analytics are fully GDPR compliant. We collect only necessary data, provide transparent privacy policies, allow users to opt out, and store data securely in EU-compliant data centers. You maintain full control over data retention and deletion.',
|
||||
},
|
||||
{
|
||||
question: 'Can QR Master track campaigns with UTM?',
|
||||
answer: 'Absolutely. QR Master supports UTM parameter integration for all dynamic QR codes. Automatically append source, medium, campaign, term, and content parameters to track QR performance in Google Analytics, Adobe Analytics, and other marketing platforms. UTM tracking enables multi-channel attribution and ROI measurement.',
|
||||
},
|
||||
{
|
||||
question: 'Difference between static and dynamic QR codes?',
|
||||
answer: 'Static QR codes encode the destination URL directly in the code pattern and cannot be changed after creation. Dynamic QR codes use a short redirect URL, allowing you to update destinations, track scans, enable/disable codes, and gather analytics—all without reprinting. Dynamic codes are essential for professional marketing campaigns.',
|
||||
},
|
||||
{
|
||||
question: 'How are QR codes used for events?',
|
||||
answer: 'QR codes streamline event check-ins, ticket validation, attendee tracking, and engagement measurement. Generate unique codes for each ticket, track scan times and locations, enable contactless entry, and analyze attendee behavior. Event organizers use QR analytics to measure session popularity and optimize future events.',
|
||||
},
|
||||
{
|
||||
question: 'Can I make QR codes for business cards?',
|
||||
answer: 'Yes. QR codes on business cards provide instant contact sharing via vCard format, link to your portfolio or LinkedIn profile, and track networking effectiveness. Use branded QR codes that match your card design, and leverage scan analytics to see how many contacts engage and when they follow up.',
|
||||
},
|
||||
{
|
||||
question: 'How do I use QR codes for bulk marketing?',
|
||||
answer: 'Bulk QR codes enable scalable campaigns across print ads, packaging, direct mail, and retail displays. Generate thousands of codes with unique tracking URLs, distribute them across channels, and use analytics to measure which placements drive the highest engagement. Bulk generation supports CSV upload, API integration, and automated workflows.',
|
||||
},
|
||||
{
|
||||
question: 'Is API access available for bulk QR generation?',
|
||||
answer: 'Yes. QR Master offers a developer-friendly REST API for programmatic QR code generation, URL management, and analytics retrieval. Integrate QR creation into your CRM, marketing automation platform, or e-commerce system. API access is included in Business plans and supports bulk operations, webhooks, and real-time updates.',
|
||||
},
|
||||
];
|
||||
|
||||
export default function FAQPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="py-20">
|
||||
<FAQ t={t} />
|
||||
</div>
|
||||
<>
|
||||
<SeoJsonLd data={faqPageSchema(faqs)} />
|
||||
<div className="py-20 bg-gradient-to-b from-gray-50 to-white">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6">
|
||||
Frequently Asked Questions
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
Everything you need to know about dynamic QR codes, security, analytics, bulk generation, events, and print quality.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{faqs.map((faq, index) => (
|
||||
<Card key={index} className="border-l-4 border-blue-500">
|
||||
<CardContent className="p-8">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-900">
|
||||
{faq.question}
|
||||
</h2>
|
||||
<p className="text-lg text-gray-700 leading-relaxed">
|
||||
{faq.answer}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</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.com" className="text-blue-600 hover:text-blue-700 font-semibold">
|
||||
support@qrmaster.com
|
||||
</a>{' '}
|
||||
or reach out through our live chat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import React, { useState } from 'react';
|
|||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import en from '@/i18n/en.json';
|
||||
|
||||
export default function MarketingLayout({
|
||||
children,
|
||||
|
|
@ -12,14 +12,16 @@ export default function MarketingLayout({
|
|||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const { t, locale, setLocale } = useTranslation();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
// Always use English for marketing pages
|
||||
const t = en;
|
||||
|
||||
const navigation = [
|
||||
{ name: t('nav.features'), href: '/#features' },
|
||||
{ name: t('nav.pricing'), href: '/pricing' },
|
||||
{ name: t('nav.faq'), href: '/faq' },
|
||||
{ name: t('nav.blog'), href: '/blog' },
|
||||
{ name: t.nav.features, href: '/#features' },
|
||||
{ name: t.nav.pricing, href: '/#pricing' },
|
||||
{ name: t.nav.faq, href: '/#faq' },
|
||||
{ name: t.nav.blog, href: '/blog' },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -49,16 +51,8 @@ export default function MarketingLayout({
|
|||
|
||||
{/* Right Actions */}
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
{/* Language Switcher */}
|
||||
<button
|
||||
onClick={() => setLocale(locale === 'en' ? 'de' : 'en')}
|
||||
className="text-gray-600 hover:text-gray-900 font-medium"
|
||||
>
|
||||
{locale === 'en' ? '🇩🇪 DE' : '🇬🇧 EN'}
|
||||
</button>
|
||||
|
||||
<Link href="/login">
|
||||
<Button variant="outline">{t('nav.login')}</Button>
|
||||
<Button variant="outline">{t.nav.login}</Button>
|
||||
</Link>
|
||||
|
||||
<Link href="/signup">
|
||||
|
|
@ -96,7 +90,7 @@ export default function MarketingLayout({
|
|||
</Link>
|
||||
))}
|
||||
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
|
||||
<Button variant="outline" className="w-full">{t('nav.login')}</Button>
|
||||
<Button variant="outline" className="w-full">{t.nav.login}</Button>
|
||||
</Link>
|
||||
<Link href="/signup" onClick={() => setMobileMenuOpen(false)}>
|
||||
<Button className="w-full">Get Started Free</Button>
|
||||
|
|
@ -128,8 +122,8 @@ export default function MarketingLayout({
|
|||
<h3 className="font-semibold mb-4">Product</h3>
|
||||
<ul className="space-y-2 text-gray-400">
|
||||
<li><Link href="/#features" className="hover:text-white">Features</Link></li>
|
||||
<li><Link href="/pricing" className="hover:text-white">Pricing</Link></li>
|
||||
<li><Link href="/faq" className="hover:text-white">FAQ</Link></li>
|
||||
<li><Link href="/#pricing" className="hover:text-white">Pricing</Link></li>
|
||||
<li><Link href="/#faq" className="hover:text-white">FAQ</Link></li>
|
||||
<li><Link href="/blog" className="hover:text-white">Blog</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,77 +1,51 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Hero } from '@/components/marketing/Hero';
|
||||
import { StatsStrip } from '@/components/marketing/StatsStrip';
|
||||
import { TemplateCards } from '@/components/marketing/TemplateCards';
|
||||
import { InstantGenerator } from '@/components/marketing/InstantGenerator';
|
||||
import { StaticVsDynamic } from '@/components/marketing/StaticVsDynamic';
|
||||
import { Features } from '@/components/marketing/Features';
|
||||
import { Pricing } from '@/components/marketing/Pricing';
|
||||
import { FAQ } from '@/components/marketing/FAQ';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
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.com/',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.com/',
|
||||
en: 'https://www.qrmaster.com/',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: 'https://www.qrmaster.com/',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const industries = [
|
||||
'Restaurant Chain',
|
||||
'Tech Startup',
|
||||
'Real Estate',
|
||||
'Event Agency',
|
||||
'Retail Store',
|
||||
'Healthcare',
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hero t={t} />
|
||||
<StatsStrip t={t} />
|
||||
|
||||
{/* Industry Buttons */}
|
||||
<section className="py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
{industries.map((industry) => (
|
||||
<Button key={industry} variant="outline" size="sm">
|
||||
{industry}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<TemplateCards t={t} />
|
||||
<InstantGenerator t={t} />
|
||||
<StaticVsDynamic t={t} />
|
||||
<Features t={t} />
|
||||
|
||||
{/* Pricing Teaser */}
|
||||
<section className="py-16 bg-primary-50">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
|
||||
Ready to get started?
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 mb-8">
|
||||
Choose the perfect plan for your needs
|
||||
</p>
|
||||
<Button size="lg">View Pricing Plans</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Teaser */}
|
||||
<section className="py-16">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
|
||||
Have questions?
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 mb-8">
|
||||
Check out our frequently asked questions
|
||||
</p>
|
||||
<Button variant="outline" size="lg">View FAQ</Button>
|
||||
</div>
|
||||
</section>
|
||||
<SeoJsonLd data={[organizationSchema(), websiteSchema()]} />
|
||||
<HomePageClient />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,267 @@
|
|||
import React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||
import { productSchema } from '@/lib/schema';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
|
||||
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 Pricing & Bulk Plans', 60);
|
||||
const description = truncateAtWord(
|
||||
'Choose flexible plans for bulk QR generation, analytics & branding.',
|
||||
160
|
||||
);
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.com/pricing',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.com/pricing',
|
||||
en: 'https://www.qrmaster.com/pricing',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: 'https://www.qrmaster.com/pricing',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function PricingPage() {
|
||||
const productData = {
|
||||
name: 'QR Master Plans',
|
||||
description: 'Flexible pricing for dynamic QR codes, bulk generation, branded designs, and analytics.',
|
||||
offers: [
|
||||
{
|
||||
name: 'Free Plan',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
availability: 'https://schema.org/InStock',
|
||||
url: 'https://www.qrmaster.com/pricing',
|
||||
},
|
||||
{
|
||||
name: 'Pro Plan',
|
||||
price: '29',
|
||||
priceCurrency: 'USD',
|
||||
availability: 'https://schema.org/InStock',
|
||||
url: 'https://www.qrmaster.com/pricing',
|
||||
},
|
||||
{
|
||||
name: 'Business Plan',
|
||||
price: '99',
|
||||
priceCurrency: 'USD',
|
||||
availability: 'https://schema.org/InStock',
|
||||
url: 'https://www.qrmaster.com/pricing',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const plans = [
|
||||
{
|
||||
name: 'Free',
|
||||
price: '$0',
|
||||
interval: '/mo',
|
||||
description: 'Perfect for personal projects',
|
||||
features: [
|
||||
'10 Dynamic QR Codes',
|
||||
'Basic Scan Analytics',
|
||||
'Standard QR Design Templates',
|
||||
'Community Support',
|
||||
],
|
||||
cta: 'Get Started',
|
||||
ctaLink: '/signup',
|
||||
popular: false,
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
price: '$29',
|
||||
interval: '/mo',
|
||||
description: 'For professionals and small teams',
|
||||
features: [
|
||||
'Unlimited Dynamic QR Codes',
|
||||
'Advanced Scan Analytics',
|
||||
'Branded QR Codes with Custom Colors & Logo',
|
||||
'Bulk QR Generation (up to 1,000)',
|
||||
'UTM Campaign Tracking',
|
||||
'Download as SVG/PNG/PDF',
|
||||
'Priority Support',
|
||||
],
|
||||
cta: 'Start Pro Trial',
|
||||
ctaLink: '/signup?plan=pro',
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
name: 'Business',
|
||||
price: '$99',
|
||||
interval: '/mo',
|
||||
description: 'For agencies and enterprises',
|
||||
features: [
|
||||
'Everything in Pro',
|
||||
'Unlimited Bulk QR Generation',
|
||||
'API Access for Automation',
|
||||
'White-Label Options',
|
||||
'Advanced Security Features',
|
||||
'Custom Integrations',
|
||||
'Dedicated Account Manager',
|
||||
'SLA Guarantee',
|
||||
],
|
||||
cta: 'Contact Sales',
|
||||
ctaLink: '/signup?plan=business',
|
||||
popular: false,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoJsonLd data={productSchema(productData)} />
|
||||
<div className="py-20 bg-gradient-to-b from-gray-50 to-white">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6">
|
||||
Pricing Plans
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||
Choose flexible plans for bulk QR generation, analytics, and branded QR codes.
|
||||
Start free, upgrade anytime.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto mb-20">
|
||||
{plans.map((plan, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`relative ${
|
||||
plan.popular
|
||||
? 'border-2 border-blue-600 shadow-xl transform scale-105'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
||||
<Badge variant="info" className="px-4 py-1 text-sm font-semibold">
|
||||
Most Popular
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader className="text-center pb-6">
|
||||
<CardTitle className="text-2xl mb-2">{plan.name}</CardTitle>
|
||||
<p className="text-sm text-gray-600 mb-6">{plan.description}</p>
|
||||
|
||||
<div className="flex items-baseline justify-center">
|
||||
<span className="text-5xl font-bold text-gray-900">{plan.price}</span>
|
||||
<span className="text-gray-600 ml-2 text-lg">{plan.interval}</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<ul className="space-y-3 mb-8">
|
||||
{plan.features.map((feature, fIndex) => (
|
||||
<li key={fIndex} className="flex items-start space-x-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-700">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Link href={plan.ctaLink}>
|
||||
<Button
|
||||
variant={plan.popular ? 'primary' : 'outline'}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{plan.cta}
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<div className="mt-20 max-w-3xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12 text-gray-900">
|
||||
Pricing FAQs
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold text-lg mb-3 text-gray-900">
|
||||
Can I cancel my subscription anytime?
|
||||
</h3>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
Yes, you can cancel your subscription at any time. Your account will remain active
|
||||
until the end of your current billing period. No refunds for partial months.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold text-lg mb-3 text-gray-900">
|
||||
What happens to my QR codes if I downgrade?
|
||||
</h3>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
All your existing QR codes remain active and functional. You won't be able to create
|
||||
new codes if you exceed your new plan's limit, but existing codes continue working.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold text-lg mb-3 text-gray-900">
|
||||
Do you offer annual billing?
|
||||
</h3>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
Yes! Annual billing saves you 17% compared to monthly billing. Contact sales for
|
||||
custom annual contracts with additional discounts for enterprise customers.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold text-lg mb-3 text-gray-900">
|
||||
Is there a free trial for paid plans?
|
||||
</h3>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
Yes, all paid plans include a 14-day free trial. No credit card required to start.
|
||||
Test all premium features before committing to a paid plan.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,24 +3,39 @@ import '@/styles/globals.css';
|
|||
import { ToastContainer } from '@/components/ui/Toast';
|
||||
import AuthProvider from '@/components/SessionProvider';
|
||||
|
||||
const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'QR Master - Create Custom QR Codes in Seconds',
|
||||
description: 'Generate static and dynamic QR codes with advanced tracking, professional templates, and seamless integrations.',
|
||||
keywords: 'QR code, QR generator, dynamic QR, QR tracking, QR analytics',
|
||||
metadataBase: new URL('https://www.qrmaster.com'),
|
||||
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 },
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
site: '@qrmaster',
|
||||
images: ['https://www.qrmaster.com/static/og-image.png'],
|
||||
},
|
||||
openGraph: {
|
||||
title: 'QR Master - Create Custom QR Codes in Seconds',
|
||||
description: 'Generate static and dynamic QR codes with advanced tracking, professional templates, and seamless integrations.',
|
||||
url: 'https://qrmaster.com',
|
||||
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.',
|
||||
url: 'https://www.qrmaster.com',
|
||||
images: [
|
||||
{
|
||||
url: '/og-image.png',
|
||||
url: 'https://www.qrmaster.com/static/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform',
|
||||
},
|
||||
],
|
||||
locale: 'en_US',
|
||||
type: 'website',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import { ImageResponse } from 'next/og';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const title = searchParams.get('title') || 'QR Master – Smart QR Generator & Analytics';
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#1a1a2e',
|
||||
backgroundImage: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '40px 80px',
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
lineHeight: 1.2,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
textAlign: 'center',
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
Dynamic QR codes with analytics & branding
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return new Response('Failed to generate image', { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
items: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
export default function Breadcrumbs({ items }: BreadcrumbsProps) {
|
||||
return (
|
||||
<nav aria-label="Breadcrumb" className="mb-6">
|
||||
<ol className="flex items-center space-x-2 text-sm text-gray-600">
|
||||
{items.map((item, index) => (
|
||||
<li key={item.url} className="flex items-center">
|
||||
{index > 0 && <span className="mx-2">/</span>}
|
||||
{index === items.length - 1 ? (
|
||||
<span className="font-semibold text-gray-900" aria-current="page">
|
||||
{item.name}
|
||||
</span>
|
||||
) : (
|
||||
<Link href={item.url} className="hover:text-blue-600 transition-colors">
|
||||
{item.name}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export { type BreadcrumbItem as BreadcrumbItemType };
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
|
||||
interface SeoJsonLdProps {
|
||||
data: object | object[];
|
||||
}
|
||||
|
||||
export default function SeoJsonLd({ data }: SeoJsonLdProps) {
|
||||
const jsonLdArray = Array.isArray(data) ? data : [data];
|
||||
|
||||
return (
|
||||
<>
|
||||
{jsonLdArray.map((item, index) => (
|
||||
<script
|
||||
key={index}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(item, null, 0),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -22,21 +22,21 @@ export const FAQ: React.FC<FAQProps> = ({ t }) => {
|
|||
];
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-gray-50">
|
||||
<section id="faq" className="py-16 bg-gray-50">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
|
||||
{t('faq.title')}
|
||||
{t.faq.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="max-w-3xl mx-auto space-y-4">
|
||||
{questions.map((key, index) => (
|
||||
<Card key={key} className="cursor-pointer" onClick={() => setOpenIndex(openIndex === index ? null : index)}>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{t(`faq.questions.${key}.question`)}
|
||||
{t.faq.questions[key].question}
|
||||
</h3>
|
||||
<svg
|
||||
className={`w-5 h-5 text-gray-500 transition-transform ${openIndex === index ? 'rotate-180' : ''}`}
|
||||
|
|
@ -47,10 +47,10 @@ export const FAQ: React.FC<FAQProps> = ({ t }) => {
|
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
{openIndex === index && (
|
||||
<div className="mt-4 text-gray-600">
|
||||
{t(`faq.questions.${key}.answer`)}
|
||||
{t.faq.questions[key].answer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,42 +27,6 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
|||
),
|
||||
color: 'text-purple-600 bg-purple-100',
|
||||
},
|
||||
{
|
||||
key: 'bulk',
|
||||
icon: (
|
||||
<svg className="w-6 h-6" 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>
|
||||
),
|
||||
color: 'text-green-600 bg-green-100',
|
||||
},
|
||||
{
|
||||
key: 'integrations',
|
||||
icon: (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z" />
|
||||
</svg>
|
||||
),
|
||||
color: 'text-orange-600 bg-orange-100',
|
||||
},
|
||||
{
|
||||
key: 'api',
|
||||
icon: (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
),
|
||||
color: 'text-indigo-600 bg-indigo-100',
|
||||
},
|
||||
{
|
||||
key: 'support',
|
||||
icon: (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
),
|
||||
color: 'text-red-600 bg-red-100',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -70,10 +34,10 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
|||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
|
||||
{t('features.title')}
|
||||
{t.features.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
||||
{features.map((feature) => (
|
||||
<Card key={feature.key} hover>
|
||||
|
|
@ -81,11 +45,11 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
|||
<div className={`w-12 h-12 rounded-lg ${feature.color} flex items-center justify-center mb-4`}>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<CardTitle>{t(`features.${feature.key}.title`)}</CardTitle>
|
||||
<CardTitle>{t.features[feature.key].title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-600">
|
||||
{t(`features.${feature.key}.description`)}
|
||||
{t.features[feature.key].description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -25,20 +25,20 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
{/* Left Content */}
|
||||
<div className="space-y-8">
|
||||
<Badge variant="info" className="inline-flex items-center space-x-2">
|
||||
<span>{t('hero.badge')}</span>
|
||||
<span>{t.hero.badge}</span>
|
||||
</Badge>
|
||||
|
||||
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
|
||||
{t('hero.title')}
|
||||
{t.hero.title}
|
||||
</h1>
|
||||
|
||||
|
||||
<p className="text-xl text-gray-600 leading-relaxed">
|
||||
{t('hero.subtitle')}
|
||||
{t.hero.subtitle}
|
||||
</p>
|
||||
|
||||
|
||||
<div className="space-y-3">
|
||||
{t('hero.features', { returnObjects: true }).map((feature: string, index: number) => (
|
||||
{t.hero.features.map((feature: string, index: number) => (
|
||||
<div key={index} className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0 w-5 h-5 bg-success-500 rounded-full flex items-center justify-center">
|
||||
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
|
|
@ -49,22 +49,22 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Link href="/signup">
|
||||
<Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
||||
{t('hero.cta_primary')}
|
||||
{t.hero.cta_primary}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/create">
|
||||
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
||||
{t('hero.cta_secondary')}
|
||||
{t.hero.cta_secondary}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Right Preview Widget */}
|
||||
<div className="relative">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
|
@ -75,10 +75,10 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Floating Badge */}
|
||||
<div className="absolute -top-4 -right-4 bg-success-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg">
|
||||
{t('hero.engagement_badge')}
|
||||
{t.hero.engagement_badge}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Hero } from '@/components/marketing/Hero';
|
||||
import { StatsStrip } from '@/components/marketing/StatsStrip';
|
||||
import { TemplateCards } from '@/components/marketing/TemplateCards';
|
||||
import { InstantGenerator } from '@/components/marketing/InstantGenerator';
|
||||
import { StaticVsDynamic } from '@/components/marketing/StaticVsDynamic';
|
||||
import { Features } from '@/components/marketing/Features';
|
||||
import { Pricing } from '@/components/marketing/Pricing';
|
||||
import { FAQ } from '@/components/marketing/FAQ';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import en from '@/i18n/en.json';
|
||||
|
||||
export default function HomePageClient() {
|
||||
// Always use English for marketing pages
|
||||
const t = en;
|
||||
|
||||
const industries = [
|
||||
'Restaurant Chain',
|
||||
'Tech Startup',
|
||||
'Real Estate',
|
||||
'Event Agency',
|
||||
'Retail Store',
|
||||
'Healthcare',
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hero t={t} />
|
||||
<StatsStrip t={t} />
|
||||
|
||||
{/* Industry Buttons */}
|
||||
<section className="py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
{industries.map((industry) => (
|
||||
<Button key={industry} variant="outline" size="sm">
|
||||
{industry}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<TemplateCards t={t} />
|
||||
<InstantGenerator t={t} />
|
||||
<StaticVsDynamic t={t} />
|
||||
<Features t={t} />
|
||||
|
||||
{/* Pricing Section */}
|
||||
<Pricing t={t} />
|
||||
|
||||
{/* FAQ Section */}
|
||||
<FAQ t={t} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -77,10 +77,10 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
|
||||
{t('generator.title')}
|
||||
{t.generator.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-12 max-w-6xl mx-auto">
|
||||
{/* Left Form */}
|
||||
<Card className="space-y-6">
|
||||
|
|
@ -88,13 +88,13 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
label="URL"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder={t('generator.url_placeholder')}
|
||||
placeholder={t.generator.url_placeholder}
|
||||
/>
|
||||
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('generator.foreground')}
|
||||
{t.generator.foreground}
|
||||
</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
|
|
@ -110,10 +110,10 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('generator.background')}
|
||||
{t.generator.background}
|
||||
</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
|
|
@ -134,7 +134,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('generator.corners')}
|
||||
{t.generator.corners}
|
||||
</label>
|
||||
<select
|
||||
value={cornerStyle}
|
||||
|
|
@ -145,10 +145,10 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
<option value="rounded">Rounded</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('generator.size')}
|
||||
{t.generator.size}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
|
|
@ -164,31 +164,31 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant={hasGoodContrast ? 'success' : 'warning'}>
|
||||
{hasGoodContrast ? t('generator.contrast_good') : 'Low contrast'}
|
||||
{hasGoodContrast ? t.generator.contrast_good : 'Low contrast'}
|
||||
</Badge>
|
||||
<div className="text-sm text-gray-500">
|
||||
Contrast: {contrast.toFixed(1)}:1
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<Button variant="outline" className="flex-1" onClick={() => downloadQR('svg')}>
|
||||
{t('generator.download_svg')}
|
||||
{t.generator.download_svg}
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1" onClick={() => downloadQR('png')}>
|
||||
{t('generator.download_png')}
|
||||
{t.generator.download_png}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<Button className="w-full">
|
||||
{t('generator.save_track')}
|
||||
{t.generator.save_track}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Right Preview */}
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Card className="text-center p-8">
|
||||
<h3 className="text-lg font-semibold mb-4">{t('generator.live_preview')}</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">{t.generator.live_preview}</h3>
|
||||
<div id="instant-qr-preview" className="flex justify-center mb-4">
|
||||
{url ? (
|
||||
<div className={`${cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}`}>
|
||||
|
|
@ -210,7 +210,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mb-2">URL</div>
|
||||
<div className="text-xs text-gray-500">{t('generator.demo_note')}</div>
|
||||
<div className="text-xs text-gray-500">{t.generator.demo_note}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,48 +26,48 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
|
|||
];
|
||||
|
||||
return (
|
||||
<section className="py-16">
|
||||
<section id="pricing" className="py-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
|
||||
{t('pricing.title')}
|
||||
{t.pricing.title}
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
{t('pricing.subtitle')}
|
||||
{t.pricing.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||||
{plans.map((plan) => (
|
||||
<Card
|
||||
key={plan.key}
|
||||
<Card
|
||||
key={plan.key}
|
||||
className={plan.popular ? 'border-primary-500 shadow-xl relative' : ''}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
||||
<Badge variant="info" className="px-3 py-1">
|
||||
{t(`pricing.${plan.key}.badge`)}
|
||||
{t.pricing[plan.key].badge}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<CardHeader className="text-center pb-8">
|
||||
<CardTitle className="text-2xl mb-4">
|
||||
{t(`pricing.${plan.key}.title`)}
|
||||
{t.pricing[plan.key].title}
|
||||
</CardTitle>
|
||||
<div className="flex items-baseline justify-center">
|
||||
<span className="text-4xl font-bold">
|
||||
{t(`pricing.${plan.key}.price`)}
|
||||
{t.pricing[plan.key].price}
|
||||
</span>
|
||||
<span className="text-gray-600 ml-2">
|
||||
{t(`pricing.${plan.key}.period`)}
|
||||
{t.pricing[plan.key].period}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<ul className="space-y-3">
|
||||
{t(`pricing.${plan.key}.features`, { returnObjects: true }).map((feature: string, index: number) => (
|
||||
{t.pricing[plan.key].features.map((feature: string, index: number) => (
|
||||
<li key={index} className="flex items-start space-x-3">
|
||||
<svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
|
|
@ -76,9 +76,9 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
variant={plan.popular ? 'primary' : 'outline'}
|
||||
|
||||
<Button
|
||||
variant={plan.popular ? 'primary' : 'outline'}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ export const StaticVsDynamic: React.FC<StaticVsDynamicProps> = ({ t }) => {
|
|||
<Card className="relative">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-2xl">{t('static_vs_dynamic.static.title')}</CardTitle>
|
||||
<Badge variant="success">{t('static_vs_dynamic.static.subtitle')}</Badge>
|
||||
<CardTitle className="text-2xl">{t.static_vs_dynamic.static.title}</CardTitle>
|
||||
<Badge variant="success">{t.static_vs_dynamic.static.subtitle}</Badge>
|
||||
</div>
|
||||
<p className="text-gray-600">{t('static_vs_dynamic.static.description')}</p>
|
||||
<p className="text-gray-600">{t.static_vs_dynamic.static.description}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-3">
|
||||
{t('static_vs_dynamic.static.features', { returnObjects: true }).map((feature: string, index: number) => (
|
||||
{t.static_vs_dynamic.static.features.map((feature: string, index: number) => (
|
||||
<li key={index} className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0 w-5 h-5 bg-gray-400 rounded-full flex items-center justify-center">
|
||||
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
|
|
@ -37,19 +37,19 @@ export const StaticVsDynamic: React.FC<StaticVsDynamicProps> = ({ t }) => {
|
|||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Dynamic QR Codes */}
|
||||
<Card className="relative border-primary-200 bg-primary-50">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-2xl">{t('static_vs_dynamic.dynamic.title')}</CardTitle>
|
||||
<Badge variant="info">{t('static_vs_dynamic.dynamic.subtitle')}</Badge>
|
||||
<CardTitle className="text-2xl">{t.static_vs_dynamic.dynamic.title}</CardTitle>
|
||||
<Badge variant="info">{t.static_vs_dynamic.dynamic.subtitle}</Badge>
|
||||
</div>
|
||||
<p className="text-gray-600">{t('static_vs_dynamic.dynamic.description')}</p>
|
||||
<p className="text-gray-600">{t.static_vs_dynamic.dynamic.description}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-3">
|
||||
{t('static_vs_dynamic.dynamic.features', { returnObjects: true }).map((feature: string, index: number) => (
|
||||
{t.static_vs_dynamic.dynamic.features.map((feature: string, index: number) => (
|
||||
<li key={index} className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0 w-5 h-5 bg-primary-500 rounded-full flex items-center justify-center">
|
||||
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ interface StatsStripProps {
|
|||
|
||||
export const StatsStrip: React.FC<StatsStripProps> = ({ t }) => {
|
||||
const stats = [
|
||||
{ key: 'users', value: '10,000+', label: t('trust.users') },
|
||||
{ key: 'codes', value: '500,000+', label: t('trust.codes') },
|
||||
{ key: 'scans', value: '50M+', label: t('trust.scans') },
|
||||
{ key: 'countries', value: '120+', label: t('trust.countries') },
|
||||
{ key: 'users', value: '10,000+', label: t.trust.users },
|
||||
{ key: 'codes', value: '500,000+', label: t.trust.codes },
|
||||
{ key: 'scans', value: '50M+', label: t.trust.scans },
|
||||
{ key: 'countries', value: '120+', label: t.trust.countries },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -12,28 +12,28 @@ export const TemplateCards: React.FC<TemplateCardsProps> = ({ t }) => {
|
|||
const templates = [
|
||||
{
|
||||
key: 'restaurant',
|
||||
title: t('templates.restaurant'),
|
||||
title: t.templates.restaurant,
|
||||
icon: '🍽️',
|
||||
color: 'bg-red-50 border-red-200',
|
||||
iconBg: 'bg-red-100',
|
||||
},
|
||||
{
|
||||
key: 'business',
|
||||
title: t('templates.business'),
|
||||
title: t.templates.business,
|
||||
icon: '💼',
|
||||
color: 'bg-blue-50 border-blue-200',
|
||||
iconBg: 'bg-blue-100',
|
||||
},
|
||||
{
|
||||
key: 'wifi',
|
||||
title: t('templates.wifi'),
|
||||
title: t.templates.wifi,
|
||||
icon: '📶',
|
||||
color: 'bg-purple-50 border-purple-200',
|
||||
iconBg: 'bg-purple-100',
|
||||
},
|
||||
{
|
||||
key: 'event',
|
||||
title: t('templates.event'),
|
||||
title: t.templates.event,
|
||||
icon: '🎫',
|
||||
color: 'bg-green-50 border-green-200',
|
||||
iconBg: 'bg-green-100',
|
||||
|
|
@ -45,10 +45,10 @@ export const TemplateCards: React.FC<TemplateCardsProps> = ({ t }) => {
|
|||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
|
||||
{t('templates.title')}
|
||||
{t.templates.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{templates.map((template) => (
|
||||
<Card key={template.key} className={`${template.color} text-center hover:scale-105 transition-transform cursor-pointer`}>
|
||||
|
|
@ -59,7 +59,7 @@ export const TemplateCards: React.FC<TemplateCardsProps> = ({ t }) => {
|
|||
{template.title}
|
||||
</h3>
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
{t('templates.use_template')}
|
||||
{t.templates.use_template}
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ export function useTranslation() {
|
|||
return {
|
||||
t,
|
||||
locale,
|
||||
language: locale,
|
||||
setLocale: changeLocale,
|
||||
setLanguage: changeLocale,
|
||||
};
|
||||
}
|
||||
|
|
@ -116,10 +116,10 @@
|
|||
"price": "€0",
|
||||
"period": "für immer",
|
||||
"features": [
|
||||
"5 dynamische QR-Codes",
|
||||
"3 dynamische QR-Codes",
|
||||
"Unbegrenzte statische QR-Codes",
|
||||
"Basis-Analytik",
|
||||
"Standard-Vorlagen"
|
||||
"Basis-Scan-Tracking",
|
||||
"Standard QR-Design-Vorlagen"
|
||||
]
|
||||
},
|
||||
"pro": {
|
||||
|
|
@ -128,25 +128,23 @@
|
|||
"period": "pro Monat",
|
||||
"badge": "Beliebteste",
|
||||
"features": [
|
||||
"Unbegrenzte QR-Codes",
|
||||
"Erweiterte Analytik",
|
||||
"Individuelles Branding",
|
||||
"Bulk-Operationen",
|
||||
"API-Zugang",
|
||||
"Prioritäts-Support"
|
||||
"50 dynamische QR-Codes",
|
||||
"Unbegrenzte statische QR-Codes",
|
||||
"Erweiterte Analytik (Scans, Geräte, Standorte)",
|
||||
"Individuelles Branding (Farben & Logo)",
|
||||
"Download als SVG/PNG"
|
||||
]
|
||||
},
|
||||
"business": {
|
||||
"title": "Business",
|
||||
"price": "€49",
|
||||
"price": "€29",
|
||||
"period": "pro Monat",
|
||||
"features": [
|
||||
"500 dynamische QR-Codes",
|
||||
"Unbegrenzte statische QR-Codes",
|
||||
"Alles aus Pro",
|
||||
"Team-Zusammenarbeit",
|
||||
"White-Label-Lösung",
|
||||
"Erweiterte Integrationen",
|
||||
"Individuelle Domains",
|
||||
"Dedizierter Support"
|
||||
"Prioritäts-E-Mail-Support",
|
||||
"Erweiterte Tracking & Insights"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -116,10 +116,10 @@
|
|||
"price": "€0",
|
||||
"period": "forever",
|
||||
"features": [
|
||||
"5 dynamic QR codes",
|
||||
"3 dynamic QR codes",
|
||||
"Unlimited static QR codes",
|
||||
"Basic analytics",
|
||||
"Standard templates"
|
||||
"Basic scan tracking",
|
||||
"Standard QR design templates"
|
||||
]
|
||||
},
|
||||
"pro": {
|
||||
|
|
@ -128,25 +128,23 @@
|
|||
"period": "per month",
|
||||
"badge": "Most Popular",
|
||||
"features": [
|
||||
"Unlimited QR codes",
|
||||
"Advanced analytics",
|
||||
"Custom branding",
|
||||
"Bulk operations",
|
||||
"API access",
|
||||
"Priority support"
|
||||
"50 dynamic QR codes",
|
||||
"Unlimited static QR codes",
|
||||
"Advanced analytics (scans, devices, locations)",
|
||||
"Custom branding (colors & logo)",
|
||||
"Download as SVG/PNG"
|
||||
]
|
||||
},
|
||||
"business": {
|
||||
"title": "Business",
|
||||
"price": "€49",
|
||||
"price": "€29",
|
||||
"period": "per month",
|
||||
"features": [
|
||||
"Everything in Pro",
|
||||
"Team collaboration",
|
||||
"White-label solution",
|
||||
"Advanced integrations",
|
||||
"Custom domains",
|
||||
"Dedicated support"
|
||||
"500 dynamic QR codes",
|
||||
"Unlimited static QR codes",
|
||||
"Everything from Pro",
|
||||
"Priority email support",
|
||||
"Advanced tracking & insights"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
export interface BreadcrumbItem {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface BlogPost {
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
author: string;
|
||||
authorUrl: string;
|
||||
datePublished: string;
|
||||
dateModified: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export interface FAQItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
export interface ProductOffer {
|
||||
name: string;
|
||||
price: string;
|
||||
priceCurrency: string;
|
||||
availability: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface HowToStep {
|
||||
name: string;
|
||||
text: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface HowToTask {
|
||||
name: string;
|
||||
description: string;
|
||||
steps: HowToStep[];
|
||||
totalTime?: string;
|
||||
}
|
||||
|
||||
export function organizationSchema() {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
'@id': 'https://www.qrmaster.com/#organization',
|
||||
name: 'QR Master',
|
||||
url: 'https://www.qrmaster.com',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://www.qrmaster.com/static/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
sameAs: [
|
||||
'https://twitter.com/qrmaster',
|
||||
],
|
||||
contactPoint: {
|
||||
'@type': 'ContactPoint',
|
||||
contactType: 'Customer Support',
|
||||
email: 'support@qrmaster.com',
|
||||
},
|
||||
description: 'Dynamic QR code generator with analytics, branding, and bulk generation for modern marketing campaigns.',
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: 'https://www.qrmaster.com',
|
||||
};
|
||||
}
|
||||
|
||||
export function websiteSchema() {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
'@id': 'https://www.qrmaster.com/#website',
|
||||
name: 'QR Master',
|
||||
url: 'https://www.qrmaster.com',
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: 'https://www.qrmaster.com',
|
||||
publisher: {
|
||||
'@id': 'https://www.qrmaster.com/#organization',
|
||||
},
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate: 'https://www.qrmaster.com/blog?q={search_term_string}',
|
||||
},
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function breadcrumbSchema(items: BreadcrumbItem[]) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
'@id': `https://www.qrmaster.com${items[items.length - 1]?.url}#breadcrumb`,
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: `https://www.qrmaster.com${items[items.length - 1]?.url}`,
|
||||
itemListElement: items.map((item, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: item.name,
|
||||
item: `https://www.qrmaster.com${item.url}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function blogPostingSchema(post: BlogPost) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
'@id': `https://www.qrmaster.com/blog/${post.slug}#article`,
|
||||
headline: post.title,
|
||||
description: post.description,
|
||||
image: post.image,
|
||||
datePublished: post.datePublished,
|
||||
dateModified: post.dateModified,
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: `https://www.qrmaster.com/blog/${post.slug}`,
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: post.author,
|
||||
url: post.authorUrl,
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'QR Master',
|
||||
url: 'https://www.qrmaster.com',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://www.qrmaster.com/static/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
},
|
||||
isPartOf: {
|
||||
'@type': 'Blog',
|
||||
'@id': 'https://www.qrmaster.com/blog#blog',
|
||||
name: 'QR Master Blog',
|
||||
url: 'https://www.qrmaster.com/blog',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function faqPageSchema(faqs: FAQItem[]) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
'@id': 'https://www.qrmaster.com/faq#faqpage',
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: 'https://www.qrmaster.com/faq',
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
'@type': 'Question',
|
||||
name: faq.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: faq.answer,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function productSchema(product: { name: string; description: string; offers: ProductOffer[] }) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Product',
|
||||
'@id': 'https://www.qrmaster.com/pricing#product',
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: 'https://www.qrmaster.com/pricing',
|
||||
brand: {
|
||||
'@type': 'Organization',
|
||||
name: 'QR Master',
|
||||
},
|
||||
offers: product.offers.map((offer) => ({
|
||||
'@type': 'Offer',
|
||||
name: offer.name,
|
||||
price: offer.price,
|
||||
priceCurrency: offer.priceCurrency,
|
||||
availability: offer.availability,
|
||||
url: offer.url,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function howToSchema(task: HowToTask) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'HowTo',
|
||||
'@id': `https://www.qrmaster.com/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}#howto`,
|
||||
name: task.name,
|
||||
description: task.description,
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: `https://www.qrmaster.com/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}`,
|
||||
totalTime: task.totalTime || 'PT5M',
|
||||
step: task.steps.map((step, index) => ({
|
||||
'@type': 'HowToStep',
|
||||
position: index + 1,
|
||||
name: step.name,
|
||||
text: step.text,
|
||||
url: step.url,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
@ -31,12 +31,9 @@ body {
|
|||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
background: linear-gradient(to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
@ -145,4 +142,31 @@ a {
|
|||
position: relative;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Blog post spacing overrides */
|
||||
.prose h2 {
|
||||
margin-top: 1.75rem !important;
|
||||
margin-bottom: 0.875rem !important;
|
||||
padding-top: 0.875rem !important;
|
||||
font-size: 2.25rem !important;
|
||||
line-height: 2.5rem !important;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
margin-bottom: 0.875rem !important;
|
||||
}
|
||||
|
||||
.prose ul {
|
||||
margin-top: 0.875rem !important;
|
||||
margin-bottom: 0.875rem !important;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
margin-top: 1.25rem !important;
|
||||
margin-bottom: 1.25rem !important;
|
||||
}
|
||||
|
||||
.prose .lead {
|
||||
margin-bottom: 1.25rem !important;
|
||||
}
|
||||
Loading…
Reference in New Issue