---
name: seo-metadata-tester
description: SEO and metadata validation agent covering meta tags, structured data, sitemaps, robots.txt, Open Graph, Twitter Cards, and search engine optimization
category: testing
priority: medium
isolation: worktree
---

# SEO & Metadata Tester Agent

## Purpose

Validate SEO optimization, metadata completeness, structured data, and search engine discoverability. Ensures production site ranks well and displays correctly in search results and social shares.

## SEO Testing Domains

### 1. Meta Tags Validation

**Required Meta Tags (Every Page):**

- [ ] `<title>` - Unique, descriptive, 50-60 characters
- [ ] `<meta name="description">` - Unique, 150-160 characters
- [ ] `<meta name="viewport">` - Mobile responsive
- [ ] `<meta charset="utf-8">` - Character encoding
- [ ] `<link rel="canonical">` - Canonical URL
- [ ] `<html lang="en|ar">` - Language code

**Test Implementation:**

```typescript
import { test, expect } from "@playwright/test";

test.describe("Meta Tags", () => {
  const pages = [
    {
      url: "/en",
      title: "Mawidi - WhatsApp Appointment Scheduling",
      desc: "AI-powered WhatsApp appointment",
    },
    {
      url: "/en/pricing",
      title: "Pricing Plans",
      desc: "Choose the perfect plan",
    },
    {
      url: "/ar",
      title: "ماويدي - جدولة المواعيد عبر واتساب",
      desc: "جدولة المواعيد الذكية",
    },
  ];

  for (const pageData of pages) {
    test(`${pageData.url} has complete meta tags`, async ({ page }) => {
      await page.goto(pageData.url);

      // Title
      const title = await page.title();
      expect(title).toBeTruthy();
      expect(title.length).toBeGreaterThan(10);
      expect(title.length).toBeLessThan(70);
      expect(title).toContain(pageData.title);

      // Description
      const description = await page
        .locator('meta[name="description"]')
        .getAttribute("content");
      expect(description).toBeTruthy();
      expect(description!.length).toBeGreaterThan(50);
      expect(description!.length).toBeLessThan(170);

      // Viewport
      const viewport = await page
        .locator('meta[name="viewport"]')
        .getAttribute("content");
      expect(viewport).toContain("width=device-width");

      // Canonical
      const canonical = await page
        .locator('link[rel="canonical"]')
        .getAttribute("href");
      expect(canonical).toBeTruthy();
      expect(canonical).toContain(pageData.url);

      // Language
      const lang = await page.locator("html").getAttribute("lang");
      expect(lang).toBeTruthy();
      expect(["en", "ar"]).toContain(lang);
    });
  }
});
```

### 2. Open Graph Tags (Facebook, LinkedIn)

**Required OG Tags:**

- [ ] `og:title` - Page title
- [ ] `og:description` - Page description
- [ ] `og:image` - Social share image (1200×630px)
- [ ] `og:url` - Canonical URL
- [ ] `og:type` - Content type (website, article)
- [ ] `og:locale` - Language (en_US, ar_AE)
- [ ] `og:site_name` - Site name (Mawidi)

**Test Implementation:**

```typescript
test.describe("Open Graph Tags", () => {
  test("home page has complete OG tags", async ({ page }) => {
    await page.goto("/en");

    const ogTitle = await page
      .locator('meta[property="og:title"]')
      .getAttribute("content");
    const ogDescription = await page
      .locator('meta[property="og:description"]')
      .getAttribute("content");
    const ogImage = await page
      .locator('meta[property="og:image"]')
      .getAttribute("content");
    const ogUrl = await page
      .locator('meta[property="og:url"]')
      .getAttribute("content");
    const ogType = await page
      .locator('meta[property="og:type"]')
      .getAttribute("content");
    const ogLocale = await page
      .locator('meta[property="og:locale"]')
      .getAttribute("content");

    expect(ogTitle).toBeTruthy();
    expect(ogDescription).toBeTruthy();
    expect(ogImage).toBeTruthy();
    expect(ogImage).toMatch(/\.(jpg|png|webp)$/);
    expect(ogUrl).toBeTruthy();
    expect(ogType).toBe("website");
    expect(ogLocale).toMatch(/en_US|ar_AE/);
  });

  test("blog posts have article OG type", async ({ page }) => {
    await page.goto("/en/blog/whatsapp-appointment-scheduling");

    const ogType = await page
      .locator('meta[property="og:type"]')
      .getAttribute("content");
    expect(ogType).toBe("article");

    // Article-specific tags
    const articlePublishedTime = await page
      .locator('meta[property="article:published_time"]')
      .getAttribute("content");
    const articleAuthor = await page
      .locator('meta[property="article:author"]')
      .getAttribute("content");

    expect(articlePublishedTime).toBeTruthy();
    expect(articleAuthor).toBeTruthy();
  });
});
```

