mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-23 23:28:04 +02:00
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:
21 files changed
+1113
-36
No files matched your search
+80
-8
@@ -1,4 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { JsonLd } from "@/components/JsonLd";
|
||||
import {
|
||||
INDEX_FOLLOW,
|
||||
SITE_DESCRIPTION,
|
||||
SITE_KEYWORDS,
|
||||
SITE_LOCALE,
|
||||
SITE_NAME,
|
||||
SITE_TITLE,
|
||||
THEME_COLOR,
|
||||
organizationJsonLd,
|
||||
webSiteJsonLd,
|
||||
} from "@/lib/seo";
|
||||
import { metadataBase, publicBaseUrl } from "@/lib/seo-server";
|
||||
import { getSiteSettings, socialLinks } from "@/lib/site-settings";
|
||||
import "./globals.css";
|
||||
|
||||
// Rendu dynamique pour toutes les pages : la CSP à nonce du middleware ne peut
|
||||
@@ -6,17 +20,73 @@ import "./globals.css";
|
||||
// interrogent de toute façon la base.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Plein R — Commerçants & entreprises du Bassin de Pompey",
|
||||
description:
|
||||
"Association des commerçants et entreprises du Bassin de Pompey. Réseau · Rencontre · Réussite.",
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
themeColor: THEME_COLOR,
|
||||
colorScheme: "light",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
/**
|
||||
* Métadonnées par défaut de tout le site. Chaque page complète ou remplace
|
||||
* titre, description et canonique ; `metadataBase` rend absolues les URLs
|
||||
* relatives (canonique, Open Graph, image de partage).
|
||||
*/
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
metadataBase: await metadataBase(),
|
||||
title: {
|
||||
default: SITE_TITLE,
|
||||
template: `%s · ${SITE_NAME}`,
|
||||
},
|
||||
description: SITE_DESCRIPTION,
|
||||
applicationName: SITE_NAME,
|
||||
keywords: SITE_KEYWORDS,
|
||||
authors: [{ name: `Association ${SITE_NAME}` }],
|
||||
creator: `Association ${SITE_NAME}`,
|
||||
publisher: `Association ${SITE_NAME}`,
|
||||
category: "business",
|
||||
referrer: "strict-origin-when-cross-origin",
|
||||
// Pas de canonique ici : chaque page publique pose la sienne via
|
||||
// `pageMetadata()` ; les pages privées (noindex) n'en ont pas.
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: SITE_LOCALE,
|
||||
siteName: SITE_NAME,
|
||||
title: SITE_TITLE,
|
||||
description: SITE_DESCRIPTION,
|
||||
url: "/",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: SITE_TITLE,
|
||||
description: SITE_DESCRIPTION,
|
||||
},
|
||||
robots: INDEX_FOLLOW,
|
||||
icons: {
|
||||
icon: [{ url: "/assets/logo.png", type: "image/png" }],
|
||||
apple: [{ url: "/assets/logo.png", type: "image/png" }],
|
||||
shortcut: ["/assets/logo.png"],
|
||||
},
|
||||
manifest: "/manifest.webmanifest",
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
// Données structurées communes : l'association (Organization) et le site
|
||||
// (WebSite + recherche dans l'annuaire). Ne bloque jamais le rendu.
|
||||
const [baseUrl, settings] = await Promise.all([
|
||||
publicBaseUrl(),
|
||||
getSiteSettings().catch(() => null),
|
||||
]);
|
||||
const structuredData = settings
|
||||
? [organizationJsonLd(baseUrl, settings, socialLinks(settings)), webSiteJsonLd(baseUrl)]
|
||||
: [webSiteJsonLd(baseUrl)];
|
||||
|
||||
return (
|
||||
<html lang="fr">
|
||||
<head>
|
||||
@@ -30,13 +100,15 @@ export default function RootLayout({
|
||||
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700;12..96,800&family=Public+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="icon" href="/assets/logo.png" type="image/png" />
|
||||
{/* Fallback: if JS is disabled, never keep revealed content hidden. */}
|
||||
<noscript>
|
||||
<style>{`.reveal,.reveal-stagger>*{opacity:1!important}`}</style>
|
||||
</noscript>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
<body>
|
||||
{children}
|
||||
<JsonLd data={structuredData} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user