feat: add comparison page functionality and related routes

- Added a new route for comparison pages in routes.ts.
- Introduced a TOOL_WORKFLOWS object in seoData.ts to define tool usage sequences.
- Updated internal link generation to include workflow slugs.
- Added Arabic, English, and French translations for comparison features and FAQs in respective i18n files.
- Implemented the ComparisonPage component to display feature comparisons, advantages, verdicts, and related tools.
- Enhanced sitemap generation script to include comparison pages.
This commit is contained in:
Your Name
2026-04-03 02:29:14 +02:00
parent 209fdb6509
commit 92ca0af3c7
11 changed files with 885 additions and 2 deletions

View File

@@ -0,0 +1,145 @@
/**
* Comparison page data — defines competitor comparisons for SEO landing pages.
*
* Adding a new comparison:
* 1. Append an entry to COMPARISON_PAGES below.
* 2. Add matching i18n keys under pages.comparison.<i18nKey>.* in en/ar/fr.json.
* 3. Add the slug to both sitemap generators (generate_sitemap.py + generate-seo-assets.mjs).
*/
export interface ComparisonFeature {
/** i18n key suffix under pages.comparison.features.* */
key: string;
/** true = has the feature, false = missing, 'partial' = limited */
us: boolean | 'partial';
competitor: boolean | 'partial';
}
export interface ComparisonPage {
/** URL slug: /compare/<slug> */
slug: string;
/** i18n key prefix: pages.comparison.<i18nKey>.* */
i18nKey: string;
/** Our tool slug (links to /tools/<ourToolSlug>) */
ourToolSlug: string;
/** Competitor display name */
competitorName: string;
/** SEO category */
category: 'PDF' | 'Image' | 'AI' | 'Convert' | 'Utility';
/** Feature comparison rows */
features: ComparisonFeature[];
/** Related comparison page slugs */
relatedComparisonSlugs: string[];
/** Tool slugs to show as related */
relatedToolSlugs: string[];
}
export const COMPARISON_PAGES: ComparisonPage[] = [
{
slug: 'compress-pdf-vs-ilovepdf',
i18nKey: 'compressPdfVsIlovepdf',
ourToolSlug: 'compress-pdf',
competitorName: 'iLovePDF',
category: 'PDF',
features: [
{ key: 'freeUnlimited', us: true, competitor: false },
{ key: 'noSignup', us: true, competitor: false },
{ key: 'batchProcessing', us: true, competitor: 'partial' },
{ key: 'compressionLevels', us: true, competitor: true },
{ key: 'autoDelete', us: true, competitor: true },
{ key: 'noAds', us: true, competitor: false },
{ key: 'apiAccess', us: true, competitor: true },
{ key: 'offlineMode', us: false, competitor: false },
],
relatedComparisonSlugs: ['merge-pdf-vs-smallpdf', 'pdf-to-word-vs-adobe-acrobat'],
relatedToolSlugs: ['compress-pdf', 'merge-pdf', 'split-pdf', 'compress-image'],
},
{
slug: 'merge-pdf-vs-smallpdf',
i18nKey: 'mergePdfVsSmallpdf',
ourToolSlug: 'merge-pdf',
competitorName: 'Smallpdf',
category: 'PDF',
features: [
{ key: 'freeUnlimited', us: true, competitor: false },
{ key: 'noSignup', us: true, competitor: false },
{ key: 'batchProcessing', us: true, competitor: true },
{ key: 'dragReorder', us: true, competitor: true },
{ key: 'autoDelete', us: true, competitor: true },
{ key: 'noAds', us: true, competitor: false },
{ key: 'apiAccess', us: true, competitor: 'partial' },
{ key: 'offlineMode', us: false, competitor: false },
],
relatedComparisonSlugs: ['compress-pdf-vs-ilovepdf', 'pdf-to-word-vs-adobe-acrobat'],
relatedToolSlugs: ['merge-pdf', 'split-pdf', 'compress-pdf', 'reorder-pdf'],
},
{
slug: 'pdf-to-word-vs-adobe-acrobat',
i18nKey: 'pdfToWordVsAdobeAcrobat',
ourToolSlug: 'pdf-to-word',
competitorName: 'Adobe Acrobat',
category: 'PDF',
features: [
{ key: 'freeUnlimited', us: true, competitor: false },
{ key: 'noSignup', us: true, competitor: false },
{ key: 'preserveFormatting', us: true, competitor: true },
{ key: 'batchProcessing', us: true, competitor: true },
{ key: 'autoDelete', us: true, competitor: true },
{ key: 'noAds', us: true, competitor: true },
{ key: 'noInstall', us: true, competitor: false },
{ key: 'offlineMode', us: false, competitor: true },
],
relatedComparisonSlugs: ['compress-pdf-vs-ilovepdf', 'merge-pdf-vs-smallpdf'],
relatedToolSlugs: ['pdf-to-word', 'word-to-pdf', 'pdf-to-excel', 'compress-pdf'],
},
{
slug: 'compress-image-vs-tinypng',
i18nKey: 'compressImageVsTinypng',
ourToolSlug: 'compress-image',
competitorName: 'TinyPNG',
category: 'Image',
features: [
{ key: 'freeUnlimited', us: true, competitor: false },
{ key: 'noSignup', us: true, competitor: true },
{ key: 'multiFormat', us: true, competitor: 'partial' },
{ key: 'batchProcessing', us: true, competitor: 'partial' },
{ key: 'qualityControl', us: true, competitor: false },
{ key: 'autoDelete', us: true, competitor: true },
{ key: 'apiAccess', us: true, competitor: true },
{ key: 'offlineMode', us: false, competitor: false },
],
relatedComparisonSlugs: ['compress-pdf-vs-ilovepdf', 'ocr-vs-adobe-scan'],
relatedToolSlugs: ['compress-image', 'image-converter', 'image-resize', 'remove-background'],
},
{
slug: 'ocr-vs-adobe-scan',
i18nKey: 'ocrVsAdobeScan',
ourToolSlug: 'ocr',
competitorName: 'Adobe Scan',
category: 'AI',
features: [
{ key: 'freeUnlimited', us: true, competitor: false },
{ key: 'noSignup', us: true, competitor: false },
{ key: 'noInstall', us: true, competitor: false },
{ key: 'multiLanguageOcr', us: true, competitor: true },
{ key: 'batchProcessing', us: true, competitor: false },
{ key: 'autoDelete', us: true, competitor: true },
{ key: 'apiAccess', us: true, competitor: false },
{ key: 'offlineMode', us: false, competitor: true },
],
relatedComparisonSlugs: ['pdf-to-word-vs-adobe-acrobat', 'compress-image-vs-tinypng'],
relatedToolSlugs: ['ocr', 'chat-pdf', 'summarize-pdf', 'pdf-to-word'],
},
];
export function getComparisonPage(slug: string): ComparisonPage | undefined {
return COMPARISON_PAGES.find((p) => p.slug === slug);
}
export function getAllComparisonSlugs(): string[] {
return COMPARISON_PAGES.map((p) => p.slug);
}
export function getComparisonPagesByTool(toolSlug: string): ComparisonPage[] {
return COMPARISON_PAGES.filter((p) => p.ourToolSlug === toolSlug);
}