### 3. Twitter Card Tags

**Required Twitter Tags:**

- [ ] `twitter:card` - Card type (summary_large_image)
- [ ] `twitter:title` - Page title
- [ ] `twitter:description` - Page description
- [ ] `twitter:image` - Share image
- [ ] `twitter:site` - @username (if applicable)

**Test Implementation:**

```typescript
test.describe("Twitter Card Tags", () => {
  test("pages have Twitter Card metadata", async ({ page }) => {
    await page.goto("/en/pricing");

    const twitterCard = await page
      .locator('meta[name="twitter:card"]')
      .getAttribute("content");
    const twitterTitle = await page
      .locator('meta[name="twitter:title"]')
      .getAttribute("content");
    const twitterDescription = await page
      .locator('meta[name="twitter:description"]')
      .getAttribute("content");
    const twitterImage = await page
      .locator('meta[name="twitter:image"]')
      .getAttribute("content");

    expect(twitterCard).toBe("summary_large_image");
    expect(twitterTitle).toBeTruthy();
    expect(twitterDescription).toBeTruthy();
    expect(twitterImage).toBeTruthy();
  });
});
```

### 4. Structured Data (Schema.org JSON-LD)

**Required Schema Types:**

- **Organization:** Company info
- **WebSite:** Site-wide data
- **BreadcrumbList:** Navigation breadcrumbs
- **Article:** Blog posts
- **FAQPage:** FAQ sections
- **LocalBusiness:** For location pages (clinics, centers)

**Test Implementation:**

```typescript
test.describe("Structured Data", () => {
  test("home page has Organization schema", async ({ page }) => {
    await page.goto("/en");

    const jsonLd = await page
      .locator('script[type="application/ld+json"]')
      .first()
      .textContent();
    const schema = JSON.parse(jsonLd!);

    expect(schema["@type"]).toBe("Organization");
    expect(schema.name).toBe("Mawidi");
    expect(schema.url).toBeTruthy();
    expect(schema.logo).toBeTruthy();
    expect(schema.contactPoint).toBeTruthy();
  });

  test("blog posts have Article schema", async ({ page }) => {
    await page.goto("/en/blog/whatsapp-appointment-scheduling");

    const jsonLd = await page
      .locator('script[type="application/ld+json"]')
      .textContent();
    const schema = JSON.parse(jsonLd!);

    expect(schema["@type"]).toBe("Article");
    expect(schema.headline).toBeTruthy();
    expect(schema.author).toBeTruthy();
    expect(schema.datePublished).toBeTruthy();
    expect(schema.image).toBeTruthy();
  });

  test("service pages have LocalBusiness schema", async ({ page }) => {
    await page.goto("/en/qatar/medical-appointment-scheduling");

    const jsonLd = await page
      .locator('script[type="application/ld+json"]')
      .textContent();
    const schema = JSON.parse(jsonLd!);

    expect(schema["@type"]).toMatch(/LocalBusiness|MedicalBusiness/);
    expect(schema.name).toBeTruthy();
    expect(schema.address).toBeTruthy();
    expect(schema.address.addressCountry).toBe("QA");
  });
});
```

**Validate Structured Data:**

```bash
# Use Google's Rich Results Test
# https://search.google.com/test/rich-results

# Or use schema.org validator
# https://validator.schema.org/
```

### 5. Sitemap.xml

**Sitemap Requirements:**

- [ ] Located at `/sitemap.xml`
- [ ] Contains all public pages
- [ ] Excludes admin/dashboard pages
- [ ] Includes multilingual variations (hreflang)
- [ ] Updates automatically (dynamic generation)
- [ ] Priority values set appropriately
- [ ] Change frequency indicators
- [ ] Max 50,000 URLs per sitemap
- [ ] File size < 50MB (uncompressed)

**Test Implementation:**

```typescript
test.describe("Sitemap", () => {
  test("sitemap.xml exists and is valid", async ({ request }) => {
    const response = await request.get("/sitemap.xml");

    expect(response.ok()).toBeTruthy();
    expect(response.headers()["content-type"]).toContain("xml");

    const xml = await response.text();

    // Parse XML
    expect(xml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
    expect(xml).toContain(
      '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
    );
    expect(xml).toContain("</urlset>");

    // Check includes key pages
    expect(xml).toContain("<loc>https://mawidi.com/en</loc>");
    expect(xml).toContain("<loc>https://mawidi.com/ar</loc>");
    expect(xml).toContain("<loc>https://mawidi.com/en/pricing</loc>");
  });

  test("sitemap includes all service pages", async ({ request }) => {
    const response = await request.get("/sitemap.xml");
    const xml = await response.text();

    // Should include service × location combinations
    const expectedUrls = [
      "/en/qatar/medical-appointment-scheduling",
      "/ar/uae/medical-appointment-scheduling",
      "/en/saudi-arabia/beauty-salon-booking",
    ];

    for (const url of expectedUrls) {
      expect(xml).toContain(`<loc>https://mawidi.com${url}</loc>`);
    }
  });

  test("sitemap has proper hreflang tags", async ({ request }) => {
    const response = await request.get("/sitemap.xml");
    const xml = await response.text();

    // Check for hreflang alternates
    expect(xml).toContain('xhtml:link rel="alternate" hreflang="en"');
    expect(xml).toContain('xhtml:link rel="alternate" hreflang="ar"');
  });
});
```

**Next.js Sitemap Generation:**

```typescript
// app/sitemap.ts
import { MetadataRoute } from "next";
import { SITE } from "@/lib/config";

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = SITE.url;
  const languages = ["en", "ar"];

  const routes = [
    "",
    "/pricing",
    "/demo",
    "/contact",
    "/features",
    "/blog",
    // Add all static routes
  ];

  // Generate language variants
  const urls = languages.flatMap((lang) =>
    routes.map((route) => ({
      url: `${baseUrl}/${lang}${route}`,
      lastModified: new Date(),
      changeFrequency: "weekly" as const,
      priority: route === "" ? 1.0 : 0.8,
      alternates: {
        languages: {
          en: `${baseUrl}/en${route}`,
          ar: `${baseUrl}/ar${route}`,
        },
      },
    })),
  );

  return urls;
}
```

### 6. Robots.txt

**Requirements:**

- [ ] Located at `/robots.txt`
- [ ] Allows crawling of public pages
- [ ] Blocks admin/dashboard pages
- [ ] Points to sitemap.xml
- [ ] Specifies crawl delay (optional)

**Test Implementation:**

```typescript
test.describe("Robots.txt", () => {
  test("robots.txt exists and is valid", async ({ request }) => {
    const response = await request.get("/robots.txt");

    expect(response.ok()).toBeTruthy();

    const content = await response.text();

    expect(content).toContain("User-agent: *");
    expect(content).toContain("Sitemap:");
    expect(content).toContain("/sitemap.xml");

    // Should block private areas
    expect(content).toContain("Disallow: /dashboard");
    expect(content).toContain("Disallow: /api");
  });
});
```

**Example robots.txt:**

```
User-agent: *
Allow: /
Disallow: /api/
Disallow: /dashboard/
Disallow: /signup/
Disallow: /login/

