SEO: Fix structured data validation errors, delete static sitemap, and update indexing scripts

This commit is contained in:
Timo Knuth
2026-01-23 23:10:22 +01:00
parent f3637fc2fe
commit eef4855c1b
147 changed files with 24590 additions and 27027 deletions

View File

@@ -0,0 +1,175 @@
'use client';
import { useEffect, useState } from 'react';
import { useParams } from 'next/navigation';
import { Copy, Check, ExternalLink, Gift } from 'lucide-react';
interface CouponData {
code: string;
discount: string;
title?: string;
description?: string;
expiryDate?: string;
redeemUrl?: string;
}
export default function CouponPage() {
const params = useParams();
const slug = params.slug as string;
const [coupon, setCoupon] = useState<CouponData | null>(null);
const [loading, setLoading] = useState(true);
const [copied, setCopied] = useState(false);
useEffect(() => {
async function fetchCoupon() {
try {
const res = await fetch(`/api/qrs/public/${slug}`);
if (res.ok) {
const data = await res.json();
if (data.contentType === 'COUPON') {
setCoupon(data.content as CouponData);
}
}
} catch (error) {
console.error('Error fetching coupon:', error);
} finally {
setLoading(false);
}
}
fetchCoupon();
}, [slug]);
const copyCode = async () => {
if (coupon?.code) {
await navigator.clipboard.writeText(coupon.code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
// Loading
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-rose-50 to-pink-100">
<div className="w-10 h-10 border-3 border-pink-200 border-t-pink-600 rounded-full animate-spin"></div>
</div>
);
}
// Not found
if (!coupon) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-rose-50 to-pink-100 px-6">
<div className="text-center bg-white rounded-2xl p-8 shadow-lg">
<p className="text-gray-500 text-lg">This coupon is not available.</p>
</div>
</div>
);
}
const isExpired = coupon.expiryDate && new Date(coupon.expiryDate) < new Date();
return (<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E] px-6 py-12">
<div className="max-w-sm w-full">
{/* Card */}
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
{/* Colorful Header */}
<div className="bg-gradient-to-br from-[#DB5375] to-[#B3FFB3] text-gray-900 p-8 text-center relative overflow-hidden">
{/* Decorative circles */}
<div className="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2"></div>
<div className="absolute bottom-0 left-0 w-24 h-24 bg-white/10 rounded-full translate-y-1/2 -translate-x-1/2"></div>
<div className="relative">
<div className="w-14 h-14 bg-[#DB5375]/10 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Gift className="w-7 h-7 text-[#DB5375]" />
</div>
<p className="text-gray-700 text-sm mb-1">{coupon.title || 'Special Offer'}</p>
<p className="text-4xl font-bold tracking-tight text-gray-900">{coupon.discount}</p>
</div>
</div>
{/* Dotted line with circles */}
<div className="relative py-4">
<div className="relative py-4">
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-5 h-10 bg-gradient-to-br from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E] rounded-r-full"></div>
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-5 h-10 bg-gradient-to-br from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E] rounded-l-full"></div>
<div className="border-t-2 border-dashed border-gray-200 mx-8"></div>
</div>
{/* Content */}
<div className="px-8 pb-8">
{coupon.description && (
<p className="text-gray-500 text-sm text-center mb-6">{coupon.description}</p>
)}
{/* Code Box */}
<div className="bg-gray-50 rounded-2xl p-5 mb-4 border border-emerald-100">
<p className="text-xs text-emerald-600 text-center mb-2 font-medium uppercase tracking-wider">Your Code</p>
<div className="flex items-center justify-center gap-3">
<code className="text-2xl font-mono font-bold text-gray-900 tracking-wider">
{coupon.code}
</code>
<button
onClick={copyCode}
className={`p-2.5 rounded-xl transition-all ${copied
? 'bg-emerald-100 text-emerald-600'
: 'bg-white text-gray-500 hover:text-rose-500 shadow-sm hover:shadow'
}`}
aria-label="Copy code"
>
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
</button>
</div>
{copied && (
<p className="text-emerald-600 text-xs text-center mt-2 font-medium">Copied!</p>
)}
</div>
{/* Expiry */}
{coupon.expiryDate && (
<p className={`text-sm text-center mb-6 font-medium ${isExpired ? 'text-red-500' : 'text-gray-400'}`}>
{isExpired
? '⚠️ This coupon has expired'
: `Valid until ${new Date(coupon.expiryDate).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
})}`
}
</p>
)}
{/* Redeem Button */}
{coupon.redeemUrl && !isExpired && (
<a
href={ensureAbsoluteUrl(coupon.redeemUrl)}
target="_blank"
rel="noopener noreferrer"
className="block w-full py-4 rounded-xl font-semibold text-center bg-gradient-to-r from-[#076653] to-[#0C342C] text-white hover:from-[#087861] hover:to-[#0E4036] transition-all shadow-lg shadow-emerald-200"
>
<span className="flex items-center justify-center gap-2">
Redeem Now
<ExternalLink className="w-4 h-4" />
</span>
</a>
)}
</div>
</div>
{/* Footer */}
<p className="text-center text-sm text-white/60 mt-6">
Powered by QR Master
</p>
</div>
</div>
</div>
);
}
function ensureAbsoluteUrl(url: string | undefined): string | undefined {
if (!url) return undefined;
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
return url;
}
return `https://${url}`;
}