Sunucu Taraflı Render Örnekleri
PHP, Node, Next.js ve Django için kopyalanabilir entegrasyon
Bu sayfa, Sunucu Taraflı Render için kopyalanıp yapıştırılabilir entegrasyon örneklerini verir: PHP, Node (Express), Next.js (server component), Django ve Jinja2 (Akinon Project Zero) ve diller arası genel algoritma. HTTP sözleşmesi: Sunucu Taraflı Render API.
Her örnek aynı dört kuralı uygular. Bunların hiçbirini gevşetmeyin:
- Asla patlamaz. Zaman aşımı, ağ hatası,
4xx,5xx, bozuk gövde: hepsi boş dize döner. Ürün sayfası bizim yüzümüzden bekleyemez ve kırılamaz; tarayıcı yolu zaten yedektir. - Kısa zaman aşımı: 400 ms (bağlantı için 200 ms).
- Yanıtı 60 saniye önbelleğe alın. API zaten
Cache-Control: max-age=60gönderir; bu onun süreç içi karşılığıdır. Bir ürün sayfası dakikada bir Selwise çağrısına iner. - HTML'i olduğu gibi basın, CSS'i sayfa başına bir kez. Dönen HTML'i ayrıca kaçırmayın ya da temizlemeyin; yalnızca bu çıktı için şablon motorunun kaçışını kapatın.
Ortak ayarlar (hiçbiri NEXT_PUBLIC_ önekli ya da tarayıcıya açık değildir):
| Değişken | Değer |
|---|---|
SELWISE_SITE_KEY | Kanal anahtarı (Channels → Channel Overview → Channel Key) |
SELWISE_SSR_API_KEY | swpk_live_...; amacı Server-side rendering olan anahtar. Bkz. API Anahtarları |
SELWISE_API_URL | İsteğe bağlı. Varsayılan https://api.selwise.com/api/v1 |
Panelde kuralın Where it renders ayarı Server, with browser fallback, Slot name alanı şablonunuzdakiyle aynı olmalı.
Birden çok yuva için tek çağrı
Bir ürün sayfasında iki üç yuva varsa slots parametresine hepsini virgülle verin ve tek istek yapın. Yuva başına ayrı çağrı, soğuk önbellekte aynı ürün için üç ayrı bekleme demektir. Aşağıdaki örneklerin hepsi çoklu yuva kullanır.
PHP
Sade bir PHP işlevi (curl uzantısı gerekir). Süreç içi önbellek için APCu varsa onu, yoksa istek ömrünce bir dizi kullanır.
<?php
// selwise_ssr.php
function selwise_enrichment_slots(string $code, array $slots): array
{
$empty = ['css' => '', 'html' => array_fill_keys($slots, '')];
$siteKey = getenv('SELWISE_SITE_KEY');
$apiKey = getenv('SELWISE_SSR_API_KEY');
$code = trim($code);
if (!$siteKey || !$apiKey || $code === '' || !$slots) {
return $empty;
}
$base = rtrim(getenv('SELWISE_API_URL') ?: 'https://api.selwise.com/api/v1', '/');
$query = http_build_query(['code' => $code, 'slots' => implode(',', $slots)]);
$cacheKey = 'selwise:' . md5($code . '|' . implode(',', $slots));
static $local = [];
if (isset($local[$cacheKey])) {
return $local[$cacheKey];
}
if (function_exists('apcu_fetch')) {
$hit = apcu_fetch($cacheKey, $found);
if ($found) {
return $local[$cacheKey] = $hit;
}
}
$ch = curl_init("$base/public/sites/" . rawurlencode($siteKey) . "/ssr/enrichments?$query");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["x-selwise-api-key: $apiKey", 'Accept: application/json'],
CURLOPT_CONNECTTIMEOUT_MS => 200,
CURLOPT_TIMEOUT_MS => 400,
CURLOPT_NOSIGNAL => 1,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($body === false || $status !== 200) {
// Başarısızlığı da kısa süre hatırla: yavaş bir uç her istekte 400 ms yemesin.
return $local[$cacheKey] = $empty;
}
$data = json_decode($body, true);
if (!is_array($data) || !isset($data['slots'])) {
return $local[$cacheKey] = $empty;
}
$result = ['css' => (string) ($data['css'] ?? ''), 'html' => []];
foreach ($slots as $slot) {
$result['html'][$slot] = (string) ($data['slots'][$slot]['html'] ?? '');
}
if (function_exists('apcu_store')) {
apcu_store($cacheKey, $result, 60);
}
return $local[$cacheKey] = $result;
}
Ürün şablonunda:
<?php
require_once __DIR__ . '/selwise_ssr.php';
$sw = selwise_enrichment_slots($product['sku'], ['urun-aciklama-alti', 'sepet-ustu']);
?>
<?php if ($sw['css'] !== ''): ?><style><?= $sw['css'] ?></style><?php endif; ?>
<section class="aciklama">…mağazanın kendi açıklaması…</section>
<?= $sw['html']['urun-aciklama-alti'] /* kaçışsız: zaten temizlenmiş */ ?>
Laravel Blade'de: {!! $sw['html']['urun-aciklama-alti'] !!} (kaçışsız yazım) ve <style>{!! $sw['css'] !!}</style>.
Node ve Express
Node 18 ve üstü (yerleşik fetch ve AbortSignal.timeout). Süreç içi önbellek yalnızca bir Map'tir; birden çok süreç çalıştırıyorsanız her süreç kendi önbelleğini tutar, bu yeterlidir.
// selwise-ssr.js
const API = (process.env.SELWISE_API_URL || 'https://api.selwise.com/api/v1').replace(/\/+$/, '');
const TIMEOUT_MS = Number(process.env.SELWISE_SSR_TIMEOUT_MS) || 400;
const cache = new Map(); // anahtar -> { until, value }
function empty(slots) {
return { css: '', html: Object.fromEntries(slots.map((slot) => [slot, ''])) };
}
async function getEnrichment(code, slots) {
const siteKey = process.env.SELWISE_SITE_KEY;
const apiKey = process.env.SELWISE_SSR_API_KEY;
code = String(code || '').trim();
if (!siteKey || !apiKey || !code || slots.length === 0) return empty(slots);
const key = `${code}|${slots.join(',')}`;
const hit = cache.get(key);
if (hit && hit.until > Date.now()) return hit.value;
let value = empty(slots);
let ttl = 5_000; // başarısızlığı kısa süre hatırla
try {
const query = new URLSearchParams({ code, slots: slots.join(',') });
const res = await fetch(
`${API}/public/sites/${encodeURIComponent(siteKey)}/ssr/enrichments?${query}`,
{
headers: { 'x-selwise-api-key': apiKey, accept: 'application/json' },
signal: AbortSignal.timeout(TIMEOUT_MS),
},
);
if (res.ok) {
const data = await res.json();
value = {
css: data.css || '',
html: Object.fromEntries(slots.map((slot) => [slot, data.slots?.[slot]?.html || ''])),
};
ttl = 60_000;
}
} catch {
// zaman aşımı, ağ hatası, bozuk JSON: boş dön, sayfayı bozma
}
if (cache.size > 2000) cache.clear();
cache.set(key, { until: Date.now() + ttl, value });
return value;
}
module.exports = { getEnrichment };
Express yolunda ve EJS şablonunda:
const { getEnrichment } = require('./selwise-ssr');
app.get('/urun/:sku', async (req, res) => {
const product = await loadProduct(req.params.sku);
const sw = await getEnrichment(product.sku, ['urun-aciklama-alti', 'sepet-ustu']);
res.render('product', { product, sw });
});
<% if (sw.css) { %><style><%- sw.css %></style><% } %>
<section class="aciklama"><%= product.description %></section>
<%- sw.html['urun-aciklama-alti'] %>
<%- EJS'te kaçışsız yazımdır; Handlebars'ta karşılığı üç süslü parantezli lookup ({{{lookup sw.html "urun-aciklama-alti"}}}), Pug'da != sw.html['urun-aciklama-alti']. <%= (kaçışlı) kullanmayın: HTML'i metne çevirir ve içerik sayfada ham etiketler olarak görünür.
Next.js (App Router, server component)
server-only paketini kurun (pnpm add server-only); bu dosyanın bir istemci bileşenine yanlışlıkla alınmasını derleme zamanında engeller ve anahtarın tarayıcı paketine girmesini önler.
// lib/selwise-ssr.ts
import 'server-only';
type SsrResponse = {
v: string;
slots: Record<string, { html: string; assignmentIds: string[] }>;
css: string;
};
export async function getSelwiseEnrichment(code: string, slots: string[]) {
const empty = {
css: '',
html: Object.fromEntries(slots.map((slot) => [slot, ''])) as Record<string, string>,
};
const siteKey = process.env.SELWISE_SITE_KEY;
const apiKey = process.env.SELWISE_SSR_API_KEY;
if (!siteKey || !apiKey || !code || slots.length === 0) return empty;
const base = (process.env.SELWISE_API_URL ?? 'https://api.selwise.com/api/v1').replace(/\/+$/, '');
const query = new URLSearchParams({ code, slots: slots.join(',') });
try {
const res = await fetch(
`${base}/public/sites/${encodeURIComponent(siteKey)}/ssr/enrichments?${query}`,
{
headers: { 'x-selwise-api-key': apiKey, accept: 'application/json' },
signal: AbortSignal.timeout(400),
// API zaten max-age=60 gönderir; bunu yeniden yazmak Next'in veri
// önbelleğinin de buna uymasını sağlar.
next: { revalidate: 60 },
},
);
if (!res.ok) return empty;
const data = (await res.json()) as SsrResponse;
return {
css: data.css ?? '',
html: Object.fromEntries(slots.map((slot) => [slot, data.slots?.[slot]?.html ?? ''])) as Record<
string,
string
>,
};
} catch {
return empty;
}
}
// app/urun/[sku]/page.tsx
import { getSelwiseEnrichment } from '@/lib/selwise-ssr';
export default async function ProductPage({ params }: { params: { sku: string } }) {
const product = await getProduct(params.sku);
const sw = await getSelwiseEnrichment(product.sku, ['urun-aciklama-alti', 'sepet-ustu']);
return (
<>
{sw.css ? <style dangerouslySetInnerHTML={{ __html: sw.css }} /> : null}
<ProductDetail product={product} />
{sw.html['urun-aciklama-alti'] ? (
<div dangerouslySetInnerHTML={{ __html: sw.html['urun-aciklama-alti'] }} />
) : null}
</>
);
}
dangerouslySetInnerHTML burada doğrudur ve yalnızca burada: HTML'i API zaten yer tutucuları kaçışlayarak doldurmuş, izin listesine göre temizlemiş ve CSS'ini kapsamlamıştır. Bu değişkeni NEXT_PUBLIC_ önekiyle tanımlamayın, bir istemci bileşeninden çağırmayın ve prop olarak istemciye geçirmeyin.
Sayfa statik ya da ISR ise (export const revalidate = 3600 gibi) sayfa, siteniz başka bir ürün için bile düzenlendiğinde widget'ın gözünde bayat kalabilir; Server, with browser fallback bu durumu kendisi onarır. Sayfa süresini kısa tutun.
Django ve Jinja2 (Akinon Project Zero)
Selwise'in Akinon entegrasyon kiti bu parçanın hazır hâlini taşır (pz-classic/selwise_ssr.py); PZ Next için pz-next/selwise-ssr.ts yukarıdaki Next.js örneğiyle aynı mantıktadır. Kit olmadan kurmak için aşağıdaki dosya yeterlidir (yalnızca standart kütüphane ve Django kullanır):
# omnife_project/selwise_ssr.py
import json
import logging
import os
import urllib.parse
import urllib.request
from django.core.cache import cache
from django.utils.safestring import mark_safe
logger = logging.getLogger(__name__)
API_URL = os.environ.get("SELWISE_API_URL", "https://api.selwise.com/api/v1").rstrip("/")
TIMEOUT = float(os.environ.get("SELWISE_SSR_TIMEOUT", "0.4"))
CACHE_TTL = 60
def _fetch(code, slots):
site_key = os.environ.get("SELWISE_SITE_KEY")
api_key = os.environ.get("SELWISE_SSR_API_KEY")
if not site_key or not api_key or not code or not slots:
return None
query = urllib.parse.urlencode({"code": code, "slots": ",".join(slots)})
url = f"{API_URL}/public/sites/{urllib.parse.quote(site_key)}/ssr/enrichments?{query}"
request = urllib.request.Request(
url, headers={"x-selwise-api-key": api_key, "Accept": "application/json"}
)
try:
with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
return json.loads(response.read().decode("utf-8"))
except Exception as error: # zaman aşımı, ağ, 4xx/5xx, bozuk JSON: sayfa etkilenmez
logger.info("Selwise SSR isteği başarısız (sayfa etkilenmedi): %s", error)
return None
def selwise_enrichment_slots(code, *slots):
"""Birden çok yuvayı tek çağrıyla getirir; sonuç sözlüğünde 'css' de bulunur."""
result = {slot: mark_safe("") for slot in slots}
result["css"] = mark_safe("")
code = (code or "").strip()
key = "selwise:%s:%s" % (code, ",".join(sorted(slots)))
payload = cache.get(key)
if payload is None:
payload = _fetch(code, slots)
# Başarısızlığı da kısa süre önbelleğe al
cache.set(key, payload or {}, CACHE_TTL if payload else 5)
if not payload:
return result
for slot in slots:
result[slot] = mark_safe(((payload.get("slots") or {}).get(slot) or {}).get("html") or "")
css = payload.get("css") or ""
result["css"] = mark_safe(f"<style>{css}</style>") if css else mark_safe("")
return result
Jinja2 ortamına global olarak ekleyin:
from omnife_project.selwise_ssr import selwise_enrichment_slots
def environment(**options):
env = Environment(**options)
env.globals.update({"selwise_enrichment_slots": selwise_enrichment_slots})
return env
Şablonda:
{% set selwise = selwise_enrichment_slots(product.sku, "urun-aciklama-alti", "sepet-ustu") %}
{{ selwise["css"] }}
<section class="aciklama">…</section>
{{ selwise["urun-aciklama-alti"] }}
mark_safe sonucu Jinja2 otomatik kaçışına karşı korur; |safe yazmanıza gerek kalmaz. Tek bir yuva için kitteki {{ selwise_enrichment("urun-aciklama-alti", product.sku) }} biçimi de kullanılabilir.
Akinon'da hangi kod
Panelde kural için ne yazdıysanız (SKU, item_code ya da sepet kodu) sunucuya da onu verin. Akinon'da ürün kodu karmaşasını ve SKU/pk farkını Akinon sayfası anlatır; SSR çağrısı üçünü de katalogda çözer.
Diğer diller ve şablon motorları: genel algoritma
Bir yığın için hazır örnek yoksa aynı yedi adımı uygulayın:
- Ürün sayfası isteği gelir; şablonunuz ürün kodunu ve basmak istediği yuva adlarını bilir.
- Kodu ve yuvaları anahtar yapıp önbelleğe bakın (60 sn). Varsa 6'ya atlayın.
GET {API}/public/sites/{siteKey}/ssr/enrichments?code={kod}&slots={a,b}isteğinix-selwise-api-keybaşlığıyla, 400 ms zaman aşımıyla yapın.- Durum
200değilse, gövde bozuksa ya da zaman aşımı olduysa boş sonucu önbelleğe 5 saniye yazıp 6'ya geçin. - Başarılıysa sonucu (
cssveslots.*.html) 60 saniye önbelleğe yazın. - Şablonda
css'i sayfa başına bir kez, her yuvanınhtml'ini kendi yerinde kaçışsız basın. Boş dize basmak güvenlidir. - Başka hiçbir yere dokunmayın: widget geri kalanını devralır.
HTTP isteği tek başına şudur; bir curl ile her yığında kolayca denenir:
curl -s -G "https://api.selwise.com/api/v1/public/sites/SITE_KEY/ssr/enrichments" \
-H "x-selwise-api-key: swpk_live_..." \
--data-urlencode "code=ABC-1" \
--data-urlencode "slots=urun-aciklama-alti,sepet-ustu"
Önbellek: sizin sayfa önbelleğiniz
Ürün sayfasını bir CDN ya da tam sayfa önbelleğiyle sunuyorsanız, o önbellek SSR çıktısını da içinde taşır ve panelde yapılan bir düzenleme oraya ulaşmaz. Önerilen:
- Sayfa önbelleğinin ömrünü kısa tutun (dakikalar, en çok birkaç saat). Ne kadar uzun tutarsanız, düzenleme o kadar geç yansır ve Server only kuralları o kadar çok kaybolur.
- Önbellek anahtarına çerez, oturum ve kimlik koymayın: yanıt ziyaretçiye göre değişmez ve ziyaretçi başına parçalanmış bir önbellek yalnızca verimsizdir.
Doğrulama
Örneklerden biri kurulduktan sonra Sunucu Taraflı Render sayfasındaki curl kontrollerini çalıştırın. Hızlı kontrol listesi:
curlile alınan HTML'dedata-selwise-ssr="1"var.- Tarayıcıda yuva başına yalnızca bir kök var.
- SSR anahtarını bilerek bozup sayfayı yeniden yükleyin: sayfa hatasız açılmalı ve içerik tarayıcıdan (kural Server, with browser fallback ise) gelmeli. Bu, fail-open davranışının kanıtıdır.
- Sunucu günlüğünüzde SSR çağrısının süresine bakın: sağlıklı bir çağrı onlarca milisaniyedir; 400 ms'ye yakınsa ağ yolunuzu inceleyin.
Sırada ne var
- Sunucu Taraflı Render: mimari ve panel
- Sunucu Taraflı Render API: HTTP sözleşmesi
- Akinon: Akinon'a özgü notlar
Son güncelleme: 10 Ekim 2026