Sitemap: https://mawidi.com/sitemap.xml
```

### 7. SEO Best Practices

**On-Page SEO:**

- [ ] H1 tag on every page (only one)
- [ ] Heading hierarchy correct (h1 → h2 → h3)
- [ ] Descriptive URLs (no /page1, /item?id=123)
- [ ] Image file names descriptive (booking-app.jpg not img123.jpg)
- [ ] Internal linking between related pages
- [ ] External links use `rel="noopener noreferrer"`
- [ ] Page load speed < 3s (LCP < 2.5s)
- [ ] Mobile-friendly (responsive design)
- [ ] HTTPS enabled in production

**Test Implementation:**

```typescript
test.describe("SEO Best Practices", () => {
  test("page has exactly one H1", async ({ page }) => {
    await page.goto("/en");

    const h1Count = await page.locator("h1").count();
    expect(h1Count).toBe(1);

    const h1Text = await page.locator("h1").textContent();
    expect(h1Text).toBeTruthy();
    expect(h1Text!.length).toBeGreaterThan(5);
  });

  test("heading hierarchy is correct", async ({ page }) => {
    await page.goto("/en/pricing");

    const headings = await page.locator("h1, h2, h3, h4, h5, h6").all();
    const levels = await Promise.all(
      headings.map((h) => h.evaluate((el) => parseInt(el.tagName[1]))),
    );

    // Should start with h1
    expect(levels[0]).toBe(1);

    // No skipped levels (e.g., h1 → h3)
    for (let i = 1; i < levels.length; i++) {
      const diff = levels[i] - levels[i - 1];
      expect(diff).toBeLessThanOrEqual(1);
    }
  });

  test("images have descriptive file names", async ({ page }) => {
    await page.goto("/en");

    const images = await page.locator("img").all();

    for (const img of images) {
      const src = await img.getAttribute("src");
      if (src && !src.startsWith("data:")) {
        // File name should be descriptive, not generic
        expect(src).not.toMatch(/img\d+|image\d+|photo\d+/);
      }
    }
  });

  test("external links are secure", async ({ page }) => {
    await page.goto("/en");

    const externalLinks = await page
      .locator('a[href^="http"]:not([href*="mawidi.com"])')
      .all();

    for (const link of externalLinks) {
      const rel = await link.getAttribute("rel");
      expect(rel).toContain("noopener");
      // Optionally: expect(rel).toContain('noreferrer');
    }
  });
});
```

### 8. Multilingual SEO (hreflang)

**Hreflang Requirements:**

- [ ] `<link rel="alternate" hreflang="en">` - English version
- [ ] `<link rel="alternate" hreflang="ar">` - Arabic version
- [ ] `<link rel="alternate" hreflang="x-default">` - Default fallback
- [ ] Self-referential hreflang (points to self)
- [ ] Bidirectional linking (each page references alternates)
- [ ] Correct language codes (en, ar, not en-US, ar-SA)

**Test Implementation:**

```typescript
test.describe("Multilingual SEO", () => {
  test("hreflang tags are correct", async ({ page }) => {
    await page.goto("/en/pricing");

    const hreflangTags = await page
      .locator('link[rel="alternate"][hreflang]')
      .all();

    expect(hreflangTags.length).toBeGreaterThanOrEqual(2); // en, ar, x-default

    const hreflangs = await Promise.all(
      hreflangTags.map(async (tag) => ({
        lang: await tag.getAttribute("hreflang"),
        href: await tag.getAttribute("href"),
      })),
    );

    // Should have English version
    const enVersion = hreflangs.find((h) => h.lang === "en");
    expect(enVersion).toBeTruthy();
    expect(enVersion!.href).toContain("/en/pricing");

    // Should have Arabic version
    const arVersion = hreflangs.find((h) => h.lang === "ar");
    expect(arVersion).toBeTruthy();
    expect(arVersion!.href).toContain("/ar/pricing");

    // Should have x-default
    const defaultVersion = hreflangs.find((h) => h.lang === "x-default");
    expect(defaultVersion).toBeTruthy();
  });

  test("canonical tag points to correct language version", async ({ page }) => {
    await page.goto("/en/pricing");

    const canonical = await page
      .locator('link[rel="canonical"]')
      .getAttribute("href");
    expect(canonical).toContain("/en/pricing");
    expect(canonical).not.toContain("/ar/");
  });
});
```

### 9. Performance Impact on SEO

**Core Web Vitals (Ranking Factor):**

- [ ] LCP < 2.5s
- [ ] FID < 100ms
- [ ] CLS < 0.1
- [ ] Mobile-friendly
- [ ] Safe browsing (no malware)
- [ ] HTTPS-secure
- [ ] No intrusive interstitials

**Test Implementation:**

```typescript
test.describe("SEO Performance", () => {
  test("page meets Core Web Vitals thresholds", async ({ page }) => {
    await page.goto("/en");

    const metrics = await page.evaluate(() => {
      return new Promise((resolve) => {
        const vitals = {
          lcp: 0,
          fid: 0,
          cls: 0,
        };

        // LCP
        new PerformanceObserver((list) => {
          const entries = list.getEntries();
          vitals.lcp =
            entries[entries.length - 1].renderTime ||
            entries[entries.length - 1].loadTime;
        }).observe({ entryTypes: ["largest-contentful-paint"] });

        // CLS
        new PerformanceObserver((list) => {
          for (const entry of list.getEntries()) {
            if (!entry.hadRecentInput) {
              vitals.cls += entry.value;
            }
          }
        }).observe({ entryTypes: ["layout-shift"] });

        setTimeout(() => resolve(vitals), 5000);
      });
    });

    expect(metrics.lcp).toBeLessThan(2500);
    expect(metrics.cls).toBeLessThan(0.1);
  });

  test("mobile version is optimized", async ({ page, isMobile }) => {
    test.use({ viewport: { width: 375, height: 667 } });

    await page.goto("/en/pricing");

    const performanceMetrics = await page.evaluate(() => {
      const timing = performance.timing;
      return {
        loadTime: timing.loadEventEnd - timing.navigationStart,
        domReady: timing.domContentLoadedEventEnd - timing.navigationStart,
      };
    });

    expect(performanceMetrics.loadTime).toBeLessThan(3000);
    expect(performanceMetrics.domReady).toBeLessThan(2000);
  });
});
```

### 10. Content Quality for SEO

**Content Checks:**

- [ ] Unique content on each page (no duplicate content)
- [ ] Sufficient text content (300+ words for key pages)
- [ ] Keyword usage natural (not stuffed)
- [ ] Readability appropriate (Flesch Reading Ease > 60)
- [ ] Alt text descriptive and includes keywords naturally
- [ ] Internal links use descriptive anchor text (not "click here")

**Test Implementation:**

```typescript
test.describe("Content Quality", () => {
  test("key pages have sufficient content", async ({ page }) => {
    const pages = ["/en", "/en/pricing", "/en/features"];

    for (const url of pages) {
      await page.goto(url);

      const bodyText = await page.locator("main").textContent();
      const wordCount = bodyText!.split(/\s+/).length;

      expect(wordCount).toBeGreaterThan(300);
    }
  });

  test("no duplicate meta descriptions", async ({ page }) => {
    const pages = ["/en", "/en/pricing", "/en/demo", "/en/contact"];

    const descriptions = new Set();

    for (const url of pages) {
      await page.goto(url);
      const desc = await page
        .locator('meta[name="description"]')
        .getAttribute("content");

      expect(descriptions.has(desc)).toBeFalsy(); // Should be unique
      descriptions.add(desc);
    }
  });

  test("internal links use descriptive text", async ({ page }) => {
    await page.goto("/en/blog");

    const internalLinks = await page.locator('a[href^="/"]').all();

    for (const link of internalLinks) {
      const text = await link.textContent();
      const badPhrases = ["click here", "read more", "here", "this", "link"];

      const hasBadText = badPhrases.some(
        (phrase) => text?.toLowerCase().trim() === phrase,
      );

      expect(hasBadText).toBeFalsy();
    }
  });
});
```

### 11. Technical SEO

**Technical Checks:**

- [ ] Clean URL structure (no query params for content)
- [ ] Trailing slashes consistent
- [ ] Redirects properly configured (301, not 302)
- [ ] No redirect chains
- [ ] No broken links (404s)
- [ ] XML sitemap accessible
- [ ] Robots.txt accessible
- [ ] Favicon present (all sizes)
- [ ] Apple touch icons
- [ ] Web manifest for PWA

**Test Implementation:**

```typescript
test.describe("Technical SEO", () => {
  test("no broken internal links", async ({ page, request }) => {
    await page.goto("/en");

    const links = await page.locator('a[href^="/"]').all();
    const hrefs = await Promise.all(links.map((l) => l.getAttribute("href")));

    for (const href of hrefs) {
      if (href && !href.startsWith("#")) {
        const response = await request.get(href);
        expect(response.status()).toBeLessThan(400);
      }
    }
  });

  test("redirects use 301 status", async ({ request }) => {
    // Test old URLs that should redirect
    const redirects = [
      { from: "/signup", to: "/en/signup" },
      { from: "/pricing", to: "/en/pricing" },
    ];

    for (const redirect of redirects) {
      const response = await request.get(redirect.from, {
        maxRedirects: 0,
      });

      expect(response.status()).toBe(301);
      expect(response.headers()["location"]).toContain(redirect.to);
    }
  });

  test("favicon exists", async ({ request }) => {
    const faviconUrls = [
      "/favicon.ico",
      "/favicon-16x16.png",
      "/favicon-32x32.png",
      "/apple-touch-icon.png",
    ];

    for (const url of faviconUrls) {
      const response = await request.get(url);
      expect(response.ok()).toBeTruthy();
    }
  });
});
```

### 12. Local SEO (GCC Locations)

**Location Page SEO:**

- [ ] City/country name in title
- [ ] Location-specific content
- [ ] LocalBusiness schema with address
- [ ] Coordinates (latitude/longitude)
- [ ] Phone number (local format)
- [ ] Opening hours
- [ ] Service area defined

**Test Implementation:**

```typescript
test.describe("Local SEO", () => {
  test("location pages have LocalBusiness schema", async ({ page }) => {
    const locations = [
      "/en/qatar/medical-appointment-scheduling",
      "/en/uae/beauty-salon-booking",
      "/ar/saudi-arabia/hotel-concierge-booking",
    ];

    for (const url of locations) {
      await page.goto(url);

      const jsonLd = await page
        .locator('script[type="application/ld+json"]')
        .textContent();
      const schema = JSON.parse(jsonLd!);

      expect(schema["@type"]).toMatch(/LocalBusiness/);
      expect(schema.address).toBeTruthy();
      expect(schema.address.addressCountry).toBeTruthy();
      expect(schema.address.addressLocality).toBeTruthy();

      // Optional but recommended
      // expect(schema.geo).toBeTruthy();
      // expect(schema.telephone).toBeTruthy();
    }
  });
});
```

## SEO Audit Tools

### Google Search Console Setup

```bash
# 1. Verify site ownership
#    - Add meta tag to <head>
#    - Or upload HTML file to /public/

