diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index d0ac02a..4c9d4a7 100644 Binary files a/backend/celerybeat-schedule and b/backend/celerybeat-schedule differ diff --git a/frontend/index.html b/frontend/index.html index 0516005..ae7744d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -10,11 +10,16 @@ - + + + + - + + + diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml index 4f3a84e..ebf64e6 100644 --- a/frontend/public/sitemap.xml +++ b/frontend/public/sitemap.xml @@ -3,6 +3,7 @@ https://yourdomain.com/daily1.0 https://yourdomain.com/aboutmonthly0.4 + https://yourdomain.com/contactmonthly0.4 https://yourdomain.com/privacyyearly0.3 https://yourdomain.com/termsyearly0.3 diff --git a/frontend/src/components/seo/SEOHead.tsx b/frontend/src/components/seo/SEOHead.tsx new file mode 100644 index 0000000..8c7f2e5 --- /dev/null +++ b/frontend/src/components/seo/SEOHead.tsx @@ -0,0 +1,61 @@ +import { Helmet } from 'react-helmet-async'; + +const SITE_NAME = 'SaaS-PDF'; + +interface SEOHeadProps { + /** Page title (will be appended with " — SaaS-PDF") */ + title: string; + /** Meta description */ + description: string; + /** Canonical URL path (e.g. "/about") — origin is auto-prefixed */ + path: string; + /** OG type — defaults to "website" */ + type?: string; + /** Optional JSON-LD objects to inject as structured data */ + jsonLd?: object | object[]; +} + +/** + * Reusable SEO head component that injects: + * - title, description, canonical URL + * - OpenGraph meta tags (title, description, url, type, site_name, locale) + * - Twitter card meta tags + * - Optional JSON-LD structured data + */ +export default function SEOHead({ title, description, path, type = 'website', jsonLd }: SEOHeadProps) { + const origin = typeof window !== 'undefined' ? window.location.origin : ''; + const canonicalUrl = `${origin}${path}`; + const fullTitle = `${title} — ${SITE_NAME}`; + + const schemas = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : []; + + return ( + + {fullTitle} + + + + {/* OpenGraph */} + + + + + + + + + + {/* Twitter */} + + + + + {/* JSON-LD Structured Data */} + {schemas.map((schema, i) => ( + + ))} + + ); +} diff --git a/frontend/src/pages/AboutPage.tsx b/frontend/src/pages/AboutPage.tsx index 2215fb1..e9a8f07 100644 --- a/frontend/src/pages/AboutPage.tsx +++ b/frontend/src/pages/AboutPage.tsx @@ -1,6 +1,7 @@ import { useTranslation } from 'react-i18next'; -import { Helmet } from 'react-helmet-async'; import { Link } from 'react-router-dom'; +import SEOHead from '@/components/seo/SEOHead'; +import { generateWebPage } from '@/utils/seo'; import { Target, Cpu, Shield, Lock, Wrench } from 'lucide-react'; import { FILE_RETENTION_MINUTES } from '@/config/toolLimits'; @@ -10,11 +11,16 @@ export default function AboutPage() { return ( <> - - {t('pages.about.title')} — {t('common.appName')} - - - +

