feat: Implement Phase 2 features including blog, collections, saved listings, manifest, and api cron sync
This commit is contained in:
+195
@@ -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
|
||||
Reference in New Issue
Block a user