diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml index 4cb365a..9857da1 100644 --- a/frontend/public/sitemap.xml +++ b/frontend/public/sitemap.xml @@ -2,391 +2,391 @@ https://dociva.io/ - 2026-03-20 + 2026-03-21 daily 1.0 https://dociva.io/about - 2026-03-20 + 2026-03-21 monthly 0.4 https://dociva.io/contact - 2026-03-20 + 2026-03-21 monthly 0.4 https://dociva.io/privacy - 2026-03-20 + 2026-03-21 yearly 0.3 https://dociva.io/terms - 2026-03-20 + 2026-03-21 yearly 0.3 https://dociva.io/pricing - 2026-03-20 + 2026-03-21 monthly 0.7 https://dociva.io/blog - 2026-03-20 + 2026-03-21 weekly 0.6 https://dociva.io/developers - 2026-03-20 + 2026-03-21 monthly 0.5 https://dociva.io/blog/how-to-compress-pdf-online - 2026-03-20 + 2026-03-21 monthly 0.6 https://dociva.io/blog/convert-images-without-losing-quality - 2026-03-20 + 2026-03-21 monthly 0.6 https://dociva.io/blog/ocr-extract-text-from-images - 2026-03-20 + 2026-03-21 monthly 0.6 https://dociva.io/blog/merge-split-pdf-files - 2026-03-20 + 2026-03-21 monthly 0.6 https://dociva.io/blog/ai-chat-with-pdf-documents - 2026-03-20 + 2026-03-21 monthly 0.6 https://dociva.io/tools/pdf-to-word - 2026-03-20 + 2026-03-21 weekly 0.9 https://dociva.io/tools/word-to-pdf - 2026-03-20 + 2026-03-21 weekly 0.9 https://dociva.io/tools/compress-pdf - 2026-03-20 + 2026-03-21 weekly 0.9 https://dociva.io/tools/merge-pdf - 2026-03-20 + 2026-03-21 weekly 0.9 https://dociva.io/tools/split-pdf - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/rotate-pdf - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/pdf-to-images - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/images-to-pdf - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/watermark-pdf - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/remove-watermark-pdf - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/protect-pdf - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/unlock-pdf - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/page-numbers - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/reorder-pdf - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/extract-pages - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/pdf-editor - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/pdf-flowchart - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/pdf-to-excel - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/sign-pdf - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/crop-pdf - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/flatten-pdf - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/repair-pdf - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/pdf-metadata - 2026-03-20 + 2026-03-21 weekly 0.6 https://dociva.io/tools/image-converter - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/image-resize - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/compress-image - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/remove-background - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/image-crop - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/image-rotate-flip - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/ocr - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/chat-pdf - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/summarize-pdf - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/translate-pdf - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/extract-tables - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/html-to-pdf - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/qr-code - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/video-to-gif - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/tools/word-counter - 2026-03-20 + 2026-03-21 weekly 0.6 https://dociva.io/tools/text-cleaner - 2026-03-20 + 2026-03-21 weekly 0.6 https://dociva.io/tools/pdf-to-pptx - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/excel-to-pdf - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/pptx-to-pdf - 2026-03-20 + 2026-03-21 weekly 0.8 https://dociva.io/tools/barcode-generator - 2026-03-20 + 2026-03-21 weekly 0.7 https://dociva.io/pdf-to-word - 2026-03-20 + 2026-03-21 weekly 0.88 https://dociva.io/word-to-pdf - 2026-03-20 + 2026-03-21 weekly 0.88 https://dociva.io/compress-pdf-online - 2026-03-20 + 2026-03-21 weekly 0.88 https://dociva.io/convert-jpg-to-pdf - 2026-03-20 + 2026-03-21 weekly 0.88 https://dociva.io/merge-pdf-files - 2026-03-20 + 2026-03-21 weekly 0.88 https://dociva.io/remove-pdf-password - 2026-03-20 + 2026-03-21 weekly 0.88 https://dociva.io/best-pdf-tools - 2026-03-20 + 2026-03-21 weekly 0.82 https://dociva.io/free-pdf-tools-online - 2026-03-20 + 2026-03-21 weekly 0.82 https://dociva.io/convert-files-online - 2026-03-20 + 2026-03-21 weekly 0.82 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 630f3ba..30d59d1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,8 +25,7 @@ const BlogPage = lazy(() => import('@/pages/BlogPage')); const BlogPostPage = lazy(() => import('@/pages/BlogPostPage')); const DevelopersPage = lazy(() => import('@/pages/DevelopersPage')); const InternalAdminPage = lazy(() => import('@/pages/InternalAdminPage')); -const SeoProgrammaticPage = lazy(() => import('@/pages/SeoProgrammaticPage')); -const SeoCollectionPage = lazy(() => import('@/pages/SeoCollectionPage')); +const SeoRoutePage = lazy(() => import('@/pages/SeoRoutePage')); // Tool Pages const PdfToWord = lazy(() => import('@/components/tools/PdfToWord')); @@ -119,15 +118,8 @@ export default function App() { } /> } /> } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> {/* PDF Tools */} } /> diff --git a/frontend/src/config/routes.test.ts b/frontend/src/config/routes.test.ts index f391c59..a432806 100644 --- a/frontend/src/config/routes.test.ts +++ b/frontend/src/config/routes.test.ts @@ -3,6 +3,7 @@ import { readFileSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { ALL_ROUTES } from '@/config/routes'; +import { getAllSeoLandingPaths } from '@/config/seoPages'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -22,6 +23,7 @@ describe('Route safety', () => { resolve(__dirname, '../App.tsx'), 'utf-8' ); + const seoLandingPaths = new Set(getAllSeoLandingPaths()); // Extract all path="..." values from elements const routePathRegex = /path="([^"]+)"/g; @@ -32,10 +34,26 @@ describe('Route safety', () => { } it('App.tsx contains routes for every entry in the route registry', () => { - const missing = ALL_ROUTES.filter((r) => !appPaths.has(r)); + const hasDynamicSeoRoute = appPaths.has('/:slug'); + const missing = ALL_ROUTES.filter((route) => { + if (appPaths.has(route)) { + return false; + } + + if (hasDynamicSeoRoute && seoLandingPaths.has(route)) { + return false; + } + + return true; + }); expect(missing, `Missing routes in App.tsx: ${missing.join(', ')}`).toEqual([]); }); + it('App.tsx contains the dynamic SEO routes', () => { + expect(appPaths.has('/:slug')).toBe(true); + expect(appPaths.has('/ar/:slug')).toBe(true); + }); + it('route registry is not empty', () => { expect(ALL_ROUTES.length).toBeGreaterThan(0); }); diff --git a/frontend/src/config/routes.ts b/frontend/src/config/routes.ts index ca9b3c9..2dcb34e 100644 --- a/frontend/src/config/routes.ts +++ b/frontend/src/config/routes.ts @@ -6,8 +6,9 @@ * (routes.test.ts) will fail if any existing route is deleted. */ -// ─── Page routes ───────────────────────────────────────────────── -export const PAGE_ROUTES = [ +import { getAllSeoLandingPaths } from '@/config/seoPages'; + +const STATIC_PAGE_ROUTES = [ '/', '/about', '/account', @@ -21,15 +22,16 @@ export const PAGE_ROUTES = [ '/blog/:slug', '/developers', '/internal/admin', - '/pdf-to-word', - '/word-to-pdf', - '/compress-pdf-online', - '/convert-jpg-to-pdf', - '/merge-pdf-files', - '/remove-pdf-password', - '/best-pdf-tools', - '/free-pdf-tools-online', - '/convert-files-online', +] as const; + +const SEO_PAGE_ROUTES = getAllSeoLandingPaths(); + +// ─── Page routes ───────────────────────────────────────────────── +export const PAGE_ROUTES = [ + ...STATIC_PAGE_ROUTES, + ...SEO_PAGE_ROUTES, + '/:slug', + '/ar/:slug', ] as const; // ─── Tool routes ───────────────────────────────────────────────── diff --git a/frontend/src/pages/SeoPage.tsx b/frontend/src/pages/SeoPage.tsx new file mode 100644 index 0000000..8a879f9 --- /dev/null +++ b/frontend/src/pages/SeoPage.tsx @@ -0,0 +1,286 @@ +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { ArrowRight, CheckCircle, FileText, Link2 } from 'lucide-react'; +import SEOHead from '@/components/seo/SEOHead'; +import FAQSection from '@/components/seo/FAQSection'; +import RelatedTools from '@/components/seo/RelatedTools'; +import SuggestedTools from '@/components/seo/SuggestedTools'; +import { + getLocalizedText, + getLocalizedTextList, + getProgrammaticToolPage, + getSeoCollectionPage, + interpolateTemplate, + normalizeSeoLocale, +} from '@/config/seoPages'; +import { getToolSEO } from '@/config/seoData'; +import { + generateBreadcrumbs, + generateFAQ, + generateHowTo, + generateToolSchema, + generateWebPage, + getSiteOrigin, +} from '@/utils/seo'; +import NotFoundPage from '@/pages/NotFoundPage'; + +interface SeoPageProps { + slug: string; +} + +const COPY = { + en: { + cta: 'Open the tool', + introHeading: 'What this page helps you do', + workflowHeading: 'Recommended workflow', + useCasesHeading: 'When this workflow fits best', + relatedHeading: 'Related guides', + internalLinksHeading: 'You may also need', + supportHeading: 'Built for fast bilingual workflows', + supportBody: + 'Dociva supports English and Arabic user flows, which makes these landing pages usable for both local and international search traffic.', + stepsName: 'How to use this workflow', + breadcrumbLabel: 'Guides', + popularTools: 'Popular tools', + }, + ar: { + cta: 'افتح الأداة', + introHeading: 'ما الذي تساعدك عليه هذه الصفحة', + workflowHeading: 'سير العمل المقترح', + useCasesHeading: 'متى يكون هذا المسار مناسباً', + relatedHeading: 'صفحات ذات صلة', + internalLinksHeading: 'قد تحتاج أيضاً', + supportHeading: 'مصممة لسير عمل ثنائي اللغة بسرعة', + supportBody: + 'يدعم Dociva سير العمل بالإنجليزية والعربية، مما يجعل صفحات الهبوط هذه قابلة للاستخدام مع الترافيك المحلي والدولي معاً.', + stepsName: 'كيفية استخدام هذا المسار', + breadcrumbLabel: 'الأدلة', + popularTools: 'أدوات شائعة', + }, +} as const; + +export default function SeoPage({ slug }: SeoPageProps) { + const { t, i18n } = useTranslation(); + const locale = normalizeSeoLocale(i18n.language); + const copy = COPY[locale]; + const page = getProgrammaticToolPage(slug); + + if (!page) { + return ; + } + + const tool = getToolSEO(page.toolSlug); + if (!tool) { + return ; + } + + const toolTitle = t(`tools.${tool.i18nKey}.title`); + const toolDescription = t(`tools.${tool.i18nKey}.description`); + const steps = t(`seo.${tool.i18nKey}.howToUse`, { returnObjects: true }) as string[]; + const benefits = t(`seo.${tool.i18nKey}.benefits`, { returnObjects: true }) as string[]; + const useCases = t(`seo.${tool.i18nKey}.useCases`, { returnObjects: true }) as string[]; + + const focusKeyword = getLocalizedText(page.focusKeyword, locale); + const keywords = [focusKeyword, ...getLocalizedTextList(page.supportingKeywords, locale)].join(', '); + const tokens = { + brand: 'Dociva', + focusKeyword, + }; + const title = interpolateTemplate(getLocalizedText(page.titleTemplate, locale), tokens); + const description = interpolateTemplate(getLocalizedText(page.descriptionTemplate, locale), tokens); + const path = locale === 'ar' ? `/ar/${page.slug}` : `/${page.slug}`; + const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : ''); + const url = `${siteOrigin}${path}`; + const faqItems = page.faqTemplates.map((item) => ({ + question: getLocalizedText(item.question, locale), + answer: getLocalizedText(item.answer, locale), + })); + const relatedCollections = page.relatedCollectionSlugs + .map((collectionSlug) => getSeoCollectionPage(collectionSlug)) + .filter((entry): entry is NonNullable => Boolean(entry)); + + const introBody = `${toolDescription} ${description}`; + const workflowBody = `${t(`seo.${tool.i18nKey}.whatItDoes`)} ${t(`tools.${tool.i18nKey}.shortDesc`)}`; + const fallbackBenefits = tool.features; + const resolvedBenefits = Array.isArray(benefits) && benefits.length > 0 ? benefits : fallbackBenefits; + const resolvedUseCases = Array.isArray(useCases) && useCases.length > 0 ? useCases : tool.relatedSlugs.map((relatedSlug) => { + const relatedTool = getToolSEO(relatedSlug); + return relatedTool ? t(`tools.${relatedTool.i18nKey}.title`) : relatedSlug; + }); + const localizedCollectionPath = (collectionSlug: string) => (locale === 'ar' ? `/ar/${collectionSlug}` : `/${collectionSlug}`); + + const jsonLd = [ + generateWebPage({ + name: title, + description, + url, + }), + generateToolSchema({ + name: toolTitle, + description, + url, + category: tool.category === 'PDF' ? 'UtilitiesApplication' : 'WebApplication', + }), + generateBreadcrumbs([ + { name: t('common.home'), url: siteOrigin }, + { name: copy.breadcrumbLabel, url: siteOrigin }, + { name: title, url }, + ]), + generateHowTo({ + name: copy.stepsName, + description, + steps: Array.isArray(steps) ? steps : [], + url, + }), + generateFAQ(faqItems), + ]; + + return ( + <> + + +
+
+
+
+

