feat: Implement Phase 2 features including blog, collections, saved listings, manifest, and api cron sync

This commit is contained in:
AyrisAI
2026-07-12 20:49:41 +03:00
parent 862544b5a1
commit f696d359b0
29 changed files with 3066 additions and 194 deletions
+195
View File
@@ -0,0 +1,195 @@
# Faz 2 Planı — Marmaris Local
**Önceki:** `prd.md` (MVP — tamamlandı)
**Bu doküman:** MVP sonrası geliştirme sırası
Sıralama mantığı: önce trafik getiren şey (içerik/SEO), sonra geleni tutan şey (etkileşim), sonra parayı getiren şey (gelir modeli — ama listeleme sayısı bir eşiğe ulaşmadan anlamsız), en altta her an eklenebilecek düşük efor altyapı işleri.
---
## Faz 2.1 — İçerik / SEO Motoru (öncelik: yüksek, ilk başlanacak)
Directory sitesi tek başına trafik çekmez — kürasyonlu içerik olmadan sadece Google'ın zaten indekslediği işletmelerin bir kopyası olursun. Bu faz olmadan diğerlerinin anlamı sınırlı.
### Blog
```prisma
model BlogPost {
id String @id @default(cuid())
slug String @unique
titleTr String
titleEn String
titleRu String
contentTr String // MDX/rich text
contentEn String
contentRu String
coverImage String? // Openinary
tags String[]
relatedListingIds String[] // Listing'lere internal link için
publishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
}
```
- Route: `/blog`, `/blog/[slug]`
- Admin: `/admin/blog` — rich text editör (Tiptap öneri, shadcn ile uyumlu)
- Her yazıda ilgili `Listing`'lere otomatik link kartı (internal linking = SEO)
- `schema.org Article` structured data
**İlk içerik önerisi (mock/gerçek karışık başlanabilir):** "Marmaris'te Nerede Yenir 2026", "Gün Batımı İçin 5 Mekan", "İçmeler mi Yat Limanı mı — Nerede Kalınır"
### Kürasyon Listeleri (Koleksiyonlar)
```prisma
model Collection {
id String @id @default(cuid())
slug String @unique
titleTr String
titleEn String
titleRu String
descriptionTr String
descriptionEn String
descriptionRu String
coverImage String?
listings Listing[] @relation("CollectionListings")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
- Route: `/seckiler`, `/secki/[slug]`
- Admin: `/admin/collections` — listeleme seç (multi-select), sürükle-bırak sıralama
- Örnek koleksiyonlar: "Aile Dostu 5 Restoran", "Bütçe Dostu Mekanlar", "Deniz Kenarında Kahvaltı"
- Bu, "Yerel Onaylı" mührünün doğal uzantısı — tekil rozetten küratörlü gruplamaya geçiş
### RSS
- `/feed.xml` — blog yazıları + yeni eklenen "Yerel Onaylı" listelemeler
- Öncelik gerekçesi: kendi başına trafik kanalı değil ama Google News / ileride newsletter otomasyonu (n8n) için altyapı — blog ile aynı sprintte, 1 saatlik ek iş
---
## Faz 2.2 — Etkileşim (öncelik: orta, 2.1 ile paralel gidebilir)
Auth gerektirmeyen, dönüşümü ucuza artıran işler.
### Favorilere Ekle
- **Backend yok** — client-side localStorage, `savedListingIds: string[]`
- Route: `/kaydedilenler` — localStorage'daki ID'leri okuyup listeler
- Paylaşılabilir link: `/kaydedilenler?ids=abc,def,ghi` — arkadaşla link paylaşımı, auth şart değil
### WhatsApp Paylaş
- Her listeleme detay sayfasında `wa.me/?text=...` deep link buton
- Şema değişikliği yok, ~30 dakikalık iş
### Instagram Feed (her listeleme sayfasında)
Sadece anasayfa değil — Instagram hesabı olan **her** listeleme (kategori fark etmez) kendi detay sayfasında son gönderilerini gösterir + profile giden link.
Veri kaynağı: RapidAPI üzerinden bir Instagram scraper endpoint'i (ör. "Instagram Scraper API2" / "Instagram Data" — RapidAPI marketplace'te birden fazla sağlayıcı var, fiyat/limit karşılaştırıp seçilecek). Instagram'ın resmi Graph API'si business-verified hesap + Facebook app review gerektirdiği için, işletme sahiplerinden bu izni almak pratik değil — RapidAPI scraper daha az sürtünmeli.
**Canlı istekte RapidAPI çağırma — YAPMA.** Her sayfa görüntülemesinde çağırırsan hem yavaş hem RapidAPI faturası trafiğe bağlı, öngörülemez olur. Bunun yerine 24 saatte bir çalışan bir arka plan job'ı ile önbelleğe al:
```prisma
model InstagramFeedCache {
id String @id @default(cuid())
listingId String @unique
handle String
posts Json // [{ imageUrl, caption, permalink, postedAt }]
fetchedAt DateTime @default(now())
listing Listing @relation(fields: [listingId], references: [id])
}
```
- `Listing.instagram` alanı zaten PRD'de vardı (handle/URL) — bunu kaynak olarak kullan
- Senkron mekanizması: n8n'de günlük (24h) tetiklenen bir workflow — `instagram` dolu olan tüm `Listing`'leri gez, her biri için RapidAPI'yi çağır, sonucu `POST /api/cron/instagram-sync` (secret header ile korunan internal endpoint) üzerinden `InstagramFeedCache`'e yaz. n8n zaten elinde (auto2.ayris.tech), bu tam onun işi.
- Frontend sadece DB'den okur, hiçbir zaman canlı RapidAPI çağrısı yapmaz — sayfa yükü hızlı kalır
- `fetchedAt` 24 saatten eskiyse UI'da sessizce eski veriyi göstermeye devam et (job bir sonraki döngüde tazeleyecek), kullanıcıya hata gösterme
### Menüyü Gör Butonu
Restoran/bar/club tarzı kategorilerde, işletmenin kendi menü sayfası/PDF'i varsa, listeleme detay sayfasında "Menüyü Gör" butonu → yeni sekmede işletmenin kendi sitesine/menü linkine gönderir.
```prisma
// Listing modeline eklenecek alan
menuUrl String?
```
- Kategoriye göre kod içinde zorlama yok — buton sadece `menuUrl` doluysa görünür, admin doğal olarak bunu sadece ilgili kategorilerde dolduracak
- Admin formunda (`/admin/listings/[id]`) yeni alan: "Menü Linki (opsiyonel)"
### "Yeni Eklenenler" Widget
- Şema değişikliği yok — `Listing.findMany({ orderBy: createdAt desc, take: 6 })`
- Anasayfa + kategori sayfalarında taze içerik sinyali
---
## Faz 2.3 — Gelir Modeli: Öne Çıkan Listeleme (öncelik: koşullu — belirli bir listeleme sayısına ulaşınca anlamlı)
**Ön koşul:** En az ~30-40 aktif "Yerel Onaylı" listeleme olmadan bu faza girmeye gerek yok — az listelemeyle "öne çıkan" kavramının hiçbir ayırt edici gücü olmaz.
```prisma
// Listing modeline eklenecek alanlar
isFeatured Boolean @default(false)
featuredUntil DateTime? // otomatik süre dolumu için
```
- Kategori/anasayfa sıralamasında `isFeatured` önce gelir
- Admin toggle + tarih seçici (`/admin/listings/[id]`)
- Gelir mekanizması: Ayris Tech'in mevcut 40+ müşteri ilişkisiyle doğal satış kanalı — ayrı bir ödeme altyapısı MVP'de gerekmez, manuel faturalama yeterli
---
## Faz 2.4 — Altyapı (öncelik: düşük efor, istenilen an araya sokulabilir)
### PWA
- `manifest.json` + service worker (`next-pwa` veya manuel)
- "Ana ekrana ekle" — turist telefonda kullanıyor, pratik fark yaratır
### Analytics
- Kendi VPS'teki self-hosted panel (Plausible-tarzı, zaten var) — yeni SaaS ödemesi gerekmiyor
- Takip edilecek event'ler: favori ekleme, WhatsApp paylaşım tıklaması, QR kaynak trafiği, kategori bazlı arama
---
## Önerilen Sıra
1. **Blog + Koleksiyonlar** (2.1) — SEO motoru olmadan geri kalanının önemi düşük
2. **Menü butonu + Instagram feed + Favoriler + WhatsApp paylaş + Yeni Eklenenler** (2.2) — düşük-orta efor, hızlı kazanım, 2.1 ile aynı sprint'te bile yapılabilir. Instagram feed'in n8n cron tarafı ayrı bir iş parçası olarak planlanmalı (senkron job önce kurulmalı, sonra frontend)
3. **PWA + Analytics** (2.4) — ne zaman uygun olursa, bağımsız işler
4. **Öne Çıkan Listeleme** (2.3) — listeleme sayısı eşiğe ulaşınca devreye al
## Env Değişkenleri (Faz 2 eklentileri)
```env
RAPIDAPI_KEY=""
RAPIDAPI_INSTAGRAM_HOST="" # seçilen sağlayıcıya göre (ör. instagram-scraper-api2.p.rapidapi.com)
CRON_SECRET="" # /api/cron/instagram-sync endpoint'ini korumak için
```
## Kararlar (önceki açık sorulardan)
- **Blog içeriği:** DeepSeek ile üretilip otomasyonla (n8n) yayınlanacak — akış detayı ayrıca kurulacak
- **Koleksiyon kapak görselleri:** stok veya yerinde çekim fark etmiyor, ikisi de kullanılabilir
- **Öne çıkan listeleme fiyatlandırması:** aylık sabit ücret (süre bazlı değil)
- **RapidAPI sağlayıcısı:** free-tier bir model kullanılacak
**Free-tier RapidAPI konusunda dikkat edilecek nokta:** RapidAPI'deki Instagram scraper'ların çoğunda (ör. "Instagram Scraper API2", "Instagram Looter2") free/basic plan genelde aylık ~50-100 istek civarında sınırlı. 24 saatte bir senkron ile bile, listeleme sayısı arttıkça (30-40 listeleme × günlük = ~900-1200 istek/ay) free tier hızla yetersiz kalır. Pratik yaklaşım:
- Az listelemeyle (MVP/Faz 2 başlangıcı) free tier rahatça yeter
- Listeleme sayısı büyüdükçe ya senkron sıklığını düşür (24h yerine 48-72h), ya sadece `isLocalApproved` olanları senkronla, ya da o noktada ücretli tiere geç
- İlk kurulumda 2-3 free-tier sağlayıcıyı denemek (aynı endpoint şeklini farklı sağlayıcıdan alacağın için `InstagramFeedCache` şeması sağlayıcıdan bağımsız kalacak şekilde tasarlandı) makul bir başlangıç
## Açık Sorular
- [ ] Free-tier adaylarından hangisi seçilecek — birkaçını test edip (aynı listeleme üzerinde) veri kalitesi/limit karşılaştırması yapılabilir