SPB Git

Repository Public

SeoListing.tsx 4.5 KB 143 lines tsx

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
Files main
Menu

Theme