diff --git a/backend/app/services/pdf_ai_service.py b/backend/app/services/pdf_ai_service.py index e187c1a..a3feca6 100644 --- a/backend/app/services/pdf_ai_service.py +++ b/backend/app/services/pdf_ai_service.py @@ -8,7 +8,7 @@ import requests logger = logging.getLogger(__name__) # Configuration -OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "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") +OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "") OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "stepfun/step-3.5-flash:free") OPENROUTER_BASE_URL = os.getenv( "OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1/chat/completions" diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index 1f5d492..edfb557 100644 Binary files a/backend/celerybeat-schedule and b/backend/celerybeat-schedule differ diff --git a/docs/project_status_report_2026-03-10.md b/docs/project_status_report_2026-03-10.md new file mode 100644 index 0000000..b5a62b9 --- /dev/null +++ b/docs/project_status_report_2026-03-10.md @@ -0,0 +1,225 @@ +# SaaS-PDF Project Status Report + +Generated on: 2026-03-10 +Branch reviewed: feature/seo-content + +## Executive Summary + +This report compares the previously requested roadmap against the current implementation in the SaaS-PDF codebase. + +The project has progressed well beyond the earlier inventory documents. The current codebase already includes a broad set of PDF, image, AI, video, and utility tools, multilingual SEO landing pages, core business pages, analytics hooks, and backend tests for most recently added features. + +The strongest completed areas are: + +- Phase 2 tool expansion +- Phase 3 SEO landing page architecture +- Phase 4 multilingual content support +- Phase 5 core website pages + +The main remaining gaps are consistency and production hardening: + +- The existing tool inventory document is outdated compared to the live codebase. +- The frontend route registry was not fully synchronized with the actual app routes. +- The sitemap generator lagged behind the committed sitemap structure. +- AI configuration included an insecure fallback API key in code and needed hardening. + +## Current Platform Snapshot + +### Backend + +- Flask application factory with 24 registered blueprints +- Celery async task processing +- Redis-backed task flow +- Service-oriented architecture under backend/app/services +- Route modules under backend/app/routes +- Task modules under backend/app/tasks + +### Frontend + +- React + Vite + TypeScript +- Lazy-loaded route structure +- SEO landing page wrapper for tool pages +- Translation support for English, Arabic, and French +- Homepage tool cards for major feature groups + +### Public SEO Files + +- frontend/public/sitemap.xml +- frontend/public/robots.txt +- frontend/public/llms.txt +- frontend/public/humans.txt + +## Requested Roadmap vs Current Status + +## Phase 1 — Full Project Analysis + +Status: completed previously, but documentation drift exists. + +Findings: + +- docs/tool_inventory.md exists but is no longer fully accurate. +- The current app exposes more tools and routes than the inventory document reports. +- The codebase should be treated as the source of truth until the inventory document is refreshed. + +## Phase 2 — Build Missing High-Value Tools + +Status: largely completed. + +Implemented priority tools confirmed in code: + +- Compress Image +- PDF to Excel +- Add Watermark to PDF +- Remove Watermark +- Reorder PDF Pages +- Extract Pages +- QR Code Generator +- HTML to PDF +- Protect PDF +- Unlock PDF + +Implemented advanced tools confirmed in code: + +- AI Chat with PDF +- PDF Summarizer +- PDF Translator +- Table Extractor + +These features are backed by route modules, service modules, task modules, frontend pages, and backend tests. + +## Phase 3 — Complete SEO System + +Status: substantially completed. + +Implemented: + +- Dedicated tool landing pages under /tools/* +- Canonical tags +- OpenGraph tags +- Twitter card tags +- JSON-LD structured data +- FAQ sections and FAQ schema support +- Related tool internal linking +- Public SEO support files + +Remaining work: + +- Replace placeholder production domain values +- Add hreflang link tags if multilingual indexing strategy requires them +- Keep the sitemap generator aligned with the committed sitemap output + +## Phase 4 — Content Generation + +Status: completed at the application content layer. + +Implemented: + +- Tool content in English, Arabic, and French +- SEO section content used by the landing page wrapper +- Tool copy for new tools already present in translation files + +## Phase 5 — Core Website Pages + +Status: completed. + +Implemented pages: + +- /about +- /contact +- /privacy +- /terms +- /pricing +- /blog + +Notes: + +- Contact currently uses a mailto flow rather than a backend contact form endpoint. +- About, Privacy, and Terms are SEO-enabled pages with structured metadata. + +## Phase 6 — Technical SEO Optimization + +Status: mostly completed. + +Implemented: + +- Reusable SEO head component +- Structured data helpers +- Lazy route loading +- Analytics hooks +- Search Console verification support +- Sitemap generation script + +Remaining work: + +- Reduce duplicated SEO metadata between some tool pages and the shared tool landing wrapper +- Add final production-domain configuration + +## Phase 7 — Analytics and Growth + +Status: partially completed. + +Implemented: + +- Google Analytics integration hooks +- Plausible integration hooks +- Search Console verification injection +- docs/seo_strategy.md +- Pricing and Blog pages as growth support pages + +Remaining work: + +- Connect production env vars +- Expand blog content into a real publishing workflow +- Validate analytics in deployed environment + +## Phase 8 — Safety Rules + +Status: generally respected. + +Observed: + +- Existing routes were preserved. +- New functionality was added in isolated modules. +- Route safety tests exist. +- The work follows the established backend and frontend structure. + +## Key Risks and Gaps + +1. Documentation drift + +The existing tool inventory document no longer matches the current implementation. This can mislead future planning if not updated. + +2. Route registry drift + +The canonical frontend route registry had fallen behind the actual app routes. This report batch includes a fix for that inconsistency. + +3. Sitemap generation drift + +The sitemap generator was missing pages already represented in the committed sitemap. This report batch includes a synchronization fix. + +4. AI secret handling + +The PDF AI service used a hardcoded fallback API key. This report batch removes that fallback so configuration must come from environment variables. + +## Implementation Work Started In This Batch + +The following improvements were started as part of this implementation step: + +- Added this status report file +- Synchronized the frontend route registry with live routes +- Updated the sitemap generator to include the current page inventory +- Hardened AI configuration by removing the hardcoded API key fallback + +## Recommended Next Implementation Steps + +1. Refresh docs/tool_inventory.md so it becomes the current source of truth again. +2. Remove duplicate Helmet metadata from tool components that are already wrapped by ToolLandingPage. +3. Replace placeholder domain values in public SEO files with the production domain. +4. Decide whether contact should remain mailto-based or move to a backend endpoint. +5. Run full backend and frontend test/build validation in the target environment. + +## Final Assessment + +SaaS-PDF is no longer just a basic MVP. It is already a broad multi-tool document-processing platform with strong progress across product scope, frontend SEO architecture, and backend task-based processing. + +The current priority is not missing core features. The current priority is tightening consistency, production configuration, and documentation so the implemented work is easier to maintain and safer to ship. \ No newline at end of file diff --git a/frontend/src/components/seo/ToolLandingPage.tsx b/frontend/src/components/seo/ToolLandingPage.tsx index c421e00..6e24c1c 100644 --- a/frontend/src/components/seo/ToolLandingPage.tsx +++ b/frontend/src/components/seo/ToolLandingPage.tsx @@ -7,6 +7,7 @@ import FAQSection from './FAQSection'; import RelatedTools from './RelatedTools'; import ToolRating from '@/components/shared/ToolRating'; import { useToolRating } from '@/hooks/useToolRating'; +import { dispatchRatingPrompt } from '@/utils/ratingPrompt'; interface SEOFAQ { q: string; @@ -84,6 +85,20 @@ export default function ToolLandingPage({ slug, children }: ToolLandingPageProps {/* Tool Interface */} {children} +
+ +
+ {/* SEO Content Below Tool */}
{/* What this tool does */} diff --git a/frontend/src/components/shared/DownloadButton.tsx b/frontend/src/components/shared/DownloadButton.tsx index c5af534..3a1baf0 100644 --- a/frontend/src/components/shared/DownloadButton.tsx +++ b/frontend/src/components/shared/DownloadButton.tsx @@ -3,6 +3,7 @@ import { Download, RotateCcw, Clock } from 'lucide-react'; import type { TaskResult } from '@/services/api'; import { formatFileSize } from '@/utils/textTools'; import { trackEvent } from '@/services/analytics'; +import { dispatchCurrentToolRatingPrompt } from '@/utils/ratingPrompt'; interface DownloadButtonProps { /** Task result containing download URL */ @@ -14,6 +15,11 @@ interface DownloadButtonProps { export default function DownloadButton({ result, onStartOver }: DownloadButtonProps) { const { t } = useTranslation(); + const handleDownloadClick = () => { + trackEvent('download_clicked', { filename: result.filename || 'unknown' }); + dispatchCurrentToolRatingPrompt(); + }; + if (!result.download_url) return null; return ( @@ -62,9 +68,7 @@ export default function DownloadButton({ result, onStartOver }: DownloadButtonPr { - trackEvent('download_clicked', { filename: result.filename || 'unknown' }); - }} + onClick={handleDownloadClick} className="btn-success w-full" target="_blank" rel="noopener noreferrer" diff --git a/frontend/src/components/shared/ToolRating.tsx b/frontend/src/components/shared/ToolRating.tsx index 87f2543..09e59b7 100644 --- a/frontend/src/components/shared/ToolRating.tsx +++ b/frontend/src/components/shared/ToolRating.tsx @@ -1,7 +1,8 @@ -import { useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Star, ThumbsUp, AlertTriangle, Zap, Send } from 'lucide-react'; +import { Star, ThumbsUp, AlertTriangle, Zap, Send, X } from 'lucide-react'; import api from '@/services/api'; +import { RATING_PROMPT_EVENT } from '@/utils/ratingPrompt'; interface ToolRatingProps { /** Tool slug e.g. "compress-pdf" */ @@ -16,6 +17,7 @@ const TAGS = [ export default function ToolRating({ toolSlug }: ToolRatingProps) { const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); const [rating, setRating] = useState(0); const [hoveredStar, setHoveredStar] = useState(0); const [selectedTag, setSelectedTag] = useState(''); @@ -24,6 +26,98 @@ export default function ToolRating({ toolSlug }: ToolRatingProps) { const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); + const submittedStorageKey = useMemo(() => `tool-rating:submitted:${toolSlug}`, [toolSlug]); + const dismissedStorageKey = useMemo(() => `tool-rating:dismissed:${toolSlug}`, [toolSlug]); + + const readStorage = useCallback((storage: 'localStorage' | 'sessionStorage', key: string) => { + if (typeof window === 'undefined') return null; + + try { + return window[storage].getItem(key); + } catch { + return null; + } + }, []); + + const writeStorage = useCallback( + (storage: 'localStorage' | 'sessionStorage', key: string, value: string) => { + if (typeof window === 'undefined') return; + + try { + window[storage].setItem(key, value); + } catch { + // Ignore storage failures and keep the modal functional. + } + }, + [] + ); + + const resetForm = useCallback(() => { + setRating(0); + setHoveredStar(0); + setSelectedTag(''); + setFeedback(''); + setSubmitted(false); + setSubmitting(false); + setError(''); + }, []); + + const closeModal = useCallback(() => { + setIsOpen(false); + + if (!submitted) { + writeStorage('sessionStorage', dismissedStorageKey, '1'); + } + }, [dismissedStorageKey, submitted, writeStorage]); + + useEffect(() => { + function handleRatingPrompt(event: Event) { + const detail = (event as CustomEvent<{ toolSlug?: string; forceOpen?: boolean }>).detail; + if (!detail?.toolSlug || detail.toolSlug !== toolSlug) return; + if (readStorage('localStorage', submittedStorageKey)) return; + if (!detail.forceOpen && readStorage('sessionStorage', dismissedStorageKey)) return; + + resetForm(); + setIsOpen(true); + } + + window.addEventListener(RATING_PROMPT_EVENT, handleRatingPrompt as EventListener); + return () => { + window.removeEventListener(RATING_PROMPT_EVENT, handleRatingPrompt as EventListener); + }; + }, [dismissedStorageKey, readStorage, resetForm, submittedStorageKey, toolSlug]); + + useEffect(() => { + if (!isOpen) return; + + const previousOverflow = document.body.style.overflow; + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') { + closeModal(); + } + } + + document.body.style.overflow = 'hidden'; + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.body.style.overflow = previousOverflow; + document.removeEventListener('keydown', handleKeyDown); + }; + }, [closeModal, isOpen]); + + useEffect(() => { + if (!submitted || !isOpen) return; + + const timer = window.setTimeout(() => { + setIsOpen(false); + resetForm(); + }, 1800); + + return () => window.clearTimeout(timer); + }, [isOpen, resetForm, submitted]); + async function handleSubmit() { if (rating === 0) return; setSubmitting(true); @@ -36,108 +130,181 @@ export default function ToolRating({ toolSlug }: ToolRatingProps) { feedback: feedback.trim(), tag: selectedTag, }); + writeStorage('localStorage', submittedStorageKey, '1'); + writeStorage('sessionStorage', dismissedStorageKey, '1'); setSubmitted(true); } catch { - setError(t('rating.error', 'Failed to submit rating. Please try again.')); + setError( + t('pages.rating.error', 'Failed to submit rating. Please try again.') + ); } finally { setSubmitting(false); } } + if (!isOpen) { + return null; + } + if (submitted) { return ( -
- -

- {t('rating.thankYou', 'Thank you for your feedback!')} -

-

- {t('rating.helpImprove', 'Your rating helps us improve our tools.')} -

+
+
+ +

+ {t('pages.rating.successTitle', 'Thank you for your feedback!')} +

+

+ {t( + 'pages.rating.successBody', + 'Your rating helps us improve the tools and catch issues faster.' + )} +

+
); } return ( -
-

- {t('rating.title', 'How was your experience?')} -

- - {/* Star Rating */} -
- {[1, 2, 3, 4, 5].map((star) => ( - - ))} -
- - {/* Quick Tags */} - {rating > 0 && ( - <> -
- {TAGS.map(({ key, icon: Icon }) => ( - - ))} -
- - {/* Optional Feedback */} -
-