diff --git a/frontend/src/pages/ContactPage.tsx b/frontend/src/pages/ContactPage.tsx index ef9e7c0..a6e547e 100644 --- a/frontend/src/pages/ContactPage.tsx +++ b/frontend/src/pages/ContactPage.tsx @@ -2,6 +2,8 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Helmet } from 'react-helmet-async'; import { Mail, Send, CheckCircle } from 'lucide-react'; +import SEOHead from '@/components/seo/SEOHead'; +import { generateWebPage } from '@/utils/seo'; const CONTACT_EMAIL = 'support@saas-pdf.com'; @@ -52,11 +54,16 @@ export default function ContactPage() { return ( <> - - {t('pages.contact.title')} — {t('common.appName')} - - - +
diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 6bee7b4..0f30a26 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next'; -import { Helmet } from 'react-helmet-async'; +import SEOHead from '@/components/seo/SEOHead'; +import { generateOrganization } from '@/utils/seo'; import { FileText, FileOutput, @@ -83,12 +84,12 @@ export default function HomePage() { return ( <> - - {t('common.appName')} — {t('home.heroSub')} - - - - + }, + generateOrganization(window.location.origin), + ]} + /> {/* Hero Section */}
diff --git a/frontend/src/pages/PrivacyPage.tsx b/frontend/src/pages/PrivacyPage.tsx index 4217118..ab62ea1 100644 --- a/frontend/src/pages/PrivacyPage.tsx +++ b/frontend/src/pages/PrivacyPage.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next'; -import { Helmet } from 'react-helmet-async'; +import SEOHead from '@/components/seo/SEOHead'; +import { generateWebPage } from '@/utils/seo'; import { FILE_RETENTION_MINUTES } from '@/config/toolLimits'; const LAST_UPDATED = '2026-03-06'; @@ -12,11 +13,16 @@ export default function PrivacyPage() { return ( <> - - {t('pages.privacy.title')} — {t('common.appName')} - - - +

{t('pages.privacy.title')}

diff --git a/frontend/src/pages/TermsPage.tsx b/frontend/src/pages/TermsPage.tsx index f1c8da3..c8336de 100644 --- a/frontend/src/pages/TermsPage.tsx +++ b/frontend/src/pages/TermsPage.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next'; -import { Helmet } from 'react-helmet-async'; +import SEOHead from '@/components/seo/SEOHead'; +import { generateWebPage } from '@/utils/seo'; import { FILE_RETENTION_MINUTES } from '@/config/toolLimits'; const LAST_UPDATED = '2026-03-06'; @@ -12,11 +13,16 @@ export default function TermsPage() { return ( <> - - {t('pages.terms.title')} — {t('common.appName')} - - - +

{t('pages.terms.title')}

diff --git a/frontend/src/utils/seo.ts b/frontend/src/utils/seo.ts index 6f1acc5..9cf841a 100644 --- a/frontend/src/utils/seo.ts +++ b/frontend/src/utils/seo.ts @@ -67,3 +67,44 @@ export function generateFAQ( })), }; } + +/** + * Generate Organization JSON-LD for the site. + */ +export function generateOrganization(origin: string): object { + return { + '@context': 'https://schema.org', + '@type': 'Organization', + name: 'SaaS-PDF', + url: origin, + logo: `${origin}/favicon.svg`, + sameAs: [], + contactPoint: { + '@type': 'ContactPoint', + email: 'support@saas-pdf.com', + contactType: 'customer support', + availableLanguage: ['English', 'Arabic', 'French'], + }, + }; +} + +/** + * Generate WebPage JSON-LD for a static page. + */ +export function generateWebPage(page: { + name: string; + description: string; + url: string; +}): object { + return { + '@context': 'https://schema.org', + '@type': 'WebPage', + name: page.name, + description: page.description, + url: page.url, + isPartOf: { + '@type': 'WebSite', + name: 'SaaS-PDF', + }, + }; +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index a3ecdf5..6afd23b 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -27,11 +27,13 @@ export default defineConfig({ build: { outDir: 'dist', sourcemap: false, + cssMinify: true, rollupOptions: { output: { manualChunks: { vendor: ['react', 'react-dom', 'react-router-dom'], i18n: ['i18next', 'react-i18next'], + helmet: ['react-helmet-async'], }, }, }, diff --git a/scripts/generate_sitemap.py b/scripts/generate_sitemap.py index 29acb9b..59a818f 100644 --- a/scripts/generate_sitemap.py +++ b/scripts/generate_sitemap.py @@ -1,39 +1,55 @@ #!/usr/bin/env python3 """ generate_sitemap.py -Generates sitemap.xml for SEO. +Generates sitemap.xml for SEO from the full route inventory. Usage: python scripts/generate_sitemap.py --domain https://yourdomain.com + python scripts/generate_sitemap.py --domain https://yourdomain.com --output frontend/public/sitemap.xml """ import argparse from datetime import datetime - -TOOLS = [ - '/tools/pdf-to-word', - '/tools/word-to-pdf', - '/tools/compress-pdf', - '/tools/merge-pdf', - '/tools/split-pdf', - '/tools/rotate-pdf', - '/tools/pdf-to-images', - '/tools/images-to-pdf', - '/tools/watermark-pdf', - '/tools/protect-pdf', - '/tools/unlock-pdf', - '/tools/page-numbers', - '/tools/image-converter', - '/tools/video-to-gif', - '/tools/word-counter', - '/tools/text-cleaner', -] +# ─── Route definitions with priority and changefreq ────────────────────────── PAGES = [ - '/', - '/about', - '/privacy', + {'path': '/', 'changefreq': 'daily', 'priority': '1.0'}, + {'path': '/about', 'changefreq': 'monthly', 'priority': '0.4'}, + {'path': '/contact', 'changefreq': 'monthly', 'priority': '0.4'}, + {'path': '/privacy', 'changefreq': 'yearly', 'priority': '0.3'}, + {'path': '/terms', 'changefreq': 'yearly', 'priority': '0.3'}, +] + +# PDF Tools +PDF_TOOLS = [ + 'pdf-to-word', 'word-to-pdf', 'compress-pdf', 'merge-pdf', + 'split-pdf', 'rotate-pdf', 'pdf-to-images', 'images-to-pdf', + 'watermark-pdf', 'remove-watermark-pdf', 'protect-pdf', 'unlock-pdf', + 'page-numbers', 'reorder-pdf', 'extract-pages', 'pdf-editor', + 'pdf-flowchart', 'pdf-to-excel', +] + +# Image Tools +IMAGE_TOOLS = [ + 'image-converter', 'image-resize', 'compress-image', 'remove-background', +] + +# AI Tools +AI_TOOLS = [ + 'ocr', 'chat-pdf', 'summarize-pdf', 'translate-pdf', 'extract-tables', +] + +# Convert / Utility Tools +UTILITY_TOOLS = [ + 'html-to-pdf', 'qr-code', 'video-to-gif', 'word-counter', 'text-cleaner', +] + +TOOL_GROUPS = [ + ('PDF Tools', PDF_TOOLS, '0.9'), + ('Image Tools', IMAGE_TOOLS, '0.8'), + ('AI Tools', AI_TOOLS, '0.8'), + ('Utility Tools', UTILITY_TOOLS, '0.7'), ] @@ -41,30 +57,24 @@ def generate_sitemap(domain: str) -> str: today = datetime.now().strftime('%Y-%m-%d') urls = [] - # Home page — highest priority - urls.append(f''' - {domain}/ + # Static pages + for page in PAGES: + urls.append(f''' + {domain}{page["path"]} + {today} + {page["changefreq"]} + {page["priority"]} + ''') + + # Tool pages by category + for label, slugs, priority in TOOL_GROUPS: + urls.append(f'\n ') + for slug in slugs: + urls.append(f''' + {domain}/tools/{slug} {today} weekly - 1.0 - ''') - - # Tool pages — high priority - for tool in TOOLS: - urls.append(f''' - {domain}{tool} - {today} - monthly - 0.9 - ''') - - # Static pages — lower priority - for page in PAGES[1:]: - urls.append(f''' - {domain}{page} - {today} - monthly - 0.5 + {priority} ''') sitemap = f''' @@ -87,8 +97,9 @@ def main(): with open(args.output, 'w', encoding='utf-8') as f: f.write(sitemap) + total = len(PAGES) + sum(len(slugs) for _, slugs, _ in TOOL_GROUPS) print(f"Sitemap generated: {args.output}") - print(f"URLs: {len(TOOLS) + len(PAGES)}") + print(f"Total URLs: {total}") if __name__ == '__main__':