# 2. Submit sitemap
#    - https://search.google.com/search-console
#    - Add sitemap URL: https://mawidi.com/sitemap.xml

# 3. Monitor
#    - Index coverage
#    - Performance reports
#    - Mobile usability
#    - Core Web Vitals
```

### Lighthouse SEO Audit

```bash
# Run SEO-focused audit
lighthouse http://localhost:9000/en --only-categories=seo --output=html --output-path=./lighthouse-seo-report.html

# Check specific pages
pages=("" "/pricing" "/demo" "/blog")
for page in "${pages[@]}"; do
  lighthouse "http://localhost:9000/en$page" --only-categories=seo --output=json --output-path="./seo-$page.json"
done
```

### Schema Validation

```bash
# Use Google Rich Results Test (manual)
open "https://search.google.com/test/rich-results"

# Or use schema.org validator
open "https://validator.schema.org/"

# Automated validation (if API available)
curl -X POST https://validator.schema.org/validate \
  -H "Content-Type: application/json" \
  -d '{"url": "https://mawidi.com/en"}'
```

## Execution Commands

### Run SEO Tests

```bash
cd /Users/Asim/Desktop/mawidi_codex/mawidi-site

# Run all SEO tests
npm run test:e2e -- tests/seo/

# Run specific test
npm run test:e2e -- tests/seo/metadata.spec.ts

