# SEO par strate — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Déployer une couverture SEO/GEO complète sur advmfr-next en trois strates de maturité (stable → moderne → expérimental), sans régression de build.

**Architecture:** On éclate `src/config/seo.ts` en un module `src/config/seo/` (meta, robots, ai, structured-data) ré-exporté par `index.ts` pour préserver les imports `@/config/seo`. On ajoute les file-conventions Next manquantes (`app/manifest.ts`, Route Handler `app/robots.txt/route.ts` pour pouvoir émettre `Content-Signal:`), des headers HTTP dans `next.config.ts`, et des builders JSON-LD par type de page. Les meta non typés par Next (`noai`, `ai-disclosure`, `format-detection`) passent par `metadata.other`.

**Tech Stack:** Next.js 16.2.6 (App Router, Metadata API), TypeScript strict, next-intl, schema-dts. Pas de test runner — vérification par `next build` (type-check inclus), `grep` sur le HTML buildé, et `curl` sur le serveur dev.

**Référence design:** `docs/superpowers/specs/2026-05-29-seo-strata-design.md`

**Conventions de branches (global-rules §3):** une strate = une branche éphémère depuis `dev`, mergée en `--ff-only`, puis supprimée.

---

## Notes transverses (lire avant de commencer)

- **Pas de framework de test.** Les "tests" de ce plan sont des commandes de vérification concrètes : `bun run build` (échoue si type-check casse), `grep` sur `.next/server/app/**/*.html`, et `curl` sur `next dev`. Toujours montrer la sortie attendue.
- **Source unique du meta `robots`.** Next type le champ `metadata.robots` mais ne connaît pas `noai`/`noimageai` (directives non standard). Pour éviter deux `<meta name="robots">`, on construit UNE string complète dans `seo/robots.ts` et on l'émet via `metadata.other.robots`. On n'utilise PAS le champ typé `metadata.robots`.
- **Pattern JSON-LD existant.** Injection via `<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(x) }} />`, voir `src/app/[locale]/page.tsx:41-52`. Réutiliser ce pattern partout.
- **`metadata.other`** accepte `{ name: content }` et émet `<meta name="name" content="content">` (doc Next `generate-metadata.md:1006`). Valeurs array supportées.
- **Thème light unique** (`brand.md`) : pas de `theme-color` en media query dark.
- **Convention namespace SEO (décision validée — option A).** Les clés SEO sont regroupées sous un sous-objet `seo` dans la namespace de chaque page : `AboutUsPage.seo.{title,metaDescription,metaKeywords}`. L'UI reste à plat (`AboutUsPage.heading`…). Les keywords globaux de fallback restent à plat dans `LocaleLayout.metaKeywords`. **Côté code, on ne touche PAS aux helpers** : on scope simplement le translator sur la sous-namespace, ex. `getTranslations({ locale, namespace: "AboutUsPage.seo" })`, puis `t("title")` / `readKeywords(t, "AboutUsPage.seo")` fonctionnent à l'identique. `getLocaleKeywords` continue de lire `LocaleLayout` (inchangé).

---

# STRATE 1 — Stable & prouvé

Branche : `feature/seo-strate-1-stable`

- [ ] **Step 0 : Créer la branche**

```bash
git checkout dev && git pull
git checkout -b feature/seo-strate-1-stable
```

## Task 0bis : Profil fabricant dans `site.ts` (fondation)

**Files:**
- Modify: `src/config/site.ts:16-41`

- [ ] **Step 1 : Ajouter `legalName`** dans l'objet `site`, après `description` :

```ts
  legalName: "ADV MFR",
```

> Consommé par `organizationJsonLd` (Task 2). Garder `as const`.

## Task 1 : Refactor `seo.ts` → module `seo/`

**Files:**
- Delete: `src/config/seo.ts`
- Create: `src/config/seo/index.ts`
- Create: `src/config/seo/meta.ts`
- Create: `src/config/seo/structured-data.ts`

Le but : aucun changement de comportement, juste la décomposition. Les imports `@/config/seo` doivent continuer de résoudre via `index.ts`.

- [ ] **Step 1 : Créer `src/config/seo/meta.ts`** (tout le code actuel SAUF les JSON-LD)

