SEO : canonical + JSON-LD (WebSite/SearchAction/Organization), robots.txt, sitemap.xml, title enrichi, lang fr-CA, noindex des résultats
- layout : metadataBase + alternates.canonical ./, title « moteur de recherche du web québécois » (template %s | Trouve-Ka), html lang=fr-CA, JSON-LD WebSite (SearchAction /search?q={search_term_string}) + Organization
- app/robots.ts : Allow /, Disallow /api/ /admin /search (espace de résultats infini), lien sitemap
- app/sitemap.ts : accueil + pages publiques statiques (trouveka-bot, status, stats, soumettre, contact)
- /search : robots noindex,follow sur les pages de résultats
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 changed files +115 −6
modified
apps/web/app/layout.tsx
+48 −5
@@ -36,20 +36,25 @@ const fraunces = Fraunces({ | ||
| 36 | 36 | variable: "--font-fraunces", |
| 37 | 37 | }); |
| 38 | 38 | |
| 39 | +const SITE_URL = process.env.PUBLIC_URL ?? "https://www.trouve-ka.com"; | |
| 40 | + | |
| 39 | 41 | export const metadata: Metadata = { |
| 40 | − metadataBase: new URL(process.env.PUBLIC_URL ?? "https://www.trouve-ka.com"), | |
| 42 | + metadataBase: new URL(SITE_URL), | |
| 41 | 43 | title: { |
| 42 | − default: "Trouve-Ka — Un service Groupe KA", | |
| 43 | − template: "%s — Trouve-KA", | |
| 44 | + default: "Trouve-Ka — moteur de recherche du web québécois", | |
| 45 | + template: "%s | Trouve-Ka", | |
| 44 | 46 | }, |
| 45 | 47 | description: |
| 46 | 48 | "Moteur de recherche indépendant du web québécois. Trouve-KA découvre, indexe et classe les sites du Québec — en continu. Un service Groupe KA.", |
| 49 | + alternates: { | |
| 50 | + canonical: "./", | |
| 51 | + }, | |
| 47 | 52 | icons: { |
| 48 | 53 | icon: "/favicon.svg", |
| 49 | 54 | apple: "/apple-touch-icon.png", |
| 50 | 55 | }, |
| 51 | 56 | openGraph: { |
| 52 | − title: "Trouve-Ka — Un service Groupe KA", | |
| 57 | + title: "Trouve-Ka — moteur de recherche du web québécois", | |
| 53 | 58 | description: |
| 54 | 59 | "Moteur de recherche indépendant du web québécois. Trouve-KA découvre, indexe et classe les sites du Québec — en continu. Un service Groupe KA.", |
| 55 | 60 | url: "https://www.trouve-ka.com", |
@@ -93,11 +98,43 @@ const LOCAL_FOOTER_LINKS = [ | ||
| 93 | 98 | { href: "/contact", label: "Contact" }, |
| 94 | 99 | ]; |
| 95 | 100 | |
| 101 | +// Données structurées Schema.org : le site comme moteur de recherche | |
| 102 | +// (WebSite + SearchAction pour la boîte de recherche des SERP) + l'organisation. | |
| 103 | +const JSON_LD = { | |
| 104 | + "@context": "https://schema.org", | |
| 105 | + "@graph": [ | |
| 106 | + { | |
| 107 | + "@type": "WebSite", | |
| 108 | + name: "Trouve-Ka", | |
| 109 | + url: SITE_URL, | |
| 110 | + inLanguage: "fr-CA", | |
| 111 | + description: | |
| 112 | + "Moteur de recherche indépendant du web québécois. Trouve-KA découvre, indexe et classe les sites du Québec — en continu.", | |
| 113 | + potentialAction: { | |
| 114 | + "@type": "SearchAction", | |
| 115 | + target: { | |
| 116 | + "@type": "EntryPoint", | |
| 117 | + urlTemplate: `${SITE_URL}/search?q={search_term_string}`, | |
| 118 | + }, | |
| 119 | + "query-input": "required name=search_term_string", | |
| 120 | + }, | |
| 121 | + publisher: { "@id": `${SITE_URL}/#organization` }, | |
| 122 | + }, | |
| 123 | + { | |
| 124 | + "@type": "Organization", | |
| 125 | + "@id": `${SITE_URL}/#organization`, | |
| 126 | + name: "Trouve-Ka (Groupe KA)", | |
| 127 | + url: SITE_URL, | |
| 128 | + logo: `${SITE_URL}/apple-touch-icon.png`, | |
| 129 | + }, | |
| 130 | + ], | |
| 131 | +}; | |
| 132 | + | |
| 96 | 133 | export default function RootLayout({ |
| 97 | 134 | children, |
| 98 | 135 | }: Readonly<{ children: React.ReactNode }>) { |
| 99 | 136 | return ( |
| 100 | − <html lang="fr"> | |
| 137 | + <html lang="fr-CA"> | |
| 101 | 138 | <body |
| 102 | 139 | className={`${spaceGrotesk.variable} ${inter.variable} ${jetbrains.variable} ${fraunces.variable} flex min-h-dvh flex-col bg-paper font-body text-ink antialiased`} |
| 103 | 140 | > |
@@ -151,6 +188,12 @@ export default function RootLayout({ | ||
| 151 | 188 | <LiveCrawl /> |
| 152 | 189 | </KaFooter> |
| 153 | 190 | |
| 191 | + {/* Données structurées Schema.org (WebSite + SearchAction + Organization) */} | |
| 192 | + <script | |
| 193 | + type="application/ld+json" | |
| 194 | + dangerouslySetInnerHTML={{ __html: JSON.stringify(JSON_LD) }} | |
| 195 | + /> | |
| 196 | + | |
| 154 | 197 | {/* KA Agent — bulle d'assistance IA commune au Groupe KA */} |
| 155 | 198 | <script |
| 156 | 199 | dangerouslySetInnerHTML={{ __html: 'window.KA_AGENT={site:"trouve-ka"}' }} |
added
apps/web/app/robots.ts
+25 −0
@@ -0,0 +1,25 @@ | ||
| 1 | +/** | |
| 2 | + * Trouve-KA — robots.txt (généré par Next App Router) | |
| 3 | + * Author: Simon-Pierre Boucher | |
| 4 | + * Contact: contact@spboucher.ai | |
| 5 | + * | |
| 6 | + * Standard moteur de recherche : les pages de résultats (/search) sont | |
| 7 | + * exclues du crawl (espace d'URL infini), tout le reste du site est ouvert. | |
| 8 | + */ | |
| 9 | + | |
| 10 | +import type { MetadataRoute } from "next"; | |
| 11 | + | |
| 12 | +const SITE_URL = process.env.PUBLIC_URL ?? "https://www.trouve-ka.com"; | |
| 13 | + | |
| 14 | +export default function robots(): MetadataRoute.Robots { | |
| 15 | + return { | |
| 16 | + rules: [ | |
| 17 | + { | |
| 18 | + userAgent: "*", | |
| 19 | + allow: "/", | |
| 20 | + disallow: ["/api/", "/admin", "/search"], | |
| 21 | + }, | |
| 22 | + ], | |
| 23 | + sitemap: `${SITE_URL}/sitemap.xml`, | |
| 24 | + }; | |
| 25 | +} | |
modified
apps/web/app/search/page.tsx
+6 −1
@@ -17,7 +17,12 @@ export async function generateMetadata({ | ||
| 17 | 17 | }: SearchPageProps): Promise<Metadata> { |
| 18 | 18 | const params = await searchParams; |
| 19 | 19 | const q = typeof params.q === "string" ? params.q.trim() : ""; |
| 20 | − return { title: q ? q : "Recherche" }; | |
| 20 | + return { | |
| 21 | + title: q ? q : "Recherche", | |
| 22 | + // Pages de résultats : jamais indexées (espace d'URL infini) — standard | |
| 23 | + // des moteurs de recherche, en plus du Disallow /search dans robots.txt. | |
| 24 | + robots: { index: false, follow: true }, | |
| 25 | + }; | |
| 21 | 26 | } |
| 22 | 27 | |
| 23 | 28 | export default function SearchPage() { |
added
apps/web/app/sitemap.ts
+36 −0
@@ -0,0 +1,36 @@ | ||
| 1 | +/** | |
| 2 | + * Trouve-KA — sitemap.xml (généré par Next App Router) | |
| 3 | + * Author: Simon-Pierre Boucher | |
| 4 | + * Contact: contact@spboucher.ai | |
| 5 | + * | |
| 6 | + * Pages publiques statiques seulement : les résultats de recherche (/search) | |
| 7 | + * sont volontairement exclus (noindex + Disallow dans robots.txt). | |
| 8 | + */ | |
| 9 | + | |
| 10 | +import type { MetadataRoute } from "next"; | |
| 11 | + | |
| 12 | +const SITE_URL = process.env.PUBLIC_URL ?? "https://www.trouve-ka.com"; | |
| 13 | + | |
| 14 | +export default function sitemap(): MetadataRoute.Sitemap { | |
| 15 | + const lastModified = new Date(); | |
| 16 | + | |
| 17 | + const routes: Array<{ | |
| 18 | + path: string; | |
| 19 | + priority: number; | |
| 20 | + changeFrequency: MetadataRoute.Sitemap[number]["changeFrequency"]; | |
| 21 | + }> = [ | |
| 22 | + { path: "/", priority: 1, changeFrequency: "daily" }, | |
| 23 | + { path: "/trouveka-bot", priority: 0.6, changeFrequency: "monthly" }, | |
| 24 | + { path: "/status", priority: 0.5, changeFrequency: "daily" }, | |
| 25 | + { path: "/stats", priority: 0.5, changeFrequency: "daily" }, | |
| 26 | + { path: "/soumettre", priority: 0.7, changeFrequency: "monthly" }, | |
| 27 | + { path: "/contact", priority: 0.5, changeFrequency: "yearly" }, | |
| 28 | + ]; | |
| 29 | + | |
| 30 | + return routes.map(({ path, priority, changeFrequency }) => ({ | |
| 31 | + url: `${SITE_URL}${path}`, | |
| 32 | + lastModified, | |
| 33 | + changeFrequency, | |
| 34 | + priority, | |
| 35 | + })); | |
| 36 | +} | |
| 37 | ||