Tema
Primeri
Svi primeri pretpostavljaju ključ u promenljivoj okruženja PROPERTIO_API_KEY i bazni URL https://app.propertio.rs/api. Za razvoj koristite ključ test agencije i https://app.test.propertio.rs/api.
Node.js (skripta, cron)
js
// oglasi.mjs: node --env-file=.env oglasi.mjs
import { PropertioClient, formatPrice, formatLocation } from "@propertio/sdk";
const propertio = new PropertioClient({ baseUrl: "https://app.propertio.rs/api", apiKey: process.env.PROPERTIO_API_KEY });
const { items, total } = await propertio.properties.search({ dealType: "sale", pageSize: 5 });
console.log(`${total} oglasa na prodaju`);
for (const p of items) console.log(`${p.reference} ${formatPrice(p)} ${formatLocation(p)} ${p.title}`);Čist JavaScript u browseru
Za jednostavan statički sajt bez build koraka. Ključ je vidljiv u kodu stranice, pa agencija mora da podesi dozvoljene sajtove.
html
<ul id="oglasi"></ul>
<script type="module">
import { PropertioClient, imageUrl, formatPrice } from "https://esm.sh/@propertio/sdk@0.1.0";
const propertio = new PropertioClient({ baseUrl: "https://app.propertio.rs/api", apiKey: "pk_…" });
const { items } = await propertio.properties.search({ pageSize: 12 });
document.querySelector("#oglasi").innerHTML = items.map((p) => `
<li>
<a href="/oglas.html?slug=${encodeURIComponent(p.slug)}">
<img src="${imageUrl(p, "card") ?? "/bez-slike.svg"}" alt="" width="400" height="267" loading="lazy">
<strong></strong> <span>${formatPrice(p)}</span>
</a>
</li>`).join("");
// naslov kao tekst, ne kao HTML (oglasi su unos korisnika)
document.querySelectorAll("#oglasi strong").forEach((el, i) => { el.textContent = items[i].title; });
</script>Tekst oglasa je unos korisnika
Naslov, opis i ime agenta ubacujte kao tekst (textContent, JSX, Astro {}), nikad kao sirov HTML. Opis čuva prelome reda: prikažite ga sa white-space: pre-line.
Next.js (App Router)
Ključ ostaje na serveru: komponente su serverske, upit ide preko server akcije.
ts
// lib/propertio.ts
import "server-only";
import { PropertioClient } from "@propertio/sdk";
export const propertio = new PropertioClient({
baseUrl: process.env.PROPERTIO_API_URL ?? "https://app.propertio.rs/api",
apiKey: process.env.PROPERTIO_API_KEY!,
requestInit: { next: { revalidate: 300 } }, // keš 5 minuta; mnogo kraće od 24 h važenja linkova slika
});tsx
// app/nekretnine/page.tsx: lista sa filterima iz URL-a
import Link from "next/link";
import { imageUrl, formatPrice, formatLocation, type SearchParams } from "@propertio/sdk";
import { propertio } from "@/lib/propertio";
export default async function Page({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {
const sp = await searchParams;
const query: SearchParams = {
dealType: sp.tip === "izdavanje" ? "rent" : "sale",
city: sp.grad, priceMax: sp.do ? Number(sp.do) : undefined,
page: Number(sp.strana ?? 1), pageSize: 12,
};
const { items, total } = await propertio.properties.search(query);
return (
<main>
<p>{total} oglasa</p>
<ul>
{items.map((p) => (
<li key={p.id}>
<Link href={`/nekretnine/${p.slug}`}>
{imageUrl(p, "card") && <img src={imageUrl(p, "card")!} alt="" width={400} height={267} loading="lazy" />}
<h2>{p.title}</h2>
<p>{formatPrice(p)} · {p.areaSqm} m² · {formatLocation(p)}</p>
</Link>
</li>
))}
</ul>
</main>
);
}tsx
// app/nekretnine/[slug]/page.tsx: detalj, 404, stari slug i metapodaci
import { notFound, permanentRedirect } from "next/navigation";
import { PropertioError, imageUrl, formatPrice } from "@propertio/sdk";
import { propertio } from "@/lib/propertio";
import { InquiryForm } from "./inquiry-form";
async function load(slug: string) {
let p;
try { p = await propertio.properties.get(slug); }
catch (e) { if (e instanceof PropertioError && e.status === 404) notFound(); throw e; }
// naslov (pa i slug) je promenjen: stari link i dalje nalazi oglas, preusmeri na novu adresu
if (p.slug !== slug) permanentRedirect(`/nekretnine/${p.slug}`);
return p;
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const p = await load((await params).slug);
return { title: p.title, description: p.description.slice(0, 160), openGraph: { images: imageUrl(p, "large") ? [imageUrl(p, "large")!] : [] } };
}
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const p = await load((await params).slug);
return (
<article>
<h1>{p.title}</h1>
<p>{formatPrice(p)} · šifra {p.reference}</p>
<p style={{ whiteSpace: "pre-line" }}>{p.description}</p>
{p.agent && <p>Agent: {p.agent.name} {p.agent.phone}</p>}
<InquiryForm propertyId={p.id} />
</article>
);
}tsx
// app/nekretnine/[slug]/actions.ts: server akcija za upit
"use server";
import { PropertioError } from "@propertio/sdk";
import { propertio } from "@/lib/propertio";
export async function sendInquiry(_prev: unknown, form: FormData) {
if (form.get("consent") !== "on") return { ok: false, error: "Potrebna je saglasnost za obradu podataka." };
try {
await propertio.inquiries.create({
propertyId: String(form.get("propertyId")),
name: String(form.get("name") ?? ""),
phone: String(form.get("phone") ?? "") || null,
email: String(form.get("email") ?? "") || null,
message: String(form.get("message") ?? ""),
consent: true,
website: String(form.get("website") ?? ""), // honeypot, prosleđen netaknut
});
return { ok: true };
} catch (e) {
if (e instanceof PropertioError && e.code === "VALIDATION") return { ok: false, error: "Proverite ime, kontakt i saglasnost." };
if (e instanceof PropertioError && e.code === "RATE_LIMITED") return { ok: false, error: "Previše pokušaja, pokušajte za minut." };
return { ok: false, error: "Slanje nije uspelo, pokušajte ponovo ili nas pozovite." };
}
}tsx
// app/sitemap.ts
import type { MetadataRoute } from "next";
import { propertio } from "@/lib/propertio";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const out: MetadataRoute.Sitemap = [];
for await (const p of propertio.properties.all()) out.push({ url: `https://www.agencija.rs/nekretnine/${p.slug}`, lastModified: p.updatedAt });
return out;
}Za trenutno osvežavanje posle izmene oglasa, umesto čekanja revalidate, koristite webhook i revalidatePath.
Astro
Statički sajt (SSG) koji se gradi na nekoliko sati, ili SSR sa adapterom. Ključ je samo u build/server kodu.
astro
---
// src/pages/nekretnine/[slug].astro (SSG)
import { PropertioClient, imageUrl, formatPrice, formatLocation } from "@propertio/sdk";
export async function getStaticPaths() {
const propertio = new PropertioClient({ baseUrl: "https://app.propertio.rs/api", apiKey: import.meta.env.PROPERTIO_API_KEY });
const paths = [];
for await (const p of propertio.properties.all()) paths.push({ params: { slug: p.slug }, props: { p } });
return paths;
}
const { p } = Astro.props;
---
<html lang="sr">
<head><title>{p.title}</title><meta property="og:image" content={imageUrl(p, "large")} /></head>
<body>
<h1>{p.title}</h1>
<p>{formatPrice(p)} · {p.areaSqm} m² · {formatLocation(p)}</p>
<img src={imageUrl(p, "large")} alt={p.title} width="1600" height="1066" />
<p style="white-space: pre-line">{p.description}</p>
</body>
</html>Statički sajt i slike
Linkovi slika važe 24 h od izgradnje. Statički sajt pravite ponovo bar jednom dnevno (npr. zakazani build na Netlify/Vercel/GitHub Actions svakih 6 sati, i odmah na webhook), ili slike učitavajte pri prikazu sa servera.
Kontakt forma u Astru: API ruta (src/pages/api/upit.ts, sa SSR adapterom) koja zove inquiries.create, isto kao server akcija iznad.
PHP (WordPress i drugi CMS-ovi)
Bez SDK-a, direktno preko HTTP-a:
php
<?php
function propertio(string $path, array $query = [], ?array $body = null): array {
$url = 'https://app.propertio.rs/api/v1' . $path . ($query ? '?' . http_build_query($query) : '');
$headers = ['x-api-key: ' . getenv('PROPERTIO_API_KEY'), 'accept: application/json'];
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15]);
if ($body !== null) {
$headers[] = 'content-type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); // POST
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$res = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$json = json_decode($res ?: 'null', true);
if ($status >= 400) throw new RuntimeException(($json['error'] ?? 'ERROR') . ': ' . ($json['message'] ?? "HTTP $status"), $status);
return $json;
}
// WordPress: keš 5 minuta
$page = get_transient('propertio_sale') ?: propertio('/properties', ['dealType' => 'sale', 'pageSize' => 12]);
set_transient('propertio_sale', $page, 300);
foreach ($page['items'] as $p) {
echo '<h2>' . esc_html($p['title']) . '</h2>';
}Primer sajta (apps/demo-site)
U repozitorijumu Propertija, apps/demo-site je mali sajt agencije (Vite + TypeScript, bez frameworka) na SDK-u:
- lista aktivnih oglasa sa naslovnom slikom, cenom i lokacijom (
properties.search,imageUrl,formatPrice); - detalj oglasa (
properties.get) sa formom za upit (inquiries.create) i dugmetom „Zakaži pokazivanje“ (viewings.request), uz izgovorenu potvrdu (voice.confirmation, saspeechSynthesiskao rezervom); - AI asistent za posetioce (
assistant.chat) sa linkovima ka oglasima koje je pronašao (listings) i izgovorom odgovora (voice.speaksaspeechToken).
Pokretanje lokalno, uz ključ test agencije:
bash
pnpm install
VITE_PROPERTIO_API_URL=https://app.test.propertio.rs/api VITE_PROPERTIO_API_KEY=pk_… pnpm --filter @propertio/demo-site devDemo sajt zove API iz browsera i ključ je u kodu stranice. To je u redu za demonstraciju; pravi sajt neka ključ drži na serveru.