Add Plausible and Google Site Verification support to analytics and environment configuration
This commit is contained in:
@@ -28,6 +28,9 @@ CORS_ORIGINS=http://localhost:5173,http://localhost:3000
|
|||||||
|
|
||||||
# Frontend Analytics / Ads (Vite)
|
# Frontend Analytics / Ads (Vite)
|
||||||
VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX
|
VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX
|
||||||
|
VITE_PLAUSIBLE_DOMAIN=
|
||||||
|
VITE_PLAUSIBLE_SRC=https://plausible.io/js/script.js
|
||||||
|
VITE_GOOGLE_SITE_VERIFICATION=
|
||||||
VITE_ADSENSE_CLIENT_ID=ca-pub-XXXXXXXXXXXXXXXX
|
VITE_ADSENSE_CLIENT_ID=ca-pub-XXXXXXXXXXXXXXXX
|
||||||
VITE_ADSENSE_SLOT_HOME_TOP=1234567890
|
VITE_ADSENSE_SLOT_HOME_TOP=1234567890
|
||||||
VITE_ADSENSE_SLOT_HOME_BOTTOM=1234567891
|
VITE_ADSENSE_SLOT_HOME_BOTTOM=1234567891
|
||||||
|
|||||||
252
docs/seo_strategy.md
Normal file
252
docs/seo_strategy.md
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
# SaaS-PDF — SEO & Growth Strategy
|
||||||
|
|
||||||
|
> Roadmap to **500 000 monthly visits** for a multilingual (EN / AR / FR) free-tool SaaS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Current Technical SEO Foundation
|
||||||
|
|
||||||
|
| Layer | Status |
|
||||||
|
|-------|--------|
|
||||||
|
| **Canonical URLs** | Every page emits `<link rel="canonical">` via `SEOHead` |
|
||||||
|
| **OpenGraph tags** | `og:title`, `og:description`, `og:url`, `og:type`, `og:site_name`, `og:locale` on all pages |
|
||||||
|
| **Twitter cards** | `twitter:card`, `twitter:title`, `twitter:description` on all pages |
|
||||||
|
| **Structured data** | `WebSite`, `Organization`, `WebPage`, `WebApplication`, `BreadcrumbList`, `FAQPage` JSON-LD |
|
||||||
|
| **Sitemap** | Auto-generated via `scripts/generate_sitemap.py` — 37 URLs (5 pages + 32 tools) |
|
||||||
|
| **robots.txt** | Allows all crawlers; blocks `/api/`, `/account`, auth pages |
|
||||||
|
| **Internationalization** | Full i18n in EN, AR, FR — all tool pages, SEO content, and static pages |
|
||||||
|
| **Font loading** | `dns-prefetch` + `preconnect` + `display=swap` for Google Fonts |
|
||||||
|
| **Analytics** | GA4 (opt-in via `VITE_GA_MEASUREMENT_ID`) + Plausible (opt-in via `VITE_PLAUSIBLE_DOMAIN`) |
|
||||||
|
| **Search Console** | Verification via `VITE_GOOGLE_SITE_VERIFICATION` meta tag |
|
||||||
|
| **Page speed** | Code-split (lazy routes), Vite manual chunks, CSS minification, nginx gzip |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Analytics Setup
|
||||||
|
|
||||||
|
### Google Analytics 4
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX
|
||||||
|
```
|
||||||
|
|
||||||
|
- Auto-loaded via `initAnalytics()` on first render
|
||||||
|
- Page views tracked on every route change
|
||||||
|
- Custom events via `trackEvent('tool_used', { tool: 'compress-pdf' })`
|
||||||
|
|
||||||
|
### Plausible (Privacy-Friendly Alternative)
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_PLAUSIBLE_DOMAIN=saas-pdf.com
|
||||||
|
VITE_PLAUSIBLE_SRC=https://plausible.io/js/script.js # or self-hosted URL
|
||||||
|
```
|
||||||
|
|
||||||
|
- Lightweight (< 1 KB), no cookies, GDPR-compliant
|
||||||
|
- Runs alongside or instead of GA4 — both are opt-in
|
||||||
|
- Custom events forwarded to Plausible automatically
|
||||||
|
|
||||||
|
### Google Search Console
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_GOOGLE_SITE_VERIFICATION=your-verification-code
|
||||||
|
```
|
||||||
|
|
||||||
|
- Injected as `<meta name="google-site-verification">` at runtime
|
||||||
|
- Enables index coverage, search performance, and Core Web Vitals reporting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SEO Content Architecture
|
||||||
|
|
||||||
|
### 3.1 Tool Landing Pages (32 pages)
|
||||||
|
|
||||||
|
Each tool page (`/tools/{slug}`) renders via `ToolLandingPage` wrapper:
|
||||||
|
|
||||||
|
1. **Helmet** — title, meta description, keywords, canonical, OG, Twitter
|
||||||
|
2. **Tool UI** — upload zone, processing, download
|
||||||
|
3. **What it does** — descriptive paragraph
|
||||||
|
4. **How to use** — ordered steps (4 items)
|
||||||
|
5. **Benefits** — bullet list (5 items)
|
||||||
|
6. **Common use cases** — bullet list (5 items)
|
||||||
|
7. **FAQ** — accordion with 3–5 Q&A pairs (generates `FAQPage` schema)
|
||||||
|
8. **Related tools** — internal link grid (4 tools)
|
||||||
|
|
||||||
|
All text is i18n-driven from `seo.{toolKey}.*` keys in EN/AR/FR.
|
||||||
|
|
||||||
|
### 3.2 Static Pages (5 pages)
|
||||||
|
|
||||||
|
| Path | Schema | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `/` | `WebSite` + `Organization` | Homepage with hero + tool grid |
|
||||||
|
| `/about` | `WebPage` | Mission, technology, security |
|
||||||
|
| `/contact` | `WebPage` | Contact form (mailto-based) |
|
||||||
|
| `/privacy` | `WebPage` | Privacy policy |
|
||||||
|
| `/terms` | `WebPage` | Terms of service |
|
||||||
|
|
||||||
|
### 3.3 SEO Support Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `sitemap.xml` | All 37 indexable URLs with priority and changefreq |
|
||||||
|
| `robots.txt` | Crawler directives + sitemap pointer |
|
||||||
|
| `llms.txt` | AI/LLM discoverability file |
|
||||||
|
| `humans.txt` | Team credits |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Growth Playbook — Path to 500K Visits/Month
|
||||||
|
|
||||||
|
### Phase A: Foundation (Month 1–2) — Target: 5K visits/month
|
||||||
|
|
||||||
|
**Technical:**
|
||||||
|
- [ ] Deploy to production with real domain
|
||||||
|
- [ ] Submit sitemap to Google Search Console and Bing Webmaster Tools
|
||||||
|
- [ ] Run Lighthouse audits → fix any issues below 90 score
|
||||||
|
- [ ] Set up GA4 + Plausible dashboards
|
||||||
|
- [ ] Verify Core Web Vitals pass (LCP < 2.5s, FID < 100ms, CLS < 0.1)
|
||||||
|
|
||||||
|
**Content:**
|
||||||
|
- [ ] Publish all 32 tool pages with full SEO content
|
||||||
|
- [ ] Ensure hreflang tags work across EN/AR/FR (add `hreflang` links if using subdomains or subdirectories)
|
||||||
|
- [ ] Add FAQ schema to all tool pages (already done)
|
||||||
|
|
||||||
|
**Indexing:**
|
||||||
|
- [ ] Request indexing for top 10 highest-priority tool pages via Search Console
|
||||||
|
- [ ] Monitor index coverage weekly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase B: Content Marketing (Month 3–6) — Target: 30K visits/month
|
||||||
|
|
||||||
|
**Blog / Resource Pages:**
|
||||||
|
- [ ] Create `/blog` section with 2–4 articles per week
|
||||||
|
- [ ] Target long-tail keywords per tool:
|
||||||
|
- "how to compress PDF without losing quality"
|
||||||
|
- "convert PDF to Word free online"
|
||||||
|
- "merge multiple PDFs into one"
|
||||||
|
- "كيفية دمج ملفات PDF" (Arabic equivalent)
|
||||||
|
- "comment fusionner des fichiers PDF" (French equivalent)
|
||||||
|
- [ ] Each blog post links to the relevant tool page (internal linking)
|
||||||
|
- [ ] Create comparison pages: "SaaS-PDF vs iLovePDF vs SmallPDF"
|
||||||
|
|
||||||
|
**Keyword Research Strategy:**
|
||||||
|
- Target 200–500 keywords across three tiers:
|
||||||
|
- **Head terms** (high volume, high competition): "PDF converter", "merge PDF" — target via homepage + tool pages
|
||||||
|
- **Mid-tail** (medium volume): "compress PDF to 1MB", "PDF to Word with formatting" — target via tool pages + blog
|
||||||
|
- **Long-tail** (low volume, low competition): "how to remove watermark from PDF free", "convert scanned PDF to text" — target via blog articles
|
||||||
|
|
||||||
|
**Multilingual Scale:**
|
||||||
|
- Every blog post published in EN, AR, and FR simultaneously
|
||||||
|
- Arabic content is underserved in the PDF tools niche — a major competitive advantage
|
||||||
|
- Target 50+ Arabic long-tail keywords with almost zero competition
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase C: Authority Building (Month 6–12) — Target: 100K visits/month
|
||||||
|
|
||||||
|
**Link Building:**
|
||||||
|
- [ ] Submit to web tool directories (Product Hunt, AlternativeTo, ToolFinder)
|
||||||
|
- [ ] Create free embeddable widgets (PDF page counter, file size estimator)
|
||||||
|
- [ ] Write guest posts on productivity and SaaS blogs
|
||||||
|
- [ ] Build a "Free PDF Tools" resource page that other sites want to link to
|
||||||
|
- [ ] Reach out to educational institutions (free tools for students = .edu backlinks)
|
||||||
|
|
||||||
|
**Technical Improvements:**
|
||||||
|
- [ ] Implement `hreflang` for multilingual SEO (subdirectories: `/en/`, `/ar/`, `/fr/`)
|
||||||
|
- [ ] Add breadcrumb navigation to tool pages
|
||||||
|
- [ ] Create topic clusters: PDF Tools Hub → individual tool pages
|
||||||
|
- [ ] Implement internal search with search analytics
|
||||||
|
|
||||||
|
**Social Proof:**
|
||||||
|
- [ ] Add user count ("X files processed this month") to homepage
|
||||||
|
- [ ] Collect and display testimonials
|
||||||
|
- [ ] Create YouTube tutorials for each tool (video SEO)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase D: Scale & Monetize (Month 12–18) — Target: 500K visits/month
|
||||||
|
|
||||||
|
**Content Flywheel:**
|
||||||
|
- [ ] 100+ blog posts across 3 languages (300+ total pages)
|
||||||
|
- [ ] Programmatic SEO: auto-generate pages for format combinations
|
||||||
|
- "/convert/pdf-to-jpg", "/convert/docx-to-pdf", "/convert/png-to-webp"
|
||||||
|
- Each page targets a specific keyword with unique content
|
||||||
|
- [ ] Create glossary pages: "What is OCR?", "What is PDF/A?"
|
||||||
|
- [ ] Build an API documentation page (drives developer traffic)
|
||||||
|
|
||||||
|
**Distribution Channels:**
|
||||||
|
- [ ] Email newsletter with PDF tips (capture leads via tool pages)
|
||||||
|
- [ ] Social media presence: Twitter/X, LinkedIn, Reddit (r/pdf, r/productivity)
|
||||||
|
- [ ] Quora/Stack Overflow answers linking back to tools
|
||||||
|
- [ ] YouTube shorts demonstrating each tool (< 60s)
|
||||||
|
|
||||||
|
**Conversion Optimization:**
|
||||||
|
- [ ] A/B test hero copy, CTA buttons, tool card layouts
|
||||||
|
- [ ] Add "suggested next tool" after file processing
|
||||||
|
- [ ] Implement PWA for repeat visits (offline capability)
|
||||||
|
|
||||||
|
**Performance Monitoring:**
|
||||||
|
- Key metrics to track weekly:
|
||||||
|
- Organic sessions (GA4/Plausible)
|
||||||
|
- Indexed pages (Search Console)
|
||||||
|
- Average position for target keywords
|
||||||
|
- Click-through rate (CTR) from SERPs
|
||||||
|
- Pages per session / bounce rate
|
||||||
|
- Core Web Vitals scores
|
||||||
|
- Backlink count (Ahrefs/Moz)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Competitive Analysis
|
||||||
|
|
||||||
|
| Competitor | Monthly Traffic | Strengths | Our Advantage |
|
||||||
|
|-----------|----------------|-----------|---------------|
|
||||||
|
| iLovePDF | ~150M | Brand recognition, wide tool set | Arabic/French i18n, free with no limits |
|
||||||
|
| SmallPDF | ~60M | UX polish, enterprise features | No signup required, fully free |
|
||||||
|
| PDF24 | ~40M | Desktop app + web tools | Lightweight, faster load, mobile-first |
|
||||||
|
| Sejda | ~10M | Advanced editing features | More tools, trilingual content |
|
||||||
|
|
||||||
|
**Key differentiators for SaaS-PDF:**
|
||||||
|
1. **Trilingual** — EN/AR/FR from day one (Arabic market is largely unserved)
|
||||||
|
2. **No signup** — zero friction, instant file processing
|
||||||
|
3. **32 tools** — broader coverage than most competitors
|
||||||
|
4. **AI-powered tools** — OCR, Chat PDF, Summarize, Translate (unique value)
|
||||||
|
5. **Privacy-first** — files auto-deleted within 30 minutes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Monthly SEO Checklist
|
||||||
|
|
||||||
|
```
|
||||||
|
□ Review Search Console for crawl errors and fix immediately
|
||||||
|
□ Check index coverage — ensure all 37+ pages are indexed
|
||||||
|
□ Review top queries — identify rising keywords to create content for
|
||||||
|
□ Publish 8–16 blog posts (2–4/week × 3 languages)
|
||||||
|
□ Build 5–10 backlinks through outreach
|
||||||
|
□ Run Lighthouse audit — maintain 90+ scores
|
||||||
|
□ Update sitemap if new pages were added
|
||||||
|
□ Monitor Core Web Vitals — fix any regressions
|
||||||
|
□ Review analytics dashboards — identify underperforming pages
|
||||||
|
□ Competitor check — new features or content gaps to exploit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Environment Variables Reference
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Google Analytics 4 (optional)
|
||||||
|
VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX
|
||||||
|
|
||||||
|
# Plausible Analytics (optional, privacy-friendly)
|
||||||
|
VITE_PLAUSIBLE_DOMAIN=saas-pdf.com
|
||||||
|
VITE_PLAUSIBLE_SRC=https://plausible.io/js/script.js
|
||||||
|
|
||||||
|
# Google Search Console verification (optional)
|
||||||
|
VITE_GOOGLE_SITE_VERIFICATION=your-verification-code
|
||||||
|
|
||||||
|
# Google AdSense (optional)
|
||||||
|
VITE_ADSENSE_CLIENT_ID=ca-pub-XXXXXXXXXXXXXXXX
|
||||||
|
```
|
||||||
|
|
||||||
|
All integrations are **opt-in** — if the env var is empty or unset, the corresponding script is not loaded, keeping the bundle clean for development.
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX
|
VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX
|
||||||
|
VITE_PLAUSIBLE_DOMAIN=
|
||||||
|
VITE_PLAUSIBLE_SRC=https://plausible.io/js/script.js
|
||||||
|
VITE_GOOGLE_SITE_VERIFICATION=
|
||||||
VITE_ADSENSE_CLIENT_ID=ca-pub-XXXXXXXXXXXXXXXX
|
VITE_ADSENSE_CLIENT_ID=ca-pub-XXXXXXXXXXXXXXXX
|
||||||
VITE_ADSENSE_SLOT_HOME_TOP=1234567890
|
VITE_ADSENSE_SLOT_HOME_TOP=1234567890
|
||||||
VITE_ADSENSE_SLOT_HOME_BOTTOM=1234567891
|
VITE_ADSENSE_SLOT_HOME_BOTTOM=1234567891
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ declare global {
|
|||||||
interface Window {
|
interface Window {
|
||||||
dataLayer: unknown[];
|
dataLayer: unknown[];
|
||||||
gtag?: (...args: unknown[]) => void;
|
gtag?: (...args: unknown[]) => void;
|
||||||
|
plausible?: (event: string, opts?: { props?: Record<string, string> }) => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const GA_MEASUREMENT_ID = (import.meta.env.VITE_GA_MEASUREMENT_ID || '').trim();
|
const GA_MEASUREMENT_ID = (import.meta.env.VITE_GA_MEASUREMENT_ID || '').trim();
|
||||||
|
const PLAUSIBLE_DOMAIN = (import.meta.env.VITE_PLAUSIBLE_DOMAIN || '').trim();
|
||||||
|
const PLAUSIBLE_SRC = (import.meta.env.VITE_PLAUSIBLE_SRC || 'https://plausible.io/js/script.js').trim();
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
|
// ─── Google Analytics ────────────────────────────────────────────
|
||||||
|
|
||||||
function ensureGtagShim() {
|
function ensureGtagShim() {
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
window.gtag =
|
window.gtag =
|
||||||
@@ -34,34 +39,89 @@ function loadGaScript() {
|
|||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initAnalytics() {
|
// ─── Plausible Analytics ─────────────────────────────────────────
|
||||||
if (initialized || !GA_MEASUREMENT_ID || typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
|
function loadPlausibleScript() {
|
||||||
|
if (!PLAUSIBLE_DOMAIN) return;
|
||||||
|
|
||||||
|
const existing = document.querySelector<HTMLScriptElement>('script[data-plausible]');
|
||||||
|
if (existing) return;
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.defer = true;
|
||||||
|
script.setAttribute('data-domain', PLAUSIBLE_DOMAIN);
|
||||||
|
script.setAttribute('data-plausible', '');
|
||||||
|
script.src = PLAUSIBLE_SRC;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Search Console verification ─────────────────────────────────
|
||||||
|
|
||||||
|
function injectSearchConsoleVerification() {
|
||||||
|
const code = (import.meta.env.VITE_GOOGLE_SITE_VERIFICATION || '').trim();
|
||||||
|
if (!code) return;
|
||||||
|
|
||||||
|
const existing = document.querySelector('meta[name="google-site-verification"]');
|
||||||
|
if (existing) return;
|
||||||
|
|
||||||
|
const meta = document.createElement('meta');
|
||||||
|
meta.name = 'google-site-verification';
|
||||||
|
meta.content = code;
|
||||||
|
document.head.appendChild(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public API ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function initAnalytics() {
|
||||||
|
if (initialized || typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
// Google Analytics
|
||||||
|
if (GA_MEASUREMENT_ID) {
|
||||||
ensureGtagShim();
|
ensureGtagShim();
|
||||||
loadGaScript();
|
loadGaScript();
|
||||||
window.gtag?.('js', new Date());
|
window.gtag?.('js', new Date());
|
||||||
window.gtag?.('config', GA_MEASUREMENT_ID, { send_page_view: false });
|
window.gtag?.('config', GA_MEASUREMENT_ID, { send_page_view: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plausible
|
||||||
|
loadPlausibleScript();
|
||||||
|
|
||||||
|
// Search Console
|
||||||
|
injectSearchConsoleVerification();
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function trackPageView(path: string) {
|
export function trackPageView(path: string) {
|
||||||
if (!initialized || !window.gtag) return;
|
// GA4
|
||||||
|
if (window.gtag) {
|
||||||
window.gtag('event', 'page_view', {
|
window.gtag('event', 'page_view', {
|
||||||
page_path: path,
|
page_path: path,
|
||||||
page_location: `${window.location.origin}${path}`,
|
page_location: `${window.location.origin}${path}`,
|
||||||
page_title: document.title,
|
page_title: document.title,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
// Plausible tracks page views automatically via its script
|
||||||
}
|
}
|
||||||
|
|
||||||
export function trackEvent(
|
export function trackEvent(
|
||||||
eventName: string,
|
eventName: string,
|
||||||
params: Record<string, AnalyticsValue> = {}
|
params: Record<string, AnalyticsValue> = {}
|
||||||
) {
|
) {
|
||||||
if (!initialized || !window.gtag) return;
|
// GA4
|
||||||
|
if (window.gtag) {
|
||||||
window.gtag('event', eventName, params);
|
window.gtag('event', eventName, params);
|
||||||
|
}
|
||||||
|
// Plausible custom event
|
||||||
|
if (window.plausible) {
|
||||||
|
const props: Record<string, string> = {};
|
||||||
|
for (const [k, v] of Object.entries(params)) {
|
||||||
|
if (v !== undefined) props[k] = String(v);
|
||||||
|
}
|
||||||
|
window.plausible(eventName, { props });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function analyticsEnabled() {
|
export function analyticsEnabled() {
|
||||||
return Boolean(GA_MEASUREMENT_ID);
|
return Boolean(GA_MEASUREMENT_ID) || Boolean(PLAUSIBLE_DOMAIN);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user