14 blog post schedule
This commit is contained in:
17
src/lib/author-data.ts
Normal file
17
src/lib/author-data.ts
Normal 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"]
|
||||
}
|
||||
];
|
||||
3817
src/lib/blog-data.ts
3817
src/lib/blog-data.ts
File diff suppressed because it is too large
Load Diff
55
src/lib/content.ts
Normal file
55
src/lib/content.ts
Normal 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];
|
||||
}
|
||||
@@ -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
66
src/lib/pillar-data.ts
Normal 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
|
||||
}
|
||||
];
|
||||
@@ -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
67
src/lib/types.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user