```ts
import type { Metadata } from "next";
import { type Locale } from "next-intl";
import { getTranslations } from "next-intl/server";
import { getPathname } from "@/i18n/navigation";
import { routing } from "@/i18n/routing";
import { site } from "../site";
import { robotsMetaContent } from "./robots";
import { aiMetaTags } from "./ai";

export type Keywords = readonly [string, ...string[]];

export const seo = {
  titleTemplate: `%s | ${site.name}`,
  defaultTitle: site.name,
  openGraph: {
    type: "website" as const,
    siteName: site.name,
  },
  twitter: {
    card: "summary_large_image" as const,
    site: site.social.twitter,
  },
  verification: {
    google: process.env.NEXT_PUBLIC_GOOGLE_VERIFICATION,
    bing: process.env.NEXT_PUBLIC_BING_VERIFICATION,
  },
  robots: {
    allow: ["/"],
    disallow: ["/api/"],
  },
  sitemap: {
    exclude: [] as string[],
    changeFreq: "weekly" as const,
    priority: 0.7,
  },
};

const OG_LOCALES: Record<Locale, string> = {
  en: "en_US",
  fr: "fr_FR",
};

export function ogLocaleFor(locale: Locale): string {
  return OG_LOCALES[locale];
}

export function ogAlternateLocalesFor(locale: Locale): string[] {
  return routing.locales.filter((l) => l !== locale).map((l) => OG_LOCALES[l]);
}

type Href = Parameters<typeof getPathname>[0]["href"];

export function buildPageAlternates({
  href,
  locale,
}: {
  href: Href;
  locale: Locale;
}) {
  const canonical = getPathname({ href, locale });
  const languages = Object.fromEntries(
    routing.locales.map((alt) => [alt, getPathname({ href, locale: alt })]),
  );
  return {
    canonical,
    languages: {
      ...languages,
      "x-default": getPathname({ href, locale: routing.defaultLocale }),
    },
  } satisfies NonNullable<Metadata["alternates"]>;
}

type RawReader = { raw: (key: string) => unknown };

function assertKeywords(value: unknown, where: string): Keywords {
  if (
    !Array.isArray(value) ||
    value.length === 0 ||
    !value.every((v): v is string => typeof v === "string")
  ) {
    throw new Error(
      `[seo] Missing or empty metaKeywords for "${where}". Each namespace must declare a non-empty string[] under "metaKeywords".`,
    );
  }
  return value as unknown as Keywords;
}

export function readKeywords(t: unknown, where: string): Keywords {
  return assertKeywords((t as RawReader).raw("metaKeywords"), where);
}

export async function getLocaleKeywords(locale: Locale): Promise<Keywords> {
  const t = await getTranslations({ locale, namespace: "LocaleLayout" });
  return readKeywords(t, "LocaleLayout");
}

export async function buildPageMetadata({
  href,
  locale,
  title,
  description,
  keywords,
}: {
  href: Href;
  locale: Locale;
  title: string;
  description: string;
  keywords: Keywords;
}): Promise<Metadata> {
  const globalKeywords = await getLocaleKeywords(locale);
  const merged = Array.from(new Set([...keywords, ...globalKeywords]));
  const alternates = buildPageAlternates({ href, locale });
  return {
    title,
    description,
    keywords: merged,
    alternates,
    authors: [{ name: site.name }],
    publisher: site.name,
    openGraph: {
      title,
      description,
      url: alternates.canonical,
      locale: ogLocaleFor(locale),
      alternateLocale: ogAlternateLocalesFor(locale),
    },
    twitter: {
      title,
      description,
    },
    other: {
      robots: robotsMetaContent,
      "format-detection": "telephone=no,date=no,address=no,email=no,url=no",
      ...aiMetaTags,
    },
  };
}
```

> Note : `authors`/`publisher`/`other` sont ajoutés ici (étaient absents) — c'est l'incrément Strate 1 (author/publisher/format-detection) + le wiring des modules robots/ai créés aux Tasks suivantes. `aiMetaTags` sera un objet vide en Strate 1 (rempli en Strate 3).

- [ ] **Step 2 : Créer `src/config/seo/structured-data.ts`** (les JSON-LD, enrichis pour fabricant)

```ts
import type {
  Organization,
  WebSite,
  WithContext,
  BreadcrumbList,
} from "schema-dts";
import { site } from "../site";

export const organizationJsonLd: WithContext<Organization> = {
  "@context": "https://schema.org",
  "@type": "Organization",
  "@id": `${site.url}#organization`,
  name: site.name,
  legalName: site.legalName,
  url: site.url,
  logo: `${site.url}${site.logo.src}`,
  email: site.contact.email,
  ...(site.contact.addressCountry || site.contact.addressLocality
    ? {
        address: {
          "@type": "PostalAddress",
          addressLocality: site.contact.addressLocality || undefined,
          addressCountry: site.contact.addressCountry || undefined,
        },
      }
    : {}),
  sameAs: [site.social.github, site.social.linkedin].filter(
    (url) => url.length > 0,
  ),
};

export const websiteJsonLd: WithContext<WebSite> = {
  "@context": "https://schema.org",
  "@id": `${site.url}#website`,
  "@type": "WebSite",
  name: site.name,
  url: site.url,
  inLanguage: [...site.locales],
  publisher: { "@id": `${site.url}#organization` },
};

export type Crumb = { name: string; path: string };

export function breadcrumbJsonLd(
  crumbs: Crumb[],
): WithContext<BreadcrumbList> {
  return {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    itemListElement: crumbs.map((crumb, index) => ({
      "@type": "ListItem",
      position: index + 1,
      name: crumb.name,
      item: `${site.url}${crumb.path === "/" ? "" : crumb.path}`,
    })),
  };
}
```

- [ ] **Step 3 : Créer `src/config/seo/index.ts`** (façade — préserve `@/config/seo`)

```ts
export * from "./meta";
export * from "./structured-data";
export * from "./robots";
export * from "./ai";
```

- [ ] **Step 4 : Supprimer l'ancien fichier**

```bash
git rm src/config/seo.ts
```

> Les Tasks 2 et 5 créent `seo/robots.ts` et `seo/ai.ts` (importés par `meta.ts` et `index.ts`). Le build ne passera qu'une fois ces deux fichiers créés — c'est pourquoi on enchaîne avant de vérifier.

## Task 2 : Module robots + Route Handler `robots.txt`

**Files:**
- Create: `src/config/seo/robots.ts`
- Create: `src/app/robots.txt/route.ts`

- [ ] **Step 1 : Créer `src/config/seo/robots.ts`**

```ts
// Directives meta robots — string unique (source de vérité).
// Strate 1 : combo AI Overviews prouvé. Les directives `noai`/`noimageai`
// sont ajoutées en Strate 2.
const baseRobotsDirectives = [
  "index",
  "follow",
  "max-snippet:-1",
  "max-image-preview:large",
  "max-video-preview:-1",
];

export const robotsMetaContent = baseRobotsDirectives.join(", ");

// Listes de crawlers AI (utilisées par le Route Handler).
// Strate 1 : vides. Remplies en Strate 2.
export const aiTrainingBots: string[] = [];
export const aiCitationBots: string[] = [];
```

- [ ] **Step 2 : Créer le Route Handler `src/app/robots.txt/route.ts`** (Strate 1 : règles standard + sitemap)

```ts
import { site } from "@/config/site";
import { aiCitationBots, aiTrainingBots } from "@/config/seo/robots";

export const dynamic = "force-static";

export function GET(): Response {
  const lines: string[] = [
    "User-agent: *",
    "Allow: /",
    "Disallow: /api/",
    "",
  ];

  // Strate 2 : blocage training + autorisation citation (listes vides en Strate 1).
  for (const bot of aiTrainingBots) {
    lines.push(`User-agent: ${bot}`, "Disallow: /", "");
  }
  for (const bot of aiCitationBots) {
    lines.push(`User-agent: ${bot}`, "Allow: /", "");
  }

  lines.push(`Sitemap: ${site.url}/sitemap.xml`);

  return new Response(`${lines.join("\n")}\n`, {
    headers: { "Content-Type": "text/plain" },
  });
}
```

- [ ] **Step 3 : VÉRIFICATION CRITIQUE — pas de collision `/robots.txt`**

Run:
```bash
bun run build && bun run start &
sleep 4 && curl -s http://localhost:3000/robots.txt && kill %1
```
Expected: la sortie contient `User-agent: *`, `Disallow: /api/`, et `Sitemap: .../sitemap.xml`.

**Fallback si la route 404 ou collisionne** (le dossier `robots.txt/` est rejeté) : renommer en Route Handler classique impossible pour ce chemin → repli sur `src/app/robots.ts` natif (`MetadataRoute.Robots`, voir `node_modules/next/dist/docs/.../robots.md`) pour les règles bot, et **reporter `Content-Signal` en stub Strate 3** (documenté, non émis). Noter ce repli dans le commit.

## Task 2bis : Migrer les messages SEO sous `<Page>.seo` (option A)

**Files:**
- Modify: `messages/en.json`
- Modify: `messages/fr.json`

Regrouper les clés SEO (`title`, `metaDescription`, `metaKeywords`) de **chaque** namespace de page sous un sous-objet `seo`. L'UI reste à plat. `LocaleLayout.metaKeywords` (global) reste INCHANGÉ (flat).

- [ ] **Step 1 : Restructurer chaque page**

Pour chaque namespace de page (`IndexPage`, `AboutUsPage`, `ProductionProcessPage`, `DesignDevelopmentPage`, `GalleryPage`, `NewsEventsPage`, `SustainabilityPage`, `ContactUsPage`, `PathnamesPage`), déplacer `title` + `metaDescription` + `metaKeywords` dans un objet `seo`. Exemple `IndexPage` (en.json) :

```jsonc
"IndexPage": {
  "description": "…",            // UI — reste à plat
  "seo": {
    "title": "next-intl example",
    "metaDescription": "A basic example…",
    "metaKeywords": [
      "fine jewellery manufacturer",
      "jewellery manufacturing",
      "B2B jewellery supplier",
      "wholesale jewellery manufacturer",
      "thailand jewellery factory"
    ]
  }
}
```

Répliquer la même transformation dans `messages/fr.json` (mêmes clés, valeurs FR).

- [ ] **Step 2 : Mettre à jour le wiring de la home** (`src/app/[locale]/page.tsx`)

Dans `generateMetadata`, scoper sur `IndexPage.seo` :

```ts
  const t = await getTranslations({
    locale: typedLocale,
    namespace: "IndexPage.seo",
  });
  return buildPageMetadata({
    href: "/",
    locale: typedLocale,
    title: t("title"),
    description: t("metaDescription"),
    keywords: readKeywords(t, "IndexPage.seo"),
  });
