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

+30 -6
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import type { DaySchedule, WeekDayKey } from "@/lib/member-profile";
import type { SiteSettings, SocialLink } from "@/lib/site-settings";
import { slugify } from "@/lib/slug";
/**
* Référencement : constantes, métadonnées et données structurées (JSON-LD).
@@ -83,9 +84,32 @@ export function absoluteUrl(baseUrl: string, path = "/"): string {
return `${base}${path.startsWith("/") ? path : `/${path}`}`;
}
/** Chemin canonique d'une fiche adhérent. */
export function memberPath(id: number): string {
return `/adherents/${id}`;
export type MemberPathInput = { id: number; name: string; city?: string | null };
/**
* Chemin canonique d'une fiche adhérent : `/adherents/12-au-bon-pain-frouard`.
*
* L'identifiant reste en tête : il suffit à retrouver la fiche, et le nom qui
* suit n'est là que pour les moteurs et les lecteurs. Toute autre écriture
* (`/adherents/12`, ancien nom…) est redirigée en 301 vers cette forme par la
* page, donc un renommage ne casse aucun lien.
*/
export function memberPath(member: MemberPathInput): string {
const slug = slugify([member.name, member.city ?? ""].filter(Boolean).join(" "));
return slug ? `/adherents/${member.id}-${slug}` : `/adherents/${member.id}`;
}
/** Identifiant numérique en tête du paramètre d'URL (`12-au-bon-pain` → 12), sinon `null`. */
export function parseMemberParam(param: string): number | null {
const match = /^(\d+)(?:-|$)/.exec(param);
if (!match) return null;
const id = Number(match[1]);
return Number.isSafeInteger(id) && id > 0 ? id : null;
}
/** Chemin d'une page métier de l'annuaire. */
export function categoryPath(slug: string): string {
return `/annuaire/${slug}`;
}
/**
@@ -259,7 +283,7 @@ export function localBusinessJsonLd(
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const url = absoluteUrl(baseUrl, memberPath(member.id));
const url = absoluteUrl(baseUrl, memberPath(member));
return compact({
"@context": "https://schema.org",
"@type": "LocalBusiness",
@@ -325,7 +349,7 @@ export function eventJsonLd(baseUrl: string, meeting: EventInput) {
});
}
export type ItemListEntry = { id: number; name: string; description?: string | null };
export type ItemListEntry = MemberPathInput;
/** Liste d'adhérents (annuaire) : chaque entrée pointe vers sa fiche. */
export function memberListJsonLd(baseUrl: string, name: string, members: ItemListEntry[]) {
@@ -337,7 +361,7 @@ export function memberListJsonLd(baseUrl: string, name: string, members: ItemLis
itemListElement: members.map((m, index) => ({
"@type": "ListItem",
position: index + 1,
url: absoluteUrl(baseUrl, memberPath(m.id)),
url: absoluteUrl(baseUrl, memberPath(m)),
name: m.name,
})),
};