SEO blog post V2
This commit is contained in:
9039
src/lib/blog-data.ts
9039
src/lib/blog-data.ts
File diff suppressed because it is too large
Load Diff
@@ -1,59 +1,76 @@
|
||||
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);
|
||||
if (!p?.published) return undefined;
|
||||
|
||||
const currentDate = new Date();
|
||||
const publishDate = p.datePublished ? new Date(p.datePublished) : new Date(p.date);
|
||||
return publishDate <= currentDate ? 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];
|
||||
}
|
||||
import { blogPosts } from "./blog-data";
|
||||
import { authors } from "./author-data";
|
||||
import type { BlogPost, PillarKey, AuthorProfile } from "./types";
|
||||
|
||||
/**
|
||||
* Blog slugs that are 301-redirected in next.config.mjs.
|
||||
* They must not appear in the blog index, the learn pillars, the sitemap
|
||||
* or IndexNow submissions - linking to a redirect source wastes crawl budget
|
||||
* and passes link equity through an unnecessary hop.
|
||||
* Keep this in sync with the redirects() block in next.config.mjs.
|
||||
*/
|
||||
export const REDIRECTED_BLOG_SLUGS = new Set<string>([
|
||||
"qr-code-analytics",
|
||||
"qr-code-restaurant-menu",
|
||||
]);
|
||||
|
||||
export function isRedirectedSlug(slug: string): boolean {
|
||||
return REDIRECTED_BLOG_SLUGS.has(slug);
|
||||
}
|
||||
|
||||
export function getPublishedPosts(): BlogPost[] {
|
||||
const currentDate = new Date();
|
||||
return blogPosts.filter(p => {
|
||||
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);
|
||||
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);
|
||||
if (!p?.published) return undefined;
|
||||
|
||||
const currentDate = new Date();
|
||||
const publishDate = p.datePublished ? new Date(p.datePublished) : new Date(p.date);
|
||||
return publishDate <= currentDate ? 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,166 +1,169 @@
|
||||
import axios from 'axios';
|
||||
import dotenv from 'dotenv';
|
||||
import { blogPosts } from '../lib/blog-data';
|
||||
import { pillarMeta } from '../lib/pillar-data';
|
||||
import { authors } from '../lib/author-data';
|
||||
import { publishedUseCases } from '../lib/growth-pages';
|
||||
import { useCasePagesDe } from '../lib/growth-pages-de';
|
||||
import { industryPages } from '../lib/industry-pages';
|
||||
import { publishedComparisonPages, publishedGuidePages } from '../lib/pseo-published-pages';
|
||||
|
||||
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
|
||||
// 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[]) {
|
||||
try {
|
||||
const payload = {
|
||||
host: HOST,
|
||||
key: KEY,
|
||||
keyLocation: KEY_LOCATION,
|
||||
urlList: urls,
|
||||
};
|
||||
|
||||
console.log(`Submitting ${urls.length} URLs to IndexNow...`);
|
||||
const response = await axios.post(INDEXNOW_ENDPOINT, payload, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 200 || response.status === 202) {
|
||||
console.log('✅ Successfully submitted URLs to IndexNow.');
|
||||
} else {
|
||||
console.error(`⚠️ IndexNow submission returned status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error('❌ Error submitting to IndexNow:', error.message);
|
||||
console.error('Response data:', error.response?.data);
|
||||
} else {
|
||||
console.error('❌ Unknown error submitting to IndexNow:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to gather all indexable URLs
|
||||
export function getAllIndexableUrls(): string[] {
|
||||
const baseUrl = `https://${HOST}`;
|
||||
|
||||
// Free tools
|
||||
const freeTools = [
|
||||
'barcode-generator', // Added as per request
|
||||
'url-qr-code', 'vcard-qr-code', 'text-qr-code', 'email-qr-code', 'sms-qr-code',
|
||||
'wifi-qr-code', 'crypto-qr-code', 'event-qr-code', 'facebook-qr-code',
|
||||
'instagram-qr-code', 'twitter-qr-code', 'youtube-qr-code', 'whatsapp-qr-code',
|
||||
'tiktok-qr-code', 'geolocation-qr-code', 'call-qr-code-generator', 'paypal-qr-code',
|
||||
'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 = [
|
||||
`${baseUrl}/use-cases`,
|
||||
...publishedUseCases.map(uc => `${baseUrl}${uc.href}`),
|
||||
];
|
||||
const germanPages = [
|
||||
`${baseUrl}/de/preise`,
|
||||
...Object.values(useCasePagesDe).map(uc => `${baseUrl}${uc.href}`),
|
||||
];
|
||||
|
||||
// Blog posts
|
||||
const blogPages = blogPosts.map(post => `${baseUrl}/blog/${post.slug}`);
|
||||
|
||||
// Main pages (synced with sitemap.ts)
|
||||
const mainPages = [
|
||||
baseUrl,
|
||||
`${baseUrl}/about`,
|
||||
`${baseUrl}/contact`,
|
||||
`${baseUrl}/press`,
|
||||
`${baseUrl}/testimonials`,
|
||||
`${baseUrl}/qr-code-erstellen`,
|
||||
`${baseUrl}/qr-code-tracking`,
|
||||
`${baseUrl}/reprint-calculator`,
|
||||
`${baseUrl}/dynamic-qr-code-generator`,
|
||||
`${baseUrl}/dynamic-barcode-generator`,
|
||||
`${baseUrl}/bulk-qr-code-generator`,
|
||||
`${baseUrl}/custom-qr-code-generator`,
|
||||
`${baseUrl}/manage-qr-codes`,
|
||||
`${baseUrl}/pricing`,
|
||||
`${baseUrl}/tools`,
|
||||
`${baseUrl}/features`,
|
||||
`${baseUrl}/faq`,
|
||||
`${baseUrl}/blog`,
|
||||
`${baseUrl}/privacy`,
|
||||
`${baseUrl}/newsletter`,
|
||||
`${baseUrl}/cookie-policy`,
|
||||
`${baseUrl}/terms`,
|
||||
`${baseUrl}/restaurants`,
|
||||
`${baseUrl}/qr-code-analytics`,
|
||||
`${baseUrl}/qr-code-print-size-guide`,
|
||||
];
|
||||
|
||||
// Alternatives & comparison hub pages
|
||||
const alternativesPages = [
|
||||
`${baseUrl}/alternatives`,
|
||||
`${baseUrl}/alternatives/beaconstac`,
|
||||
`${baseUrl}/alternatives/bitly`,
|
||||
`${baseUrl}/alternatives/flowcode`,
|
||||
`${baseUrl}/alternatives/qr-code-generator`,
|
||||
`${baseUrl}/vs`,
|
||||
`${baseUrl}/vs/beaconstac`,
|
||||
];
|
||||
|
||||
// pSEO comparison & guide pages (published subset)
|
||||
const pseoPages = [
|
||||
...publishedComparisonPages.map(page => `${baseUrl}${page.canonicalPath}`),
|
||||
...publishedGuidePages.map(page => `${baseUrl}${page.canonicalPath}`),
|
||||
];
|
||||
|
||||
// Learn hub pillars (the /guide/* URLs these replaced are 301'd in next.config.mjs
|
||||
// and must never be submitted to IndexNow - see redirects() there)
|
||||
const guidePages = [
|
||||
`${baseUrl}/learn/developer`,
|
||||
`${baseUrl}/learn/basics`,
|
||||
`${baseUrl}/learn/tracking`,
|
||||
];
|
||||
|
||||
// Industry landing pages
|
||||
const industryHubPages = [
|
||||
`${baseUrl}/qr-code-for`,
|
||||
...industryPages.map(industry => `${baseUrl}/qr-code-for/${industry.slug}`),
|
||||
];
|
||||
|
||||
// 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,
|
||||
...useCasePages,
|
||||
...germanPages,
|
||||
...blogPages,
|
||||
...learnPages,
|
||||
...authorPages,
|
||||
...alternativesPages,
|
||||
...pseoPages,
|
||||
...guidePages,
|
||||
...industryHubPages,
|
||||
];
|
||||
}
|
||||
|
||||
// If run directly
|
||||
if (require.main === module) {
|
||||
const urls = getAllIndexableUrls();
|
||||
submitToIndexNow(urls);
|
||||
}
|
||||
import axios from 'axios';
|
||||
import dotenv from 'dotenv';
|
||||
import { blogPosts } from '../lib/blog-data';
|
||||
import { REDIRECTED_BLOG_SLUGS } from './content';
|
||||
import { pillarMeta } from '../lib/pillar-data';
|
||||
import { authors } from '../lib/author-data';
|
||||
import { publishedUseCases } from '../lib/growth-pages';
|
||||
import { useCasePagesDe } from '../lib/growth-pages-de';
|
||||
import { industryPages } from '../lib/industry-pages';
|
||||
import { publishedComparisonPages, publishedGuidePages } from '../lib/pseo-published-pages';
|
||||
|
||||
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
|
||||
// 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[]) {
|
||||
try {
|
||||
const payload = {
|
||||
host: HOST,
|
||||
key: KEY,
|
||||
keyLocation: KEY_LOCATION,
|
||||
urlList: urls,
|
||||
};
|
||||
|
||||
console.log(`Submitting ${urls.length} URLs to IndexNow...`);
|
||||
const response = await axios.post(INDEXNOW_ENDPOINT, payload, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 200 || response.status === 202) {
|
||||
console.log('✅ Successfully submitted URLs to IndexNow.');
|
||||
} else {
|
||||
console.error(`⚠️ IndexNow submission returned status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error('❌ Error submitting to IndexNow:', error.message);
|
||||
console.error('Response data:', error.response?.data);
|
||||
} else {
|
||||
console.error('❌ Unknown error submitting to IndexNow:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to gather all indexable URLs
|
||||
export function getAllIndexableUrls(): string[] {
|
||||
const baseUrl = `https://${HOST}`;
|
||||
|
||||
// Free tools
|
||||
const freeTools = [
|
||||
'barcode-generator', // Added as per request
|
||||
'url-qr-code', 'vcard-qr-code', 'text-qr-code', 'email-qr-code', 'sms-qr-code',
|
||||
'wifi-qr-code', 'crypto-qr-code', 'event-qr-code', 'facebook-qr-code',
|
||||
'instagram-qr-code', 'twitter-qr-code', 'youtube-qr-code', 'whatsapp-qr-code',
|
||||
'tiktok-qr-code', 'geolocation-qr-code', 'call-qr-code-generator', 'paypal-qr-code',
|
||||
'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 = [
|
||||
`${baseUrl}/use-cases`,
|
||||
...publishedUseCases.map(uc => `${baseUrl}${uc.href}`),
|
||||
];
|
||||
const germanPages = [
|
||||
`${baseUrl}/de/preise`,
|
||||
...Object.values(useCasePagesDe).map(uc => `${baseUrl}${uc.href}`),
|
||||
];
|
||||
|
||||
// Blog posts
|
||||
const blogPages = blogPosts
|
||||
.filter(post => !REDIRECTED_BLOG_SLUGS.has(post.slug))
|
||||
.map(post => `${baseUrl}/blog/${post.slug}`);
|
||||
|
||||
// Main pages (synced with sitemap.ts)
|
||||
const mainPages = [
|
||||
baseUrl,
|
||||
`${baseUrl}/about`,
|
||||
`${baseUrl}/contact`,
|
||||
`${baseUrl}/press`,
|
||||
`${baseUrl}/testimonials`,
|
||||
`${baseUrl}/qr-code-erstellen`,
|
||||
`${baseUrl}/qr-code-tracking`,
|
||||
`${baseUrl}/reprint-calculator`,
|
||||
`${baseUrl}/dynamic-qr-code-generator`,
|
||||
`${baseUrl}/dynamic-barcode-generator`,
|
||||
`${baseUrl}/bulk-qr-code-generator`,
|
||||
`${baseUrl}/custom-qr-code-generator`,
|
||||
`${baseUrl}/manage-qr-codes`,
|
||||
`${baseUrl}/pricing`,
|
||||
`${baseUrl}/tools`,
|
||||
`${baseUrl}/features`,
|
||||
`${baseUrl}/faq`,
|
||||
`${baseUrl}/blog`,
|
||||
`${baseUrl}/privacy`,
|
||||
`${baseUrl}/newsletter`,
|
||||
`${baseUrl}/cookie-policy`,
|
||||
`${baseUrl}/terms`,
|
||||
`${baseUrl}/restaurants`,
|
||||
`${baseUrl}/qr-code-analytics`,
|
||||
`${baseUrl}/qr-code-print-size-guide`,
|
||||
];
|
||||
|
||||
// Alternatives & comparison hub pages
|
||||
const alternativesPages = [
|
||||
`${baseUrl}/alternatives`,
|
||||
`${baseUrl}/alternatives/beaconstac`,
|
||||
`${baseUrl}/alternatives/bitly`,
|
||||
`${baseUrl}/alternatives/flowcode`,
|
||||
`${baseUrl}/alternatives/qr-code-generator`,
|
||||
`${baseUrl}/vs`,
|
||||
`${baseUrl}/vs/beaconstac`,
|
||||
];
|
||||
|
||||
// pSEO comparison & guide pages (published subset)
|
||||
const pseoPages = [
|
||||
...publishedComparisonPages.map(page => `${baseUrl}${page.canonicalPath}`),
|
||||
...publishedGuidePages.map(page => `${baseUrl}${page.canonicalPath}`),
|
||||
];
|
||||
|
||||
// Learn hub pillars (the /guide/* URLs these replaced are 301'd in next.config.mjs
|
||||
// and must never be submitted to IndexNow - see redirects() there)
|
||||
const guidePages = [
|
||||
`${baseUrl}/learn/developer`,
|
||||
`${baseUrl}/learn/basics`,
|
||||
`${baseUrl}/learn/tracking`,
|
||||
];
|
||||
|
||||
// Industry landing pages
|
||||
const industryHubPages = [
|
||||
`${baseUrl}/qr-code-for`,
|
||||
...industryPages.map(industry => `${baseUrl}/qr-code-for/${industry.slug}`),
|
||||
];
|
||||
|
||||
// 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,
|
||||
...useCasePages,
|
||||
...germanPages,
|
||||
...blogPages,
|
||||
...learnPages,
|
||||
...authorPages,
|
||||
...alternativesPages,
|
||||
...pseoPages,
|
||||
...guidePages,
|
||||
...industryHubPages,
|
||||
];
|
||||
}
|
||||
|
||||
// If run directly
|
||||
if (require.main === module) {
|
||||
const urls = getAllIndexableUrls();
|
||||
submitToIndexNow(urls);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user