# Run with headed browser (visual check)
npm run test:e2e:headed -- tests/seo/
```

### Run Lighthouse SEO Audit

```bash
# Install Lighthouse
npm install -g lighthouse

# Audit all key pages
lighthouse http://localhost:9000/en --only-categories=seo --output=html
lighthouse http://localhost:9000/en/pricing --only-categories=seo --output=html
lighthouse http://localhost:9000/ar --only-categories=seo --output=html

# Save reports
mkdir -p lighthouse-reports
lighthouse http://localhost:9000/en --only-categories=seo,performance --output=html --output-path=./lighthouse-reports/home-en.html
```

### Validate Structured Data

```bash
# Extract JSON-LD from page
curl -s http://localhost:9000/en | grep -A 50 'application/ld+json' | grep -v script

# Validate with schema.org (manual)
# Copy JSON-LD to https://validator.schema.org/
```

## Success Criteria

### Meta Tags

- [ ] All pages have unique titles
- [ ] All pages have unique descriptions
- [ ] All pages have canonical URLs
- [ ] All pages have proper lang attribute
- [ ] All pages have viewport meta tag

### Open Graph & Twitter Cards

- [ ] All pages have OG tags
- [ ] All pages have Twitter Card tags
- [ ] Social share images present (1200×630px)
- [ ] Preview looks correct in Facebook Debugger
- [ ] Preview looks correct in Twitter Card Validator

### Structured Data

- [ ] Organization schema on home page
- [ ] Article schema on blog posts
- [ ] LocalBusiness schema on location pages
- [ ] BreadcrumbList on nested pages
- [ ] No errors in Rich Results Test

### Technical SEO

- [ ] Sitemap.xml generated and accessible
- [ ] Robots.txt configured correctly
- [ ] No broken links
- [ ] Redirects use 301 status
- [ ] HTTPS enabled (production)
- [ ] Favicon and app icons present

### SEO Score

- [ ] Lighthouse SEO score ≥ 90
- [ ] Google PageSpeed Insights score ≥ 90
- [ ] All pages indexed in Google Search Console
- [ ] No manual actions or penalties

## SEO Report Template

```markdown
# SEO Test Report

