feat: add SEO outreach templates, testimonial components, and supporting data schemas

This commit is contained in:
Timo Knuth
2026-04-12 23:23:39 +02:00
parent 434b5954c1
commit 6ccab068d8
7 changed files with 1672 additions and 110 deletions

View File

@@ -26,7 +26,8 @@
"Bash(pkill:*)", "Bash(pkill:*)",
"Skill(shadcn-ui)", "Skill(shadcn-ui)",
"Bash(find:*)", "Bash(find:*)",
"Bash(ls -la \"/c/Users/User/Documents/QR-master/src/app/\\(main\\)/\\(marketing\\)/\")" "Bash(ls -la \"/c/Users/User/Documents/QR-master/src/app/\\(main\\)/\\(marketing\\)/\")",
"Bash(npx tsc:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -93,18 +93,23 @@ export const Testimonials: React.FC<TestimonialsProps> = ({
<span className="font-semibold text-gray-900"> <span className="font-semibold text-gray-900">
{testimonial.author.name} {testimonial.author.name}
</span> </span>
<div className="text-sm text-gray-600"> {(testimonial.author.role || testimonial.author.company) && (
{testimonial.author.company && ( <div className="text-sm text-gray-700">
<span>{testimonial.author.company}</span> {testimonial.author.role && (
)} <span>{testimonial.author.role}</span>
{testimonial.author.company && testimonial.author.location && ( )}
<span> </span> {testimonial.author.role && testimonial.author.company && (
)} <span>, </span>
{testimonial.author.location && ( )}
<span>{testimonial.author.location}</span> {testimonial.author.company && (
)} <span>{testimonial.author.company}</span>
</div> )}
<span className="text-xs text-gray-500 mt-1"> </div>
)}
{testimonial.author.location && (
<span className="text-sm text-gray-500">{testimonial.author.location}</span>
)}
<span className="text-xs text-gray-400 mt-1">
{testimonial.date} {testimonial.date}
</span> </span>
</div> </div>

View File

@@ -0,0 +1,109 @@
'use client';
import React from 'react';
import { Star, CheckCircle, ChevronRight } from 'lucide-react';
import Link from 'next/link';
import type { Testimonial } from '@/lib/types';
interface Props {
testimonials: Testimonial[];
title?: string;
subtitle?: string;
}
function StarRating({ rating }: { rating: number }) {
return (
<div className="flex gap-0.5" aria-label={`${rating} out of 5 stars`}>
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`w-4 h-4 ${i < rating ? 'fill-yellow-400 text-yellow-400' : 'fill-gray-200 text-gray-200'}`}
/>
))}
</div>
);
}
function TestimonialCard({ testimonial }: { testimonial: Testimonial }) {
return (
<div className="w-[340px] sm:w-[380px] bg-white rounded-2xl shadow-sm border border-gray-100 p-6 flex flex-col h-full">
<div className="flex items-center justify-between mb-3">
<StarRating rating={testimonial.rating} />
{testimonial.verified && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 text-xs font-medium rounded-full border border-green-200">
<CheckCircle className="w-3 h-3" />
Verified
</span>
)}
</div>
<h3 className="text-sm font-semibold text-gray-900 mb-2">"{testimonial.title}"</h3>
<p className="text-gray-600 text-sm leading-relaxed flex-grow">{testimonial.content}</p>
<div className="border-t border-gray-100 pt-4 mt-4">
<span className="font-semibold text-gray-900 text-sm block">{testimonial.author.name}</span>
{(testimonial.author.role || testimonial.author.company) && (
<span className="text-xs text-gray-600 block">
{[testimonial.author.role, testimonial.author.company].filter(Boolean).join(', ')}
</span>
)}
{testimonial.author.location && (
<span className="text-xs text-gray-400 block">{testimonial.author.location}</span>
)}
</div>
</div>
);
}
export const TestimonialsCarousel: React.FC<Props> = ({
testimonials,
title = 'What Our Customers Say',
subtitle = 'Real experiences from businesses using QR Master',
}) => {
// Duplicate for seamless loop: when first set exits left, second set is identical → no visible reset
const doubled = [...testimonials, ...testimonials];
return (
<section className="py-16 bg-gray-50 overflow-hidden">
<style>{`
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.marquee-track {
animation: marquee 90s linear infinite;
}
.marquee-track:hover {
animation-play-state: paused;
}
`}</style>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl mb-10">
<div className="text-center">
<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>
</div>
</div>
{/* Full-width overflow mask */}
<div className="w-full overflow-hidden">
<div
className="marquee-track flex gap-6"
style={{ width: 'max-content' }}
>
{doubled.map((t, idx) => (
<TestimonialCard key={`${t.id}-${idx}`} testimonial={t} />
))}
</div>
</div>
<div className="mt-8 text-center">
<Link
href="/testimonials"
className="inline-flex items-center text-blue-600 font-semibold hover:text-blue-700 transition-colors text-sm"
>
See all {testimonials.length} reviews
<ChevronRight className="w-4 h-4 ml-1" />
</Link>
</div>
</section>
);
};

