From 2698b51b94729f44c6a7773e8efd48e58921a365 Mon Sep 17 00:00:00 2001 From: Zack Proser Date: Sat, 17 May 2025 20:03:11 -0400 Subject: [PATCH] feat: generate comparison pages dynamically --- .gitignore | 1 + package.json | 2 +- src/app/comparisons/[comparison]/page.tsx | 40 ++++++++++++++++++++ src/app/sitemap.js | 15 ++++++++ src/lib/comparisons.ts | 46 +++++++++++------------ src/lib/getTools.js | 3 +- 6 files changed, 80 insertions(+), 27 deletions(-) create mode 100644 src/app/comparisons/[comparison]/page.tsx diff --git a/.gitignore b/.gitignore index 00e6126a..8bf294e9 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ metadata-report* # Generated pages (excluding page files) src/app/comparisons/* +!src/app/comparisons/**/ !src/app/comparisons/**/page.tsx !src/app/comparisons/**/page.jsx !src/app/comparisons/**/page.mdx diff --git a/package.json b/package.json index 122b5c3b..2d0a42d0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "packageManager": "pnpm@10.8.1", "scripts": { "dev": "concurrently \"next dev\" \"pnpm stripe:webhook\"", - "prebuild": "node scripts/check-metadata.js && node scripts/generate-collections.js && node scripts/create-vector-db-comparison-posts.js && node scripts/create-ai-assisted-dev-tools-comparison-post.js && node scripts/create-ai-assisted-dev-tools-comparison-pages.js", + "prebuild": "node scripts/check-metadata.js && node scripts/generate-collections.js && node scripts/create-vector-db-comparison-posts.js && node scripts/create-ai-assisted-dev-tools-comparison-post.js", "build": "npm run prebuild && prisma generate && (prisma migrate deploy || echo 'Database migration failed, continuing with build...') && NODE_OPTIONS=--max-old-space-size=6144 next build", "build-no-db": "npm run prebuild && prisma generate && NODE_OPTIONS=--max-old-space-size=6144 next build", "build-with-tests": "npm run test && npm run prebuild && prisma generate && prisma migrate deploy && NODE_OPTIONS=--max-old-space-size=6144 next build", diff --git a/src/app/comparisons/[comparison]/page.tsx b/src/app/comparisons/[comparison]/page.tsx new file mode 100644 index 00000000..0588da35 --- /dev/null +++ b/src/app/comparisons/[comparison]/page.tsx @@ -0,0 +1,40 @@ +import { Metadata } from 'next' +import { getTools, getCategories } from '@/lib/getTools' +import ComparisonPageLayout from '@/components/ComparisonPageLayout' +import { generateComparison } from '@/templates/comparison-tool-prose.jsx' + +function slugify(str: string) { + return str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, '') +} + +interface PageProps { + params: { comparison: string } +} + +export async function generateMetadata({ params }: PageProps): Promise { + const [slug1, slug2] = params.comparison.split('-vs-') + const tools = getTools() + const tool1 = tools.find(t => slugify(t.name) === slug1) + const tool2 = tools.find(t => slugify(t.name) === slug2) + const title = tool1 && tool2 ? `${tool1.name} vs ${tool2.name}` : 'Tool comparison' + return { + title, + description: `A detailed comparison of ${tool1?.name || slug1} and ${tool2?.name || slug2}`, + } +} + +export default function ComparisonPage({ params }: PageProps) { + const [slug1, slug2] = params.comparison.split('-vs-') + const tools = getTools() + const categories = getCategories() + const tool1 = tools.find(t => slugify(t.name) === slug1) + const tool2 = tools.find(t => slugify(t.name) === slug2) + + if (!tool1 || !tool2) { + return null + } + + const proseParagraphs = generateComparison(tool1, tool2, categories) + + return +} diff --git a/src/app/sitemap.js b/src/app/sitemap.js index d84372a2..bfdf05ee 100644 --- a/src/app/sitemap.js +++ b/src/app/sitemap.js @@ -66,6 +66,21 @@ async function getRoutes() { } }); + // Add dynamic comparison routes generated from AI tools data + const toolsPath = path.join(process.cwd(), 'schema/data/ai-assisted-developer-tools.json'); + if (fs.existsSync(toolsPath)) { + const toolsData = JSON.parse(fs.readFileSync(toolsPath, 'utf-8')); + const tools = toolsData.tools || []; + const slugify = (str) => str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, ''); + + for (let i = 0; i < tools.length; i++) { + for (let j = i + 1; j < tools.length; j++) { + const slug = `${slugify(tools[i].name)}-vs-${slugify(tools[j].name)}`; + routes.add(`/comparisons/${slug}`); + } + } + } + // Add RSS feed routes routes.add('/rss/feed.json'); routes.add('/rss/feed.xml'); diff --git a/src/lib/comparisons.ts b/src/lib/comparisons.ts index 9d98d617..a32626d6 100644 --- a/src/lib/comparisons.ts +++ b/src/lib/comparisons.ts @@ -1,39 +1,35 @@ import { ArticleWithSlug } from '@/types/content' -import glob from 'fast-glob' -import path from 'path' +import { getTools } from './getTools' -export async function importComparison( - comparisonFilename: string, -): Promise { - let { metadata } = (await import(`@/app/comparisons/${comparisonFilename}`)) as { - default: React.ComponentType - metadata: ArticleWithSlug - } - - return { - ...metadata, - type: 'blog', - slug: path.basename(comparisonFilename, '.mdx') - } +function slugify(str: string) { + return str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, '') } export async function getAllComparisons(): Promise { - const files = await glob('**/page.mdx', { - cwd: path.join(process.cwd(), 'src/app/comparisons') - }) + const tools = getTools() + const comparisons: ArticleWithSlug[] = [] - const comparisons = await Promise.all( - files.map(async (filename) => { - return importComparison(filename) - }) - ) + for (let i = 0; i < tools.length; i++) { + for (let j = i + 1; j < tools.length; j++) { + const tool1 = tools[i] + const tool2 = tools[j] + comparisons.push({ + title: `${tool1.name} vs ${tool2.name}`, + description: `A detailed comparison of ${tool1.name} and ${tool2.name}.`, + author: 'Zachary Proser', + date: new Date().toISOString(), + type: 'comparison', + slug: `${slugify(tool1.name)}-vs-${slugify(tool2.name)}` + }) + } + } - return comparisons.sort((a, z) => +new Date(z.date) - +new Date(a.date)) + return comparisons } export async function getComparisonsForProduct(productSlug: string): Promise { const comparisons = await getAllComparisons() - return comparisons.filter(comparison => + return comparisons.filter(comparison => comparison.slug.includes(productSlug) ) } \ No newline at end of file diff --git a/src/lib/getTools.js b/src/lib/getTools.js index 3ab89258..18cf649b 100644 --- a/src/lib/getTools.js +++ b/src/lib/getTools.js @@ -10,6 +10,7 @@ export function getCategories() { export function getToolByName(name) { const normalizedName = name.toLowerCase().replace(/-/g, ' '); - const tool = data.tools.find(tool => tool.slug === normalizedName || tool.name.toLowerCase() === normalizedName); + const slugify = (str) => str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, ''); + const tool = data.tools.find(tool => slugify(tool.name) === slugify(normalizedName)); return tool } \ No newline at end of file