تحسين خصائص تحسين محركات البحث عبر صفحات ومكونات متعددة، بما في ذلك إضافة البيانات المنظمة، وعلامات OpenGraph، ومكون SEOHead قابل لإعادة الاستخدام. تحديث عملية إنشاء خريطة الموقع لتشمل مسارات جديدة وتحسين ظهور الموقع بشكل عام.---Enhance SEO features across multiple pages and components, including the addition of structured data, OpenGraph tags, and a reusable SEOHead component. Update sitemap generation to include new routes and improve overall site visibility.
This commit is contained in:
Binary file not shown.
@@ -10,11 +10,16 @@
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="SaaS-PDF — Free Online File Tools" />
|
||||
<meta property="og:description" content="18+ free tools: merge, split, compress, convert PDFs, images, videos & text. No signup required." />
|
||||
<meta property="og:description" content="30+ free tools: merge, split, compress, convert PDFs, images, videos & text. No signup required." />
|
||||
<meta property="og:site_name" content="SaaS-PDF" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:locale:alternate" content="ar_SA" />
|
||||
<meta property="og:locale:alternate" content="fr_FR" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="SaaS-PDF — Free Online File Tools" />
|
||||
<meta name="twitter:description" content="18+ free tools: merge, split, compress, convert PDFs, images, videos & text. No signup required." />
|
||||
<meta name="twitter:description" content="30+ free tools: merge, split, compress, convert PDFs, images, videos & text. No signup required." />
|
||||
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
|
||||
<link rel="dns-prefetch" href="https://fonts.gstatic.com" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Tajawal:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<!-- Pages -->
|
||||
<url><loc>https://yourdomain.com/</loc><changefreq>daily</changefreq><priority>1.0</priority></url>
|
||||
<url><loc>https://yourdomain.com/about</loc><changefreq>monthly</changefreq><priority>0.4</priority></url>
|
||||
<url><loc>https://yourdomain.com/contact</loc><changefreq>monthly</changefreq><priority>0.4</priority></url>
|
||||
<url><loc>https://yourdomain.com/privacy</loc><changefreq>yearly</changefreq><priority>0.3</priority></url>
|
||||
<url><loc>https://yourdomain.com/terms</loc><changefreq>yearly</changefreq><priority>0.3</priority></url>
|
||||
|
||||
|
||||
61
frontend/src/components/seo/SEOHead.tsx
Normal file
61
frontend/src/components/seo/SEOHead.tsx
Normal file
@@ -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 (
|
||||
<Helmet>
|
||||
<title>{fullTitle}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
{/* OpenGraph */}
|
||||
<meta property="og:title" content={fullTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:type" content={type} />
|
||||
<meta property="og:site_name" content={SITE_NAME} />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:locale:alternate" content="ar_SA" />
|
||||
<meta property="og:locale:alternate" content="fr_FR" />
|
||||
|
||||
{/* Twitter */}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content={fullTitle} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
|
||||
{/* JSON-LD Structured Data */}
|
||||
{schemas.map((schema, i) => (
|
||||
<script key={i} type="application/ld+json">
|
||||
{JSON.stringify(schema)}
|
||||
</script>
|
||||
))}
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t('pages.about.title')} — {t('common.appName')}</title>
|
||||
<meta name="description" content={t('pages.about.metaDescription')} />
|
||||
<link rel="canonical" href={`${window.location.origin}/about`} />
|
||||
</Helmet>
|
||||
<SEOHead
|
||||
title={t('pages.about.title')}
|
||||
description={t('pages.about.metaDescription')}
|
||||
path="/about"
|
||||
jsonLd={generateWebPage({
|
||||
name: t('pages.about.title'),
|
||||
description: t('pages.about.metaDescription'),
|
||||
url: `${window.location.origin}/about`,
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<h1 className="mb-8 text-3xl font-bold text-slate-900 dark:text-white">
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t('pages.contact.title')} — {t('common.appName')}</title>
|
||||
<meta name="description" content={t('pages.contact.metaDescription')} />
|
||||
<link rel="canonical" href={`${window.location.origin}/contact`} />
|
||||
</Helmet>
|
||||
<SEOHead
|
||||
title={t('pages.contact.title')}
|
||||
description={t('pages.contact.metaDescription')}
|
||||
path="/contact"
|
||||
jsonLd={generateWebPage({
|
||||
name: t('pages.contact.title'),
|
||||
description: t('pages.contact.metaDescription'),
|
||||
url: `${window.location.origin}/contact`,
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<div className="mb-8 text-center">
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t('common.appName')} — {t('home.heroSub')}</title>
|
||||
<meta name="description" content={t('home.heroSub')} />
|
||||
<link rel="canonical" href={window.location.origin} />
|
||||
<script type="application/ld+json">
|
||||
{JSON.stringify({
|
||||
<SEOHead
|
||||
title={t('common.appName')}
|
||||
description={t('home.heroSub')}
|
||||
path="/"
|
||||
jsonLd={[
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: t('common.appName'),
|
||||
@@ -99,9 +100,10 @@ export default function HomePage() {
|
||||
target: `${window.location.origin}/tools/{search_term_string}`,
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
})}
|
||||
</script>
|
||||
</Helmet>
|
||||
},
|
||||
generateOrganization(window.location.origin),
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="py-12 sm:py-20 bg-gradient-to-b from-slate-50 to-white dark:from-slate-900 dark:to-slate-950 px-4 mb-10 rounded-b-[3rem]">
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t('pages.privacy.title')} — {t('common.appName')}</title>
|
||||
<meta name="description" content={t('pages.privacy.metaDescription')} />
|
||||
<link rel="canonical" href={`${window.location.origin}/privacy`} />
|
||||
</Helmet>
|
||||
<SEOHead
|
||||
title={t('pages.privacy.title')}
|
||||
description={t('pages.privacy.metaDescription')}
|
||||
path="/privacy"
|
||||
jsonLd={generateWebPage({
|
||||
name: t('pages.privacy.title'),
|
||||
description: t('pages.privacy.metaDescription'),
|
||||
url: `${window.location.origin}/privacy`,
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className="prose mx-auto max-w-2xl dark:prose-invert">
|
||||
<h1>{t('pages.privacy.title')}</h1>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t('pages.terms.title')} — {t('common.appName')}</title>
|
||||
<meta name="description" content={t('pages.terms.metaDescription')} />
|
||||
<link rel="canonical" href={`${window.location.origin}/terms`} />
|
||||
</Helmet>
|
||||
<SEOHead
|
||||
title={t('pages.terms.title')}
|
||||
description={t('pages.terms.metaDescription')}
|
||||
path="/terms"
|
||||
jsonLd={generateWebPage({
|
||||
name: t('pages.terms.title'),
|
||||
description: t('pages.terms.metaDescription'),
|
||||
url: `${window.location.origin}/terms`,
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className="prose mx-auto max-w-2xl dark:prose-invert">
|
||||
<h1>{t('pages.terms.title')}</h1>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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''' <url>
|
||||
<loc>{domain}/</loc>
|
||||
# Static pages
|
||||
for page in PAGES:
|
||||
urls.append(f''' <url>
|
||||
<loc>{domain}{page["path"]}</loc>
|
||||
<lastmod>{today}</lastmod>
|
||||
<changefreq>{page["changefreq"]}</changefreq>
|
||||
<priority>{page["priority"]}</priority>
|
||||
</url>''')
|
||||
|
||||
# Tool pages by category
|
||||
for label, slugs, priority in TOOL_GROUPS:
|
||||
urls.append(f'\n <!-- {label} -->')
|
||||
for slug in slugs:
|
||||
urls.append(f''' <url>
|
||||
<loc>{domain}/tools/{slug}</loc>
|
||||
<lastmod>{today}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>''')
|
||||
|
||||
# Tool pages — high priority
|
||||
for tool in TOOLS:
|
||||
urls.append(f''' <url>
|
||||
<loc>{domain}{tool}</loc>
|
||||
<lastmod>{today}</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>''')
|
||||
|
||||
# Static pages — lower priority
|
||||
for page in PAGES[1:]:
|
||||
urls.append(f''' <url>
|
||||
<loc>{domain}{page}</loc>
|
||||
<lastmod>{today}</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
<priority>{priority}</priority>
|
||||
</url>''')
|
||||
|
||||
sitemap = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -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__':
|
||||
|
||||
Reference in New Issue
Block a user