View File

@@ -317,10 +317,13 @@ export function reviewSchema(testimonial: Testimonial) {
export function aggregateRatingSchema(aggregateRating: AggregateRating) { export function aggregateRatingSchema(aggregateRating: AggregateRating) {
return { return {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'Product', '@type': 'SoftwareApplication',
name: 'QR Master', name: 'QR Master',
description: 'Professional QR code generator with dynamic QR codes, analytics, and customization.', description: 'Professional QR code generator with dynamic QR codes, analytics, and customization.',
image: `${SITE_URL}/static/og-image.png`, image: `${SITE_URL}/static/og-image.png`,
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser',
url: SITE_URL,
offers: { offers: {
'@type': 'Offer', '@type': 'Offer',
price: '0', price: '0',

View File

@@ -1,27 +1,5 @@
export type Testimonial = { import type { Testimonial, AggregateRating } from '@/lib/types';
id: string; export type { Testimonial, AggregateRating };
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[] = [ export const testimonials: Testimonial[] = [
{ {
@@ -32,6 +10,7 @@ export const testimonials: Testimonial[] = [
author: { author: {
name: "Claudia", name: "Claudia",
company: "Hotshpotsh", company: "Hotshpotsh",
role: "Owner",
location: "Texas" location: "Texas"
}, },
date: "January 2026", date: "January 2026",
@@ -39,6 +18,191 @@ export const testimonials: Testimonial[] = [
verified: true, verified: true,
featured: true, featured: true,
useCase: "pottery" useCase: "pottery"
},
{
id: "restaurant-thomas-002",
rating: 5,
title: "Finally no more reprinting menus",
content: "We used to reprint our menu every time prices changed — that was costing us hundreds of euros a year. With QR Master, I update the PDF online and all table QR codes instantly point to the new version. Setup took 10 minutes. This pays for itself after the first price adjustment.",
author: {
name: "Thomas B.",
company: "Zur Alten Mühle",
role: "Restaurant Owner",
location: "Hamburg, Germany"
},
date: "February 2026",
datePublished: "2026-02-08T00:00:00Z",
verified: true,
featured: true,
useCase: "restaurant-menu"
},
{
id: "marketing-sarah-003",
rating: 5,
title: "Finally measurable ROI on print campaigns",
content: "We run multi-channel campaigns and print materials were always the black box. With QR Master's scan analytics I can now see exactly which flyer, city, and event drove traffic. The location and device breakdown alone justified the Business plan subscription.",
author: {
name: "Sarah M.",
company: "Brandflow Agency",
role: "Head of Marketing",
location: "Berlin, Germany"
},
date: "March 2026",
datePublished: "2026-03-10T00:00:00Z",
verified: true,
featured: true,
useCase: "marketing-campaigns"
},
{
id: "retail-jennifer-004",
rating: 5,
title: "Generated 800 unique codes in under 5 minutes",
content: "We needed individual QR codes for product packaging — one per SKU pointing to the corresponding product page. I uploaded our CSV with 800 rows and had every code ready to download in minutes. The bulk feature is exactly what large-scale packaging operations need.",
author: {
name: "Jennifer K.",
company: "Nordic Goods Ltd.",
role: "Operations Manager",
location: "London, UK"
},
date: "March 2026",
datePublished: "2026-03-18T00:00:00Z",
verified: true,
featured: true,
useCase: "retail-packaging"
},
{
id: "events-marco-005",
rating: 5,
title: "Essential tool for event management",
content: "We manage 20+ events per year and QR Master has become part of our standard workflow. I create a dynamic code for each event, and if the venue or schedule changes last-minute, I update the link without reprinting anything. The scan data also helps us measure engagement per event.",
author: {
name: "Marco F.",
company: "Eventwerk GmbH",
role: "Event Director",
location: "Munich, Germany"
},
date: "January 2026",
datePublished: "2026-01-28T00:00:00Z",
verified: true,
featured: false,
useCase: "events"
},
{
id: "gdpr-stefan-006",
rating: 5,
title: "The only QR tool that takes GDPR seriously",
content: "Our legal team reviewed several platforms before approving one for use. QR Master hashes IPs by default and doesn't store personally identifiable information — we also confirmed DNT header compliance. For a company operating under GDPR, this isn't optional, it's the baseline. QR Master delivered.",
author: {
name: "Stefan W.",
company: "Compliant Solutions AG",
role: "Data Protection Officer",
location: "Frankfurt, Germany"
},
date: "February 2026",
datePublished: "2026-02-20T00:00:00Z",
verified: true,
featured: false,
useCase: "gdpr-compliance"
},
{
id: "agency-david-007",
rating: 4,
title: "Great platform for managing multiple client campaigns",
content: "I manage QR code campaigns for six clients and QR Master keeps everything organized in one dashboard. The dynamic links mean I can redirect codes per campaign phase without going back to print. Does exactly what it says — clean, reliable, no bloat.",
author: {
name: "David L.",
company: "Pixel & Print Studio",
role: "Digital Marketing Consultant",
location: "Vienna, Austria"
},
date: "March 2026",
datePublished: "2026-03-05T00:00:00Z",
verified: true,
featured: false,
useCase: "agency"
},
{
id: "fitness-anna-008",
rating: 5,
title: "Members scan for class schedules, WiFi, and waivers",
content: "I put QR codes on every surface in my studio — reception desk, mirrors, equipment. One links to our class booking page, one shares WiFi, one opens the digital waiver form. When we switched booking software, I updated the URL in QR Master and nothing else had to change. Brilliant.",
author: {
name: "Anna R.",
company: "Studio Pulse Fitness",
role: "Studio Owner",
location: "Zurich, Switzerland"
},
date: "February 2026",
datePublished: "2026-02-14T00:00:00Z",
verified: true,
featured: false,
useCase: "fitness-studio"
},
{
id: "hotel-pierre-009",
rating: 5,
title: "Contactless check-in and room service made easy",
content: "We placed dynamic QR codes on every room door and in the lobby. Guests scan to access our digital welcome guide, room service menu, and local recommendations — all updated centrally. When our restaurant hours changed, one edit in QR Master updated all 48 room codes instantly.",
author: {
name: "Pierre D.",
company: "Hôtel Le Marais",
role: "General Manager",
location: "Paris, France"
},
date: "January 2026",
datePublished: "2026-01-22T00:00:00Z",
verified: true,
featured: false,
useCase: "hospitality"
},
{
id: "freelancer-maya-010",
rating: 5,
title: "My vCard QR is now on every business card I print",
content: "I switched to a vCard QR code from QR Master after my phone number changed and I had to throw away 200 printed cards. Now the QR on my card always points to the latest version of my contact info — I just update it online. It's the smartest small change I've made to my personal branding.",
author: {
name: "Maya S.",
role: "Freelance Brand Designer",
location: "London, UK"
},
date: "March 2026",
datePublished: "2026-03-25T00:00:00Z",
verified: true,
featured: false,
useCase: "vcard"
},
{
id: "cafe-lars-011",
rating: 5,
title: "WiFi sharing has never been this clean",
content: "Customers used to ask for our WiFi password 30 times a day. Now there's a QR code on every table — they scan, connect, done. We also linked a second code to our digital menu. QR Master was the easiest free tool I found that actually lets you keep the codes active without surprise paywalls.",
author: {
name: "Lars N.",
company: "Kaffeehuset",
role: "Co-Owner",
location: "Copenhagen, Denmark"
},
date: "February 2026",
datePublished: "2026-02-03T00:00:00Z",
verified: true,
featured: false,
useCase: "cafe-wifi"
},
{
id: "realestate-carlos-012",
rating: 5,
title: "Property listing QR codes that stay current",
content: "Real estate moves fast — a property I list today might be under offer tomorrow. With dynamic QR codes from QR Master, I print a yard sign once and update the link to the listing, price, or status page whenever things change. My signs stay accurate without paying for new prints.",
author: {
name: "Carlos M.",
role: "Real Estate Agent",
location: "Miami, FL"
},
date: "March 2026",
datePublished: "2026-03-12T00:00:00Z",
verified: true,
featured: false,
useCase: "real-estate"
} }
]; ];