SEO: compare/guide pSEO pages + internal links + intent differentiation

This commit is contained in:
Timo Knuth
2026-06-22 13:22:11 +02:00
parent 0866c200a0
commit 22a987029b
22 changed files with 9425 additions and 8218 deletions

View File

@@ -6,12 +6,10 @@ import { usePathname } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Footer } from '@/components/ui/Footer';
import en from '@/i18n/en.json';
import { ChevronDown, BookOpen, Building2, Wifi, Contact, MessageCircle, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users, Barcode as BarcodeIcon, Star } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'framer-motion';
import { allUseCases } from '@/lib/growth-pages';
import { industryPages } from '@/lib/industry-pages';
import Image from 'next/image';
import { ChevronDown, BookOpen, Building2, Wifi, Contact, MessageCircle, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users, Barcode as BarcodeIcon, Star } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'framer-motion';
import Image from 'next/image';
export default function MarketingLayout({
children,
@@ -80,83 +78,26 @@ export default function MarketingLayout({
return (
<div className="min-h-screen bg-white">
{/* Server-rendered navigation links for SEO (crawlers) - Placed first for priority */}
<div className="sr-only" aria-hidden="false">
<nav aria-label="Site Map">
<ul>
<li><a href="/">Home</a></li>
<li><Link href="/pricing">{t.nav.pricing}</Link></li>
<li><Link href="/blog">{t.nav.blog}</Link></li>
<li><Link href="/learn">{t.nav.learn}</Link></li>
<li><Link href="/use-cases">Use Cases</Link></li>
<li><Link href="/restaurants">Restaurant Menu QR Codes</Link></li>
<li><Link href="/faq">{t.nav.faq}</Link></li>
<li><Link href="/about">{t.nav.about}</Link></li>
<li><Link href="/contact">{t.nav.contact}</Link></li>
<li><Link href="/login">{t.nav.login}</Link></li>
<li><Link href="/signup">{t.nav.signup || "Sign Up"}</Link></li>
{/* Commercial pages */}
<li><a href="/dynamic-qr-code-generator">Dynamic QR Code Generator</a></li>
<li><a href="/qr-code-tracking">QR Code Tracking</a></li>
<li><a href="/qr-code-analytics">QR Code Analytics</a></li>
<li><a href="/custom-qr-code-generator">Custom QR Code Generator</a></li>
<li><a href="/bulk-qr-code-generator">Bulk QR Code Generator</a></li>
<li><a href="/qr-code-for-marketing-campaigns">QR Codes for Marketing Campaigns</a></li>
<li><a href="/qr-code-erstellen">QR Code Erstellen</a></li>
{/* Use case pages */}
{allUseCases.map((uc) => (
<li key={uc.slug}><a href={uc.href}>{uc.title}</a></li>
))}
{/* Industry pages */}
{industryPages.map((ind) => (
<li key={ind.slug}><a href={`/qr-code-for/${ind.slug}`}>{ind.title}</a></li>
))}
{/* Tools */}
<li><a href="/tools/url-qr-code">URL QR Code</a></li>
<li><a href="/tools/text-qr-code">Text QR Code</a></li>
<li><a href="/tools/wifi-qr-code">WiFi QR Code</a></li>
<li><a href="/tools/vcard-qr-code">vCard QR Code</a></li>
<li><a href="/tools/whatsapp-qr-code">WhatsApp QR Code</a></li>
<li><a href="/tools/email-qr-code">Email QR Code</a></li>
<li><a href="/tools/sms-qr-code">SMS QR Code</a></li>
<li><a href="/tools/call-qr-code-generator">Call QR Code</a></li>
<li><a href="/tools/event-qr-code">Event QR Code</a></li>
<li><a href="/tools/geolocation-qr-code">Location QR Code</a></li>
<li><a href="/tools/facebook-qr-code">Facebook QR Code</a></li>
<li><a href="/tools/instagram-qr-code">Instagram QR Code</a></li>
<li><a href="/tools/twitter-qr-code">Twitter QR Code</a></li>
<li><a href="/tools/youtube-qr-code">YouTube QR Code</a></li>
<li><a href="/tools/tiktok-qr-code">TikTok QR Code</a></li>
<li><a href="/tools/crypto-qr-code">Crypto QR Code</a></li>
<li><a href="/tools/paypal-qr-code">PayPal QR Code</a></li>
<li><a href="/tools/zoom-qr-code">Zoom QR Code</a></li>
<li><a href="/tools/teams-qr-code">Teams QR Code</a></li>
<li><a href="/tools/barcode-generator">Barcode Generator</a></li>
<li><a href="/tools/google-review-qr-code">Google Review QR Code</a></li>
</ul>
</nav>
</div>
{/* Header */}
<header
className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm"
>
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl h-20 flex items-center justify-between">
{/* Logo */}
<Link href="/" className="flex items-center space-x-3 group">
<div className="relative w-16 h-16 overflow-hidden rounded-full shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
<Image
src="/logo.svg"
alt="QR Master"
fill
sizes="64px"
className="object-cover"
priority
/>
</div>
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
</Link>
{/* Logo */}
<Link href="/" className="flex items-center space-x-3 group">
<div className="relative w-16 h-16 overflow-hidden rounded-full shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
<Image
src="/favicon1.png"
alt="QR Master"
fill
sizes="64px"
className="object-cover"
priority
/>
</div>
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
</Link>
{/* Desktop Navigation */}

View File

@@ -0,0 +1,416 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ArrowRight, Check, Lock, RefreshCw } from "lucide-react";
import Breadcrumbs, { type BreadcrumbItem } from "@/components/Breadcrumbs";
import SeoJsonLd from "@/components/SeoJsonLd";
import { getComparisonPage } from "@/lib/comparison-pages";
import type { ComparisonPage } from "@/lib/pseo-page-types";
import {
isPublishedComparisonSlug,
publishedComparisonPages,
} from "@/lib/pseo-published-pages";
import { articleSchema, breadcrumbSchema, faqPageSchema } from "@/lib/schema";
const SITE_URL = "https://www.qrmaster.net";
const LAST_MODIFIED = "2026-06-22";
type PageProps = {
params: {
slug: string;
};
};
export const dynamicParams = false;
export function generateStaticParams() {
return publishedComparisonPages.map((page) => ({ slug: page.slug }));
}
function getPublishedPage(slug: string): ComparisonPage {
if (!isPublishedComparisonSlug(slug)) {
notFound();
}
const page = getComparisonPage(slug);
if (!page) {
notFound();
}
return page;
}
function titleFor(page: ComparisonPage) {
if (page.slug === "dynamic-vs-static-qr-codes") {
return "Dynamic or Static QR Codes: Which to Choose for Your Campaign";
}
if (page.slug === "free-vs-paid-qr-code-generator") {
return "Free vs Paid QR Code Generator";
}
return page.title;
}
function descriptionFor(page: ComparisonPage) {
if (page.slug === "dynamic-vs-static-qr-codes") {
return "Decide between dynamic and static QR codes for your next campaign: a buyer-focused comparison of reprint risk, tracking, and cost before you commit to print.";
}
if (page.slug === "free-vs-paid-qr-code-generator") {
return "See when a free QR generator is enough and when paid dynamic QR features, tracking, branding, and reprint savings are worth it.";
}
return page.description;
}
function intentLabel(intentStage: ComparisonPage["intentStage"]) {
if (intentStage === "bottom") return "Commercial investigation";
if (intentStage === "middle") return "Comparison research";
return "Educational comparison";
}
export function generateMetadata({ params }: PageProps): Metadata {
const page = getPublishedPage(params.slug);
const title = titleFor(page);
const description = descriptionFor(page);
const canonical = `${SITE_URL}${page.canonicalPath}`;
return {
title: {
absolute: title,
},
description,
alternates: {
canonical,
languages: {
"x-default": canonical,
en: canonical,
},
},
robots: {
index: true,
follow: true,
},
icons: {
icon: [{ url: "/favicon1.png", type: "image/png" }],
shortcut: "/favicon1.png",
apple: "/favicon1.png",
},
openGraph: {
title,
description,
url: canonical,
type: "article",
images: ["/og-image.png"],
},
twitter: {
title,
description,
},
};
}
function comparisonItemListSchema(page: ComparisonPage) {
return {
"@context": "https://schema.org",
"@type": "ItemList",
"@id": `${SITE_URL}${page.canonicalPath}#comparison`,
name: `${page.primaryEntity} compared with ${page.secondaryEntity}`,
numberOfItems: page.scorecard.length,
itemListElement: page.scorecard.map((row, index) => ({
"@type": "ListItem",
position: index + 1,
name: row.topic,
description: `${page.primaryEntity}: ${row.qrMaster}. ${page.secondaryEntity}: ${row.alternative}.`,
})),
};
}
function breadcrumbsFor(page: ComparisonPage): BreadcrumbItem[] {
return [
{ name: "Home", url: "/" },
{ name: page.title, url: page.canonicalPath },
];
}
function DynamicFlowVisual() {
return (
<div className="relative overflow-hidden rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(98%_0.006_270)] p-5 shadow-[0_28px_48px_-36px_rgba(50,50,93,0.32)]">
<div className="grid gap-4 sm:grid-cols-[1fr_auto_1fr] sm:items-center">
<div className="rounded-md border border-[oklch(88%_0.018_270)] bg-[oklch(99%_0.004_270)] p-4">
<div className="mb-4 grid size-24 grid-cols-5 gap-1" aria-hidden="true">
{Array.from({ length: 25 }).map((_, index) => (
<span
key={index}
className={
[0, 1, 3, 5, 6, 8, 10, 12, 14, 16, 18, 20, 21, 22, 24].includes(index)
? "bg-[oklch(20%_0.035_270)]"
: "bg-[oklch(94%_0.01_270)]"
}
/>
))}
</div>
<div className="text-sm font-semibold text-[oklch(24%_0.04_270)]">
Printed QR
</div>
<p className="mt-1 text-sm leading-6 text-[oklch(46%_0.035_270)]">
The physical code stays in place.
</p>
</div>
<div className="flex justify-center">
<div className="flex h-12 w-12 items-center justify-center rounded-md border border-[oklch(84%_0.06_275)] bg-[oklch(96%_0.035_275)] text-[oklch(48%_0.19_275)]">
<RefreshCw className="h-5 w-5" aria-hidden="true" />
</div>
</div>
<div className="rounded-md border border-[oklch(88%_0.018_270)] bg-[oklch(99%_0.004_270)] p-4">
<div className="mb-4 h-24 rounded-md border border-[oklch(89%_0.016_270)] bg-[oklch(97%_0.009_270)] p-3">
<div className="mb-3 h-2 w-24 rounded-sm bg-[oklch(57%_0.18_275)]" />
<div className="space-y-2">
<div className="h-2 w-full rounded-sm bg-[oklch(86%_0.017_270)]" />
<div className="h-2 w-3/4 rounded-sm bg-[oklch(88%_0.015_270)]" />
<div className="h-2 w-1/2 rounded-sm bg-[oklch(90%_0.012_270)]" />
</div>
</div>
<div className="text-sm font-semibold text-[oklch(24%_0.04_270)]">
Current destination
</div>
<p className="mt-1 text-sm leading-6 text-[oklch(46%_0.035_270)]">
The URL can change after print.
</p>
</div>
</div>
</div>
);
}
export default function ComparisonPageRoute({ params }: PageProps) {
const page = getPublishedPage(params.slug);
const title = titleFor(page);
const description = descriptionFor(page);
const breadcrumbs = breadcrumbsFor(page);
const canonical = `${SITE_URL}${page.canonicalPath}`;
const structuredData = [
articleSchema({
headline: title,
description,
datePublished: LAST_MODIFIED,
dateModified: LAST_MODIFIED,
author: "QR Master",
url: canonical,
image: `${SITE_URL}/og-image.png`,
}),
faqPageSchema(page.faq),
breadcrumbSchema(breadcrumbs),
comparisonItemListSchema(page),
];
return (
<>
<SeoJsonLd data={structuredData} />
<main className="min-h-screen bg-[oklch(98.5%_0.006_270)] text-[oklch(23%_0.04_270)]">
<section className="border-b border-[oklch(91%_0.014_270)] bg-[oklch(99%_0.004_270)]">
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<Breadcrumbs items={breadcrumbs} />
<div className="grid gap-12 lg:grid-cols-[minmax(0,0.95fr)_minmax(420px,1fr)] lg:items-center">
<div>
<p className="mb-5 text-sm font-semibold uppercase tracking-[0.12em] text-[oklch(47%_0.18_275)]">
{intentLabel(page.intentStage)}
</p>
<h1 className="max-w-3xl text-balance text-4xl font-light leading-[1.05] tracking-normal text-[oklch(22%_0.045_270)] sm:text-5xl lg:text-6xl">
{title}
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-[oklch(46%_0.035_270)]">
{page.quickAnswer}
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Link
href={page.primaryCta.href}
className="inline-flex min-h-11 items-center justify-center rounded-md bg-[oklch(50%_0.22_275)] px-5 text-sm font-semibold text-[oklch(99%_0.004_270)] transition hover:bg-[oklch(43%_0.2_275)] focus:outline-none focus:ring-2 focus:ring-[oklch(50%_0.22_275)] focus:ring-offset-2"
>
{page.primaryCta.label}
</Link>
<Link
href={page.secondaryCta.href}
className="inline-flex min-h-11 items-center justify-center rounded-md border border-[oklch(79%_0.08_275)] px-5 text-sm font-semibold text-[oklch(45%_0.18_275)] transition hover:bg-[oklch(96%_0.025_275)] focus:outline-none focus:ring-2 focus:ring-[oklch(50%_0.22_275)] focus:ring-offset-2"
>
{page.secondaryCta.label}
</Link>
</div>
</div>
<DynamicFlowVisual />
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:px-8">
<div className="grid gap-8 lg:grid-cols-[0.85fr_1.15fr]">
<div>
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
The short answer
</h2>
<p className="mt-4 max-w-[65ch] text-base leading-8 text-[oklch(45%_0.035_270)]">
{page.intro} {page.quickAnswer}
</p>
</div>
<div className="rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)] p-5 shadow-[0_24px_42px_-34px_rgba(50,50,93,0.28)]">
<div className="overflow-x-auto">
<table className="w-full min-w-[680px] text-left text-sm">
<caption className="sr-only">
Comparison of {page.primaryEntity} and {page.secondaryEntity}
</caption>
<thead>
<tr className="border-b border-[oklch(89%_0.016_270)]">
<th className="py-3 pr-4 font-semibold text-[oklch(28%_0.04_270)]">
Decision point
</th>
<th className="px-4 py-3 font-semibold text-[oklch(28%_0.04_270)]">
{page.secondaryEntity}
</th>
<th className="px-4 py-3 font-semibold text-[oklch(45%_0.18_275)]">
{page.primaryEntity}
</th>
</tr>
</thead>
<tbody className="divide-y divide-[oklch(92%_0.012_270)]">
{page.scorecard.map((row) => (
<tr key={row.topic}>
<th className="py-4 pr-4 align-top font-medium text-[oklch(28%_0.04_270)]">
{row.topic}
</th>
<td className="px-4 py-4 align-top leading-6 text-[oklch(47%_0.03_270)]">
{row.alternative}
</td>
<td className="px-4 py-4 align-top leading-6 text-[oklch(35%_0.05_270)]">
{row.qrMaster}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</section>
<section className="border-y border-[oklch(91%_0.014_270)] bg-[oklch(19%_0.055_270)] py-16 text-[oklch(96%_0.01_270)]">
<div className="mx-auto grid max-w-7xl gap-8 px-4 sm:px-6 lg:grid-cols-3 lg:px-8">
<div>
<h2 className="text-3xl font-light leading-tight tracking-normal">
Which option should you choose?
</h2>
<p className="mt-4 leading-7 text-[oklch(78%_0.025_270)]">
Use this page as a buying decision, not as a feature glossary.
The right answer depends on whether the printed asset must
survive a future destination change.
</p>
</div>
<div className="rounded-lg border border-[oklch(35%_0.06_270)] bg-[oklch(23%_0.058_270)] p-6">
<Lock className="mb-5 h-5 w-5 text-[oklch(78%_0.05_270)]" aria-hidden="true" />
<h3 className="text-xl font-light">Choose the simpler option when...</h3>
<ul className="mt-5 space-y-3 text-sm leading-6 text-[oklch(80%_0.022_270)]">
{page.bestForAlternative.map((item) => (
<li key={item}>- {item}</li>
))}
</ul>
</div>
<div className="rounded-lg border border-[oklch(54%_0.16_275)] bg-[oklch(26%_0.08_275)] p-6">
<Check className="mb-5 h-5 w-5 text-[oklch(82%_0.13_150)]" aria-hidden="true" />
<h3 className="text-xl font-light">Choose QR Master when...</h3>
<ul className="mt-5 space-y-3 text-sm leading-6 text-[oklch(88%_0.018_270)]">
{page.bestForQrMaster.map((item) => (
<li key={item}>- {item}</li>
))}
</ul>
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
<div className="grid gap-10 lg:grid-cols-[0.75fr_1.25fr]">
<div>
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
Migration steps
</h2>
<p className="mt-4 leading-8 text-[oklch(45%_0.035_270)]">
If this comparison changes your decision, move the highest-risk
printed assets first. That keeps the work focused on real
reprint and tracking value.
</p>
</div>
<ol className="grid gap-4">
{page.migrationSteps.map((step, index) => (
<li
key={step}
className="grid gap-4 rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)] p-5 shadow-[0_16px_30px_-28px_rgba(50,50,93,0.28)] sm:grid-cols-[3rem_1fr]"
>
<span className="flex h-10 w-10 items-center justify-center rounded-md bg-[oklch(94%_0.04_275)] text-sm font-semibold tabular-nums text-[oklch(45%_0.18_275)]">
{index + 1}
</span>
<p className="leading-7 text-[oklch(39%_0.035_270)]">{step}</p>
</li>
))}
</ol>
</div>
</section>
<section className="bg-[oklch(99%_0.004_270)] py-16">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-10 max-w-3xl">
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
Related QR Master workflows
</h2>
<p className="mt-4 leading-8 text-[oklch(45%_0.035_270)]">
These links connect the comparison to creation, analytics,
pricing, and operational guidance.
</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
{page.relatedLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="group rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(98%_0.006_270)] p-5 transition hover:border-[oklch(70%_0.1_275)] hover:bg-[oklch(97%_0.018_275)]"
>
<h3 className="font-semibold text-[oklch(26%_0.045_270)]">
{link.title}
</h3>
<p className="mt-3 text-sm leading-6 text-[oklch(46%_0.035_270)]">
{link.description}
</p>
<span className="mt-5 inline-flex items-center gap-2 text-sm font-semibold text-[oklch(45%_0.18_275)]">
Open page
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</span>
</Link>
))}
</div>
</div>
</section>
<section className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
Common questions
</h2>
<div className="mt-8 divide-y divide-[oklch(90%_0.018_270)] rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)]">
{page.faq.map((item) => (
<details key={item.question} className="group p-5">
<summary className="flex cursor-pointer list-none items-center justify-between gap-5 font-semibold text-[oklch(26%_0.045_270)]">
{item.question}
<span className="text-[oklch(45%_0.18_275)] group-open:rotate-90">+</span>
</summary>
<p className="mt-4 max-w-[65ch] leading-7 text-[oklch(45%_0.035_270)]">
{item.answer}
</p>
</details>
))}
</div>
</section>
</main>
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,381 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ArrowRight, CheckCircle2, ClipboardCheck, Gauge, RefreshCw } from "lucide-react";
import Breadcrumbs, { type BreadcrumbItem } from "@/components/Breadcrumbs";
import SeoJsonLd from "@/components/SeoJsonLd";
import { getGuidePage } from "@/lib/guide-pages";
import type { GuidePage } from "@/lib/pseo-page-types";
import {
isPublishedGuideSlug,
publishedGuidePages,
} from "@/lib/pseo-published-pages";
import { articleSchema, breadcrumbSchema, faqPageSchema } from "@/lib/schema";
const SITE_URL = "https://www.qrmaster.net";
const LAST_MODIFIED = "2026-06-22";
type PageProps = {
params: {
slug: string;
};
};
export const dynamicParams = false;
export function generateStaticParams() {
return publishedGuidePages.map((page) => ({ slug: page.slug }));
}
function getPublishedPage(slug: string): GuidePage {
if (!isPublishedGuideSlug(slug)) {
notFound();
}
const page = getGuidePage(slug);
if (!page) {
notFound();
}
return page;
}
function titleFor(page: GuidePage) {
if (page.slug === "dynamic-qr-code-best-practices") {
return "Dynamic QR Code Best Practices";
}
return page.title;
}
function descriptionFor(page: GuidePage) {
if (page.slug === "dynamic-qr-code-best-practices") {
return "Plan dynamic QR codes with better naming, tracking, print testing, privacy checks, and update workflows before campaigns go live.";
}
return page.description;
}
export function generateMetadata({ params }: PageProps): Metadata {
const page = getPublishedPage(params.slug);
const title = titleFor(page);
const description = descriptionFor(page);
const canonical = `${SITE_URL}${page.canonicalPath}`;
return {
title: {
absolute: title,
},
description,
alternates: {
canonical,
languages: {
"x-default": canonical,
en: canonical,
},
},
robots: {
index: true,
follow: true,
},
icons: {
icon: [{ url: "/favicon1.png", type: "image/png" }],
shortcut: "/favicon1.png",
apple: "/favicon1.png",
},
openGraph: {
title,
description,
url: canonical,
type: "article",
images: ["/og-image.png"],
},
twitter: {
title,
description,
},
};
}
function breadcrumbsFor(page: GuidePage): BreadcrumbItem[] {
return [
{ name: "Home", url: "/" },
{ name: page.title, url: page.canonicalPath },
];
}
function howToStructuredData(page: GuidePage) {
return {
"@context": "https://schema.org",
"@type": "HowTo",
"@id": `${SITE_URL}${page.canonicalPath}#howto`,
name: titleFor(page),
description: descriptionFor(page),
datePublished: LAST_MODIFIED,
dateModified: LAST_MODIFIED,
totalTime: "PT15M",
author: {
"@type": "Organization",
name: "QR Master",
url: SITE_URL,
},
step: page.howToSteps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: `Step ${index + 1}`,
text: step,
})),
};
}
function PracticeRail() {
const items = [
{
label: "Before print",
detail: "Name the QR by campaign, surface, and destination risk.",
icon: ClipboardCheck,
},
{
label: "During launch",
detail: "Test the printed asset on real devices and final material.",
icon: Gauge,
},
{
label: "After launch",
detail: "Review scan context before changing or reprinting assets.",
icon: RefreshCw,
},
];
return (
<div className="rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)] p-5 shadow-[0_28px_48px_-36px_rgba(50,50,93,0.32)]">
<div className="mb-5 text-sm font-semibold uppercase tracking-[0.12em] text-[oklch(47%_0.18_275)]">
Operating rhythm
</div>
<div className="space-y-4">
{items.map((item) => {
const Icon = item.icon;
return (
<div key={item.label} className="grid grid-cols-[2.75rem_1fr] gap-4">
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-[oklch(94%_0.04_275)] text-[oklch(45%_0.18_275)]">
<Icon className="h-5 w-5" aria-hidden="true" />
</div>
<div>
<h2 className="text-base font-semibold text-[oklch(25%_0.045_270)]">
{item.label}
</h2>
<p className="mt-1 text-sm leading-6 text-[oklch(46%_0.035_270)]">
{item.detail}
</p>
</div>
</div>
);
})}
</div>
</div>
);
}
export default function GuidePageRoute({ params }: PageProps) {
const page = getPublishedPage(params.slug);
const title = titleFor(page);
const description = descriptionFor(page);
const canonical = `${SITE_URL}${page.canonicalPath}`;
const breadcrumbs = breadcrumbsFor(page);
const structuredData = [
articleSchema({
headline: title,
description,
datePublished: LAST_MODIFIED,
dateModified: LAST_MODIFIED,
author: "QR Master",
url: canonical,
image: `${SITE_URL}/og-image.png`,
}),
howToStructuredData(page),
faqPageSchema(page.faq),
breadcrumbSchema(breadcrumbs),
];
return (
<>
<SeoJsonLd data={structuredData} />
<main className="min-h-screen bg-[oklch(98.5%_0.006_270)] text-[oklch(23%_0.04_270)]">
<section className="border-b border-[oklch(91%_0.014_270)] bg-[oklch(99%_0.004_270)]">
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<Breadcrumbs items={breadcrumbs} />
<div className="grid gap-12 lg:grid-cols-[minmax(0,0.95fr)_minmax(390px,0.75fr)] lg:items-center">
<div>
<p className="mb-5 text-sm font-semibold uppercase tracking-[0.12em] text-[oklch(47%_0.18_275)]">
Dynamic QR operating guide
</p>
<h1 className="max-w-3xl text-balance text-4xl font-light leading-[1.05] tracking-normal text-[oklch(22%_0.045_270)] sm:text-5xl lg:text-6xl">
{title}
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-[oklch(46%_0.035_270)]">
{page.quickAnswer}
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Link
href={page.primaryCta.href}
className="inline-flex min-h-11 items-center justify-center rounded-md bg-[oklch(50%_0.22_275)] px-5 text-sm font-semibold text-[oklch(99%_0.004_270)] transition hover:bg-[oklch(43%_0.2_275)] focus:outline-none focus:ring-2 focus:ring-[oklch(50%_0.22_275)] focus:ring-offset-2"
>
{page.primaryCta.label}
</Link>
<Link
href={page.secondaryCta.href}
className="inline-flex min-h-11 items-center justify-center rounded-md border border-[oklch(79%_0.08_275)] px-5 text-sm font-semibold text-[oklch(45%_0.18_275)] transition hover:bg-[oklch(96%_0.025_275)] focus:outline-none focus:ring-2 focus:ring-[oklch(50%_0.22_275)] focus:ring-offset-2"
>
{page.secondaryCta.label}
</Link>
</div>
</div>
<PracticeRail />
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:px-8">
<div className="grid gap-10 lg:grid-cols-[0.75fr_1.25fr]">
<div>
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
The working principle
</h2>
<p className="mt-4 max-w-[65ch] text-base leading-8 text-[oklch(45%_0.035_270)]">
{page.intro} A dynamic QR code is strongest when the team treats
the printed asset, the destination, and the reporting convention
as one workflow.
</p>
</div>
<div className="rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)] p-5 shadow-[0_24px_42px_-34px_rgba(50,50,93,0.28)]">
<div className="grid gap-4 md:grid-cols-2">
{page.keyTakeaways.map((item) => (
<div key={item} className="rounded-md border border-[oklch(91%_0.014_270)] bg-[oklch(98%_0.006_270)] p-4">
<CheckCircle2 className="mb-4 h-5 w-5 text-[oklch(55%_0.16_150)]" aria-hidden="true" />
<p className="text-sm leading-6 text-[oklch(39%_0.035_270)]">
{item}
</p>
</div>
))}
</div>
</div>
</div>
</section>
<section className="border-y border-[oklch(91%_0.014_270)] bg-[oklch(19%_0.055_270)] py-16 text-[oklch(96%_0.01_270)]">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-10 max-w-3xl">
<h2 className="text-3xl font-light leading-tight tracking-normal">
Dynamic QR setup sequence
</h2>
<p className="mt-4 leading-7 text-[oklch(78%_0.025_270)]">
Start with naming and placement. Tracking data only becomes
useful when the scan can be traced back to a real campaign,
surface, or operational context.
</p>
</div>
<ol className="grid gap-4 md:grid-cols-5">
{page.howToSteps.map((step, index) => (
<li
key={step}
className="rounded-lg border border-[oklch(35%_0.06_270)] bg-[oklch(23%_0.058_270)] p-5"
>
<span className="text-sm font-semibold tabular-nums text-[oklch(74%_0.12_275)]">
0{index + 1}
</span>
<p className="mt-5 text-sm leading-6 text-[oklch(84%_0.02_270)]">
{step}
</p>
</li>
))}
</ol>
</div>
</section>
{page.checklist && (
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
<div className="grid gap-10 lg:grid-cols-[0.8fr_1.2fr]">
<div>
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
{page.checklist.title}
</h2>
<p className="mt-4 max-w-[65ch] leading-8 text-[oklch(45%_0.035_270)]">
Use this before the QR code reaches print. Most expensive QR
mistakes are workflow mistakes, not image-generation mistakes.
</p>
</div>
<div className="rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)] p-5 shadow-[0_24px_42px_-34px_rgba(50,50,93,0.28)]">
<ul className="divide-y divide-[oklch(91%_0.014_270)]">
{page.checklist.items.map((item) => (
<li key={item} className="flex gap-4 py-4 first:pt-0 last:pb-0">
<CheckCircle2 className="mt-1 h-5 w-5 flex-none text-[oklch(55%_0.16_150)]" aria-hidden="true" />
<span className="leading-7 text-[oklch(39%_0.035_270)]">
{item}
</span>
</li>
))}
</ul>
</div>
</div>
</section>
)}
<section className="bg-[oklch(99%_0.004_270)] py-16">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-10 max-w-3xl">
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
Pages to pair with this guide
</h2>
<p className="mt-4 leading-8 text-[oklch(45%_0.035_270)]">
These companion pages cover the buying decision, tracking
setup, and the product workflow behind dynamic QR codes.
</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
{page.relatedLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="group rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(98%_0.006_270)] p-5 transition hover:border-[oklch(70%_0.1_275)] hover:bg-[oklch(97%_0.018_275)]"
>
<h3 className="font-semibold text-[oklch(26%_0.045_270)]">
{link.title}
</h3>
<p className="mt-3 text-sm leading-6 text-[oklch(46%_0.035_270)]">
{link.description}
</p>
<span className="mt-5 inline-flex items-center gap-2 text-sm font-semibold text-[oklch(45%_0.18_275)]">
Open page
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</span>
</Link>
))}
</div>
</div>
</section>
<section className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
Common questions
</h2>
<div className="mt-8 divide-y divide-[oklch(90%_0.018_270)] rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)]">
{page.faq.map((item) => (
<details key={item.question} className="group p-5">
<summary className="flex cursor-pointer list-none items-center justify-between gap-5 font-semibold text-[oklch(26%_0.045_270)]">
{item.question}
<span className="text-[oklch(45%_0.18_275)] group-open:rotate-90">+</span>
</summary>
<p className="mt-4 max-w-[65ch] leading-7 text-[oklch(45%_0.035_270)]">
{item.answer}
</p>
</details>
))}
</div>
</section>
</main>
</>
);
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import type { Metadata } from 'next';
import PricingClient from './PricingClient';
import React from 'react';
import type { Metadata } from 'next';
import Link from 'next/link';
import PricingClient from './PricingClient';
import SeoJsonLd from '@/components/SeoJsonLd';
import { AnswerFirstBlock } from '@/components/marketing/AnswerFirstBlock';
import { FAQSection } from '@/components/aeo/FAQSection';
@@ -97,11 +98,30 @@ export default function PricingPage() {
/>
</div>
<div className="container mx-auto max-w-5xl px-4 pb-4 sm:px-6 lg:px-8">
<FAQSection items={faqItems} title="Pricing questions" />
</div>
<div className="sr-only">
<div className="container mx-auto max-w-5xl px-4 pb-4 sm:px-6 lg:px-8">
<FAQSection items={faqItems} title="Pricing questions" />
</div>
<section className="container mx-auto max-w-5xl px-4 pb-10 sm:px-6 lg:px-8">
<div className="rounded-lg border border-slate-200 bg-slate-50 p-5">
<h2 className="text-xl font-semibold text-slate-900">
Not sure whether free is enough?
</h2>
<p className="mt-3 max-w-3xl leading-7 text-slate-600">
Compare free QR generators with paid dynamic QR workflows before
choosing a plan. The difference usually comes down to editing
after print, scan tracking, branding, and reprint risk.
</p>
<Link
href="/compare/free-vs-paid-qr-code-generator"
className="mt-4 inline-flex text-sm font-semibold text-blue-700 hover:text-blue-800"
>
Read the free vs paid QR generator comparison
</Link>
</div>
</section>
<div className="sr-only">
<h2>Compare our plans</h2>
<p>
Free includes 3 active dynamic QR codes and unlimited static QR codes. Pro

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,14 @@ import type { Metadata } from 'next';
import ReprintSavingsCalculator from '@/components/marketing/ReprintSavingsCalculator';
import { ArrowDown, Check, ShieldCheck, Zap } from 'lucide-react';
import SeoJsonLd from '@/components/SeoJsonLd';
import { FAQSection } from '@/components/aeo/FAQSection';
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
import { breadcrumbSchema } from '@/lib/schema';
import { breadcrumbSchema, faqPageSchema } from '@/lib/schema';
export const metadata: Metadata = {
title: 'Reprint Cost Calculator | QR Master',
title: 'QR Code Reprint Cost Calculator',
description:
'Calculate how much you are wasting on QR code reprints. See your potential savings with dynamic QR codes that never need to be reprinted.',
'Calculate QR code reprint costs and compare static QR reprints with dynamic QR codes that can be edited after printing.',
alternates: {
canonical: 'https://www.qrmaster.net/reprint-calculator',
},
@@ -18,8 +19,8 @@ export const metadata: Metadata = {
follow: true,
},
openGraph: {
title: 'Reprint Cost Calculator | QR Master',
description: 'Stop wasting money on reprints. Calculate your savings now.',
title: 'QR Code Reprint Cost Calculator',
description: 'Calculate reprint savings when static QR codes are replaced with editable dynamic QR codes.',
url: 'https://www.qrmaster.net/reprint-calculator',
type: 'website',
images: [
@@ -60,7 +61,52 @@ const breadcrumbs = [
{ name: 'Reprint Calculator', url: '/reprint-calculator' },
];
const faqItems = [
{
question: 'What is a QR code reprint cost calculator?',
answer: 'A QR code reprint cost calculator estimates how much money is lost when printed static QR codes need to be replaced after a URL, menu, campaign, or landing page changes.',
},
{
question: 'Why do static QR codes cause reprint costs?',
answer: 'Static QR codes store the final destination directly in the code. If the destination changes after printing, the old printed QR code cannot be edited and the asset usually needs to be reprinted.',
},
{
question: 'Can dynamic QR codes reduce reprint costs?',
answer: 'Yes. Dynamic QR codes use an editable redirect, so the printed code can stay the same while the destination URL changes in the dashboard.',
},
{
question: 'When is a paid dynamic QR code cheaper than a free static QR code?',
answer: 'A paid dynamic QR code can be cheaper when one changed URL would force a menu, flyer, label, package, or campaign asset to be printed again.',
},
{
question: 'What should I calculate before printing a QR code?',
answer: 'Estimate print volume, replacement frequency, design time, distribution cost, and the likelihood that the destination URL will change during the life of the printed asset.',
},
{
question: 'Do QR code tracking and reprint savings belong together?',
answer: 'Often yes. Dynamic QR codes can reduce reprint risk and add scan tracking, which helps teams measure whether printed placements are worth repeating.',
},
];
const growthLinks = [
{
href: '/compare/dynamic-vs-static-qr-codes',
title: 'Dynamic vs Static QR Codes',
description: 'See why static QR codes create reprint risk and when dynamic QR codes are the safer print workflow.',
ctaLabel: 'Compare QR code types',
},
{
href: '/compare/free-vs-paid-qr-code-generator',
title: 'Free vs Paid QR Generator',
description: 'Check when free generation is enough and when paid editability, tracking, and branding justify the switch.',
ctaLabel: 'Compare free and paid QR tools',
},
{
href: '/guide/dynamic-qr-code-best-practices',
title: 'Dynamic QR Code Best Practices',
description: 'Plan naming, tracking, print testing, and update workflows before you commit a dynamic QR code to print.',
ctaLabel: 'Read the best-practices guide',
},
{
href: '/dynamic-qr-code-generator',
title: 'Dynamic QR Code Generator',
@@ -84,7 +130,7 @@ const growthLinks = [
export default function ReprintCalculatorPage() {
return (
<>
<SeoJsonLd data={[softwareSchema, howToSchema, breadcrumbSchema(breadcrumbs)]} />
<SeoJsonLd data={[softwareSchema, howToSchema, faqPageSchema(faqItems), breadcrumbSchema(breadcrumbs)]} />
{/* Hero Section */}
<section className="pt-24 pb-12 bg-white relative overflow-hidden">
<div className="container mx-auto px-4 text-center max-w-3xl relative z-10">
@@ -165,6 +211,12 @@ export default function ReprintCalculatorPage() {
</div>
</section>
<section className="py-16 bg-slate-50 border-t border-slate-100">
<div className="container mx-auto px-4 max-w-4xl">
<FAQSection items={faqItems} title="QR code reprint cost questions" />
</div>
</section>
<GrowthLinksSection
eyebrow="Next steps"
title="Stop reprinting. Start updating."