feat: Add marketing and use case pages, their data structures, and supporting UI components.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import Script from "next/script";
|
||||
import { getPublishedPostBySlug, getAuthorBySlug, getRelatedPosts, getPublishedPosts } from "@/lib/content";
|
||||
import { AnswerBox } from "@/components/aeo/AnswerBox";
|
||||
import { StepList } from "@/components/aeo/StepList";
|
||||
@@ -72,17 +71,17 @@ export default function BlogPostPage({ params }: { params: { slug: string } }) {
|
||||
|
||||
return (
|
||||
<main className="container mx-auto max-w-4xl py-12 px-4">
|
||||
<Script id="ld-blogposting" type="application/ld+json" strategy="afterInteractive"
|
||||
<script type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||
{howtoLd && (
|
||||
<Script id="ld-howto" type="application/ld+json" strategy="afterInteractive"
|
||||
<script type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(howtoLd) }} />
|
||||
)}
|
||||
{faqLd && (
|
||||
<Script id="ld-faq" type="application/ld+json" strategy="afterInteractive"
|
||||
<script type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }} />
|
||||
)}
|
||||
<Script id="ld-breadcrumb" type="application/ld+json" strategy="afterInteractive"
|
||||
<script type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbLd) }} />
|
||||
|
||||
<header className="space-y-6 text-center max-w-3xl mx-auto mb-10">
|
||||
|
||||
145
src/app/(main)/(marketing)/blog/[slug]/raw/route.ts
Normal file
145
src/app/(main)/(marketing)/blog/[slug]/raw/route.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { getPublishedPostBySlug } from '@/lib/content';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
const RAW_ENABLED_SLUGS = new Set([
|
||||
'dynamic-vs-static-qr-codes',
|
||||
'qr-code-small-business',
|
||||
'qr-code-tracking-guide-2025',
|
||||
'utm-parameter-qr-codes',
|
||||
'trackable-qr-codes',
|
||||
]);
|
||||
|
||||
function decodeHtmlEntities(text: string): string {
|
||||
return text
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/—/g, '--')
|
||||
.replace(/–/g, '-')
|
||||
.replace(/…/g, '...')
|
||||
.replace(/'/g, "'")
|
||||
.replace(///g, '/')
|
||||
.replace(/&#(\d+);/g, (_, code) => {
|
||||
const value = Number.parseInt(code, 10);
|
||||
return Number.isNaN(value) ? '' : String.fromCharCode(value);
|
||||
});
|
||||
}
|
||||
|
||||
function cleanHtmlToText(html: string): string {
|
||||
const normalized = html
|
||||
.replace(/<div\b[^>]*class=(['"])[^'"]*post-metadata[^'"]*\1[^>]*>[\s\S]*?<\/div>/gi, '')
|
||||
.replace(/<div\b[^>]*class=(['"])[^'"]*blog-content[^'"]*\1[^>]*>/gi, '')
|
||||
.replace(/<\/div>\s*$/i, '');
|
||||
|
||||
const withLinks = normalized.replace(
|
||||
/<a\b[^>]*href=(['"])(.*?)\1[^>]*>([\s\S]*?)<\/a>/gi,
|
||||
(_, __, href: string, text: string) => `[${cleanHtmlToText(text)}](${href})`,
|
||||
);
|
||||
|
||||
const structured = withLinks
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<li\b[^>]*>/gi, '- ')
|
||||
.replace(/<\/li>/gi, '\n')
|
||||
.replace(/<h([1-6])\b[^>]*>/gi, (_, level: string) => `${'#'.repeat(Number.parseInt(level, 10))} `)
|
||||
.replace(/<\/h[1-6]>/gi, '\n\n')
|
||||
.replace(/<\/p>/gi, '\n\n')
|
||||
.replace(/<\/div>/gi, '\n\n')
|
||||
.replace(/<\/section>/gi, '\n\n')
|
||||
.replace(/<\/ul>/gi, '\n')
|
||||
.replace(/<\/ol>/gi, '\n');
|
||||
|
||||
const stripped = sanitizeHtml(structured, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: {},
|
||||
});
|
||||
|
||||
return decodeHtmlEntities(stripped)
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.replace(/[ \t]+\n/g, '\n')
|
||||
.replace(/\n[ \t]+/g, '\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function renderRawPost(slug: string): string | null {
|
||||
if (!RAW_ENABLED_SLUGS.has(slug)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const post = getPublishedPostBySlug(slug);
|
||||
if (!post) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sections: string[] = [
|
||||
`# ${post.title}`,
|
||||
'',
|
||||
post.description,
|
||||
'',
|
||||
`Canonical URL: https://www.qrmaster.net/blog/${post.slug}`,
|
||||
`Published: ${post.datePublished}`,
|
||||
`Updated: ${post.dateModified || post.updatedAt || post.datePublished}`,
|
||||
];
|
||||
|
||||
if (post.quickAnswer) {
|
||||
sections.push('', '## Quick Answer', '', cleanHtmlToText(post.quickAnswer));
|
||||
}
|
||||
|
||||
if (post.keySteps?.length) {
|
||||
sections.push('', '## Steps', '', ...post.keySteps.map((step, index) => `${index + 1}. ${step}`));
|
||||
}
|
||||
|
||||
const mainText = cleanHtmlToText(post.content);
|
||||
if (mainText) {
|
||||
sections.push('', '## Article', '', mainText);
|
||||
}
|
||||
|
||||
if (post.faq?.length) {
|
||||
sections.push('', '## FAQ', '');
|
||||
for (const item of post.faq) {
|
||||
sections.push(`Q: ${cleanHtmlToText(item.question)}`);
|
||||
sections.push(`A: ${cleanHtmlToText(item.answer)}`, '');
|
||||
}
|
||||
if (sections[sections.length - 1] === '') {
|
||||
sections.pop();
|
||||
}
|
||||
}
|
||||
|
||||
if (post.sources?.length) {
|
||||
sections.push('', '## Sources', '');
|
||||
for (const source of post.sources) {
|
||||
const accessDate = source.accessDate ? ` (accessed ${source.accessDate})` : '';
|
||||
sections.push(`- ${source.name}: ${source.url}${accessDate}`);
|
||||
}
|
||||
}
|
||||
|
||||
return `${sections.join('\n').trim()}\n`;
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: { slug: string } },
|
||||
) {
|
||||
const content = renderRawPost(params.slug);
|
||||
|
||||
if (!content) {
|
||||
return new Response('Not Found', {
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'X-Robots-Tag': 'noindex, nofollow',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(content, {
|
||||
headers: {
|
||||
'Content-Type': 'text/markdown; charset=utf-8',
|
||||
'X-Robots-Tag': 'noindex, nofollow',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user