From 03c451abe5c8e832fd8ac521e9d9883da836893b Mon Sep 17 00:00:00 2001 From: Your Name <119736744+aborayan2022@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:39:08 +0200 Subject: [PATCH 1/6] feat: add design system with colors, components, and theme configuration - Introduced a comprehensive color palette in colors.ts, including primary, accent, success, warning, error, info, neutral, slate, and semantic colors for light and dark modes. - Created a components-registry.ts to manage UI components with metadata, including buttons, inputs, cards, layout, feedback, and navigation components. - Developed a theme.ts file to centralize typography, spacing, border radius, shadows, z-index, transitions, breakpoints, containers, and responsive utilities. - Configured Nginx for development with a new nginx.dev.conf, routing API requests to the Flask backend and frontend requests to the Vite development server. --- backend/app/routes/account.py | 72 +++ backend/app/services/quota_service.py | 460 ++++++++++++++++ backend/app/utils/auth.py | 22 + docker-compose.yml | 6 +- frontend/public/sitemap.xml | 490 ++++++++--------- .../src/components/shared/ErrorMessage.tsx | 168 ++++++ .../src/components/shared/ProgressBar.tsx | 139 ++++- .../src/components/shared/ToolTemplate.tsx | 226 ++++++++ .../src/components/tools/PdfCompressor.tsx | 164 ++---- frontend/src/design-system/colors.ts | 250 +++++++++ .../src/design-system/components-registry.ts | 512 ++++++++++++++++++ frontend/src/design-system/theme.ts | 296 ++++++++++ nginx/nginx.dev.conf | 40 ++ 13 files changed, 2443 insertions(+), 402 deletions(-) create mode 100644 backend/app/services/quota_service.py create mode 100644 frontend/src/components/shared/ErrorMessage.tsx create mode 100644 frontend/src/components/shared/ToolTemplate.tsx create mode 100644 frontend/src/design-system/colors.ts create mode 100644 frontend/src/design-system/components-registry.ts create mode 100644 frontend/src/design-system/theme.ts create mode 100644 nginx/nginx.dev.conf diff --git a/backend/app/routes/account.py b/backend/app/routes/account.py index 1237c40..c912844 100644 --- a/backend/app/routes/account.py +++ b/backend/app/routes/account.py @@ -1,4 +1,5 @@ """Authenticated account endpoints — usage summary and API key management.""" + from flask import Blueprint, jsonify, request from app.extensions import limiter @@ -9,7 +10,15 @@ from app.services.account_service import ( revoke_api_key, ) from app.services.policy_service import get_usage_summary_for_user +from app.services.stripe_service import ( + is_stripe_configured, + get_stripe_price_id, +) from app.utils.auth import get_current_user_id +import stripe +import logging + +logger = logging.getLogger(__name__) account_bp = Blueprint("account", __name__) @@ -29,6 +38,69 @@ def get_usage_route(): return jsonify(get_usage_summary_for_user(user_id, user["plan"])), 200 +@account_bp.route("/subscription", methods=["GET"]) +@limiter.limit("60/hour") +def get_subscription_status(): + """Return subscription status for the authenticated user.""" + user_id = get_current_user_id() + if user_id is None: + return jsonify({"error": "Authentication required."}), 401 + + user = get_user_by_id(user_id) + if user is None: + return jsonify({"error": "User not found."}), 404 + + # If Stripe is not configured, return basic info + if not is_stripe_configured(): + return jsonify( + { + "plan": user["plan"], + "stripe_configured": False, + "subscription": None, + } + ), 200 + + # Retrieve subscription info from Stripe if available + subscription_info = None + if user.get("stripe_subscription_id"): + try: + from app.services.stripe_service import get_stripe_secret_key + + stripe.api_key = get_stripe_secret_key() + + subscription = stripe.Subscription.retrieve(user["stripe_subscription_id"]) + subscription_info = { + "id": subscription.id, + "status": subscription.status, + "current_period_start": subscription.current_period_start, + "current_period_end": subscription.current_period_end, + "cancel_at_period_end": subscription.cancel_at_period_end, + "items": [ + { + "price": item.price.id, + "quantity": item.quantity, + } + for item in subscription.items.data + ], + } + except Exception as e: + logger.error( + f"Failed to retrieve subscription {user['stripe_subscription_id']}: {e}" + ) + + return jsonify( + { + "plan": user["plan"], + "stripe_configured": True, + "subscription": subscription_info, + "pricing": { + "monthly_price_id": get_stripe_price_id("monthly"), + "yearly_price_id": get_stripe_price_id("yearly"), + }, + } + ), 200 + + @account_bp.route("/api-keys", methods=["GET"]) @limiter.limit("60/hour") def list_api_keys_route(): diff --git a/backend/app/services/quota_service.py b/backend/app/services/quota_service.py new file mode 100644 index 0000000..3b27a63 --- /dev/null +++ b/backend/app/services/quota_service.py @@ -0,0 +1,460 @@ +""" +QuotaService +Manages usage quotas and limits for Free, Pro, and Business tiers + +Quota Structure: +- Free: 5 conversions/day, 10MB max file size, no batch processing +- Pro: 100 conversions/day, 100MB max file size, batch processing (5 files) +- Business: Unlimited conversions, 500MB max file size, batch processing (20 files) + +Tracks: +- Daily usage (resets at UTC midnight) +- Storage usage +- API rate limits +- Feature access (premium features) +""" + +import logging +from datetime import datetime, timedelta +from typing import Dict, Optional, Tuple +from flask import current_app +from app.services.account_service import _connect, _utc_now + +logger = logging.getLogger(__name__) + + +class QuotaLimits: + """Define quota limits for each tier""" + + # Conversions per day + CONVERSIONS_PER_DAY = { + "free": 5, + "pro": 100, + "business": float("inf"), + } + + # Maximum file size in MB + MAX_FILE_SIZE_MB = { + "free": 10, + "pro": 100, + "business": 500, + } + + # Storage limit in MB (monthly) + STORAGE_LIMIT_MB = { + "free": 500, + "pro": 5000, + "business": float("inf"), + } + + # API rate limit (requests per minute) + API_RATE_LIMIT = { + "free": 10, + "pro": 60, + "business": float("inf"), + } + + # Concurrent processing jobs + CONCURRENT_JOBS = { + "free": 1, + "pro": 3, + "business": 10, + } + + # Batch file limit + BATCH_FILE_LIMIT = { + "free": 1, + "pro": 5, + "business": 20, + } + + # Premium features (Pro/Business) + PREMIUM_FEATURES = { + "free": set(), + "pro": {"batch_processing", "priority_queue", "email_delivery", "api_access"}, + "business": { + "batch_processing", + "priority_queue", + "email_delivery", + "api_access", + "webhook", + "sso", + }, + } + + +class QuotaService: + """Service for managing user quotas and usage tracking""" + + @staticmethod + def _ensure_quota_tables(): + """Create quota tracking tables if they don't exist""" + conn = _connect() + try: + # Daily usage tracking + conn.execute(""" + CREATE TABLE IF NOT EXISTS daily_usage ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + date TEXT NOT NULL, + conversions INTEGER DEFAULT 0, + files_processed INTEGER DEFAULT 0, + total_size_mb REAL DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, date), + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + """) + + # Storage usage tracking + conn.execute(""" + CREATE TABLE IF NOT EXISTS storage_usage ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + month TEXT NOT NULL, + total_size_mb REAL DEFAULT 0, + file_count INTEGER DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, month), + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + """) + + # API rate limiting + conn.execute(""" + CREATE TABLE IF NOT EXISTS api_requests ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + endpoint TEXT NOT NULL, + timestamp TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + """) + + # Feature access log + conn.execute(""" + CREATE TABLE IF NOT EXISTS feature_access ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + feature TEXT NOT NULL, + accessed_at TEXT DEFAULT CURRENT_TIMESTAMP, + allowed BOOLEAN DEFAULT 1, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + """) + + conn.commit() + logger.info("Quota tables initialized") + except Exception as e: + logger.error(f"Failed to create quota tables: {e}") + raise + finally: + conn.close() + + @staticmethod + def init_quota_db(): + """Initialize quota tracking database""" + QuotaService._ensure_quota_tables() + + @staticmethod + def get_user_plan(user_id: int) -> str: + """Get user's current plan""" + conn = _connect() + try: + row = conn.execute( + "SELECT plan FROM users WHERE id = ?", (user_id,) + ).fetchone() + return row["plan"] if row else "free" + finally: + conn.close() + + @staticmethod + def get_daily_usage(user_id: int, date: Optional[str] = None) -> Dict: + """ + Get daily usage for a user + + Args: + user_id: User ID + date: Date in YYYY-MM-DD format (defaults to today) + + Returns: + Usage stats dict + """ + if not date: + date = datetime.utcnow().strftime("%Y-%m-%d") + + conn = _connect() + try: + row = conn.execute( + "SELECT * FROM daily_usage WHERE user_id = ? AND date = ?", + (user_id, date), + ).fetchone() + + if not row: + return { + "conversions": 0, + "files_processed": 0, + "total_size_mb": 0, + } + + return dict(row) + finally: + conn.close() + + @staticmethod + def record_conversion(user_id: int, file_size_mb: float) -> bool: + """ + Record a file conversion + + Args: + user_id: User ID + file_size_mb: File size in MB + + Returns: + True if allowed, False if quota exceeded + """ + plan = QuotaService.get_user_plan(user_id) + today = datetime.utcnow().strftime("%Y-%m-%d") + + conn = _connect() + try: + # Check daily conversion limit + usage = conn.execute( + "SELECT conversions FROM daily_usage WHERE user_id = ? AND date = ?", + (user_id, today), + ).fetchone() + + current_conversions = usage["conversions"] if usage else 0 + limit = QuotaLimits.CONVERSIONS_PER_DAY[plan] + + if current_conversions >= limit and limit != float("inf"): + logger.warning(f"User {user_id} exceeded daily conversion limit") + return False + + # Check file size limit + max_size = QuotaLimits.MAX_FILE_SIZE_MB[plan] + if file_size_mb > max_size: + logger.warning( + f"User {user_id} file exceeds size limit: {file_size_mb}MB > {max_size}MB" + ) + return False + + # Record the conversion + conn.execute( + """ + INSERT INTO daily_usage (user_id, date, conversions, files_processed, total_size_mb) + VALUES (?, ?, 1, 1, ?) + ON CONFLICT(user_id, date) DO UPDATE SET + conversions = conversions + 1, + files_processed = files_processed + 1, + total_size_mb = total_size_mb + ? + """, + (user_id, today, file_size_mb, file_size_mb), + ) + conn.commit() + + logger.info(f"Recorded conversion for user {user_id}: {file_size_mb}MB") + return True + except Exception as e: + logger.error(f"Failed to record conversion: {e}") + return False + finally: + conn.close() + + @staticmethod + def check_rate_limit(user_id: int) -> Tuple[bool, int]: + """ + Check if user has exceeded API rate limit + + Args: + user_id: User ID + + Returns: + (allowed, remaining_requests_in_window) tuple + """ + plan = QuotaService.get_user_plan(user_id) + limit = QuotaLimits.API_RATE_LIMIT[plan] + + if limit == float("inf"): + return True, -1 # Unlimited + + # Check requests in last minute + one_minute_ago = (datetime.utcnow() - timedelta(minutes=1)).isoformat() + + conn = _connect() + try: + count = conn.execute( + "SELECT COUNT(*) as count FROM api_requests WHERE user_id = ? AND timestamp > ?", + (user_id, one_minute_ago), + ).fetchone()["count"] + + if count >= limit: + return False, 0 + + # Record this request + conn.execute( + "INSERT INTO api_requests (user_id, endpoint) VALUES (?, ?)", + (user_id, "api"), + ) + conn.commit() + + return True, limit - count - 1 + finally: + conn.close() + + @staticmethod + def has_feature(user_id: int, feature: str) -> bool: + """ + Check if user has access to a premium feature + + Args: + user_id: User ID + feature: Feature name (e.g., 'batch_processing') + + Returns: + True if user can access feature + """ + plan = QuotaService.get_user_plan(user_id) + allowed = feature in QuotaLimits.PREMIUM_FEATURES[plan] + + # Log feature access attempt + conn = _connect() + try: + conn.execute( + "INSERT INTO feature_access (user_id, feature, allowed) VALUES (?, ?, ?)", + (user_id, feature, allowed), + ) + conn.commit() + except Exception as e: + logger.error(f"Failed to log feature access: {e}") + finally: + conn.close() + + return allowed + + @staticmethod + def get_quota_status(user_id: int) -> Dict: + """ + Get comprehensive quota status for a user + + Args: + user_id: User ID + + Returns: + Complete quota status dict + """ + plan = QuotaService.get_user_plan(user_id) + today = datetime.utcnow().strftime("%Y-%m-%d") + + conn = _connect() + try: + # Get daily usage + daily = conn.execute( + "SELECT conversions FROM daily_usage WHERE user_id = ? AND date = ?", + (user_id, today), + ).fetchone() + + conversions_used = daily["conversions"] if daily else 0 + conversions_limit = QuotaLimits.CONVERSIONS_PER_DAY[plan] + + return { + "plan": plan, + "conversions": { + "used": conversions_used, + "limit": conversions_limit, + "remaining": conversions_limit - conversions_used + if conversions_limit != float("inf") + else -1, + "reset_at": (datetime.utcnow() + timedelta(days=1)).replace( + hour=0, minute=0, second=0 + ), + }, + "file_size_limit_mb": QuotaLimits.MAX_FILE_SIZE_MB[plan], + "storage_limit_mb": QuotaLimits.STORAGE_LIMIT_MB[plan], + "api_rate_limit": QuotaLimits.API_RATE_LIMIT[plan], + "concurrent_jobs": QuotaLimits.CONCURRENT_JOBS[plan], + "batch_file_limit": QuotaLimits.BATCH_FILE_LIMIT[plan], + "features": list(QuotaLimits.PREMIUM_FEATURES[plan]), + "can_batch_process": QuotaService.has_feature( + user_id, "batch_processing" + ), + "can_use_api": QuotaService.has_feature(user_id, "api_access"), + "can_schedule_delivery": QuotaService.has_feature( + user_id, "email_delivery" + ), + } + finally: + conn.close() + + @staticmethod + def get_monthly_storage_usage(user_id: int, year: int, month: int) -> float: + """Get storage usage for a specific month""" + month_key = f"{year}-{month:02d}" + + conn = _connect() + try: + row = conn.execute( + "SELECT total_size_mb FROM storage_usage WHERE user_id = ? AND month = ?", + (user_id, month_key), + ).fetchone() + + return row["total_size_mb"] if row else 0 + finally: + conn.close() + + @staticmethod + def upgrade_plan(user_id: int, new_plan: str) -> bool: + """ + Upgrade user to a new plan + + Args: + user_id: User ID + new_plan: New plan ('pro' or 'business') + + Returns: + Success status + """ + if new_plan not in QuotaLimits.CONVERSIONS_PER_DAY: + logger.error(f"Invalid plan: {new_plan}") + return False + + conn = _connect() + try: + conn.execute( + "UPDATE users SET plan = ?, updated_at = ? WHERE id = ?", + (new_plan, _utc_now(), user_id), + ) + conn.commit() + logger.info(f"User {user_id} upgraded to {new_plan}") + return True + except Exception as e: + logger.error(f"Failed to upgrade user plan: {e}") + return False + finally: + conn.close() + + @staticmethod + def downgrade_plan(user_id: int, new_plan: str = "free") -> bool: + """Downgrade user to a lower plan""" + return QuotaService.upgrade_plan(user_id, new_plan) + + +# Convenience functions +def init_quota_db(): + return QuotaService.init_quota_db() + + +def get_quota_status(user_id: int) -> Dict: + return QuotaService.get_quota_status(user_id) + + +def record_conversion(user_id: int, file_size_mb: float) -> bool: + return QuotaService.record_conversion(user_id, file_size_mb) + + +def check_rate_limit(user_id: int) -> Tuple[bool, int]: + return QuotaService.check_rate_limit(user_id) + + +def has_feature(user_id: int, feature: str) -> bool: + return QuotaService.has_feature(user_id, feature) diff --git a/backend/app/utils/auth.py b/backend/app/utils/auth.py index d70f385..c6828a6 100644 --- a/backend/app/utils/auth.py +++ b/backend/app/utils/auth.py @@ -2,7 +2,9 @@ from flask import session TASK_ACCESS_SESSION_KEY = "task_access_ids" +STAGED_UPLOAD_SESSION_KEY = "staged_upload_ids" MAX_TRACKED_TASK_IDS = 200 +MAX_TRACKED_STAGED_UPLOAD_IDS = 50 def get_current_user_id() -> int | None: @@ -28,14 +30,34 @@ def has_session_task_access(task_id: str) -> bool: return isinstance(tracked, list) and task_id in tracked +def remember_staged_upload(upload_id: str): + """Persist one staged upload id in the active browser session.""" + tracked = session.get(STAGED_UPLOAD_SESSION_KEY, []) + if not isinstance(tracked, list): + tracked = [] + + normalized = [value for value in tracked if isinstance(value, str) and value != upload_id] + normalized.append(upload_id) + session[STAGED_UPLOAD_SESSION_KEY] = normalized[-MAX_TRACKED_STAGED_UPLOAD_IDS:] + + +def has_staged_upload_access(upload_id: str) -> bool: + """Return whether the active browser session owns one staged upload id.""" + tracked = session.get(STAGED_UPLOAD_SESSION_KEY, []) + return isinstance(tracked, list) and upload_id in tracked + + def login_user_session(user_id: int): """Persist the authenticated user in the Flask session.""" tracked_task_ids = session.get(TASK_ACCESS_SESSION_KEY, []) + staged_upload_ids = session.get(STAGED_UPLOAD_SESSION_KEY, []) session.clear() session.permanent = True session["user_id"] = user_id if isinstance(tracked_task_ids, list) and tracked_task_ids: session[TASK_ACCESS_SESSION_KEY] = tracked_task_ids[-MAX_TRACKED_TASK_IDS:] + if isinstance(staged_upload_ids, list) and staged_upload_ids: + session[STAGED_UPLOAD_SESSION_KEY] = staged_upload_ids[-MAX_TRACKED_STAGED_UPLOAD_IDS:] def logout_user_session(): diff --git a/docker-compose.yml b/docker-compose.yml index 615400e..9ddc5c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -117,16 +117,14 @@ services: - VITE_SITE_DOMAIN=${VITE_SITE_DOMAIN:-} - VITE_SENTRY_DSN=${VITE_SENTRY_DSN:-} - # --- Nginx Reverse Proxy --- + # --- Nginx Reverse Proxy --- nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - - ./certbot/www:/var/www/certbot:ro - - ./certbot/conf:/etc/letsencrypt:ro + - ./nginx/nginx.dev.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - backend - frontend diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml index be52981..4f794d1 100644 --- a/frontend/public/sitemap.xml +++ b/frontend/public/sitemap.xml @@ -2,1471 +2,1471 @@ https://dociva.io/ - 2026-03-25 + 2026-03-27 daily 1.0 https://dociva.io/about - 2026-03-25 + 2026-03-27 monthly 0.4 https://dociva.io/contact - 2026-03-25 + 2026-03-27 monthly 0.4 https://dociva.io/privacy - 2026-03-25 + 2026-03-27 yearly 0.3 https://dociva.io/terms - 2026-03-25 + 2026-03-27 yearly 0.3 https://dociva.io/pricing - 2026-03-25 + 2026-03-27 monthly 0.7 https://dociva.io/blog - 2026-03-25 + 2026-03-27 weekly 0.6 https://dociva.io/developers - 2026-03-25 + 2026-03-27 monthly 0.5 https://dociva.io/blog/how-to-compress-pdf-online - 2026-03-25 + 2026-03-27 monthly 0.6 https://dociva.io/blog/convert-images-without-losing-quality - 2026-03-25 + 2026-03-27 monthly 0.6 https://dociva.io/blog/ocr-extract-text-from-images - 2026-03-25 + 2026-03-27 monthly 0.6 https://dociva.io/blog/merge-split-pdf-files - 2026-03-25 + 2026-03-27 monthly 0.6 https://dociva.io/blog/ai-chat-with-pdf-documents - 2026-03-25 + 2026-03-27 monthly 0.6 https://dociva.io/tools/pdf-to-word - 2026-03-25 + 2026-03-27 weekly 0.9 https://dociva.io/tools/word-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.9 https://dociva.io/tools/compress-pdf - 2026-03-25 + 2026-03-27 weekly 0.9 https://dociva.io/tools/merge-pdf - 2026-03-25 + 2026-03-27 weekly 0.9 https://dociva.io/tools/split-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/rotate-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/pdf-to-images - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/images-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/watermark-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/protect-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/unlock-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/page-numbers - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/pdf-editor - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/pdf-flowchart - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/pdf-to-excel - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/remove-watermark-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/reorder-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/extract-pages - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/image-converter - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/image-resize - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/compress-image - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/ocr - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/remove-background - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/image-to-svg - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/html-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/chat-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/summarize-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/translate-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/extract-tables - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/qr-code - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/video-to-gif - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/word-counter - 2026-03-25 + 2026-03-27 weekly 0.6 https://dociva.io/tools/text-cleaner - 2026-03-25 + 2026-03-27 weekly 0.6 https://dociva.io/tools/pdf-to-pptx - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/excel-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/pptx-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/sign-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/crop-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/flatten-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/repair-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/pdf-metadata - 2026-03-25 + 2026-03-27 weekly 0.6 https://dociva.io/tools/image-crop - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/image-rotate-flip - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/barcode-generator - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/pdf-to-word - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-word - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/word-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/word-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-jpg-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-jpg-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/merge-pdf-files - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/merge-pdf-files - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/remove-pdf-password - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/remove-pdf-password - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-word-editable - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-word-editable - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-pdf-to-text - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-pdf-to-text - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/split-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/split-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/jpg-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/jpg-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/png-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/png-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/images-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/images-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-jpg - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-jpg - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-png - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-png - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-pdf-for-email - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-pdf-for-email - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-scanned-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-scanned-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/merge-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/merge-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/combine-pdf-files - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/combine-pdf-files - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/extract-pages-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/extract-pages-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/reorder-pdf-pages - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/reorder-pdf-pages - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/rotate-pdf-pages - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/rotate-pdf-pages - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/add-page-numbers-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/add-page-numbers-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/protect-pdf-with-password - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/protect-pdf-with-password - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/unlock-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/unlock-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/watermark-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/watermark-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/remove-watermark-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/remove-watermark-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/edit-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/edit-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-excel-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-excel-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/extract-tables-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/extract-tables-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/html-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/html-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/scan-pdf-to-text - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/scan-pdf-to-text - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/chat-with-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/chat-with-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/summarize-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/summarize-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/translate-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/translate-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-image-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-image-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-webp-to-jpg - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-webp-to-jpg - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/resize-image-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/resize-image-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-image-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-image-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/remove-image-background - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/remove-image-background - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-word-editable-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-word-editable-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-pdf-to-100kb - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-pdf-to-100kb - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ai-extract-text-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ai-extract-text-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-excel-accurate-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-excel-accurate-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/split-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/split-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/unlock-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/unlock-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/summarize-pdf-ai - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/summarize-pdf-ai - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-pdf-to-text-ai - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-pdf-to-text-ai - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-jpg-high-quality - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-jpg-high-quality - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/jpg-to-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/jpg-to-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/reduce-pdf-size-for-email - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/reduce-pdf-size-for-email - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ocr-for-scanned-pdfs - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ocr-for-scanned-pdfs - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/remove-watermark-from-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/remove-watermark-from-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/add-watermark-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/add-watermark-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/repair-corrupted-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/repair-corrupted-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/rotate-pdf-pages-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/rotate-pdf-pages-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/reorder-pdf-pages-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/reorder-pdf-pages-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-png-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-png-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/images-to-pdf-multiple - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/images-to-pdf-multiple - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/split-pdf-by-range-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/split-pdf-by-range-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-scanned-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-scanned-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-metadata-editor-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-metadata-editor-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/add-page-numbers-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/add-page-numbers-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/protect-pdf-with-password-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/protect-pdf-with-password-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/unlock-encrypted-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/unlock-encrypted-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ocr-table-extraction-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ocr-table-extraction-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-excel-converter-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-excel-converter-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/extract-text-from-protected-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/extract-text-from-protected-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/bulk-convert-pdf-to-word - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/bulk-convert-pdf-to-word - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-pdf-for-web-upload - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-pdf-for-web-upload - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ocr-multi-language-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ocr-multi-language-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/summarize-long-pdf-ai - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/summarize-long-pdf-ai - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-pdf-to-ppt-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-pdf-to-ppt-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-pptx-free-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-pptx-free-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/دمج-ملفات-pdf-مجاناً - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/دمج-ملفات-pdf-مجاناً - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ضغط-بي-دي-اف-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ضغط-بي-دي-اف-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/تحويل-jpg-الى-pdf-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/تحويل-jpg-الى-pdf-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/فصل-صفحات-pdf-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/فصل-صفحات-pdf-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ازالة-كلمة-مرور-من-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ازالة-كلمة-مرور-من-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/تحويل-pdf-الى-excel-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-excel-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/تحويل-pdf-الى-صور - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-صور - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/best-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/best-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/free-pdf-tools-online - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/free-pdf-tools-online - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/convert-files-online - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/convert-files-online - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/pdf-converter-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/pdf-converter-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/secure-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/secure-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/ai-document-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/ai-document-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/image-to-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/image-to-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/online-image-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/online-image-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/office-to-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/office-to-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/scanned-document-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/scanned-document-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/arabic-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/arabic-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.74 diff --git a/frontend/src/components/shared/ErrorMessage.tsx b/frontend/src/components/shared/ErrorMessage.tsx new file mode 100644 index 0000000..9a0095b --- /dev/null +++ b/frontend/src/components/shared/ErrorMessage.tsx @@ -0,0 +1,168 @@ +import React, { useState } from 'react'; +import { AlertCircle, RefreshCw, HelpCircle, AlertTriangle } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; + +export interface ErrorMessageProps { + message?: string; + type?: 'error' | 'warning' | 'info'; + details?: string; + showDetails?: boolean; + onRetry?: () => void; + showRetry?: boolean; + actions?: Array<{ + label: string; + onClick: () => void; + variant?: 'primary' | 'secondary'; + }>; + suggestion?: string; + helpLink?: string; + dismissible?: boolean; + onDismiss?: () => void; +} + +export default function ErrorMessage({ + message = 'An error occurred', + type = 'error', + details, + showDetails: initialShowDetails = false, + onRetry, + showRetry = true, + actions, + suggestion, + helpLink, + dismissible = true, + onDismiss, +}: ErrorMessageProps) { + const { t } = useTranslation(); + const [showDetails, setShowDetails] = useState(initialShowDetails); + const [isDismissed, setIsDismissed] = useState(false); + + if (isDismissed) return null; + + const bgColor = + type === 'error' + ? 'bg-red-50 dark:bg-red-900/20' + : type === 'warning' + ? 'bg-amber-50 dark:bg-amber-900/20' + : 'bg-blue-50 dark:bg-blue-900/20'; + + const borderColor = + type === 'error' + ? 'border-red-200 dark:border-red-800' + : type === 'warning' + ? 'border-amber-200 dark:border-amber-800' + : 'border-blue-200 dark:border-blue-800'; + + const textColor = + type === 'error' + ? 'text-red-900 dark:text-red-200' + : type === 'warning' + ? 'text-amber-900 dark:text-amber-200' + : 'text-blue-900 dark:text-blue-200'; + + const iconColor = + type === 'error' + ? 'text-red-600 dark:text-red-400' + : type === 'warning' + ? 'text-amber-600 dark:text-amber-400' + : 'text-blue-600 dark:text-blue-400'; + + const Icon = type === 'warning' ? AlertTriangle : AlertCircle; + + return ( +
+
+
+
+ ); +} diff --git a/frontend/src/components/shared/ProgressBar.tsx b/frontend/src/components/shared/ProgressBar.tsx index 9f44ef3..58ec3d1 100644 --- a/frontend/src/components/shared/ProgressBar.tsx +++ b/frontend/src/components/shared/ProgressBar.tsx @@ -1,40 +1,137 @@ import { useTranslation } from 'react-i18next'; -import { Loader2, CheckCircle2 } from 'lucide-react'; +import { Loader2, CheckCircle2, AlertCircle, Clock } from 'lucide-react'; interface ProgressBarProps { /** Current task state */ - state: 'PENDING' | 'PROCESSING' | 'SUCCESS' | 'FAILURE' | string; + state?: 'PENDING' | 'PROCESSING' | 'SUCCESS' | 'FAILURE' | string; + status?: string; // Alternative to state (for compatibility) /** Progress message */ message?: string; + /** Progress percentage (0-100) */ + progress?: number; + /** Show detailed steps */ + steps?: Array<{ + name: string; + status: 'pending' | 'active' | 'complete' | 'error'; + message?: string; + }>; + /** Show a simple indeterminate progress bar */ + indeterminate?: boolean; } -export default function ProgressBar({ state, message }: ProgressBarProps) { +export default function ProgressBar({ + state, + status, + message, + progress, + steps, + indeterminate = true, +}: ProgressBarProps) { const { t } = useTranslation(); + const taskState = state || status || 'PROCESSING'; - const isActive = state === 'PENDING' || state === 'PROCESSING'; - const isComplete = state === 'SUCCESS'; + const isActive = taskState === 'PENDING' || taskState === 'PROCESSING'; + const isComplete = taskState === 'SUCCESS'; + const isError = taskState === 'FAILURE'; return ( -
-
- {isActive && ( - - )} - {isComplete && ( - +
+ {/* Main Progress Card */} +
+
+ {isActive && ( + + )} + {isComplete && ( + + )} + {isError && ( + + )} + {!isActive && !isComplete && !isError && ( + + )} + +
+

+ {message || t('common.processing', { defaultValue: 'Processing...' })} +

+ {progress !== undefined && ( +

+ {progress}% +

+ )} +
+
+ + {/* Progress Bar */} + {indeterminate && isActive && ( +
+
+
)} -
-

- {message || t('common.processing')} -

-
+ {/* Determinate Progress Bar */} + {!indeterminate && progress !== undefined && ( +
+
+
+
+
+ )}
- {/* Animated progress bar for active states */} - {isActive && ( -
-
+ {/* Step-by-Step Progress */} + {steps && steps.length > 0 && ( +
+

+ {t('common.processingSteps', { defaultValue: 'Processing Steps' })} +

+
+ {steps.map((step, idx) => ( +
+
+ {step.status === 'complete' && ( + + )} + {step.status === 'active' && ( + + )} + {step.status === 'error' && ( + + )} + {step.status === 'pending' && ( +
+ )} +
+
+

+ {step.name} +

+ {step.message && ( +

+ {step.message} +

+ )} +
+
+ ))} +
)}
diff --git a/frontend/src/components/shared/ToolTemplate.tsx b/frontend/src/components/shared/ToolTemplate.tsx new file mode 100644 index 0000000..16fe662 --- /dev/null +++ b/frontend/src/components/shared/ToolTemplate.tsx @@ -0,0 +1,226 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Helmet } from 'react-helmet-async'; +import { LucideIcon, AlertCircle, CheckCircle, Clock } from 'lucide-react'; +import FileUploader from '@/components/shared/FileUploader'; +import ProgressBar from '@/components/shared/ProgressBar'; +import DownloadButton from '@/components/shared/DownloadButton'; +import AdSlot from '@/components/layout/AdSlot'; +import { useFileUpload } from '@/hooks/useFileUpload'; +import { useTaskPolling } from '@/hooks/useTaskPolling'; +import { generateToolSchema } from '@/utils/seo'; +import { useFileStore } from '@/stores/fileStore'; + +export interface ToolConfig { + slug: string; + icon: LucideIcon; + color?: 'orange' | 'red' | 'blue' | 'green' | 'purple' | 'pink' | 'amber' | 'cyan'; + i18nKey: string; + endpoint: string; + maxSizeMB?: number; + acceptedTypes?: string[]; + isPremium?: boolean; + extraData?: Record; +} + +export interface ToolTemplateProps { + file: File | null; + uploadProgress: number; + isUploading: boolean; + isProcessing: boolean; + result: any; + error: string | null; + selectFile: (file: File) => void; + reset: () => void; +} + +const colorMap: Record = { + orange: { bg: 'bg-orange-50 dark:bg-orange-900/20', icon: 'text-orange-600 dark:text-orange-400' }, + red: { bg: 'bg-red-50 dark:bg-red-900/20', icon: 'text-red-600 dark:text-red-400' }, + blue: { bg: 'bg-blue-50 dark:bg-blue-900/20', icon: 'text-blue-600 dark:text-blue-400' }, + green: { bg: 'bg-green-50 dark:bg-green-900/20', icon: 'text-green-600 dark:text-green-400' }, + purple: { bg: 'bg-purple-50 dark:bg-purple-900/20', icon: 'text-purple-600 dark:text-purple-400' }, + pink: { bg: 'bg-pink-50 dark:bg-pink-900/20', icon: 'text-pink-600 dark:text-pink-400' }, + amber: { bg: 'bg-amber-50 dark:bg-amber-900/20', icon: 'text-amber-600 dark:text-amber-400' }, + cyan: { bg: 'bg-cyan-50 dark:bg-cyan-900/20', icon: 'text-cyan-600 dark:text-cyan-400' }, +}; + +interface ToolTemplateComponentProps { + config: ToolConfig; + onGetExtraData?: () => Record; + children?: (props: ToolTemplateProps) => React.ReactNode; +} + +export default function ToolTemplate({ config, onGetExtraData, children }: ToolTemplateComponentProps) { + const { t } = useTranslation(); + const [phase, setPhase] = useState<'upload' | 'processing' | 'done'>('upload'); + const [extraData, setExtraData] = useState>(config.extraData || {}); + + const colors = colorMap[config.color || 'blue']; + const bgColor = colors.bg; + const iconColor = colors.icon; + + const { file, uploadProgress, isUploading, taskId, error, selectFile, startUpload, reset } = useFileUpload({ + endpoint: config.endpoint, + maxSizeMB: config.maxSizeMB || 20, + acceptedTypes: config.acceptedTypes || ['pdf'], + extraData, + }); + + const { status, result } = useTaskPolling({ + taskId, + onComplete: () => setPhase('done'), + onError: () => setPhase('done'), + }); + + const storeFile = useFileStore((s) => s.file); + const clearStoreFile = useFileStore((s) => s.clearFile); + + useEffect(() => { + if (storeFile && config.acceptedTypes?.some((type) => storeFile.name.endsWith(`.${type}`))) { + selectFile(storeFile); + clearStoreFile(); + setPhase('upload'); + } + }, []); + + const handleUpload = useCallback(async () => { + // Get fresh extraData from child if callback provided + if (onGetExtraData) { + const freshExtraData = onGetExtraData(); + setExtraData(freshExtraData); + } + const id = await startUpload(); + if (id) setPhase('processing'); + }, [onGetExtraData, startUpload]); + + const handleReset = () => { + reset(); + setPhase('upload'); + }; + + const title = t(`${config.i18nKey}.title`, { defaultValue: config.slug }); + const description = t(`${config.i18nKey}.description`, { defaultValue: '' }); + + const schema = generateToolSchema({ + name: title, + description, + url: `${window.location.origin}/tools/${config.slug}`, + }); + + const templateProps: ToolTemplateProps = { + file, + uploadProgress, + isUploading, + isProcessing: phase === 'processing', + result, + error, + selectFile, + reset: handleReset, + }; + + return ( + <> + + {title} — Dociva + + + + + +
+
+
+
+

{title}

+

{description}

+
+ + + +
+ {phase === 'upload' && ( +
+ { + selectFile(f); + setPhase('upload'); + }} + file={file} + accept={config.acceptedTypes?.reduce( + (acc, type) => ({ + ...acc, + [`application/${type}`]: [`.${type}`], + }), + {} + ) || {}} + /> + + {children && ( +
{children(templateProps)}
+ )} + + +
+ )} + + {phase === 'processing' && ( +
+ +
+ )} + + {phase === 'done' && ( +
+ {result ? ( + <> +
+
+ +
+

Success!

+

Your file is ready

+
+
+
+ + + + ) : ( +
+
+ +
+

Error

+

{error || 'Processing failed'}

+
+
+
+ )} + + +
+ )} +
+ + +
+ + ); +} diff --git a/frontend/src/components/tools/PdfCompressor.tsx b/frontend/src/components/tools/PdfCompressor.tsx index 19e3d4e..b2aea33 100644 --- a/frontend/src/components/tools/PdfCompressor.tsx +++ b/frontend/src/components/tools/PdfCompressor.tsx @@ -1,63 +1,22 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Helmet } from 'react-helmet-async'; import { Minimize2 } from 'lucide-react'; -import FileUploader from '@/components/shared/FileUploader'; -import ProgressBar from '@/components/shared/ProgressBar'; -import DownloadButton from '@/components/shared/DownloadButton'; -import AdSlot from '@/components/layout/AdSlot'; -import { useFileUpload } from '@/hooks/useFileUpload'; -import { useTaskPolling } from '@/hooks/useTaskPolling'; -import { generateToolSchema } from '@/utils/seo'; -import { useFileStore } from '@/stores/fileStore'; +import ToolTemplate, { ToolConfig, ToolTemplateProps } from '@/components/shared/ToolTemplate'; type Quality = 'low' | 'medium' | 'high'; export default function PdfCompressor() { const { t } = useTranslation(); - const [phase, setPhase] = useState<'upload' | 'processing' | 'done'>('upload'); const [quality, setQuality] = useState('medium'); - const { - file, - uploadProgress, - isUploading, - taskId, - error: uploadError, - selectFile, - startUpload, - reset, - } = useFileUpload({ + const toolConfig: ToolConfig = { + slug: 'compress-pdf', + icon: Minimize2, + color: 'orange', + i18nKey: 'tools.compressPdf', endpoint: '/compress/pdf', maxSizeMB: 20, acceptedTypes: ['pdf'], - extraData: { quality }, - }); - - const { status, result, error: taskError } = useTaskPolling({ - taskId, - onComplete: () => setPhase('done'), - onError: () => setPhase('done'), - }); - - // Accept file from homepage smart upload - const storeFile = useFileStore((s) => s.file); - const clearStoreFile = useFileStore((s) => s.clearFile); - useEffect(() => { - if (storeFile) { - selectFile(storeFile); - clearStoreFile(); - } - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - const handleUpload = async () => { - const id = await startUpload(); - if (id) setPhase('processing'); - }; - - const handleReset = () => { - reset(); - setPhase('upload'); }; const qualityOptions: { value: Quality; label: string; desc: string }[] = [ @@ -66,94 +25,35 @@ export default function PdfCompressor() { { value: 'high', label: t('tools.compressPdf.qualityHigh'), desc: '300 DPI' }, ]; - const schema = generateToolSchema({ - name: t('tools.compressPdf.title'), - description: t('tools.compressPdf.description'), - url: `${window.location.origin}/tools/compress-pdf`, - }); + const getExtraData = () => ({ quality }); return ( - <> - - {t('tools.compressPdf.title')} — {t('common.appName')} - - - - - -
-
-
- -
-

{t('tools.compressPdf.title')}

-

{t('tools.compressPdf.description')}

-
- - - - {phase === 'upload' && ( -
- - - {/* Quality Selector */} - {file && !isUploading && ( - <> -
- {qualityOptions.map((opt) => ( - - ))} -
- - - )} -
- )} - - {phase === 'processing' && !result && ( - - )} - - {phase === 'done' && result && result.status === 'completed' && ( - - )} - - {phase === 'done' && taskError && ( -
-
-

{taskError}

+ ))}
-
- )} - - -
- +
+ )} + ); } diff --git a/frontend/src/design-system/colors.ts b/frontend/src/design-system/colors.ts new file mode 100644 index 0000000..72acae6 --- /dev/null +++ b/frontend/src/design-system/colors.ts @@ -0,0 +1,250 @@ +/** + * Color Design System for Dociva + * Comprehensive color palette matching competitive standards + * References: PDFSimpli (bright blues), Smallpdf (modern purple), ILovePDF (bold oranges) + */ + +export const colors = { + // Primary Palette (Main brand color - Blue) + primary: { + 50: '#eff6ff', + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', // Primary brand color (buttons, links, highlights) + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + 950: '#172554', + }, + + // Accent Palette (Secondary accent - Purple/Fuchsia for CTAs) + accent: { + 50: '#fdf4ff', + 100: '#fae8ff', + 200: '#f5d0fe', + 300: '#f0abfc', + 400: '#e879f9', + 500: '#d946ef', + 600: '#c026d3', // Accent for premium tier, special offers + 700: '#a21caf', + 800: '#86198f', + 900: '#701a75', + }, + + // Success Palette (For positive feedback, completed actions) + success: { + 50: '#f0fdf4', + 100: '#dcfce7', + 200: '#bbf7d0', + 300: '#86efac', + 400: '#4ade80', + 500: '#22c55e', + 600: '#16a34a', // Success button/feedback + 700: '#15803d', + 800: '#166534', + 900: '#145231', + }, + + // Warning Palette (For alerts, warnings, secondary actions) + warning: { + 50: '#fffbeb', + 100: '#fef3c7', + 200: '#fde68a', + 300: '#fcd34d', + 400: '#fbbf24', + 500: '#f59e0b', + 600: '#d97706', // Warning alerts + 700: '#b45309', + 800: '#92400e', + 900: '#78350f', + }, + + // Error Palette (For errors, destructive actions) + error: { + 50: '#fef2f2', + 100: '#fee2e2', + 200: '#fecaca', + 300: '#fca5a5', + 400: '#f87171', + 500: '#ef4444', + 600: '#dc2626', // Error states + 700: '#b91c1c', + 800: '#991b1b', + 900: '#7f1d1d', + }, + + // Info Palette (For informational messages) + info: { + 50: '#ecf0ff', + 100: '#e0e7ff', + 200: '#c7d2fe', + 300: '#a5b4fc', + 400: '#818cf8', + 500: '#6366f1', + 600: '#4f46e5', // Info messages + 700: '#4338ca', + 800: '#3730a3', + 900: '#312e81', + }, + + // Neutral Grayscale (For text, borders, backgrounds) + neutral: { + 50: '#fafafa', + 100: '#f5f5f5', + 150: '#efefef', // Custom: between 100 and 200 + 200: '#e5e5e5', + 300: '#d4d4d4', + 400: '#a3a3a3', + 500: '#737373', + 600: '#525252', + 700: '#404040', + 800: '#262626', + 900: '#171717', + 950: '#0a0a0a', + }, + + // Slate Grayscale (Alternative neutral - used in current design) + slate: { + 50: '#f8fafc', + 100: '#f1f5f9', + 200: '#e2e8f0', + 300: '#cbd5e1', + 400: '#94a3b8', + 500: '#64748b', + 600: '#475569', + 700: '#334155', + 800: '#1e293b', + 900: '#0f172a', + 950: '#020617', + }, + + // Semantic Colors (Light mode) + light: { + background: '#ffffff', + surface: '#f8fafc', + surfaceHover: '#f1f5f9', + text: '#0f172a', + textSecondary: '#64748b', + textTertiary: '#94a3b8', + border: '#e2e8f0', + borderHover: '#cbd5e1', + }, + + // Semantic Colors (Dark mode) + dark: { + background: '#0f172a', + surface: '#1e293b', + surfaceHover: '#334155', + text: '#f1f5f9', + textSecondary: '#94a3b8', + textTertiary: '#64748b', + border: '#334155', + borderHover: '#475569', + }, + + // Tool Category Colors (for visual differentiation) + tools: { + pdf: '#dc2626', // Red for PDF tools + image: '#f59e0b', // Amber for image tools + video: '#06b6d4', // Cyan for video tools + document: '#3b82f6', // Blue for document tools + text: '#8b5cf6', // Violet for text tools + convert: '#ec4899', // Pink for conversion tools + edit: '#10b981', // Emerald for editing tools + secure: '#f97316', // Orange for security tools + }, + + // Premium Gradient (for Pro/Business badges) + gradients: { + premium: { + from: '#f59e0b', + to: '#d97706', + }, + business: { + from: '#8b5cf6', + to: '#6366f1', + }, + featured: { + from: '#06b6d4', + to: '#0ea5e9', + }, + }, +} as const; + +/** + * Color Assignments for UI Elements + * These are semantic usage guidelines + */ +export const colorAssignments = { + // Button Colors + buttons: { + primary: 'primary-600', // Main CTAs + secondary: 'slate-600', // Secondary actions + success: 'success-600', // Confirm/accept + danger: 'error-600', // Delete/destructive + ghost: 'slate-500', // Tertiary/icon buttons + }, + + // Badge/Pill Colors + badges: { + default: 'slate-100', + success: 'success-100', + warning: 'warning-100', + error: 'error-100', + info: 'info-100', + pro: 'accent-100', + }, + + // Alert/Toast Colors + alerts: { + success: 'success-600', + warning: 'warning-600', + error: 'error-600', + info: 'info-600', + }, + + // Text Colors + text: { + primary: 'slate-900', + secondary: 'slate-600', + tertiary: 'slate-500', + muted: 'slate-400', + link: 'primary-600', + linkHover: 'primary-700', + }, + + // Border Colors + borders: { + default: 'slate-200', + focus: 'primary-500', + error: 'error-400', + success: 'success-400', + }, + + // Background Colors + backgrounds: { + page: 'white', + surface: 'slate-50', + surfaceAlt: 'slate-100', + overlay: 'rgba(0, 0, 0, 0.5)', + }, +} as const; + +/** + * Utility function to get color with proper contrast + * @param colorName - The color name (e.g., 'primary', 'error') + * @param lightValue - Shade number for light mode (e.g., 600) + * @param darkValue - Shade number for dark mode (e.g., 500) + */ +export function getColorClass( + colorName: keyof typeof colors, + lightValue: number = 600, + darkValue: number = 500 +): string { + return `${colorName}-${lightValue} dark:${colorName}-${darkValue}`; +} + +export default colors; diff --git a/frontend/src/design-system/components-registry.ts b/frontend/src/design-system/components-registry.ts new file mode 100644 index 0000000..a76e1e7 --- /dev/null +++ b/frontend/src/design-system/components-registry.ts @@ -0,0 +1,512 @@ +/** + * Component Registry & System + * Central registry of all UI components with metadata and styling + */ + +import { LucideIcon } from 'lucide-react'; + +/** + * Component Type Definitions + */ +export interface ComponentMetadata { + name: string; + description: string; + category: 'button' | 'input' | 'card' | 'layout' | 'overlay' | 'feedback' | 'navigation' | 'form'; + variants?: string[]; + props?: Record; + a11y?: { + role?: string; + ariaLabel?: string; + ariaDescribedBy?: string; + }; +} + +export interface ComponentColors { + light: { + bg: string; + text: string; + border: string; + hover?: string; + }; + dark: { + bg: string; + text: string; + border: string; + hover?: string; + }; +} + +export interface ToolCardMetadata { + name: string; + slug: string; + icon: string; // Lucide icon name + category: 'pdf' | 'image' | 'video' | 'document' | 'text' | 'convert' | 'edit' | 'secure'; + colorBg: string; // Tailwind bg class + colorIcon: string; // Tailwind text color class + description: string; + i18nKey: string; + isPremium?: boolean; + isNew?: boolean; + isPopular?: boolean; + orderPriority: number; // 1 = highest priority on homepage +} + +/** + * UI Component Registry + */ +export const componentRegistry: Record = { + // Buttons + button: { + name: 'Button', + description: 'Primary interactive element', + category: 'button', + variants: ['primary', 'secondary', 'success', 'danger', 'ghost', 'icon'], + }, + buttonPrimary: { + name: 'Primary Button', + description: 'Main call-to-action button', + category: 'button', + a11y: { role: 'button' }, + }, + buttonSecondary: { + name: 'Secondary Button', + description: 'Alternative action button', + category: 'button', + }, + buttonIcon: { + name: 'Icon Button', + description: 'Icon-only interactive button', + category: 'button', + a11y: { role: 'button', ariaLabel: 'Required for icon buttons' }, + }, + + // Inputs & Forms + input: { + name: 'Text Input', + description: 'Single-line text input field', + category: 'input', + }, + inputFile: { + name: 'File Input', + description: 'File upload field', + category: 'input', + }, + fileUploader: { + name: 'File Uploader', + description: 'Drag-and-drop file upload zone', + category: 'input', + }, + formSelect: { + name: 'Select Dropdown', + description: 'Option selection dropdown', + category: 'form', + }, + formCheckbox: { + name: 'Checkbox', + description: 'Multi-select checkbox input', + category: 'form', + }, + formRadio: { + name: 'Radio Button', + description: 'Single-select radio input', + category: 'form', + }, + formToggle: { + name: 'Toggle Switch', + description: 'On/off toggle switch', + category: 'form', + }, + + // Cards & Containers + card: { + name: 'Card', + description: 'Elevation container with padding', + category: 'card', + }, + toolCard: { + name: 'Tool Card', + description: 'Tool preview card for homepage grid', + category: 'card', + }, + pricingCard: { + name: 'Pricing Card', + description: 'Subscription plan card', + category: 'card', + }, + + // Layout + header: { + name: 'Header', + description: 'Application header with navigation', + category: 'layout', + }, + footer: { + name: 'Footer', + description: 'Application footer', + category: 'layout', + }, + sidebar: { + name: 'Sidebar', + description: 'Side navigation panel', + category: 'layout', + }, + container: { + name: 'Container', + description: 'Max-width wrapper', + category: 'layout', + }, + + // Feedback + alert: { + name: 'Alert', + description: 'Alert message container', + category: 'feedback', + variants: ['success', 'warning', 'error', 'info'], + }, + badge: { + name: 'Badge', + description: 'Small labeling component', + category: 'feedback', + }, + progressBar: { + name: 'Progress Bar', + description: 'Linear progress indicator', + category: 'feedback', + }, + spinner: { + name: 'Spinner', + description: 'Loading spinner indicator', + category: 'feedback', + }, + toast: { + name: 'Toast', + description: 'Temporary notification message', + category: 'feedback', + }, + + // Overlay & Navigation + modal: { + name: 'Modal Dialog', + description: 'Modal dialog overlay', + category: 'overlay', + }, + dropdown: { + name: 'Dropdown Menu', + description: 'Dropdown menu with options', + category: 'navigation', + }, + tabs: { + name: 'Tabs', + description: 'Tabbed content navigation', + category: 'navigation', + }, + breadcrumb: { + name: 'Breadcrumb', + description: 'Breadcrumb navigation trail', + category: 'navigation', + }, + pagination: { + name: 'Pagination', + description: 'Page navigation controls', + category: 'navigation', + }, +}; + +/** + * Tool Categories with Colors + */ +export const toolCategories = { + pdf: { + name: 'PDF Tools', + color: 'red-600', + bgLight: 'bg-red-50', + bgDark: 'dark:bg-red-900/20', + borderColor: 'border-red-200 dark:border-red-800', + }, + image: { + name: 'Image Tools', + color: 'amber-600', + bgLight: 'bg-amber-50', + bgDark: 'dark:bg-amber-900/20', + borderColor: 'border-amber-200 dark:border-amber-800', + }, + video: { + name: 'Video Tools', + color: 'cyan-600', + bgLight: 'bg-cyan-50', + bgDark: 'dark:bg-cyan-900/20', + borderColor: 'border-cyan-200 dark:border-cyan-800', + }, + document: { + name: 'Document Tools', + color: 'blue-600', + bgLight: 'bg-blue-50', + bgDark: 'dark:bg-blue-900/20', + borderColor: 'border-blue-200 dark:border-blue-800', + }, + text: { + name: 'Text Tools', + color: 'violet-600', + bgLight: 'bg-violet-50', + bgDark: 'dark:bg-violet-900/20', + borderColor: 'border-violet-200 dark:border-violet-800', + }, + convert: { + name: 'Conversion Tools', + color: 'pink-600', + bgLight: 'bg-pink-50', + bgDark: 'dark:bg-pink-900/20', + borderColor: 'border-pink-200 dark:border-pink-800', + }, + edit: { + name: 'Editing Tools', + color: 'emerald-600', + bgLight: 'bg-emerald-50', + bgDark: 'dark:bg-emerald-900/20', + borderColor: 'border-emerald-200 dark:border-emerald-800', + }, + secure: { + name: 'Security Tools', + color: 'orange-600', + bgLight: 'bg-orange-50', + bgDark: 'dark:bg-orange-900/20', + borderColor: 'border-orange-200 dark:border-orange-800', + }, +} as const; + +/** + * Complete Tool Registry + * This should be updated to include ALL 40+ tools + */ +export const toolRegistry: Record = { + pdfCompressor: { + name: 'Compress PDF', + slug: 'compress-pdf', + icon: 'Minimize2', + category: 'pdf', + colorBg: 'bg-red-50 dark:bg-red-900/20', + colorIcon: 'text-red-600 dark:text-red-400', + description: 'Reduce PDF file size without losing quality', + i18nKey: 'tools.compressPdf.title', + isPopular: true, + orderPriority: 1, + }, + pdfToWord: { + name: 'PDF to Word', + slug: 'pdf-to-word', + icon: 'FileText', + category: 'convert', + colorBg: 'bg-pink-50 dark:bg-pink-900/20', + colorIcon: 'text-pink-600 dark:text-pink-400', + description: 'Convert PDF documents to editable Word files', + i18nKey: 'tools.pdfToWord.title', + isPopular: true, + orderPriority: 2, + }, + wordToPdf: { + name: 'Word to PDF', + slug: 'word-to-pdf', + icon: 'FilePdf', + category: 'convert', + colorBg: 'bg-blue-50 dark:bg-blue-900/20', + colorIcon: 'text-blue-600 dark:text-blue-400', + description: 'Convert Word documents to PDF format', + i18nKey: 'tools.wordToPdf.title', + isPopular: true, + orderPriority: 3, + }, + mergePdf: { + name: 'Merge PDF', + slug: 'merge-pdf', + icon: 'Layers', + category: 'pdf', + colorBg: 'bg-violet-50 dark:bg-violet-900/20', + colorIcon: 'text-violet-600 dark:text-violet-400', + description: 'Combine multiple PDF files into one', + i18nKey: 'tools.mergePdf.title', + isPopular: true, + orderPriority: 4, + }, + splitPdf: { + name: 'Split PDF', + slug: 'split-pdf', + icon: 'Scissors', + category: 'pdf', + colorBg: 'bg-pink-50 dark:bg-pink-900/20', + colorIcon: 'text-pink-600 dark:text-pink-400', + description: 'Extract pages or split PDF into separate files', + i18nKey: 'tools.splitPdf.title', + orderPriority: 5, + }, + rotatePdf: { + name: 'Rotate PDF', + slug: 'rotate-pdf', + icon: 'RotateCw', + category: 'edit', + colorBg: 'bg-teal-50 dark:bg-teal-900/20', + colorIcon: 'text-teal-600 dark:text-teal-400', + description: 'Rotate PDF pages at any angle', + i18nKey: 'tools.rotatePdf.title', + orderPriority: 6, + }, + pdfToImages: { + name: 'PDF to Images', + slug: 'pdf-to-images', + icon: 'Image', + category: 'convert', + colorBg: 'bg-amber-50 dark:bg-amber-900/20', + colorIcon: 'text-amber-600 dark:text-amber-400', + description: 'Convert PDF pages to individual image files', + i18nKey: 'tools.pdfToImages.title', + orderPriority: 7, + }, + imagesToPdf: { + name: 'Images to PDF', + slug: 'images-to-pdf', + icon: 'FileImage', + category: 'convert', + colorBg: 'bg-lime-50 dark:bg-lime-900/20', + colorIcon: 'text-lime-600 dark:text-lime-400', + description: 'Combine images into a single PDF file', + i18nKey: 'tools.imagesToPdf.title', + orderPriority: 8, + }, + watermarkPdf: { + name: 'Watermark PDF', + slug: 'watermark-pdf', + icon: 'Droplets', + category: 'edit', + colorBg: 'bg-cyan-50 dark:bg-cyan-900/20', + colorIcon: 'text-cyan-600 dark:text-cyan-400', + description: 'Add watermarks to PDF documents', + i18nKey: 'tools.watermarkPdf.title', + orderPriority: 9, + }, + protectPdf: { + name: 'Protect PDF', + slug: 'protect-pdf', + icon: 'Lock', + category: 'secure', + colorBg: 'bg-red-50 dark:bg-red-900/20', + colorIcon: 'text-red-600 dark:text-red-400', + description: 'Password-protect PDF files', + i18nKey: 'tools.protectPdf.title', + isPremium: true, + orderPriority: 10, + }, + unlockPdf: { + name: 'Unlock PDF', + slug: 'unlock-pdf', + icon: 'Unlock', + category: 'secure', + colorBg: 'bg-green-50 dark:bg-green-900/20', + colorIcon: 'text-green-600 dark:text-green-400', + description: 'Remove password protection from PDF files', + i18nKey: 'tools.unlockPdf.title', + isPremium: true, + orderPriority: 11, + }, + addPageNumbers: { + name: 'Add Page Numbers', + slug: 'add-page-numbers', + icon: 'ListOrdered', + category: 'edit', + colorBg: 'bg-sky-50 dark:bg-sky-900/20', + colorIcon: 'text-sky-600 dark:text-sky-400', + description: 'Add page numbers to PDF documents', + i18nKey: 'tools.addPageNumbers.title', + isPremium: true, + orderPriority: 12, + }, + imageConverter: { + name: 'Image Converter', + slug: 'image-converter', + icon: 'ImageIcon', + category: 'image', + colorBg: 'bg-purple-50 dark:bg-purple-900/20', + colorIcon: 'text-purple-600 dark:text-purple-400', + description: 'Convert images between different formats', + i18nKey: 'tools.imageConverter.title', + orderPriority: 13, + }, + videoToGif: { + name: 'Video to GIF', + slug: 'video-to-gif', + icon: 'Film', + category: 'video', + colorBg: 'bg-emerald-50 dark:bg-emerald-900/20', + colorIcon: 'text-emerald-600 dark:text-emerald-400', + description: 'Convert video files to animated GIFs', + i18nKey: 'tools.videoToGif.title', + isPremium: true, + orderPriority: 14, + }, + wordCounter: { + name: 'Word Counter', + slug: 'word-counter', + icon: 'Hash', + category: 'text', + colorBg: 'bg-blue-50 dark:bg-blue-900/20', + colorIcon: 'text-blue-600 dark:text-blue-400', + description: 'Count words, characters, and paragraphs', + i18nKey: 'tools.wordCounter.title', + orderPriority: 15, + }, + textCleaner: { + name: 'Text Cleaner', + slug: 'text-cleaner', + icon: 'Eraser', + category: 'text', + colorBg: 'bg-indigo-50 dark:bg-indigo-900/20', + colorIcon: 'text-indigo-600 dark:text-indigo-400', + description: 'Clean and format text content', + i18nKey: 'tools.textCleaner.title', + orderPriority: 16, + }, +}; + +/** + * Get all tools sorted by priority + */ +export function getToolsByPriority(): ToolCardMetadata[] { + return Object.values(toolRegistry).sort( + (a, b) => a.orderPriority - b.orderPriority + ); +} + +/** + * Get tools by category + */ +export function getToolsByCategory( + category: ToolCardMetadata['category'] +): ToolCardMetadata[] { + return Object.values(toolRegistry) + .filter((tool) => tool.category === category) + .sort((a, b) => a.orderPriority - b.orderPriority); +} + +/** + * Get popular tools + */ +export function getPopularTools(): ToolCardMetadata[] { + return Object.values(toolRegistry) + .filter((tool) => tool.isPopular) + .sort((a, b) => a.orderPriority - b.orderPriority); +} + +/** + * Get premium tools + */ +export function getPremiumTools(): ToolCardMetadata[] { + return Object.values(toolRegistry).filter((tool) => tool.isPremium); +} + +export default { + componentRegistry, + toolRegistry, + toolCategories, + getToolsByPriority, + getToolsByCategory, + getPopularTools, + getPremiumTools, +}; diff --git a/frontend/src/design-system/theme.ts b/frontend/src/design-system/theme.ts new file mode 100644 index 0000000..99e57c0 --- /dev/null +++ b/frontend/src/design-system/theme.ts @@ -0,0 +1,296 @@ +/** + * Design System Theme Configuration + * Centralized theme utilities and token system + */ + +import colors, { colorAssignments, getColorClass } from './colors'; + +/** + * Typography Scale + * Matches Tailwind CSS + custom adjustments for accessibility + */ +export const typography = { + // Headings + h1: { + fontSize: '3rem', // 48px + fontWeight: 700, + lineHeight: '3.5rem', // 56px + letterSpacing: '-0.02em', + }, + h2: { + fontSize: '2.25rem', // 36px + fontWeight: 700, + lineHeight: '2.5rem', // 40px + letterSpacing: '-0.01em', + }, + h3: { + fontSize: '1.875rem', // 30px + fontWeight: 600, + lineHeight: '2.25rem', // 36px + letterSpacing: '-0.01em', + }, + h4: { + fontSize: '1.5rem', // 24px + fontWeight: 600, + lineHeight: '2rem', // 32px + }, + h5: { + fontSize: '1.25rem', // 20px + fontWeight: 600, + lineHeight: '1.75rem', // 28px + }, + h6: { + fontSize: '1rem', // 16px + fontWeight: 600, + lineHeight: '1.5rem', // 24px + }, + + // Body text + body: { + large: { + fontSize: '1.125rem', // 18px + fontWeight: 400, + lineHeight: '1.75rem', // 28px + }, + base: { + fontSize: '1rem', // 16px + fontWeight: 400, + lineHeight: '1.5rem', // 24px + }, + small: { + fontSize: '0.875rem', // 14px + fontWeight: 400, + lineHeight: '1.25rem', // 20px + }, + xs: { + fontSize: '0.75rem', // 12px + fontWeight: 400, + lineHeight: '1rem', // 16px + }, + }, + + // Labels & UI text + label: { + fontSize: '0.875rem', // 14px + fontWeight: 500, + lineHeight: '1.25rem', // 20px + }, + caption: { + fontSize: '0.75rem', // 12px + fontWeight: 500, + lineHeight: '1rem', // 16px + }, +} as const; + +/** + * Spacing Scale + * 4px base unit (Tailwind default) + */ +export const spacing = { + xs: '0.25rem', // 4px + sm: '0.5rem', // 8px + md: '1rem', // 16px + lg: '1.5rem', // 24px + xl: '2rem', // 32px + '2xl': '2.5rem', // 40px + '3xl': '3rem', // 48px + '4xl': '4rem', // 64px + '5xl': '5rem', // 80px + '6xl': '6rem', // 96px +} as const; + +/** + * Border Radius Scale + */ +export const borderRadius = { + none: '0', + sm: '0.375rem', // 6px + base: '0.5rem', // 8px + md: '0.75rem', // 12px + lg: '1rem', // 16px + xl: '1.25rem', // 20px + '2xl': '1.5rem', // 24px + full: '9999px', +} as const; + +/** + * Shadow Scale + */ +export const shadows = { + none: '0 0 #0000', + xs: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + sm: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', + md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', + lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', + xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', + '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)', + lg_dark: '0 20px 25px -5px rgba(0, 0, 0, 0.5)', +} as const; + +/** + * Z-Index Scale + * Structured layering system + */ +export const zIndex = { + auto: 'auto', + hide: '-1', + base: '0', + dropdown: '1000', + sticky: '1010', + fixed: '1020', + backdrop: '1030', + offcanvas: '1040', + modal: '1050', + popover: '1060', + tooltip: '1070', + notification: '1080', +} as const; + +/** + * Transitions & Animations + */ +export const transitions = { + fast: '0.15s', + base: '0.2s', + slow: '0.3s', + slower: '0.5s', + + easing: { + in: 'cubic-bezier(0.4, 0, 1, 1)', + out: 'cubic-bezier(0, 0, 0.2, 1)', + inOut: 'cubic-bezier(0.4, 0, 0.2, 1)', + }, +} as const; + +/** + * Breakpoints (matching Tailwind) + */ +export const breakpoints = { + xs: '0px', + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', +} as const; + +/** + * Container Width + */ +export const containers = { + sm: '24rem', // 384px + md: '28rem', // 448px + lg: '32rem', // 512px + xl: '36rem', // 576px + '2xl': '42rem', // 672px + '3xl': '48rem', // 768px + '4xl': '56rem', // 896px + '5xl': '64rem', // 1024px + '6xl': '72rem', // 1152px + '7xl': '80rem', // 1280px +} as const; + +/** + * Component Size Presets + */ +export const componentSizes = { + // Button sizes + button: { + xs: { + padding: '0.375rem 0.75rem', + fontSize: '0.75rem', + height: '1.5rem', + }, + sm: { + padding: '0.5rem 1rem', + fontSize: '0.875rem', + height: '2rem', + }, + md: { + padding: '0.75rem 1.5rem', + fontSize: '1rem', + height: '2.5rem', + }, + lg: { + padding: '1rem 2rem', + fontSize: '1.125rem', + height: '3rem', + }, + xl: { + padding: '1.25rem 2.5rem', + fontSize: '1.25rem', + height: '3.5rem', + }, + }, + + // Input sizes + input: { + sm: { padding: '0.375rem 0.75rem', fontSize: '0.875rem' }, + md: { padding: '0.75rem 1rem', fontSize: '1rem' }, + lg: { padding: '1rem 1.25rem', fontSize: '1.125rem' }, + }, + + // Icon sizes + icon: { + xs: '1rem', // 16px + sm: '1.25rem', // 20px + md: '1.5rem', // 24px + lg: '2rem', // 32px + xl: '2.5rem', // 40px + '2xl': '3rem', // 48px + }, +} as const; + +/** + * Responsive Utilities + */ +export const responsive = { + // Stack direction + stackMobile: 'flex flex-col', + stackDesktop: 'lg:flex-row', + + // Grid column count + gridAuto: 'grid gap-4', + grid2: 'grid grid-cols-1 sm:grid-cols-2 gap-4', + grid3: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4', + grid4: 'grid grid-cols-2 lg:grid-cols-4 gap-4', + + // Common padding + pagePaddingX: 'px-4 sm:px-6 lg:px-8', + pagePaddingY: 'py-8 sm:py-12 lg:py-16', + sectionPadding: 'px-4 sm:px-6 lg:px-8 py-8 sm:py-12 lg:py-16', + + // Container width + containerMax: 'max-w-7xl', + containerContent: 'max-w-4xl', + containerSmall: 'max-w-2xl', +} as const; + +/** + * Complete Theme Object + */ +export const theme = { + colors, + colorAssignments, + typography, + spacing, + borderRadius, + shadows, + zIndex, + transitions, + breakpoints, + containers, + componentSizes, + responsive, +} as const; + +/** + * Utility: Get CSS variable or Tailwind class for a color + */ +export const useColor = (semantic: string, mode: 'light' | 'dark' = 'light') => { + const colorObj = mode === 'light' ? colors.light : colors.dark; + return colorObj[semantic as keyof typeof colorObj]; +}; + +export default theme; diff --git a/nginx/nginx.dev.conf b/nginx/nginx.dev.conf new file mode 100644 index 0000000..a72a3fc --- /dev/null +++ b/nginx/nginx.dev.conf @@ -0,0 +1,40 @@ +upstream frontend { + server frontend:5173; +} + +# ── HTTP Development Server ── +server { + listen 80 default_server; + client_max_body_size 100M; + resolver 127.0.0.11 valid=30s ipv6=off; + set $backend_upstream backend:5000; + + # API requests → Flask backend + location /api/ { + proxy_pass http://$backend_upstream; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Timeout for large file uploads + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_connect_timeout 60s; + } + + # Frontend (Vite dev server) + location / { + proxy_pass http://frontend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Health check + location /health { + proxy_pass http://$backend_upstream/api/health; + } +} From 83bb610f9f18836300a6df5bfccd3a6fbbead4f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 09:41:33 +0000 Subject: [PATCH 2/6] Initial plan From f82a77febe709dd3bef229b367f14d6dced51c58 Mon Sep 17 00:00:00 2001 From: Your Name <119736744+aborayan2022@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:17:52 +0200 Subject: [PATCH 3/6] Refactor code structure for improved readability and maintainability --- .github/copilot-instructions.md | 71 ++++ backend/app/utils/file_validator.py | 119 +++++-- backend/tests/test_file_validator.py | 134 +++++--- frontend/public/sitemap.xml | 490 +++++++++++++-------------- 4 files changed, 502 insertions(+), 312 deletions(-) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..25c5b7b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,71 @@ +# Copilot Workspace Instructions + +Purpose +- Help Copilot-style agents and contributors be productive and safe in this repository. +- Surface where to find authoritative docs and which conventions to follow. + +Principles +- Link, don't embed: prefer linking to existing docs in `docs/`, `CONTRIBUTING.md`, or `README.md` rather than duplicating content. +- Minimize blast radius: make minimal, focused changes and explain rationale in PRs. +- Ask clarifying questions before large or ambiguous changes. + +What the agent is allowed to do +- Suggest edits, create focused patches, and propose new files following repo style. +- Use `apply_patch` for file edits; create new files only when necessary. +- Run or suggest commands to run tests locally, but do not push or merge without human approval. + +Conventions & expectations +- Follow existing code style and directory boundaries (`backend/` for Flask/Python, `frontend/` for Vite/TypeScript). +- When changing behavior, run tests and list the commands to reproduce the failure/fix. +- Keep PRs small and target a single logical change. + +Key files & links (authoritative sources) +- README: [README.md](README.md#L1) +- Contribution & tests: [CONTRIBUTING.md](CONTRIBUTING.md#L1) +- Docker & run commands: [docs/Docker-Commands-Guide.md](docs/Docker-Commands-Guide.md#L1) +- Backend entry & requirements: [backend/requirements.txt](backend/requirements.txt#L1), [backend/Dockerfile](backend/Dockerfile#L1) +- Frontend scripts: [frontend/package.json](frontend/package.json#L1), [frontend/Dockerfile](frontend/Dockerfile#L1) +- Compose files: [docker-compose.yml](docker-compose.yml#L1), [docker-compose.prod.yml](docker-compose.prod.yml#L1) +- Deployment scripts: [scripts/deploy.sh](scripts/deploy.sh#L1) + +Common build & test commands +- Backend tests (project root): +``` +cd backend && python -m pytest tests/ -q +``` +- Frontend dev & tests: +``` +cd frontend && npm install +cd frontend && npm run dev +cd frontend && npx vitest run +``` +- Dev compose (full stack): +``` +docker compose up --build +``` +- Prod deploy (refer to `scripts/deploy.sh`): +``` +./scripts/deploy.sh +``` + +Anti-patterns (avoid) +- Don't invent architectural decisions or rewrite large areas without explicit approval. +- Don't add secrets, large binary files, or unrelated formatting changes. +- Don't run destructive commands or modify CI/CD configuration without coordination. + +Agent prompts & examples +- "Create a small Flask route in `backend/app/routes` that returns health JSON and add a unit test." +- "Refactor the image compression service to extract a helper; update callers and tests." +- "List the exact commands I should run to reproduce the failing tests for `backend/tests/test_pdf_service.py`." + +Suggested follow-ups (agent customizations) +- `create-agent:backend` — focused on Python/Flask edits, runs `pytest`, and knows `backend/` structure. +- `create-agent:frontend` — focused on Vite/TypeScript, runs `vitest`, and uses `npm` scripts. +- `create-agent:ci` — analyzes `docker-compose.yml` and `scripts/deploy.sh`, suggests CI checks and smoke tests. + +If you want, I can: +- Open a draft PR with this file, or +- Expand the file with more precise command snippets and per-service README links. + +--- +Generated by a workspace bootstrap; iterate as needed. diff --git a/backend/app/utils/file_validator.py b/backend/app/utils/file_validator.py index 3dd1fa7..a0ff552 100644 --- a/backend/app/utils/file_validator.py +++ b/backend/app/utils/file_validator.py @@ -1,11 +1,6 @@ """File validation utilities — multi-layer security checks.""" -import os -try: - import magic - HAS_MAGIC = True -except (ImportError, OSError): - HAS_MAGIC = False +import os from flask import current_app from werkzeug.utils import secure_filename @@ -45,30 +40,60 @@ def validate_file( if not file_storage or file_storage.filename == "": raise FileValidationError("No file provided.") - filename = secure_filename(file_storage.filename) - if not filename: - raise FileValidationError("Invalid filename.") + raw_filename = str(file_storage.filename).strip() + if not raw_filename: + raise FileValidationError("No file provided.") - # Layer 2: Check file extension against whitelist - ext = _get_extension(filename) + filename = secure_filename(raw_filename) allowed_extensions = config.get("ALLOWED_EXTENSIONS", {}) if allowed_types: - valid_extensions = {k: v for k, v in allowed_extensions.items() if k in allowed_types} + valid_extensions = { + k: v for k, v in allowed_extensions.items() if k in allowed_types + } else: valid_extensions = allowed_extensions + # Layer 2: Reject clearly invalid extensions before touching file streams. + ext = _get_extension(raw_filename) or _get_extension(filename) + if ext and ext not in valid_extensions: + raise FileValidationError( + f"File type '.{ext}' is not allowed. " + f"Allowed types: {', '.join(valid_extensions.keys())}" + ) + + # Layer 3: Check basic file size and header first so we can recover + # from malformed filenames like ".pdf" or "." using content sniffing. + file_storage.seek(0, os.SEEK_END) + file_size = file_storage.tell() + file_storage.seek(0) + + if file_size == 0: + raise FileValidationError("File is empty.") + + file_header = file_storage.read(8192) + file_storage.seek(0) + + detected_mime = _detect_mime(file_header) + + if not ext: + ext = _infer_extension_from_content( + file_header, detected_mime, valid_extensions + ) + + if raw_filename.startswith(".") and not _get_extension(filename): + filename = "" + + if not filename: + filename = f"upload.{ext}" if ext else "upload" + if ext not in valid_extensions: raise FileValidationError( f"File type '.{ext}' is not allowed. " f"Allowed types: {', '.join(valid_extensions.keys())}" ) - # Layer 3: Check file size against type-specific limits - file_storage.seek(0, os.SEEK_END) - file_size = file_storage.tell() - file_storage.seek(0) - + # Layer 4: Check file size against type-specific limits size_limits = size_limit_overrides or config.get("FILE_SIZE_LIMITS", {}) max_size = size_limits.get(ext, 20 * 1024 * 1024) # Default 20MB @@ -78,15 +103,8 @@ def validate_file( f"File too large. Maximum size for .{ext} files is {max_mb:.0f}MB." ) - if file_size == 0: - raise FileValidationError("File is empty.") - - # Layer 4: Check MIME type using magic bytes (if libmagic is available) - file_header = file_storage.read(8192) - file_storage.seek(0) - - if HAS_MAGIC: - detected_mime = magic.from_buffer(file_header, mime=True) + # Layer 5: Check MIME type using magic bytes (if libmagic is available) + if detected_mime: expected_mimes = valid_extensions.get(ext, []) if detected_mime not in expected_mimes: @@ -95,7 +113,7 @@ def validate_file( f"Detected type: {detected_mime}" ) - # Layer 5: Additional content checks for specific types + # Layer 6: Additional content checks for specific types if ext == "pdf": _check_pdf_safety(file_header) @@ -104,9 +122,52 @@ def validate_file( def _get_extension(filename: str) -> str: """Extract and normalize file extension.""" - if "." not in filename: + filename = str(filename or "").strip() + if not filename or "." not in filename: return "" - return filename.rsplit(".", 1)[1].lower() + stem, ext = filename.rsplit(".", 1) + if not ext: + return "" + if not stem and filename.startswith("."): + return ext.lower() + return ext.lower() + + +def _detect_mime(file_header: bytes) -> str | None: + """Detect MIME type lazily so environments without libmagic stay usable.""" + try: + import magic as magic_module + except (ImportError, OSError): + return None + + try: + return magic_module.from_buffer(file_header, mime=True) + except Exception: + return None + + +def _infer_extension_from_content( + file_header: bytes, + detected_mime: str | None, + valid_extensions: dict[str, list[str]], +) -> str: + """Infer a safe extension from MIME type or common signatures.""" + if detected_mime: + for ext, mimes in valid_extensions.items(): + if detected_mime in mimes: + return ext + + signature_map = { + b"%PDF": "pdf", + b"\x89PNG\r\n\x1a\n": "png", + b"\xff\xd8\xff": "jpg", + b"RIFF": "webp", + } + for signature, ext in signature_map.items(): + if file_header.startswith(signature) and ext in valid_extensions: + return ext + + return "" def _check_pdf_safety(file_header: bytes): diff --git a/backend/tests/test_file_validator.py b/backend/tests/test_file_validator.py index c95d099..16c8424 100644 --- a/backend/tests/test_file_validator.py +++ b/backend/tests/test_file_validator.py @@ -1,6 +1,7 @@ """Tests for file validation utility.""" + import io -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock from app.utils.file_validator import validate_file, FileValidationError import pytest @@ -16,7 +17,7 @@ class TestFileValidator: """Should raise when filename is empty.""" with app.app_context(): mock_file = MagicMock() - mock_file.filename = '' + mock_file.filename = "" with pytest.raises(FileValidationError, match="No file provided"): validate_file(mock_file, allowed_types=["pdf"]) @@ -24,16 +25,16 @@ class TestFileValidator: """Should raise when file extension is not allowed.""" with app.app_context(): mock_file = MagicMock() - mock_file.filename = 'test.exe' + mock_file.filename = "test.exe" with pytest.raises(FileValidationError, match="not allowed"): validate_file(mock_file, allowed_types=["pdf"]) def test_empty_file_raises(self, app): """Should raise when file is empty (0 bytes).""" with app.app_context(): - content = io.BytesIO(b'') + content = io.BytesIO(b"") mock_file = MagicMock() - mock_file.filename = 'test.pdf' + mock_file.filename = "test.pdf" mock_file.seek = content.seek mock_file.tell = content.tell mock_file.read = content.read @@ -43,93 +44,150 @@ class TestFileValidator: def test_valid_pdf_passes(self, app): """Should accept valid PDF file with correct magic bytes.""" with app.app_context(): - pdf_bytes = b'%PDF-1.4 test content' + b'\x00' * 8192 + pdf_bytes = b"%PDF-1.4 test content" + b"\x00" * 8192 content = io.BytesIO(pdf_bytes) mock_file = MagicMock() - mock_file.filename = 'document.pdf' + mock_file.filename = "document.pdf" mock_file.seek = content.seek mock_file.tell = content.tell mock_file.read = content.read - with patch('app.utils.file_validator.HAS_MAGIC', True), patch( - 'app.utils.file_validator.magic', create=True - ) as mock_magic: - mock_magic.from_buffer.return_value = 'application/pdf' + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + "app.utils.file_validator._detect_mime", + lambda _header: "application/pdf", + ) filename, ext = validate_file(mock_file, allowed_types=["pdf"]) - assert filename == 'document.pdf' - assert ext == 'pdf' + assert filename == "document.pdf" + assert ext == "pdf" def test_valid_html_passes(self, app): """Should accept valid HTML file with correct MIME type.""" with app.app_context(): - html_bytes = b'Hello' + html_bytes = b"Hello" content = io.BytesIO(html_bytes) mock_file = MagicMock() - mock_file.filename = 'page.html' + mock_file.filename = "page.html" mock_file.seek = content.seek mock_file.tell = content.tell mock_file.read = content.read - with patch('app.utils.file_validator.HAS_MAGIC', True), patch( - 'app.utils.file_validator.magic', create=True - ) as mock_magic: - mock_magic.from_buffer.return_value = 'text/html' + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + "app.utils.file_validator._detect_mime", + lambda _header: "text/html", + ) filename, ext = validate_file(mock_file, allowed_types=["html", "htm"]) - assert filename == 'page.html' - assert ext == 'html' + assert filename == "page.html" + assert ext == "html" def test_mime_mismatch_raises(self, app): """Should raise when MIME type doesn't match extension.""" with app.app_context(): - content = io.BytesIO(b'not a real pdf' + b'\x00' * 8192) + content = io.BytesIO(b"not a real pdf" + b"\x00" * 8192) mock_file = MagicMock() - mock_file.filename = 'fake.pdf' + mock_file.filename = "fake.pdf" mock_file.seek = content.seek mock_file.tell = content.tell mock_file.read = content.read - with patch('app.utils.file_validator.HAS_MAGIC', True), patch( - 'app.utils.file_validator.magic', create=True - ) as mock_magic: - mock_magic.from_buffer.return_value = 'text/plain' + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + "app.utils.file_validator._detect_mime", + lambda _header: "text/plain", + ) with pytest.raises(FileValidationError, match="does not match"): validate_file(mock_file, allowed_types=["pdf"]) def test_file_too_large_raises(self, app): """Should raise when file exceeds size limit.""" with app.app_context(): - # Create a file larger than the PDF size limit (20MB) - large_content = io.BytesIO(b'%PDF-1.4' + b'\x00' * (21 * 1024 * 1024)) + # Use a small override to keep the test stable on Windows/Python 3.13. + large_content = io.BytesIO(b"%PDF-1.4" + b"\x00" * 2048) mock_file = MagicMock() - mock_file.filename = 'large.pdf' + mock_file.filename = "large.pdf" mock_file.seek = large_content.seek mock_file.tell = large_content.tell mock_file.read = large_content.read - with pytest.raises(FileValidationError, match="too large"): - validate_file(mock_file, allowed_types=["pdf"]) + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + "app.utils.file_validator._detect_mime", + lambda _header: "application/pdf", + ) + with pytest.raises(FileValidationError, match="too large"): + validate_file( + mock_file, + allowed_types=["pdf"], + size_limit_overrides={"pdf": 1024}, + ) def test_dangerous_pdf_raises(self, app): """Should raise when PDF contains dangerous patterns.""" with app.app_context(): - pdf_bytes = b'%PDF-1.4 /JavaScript evil_code' + b'\x00' * 8192 + pdf_bytes = b"%PDF-1.4 /JavaScript evil_code" + b"\x00" * 8192 content = io.BytesIO(pdf_bytes) mock_file = MagicMock() - mock_file.filename = 'evil.pdf' + mock_file.filename = "evil.pdf" mock_file.seek = content.seek mock_file.tell = content.tell mock_file.read = content.read - with patch('app.utils.file_validator.HAS_MAGIC', True), patch( - 'app.utils.file_validator.magic', create=True - ) as mock_magic: - mock_magic.from_buffer.return_value = 'application/pdf' + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + "app.utils.file_validator._detect_mime", + lambda _header: "application/pdf", + ) with pytest.raises(FileValidationError, match="unsafe"): validate_file(mock_file, allowed_types=["pdf"]) + + def test_pdf_with_missing_extension_name_is_inferred(self, app): + """Should infer PDF extension from content when filename lacks one.""" + with app.app_context(): + pdf_bytes = b"%PDF-1.4 test content" + b"\x00" * 8192 + content = io.BytesIO(pdf_bytes) + + mock_file = MagicMock() + mock_file.filename = "." + mock_file.seek = content.seek + mock_file.tell = content.tell + mock_file.read = content.read + + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + "app.utils.file_validator._detect_mime", + lambda _header: "application/pdf", + ) + filename, ext = validate_file(mock_file, allowed_types=["pdf"]) + + assert filename == "upload.pdf" + assert ext == "pdf" + + def test_pdf_hidden_filename_keeps_pdf_extension(self, app): + """Should preserve .pdf from hidden-style filenames like .pdf.""" + with app.app_context(): + pdf_bytes = b"%PDF-1.4 test content" + b"\x00" * 8192 + content = io.BytesIO(pdf_bytes) + + mock_file = MagicMock() + mock_file.filename = ".pdf" + mock_file.seek = content.seek + mock_file.tell = content.tell + mock_file.read = content.read + + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + "app.utils.file_validator._detect_mime", + lambda _header: "application/pdf", + ) + filename, ext = validate_file(mock_file, allowed_types=["pdf"]) + + assert filename == "upload.pdf" + assert ext == "pdf" diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml index 4f794d1..2eea23f 100644 --- a/frontend/public/sitemap.xml +++ b/frontend/public/sitemap.xml @@ -2,1471 +2,1471 @@ https://dociva.io/ - 2026-03-27 + 2026-03-29 daily 1.0 https://dociva.io/about - 2026-03-27 + 2026-03-29 monthly 0.4 https://dociva.io/contact - 2026-03-27 + 2026-03-29 monthly 0.4 https://dociva.io/privacy - 2026-03-27 + 2026-03-29 yearly 0.3 https://dociva.io/terms - 2026-03-27 + 2026-03-29 yearly 0.3 https://dociva.io/pricing - 2026-03-27 + 2026-03-29 monthly 0.7 https://dociva.io/blog - 2026-03-27 + 2026-03-29 weekly 0.6 https://dociva.io/developers - 2026-03-27 + 2026-03-29 monthly 0.5 https://dociva.io/blog/how-to-compress-pdf-online - 2026-03-27 + 2026-03-29 monthly 0.6 https://dociva.io/blog/convert-images-without-losing-quality - 2026-03-27 + 2026-03-29 monthly 0.6 https://dociva.io/blog/ocr-extract-text-from-images - 2026-03-27 + 2026-03-29 monthly 0.6 https://dociva.io/blog/merge-split-pdf-files - 2026-03-27 + 2026-03-29 monthly 0.6 https://dociva.io/blog/ai-chat-with-pdf-documents - 2026-03-27 + 2026-03-29 monthly 0.6 https://dociva.io/tools/pdf-to-word - 2026-03-27 + 2026-03-29 weekly 0.9 https://dociva.io/tools/word-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.9 https://dociva.io/tools/compress-pdf - 2026-03-27 + 2026-03-29 weekly 0.9 https://dociva.io/tools/merge-pdf - 2026-03-27 + 2026-03-29 weekly 0.9 https://dociva.io/tools/split-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/rotate-pdf - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/pdf-to-images - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/images-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/watermark-pdf - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/protect-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/unlock-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/page-numbers - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/pdf-editor - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/pdf-flowchart - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/pdf-to-excel - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/remove-watermark-pdf - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/reorder-pdf - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/extract-pages - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/image-converter - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/image-resize - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/compress-image - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/ocr - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/remove-background - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/image-to-svg - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/html-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/chat-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/summarize-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/translate-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/extract-tables - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/qr-code - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/video-to-gif - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/word-counter - 2026-03-27 + 2026-03-29 weekly 0.6 https://dociva.io/tools/text-cleaner - 2026-03-27 + 2026-03-29 weekly 0.6 https://dociva.io/tools/pdf-to-pptx - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/excel-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/pptx-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/sign-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/tools/crop-pdf - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/flatten-pdf - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/repair-pdf - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/pdf-metadata - 2026-03-27 + 2026-03-29 weekly 0.6 https://dociva.io/tools/image-crop - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/image-rotate-flip - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/tools/barcode-generator - 2026-03-27 + 2026-03-29 weekly 0.7 https://dociva.io/pdf-to-word - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-to-word - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/word-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/word-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/compress-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/compress-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/convert-jpg-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/convert-jpg-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/merge-pdf-files - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/merge-pdf-files - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/remove-pdf-password - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/remove-pdf-password - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/pdf-to-word-editable - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-to-word-editable - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/convert-pdf-to-text - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/convert-pdf-to-text - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/split-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/split-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/jpg-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/jpg-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/png-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/png-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/images-to-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/images-to-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/pdf-to-jpg - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-to-jpg - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/pdf-to-png - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-to-png - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/compress-pdf-for-email - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/compress-pdf-for-email - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/compress-scanned-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/compress-scanned-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/merge-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/merge-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/combine-pdf-files - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/combine-pdf-files - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/extract-pages-from-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/extract-pages-from-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/reorder-pdf-pages - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/reorder-pdf-pages - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/rotate-pdf-pages - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/rotate-pdf-pages - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/add-page-numbers-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/add-page-numbers-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/protect-pdf-with-password - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/protect-pdf-with-password - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/unlock-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/unlock-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/watermark-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/watermark-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/remove-watermark-from-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/remove-watermark-from-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/edit-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/edit-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/pdf-to-excel-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-to-excel-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/extract-tables-from-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/extract-tables-from-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/html-to-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/html-to-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/scan-pdf-to-text - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/scan-pdf-to-text - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/chat-with-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/chat-with-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/summarize-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/summarize-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/translate-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/translate-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/convert-image-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/convert-image-to-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/convert-webp-to-jpg - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/convert-webp-to-jpg - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/resize-image-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/resize-image-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/compress-image-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/compress-image-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/remove-image-background - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/remove-image-background - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/pdf-to-word-editable-free - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-to-word-editable-free - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/compress-pdf-to-100kb - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/compress-pdf-to-100kb - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/ai-extract-text-from-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/ai-extract-text-from-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/pdf-to-excel-accurate-free - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-to-excel-accurate-free - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/split-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/split-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/compress-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/compress-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/unlock-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/unlock-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/summarize-pdf-ai - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/summarize-pdf-ai - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/convert-pdf-to-text-ai - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/convert-pdf-to-text-ai - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/pdf-to-jpg-high-quality - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-to-jpg-high-quality - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/jpg-to-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/jpg-to-pdf-online-free - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/reduce-pdf-size-for-email - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/reduce-pdf-size-for-email - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/ocr-for-scanned-pdfs - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/ocr-for-scanned-pdfs - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/remove-watermark-from-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/remove-watermark-from-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/add-watermark-to-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/add-watermark-to-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/repair-corrupted-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/repair-corrupted-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/rotate-pdf-pages-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/rotate-pdf-pages-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/reorder-pdf-pages-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/reorder-pdf-pages-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/pdf-to-png-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-to-png-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/images-to-pdf-multiple - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/images-to-pdf-multiple - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/split-pdf-by-range-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/split-pdf-by-range-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/compress-scanned-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/compress-scanned-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/pdf-metadata-editor-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-metadata-editor-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/add-page-numbers-to-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/add-page-numbers-to-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/protect-pdf-with-password-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/protect-pdf-with-password-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/unlock-encrypted-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/unlock-encrypted-pdf-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/ocr-table-extraction-from-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/ocr-table-extraction-from-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/pdf-to-excel-converter-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-to-excel-converter-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/extract-text-from-protected-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/extract-text-from-protected-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/bulk-convert-pdf-to-word - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/bulk-convert-pdf-to-word - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/compress-pdf-for-web-upload - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/compress-pdf-for-web-upload - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/ocr-multi-language-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/ocr-multi-language-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/summarize-long-pdf-ai - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/summarize-long-pdf-ai - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/convert-pdf-to-ppt-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/convert-pdf-to-ppt-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/pdf-to-pptx-free-online - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/pdf-to-pptx-free-online - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/دمج-ملفات-pdf-مجاناً - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/دمج-ملفات-pdf-مجاناً - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/ضغط-بي-دي-اف-اونلاين - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/ضغط-بي-دي-اف-اونلاين - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/تحويل-jpg-الى-pdf-اونلاين - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/تحويل-jpg-الى-pdf-اونلاين - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/فصل-صفحات-pdf-اونلاين - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/فصل-صفحات-pdf-اونلاين - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/ازالة-كلمة-مرور-من-pdf - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/ازالة-كلمة-مرور-من-pdf - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/تحويل-pdf-الى-excel-اونلاين - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-excel-اونلاين - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/تحويل-pdf-الى-صور - 2026-03-27 + 2026-03-29 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-صور - 2026-03-27 + 2026-03-29 weekly 0.8 https://dociva.io/best-pdf-tools - 2026-03-27 + 2026-03-29 weekly 0.82 https://dociva.io/ar/best-pdf-tools - 2026-03-27 + 2026-03-29 weekly 0.74 https://dociva.io/free-pdf-tools-online - 2026-03-27 + 2026-03-29 weekly 0.82 https://dociva.io/ar/free-pdf-tools-online - 2026-03-27 + 2026-03-29 weekly 0.74 https://dociva.io/convert-files-online - 2026-03-27 + 2026-03-29 weekly 0.82 https://dociva.io/ar/convert-files-online - 2026-03-27 + 2026-03-29 weekly 0.74 https://dociva.io/pdf-converter-tools - 2026-03-27 + 2026-03-29 weekly 0.82 https://dociva.io/ar/pdf-converter-tools - 2026-03-27 + 2026-03-29 weekly 0.74 https://dociva.io/secure-pdf-tools - 2026-03-27 + 2026-03-29 weekly 0.82 https://dociva.io/ar/secure-pdf-tools - 2026-03-27 + 2026-03-29 weekly 0.74 https://dociva.io/ai-document-tools - 2026-03-27 + 2026-03-29 weekly 0.82 https://dociva.io/ar/ai-document-tools - 2026-03-27 + 2026-03-29 weekly 0.74 https://dociva.io/image-to-pdf-tools - 2026-03-27 + 2026-03-29 weekly 0.82 https://dociva.io/ar/image-to-pdf-tools - 2026-03-27 + 2026-03-29 weekly 0.74 https://dociva.io/online-image-tools - 2026-03-27 + 2026-03-29 weekly 0.82 https://dociva.io/ar/online-image-tools - 2026-03-27 + 2026-03-29 weekly 0.74 https://dociva.io/office-to-pdf-tools - 2026-03-27 + 2026-03-29 weekly 0.82 https://dociva.io/ar/office-to-pdf-tools - 2026-03-27 + 2026-03-29 weekly 0.74 https://dociva.io/scanned-document-tools - 2026-03-27 + 2026-03-29 weekly 0.82 https://dociva.io/ar/scanned-document-tools - 2026-03-27 + 2026-03-29 weekly 0.74 https://dociva.io/arabic-pdf-tools - 2026-03-27 + 2026-03-29 weekly 0.82 https://dociva.io/ar/arabic-pdf-tools - 2026-03-27 + 2026-03-29 weekly 0.74 From 5ac1d587424768e7f1b1b05d3e3bec26ba231165 Mon Sep 17 00:00:00 2001 From: Your Name <119736744+aborayan2022@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:04:34 +0200 Subject: [PATCH 4/6] feat: enhance ImagesToPdf component with mobile-friendly file picker and unique file selection logic; update translations for mobile picker guidance --- frontend/src/components/tools/ImagesToPdf.tsx | 55 ++++++++++++++++--- frontend/src/i18n/ar.json | 3 +- frontend/src/i18n/en.json | 3 +- frontend/src/i18n/fr.json | 3 +- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/tools/ImagesToPdf.tsx b/frontend/src/components/tools/ImagesToPdf.tsx index cd45889..c393ebb 100644 --- a/frontend/src/components/tools/ImagesToPdf.tsx +++ b/frontend/src/components/tools/ImagesToPdf.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Helmet } from 'react-helmet-async'; import { FileImage } from 'lucide-react'; @@ -18,6 +18,8 @@ export default function ImagesToPdf() { const [isUploading, setIsUploading] = useState(false); const [taskId, setTaskId] = useState(null); const [error, setError] = useState(null); + const [useSinglePickerFlow, setUseSinglePickerFlow] = useState(false); + const inputRef = useRef(null); const { status, result, error: taskError } = useTaskPolling({ taskId, @@ -35,7 +37,22 @@ export default function ImagesToPdf() { } }, []); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + const coarsePointer = window.matchMedia?.('(pointer: coarse)').matches ?? false; + const mobileUserAgent = /android|iphone|ipad|ipod|mobile/i.test(navigator.userAgent); + setUseSinglePickerFlow(coarsePointer || mobileUserAgent); + }, []); + const acceptedTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/bmp']; + const acceptValue = acceptedTypes.join(','); + + const openPicker = () => { + inputRef.current?.click(); + }; const handleFilesSelect = (newFiles: FileList | File[]) => { const fileArray = Array.from(newFiles).filter((f) => @@ -45,7 +62,19 @@ export default function ImagesToPdf() { setError(t('tools.imagesToPdf.invalidFiles')); return; } - setFiles((prev) => [...prev, ...fileArray]); + setFiles((prev) => { + const seen = new Set(prev.map((file) => `${file.name}:${file.size}:${file.lastModified}`)); + const uniqueNewFiles = fileArray.filter((file) => { + const key = `${file.name}:${file.size}:${file.lastModified}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); + + return [...prev, ...uniqueNewFiles]; + }); setError(null); }; @@ -112,8 +141,7 @@ export default function ImagesToPdf() {
{/* Drop zone */}
document.getElementById('images-file-input')?.click()} + className="upload-zone" onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); @@ -122,9 +150,10 @@ export default function ImagesToPdf() { > { if (e.target.files) handleFilesSelect(e.target.files); @@ -133,12 +162,24 @@ export default function ImagesToPdf() { />

- {t('common.dragDrop')} + {files.length > 0 ? t('tools.imagesToPdf.addMore') : t('tools.imagesToPdf.selectImages')}

PNG, JPG, WebP, BMP

+ {useSinglePickerFlow && ( +

+ {t('tools.imagesToPdf.mobilePickerHint')} +

+ )}

{t('common.maxSize', { size: 10 })}

+
{/* File list */} diff --git a/frontend/src/i18n/ar.json b/frontend/src/i18n/ar.json index 4f00bdc..74d94a7 100644 --- a/frontend/src/i18n/ar.json +++ b/frontend/src/i18n/ar.json @@ -564,7 +564,8 @@ "addMore": "أضف صور أخرى", "imagesSelected": "{{count}} صور مختارة", "invalidFiles": "يرجى اختيار ملفات صور صالحة (JPG أو PNG أو WebP).", - "minFiles": "يرجى اختيار صورة واحدة على الأقل." + "minFiles": "يرجى اختيار صورة واحدة على الأقل.", + "mobilePickerHint": "في بعض الهواتف يفضَّل اختيار صورة واحدة كل مرة ثم الضغط على حفظ لتأكيدها قبل إضافة الصورة التالية." }, "watermarkPdf": { "title": "علامة مائية PDF", diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 3440a4e..ba03ae4 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -564,7 +564,8 @@ "addMore": "Add More Images", "imagesSelected": "{{count}} images selected", "invalidFiles": "Please select valid image files (JPG, PNG, WebP).", - "minFiles": "Please select at least one image." + "minFiles": "Please select at least one image.", + "mobilePickerHint": "On some phones, select one image at a time and tap Save to confirm it before adding the next image." }, "watermarkPdf": { "title": "Watermark PDF", diff --git a/frontend/src/i18n/fr.json b/frontend/src/i18n/fr.json index 81e155a..24f1dc9 100644 --- a/frontend/src/i18n/fr.json +++ b/frontend/src/i18n/fr.json @@ -564,7 +564,8 @@ "addMore": "Ajouter plus d'images", "imagesSelected": "{{count}} images sélectionnées", "invalidFiles": "Veuillez sélectionner des fichiers images valides (JPG, PNG, WebP).", - "minFiles": "Veuillez sélectionner au moins une image." + "minFiles": "Veuillez sélectionner au moins une image.", + "mobilePickerHint": "Sur certains téléphones, sélectionnez une image à la fois puis appuyez sur Enregistrer avant d'ajouter la suivante." }, "watermarkPdf": { "title": "Filigrane PDF", From 4ac4bf4e4220d403d93db78505d3081e79d9e57b Mon Sep 17 00:00:00 2001 From: Your Name <119736744+aborayan2022@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:12:34 +0200 Subject: [PATCH 5/6] refactor: improve app initialization and update rate limiter tests for consistency --- backend/app/__init__.py | 12 +++++-- backend/tests/test_rate_limiter.py | 51 +++++++++++++----------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 4edb772..689db5d 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1,4 +1,5 @@ """Flask Application Factory.""" + import os from flask import Flask, jsonify @@ -11,7 +12,12 @@ from app.services.ai_cost_service import init_ai_cost_db from app.services.site_assistant_service import init_site_assistant_db from app.services.contact_service import init_contact_db from app.services.stripe_service import init_stripe_db -from app.utils.csrf import CSRFError, apply_csrf_cookie, should_enforce_csrf, validate_csrf_request +from app.utils.csrf import ( + CSRFError, + apply_csrf_cookie, + should_enforce_csrf, + validate_csrf_request, +) def _init_sentry(app): @@ -35,13 +41,15 @@ def _init_sentry(app): app.logger.warning("sentry-sdk not installed — monitoring disabled.") -def create_app(config_name=None): +def create_app(config_name=None, config_overrides=None): """Create and configure the Flask application.""" if config_name is None: config_name = os.getenv("FLASK_ENV", "development") app = Flask(__name__) app.config.from_object(config[config_name]) + if config_overrides: + app.config.update(config_overrides) # Initialize Sentry early _init_sentry(app) diff --git a/backend/tests/test_rate_limiter.py b/backend/tests/test_rate_limiter.py index 35f4c31..526f224 100644 --- a/backend/tests/test_rate_limiter.py +++ b/backend/tests/test_rate_limiter.py @@ -1,6 +1,8 @@ """Tests for rate limiting middleware.""" + import pytest from app import create_app +from tests.conftest import CSRFTestClient @pytest.fixture @@ -11,33 +13,24 @@ def rate_limited_app(tmp_path): never throttled. Here we force the extension's internal flag back to True *after* init_app so the decorator limits are enforced. """ - app = create_app('testing') - app.config.update({ - 'TESTING': True, - 'RATELIMIT_STORAGE_URI': 'memory://', - 'UPLOAD_FOLDER': str(tmp_path / 'uploads'), - 'OUTPUT_FOLDER': str(tmp_path / 'outputs'), - }) + app = create_app( + "testing", + { + "TESTING": True, + "RATELIMIT_ENABLED": True, + "RATELIMIT_STORAGE_URI": "memory://", + "UPLOAD_FOLDER": str(tmp_path / "uploads"), + "OUTPUT_FOLDER": str(tmp_path / "outputs"), + }, + ) + app.test_client_class = CSRFTestClient import os - os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) - os.makedirs(app.config['OUTPUT_FOLDER'], exist_ok=True) - # flask-limiter 3.x returns from init_app immediately when - # RATELIMIT_ENABLED=False (TestingConfig default), so `initialized` - # stays False and no limits are enforced. We override the config key - # and call init_app a SECOND time so the extension fully initialises. - # It is safe to call twice — flask-limiter guards against duplicate - # before_request hook registration via app.extensions["limiter"]. - from app.extensions import limiter as _limiter - app.config['RATELIMIT_ENABLED'] = True - _limiter.init_app(app) # second call — now RATELIMIT_ENABLED=True + os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True) + os.makedirs(app.config["OUTPUT_FOLDER"], exist_ok=True) yield app - # Restore so other tests are unaffected - _limiter.enabled = False - _limiter.initialized = False - @pytest.fixture def rate_limited_client(rate_limited_app): @@ -48,12 +41,12 @@ class TestRateLimiter: def test_health_endpoint_not_rate_limited(self, client): """Health endpoint should handle many rapid requests.""" for _ in range(20): - response = client.get('/api/health') + response = client.get("/api/health") assert response.status_code == 200 def test_rate_limit_header_present(self, client): """Response should include a valid HTTP status code.""" - response = client.get('/api/health') + response = client.get("/api/health") assert response.status_code == 200 @@ -68,7 +61,7 @@ class TestRateLimitEnforcement: """ blocked = False for i in range(15): - r = rate_limited_client.post('/api/compress/pdf') + r = rate_limited_client.post("/api/compress/pdf") if r.status_code == 429: blocked = True break @@ -81,7 +74,7 @@ class TestRateLimitEnforcement: """POST /api/convert/pdf-to-word is also rate-limited.""" blocked = False for _ in range(15): - r = rate_limited_client.post('/api/convert/pdf-to-word') + r = rate_limited_client.post("/api/convert/pdf-to-word") if r.status_code == 429: blocked = True break @@ -94,8 +87,8 @@ class TestRateLimitEnforcement: """ # Exhaust compress limit for _ in range(15): - rate_limited_client.post('/api/compress/pdf') + rate_limited_client.post("/api/compress/pdf") # Health should still respond normally - r = rate_limited_client.get('/api/health') - assert r.status_code == 200 \ No newline at end of file + r = rate_limited_client.get("/api/health") + assert r.status_code == 200 From 736d08ef0439a6c7618904dab8791f5dda89ab38 Mon Sep 17 00:00:00 2001 From: Your Name <119736744+aborayan2022@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:31:27 +0200 Subject: [PATCH 6/6] feat: enhance SEO capabilities and add All Tools page - Updated generate-seo-assets script to create separate sitemap files for static, blog, tools, and SEO pages. - Introduced render-seo-shells script to generate HTML shells for SEO pages with dynamic metadata. - Added All Tools page with categorized tool listings and SEO metadata. - Updated routing to include /tools path and linked it in the footer. - Enhanced SEOHead component to remove unused keywords and improve OpenGraph metadata. - Updated translations for tools hub in English, Arabic, and French. - Refactored SEO-related utility functions to support new structured data formats. --- .env.example | 2 +- frontend/index.html | 9 +- frontend/package.json | 2 +- frontend/public/sitemap.xml | 1482 +---------------- frontend/public/sitemaps/blog.xml | 33 + frontend/public/sitemaps/seo.xml | 1131 +++++++++++++ frontend/public/sitemaps/static.xml | 57 + frontend/public/sitemaps/tools.xml | 267 +++ frontend/scripts/generate-seo-assets.mjs | 50 +- frontend/scripts/render-seo-shells.mjs | 166 ++ frontend/src/App.tsx | 2 + frontend/src/components/layout/Footer.tsx | 6 + frontend/src/components/seo/SEOHead.tsx | 16 +- .../src/components/seo/ToolLandingPage.tsx | 18 +- frontend/src/config/routes.ts | 1 + frontend/src/i18n/ar.json | 13 + frontend/src/i18n/en.json | 13 + frontend/src/i18n/fr.json | 13 + frontend/src/pages/AllToolsPage.tsx | 99 ++ frontend/src/pages/BlogPage.tsx | 18 +- frontend/src/pages/HomePage.tsx | 16 +- frontend/src/pages/SeoCollectionPage.tsx | 27 +- frontend/src/pages/SeoPage.tsx | 21 +- frontend/src/utils/seo.ts | 117 +- 24 files changed, 2030 insertions(+), 1549 deletions(-) create mode 100644 frontend/public/sitemaps/blog.xml create mode 100644 frontend/public/sitemaps/seo.xml create mode 100644 frontend/public/sitemaps/static.xml create mode 100644 frontend/public/sitemaps/tools.xml create mode 100644 frontend/scripts/render-seo-shells.mjs create mode 100644 frontend/src/pages/AllToolsPage.tsx diff --git a/.env.example b/.env.example index c53637e..018d569 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ FLASK_ENV=production FLASK_DEBUG=0 SECRET_KEY=replace-with-a-long-random-secret-key -INTERNAL_ADMIN_EMAILS=admin@dociva.io +INTERNAL_ADMIN_EMAILS=support@dociva.io # Site Domain (used in sitemap, robots.txt, emails) SITE_DOMAIN=https://dociva.io diff --git a/frontend/index.html b/frontend/index.html index 4d2e5b1..6ff05ac 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,12 +7,13 @@ + + + - - + - \ No newline at end of file + diff --git a/frontend/package.json b/frontend/package.json index a69e911..17503f1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "prebuild": "node scripts/merge-keywords.mjs && node scripts/generate-seo-assets.mjs", - "build": "tsc --noEmit && vite build", + "build": "tsc --noEmit && vite build && node scripts/render-seo-shells.mjs", "preview": "vite preview", "lint": "eslint .", "test": "vitest run", diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml index 2eea23f..b80c3c2 100644 --- a/frontend/public/sitemap.xml +++ b/frontend/public/sitemap.xml @@ -1,1473 +1,19 @@ - - - https://dociva.io/ + + + https://dociva.io/sitemaps/static.xml 2026-03-29 - daily - 1.0 - - - https://dociva.io/about + + + https://dociva.io/sitemaps/blog.xml 2026-03-29 - monthly - 0.4 - - - https://dociva.io/contact + + + https://dociva.io/sitemaps/tools.xml 2026-03-29 - monthly - 0.4 - - - https://dociva.io/privacy + + + https://dociva.io/sitemaps/seo.xml 2026-03-29 - yearly - 0.3 - - - https://dociva.io/terms - 2026-03-29 - yearly - 0.3 - - - https://dociva.io/pricing - 2026-03-29 - monthly - 0.7 - - - https://dociva.io/blog - 2026-03-29 - weekly - 0.6 - - - https://dociva.io/developers - 2026-03-29 - monthly - 0.5 - - - https://dociva.io/blog/how-to-compress-pdf-online - 2026-03-29 - monthly - 0.6 - - - https://dociva.io/blog/convert-images-without-losing-quality - 2026-03-29 - monthly - 0.6 - - - https://dociva.io/blog/ocr-extract-text-from-images - 2026-03-29 - monthly - 0.6 - - - https://dociva.io/blog/merge-split-pdf-files - 2026-03-29 - monthly - 0.6 - - - https://dociva.io/blog/ai-chat-with-pdf-documents - 2026-03-29 - monthly - 0.6 - - - https://dociva.io/tools/pdf-to-word - 2026-03-29 - weekly - 0.9 - - - https://dociva.io/tools/word-to-pdf - 2026-03-29 - weekly - 0.9 - - - https://dociva.io/tools/compress-pdf - 2026-03-29 - weekly - 0.9 - - - https://dociva.io/tools/merge-pdf - 2026-03-29 - weekly - 0.9 - - - https://dociva.io/tools/split-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/rotate-pdf - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/pdf-to-images - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/images-to-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/watermark-pdf - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/protect-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/unlock-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/page-numbers - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/pdf-editor - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/pdf-flowchart - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/pdf-to-excel - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/remove-watermark-pdf - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/reorder-pdf - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/extract-pages - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/image-converter - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/image-resize - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/compress-image - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/ocr - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/remove-background - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/image-to-svg - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/html-to-pdf - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/chat-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/summarize-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/translate-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/extract-tables - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/qr-code - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/video-to-gif - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/word-counter - 2026-03-29 - weekly - 0.6 - - - https://dociva.io/tools/text-cleaner - 2026-03-29 - weekly - 0.6 - - - https://dociva.io/tools/pdf-to-pptx - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/excel-to-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/pptx-to-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/sign-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/tools/crop-pdf - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/flatten-pdf - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/repair-pdf - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/pdf-metadata - 2026-03-29 - weekly - 0.6 - - - https://dociva.io/tools/image-crop - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/image-rotate-flip - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/tools/barcode-generator - 2026-03-29 - weekly - 0.7 - - - https://dociva.io/pdf-to-word - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-word - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/word-to-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/word-to-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/compress-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/compress-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/convert-jpg-to-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/convert-jpg-to-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/merge-pdf-files - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/merge-pdf-files - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/remove-pdf-password - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/remove-pdf-password - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/pdf-to-word-editable - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-word-editable - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/convert-pdf-to-text - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/convert-pdf-to-text - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/split-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/split-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/jpg-to-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/jpg-to-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/png-to-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/png-to-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/images-to-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/images-to-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/pdf-to-jpg - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-jpg - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/pdf-to-png - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-png - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/compress-pdf-for-email - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/compress-pdf-for-email - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/compress-scanned-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/compress-scanned-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/merge-pdf-online-free - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/merge-pdf-online-free - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/combine-pdf-files - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/combine-pdf-files - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/extract-pages-from-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/extract-pages-from-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/reorder-pdf-pages - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/reorder-pdf-pages - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/rotate-pdf-pages - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/rotate-pdf-pages - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/add-page-numbers-to-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/add-page-numbers-to-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/protect-pdf-with-password - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/protect-pdf-with-password - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/unlock-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/unlock-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/watermark-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/watermark-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/remove-watermark-from-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/remove-watermark-from-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/edit-pdf-online-free - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/edit-pdf-online-free - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/pdf-to-excel-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-excel-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/extract-tables-from-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/extract-tables-from-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/html-to-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/html-to-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/scan-pdf-to-text - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/scan-pdf-to-text - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/chat-with-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/chat-with-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/summarize-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/summarize-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/translate-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/translate-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/convert-image-to-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/convert-image-to-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/convert-webp-to-jpg - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/convert-webp-to-jpg - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/resize-image-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/resize-image-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/compress-image-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/compress-image-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/remove-image-background - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/remove-image-background - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/pdf-to-word-editable-free - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-word-editable-free - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/compress-pdf-to-100kb - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/compress-pdf-to-100kb - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/ai-extract-text-from-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/ai-extract-text-from-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/pdf-to-excel-accurate-free - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-excel-accurate-free - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/split-pdf-online-free - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/split-pdf-online-free - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/compress-pdf-online-free - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/compress-pdf-online-free - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/unlock-pdf-online-free - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/unlock-pdf-online-free - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/summarize-pdf-ai - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/summarize-pdf-ai - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/convert-pdf-to-text-ai - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/convert-pdf-to-text-ai - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/pdf-to-jpg-high-quality - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-jpg-high-quality - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/jpg-to-pdf-online-free - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/jpg-to-pdf-online-free - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/reduce-pdf-size-for-email - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/reduce-pdf-size-for-email - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/ocr-for-scanned-pdfs - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/ocr-for-scanned-pdfs - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/remove-watermark-from-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/remove-watermark-from-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/add-watermark-to-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/add-watermark-to-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/repair-corrupted-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/repair-corrupted-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/rotate-pdf-pages-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/rotate-pdf-pages-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/reorder-pdf-pages-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/reorder-pdf-pages-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/pdf-to-png-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-png-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/images-to-pdf-multiple - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/images-to-pdf-multiple - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/split-pdf-by-range-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/split-pdf-by-range-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/compress-scanned-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/compress-scanned-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/pdf-metadata-editor-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-metadata-editor-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/add-page-numbers-to-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/add-page-numbers-to-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/protect-pdf-with-password-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/protect-pdf-with-password-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/unlock-encrypted-pdf-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/unlock-encrypted-pdf-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/ocr-table-extraction-from-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/ocr-table-extraction-from-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/pdf-to-excel-converter-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-excel-converter-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/extract-text-from-protected-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/extract-text-from-protected-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/bulk-convert-pdf-to-word - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/bulk-convert-pdf-to-word - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/compress-pdf-for-web-upload - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/compress-pdf-for-web-upload - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/ocr-multi-language-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/ocr-multi-language-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/summarize-long-pdf-ai - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/summarize-long-pdf-ai - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/convert-pdf-to-ppt-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/convert-pdf-to-ppt-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/pdf-to-pptx-free-online - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-pptx-free-online - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/دمج-ملفات-pdf-مجاناً - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/دمج-ملفات-pdf-مجاناً - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/ضغط-بي-دي-اف-اونلاين - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/ضغط-بي-دي-اف-اونلاين - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/تحويل-jpg-الى-pdf-اونلاين - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/تحويل-jpg-الى-pdf-اونلاين - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/فصل-صفحات-pdf-اونلاين - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/فصل-صفحات-pdf-اونلاين - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/ازالة-كلمة-مرور-من-pdf - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/ازالة-كلمة-مرور-من-pdf - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/تحويل-pdf-الى-excel-اونلاين - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/تحويل-pdf-الى-excel-اونلاين - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/تحويل-pdf-الى-صور - 2026-03-29 - weekly - 0.88 - - - https://dociva.io/ar/تحويل-pdf-الى-صور - 2026-03-29 - weekly - 0.8 - - - https://dociva.io/best-pdf-tools - 2026-03-29 - weekly - 0.82 - - - https://dociva.io/ar/best-pdf-tools - 2026-03-29 - weekly - 0.74 - - - https://dociva.io/free-pdf-tools-online - 2026-03-29 - weekly - 0.82 - - - https://dociva.io/ar/free-pdf-tools-online - 2026-03-29 - weekly - 0.74 - - - https://dociva.io/convert-files-online - 2026-03-29 - weekly - 0.82 - - - https://dociva.io/ar/convert-files-online - 2026-03-29 - weekly - 0.74 - - - https://dociva.io/pdf-converter-tools - 2026-03-29 - weekly - 0.82 - - - https://dociva.io/ar/pdf-converter-tools - 2026-03-29 - weekly - 0.74 - - - https://dociva.io/secure-pdf-tools - 2026-03-29 - weekly - 0.82 - - - https://dociva.io/ar/secure-pdf-tools - 2026-03-29 - weekly - 0.74 - - - https://dociva.io/ai-document-tools - 2026-03-29 - weekly - 0.82 - - - https://dociva.io/ar/ai-document-tools - 2026-03-29 - weekly - 0.74 - - - https://dociva.io/image-to-pdf-tools - 2026-03-29 - weekly - 0.82 - - - https://dociva.io/ar/image-to-pdf-tools - 2026-03-29 - weekly - 0.74 - - - https://dociva.io/online-image-tools - 2026-03-29 - weekly - 0.82 - - - https://dociva.io/ar/online-image-tools - 2026-03-29 - weekly - 0.74 - - - https://dociva.io/office-to-pdf-tools - 2026-03-29 - weekly - 0.82 - - - https://dociva.io/ar/office-to-pdf-tools - 2026-03-29 - weekly - 0.74 - - - https://dociva.io/scanned-document-tools - 2026-03-29 - weekly - 0.82 - - - https://dociva.io/ar/scanned-document-tools - 2026-03-29 - weekly - 0.74 - - - https://dociva.io/arabic-pdf-tools - 2026-03-29 - weekly - 0.82 - - - https://dociva.io/ar/arabic-pdf-tools - 2026-03-29 - weekly - 0.74 - - + + diff --git a/frontend/public/sitemaps/blog.xml b/frontend/public/sitemaps/blog.xml new file mode 100644 index 0000000..e80f10a --- /dev/null +++ b/frontend/public/sitemaps/blog.xml @@ -0,0 +1,33 @@ + + + + https://dociva.io/blog/how-to-compress-pdf-online + 2026-03-29 + monthly + 0.6 + + + https://dociva.io/blog/convert-images-without-losing-quality + 2026-03-29 + monthly + 0.6 + + + https://dociva.io/blog/ocr-extract-text-from-images + 2026-03-29 + monthly + 0.6 + + + https://dociva.io/blog/merge-split-pdf-files + 2026-03-29 + monthly + 0.6 + + + https://dociva.io/blog/ai-chat-with-pdf-documents + 2026-03-29 + monthly + 0.6 + + diff --git a/frontend/public/sitemaps/seo.xml b/frontend/public/sitemaps/seo.xml new file mode 100644 index 0000000..87ce702 --- /dev/null +++ b/frontend/public/sitemaps/seo.xml @@ -0,0 +1,1131 @@ + + + + https://dociva.io/pdf-to-word + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-to-word + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/word-to-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/word-to-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/compress-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/compress-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/convert-jpg-to-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/convert-jpg-to-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/merge-pdf-files + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/merge-pdf-files + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/remove-pdf-password + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/remove-pdf-password + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/pdf-to-word-editable + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-to-word-editable + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/convert-pdf-to-text + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/convert-pdf-to-text + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/split-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/split-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/jpg-to-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/jpg-to-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/png-to-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/png-to-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/images-to-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/images-to-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/pdf-to-jpg + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-to-jpg + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/pdf-to-png + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-to-png + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/compress-pdf-for-email + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/compress-pdf-for-email + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/compress-scanned-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/compress-scanned-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/merge-pdf-online-free + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/merge-pdf-online-free + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/combine-pdf-files + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/combine-pdf-files + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/extract-pages-from-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/extract-pages-from-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/reorder-pdf-pages + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/reorder-pdf-pages + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/rotate-pdf-pages + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/rotate-pdf-pages + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/add-page-numbers-to-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/add-page-numbers-to-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/protect-pdf-with-password + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/protect-pdf-with-password + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/unlock-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/unlock-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/watermark-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/watermark-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/remove-watermark-from-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/remove-watermark-from-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/edit-pdf-online-free + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/edit-pdf-online-free + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/pdf-to-excel-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-to-excel-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/extract-tables-from-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/extract-tables-from-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/html-to-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/html-to-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/scan-pdf-to-text + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/scan-pdf-to-text + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/chat-with-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/chat-with-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/summarize-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/summarize-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/translate-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/translate-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/convert-image-to-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/convert-image-to-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/convert-webp-to-jpg + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/convert-webp-to-jpg + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/resize-image-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/resize-image-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/compress-image-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/compress-image-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/remove-image-background + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/remove-image-background + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/pdf-to-word-editable-free + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-to-word-editable-free + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/compress-pdf-to-100kb + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/compress-pdf-to-100kb + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/ai-extract-text-from-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/ai-extract-text-from-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/pdf-to-excel-accurate-free + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-to-excel-accurate-free + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/split-pdf-online-free + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/split-pdf-online-free + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/compress-pdf-online-free + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/compress-pdf-online-free + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/unlock-pdf-online-free + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/unlock-pdf-online-free + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/summarize-pdf-ai + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/summarize-pdf-ai + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/convert-pdf-to-text-ai + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/convert-pdf-to-text-ai + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/pdf-to-jpg-high-quality + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-to-jpg-high-quality + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/jpg-to-pdf-online-free + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/jpg-to-pdf-online-free + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/reduce-pdf-size-for-email + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/reduce-pdf-size-for-email + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/ocr-for-scanned-pdfs + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/ocr-for-scanned-pdfs + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/remove-watermark-from-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/remove-watermark-from-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/add-watermark-to-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/add-watermark-to-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/repair-corrupted-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/repair-corrupted-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/rotate-pdf-pages-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/rotate-pdf-pages-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/reorder-pdf-pages-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/reorder-pdf-pages-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/pdf-to-png-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-to-png-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/images-to-pdf-multiple + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/images-to-pdf-multiple + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/split-pdf-by-range-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/split-pdf-by-range-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/compress-scanned-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/compress-scanned-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/pdf-metadata-editor-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-metadata-editor-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/add-page-numbers-to-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/add-page-numbers-to-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/protect-pdf-with-password-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/protect-pdf-with-password-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/unlock-encrypted-pdf-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/unlock-encrypted-pdf-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/ocr-table-extraction-from-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/ocr-table-extraction-from-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/pdf-to-excel-converter-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-to-excel-converter-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/extract-text-from-protected-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/extract-text-from-protected-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/bulk-convert-pdf-to-word + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/bulk-convert-pdf-to-word + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/compress-pdf-for-web-upload + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/compress-pdf-for-web-upload + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/ocr-multi-language-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/ocr-multi-language-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/summarize-long-pdf-ai + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/summarize-long-pdf-ai + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/convert-pdf-to-ppt-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/convert-pdf-to-ppt-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/pdf-to-pptx-free-online + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/pdf-to-pptx-free-online + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/دمج-ملفات-pdf-مجاناً + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/دمج-ملفات-pdf-مجاناً + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/ضغط-بي-دي-اف-اونلاين + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/ضغط-بي-دي-اف-اونلاين + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/تحويل-pdf-الى-word-قابل-للتعديل + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/تحويل-pdf-الى-word-قابل-للتعديل + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/تحويل-jpg-الى-pdf-اونلاين + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/تحويل-jpg-الى-pdf-اونلاين + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/فصل-صفحات-pdf-اونلاين + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/فصل-صفحات-pdf-اونلاين + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/ازالة-كلمة-مرور-من-pdf + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/ازالة-كلمة-مرور-من-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/تحويل-pdf-الى-نص-باستخدام-ocr + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/تحويل-pdf-الى-نص-باستخدام-ocr + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/تحويل-pdf-الى-excel-اونلاين + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/تحويل-pdf-الى-excel-اونلاين + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/تحويل-pdf-الى-صور + 2026-03-29 + weekly + 0.88 + + + https://dociva.io/ar/تحويل-pdf-الى-صور + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/best-pdf-tools + 2026-03-29 + weekly + 0.82 + + + https://dociva.io/ar/best-pdf-tools + 2026-03-29 + weekly + 0.74 + + + https://dociva.io/free-pdf-tools-online + 2026-03-29 + weekly + 0.82 + + + https://dociva.io/ar/free-pdf-tools-online + 2026-03-29 + weekly + 0.74 + + + https://dociva.io/convert-files-online + 2026-03-29 + weekly + 0.82 + + + https://dociva.io/ar/convert-files-online + 2026-03-29 + weekly + 0.74 + + + https://dociva.io/pdf-converter-tools + 2026-03-29 + weekly + 0.82 + + + https://dociva.io/ar/pdf-converter-tools + 2026-03-29 + weekly + 0.74 + + + https://dociva.io/secure-pdf-tools + 2026-03-29 + weekly + 0.82 + + + https://dociva.io/ar/secure-pdf-tools + 2026-03-29 + weekly + 0.74 + + + https://dociva.io/ai-document-tools + 2026-03-29 + weekly + 0.82 + + + https://dociva.io/ar/ai-document-tools + 2026-03-29 + weekly + 0.74 + + + https://dociva.io/image-to-pdf-tools + 2026-03-29 + weekly + 0.82 + + + https://dociva.io/ar/image-to-pdf-tools + 2026-03-29 + weekly + 0.74 + + + https://dociva.io/online-image-tools + 2026-03-29 + weekly + 0.82 + + + https://dociva.io/ar/online-image-tools + 2026-03-29 + weekly + 0.74 + + + https://dociva.io/office-to-pdf-tools + 2026-03-29 + weekly + 0.82 + + + https://dociva.io/ar/office-to-pdf-tools + 2026-03-29 + weekly + 0.74 + + + https://dociva.io/scanned-document-tools + 2026-03-29 + weekly + 0.82 + + + https://dociva.io/ar/scanned-document-tools + 2026-03-29 + weekly + 0.74 + + + https://dociva.io/arabic-pdf-tools + 2026-03-29 + weekly + 0.82 + + + https://dociva.io/ar/arabic-pdf-tools + 2026-03-29 + weekly + 0.74 + + diff --git a/frontend/public/sitemaps/static.xml b/frontend/public/sitemaps/static.xml new file mode 100644 index 0000000..b8e0d48 --- /dev/null +++ b/frontend/public/sitemaps/static.xml @@ -0,0 +1,57 @@ + + + + https://dociva.io/ + 2026-03-29 + daily + 1.0 + + + https://dociva.io/tools + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/about + 2026-03-29 + monthly + 0.4 + + + https://dociva.io/contact + 2026-03-29 + monthly + 0.4 + + + https://dociva.io/privacy + 2026-03-29 + yearly + 0.3 + + + https://dociva.io/terms + 2026-03-29 + yearly + 0.3 + + + https://dociva.io/pricing + 2026-03-29 + monthly + 0.7 + + + https://dociva.io/blog + 2026-03-29 + weekly + 0.6 + + + https://dociva.io/developers + 2026-03-29 + monthly + 0.5 + + diff --git a/frontend/public/sitemaps/tools.xml b/frontend/public/sitemaps/tools.xml new file mode 100644 index 0000000..b0c77e0 --- /dev/null +++ b/frontend/public/sitemaps/tools.xml @@ -0,0 +1,267 @@ + + + + https://dociva.io/tools/pdf-to-word + 2026-03-29 + weekly + 0.9 + + + https://dociva.io/tools/word-to-pdf + 2026-03-29 + weekly + 0.9 + + + https://dociva.io/tools/compress-pdf + 2026-03-29 + weekly + 0.9 + + + https://dociva.io/tools/merge-pdf + 2026-03-29 + weekly + 0.9 + + + https://dociva.io/tools/split-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/rotate-pdf + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/pdf-to-images + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/images-to-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/watermark-pdf + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/protect-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/unlock-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/page-numbers + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/pdf-editor + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/pdf-flowchart + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/pdf-to-excel + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/remove-watermark-pdf + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/reorder-pdf + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/extract-pages + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/image-converter + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/image-resize + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/compress-image + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/ocr + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/remove-background + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/image-to-svg + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/html-to-pdf + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/chat-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/summarize-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/translate-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/extract-tables + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/qr-code + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/video-to-gif + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/word-counter + 2026-03-29 + weekly + 0.6 + + + https://dociva.io/tools/text-cleaner + 2026-03-29 + weekly + 0.6 + + + https://dociva.io/tools/pdf-to-pptx + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/excel-to-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/pptx-to-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/sign-pdf + 2026-03-29 + weekly + 0.8 + + + https://dociva.io/tools/crop-pdf + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/flatten-pdf + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/repair-pdf + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/pdf-metadata + 2026-03-29 + weekly + 0.6 + + + https://dociva.io/tools/image-crop + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/image-rotate-flip + 2026-03-29 + weekly + 0.7 + + + https://dociva.io/tools/barcode-generator + 2026-03-29 + weekly + 0.7 + + diff --git a/frontend/scripts/generate-seo-assets.mjs b/frontend/scripts/generate-seo-assets.mjs index 92deb37..c86a615 100644 --- a/frontend/scripts/generate-seo-assets.mjs +++ b/frontend/scripts/generate-seo-assets.mjs @@ -1,4 +1,4 @@ -import { readFile, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -6,6 +6,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const frontendRoot = path.resolve(__dirname, '..'); const publicDir = path.join(frontendRoot, 'public'); +const sitemapDir = path.join(publicDir, 'sitemaps'); const siteOrigin = String(process.env.VITE_SITE_DOMAIN || 'https://dociva.io').trim().replace(/\/$/, ''); const today = new Date().toISOString().slice(0, 10); @@ -26,6 +27,7 @@ const routeRegistrySource = await readFile(path.join(frontendRoot, 'src', 'confi const staticPages = [ { path: '/', changefreq: 'daily', priority: '1.0' }, + { path: '/tools', changefreq: 'weekly', priority: '0.8' }, { path: '/about', changefreq: 'monthly', priority: '0.4' }, { path: '/contact', changefreq: 'monthly', priority: '0.4' }, { path: '/privacy', changefreq: 'yearly', priority: '0.3' }, @@ -94,6 +96,18 @@ function makeUrlTag({ loc, changefreq, priority }) { return ` \n ${loc}\n ${today}\n ${changefreq}\n ${priority}\n `; } +function makeSitemapUrlSet(entries) { + return `\n\n${entries.join('\n')}\n\n`; +} + +function makeSitemapIndex(entries) { + const items = entries + .map((entry) => ` \n ${entry.loc}\n ${today}\n `) + .join('\n'); + + return `\n\n${items}\n\n`; +} + function dedupeEntries(entries) { const seen = new Set(); return entries.filter((entry) => { @@ -110,22 +124,33 @@ const blogSource = await readFile(path.join(frontendRoot, 'src', 'content', 'blo const blogSlugs = extractBlogSlugs(blogSource); const toolSlugs = extractToolSlugs(routeRegistrySource); -const sitemapEntries = dedupeEntries([ - ...staticPages.map((page) => ({ +await mkdir(sitemapDir, { recursive: true }); + +const staticEntries = dedupeEntries( + staticPages.map((page) => ({ loc: `${siteOrigin}${page.path}`, changefreq: page.changefreq, priority: page.priority, })), - ...blogSlugs.map((slug) => ({ +).map((entry) => makeUrlTag(entry)); + +const blogEntries = dedupeEntries( + blogSlugs.map((slug) => ({ loc: `${siteOrigin}/blog/${slug}`, changefreq: 'monthly', priority: '0.6', })), - ...toolSlugs.map((slug) => ({ +).map((entry) => makeUrlTag(entry)); + +const toolEntries = dedupeEntries( + toolSlugs.map((slug) => ({ loc: `${siteOrigin}/tools/${slug}`, changefreq: 'weekly', priority: toolRoutePriorities.get(slug) || '0.6', })), +).map((entry) => makeUrlTag(entry)); + +const seoEntries = dedupeEntries([ ...seoConfig.toolPageSeeds.flatMap((page) => ([ { loc: `${siteOrigin}/${page.slug}`, changefreq: 'weekly', priority: '0.88' }, { loc: `${siteOrigin}/ar/${page.slug}`, changefreq: 'weekly', priority: '0.8' }, @@ -136,7 +161,12 @@ const sitemapEntries = dedupeEntries([ ])), ]).map((entry) => makeUrlTag(entry)); -const sitemap = `\n\n${sitemapEntries.join('\n')}\n\n`; +const sitemapIndex = makeSitemapIndex([ + { loc: `${siteOrigin}/sitemaps/static.xml` }, + { loc: `${siteOrigin}/sitemaps/blog.xml` }, + { loc: `${siteOrigin}/sitemaps/tools.xml` }, + { loc: `${siteOrigin}/sitemaps/seo.xml` }, +]); const robots = [ '# robots.txt — Dociva', @@ -156,7 +186,11 @@ const robots = [ '', ].join('\n'); -await writeFile(path.join(publicDir, 'sitemap.xml'), sitemap, 'utf8'); +await writeFile(path.join(publicDir, 'sitemap.xml'), sitemapIndex, 'utf8'); +await writeFile(path.join(sitemapDir, 'static.xml'), makeSitemapUrlSet(staticEntries), 'utf8'); +await writeFile(path.join(sitemapDir, 'blog.xml'), makeSitemapUrlSet(blogEntries), 'utf8'); +await writeFile(path.join(sitemapDir, 'tools.xml'), makeSitemapUrlSet(toolEntries), 'utf8'); +await writeFile(path.join(sitemapDir, 'seo.xml'), makeSitemapUrlSet(seoEntries), 'utf8'); await writeFile(path.join(publicDir, 'robots.txt'), robots, 'utf8'); -console.log(`Generated SEO assets for ${siteOrigin}`); \ No newline at end of file +console.log(`Generated SEO assets for ${siteOrigin}`); diff --git a/frontend/scripts/render-seo-shells.mjs b/frontend/scripts/render-seo-shells.mjs new file mode 100644 index 0000000..c6674d0 --- /dev/null +++ b/frontend/scripts/render-seo-shells.mjs @@ -0,0 +1,166 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const frontendRoot = path.resolve(__dirname, '..'); +const distRoot = path.join(frontendRoot, 'dist'); +const siteOrigin = String(process.env.VITE_SITE_DOMAIN || 'https://dociva.io').trim().replace(/\/$/, ''); +const seoPagesPath = path.join(frontendRoot, 'src', 'config', 'seo-tools.json'); +const seoToolsPath = path.join(frontendRoot, 'src', 'config', 'seoData.ts'); +const blogPath = path.join(frontendRoot, 'src', 'content', 'blogArticles.ts'); + +const baseHtml = await readFile(path.join(distRoot, 'index.html'), 'utf8'); +const seoPages = JSON.parse(await readFile(seoPagesPath, 'utf8')); +const seoToolsSource = await readFile(seoToolsPath, 'utf8'); +const blogSource = await readFile(blogPath, 'utf8'); + +const defaultTitle = 'Dociva — Free Online File Tools'; +const defaultDescription = 'Free online tools for PDF, image, video, and text processing. Merge, split, compress, convert, watermark, protect & more — instantly.'; + +function escapeHtml(value) { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function extractToolMetadata(source) { + const entries = new Map(); + const pattern = /i18nKey:\s*'([^']+)'[\s\S]*?slug:\s*'([^']+)'[\s\S]*?titleSuffix:\s*'([^']+)'[\s\S]*?metaDescription:\s*'([^']+)'/g; + + for (const match of source.matchAll(pattern)) { + const [, i18nKey, slug, titleSuffix, metaDescription] = match; + entries.set(slug, { + i18nKey, + title: `${titleSuffix} | Dociva`, + description: metaDescription, + }); + } + + return entries; +} + +function extractBlogEntries(source) { + const entries = []; + const pattern = /slug:\s*'([^']+)'[\s\S]*?title:\s*\{[\s\S]*?en:\s*'([^']+)'[\s\S]*?seoDescription:\s*\{[\s\S]*?en:\s*'([^']+)'/g; + + for (const match of source.matchAll(pattern)) { + const [, slug, title, description] = match; + entries.push({ slug, title: `${title} — Dociva`, description }); + } + + return entries; +} + +function interpolate(template, values) { + return template.replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (_, key) => values[key] ?? ''); +} + +function withMeta(html, { title, description, url }) { + const safeTitle = escapeHtml(title); + const safeDescription = escapeHtml(description); + const safeUrl = escapeHtml(url); + + let result = html + .replace(/.*?<\/title>/, `<title>${safeTitle}`) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``); + + result = result.replace( + '', + ` \n \n`, + ); + + return result; +} + +async function writeRouteShell(routePath, title, description) { + const normalizedPath = routePath === '/' ? '' : routePath.replace(/^\//, ''); + const targetDir = normalizedPath ? path.join(distRoot, normalizedPath) : distRoot; + const html = withMeta(baseHtml, { + title, + description, + url: `${siteOrigin}${routePath}`, + }); + + await mkdir(targetDir, { recursive: true }); + await writeFile(path.join(targetDir, 'index.html'), html, 'utf8'); +} + +const staticPages = [ + { path: '/', title: defaultTitle, description: defaultDescription }, + { path: '/tools', title: 'All Tools — Dociva', description: 'Browse every Dociva tool in one place. Explore PDF, image, AI, conversion, and utility workflows from a single search-friendly directory.' }, + { path: '/about', title: 'About Dociva', description: 'Learn about Dociva — free, fast, and secure online file tools for PDFs, images, video, and text. No registration required.' }, + { path: '/contact', title: 'Contact Dociva', description: 'Contact the Dociva team. Report bugs, request features, or send us a message.' }, + { path: '/privacy', title: 'Privacy Policy — Dociva', description: 'Privacy policy for Dociva. Learn how we handle your files and data with full transparency.' }, + { path: '/terms', title: 'Terms of Service — Dociva', description: 'Terms of service for Dociva. Understand the rules and guidelines for using our free online tools.' }, + { path: '/pricing', title: 'Pricing — Dociva', description: 'Compare free and pro plans for Dociva. Access 30+ tools for free, or upgrade for unlimited processing.' }, + { path: '/blog', title: 'Blog — Tips, Tutorials & Updates', description: 'Learn how to compress, convert, edit, and manage PDF files with our expert guides and tutorials.' }, + { path: '/developers', title: 'Developers — Dociva', description: 'Explore the Dociva developer portal, async API flow, and production-ready endpoints for document automation.' }, +]; + +for (const page of staticPages) { + await writeRouteShell(page.path, page.title, page.description); +} + +for (const blog of extractBlogEntries(blogSource)) { + await writeRouteShell(`/blog/${blog.slug}`, blog.title, blog.description); +} + +for (const [slug, tool] of extractToolMetadata(seoToolsSource)) { + await writeRouteShell(`/tools/${slug}`, tool.title, tool.description); +} + +for (const page of seoPages.toolPages) { + const englishTitle = interpolate(page.titleTemplate.en, { + brand: 'Dociva', + focusKeyword: page.focusKeyword.en, + }); + const arabicTitle = interpolate(page.titleTemplate.ar, { + brand: 'Dociva', + focusKeyword: page.focusKeyword.ar, + }); + + const englishDescription = interpolate(page.descriptionTemplate.en, { + brand: 'Dociva', + focusKeyword: page.focusKeyword.en, + }); + const arabicDescription = interpolate(page.descriptionTemplate.ar, { + brand: 'Dociva', + focusKeyword: page.focusKeyword.ar, + }); + + await writeRouteShell(`/${page.slug}`, `${englishTitle} — Dociva`, englishDescription); + await writeRouteShell(`/ar/${page.slug}`, `${arabicTitle} — Dociva`, arabicDescription); +} + +for (const page of seoPages.collectionPages) { + const englishTitle = interpolate(page.titleTemplate.en, { + brand: 'Dociva', + focusKeyword: page.focusKeyword.en, + }); + const arabicTitle = interpolate(page.titleTemplate.ar, { + brand: 'Dociva', + focusKeyword: page.focusKeyword.ar, + }); + + const englishDescription = interpolate(page.descriptionTemplate.en, { + brand: 'Dociva', + focusKeyword: page.focusKeyword.en, + }); + const arabicDescription = interpolate(page.descriptionTemplate.ar, { + brand: 'Dociva', + focusKeyword: page.focusKeyword.ar, + }); + + await writeRouteShell(`/${page.slug}`, `${englishTitle} — Dociva`, englishDescription); + await writeRouteShell(`/ar/${page.slug}`, `${arabicTitle} — Dociva`, arabicDescription); +} + +console.log('Rendered route-specific SEO shells.'); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d008eaf..2745264 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,6 +24,7 @@ const PricingPage = lazy(() => import('@/pages/PricingPage')); const BlogPage = lazy(() => import('@/pages/BlogPage')); const BlogPostPage = lazy(() => import('@/pages/BlogPostPage')); const DevelopersPage = lazy(() => import('@/pages/DevelopersPage')); +const AllToolsPage = lazy(() => import('@/pages/AllToolsPage')); const InternalAdminPage = lazy(() => import('@/pages/InternalAdminPage')); const SeoRoutePage = lazy(() => import('@/pages/SeoRoutePage')); const CookieConsent = lazy(() => import('@/components/layout/CookieConsent')); @@ -129,6 +130,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/layout/Footer.tsx b/frontend/src/components/layout/Footer.tsx index 4ec5e7c..11f56b0 100644 --- a/frontend/src/components/layout/Footer.tsx +++ b/frontend/src/components/layout/Footer.tsx @@ -92,6 +92,12 @@ export default function Footer() { > {t('common.terms')} + + {t('common.allTools')} + alternate.hrefLang === 'en')?.href ?? canonicalUrl; const schemas = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : []; @@ -45,7 +43,8 @@ export default function SEOHead({ title, description, keywords, path, type = 'we {fullTitle} - {keywords ? : null} + + {languageAlternates.map((alternate) => ( + {languageAlternates diff --git a/frontend/src/components/seo/ToolLandingPage.tsx b/frontend/src/components/seo/ToolLandingPage.tsx index ecae8c6..57bd929 100644 --- a/frontend/src/components/seo/ToolLandingPage.tsx +++ b/frontend/src/components/seo/ToolLandingPage.tsx @@ -2,7 +2,7 @@ import { Helmet } from 'react-helmet-async'; import { useTranslation } from 'react-i18next'; import { CheckCircle } from 'lucide-react'; import { getToolSEO } from '@/config/seoData'; -import { buildLanguageAlternates, buildSocialImageUrl, generateToolSchema, generateBreadcrumbs, generateFAQ, generateHowTo, getOgLocale, getSiteOrigin } from '@/utils/seo'; +import { buildSocialImageUrl, generateToolSchema, generateBreadcrumbs, generateFAQ, generateHowTo, getOgLocale, getSiteOrigin } from '@/utils/seo'; import BreadcrumbNav from './BreadcrumbNav'; import FAQSection from './FAQSection'; import RelatedTools from './RelatedTools'; @@ -43,7 +43,6 @@ export default function ToolLandingPage({ slug, children }: ToolLandingPageProps const path = `/tools/${slug}`; const canonicalUrl = `${origin}${path}`; const socialImageUrl = buildSocialImageUrl(origin); - const languageAlternates = buildLanguageAlternates(origin, path); const currentOgLocale = getOgLocale(i18n.language); const toolSchema = generateToolSchema({ @@ -77,18 +76,8 @@ export default function ToolLandingPage({ slug, children }: ToolLandingPageProps {toolTitle} — {seo.titleSuffix} | {t('common.appName')} - - {languageAlternates.map((alternate) => ( - - ))} - {/* Open Graph */} @@ -98,11 +87,6 @@ export default function ToolLandingPage({ slug, children }: ToolLandingPageProps - {languageAlternates - .filter((alternate) => alternate.ogLocale !== currentOgLocale) - .map((alternate) => ( - - ))} {/* Twitter */} diff --git a/frontend/src/config/routes.ts b/frontend/src/config/routes.ts index c35fad9..b774deb 100644 --- a/frontend/src/config/routes.ts +++ b/frontend/src/config/routes.ts @@ -21,6 +21,7 @@ const STATIC_PAGE_ROUTES = [ '/blog', '/blog/:slug', '/developers', + '/tools', '/internal/admin', ] as const; diff --git a/frontend/src/i18n/ar.json b/frontend/src/i18n/ar.json index 74d94a7..a60e3c5 100644 --- a/frontend/src/i18n/ar.json +++ b/frontend/src/i18n/ar.json @@ -256,6 +256,19 @@ "contactTitle": "8. الاتصال", "contactText": "أسئلة حول هذه الشروط؟ تواصل معنا على" }, + "toolsHub": { + "metaTitle": "كل الأدوات", + "metaDescription": "تصفح جميع أدوات Dociva في مكان واحد. استكشف مسارات PDF والصور والذكاء الاصطناعي والتحويل والأدوات المساعدة من دليل واحد سهل للأرشفة والزحف.", + "title": "جميع أدوات Dociva", + "description": "استخدم هذا الدليل لاستكشاف كل مسارات Dociva حسب الفئة ثم انتقل مباشرة إلى الأداة التي تحتاجها.", + "categories": { + "PDF": "أدوات PDF", + "Convert": "أدوات التحويل", + "Image": "أدوات الصور", + "AI": "أدوات الذكاء الاصطناعي", + "Utility": "أدوات مساعدة" + } + }, "cookie": { "title": "إعدادات ملفات الارتباط", "message": "نستخدم ملفات الارتباط لتحسين تجربتك وتحليل حركة الموقع. بالموافقة، فإنك توافق على ملفات الارتباط التحليلية.", diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index ba03ae4..475e303 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -256,6 +256,19 @@ "contactTitle": "8. Contact", "contactText": "Questions about these terms? Contact us at" }, + "toolsHub": { + "metaTitle": "All Tools", + "metaDescription": "Browse every Dociva tool in one place. Explore PDF, image, AI, conversion, and utility workflows from a single search-friendly directory.", + "title": "All Dociva Tools", + "description": "Use this directory to explore every Dociva workflow by category and jump directly to the tool you need.", + "categories": { + "PDF": "PDF Tools", + "Convert": "Convert Tools", + "Image": "Image Tools", + "AI": "AI Tools", + "Utility": "Utility Tools" + } + }, "cookie": { "title": "Cookie Settings", "message": "We use cookies to improve your experience and analyze site traffic. By accepting, you consent to analytics cookies.", diff --git a/frontend/src/i18n/fr.json b/frontend/src/i18n/fr.json index 24f1dc9..27bc679 100644 --- a/frontend/src/i18n/fr.json +++ b/frontend/src/i18n/fr.json @@ -256,6 +256,19 @@ "contactTitle": "8. Contact", "contactText": "Des questions sur ces conditions ? Contactez-nous à" }, + "toolsHub": { + "metaTitle": "Tous les outils", + "metaDescription": "Parcourez tous les outils Dociva depuis une seule page. Explorez les workflows PDF, image, IA, conversion et utilitaires dans un répertoire clair et optimisé pour la découverte.", + "title": "Tous les outils Dociva", + "description": "Utilisez ce répertoire pour parcourir chaque workflow Dociva par catégorie et ouvrir directement l'outil dont vous avez besoin.", + "categories": { + "PDF": "Outils PDF", + "Convert": "Outils de conversion", + "Image": "Outils d'image", + "AI": "Outils IA", + "Utility": "Outils utilitaires" + } + }, "cookie": { "title": "Paramètres des cookies", "message": "Nous utilisons des cookies pour améliorer votre expérience et analyser le trafic du site. En acceptant, vous consentez aux cookies analytiques.", diff --git a/frontend/src/pages/AllToolsPage.tsx b/frontend/src/pages/AllToolsPage.tsx new file mode 100644 index 0000000..97f0b3b --- /dev/null +++ b/frontend/src/pages/AllToolsPage.tsx @@ -0,0 +1,99 @@ +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import SEOHead from '@/components/seo/SEOHead'; +import BreadcrumbNav from '@/components/seo/BreadcrumbNav'; +import { TOOLS_SEO } from '@/config/seoData'; +import { generateBreadcrumbs, generateCollectionPage, generateItemList, getSiteOrigin } from '@/utils/seo'; + +const CATEGORY_ORDER = ['PDF', 'Convert', 'Image', 'AI', 'Utility'] as const; + +export default function AllToolsPage() { + const { t } = useTranslation(); + const origin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : ''); + const path = '/tools'; + const url = `${origin}${path}`; + + const groupedTools = CATEGORY_ORDER.map((category) => ({ + category, + items: TOOLS_SEO.filter((tool) => tool.category === category), + })).filter((group) => group.items.length > 0); + + const jsonLd = [ + generateCollectionPage({ + name: t('pages.toolsHub.metaTitle'), + description: t('pages.toolsHub.metaDescription'), + url, + }), + generateBreadcrumbs([ + { name: t('common.home'), url: origin }, + { name: t('common.allTools'), url }, + ]), + generateItemList( + TOOLS_SEO.map((tool) => ({ + name: t(`tools.${tool.i18nKey}.title`), + url: `${origin}/tools/${tool.slug}`, + })), + ), + ]; + + return ( + <> + + +
+
+ + +

+ {t('pages.toolsHub.title')} +

+

+ {t('pages.toolsHub.description')} +

+
+ + {groupedTools.map((group) => ( +
+

+ {t(`pages.toolsHub.categories.${group.category}`)} +

+ +
+ {group.items.map((tool) => ( + +

+ {group.category} +

+

+ {t(`tools.${tool.i18nKey}.title`)} +

+

+ {t(`tools.${tool.i18nKey}.shortDesc`)} +

+ + ))} +
+
+ ))} +
+ + ); +} diff --git a/frontend/src/pages/BlogPage.tsx b/frontend/src/pages/BlogPage.tsx index a098367..5c7ba99 100644 --- a/frontend/src/pages/BlogPage.tsx +++ b/frontend/src/pages/BlogPage.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom'; import SEOHead from '@/components/seo/SEOHead'; -import { generateWebPage, getSiteOrigin } from '@/utils/seo'; +import { generateCollectionPage, generateItemList, getSiteOrigin } from '@/utils/seo'; import { BookOpen, Calendar, ArrowRight, Search, X } from 'lucide-react'; import { BLOG_ARTICLES, @@ -44,11 +44,17 @@ export default function BlogPage() { title={t('pages.blog.metaTitle')} description={t('pages.blog.metaDescription')} path="/blog" - jsonLd={generateWebPage({ - name: t('pages.blog.metaTitle'), - description: t('pages.blog.metaDescription'), - url: `${siteOrigin}/blog`, - })} + jsonLd={[ + generateCollectionPage({ + name: t('pages.blog.metaTitle'), + description: t('pages.blog.metaDescription'), + url: `${siteOrigin}/blog`, + }), + generateItemList(posts.map((post) => ({ + name: post.title, + url: `${siteOrigin}/blog/${post.slug}`, + }))), + ]} />
diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index e850299..1bfc329 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -2,7 +2,7 @@ import { useDeferredValue } from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import SEOHead from '@/components/seo/SEOHead'; -import { generateOrganization, getSiteOrigin } from '@/utils/seo'; +import { generateOrganization, generateWebSite, getSiteOrigin } from '@/utils/seo'; import { FileText, FileOutput, @@ -121,18 +121,10 @@ export default function HomePage() { description={t('home.heroSub')} path="/" jsonLd={[ - { - '@context': 'https://schema.org', - '@type': 'WebSite', - name: t('common.appName'), - url: siteOrigin, + generateWebSite({ + origin: siteOrigin, description: t('home.heroSub'), - potentialAction: { - '@type': 'SearchAction', - target: `${siteOrigin}/?q={search_term_string}`, - 'query-input': 'required name=search_term_string', - }, - }, + }), generateOrganization(siteOrigin), ]} /> diff --git a/frontend/src/pages/SeoCollectionPage.tsx b/frontend/src/pages/SeoCollectionPage.tsx index b4073d4..bbc88f2 100644 --- a/frontend/src/pages/SeoCollectionPage.tsx +++ b/frontend/src/pages/SeoCollectionPage.tsx @@ -6,13 +6,19 @@ import SEOHead from '@/components/seo/SEOHead'; import FAQSection from '@/components/seo/FAQSection'; import { getLocalizedText, - getLocalizedTextList, getSeoCollectionPage, interpolateTemplate, normalizeSeoLocale, } from '@/config/seoPages'; import { getToolSEO } from '@/config/seoData'; -import { generateBreadcrumbs, generateFAQ, generateWebPage, getSiteOrigin } from '@/utils/seo'; +import { + generateBreadcrumbs, + generateCollectionPage, + generateFAQ, + generateItemList, + generateWebPage, + getSiteOrigin, +} from '@/utils/seo'; import NotFoundPage from '@/pages/NotFoundPage'; interface SeoCollectionPageProps { @@ -64,7 +70,6 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) { const title = interpolateTemplate(getLocalizedText(page.titleTemplate, locale), tokens); const description = interpolateTemplate(getLocalizedText(page.descriptionTemplate, locale), tokens); const intro = interpolateTemplate(getLocalizedText(page.introTemplate, locale), tokens); - const keywords = [focusKeyword, ...getLocalizedTextList(page.supportingKeywords, locale)].join(', '); const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : ''); const faqItems = page.faqTemplates.map((item) => ({ question: getLocalizedText(item.question, locale), @@ -82,6 +87,11 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) { ]; const jsonLd = [ + generateCollectionPage({ + name: title, + description, + url, + }), generateWebPage({ name: title, description, @@ -93,11 +103,18 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) { { name: title, url }, ]), generateFAQ(faqItems), + generateItemList(page.targetToolSlugs.map((toolSlug) => { + const tool = getToolSEO(toolSlug); + return { + name: tool ? t(`tools.${tool.i18nKey}.title`) : toolSlug, + url: `${siteOrigin}/tools/${toolSlug}`, + }; + })), ]; return ( <> - +
@@ -227,4 +244,4 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) {
); -} \ No newline at end of file +} diff --git a/frontend/src/pages/SeoPage.tsx b/frontend/src/pages/SeoPage.tsx index 1411125..a626820 100644 --- a/frontend/src/pages/SeoPage.tsx +++ b/frontend/src/pages/SeoPage.tsx @@ -8,7 +8,6 @@ import RelatedTools from '@/components/seo/RelatedTools'; import SuggestedTools from '@/components/seo/SuggestedTools'; import { getLocalizedText, - getLocalizedTextList, getProgrammaticToolPage, getSeoCollectionPage, interpolateTemplate, @@ -19,6 +18,7 @@ import { generateBreadcrumbs, generateFAQ, generateHowTo, + generateItemList, generateToolSchema, generateWebPage, getSiteOrigin, @@ -82,7 +82,6 @@ export default function SeoPage({ slug }: SeoPageProps) { 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, @@ -139,11 +138,25 @@ export default function SeoPage({ slug }: SeoPageProps) { url, }), generateFAQ(faqItems), + generateItemList(page.relatedCollectionSlugs.map((collectionSlug) => { + const collection = getSeoCollectionPage(collectionSlug); + const collectionTitle = collection + ? interpolateTemplate(getLocalizedText(collection.titleTemplate, locale), { + brand: 'Dociva', + focusKeyword: getLocalizedText(collection.focusKeyword, locale), + }) + : collectionSlug; + + return { + name: collectionTitle, + url: `${siteOrigin}${localizedCollectionPath(collectionSlug)}`, + }; + })), ]; return ( <> - +
@@ -312,4 +325,4 @@ export default function SeoPage({ slug }: SeoPageProps) {
); -} \ No newline at end of file +} diff --git a/frontend/src/utils/seo.ts b/frontend/src/utils/seo.ts index 72e8a29..21ffcea 100644 --- a/frontend/src/utils/seo.ts +++ b/frontend/src/utils/seo.ts @@ -9,6 +9,7 @@ export interface ToolSeoData { category?: string; ratingValue?: number; ratingCount?: number; + features?: string[]; } export interface LanguageAlternate { @@ -19,6 +20,7 @@ export interface LanguageAlternate { const DEFAULT_SOCIAL_IMAGE_PATH = '/social-preview.svg'; const DEFAULT_SITE_ORIGIN = 'https://dociva.io'; +const DEFAULT_SITE_NAME = 'Dociva'; const LANGUAGE_CONFIG: Record<'en' | 'ar' | 'fr', { hrefLang: string; ogLocale: string }> = { en: { hrefLang: 'en', ogLocale: 'en_US' }, @@ -35,13 +37,16 @@ export function getOgLocale(language: string): string { return LANGUAGE_CONFIG[normalizeSiteLanguage(language)].ogLocale; } -export function buildLanguageAlternates(origin: string, path: string): LanguageAlternate[] { - const separator = path.includes('?') ? '&' : '?'; - return (Object.entries(LANGUAGE_CONFIG) as Array<[keyof typeof LANGUAGE_CONFIG, (typeof LANGUAGE_CONFIG)[keyof typeof LANGUAGE_CONFIG]]>) - .map(([language, config]) => ({ - hrefLang: config.hrefLang, - href: `${origin}${path}${separator}lng=${language}`, - ogLocale: config.ogLocale, +export function buildLanguageAlternates( + origin: string, + localizedPaths: Partial>, +): LanguageAlternate[] { + return (Object.entries(localizedPaths) as Array<[keyof typeof LANGUAGE_CONFIG, string | undefined]>) + .filter(([, path]) => Boolean(path)) + .map(([language, path]) => ({ + hrefLang: LANGUAGE_CONFIG[language].hrefLang, + href: `${origin}${path}`, + ogLocale: LANGUAGE_CONFIG[language].ogLocale, })); } @@ -68,20 +73,33 @@ export function buildSocialImageUrl(origin: string): string { export function generateToolSchema(tool: ToolSeoData): object { const schema: Record = { '@context': 'https://schema.org', - '@type': 'WebApplication', + '@type': 'SoftwareApplication', name: tool.name, url: tool.url, applicationCategory: tool.category || 'UtilitiesApplication', + applicationSubCategory: tool.category || 'UtilitiesApplication', operatingSystem: 'Any', + browserRequirements: 'Requires JavaScript. Works in modern browsers.', + isAccessibleForFree: true, offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD', + availability: 'https://schema.org/InStock', }, description: tool.description, inLanguage: ['en', 'ar', 'fr'], + provider: { + '@type': 'Organization', + name: DEFAULT_SITE_NAME, + url: getSiteOrigin(), + }, }; + if (tool.features && tool.features.length > 0) { + schema.featureList = tool.features; + } + if (tool.ratingValue && tool.ratingCount && tool.ratingCount > 0) { schema.aggregateRating = { '@type': 'AggregateRating', @@ -161,10 +179,14 @@ export function generateOrganization(origin: string): object { return { '@context': 'https://schema.org', '@type': 'Organization', - name: 'Dociva', + '@id': `${origin}/#organization`, + name: DEFAULT_SITE_NAME, + alternateName: 'Dociva File Tools', url: origin, - logo: `${origin}/favicon.svg`, - sameAs: [], + logo: { + '@type': 'ImageObject', + url: `${origin}/logo.svg`, + }, contactPoint: { '@type': 'ContactPoint', email: 'support@dociva.io', @@ -188,13 +210,68 @@ export function generateWebPage(page: { name: page.name, description: page.description, url: page.url, + inLanguage: ['en', 'ar', 'fr'], isPartOf: { '@type': 'WebSite', - name: 'Dociva', + '@id': `${getSiteOrigin()}/#website`, + name: DEFAULT_SITE_NAME, }, }; } +export function generateWebSite(data: { + origin: string; + description: string; +}): object { + return { + '@context': 'https://schema.org', + '@type': 'WebSite', + '@id': `${data.origin}/#website`, + name: DEFAULT_SITE_NAME, + url: data.origin, + description: data.description, + publisher: { + '@id': `${data.origin}/#organization`, + }, + inLanguage: ['en', 'ar', 'fr'], + potentialAction: { + '@type': 'SearchAction', + target: `${data.origin}/?q={search_term_string}`, + 'query-input': 'required name=search_term_string', + }, + }; +} + +export function generateCollectionPage(data: { + name: string; + description: string; + url: string; +}): object { + return { + '@context': 'https://schema.org', + '@type': 'CollectionPage', + name: data.name, + description: data.description, + url: data.url, + isPartOf: { + '@id': `${getSiteOrigin()}/#website`, + }, + }; +} + +export function generateItemList(items: { name: string; url: string }[]): object { + return { + '@context': 'https://schema.org', + '@type': 'ItemList', + itemListElement: items.map((item, index) => ({ + '@type': 'ListItem', + position: index + 1, + name: item.name, + url: item.url, + })), + }; +} + export function generateBlogPosting(post: { headline: string; description: string; @@ -202,6 +279,7 @@ export function generateBlogPosting(post: { datePublished: string; inLanguage: string; }): object { + const origin = getSiteOrigin(); return { '@context': 'https://schema.org', '@type': 'BlogPosting', @@ -211,14 +289,23 @@ export function generateBlogPosting(post: { datePublished: post.datePublished, dateModified: post.datePublished, inLanguage: post.inLanguage, + isAccessibleForFree: true, author: { '@type': 'Organization', - name: 'Dociva', + name: DEFAULT_SITE_NAME, }, publisher: { '@type': 'Organization', - name: 'Dociva', + '@id': `${origin}/#organization`, + name: DEFAULT_SITE_NAME, + logo: { + '@type': 'ImageObject', + url: `${origin}/logo.svg`, + }, + }, + mainEntityOfPage: { + '@type': 'WebPage', + '@id': post.url, }, - mainEntityOfPage: post.url, }; }