This commit is contained in:
Timo Knuth 2026-01-28 16:02:32 +01:00
parent 1a6dc01291
commit a76bcb70e1
5 changed files with 107 additions and 3 deletions

View File

@ -4,18 +4,41 @@ import { getPublishedPostBySlug, getAuthorBySlug, getRelatedPosts, getPublishedP
import { AnswerBox } from "@/components/aeo/AnswerBox"; import { AnswerBox } from "@/components/aeo/AnswerBox";
import { StepList } from "@/components/aeo/StepList"; import { StepList } from "@/components/aeo/StepList";
import { FAQSection } from "@/components/aeo/FAQSection"; import { FAQSection } from "@/components/aeo/FAQSection";
import { SourcesList } from "@/components/aeo/SourcesList";
import { AuthorCard } from "@/components/author/AuthorCard"; import { AuthorCard } from "@/components/author/AuthorCard";
import { RelatedPosts } from "@/components/blog/RelatedPosts"; import { RelatedPosts } from "@/components/blog/RelatedPosts";
import { blogPostingSchema, howToSchema, faqPageSchema } from "@/lib/schema"; import { blogPostingSchema, howToSchema, faqPageSchema, breadcrumbSchema } from "@/lib/schema";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
export function generateMetadata({ params }: { params: { slug: string } }) { export function generateMetadata({ params }: { params: { slug: string } }) {
const post = getPublishedPostBySlug(params.slug); const post = getPublishedPostBySlug(params.slug);
if (!post) return {}; if (!post) return {};
const ogImage = post.heroImage ? `https://www.qrmaster.net${post.heroImage}` : undefined;
return { return {
title: post.title, title: post.title,
description: post.description, description: post.description,
alternates: {
canonical: `https://www.qrmaster.net/blog/${post.slug}`,
},
openGraph: {
title: post.title,
description: post.description,
type: 'article',
publishedTime: post.datePublished,
modifiedTime: post.dateModified || post.datePublished,
authors: ['https://www.qrmaster.net'],
tags: post.keywords,
images: ogImage ? [{ url: ogImage, alt: post.imageAlt || post.title }] : undefined,
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.description,
images: ogImage ? [ogImage] : undefined,
}
}; };
} }
@ -38,6 +61,15 @@ export default function BlogPostPage({ params }: { params: { slug: string } }) {
const howtoLd = post.keySteps?.length ? howToSchema(post, author) : null; const howtoLd = post.keySteps?.length ? howToSchema(post, author) : null;
const faqLd = post.faq ? faqPageSchema(post.faq) : null; const faqLd = post.faq ? faqPageSchema(post.faq) : null;
// Generate breadcrumb schema: Home → Learn → Pillar → Post
const pillarName = post.pillar ? post.pillar.charAt(0).toUpperCase() + post.pillar.slice(1) : 'Blog';
const breadcrumbLd = breadcrumbSchema([
{ name: 'Home', url: '/' },
{ name: 'Learn', url: '/learn' },
{ name: pillarName, url: `/learn/${post.pillar}` },
{ name: post.title, url: `/blog/${post.slug}` },
]);
return ( return (
<main className="container mx-auto max-w-4xl py-12 px-4"> <main className="container mx-auto max-w-4xl py-12 px-4">
<Script id="ld-blogposting" type="application/ld+json" strategy="afterInteractive" <Script id="ld-blogposting" type="application/ld+json" strategy="afterInteractive"
@ -50,6 +82,8 @@ export default function BlogPostPage({ params }: { params: { slug: string } }) {
<Script id="ld-faq" type="application/ld+json" strategy="afterInteractive" <Script id="ld-faq" type="application/ld+json" strategy="afterInteractive"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }} /> dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }} />
)} )}
<Script id="ld-breadcrumb" type="application/ld+json" strategy="afterInteractive"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbLd) }} />
<header className="space-y-6 text-center max-w-3xl mx-auto mb-10"> <header className="space-y-6 text-center max-w-3xl mx-auto mb-10">
<div className="flex justify-center gap-2 text-sm text-gray-500 font-medium"> <div className="flex justify-center gap-2 text-sm text-gray-500 font-medium">
@ -71,7 +105,12 @@ export default function BlogPostPage({ params }: { params: { slug: string } }) {
)} )}
<div className="text-left text-sm"> <div className="text-left text-sm">
<div className="font-bold text-gray-900">{author.name}</div> <div className="font-bold text-gray-900">{author.name}</div>
<div className="text-gray-500">Updated {post.updatedAt || post.date}</div> <div className="text-gray-500 text-xs mt-0.5">
Published {post.date}
{post.updatedAt && (
<> <span className="mx-1"></span> Updated {post.updatedAt}</>
)}
</div>
</div> </div>
</div> </div>
)} )}
@ -97,6 +136,9 @@ export default function BlogPostPage({ params }: { params: { slug: string } }) {
{/* AEO BLOCK: FAQ */} {/* AEO BLOCK: FAQ */}
{!!post.faq?.length && <div className="mt-12"><FAQSection items={post.faq} /></div>} {!!post.faq?.length && <div className="mt-12"><FAQSection items={post.faq} /></div>}
{/* AEO BLOCK: SOURCES */}
{!!post.sources?.length && <SourcesList sources={post.sources} />}
<div className="border-t border-gray-100 my-12"></div> <div className="border-t border-gray-100 my-12"></div>
{author && <AuthorCard author={author} />} {author && <AuthorCard author={author} />}

View File

@ -0,0 +1,40 @@
"use client";
import React from 'react';
import type { Source } from "@/lib/types";
type Props = {
sources: Source[];
title?: string;
};
export function SourcesList({ sources, title = "Sources & References" }: Props) {
if (!sources?.length) return null;
return (
<section className="rounded-xl border border-gray-100 bg-gray-50/50 p-6 my-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">{title}</h3>
<ol className="space-y-2 list-decimal list-inside">
{sources.map((source, index) => (
<li key={index} className="text-sm text-gray-700">
<cite className="not-italic">
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 hover:underline"
>
{source.name}
</a>
</cite>
{source.accessDate && (
<span className="text-gray-500 ml-2">
(accessed {source.accessDate})
</span>
)}
</li>
))}
</ol>
</section>
);
}

View File

@ -317,6 +317,12 @@ export const blogPosts: BlogPost[] = [
{ question: "How do I connect QR analytics to Google Analytics?", answer: "Use UTMs and track landing page events (forms, purchases, calls) inside GA." }, { question: "How do I connect QR analytics to Google Analytics?", answer: "Use UTMs and track landing page events (forms, purchases, calls) inside GA." },
], ],
relatedSlugs: ["qr-code-tracking-guide-2025", "utm-parameter-qr-codes", "qr-code-scan-statistics-2026", "trackable-qr-codes"], relatedSlugs: ["qr-code-tracking-guide-2025", "utm-parameter-qr-codes", "qr-code-scan-statistics-2026", "trackable-qr-codes"],
sources: [
{ name: "Yahoo Finance: Global QR Code Payments Market Analysis 2025-2030", url: "https://finance.yahoo.com/news/analysis-global-qr-code-payments-155300360.html", accessDate: "January 2026" },
{ name: "QRCodeChimp: QR Code Statistics for 2026", url: "https://www.qrcodechimp.com/qr-code-statistics/", accessDate: "January 2026" },
{ name: "FBI IC3: Warning on QR Code Phishing Attacks", url: "https://www.ic3.gov/CSA/2026/260108.pdf", accessDate: "January 2026" },
{ name: "Barracuda Networks: Email Threat Radar January 2026", url: "https://blog.barracuda.com/2026/01/22/email-threat-radar-january-2026", accessDate: "January 2026" },
],
content: `<div class="blog-content"> content: `<div class="blog-content">
<h2>What Are Scan Analytics?</h2> <h2>What Are Scan Analytics?</h2>
<p>Scan analytics provide comprehensive insights into how users interact with your <a href="/qr-code-tracking" class="text-primary-600 hover:underline">dynamic QR codes</a>. Our advanced dashboard tracks scan analytics including geographic location, device types, scan timestamps, and user engagement patterns.</p> <p>Scan analytics provide comprehensive insights into how users interact with your <a href="/qr-code-tracking" class="text-primary-600 hover:underline">dynamic QR codes</a>. Our advanced dashboard tracks scan analytics including geographic location, device types, scan timestamps, and user engagement patterns.</p>
@ -713,6 +719,11 @@ export const blogPosts: BlogPost[] = [
], ],
relatedSlugs: ["qr-code-tracking-guide-2025", "qr-code-analytics", "dynamic-vs-static-qr-codes", "qr-code-small-business"], relatedSlugs: ["qr-code-tracking-guide-2025", "qr-code-analytics", "dynamic-vs-static-qr-codes", "qr-code-small-business"],
sources: [
{ name: "Mordor Intelligence: QR Codes Market Size & Trend Analysis 2026-2031", url: "https://www.mordorintelligence.com/industry-reports/qr-codes-market", accessDate: "January 2026" },
{ name: "Bitly: 30+ QR Code Statistics for 2026", url: "https://bitly.com/blog/qr-code-statistics/", accessDate: "January 2026" },
{ name: "QR Code Tiger: QR Code Adoption Rate Stats 2026", url: "https://www.qrcode-tiger.com/qr-code-adoption-rate", accessDate: "January 2026" },
],
content: `<div class="blog-content"> content: `<div class="blog-content">
<p>Most QR codes are dumb. They work, but you have zero idea what happens after people scan. Thats why <strong>trackable QR codes</strong> are a game changer: you can measure scans, compare placements, and optimize campaigns like a real marketer.</p> <p>Most QR codes are dumb. They work, but you have zero idea what happens after people scan. Thats why <strong>trackable QR codes</strong> are a game changer: you can measure scans, compare placements, and optimize campaigns like a real marketer.</p>

