feat: Add marketing and use case pages, their data structures, and supporting UI components.

This commit is contained in:
Timo Knuth
2026-03-09 12:47:52 +01:00
parent f5f3979996
commit 3c8e6bd19f
22 changed files with 4047 additions and 184 deletions

View File

@@ -10,7 +10,9 @@ export function generateMetadata({ params }: { params: { slug: string } }) {
const author = getAuthorBySlug(params.slug);
if (!author) return {};
return {
title: `${author.name} - ${author.role} | QR Master`,
title: {
absolute: `${author.name} - ${author.role}`,
},
description: author.bio,
alternates: {
canonical: `https://www.qrmaster.net/authors/${author.slug}`,

View File

@@ -1,5 +1,4 @@
import { notFound } from "next/navigation";
import Script from "next/script";
import { getPublishedPostBySlug, getAuthorBySlug, getRelatedPosts, getPublishedPosts } from "@/lib/content";
import { AnswerBox } from "@/components/aeo/AnswerBox";
import { StepList } from "@/components/aeo/StepList";
@@ -72,17 +71,17 @@ export default function BlogPostPage({ params }: { params: { slug: string } }) {
return (
<main className="container mx-auto max-w-4xl py-12 px-4">
<Script id="ld-blogposting" type="application/ld+json" strategy="afterInteractive"
<script type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
{howtoLd && (
<Script id="ld-howto" type="application/ld+json" strategy="afterInteractive"
<script type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(howtoLd) }} />
)}
{faqLd && (
<Script id="ld-faq" type="application/ld+json" strategy="afterInteractive"
<script type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }} />
)}
<Script id="ld-breadcrumb" type="application/ld+json" strategy="afterInteractive"
<script type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbLd) }} />
<header className="space-y-6 text-center max-w-3xl mx-auto mb-10">

View File

