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:
Ege Can Komur
2026-05-20 04:03:21 +03:00
parent 304a344955
commit cf46e30a7e
25 changed files with 1390 additions and 1 deletions
+106
View File
@@ -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) {
+2
View File
@@ -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
View File
@@ -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>(
+34
View File
@@ -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 {