14 blog post schedule

This commit is contained in:
Timo Knuth
2026-01-26 16:43:47 +01:00
parent 702e2710de
commit 2771faf3ba
44 changed files with 3561 additions and 2174 deletions

17
src/lib/author-data.ts Normal file
View File

@@ -0,0 +1,17 @@
import type { AuthorProfile } from "./types";
export const authors: AuthorProfile[] = [
{
slug: "timo",
name: "Timo Knuth",
role: "Founder, Growth & Product Marketing",
bio: "Building QR Master: dynamic QR management, tracking, and bulk ops for B2B teams.",
image: "/favicon.svg",
sameAs: [
"https://www.linkedin.com/in/qr-master-44b6863a2/",
"https://x.com/TIMO_QRMASTER",
"https://www.instagram.com/qrmaster_net/"
],
knowsAbout: ["Dynamic QR Codes", "Tracking & Analytics", "B2B SaaS", "UTM"]
}
];

File diff suppressed because it is too large Load Diff

55
src/lib/content.ts Normal file
View File

@@ -0,0 +1,55 @@
import { blogPosts } from "./blog-data";
import { authors } from "./author-data";
import type { BlogPost, PillarKey, AuthorProfile } from "./types";
export function getPublishedPosts(): BlogPost[] {
const currentDate = new Date();
return blogPosts.filter(p => {
if (!p.published) return false;
const publishDate = p.datePublished ? new Date(p.datePublished) : new Date(p.date);
return publishDate <= currentDate;
});
}
export function getPostBySlug(slug: string): BlogPost | undefined {
return blogPosts.find(p => p.slug === slug);
}
export function getPublishedPostBySlug(slug: string): BlogPost | undefined {
const p = getPostBySlug(slug);
return p?.published ? p : undefined;
}
export function getPostsByPillar(pillar: PillarKey): BlogPost[] {
return getPublishedPosts()
.filter(p => p.pillar === pillar)
.sort((a, b) => (new Date(a.datePublished).getTime() < new Date(b.datePublished).getTime() ? 1 : -1));
}
export function getAuthorBySlug(slug: string): AuthorProfile | undefined {
return authors.find(a => a.slug === slug);
}
export function getPostsByAuthor(slug: string): BlogPost[] {
return getPublishedPosts()
.filter(p => p.authorSlug === slug)
.sort((a, b) => (new Date(a.datePublished).getTime() < new Date(b.datePublished).getTime() ? 1 : -1));
}
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,14 +1,16 @@
import axios from 'axios';
import dotenv from 'dotenv';
import { blogPostList } from '../lib/blog-data';
import { blogPosts } from '../lib/blog-data';
import { pillarMeta } from '../lib/pillar-data';
import { authors } from '../lib/author-data';
dotenv.config();
const INDEXNOW_ENDPOINT = 'https://api.indexnow.org/indexnow';
const HOST = 'www.qrmaster.net';
// You need to generate a key from https://www.bing.com/indexnow and place it in your public folder
// For now, we'll assume a key exists or is provided via env
const KEY = process.env.INDEXNOW_KEY || 'bb6dfaacf1ed41a880281c426c54ed7c';
// Key must be set in .env as INDEXNOW_KEY
const KEY = process.env.INDEXNOW_KEY!;
const KEY_LOCATION = `https://${HOST}/${KEY}.txt`;
export async function submitToIndexNow(urls: string[]) {
@@ -57,7 +59,7 @@ export function getAllIndexableUrls(): string[] {
].map(slug => `${baseUrl}/tools/${slug}`);
// Blog posts
const blogPages = blogPostList.map(post => `${baseUrl}/blog/${post.slug}`);
const blogPages = blogPosts.map(post => `${baseUrl}/blog/${post.slug}`);
// Main pages (synced with sitemap.ts)
const mainPages = [
@@ -82,7 +84,16 @@ export function getAllIndexableUrls(): string[] {
`${baseUrl}/guide/qr-code-best-practices`,
];
return [...mainPages, ...freeTools, ...blogPages];
// Learn hub and pillar pages
const learnPages = [
`${baseUrl}/learn`,
...pillarMeta.map(pillar => `${baseUrl}/learn/${pillar.key}`)
];
// Author pages
const authorPages = authors.map(author => `${baseUrl}/authors/${author.slug}`);
return [...mainPages, ...freeTools, ...blogPages, ...learnPages, ...authorPages];
}
// If run directly

66
src/lib/pillar-data.ts Normal file
View File

@@ -0,0 +1,66 @@
import type { PillarMeta } from "./types";
export const pillarMeta: PillarMeta[] = [
{
key: "basics",
title: "QR Code Basics",
description: "Foundations, best practices, and comparisons to pick the right QR setup.",
quickAnswer: "QR code basics cover static vs dynamic, formats, sizing, and best practices for reliable scanning. Understanding these fundamentals ensures your QR codes work every time.",
miniFaq: [
{ question: "Static vs dynamic QR?", answer: "Dynamic lets you edit the destination and track scans; static is fixed forever." },
{ question: "Best print format?", answer: "Vector formats like <strong>SVG</strong> or <strong>EPS</strong> are best for professional printing." }
],
order: 1
},
{
key: "tracking",
title: "Tracking & Analytics",
description: "Measure scans, devices, and locations. Build ROI-ready QR campaigns.",
quickAnswer: "Tracking turns QR codes into a measurable marketing channel. Monitor real-time scans, device types, geographic locations, and campaign attribution via UTM parameters.",
miniFaq: [
{ question: "What metrics can I track?", answer: "Track scan count, timestamps, device types, operating systems, geographic locations, and referrer sources in real-time." },
{ question: "How do UTM parameters work?", answer: "Add UTM tags to track campaigns in Google Analytics. QR Master auto-generates UTM parameters for attribution tracking." },
{ question: "Can I export analytics data?", answer: "Yes, export scan data as CSV for custom reporting and integration with your CRM or marketing tools." }
],
order: 2
},
{
key: "use-cases",
title: "Use Cases",
description: "WhatsApp, Instagram, vCard, restaurants, events, and real-world playbooks.",
quickAnswer: "Explore practical guides for specific industries and goals. From digital business cards (vCard) to restaurant menus and event check-ins, see how to deploy QR codes effectively.",
miniFaq: [
{ question: "Best QR code for business cards?", answer: "Use <strong>vCard QR codes</strong> to share contact info that auto-saves to phones. Include name, email, phone, company, and social links." },
{ question: "How to use QR codes for WhatsApp?", answer: "Create a WhatsApp QR that opens a pre-filled chat. Perfect for customer support, sales inquiries, or event registration." },
{ question: "QR codes for restaurant menus?", answer: "Dynamic menu QR codes let you update items and prices without reprinting. Add images, allergen info, and multilingual support." },
{ question: "Event check-in with QR?", answer: "Generate unique QR tickets for each attendee. Scan at entry for instant validation and attendance tracking." }
],
order: 3
},
{
key: "security",
title: "Security",
description: "Quishing prevention and safe QR rollouts.",
quickAnswer: "Security is critical for trust. Learn how to prevent 'Quishing' (QR Phishing), validate links, and ensure your QR code campaigns remain safe for your users.",
miniFaq: [
{ question: "What is Quishing?", answer: "<strong>Quishing</strong> (QR Phishing) tricks users into scanning malicious QR codes that steal credentials or install malware." },
{ question: "How to prevent QR code fraud?", answer: "Use short, branded links. Enable URL preview before redirect. Educate users to check the destination before scanning unknown codes." },
{ question: "Are dynamic QR codes secure?", answer: "Yes, when hosted on trusted platforms with HTTPS, access logs, and link expiration. Avoid free generators with sketchy redirects." },
{ question: "Can QR codes be hacked?", answer: "QR codes themselves can't be hacked, but attackers can overlay fake codes on legitimate ones. Use tamper-proof stickers and regular audits." }
],
order: 4
},
{
key: "developer",
title: "Developer",
description: "API, bulk generation, and automation.",
quickAnswer: "For large-scale operations, use our API or Bulk Generator. Automate QR creation, integrate with your CRM, and manage thousands of codes programmatically.",
miniFaq: [
{ question: "Does QR Master have an API?", answer: "Currently, we do not offer a public API. However, we are working on a developer API for future releases to support automated QR generation." },
{ question: "How to generate QR codes in bulk?", answer: "Use our Bulk Generator tool to upload a CSV file. This feature allows you to create hundreds of QR codes at once for inventory, events, or marketing." },
{ question: "Can I integrate with Zapier?", answer: "Direct Zapier integration is on our roadmap. For now, you can use our Bulk Generator to import data from other tools via CSV." },
{ question: "What file formats are supported?", answer: "We support high-quality downloads in <strong>PNG</strong>, <strong>SVG</strong>, and <strong>PDF</strong>. SVG is recommended for professional printing." }
],
order: 5
}
];

View File

@@ -1,60 +1,44 @@
export interface BreadcrumbItem {
name: string;
url: string;
}
import type { BlogPost, AuthorProfile, PillarMeta } from "./types";
export interface BlogPost {
title: string;
description: string;
slug: string;
author: string;
authorUrl: string;
datePublished: string;
dateModified: string;
image: string;
}
const SITE_URL = "https://www.qrmaster.net";
export interface FAQItem {
question: string;
answer: string;
}
export interface ProductOffer {
name: string;
price: string;
priceCurrency: string;
availability: string;
url: string;
}
export interface HowToStep {
name: string;
text: string;
url?: string;
}
export interface HowToTask {
name: string;
description: string;
steps: HowToStep[];
totalTime?: string;
export function websiteSchema() {
return {
'@context': 'https://schema.org',
'@type': 'WebSite',
'@id': `${SITE_URL}/#website`,
name: 'QR Master',
url: SITE_URL,
inLanguage: 'en',
mainEntityOfPage: SITE_URL,
publisher: {
'@id': `${SITE_URL}/#organization`,
},
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${SITE_URL}/blog?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
};
}
export function organizationSchema() {
return {
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': 'https://www.qrmaster.net/#organization',
'@id': `${SITE_URL}/#organization`,
name: 'QR Master',
alternateName: 'QRMaster',
url: 'https://www.qrmaster.net',
url: SITE_URL,
logo: {
'@type': 'ImageObject',
url: 'https://www.qrmaster.net/static/og-image.png',
url: `${SITE_URL}/static/og-image.png`,
width: 1200,
height: 630,
},
image: 'https://www.qrmaster.net/static/og-image.png',
sameAs: [
'https://twitter.com/qrmaster',
],
@@ -64,139 +48,97 @@ export function organizationSchema() {
email: 'support@qrmaster.net',
availableLanguage: ['en', 'de'],
},
description: 'B2B SaaS platform for dynamic QR code generation with analytics, branding, and bulk generation for enterprise marketing campaigns.',
slogan: 'Dynamic QR codes that work smarter',
foundingDate: '2025',
areaServed: 'Worldwide',
knowsAbout: [
'QR Code Generation',
'Marketing Analytics',
'Campaign Tracking',
'Dynamic QR Codes',
'Bulk QR Generation',
],
hasOfferCatalog: {
'@type': 'OfferCatalog',
name: 'QR Master Plans',
itemListElement: [
{
'@type': 'Offer',
itemOffered: {
'@type': 'SoftwareApplication',
name: 'QR Master Free',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser',
image: 'https://www.qrmaster.net/static/og-image.png',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'EUR',
},
},
},
{
'@type': 'Offer',
itemOffered: {
'@type': 'SoftwareApplication',
name: 'QR Master Pro',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser',
image: 'https://www.qrmaster.net/static/og-image.png',
offers: {
'@type': 'Offer',
price: '9',
priceCurrency: 'EUR',
},
},
},
],
},
};
}
export function websiteSchema() {
return {
'@context': 'https://schema.org',
'@type': 'WebSite',
'@id': 'https://www.qrmaster.net/#website',
name: 'QR Master',
url: 'https://www.qrmaster.net',
inLanguage: 'en',
mainEntityOfPage: 'https://www.qrmaster.net',
publisher: {
'@id': 'https://www.qrmaster.net/#organization',
},
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: 'https://www.qrmaster.net/blog?q={search_term_string}',
},
'query-input': 'required name=search_term_string',
},
};
}
export function breadcrumbSchema(items: BreadcrumbItem[]) {
export function blogPostingSchema(post: BlogPost, author?: AuthorProfile) {
const url = `${SITE_URL}/blog/${post.slug}`;
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
'@id': `https://www.qrmaster.net${items[items.length - 1]?.url}#breadcrumb`,
inLanguage: 'en',
mainEntityOfPage: `https://www.qrmaster.net${items[items.length - 1]?.url}`,
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: `https://www.qrmaster.net${item.url}`,
})),
};
}
export function blogPostingSchema(post: BlogPost) {
return {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
'@id': `https://www.qrmaster.net/blog/${post.slug}#article`,
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.description,
image: post.image,
url,
datePublished: post.datePublished,
dateModified: post.dateModified,
inLanguage: 'en',
mainEntityOfPage: `https://www.qrmaster.net/blog/${post.slug}`,
author: {
'@type': 'Person',
name: post.author,
url: post.authorUrl,
},
dateModified: post.dateModified || post.datePublished,
image: post.heroImage ? `${SITE_URL}${post.heroImage}` : undefined,
author: author
? {
"@type": "Person",
name: author.name,
url: `${SITE_URL}/authors/${author.slug}`,
sameAs: author.sameAs ?? undefined,
knowsAbout: author.knowsAbout ?? undefined
}
: {
"@type": "Organization",
name: "QR Master"
},
publisher: {
'@type': 'Organization',
name: 'QR Master',
url: 'https://www.qrmaster.net',
"@type": "Organization",
name: "QR Master",
url: SITE_URL,
logo: {
'@type': 'ImageObject',
url: 'https://www.qrmaster.net/static/og-image.png',
width: 1200,
height: 630,
},
url: `${SITE_URL}/static/og-image.png`,
}
},
isPartOf: {
'@type': 'Blog',
'@id': 'https://www.qrmaster.net/blog#blog',
'@id': `${SITE_URL}/blog#blog`,
name: 'QR Master Blog',
url: 'https://www.qrmaster.net/blog',
url: `${SITE_URL}/blog`,
},
};
}
export function faqPageSchema(faqs: FAQItem[]) {
export function howToSchema(post: BlogPost, author?: AuthorProfile) {
const url = `${SITE_URL}/blog/${post.slug}`;
const steps = (post.keySteps ?? []).map((text, idx) => ({
"@type": "HowToStep",
position: idx + 1,
name: `Step ${idx + 1}`,
text
}));
return {
"@context": "https://schema.org",
"@type": "HowTo",
name: post.title,
description: post.description,
url: `${url}#howto`,
step: steps,
author: author
? { "@type": "Person", name: author.name, url: `${SITE_URL}/authors/${author.slug}` }
: undefined
};
}
export function pillarPageSchema(meta: PillarMeta, posts: BlogPost[]) {
const url = `${SITE_URL}/learn/${meta.key}`;
return {
"@context": "https://schema.org",
"@type": "WebPage",
name: meta.title,
description: meta.description,
url,
mainEntity: {
"@type": "ItemList",
itemListElement: posts.map((p, i) => ({
"@type": "ListItem",
position: i + 1,
url: `${SITE_URL}/blog/${p.slug}`,
name: p.title
}))
}
};
}
export function faqPageSchema(faqs: { question: string, answer: string }[]) {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
'@id': 'https://www.qrmaster.net/faq#faqpage',
inLanguage: 'en',
mainEntityOfPage: 'https://www.qrmaster.net/faq',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
@@ -208,76 +150,97 @@ export function faqPageSchema(faqs: FAQItem[]) {
};
}
export function productSchema(product: { name: string; description: string; offers: ProductOffer[] }) {
export function breadcrumbSchema(items: { name: string; url: string }[]) {
return {
'@context': 'https://schema.org',
'@type': 'Product',
'@id': 'https://www.qrmaster.net/pricing#product',
name: product.name,
description: product.description,
inLanguage: 'en',
mainEntityOfPage: 'https://www.qrmaster.net/pricing',
brand: {
'@type': 'Organization',
name: 'QR Master',
},
offers: product.offers.map((offer) => ({
'@type': 'Offer',
name: offer.name,
price: offer.price,
priceCurrency: offer.priceCurrency,
availability: offer.availability,
url: offer.url,
})),
};
}
export function howToSchema(task: HowToTask) {
return {
'@context': 'https://schema.org',
'@type': 'HowTo',
'@id': `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}#howto`,
name: task.name,
description: task.description,
inLanguage: 'en',
mainEntityOfPage: `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}`,
totalTime: task.totalTime || 'PT5M',
step: task.steps.map((step, index) => ({
'@type': 'HowToStep',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: step.name,
text: step.text,
url: step.url,
name: item.name,
item: item.url.startsWith('http') ? item.url : `https://www.qrmaster.net${item.url}`,
})),
};
}
export function articleSchema(article: { headline: string; description: string; image: string; datePublished: string; dateModified: string; author: string; url: string }) {
export function softwareApplicationSchema() {
return {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'QR Master',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'EUR'
},
publisher: {
'@id': `${SITE_URL}/#organization`,
}
};
}
export function authorPageSchema(author: AuthorProfile, posts?: BlogPost[]) {
const url = `${SITE_URL}/authors/${author.slug}`;
return {
'@context': 'https://schema.org',
'@type': 'ProfilePage',
mainEntity: {
'@type': 'Person',
'@id': url,
name: author.name,
jobTitle: author.role,
description: author.bio,
image: author.image ? `${SITE_URL}${author.image}` : undefined,
sameAs: author.sameAs?.length ? author.sameAs : undefined,
knowsAbout: author.knowsAbout?.length ? author.knowsAbout : undefined,
url,
},
about: posts?.length
? {
'@type': 'ItemList',
itemListElement: posts.map((p, i) => ({
'@type': 'ListItem',
position: i + 1,
url: `${SITE_URL}/blog/${p.slug}`,
name: p.title,
})),
}
: undefined,
};
}
export function articleSchema(params: {
headline: string;
description: string;
image?: string;
datePublished: string;
dateModified?: string;
author: string;
url?: string;
}) {
return {
'@context': 'https://schema.org',
'@type': 'Article',
'@id': `${article.url}#article`,
headline: article.headline,
description: article.description,
image: article.image,
datePublished: article.datePublished,
dateModified: article.dateModified,
inLanguage: 'en',
mainEntityOfPage: article.url,
headline: params.headline,
description: params.description,
image: params.image,
datePublished: params.datePublished,
dateModified: params.dateModified || params.datePublished,
author: {
'@type': 'Person',
name: article.author,
'@type': 'Organization',
name: params.author,
},
publisher: {
'@type': 'Organization',
name: 'QR Master',
url: 'https://www.qrmaster.net',
url: SITE_URL,
logo: {
'@type': 'ImageObject',
url: 'https://www.qrmaster.net/static/og-image.png',
width: 1200,
height: 630,
url: `${SITE_URL}/static/og-image.png`,
},
},
url: params.url,
};
}

67
src/lib/types.ts Normal file
View File

@@ -0,0 +1,67 @@
export type PillarKey = "basics" | "tracking" | "use-cases" | "security" | "developer";
export type FAQItem = {
question: string;
answer: string; // allow HTML or plain
};
export type BlogPost = {
slug: string;
title: string;
excerpt: string; // kept for backward compatibility if needed, maps to description
description: string;
date: string; // display string "January 29, 2026"
readTime: string;
category: string; // display label
image: string;
imageAlt: string;
heroImage?: string;
// Architecture
pillar: PillarKey;
published: boolean;
datePublished: string; // ISO: "2026-02-01"
dateModified: string; // ISO
publishDate?: string; // User-provided alternate date field
updatedAt?: string; // User-provided alternate date field
authorSlug: string;
// SEO
keywords?: string[];
// AEO blocks
quickAnswer: string; // HTML or text
keySteps?: string[]; // plain
faq?: FAQItem[];
relatedSlugs?: string[];
// 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)
// Let's use 'content' to minimize refactor friction if existing code uses 'content',
// but the spec said 'contentHtml'. I will use 'content' to match the existing file structure
// which uses 'content' property, or I can map it.
// Existing: 'content'. Spec: 'contentHtml'.
// I will use 'content' to avoid breaking changes in other files I might not touch immediately,
// or I should just follow the spec strictly.
// The spec is "Final Spec v2", so I'll add 'contentHtml' but also keep 'content'
// or just rename. Let's use 'content' as the key to support existing code calling .content
};
export type AuthorProfile = {
slug: string;
name: string;
role: string;
bio: string; // HTML or text
image?: string; // "/authors/max.png"
sameAs?: string[]; // LinkedIn/GitHub/etc.
knowsAbout?: string[]; // ["QR codes", "Analytics", ...]
};
export type PillarMeta = {
key: PillarKey;
title: string;
description: string;
quickAnswer: string; // short definition (text/HTML)
miniFaq?: FAQItem[];
order: number;
};