Référencement : métadonnées complètes, données structurées, sitemap et robots

- Layout : metadataBase, gabarit de titre « %s · Plein R », description,
  mots-clés, Open Graph / Twitter, robots, icônes, manifeste, viewport et
  couleur de thème ; JSON-LD Organization + WebSite (recherche annuaire).
- Chaque page publique pose titre, description (≤ 160 caractères) et
  canonique via pageMetadata() ; l'annuaire ignore ainsi ?q=. Fiche adhérent :
  titre « Nom · Catégorie à Ville », description dérivée, LocalBusiness avec
  adresse, contacts et horaires (OpeningHoursSpecification), BreadcrumbList.
  Annuaire : ItemList des adhérents. Association : BusinessEvent par
  rencontre à venir. Rencontres passées : fil d'Ariane.
- Nouvelles routes : robots.txt, sitemap.xml (pages statiques + adhérents
  actifs), manifest.webmanifest, image de partage 1200×630 générée avec le
  logo, page 404 dédiée (noindex).
- /backend, /login, /inscription/* et les fiches inactives en noindex.
- Landmarks <main> sur toutes les pages publiques, fil d'Ariane en <nav>.
- serializeJsonLd() échappe <, > et & : les saisies des adhérents ne peuvent
  pas fermer le bloc <script>. Tests unitaires dans tests/seo.test.ts.

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-05 10:41:53 +00:00
1 parent dd350038f0
commit a8adb9ae6a
21 files changed
+1113 -36

No files matched your search

+21 -1
View File
@@ -1,21 +1,34 @@
import type { Metadata } from "next";
import { asc, desc, inArray } from "drizzle-orm";
import { JsonLd } from "@/components/JsonLd";
import { PastMeetingGallery } from "@/components/PastMeetingGallery";
import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader";
import { db } from "@/db";
import { meetingRegistrations, pastMeetingPhotos, pastMeetings } from "@/db/schema";
import { splitLines } from "@/lib/site-settings";
import { breadcrumbJsonLd, pageMetadata } from "@/lib/seo";
import { publicBaseUrl } from "@/lib/seo-server";
export const dynamic = "force-dynamic";
export const metadata: Metadata = pageMetadata({
title: "Rencontres passées de l'association",
description:
"Retour en images sur les rencontres Plein R : soirées réseau, visites d'entreprises et moments " +
"partagés entre entrepreneurs du Bassin de Pompey.",
path: "/rencontres-passees",
});
function formatDate(date: Date) {
return new Intl.DateTimeFormat("fr-FR", { day: "numeric", month: "long", year: "numeric" }).format(date);
}
export default async function PastMeetingsPage() {
const [archives, photos] = await Promise.all([
const [archives, photos, baseUrl] = await Promise.all([
db.select().from(pastMeetings).orderBy(desc(pastMeetings.eventDate)),
db.select().from(pastMeetingPhotos).orderBy(asc(pastMeetingPhotos.position), asc(pastMeetingPhotos.id)),
publicBaseUrl(),
]);
const linkedIds = archives.map((archive) => archive.meetingId).filter((id): id is number => id !== null);
const linkedRegistrations = linkedIds.length > 0
@@ -37,6 +50,13 @@ export default async function PastMeetingsPage() {
return (
<div style={{ background: "#F6F2E8", minHeight: "100vh", fontFamily: "'Public Sans',sans-serif", color: "#33291D" }}>
<SiteHeader active="association" logo />
<JsonLd
data={breadcrumbJsonLd(baseUrl, [
{ name: "Accueil", path: "/" },
{ name: "L'association", path: "/association" },
{ name: "Rencontres passées", path: "/rencontres-passees" },
])}
/>
<main>
<section style={{ background: "#13324F", color: "#fff" }}>
<div className="container" style={{ paddingTop: 58, paddingBottom: 54 }}>