@@ -0,0 +1,145 @@
import { getPublishedPostBySlug } from '@/lib/content';
import sanitizeHtml from 'sanitize-html';
const RAW_ENABLED_SLUGS = new Set([
'dynamic-vs-static-qr-codes',
'qr-code-small-business',
'qr-code-tracking-guide-2025',
'utm-parameter-qr-codes',
'trackable-qr-codes',
]);
function decodeHtmlEntities(text: string): string {
return text
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&mdash;/g, '--')
.replace(/&ndash;/g, '-')
.replace(/&hellip;/g, '...')
.replace(/&#x27;/g, "'")
.replace(/&#x2F;/g, '/')
.replace(/&#(\d+);/g, (_, code) => {
const value = Number.parseInt(code, 10);
return Number.isNaN(value) ? '' : String.fromCharCode(value);
});
}
function cleanHtmlToText(html: string): string {
const normalized = html
.replace(/<div\b[^>]*class=(['"])[^'"]*post-metadata[^'"]*\1[^>]*>[\s\S]*?<\/div>/gi, '')
.replace(/<div\b[^>]*class=(['"])[^'"]*blog-content[^'"]*\1[^>]*>/gi, '')
.replace(/<\/div>\s*$/i, '');
const withLinks = normalized.replace(
/<a\b[^>]*href=(['"])(.*?)\1[^>]*>([\s\S]*?)<\/a>/gi,
(_, __, href: string, text: string) => `[${cleanHtmlToText(text)}](${href})`,
);
const structured = withLinks
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<li\b[^>]*>/gi, '- ')
.replace(/<\/li>/gi, '\n')
.replace(/<h([1-6])\b[^>]*>/gi, (_, level: string) => `${'#'.repeat(Number.parseInt(level, 10))} `)
.replace(/<\/h[1-6]>/gi, '\n\n')
.replace(/<\/p>/gi, '\n\n')
.replace(/<\/div>/gi, '\n\n')
.replace(/<\/section>/gi, '\n\n')
.replace(/<\/ul>/gi, '\n')
.replace(/<\/ol>/gi, '\n');
const stripped = sanitizeHtml(structured, {
allowedTags: [],
allowedAttributes: {},
});
return decodeHtmlEntities(stripped)
.replace(/\r\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.replace(/[ \t]+\n/g, '\n')
.replace(/\n[ \t]+/g, '\n')
.trim();
}
function renderRawPost(slug: string): string | null {
if (!RAW_ENABLED_SLUGS.has(slug)) {
return null;
}
const post = getPublishedPostBySlug(slug);
if (!post) {
return null;
}
const sections: string[] = [
`# ${post.title}`,
'',
post.description,
'',
`Canonical URL: https://www.qrmaster.net/blog/${post.slug}`,
`Published: ${post.datePublished}`,
`Updated: ${post.dateModified || post.updatedAt || post.datePublished}`,
];
if (post.quickAnswer) {
sections.push('', '## Quick Answer', '', cleanHtmlToText(post.quickAnswer));
}
if (post.keySteps?.length) {
sections.push('', '## Steps', '', ...post.keySteps.map((step, index) => `${index + 1}. ${step}`));
}
const mainText = cleanHtmlToText(post.content);
if (mainText) {
sections.push('', '## Article', '', mainText);
}
if (post.faq?.length) {
sections.push('', '## FAQ', '');
for (const item of post.faq) {
sections.push(`Q: ${cleanHtmlToText(item.question)}`);
sections.push(`A: ${cleanHtmlToText(item.answer)}`, '');
}
if (sections[sections.length - 1] === '') {
sections.pop();
}
}
if (post.sources?.length) {
sections.push('', '## Sources', '');
for (const source of post.sources) {
const accessDate = source.accessDate ? ` (accessed ${source.accessDate})` : '';
sections.push(`- ${source.name}: ${source.url}${accessDate}`);
}
}
return `${sections.join('\n').trim()}\n`;
}
export async function GET(
_request: Request,
{ params }: { params: { slug: string } },
) {
const content = renderRawPost(params.slug);
if (!content) {
return new Response('Not Found', {
status: 404,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'X-Robots-Tag': 'noindex, nofollow',
},
});
}
return new Response(content, {
headers: {
'Content-Type': 'text/markdown; charset=utf-8',
'X-Robots-Tag': 'noindex, nofollow',
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}

View File

@@ -8,7 +8,6 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import { breadcrumbSchema } from '@/lib/schema';
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
import { MarketingPageTracker } from '@/components/marketing/MarketingAnalytics';
import { featuredUseCases } from '@/lib/growth-pages';
export const metadata: Metadata = {
title: {
@@ -289,18 +288,18 @@ export default function BulkQRCodeGeneratorPage() {
];
const relatedUseCaseLinks = [
{
href: '/use-cases/packaging-qr-codes',
title: 'Packaging QR Codes',
description: 'Use bulk generation when packaging, inserts, or labels need measurable scan routing at scale.',
ctaLabel: 'Create your packaging QR code',
},
{
href: '/qr-code-for-marketing-campaigns',
title: 'QR Codes for Marketing Campaigns',
description: 'Use bulk generation when campaign placement or print distribution needs multiple trackable codes.',
ctaLabel: 'Create a trackable campaign QR',
},
{
href: featuredUseCases[2].href,
title: featuredUseCases[2].title,
description: featuredUseCases[2].summary,
ctaLabel: featuredUseCases[2].ctaLabel,
},
{
href: '/use-cases',
title: 'Explore the use-case hub',

View File

@@ -295,25 +295,25 @@ export default function CustomQRCodeGeneratorPage() {
{ name: 'Custom QR Code Generator', url: '/custom-qr-code-generator' },
];
const relatedUseCaseLinks = [
{
href: '/qr-code-for-marketing-campaigns',
title: 'QR Codes for Marketing Campaigns',
description: 'Connect branded print QR codes to campaign-specific destinations and measurement.',
ctaLabel: 'Create a trackable campaign QR',
},
{
href: featuredUseCases[0].href,
title: featuredUseCases[0].title,
description: featuredUseCases[0].summary,
ctaLabel: featuredUseCases[0].ctaLabel,
},
{
href: featuredUseCases[2].href,
title: featuredUseCases[2].title,
description: featuredUseCases[2].summary,
ctaLabel: featuredUseCases[2].ctaLabel,
},
const relatedUseCaseLinks = [
{
href: '/use-cases/flyer-qr-codes',
title: 'Flyer QR Codes',
description: 'Use branded print QR codes when campaign assets need both visual fit and measurable routing.',
ctaLabel: 'Create your flyer QR code',
},
{
href: featuredUseCases[0].href,
title: featuredUseCases[0].title,
description: featuredUseCases[0].summary,
ctaLabel: featuredUseCases[0].ctaLabel,
},
{
href: '/use-cases/real-estate-sign-qr-codes',
title: 'Real Estate Sign QR Codes',
description: 'Useful when branded property signage needs a cleaner listing handoff and trackable scan context.',
ctaLabel: 'Create your real estate QR code',
},
{
href: '/use-cases',
title: 'Explore the use-case hub',

View File

@@ -244,18 +244,18 @@ export default function DynamicQRCodeGeneratorPage() {
description: featuredUseCases[0].summary,
ctaLabel: featuredUseCases[0].ctaLabel,
},
{
href: '/use-cases/payment-qr-codes',
title: 'Payment QR Codes',
description: 'Use one printed payment prompt that stays useful even when the checkout or provider path changes.',
ctaLabel: 'Create your payment QR code',
},
{
href: featuredUseCases[1].href,
title: featuredUseCases[1].title,
description: featuredUseCases[1].summary,
ctaLabel: featuredUseCases[1].ctaLabel,
},
{
href: featuredUseCases[2].href,
title: featuredUseCases[2].title,
description: featuredUseCases[2].summary,
ctaLabel: featuredUseCases[2].ctaLabel,
},
{
href: '/use-cases',
title: 'Explore the use-case hub',

View File

@@ -0,0 +1,143 @@
import type { Metadata } from "next";
import {
buildUseCaseMetadata,
UseCasePageTemplate,
} from "@/components/marketing/UseCasePageTemplate";
export const metadata: Metadata = buildUseCaseMetadata({
title: "QR Code Analytics",
description:
"Measure QR code scans by placement, timing, device context, and campaign route so offline workflows become reportable.",
canonicalPath: "/qr-code-analytics",
});
const softwareSchema = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"@id": "https://www.qrmaster.net/qr-code-analytics#software",
name: "QR Master - QR Code Analytics",
applicationCategory: "BusinessApplication",
operatingSystem: "Web Browser",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD",
availability: "https://schema.org/InStock",
},
description:
"QR analytics software for measuring scans by placement, timing, device context, and offline campaign routing.",
featureList: [
"Placement-level scan reporting",
"Device and timing context",
"Offline-to-online campaign attribution",
"Scan visibility across print workflows",
"Destination updates without reprinting",
],
};
export default function QRCodeAnalyticsPage() {
return (
<UseCasePageTemplate
title="QR Code Analytics"
description="Measure QR code scans by placement, timing, device context, and campaign route so offline workflows become reportable."
eyebrow="Analytics"
intro="QR code analytics matters when a scan is not just a click, but evidence that a sign, flyer, package, or service prompt is doing its job in the real world."
pageType="commercial"
cluster="qr-analytics"
useCase="qr-analytics"
breadcrumbs={[
{ name: "Home", url: "/" },
{ name: "QR Code Analytics", url: "/qr-code-analytics" },
]}
answer="QR code analytics helps you understand which printed placements, campaigns, and post-scan routes generate useful activity so you can improve what gets reprinted, redistributed, or scaled next."
whenToUse={[
"You need more than raw scan counts from campaigns, packaging, or offline placements.",
"You want to compare where scans happen and which printed surfaces actually drive action.",
"You need a clearer bridge between QR scans and business outcomes such as signup, offers, or support engagement.",
]}
comparisonItems={[
{ label: "Placement visibility", text: "Usually blended", value: true },
{ label: "Post-print reporting", text: "Weak", value: true },
{ label: "Campaign comparison", text: "Manual or partial", value: true },
]}
howToSteps={[
"Create QR flows that map to real placements or workflow contexts instead of one generic code for every use case.",
"Track scans with enough context to compare signs, flyers, inserts, or support surfaces cleanly.",
"Use the reporting to decide which destinations, offers, or print placements deserve the next round of investment.",
]}
primaryCta={{
href: "/signup",
label: "Start measuring QR scans",
}}
secondaryCta={{
href: "/use-cases",
label: "Browse measured workflows",
}}
workflowTitle="What useful QR analytics should help you answer"
workflowIntro="The point of analytics is not to produce dashboards for their own sake. It is to make better decisions about what to print again, where to place it, and what happens after the scan."
workflowCards={[
{
title: "Placement comparison",
description:
"Separate flyer, packaging, sign, event, or service-surface traffic so you know which printed context actually creates useful scan activity.",
},
{
title: "Post-print flexibility",
description:
"Review performance and then improve the destination, offer, or next action without replacing every physical code already in circulation.",
},
{
title: "Operational reporting",
description:
"Give marketing, operations, or support teams a better view of what physical QR programs are doing once they are live in the field.",
},
]}
checklistTitle="QR analytics checklist"
checklist={[
"Define which placements or workflow surfaces should be compared before launching the QR program.",
"Use naming or routing that lets scans be grouped by real business context, not only by one generic campaign.",
"Make the first post-scan step relevant enough that a scan can become a useful action, not a bounce.",
"Review analytics before reprinting so the next batch reflects real-world performance.",
]}
supportLinks={[
{
href: "/use-cases/packaging-qr-codes",
title: "Use case: Packaging QR Codes",
description:
"See how packaging scans can become a measurable post-purchase signal instead of a blind spot.",
},
{
href: "/use-cases/flyer-qr-codes",
title: "Use case: Flyer QR Codes",
description:
"Useful when scan performance needs to be reviewed by distribution point or campaign wave.",
},
{
href: "/blog/trackable-qr-codes",
title: "Trackable QR Codes",
description:
"Support article for understanding what measurable QR setups should capture and why it matters.",
},
]}
faq={[
{
question: "What can QR code analytics show?",
answer:
"QR code analytics can show scan activity by placement, time, device context, and campaign route so teams can see which physical programs are actually performing.",
},
{
question: "Why are QR code analytics useful for offline campaigns?",
answer:
"They help turn offline placements such as flyers, packaging, signs, or event materials into something measurable instead of relying on assumptions about what worked.",
},
{
question: "Do I need dynamic QR codes for analytics?",
answer:
"In most cases yes, because analytics usually depends on a managed redirect or reporting layer that also lets you update destinations without reprinting.",
},
]}
schemaData={[softwareSchema]}
/>
);
}

View File

@@ -7,7 +7,6 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import { breadcrumbSchema } from '@/lib/schema';
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
import { MarketingPageTracker, TrackedCtaLink } from '@/components/marketing/MarketingAnalytics';
import { featuredUseCases } from '@/lib/growth-pages';
export const metadata: Metadata = {
title: {
@@ -173,22 +172,22 @@ export default function QRCodeTrackingPage() {
const relatedUseCaseLinks = [
{
href: featuredUseCases[2].href,
title: featuredUseCases[2].title,
description: featuredUseCases[2].summary,
ctaLabel: featuredUseCases[2].ctaLabel,
href: '/use-cases/real-estate-sign-qr-codes',
title: 'Real Estate Sign QR Codes',
description: 'Compare sign, brochure, and open-house scans without blending every property source together.',
ctaLabel: 'Create your real estate QR code',
},
{
href: '/qr-code-for-marketing-campaigns',
title: 'QR Codes for Marketing Campaigns',
description: 'Map scans to campaign placements, creative tests, and offline-to-online attribution.',
ctaLabel: 'Create a trackable campaign QR',
href: '/use-cases/feedback-qr-codes',
title: 'Feedback QR Codes',
description: 'Measure which service surfaces and follow-up prompts actually generate customer responses.',
ctaLabel: 'Create your feedback QR code',
},
{
href: featuredUseCases[0].href,
title: featuredUseCases[0].title,
description: featuredUseCases[0].summary,
ctaLabel: featuredUseCases[0].ctaLabel,
href: '/use-cases/coupon-qr-codes',
title: 'Coupon QR Codes',
description: 'Tie printed discount placements to measurable scans so you can compare promotion performance.',
ctaLabel: 'Create your coupon QR code',
},
{
href: '/use-cases',

View File

@@ -5,12 +5,12 @@ import {
UseCasePageTemplate,
} from "@/components/marketing/UseCasePageTemplate";
import {
featuredUseCases,
allUseCases,
getUseCasePage,
} from "@/lib/growth-pages";
export function generateStaticParams() {
return featuredUseCases.map((item) => ({
return allUseCases.map((item) => ({
slug: item.slug,
}));
}

View File

@@ -19,6 +19,7 @@ import {
import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import {
allUseCases,
commercialPages,
featuredUseCases,
supportResources,
@@ -170,20 +171,20 @@ export default function UseCasesHubPage() {
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-10 max-w-3xl">
<div className="text-sm font-semibold uppercase tracking-[0.22em] text-blue-700">
Featured use cases
All live use cases
</div>
<h2 className="mt-3 text-3xl font-bold text-slate-900">
First workflows in the growth rollout
Every currently published workflow route
</h2>
<p className="mt-4 text-lg leading-8 text-slate-600">
These are the first routes worth surfacing because they connect
cleanly to QR Master's strongest product angles and existing
supporting content.
These are all currently published use-case routes in the growth
layer. Each one maps back to a clear commercial parent and a
measurable print or post-scan workflow.
</p>
</div>
<div className="grid gap-6 lg:grid-cols-3">
{featuredUseCases.map((page) => (
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
{allUseCases.map((page) => (
<Link key={page.slug} href={page.href} className="group block">
<Card className="flex h-full flex-col rounded-3xl border-slate-200 bg-white p-7 shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg">
<div className="text-sm font-semibold uppercase tracking-[0.18em] text-blue-700">