ecommerce-insight-mcp
README.md
# E-Commerce Insight MCP
**Türkçe** · 🇬🇧 [English](README.en.md)
Ürün ve kategori sayfalarından sunum için veri çeken bir MCP sunucusu. Üç araç sunar:
| Araç | Ne yapar |
|---|---|
| `extract_product_presentation_data` | Ürün sayfasından başlık, görseller ve temizlenmiş metin |
| `extract_structured_product_schema` | JSON-LD + OpenGraph: fiyat, stok, marka, puan |
| `get_category_presentation_data` | Kategori sayfasından öne çıkan ürün listesi |
| `extract_seller_trust_signals` | Satıcı puanı, kargoyu kimin yaptığı, iade durumu + gerekçeli karar özeti |
| `get_presentation_style_guide` | Sabit sunum paleti, tipografi ve grafik kuralları |
| **`analyze_category`** | **Birden çok pazaryerinin kategorisi + tüm ürünler tek çağrıda** — sunum için giriş noktası |
| **`build_slide_visual`** | **Tema-sadık SVG görseller**: yolculuk haritası, halka, waffle, konumlandırma, sıralı bar |
Canlı sunucu: **https://ecommerce-insight-mcp.onrender.com**
---
## Kullanım yolları
Üç ayrı yol var. MCP client'ınız yoksa ya da terminal kullanmıyorsanız 1. yol yeterli.
### 1. Tarayıcıdan (kurulum gerekmez)
**https://ecommerce-insight-mcp.onrender.com** adresini açın, linki yapıştırın, butona basın.
> Sunucu ücretsiz katmanda çalıştığı için uykudan uyanması gerekebilir — **ilk istek 1 dakikaya kadar sürebilir**, sonrakiler hızlıdır.
### 2. curl / Postman / n8n / Google Sheets
Düz `GET` endpoint'leri, JSON döner. Handshake, session, SSE yok:
```bash
curl "https://ecommerce-insight-mcp.onrender.com/api/product?url=https://ornek.com/urun"
curl "https://ecommerce-insight-mcp.onrender.com/api/schema?url=https://ornek.com/urun"
curl "https://ecommerce-insight-mcp.onrender.com/api/category?url=https://ornek.com/kategori"
curl "https://ecommerce-insight-mcp.onrender.com/api/seller?url=https://ornek.com/urun"
curl "https://ecommerce-insight-mcp.onrender.com/api/analyze?url=https://ornek.com/kategori&limit=8"
```
Sunucunun ayakta olup olmadığını kontrol etmek için:
```bash
curl https://ecommerce-insight-mcp.onrender.com/health
```
### 3. MCP client ile
```json
{
"mcpServers": {
"ecommerce-insight": {
"url": "https://ecommerce-insight-mcp.onrender.com/mcp"
}
}
}
```
Client'ınız uzak sunucu URL'sini desteklemiyorsa (yalnızca stdio destekleyen eski sürümler), araya köprü koyun:
```json
{
"mcpServers": {
"ecommerce-insight": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://ecommerce-insight-mcp.onrender.com/mcp"]
}
}
}
```
**Not:** `/mcp` yolu doğrudan curl ile çağrılamaz. MCP Streamable HTTP protokolü `Accept: application/json, text/event-stream` başlığını, `initialize` → `notifications/initialized` → `tools/call` sırasını ve `Mcp-Session-Id` başlığını ister; yanıtı da SSE frame'i olarak verir. Terminalden kullanmak için 2. yolu tercih edin.
---
## Yerel kurulum
Yerel kurulum, Playwright yedeğini de açar. Bot korumalı siteler için genellikle gerekmez (aşağıdaki TLS taklidi bunu zaten hallediyor), ama JavaScript ile render edilen sayfalarda işe yarar.
```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Bot korumalı siteler için:
pip install playwright==1.62.0
playwright install chromium
```
MCP client config'i (stdio):
```json
{
"mcpServers": {
"ecommerce-insight": {
"command": "/tam/yol/.venv/bin/python",
"args": ["/tam/yol/ecommerce_mcp.py"]
}
}
}
```
HTTP sunucusu olarak çalıştırmak için:
```bash
.venv/bin/python -m uvicorn ecommerce_mcp:app --port 8000
```
Ardından `http://localhost:8000` adresini açın.
---
## Yerel test
### En hızlı kontrol
```bash
.venv/bin/python -m uvicorn ecommerce_mcp:app --port 8000
```
Başka bir terminalde:
```bash
curl -s http://localhost:8000/health
```
`impersonate` ve `playwright` ikisi de `true` ise her şey hazır:
```json
{"status":"ok","impersonate":true,"playwright":true,"tools":["category","product","schema"]}
```
### Sunum çıktısı
```bash
curl -s "http://localhost:8000/api/product?url=<ÜRÜN_LİNKİ>" | python -m json.tool
```
`title`, `product_images` (en fazla 6 farklı fotoğraf) ve `extracted_content` döner.
Fiyat, stok, marka ve puan için şema aracını kullanın:
```bash
curl -s "http://localhost:8000/api/schema?url=<ÜRÜN_LİNKİ>" | python -m json.tool
```
Her site şemasını `Product` diye etiketlemiyor — Trendyol örneğin `ProductGroup` kullanıyor ve fiyatları `hasVariant` altına koyuyor.
Satıcı kararı için:
```bash
curl -s "http://localhost:8000/api/seller?url=<ÜRÜN_LİNKİ>" | python -m json.tool
```
### Tarayıcı formu
`http://localhost:8000` adresini açın, linki yapıştırın, butona basın. Aynı veri, terminal yok.
### Araçları doğrudan test etmek
`python test_local.py` üç aracı da örnek linklerle çağırıp ham JSON'u basar.
### Bulut ortamını taklit etmek
Sunucunun Render'da nasıl davrandığını yerelde görmek için tarayıcı yedeğini kapatarak başlatın:
```bash
ENABLE_PLAYWRIGHT=0 .venv/bin/python -m uvicorn ecommerce_mcp:app --port 8000
```
### MCP protokolünü test etmek
`/mcp` tam handshake ister. Elle doğrulamak için:
```bash
# 1. initialize — yanıt başlıklarındaki Mcp-Session-Id'yi not edin
curl -i -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}'
# 2. araçları listele (1. adımdaki session id'yi yapıştırın)
curl -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: <SESSION_ID>" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
```
---
## Satıcı güven sinyalleri
`extract_seller_trust_signals` aracı "hangi ürünü ve satıcıyı neden seçmeli" sorusunu gerekçelendirmek için tasarlandı. Ham alanların yanında `decision_summary` döndürür: **strengths** (kararı destekleyen), **concerns** (uyarı) ve **unknowns** (bilinemeyen).
Trendyol örneği:
```json
{
"seller": {
"name": "SuperStep",
"official_name": "EREN PERAKENDE VE TEKSTİL ANONİM ŞİRKETİ",
"city": "İstanbul",
"score": 8.4, "score_scale": 10
},
"logistics": {
"fulfilled_by": "Satıcı kendi gönderiyor (pazaryeri)",
"refundable": true, "free_shipping": false, "max_installment": 12
},
"decision_summary": {
"strengths": ["Satıcı puanı 8.4/10 — iyi.", "Ürün iade edilebilir.", "629 favori — talep görüyor."],
"concerns": [
"Ürün puanı 5 ama yalnızca 1 yoruma dayanıyor — istatistiksel olarak zayıf.",
"Kargoyu satıcı kendi yapıyor — kargo ve iade süreci satıcının performansına bağlı.",
"Stok tükeniyor — fiyat ve bulunurluk değişebilir."
]
}
}
```
### Neyin ölçülebildiği, neyin ölçülemediği
Kargo hasar/kayıp oranı, müşteri hizmetleri yanıt kalitesi ve iade taleplerinde ulaşılabilirlik **hiçbir pazaryerinde satıcı bazında yayınlanmıyor.** Bu yüzden çıktıda `unknowns` listesi var — sunumda "veri yok" ile "sorun yok" karışmasın diye.
Bu üçünün yerine geçen iki gösterge var:
- **`seller.score`** — platformun tam olarak bu faktörlerden hesapladığı bileşke puan.
- **`logistics.fulfilled_by`** — kargoyu platform deposu mu yoksa satıcı mı yapıyor. Satıcı kendi gönderiyorsa kargo ve iade süreci tamamen o satıcının performansına bağlıdır; bu, kargo riskinin en iyi yayınlanan vekil göstergesidir.
Ayrıca ürün puanı **her zaman yorum sayısıyla birlikte** değerlendirilir: 1 yoruma dayanan 5/5, 500 yoruma dayanan 4.3'ten zayıf bir sinyaldir.
### Kaynak farkı
`source` alanı verinin nereden geldiğini söyler:
| Değer | Anlamı |
|---|---|
| `trendyol_embedded` | Trendyol'un sayfaya gömdüğü veri — yukarıdaki tüm alanlar dolu gelir |
| `json_ld` | Siteden bağımsız JSON-LD `offers.seller` — genelde yalnızca satıcı adı |
`json_ld` kaynağında alanların boş gelmesi **satıcının zayıf olduğu anlamına gelmez**, site o veriyi yayınlamıyor demektir. Hiç sinyal bulunamazsa `decision_summary.notes` bunu açıkça söyler.
---
## Tool kullanım sınırına takılmamak
Kategori analizi için ürün başına ayrı ayrı `extract_*` çağırmak client'ın tool kullanım sınırını doldurur. 8 ürünlük bir analiz bu şekilde **25 tool çağrısı ve ~66.000 token** demekti; en büyük pay ham JSON-LD dökümündendi (tek üründe 23 KB, bunun 8 KB'ı `hasVariant`).
İki değişiklikle çözüldü:
**1. `analyze_category` — tek çağrı.** Kategoriyi ve içindeki ürünleri eşzamanlı çeker, yalnızca sunuma giren alanları döndürür.
```bash
curl -s "http://localhost:8000/api/analyze?url=<KATEGORİ_LİNKİ>&limit=8" | python -m json.tool
```
**Birden çok pazaryeri tek çağrıda.** `url` virgülle ayrılmış birden çok adres alır; her ürün hangi pazaryerinden geldiğiyle döner:
```bash
curl -s -G "http://localhost:8000/api/analyze" \
--data-urlencode "url=https://www.trendyol.com/<kategori>,https://www.hepsiburada.com/ara?q=<terim>,https://www.amazon.com.tr/s?k=<terim>" \
--data-urlencode "limit=4" | python -m json.tool
```
Ölçüldü: üç pazaryeri, 12 ürün, **7.3 saniye, tek çağrı**. Bir kaynak okunamazsa `sources` içinde hatasıyla görünür ve diğerleri devam eder; hiçbiri okunamazsa araç hata döndürür.
**2. Ham şema budandı.** `extract_structured_product_schema` artık `key_facts` (ad, marka, fiyat, puan, yorum sayısı, stok) döndürüyor; ham şemadan `hasVariant`, `isRelatedTo`, `additionalProperty` gibi alanlar çıkarılıyor.
Ölçülen fark:
| | Önce | Sonra |
|---|---|---|
| Şema aracı (tek ürün) | 23.474 karakter (~6.700 token) | 4.983 karakter (~1.423 token) |
| 8 ürünlük kategori analizi | 25 çağrı, ~66.000 token | **1 çağrı, ~1.261 token** |
`analyze_category` çıktısındaki `summary.average_rating` yalnızca **yorumu olan** ürünlerden hesaplanır; kaç ürüne dayandığı `rated_product_count` alanında verilir. Yorum almamış ürünün `0` puanı ortalamaya katılmaz.
---
## Playwright katmanı
Playwright son çaredir: `curl_cffi` → `httpx` → **Playwright**. TLS taklidi bot korumalarının çoğunu zaten geçtiği için nadiren devreye girer, ama JavaScript ile render edilen sayfalarda gerekir. Yalnızca yerel kurulumda çalışır.
### Paylaşılan tarayıcı
Her çağrıda yeni bir Chromium açılıyordu — istek başına ~2 saniye sabit maliyet ve sekiz ürünlük bir analizde sekiz ayrı tarayıcı süreci. Artık **tek Chromium paylaşılıyor**; izolasyon istek başına yeni bağlam (context) açılarak sağlanıyor, çerezler istekler arasında sızmıyor.
| | Süre |
|---|---|
| 5 ardışık çağrı, her seferinde yeni tarayıcı | 10,68 sn |
| 5 ardışık çağrı, paylaşılan tarayıcı | **9,03 sn** (%15 kazanç) |
| 8 eşzamanlı çağrı, 2'li semafor | 7,06 sn |
Asıl kazanç eşzamanlılıkta: eski kod 8 paralel istekte 8 ayrı Chromium açardı ve dar bellekli ortamda OOM riski taşırdı. `PLAYWRIGHT_MAX_CONCURRENCY` (varsayılan 2) bunu sınırlar.
### Dayanıklılık
- **Çökme kurtarması:** tarayıcı ölürse bir sonraki çağrıda kendiliğinden yeniden başlar.
- **Temiz kapanış:** sunucu kapanırken Chromium serbest bırakılır. Ölçüldü — süreç sayısı 0 → 3 → 0, sızıntı yok.
- **İndirme tuzağı:** bazı siteler bot şüphesinde sayfa yerine dosya indirmesi tetikliyor; Playwright'ın ham hatası bunu anlatmadığı için açıklayıcı mesaja çevriliyor.
- **Otomasyon izleri:** `navigator.webdriver` tek başına yetmiyor; dil listesi, eklenti sayısı, `window.chrome` ve permissions sorgusu da maskeleniyor.
### Denenip çıkarılan: görsel/font engelleme
Görsel, font ve medya isteklerini engellemek sezgisel olarak hızlandırmalıydı. **Ölçüm tersini söyledi** ve özellik çıkarıldı:
| Yöntem | Trendyol ürün sayfası |
|---|---|
| Engelleme yok | **3,47 sn** |
| `route("**/*")` + resource_type | 7,39 sn |
| Uzantı deseni ile engelleme | 7,39 sn |
İki nedenden: `route("**/*")` her isteği Python'a round-trip ettiriyor, ve daha önemlisi iptal edilen istekler yüzünden `networkidle` hiç oturmuyor — her çağrıda 6 saniyelik zaman aşımı yanıyor. Tasarruf edilen bant genişliği bu maliyeti karşılamıyor.
---
## Slayt görselleri
`build_slide_visual` sunum görsellerini **tema içinde** üretir: renk ve yazı tipi seçimi çağırana bırakılmaz, hepsi sabit paletten gelir. Dönen SVG kendi kendine yeter (harici font/asset yok) ve hem HTML sunumlara hem PowerPoint'e gömülebilir.
| Tip | Ne için |
|---|---|
| `cover` | **Kapak slaytı** — koyu zemin, sıcak vurgu, büyük serif başlık. Sunumun ilk slaytı bununla üretilir |
| `journey_map` | Müşteri yolculuğu haritası — aşamalar, her aşamada ölçü ve not |
| `donut` | Tek ölçülü halka göstergeleri — çok dilimli pastanın modern karşılığı |
| `quadrant` | Fiyat–puan konumlandırma; daire büyüklüğü yorum sayısı |
| `waffle` | 100 kareli oran ızgarası — pastanın okunabilir alternatifi |
| `ranked_bars` | Büyükten küçüğe sıralı yatay bar |
```bash
curl -s -G "http://localhost:8000/api/visual" \
--data-urlencode "kind=donut" \
--data-urlencode 'data={"rings":[{"label":"Ortalama puan","value":4.2,"max":5,"display":"4.2","caption":"3 üründen"}]}' \
--data-urlencode "title=Kategori Sağlık Göstergeleri" > gorsel.svg
```
### Pasta grafik kuralı — bilinçli revizyon
Tasarım sistemi önce **tüm pasta ve halka grafikleri** yasaklıyordu. Bu kural şöyle inceltildi:
- **Yasak kalan:** çok dilimli pasta (`multi_slice_pie`) ve çok serili halka. Okuyucu dilim açılarını gözle kıyaslayamaz — asıl sorun buydu.
- **Serbest olan:** tek ölçülü halka (`donut`). Tek bir oranı gösterir, kıyaslama sorunu doğurmaz; modern sunum araçlarının KPI göstergesi budur.
- **Oran dağılımı için:** `waffle`. Okuyucu kare sayar, açı tahmin etmez.
### Çarpık fiyat verisi
`quadrant` görselinde fiyat ekseni için `"x_scale": "log"` verin. Kategori fiyatları tipik olarak çarpık dağılır (1.290 TL ile 44.500 TL bir arada); doğrusal eksende noktalar sola yığılır, logaritmik eksen segmentleri ayırır. Eksen etiketine "(log ölçek)" notu otomatik eklenir.
### Geçici bloklar ve yeniden deneme
Bot korumaları `403/429/503`'ü çoğu zaman **kalıcı yasak değil, anlık hız sınırı** olarak döndürür — aynı adres saniyeler sonra 200 verebilir. Önceden tek denemede pes ediliyordu ve bir pazaryeri sunumdan tamamen düşüyordu.
Artık engellenen statülerde **artan beklemeyle 3 deneme** yapılıyor (1,5 sn → 3 sn). Geçici bloklar kurtarılıyor; kalıcı olanlar yine hata döndürüyor ve `sources` içinde görünüyor.
Ölçülen davranış farkı — aynı anda çalıştırılan testlerde:
| Kaynak | Yerel makine | Render (bulut) |
|---|---|---|
| Trendyol | çalışıyor | **0 ürün** (IP bazlı içerik ayrımı) |
| Hepsiburada | çalışıyor | çalışıyor |
| Amazon.com.tr | 503 (kalıcı, bu IP'den) | çalışıyor |
| N11 | çalışıyor | çalışıyor |
İki ortam birbirinin tersini yaşayabiliyor: **Trendyol yerelde, diğerleri bulutta daha güvenilir.** Kapsamlı bir karşılaştırma için ikisini birlikte kullanmak gerekebilir.
### Arama sonucu alaka sorunu
`analyze_category`'ye arama adresi verdiğinizde pazaryerinin kendi sıralaması geçerlidir. Örneğin N11'de `fujifilm fotograf makinesi` araması film, çanta ve tripod gibi aksesuarları öne çıkarabiliyor. Bu bir engelleme değil, alaka sorunudur — daha dar bir arama adresi (marka filtresi uygulanmış kategori linki) belirgin biçimde daha temiz sonuç verir.
---
## Sonuç bölümü zorunludur
Sunum, **"Yapay Zekâ Özeti ve Sonuç"** slaytıyla biter. Veriyi gösterip yorumsuz bırakmak, çıkarımı okuyucuya bırakmak demektir.
`analyze_category` çıktısındaki **`findings`** bu bölümün ham maddesidir — veriden doğrudan türetilmiş, elle doğrulanabilir gözlemler:
```
* 12 üründen 7 tanesinin hiç yorumu yok; kategori ortalaması yalnızca
yorumu olan ürünlerden hesaplandı.
* Fiyat aralığı 1.290 TL – 68.500 TL; en pahalı ürün en ucuzun 53 katı.
Kategori tek bir segment değil.
* 'Rosem Ahsap' listedeki 4 üründe satıcı konumunda — satıcı yoğunlaşması var.
* 4 üründe kargoyu satıcı kendi yapıyor; teslimat ve iade deneyimi satıcıya bağlı.
```
Sonuç slaytının kuralları (`get_presentation_style_guide` → `conclusion`):
- Koyu zeminde (`#1E293B`), sunumun **son** slaytı.
- Önce 2-3 cümlelik özet paragrafı, ardından 01-04 numaralı 3-4 eyleme dönük öneri.
- Her öneri hangi bulguya dayandığını söyler: *"X olduğu için Y yap."*
- Özet yalnızca `findings` ve sunumda gösterilen sayılardan türetilir — yeni veri uydurulmaz.
- Okunamayan kaynak varsa sonuçta açıkça belirtilir.
---
## Sunum tasarım sistemi
Sunumlar arasında görünüm tutarlılığını sağlamak için palet, tipografi ve grafik kuralları sunucuda sabitlenmiştir. Onaylanan referans sunumdan ölçülerek çıkarıldı.
### Palet
| Rol | Renk |
|---|---|
| Yüzey | `#FFFFFF` · `#F1F5F9` · `#F7F5F2` |
| Çizgi | `#E2E8F0` · `#CBD5E1` |
| Metin | `#1E293B` · `#334155` · `#6B7280` · `#94A3B8` |
| Koyu zemin | `#1E293B` · `#24334A` |
| **Vurgu seti** | **`#D97706`** amber · **`#0F766E`** teal · **`#DC5F4E`** terracotta · **`#15803D`** yeşil |
Kategorik ayrım gereken yerde vurgu seti **sırayla** kullanılır; beşinci renk eklenmez. Mavi ton yoktur — lacivert yalnızca koyu slayt zemini ve metin rengidir. Kapak ve bölüm ayracı koyu, içerik slaytları açık zemindedir.
### Yerleşim — metin çakışmasını önleme
Alt başlık, başlığa **sabit dikey ofsetle değil**, başlık kutusunun alt kenarına göre konumlandırılır. Başlık kutusu için her zaman iki satırlık yükseklik ayrılır. Başlık 45 karakteri aşarsa metin kısaltılır — punto küçültülmez. Bu kural, başlığın iki satıra taşıp alt başlığın üstüne binmesini önlemek için eklendi.
### Tipografi
Başlık **Georgia**, gövde **Calibri**. Başlık 28pt · bölüm 16pt · alt başlık 13pt · gövde 11pt · açıklama 9pt. Gövde 11pt'nin altına düşürülmez — sığmıyorsa içerik azaltılır, punto değil.
### Grafikler
İzin verilen: **bar, yatay bar, tablo, KPI kutusu.** Yasak: **pasta, halka, 3D, radar, alan.** Oran göstermek için yatay bar kullanılır — gözle kıyaslaması pastadan kolaydır. Seri rengi tek: `#D97706` ya da `#0F766E`; ızgara `#E2E8F0`.
### Yoğunluk
Slayt başına en fazla 11 şekil, sunum başına en fazla 4 daire/halka biçimi, dekoratif görsel yok. Görsel yalnızca ürün fotoğrafı olarak, veriyi gösterdiği yerde kullanılır.
### Nasıl uygulanır
Üç yoldan da aynı veri gelir:
```bash
curl -s http://localhost:8000/api/style | python -m json.tool
```
- **Araç:** `get_presentation_style_guide` — model veriyi çekerken stili de okuyabilsin diye.
- **Kaynak:** `style://presentation`
- **Prompt:** `sunum_hazirla(url)` — veri çekme + stile uyma talimatını tek adımda verir.
---
## Yapılandırma
| Ortam değişkeni | Varsayılan | Açıklama |
|---|---|---|
| `ENABLE_PLAYWRIGHT` | `1` | `0` yapıldığında tarayıcı yedeği kapanır. Render'da kurulu olmadığı için kod bunu kendiliğinden tespit eder; ayarlamak zorunda değilsiniz. |
Sunucu yalnızca public `http`/`https` adreslerine istek atar; loopback, iç ağ ve bulut metadata adresleri (`169.254.169.254`) reddedilir.
---
## Bot koruması nasıl aşılıyor
Cloudflare (Trendyol) ve Akamai (Hepsiburada) gibi korumalar istemciyi büyük ölçüde **TLS el sıkışmasından** tanır. Python'un `httpx`'i kendine özgü bir parmak izi bırakır (JA4 `t13d1712h1`, HTTP/1.1) ve bu doğrudan ele verir — datacenter IP'siyle birleşince 403 gelir.
Sunucu bu yüzden birincil çekici olarak **`curl_cffi`** kullanır: gerçek Chrome'un TLS parmak izini (JA4 `t13d1516h2`, HTTP/2) sunar. Ölçülen fark, aynı IP ve aynı User-Agent ile:
| Site | `httpx` | `curl_cffi` |
|---|---|---|
| Hepsiburada | 403 (1.7 KB blok sayfası) | 200 (610 KB, JSON-LD dahil) |
Tarayıcı çalıştırmadığı için Playwright'ın aksine ek RAM ya da Chromium binary'si istemez; ücretsiz katmanda sorunsuz çalışır.
Çekim sırası: **`curl_cffi` → `httpx` (curl_cffi yoksa) → Playwright (yalnızca yerel, JS render için)**.
### Aşılamayan durum: IP bazlı içerik ayrımı
Trendyol, bulut sunucusunun IP'sine **içerik olarak farklı bir sayfa** servis ediyor. Ölçüm — aynı URL (`https://www.trendyol.com/`), aynı anda:
| İstek nereden | Sayfa başlığı | Bulunan ürün linki |
|---|---|---|
| Render (bulut) | "Online Alışveriş Sitesi, Türkiye'nin Trend Yolu \| Trendyol" | 0 |
| Yerel makine | "En Trend Ürünler Türkiye'nin Online Alışveriş Sitesi Trendyol'da" | 8 |
Bu ne TLS parmak izi, ne çerez, ne de JavaScript sorunu — sunucu daha isteği karşılarken IP'ye bakıp karar veriyor. Dolayısıyla TLS taklidi, oturum çerezi ya da Playwright bunu **çözmez**; hepsi aynı IP'den çıkar.
**Trendyol için çözüm: sunucuyu yerelde çalıştırın.** Bulut sunucusu Hepsiburada, Amazon ve korumasız mağazalarda tam veri vermeye devam eder. Trendyol istendiğinde hata değil, kısmi veri + açıklayıcı `warning` alanı döner.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues