SEO: Fix structured data validation errors, delete static sitemap, and update indexing scripts
This commit is contained in:
parent
f3637fc2fe
commit
eef4855c1b
|
|
@ -1,33 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
||||||
<url>
|
|
||||||
<loc>https://www.qrmaster.net/</loc>
|
|
||||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>0.9</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://www.qrmaster.net/blog</loc>
|
|
||||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>0.7</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://www.qrmaster.net/pricing</loc>
|
|
||||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.8</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://www.qrmaster.net/faq</loc>
|
|
||||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.6</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://www.qrmaster.net/blog/qr-code-analytics</loc>
|
|
||||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.6</priority>
|
|
||||||
</url>
|
|
||||||
</urlset>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
export default function AuthLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
|
||||||
import { Input } from '@/components/ui/Input';
|
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
import { useTranslation } from '@/hooks/useTranslation';
|
|
||||||
import { useCsrf } from '@/hooks/useCsrf';
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetchWithCsrf('/api/auth/simple-login', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ email, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
|
||||||
// Store user in localStorage for client-side
|
|
||||||
localStorage.setItem('user', JSON.stringify(data.user));
|
|
||||||
|
|
||||||
// Track successful login with PostHog
|
|
||||||
try {
|
|
||||||
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
|
|
||||||
identifyUser(data.user.id, {
|
|
||||||
email: data.user.email,
|
|
||||||
name: data.user.name,
|
|
||||||
plan: data.user.plan || 'FREE',
|
|
||||||
});
|
|
||||||
trackEvent('user_login', {
|
|
||||||
method: 'email',
|
|
||||||
email: data.user.email,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('PostHog tracking error:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for redirect parameter
|
|
||||||
const redirectUrl = searchParams.get('redirect') || '/dashboard';
|
|
||||||
router.push(redirectUrl);
|
|
||||||
router.refresh();
|
|
||||||
} else {
|
|
||||||
setError(data.error || 'Invalid email or password');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('An error occurred. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGoogleSignIn = () => {
|
|
||||||
// Redirect to Google OAuth API route
|
|
||||||
window.location.href = '/api/auth/google';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
|
|
||||||
<img src="/logo.svg" alt="QR Master" className="w-10 h-10" />
|
|
||||||
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
|
||||||
</Link>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
|
|
||||||
<p className="text-gray-600 mt-2">Sign in to your account</p>
|
|
||||||
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
|
|
||||||
← Back to Home
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Email"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
placeholder="you@example.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
placeholder="••••••••"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input type="checkbox" className="mr-2" />
|
|
||||||
<span className="text-sm text-gray-600">Remember me</span>
|
|
||||||
</label>
|
|
||||||
<Link href="/forgot-password" className="text-sm text-primary-600 hover:text-primary-700">
|
|
||||||
Forgot password?
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full" loading={loading} disabled={csrfLoading || loading}>
|
|
||||||
{csrfLoading ? 'Loading...' : 'Sign In'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="relative my-6">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-sm">
|
|
||||||
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
onClick={handleGoogleSignIn}
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
fill="#4285F4"
|
|
||||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#34A853"
|
|
||||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#FBBC05"
|
|
||||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#EA4335"
|
|
||||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Sign in with Google
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Don't have an account?{' '}
|
|
||||||
<Link href="/signup" className="text-primary-600 hover:text-primary-700 font-medium">
|
|
||||||
Sign up
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<p className="text-center text-sm text-gray-500 mt-6">
|
|
||||||
By signing in, you agree to our{' '}
|
|
||||||
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
|
|
||||||
Privacy Policy
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
|
||||||
import { Input } from '@/components/ui/Input';
|
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
import { useTranslation } from '@/hooks/useTranslation';
|
|
||||||
import { useCsrf } from '@/hooks/useCsrf';
|
|
||||||
|
|
||||||
export default function SignupPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { fetchWithCsrf } = useCsrf();
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
if (password !== confirmPassword) {
|
|
||||||
setError('Passwords do not match');
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length < 8) {
|
|
||||||
setError('Password must be at least 8 characters');
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetchWithCsrf('/api/auth/signup', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ name, email, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
|
||||||
// Store user in localStorage for client-side
|
|
||||||
localStorage.setItem('user', JSON.stringify(data.user));
|
|
||||||
|
|
||||||
// Track successful signup with PostHog
|
|
||||||
try {
|
|
||||||
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
|
|
||||||
identifyUser(data.user.id, {
|
|
||||||
email: data.user.email,
|
|
||||||
name: data.user.name,
|
|
||||||
plan: data.user.plan || 'FREE',
|
|
||||||
signupMethod: 'email',
|
|
||||||
});
|
|
||||||
trackEvent('user_signup', {
|
|
||||||
method: 'email',
|
|
||||||
email: data.user.email,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('PostHog tracking error:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to dashboard
|
|
||||||
router.push('/dashboard');
|
|
||||||
router.refresh();
|
|
||||||
} else {
|
|
||||||
setError(data.error || 'Failed to create account');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('An error occurred. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGoogleSignIn = () => {
|
|
||||||
// Redirect to Google OAuth API route
|
|
||||||
window.location.href = '/api/auth/google';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
|
|
||||||
<img src="/logo.svg" alt="QR Master" className="w-10 h-10" />
|
|
||||||
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
|
||||||
</Link>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Create Account</h1>
|
|
||||||
<p className="text-gray-600 mt-2">Start creating QR codes in seconds</p>
|
|
||||||
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
|
|
||||||
← Back to Home
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Full Name"
|
|
||||||
type="text"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
placeholder="John Doe"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Email"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
placeholder="you@example.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
placeholder="••••••••"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Confirm Password"
|
|
||||||
type="password"
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
||||||
placeholder="••••••••"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full" loading={loading}>
|
|
||||||
Create Account
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="relative my-6">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-sm">
|
|
||||||
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
onClick={handleGoogleSignIn}
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
fill="#4285F4"
|
|
||||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#34A853"
|
|
||||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#FBBC05"
|
|
||||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#EA4335"
|
|
||||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Sign up with Google
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Already have an account?{' '}
|
|
||||||
<Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium">
|
|
||||||
Sign in
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<p className="text-center text-sm text-gray-500 mt-6">
|
|
||||||
By signing up, you agree to our{' '}
|
|
||||||
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
|
|
||||||
Privacy Policy
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function AuthLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex flex-col">
|
||||||
|
<div className="flex-grow">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="py-6 text-center text-sm text-gray-500 bg-transparent">
|
||||||
|
<div className="space-x-6 mb-2">
|
||||||
|
<Link href="/" className="hover:text-gray-900 transition-colors">Home</Link>
|
||||||
|
<Link href="/pricing" className="hover:text-gray-900 transition-colors">Pricing</Link>
|
||||||
|
<Link href="/privacy" className="hover:text-gray-900 transition-colors">Privacy</Link>
|
||||||
|
<Link href="/faq" className="hover:text-gray-900 transition-colors">FAQ</Link>
|
||||||
|
</div>
|
||||||
|
<p>© {new Date().getFullYear()} QR Master</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Card, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
|
import { useCsrf } from '@/hooks/useCsrf';
|
||||||
|
|
||||||
|
export default function LoginClient() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchWithCsrf('/api/auth/simple-login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
// Store user in localStorage for client-side
|
||||||
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
|
|
||||||
|
// Track successful login with PostHog
|
||||||
|
try {
|
||||||
|
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
|
||||||
|
identifyUser(data.user.id, {
|
||||||
|
email: data.user.email,
|
||||||
|
name: data.user.name,
|
||||||
|
plan: data.user.plan || 'FREE',
|
||||||
|
});
|
||||||
|
trackEvent('user_login', {
|
||||||
|
method: 'email',
|
||||||
|
email: data.user.email,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PostHog tracking error:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for redirect parameter
|
||||||
|
const redirectUrl = searchParams.get('redirect') || '/dashboard';
|
||||||
|
router.push(redirectUrl);
|
||||||
|
router.refresh();
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Invalid email or password');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('An error occurred. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoogleSignIn = () => {
|
||||||
|
// Redirect to Google OAuth API route
|
||||||
|
window.location.href = '/api/auth/google';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
|
||||||
|
<img src="/logo.svg" alt="QR Master" className="w-10 h-10" />
|
||||||
|
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
|
||||||
|
<p className="text-gray-600 mt-2">Sign in to your account</p>
|
||||||
|
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
|
||||||
|
← Back to Home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input type="checkbox" className="mr-2" />
|
||||||
|
<span className="text-sm text-gray-600">Remember me</span>
|
||||||
|
</label>
|
||||||
|
<Link href="/forgot-password" className="text-sm text-primary-600 hover:text-primary-700">
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" loading={loading} disabled={csrfLoading || loading}>
|
||||||
|
{csrfLoading ? 'Loading...' : 'Sign In'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="relative my-6">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleGoogleSignIn}
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="#4285F4"
|
||||||
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC05"
|
||||||
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Sign in with Google
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<Link href="/signup" className="text-primary-600 hover:text-primary-700 font-medium">
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<p className="text-center text-sm text-gray-500 mt-6">
|
||||||
|
By signing in, you agree to our{' '}
|
||||||
|
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import LoginClient from './LoginClient';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'QR Master – Smart QR Generator & Analytics',
|
||||||
|
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics. Free advanced features, bulk generation, and custom branding available.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
return <LoginClient />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
|
import { useCsrf } from '@/hooks/useCsrf';
|
||||||
|
|
||||||
|
export default function SignupClient() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { fetchWithCsrf } = useCsrf();
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError('Passwords do not match');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
setError('Password must be at least 8 characters');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchWithCsrf('/api/auth/signup', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ name, email, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
// Store user in localStorage for client-side
|
||||||
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
|
|
||||||
|
// Track successful signup with PostHog
|
||||||
|
try {
|
||||||
|
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
|
||||||
|
identifyUser(data.user.id, {
|
||||||
|
email: data.user.email,
|
||||||
|
name: data.user.name,
|
||||||
|
plan: data.user.plan || 'FREE',
|
||||||
|
signupMethod: 'email',
|
||||||
|
});
|
||||||
|
trackEvent('user_signup', {
|
||||||
|
method: 'email',
|
||||||
|
email: data.user.email,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PostHog tracking error:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to dashboard
|
||||||
|
router.push('/dashboard');
|
||||||
|
router.refresh();
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Failed to create account');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('An error occurred. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoogleSignIn = () => {
|
||||||
|
// Redirect to Google OAuth API route
|
||||||
|
window.location.href = '/api/auth/google';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
|
||||||
|
<img src="/logo.svg" alt="QR Master" className="w-10 h-10" />
|
||||||
|
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Create Account</h1>
|
||||||
|
<p className="text-gray-600 mt-2">Start creating QR codes in seconds</p>
|
||||||
|
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
|
||||||
|
← Back to Home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Full Name"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="John Doe"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Confirm Password"
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" loading={loading}>
|
||||||
|
Create Account
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="relative my-6">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleGoogleSignIn}
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="#4285F4"
|
||||||
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC05"
|
||||||
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Sign up with Google
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Already have an account?{' '}
|
||||||
|
<Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium">
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<p className="text-center text-sm text-gray-500 mt-6">
|
||||||
|
By signing up, you agree to our{' '}
|
||||||
|
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
import SignupClient from './SignupClient';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Create Account | QR Master',
|
||||||
|
description: 'Start creating dynamic QR codes in seconds. Join thousands of businesses using QR Master.',
|
||||||
|
alternates: {
|
||||||
|
canonical: 'https://www.qrmaster.net/signup',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SignupPage() {
|
||||||
|
return <SignupClient />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
import React from 'react';
|
||||||
|
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 from '@/components/Breadcrumbs';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
import { blogPosts } from '@/lib/blog-data';
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
params: {
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return Object.keys(blogPosts).map((slug) => ({
|
||||||
|
slug,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateMetadata({ params }: PageProps): Metadata {
|
||||||
|
const post = blogPosts[params.slug];
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
notFound();
|
||||||
|
return {} as Metadata; // Typescript satisfaction (unreachable)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: {
|
||||||
|
absolute: `${post.title} | QR Master Blog`,
|
||||||
|
},
|
||||||
|
description: post.excerpt,
|
||||||
|
openGraph: {
|
||||||
|
title: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
|
type: 'article',
|
||||||
|
publishedTime: post.datePublished,
|
||||||
|
modifiedTime: post.dateModified,
|
||||||
|
authors: [post.author],
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: post.image,
|
||||||
|
alt: post.imageAlt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
|
images: [post.image],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlogPostPage({ params }: PageProps) {
|
||||||
|
const post = blogPosts[params.slug];
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonLd = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BlogPosting',
|
||||||
|
headline: post.title,
|
||||||
|
image: post.image,
|
||||||
|
datePublished: post.datePublished,
|
||||||
|
dateModified: post.dateModified,
|
||||||
|
author: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: post.author,
|
||||||
|
url: post.authorUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const breadcrumbItems = [
|
||||||
|
{ name: 'Home', url: '/' },
|
||||||
|
{ name: 'Blog', url: '/blog' },
|
||||||
|
{ name: post.title, url: `/blog/${post.slug}` },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SeoJsonLd data={[jsonLd]} />
|
||||||
|
|
||||||
|
<div className="min-h-screen bg-white pb-20">
|
||||||
|
{/* Hero Header */}
|
||||||
|
<div className="bg-gray-50 border-b border-gray-100">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-12 max-w-4xl">
|
||||||
|
<Breadcrumbs items={breadcrumbItems} className="mb-8" />
|
||||||
|
|
||||||
|
<Badge variant="info" className="mb-6">
|
||||||
|
{post.category}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 leading-tight mb-6">
|
||||||
|
{post.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex items-center text-gray-600 mb-8 space-x-6 text-sm">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="font-medium text-gray-900 mr-2">{post.author}</span>
|
||||||
|
</div>
|
||||||
|
<div>•</div>
|
||||||
|
<div>{post.date}</div>
|
||||||
|
<div>•</div>
|
||||||
|
<div>{post.readTime} read</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Featured Image */}
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl -mt-8 mb-12">
|
||||||
|
<div className="relative aspect-video w-full overflow-hidden rounded-2xl shadow-xl">
|
||||||
|
<Image
|
||||||
|
src={post.image}
|
||||||
|
alt={post.imageAlt}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<article className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-3xl">
|
||||||
|
<div
|
||||||
|
className="prose prose-lg prose-blue max-w-none hover:prose-a:text-blue-600 transition-colors"
|
||||||
|
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Share / CTA */}
|
||||||
|
<div className="mt-16 pt-8 border-t border-gray-200">
|
||||||
|
<div className="bg-blue-50 rounded-2xl p-8 text-center">
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 mb-4">
|
||||||
|
Enjoyed this article?
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 mb-6 text-lg">
|
||||||
|
Create your first dynamic QR code for free and start tracking your campaigns today.
|
||||||
|
</p>
|
||||||
|
<Link href="/signup">
|
||||||
|
<Button size="lg" className="px-8">
|
||||||
|
Create Free QR Code
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ function truncateAtWord(text: string, maxLength: number): string {
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const title = truncateAtWord('QR Insights: Latest QR Strategies', 60);
|
const title = truncateAtWord('QR Insights: Latest QR Strategies', 60);
|
||||||
const description = truncateAtWord(
|
const description = truncateAtWord(
|
||||||
'Expert guides on QR analytics, dynamic codes & smart marketing uses.',
|
'Expert guides on QR code strategies, marketing campaigns, tracking analytics, and best practices for small businesses and enterprises.',
|
||||||
160
|
160
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -91,7 +91,7 @@ const blogPosts = [
|
||||||
date: 'October 18, 2025',
|
date: 'October 18, 2025',
|
||||||
readTime: '12 Min',
|
readTime: '12 Min',
|
||||||
category: 'Tracking & Analytics',
|
category: 'Tracking & Analytics',
|
||||||
image: '/blog/1-hero.png',
|
image: '/blog/1-hero.webp',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'dynamic-vs-static-qr-codes',
|
slug: 'dynamic-vs-static-qr-codes',
|
||||||
|
|
@ -100,7 +100,7 @@ const blogPosts = [
|
||||||
date: 'October 17, 2025',
|
date: 'October 17, 2025',
|
||||||
readTime: '10 Min',
|
readTime: '10 Min',
|
||||||
category: 'QR Code Basics',
|
category: 'QR Code Basics',
|
||||||
image: '/blog/2-hero.png',
|
image: '/blog/2-hero.webp',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'bulk-qr-code-generator-excel',
|
slug: 'bulk-qr-code-generator-excel',
|
||||||
|
|
@ -109,7 +109,7 @@ const blogPosts = [
|
||||||
date: 'October 16, 2025',
|
date: 'October 16, 2025',
|
||||||
readTime: '13 Min',
|
readTime: '13 Min',
|
||||||
category: 'Bulk Generation',
|
category: 'Bulk Generation',
|
||||||
image: '/blog/3-hero.png',
|
image: '/blog/bulk-qr-events-hero.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'qr-code-analytics',
|
slug: 'qr-code-analytics',
|
||||||
|
|
@ -118,7 +118,7 @@ const blogPosts = [
|
||||||
date: 'October 16, 2025',
|
date: 'October 16, 2025',
|
||||||
readTime: '15 Min',
|
readTime: '15 Min',
|
||||||
category: 'Analytics',
|
category: 'Analytics',
|
||||||
image: '/blog/4-hero.png',
|
image: '/blog/qr-code-analytics-hero.webp',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -8,8 +8,10 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
||||||
import { breadcrumbSchema } from '@/lib/schema';
|
import { breadcrumbSchema } from '@/lib/schema';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel | QR Master',
|
title: {
|
||||||
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Create URLs, vCards, locations, phone numbers, and text QR codes in bulk. Perfect for products, events, inventory management.',
|
absolute: 'Bulk QR Code Generator - Create 1000s from Excel',
|
||||||
|
},
|
||||||
|
description: 'Generate hundreds of QR codes at once from Excel/CSV. Create URLs, vCards, and more in bulk with custom branding. Perfect for products and events.',
|
||||||
keywords: 'bulk qr code generator, batch qr code, qr code from excel, csv qr code generator, mass qr code generation, bulk vcard qr code, bulk qr codes free',
|
keywords: 'bulk qr code generator, batch qr code, qr code from excel, csv qr code generator, mass qr code generation, bulk vcard qr code, bulk qr codes free',
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||||
|
|
@ -19,14 +21,14 @@ export const metadata: Metadata = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel',
|
title: 'Bulk QR Code Generator - Create 1000s from Excel',
|
||||||
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.',
|
description: 'Generate hundreds of QR codes at once from Excel/CSV. Create URLs, vCards, and more in bulk with custom branding.',
|
||||||
url: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
url: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel',
|
title: 'Bulk QR Code Generator - Create 1000s from Excel',
|
||||||
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.',
|
description: 'Generate hundreds of QR codes at once from Excel/CSV. Create URLs, vCards, and more in bulk with custom branding.',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -46,7 +48,7 @@ export default function BulkQRCodeGeneratorPage() {
|
||||||
title: 'Contact Cards',
|
title: 'Contact Cards',
|
||||||
description: 'Create vCard QR codes with contact information',
|
description: 'Create vCard QR codes with contact information',
|
||||||
format: 'FirstName,LastName,Email,Phone,Organization,Title',
|
format: 'FirstName,LastName,Email,Phone,Organization,Title',
|
||||||
example: 'John Doe,VCARD,John,Doe,john@example.com,+1234567890,Company Inc,CEO',
|
example: 'John Doe,VCARD,John,Doe,john[at]example.com,+1234567890,Company Inc,CEO',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'GEO',
|
type: 'GEO',
|
||||||
|
|
@ -333,7 +335,7 @@ export default function BulkQRCodeGeneratorPage() {
|
||||||
Start Bulk Generation
|
Start Bulk Generation
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/create">
|
<Link href="/signup">
|
||||||
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
||||||
Try Single QR First
|
Try Single QR First
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -440,7 +442,7 @@ export default function BulkQRCodeGeneratorPage() {
|
||||||
<tr className="border-b border-gray-200">
|
<tr className="border-b border-gray-200">
|
||||||
<td className="py-2 px-3">John Doe</td>
|
<td className="py-2 px-3">John Doe</td>
|
||||||
<td className="py-2 px-3">VCARD</td>
|
<td className="py-2 px-3">VCARD</td>
|
||||||
<td className="py-2 px-3">John,Doe,john@example.com,+1234567890,Company,CEO</td>
|
<td className="py-2 px-3">John,Doe,john<span>@</span>example.com,+1234567890,Company,CEO</td>
|
||||||
<td className="py-2 px-3">contact</td>
|
<td className="py-2 px-3">contact</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="border-b border-gray-200">
|
<tr className="border-b border-gray-200">
|
||||||
|
|
@ -331,7 +331,7 @@ export default function CustomQRCodeGeneratorPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<Link href="/create">
|
<Link href="/signup">
|
||||||
<Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
<Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
||||||
Try Designer Now
|
Try Designer Now
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -452,7 +452,7 @@ export default function CustomQRCodeGeneratorPage() {
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-gray-900">{example.name}</h3>
|
<h3 className="font-bold text-gray-900">{example.name}</h3>
|
||||||
<p className="text-sm text-gray-600">{example.description}</p>
|
<p className="text-sm text-gray-600">{example.description}</p>
|
||||||
<Link href="/create">
|
<Link href="/signup">
|
||||||
<Button variant="outline" size="sm" className="w-full mt-3">
|
<Button variant="outline" size="sm" className="w-full mt-3">
|
||||||
Use This Style
|
Use This Style
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -634,7 +634,7 @@ export default function CustomQRCodeGeneratorPage() {
|
||||||
Free static QR codes with logo, colors, and frames. No signup required to try. Upgrade for tracking and bulk generation.
|
Free static QR codes with logo, colors, and frames. No signup required to try. Upgrade for tracking and bulk generation.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<Link href="/create">
|
<Link href="/signup">
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
@ -8,8 +8,10 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
||||||
import { breadcrumbSchema } from '@/lib/schema';
|
import { breadcrumbSchema } from '@/lib/schema';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
|
title: {
|
||||||
description: 'Create dynamic QR codes that can be edited after printing. Change destination URL, track scans, and update content without reprinting. Free dynamic QR code generator.',
|
absolute: 'Dynamic QR Code Generator - Edit Anytime',
|
||||||
|
},
|
||||||
|
description: 'Create dynamic QR codes that can be edited after printing. Change destination URLs and track scans without reprinting. Free generator with advanced features.',
|
||||||
keywords: 'dynamic qr code generator, editable qr code, dynamic qr code, free dynamic qr code, qr code generator dynamic, best dynamic qr code generator',
|
keywords: 'dynamic qr code generator, editable qr code, dynamic qr code, free dynamic qr code, qr code generator dynamic, best dynamic qr code generator',
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
canonical: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
||||||
|
|
@ -19,13 +21,13 @@ export const metadata: Metadata = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
|
title: 'Dynamic QR Code Generator - Edit Anytime',
|
||||||
description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.',
|
description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.',
|
||||||
url: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
url: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
|
title: 'Dynamic QR Code Generator - Edit Anytime',
|
||||||
description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.',
|
description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -180,7 +182,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||||
position: 2,
|
position: 2,
|
||||||
name: 'Generate QR Code',
|
name: 'Generate QR Code',
|
||||||
text: 'Enter your destination URL and customize the design with your branding',
|
text: 'Enter your destination URL and customize the design with your branding',
|
||||||
url: 'https://www.qrmaster.net/create',
|
url: 'https://www.qrmaster.net/signup',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@type': 'HowToStep',
|
'@type': 'HowToStep',
|
||||||
|
|
@ -504,7 +506,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
||||||
Get Started Free
|
Get Started Free
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/create">
|
<Link href="/signup">
|
||||||
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
|
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
|
||||||
Create QR Code Now
|
Create QR Code Now
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -3,6 +3,7 @@ import type { Metadata } from 'next';
|
||||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||||
import { faqPageSchema } from '@/lib/schema';
|
import { faqPageSchema } from '@/lib/schema';
|
||||||
import { Card, CardContent } from '@/components/ui/Card';
|
import { Card, CardContent } from '@/components/ui/Card';
|
||||||
|
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||||
|
|
||||||
function truncateAtWord(text: string, maxLength: number): string {
|
function truncateAtWord(text: string, maxLength: number): string {
|
||||||
if (text.length <= maxLength) return text;
|
if (text.length <= maxLength) return text;
|
||||||
|
|
@ -129,9 +130,7 @@ export default function FAQPage() {
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-gray-700 mb-6 leading-relaxed">
|
<p className="text-lg text-gray-700 mb-6 leading-relaxed">
|
||||||
Our support team is here to help. Contact us at{' '}
|
Our support team is here to help. Contact us at{' '}
|
||||||
<a href="mailto:support@qrmaster.net" className="text-blue-600 hover:text-blue-700 font-semibold">
|
<ObfuscatedMailto email="support@qrmaster.net" className="text-blue-600 hover:text-blue-700 font-semibold" />{' '}
|
||||||
support@qrmaster.net
|
|
||||||
</a>{' '}
|
|
||||||
or reach out through our live chat.
|
or reach out through our live chat.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -22,7 +22,9 @@ import {
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'QR Code Management Software – Organize, Edit & Scale | QR Master',
|
title: {
|
||||||
|
absolute: 'QR Code Management Software – Organize, Edit & Scale',
|
||||||
|
},
|
||||||
description: 'Manage QR codes at scale with folders, bulk editing, team collaboration, and campaign organization. Centralized dashboard for businesses. Free trial available.',
|
description: 'Manage QR codes at scale with folders, bulk editing, team collaboration, and campaign organization. Centralized dashboard for businesses. Free trial available.',
|
||||||
keywords: [
|
keywords: [
|
||||||
'manage qr codes',
|
'manage qr codes',
|
||||||
|
|
@ -14,7 +14,7 @@ function truncateAtWord(text: string, maxLength: number): string {
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const title = truncateAtWord('QR Master: Dynamic QR Generator', 60);
|
const title = truncateAtWord('QR Master: Dynamic QR Generator', 60);
|
||||||
const description = truncateAtWord(
|
const description = truncateAtWord(
|
||||||
'Dynamic QR, branding, bulk generation & analytics for all campaigns.',
|
'Create dynamic QR codes, track scans, and scale campaigns with secure analytics. Free advanced features, bulk generation, and custom branding available.',
|
||||||
160
|
160
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -27,6 +27,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||||
languages: {
|
languages: {
|
||||||
'x-default': 'https://www.qrmaster.net/',
|
'x-default': 'https://www.qrmaster.net/',
|
||||||
en: 'https://www.qrmaster.net/',
|
en: 'https://www.qrmaster.net/',
|
||||||
|
de: 'https://www.qrmaster.net/qr-code-erstellen',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
|
|
@ -49,7 +50,7 @@ export default function HomePage() {
|
||||||
|
|
||||||
{/* Server-rendered SEO content for crawlers */}
|
{/* Server-rendered SEO content for crawlers */}
|
||||||
<div className="sr-only" aria-hidden="false">
|
<div className="sr-only" aria-hidden="false">
|
||||||
<h1>QR Master: Free Dynamic QR Code Generator with Tracking & Analytics</h1>
|
|
||||||
<p>
|
<p>
|
||||||
Create professional QR codes for your business with QR Master. Our dynamic QR code generator
|
Create professional QR codes for your business with QR Master. Our dynamic QR code generator
|
||||||
lets you create trackable QR codes, edit destinations anytime, and view detailed analytics.
|
lets you create trackable QR codes, edit destinations anytime, and view detailed analytics.
|
||||||
|
|
@ -7,6 +7,7 @@ import { Badge } from '@/components/ui/Badge';
|
||||||
import { showToast } from '@/components/ui/Toast';
|
import { showToast } from '@/components/ui/Toast';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { BillingToggle } from '@/components/ui/BillingToggle';
|
import { BillingToggle } from '@/components/ui/BillingToggle';
|
||||||
|
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -260,7 +261,7 @@ export default function PricingPage() {
|
||||||
All plans include unlimited static QR codes and basic customization.
|
All plans include unlimited static QR codes and basic customization.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-600 mt-2">
|
<p className="text-gray-600 mt-2">
|
||||||
Need help choosing? <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a>
|
Need help choosing? <ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</ObfuscatedMailto>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -33,8 +33,7 @@ export const metadata: Metadata = {
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Server-rendered H1 for SEO */}
|
|
||||||
<h1 className="sr-only">QR Master Pricing – Choose Your QR Code Plan</h1>
|
|
||||||
<div className="sr-only">
|
<div className="sr-only">
|
||||||
<h2>Compare our plans</h2>
|
<h2>Compare our plans</h2>
|
||||||
<p>Find the best QR code solution for your business. From free personal tiers to enterprise-grade dynamic code management.</p>
|
<p>Find the best QR code solution for your business. From free personal tiers to enterprise-grade dynamic code management.</p>
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Privacy Policy | QR Master',
|
title: 'Privacy Policy | QR Master',
|
||||||
description: 'Privacy Policy and data protection information for QR Master',
|
description: 'Read our Privacy Policy to understand how QR Master collects, uses, and protects your data. We are GDPR compliant and committed to user privacy and security.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PrivacyPage() {
|
export default function PrivacyPage() {
|
||||||
|
|
@ -111,9 +112,7 @@ export default function PrivacyPage() {
|
||||||
<div className="bg-gray-50 p-6 rounded-lg">
|
<div className="bg-gray-50 p-6 rounded-lg">
|
||||||
<p className="text-gray-700 mb-2">
|
<p className="text-gray-700 mb-2">
|
||||||
<strong>Email:</strong>{' '}
|
<strong>Email:</strong>{' '}
|
||||||
<a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700">
|
<ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700" />
|
||||||
support@qrmaster.net
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.net</a></p>
|
<p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.net</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -8,7 +8,9 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
||||||
import { breadcrumbSchema } from '@/lib/schema';
|
import { breadcrumbSchema } from '@/lib/schema';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
|
title: {
|
||||||
|
absolute: 'QR Code Tracking & Analytics - Track Every Scan',
|
||||||
|
},
|
||||||
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior. Free QR code tracking software with detailed reports.',
|
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior. Free QR code tracking software with detailed reports.',
|
||||||
keywords: 'qr code tracking, qr code analytics, track qr scans, qr code statistics, free qr tracking, qr code monitoring',
|
keywords: 'qr code tracking, qr code analytics, track qr scans, qr code statistics, free qr tracking, qr code monitoring',
|
||||||
alternates: {
|
alternates: {
|
||||||
|
|
@ -19,13 +21,13 @@ export const metadata: Metadata = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
|
title: 'QR Code Tracking & Analytics - Track Every Scan',
|
||||||
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
||||||
url: 'https://www.qrmaster.net/qr-code-tracking',
|
url: 'https://www.qrmaster.net/qr-code-tracking',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
|
title: 'QR Code Tracking & Analytics - Track Every Scan',
|
||||||
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -199,7 +201,7 @@ export default function QRCodeTrackingPage() {
|
||||||
Start Tracking Free
|
Start Tracking Free
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/create">
|
<Link href="/signup">
|
||||||
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
||||||
Create Trackable QR Code
|
Create Trackable QR Code
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -319,79 +321,79 @@ export default function QRCodeTrackingPage() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Comparison Table */}
|
{/* Comparison Table */}
|
||||||
<section className="py-20 bg-gray-50">
|
<section className="py-20 bg-gray-50">
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl">
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl">
|
||||||
<div className="text-center mb-16">
|
<div className="text-center mb-16">
|
||||||
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
||||||
QR Master vs Free Tools
|
QR Master vs Free Tools
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl text-gray-600">
|
<p className="text-xl text-gray-600">
|
||||||
See why businesses choose QR Master for QR code tracking
|
See why businesses choose QR Master for QR code tracking
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="overflow-hidden">
|
<Card className="overflow-hidden">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gray-100">
|
<thead className="bg-gray-100">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-4 text-left text-gray-900 font-semibold">Feature</th>
|
<th className="px-6 py-4 text-left text-gray-900 font-semibold">Feature</th>
|
||||||
<th className="px-6 py-4 text-center text-gray-900 font-semibold">Free Tools</th>
|
<th className="px-6 py-4 text-center text-gray-900 font-semibold">Free Tools</th>
|
||||||
<th className="px-6 py-4 text-center text-primary-600 font-semibold">QR Master</th>
|
<th className="px-6 py-4 text-center text-primary-600 font-semibold">QR Master</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{comparisonData.map((row, index) => (
|
{comparisonData.map((row, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td className="px-6 py-4 text-gray-900 font-medium">{row.feature}</td>
|
<td className="px-6 py-4 text-gray-900 font-medium">{row.feature}</td>
|
||||||
<td className="px-6 py-4 text-center">
|
<td className="px-6 py-4 text-center">
|
||||||
{typeof row.free === 'boolean' ? (
|
{typeof row.free === 'boolean' ? (
|
||||||
row.free ? (
|
row.free ? (
|
||||||
<span className="text-green-500 text-2xl">✓</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-red-500 text-2xl">✗</span>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-600">{row.free}</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-center">
|
|
||||||
{typeof row.qrMaster === 'boolean' ? (
|
|
||||||
<span className="text-green-500 text-2xl">✓</span>
|
<span className="text-green-500 text-2xl">✓</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-primary-600 font-semibold">{row.qrMaster}</span>
|
<span className="text-red-500 text-2xl">✗</span>
|
||||||
)}
|
)
|
||||||
</td>
|
) : (
|
||||||
</tr>
|
<span className="text-gray-600">{row.free}</span>
|
||||||
))}
|
)}
|
||||||
</tbody>
|
</td>
|
||||||
</table>
|
<td className="px-6 py-4 text-center">
|
||||||
</Card>
|
{typeof row.qrMaster === 'boolean' ? (
|
||||||
</div>
|
<span className="text-green-500 text-2xl">✓</span>
|
||||||
</section>
|
) : (
|
||||||
|
<span className="text-primary-600 font-semibold">{row.qrMaster}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<section className="py-20 bg-gradient-to-r from-primary-600 to-purple-600 text-white">
|
<section className="py-20 bg-gradient-to-r from-primary-600 to-purple-600 text-white">
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center">
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center">
|
||||||
<h2 className="text-4xl font-bold mb-6">
|
<h2 className="text-4xl font-bold mb-6">
|
||||||
Start Tracking Your QR Codes Today
|
Start Tracking Your QR Codes Today
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl mb-8 text-primary-100">
|
<p className="text-xl mb-8 text-primary-100">
|
||||||
Join thousands of businesses using QR Master to track and optimize their QR code campaigns
|
Join thousands of businesses using QR Master to track and optimize their QR code campaigns
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<Link href="/signup">
|
<Link href="/signup">
|
||||||
<Button size="lg" variant="secondary" className="text-lg px-8 py-4 w-full sm:w-auto bg-white text-primary-600 hover:bg-gray-100">
|
<Button size="lg" variant="secondary" className="text-lg px-8 py-4 w-full sm:w-auto bg-white text-primary-600 hover:bg-gray-100">
|
||||||
Create Free Account
|
Create Free Account
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/pricing">
|
<Link href="/pricing">
|
||||||
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
|
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
|
||||||
View Pricing
|
View Pricing
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue