SEO : URLs de fiche parlantes, pages métier indexables, aides de rédaction

- Fiches adhérents : URL canonique /adherents/12-au-bon-pain-frouard
  (memberPath). L'identifiant en tête suffit ; /adherents/12 et tout slug
  périmé sont redirigés de façon permanente vers la forme canonique, donc
  les anciens liens et les partages sociaux restent valides. Tous les liens
  (accueil, carrousel, annuaire, JSON-LD, sitemap, publications Facebook /
  LinkedIn) passent par memberPath.
- Pages métier /annuaire/[categorie] : une page indexable par catégorie
  avec titre, introduction (effectif, communes), grille d'adhérents, fil
  d'Ariane et ItemList. Sans adhérent : noindex et hors sitemap. Section
  « Parcourir par métier » sur l'annuaire et « Autres métiers » sur chaque
  page pour le maillage interne.
- Composants partagés MemberCard et CategoryLinks ; slugify déplacé dans
  src/lib/slug.ts.
- Backend : conseils de rédaction sous les champs description et tags des
  formulaires adhérent (espace adhérent et fiche staff).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
This commit is contained in:
Claude committed 2026-09-06 06:29:28 +00:00
1 parent f9d0d6c825
commit c8417d8560
19 files changed
+489 -99

No files matched your search

+22 -9
View File
@@ -2,7 +2,8 @@ import type { MetadataRoute } from "next";
import { asc, eq } from "drizzle-orm";
import { db } from "@/db";
import { members } from "@/db/schema";
import { STATIC_ROUTES, absoluteUrl, memberPath } from "@/lib/seo";
import { getCategoriesInUse } from "@/lib/queries";
import { STATIC_ROUTES, absoluteUrl, categoryPath, memberPath } from "@/lib/seo";
import { publicBaseUrl } from "@/lib/seo-server";
export const dynamic = "force-dynamic";
@@ -19,22 +20,34 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
priority: route.priority,
}));
let rows: { id: number }[] = [];
let rows: { id: number; name: string; city: string | null }[] = [];
let categoriesInUse: { slug: string }[] = [];
try {
rows = await db
.select({ id: members.id })
.from(members)
.where(eq(members.status, "active"))
.orderBy(asc(members.id));
[rows, categoriesInUse] = await Promise.all([
db
.select({ id: members.id, name: members.name, city: members.city })
.from(members)
.where(eq(members.status, "active"))
.orderBy(asc(members.id)),
getCategoriesInUse(),
]);
} catch {
// Base indisponible : on livre au moins les pages statiques.
}
// Pages métier : seulement celles qui ont au moins un adhérent (les autres
// sont en noindex, une page vide n'apporte rien aux moteurs).
const metiers: MetadataRoute.Sitemap = categoriesInUse.map((c) => ({
url: absoluteUrl(base, categoryPath(c.slug)),
changeFrequency: "weekly",
priority: 0.8,
}));
const fiches: MetadataRoute.Sitemap = rows.map((m) => ({
url: absoluteUrl(base, memberPath(m.id)),
url: absoluteUrl(base, memberPath(m)),
changeFrequency: "weekly",
priority: 0.7,
}));
return [...statics, ...fiches];
return [...statics, ...metiers, ...fiches];
}