```

Dans le composant, l'attribut cosmétique `<div title={t("title")}>` lit l'UI — remplacer par un texte UI réel (ex. un heading) OU scoper un translator dédié `useTranslations("IndexPage.seo")` si on veut réutiliser le titre SEO. Choix laissé à l'exécutant selon le contenu de la page.

- [ ] **Step 3 : Vérifier la déclaration de types next-intl**

Le projet génère `createMessagesDeclaration: "./messages/en.json"` (next.config). Lancer le build régénère les types ; toute clé manquante en FR ou mal scopée casse le type-check.

Run:
```bash
bun run build
```
Expected: build OK. Si erreur « Property 'seo' does not exist » → une page n'a pas été migrée des deux côtés (en+fr).

## Task 3 : Manifest PWA

**Files:**
- Create: `src/app/manifest.ts`

- [ ] **Step 1 : Créer `src/app/manifest.ts`**

```ts
import type { MetadataRoute } from "next";
import { site } from "@/config/site";

export default function manifest(): MetadataRoute.Manifest {
  return {
    name: site.name,
    short_name: site.shortName,
    description: site.description,
    start_url: "/",
    display: "standalone",
    background_color: site.backgroundColor,
    theme_color: site.themeColor,
    icons: [
      {
        src: site.logo.src,
        sizes: "any",
        type: "image/svg+xml",
      },
    ],
  };
}
```

- [ ] **Step 2 : Vérifier**

Run:
```bash
bun run build && bun run start &
sleep 4 && curl -s http://localhost:3000/manifest.webmanifest && kill %1
```
Expected: JSON contenant `"name":"ADV MFR"`, `"theme_color":"#101E33"`, `"display":"standalone"`.

## Task 4 : Viewport `viewport-fit=cover` + theme-color/color-scheme

**Files:**
- Modify: `src/app/[locale]/layout.tsx:35-39` (objet `viewport`)

- [ ] **Step 1 : Remplacer l'export `viewport`**

```tsx
export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  viewportFit: "cover",
  themeColor: site.themeColor,
  colorScheme: "light",
};
```

- [ ] **Step 2 : Vérifier le rendu**

Run:
```bash
bun run build
grep -ro 'viewport-fit=cover\|name="color-scheme" content="light"\|name="theme-color"' .next/server/app/en.html | head
```
Expected: les trois motifs présents.

## Task 5 : Module `ai.ts` (placeholder Strate 1) + aiMetaTags vide

**Files:**
- Create: `src/config/seo/ai.ts`

- [ ] **Step 1 : Créer `src/config/seo/ai.ts`** (vide en Strate 1, structure prête)

```ts
// Meta tags AI/GEO émis via metadata.other.
// Strate 1 : aucun. Strate 3 : ai-disclosure.
export const aiMetaTags: Record<string, string> = {};
```

## Task 6 : Vérification fondation Strate 1

**Files:**
- Modify: `src/app/[locale]/page.tsx` (déjà migré en Task 2bis — vérifier import inchangé)

- [ ] **Step 1 : Vérifier que les pages existantes importent toujours depuis `@/config/seo`**

Run:
```bash
grep -rn 'from "@/config/seo"' src/app
```
Expected: les imports existants (`page.tsx`, `layout.tsx`) inchangés, résolus via `seo/index.ts`.

- [ ] **Step 2 : Build complet + lint**

Run:
```bash
bun run build && bun run lint
```
Expected: build OK (type-check inclus), 0 erreur lint.

- [ ] **Step 3 : Vérifier les meta Strate 1 dans le HTML buildé**

Run:
```bash
grep -ro 'name="robots" content="index, follow, max-snippet:-1[^"]*"\|name="format-detection"\|rel="manifest"\|name="author"' .next/server/app/en.html | sort -u
```
Expected: robots avec le combo, format-detection, manifest link, author présents.

- [ ] **Step 4 : Commit Strate 1**

```bash
git add -A
git commit -m "feat(seo): strate 1 — fondation stable (modules, robots.txt, manifest, viewport, JSON-LD fabricant)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

- [ ] **Step 5 : Merge ff dans dev + cleanup**

```bash
git checkout dev && git merge feature/seo-strate-1-stable --ff-only && git push
git branch -d feature/seo-strate-1-stable
```

---

# STRATE 2 — Moderne recommandé

Branche : `feature/seo-strate-2-moderne`

- [ ] **Step 0 : Créer la branche**

```bash
git checkout dev && git pull
git checkout -b feature/seo-strate-2-moderne
```

## Task 7 : Posture AI dans robots.txt + meta noai/noimageai

**Files:**
- Modify: `src/config/seo/robots.ts`

- [ ] **Step 1 : Remplir les listes de bots et ajouter noai/noimageai**

Dans `src/config/seo/robots.ts`, remplacer le contenu par :

```ts
// Directives meta robots — string unique (source de vérité).
const baseRobotsDirectives = [
  "index",
  "follow",
  "max-snippet:-1",
  "max-image-preview:large",
  "max-video-preview:-1",
  // Strate 2 — protection des visuels joaillerie contre l'entraînement.
  "noai",
  "noimageai",
];

export const robotsMetaContent = baseRobotsDirectives.join(", ");

// Crawlers d'entraînement — bloqués (cèdent le contenu aux datasets).
export const aiTrainingBots: string[] = [
  "GPTBot",
  "Google-Extended",
  "ClaudeBot",
  "CCBot",
  "Bytespider",
  "Meta-ExternalAgent",
  "Applebot-Extended",
];

// Crawlers de citation/retrieval — autorisés (visibilité GEO).
export const aiCitationBots: string[] = [
  "OAI-SearchBot",
  "PerplexityBot",
  "Claude-SearchBot",
  "DuckAssistBot",
];
```

- [ ] **Step 2 : Vérifier robots.txt rendu**

Run:
```bash
bun run build && bun run start &
sleep 4 && curl -s http://localhost:3000/robots.txt && kill %1
```
Expected: blocs `User-agent: GPTBot` / `Disallow: /`, `User-agent: OAI-SearchBot` / `Allow: /`, et le `Sitemap:` final.

- [ ] **Step 3 : Vérifier le meta robots avec noai**

Run:
```bash
grep -ro 'name="robots" content="[^"]*noai, noimageai"' .next/server/app/en.html | head
```
Expected: un match (la string complète se termine par `noai, noimageai`).

## Task 8 : Headers HTTP de sécurité

**Files:**
- Modify: `next.config.ts`

- [ ] **Step 1 : Ajouter `headers()` au config**

Dans `next.config.ts`, ajouter dans l'objet `nextConfig` (après `images`) :

```ts
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          {
            key: "Referrer-Policy",
            value: "strict-origin-when-cross-origin",
          },
          {
            key: "Permissions-Policy",
            value: "browsing-topics=(), interest-cohort=()",
          },
        ],
      },
      {
        source: "/_next/image",
        headers: [{ key: "X-Robots-Tag", value: "noimageai" }],
      },
    ];
  },
```

- [ ] **Step 2 : Vérifier les headers**

Run:
```bash
bun run build && bun run start &
sleep 4 && curl -sI http://localhost:3000/en | grep -i 'referrer-policy\|permissions-policy' && kill %1
```
Expected: `Referrer-Policy: strict-origin-when-cross-origin` et `Permissions-Policy: browsing-topics=(), interest-cohort=()`.

## Task 9 : Perf/mobile — preconnect, apple-touch-icon, web-app meta

**Files:**
- Modify: `src/config/seo/meta.ts` (ajout dans `metadata.other` global du layout)
- Modify: `src/app/[locale]/layout.tsx` (appletouchicon + preconnect via metadata)

- [ ] **Step 1 : Ajouter au layout la metadata web-app + icons**

Dans `src/app/[locale]/layout.tsx`, dans `generateMetadata`, ajouter au `return` :

```ts
    appleWebApp: {
      capable: true,
      title: site.shortName,
      statusBarStyle: "default",
    },
    icons: {
      apple: [{ url: "/apple-touch-icon.png", sizes: "180x180" }],
    },
```

> `appleWebApp.capable` émet à la fois `mobile-web-app-capable` et le fallback `apple-mobile-web-app-capable` (comportement natif Next, cf. `generate-metadata.md`).

- [ ] **Step 2 : Ajouter les resource hints fonts dans le `<head>` du layout**

Dans `src/app/[locale]/layout.tsx`, le rendu JSX — ajouter avant `<body>` un `<head>` minimal pour les preconnect Google Fonts (next/font précharge déjà, mais preconnect explicite aide le LCP) :

> ⚠️ Attention : ne PAS réintroduire `<meta charSet>` (doublon corrigé précédemment). next/font gère le préchargement des fontes self-hosted ; les preconnect Google ne sont utiles que si les fontes restent sur le CDN Google. Comme `next/font/google` self-host à la compilation, **cette étape est SKIP** sauf si un audit montre un appel réseau vers `fonts.gstatic.com`.

Run (vérifier qu'aucune requête fonts externe n'existe) :
```bash
grep -ro 'fonts.gstatic.com\|fonts.googleapis.com' .next/server/app/en.html | head
```
Expected: aucun match → SKIP des preconnect (next/font self-host). Si match → ajouter `<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />`.

- [ ] **Step 3 : Vérifier apple-web-app meta**

Run:
```bash
bun run build
grep -ro 'name="mobile-web-app-capable"\|name="apple-mobile-web-app-capable"\|rel="apple-touch-icon"' .next/server/app/en.html | sort -u
```
Expected: les trois présents.

## Task 10 : Builders JSON-LD par type de page

**Files:**
- Modify: `src/config/seo/structured-data.ts`

- [ ] **Step 1 : Ajouter les builders typés**

Dans `src/config/seo/structured-data.ts`, ajouter les imports et builders :

```ts
import type {
  AboutPage,
  ContactPage,
  ImageGallery,
  WebPage,
  Article,
  Event,
  ImageObject,
} from "schema-dts";

export function webPageJsonLd(args: {
  name: string;
  description: string;
  path: string;
  inLanguage: string;
}): WithContext<WebPage> {
  return {
    "@context": "https://schema.org",
    "@type": "WebPage",
    name: args.name,
    description: args.description,
    url: `${site.url}${args.path === "/" ? "" : args.path}`,
    inLanguage: args.inLanguage,
    isPartOf: { "@id": `${site.url}#website` },
  };
}

export function aboutPageJsonLd(args: {
  name: string;
  description: string;
  path: string;
  inLanguage: string;
}): WithContext<AboutPage> {
  return {
    "@context": "https://schema.org",
    "@type": "AboutPage",
    name: args.name,
    description: args.description,
    url: `${site.url}${args.path}`,
    inLanguage: args.inLanguage,
    about: { "@id": `${site.url}#organization` },
  };
}

export function contactPageJsonLd(args: {
  name: string;
  description: string;
  path: string;
  inLanguage: string;
}): WithContext<ContactPage> {
  return {
    "@context": "https://schema.org",
    "@type": "ContactPage",
    name: args.name,
    description: args.description,
    url: `${site.url}${args.path}`,
    inLanguage: args.inLanguage,
    about: { "@id": `${site.url}#organization` },
    mainEntity: {
      "@type": "Organization",
      "@id": `${site.url}#organization`,
      email: site.contact.email,
      ...(site.contact.addressCountry || site.contact.addressLocality
        ? {
            address: {
              "@type": "PostalAddress",
              addressLocality: site.contact.addressLocality || undefined,
              addressCountry: site.contact.addressCountry || undefined,
            },
          }
        : {}),
    },
  };
}

export function imageGalleryJsonLd(args: {
  name: string;
  description: string;
  path: string;
  inLanguage: string;
  images: { url: string; caption?: string }[];
}): WithContext<ImageGallery> {
  return {
    "@context": "https://schema.org",
    "@type": "ImageGallery",
    name: args.name,
    description: args.description,
    url: `${site.url}${args.path}`,
    inLanguage: args.inLanguage,
    image: args.images.map(
      (img): ImageObject => ({
        "@type": "ImageObject",
        contentUrl: img.url.startsWith("http")
          ? img.url
          : `${site.url}${img.url}`,
        ...(img.caption ? { caption: img.caption } : {}),
      }),
    ),
  };
}

// Builders prêts pour news-events — wirés quand le contenu réel existera
// (actuellement pas de données d'articles/événements typées dans le repo).
export function articleJsonLd(args: {
  headline: string;
  description: string;
  path: string;
  inLanguage: string;
  datePublished: string;
  dateModified?: string;
  image?: string;
}): WithContext<Article> {
  return {
    "@context": "https://schema.org",
    "@type": "Article",
    headline: args.headline,
    description: args.description,
    url: `${site.url}${args.path}`,
    inLanguage: args.inLanguage,
    datePublished: args.datePublished,
    ...(args.dateModified ? { dateModified: args.dateModified } : {}),
    ...(args.image ? { image: `${site.url}${args.image}` } : {}),
    publisher: { "@id": `${site.url}#organization` },
  };
}

