feat: TR rakip analizi → satış blokerleri düzeltildi
Rakip analizi (kocaelidijital.com, promedyanet.com, lf.com.tr) sonrası
satış blokerleri tespit edildi ve aşağıdaki bölümler eklendi:
1. ANASAYFADA SSS (8 hazır soru, admin'den düzenlenebilir)
- Fiyat, süre, ödeme, garanti, hosting, SEO, mevcut site yenileme,
sadece tasarım hizmeti gibi en sık sorulan sorular
- HomepageFaq component (sticky sol + accordion sağ)
- site_settings.homepage_faq[] (JSON {q,a})
2. RISK REVERSAL bölümü (Guarantee component)
- 'İlk taslak ücretsiz, memnun değilseniz devam etmiyoruz'
- 4 garanti maddesi checklist
- site_settings.guarantee_title/description/items
3. PROJE METRİKLERİ (vaka çalışması güçlendirme)
- projects.metrics[] (JSON {value,label})
- Detay sayfada büyük metric kartları
- Admin formda 'değer | etiket' satır formatı
4. HERO COPY GÜNCELLEMESİ (admin'den düzenlenebilir)
- 'Kocaeli'de 2-3 hafta içinde yayında olan, satan kurumsal web siteleri'
- 'İlk tasarım taslakı ücretsiz' vurgusu
- Trust band: 30 dk yanıt + ücretsiz taslak + 4.9 memnuniyet
5. /SITE-ANALIZI LEAD MAGNET SAYFASI
- URL + ad + email + telefon formu
- 6 analiz başlığı (CWV, mobil, SEO, güvenlik, içerik, rakip)
- contact_messages'a source=quick-site-audit ile yazılır
- 'subject' alanı ile inbox'ta ayırt edilebilir
6. EKİP BÖLÜMÜ (Hakkımızda sayfasında)
- Yeni team_members tablosu (name, role, bio, photo, linkedin)
- /admin/ekip CRUD sayfası
- TeamGrid component
7. SEKTÖR LANDING SAYFALARI (/sektor/[slug])
- Yeni industries tablosu (slug, title, content, features, faq, SEO)
- /admin/sektorler CRUD sayfası
- SEO + ad-targeted landing template
- Hero + trust + features + content + garanti + projeler + hizmetler + FAQ + JSON-LD
Admin /admin/site formuna yeni bölümler:
- 'Risk reversal / Garanti' (title + description + items)
- 'Anasayfa SSS' (---' bloklarla)
App sidebar'a 'Sektörler' ve 'Ekip' linkleri eklendi.
Footer'a 'Ücretsiz Site Analizi' linki eklendi.
36 route üretiliyor (önceki 31'den +5: /site-analizi, /sektor/[slug],
/admin/ekip + alt, /admin/sektorler + alt).
This commit is contained in:
@@ -180,6 +180,17 @@ export async function deleteService(formData: FormData) {
|
||||
|
||||
// ─── Projects ────────────────────────────────────────────────────
|
||||
|
||||
function parseMetricsInput(raw: string): string[] {
|
||||
return raw
|
||||
.split("\n")
|
||||
.map((line) => {
|
||||
const [value, label] = line.split("|").map((s) => s.trim());
|
||||
if (!value || !label) return null;
|
||||
return JSON.stringify({ value, label });
|
||||
})
|
||||
.filter((x): x is string => x !== null);
|
||||
}
|
||||
|
||||
export async function saveProject(formData: FormData) {
|
||||
const secret = await requireSessionSecret();
|
||||
const id = str(formData.get("id"));
|
||||
@@ -212,6 +223,10 @@ export async function saveProject(formData: FormData) {
|
||||
industry: str(formData.get("industry")),
|
||||
duration: str(formData.get("duration")),
|
||||
service_slug: str(formData.get("service_slug")),
|
||||
metrics: (() => {
|
||||
const m = parseMetricsInput(String(formData.get("metrics") ?? ""));
|
||||
return m.length > 0 ? m : null;
|
||||
})(),
|
||||
};
|
||||
|
||||
if (id) {
|
||||
@@ -497,6 +512,97 @@ export async function deleteSeoPage(formData: FormData) {
|
||||
revalidatePath("/admin/seo");
|
||||
}
|
||||
|
||||
// ─── Team Members ────────────────────────────────────────────────
|
||||
|
||||
export async function saveTeamMember(formData: FormData) {
|
||||
const secret = await requireSessionSecret();
|
||||
const id = str(formData.get("id"));
|
||||
const name = str(formData.get("name"));
|
||||
if (!name) throw new Error("İsim zorunlu");
|
||||
|
||||
const data = {
|
||||
name,
|
||||
role: str(formData.get("role")),
|
||||
bio: str(formData.get("bio")),
|
||||
photo_url: str(formData.get("photo_url")),
|
||||
linkedin_url: str(formData.get("linkedin_url")),
|
||||
order: num(formData.get("order")) ?? 0,
|
||||
};
|
||||
|
||||
if (id) {
|
||||
await tablesDB.updateRow(DATABASE_ID, TABLES.teamMembers, id, data, secret);
|
||||
} else {
|
||||
await tablesDB.createRow(
|
||||
DATABASE_ID,
|
||||
TABLES.teamMembers,
|
||||
ID.unique(),
|
||||
data,
|
||||
secret,
|
||||
);
|
||||
}
|
||||
revalidatePath("/admin/ekip");
|
||||
revalidatePath("/hakkimizda");
|
||||
}
|
||||
|
||||
export async function deleteTeamMember(formData: FormData) {
|
||||
const secret = await requireSessionSecret();
|
||||
const id = String(formData.get("id"));
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.teamMembers, id, secret);
|
||||
revalidatePath("/admin/ekip");
|
||||
revalidatePath("/hakkimizda");
|
||||
}
|
||||
|
||||
// ─── Industries ──────────────────────────────────────────────────
|
||||
|
||||
export async function saveIndustry(formData: FormData) {
|
||||
const secret = await requireSessionSecret();
|
||||
const id = str(formData.get("id"));
|
||||
const title = str(formData.get("title"));
|
||||
if (!title) throw new Error("Başlık zorunlu");
|
||||
const slug = str(formData.get("slug")) || slugify(title);
|
||||
|
||||
const faqRaw = String(formData.get("faq") ?? "");
|
||||
const faq = faqRaw
|
||||
.split("\n---\n")
|
||||
.map((block) => {
|
||||
const lines = block.trim().split("\n");
|
||||
const q = lines[0]?.trim();
|
||||
const a = lines.slice(1).join("\n").trim();
|
||||
if (!q || !a) return null;
|
||||
return JSON.stringify({ q, a });
|
||||
})
|
||||
.filter((x): x is string => x !== null);
|
||||
|
||||
const data = {
|
||||
slug,
|
||||
title,
|
||||
subtitle: str(formData.get("subtitle")),
|
||||
content: str(formData.get("content")),
|
||||
hero_image: str(formData.get("hero_image")),
|
||||
features: strArr(formData.get("features")),
|
||||
faq: faq.length > 0 ? faq : null,
|
||||
seo_title: str(formData.get("seo_title")),
|
||||
seo_description: str(formData.get("seo_description")),
|
||||
featured: bool(formData.get("featured")),
|
||||
order: num(formData.get("order")) ?? 0,
|
||||
};
|
||||
|
||||
if (id) {
|
||||
await tablesDB.updateRow(DATABASE_ID, TABLES.industries, id, data, secret);
|
||||
} else {
|
||||
await tablesDB.createRow(DATABASE_ID, TABLES.industries, slug, data, secret);
|
||||
}
|
||||
revalidatePath("/admin/sektorler");
|
||||
revalidatePath(`/sektor/${slug}`);
|
||||
}
|
||||
|
||||
export async function deleteIndustry(formData: FormData) {
|
||||
const secret = await requireSessionSecret();
|
||||
const id = String(formData.get("id"));
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.industries, id, secret);
|
||||
revalidatePath("/admin/sektorler");
|
||||
}
|
||||
|
||||
// ─── Contact ─────────────────────────────────────────────────────
|
||||
|
||||
export async function updateMessageStatus(formData: FormData) {
|
||||
|
||||
@@ -22,6 +22,8 @@ export const TABLES = {
|
||||
seoPages: "seo_pages",
|
||||
seoSettings: "seo_settings",
|
||||
siteSettings: "site_settings",
|
||||
teamMembers: "team_members",
|
||||
industries: "industries",
|
||||
} as const;
|
||||
|
||||
export class AppwriteError extends Error {
|
||||
|
||||
+23
@@ -4,11 +4,13 @@ import { getSessionSecret } from "@/lib/auth";
|
||||
import type {
|
||||
BlogPostRow,
|
||||
ContactMessageRow,
|
||||
IndustryRow,
|
||||
ProjectRow,
|
||||
ServiceRow,
|
||||
SeoPageRow,
|
||||
SeoSettingsRow,
|
||||
SiteSettingsRow,
|
||||
TeamMemberRow,
|
||||
TestimonialRow,
|
||||
} from "@/lib/types";
|
||||
|
||||
@@ -119,6 +121,27 @@ export async function listSeoPages() {
|
||||
]);
|
||||
}
|
||||
|
||||
export async function listTeamMembers() {
|
||||
return safeList<TeamMemberRow>(TABLES.teamMembers, [
|
||||
Q.orderAsc("order"),
|
||||
Q.limit(50),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function listIndustries(opts?: { featured?: boolean }) {
|
||||
const q = [Q.orderAsc("order"), Q.limit(100)];
|
||||
if (opts?.featured) q.unshift(Q.equal("featured", true));
|
||||
return safeList<IndustryRow>(TABLES.industries, q);
|
||||
}
|
||||
|
||||
export async function getIndustryBySlug(slug: string): Promise<IndustryRow | null> {
|
||||
const res = await safeList<IndustryRow>(TABLES.industries, [
|
||||
Q.equal("slug", slug),
|
||||
Q.limit(1),
|
||||
]);
|
||||
return res[0] ?? null;
|
||||
}
|
||||
|
||||
export async function getSiteSettings(): Promise<SiteSettingsRow | null> {
|
||||
try {
|
||||
return await tablesDB.getRow<SiteSettingsRow>(
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface ProjectRow extends AwRow {
|
||||
industry?: string | null;
|
||||
duration?: string | null;
|
||||
service_slug?: string | null;
|
||||
metrics?: string[] | null; // JSON {"value":"+150%","label":"Trafik artışı"}
|
||||
}
|
||||
|
||||
export interface BlogPostRow extends AwRow {
|
||||
@@ -142,6 +143,39 @@ export interface SiteSettingsRow extends AwRow {
|
||||
google_review_url?: string | null;
|
||||
google_rating?: number | null;
|
||||
google_review_count?: number | null;
|
||||
|
||||
homepage_faq?: string[] | null; // JSON {"q","a"}
|
||||
guarantee_title?: string | null;
|
||||
guarantee_description?: string | null;
|
||||
guarantee_items?: string[] | null;
|
||||
}
|
||||
|
||||
export interface TeamMemberRow extends AwRow {
|
||||
name: string;
|
||||
role?: string | null;
|
||||
bio?: string | null;
|
||||
photo_url?: string | null;
|
||||
linkedin_url?: string | null;
|
||||
order?: number | null;
|
||||
}
|
||||
|
||||
export interface IndustryRow extends AwRow {
|
||||
slug: string;
|
||||
title: string;
|
||||
subtitle?: string | null;
|
||||
content?: string | null;
|
||||
hero_image?: string | null;
|
||||
features?: string[] | null;
|
||||
faq?: string[] | null;
|
||||
seo_title?: string | null;
|
||||
seo_description?: string | null;
|
||||
featured?: boolean | null;
|
||||
order?: number | null;
|
||||
}
|
||||
|
||||
export interface ProjectMetric {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface TrustItem {
|
||||
|
||||
Reference in New Issue
Block a user