+ {focusKeyword} +

+

+ {title} +

+

+ {description} +

+ +
+ + {copy.cta} + + + + {toolTitle} + +
+
+ +
+
+
+ +
+
+

{copy.popularTools}

+

{toolTitle}

+
+
+

+ {toolDescription} +

+
    + {resolvedBenefits.slice(0, 4).map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+
+ +
+
+

+ {copy.introHeading} +

+

+ {introBody} +

+
+ +
+

+ {copy.workflowHeading} +

+

+ {workflowBody} +

+
    + {(Array.isArray(steps) ? steps : []).map((step) => ( +
  1. {step}
  2. + ))} +
+
+
+ +
+

+ {copy.useCasesHeading} +

+
+ {resolvedUseCases.slice(0, 6).map((item) => ( +
+ {item} +
+ ))} +
+
+ +
+

+ {copy.relatedHeading} +

+
+ {relatedCollections.map((collection) => { + const collectionTitle = interpolateTemplate(getLocalizedText(collection.titleTemplate, locale), { + brand: 'Dociva', + focusKeyword: getLocalizedText(collection.focusKeyword, locale), + }); + + return ( + +
+ + {localizedCollectionPath(collection.slug)} +
+

{collectionTitle}

+

+ {interpolateTemplate(getLocalizedText(collection.descriptionTemplate, locale), { + brand: 'Dociva', + focusKeyword: getLocalizedText(collection.focusKeyword, locale), + })} +

+ + ); + })} +
+
+ +
+

+ {copy.internalLinksHeading} +

+ + +
+ +
+

+ {copy.supportHeading} +

+

{copy.supportBody}

+
+ + +
+ + ); +} \ No newline at end of file diff --git a/frontend/src/pages/SeoProgrammaticPage.tsx b/frontend/src/pages/SeoProgrammaticPage.tsx index 308e9f8..c2cacc3 100644 --- a/frontend/src/pages/SeoProgrammaticPage.tsx +++ b/frontend/src/pages/SeoProgrammaticPage.tsx @@ -1,275 +1 @@ -import { Link } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { ArrowRight, CheckCircle, FileText, Link2 } from 'lucide-react'; -import SEOHead from '@/components/seo/SEOHead'; -import FAQSection from '@/components/seo/FAQSection'; -import SuggestedTools from '@/components/seo/SuggestedTools'; -import { - getLocalizedText, - getLocalizedTextList, - getProgrammaticToolPage, - getSeoCollectionPage, - interpolateTemplate, - normalizeSeoLocale, -} from '@/config/seoPages'; -import { getToolSEO } from '@/config/seoData'; -import { - generateBreadcrumbs, - generateFAQ, - generateHowTo, - generateToolSchema, - generateWebPage, - getSiteOrigin, -} from '@/utils/seo'; -import NotFoundPage from '@/pages/NotFoundPage'; - -interface SeoProgrammaticPageProps { - slug: string; -} - -const COPY = { - en: { - cta: 'Open the tool', - introHeading: 'What this page helps you do', - workflowHeading: 'Recommended workflow', - useCasesHeading: 'When this workflow fits best', - relatedHeading: 'Related guides', - supportHeading: 'Built for fast bilingual workflows', - supportBody: - 'Dociva supports English and Arabic user flows, which makes these landing pages usable for both local and international search traffic.', - stepsName: 'How to use this workflow', - breadcrumbLabel: 'Guides', - popularTools: 'Popular tools', - }, - ar: { - cta: 'افتح الأداة', - introHeading: 'ما الذي تساعدك عليه هذه الصفحة', - workflowHeading: 'سير العمل المقترح', - useCasesHeading: 'متى يكون هذا المسار مناسباً', - relatedHeading: 'صفحات ذات صلة', - supportHeading: 'مصممة لسير عمل ثنائي اللغة بسرعة', - supportBody: - 'يدعم Dociva سير العمل بالإنجليزية والعربية، مما يجعل صفحات الهبوط هذه قابلة للاستخدام مع الترافيك المحلي والدولي معاً.', - stepsName: 'كيفية استخدام هذا المسار', - breadcrumbLabel: 'الأدلة', - popularTools: 'أدوات شائعة', - }, -} as const; - -export default function SeoProgrammaticPage({ slug }: SeoProgrammaticPageProps) { - const { t, i18n } = useTranslation(); - const locale = normalizeSeoLocale(i18n.language); - const copy = COPY[locale]; - const page = getProgrammaticToolPage(slug); - - if (!page) { - return ; - } - - const tool = getToolSEO(page.toolSlug); - if (!tool) { - return ; - } - - const toolTitle = t(`tools.${tool.i18nKey}.title`); - const toolDescription = t(`tools.${tool.i18nKey}.description`); - const steps = t(`seo.${tool.i18nKey}.howToUse`, { returnObjects: true }) as string[]; - const benefits = t(`seo.${tool.i18nKey}.benefits`, { returnObjects: true }) as string[]; - const useCases = t(`seo.${tool.i18nKey}.useCases`, { returnObjects: true }) as string[]; - - const focusKeyword = getLocalizedText(page.focusKeyword, locale); - const keywords = [focusKeyword, ...getLocalizedTextList(page.supportingKeywords, locale)].join(', '); - const tokens = { - brand: 'Dociva', - focusKeyword, - }; - const title = interpolateTemplate(getLocalizedText(page.titleTemplate, locale), tokens); - const description = interpolateTemplate(getLocalizedText(page.descriptionTemplate, locale), tokens); - const path = `/${page.slug}`; - const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : ''); - const url = `${siteOrigin}${path}`; - const faqItems = page.faqTemplates.map((item) => ({ - question: getLocalizedText(item.question, locale), - answer: getLocalizedText(item.answer, locale), - })); - const relatedCollections = page.relatedCollectionSlugs - .map((collectionSlug) => getSeoCollectionPage(collectionSlug)) - .filter((entry): entry is NonNullable => Boolean(entry)); - - const introBody = `${toolDescription} ${description}`; - const workflowBody = `${t(`seo.${tool.i18nKey}.whatItDoes`)} ${t(`tools.${tool.i18nKey}.shortDesc`)}`; - const fallbackBenefits = tool.features; - const resolvedBenefits = Array.isArray(benefits) && benefits.length > 0 ? benefits : fallbackBenefits; - const resolvedUseCases = Array.isArray(useCases) && useCases.length > 0 ? useCases : tool.relatedSlugs.map((relatedSlug) => { - const relatedTool = getToolSEO(relatedSlug); - return relatedTool ? t(`tools.${relatedTool.i18nKey}.title`) : relatedSlug; - }); - - const jsonLd = [ - generateWebPage({ - name: title, - description, - url, - }), - generateToolSchema({ - name: toolTitle, - description, - url, - category: tool.category === 'PDF' ? 'UtilitiesApplication' : 'WebApplication', - }), - generateBreadcrumbs([ - { name: t('common.home'), url: siteOrigin }, - { name: copy.breadcrumbLabel, url: siteOrigin }, - { name: title, url }, - ]), - generateHowTo({ - name: copy.stepsName, - description, - steps: Array.isArray(steps) ? steps : [], - url, - }), - generateFAQ(faqItems), - ]; - - return ( - <> - - -
-
-
-
-

- {focusKeyword} -

-

- {title} -

-

- {description} -

- -
- - {copy.cta} - - - - {toolTitle} - -
-
- -
-
-
- -
-
-

{copy.popularTools}

-

{toolTitle}

-
-
-

- {toolDescription} -

-
    - {resolvedBenefits.slice(0, 4).map((item) => ( -
  • - - {item} -
  • - ))} -
-
-
-
- -
-
-

- {copy.introHeading} -

-

- {introBody} -

-
- -
-

- {copy.workflowHeading} -

-

- {workflowBody} -

-
    - {(Array.isArray(steps) ? steps : []).map((step) => ( -
  1. {step}
  2. - ))} -
-
-
- -
-

- {copy.useCasesHeading} -

-
- {resolvedUseCases.slice(0, 6).map((item) => ( -
- {item} -
- ))} -
-
- -
-

- {copy.relatedHeading} -

-
- {relatedCollections.map((collection) => { - const collectionTitle = interpolateTemplate(getLocalizedText(collection.titleTemplate, locale), { - brand: 'Dociva', - focusKeyword: getLocalizedText(collection.focusKeyword, locale), - }); - - return ( - -
- - /{collection.slug} -
-

{collectionTitle}

-

- {interpolateTemplate(getLocalizedText(collection.descriptionTemplate, locale), { - brand: 'Dociva', - focusKeyword: getLocalizedText(collection.focusKeyword, locale), - })} -

- - ); - })} -
-
- -
-

- {copy.supportHeading} -

-

{copy.supportBody}

-
- - - -
- - ); -} \ No newline at end of file +export { default } from '@/pages/SeoPage'; \ No newline at end of file diff --git a/frontend/src/pages/SeoRoutePage.tsx b/frontend/src/pages/SeoRoutePage.tsx new file mode 100644 index 0000000..dd61b86 --- /dev/null +++ b/frontend/src/pages/SeoRoutePage.tsx @@ -0,0 +1,38 @@ +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { getProgrammaticToolPage, getSeoCollectionPage } from '@/config/seoPages'; +import NotFoundPage from '@/pages/NotFoundPage'; +import SeoCollectionPage from '@/pages/SeoCollectionPage'; +import SeoPage from '@/pages/SeoPage'; + +type SeoRouteParams = { + locale?: string; + slug?: string; +}; + +export default function SeoRoutePage() { + const { i18n } = useTranslation(); + const { locale, slug } = useParams(); + const resolvedLocale = locale === 'ar' ? 'ar' : 'en'; + + useEffect(() => { + if (i18n.language !== resolvedLocale) { + void i18n.changeLanguage(resolvedLocale); + } + }, [i18n, resolvedLocale]); + + if (!slug) { + return ; + } + + if (getProgrammaticToolPage(slug)) { + return ; + } + + if (getSeoCollectionPage(slug)) { + return ; + } + + return ; +} \ No newline at end of file