SEO blog post V2

This commit is contained in:
2026-08-05 22:28:34 +02:00
parent 8c50bf71c5
commit ca605f8852
7 changed files with 7456 additions and 8477 deletions

5
.gitignore vendored
View File

@@ -77,6 +77,11 @@ meta-fix.js
read-inbox.mjs read-inbox.mjs
quora_antwort_statisch_dynamisch.txt quora_antwort_statisch_dynamisch.txt
# Local blog audit reports and temporary snapshots
scratch_blog_analysis.json
scratch_scored_blog_posts.json
src/lib/blog-data.snapshot-*.ts
# Local developer-package workspaces and unreferenced generated media # Local developer-package workspaces and unreferenced generated media
/packages/ /packages/
/public/Events/ /public/Events/

5569
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,97 +1,99 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import SeoJsonLd from '@/components/SeoJsonLd'; import SeoJsonLd from '@/components/SeoJsonLd';
import { websiteSchema, breadcrumbSchema } from '@/lib/schema'; import { websiteSchema, breadcrumbSchema } from '@/lib/schema';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import { blogPosts } from '@/lib/blog-data'; import { blogPosts } from '@/lib/blog-data';
import { REDIRECTED_BLOG_SLUGS } from "@/lib/content";
// Enable Incremental Static Regeneration (ISR)
// Revalidate every hour // Enable Incremental Static Regeneration (ISR)
export const revalidate = 3600; // Revalidate every hour
export const revalidate = 3600;
function truncateAtWord(text: string, maxLength: number): string {
if (text.length <= maxLength) return text; function truncateAtWord(text: string, maxLength: number): string {
const truncated = text.slice(0, maxLength); if (text.length <= maxLength) return text;
const lastSpace = truncated.lastIndexOf(' '); const truncated = text.slice(0, maxLength);
return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated; const lastSpace = truncated.lastIndexOf(' ');
} return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated;
}
export async function generateMetadata(): Promise<Metadata> {
const title = truncateAtWord('QR Insights: Latest QR Strategies', 60); export async function generateMetadata(): Promise<Metadata> {
const description = truncateAtWord( const title = truncateAtWord('QR Insights: Latest QR Strategies', 60);
'Expert guides on QR code strategies, marketing campaigns, tracking analytics, and best practices for small businesses and enterprises.', const description = truncateAtWord(
160 'Expert guides on QR code strategies, marketing campaigns, tracking analytics, and best practices for small businesses and enterprises.',
); 160
);
return {
title, return {
description, title,
alternates: { description,
canonical: 'https://www.qrmaster.net/blog', alternates: {
languages: { canonical: 'https://www.qrmaster.net/blog',
'x-default': 'https://www.qrmaster.net/blog', languages: {
en: 'https://www.qrmaster.net/blog', 'x-default': 'https://www.qrmaster.net/blog',
}, en: 'https://www.qrmaster.net/blog',
}, },
openGraph: { },
title, openGraph: {
description, title,
url: 'https://www.qrmaster.net/blog', description,
type: 'website', url: 'https://www.qrmaster.net/blog',
images: ['/og-image.png'], type: 'website',
}, images: ['/og-image.png'],
twitter: { },
title, twitter: {
description, title,
}, description,
}; },
} };
}
export default function BlogPage() {
const breadcrumbItems: BreadcrumbItem[] = [ export default function BlogPage() {
{ name: 'Home', url: '/' }, const breadcrumbItems: BreadcrumbItem[] = [
{ name: 'Blog', url: '/blog' }, { name: 'Home', url: '/' },
]; { name: 'Blog', url: '/blog' },
];
// Filter posts to only show those published in the past or today
// sort by date descending (newest first) // Filter posts to only show those published in the past or today
const currentDate = new Date(); // sort by date descending (newest first)
const currentDate = new Date();
const publishedPosts = blogPosts
.filter(post => { const publishedPosts = blogPosts
const publishDate = post.datePublished ? new Date(post.datePublished) : new Date(post.date); .filter(post => {
return publishDate <= currentDate; if (REDIRECTED_BLOG_SLUGS.has(post.slug)) return false;
}) const publishDate = post.datePublished ? new Date(post.datePublished) : new Date(post.date);
.sort((a, b) => { return publishDate <= currentDate;
const dateA = a.datePublished ? new Date(a.datePublished) : new Date(a.date); })
const dateB = b.datePublished ? new Date(b.datePublished) : new Date(b.date); .sort((a, b) => {
return dateB.getTime() - dateA.getTime(); const dateA = a.datePublished ? new Date(a.datePublished) : new Date(a.date);
}); const dateB = b.datePublished ? new Date(b.datePublished) : new Date(b.date);
return dateB.getTime() - dateA.getTime();
return ( });
<>
<SeoJsonLd data={[websiteSchema(), breadcrumbSchema(breadcrumbItems)]} /> return (
<div className="py-20 bg-gradient-to-b from-gray-50 to-white"> <>
<div className="container mx-auto px-4"> <SeoJsonLd data={[websiteSchema(), breadcrumbSchema(breadcrumbItems)]} />
<Breadcrumbs items={breadcrumbItems} /> <div className="py-20 bg-gradient-to-b from-gray-50 to-white">
<div className="text-center mb-16"> <div className="container mx-auto px-4">
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6"> <Breadcrumbs items={breadcrumbItems} />
QR Code Insights <div className="text-center mb-16">
</h1> <h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6">
<p className="text-xl text-gray-600 max-w-2xl mx-auto"> QR Code Insights
Expert guides on dynamic QR codes, campaign tracking, UTM analytics, and smart marketing use cases. </h1>
Discover how-to tutorials and best practices for QR code analytics. <p className="text-xl text-gray-600 max-w-2xl mx-auto">
</p> Expert guides on dynamic QR codes, campaign tracking, UTM analytics, and smart marketing use cases.
</div> Discover how-to tutorials and best practices for QR code analytics.
</p>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto"> </div>
{publishedPosts.map((post) => (
<Link key={post.slug} href={`/blog/${post.slug}`}> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
<Card hover className="h-full overflow-hidden shadow-md hover:shadow-xl transition-all duration-300"> {publishedPosts.map((post) => (
<Link key={post.slug} href={`/blog/${post.slug}`}>
<Card hover className="h-full overflow-hidden shadow-md hover:shadow-xl transition-all duration-300">
<div className="relative h-56 overflow-hidden"> <div className="relative h-56 overflow-hidden">
<Image <Image
src={post.image} src={post.image}
@@ -102,26 +104,26 @@ export default function BlogPage() {
className="w-full h-full object-cover transition-transform duration-500 hover:scale-110" className="w-full h-full object-cover transition-transform duration-500 hover:scale-110"
/> />
</div> </div>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<Badge variant="info">{post.category}</Badge> <Badge variant="info">{post.category}</Badge>
<span className="text-sm text-gray-500 font-medium">{post.readTime} read</span> <span className="text-sm text-gray-500 font-medium">{post.readTime} read</span>
</div> </div>
<CardTitle className="text-xl leading-tight mb-3">{post.title}</CardTitle> <CardTitle className="text-xl leading-tight mb-3">{post.title}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="pt-0">
<p className="text-gray-600 mb-4 leading-relaxed">{post.excerpt}</p> <p className="text-gray-600 mb-4 leading-relaxed">{post.excerpt}</p>
<div className="flex items-center justify-between pt-4 border-t border-gray-100"> <div className="flex items-center justify-between pt-4 border-t border-gray-100">
<p className="text-sm text-gray-500">{post.date}</p> <p className="text-sm text-gray-500">{post.date}</p>
<span className="text-primary-600 text-sm font-medium">Read more </span> <span className="text-primary-600 text-sm font-medium">Read more </span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</Link> </Link>
))} ))}
</div> </div>
</div> </div>
</div> </div>
</> </>
); );
} }

View File

@@ -1,306 +1,308 @@
import { MetadataRoute } from 'next'; import { MetadataRoute } from 'next';
import { blogPosts } from '../lib/blog-data'; import { blogPosts } from '../lib/blog-data';
import { pillarMeta } from '../lib/pillar-data'; import { REDIRECTED_BLOG_SLUGS } from '../lib/content';
import { authors } from '../lib/author-data'; import { pillarMeta } from '../lib/pillar-data';
import { industryPages } from '../lib/industry-pages'; import { authors } from '../lib/author-data';
import { import { industryPages } from '../lib/industry-pages';
publishedComparisonPages, import {
publishedGuidePages, publishedComparisonPages,
} from '../lib/pseo-published-pages'; publishedGuidePages,
import { publishedUseCases } from '../lib/growth-pages'; } from '../lib/pseo-published-pages';
import { useCasePagesDe } from '../lib/growth-pages-de'; import { publishedUseCases } from '../lib/growth-pages';
import { useCasePagesDe } from '../lib/growth-pages-de';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://www.qrmaster.net'; export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://www.qrmaster.net';
// All free tool slugs
const freeTools = [ // All free tool slugs
'url-qr-code', const freeTools = [
'vcard-qr-code', 'url-qr-code',
'text-qr-code', 'vcard-qr-code',
'email-qr-code', 'text-qr-code',
'sms-qr-code', 'email-qr-code',
'wifi-qr-code', 'sms-qr-code',
'crypto-qr-code', 'wifi-qr-code',
'event-qr-code', 'crypto-qr-code',
'facebook-qr-code', 'event-qr-code',
'instagram-qr-code', 'facebook-qr-code',
'twitter-qr-code', 'instagram-qr-code',
'youtube-qr-code', 'twitter-qr-code',
'whatsapp-qr-code', 'youtube-qr-code',
'tiktok-qr-code', 'whatsapp-qr-code',
'geolocation-qr-code', 'tiktok-qr-code',
'call-qr-code-generator', 'geolocation-qr-code',
'paypal-qr-code', 'call-qr-code-generator',
'zoom-qr-code', 'paypal-qr-code',
'teams-qr-code', 'zoom-qr-code',
'google-review-qr-code', 'teams-qr-code',
'barcode-generator', 'google-review-qr-code',
]; 'barcode-generator',
];
// All blog posts
// Filter out future posts so Google doesn't see them // All blog posts
const blogPages = blogPosts // Filter out future posts so Google doesn't see them
.filter((post) => { const blogPages = blogPosts
const publishDate = post.datePublished .filter((post) => {
? new Date(post.datePublished) if (REDIRECTED_BLOG_SLUGS.has(post.slug)) return false;
: new Date(post.date); const publishDate = post.datePublished
return publishDate <= new Date(); ? new Date(post.datePublished)
}) : new Date(post.date);
.map((post) => ({ return publishDate <= new Date();
url: `${baseUrl}/blog/${post.slug}`, })
// Use updatedAt if available, otherwise dateModified or datePublished .map((post) => ({
lastModified: post.updatedAt url: `${baseUrl}/blog/${post.slug}`,
? new Date(post.updatedAt) // Use updatedAt if available, otherwise dateModified or datePublished
: post.dateModified lastModified: post.updatedAt
? new Date(post.dateModified) ? new Date(post.updatedAt)
: new Date(), : post.dateModified
changeFrequency: 'monthly' as const, ? new Date(post.dateModified)
priority: 0.8, : new Date(),
})); changeFrequency: 'monthly' as const,
priority: 0.8,
const highPriorityTools: Record<string, number> = { }));
'barcode-generator': 0.95,
'google-review-qr-code': 0.9, const highPriorityTools: Record<string, number> = {
}; 'barcode-generator': 0.95,
'google-review-qr-code': 0.9,
const toolLastModified: Record<string, Date> = { };
'barcode-generator': new Date('2026-05-10'),
'vcard-qr-code': new Date('2026-04-27'), const toolLastModified: Record<string, Date> = {
'instagram-qr-code': new Date('2026-04-27'), 'barcode-generator': new Date('2026-05-10'),
'wifi-qr-code': new Date('2026-04-27'), 'vcard-qr-code': new Date('2026-04-27'),
}; 'instagram-qr-code': new Date('2026-04-27'),
'wifi-qr-code': new Date('2026-04-27'),
const toolPages = freeTools.map((slug) => ({ };
url: `${baseUrl}/tools/${slug}`,
lastModified: toolLastModified[slug] ?? new Date('2026-01-01'), const toolPages = freeTools.map((slug) => ({
changeFrequency: url: `${baseUrl}/tools/${slug}`,
slug === 'barcode-generator' ? ('weekly' as const) : ('monthly' as const), lastModified: toolLastModified[slug] ?? new Date('2026-01-01'),
priority: highPriorityTools[slug] ?? 0.8, changeFrequency:
})); slug === 'barcode-generator' ? ('weekly' as const) : ('monthly' as const),
priority: highPriorityTools[slug] ?? 0.8,
// Learn hub and pillar pages }));
const learnPages = [
{ // Learn hub and pillar pages
url: `${baseUrl}/learn`, const learnPages = [
lastModified: new Date(), {
changeFrequency: 'weekly' as const, url: `${baseUrl}/learn`,
priority: 0.9, lastModified: new Date(),
}, changeFrequency: 'weekly' as const,
...pillarMeta.map((pillar) => ({ priority: 0.9,
url: `${baseUrl}/learn/${pillar.key}`, },
lastModified: new Date(), ...pillarMeta.map((pillar) => ({
changeFrequency: 'monthly' as const, url: `${baseUrl}/learn/${pillar.key}`,
priority: 0.8, lastModified: new Date(),
})), changeFrequency: 'monthly' as const,
]; priority: 0.8,
})),
const industryUrls = industryPages.map((industry) => ({ ];
url: `${baseUrl}/qr-code-for/${industry.slug}`,
lastModified: new Date(), const industryUrls = industryPages.map((industry) => ({
changeFrequency: 'monthly' as const, url: `${baseUrl}/qr-code-for/${industry.slug}`,
priority: 0.85, lastModified: new Date(),
})); changeFrequency: 'monthly' as const,
priority: 0.85,
const growthUseCasePages = [ }));
{
url: `${baseUrl}/use-cases`, const growthUseCasePages = [
lastModified: new Date(), {
changeFrequency: 'weekly' as const, url: `${baseUrl}/use-cases`,
priority: 0.9, lastModified: new Date(),
}, changeFrequency: 'weekly' as const,
{ priority: 0.9,
url: `${baseUrl}/qr-code-for`, },
lastModified: new Date(), {
changeFrequency: 'weekly' as const, url: `${baseUrl}/qr-code-for`,
priority: 0.9, lastModified: new Date(),
}, changeFrequency: 'weekly' as const,
...publishedUseCases.map((useCase) => ({ priority: 0.9,
url: `${baseUrl}${useCase.href}`, },
lastModified: new Date(), ...publishedUseCases.map((useCase) => ({
changeFrequency: 'monthly' as const, url: `${baseUrl}${useCase.href}`,
priority: 0.8, lastModified: new Date(),
})), changeFrequency: 'monthly' as const,
// German pages under /de priority: 0.8,
{ })),
url: `${baseUrl}/de/preise`, // German pages under /de
lastModified: new Date(), {
changeFrequency: 'monthly' as const, url: `${baseUrl}/de/preise`,
priority: 0.8, lastModified: new Date(),
}, changeFrequency: 'monthly' as const,
...Object.values(useCasePagesDe).map((useCase) => ({ priority: 0.8,
url: `${baseUrl}${useCase.href}`, },
lastModified: new Date(), ...Object.values(useCasePagesDe).map((useCase) => ({
changeFrequency: 'monthly' as const, url: `${baseUrl}${useCase.href}`,
priority: 0.8, lastModified: new Date(),
})), changeFrequency: 'monthly' as const,
{ priority: 0.8,
url: `${baseUrl}/qr-code-for-marketing-campaigns`, })),
lastModified: new Date(), {
changeFrequency: 'monthly' as const, url: `${baseUrl}/qr-code-for-marketing-campaigns`,
priority: 0.85, lastModified: new Date(),
}, changeFrequency: 'monthly' as const,
]; priority: 0.85,
},
const publishedPseoPages = [ ];
...publishedComparisonPages.map((page) => ({
url: `${baseUrl}${page.canonicalPath}`, const publishedPseoPages = [
lastModified: new Date('2026-06-22'), ...publishedComparisonPages.map((page) => ({
changeFrequency: 'monthly' as const, url: `${baseUrl}${page.canonicalPath}`,
priority: page.intentStage === 'bottom' ? 0.88 : 0.84, lastModified: new Date('2026-06-22'),
})), changeFrequency: 'monthly' as const,
...publishedGuidePages.map((page) => ({ priority: page.intentStage === 'bottom' ? 0.88 : 0.84,
url: `${baseUrl}${page.canonicalPath}`, })),
lastModified: new Date('2026-06-22'), ...publishedGuidePages.map((page) => ({
changeFrequency: 'monthly' as const, url: `${baseUrl}${page.canonicalPath}`,
priority: 0.82, lastModified: new Date('2026-06-22'),
})), changeFrequency: 'monthly' as const,
]; priority: 0.82,
})),
// Author pages ];
const authorPages = authors.map((author) => ({
url: `${baseUrl}/authors/${author.slug}`, // Author pages
lastModified: new Date(), const authorPages = authors.map((author) => ({
changeFrequency: 'monthly' as const, url: `${baseUrl}/authors/${author.slug}`,
priority: 0.7, lastModified: new Date(),
})); changeFrequency: 'monthly' as const,
priority: 0.7,
return [ }));
{
url: baseUrl, return [
lastModified: new Date(), {
changeFrequency: 'weekly', url: baseUrl,
priority: 1.0, lastModified: new Date(),
}, changeFrequency: 'weekly',
{ priority: 1.0,
url: `${baseUrl}/newsletter`, },
lastModified: new Date(), {
changeFrequency: 'monthly', url: `${baseUrl}/newsletter`,
priority: 0.7, lastModified: new Date(),
}, changeFrequency: 'monthly',
{ priority: 0.7,
url: `${baseUrl}/restaurants`, },
lastModified: new Date('2026-04-30'), {
changeFrequency: 'weekly', url: `${baseUrl}/restaurants`,
priority: 0.9, lastModified: new Date('2026-04-30'),
}, changeFrequency: 'weekly',
{ priority: 0.9,
url: `${baseUrl}/qr-code-erstellen`, },
lastModified: new Date(), {
changeFrequency: 'weekly', url: `${baseUrl}/qr-code-erstellen`,
priority: 1.0, lastModified: new Date(),
}, changeFrequency: 'weekly',
{ priority: 1.0,
url: `${baseUrl}/qr-code-tracking`, },
lastModified: new Date('2026-05-10'), {
changeFrequency: 'monthly', url: `${baseUrl}/qr-code-tracking`,
priority: 0.9, lastModified: new Date('2026-05-10'),
}, changeFrequency: 'monthly',
{ priority: 0.9,
url: `${baseUrl}/qr-code-analytics`, },
lastModified: new Date(), {
changeFrequency: 'monthly', url: `${baseUrl}/qr-code-analytics`,
priority: 0.9, lastModified: new Date(),
}, changeFrequency: 'monthly',
{ priority: 0.9,
url: `${baseUrl}/reprint-calculator`, },
lastModified: new Date(), {
changeFrequency: 'monthly', url: `${baseUrl}/reprint-calculator`,
priority: 0.9, lastModified: new Date(),
}, changeFrequency: 'monthly',
{ priority: 0.9,
url: `${baseUrl}/dynamic-qr-code-generator`, },
lastModified: new Date('2026-05-10'), {
changeFrequency: 'weekly', url: `${baseUrl}/dynamic-qr-code-generator`,
priority: 0.95, lastModified: new Date('2026-05-10'),
}, changeFrequency: 'weekly',
{ priority: 0.95,
url: `${baseUrl}/dynamic-barcode-generator`, },
lastModified: new Date(), {
changeFrequency: 'monthly', url: `${baseUrl}/dynamic-barcode-generator`,
priority: 0.9, lastModified: new Date(),
}, changeFrequency: 'monthly',
{ priority: 0.9,
url: `${baseUrl}/bulk-qr-code-generator`, },
lastModified: new Date(), {
changeFrequency: 'monthly', url: `${baseUrl}/bulk-qr-code-generator`,
priority: 0.9, lastModified: new Date(),
}, changeFrequency: 'monthly',
{ priority: 0.9,
url: `${baseUrl}/custom-qr-code-generator`, },
lastModified: new Date(), {
changeFrequency: 'weekly', url: `${baseUrl}/custom-qr-code-generator`,
priority: 0.9, lastModified: new Date(),
}, changeFrequency: 'weekly',
priority: 0.9,
{ },
url: `${baseUrl}/pricing`,
lastModified: new Date(), {
changeFrequency: 'monthly', url: `${baseUrl}/pricing`,
priority: 0.8, lastModified: new Date(),
}, changeFrequency: 'monthly',
{ priority: 0.8,
url: `${baseUrl}/tools`, },
lastModified: new Date(), {
changeFrequency: 'monthly', url: `${baseUrl}/tools`,
priority: 0.9, lastModified: new Date(),
}, changeFrequency: 'monthly',
{ priority: 0.9,
url: `${baseUrl}/features`, },
lastModified: new Date(), {
changeFrequency: 'monthly', url: `${baseUrl}/features`,
priority: 0.8, lastModified: new Date(),
}, changeFrequency: 'monthly',
{ priority: 0.8,
url: `${baseUrl}/faq`, },
lastModified: new Date(), {
changeFrequency: 'monthly', url: `${baseUrl}/faq`,
priority: 0.7, lastModified: new Date(),
}, changeFrequency: 'monthly',
{ priority: 0.7,
url: `${baseUrl}/blog`, },
lastModified: new Date(), {
changeFrequency: 'weekly', url: `${baseUrl}/blog`,
priority: 0.8, lastModified: new Date(),
}, changeFrequency: 'weekly',
priority: 0.8,
{ },
url: `${baseUrl}/privacy`,
lastModified: new Date(), {
changeFrequency: 'yearly', url: `${baseUrl}/privacy`,
priority: 0.4, lastModified: new Date(),
}, changeFrequency: 'yearly',
{ priority: 0.4,
url: `${baseUrl}/contact`, },
lastModified: new Date(), {
changeFrequency: 'yearly', url: `${baseUrl}/contact`,
priority: 0.5, lastModified: new Date(),
}, changeFrequency: 'yearly',
{ priority: 0.5,
url: `${baseUrl}/about`, },
lastModified: new Date(), {
changeFrequency: 'yearly', url: `${baseUrl}/about`,
priority: 0.6, lastModified: new Date(),
}, changeFrequency: 'yearly',
{ priority: 0.6,
url: `${baseUrl}/press`, },
lastModified: new Date(), {
changeFrequency: 'monthly', url: `${baseUrl}/press`,
priority: 0.7, lastModified: new Date(),
}, changeFrequency: 'monthly',
{ priority: 0.7,
url: `${baseUrl}/testimonials`, },
lastModified: new Date(), {
changeFrequency: 'monthly', url: `${baseUrl}/testimonials`,
priority: 0.7, lastModified: new Date(),
}, changeFrequency: 'monthly',
priority: 0.7,
...toolPages, },
...blogPages,
...learnPages, ...toolPages,
...growthUseCasePages, ...blogPages,
...publishedPseoPages, ...learnPages,
...industryUrls, ...growthUseCasePages,
...authorPages, ...publishedPseoPages,
]; ...industryUrls,
} ...authorPages,
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,59 +1,76 @@
import { blogPosts } from "./blog-data"; import { blogPosts } from "./blog-data";
import { authors } from "./author-data"; import { authors } from "./author-data";
import type { BlogPost, PillarKey, AuthorProfile } from "./types"; import type { BlogPost, PillarKey, AuthorProfile } from "./types";
export function getPublishedPosts(): BlogPost[] { /**
const currentDate = new Date(); * Blog slugs that are 301-redirected in next.config.mjs.
return blogPosts.filter(p => { * They must not appear in the blog index, the learn pillars, the sitemap
if (!p.published) return false; * or IndexNow submissions - linking to a redirect source wastes crawl budget
const publishDate = p.datePublished ? new Date(p.datePublished) : new Date(p.date); * and passes link equity through an unnecessary hop.
return publishDate <= currentDate; * Keep this in sync with the redirects() block in next.config.mjs.
}); */
} export const REDIRECTED_BLOG_SLUGS = new Set<string>([
"qr-code-analytics",
export function getPostBySlug(slug: string): BlogPost | undefined { "qr-code-restaurant-menu",
return blogPosts.find(p => p.slug === slug); ]);
}
export function isRedirectedSlug(slug: string): boolean {
export function getPublishedPostBySlug(slug: string): BlogPost | undefined { return REDIRECTED_BLOG_SLUGS.has(slug);
const p = getPostBySlug(slug); }
if (!p?.published) return undefined;
export function getPublishedPosts(): BlogPost[] {
const currentDate = new Date(); const currentDate = new Date();
const publishDate = p.datePublished ? new Date(p.datePublished) : new Date(p.date); return blogPosts.filter(p => {
return publishDate <= currentDate ? p : undefined; if (!p.published) return false;
} if (REDIRECTED_BLOG_SLUGS.has(p.slug)) return false;
const publishDate = p.datePublished ? new Date(p.datePublished) : new Date(p.date);
export function getPostsByPillar(pillar: PillarKey): BlogPost[] { return publishDate <= currentDate;
return getPublishedPosts() });
.filter(p => p.pillar === pillar) }
.sort((a, b) => (new Date(a.datePublished).getTime() < new Date(b.datePublished).getTime() ? 1 : -1));
} export function getPostBySlug(slug: string): BlogPost | undefined {
return blogPosts.find(p => p.slug === slug);
export function getAuthorBySlug(slug: string): AuthorProfile | undefined { }
return authors.find(a => a.slug === slug);
} export function getPublishedPostBySlug(slug: string): BlogPost | undefined {
const p = getPostBySlug(slug);
export function getPostsByAuthor(slug: string): BlogPost[] { if (!p?.published) return undefined;
return getPublishedPosts()
.filter(p => p.authorSlug === slug) const currentDate = new Date();
.sort((a, b) => (new Date(a.datePublished).getTime() < new Date(b.datePublished).getTime() ? 1 : -1)); const publishDate = p.datePublished ? new Date(p.datePublished) : new Date(p.date);
} return publishDate <= currentDate ? p : undefined;
}
export function getRelatedPosts(post: BlogPost, limit = 4): BlogPost[] {
const published = getPublishedPosts(); export function getPostsByPillar(pillar: PillarKey): BlogPost[] {
return getPublishedPosts()
// explicit relatedSlugs first .filter(p => p.pillar === pillar)
const explicit = (post.relatedSlugs ?? []) .sort((a, b) => (new Date(a.datePublished).getTime() < new Date(b.datePublished).getTime() ? 1 : -1));
.map(s => published.find(p => p.slug === s)) }
.filter((p): p is BlogPost => !!p);
export function getAuthorBySlug(slug: string): AuthorProfile | undefined {
if (explicit.length >= limit) return explicit.slice(0, limit); return authors.find(a => a.slug === slug);
}
// fallback: same pillar, not itself, newest
const fallback = published export function getPostsByAuthor(slug: string): BlogPost[] {
.filter(p => p.slug !== post.slug && p.pillar === post.pillar) return getPublishedPosts()
.slice(0, limit - explicit.length); .filter(p => p.authorSlug === slug)
.sort((a, b) => (new Date(a.datePublished).getTime() < new Date(b.datePublished).getTime() ? 1 : -1));
return [...explicit, ...fallback]; }
}
export function getRelatedPosts(post: BlogPost, limit = 4): BlogPost[] {
const published = getPublishedPosts();
// explicit relatedSlugs first
const explicit = (post.relatedSlugs ?? [])
.map(s => published.find(p => p.slug === s))
.filter((p): p is BlogPost => !!p);
if (explicit.length >= limit) return explicit.slice(0, limit);
// fallback: same pillar, not itself, newest
const fallback = published
.filter(p => p.slug !== post.slug && p.pillar === post.pillar)
.slice(0, limit - explicit.length);
return [...explicit, ...fallback];
}

View File

@@ -1,166 +1,169 @@
import axios from 'axios'; import axios from 'axios';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { blogPosts } from '../lib/blog-data'; import { blogPosts } from '../lib/blog-data';
import { pillarMeta } from '../lib/pillar-data'; import { REDIRECTED_BLOG_SLUGS } from './content';
import { authors } from '../lib/author-data'; import { pillarMeta } from '../lib/pillar-data';
import { publishedUseCases } from '../lib/growth-pages'; import { authors } from '../lib/author-data';
import { useCasePagesDe } from '../lib/growth-pages-de'; import { publishedUseCases } from '../lib/growth-pages';
import { industryPages } from '../lib/industry-pages'; import { useCasePagesDe } from '../lib/growth-pages-de';
import { publishedComparisonPages, publishedGuidePages } from '../lib/pseo-published-pages'; import { industryPages } from '../lib/industry-pages';
import { publishedComparisonPages, publishedGuidePages } from '../lib/pseo-published-pages';
dotenv.config();
dotenv.config();
const INDEXNOW_ENDPOINT = 'https://api.indexnow.org/indexnow';
const HOST = 'www.qrmaster.net'; const INDEXNOW_ENDPOINT = 'https://api.indexnow.org/indexnow';
// You need to generate a key from https://www.bing.com/indexnow and place it in your public folder const HOST = 'www.qrmaster.net';
// Key must be set in .env as INDEXNOW_KEY // You need to generate a key from https://www.bing.com/indexnow and place it in your public folder
const KEY = process.env.INDEXNOW_KEY!; // Key must be set in .env as INDEXNOW_KEY
const KEY_LOCATION = `https://${HOST}/${KEY}.txt`; const KEY = process.env.INDEXNOW_KEY!;
const KEY_LOCATION = `https://${HOST}/${KEY}.txt`;
export async function submitToIndexNow(urls: string[]) {
try { export async function submitToIndexNow(urls: string[]) {
const payload = { try {
host: HOST, const payload = {
key: KEY, host: HOST,
keyLocation: KEY_LOCATION, key: KEY,
urlList: urls, keyLocation: KEY_LOCATION,
}; urlList: urls,
};
console.log(`Submitting ${urls.length} URLs to IndexNow...`);
const response = await axios.post(INDEXNOW_ENDPOINT, payload, { console.log(`Submitting ${urls.length} URLs to IndexNow...`);
headers: { const response = await axios.post(INDEXNOW_ENDPOINT, payload, {
'Content-Type': 'application/json', headers: {
}, 'Content-Type': 'application/json',
}); },
});
if (response.status === 200 || response.status === 202) {
console.log('✅ Successfully submitted URLs to IndexNow.'); if (response.status === 200 || response.status === 202) {
} else { console.log('✅ Successfully submitted URLs to IndexNow.');
console.error(`⚠️ IndexNow submission returned status: ${response.status}`); } else {
} console.error(`⚠️ IndexNow submission returned status: ${response.status}`);
} catch (error) { }
if (axios.isAxiosError(error)) { } catch (error) {
console.error('❌ Error submitting to IndexNow:', error.message); if (axios.isAxiosError(error)) {
console.error('Response data:', error.response?.data); console.error('❌ Error submitting to IndexNow:', error.message);
} else { console.error('Response data:', error.response?.data);
console.error('❌ Unknown error submitting to IndexNow:', error); } else {
} console.error('❌ Unknown error submitting to IndexNow:', error);
} }
} }
}
// Function to gather all indexable URLs
export function getAllIndexableUrls(): string[] { // Function to gather all indexable URLs
const baseUrl = `https://${HOST}`; export function getAllIndexableUrls(): string[] {
const baseUrl = `https://${HOST}`;
// Free tools
const freeTools = [ // Free tools
'barcode-generator', // Added as per request const freeTools = [
'url-qr-code', 'vcard-qr-code', 'text-qr-code', 'email-qr-code', 'sms-qr-code', 'barcode-generator', // Added as per request
'wifi-qr-code', 'crypto-qr-code', 'event-qr-code', 'facebook-qr-code', 'url-qr-code', 'vcard-qr-code', 'text-qr-code', 'email-qr-code', 'sms-qr-code',
'instagram-qr-code', 'twitter-qr-code', 'youtube-qr-code', 'whatsapp-qr-code', 'wifi-qr-code', 'crypto-qr-code', 'event-qr-code', 'facebook-qr-code',
'tiktok-qr-code', 'geolocation-qr-code', 'call-qr-code-generator', 'paypal-qr-code', 'instagram-qr-code', 'twitter-qr-code', 'youtube-qr-code', 'whatsapp-qr-code',
'zoom-qr-code', 'teams-qr-code', 'google-review-qr-code', 'tiktok-qr-code', 'geolocation-qr-code', 'call-qr-code-generator', 'paypal-qr-code',
].map(slug => `${baseUrl}/tools/${slug}`); 'zoom-qr-code', 'teams-qr-code', 'google-review-qr-code',
].map(slug => `${baseUrl}/tools/${slug}`);
// Use-case pages (English) and their German twins under /de
const useCasePages = [ // Use-case pages (English) and their German twins under /de
`${baseUrl}/use-cases`, const useCasePages = [
...publishedUseCases.map(uc => `${baseUrl}${uc.href}`), `${baseUrl}/use-cases`,
]; ...publishedUseCases.map(uc => `${baseUrl}${uc.href}`),
const germanPages = [ ];
`${baseUrl}/de/preise`, const germanPages = [
...Object.values(useCasePagesDe).map(uc => `${baseUrl}${uc.href}`), `${baseUrl}/de/preise`,
]; ...Object.values(useCasePagesDe).map(uc => `${baseUrl}${uc.href}`),
];
// Blog posts
const blogPages = blogPosts.map(post => `${baseUrl}/blog/${post.slug}`); // Blog posts
const blogPages = blogPosts
// Main pages (synced with sitemap.ts) .filter(post => !REDIRECTED_BLOG_SLUGS.has(post.slug))
const mainPages = [ .map(post => `${baseUrl}/blog/${post.slug}`);
baseUrl,
`${baseUrl}/about`, // Main pages (synced with sitemap.ts)
`${baseUrl}/contact`, const mainPages = [
`${baseUrl}/press`, baseUrl,
`${baseUrl}/testimonials`, `${baseUrl}/about`,
`${baseUrl}/qr-code-erstellen`, `${baseUrl}/contact`,
`${baseUrl}/qr-code-tracking`, `${baseUrl}/press`,
`${baseUrl}/reprint-calculator`, `${baseUrl}/testimonials`,
`${baseUrl}/dynamic-qr-code-generator`, `${baseUrl}/qr-code-erstellen`,
`${baseUrl}/dynamic-barcode-generator`, `${baseUrl}/qr-code-tracking`,
`${baseUrl}/bulk-qr-code-generator`, `${baseUrl}/reprint-calculator`,
`${baseUrl}/custom-qr-code-generator`, `${baseUrl}/dynamic-qr-code-generator`,
`${baseUrl}/manage-qr-codes`, `${baseUrl}/dynamic-barcode-generator`,
`${baseUrl}/pricing`, `${baseUrl}/bulk-qr-code-generator`,
`${baseUrl}/tools`, `${baseUrl}/custom-qr-code-generator`,
`${baseUrl}/features`, `${baseUrl}/manage-qr-codes`,
`${baseUrl}/faq`, `${baseUrl}/pricing`,
`${baseUrl}/blog`, `${baseUrl}/tools`,
`${baseUrl}/privacy`, `${baseUrl}/features`,
`${baseUrl}/newsletter`, `${baseUrl}/faq`,
`${baseUrl}/cookie-policy`, `${baseUrl}/blog`,
`${baseUrl}/terms`, `${baseUrl}/privacy`,
`${baseUrl}/restaurants`, `${baseUrl}/newsletter`,
`${baseUrl}/qr-code-analytics`, `${baseUrl}/cookie-policy`,
`${baseUrl}/qr-code-print-size-guide`, `${baseUrl}/terms`,
]; `${baseUrl}/restaurants`,
`${baseUrl}/qr-code-analytics`,
// Alternatives & comparison hub pages `${baseUrl}/qr-code-print-size-guide`,
const alternativesPages = [ ];
`${baseUrl}/alternatives`,
`${baseUrl}/alternatives/beaconstac`, // Alternatives & comparison hub pages
`${baseUrl}/alternatives/bitly`, const alternativesPages = [
`${baseUrl}/alternatives/flowcode`, `${baseUrl}/alternatives`,
`${baseUrl}/alternatives/qr-code-generator`, `${baseUrl}/alternatives/beaconstac`,
`${baseUrl}/vs`, `${baseUrl}/alternatives/bitly`,
`${baseUrl}/vs/beaconstac`, `${baseUrl}/alternatives/flowcode`,
]; `${baseUrl}/alternatives/qr-code-generator`,
`${baseUrl}/vs`,
// pSEO comparison & guide pages (published subset) `${baseUrl}/vs/beaconstac`,
const pseoPages = [ ];
...publishedComparisonPages.map(page => `${baseUrl}${page.canonicalPath}`),
...publishedGuidePages.map(page => `${baseUrl}${page.canonicalPath}`), // pSEO comparison & guide pages (published subset)
]; const pseoPages = [
...publishedComparisonPages.map(page => `${baseUrl}${page.canonicalPath}`),
// Learn hub pillars (the /guide/* URLs these replaced are 301'd in next.config.mjs ...publishedGuidePages.map(page => `${baseUrl}${page.canonicalPath}`),
// and must never be submitted to IndexNow - see redirects() there) ];
const guidePages = [
`${baseUrl}/learn/developer`, // Learn hub pillars (the /guide/* URLs these replaced are 301'd in next.config.mjs
`${baseUrl}/learn/basics`, // and must never be submitted to IndexNow - see redirects() there)
`${baseUrl}/learn/tracking`, const guidePages = [
]; `${baseUrl}/learn/developer`,
`${baseUrl}/learn/basics`,
// Industry landing pages `${baseUrl}/learn/tracking`,
const industryHubPages = [ ];
`${baseUrl}/qr-code-for`,
...industryPages.map(industry => `${baseUrl}/qr-code-for/${industry.slug}`), // Industry landing pages
]; const industryHubPages = [
`${baseUrl}/qr-code-for`,
// Learn hub and pillar pages ...industryPages.map(industry => `${baseUrl}/qr-code-for/${industry.slug}`),
const learnPages = [ ];
`${baseUrl}/learn`,
...pillarMeta.map(pillar => `${baseUrl}/learn/${pillar.key}`) // Learn hub and pillar pages
]; const learnPages = [
`${baseUrl}/learn`,
// Author pages ...pillarMeta.map(pillar => `${baseUrl}/learn/${pillar.key}`)
const authorPages = authors.map(author => `${baseUrl}/authors/${author.slug}`); ];
return [ // Author pages
...mainPages, const authorPages = authors.map(author => `${baseUrl}/authors/${author.slug}`);
...freeTools,
...useCasePages, return [
...germanPages, ...mainPages,
...blogPages, ...freeTools,
...learnPages, ...useCasePages,
...authorPages, ...germanPages,
...alternativesPages, ...blogPages,
...pseoPages, ...learnPages,
...guidePages, ...authorPages,
...industryHubPages, ...alternativesPages,
]; ...pseoPages,
} ...guidePages,
...industryHubPages,
// If run directly ];
if (require.main === module) { }
const urls = getAllIndexableUrls();
submitToIndexNow(urls); // If run directly
} if (require.main === module) {
const urls = getAllIndexableUrls();
submitToIndexNow(urls);
}