View File

@ -40,7 +40,10 @@ export function organizationSchema() {
height: 630, height: 630,
}, },
sameAs: [ sameAs: [
'https://twitter.com/qrmaster', 'https://www.wikidata.org/wiki/Q137918857',
'https://x.com/TIMO_QRMASTER',
'https://www.linkedin.com/in/qr-master-44b6863a2/',
'https://www.instagram.com/qrmaster_net/',
], ],
contactPoint: [{ contactPoint: [{
'@type': 'ContactPoint', '@type': 'ContactPoint',

View File

@ -5,7 +5,14 @@ export type FAQItem = {
answer: string; // allow HTML or plain answer: string; // allow HTML or plain
}; };
export type Source = {
name: string; // "Statista QR Code Market Report 2026"
url: string; // "https://www.statista.com/..."
accessDate?: string; // "January 2026"
};
export type BlogPost = { export type BlogPost = {
slug: string; slug: string;
title: string; title: string;
excerpt: string; // kept for backward compatibility if needed, maps to description excerpt: string; // kept for backward compatibility if needed, maps to description
@ -34,6 +41,7 @@ export type BlogPost = {
keySteps?: string[]; // plain keySteps?: string[]; // plain
faq?: FAQItem[]; faq?: FAQItem[];
relatedSlugs?: string[]; relatedSlugs?: string[];
sources?: Source[]; // Primary sources for AEO trust signals
// Main content // Main content
content: string; // HTML string (mapped from contentHtml in spec to content here to match existing usage if preferred, or we stick to contentHtml) content: string; // HTML string (mapped from contentHtml in spec to content here to match existing usage if preferred, or we stick to contentHtml)