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

@@ -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>
</>
);
}