press releases

This commit is contained in:
Timo Knuth 2026-01-27 12:29:44 +01:00
parent be5db36b7f
commit 4dc7c29134
19 changed files with 606 additions and 16 deletions

6
.eslintrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": [
"next/core-web-vitals",
"next/typescript"
]
}

16
package-lock.json generated
View File

@ -68,7 +68,7 @@
"autoprefixer": "^10.4.16",
"cross-env": "^10.1.0",
"eslint": "^8.56.0",
"eslint-config-next": "^16.1.1",
"eslint-config-next": "16.1.5",
"postcss": "^8.4.32",
"prettier": "^3.1.1",
"prisma": "^5.7.0",
@ -2625,9 +2625,9 @@
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.1.tgz",
"integrity": "sha512-Ovb/6TuLKbE1UiPcg0p39Ke3puyTCIKN9hGbNItmpQsp+WX3qrjO3WaMVSi6JHr9X1NrmthqIguVHodMJbh/dw==",
"version": "16.1.5",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.5.tgz",
"integrity": "sha512-gUWcEsOl+1W7XakmouClcJ0TNFCkblvDUho31wulbDY9na0C6mGtBTSXGRU5GXJY65GjGj0zNaCD/GaBp888Mg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -6783,13 +6783,13 @@
}
},
"node_modules/eslint-config-next": {
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.1.tgz",
"integrity": "sha512-55nTpVWm3qeuxoQKLOjQVciKZJUphKrNM0fCcQHAIOGl6VFXgaqeMfv0aKJhs7QtcnlAPhNVqsqRfRjeKBPIUA==",
"version": "16.1.5",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.5.tgz",
"integrity": "sha512-XwXyv65DC1HXI3gMxm13jvgx0IxKu6XhZhIWTfCDt4c45njHYUM2pk1Y8QXMAWMMnqPy94I2OLMmvIrNGcwLwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/eslint-plugin-next": "16.1.1",
"@next/eslint-plugin-next": "16.1.5",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.32.0",

View File

@ -87,7 +87,7 @@
"autoprefixer": "^10.4.16",
"cross-env": "^10.1.0",
"eslint": "^8.56.0",
"eslint-config-next": "^16.1.1",
"eslint-config-next": "16.1.5",
"postcss": "^8.4.32",
"prettier": "^3.1.1",
"prisma": "^5.7.0",

3
public/grid-pattern.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 0H0V24" stroke="currentColor" stroke-width="0.5" stroke-opacity="0.1"/>
</svg>

After

Width:  |  Height:  |  Size: 193 B

View File

@ -1,7 +1,8 @@
import React from 'react';
import type { Metadata } from 'next';
import SeoJsonLd from '@/components/SeoJsonLd';
import { organizationSchema, websiteSchema, softwareApplicationSchema } from '@/lib/schema';
import { organizationSchema, websiteSchema, softwareApplicationSchema, reviewSchema, aggregateRatingSchema } from '@/lib/schema';
import { getFeaturedTestimonials, getAggregateRating } from '@/lib/testimonial-data';
import HomePageClient from '@/components/marketing/HomePageClient';
function truncateAtWord(text: string, maxLength: number): string {
@ -52,9 +53,19 @@ export async function generateMetadata(): Promise<Metadata> {
}
export default function HomePage() {
const featuredTestimonials = getFeaturedTestimonials();
const aggregateRating = getAggregateRating();
const reviewSchemas = featuredTestimonials.map(t => reviewSchema(t));
return (
<>
<SeoJsonLd data={[websiteSchema(), organizationSchema(), softwareApplicationSchema()]} />
<SeoJsonLd data={[
websiteSchema(),
organizationSchema(),
softwareApplicationSchema(),
aggregateRatingSchema(aggregateRating),
...reviewSchemas
]} />
{/* Server-rendered SEO content for crawlers */}
<div className="sr-only" aria-hidden="false">

View File

@ -0,0 +1,139 @@
import React from 'react';
import { Metadata } from 'next';
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import SeoJsonLd from '@/components/SeoJsonLd';
import { organizationSchema, websiteSchema } from '@/lib/schema';
import { ChevronRight, ExternalLink, Newspaper, Award, Calendar } from 'lucide-react';
export const metadata: Metadata = {
title: 'Press & News | QR Master',
description: 'Latest news, press releases, and updates from QR Master. Stay informed about new features, company announcements, and industry insights.',
keywords: ['qr master press', 'qr code generator news', 'company updates', 'press releases'],
openGraph: {
title: 'Press & News | QR Master',
description: 'Latest news, press releases, and updates from QR Master.',
url: 'https://www.qrmaster.net/press',
type: 'website',
}
};
export default function PressPage() {
const pressReleases = [
{
id: "launch-2026",
title: "qrmaster.net Launches Free, Professional QR Code Generator for Global Users",
date: "January 27, 2026",
excerpt: "Duesseldorf-based startup unveils qrmaster.net, a comprehensive, free online QR code generator designed to serve as the ultimate bridge between the physical and digital worlds.",
bullets: [
"Advanced tracking capabilities with UTM builder for GA4 integration",
"100% Free professional templates and design customization",
"Privacy-focused architecture with no data selling",
"Dynamic QR codes that can be edited after printing"
],
link: "https://www.prlog.org/13123883-qrmasternet-launches-free-professional-qr-code-generator-for-global-users.html",
source: "PRLog"
}
];
return (
<>
<SeoJsonLd data={[organizationSchema(), websiteSchema()]} />
<div className="bg-white min-h-screen">
{/* Hero Section */}
<section className="relative overflow-hidden bg-gradient-to-br from-gray-900 to-gray-800 text-white py-20 sm:py-24">
<div className="absolute inset-0 bg-[url('/grid-pattern.svg')] opacity-10"></div>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative z-10">
<div className="max-w-3xl">
<h1 className="text-4xl sm:text-5xl font-bold mb-6 tracking-tight">
Newsroom
</h1>
<p className="text-xl text-gray-300 max-w-2xl leading-relaxed">
Latest updates, press releases, and announcements from the QR Master team.
</p>
</div>
</div>
</section>
{/* Press Releases List */}
<section className="py-16 sm:py-24">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl">
<div className="flex items-center gap-3 mb-12">
<Newspaper className="w-6 h-6 text-blue-600" />
<h2 className="text-2xl font-bold text-gray-900">Latest Releases</h2>
</div>
<div className="space-y-12">
{pressReleases.map((pr) => (
<div key={pr.id} className="group relative bg-white border border-gray-100 rounded-2xl shadow-sm hover:shadow-md transition-all duration-300 overflow-hidden">
<div className="absolute top-0 left-0 w-1 h-full bg-blue-600 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div className="p-8 sm:p-10">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<div className="flex items-center gap-2 text-sm text-gray-500 font-medium">
<Calendar className="w-4 h-4" />
{pr.date}
</div>
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-50 text-blue-700">
Press Release
</span>
</div>
<h3 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-4 group-hover:text-blue-600 transition-colors">
<a href={pr.link} target="_blank" rel="noopener noreferrer" className="focus:outline-none">
<span className="absolute inset-0" aria-hidden="true"></span>
{pr.title}
</a>
</h3>
<p className="text-gray-600 text-lg mb-8 leading-relaxed">
{pr.excerpt}
</p>
<div className="bg-gray-50 rounded-xl p-6 mb-8">
<h4 className="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">Highlights</h4>
<ul className="grid sm:grid-cols-2 gap-4">
{pr.bullets.map((bullet, idx) => (
<li key={idx} className="flex items-start gap-2 text-gray-700">
<span className="mt-1.5 w-1.5 h-1.5 rounded-full bg-blue-500 flex-shrink-0"></span>
<span>{bullet}</span>
</li>
))}
</ul>
</div>
<div className="flex items-center justify-between mt-auto">
<div className="flex items-center gap-2 text-sm text-gray-500">
<span>Source:</span>
<span className="font-semibold text-gray-900">{pr.source}</span>
</div>
<div className="flex items-center gap-1 text-blue-600 font-semibold group-hover:gap-2 transition-all">
Read full release <ExternalLink className="w-4 h-4" />
</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>
{/* Media Kit CTA (Future proofing) */}
<section className="bg-gray-50 py-16 border-t border-gray-100">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Media Inquiries</h2>
<p className="text-gray-600 mb-8 max-w-xl mx-auto">
For press inquiries, assets, or interview requests from our leadership team.
</p>
<a href="mailto:press@qrmaster.net">
<Button variant="outline" size="lg">
Contact Press Team
</Button>
</a>
</div>
</section>
</div>
</>
);
}

View File

@ -0,0 +1,142 @@
import React from 'react';
import Link from 'next/link';
import { Metadata } from 'next';
import { Button } from '@/components/ui/Button';
import SeoJsonLd from '@/components/SeoJsonLd';
import { organizationSchema, reviewSchema, aggregateRatingSchema } from '@/lib/schema';
import { testimonials, getAggregateRating } from '@/lib/testimonial-data';
import { Testimonials } from '@/components/marketing/Testimonials';
import { Star } from 'lucide-react';
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('Customer Testimonials | QR Master Reviews', 60);
const description = truncateAtWord(
'Read what our customers say about QR Master. Real reviews from businesses using dynamic QR codes for restaurants, pottery, retail, events, and more.',
160
);
return {
title,
description,
keywords: ['qr master reviews', 'qr code testimonials', 'customer reviews', 'qr code generator reviews', 'dynamic qr code reviews'],
alternates: {
canonical: 'https://www.qrmaster.net/testimonials',
},
openGraph: {
title,
description,
url: 'https://www.qrmaster.net/testimonials',
type: 'website',
images: [
{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'QR Master Customer Testimonials',
},
],
},
twitter: {
title,
description,
},
};
}
export default function TestimonialsPage() {
const aggregateRating = getAggregateRating();
const reviewSchemas = testimonials.map(t => reviewSchema(t));
return (
<>
<SeoJsonLd data={[
organizationSchema(),
aggregateRatingSchema(aggregateRating),
...reviewSchemas
]} />
<div className="bg-white">
{/* Hero Section with Aggregate Rating */}
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 py-20 sm:py-24">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl text-center">
<h1 className="text-4xl sm:text-5xl font-bold text-gray-900 leading-tight mb-6">
Customer <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-purple-600">Testimonials</span>
</h1>
<p className="text-xl text-gray-600 max-w-2xl mx-auto mb-8 leading-relaxed">
Real experiences from businesses using QR Master to create dynamic QR codes
</p>
{/* Aggregate Rating Display */}
<div className="flex flex-col items-center justify-center gap-3 mb-10">
<div className="flex gap-1" aria-label={`${aggregateRating.ratingValue} out of 5 stars`}>
{[...Array(5)].map((_, index) => (
<Star
key={index}
className={`w-8 h-8 ${
index < aggregateRating.ratingValue
? 'fill-yellow-400 text-yellow-400'
: 'fill-gray-200 text-gray-200'
}`}
/>
))}
</div>
<p className="text-lg text-gray-700">
<span className="font-bold text-2xl">{aggregateRating.ratingValue}</span> out of 5 stars
</p>
<p className="text-sm text-gray-500">
Based on {aggregateRating.reviewCount} {aggregateRating.reviewCount === 1 ? 'review' : 'reviews'}
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Link href="/signup">
<Button size="lg" className="text-lg px-8 py-6 shadow-lg shadow-blue-500/25">
Get Started Free
</Button>
</Link>
</div>
</div>
</section>
{/* Testimonials Grid */}
<Testimonials
testimonials={testimonials}
showAll={true}
title="What Our Customers Are Saying"
subtitle="Discover how businesses use QR Master for their unique needs"
/>
{/* CTA Section */}
<section className="py-20 bg-gradient-to-br from-blue-50 via-white to-purple-50">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center">
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-6">
Ready to create your own QR codes?
</h2>
<p className="text-xl text-gray-600 mb-10 max-w-2xl mx-auto">
Join businesses using QR Master to create dynamic, trackable QR codes for their products, menus, events, and campaigns.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Link href="/signup">
<Button size="lg" className="text-lg px-8 py-6">
Start Free Today
</Button>
</Link>
<Link href="/pricing">
<Button variant="outline" size="lg" className="text-lg px-8 py-6">
View Pricing
</Button>
</Link>
</div>
</div>
</section>
</div>
</>
);
}

View File

@ -175,6 +175,18 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'yearly',
priority: 0.6,
},
{
url: `${baseUrl}/press`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/testimonials`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
...toolPages,

View File

@ -50,7 +50,7 @@ export default function AdBanner({
session.user.plan === 'LIFETIME'
);
if (shouldExclude) return null;
useEffect(() => {
// Don't load if loading session or if user is paid
@ -92,6 +92,8 @@ export default function AdBanner({
// Don't render anything while session is loading
if (status === 'loading') return null;
if (shouldExclude) return null;
return (
<div
className={`ad-container flex justify-center items-center overflow-hidden transition-opacity duration-300 ${adFilled ? 'opacity-100' : 'opacity-0 h-0'} ${className}`}

View File

@ -105,7 +105,7 @@ const AIComingSoonBanner = () => {
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-blue-100 mb-4 animate-pulse">
<Sparkles className="w-4 h-4 text-blue-600" />
<span className="text-sm font-medium text-blue-700">
Coming Soon
The Future is Here
</span>
</div>

View File

@ -14,11 +14,14 @@ import { Button } from '@/components/ui/Button';
import { ReprintCalculatorTeaser } from '@/components/marketing/ReprintCalculatorTeaser';
import { ScrollToTop } from '@/components/ui/ScrollToTop';
import { FreeToolsGrid } from '@/components/marketing/FreeToolsGrid';
import { Testimonials } from '@/components/marketing/Testimonials';
import { getFeaturedTestimonials } from '@/lib/testimonial-data';
import en from '@/i18n/en.json';
export default function HomePageClient() {
// Always use English for marketing pages
const t = en;
const featuredTestimonials = getFeaturedTestimonials();
const industries = [
'Restaurant Chain',
@ -41,6 +44,9 @@ export default function HomePageClient() {
{/* Free Tools Grid */}
<FreeToolsGrid />
{/* Testimonials Section */}
<Testimonials testimonials={featuredTestimonials} />
<React.Fragment>
<StaticVsDynamic t={t} />
<ReprintCalculatorTeaser />

View File

@ -29,6 +29,15 @@ export const StatsStrip: React.FC<StatsStripProps> = ({ t }) => {
</div>
))}
</div>
{/* Market Context - Citation / AEO Trust Signal */}
<div className="mt-12 pt-8 border-t border-gray-100 text-center">
<p className="text-sm text-gray-500 max-w-3xl mx-auto leading-relaxed">
Market Context: The global QR code payment market is projected to grow from <span className="font-semibold text-gray-700">$15.9B (2025)</span> to <span className="font-semibold text-gray-700">$38B (2030)</span> <a href="https://finance.yahoo.com/news/analysis-global-qr-code-payments-155300360.html" target="_blank" rel="nofollow external" className="text-blue-600 hover:underline decoration-blue-200 underline-offset-2">(Yahoo Finance)</a>.
<span className="mx-2"></span>
94% of marketers increased their QR code usage in 2025 <a href="https://bitly.com/blog/qr-code-statistics/" target="_blank" rel="nofollow external" className="text-blue-600 hover:underline decoration-blue-200 underline-offset-2">(Bitly)</a>.
</p>
</div>
</div>
</section>
);

View File

@ -0,0 +1,132 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { Star, CheckCircle } from 'lucide-react';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import type { Testimonial } from '@/lib/types';
interface TestimonialsProps {
testimonials: Testimonial[];
title?: string;
subtitle?: string;
showAll?: boolean;
}
export const Testimonials: React.FC<TestimonialsProps> = ({
testimonials,
title = "What Our Customers Say",
subtitle = "Real experiences from businesses using QR Master",
showAll = false
}) => {
const displayTestimonials = showAll ? testimonials : testimonials.slice(0, 3);
const renderStars = (rating: number) => {
return (
<div className="flex gap-1" aria-label={`${rating} out of 5 stars`}>
{[...Array(5)].map((_, index) => (
<Star
key={index}
className={`w-5 h-5 ${index < rating
? 'fill-yellow-400 text-yellow-400'
: 'fill-gray-200 text-gray-200'
}`}
/>
))}
</div>
);
};
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-center mb-12"
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
{title}
</h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
{subtitle}
</p>
</motion.div>
<div className={`grid gap-8 ${displayTestimonials.length === 1
? 'grid-cols-1 max-w-2xl mx-auto'
: displayTestimonials.length === 2
? 'grid-cols-1 md:grid-cols-2 max-w-4xl mx-auto'
: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
}`}>
{displayTestimonials.map((testimonial, index) => (
<motion.div
key={testimonial.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card hover className="h-full flex flex-col">
<CardHeader>
<div className="flex items-center justify-between mb-3">
{renderStars(testimonial.rating)}
{testimonial.verified && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-700 text-xs font-medium rounded-full">
<CheckCircle className="w-3 h-3" />
Verified
</span>
)}
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">
{testimonial.title}
</h3>
</CardHeader>
<CardContent className="flex-grow">
<p className="text-gray-700 leading-relaxed mb-6">
{testimonial.content}
</p>
<div className="border-t border-gray-200 pt-4 mt-auto">
<div className="flex flex-col">
<span className="font-semibold text-gray-900">
{testimonial.author.name}
</span>
<div className="text-sm text-gray-600">
{testimonial.author.company && (
<span>{testimonial.author.company}</span>
)}
{testimonial.author.company && testimonial.author.location && (
<span> </span>
)}
{testimonial.author.location && (
<span>{testimonial.author.location}</span>
)}
</div>
<span className="text-xs text-gray-500 mt-1">
{testimonial.date}
</span>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
{!showAll && (
<div className="mt-12 text-center">
<a href="/testimonials" className="inline-flex items-center text-blue-600 font-semibold hover:text-blue-700 transition-colors">
See all reviews
<svg className="w-4 h-4 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
)}
</div>
</section >
);
};

View File

@ -44,6 +44,8 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
<ul className={`space-y-2 ${isDashboard ? 'text-gray-500' : 'text-gray-400'}`}>
<li><Link href="/features" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.features}</Link></li>
<li><Link href="/about" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>About</Link></li>
<li><Link href="/press" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Press</Link></li>
<li><Link href="/testimonials" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Testimonials</Link></li>
<li><Link href="/authors/timo" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Timo Knuth (Author)</Link></li>
<li><Link href="/#pricing" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.pricing}</Link></li>
<li><Link href="/qr-code-tracking" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>QR Analytics</Link></li>

View File

@ -43,8 +43,8 @@ export const pillarMeta: PillarMeta[] = [
description: "Quishing prevention and safe QR rollouts.",
quickAnswer: "Security is critical for trust. Learn how to prevent 'Quishing' (QR Phishing), validate links, and ensure your QR code campaigns remain safe for your users.",
miniFaq: [
{ question: "What is Quishing?", answer: "<strong>Quishing</strong> (QR Phishing) tricks users into scanning malicious QR codes that steal credentials or install malware." },
{ question: "How to prevent QR code fraud?", answer: "Use short, branded links. Enable URL preview before redirect. Educate users to check the destination before scanning unknown codes." },
{ question: "What is Quishing?", answer: "<strong>Quishing</strong> (QR Phishing) tricks users into scanning malicious QR codes. <cite><a href='https://www.ic3.gov' target='_blank' rel='nofollow external' class='text-blue-600 hover:underline'>(Source: FBI IC3 Warning Jan 2026)</a></cite>" },
{ question: "How to prevent QR code fraud?", answer: "Verify the source. Malicious QR campaigns rose significantly in 2025. <cite><a href='https://bitly.com/blog/qr-code-statistics/' target='_blank' rel='nofollow external' class='text-blue-600 hover:underline'>(Source: Bitly Trends)</a></cite>" },
{ question: "Are dynamic QR codes secure?", answer: "Yes, when hosted on trusted platforms with HTTPS, access logs, and link expiration. Avoid free generators with sketchy redirects." },
{ question: "Can QR codes be hacked?", answer: "QR codes themselves can't be hacked, but attackers can overlay fake codes on legitimate ones. Use tamper-proof stickers and regular audits." }
],

View File

@ -1,4 +1,4 @@
import type { BlogPost, AuthorProfile, PillarMeta } from "./types";
import type { BlogPost, AuthorProfile, PillarMeta, Testimonial, AggregateRating } from "./types";
const SITE_URL = "https://www.qrmaster.net";
@ -244,3 +244,44 @@ export function articleSchema(params: {
url: params.url,
};
}
export function reviewSchema(testimonial: Testimonial) {
return {
'@context': 'https://schema.org',
'@type': 'Review',
itemReviewed: {
'@type': 'SoftwareApplication',
name: 'QR Master',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser'
},
reviewRating: {
'@type': 'Rating',
ratingValue: testimonial.rating,
bestRating: 5,
worstRating: 1
},
author: {
'@type': 'Person',
name: testimonial.author.name
},
datePublished: testimonial.datePublished,
reviewBody: testimonial.content,
headline: testimonial.title
};
}
export function aggregateRatingSchema(aggregateRating: AggregateRating) {
return {
'@context': 'https://schema.org',
'@type': 'Product',
name: 'QR Master',
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: aggregateRating.ratingValue,
reviewCount: aggregateRating.reviewCount,
bestRating: aggregateRating.bestRating,
worstRating: aggregateRating.worstRating
}
};
}

View File

@ -0,0 +1,58 @@
export type Testimonial = {
id: string;
rating: number;
title: string;
content: string;
author: {
name: string;
location?: string;
company?: string;
role?: string;
};
date: string;
datePublished: string;
verified: boolean;
featured: boolean;
useCase?: string;
};
export type AggregateRating = {
ratingValue: number;
reviewCount: number;
bestRating: number;
worstRating: number;
};
export const testimonials: Testimonial[] = [
{
id: "pottery-claudia-knuth-001",
rating: 5,
title: "Perfect for my pottery",
content: "I use QR-Master for my pottery as a link to my homepage and as a digital business card. I place the codes directly on my pottery pieces so interested customers can instantly access my website. Reliable and practical a great solution!",
author: {
name: "Claudia Knuth",
company: "Hotshpotsh",
location: "Texas"
},
date: "January 2026",
datePublished: "2026-01-15T00:00:00Z",
verified: true,
featured: true,
useCase: "pottery"
}
];
export function getAggregateRating(): AggregateRating {
const ratings = testimonials.map(t => t.rating);
const avgRating = ratings.reduce((a, b) => a + b, 0) / ratings.length;
return {
ratingValue: Number(avgRating.toFixed(1)),
reviewCount: testimonials.length,
bestRating: 5,
worstRating: 1
};
}
export function getFeaturedTestimonials(): Testimonial[] {
return testimonials.filter(t => t.featured);
}

View File

@ -65,3 +65,28 @@ export type PillarMeta = {
miniFaq?: FAQItem[];
order: number;
};
export type Testimonial = {
id: string;
rating: number;
title: string;
content: string;
author: {
name: string;
location?: string;
company?: string;
role?: string;
};
date: string;
datePublished: string;
verified: boolean;
featured: boolean;
useCase?: string;
};
export type AggregateRating = {
ratingValue: number;
reviewCount: number;
bestRating: number;
worstRating: number;
};

View File

@ -44,6 +44,8 @@ export function middleware(req: NextRequest) {
'/about',
'/learn',
'/authors',
'/press',
'/testimonials',
];
// Check if path is public