SPB Git forge

spb/trouve-ka

Public ★ Pinned

Trouve-KA — moteur de recherche web indépendant, Québec-first. Crawler distribué, index OpenSearch, ranking bilingue, galerie d'images. En prod : www.trouve-ka.com

40commits 1branches 0releases
9.2 MBsize
maindefault branch
23 days agolast push
Python 51.9% TypeScript 32.7% JavaScript 4.8% CSS 4.1% Shell 2.9% HTML 1.8% SQL 1.5%

web : rebrand — le moteur de recherche du Groupe KA (filtre par site, badges par site, textes bot/soumettre)

Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent 3fe7794

12 changed files +169 −129

modified apps/web/app/contact/page.tsx +1 −1
@@ -25,7 +25,7 @@ export default function ContactPage() {
25 25 Nous <span className="hl">joindre</span>
26 26 </h1>
27 27 <p className="mt-4 max-w-xl text-[15px] text-ink-2">
28 − Trouve-KA — le moteur de recherche du web québécois — est un service de{" "}
28 + Trouve-KA — le moteur de recherche du Groupe KA — est un service de{" "}
29 29 <a
30 30 href={eco.hub.url}
31 31 target="_blank"
modified apps/web/app/layout.tsx +7 −7
@@ -41,11 +41,11 @@ const SITE_URL = process.env.PUBLIC_URL ?? "https://www.trouve-ka.com";
41 41 export const metadata: Metadata = {
42 42 metadataBase: new URL(SITE_URL),
43 43 title: {
44 − default: "Trouve-Ka — moteur de recherche du web québécois",
44 + default: "Trouve-Ka — le moteur de recherche du Groupe KA",
45 45 template: "%s | Trouve-Ka",
46 46 },
47 47 description:
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.",
48 + "Le moteur de recherche du Groupe KA — cherchez dans les 14 sites de l'écosystème : logements, propriétés, emplois, restos, événements, produits, véhicules, créateurs…",
49 49 alternates: {
50 50 canonical: "./",
51 51 },
@@ -54,9 +54,9 @@ export const metadata: Metadata = {
54 54 apple: "/apple-touch-icon.png",
55 55 },
56 56 openGraph: {
57 − title: "Trouve-Ka — moteur de recherche du web québécois",
57 + title: "Trouve-Ka — le moteur de recherche du Groupe KA",
58 58 description:
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.",
59 + "Le moteur de recherche du Groupe KA — cherchez dans les 14 sites de l'écosystème : logements, propriétés, emplois, restos, événements, produits, véhicules, créateurs…",
60 60 url: "https://www.trouve-ka.com",
61 61 siteName: "Trouve-Ka",
62 62 locale: "fr_CA",
@@ -86,7 +86,7 @@ export const viewport: Viewport = {
86 86 const NAV_LINKS = [
87 87 { href: "/status", label: "État du moteur" },
88 88 { href: "/stats", label: "Stats" },
89 − { href: "/soumettre", label: "Soumettre un site" },
89 + { href: "/soumettre", label: "Soumettre une page" },
90 90 { href: "/contact", label: "Contact" },
91 91 ] as const;
92 92
@@ -94,7 +94,7 @@ const LOCAL_FOOTER_LINKS = [
94 94 { href: "/trouveka-bot", label: "À propos du robot" },
95 95 { href: "/status", label: "État du moteur" },
96 96 { href: "/stats", label: "Statistiques" },
97 − { href: "/soumettre", label: "Soumettre un site" },
97 + { href: "/soumettre", label: "Soumettre une page" },
98 98 { href: "/contact", label: "Contact" },
99 99 ];
100 100
@@ -109,7 +109,7 @@ const JSON_LD = {
109 109 url: SITE_URL,
110 110 inLanguage: "fr-CA",
111 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.",
112 + "Le moteur de recherche du Groupe KA — cherchez dans les 14 sites de l'écosystème : logements, propriétés, emplois, restos, événements, produits, véhicules, créateurs…",
113 113 potentialAction: {
114 114 "@type": "SearchAction",
115 115 target: {
modified apps/web/app/page.tsx +13 −9
@@ -29,8 +29,12 @@ export default function HomePage() {
29 29 <Wordmark variant="hero" />
30 30 </h1>
31 31 <p className="serif-accent mt-4 text-[19px] text-ink-2 sm:text-[21px]">
32 − Le moteur de recherche <span className="text-ink">indépendant</span> du
33 − web québécois.
32 + Le moteur de recherche <span className="text-ink">du Groupe KA</span>.
33 + </p>
34 + <p className="mt-3 text-[14px] leading-relaxed text-ink-2 sm:text-[15px]">
35 + Cherchez dans les 14 sites de l&apos;écosystème&nbsp;: logements,
36 + propriétés, emplois, restos, événements, produits, véhicules,
37 + créateurs…
34 38 </p>
35 39
36 40 <div className="mt-9">
@@ -39,18 +43,18 @@ export default function HomePage() {
39 43 autoFocus
40 44 buttonPlacement="below"
41 45 placeholderSuggestions={[
42 − "plombier Gatineau",
43 − "subvention thermopompe",
44 − "cégep Sherbrooke",
45 − "marché public Trois-Rivières",
46 − "programme Hydro-Québec",
47 − "cabane à sucre Lanaudière",
46 + "4½ à louer Montréal",
47 + "maison à vendre Gatineau",
48 + "emploi infirmière Québec",
49 + "resto italien Trois-Rivières",
50 + "spectacle cette fin de semaine",
51 + "VUS d'occasion Sherbrooke",
48 52 ]}
49 53 />
50 54 </div>
51 55
52 56 <p className="gk-display mt-9 text-[17px] font-bold tracking-[-0.02em] text-ink">
53 − Cherche <span className="hl">le Québec</span>.
57 + Cherche <span className="hl">l&apos;écosystème KA</span>.
54 58 </p>
55 59
56 60 <div className="mt-9">
modified apps/web/app/search/search-content.tsx +9 −13
@@ -28,9 +28,8 @@ function buildSearchUrl(
28 28 ): string {
29 29 const params = new URLSearchParams({ q });
30 30 if (page > 1) params.set("page", String(page));
31 − if (filters.category) params.set("category", filters.category);
31 + if (filters.site) params.set("site", filters.site);
32 32 if (filters.language) params.set("language", filters.language);
33 − if (filters.quebecOnly) params.set("quebec_only", "true");
34 33 if (filters.images) params.set("images", "true");
35 34 if (filters.freshness) params.set("freshness", filters.freshness);
36 35 return `/search?${params.toString()}`;
@@ -84,9 +83,8 @@ export function SearchContent() {
84 83 const q = searchParams.get("q")?.trim() ?? "";
85 84 const page = Math.max(1, Number.parseInt(searchParams.get("page") ?? "1", 10) || 1);
86 85 const filters: SearchFilters = {
87 − category: searchParams.get("category") ?? undefined,
86 + site: searchParams.get("site") ?? undefined,
88 87 language: searchParams.get("language") ?? undefined,
89 − quebecOnly: searchParams.get("quebec_only") === "true",
90 88 images: searchParams.get("images") === "true",
91 89 freshness: searchParams.get("freshness") ?? undefined,
92 90 };
@@ -113,13 +111,10 @@ export function SearchContent() {
113 111 page: String(page),
114 112 limit: String(gallery ? GALLERY_PAGE_SIZE : PAGE_SIZE),
115 113 });
116 − const category = searchParams.get("category");
114 + const site = searchParams.get("site");
117 115 const language = searchParams.get("language");
118 − if (category) params.set("category", category);
116 + if (site) params.set("site", site);
119 117 if (language) params.set("language", language);
120 − if (searchParams.get("quebec_only") === "true") {
121 − params.set("quebec_only", "true");
122 − }
123 118 if (gallery) params.set("images", "true");
124 119 const freshness = searchParams.get("freshness");
125 120 if (freshness) params.set("freshness", freshness);
@@ -169,7 +164,8 @@ export function SearchContent() {
169 164 <main id="contenu" className="mx-auto max-w-3xl px-4 py-6">
170 165 {!q ? (
171 166 <p className="py-12 text-center text-ink-2">
172 − Entrez une requête pour chercher le web québécois.
167 + Entrez une requête pour chercher dans les 14 sites de
168 + l&apos;écosystème KA.
173 169 </p>
174 170 ) : loading ? (
175 171 imagesMode ? <GallerySkeleton /> : <ResultsSkeleton />
@@ -197,11 +193,11 @@ export function SearchContent() {
197 193 </h2>
198 194 <p className="mx-auto mt-5 max-w-md text-sm leading-relaxed text-ink-2">
199 195 Vérifiez l'orthographe, essayez des termes plus généraux ou
200 − retirez un filtre. Le robot crawle le Québec en continu&nbsp;: ce
201 − qui manque aujourd'hui peut apparaître demain.
196 + retirez un filtre. Le robot indexe les 14 sites du Groupe KA en
197 + continu&nbsp;: ce qui manque aujourd'hui peut apparaître demain.
202 198 </p>
203 199 <Link href="/soumettre" className="btn btn-primary mt-8">
204 − Soumettre un site québécois
200 + Soumettre une page KA
205 201 </Link>
206 202 </div>
207 203 ) : data ? (
modified apps/web/app/soumettre/page.tsx +9 −8
@@ -8,9 +8,9 @@ import type { Metadata } from "next";
8 8 import { SubmitForm } from "./submit-form";
9 9
10 10 export const metadata: Metadata = {
11 − title: "Soumettre un site",
11 + title: "Soumettre une page",
12 12 description:
13 − "Proposez un site québécois au robot d'indexation de Trouve-KA.",
13 + "Signalez une page d'un site du Groupe KA au robot d'indexation de Trouve-KA.",
14 14 };
15 15
16 16 export default function SubmitPage() {
@@ -18,17 +18,18 @@ export default function SubmitPage() {
18 18 <main id="contenu" className="mx-auto w-full max-w-xl flex-1 px-4 py-10">
19 19 <p className="kicker">Découverte</p>
20 20 <h1 className="mt-3 text-3xl font-bold tracking-[-0.03em] text-ink">
21 − Soumettre un site québécois
21 + Soumettre une page d&apos;un site KA
22 22 </h1>
23 23 <p className="mt-3 leading-relaxed text-ink-2">
24 − Vous connaissez un site québécois que Trouve-KA devrait indexer?
25 − Proposez-le ici : il sera ajouté à la file du robot.
24 + Une page d&apos;un des 14 sites du Groupe KA manque à l&apos;index, ou
25 + son contenu a changé? Signalez son adresse ici : elle sera ajoutée en
26 + priorité à la file du robot.
26 27 </p>
27 28 <SubmitForm />
28 29 <p className="mt-6 text-sm text-ink-2">
29 − Soumettre un site ne garantit pas son inclusion : le robot le visite,
30 − vérifie son fichier <code>robots.txt</code> et évalue sa pertinence
31 − québécoise avant de l'indexer.
30 + Trouve-KA n&apos;indexe que l&apos;écosystème KA : toute URL hors des
31 + 14 sites du Groupe KA est refusée. Le robot visite la page, vérifie le{" "}
32 + <code>robots.txt</code> du site et l&apos;indexe si elle est publique.
32 33 </p>
33 34 </main>
34 35 );
modified apps/web/app/soumettre/submit-form.tsx +4 −4
@@ -38,7 +38,7 @@ export function SubmitForm() {
38 38 setState({
39 39 kind: "error",
40 40 message:
41 − "Entrez une URL valide commençant par http:// ou https:// (ex. https://exemple.quebec).",
41 + "Entrez une URL valide commençant par http:// ou https:// (ex. https://www.lou-ka.com/…).",
42 42 });
43 43 return;
44 44 }
@@ -57,7 +57,7 @@ export function SubmitForm() {
57 57 kind: "success",
58 58 message:
59 59 data.message ||
60 − "Merci! Le site a été ajouté à la file du robot d'indexation.",
60 + "Merci! La page a été ajoutée à la file du robot d'indexation.",
61 61 });
62 62 setUrl("");
63 63 } else {
@@ -78,14 +78,14 @@ export function SubmitForm() {
78 78 return (
79 79 <form onSubmit={onSubmit} className="mt-6">
80 80 <label htmlFor="url-soumission" className="klabel block">
81 − Adresse du site (URL)
81 + Adresse de la page (URL d&apos;un site KA)
82 82 </label>
83 83 <div className="mt-2 flex flex-col gap-2 sm:flex-row">
84 84 <Input
85 85 id="url-soumission"
86 86 type="url"
87 87 inputMode="url"
88 − placeholder="https://exemple.quebec"
88 + placeholder="https://www.lou-ka.com/…"
89 89 value={url}
90 90 onChange={(event) => setUrl(event.target.value)}
91 91 required
modified apps/web/app/trouveka-bot/page.tsx +19 −17
@@ -9,7 +9,7 @@ import type { Metadata } from "next";
9 9 export const metadata: Metadata = {
10 10 title: "À propos de TrouveKABot",
11 11 description:
12 − "TrouveKABot est le robot d'indexation de Trouve-KA. Identité, comportement, respect de robots.txt et façon de le bloquer.",
12 + "TrouveKABot est le robot d'indexation de Trouve-KA. Il ne crawle que les 14 sites du Groupe KA, via leurs sitemaps. Identité, comportement et respect de robots.txt.",
13 13 };
14 14
15 15 const USER_AGENT =
@@ -30,23 +30,24 @@ export default function TrouvekaBotPage() {
30 30 </h2>
31 31 <p className="mt-2 leading-relaxed">
32 32 TrouveKABot est le robot d'indexation de{" "}
33 − <strong className="text-ink">Trouve-KA</strong>, un moteur de
34 − recherche indépendant consacré au web québécois. Il visite des
35 − pages web publiques pour les comprendre, les classer et les rendre
36 − trouvables par les gens du Québec.
33 + <strong className="text-ink">Trouve-KA</strong>, le moteur de
34 + recherche du Groupe KA. Il visite les pages publiques des sites de
35 + l'écosystème pour les comprendre, les classer et les rendre
36 + trouvables au même endroit.
37 37 </p>
38 38 </section>
39 39
40 − <section aria-labelledby="pourquoi">
41 − <h2 id="pourquoi" className="text-lg font-bold text-ink">
42 − Pourquoi crawle-t-il votre site?
40 + <section aria-labelledby="perimetre">
41 + <h2 id="perimetre" className="text-lg font-bold text-ink">
42 + Quels sites visite-t-il?
43 43 </h2>
44 44 <p className="mt-2 leading-relaxed">
45 − Votre site a probablement été découvert par un lien depuis un
46 − autre site québécois, un sitemap public ou une soumission
47 − d'utilisateur. TrouveKABot lit les pages accessibles publiquement
48 − afin de construire un index de recherche : il renvoie ensuite les
49 − internautes directement vers votre site — jamais l'inverse.
45 + TrouveKABot ne crawle <strong className="text-ink">que les 14
46 + sites du Groupe KA</strong> (Lou·Ka, Immo·Ka, Vrai-Prix, Toit·Ka,
47 + Valoplex, Auto·Ka, Fabri·Ka, Food·Ka, Resto·Ka, Sorti·Ka, Créa·Ka,
48 + Job·Ka, Trouve·Ka et le hub groupe-ka.com), découverts via leurs
49 + sitemaps. Il ne visite aucun site extérieur à l'écosystème : si
50 + vous voyez ce user-agent ailleurs, il s'agit d'une usurpation.
50 51 </p>
51 52 </section>
52 53
@@ -67,10 +68,11 @@ export default function TrouvekaBotPage() {
67 68 Respect de robots.txt
68 69 </h2>
69 70 <p className="mt-2 leading-relaxed">
70 − TrouveKABot respecte le standard <code>robots.txt</code> ainsi
71 − que les directives <code>noindex</code>, <code>nofollow</code>,{" "}
72 − <code>canonical</code> et l'en-tête <code>X-Robots-Tag</code>.
73 − Les règles sont mises en cache et revérifiées régulièrement.
71 + Même à l'intérieur de l'écosystème KA, TrouveKABot respecte le
72 + standard <code>robots.txt</code> ainsi que les directives{" "}
73 + <code>noindex</code>, <code>nofollow</code>, <code>canonical</code>{" "}
74 + et l'en-tête <code>X-Robots-Tag</code>. Les règles sont mises en
75 + cache et revérifiées régulièrement.
74 76 </p>
75 77 </section>
76 78
modified apps/web/components/filter-pills.tsx +50 −54
@@ -3,20 +3,23 @@
3 3 * Author: Simon-Pierre Boucher
4 4 * Contact: contact@spboucher.ai
5 5 *
6 − * Desktop : tout est visible. Mobile : une rangée défilante de catégories +
7 − * un bouton « Filtres (n) » qui replie langue / fraîcheur / Québec seulement.
6 + * Rangée 1 : les 14 sites du Groupe KA (« Tous | Lou·Ka | Immo·Ka | … »),
7 + * défilante — pilote le paramètre `site` de /api/search.
8 + * Rangée 2 : Images + (mobile) bouton « Filtres (n) » qui replie
9 + * langue / fraîcheur ; (desktop) tout est visible en ligne.
8 10 */
9 11
10 12 "use client";
11 13
12 14 import { useState } from "react";
15 +import { KA_SITES } from "@/lib/ka-sites";
13 16 import { cn } from "@/lib/utils";
14 17
15 18 /** Filtres actifs de la page de résultats. */
16 19 export interface SearchFilters {
17 − category?: string;
20 + /** Domaine d'un site KA (ex. « www.lou-ka.com ») — vide = tous. */
21 + site?: string;
18 22 language?: string;
19 − quebecOnly: boolean;
20 23 images?: boolean;
21 24 freshness?: string;
22 25 }
@@ -36,40 +39,6 @@ const FRESHNESS_OPTIONS = [
36 39 export function FilterPills({ filters, onChange }: FilterPillsProps) {
37 40 const [moreOpen, setMoreOpen] = useState(false);
38 41
39 − const allActive =
40 − !filters.category &&
41 − !filters.language &&
42 − !filters.quebecOnly &&
43 − !filters.images &&
44 − !filters.freshness;
45 −
46 − function toggleCategory(category: string): SearchFilters {
47 − return {
48 − ...filters,
49 − category: filters.category === category ? undefined : category,
50 − };
51 − }
52 −
53 − const primary = [
54 − { label: "Tout", active: allActive, next: { quebecOnly: false } as SearchFilters },
55 − {
56 − label: "Images",
57 − active: Boolean(filters.images),
58 − next: { ...filters, images: !filters.images },
59 − },
60 − { label: "Actualités", active: filters.category === "news", next: toggleCategory("news") },
61 − {
62 − label: "Gouvernement",
63 − active: filters.category === "government",
64 − next: toggleCategory("government"),
65 − },
66 − {
67 − label: "Éducation",
68 − active: filters.category === "education",
69 − next: toggleCategory("education"),
70 − },
71 − ];
72 −
73 42 const secondary = [
74 43 {
75 44 label: "Français",
@@ -81,11 +50,6 @@ export function FilterPills({ filters, onChange }: FilterPillsProps) {
81 50 active: filters.language === "en",
82 51 next: { ...filters, language: filters.language === "en" ? undefined : "en" } as SearchFilters,
83 52 },
84 − {
85 − label: "Québec seulement",
86 − active: filters.quebecOnly,
87 − next: { ...filters, quebecOnly: !filters.quebecOnly } as SearchFilters,
88 − },
89 53 ...FRESHNESS_OPTIONS.map(([value, label]) => ({
90 54 label,
91 55 active: filters.freshness === value,
@@ -100,23 +64,55 @@ export function FilterPills({ filters, onChange }: FilterPillsProps) {
100 64
101 65 return (
102 66 <div>
103 − {/* Rangée principale : défilante sur mobile, complète sur desktop */}
67 + {/* Rangée 1 : filtre par site du Groupe KA (défilante) */}
104 68 <div
105 69 role="group"
106 − aria-label="Filtres de recherche"
107 − className="flex items-center gap-2 overflow-x-auto pb-0.5 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden sm:flex-wrap sm:overflow-visible"
70 + aria-label="Filtrer par site du Groupe KA"
71 + className="flex items-center gap-2 overflow-x-auto pb-0.5 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
108 72 >
109 − {primary.map((pill) => (
73 + <button
74 + type="button"
75 + aria-pressed={!filters.site}
76 + onClick={() => onChange({ ...filters, site: undefined })}
77 + className={cn("pill shrink-0", !filters.site && "pill-active")}
78 + >
79 + Tous
80 + </button>
81 + {KA_SITES.map((site) => (
110 82 <button
111 − key={pill.label}
83 + key={site.domain}
112 84 type="button"
113 − aria-pressed={pill.active}
114 − onClick={() => onChange(pill.next)}
115 − className={cn("pill shrink-0", pill.active && "pill-active")}
85 + aria-pressed={filters.site === site.domain}
86 + onClick={() =>
87 + onChange({
88 + ...filters,
89 + site: filters.site === site.domain ? undefined : site.domain,
90 + })
91 + }
92 + className={cn(
93 + "pill shrink-0",
94 + filters.site === site.domain && "pill-active",
95 + )}
116 96 >
117 − {pill.label}
97 + {site.wordmark}
118 98 </button>
119 99 ))}
100 + </div>
101 +
102 + {/* Rangée 2 : images + langue / fraîcheur */}
103 + <div
104 + role="group"
105 + aria-label="Filtres de recherche"
106 + className="mt-2 flex items-center gap-2 overflow-x-auto pb-0.5 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden sm:flex-wrap sm:overflow-visible"
107 + >
108 + <button
109 + type="button"
110 + aria-pressed={Boolean(filters.images)}
111 + onClick={() => onChange({ ...filters, images: !filters.images })}
112 + className={cn("pill shrink-0", filters.images && "pill-active")}
113 + >
114 + Images
115 + </button>
120 116
121 117 {/* Mobile : bouton d'ouverture des filtres secondaires */}
122 118 <button
@@ -139,7 +135,7 @@ export function FilterPills({ filters, onChange }: FilterPillsProps) {
139 135 <span aria-hidden="true" className="mx-1 hidden h-5 w-px shrink-0 bg-line sm:block" />
140 136 <div
141 137 role="group"
142 − aria-label="Langue, portée et fraîcheur"
138 + aria-label="Langue et fraîcheur"
143 139 className="hidden gap-2 sm:flex sm:flex-wrap"
144 140 >
145 141 {secondary.map((pill) => (
@@ -161,7 +157,7 @@ export function FilterPills({ filters, onChange }: FilterPillsProps) {
161 157 <div
162 158 id="filtres-avances"
163 159 role="group"
164 − aria-label="Langue, portée et fraîcheur"
160 + aria-label="Langue et fraîcheur"
165 161 className="mt-2 flex flex-wrap gap-2 border-t border-line pt-2.5 sm:hidden"
166 162 >
167 163 {secondary.map((pill) => (
modified apps/web/components/result-item.tsx +7 −13
@@ -9,6 +9,7 @@ import { ResultThumbnail } from "@/components/result-thumbnail";
9 9 import { Snippet } from "@/components/snippet";
10 10 import { Badge } from "@/components/ui/badge";
11 11 import { formatDate } from "@/lib/format";
12 +import { siteWordmark } from "@/lib/ka-sites";
12 13 import type { SearchResult } from "@/lib/types";
13 14
14 15 function badgeVariant(
@@ -21,13 +22,6 @@ function badgeVariant(
21 22 return "default";
22 23 }
23 24
24 −/** Intensité québécoise de la pastille (◆ / ◆◆ / ◆◆◆), sobre et graduée. */
25 −function quebecMarks(score: number): string {
26 − if (score >= 0.85) return "◆◆◆";
27 − if (score >= 0.65) return "◆◆";
28 − return "◆";
29 −}
30 −
31 25 interface ResultItemProps {
32 26 result: SearchResult;
33 27 }
@@ -66,20 +60,20 @@ export function ResultItem({ result }: ResultItemProps) {
66 60 key={badge}
67 61 variant={badgeVariant(badge)}
68 62 title={
69 − badge === "Québec" && result.quebec_score >= 0.45
70 − ? `Score Québec ${Math.round(result.quebec_score * 100)} %`
63 + badge === "Québec"
64 + ? "Site de l'écosystème Groupe KA"
71 65 : undefined
72 66 }
73 67 className={index === 0 ? "-rotate-1 hover:rotate-0" : undefined}
74 68 >
75 69 {badge === "Québec" ? (
76 70 <span aria-hidden="true" className="text-[8px] leading-none">
77 − {result.quebec_score >= 0.45
78 − ? quebecMarks(result.quebec_score)
79 − : "◆"}
71 + ◆
80 72 </span>
81 73 ) : null}
82 − {badge}
74 + {/* Le badge « Québec » du backend devient le wordmark du site
75 + d'origine (dérivé du domaine ; domaine inconnu → domaine). */}
76 + {badge === "Québec" ? siteWordmark(result.domain) : badge}
83 77 </Badge>
84 78 ))}
85 79 {publishedAt ? (
modified apps/web/components/search-bar.tsx +2 −2
@@ -171,14 +171,14 @@ export function SearchBar({
171 171 const placeholder =
172 172 placeholderCount > 0
173 173 ? `Essaie « ${placeholderSuggestions![placeholderIndex % placeholderCount]} »…`
174 − : "Rechercher…";
174 + : "Chercher dans l'écosystème KA…";
175 175
176 176 const isLarge = size === "lg";
177 177
178 178 return (
179 179 <form role="search" onSubmit={onSubmit} className={cn("w-full", className)}>
180 180 <label htmlFor="champ-recherche" className="sr-only">
181 − Rechercher sur le web québécois
181 + Chercher dans les 14 sites du Groupe KA
182 182 </label>
183 183 <div ref={rootRef} className="relative">
184 184 <svg
modified apps/web/ka/ecosystem.json +1 −1
@@ -62,7 +62,7 @@
62 62 "accentSoft": "#e7f2fd",
63 63 "accentDeep": "#14508f",
64 64 "onAccent": "#ffffff",
65 − "tagline": "Le moteur de recherche du web québécois"
65 + "tagline": "Le moteur de recherche du Groupe KA"
66 66 },
67 67 {
68 68 "id": "lou-ka",
added apps/web/lib/ka-sites.ts +47 −0
@@ -0,0 +1,47 @@
1 +/**
2 + * Trouve-KA — les 14 sites indexés de l'écosystème Groupe KA
3 + * Author: Simon-Pierre Boucher
4 + * Contact: contact@spboucher.ai
5 + *
6 + * Source des pilules de filtre « par site » de la page de recherche et du
7 + * wordmark affiché sur chaque résultat (dérivé du champ `domain`).
8 + */
9 +
10 +export interface KaSite {
11 + /** Wordmark officiel (ex. « Lou·Ka »). */
12 + wordmark: string;
13 + /** Domaine canonique indexé (avec www). */
14 + domain: string;
15 +}
16 +
17 +export const KA_SITES: readonly KaSite[] = [
18 + { wordmark: "Groupe KA", domain: "www.groupe-ka.com" },
19 + { wordmark: "Trouve·Ka", domain: "www.trouve-ka.com" },
20 + { wordmark: "Lou·Ka", domain: "www.lou-ka.com" },
21 + { wordmark: "Immo·Ka", domain: "www.immo-ka.com" },
22 + { wordmark: "Vrai-Prix", domain: "www.vrai-prix.com" },
23 + { wordmark: "Toit·Ka", domain: "www.toit-ka.com" },
24 + { wordmark: "Valoplex", domain: "www.valoplex.com" },
25 + { wordmark: "Auto·Ka", domain: "www.auto-ka.com" },
26 + { wordmark: "Fabri·Ka", domain: "www.fabri-ka.com" },
27 + { wordmark: "Food·Ka", domain: "www.food-ka.com" },
28 + { wordmark: "Resto·Ka", domain: "www.resto-ka.com" },
29 + { wordmark: "Sorti·Ka", domain: "www.sorti-ka.com" },
30 + { wordmark: "Créa·Ka", domain: "www.crea-ka.com" },
31 + { wordmark: "Job·Ka", domain: "www.job-ka.com" },
32 +];
33 +
34 +/** Normalise un domaine pour la comparaison (retire le préfixe www.). */
35 +function bareDomain(domain: string): string {
36 + return domain.trim().toLowerCase().replace(/^www\./, "");
37 +}
38 +
39 +/**
40 + * Wordmark du site KA correspondant à un domaine (`www.lou-ka.com` ou
41 + * `lou-ka.com`). Domaine inconnu → le domaine tel quel.
42 + */
43 +export function siteWordmark(domain: string): string {
44 + const bare = bareDomain(domain);
45 + const site = KA_SITES.find((s) => bareDomain(s.domain) === bare);
46 + return site ? site.wordmark : domain;
47 +}
48