# -----------------------------------------------------------------------------
# House-Ka — Homes-for-sale aggregator (Canada outside Québec, Ontario first)
# Author: Simon-Pierre Boucher — contact@spboucher.ai
# seo.py : server-side HTML rendering for search engines.
#
# The React SPA is untouched: this module pre-fills the initial HTML served by
# the web.py catch-all — unique
/meta/canonical/og:, schema.org JSON-LD,
# content and internal links inside
(replaced by React on
# mount). Also generates robots.txt and the sitemaps.
#
# Pages served:
# / enriched home (stats + city/type links)
# /property/{uid}[/{slug}] listing page (301 to the canonical slug,
# 410 if withdrawn, 404 if unknown)
# /for-sale/{city}[/{type}] programmatic city pages (+ type)
# /type/{type} per-property-type page (Canada-wide)
# /stats /agencies /terms /privacy /account /rates /contact dedicated meta
# /robots.txt /sitemap.xml /sitemaps/*.xml
# -----------------------------------------------------------------------------
from __future__ import annotations
import html
import json
import os
import re
import time
import unicodedata
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import quote
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse, Response
from . import db
ROOT = Path(__file__).resolve().parent.parent
FRONTEND_DIST = ROOT / "frontend" / "dist"
FRONTEND_DIR = FRONTEND_DIST if FRONTEND_DIST.exists() else ROOT / "frontend"
BASE_URL = os.environ.get("IMMOKA_BASE_URL", "https://www.house-ka.com").rstrip("/")
SITE_NAME = "House-Ka"
# same visibility rule as /api/listings (DDF dedup + displayable price)
VISIBLE = "active=1 AND dup_hidden=0 AND published=1"
MIN_LISTINGS = 3 # quality floor: no near-empty city/type pages
PAGE_SIZE = 48 # listings per page on programmatic pages
# province name (region column) -> two-letter code for schema.org
_PROVINCE_CODE = {
"ontario": "ON", "british columbia": "BC", "alberta": "AB",
"saskatchewan": "SK", "manitoba": "MB", "new brunswick": "NB",
"nova scotia": "NS", "prince edward island": "PE",
"newfoundland and labrador": "NL", "yukon": "YT",
"northwest territories": "NT", "nunavut": "NU", "quebec": "QC",
}
# -----------------------------------------------------------------------------
# Utilities
# -----------------------------------------------------------------------------
def slugify(s: str) -> str:
"""URL slug — SAME algorithm as slugify() in frontend/src/api.ts."""
s = unicodedata.normalize("NFKD", s or "").encode("ascii", "ignore").decode()
s = re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")
return s[:80].strip("-")
def listing_slug(row) -> str:
"""Listing slug — SAME logic as listingPath() in frontend/src/api.ts."""
base = slugify(row["address"] or row["title"] or "")
city = slugify(row["city"] or "")
if city and city not in base:
base = slugify(f"{base} {city}") if base else city
return base
def _esc(s) -> str:
return html.escape(str(s or ""), quote=True)
def _fmt_n(n) -> str:
return f"{int(n):,}"
def _fmt_price(p) -> str:
return f"${_fmt_n(round(p))}" if p is not None else "Price on request"
def _iso_date(ts) -> str:
try:
return datetime.fromtimestamp(float(ts), tz=timezone.utc).strftime("%Y-%m-%d")
except (TypeError, ValueError):
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
# small in-memory TTL cache (data changes at watch pace, ~1 h)
_cache: dict[str, tuple[float, object]] = {}
def _cached(key: str, ttl: float, build):
now = time.time()
hit = _cache.get(key)
if hit and now - hit[0] < ttl:
return hit[1]
val = build()
_cache[key] = (now, val)
return val
# -----------------------------------------------------------------------------
# Slug registries (cities, types) — rebuilt every 15 min
# -----------------------------------------------------------------------------
def _build_registry() -> dict:
con = db.connect()
cities: dict[str, dict] = {}
for r in con.execute(
f"SELECT city, COUNT(*) n FROM listings WHERE {VISIBLE}"
" AND city<>'' GROUP BY city"):
slug = slugify(r["city"])
if len(slug) < 2:
continue
e = cities.setdefault(slug, {"label": r["city"], "n": 0, "values": [], "best": 0})
e["n"] += r["n"]
e["values"].append(r["city"])
if r["n"] > e["best"]:
e["best"] = r["n"]; e["label"] = r["city"]
types: dict[str, dict] = {}
for r in con.execute(
f"SELECT property_type, COUNT(*) n FROM listings WHERE {VISIBLE}"
" AND property_type<>'' GROUP BY property_type"):
slug = slugify(r["property_type"])
if len(slug) < 2:
continue
e = types.setdefault(slug, {"label": r["property_type"], "n": 0, "values": [], "best": 0})
e["n"] += r["n"]
e["values"].append(r["property_type"])
if r["n"] > e["best"]:
e["best"] = r["n"]; e["label"] = r["property_type"]
city_types: dict[tuple[str, str], int] = {}
for r in con.execute(
f"SELECT city, property_type, COUNT(*) n FROM listings WHERE {VISIBLE}"
" AND city<>'' AND property_type<>'' GROUP BY city, property_type"):
cs, ts_ = slugify(r["city"]), slugify(r["property_type"])
if len(cs) < 2 or len(ts_) < 2:
continue
city_types[(cs, ts_)] = city_types.get((cs, ts_), 0) + r["n"]
con.close()
return {"cities": cities, "types": types, "city_types": city_types}
def registry() -> dict:
return _cached("registry", 900, _build_registry)
# -----------------------------------------------------------------------------
# Template: dist/index.html, stripped of its static /description
# -----------------------------------------------------------------------------
_tpl_cache: tuple[float, str] | None = None
def _template() -> str:
global _tpl_cache
path = FRONTEND_DIR / "index.html"
mtime = path.stat().st_mtime
if _tpl_cache and _tpl_cache[0] == mtime:
return _tpl_cache[1]
tpl = path.read_text(encoding="utf-8")
tpl = re.sub(r".*?\s*", "", tpl, flags=re.S)
tpl = re.sub(r']*>\s*', "", tpl)
tpl = re.sub(r']*>\s*', "", tpl)
_tpl_cache = (mtime, tpl)
return tpl
def _page(title: str, description: str, canonical: str, body: str,
jsonld: list[dict] | None = None, og_image: str | None = None,
og_type: str = "website", noindex: bool = False,
status: int = 200) -> HTMLResponse:
head = [
f"{_esc(title)}",
f'',
f'',
f'',
'',
f'',
f'',
f'',
f'',
]
if og_image:
head.append(f'')
else:
og_image = BASE_URL + "/og.png"
head.append(f'')
head.append('')
head.append('')
head.append('')
head.append(f'')
if noindex:
head.append('')
for obj in (jsonld or []):
head.append('")
tpl = _template()
out = tpl.replace("", " " + "\n ".join(head) + "\n ", 1)
out = out.replace('',
f'
{body}
', 1)
return HTMLResponse(out, status_code=status)
# -----------------------------------------------------------------------------
# Reusable HTML blocks
# -----------------------------------------------------------------------------
def fiche_href(uid: str, slug: str = "") -> str:
path = f"/property/{quote(uid, safe='')}"
return path + (f"/{slug}" if slug else "")
def _item_li(r) -> str:
href = fiche_href(r["uid"], listing_slug(r))
bits = [b for b in [
r["property_type"],
f"{r['bedrooms']} bed" if r["bedrooms"] is not None else "",
f"{r['bathrooms']} bath" if r["bathrooms"] is not None else "",
f"{_fmt_n(round(r['area_sqft']))} sq ft" if r["area_sqft"] else "",
] if b]
name = r["address"] or r["title"] or "Property"
loc = ", ".join(x for x in [r["sector"], r["city"]] if x)
return (f'
{_esc(name)} — '
f'{_esc(_fmt_price(r["price"]))}'
+ (f' · {_esc(" · ".join(bits))}' if bits else "")
+ (f' · {_esc(loc)}' if loc else "") + "
")
def _agg_stats(con, cities: list[str] | None = None,
types_values: list[str] | None = None) -> dict:
where = VISIBLE
args: list = []
if cities:
where += f" AND city IN ({','.join('?' * len(cities))})"
args += cities
if types_values:
where += f" AND property_type IN ({','.join('?' * len(types_values))})"
args += types_values
row = con.execute(
f"SELECT COUNT(*) n, AVG(price) avg_p, MIN(price) min_p, MAX(price) max_p"
f" FROM listings WHERE {where}", args).fetchone()
med = None
if row["n"]:
med_row = con.execute(
f"SELECT price FROM listings WHERE {where}"
f" ORDER BY price LIMIT 1 OFFSET ?", args + [row["n"] // 2]).fetchone()
med = med_row["price"] if med_row else None
return {"n": row["n"], "avg": row["avg_p"], "med": med,
"min": row["min_p"], "max": row["max_p"], "where": where, "args": args}
def _pagination_html(base_path: str, page: int, pages: int) -> str:
if pages <= 1:
return ""
out = ['")
return "".join(out)
def _breadcrumb_ld(crumbs: 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(crumbs)
],
}
# -----------------------------------------------------------------------------
# Home
# -----------------------------------------------------------------------------
def _home_data() -> dict:
def build():
con = db.connect()
row = con.execute(
f"SELECT COUNT(*) total, COUNT(DISTINCT city) cities,"
f" COUNT(DISTINCT source) sources, AVG(price) avg_p"
f" FROM listings WHERE {VISIBLE}").fetchone()
recent = [dict(r) for r in con.execute(
f"SELECT uid, address, title, city, sector, property_type, price,"
f" bedrooms, bathrooms, area_sqft FROM listings WHERE {VISIBLE}"
f" ORDER BY first_seen DESC LIMIT 12")]
con.close()
return {**dict(row), "recent": recent}
return _cached("home", 900, build)
def render_home() -> HTMLResponse:
d = _home_data()
reg = registry()
total = _fmt_n(d["total"])
title = f"House-Ka — {total} homes for sale across Canada · A Groupe KA service"
desc = (f"{total} homes for sale in {_fmt_n(d['cities'])} Canadian cities and towns, "
f"aggregated from {d['sources']} brokerage sources on the CREA DDF feed — "
f"continuously updated. Average asking price: {_fmt_price(d['avg_p'])}.")
top_cities = sorted(reg["cities"].items(), key=lambda kv: -kv[1]["n"])[:60]
types = sorted(reg["types"].items(), key=lambda kv: -kv[1]["n"])
body = [
f"
Homes for sale across Canada — {total} listings from real-estate brokerages
",
f"
House-Ka continuously aggregates homes for sale publicly listed by Canadian "
f"real-estate brokerages and teams (CREA DDF feed) — {d['sources']} sources, "
f"{_fmt_n(d['cities'])} cities, average asking price {_esc(_fmt_price(d['avg_p']))}. "
f"Every listing links back to the brokerage's original page. Coverage starts with "
f"Ontario and grows across the rest of Canada. Looking for Québec? "
f'See our sister site Immo-Ka.
'
for s, e in top if s != city_slug and e["n"] >= MIN_LISTINGS) + "
")
con.close()
if city and ptype:
base_path = f"/for-sale/{city_slug}/{type_slug}"
h1 = f"{ptype['label']} for sale in {city['label']}"
what = f"{ptype['label'].lower()} listings in {city['label']}"
elif city:
base_path = f"/for-sale/{city_slug}"
h1 = f"Homes for sale in {city['label']}"
what = f"homes for sale in {city['label']}"
else:
base_path = f"/type/{type_slug}"
h1 = f"{ptype['label']} for sale in Canada"
what = f"{ptype['label'].lower()} listings across Canada"
canonical = BASE_URL + base_path + (f"?page={page}" if page > 1 else "")
title = f"{h1} — {_fmt_n(st['n'])} listings" + (f" (page {page})" if page > 1 else "") + " | House-Ka"
desc = (f"{_fmt_n(st['n'])} {what}: median price {_fmt_price(st['med'])}, "
f"average price {_fmt_price(st['avg'])}. Listings from Canadian brokerages "
f"on the CREA DDF feed, continuously updated.")
stats_p = (f"
{_fmt_n(st['n'])} listings · median price "
f"{_esc(_fmt_price(st['med']))} · average price "
f"{_esc(_fmt_price(st['avg']))} · from "
f"{_esc(_fmt_price(st['min']))} to {_esc(_fmt_price(st['max']))}.
")
crumbs = [("Home", "/")]
if city:
crumbs.append((f"For sale in {city['label']}", f"/for-sale/{city_slug}"))
if ptype:
crumbs.append((f"{ptype['label']}", base_path))
else:
crumbs.append((h1, base_path))
body = ('
{_esc(h1)}
" + stats_p
+ "
" + "".join(_item_li(r) for r in rows) + "
"
+ _pagination_html(base_path, page, pages)
+ "".join(links))
return _page(title, desc, canonical, body, [_breadcrumb_ld(crumbs)])
# -----------------------------------------------------------------------------
# Listing page
# -----------------------------------------------------------------------------
_TYPE_SCHEMA = {
"house": "SingleFamilyResidence", "condo": "Apartment",
"cottage": "House", "semi-detached": "House", "townhouse": "House",
"duplex": "Residence", "triplex": "Residence",
"multi-family": "Residence", "mobile-home": "House",
}
def render_listing(uid: str, slug: str | None) -> Response:
con = db.connect()
row = con.execute("SELECT * FROM listings WHERE uid=?", (uid,)).fetchone()
con.close()
if row is None:
return render_404()
city_slug = slugify(row["city"] or "")
city_known = city_slug in registry()["cities"]
city_href = f"/for-sale/{city_slug}" if city_known else "/"
if not row["active"]:
# withdrawn / sold → 410 Gone, with escape hatches
name = row["address"] or row["title"] or "Property"
body = (f"
This property is no longer for sale
"
f"
The listing “{_esc(name)}” ({_esc(row['city'] or 'Canada')}) has been "
f"withdrawn or sold.
')
return _page(f"Listing withdrawn — {name} | {SITE_NAME}",
"This listing has been withdrawn or sold.",
BASE_URL + fiche_href(uid), body, noindex=True, status=410)
expected = listing_slug(row)
if expected and slug != expected:
return RedirectResponse(BASE_URL + fiche_href(uid, expected), status_code=301)
d = dict(row)
images = json.loads(d.get("images") or "[]")
features = json.loads(d.get("features") or "[]")
name = d["address"] or d["title"] or "Property for sale"
loc = ", ".join(x for x in [d["sector"], d["city"]] if x) or "Canada"
canonical = BASE_URL + fiche_href(uid, expected)
ptype = d["property_type"] or "Property"
specs = [(lbl, val) for lbl, val in [
("Type", ptype),
("Price", _fmt_price(d["price"]) if d["price"] is not None else d["price_label"]),
("Bedrooms", d["bedrooms"]),
("Bathrooms", d["bathrooms"]),
("Half baths", d["powder_rooms"]),
("Living area", f"{_fmt_n(round(d['area_sqft']))} sq ft" if d["area_sqft"] else None),
("Lot", f"{_fmt_n(round(d['lot_sqft']))} sq ft" if d["lot_sqft"] else None),
("Year built", d["year_built"]),
("City", d["city"]),
("Neighbourhood", d["sector"]),
("MLS® number", d["mls"]),
("Agent", d["broker_name"]),
("Brokerage", d["agency"]),
] if val not in (None, "", 0)]
descr = (d["description"] or "").strip()
if len(descr) > 1500:
descr = descr[:1500].rsplit(" ", 1)[0] + "…"
type_slug = slugify(ptype)
crumbs = [("Home", "/")]
if city_known:
crumbs.append((f"For sale in {d['city']}", city_href))
if (city_slug, type_slug) in registry()["city_types"]:
crumbs.append((ptype, f"/for-sale/{city_slug}/{type_slug}"))
crumbs.append((name, fiche_href(uid, expected)))
body = [
'",
f"
{_esc(name)}
",
f"
{_esc(ptype)} for sale in {_esc(loc)} — "
f"{_esc(_fmt_price(d['price']) if d['price'] is not None else (d['price_label'] or 'Price on request'))}
",
]
if images:
body.append("".join(
f''
for i, u in enumerate(images[:3])))
body.append("
")
about_type = _TYPE_SCHEMA.get(type_slug, "Residence")
region_code = _PROVINCE_CODE.get((d["region"] or "").strip().lower(), "ON")
about: dict = {
"@type": about_type,
"name": name,
"address": {"@type": "PostalAddress",
"streetAddress": d["address"] or None,
"addressLocality": d["city"] or None,
"addressRegion": region_code, "addressCountry": "CA"},
}
if d["lat"] is not None and d["lng"] is not None:
about["geo"] = {"@type": "GeoCoordinates",
"latitude": d["lat"], "longitude": d["lng"]}
if d["bedrooms"] is not None:
about["numberOfBedrooms"] = d["bedrooms"]
if d["bathrooms"] is not None:
about["numberOfBathroomsTotal"] = d["bathrooms"]
if d["area_sqft"]:
about["floorSize"] = {"@type": "QuantitativeValue",
"value": round(d["area_sqft"]), "unitCode": "FTK"}
if d["year_built"]:
about["yearBuilt"] = d["year_built"]
about = {k: v for k, v in about.items() if v is not None}
about["address"] = {k: v for k, v in about["address"].items() if v is not None}
jsonld: list[dict] = [{
"@context": "https://schema.org",
"@type": "RealEstateListing",
"name": name,
"url": canonical,
"inLanguage": "en-CA",
"datePosted": _iso_date(d.get("first_seen")),
"image": images[:6] or None,
"about": about,
}, _breadcrumb_ld(crumbs)]
if d["price"] is not None:
jsonld[0]["offers"] = {"@type": "Offer", "price": round(d["price"], 2),
"priceCurrency": "CAD",
"availability": "https://schema.org/InStock"}
jsonld[0] = {k: v for k, v in jsonld[0].items() if v is not None}
title = f"{name} — {ptype} for sale, {d['city'] or 'Canada'} | {_fmt_price(d['price']) if d['price'] is not None else 'Price on request'}"
meta_desc = (f"{ptype} for sale in {loc}"
+ (f", {d['bedrooms']} bedrooms" if d["bedrooms"] else "")
+ (f", {_fmt_n(round(d['area_sqft']))} sq ft" if d["area_sqft"] else "")
+ f" — {_fmt_price(d['price']) if d['price'] is not None else 'price on request'}. "
+ (descr[:120] + "…" if len(descr) > 120 else descr))
return _page(title, meta_desc, canonical, "".join(body), jsonld,
og_image=images[0] if images else None, og_type="article")
# -----------------------------------------------------------------------------
# Static SPA pages (dedicated meta) and 404
# -----------------------------------------------------------------------------
_STATIC_META = {
"/stats": ("Canadian housing market statistics",
"Average prices, listing volumes by source and data quality — "
"continuous statistics from House-Ka.",
False),
"/agencies": ("Covered brokerages",
"All the Canadian real-estate brokerages and teams aggregated by "
"House-Ka through the CREA DDF feed.",
False),
"/rates": (
"Mortgage rates in Canada — live comparator",
"Compare mortgage rates actually published by RBC, TD, BMO, CIBC, "
"Scotiabank, NBC, Desjardins, Tangerine, EQ and more — fixed and variable, "
"with official source, freshness and history. Continuously collected by House-Ka.",
False),
"/terms": ("Terms of use",
"Terms of use of the House-Ka platform (Groupe-Ka).", False),
"/privacy": ("Privacy policy",
"House-Ka privacy policy (Groupe-Ka) — PIPEDA.", False),
"/account": ("My account", "Your Groupe KA account on House-Ka.", True),
"/contact": ("Contact — Groupe KA",
"Write to Groupe KA: contact@groupe-ka.com (projects and data), "
"info@groupe-ka.com (media), admin@groupe-ka.com (legal and privacy). "
"House-Ka is a Groupe KA service — https://www.groupe-ka.com.",
False),
}
def render_static(path: str) -> HTMLResponse:
t, desc, noindex = _STATIC_META[path]
body = f"
')
return _page(f"Page not found | {SITE_NAME}", "Page not found.",
BASE_URL + "/", body, noindex=True, status=404)
# -----------------------------------------------------------------------------
# robots.txt and sitemaps
# -----------------------------------------------------------------------------
FICHES_PER_SITEMAP = 40000
def robots_txt() -> PlainTextResponse:
return PlainTextResponse(
"User-agent: *\n"
"Allow: /\n"
"Disallow: /api/\n"
"Disallow: /account\n"
f"\nSitemap: {BASE_URL}/sitemap.xml\n")
def _xml(urls: list[str]) -> Response:
body = ('\n'
'\n'
+ "\n".join(urls) + "\n")
return Response(body, media_type="application/xml")
def _url_el(loc: str, lastmod: str | None = None) -> str:
lm = f"{lastmod}" if lastmod else ""
return f" {html.escape(loc)}{lm}"
def sitemap_index() -> Response:
def build():
con = db.connect()
n = con.execute(f"SELECT COUNT(*) c FROM listings WHERE {VISIBLE}").fetchone()["c"]
last = con.execute(
f"SELECT MAX(updated_at) m FROM listings WHERE {VISIBLE}").fetchone()["m"]
con.close()
parts = -(-n // FICHES_PER_SITEMAP) or 1
lm = _iso_date(last)
maps = [f"{BASE_URL}/sitemaps/listings-{i + 1}.xml" for i in range(parts)]
maps += [f"{BASE_URL}/sitemaps/cities.xml",
f"{BASE_URL}/sitemaps/cities-types.xml",
f"{BASE_URL}/sitemaps/types.xml",
f"{BASE_URL}/sitemaps/pages.xml"]
body = ('\n'
'\n'
+ "\n".join(f" {html.escape(m)}"
f"{lm}" for m in maps)
+ "\n")
return body
return Response(_cached("sm:index", 3600, build), media_type="application/xml")
def sitemap_file(name: str) -> Response:
m = re.fullmatch(r"listings-(\d+)\.xml", name)
if m:
part = int(m.group(1))
def build():
con = db.connect()
rows = con.execute(
f"SELECT uid, address, title, city, updated_at FROM listings"
f" WHERE {VISIBLE} ORDER BY uid LIMIT ? OFFSET ?",
(FICHES_PER_SITEMAP, (part - 1) * FICHES_PER_SITEMAP)).fetchall()
con.close()
if not rows:
return None
return [_url_el(BASE_URL + fiche_href(r["uid"], listing_slug(r)),
_iso_date(r["updated_at"])) for r in rows]
urls = _cached(f"sm:listings:{part}", 3600, build)
if urls is None:
return Response("Sitemap not found", status_code=404)
return _xml(urls)
if name == "cities.xml":
def build():
reg = registry()
lm = _city_lastmod()
return [_url_el(f"{BASE_URL}/for-sale/{s}", lm.get(s))
for s, e in sorted(reg["cities"].items())
if e["n"] >= MIN_LISTINGS]
return _xml(_cached("sm:cities", 3600, build))
if name == "cities-types.xml":
def build():
reg = registry()
lm = _city_lastmod()
return [_url_el(f"{BASE_URL}/for-sale/{cs}/{ts_}", lm.get(cs))
for (cs, ts_), n in sorted(reg["city_types"].items())
if n >= MIN_LISTINGS and cs in reg["cities"] and ts_ in reg["types"]
and reg["cities"][cs]["n"] >= MIN_LISTINGS]
return _xml(_cached("sm:cities-types", 3600, build))
if name == "types.xml":
def build():
reg = registry()
return [_url_el(f"{BASE_URL}/type/{s}")
for s, e in sorted(reg["types"].items()) if e["n"] >= MIN_LISTINGS]
return _xml(_cached("sm:types", 3600, build))
if name == "pages.xml":
return _xml([_url_el(f"{BASE_URL}{p}")
for p in ["/", "/stats", "/agencies", "/rates",
"/terms", "/privacy"]])
return Response("Sitemap not found", status_code=404)
def _city_lastmod() -> dict[str, str]:
def build():
con = db.connect()
out: dict[str, str] = {}
for r in con.execute(
f"SELECT city, MAX(updated_at) m FROM listings WHERE {VISIBLE}"
" AND city<>'' GROUP BY city"):
s = slugify(r["city"])
if s:
prev = out.get(s)
cur = _iso_date(r["m"])
out[s] = max(prev, cur) if prev else cur
con.close()
return out
return _cached("sm:citylastmod", 3600, build)
# -----------------------------------------------------------------------------
# Slug resolution for the frontend (client-side /for-sale pages)
# -----------------------------------------------------------------------------
def resolve_slugs(ville: str | None, ptype: str | None) -> dict | None:
reg = registry()
out: dict = {}
if ville:
e = reg["cities"].get(ville)
if not e:
return None
out["city"] = e["label"]
out["city_n"] = e["n"]
if ptype:
e = reg["types"].get(ptype)
if not e:
return None
out["property_type"] = e["label"]
out["type_n"] = e["n"]
return out
# -----------------------------------------------------------------------------
# Routing: called by the web.py catch-all
# -----------------------------------------------------------------------------
def render_for_path(path: str, query: dict) -> Response | None:
"""SEO HTML for `path` (e.g. “/for-sale/ottawa”), or None → raw index."""
path = path.rstrip("/") or "/"
try:
page = max(1, int(query.get("page", "1")))
except ValueError:
page = 1
if path == "/":
return render_home()
if path in _STATIC_META:
return render_static(path)
parts = [p for p in path.split("/") if p]
if parts[0] == "property" and len(parts) in (2, 3):
return render_listing(parts[1], parts[2] if len(parts) == 3 else None)
if parts[0] == "for-sale" and len(parts) in (2, 3):
return _render_category(parts[1], parts[2] if len(parts) == 3 else None, page)
if parts[0] == "type" and len(parts) == 2:
return _render_category(None, parts[1], page)
return render_404()