Refactor SEO routing and page structure
- Replaced SeoProgrammaticPage with SeoRoutePage to handle dynamic routing for SEO pages. - Updated App.tsx to use SeoRoutePage for dynamic routes. - Consolidated SEO page logic into SeoPage component. - Removed individual SEO programmatic routes and replaced them with a dynamic route structure. - Added tests to ensure all routes are accounted for and dynamic SEO routes are present. - Introduced new SeoRoutePage to manage locale and slug parameters for SEO pages.
This commit is contained in:
@@ -2,391 +2,391 @@
|
|||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/</loc>
|
<loc>https://dociva.io/</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/about</loc>
|
<loc>https://dociva.io/about</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.4</priority>
|
<priority>0.4</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/contact</loc>
|
<loc>https://dociva.io/contact</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.4</priority>
|
<priority>0.4</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/privacy</loc>
|
<loc>https://dociva.io/privacy</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>yearly</changefreq>
|
<changefreq>yearly</changefreq>
|
||||||
<priority>0.3</priority>
|
<priority>0.3</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/terms</loc>
|
<loc>https://dociva.io/terms</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>yearly</changefreq>
|
<changefreq>yearly</changefreq>
|
||||||
<priority>0.3</priority>
|
<priority>0.3</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/pricing</loc>
|
<loc>https://dociva.io/pricing</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/blog</loc>
|
<loc>https://dociva.io/blog</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/developers</loc>
|
<loc>https://dociva.io/developers</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.5</priority>
|
<priority>0.5</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/blog/how-to-compress-pdf-online</loc>
|
<loc>https://dociva.io/blog/how-to-compress-pdf-online</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/blog/convert-images-without-losing-quality</loc>
|
<loc>https://dociva.io/blog/convert-images-without-losing-quality</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/blog/ocr-extract-text-from-images</loc>
|
<loc>https://dociva.io/blog/ocr-extract-text-from-images</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/blog/merge-split-pdf-files</loc>
|
<loc>https://dociva.io/blog/merge-split-pdf-files</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/blog/ai-chat-with-pdf-documents</loc>
|
<loc>https://dociva.io/blog/ai-chat-with-pdf-documents</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/pdf-to-word</loc>
|
<loc>https://dociva.io/tools/pdf-to-word</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/word-to-pdf</loc>
|
<loc>https://dociva.io/tools/word-to-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/compress-pdf</loc>
|
<loc>https://dociva.io/tools/compress-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/merge-pdf</loc>
|
<loc>https://dociva.io/tools/merge-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/split-pdf</loc>
|
<loc>https://dociva.io/tools/split-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/rotate-pdf</loc>
|
<loc>https://dociva.io/tools/rotate-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/pdf-to-images</loc>
|
<loc>https://dociva.io/tools/pdf-to-images</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/images-to-pdf</loc>
|
<loc>https://dociva.io/tools/images-to-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/watermark-pdf</loc>
|
<loc>https://dociva.io/tools/watermark-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/remove-watermark-pdf</loc>
|
<loc>https://dociva.io/tools/remove-watermark-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/protect-pdf</loc>
|
<loc>https://dociva.io/tools/protect-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/unlock-pdf</loc>
|
<loc>https://dociva.io/tools/unlock-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/page-numbers</loc>
|
<loc>https://dociva.io/tools/page-numbers</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/reorder-pdf</loc>
|
<loc>https://dociva.io/tools/reorder-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/extract-pages</loc>
|
<loc>https://dociva.io/tools/extract-pages</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/pdf-editor</loc>
|
<loc>https://dociva.io/tools/pdf-editor</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/pdf-flowchart</loc>
|
<loc>https://dociva.io/tools/pdf-flowchart</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/pdf-to-excel</loc>
|
<loc>https://dociva.io/tools/pdf-to-excel</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/sign-pdf</loc>
|
<loc>https://dociva.io/tools/sign-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/crop-pdf</loc>
|
<loc>https://dociva.io/tools/crop-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/flatten-pdf</loc>
|
<loc>https://dociva.io/tools/flatten-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/repair-pdf</loc>
|
<loc>https://dociva.io/tools/repair-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/pdf-metadata</loc>
|
<loc>https://dociva.io/tools/pdf-metadata</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/image-converter</loc>
|
<loc>https://dociva.io/tools/image-converter</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/image-resize</loc>
|
<loc>https://dociva.io/tools/image-resize</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/compress-image</loc>
|
<loc>https://dociva.io/tools/compress-image</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/remove-background</loc>
|
<loc>https://dociva.io/tools/remove-background</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/image-crop</loc>
|
<loc>https://dociva.io/tools/image-crop</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/image-rotate-flip</loc>
|
<loc>https://dociva.io/tools/image-rotate-flip</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/ocr</loc>
|
<loc>https://dociva.io/tools/ocr</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/chat-pdf</loc>
|
<loc>https://dociva.io/tools/chat-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/summarize-pdf</loc>
|
<loc>https://dociva.io/tools/summarize-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/translate-pdf</loc>
|
<loc>https://dociva.io/tools/translate-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/extract-tables</loc>
|
<loc>https://dociva.io/tools/extract-tables</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/html-to-pdf</loc>
|
<loc>https://dociva.io/tools/html-to-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/qr-code</loc>
|
<loc>https://dociva.io/tools/qr-code</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/video-to-gif</loc>
|
<loc>https://dociva.io/tools/video-to-gif</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/word-counter</loc>
|
<loc>https://dociva.io/tools/word-counter</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/text-cleaner</loc>
|
<loc>https://dociva.io/tools/text-cleaner</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/pdf-to-pptx</loc>
|
<loc>https://dociva.io/tools/pdf-to-pptx</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/excel-to-pdf</loc>
|
<loc>https://dociva.io/tools/excel-to-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/pptx-to-pdf</loc>
|
<loc>https://dociva.io/tools/pptx-to-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/tools/barcode-generator</loc>
|
<loc>https://dociva.io/tools/barcode-generator</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/pdf-to-word</loc>
|
<loc>https://dociva.io/pdf-to-word</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.88</priority>
|
<priority>0.88</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/word-to-pdf</loc>
|
<loc>https://dociva.io/word-to-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.88</priority>
|
<priority>0.88</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/compress-pdf-online</loc>
|
<loc>https://dociva.io/compress-pdf-online</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.88</priority>
|
<priority>0.88</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/convert-jpg-to-pdf</loc>
|
<loc>https://dociva.io/convert-jpg-to-pdf</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.88</priority>
|
<priority>0.88</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/merge-pdf-files</loc>
|
<loc>https://dociva.io/merge-pdf-files</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.88</priority>
|
<priority>0.88</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/remove-pdf-password</loc>
|
<loc>https://dociva.io/remove-pdf-password</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.88</priority>
|
<priority>0.88</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/best-pdf-tools</loc>
|
<loc>https://dociva.io/best-pdf-tools</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.82</priority>
|
<priority>0.82</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/free-pdf-tools-online</loc>
|
<loc>https://dociva.io/free-pdf-tools-online</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.82</priority>
|
<priority>0.82</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dociva.io/convert-files-online</loc>
|
<loc>https://dociva.io/convert-files-online</loc>
|
||||||
<lastmod>2026-03-20</lastmod>
|
<lastmod>2026-03-21</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.82</priority>
|
<priority>0.82</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ const BlogPage = lazy(() => import('@/pages/BlogPage'));
|
|||||||
const BlogPostPage = lazy(() => import('@/pages/BlogPostPage'));
|
const BlogPostPage = lazy(() => import('@/pages/BlogPostPage'));
|
||||||
const DevelopersPage = lazy(() => import('@/pages/DevelopersPage'));
|
const DevelopersPage = lazy(() => import('@/pages/DevelopersPage'));
|
||||||
const InternalAdminPage = lazy(() => import('@/pages/InternalAdminPage'));
|
const InternalAdminPage = lazy(() => import('@/pages/InternalAdminPage'));
|
||||||
const SeoProgrammaticPage = lazy(() => import('@/pages/SeoProgrammaticPage'));
|
const SeoRoutePage = lazy(() => import('@/pages/SeoRoutePage'));
|
||||||
const SeoCollectionPage = lazy(() => import('@/pages/SeoCollectionPage'));
|
|
||||||
|
|
||||||
// Tool Pages
|
// Tool Pages
|
||||||
const PdfToWord = lazy(() => import('@/components/tools/PdfToWord'));
|
const PdfToWord = lazy(() => import('@/components/tools/PdfToWord'));
|
||||||
@@ -119,15 +118,8 @@ export default function App() {
|
|||||||
<Route path="/blog/:slug" element={<BlogPostPage />} />
|
<Route path="/blog/:slug" element={<BlogPostPage />} />
|
||||||
<Route path="/developers" element={<DevelopersPage />} />
|
<Route path="/developers" element={<DevelopersPage />} />
|
||||||
<Route path="/internal/admin" element={<InternalAdminPage />} />
|
<Route path="/internal/admin" element={<InternalAdminPage />} />
|
||||||
<Route path="/pdf-to-word" element={<SeoProgrammaticPage slug="pdf-to-word" />} />
|
<Route path="/ar/:slug" element={<SeoRoutePage />} />
|
||||||
<Route path="/word-to-pdf" element={<SeoProgrammaticPage slug="word-to-pdf" />} />
|
<Route path="/:slug" element={<SeoRoutePage />} />
|
||||||
<Route path="/compress-pdf-online" element={<SeoProgrammaticPage slug="compress-pdf-online" />} />
|
|
||||||
<Route path="/convert-jpg-to-pdf" element={<SeoProgrammaticPage slug="convert-jpg-to-pdf" />} />
|
|
||||||
<Route path="/merge-pdf-files" element={<SeoProgrammaticPage slug="merge-pdf-files" />} />
|
|
||||||
<Route path="/remove-pdf-password" element={<SeoProgrammaticPage slug="remove-pdf-password" />} />
|
|
||||||
<Route path="/best-pdf-tools" element={<SeoCollectionPage slug="best-pdf-tools" />} />
|
|
||||||
<Route path="/free-pdf-tools-online" element={<SeoCollectionPage slug="free-pdf-tools-online" />} />
|
|
||||||
<Route path="/convert-files-online" element={<SeoCollectionPage slug="convert-files-online" />} />
|
|
||||||
|
|
||||||
{/* PDF Tools */}
|
{/* PDF Tools */}
|
||||||
<Route path="/tools/pdf-to-word" element={<ToolLandingPage slug="pdf-to-word"><PdfToWord /></ToolLandingPage>} />
|
<Route path="/tools/pdf-to-word" element={<ToolLandingPage slug="pdf-to-word"><PdfToWord /></ToolLandingPage>} />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { readFileSync } from 'fs';
|
|||||||
import { resolve, dirname } from 'path';
|
import { resolve, dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { ALL_ROUTES } from '@/config/routes';
|
import { ALL_ROUTES } from '@/config/routes';
|
||||||
|
import { getAllSeoLandingPaths } from '@/config/seoPages';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -22,6 +23,7 @@ describe('Route safety', () => {
|
|||||||
resolve(__dirname, '../App.tsx'),
|
resolve(__dirname, '../App.tsx'),
|
||||||
'utf-8'
|
'utf-8'
|
||||||
);
|
);
|
||||||
|
const seoLandingPaths = new Set(getAllSeoLandingPaths());
|
||||||
|
|
||||||
// Extract all path="..." values from <Route> elements
|
// Extract all path="..." values from <Route> elements
|
||||||
const routePathRegex = /path="([^"]+)"/g;
|
const routePathRegex = /path="([^"]+)"/g;
|
||||||
@@ -32,10 +34,26 @@ describe('Route safety', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('App.tsx contains routes for every entry in the route registry', () => {
|
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([]);
|
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', () => {
|
it('route registry is not empty', () => {
|
||||||
expect(ALL_ROUTES.length).toBeGreaterThan(0);
|
expect(ALL_ROUTES.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
* (routes.test.ts) will fail if any existing route is deleted.
|
* (routes.test.ts) will fail if any existing route is deleted.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ─── Page routes ─────────────────────────────────────────────────
|
import { getAllSeoLandingPaths } from '@/config/seoPages';
|
||||||
export const PAGE_ROUTES = [
|
|
||||||
|
const STATIC_PAGE_ROUTES = [
|
||||||
'/',
|
'/',
|
||||||
'/about',
|
'/about',
|
||||||
'/account',
|
'/account',
|
||||||
@@ -21,15 +22,16 @@ export const PAGE_ROUTES = [
|
|||||||
'/blog/:slug',
|
'/blog/:slug',
|
||||||
'/developers',
|
'/developers',
|
||||||
'/internal/admin',
|
'/internal/admin',
|
||||||
'/pdf-to-word',
|
] as const;
|
||||||
'/word-to-pdf',
|
|
||||||
'/compress-pdf-online',
|
const SEO_PAGE_ROUTES = getAllSeoLandingPaths();
|
||||||
'/convert-jpg-to-pdf',
|
|
||||||
'/merge-pdf-files',
|
// ─── Page routes ─────────────────────────────────────────────────
|
||||||
'/remove-pdf-password',
|
export const PAGE_ROUTES = [
|
||||||
'/best-pdf-tools',
|
...STATIC_PAGE_ROUTES,
|
||||||
'/free-pdf-tools-online',
|
...SEO_PAGE_ROUTES,
|
||||||
'/convert-files-online',
|
'/:slug',
|
||||||
|
'/ar/:slug',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// ─── Tool routes ─────────────────────────────────────────────────
|
// ─── Tool routes ─────────────────────────────────────────────────
|
||||||
|
|||||||
286
frontend/src/pages/SeoPage.tsx
Normal file
286
frontend/src/pages/SeoPage.tsx
Normal file
@@ -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 <NotFoundPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = getToolSEO(page.toolSlug);
|
||||||
|
if (!tool) {
|
||||||
|
return <NotFoundPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<typeof entry> => 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 (
|
||||||
|
<>
|
||||||
|
<SEOHead title={title} description={description} path={path} keywords={keywords} jsonLd={jsonLd} />
|
||||||
|
|
||||||
|
<div className="mx-auto max-w-6xl space-y-12">
|
||||||
|
<section className="rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 sm:p-10">
|
||||||
|
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
|
||||||
|
<div>
|
||||||
|
<p className="mb-3 text-sm font-semibold uppercase tracking-[0.2em] text-primary-600 dark:text-primary-400">
|
||||||
|
{focusKeyword}
|
||||||
|
</p>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 max-w-3xl text-lg leading-8 text-slate-600 dark:text-slate-400">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
|
<Link
|
||||||
|
to={`/tools/${page.toolSlug}`}
|
||||||
|
className="inline-flex items-center gap-2 rounded-xl bg-primary-600 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-primary-700"
|
||||||
|
>
|
||||||
|
{copy.cta}
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
<span className="inline-flex items-center rounded-xl border border-slate-200 px-4 py-3 text-sm text-slate-600 dark:border-slate-700 dark:text-slate-300">
|
||||||
|
{toolTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-6 dark:border-slate-700 dark:bg-slate-800/70">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300">
|
||||||
|
<FileText className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">{copy.popularTools}</p>
|
||||||
|
<p className="text-base font-semibold text-slate-900 dark:text-white">{toolTitle}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-sm leading-6 text-slate-600 dark:text-slate-400">
|
||||||
|
{toolDescription}
|
||||||
|
</p>
|
||||||
|
<ul className="mt-4 space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
||||||
|
{resolvedBenefits.slice(0, 4).map((item) => (
|
||||||
|
<li key={item} className="flex items-start gap-2">
|
||||||
|
<CheckCircle className="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-8 lg:grid-cols-2">
|
||||||
|
<div className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||||
|
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||||
|
{copy.introHeading}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 leading-8 text-slate-700 dark:text-slate-300">
|
||||||
|
{introBody}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||||
|
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||||
|
{copy.workflowHeading}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 leading-8 text-slate-700 dark:text-slate-300">
|
||||||
|
{workflowBody}
|
||||||
|
</p>
|
||||||
|
<ol className="mt-5 list-decimal space-y-2 pl-5 text-slate-700 dark:text-slate-300">
|
||||||
|
{(Array.isArray(steps) ? steps : []).map((step) => (
|
||||||
|
<li key={step}>{step}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||||
|
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||||
|
{copy.useCasesHeading}
|
||||||
|
</h2>
|
||||||
|
<div className="mt-5 grid gap-3 sm:grid-cols-2">
|
||||||
|
{resolvedUseCases.slice(0, 6).map((item) => (
|
||||||
|
<div key={item} className="rounded-xl bg-slate-50 p-4 text-sm leading-6 text-slate-700 dark:bg-slate-800 dark:text-slate-300">
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||||
|
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||||
|
{copy.relatedHeading}
|
||||||
|
</h2>
|
||||||
|
<div className="mt-5 grid gap-4 md:grid-cols-2">
|
||||||
|
{relatedCollections.map((collection) => {
|
||||||
|
const collectionTitle = interpolateTemplate(getLocalizedText(collection.titleTemplate, locale), {
|
||||||
|
brand: 'Dociva',
|
||||||
|
focusKeyword: getLocalizedText(collection.focusKeyword, locale),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={collection.slug}
|
||||||
|
to={localizedCollectionPath(collection.slug)}
|
||||||
|
className="rounded-2xl border border-slate-200 p-5 transition-colors hover:border-primary-300 hover:bg-slate-50 dark:border-slate-700 dark:hover:border-primary-600 dark:hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 text-primary-600 dark:text-primary-400">
|
||||||
|
<Link2 className="h-4 w-4" />
|
||||||
|
<span className="text-sm font-medium">{localizedCollectionPath(collection.slug)}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 font-semibold text-slate-900 dark:text-white">{collectionTitle}</p>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-slate-600 dark:text-slate-400">
|
||||||
|
{interpolateTemplate(getLocalizedText(collection.descriptionTemplate, locale), {
|
||||||
|
brand: 'Dociva',
|
||||||
|
focusKeyword: getLocalizedText(collection.focusKeyword, locale),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||||
|
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||||
|
{copy.internalLinksHeading}
|
||||||
|
</h2>
|
||||||
|
<RelatedTools currentSlug={page.toolSlug} />
|
||||||
|
<SuggestedTools currentSlug={page.toolSlug} limit={4} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||||
|
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||||
|
{copy.supportHeading}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 leading-8 text-slate-700 dark:text-slate-300">{copy.supportBody}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<FAQSection faqs={faqItems} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,275 +1 @@
|
|||||||
import { Link } from 'react-router-dom';
|
export { default } from '@/pages/SeoPage';
|
||||||
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 <NotFoundPage />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tool = getToolSEO(page.toolSlug);
|
|
||||||
if (!tool) {
|
|
||||||
return <NotFoundPage />;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<typeof entry> => 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 (
|
|
||||||
<>
|
|
||||||
<SEOHead title={title} description={description} path={path} keywords={keywords} jsonLd={jsonLd} />
|
|
||||||
|
|
||||||
<div className="mx-auto max-w-6xl space-y-12">
|
|
||||||
<section className="rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 sm:p-10">
|
|
||||||
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
|
|
||||||
<div>
|
|
||||||
<p className="mb-3 text-sm font-semibold uppercase tracking-[0.2em] text-primary-600 dark:text-primary-400">
|
|
||||||
{focusKeyword}
|
|
||||||
</p>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
<p className="mt-4 max-w-3xl text-lg leading-8 text-slate-600 dark:text-slate-400">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-6 flex flex-wrap gap-3">
|
|
||||||
<Link
|
|
||||||
to={`/tools/${page.toolSlug}`}
|
|
||||||
className="inline-flex items-center gap-2 rounded-xl bg-primary-600 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-primary-700"
|
|
||||||
>
|
|
||||||
{copy.cta}
|
|
||||||
<ArrowRight className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
<span className="inline-flex items-center rounded-xl border border-slate-200 px-4 py-3 text-sm text-slate-600 dark:border-slate-700 dark:text-slate-300">
|
|
||||||
{toolTitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-6 dark:border-slate-700 dark:bg-slate-800/70">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300">
|
|
||||||
<FileText className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">{copy.popularTools}</p>
|
|
||||||
<p className="text-base font-semibold text-slate-900 dark:text-white">{toolTitle}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="mt-4 text-sm leading-6 text-slate-600 dark:text-slate-400">
|
|
||||||
{toolDescription}
|
|
||||||
</p>
|
|
||||||
<ul className="mt-4 space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
{resolvedBenefits.slice(0, 4).map((item) => (
|
|
||||||
<li key={item} className="flex items-start gap-2">
|
|
||||||
<CheckCircle className="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
|
|
||||||
<span>{item}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid gap-8 lg:grid-cols-2">
|
|
||||||
<div className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
|
||||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
|
||||||
{copy.introHeading}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-4 leading-8 text-slate-700 dark:text-slate-300">
|
|
||||||
{introBody}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
|
||||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
|
||||||
{copy.workflowHeading}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-4 leading-8 text-slate-700 dark:text-slate-300">
|
|
||||||
{workflowBody}
|
|
||||||
</p>
|
|
||||||
<ol className="mt-5 list-decimal space-y-2 pl-5 text-slate-700 dark:text-slate-300">
|
|
||||||
{(Array.isArray(steps) ? steps : []).map((step) => (
|
|
||||||
<li key={step}>{step}</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
|
||||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
|
||||||
{copy.useCasesHeading}
|
|
||||||
</h2>
|
|
||||||
<div className="mt-5 grid gap-3 sm:grid-cols-2">
|
|
||||||
{resolvedUseCases.slice(0, 6).map((item) => (
|
|
||||||
<div key={item} className="rounded-xl bg-slate-50 p-4 text-sm leading-6 text-slate-700 dark:bg-slate-800 dark:text-slate-300">
|
|
||||||
{item}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
|
||||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
|
||||||
{copy.relatedHeading}
|
|
||||||
</h2>
|
|
||||||
<div className="mt-5 grid gap-4 md:grid-cols-2">
|
|
||||||
{relatedCollections.map((collection) => {
|
|
||||||
const collectionTitle = interpolateTemplate(getLocalizedText(collection.titleTemplate, locale), {
|
|
||||||
brand: 'Dociva',
|
|
||||||
focusKeyword: getLocalizedText(collection.focusKeyword, locale),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={collection.slug}
|
|
||||||
to={`/${collection.slug}`}
|
|
||||||
className="rounded-2xl border border-slate-200 p-5 transition-colors hover:border-primary-300 hover:bg-slate-50 dark:border-slate-700 dark:hover:border-primary-600 dark:hover:bg-slate-800"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-primary-600 dark:text-primary-400">
|
|
||||||
<Link2 className="h-4 w-4" />
|
|
||||||
<span className="text-sm font-medium">/{collection.slug}</span>
|
|
||||||
</div>
|
|
||||||
<p className="mt-3 font-semibold text-slate-900 dark:text-white">{collectionTitle}</p>
|
|
||||||
<p className="mt-2 text-sm leading-6 text-slate-600 dark:text-slate-400">
|
|
||||||
{interpolateTemplate(getLocalizedText(collection.descriptionTemplate, locale), {
|
|
||||||
brand: 'Dociva',
|
|
||||||
focusKeyword: getLocalizedText(collection.focusKeyword, locale),
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
|
||||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
|
||||||
{copy.supportHeading}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-4 leading-8 text-slate-700 dark:text-slate-300">{copy.supportBody}</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<FAQSection faqs={faqItems} />
|
|
||||||
<SuggestedTools currentSlug={page.toolSlug} limit={4} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
38
frontend/src/pages/SeoRoutePage.tsx
Normal file
38
frontend/src/pages/SeoRoutePage.tsx
Normal file
@@ -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<SeoRouteParams>();
|
||||||
|
const resolvedLocale = locale === 'ar' ? 'ar' : 'en';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (i18n.language !== resolvedLocale) {
|
||||||
|
void i18n.changeLanguage(resolvedLocale);
|
||||||
|
}
|
||||||
|
}, [i18n, resolvedLocale]);
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
return <NotFoundPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getProgrammaticToolPage(slug)) {
|
||||||
|
return <SeoPage slug={slug} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getSeoCollectionPage(slug)) {
|
||||||
|
return <SeoCollectionPage slug={slug} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <NotFoundPage />;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user