' + seo_div, 1)
return HTMLResponse(page, status_code=status,
headers={"Cache-Control": "no-cache"})
def _e(t) -> str:
return html.escape(str(t or ""))
def _fmt_price(p) -> str:
return f"${int(round(p)):,}" if p else ""
def _price_ok(p) -> bool:
return p is not None and PRICE_MIN <= p <= PRICE_MAX
def _iso(ts) -> str:
if not ts:
return date.today().isoformat()
return datetime.fromtimestamp(ts, tz=timezone.utc).date().isoformat()
def _listing_li(r) -> str:
"""One listing in a server-rendered HTML list."""
label = r["title"] or r["address"] or r["uid"]
bits = [b for b in (r["unit_type"], _fmt_price(r["price"]) + "/month" if _price_ok(r["price"]) else "",
r["sector"] or r["city"]) if b]
return (f'
{_e(label)}'
f'{" — " + _e(" · ".join(bits)) if bits else ""}')
def _not_found(message: str, path: str) -> HTMLResponse:
"""HTML 404: the React shell is served (the SPA shows its page), but the
status code and server content clearly say «not found» to bots."""
return _render(title="Page not found | Rent-Ka",
description="This page does not exist on Rent-Ka.",
path=path,
body=f"
{_e(message)}
"
'
Browse all rentals across Canada · '
'Rentals by city
',
status=404)
def _breadcrumb(items: list[tuple[str, str]]) -> dict:
return {"@context": "https://schema.org", "@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": i + 1, "name": name,
"item": BASE_URL + path}
for i, (name, path) in enumerate(items)]}
# --- Data ----------------------------------------------------------------------
def _city_stats(con, city: str) -> dict:
n = con.execute("SELECT COUNT(*) c FROM listings WHERE active=1 AND city=?",
(city,)).fetchone()["c"]
prices = [r["price"] for r in con.execute(
f"SELECT price FROM listings WHERE active=1 AND city=? AND {PRICE_OK}", (city,))]
types = [dict(r) for r in con.execute(
"""SELECT unit_type, COUNT(*) n FROM listings
WHERE active=1 AND city=? AND unit_type<>''
GROUP BY unit_type ORDER BY n DESC""", (city,))]
for t in types:
t["slug"] = slugify(t["unit_type"])
return {"n": n,
"avg": round(statistics.mean(prices)) if prices else None,
"med": round(statistics.median(prices)) if prices else None,
"types": types}
def _cities_rows(con, minimum: int = 1) -> list[dict]:
rows = [dict(r) for r in con.execute(
f"""SELECT city, province, COUNT(*) n,
AVG(CASE WHEN {PRICE_OK} THEN price END) avg_price,
MAX(updated_at) last
FROM listings WHERE active=1 AND city<>''
GROUP BY city HAVING n>=? ORDER BY n DESC""", (minimum,))]
for r in rows:
r["slug"] = slugify(r["city"])
r["avg_price"] = round(r["avg_price"]) if r["avg_price"] else None
return rows
def _parse_row(r) -> dict:
d = dict(r)
for k in ("amenities", "images"):
d[k] = json.loads(d.get(k) or "[]")
d["details"] = json.loads(d.get("details") or "{}")
return d
# --- JSON API for the frontend city pages ---------------------------------------
@router.get("/api/seo/cities")
def api_cities():
con = db.connect()
rows = _cities_rows(con)
con.close()
return {"cities": rows}
@router.get("/api/seo/city/{slug}")
def api_city(slug: str, type: str | None = None):
cities, types = _slug_maps()
city = cities.get(slug)
if not city:
raise HTTPException(404, "Unknown city")
unit_type = None
if type:
unit_type = types.get(type)
if not unit_type:
raise HTTPException(404, "Unknown unit type")
con = db.connect()
stats = _city_stats(con, city)
sql = "SELECT * FROM listings WHERE active=1 AND city=?"
args: list = [city]
if unit_type:
sql += " AND unit_type=?"
args.append(unit_type)
listings = [_parse_row(r) for r in con.execute(
sql + " ORDER BY updated_at DESC LIMIT 100", args)]
neighbors = [v for v in _cities_rows(con, MIN_LISTINGS_PAGE) if v["city"] != city][:12]
con.close()
return {"city": city, "slug": slug, "unit_type": unit_type,
**stats, "listings": listings, "neighbors": neighbors}
# --- robots.txt & sitemaps -------------------------------------------------------
@router.get("/robots.txt", include_in_schema=False)
def robots() -> PlainTextResponse:
return PlainTextResponse(
"User-agent: *\n"
"Allow: /\n"
"Disallow: /api/\n"
"Disallow: /uploads/\n"
"Disallow: /profile\n"
"Disallow: /favorites\n"
"Disallow: /manage\n"
"Disallow: /welcome\n"
"Disallow: /bot\n"
"Disallow: /gateway/\n"
f"\nSitemap: {BASE_URL}/sitemap.xml\n")
def _xml(content: str) -> Response:
return Response('\n' + content,
media_type="application/xml",
headers={"Cache-Control": "public, max-age=3600"})
def _urlset(urls: list[tuple[str, str | None]]) -> Response:
rows = []
for loc, lastmod in urls:
lm = f"
{lastmod}" if lastmod else ""
rows.append(f"
{xml_escape(loc)}{lm}")
return _xml('
\n'
+ "\n".join(rows) + "\n")
@router.get("/sitemap.xml", include_in_schema=False)
def sitemap_index():
con = db.connect()
total = con.execute("SELECT COUNT(*) c FROM listings WHERE active=1").fetchone()["c"]
con.close()
chunks = max(1, math.ceil(total / SITEMAP_CHUNK))
names = (["sitemap-pages.xml", "sitemap-cities.xml"]
+ [f"sitemap-listings-{i}.xml" for i in range(1, chunks + 1)])
today = date.today().isoformat()
rows = "\n".join(
f"
{BASE_URL}/{n}{today}"
for n in names)
return _xml('
\n'
+ rows + "\n")
@router.get("/sitemap-pages.xml", include_in_schema=False)
def sitemap_pages():
urls: list[tuple[str, str | None]] = [
(f"{BASE_URL}/", None), (f"{BASE_URL}/cities", None),
(f"{BASE_URL}/stats", None), (f"{BASE_URL}/sources", None),
(f"{BASE_URL}/privacy", None), (f"{BASE_URL}/terms", None)]
con = db.connect()
for r in con.execute(
"""SELECT source, MAX(updated_at) last FROM listings
WHERE active=1 GROUP BY source"""):
urls.append((f"{BASE_URL}/g/{r['source']}", _iso(r["last"])))
con.close()
return _urlset(urls)
@router.get("/sitemap-cities.xml", include_in_schema=False)
def sitemap_cities():
con = db.connect()
urls: list[tuple[str, str | None]] = []
for v in _cities_rows(con, MIN_LISTINGS_PAGE):
urls.append((f"{BASE_URL}/city/{v['slug']}", _iso(v["last"])))
for r in con.execute(
f"""SELECT city, unit_type, COUNT(*) n, MAX(updated_at) last
FROM listings WHERE active=1 AND city<>'' AND unit_type<>''
GROUP BY city, unit_type HAVING n>=?""", (MIN_LISTINGS_PAGE,)):
urls.append((f"{BASE_URL}/city/{slugify(r['city'])}/{slugify(r['unit_type'])}",
_iso(r["last"])))
con.close()
return _urlset(urls)
@router.get("/sitemap-listings-{num}.xml", include_in_schema=False)
def sitemap_listings(num: int):
if num < 1:
raise HTTPException(404)
con = db.connect()
rows = con.execute(
"""SELECT uid, updated_at FROM listings WHERE active=1
ORDER BY uid LIMIT ? OFFSET ?""",
(SITEMAP_CHUNK, (num - 1) * SITEMAP_CHUNK)).fetchall()
con.close()
if not rows:
raise HTTPException(404)
return _urlset([(f"{BASE_URL}/listing/{r['uid']}", _iso(r["updated_at"]))
for r in rows])
# --- SSR pages -------------------------------------------------------------------
@router.get("/", include_in_schema=False)
def home_ssr():
con = db.connect()
total = con.execute("SELECT COUNT(*) c FROM listings WHERE active=1").fetchone()["c"]
nsources = con.execute(
"SELECT COUNT(DISTINCT source) c FROM listings WHERE active=1").fetchone()["c"]
cities = _cities_rows(con, MIN_LISTINGS_PAGE)
provinces = [dict(r) for r in con.execute(
"""SELECT province, COUNT(*) n FROM listings
WHERE active=1 AND published=1 AND province<>''
GROUP BY province ORDER BY n DESC""")]
recents = con.execute(
f"""SELECT uid, title, address, sector, city, unit_type, price FROM listings
WHERE active=1 AND {PRICE_OK} ORDER BY first_seen DESC LIMIT 20""").fetchall()
con.close()
title = f"Rent-Ka — A Groupe KA service · {total:,} rentals across Canada"
description = (f"{total:,} apartments and homes for rent across Canada "
f"(outside Québec), aggregated from {nsources} property "
"managers and always up to date: Toronto, Ottawa, Vancouver, "
"Calgary, Halifax and more. Photos, prices, availability and "
"a direct link to the original listing.")
top = cities[:30]
body = (
f"
Apartments for rent across Canada — {total:,} live listings
"
+ f"
Rent-Ka aggregates the rentals published by {nsources} property "
"managers across Canada (outside Québec): every listing with its "
"photos, price, availability and a direct link to the manager's "
"website.
"
+ "
Rentals by province
"
+ "".join(f"- {_e(PROVINCE_NAMES.get(p['province'], p['province']))}"
f" — {p['n']} listings
" for p in provinces)
+ "
Rentals by city
All cities ({len(cities)})
'
+ "
Latest listings
"
+ "".join(_listing_li(r) for r in recents)
+ "
")
jsonld = [
{"@context": "https://schema.org", "@type": "WebSite",
"name": SITE_NAME, "url": BASE_URL + "/",
"description": description, "inLanguage": "en-CA"},
{"@context": "https://schema.org", "@type": "Organization",
"name": "Rent-Ka (Groupe KA)", "url": BASE_URL + "/"}]
return _render(title=title, description=description, path="/",
jsonld=jsonld, body=body)
@router.get("/cities", include_in_schema=False)
def cities_ssr():
con = db.connect()
cities = _cities_rows(con)
con.close()
total = sum(v["n"] for v in cities)
title = f"Rentals by city across Canada ({len(cities)} cities) | Rent-Ka"
description = (f"Every Canadian city where Rent-Ka tracks rentals: "
f"{total:,} listings in {len(cities)} cities, with the "
"average rent and the number of available apartments per city.")
body = (f"
Rentals by city — {len(cities)} cities across Canada
"
+ "".join(f'- {_e(v["city"])}'
+ (f" ({_e(v['province'])})" if v.get("province") else "")
+ f' — {v["n"]} listings'
+ (f", average rent {_fmt_price(v['avg_price'])}" if v["avg_price"] else "")
+ "
" for v in cities)
+ "
")
jsonld = [_breadcrumb([("Home", "/"), ("Cities", "/cities")]),
{"@context": "https://schema.org", "@type": "ItemList",
"name": "Rentals by city across Canada",
"numberOfItems": len(cities),
"itemListElement": [
{"@type": "ListItem", "position": i + 1,
"name": f"Apartments for rent in {v['city']}",
"url": f"{BASE_URL}/city/{v['slug']}"}
for i, v in enumerate(cities[:100])]}]
return _render(title=title, description=description, path="/cities",
jsonld=jsonld, body=body)
def _city_ssr(slug: str, type_slug: str | None = None):
cities, types = _slug_maps()
path = f"/city/{slug}" + (f"/{type_slug}" if type_slug else "")
city = cities.get(slug)
if not city:
return _not_found("No rentals tracked for this city", path)
unit_type = None
if type_slug is not None:
unit_type = types.get(type_slug)
if not unit_type:
return _not_found("Unknown unit type", path)
con = db.connect()
stats = _city_stats(con, city)
sql = "SELECT * FROM listings WHERE active=1 AND city=?"
args: list = [city]
if unit_type:
sql += " AND unit_type=?"
args.append(unit_type)
n = con.execute(f"SELECT COUNT(*) c FROM ({sql})", args).fetchone()["c"]
prices = [r["price"] for r in con.execute(
f"SELECT price FROM listings WHERE active=1 AND city=? AND unit_type=? AND {PRICE_OK}",
(city, unit_type))]
avg = round(statistics.mean(prices)) if prices else None
med = round(statistics.median(prices)) if prices else None
else:
n, avg, med = stats["n"], stats["avg"], stats["med"]
rows = con.execute(sql + " ORDER BY updated_at DESC LIMIT 100", args).fetchall()
neighbors = [v for v in _cities_rows(con, MIN_LISTINGS_PAGE) if v["city"] != city][:12]
con.close()
if n == 0:
return _not_found(f"No active listing in {city} for this type", path)
what = f"{unit_type} rentals" if unit_type else "Apartments for rent"
title = f"{what} in {city} — {n} listings | Rent-Ka"
desc_stats = (f"average rent {_fmt_price(avg)}, median {_fmt_price(med)}"
if avg and med else "")
description = (f"{n} {what.lower()} in {city}"
+ (f" ({desc_stats})" if desc_stats else "")
+ ". Up-to-date listings from property managers, with photos, "
"prices, availability and a direct link to the original listing.")
body = [f"
{_e(what)} in {_e(city)} — {n} listings
"]
if avg and med:
body.append(f"
Average rent: {_fmt_price(avg)}/month · "
f"median rent: {_fmt_price(med)}/month.
")
if not unit_type and stats["types"]:
body.append("
By unit type
")
body.append("
Listings
"
+ "".join(_listing_li(r) for r in rows) + "
")
if unit_type:
body.append(f'
All rentals in {_e(city)}
')
body.append("
Other cities
")
crumbs = [("Home", "/"), ("Cities", "/cities"), (city, f"/city/{slug}")]
if unit_type:
crumbs.append((f"{unit_type} in {city}", path))
jsonld = [_breadcrumb(crumbs),
{"@context": "https://schema.org", "@type": "ItemList",
"name": f"{what} in {city}", "numberOfItems": n,
"itemListElement": [
{"@type": "ListItem", "position": i + 1,
"name": r["title"] or r["address"] or r["uid"],
"url": f"{BASE_URL}/listing/{r['uid']}"}
for i, r in enumerate(rows[:50])]}]
return _render(title=title, description=description, path=path,
jsonld=jsonld, body="".join(body))
@router.get("/city/{slug}", include_in_schema=False)
def city_ssr(slug: str):
return _city_ssr(slug)
@router.get("/city/{slug}/{type_slug}", include_in_schema=False)
def city_type_ssr(slug: str, type_slug: str):
return _city_ssr(slug, type_slug)
@router.get("/listing/{uid:path}", include_in_schema=False)
def listing_ssr(uid: str):
con = db.connect()
row = con.execute("SELECT * FROM listings WHERE uid=?", (uid,)).fetchone()
con.close()
if row is None:
return _render(title="Listing not found | Rent-Ka",
description="This listing does not or no longer exists on Rent-Ka.",
path=f"/listing/{uid}",
body="
Listing not found
"
'
Browse all rentals across Canada
',
status=404)
d = _parse_row(row)
city_slug = slugify(d["city"]) if d["city"] else ""
if not d["active"]:
# listing removed at the source: 410 Gone + link to the parent city
link = (f'
Apartments for rent in {_e(d["city"])}'
if city_slug else '
All rentals')
return _render(
title="Listing removed | Rent-Ka",
description="This listing was removed by the property manager.",
path=f"/listing/{uid}",
body=f"
This listing is no longer available
"
f"
It was removed by the manager. {link}.
",
status=410)
label = d["title"] or d["address"] or "Rental unit"
where = d["city"] if d["city"] and d["city"] not in label else ""
title = (f"{d['unit_type'] + ' for rent — ' if d['unit_type'] else ''}{label}"
+ (f", {where}" if where else "") + " | Rent-Ka")
price_txt = f"{_fmt_price(d['price'])}/month" if _price_ok(d["price"]) else (d["price_label"] or "")
bits = [b for b in (d["unit_type"], price_txt, d["sector"], d["city"],
d["availability"]) if b]
description = (" · ".join(bits) + ". " if bits else "") + \
(d["description"][:150].strip() + "…" if len(d["description"] or "") > 150
else (d["description"] or "")).strip()
description = description[:300] or f"Rental listing in {d['city']} on Rent-Ka."
body = [f"
{_e(label)}{' — ' + _e(d['unit_type']) if d['unit_type'] else ''}
"]
facts = [("Address", d["address"]), ("City", d["city"]),
("Neighbourhood", d["sector"]),
("Type", d["unit_type"]), ("Rent", price_txt),
("Availability", d["availability"]),
("Area", f"{int(d['area_sqft'])} sq ft" if d["area_sqft"] else "")]
body.append("
" + "".join(f"- {k}: {_e(v)}
"
for k, v in facts if v) + "
")
if d["description"]:
body.append(f"
{_e(d['description'][:600])}
")
if d["amenities"]:
body.append("
Amenities: "
+ _e(", ".join(d["amenities"][:15])) + "
")
if d["url"]:
body.append(f'
'
"See the original listing at the manager's site
")
if city_slug:
body.append(f'
Other rentals in '
f'{_e(d["city"])}
')
beds = None
m = re.match(r"(\d+)", d["unit_type"] or "")
if m:
beds = int(m.group(1))
apartment: dict = {
"@type": "Apartment", "name": label,
"address": {"@type": "PostalAddress",
"streetAddress": d["address"] or None,
"addressLocality": d["city"] or None,
"addressRegion": d.get("province") or "ON",
"addressCountry": "CA"}}
if d["lat"] and d["lng"]:
apartment["geo"] = {"@type": "GeoCoordinates",
"latitude": d["lat"], "longitude": d["lng"]}
if beds:
apartment["numberOfBedrooms"] = beds
if d["area_sqft"]:
apartment["floorSize"] = {"@type": "QuantitativeValue",
"value": d["area_sqft"], "unitCode": "FTK"}
if d["images"]:
apartment["photo"] = d["images"][:5]
listing_ld: dict = {
"@context": "https://schema.org", "@type": "RealEstateListing",
"name": title.removesuffix(" | Rent-Ka"),
"url": f"{BASE_URL}/listing/{uid}",
"datePosted": _iso(d["first_seen"]), "inLanguage": "en-CA",
"about": apartment}
if _price_ok(d["price"]):
listing_ld["offers"] = {
"@type": "Offer", "price": d["price"], "priceCurrency": "CAD",
"availability": "https://schema.org/InStock",
"businessFunction": "http://purl.org/goodrelations/v1#LeaseOut"}
crumbs = [("Home", "/")]
if city_slug:
crumbs.append((d["city"], f"/city/{city_slug}"))
crumbs.append((label, f"/listing/{uid}"))
return _render(title=title, description=description, path=f"/listing/{uid}",
jsonld=[listing_ld, _breadcrumb(crumbs)], body="".join(body),
og_image=d["images"][0] if d["images"] else None)
@router.get("/g/{source_id}", include_in_schema=False)
def manager_ssr(source_id: str):
registry = {s["id"]: s for s in
json.loads(SOURCES_PATH.read_text(encoding="utf-8"))["sources"]}
src = registry.get(source_id)
if not src:
return _not_found("Unknown manager", f"/g/{source_id}")
con = db.connect()
n = con.execute("SELECT COUNT(*) c FROM listings WHERE active=1 AND source=?",
(source_id,)).fetchone()["c"]
rows = con.execute(
"""SELECT uid, title, address, sector, city, unit_type, price FROM listings
WHERE active=1 AND source=? ORDER BY updated_at DESC LIMIT 60""",
(source_id,)).fetchall()
con.close()
name = src["name"]
title = f"{name} — {n} rentals | Rent-Ka"
description = (f"The {n} rentals from {name}"
+ (f" ({src['region']})" if src.get("region") else "")
+ " tracked by Rent-Ka, with prices, photos and a direct "
"link to the original listing.")
body = (f"
{_e(name)} — {n} rentals
"
+ (f"
Areas: {_e(src['sectors'])}.
" if src.get("sectors") else "")
+ "
" + "".join(_listing_li(r) for r in rows) + "
"
+ '
All property managers
')
jsonld = [_breadcrumb([("Home", "/"), ("Sources", "/sources"),
(name, f"/g/{source_id}")]),
{"@context": "https://schema.org", "@type": "Organization",
"name": name, "url": src.get("url") or f"{BASE_URL}/g/{source_id}"}]
return _render(title=title, description=description, path=f"/g/{source_id}",
jsonld=jsonld, body=body)
# static app pages: unique head, content rendered by React
_STATIC_META = {
"/stats": ("Canadian rental market statistics | Rent-Ka",
"Average and median rents, breakdowns by city and unit type: "
"the Canadian rental market statistics, computed continuously "
"by Rent-Ka."),
"/sources": ("Aggregated sources — property managers and portals | Rent-Ka",
"Every source aggregated by Rent-Ka: Canadian property "
"managers and rental portals, with each one's number of "
"active listings."),
"/privacy": ("Privacy policy | Rent-Ka",
"Rent-Ka's privacy policy: collected data, cookies and "
"user rights."),
"/terms": ("Terms of use | Rent-Ka",
"Terms of use of the Rent-Ka service, an independent rental "
"listings aggregator covering Canada outside Québec."),
}
def _static_page(path: str):
title, description = _STATIC_META[path]
return _render(title=title, description=description, path=path)
@router.get("/stats", include_in_schema=False)
def stats_page():
return _static_page("/stats")
@router.get("/sources", include_in_schema=False)
def sources_page():
return _static_page("/sources")
@router.get("/privacy", include_in_schema=False)
def privacy_page():
return _static_page("/privacy")
@router.get("/terms", include_in_schema=False)
def terms_page():
return _static_page("/terms")