"""
Helpers de SEO usados pelas páginas renderizadas no servidor (busca e anúncio):
- slugs amigáveis para URLs com palavra-chave;
- montagem de
/meta description específicos por página;
- dados estruturados (JSON-LD) no padrão schema.org.
"""
import json
import re
import unicodedata
from typing import Optional
# As três cidades atendidas pela SK — mapeamento fixo entre o "slug" usado
# na URL (sem acento, minúsculo) e o nome oficial armazenado na planilha.
CIDADES_SLUG_PARA_NOME = {
"matinhos": "Matinhos",
"guaratuba": "Guaratuba",
"pontal-do-parana": "Pontal do Paraná",
}
CIDADES_NOME_PARA_SLUG = {v: k for k, v in CIDADES_SLUG_PARA_NOME.items()}
NEGOCIO_SLUG_PARA_NOME = {"comprar": "Venda", "alugar": "Locação"}
NEGOCIO_NOME_PARA_SLUG = {v: k for k, v in NEGOCIO_SLUG_PARA_NOME.items()}
def slugify(texto: str) -> str:
"""Converte texto livre em slug de URL: sem acento, minúsculo, hifens."""
texto = unicodedata.normalize("NFKD", texto or "").encode("ascii", "ignore").decode("ascii")
texto = re.sub(r"[^a-zA-Z0-9]+", "-", texto).strip("-").lower()
return texto or "imovel"
def slug_anuncio(anuncio: dict) -> str:
"""Slug com palavras-chave (tipo + bairro/cidade) para a URL do anúncio."""
partes = [anuncio.get("tipo_imovel", ""), anuncio.get("bairro", ""), anuncio.get("cidade", "")]
base = slugify(" ".join(p for p in partes if p))
return base or slugify(anuncio.get("titulo", "imovel"))
def cidade_slug(nome_cidade: str) -> str:
return CIDADES_NOME_PARA_SLUG.get(nome_cidade, slugify(nome_cidade))
def url_anuncio(anuncio: dict, base_url: str = "") -> str:
return f"{base_url}/imovel/{anuncio['id']}/{slug_anuncio(anuncio)}"
def url_busca(cidade: Optional[str] = None, tipo_negocio: Optional[str] = None, base_url: str = "") -> str:
prefixo = "/busca"
if tipo_negocio in NEGOCIO_NOME_PARA_SLUG:
prefixo = f"/{NEGOCIO_NOME_PARA_SLUG[tipo_negocio]}"
if cidade:
return f"{base_url}{prefixo}/{cidade_slug(cidade)}"
return f"{base_url}{prefixo}"
def truncar(texto: str, tamanho: int) -> str:
texto = (texto or "").strip()
if len(texto) <= tamanho:
return texto
return texto[:tamanho].rsplit(" ", 1)[0].rstrip(",.;") + "…"
def preco_formatado(valor: float) -> str:
return "R$ " + f"{float(valor or 0):,.0f}".replace(",", ".")
def preco_exibicao(a: dict) -> str:
"""Texto do valor pronto para exibição. Retorna "" quando o preço não foi
informado (0/vazio) — nesses casos o valor não deve aparecer no site."""
if not a.get("preco"):
return ""
valor = preco_formatado(a["preco"])
return f"{valor}/mês" if a["tipo_negocio"] == "Locação" else valor
STATUS_CLASSE = {
"Disponível": "status-disponivel", "Reservado": "status-reservado", "Inativo": "status-inativo",
"Vendido": "status-vendido", "Alugado": "status-alugado",
}
def status_classe(status: str) -> str:
return STATUS_CLASSE.get(status, "status-disponivel")
def render_card_ssr(a: dict, base_url: str = "") -> str:
"""Card de imóvel renderizado no servidor (link real, crawlável) para a listagem inicial da busca.
Usa o mesmo HTML/atributos (`data-midias`, `.media-nav`, `.media-slide`) que o JS gera em
cardImovel(), então o carrossel de fotos/vídeos e o popup já funcionam via main.js mesmo
antes do busca.js "hidratar" a página com a versão totalmente interativa."""
from html import escape
midias_raw = a.get("midias")
try:
midias = json.loads(midias_raw) if isinstance(midias_raw, str) else (midias_raw or [])
except (json.JSONDecodeError, TypeError):
midias = []
midias = [str(m) for m in midias if m]
if midias:
primeira = midias[0]
eh_video = bool(re.search(r"\.(mp4|webm|mov|ogg)(\?|#|$)", primeira, re.I))
eh_audio = bool(re.search(r"\.(mp3|wav|m4a|aac|oga|opus|weba)(\?|#|$)", primeira, re.I))
if eh_video:
primeiro_slide = f'▶'
elif eh_audio:
primeiro_slide = (
''
)
else:
primeiro_slide = f''
setas_html = ""
if len(midias) > 1:
setas_html = (
''
''
f'1/{len(midias)}'
)
midias_json_attr = escape(json.dumps(midias, ensure_ascii=False))
media_html = (
f'
'
f'
{primeiro_slide}
{setas_html}'
)
else:
media_html = (
'
'
''
)
tag_cls = "tag-venda" if a["tipo_negocio"] == "Venda" else "tag-locacao"
destaque_html = 'Destaque' if a.get("destaque") else ""
specs = []
if a.get("quartos"):
specs.append(f"{a['quartos']} quarto{'s' if a['quartos'] != 1 else ''}")
area = a.get("area_util") or a.get("area_total")
if area:
specs.append(f"{area}m²")
specs_html = " · ".join(specs)
url = url_anuncio(a, base_url)
status_cls = status_classe(a["status"])
preco_html = f'
'
)
def render_galeria_ssr(midias: list[str], titulo: str) -> str:
"""Galeria da página de detalhe do anúncio, renderizada no servidor.
Suporta foto, vídeo e áudio na mídia principal e nas miniaturas — evita o
bug de tentar exibir vídeo/áudio dentro de uma tag ."""
from html import escape
if not midias:
return ""
def _tag_grande(url: str) -> str:
if re.search(r"\.(mp4|webm|mov|ogg)(\?|#|$)", url, re.I):
return f''
if re.search(r"\.(mp3|wav|m4a|aac|oga|opus|weba)(\?|#|$)", url, re.I):
return (
''
)
return f''
def _thumb(url: str, indice: int) -> str:
if re.search(r"\.(mp4|webm|mov|ogg)(\?|#|$)", url, re.I):
return '▶'
if re.search(r"\.(mp3|wav|m4a|aac|oga|opus|weba)(\?|#|$)", url, re.I):
return (
''
)
return f''
thumbs_html = ""
if len(midias) > 1:
itens = "".join(
f''
for i, m in enumerate(midias)
)
thumbs_html = f'
{itens}
'
return (
f'
'
f'
{_tag_grande(midias[0])}'
f'📷 1/{len(midias)}
'
f'{thumbs_html}
'
)
def render_showcase_ssr(destaques: list[dict], base_url: str = "") -> str:
"""Carrossel de destaques da página inicial, renderizado no servidor —
mesma marcação (classes/atributos) que static/js/home.js gera em
montarShowcase(), para que o JS só precise "ligar" a interatividade
(troca de slide/dots) em cima do HTML que já chegou pronto, sem precisar
buscar e desenhar os destaques via API antes de aparecerem para o
usuário (ou para o Google)."""
from html import escape
if not destaques:
return '
Nenhum imóvel em destaque no momento.
'
def _slide(a: dict) -> str:
midias_raw = a.get("midias")
try:
midias = json.loads(midias_raw) if isinstance(midias_raw, str) else (midias_raw or [])
except (json.JSONDecodeError, TypeError):
midias = []
foto = next((str(m) for m in midias if m), "") if midias else ""
tag_cls = "tag-venda" if a["tipo_negocio"] == "Venda" else "tag-locacao"
url = url_anuncio(a, base_url)
img_html = f'' if foto else ""
estilo = "" if foto else ' style="background:var(--midia-placeholder-2)"'
return (
f''
f'{img_html}'
f'{escape(a["tipo_negocio"])}'
f'