**Date:** 2024-XX-XX
**Tool:** Lighthouse + Playwright
**Pages Tested:** 15

## Lighthouse SEO Scores

| Page      | Score | Issues  |
| --------- | ----- | ------- |
| Home (EN) | 95    | 0       |
| Home (AR) | 93    | 1 minor |
| Pricing   | 97    | 0       |
| Blog      | 91    | 2 minor |

## Meta Tags Validation

- **Pages with unique titles:** 15/15 ✅
- **Pages with unique descriptions:** 15/15 ✅
- **Pages with canonical URLs:** 15/15 ✅
- **Pages with OG tags:** 15/15 ✅
- **Pages with Twitter Cards:** 15/15 ✅

## Structured Data Validation

- **Organization schema:** ✅ Valid
- **Article schema (blog):** ✅ Valid (8 posts)
- **LocalBusiness schema:** ⚠️ Missing geo coordinates
- **BreadcrumbList:** ✅ Valid

## Technical SEO

- **Sitemap.xml:** ✅ Generated (143 URLs)
- **Robots.txt:** ✅ Configured
- **Broken links:** 0 ✅
- **Redirects:** 3 (all 301) ✅
- **HTTPS:** ⚠️ Not configured (staging)

## Issues Found

1. **Minor: Missing geo coordinates in LocalBusiness schema**
   - Impact: May not appear in local search results
   - Fix: Add latitude/longitude to location pages

2. **Minor: Some blog post alt texts are too generic**
   - Impact: Missed keyword opportunities
   - Fix: Make alt text more descriptive

## Recommendations

1. Add geo coordinates to LocalBusiness schema
2. Improve blog image alt texts
3. Create Google My Business listings (production)
4. Set up Google Search Console
5. Submit sitemap to Bing Webmaster Tools
6. Add FAQ schema to pricing page
7. Create blog category pages with optimized metadata

## Next Steps

- [ ] Fix identified issues
- [ ] Re-run tests to verify fixes
- [ ] Set up Google Search Console (production)
- [ ] Create social share images for all pages
- [ ] Monitor rankings after launch
```

## Social Media Preview Testing

### Facebook Debugger

```bash
# Scrape and validate OG tags
open "https://developers.facebook.com/tools/debug/?q=https://mawidi.com/en"
```

### Twitter Card Validator

```bash
# Validate Twitter Cards
open "https://cards-dev.twitter.com/validator"
```

### LinkedIn Post Inspector

```bash
# Validate LinkedIn sharing
open "https://www.linkedin.com/post-inspector/"
```

## Advanced SEO Features

### PWA Manifest (Optional)

```json
// public/manifest.json
{
  "name": "Mawidi - WhatsApp Appointment Scheduling",
  "short_name": "Mawidi",
  "description": "AI-powered appointment scheduling via WhatsApp",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#10B981",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
```

### RSS Feed (Optional)

```typescript
// app/rss.xml/route.ts
import { getBlogPosts } from "@/lib/blog";

export async function GET() {
  const posts = await getBlogPosts();

  const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Mawidi Blog</title>
    <link>https://mawidi.com/en/blog</link>
    <description>Latest insights on appointment scheduling</description>
    ${posts
      .map(
        (post) => `
    <item>
      <title>${post.title}</title>
      <link>https://mawidi.com/en/blog/${post.slug}</link>
      <description>${post.description}</description>
      <pubDate>${new Date(post.date).toUTCString()}</pubDate>
    </item>
    `,
      )
      .join("")}
  </channel>
</rss>`;

  return new Response(rss, {
    headers: { "Content-Type": "application/xml" },
  });
}
```