View File

@@ -27,6 +27,7 @@ const STATIC_PAGE_ROUTES = [
'/tools',
'/internal/admin',
'/pricing-transparency',
'/compare/:slug',
] as const;
const SEO_PAGE_ROUTES = getAllSeoLandingPaths();

View File

@@ -917,6 +917,31 @@ const POPULAR_TOOL_SLUGS = [
'video-to-gif',
] as const;
/** Workflow chains: tools users are likely to use in sequence */
const TOOL_WORKFLOWS: Record<string, string[]> = {
'compress-pdf': ['merge-pdf', 'split-pdf', 'pdf-to-word'],
'merge-pdf': ['compress-pdf', 'split-pdf', 'rotate-pdf'],
'split-pdf': ['merge-pdf', 'compress-pdf', 'pdf-to-word'],
'pdf-to-word': ['word-to-pdf', 'compress-pdf', 'ocr'],
'word-to-pdf': ['compress-pdf', 'merge-pdf', 'pdf-editor'],
'pdf-to-excel': ['pdf-to-word', 'ocr', 'compress-pdf'],
'pdf-to-pptx': ['pdf-to-word', 'compress-pdf', 'merge-pdf'],
'rotate-pdf': ['merge-pdf', 'split-pdf', 'compress-pdf'],
'pdf-editor': ['compress-pdf', 'merge-pdf', 'watermark-pdf'],
'watermark-pdf': ['compress-pdf', 'pdf-editor', 'merge-pdf'],
'protect-pdf': ['watermark-pdf', 'compress-pdf', 'merge-pdf'],
'unlock-pdf': ['compress-pdf', 'pdf-to-word', 'merge-pdf'],
'ocr': ['pdf-to-word', 'compress-pdf', 'image-converter'],
'compress-image': ['image-resize', 'image-converter', 'image-to-pdf'],
'image-resize': ['compress-image', 'image-converter', 'image-crop'],
'image-converter': ['compress-image', 'image-resize', 'image-to-pdf'],
'image-to-pdf': ['compress-pdf', 'merge-pdf', 'compress-image'],
'image-crop': ['image-resize', 'compress-image', 'image-converter'],
'html-to-pdf': ['compress-pdf', 'merge-pdf', 'pdf-editor'],
'qr-code': ['barcode-generator', 'html-to-pdf'],
'barcode-generator': ['qr-code', 'html-to-pdf'],
};
function dedupeExistingToolSlugs(slugs: string[], excludeSlugs: string[] = []): string[] {
const excluded = new Set(excludeSlugs);
const seen = new Set<string>();
@@ -952,12 +977,14 @@ export function getInternalLinkToolSlugs(currentSlug: string, limit = 8): string
return [];
}
const workflowSlugs = TOOL_WORKFLOWS[currentSlug] || [];
const sameCategorySlugs = TOOLS_SEO
.filter((tool) => tool.category === currentTool.category && tool.slug !== currentSlug)
.map((tool) => tool.slug);
const internalLinks = dedupeExistingToolSlugs(
[...currentTool.relatedSlugs, ...sameCategorySlugs, ...POPULAR_TOOL_SLUGS],
[...currentTool.relatedSlugs, ...workflowSlugs, ...sameCategorySlugs, ...POPULAR_TOOL_SLUGS],
[currentSlug]
);