// ----------------------------------------------------------------------------- // Rent-Ka — Rental listings aggregator (Canada, outside Québec) // Author: Simon-Pierre Boucher — contact@spboucher.ai // pages/Ville.tsx: programmatic SEO pages — /cities, /city/:ville[/:type] // The server renders the same content as HTML (rentka/seo.py); here, the // interactive version with the app's listing cards. // ----------------------------------------------------------------------------- import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { Listing } from "../api"; import ListingCard from "../components/ListingCard"; interface VilleRow { city: string; slug: string; n: number; avg_price: number | null; } interface VilleData { city: string; slug: string; unit_type: string | null; n: number; avg: number | null; med: number | null; types: { unit_type: string; slug: string; n: number }[]; listings: Listing[]; neighbors: VilleRow[]; } const fmt = (p: number | null) => p == null ? "—" : `$${Math.round(p).toLocaleString("en-CA")}`; async function get(path: string): Promise { const res = await fetch(path); if (!res.ok) throw new Error(`${res.status}`); return res.json(); } export function VillesPage() { const [villes, setVilles] = useState(null); const [error, setError] = useState(null); useEffect(() => { get<{ villes: VilleRow[] }>("/api/seo/villes") .then((r) => setVilles(r.villes)) .catch((e) => setError(String(e))); }, []); return (
Directory — rentals by city

Rentals by city

Every Canadian city where Rent-Ka tracks rentals, with the number of active listings and the average rent.

{error &&
⚠️ {error}
} {!villes && !error &&
Loading…
} {villes && (
    {villes.map((v) => (
  • {v.city} {" — "} {v.n} listing{v.n > 1 ? "s" : ""} {v.avg_price != null && <> · average rent {fmt(v.avg_price)}}
  • ))}
)}
); } export default function VillePage() { const { ville = "", type } = useParams(); const [data, setData] = useState(null); const [error, setError] = useState(null); useEffect(() => { setData(null); setError(null); const qs = type ? `?type=${encodeURIComponent(type)}` : ""; get(`/api/seo/ville/${encodeURIComponent(ville)}${qs}`) .then(setData) .catch((e) => setError(e.message === "404" ? "Unknown city or type." : String(e))); window.scrollTo(0, 0); }, [ville, type]); if (error) return (

Page not found

⚠️ {error} See all cities
); if (!data) return (
Loading…
); const what = data.unit_type ? `${data.unit_type} rentals` : "Rentals"; return (
Cities — {data.city}

{what} in {data.city}

{data.n} active listing{data.n > 1 ? "s" : ""} {data.avg != null && <> · average rent {fmt(data.avg)}} {data.med != null && <> · median {fmt(data.med)}}

{!data.unit_type && data.types.length > 1 && (

{data.types.map((t) => ( {t.unit_type} ({t.n}) ))}

)} {data.unit_type && (

← All rentals in {data.city}

)}
{data.listings.map((l) => ( ))}
{data.neighbors.length > 0 && ( <>

Other cities

{data.neighbors.map((v) => ( {v.city} ({v.n}) ))}

)}
); }