Repository Public
1// -----------------------------------------------------------------------------2// Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// SeoListing.tsx : pages programmatiques (/usagees/:marque, /region/:r, …).5// Le serveur (autoka/seo.py) rend le HTML complet et injecte6// window.__SEO_CTX__ (valeurs exactes des filtres + H1) ; ce composant le7// récupère au montage et rend <Home> avec les filtres imposés. En cas de8// navigation purement client (rare), il résout le slug via les facettes.9// -----------------------------------------------------------------------------10import { useEffect, useState } from "react";11import { useLocation, useParams } from "react-router-dom";12import { VehicleQuery, fetchFacets, slugify } from "../api";13import Home from "./Home";1415export type SeoDim =16 | "make" | "model" | "moto-make" | "scooter-make"17 | "region" | "ville" | "carrosserie";1819interface SeoCtx {20 path: string;21 kind: string;22 h1: string;23 intro?: string;24 filters: Partial<VehicleQuery>;25}2627declare global {28 interface Window { __SEO_CTX__?: SeoCtx }29}3031const DIM_KIND: Record<SeoDim, string> = {32 make: "auto", model: "auto", "moto-make": "moto", "scooter-make": "scooter",33 region: "auto", ville: "auto", carrosserie: "auto",34};3536/** Résolution client d'un slug vers la valeur exacte, via les facettes. */37async function resolveCtx(38 dim: SeoDim,39 params: Record<string, string | undefined>,40 path: string41): Promise<SeoCtx | null> {42 const kind = DIM_KIND[dim];43 const facets = await fetchFacets(undefined, kind);44 const bySlug = <T,>(list: T[], get: (x: T) => string, slug?: string) =>45 list.find((x) => slugify(get(x)) === slug);4647 if (dim === "make" || dim === "moto-make" || dim === "scooter-make") {48 const m = bySlug(facets.makes, (x) => x.make, params.makeSlug);49 if (!m) return null;50 const h1 =51 dim === "make"52 ? `${m.make} usagées à vendre au Québec`53 : dim === "moto-make"54 ? `Motos ${m.make} usagées à vendre au Québec`55 : `Scooters ${m.make} usagés à vendre au Québec`;56 return { path, kind, h1, filters: { make: m.make } };57 }58 if (dim === "model") {59 const m = bySlug(facets.makes, (x) => x.make, params.makeSlug);60 if (!m) return null;61 const withModels = await fetchFacets(m.make, kind);62 const mo = bySlug(withModels.models, (x) => x.model, params.modelSlug);63 if (!mo) return null;64 return {65 path, kind,66 h1: `${m.make} ${mo.model} usagés à vendre au Québec`,67 filters: { make: m.make, model: mo.model },68 };69 }70 if (dim === "region") {71 const r = bySlug(facets.regions, (x) => x.region, params.slug);72 if (!r) return null;73 return {74 path, kind,75 h1: `Voitures usagées à vendre — ${r.region}`,76 filters: { region: r.region },77 };78 }79 if (dim === "carrosserie") {80 const b = bySlug(facets.body_types, (x) => x, params.slug);81 if (!b) return null;82 return {83 path, kind,84 h1: `${b} usagés à vendre au Québec`,85 filters: { body_type: b },86 };87 }88 // ville : pas de facette ville — meilleure approximation côté client89 const cityText = (params.slug || "").replace(/-/g, " ");90 return {91 path, kind,92 h1: `Voitures usagées à vendre à ${cityText}`,93 filters: { city: cityText },94 };95}9697export default function SeoListing({ dim }: { dim: SeoDim }) {98 const loc = useLocation();99 const params = useParams();100 const injected =101 typeof window !== "undefined" &&102 window.__SEO_CTX__ &&103 window.__SEO_CTX__.path === loc.pathname104 ? window.__SEO_CTX__105 : null;106 const [ctx, setCtx] = useState<SeoCtx | null>(injected);107 const [failed, setFailed] = useState(false);108109 useEffect(() => {110 if (ctx && ctx.path === loc.pathname) return;111 setCtx(null);112 setFailed(false);113 resolveCtx(dim, params as Record<string, string | undefined>, loc.pathname)114 .then((c) => (c ? setCtx(c) : setFailed(true)))115 .catch(() => setFailed(true));116 }, [loc.pathname]);117118 if (failed)119 return (120 <div className="notice container">121 <div className="big">🧭</div>122 <h2>Page introuvable</h2>123 <p>Le lien demandé n'existe pas.</p>124 </div>125 );126127 if (!ctx)128 return (129 <div className="container">130 <div className="skeleton" style={{ height: 220, marginTop: 24 }} />131 </div>132 );133134 return (135 <Home136 kind={ctx.kind}137 fixed={ctx.filters}138 seoH1={ctx.h1}139 seoIntro={ctx.intro}140 />141 );142}143