export function eventJsonLd(args: {
  name: string;
  description: string;
  startDate: string;
  endDate?: string;
  location?: string;
}): WithContext<Event> {
  return {
    "@context": "https://schema.org",
    "@type": "Event",
    name: args.name,
    description: args.description,
    startDate: args.startDate,
    ...(args.endDate ? { endDate: args.endDate } : {}),
    ...(args.location ? { location: { "@type": "Place", name: args.location } } : {}),
    organizer: { "@id": `${site.url}#organization` },
  };
}
```

- [ ] **Step 2 : Wirer WebPage + Breadcrumb sur about-us** (modèle réplicable)

Ouvrir `src/app/[locale]/about-us/page.tsx`. Dans le composant page, injecter le JSON-LD avec le pattern de `src/app/[locale]/page.tsx:41-52`. Le name/description SEO viennent de la sous-namespace `.seo` (option A) — utiliser un translator scopé `useTranslations("AboutUsPage.seo")` (`tSeo`). Bloc à insérer au début du JSX retourné :

```tsx
      <script
        type="application/ld+json"
        // biome-ignore lint/security/noDangerouslySetInnerHtml: structured data
        dangerouslySetInnerHTML={{
          __html: JSON.stringify(
            aboutPageJsonLd({
              name: tSeo("title"),
              description: tSeo("metaDescription"),
              path: "/about-us",
              inLanguage: locale,
            }),
          ),
        }}
      />
      <script
        type="application/ld+json"
        // biome-ignore lint/security/noDangerouslySetInnerHtml: structured data
        dangerouslySetInnerHTML={{
          __html: JSON.stringify(
            breadcrumbJsonLd([
              { name: site.name, path: "/" },
              { name: tSeo("title"), path: "/about-us" },
            ]),
          ),
        }}
      />
```

> Imports à ajouter : `aboutPageJsonLd`, `breadcrumbJsonLd` depuis `@/config/seo`, `site` depuis `@/config/site`. Déclarer `const tSeo = useTranslations("AboutUsPage.seo");` (et garder le translator UI existant pour le reste du composant). `generateMetadata` de la page doit aussi être scopé sur `"AboutUsPage.seo"` comme la home (Task 2bis Step 2).

- [ ] **Step 3 : Répliquer sur contact-us, gallery, et les autres pages**

Appliquer le même pattern :
- `contact-us` → `contactPageJsonLd` + breadcrumb
- `gallery` → `imageGalleryJsonLd` (images depuis `public/home/` ou la liste réelle de la galerie) + breadcrumb
- `production-process`, `design-development`, `sustainability`, `news-events` → `webPageJsonLd` + breadcrumb

> `articleJsonLd`/`eventJsonLd` restent définis mais NON wirés tant que news-events n'a pas de données réelles d'articles/événements (éviter un JSON-LD vide). Documenté ici, pas un placeholder de code.

- [ ] **Step 4 : Build + lint + vérif JSON-LD**

Run:
```bash
bun run build && bun run lint
grep -ro '"@type":"AboutPage"\|"@type":"BreadcrumbList"\|"@type":"ImageGallery"\|"@type":"ContactPage"' .next/server/app/en/*.html | sort -u
```
Expected: build OK, lint OK, et les `@type` attendus présents dans les pages correspondantes.

## Task 11 : Images dans le sitemap (gallery)

**Files:**
- Modify: `src/app/sitemap.ts`

- [ ] **Step 1 : Ajouter les images à l'entrée gallery**

Dans `src/app/sitemap.ts`, dans le `.map` qui construit chaque entrée, ajouter conditionnellement le champ `images` pour la route `/gallery` :

```ts
        return {
          url: absoluteUrl(localizedPath),
          lastModified,
          changeFrequency: seo.sitemap.changeFreq,
          priority: href === "/" ? 1 : seo.sitemap.priority,
          ...(href === "/gallery"
            ? {
                images: [
                  `${site.url}/home/white-gold-set-advance-manufacturer.webp`,
                ],
              }
            : {}),
          alternates: {
            // ... inchangé
          },
        } satisfies MetadataRoute.Sitemap[number];
```

> Remplacer la liste d'images par les fichiers réels présents dans `public/` pour la galerie.

- [ ] **Step 2 : Vérifier le sitemap**

Run:
```bash
bun run build && bun run start &
sleep 4 && curl -s http://localhost:3000/sitemap.xml | grep -i 'image:' | head && kill %1
```
Expected: au moins une balise `<image:image>` / `<image:loc>` pour la page gallery.

- [ ] **Step 3 : Commit + merge Strate 2**

```bash
git add -A
git commit -m "feat(seo): strate 2 — posture AI, headers securite, JSON-LD par page, images sitemap

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
git checkout dev && git merge feature/seo-strate-2-moderne --ff-only && git push
git branch -d feature/seo-strate-2-moderne
```

---

# STRATE 3 — Expérimental 2026

Branche : `feature/seo-strate-3-experimental`

- [ ] **Step 0 : Créer la branche**

```bash
git checkout dev && git pull
git checkout -b feature/seo-strate-3-experimental
```

## Task 12 : `ai-disclosure` meta (rentable — EU AI Act)

**Files:**
- Modify: `src/config/seo/ai.ts`

- [ ] **Step 1 : Remplir `aiMetaTags`**

```ts
// Meta tags AI/GEO émis via metadata.other.
// ai-disclosure : assertion positive "contenu humain" (préemptif EU AI Act
// Article 50, applicable août 2026). Valeurs possibles :
// none | ai-assisted | ai-generated | autonomous | mixed.
export const aiMetaTags: Record<string, string> = {
  "ai-disclosure": "none",
};
```

- [ ] **Step 2 : Vérifier**

Run:
```bash
bun run build
grep -ro 'name="ai-disclosure" content="none"' .next/server/app/en.html | head
```
Expected: un match.

> ⚠️ Cohérence : `ai-disclosure="none"` affirme que TOUT le contenu est 100% humain. Tant que le site contient du lorem ipsum c'est neutre, mais à la mise en ligne du contenu réel, si une page utilise de l'IA, surcharger cette page (passer `aiMetaTags` en paramètre par page). Noté dans le spec §9.

## Task 13 : `Content-Signal` dans robots.txt (rentable — 3,8M domaines)

**Files:**
- Modify: `src/app/robots.txt/route.ts`

- [ ] **Step 1 : Ajouter la ligne Content-Signal**

Dans `src/app/robots.txt/route.ts`, modifier le bloc `User-agent: *` initial :

```ts
  const lines: string[] = [
    "User-agent: *",
    "Content-Signal: search=yes, ai-train=no, ai-input=no",
    "Allow: /",
    "Disallow: /api/",
    "",
  ];
```

- [ ] **Step 2 : Vérifier**

Run:
```bash
bun run build && bun run start &
sleep 4 && curl -s http://localhost:3000/robots.txt | grep -i 'content-signal' && kill %1
```
Expected: `Content-Signal: search=yes, ai-train=no, ai-input=no`.

> Si la Task 2 a basculé sur le fallback `robots.ts` natif (collision route), cette task est SKIP et Content-Signal reste un stub documenté (voir Task 14).

## Task 14 : Stubs documentés (NON branchés)

**Files:**
- Create: `docs/seo/future-tags.md`

- [ ] **Step 1 : Documenter les tags spéculatifs non implémentés**

Créer `docs/seo/future-tags.md` :

```markdown
# Tags SEO/GEO en anticipation (non branchés)

Statut : architecturés/documentés, NON émis. À réévaluer à la review SEO de septembre 2026.

## llms.txt + tags llms:*
- `/llms.txt`, `<link rel="llms">`, `llms:website|description|instructions`.
- **Décision : non implémenté.** Verbatim du référentiel : Mueller (Google) « comparable au keywords meta tag » ; Illyes « Google doesn't support LLMs.txt » ; Limy.ai (mai 2026) part de trafic « statistically negligible ». Pas un levier GEO 2026.

## C2PA manifest link
- `<link rel="c2pa-manifest">` pour la provenance des images.
- À considérer quand les visuels joaillerie seront signés (cf. Pixel 10 signe chaque photo). Pas d'outil de signature en place.

## fediverse:creator / Bluesky site.standard.*
- Attribution éditoriale décentralisée. Pertinent seulement si présence Fediverse/Bluesky active.
```

- [ ] **Step 2 : Commit + merge Strate 3**

```bash
git add -A
git commit -m "feat(seo): strate 3 — ai-disclosure, Content-Signal, stubs documentes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
git checkout dev && git merge feature/seo-strate-3-experimental --ff-only && git push
git branch -d feature/seo-strate-3-experimental
```

---

## Vérification finale (après les 3 strates)

- [ ] **Build + CI complet**

Run:
```bash
bun run ci && bun run build
```
Expected: format/lint/deadcode/build tous verts.

- [ ] **Inventaire des tags rendus**

Run:
```bash
bun run start &
sleep 4
echo "=== robots.txt ===" && curl -s localhost:3000/robots.txt
echo "=== manifest ===" && curl -s localhost:3000/manifest.webmanifest
echo "=== headers ===" && curl -sI localhost:3000/en | grep -iE 'referrer|permissions'
kill %1
```
Expected: robots avec bots AI + Content-Signal, manifest JSON, headers sécurité présents.

- [ ] **QA Next.js avant PR** (nextjs-16.x.md §5) : lancer les skills `nextjs-best-practices` / `nextjs-performance` sur le diff.
