From f833d429fcc75a137bddd31e82f9478cbce4b55d Mon Sep 17 00:00:00 2001 From: Ege Can Komur Date: Wed, 20 May 2026 02:13:09 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20admin=20paneli=20+=20blog=20+=20testimo?= =?UTF-8?q?nials=20+=20SEO=20y=C3=B6neticisi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend altyapısı: - 4 yeni Appwrite tablosu: blog_posts, testimonials, seo_pages, seo_settings - Appwrite Storage bucket: kovak-yazilim-media (görsel yüklemeleri) - Appwrite Auth ile session cookie tabanlı koruma Admin paneli (/admin): - Login akışı (email/password) + protected layout - Dashboard: sayım kartları + hızlı aksiyonlar - Blog CRUD: markdown content, kapak görseli, draft/published, SEO alanları - Services CRUD: lucide ikon seçici - Projects CRUD: teknoloji etiketleri, live URL - Testimonials CRUD: puanlama - SEO yöneticisi: global ayarlar + sayfa bazlı override - Mesaj inbox: status filtreleme + güncelleme - Medya kütüphanesi: Appwrite Storage upload/delete Public: - /blog ve /blog/[slug] sayfaları (markdown render) - Anasayfaya Testimonials bölümü - Tüm public sayfalarda generateMetadata + seo_pages override - Header'a Blog linki Route yapısı: - app/(site)/ — public site, Header/Footer ortak - app/admin/login — auth dışı - app/admin/(protected)/ — requireUser() korumalı 23 route üretiliyor, public static, admin dynamic. --- README.md | 124 ++++-- app/(site)/blog/[slug]/page.tsx | 102 +++++ app/(site)/blog/page.tsx | 84 ++++ app/{ => (site)}/hakkimizda/page.tsx | 13 +- app/{ => (site)}/hizmetler/page.tsx | 13 +- app/{ => (site)}/iletisim/page.tsx | 13 +- app/(site)/layout.tsx | 16 + app/{ => (site)}/page.tsx | 27 +- app/{ => (site)}/projeler/page.tsx | 11 +- app/actions.ts | 4 +- app/admin/(protected)/blog/[id]/edit/page.tsx | 16 + app/admin/(protected)/blog/form.tsx | 137 ++++++ app/admin/(protected)/blog/new/page.tsx | 5 + app/admin/(protected)/blog/page.tsx | 103 +++++ .../(protected)/hizmetler/[id]/edit/page.tsx | 16 + app/admin/(protected)/hizmetler/form.tsx | 90 ++++ app/admin/(protected)/hizmetler/new/page.tsx | 5 + app/admin/(protected)/hizmetler/page.tsx | 80 ++++ app/admin/(protected)/iletisim/page.tsx | 137 ++++++ app/admin/(protected)/layout.tsx | 27 ++ app/admin/(protected)/medya/page.tsx | 119 +++++ app/admin/(protected)/page.tsx | 124 ++++++ .../(protected)/projeler/[id]/edit/page.tsx | 16 + app/admin/(protected)/projeler/form.tsx | 81 ++++ app/admin/(protected)/projeler/new/page.tsx | 5 + app/admin/(protected)/projeler/page.tsx | 88 ++++ .../referanslar/[id]/edit/page.tsx | 16 + app/admin/(protected)/referanslar/form.tsx | 64 +++ .../(protected)/referanslar/new/page.tsx | 5 + app/admin/(protected)/referanslar/page.tsx | 73 ++++ app/admin/(protected)/seo/[id]/edit/page.tsx | 16 + app/admin/(protected)/seo/new/page.tsx | 5 + app/admin/(protected)/seo/page-form.tsx | 70 +++ app/admin/(protected)/seo/page.tsx | 175 ++++++++ app/admin/login/actions.ts | 46 ++ app/admin/login/form.tsx | 56 +++ app/admin/login/page.tsx | 29 ++ app/layout.tsx | 6 +- components/admin/delete-button.tsx | 24 ++ components/admin/form.tsx | 202 +++++++++ components/admin/sidebar.tsx | 80 ++++ components/admin/topbar.tsx | 24 ++ components/header.tsx | 1 + components/testimonials.tsx | 50 +++ lib/admin-actions.ts | 405 ++++++++++++++++++ lib/appwrite-server.ts | 24 +- lib/auth.ts | 25 ++ lib/data.ts | 106 ++++- lib/seo.ts | 53 +++ lib/types.ts | 55 ++- package-lock.json | 13 + package.json | 1 + 52 files changed, 2999 insertions(+), 81 deletions(-) create mode 100644 app/(site)/blog/[slug]/page.tsx create mode 100644 app/(site)/blog/page.tsx rename app/{ => (site)}/hakkimizda/page.tsx (90%) rename app/{ => (site)}/hizmetler/page.tsx (69%) rename app/{ => (site)}/iletisim/page.tsx (90%) create mode 100644 app/(site)/layout.tsx rename app/{ => (site)}/page.tsx (74%) rename app/{ => (site)}/projeler/page.tsx (72%) create mode 100644 app/admin/(protected)/blog/[id]/edit/page.tsx create mode 100644 app/admin/(protected)/blog/form.tsx create mode 100644 app/admin/(protected)/blog/new/page.tsx create mode 100644 app/admin/(protected)/blog/page.tsx create mode 100644 app/admin/(protected)/hizmetler/[id]/edit/page.tsx create mode 100644 app/admin/(protected)/hizmetler/form.tsx create mode 100644 app/admin/(protected)/hizmetler/new/page.tsx create mode 100644 app/admin/(protected)/hizmetler/page.tsx create mode 100644 app/admin/(protected)/iletisim/page.tsx create mode 100644 app/admin/(protected)/layout.tsx create mode 100644 app/admin/(protected)/medya/page.tsx create mode 100644 app/admin/(protected)/page.tsx create mode 100644 app/admin/(protected)/projeler/[id]/edit/page.tsx create mode 100644 app/admin/(protected)/projeler/form.tsx create mode 100644 app/admin/(protected)/projeler/new/page.tsx create mode 100644 app/admin/(protected)/projeler/page.tsx create mode 100644 app/admin/(protected)/referanslar/[id]/edit/page.tsx create mode 100644 app/admin/(protected)/referanslar/form.tsx create mode 100644 app/admin/(protected)/referanslar/new/page.tsx create mode 100644 app/admin/(protected)/referanslar/page.tsx create mode 100644 app/admin/(protected)/seo/[id]/edit/page.tsx create mode 100644 app/admin/(protected)/seo/new/page.tsx create mode 100644 app/admin/(protected)/seo/page-form.tsx create mode 100644 app/admin/(protected)/seo/page.tsx create mode 100644 app/admin/login/actions.ts create mode 100644 app/admin/login/form.tsx create mode 100644 app/admin/login/page.tsx create mode 100644 components/admin/delete-button.tsx create mode 100644 components/admin/form.tsx create mode 100644 components/admin/sidebar.tsx create mode 100644 components/admin/topbar.tsx create mode 100644 components/testimonials.tsx create mode 100644 lib/admin-actions.ts create mode 100644 lib/auth.ts create mode 100644 lib/seo.ts diff --git a/README.md b/README.md index 98525fd..1d5a198 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ -# Kovak Yazılım — Kurumsal Web Sitesi +# Kovak Yazılım — Kurumsal Site + Admin Panel -Next.js 16 + TypeScript + Tailwind v4 + Appwrite ile geliştirilmiş kurumsal site. +Next.js 16 + TypeScript + Tailwind v4 + Appwrite ile geliştirilmiş kurumsal site ve içerik yönetim paneli. ## Teknoloji - **Framework:** Next.js 16 (App Router, Turbopack, React 19) - **Stil:** Tailwind CSS v4 -- **Backend:** Appwrite (TablesDB) — `https://db.kovaksoft.com` -- **İkonlar:** lucide-react + inline SVG brand icons +- **Backend:** Appwrite (TablesDB + Storage + Auth) — `https://db.kovaksoft.com` +- **İçerik:** Markdown (marked.js) +- **İkonlar:** lucide-react + inline SVG - **Form:** React Server Actions + `useActionState` ## Kurulum @@ -15,59 +16,120 @@ Next.js 16 + TypeScript + Tailwind v4 + Appwrite ile geliştirilmiş kurumsal si ```bash npm install cp .env.example .env.local -# .env.local dosyasındaki APPWRITE_API_KEY'i Appwrite Console'dan oluşturup ekle +# .env.local içine APPWRITE_API_KEY'i Appwrite Console'dan oluşturup ekle npm run dev ``` Site: +Admin: + +## İlk Admin Kullanıcısı + +1. Appwrite Console → Auth → Users → Create User +2. Email + şifre belirle +3. `/admin/login` üzerinden giriş yap ## Appwrite Yapılandırması **Project ID:** `69f27b51000a5bee46ce` **Database ID:** `kovak-yazilim-db` +**Bucket ID:** `kovak-yazilim-media` ### Tablolar -| Tablo | İçerik | Yazma izni | -|---|---|---| -| `services` | Hizmetler (slug, title, description, icon, order, featured) | yalnız users | -| `projects` | Referans projeler (slug, title, description, image_url, live_url, category, technologies[], year, featured) | yalnız users | -| `contact_messages` | İletişim formu kayıtları (name, email, phone, subject, message, status) | herkes create, users read/update/delete | +| Tablo | İçerik | +|---|---| +| `services` | Hizmet kartları (slug, title, description, icon, order, featured) | +| `projects` | Referans projeler (slug, title, description, image_url, live_url, category, technologies[], year, featured) | +| `blog_posts` | Blog yazıları (slug, title, excerpt, content, cover_image, author, status, published_at, tags[], seo_*) | +| `testimonials` | Müşteri yorumları (name, role, company, message, rating, image_url, order, featured) | +| `seo_pages` | Sayfa bazlı SEO override (path, title, description, og_image, canonical, noindex) | +| `seo_settings` | Global SEO ayarları (singleton — rowId: `global`) | +| `contact_messages` | İletişim formu kayıtları (anonim create, users read/update/delete) | -`contact_messages.create` izni `any` — anonim kullanıcılar form gönderebilsin diye. -Diğer tüm yazma işlemleri yetkili kullanıcı (admin) gerektirir. +### Storage -### API Key Oluşturma +`kovak-yazilim-media` — 10 MB max, image-only (jpg/png/webp/gif/svg/avif). Public read. -1. Appwrite Console → Settings → API Keys → Create -2. Scopes: `databases.read`, `tables.read`, `rows.read`, `rows.write` -3. `.env.local` içine `APPWRITE_API_KEY=` olarak yapıştır +### API Key + +Appwrite Console → Settings → API Keys → Create +Scopes: `databases.read`, `tables.read`, `rows.read`, `rows.write`, `files.read`, `files.write`, `users.read` + +## Admin Paneli + +`/admin` altında: + +- **Pano** (`/admin`) — Sayım kartları + hızlı aksiyonlar +- **Blog** (`/admin/blog`) — Yazı CRUD, draft/published durumu, markdown editor +- **Hizmetler** (`/admin/hizmetler`) — Hizmet CRUD, lucide ikon seçici +- **Projeler** (`/admin/projeler`) — Portfolyo CRUD +- **Referanslar** (`/admin/referanslar`) — Müşteri yorumları +- **SEO** (`/admin/seo`) — Global meta + sayfa bazlı override +- **Mesajlar** (`/admin/iletisim`) — Form inbox, status (new/read/replied/archived) +- **Medya** (`/admin/medya`) — Appwrite Storage browser, upload/delete + +### Auth Akışı + +`lib/auth.ts` → `getCurrentUser()` & `requireUser()` +Login → `account.createEmailPasswordSession` → session secret HTTP-only cookie (`kovak_session`) +Admin layout (`app/admin/(protected)/layout.tsx`) `requireUser()` çağrısı yapar — yetkisiz giriş `/admin/login`'e redirect. + +## SEO Sistemi + +`lib/seo.ts` → `buildMetadata(path, fallback)` + +Sıralama (override öncelikli): +1. `seo_pages` tablosunda o path için kayıt varsa → onun title/description/og_image kullanılır +2. Yoksa sayfanın kendi fallback `Metadata` objesi +3. O da yoksa `seo_settings` (global) +4. O da yoksa `lib/site-config.ts` ## Yapı ``` app/ - actions.ts # Server Action: submitContact - layout.tsx # Root layout + Header/Footer - page.tsx # Anasayfa - hizmetler/ # /hizmetler - projeler/ # /projeler - hakkimizda/ # /hakkimizda - iletisim/ # /iletisim -components/ # Header, Footer, Hero, ContactForm, … + (site)/ # Public site (Header + Footer ortak) + page.tsx # Anasayfa + hizmetler/ # /hizmetler + projeler/ # /projeler + blog/ # /blog, /blog/[slug] + hakkimizda/ + iletisim/ + admin/ + login/ # /admin/login (auth dışı) + (protected)/ # requireUser() ile korunan grup + page.tsx # /admin + blog/ + hizmetler/ + projeler/ + referanslar/ + seo/ + iletisim/ + medya/ + actions.ts # Public Server Action: submitContact + layout.tsx # Root layout (html/body) +components/ + admin/ # Sidebar, topbar, form helpers, delete button + header.tsx, footer.tsx + hero.tsx, services-grid.tsx, projects-grid.tsx, testimonials.tsx + contact-form.tsx lib/ - appwrite.ts # Browser client - appwrite-server.ts # Server client (uses APPWRITE_API_KEY) - data.ts # listServices / listProjects (Server-only) - site-config.ts # Marka, iletişim, fallback hizmetler - types.ts -public/logo.png # Logo (kovakyazilim.com'dan) + appwrite.ts # Browser client + appwrite-server.ts # adminClient (API key) + sessionClient (cookie) + auth.ts # Session helpers + admin-actions.ts # Tüm CRUD server actions (gate() ile auth check) + data.ts # listX / getX sorguları + seo.ts # buildMetadata + site-config.ts # Marka + fallback değerler + types.ts # Row tipleri +public/logo.png # Logo ``` ## Build ```bash -npm run build +npm run build # 23 route, public sayfalar static, admin dynamic npm start ``` diff --git a/app/(site)/blog/[slug]/page.tsx b/app/(site)/blog/[slug]/page.tsx new file mode 100644 index 0000000..62a3ffa --- /dev/null +++ b/app/(site)/blog/[slug]/page.tsx @@ -0,0 +1,102 @@ +import Image from "next/image"; +import Link from "next/link"; +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { ArrowLeft, Calendar } from "lucide-react"; +import { marked } from "marked"; +import { getPostBySlug } from "@/lib/data"; +import { buildMetadata } from "@/lib/seo"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await params; + const post = await getPostBySlug(slug); + if (!post) return { title: "Yazı bulunamadı" }; + return buildMetadata(`/blog/${slug}`, { + title: post.seo_title || post.title, + description: post.seo_description || post.excerpt || undefined, + openGraph: { + title: post.seo_title || post.title, + description: post.seo_description || post.excerpt || undefined, + images: post.seo_image || post.cover_image ? [{ url: (post.seo_image || post.cover_image) as string }] : undefined, + type: "article", + }, + }); +} + +export default async function BlogPostPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const post = await getPostBySlug(slug); + if (!post || post.status !== "published") notFound(); + + const html = post.content ? marked.parse(post.content, { async: false }) as string : ""; + + return ( +
+ + Tüm yazılar + + +
+ {post.tags && post.tags.length > 0 && ( +
+ {post.tags.map((t) => ( + + {t} + + ))} +
+ )} +

+ {post.title} +

+ {post.excerpt && ( +

+ {post.excerpt} +

+ )} +
+ {post.author && {post.author}} + {post.author && post.published_at && } + {post.published_at && ( + + + {new Date(post.published_at).toLocaleDateString("tr-TR")} + + )} +
+
+ + {post.cover_image && ( +
+ {post.title} +
+ )} + +
+
+ ); +} diff --git a/app/(site)/blog/page.tsx b/app/(site)/blog/page.tsx new file mode 100644 index 0000000..da82b3f --- /dev/null +++ b/app/(site)/blog/page.tsx @@ -0,0 +1,84 @@ +import Image from "next/image"; +import Link from "next/link"; +import type { Metadata } from "next"; +import { ArrowRight, Calendar } from "lucide-react"; +import { SectionTitle } from "@/components/section-title"; +import { listPublishedPosts } from "@/lib/data"; +import { buildMetadata } from "@/lib/seo"; + +export async function generateMetadata(): Promise { + return buildMetadata("/blog", { + title: "Blog", + description: "Yazılım, web tasarım, SEO ve dijital pazarlama üzerine yazılar.", + }); +} + +export default async function BlogIndex() { + const posts = await listPublishedPosts(); + + return ( +
+ + +
+ {posts.length === 0 ? ( +
+

+ Henüz yayınlanmış yazı yok. +

+
+ ) : ( +
+ {posts.map((p) => ( +
+ +
+ {p.cover_image ? ( + {p.title} + ) : ( +
+ {p.title.charAt(0)} +
+ )} +
+
+

+ + {p.published_at + ? new Date(p.published_at).toLocaleDateString("tr-TR") + : "—"} +

+

+ {p.title} +

+ {p.excerpt && ( +

+ {p.excerpt} +

+ )} + + Devamını oku + +
+ +
+ ))} +
+ )} +
+
+ ); +} diff --git a/app/hakkimizda/page.tsx b/app/(site)/hakkimizda/page.tsx similarity index 90% rename from app/hakkimizda/page.tsx rename to app/(site)/hakkimizda/page.tsx index f9e6cd0..b107ce0 100644 --- a/app/hakkimizda/page.tsx +++ b/app/(site)/hakkimizda/page.tsx @@ -2,12 +2,15 @@ import type { Metadata } from "next"; import Image from "next/image"; import { SectionTitle } from "@/components/section-title"; import { CheckCircle2 } from "lucide-react"; +import { buildMetadata } from "@/lib/seo"; -export const metadata: Metadata = { - title: "Hakkımızda", - description: - "Kovak Yazılım, Kocaeli merkezli bir teknoloji ajansıdır. Web, mobil ve CRM çözümleri üretir.", -}; +export async function generateMetadata(): Promise { + return buildMetadata("/hakkimizda", { + title: "Hakkımızda", + description: + "Kovak Yazılım, Kocaeli merkezli bir teknoloji ajansıdır. Web, mobil ve CRM çözümleri üretir.", + }); +} const values = [ { diff --git a/app/hizmetler/page.tsx b/app/(site)/hizmetler/page.tsx similarity index 69% rename from app/hizmetler/page.tsx rename to app/(site)/hizmetler/page.tsx index 9feb324..f57e562 100644 --- a/app/hizmetler/page.tsx +++ b/app/(site)/hizmetler/page.tsx @@ -2,12 +2,15 @@ import type { Metadata } from "next"; import { SectionTitle } from "@/components/section-title"; import { ServicesGrid } from "@/components/services-grid"; import { listServices } from "@/lib/data"; +import { buildMetadata } from "@/lib/seo"; -export const metadata: Metadata = { - title: "Hizmetler", - description: - "Web tasarım, e-ticaret, mobil uygulama, yazılım geliştirme, CRM ve dijital pazarlama hizmetleri.", -}; +export async function generateMetadata(): Promise { + return buildMetadata("/hizmetler", { + title: "Hizmetler", + description: + "Web tasarım, e-ticaret, mobil uygulama, yazılım geliştirme, CRM ve dijital pazarlama hizmetleri.", + }); +} export default async function ServicesPage() { const services = await listServices(); diff --git a/app/iletisim/page.tsx b/app/(site)/iletisim/page.tsx similarity index 90% rename from app/iletisim/page.tsx rename to app/(site)/iletisim/page.tsx index 6119286..bf28ac6 100644 --- a/app/iletisim/page.tsx +++ b/app/(site)/iletisim/page.tsx @@ -3,12 +3,15 @@ import { Mail, MapPin, Phone, Clock } from "lucide-react"; import { SectionTitle } from "@/components/section-title"; import { ContactForm } from "@/components/contact-form"; import { siteConfig } from "@/lib/site-config"; +import { buildMetadata } from "@/lib/seo"; -export const metadata: Metadata = { - title: "İletişim", - description: - "Projeniz hakkında konuşmak için bize ulaşın. İzmit Sanayi Sitesi, Kocaeli.", -}; +export async function generateMetadata(): Promise { + return buildMetadata("/iletisim", { + title: "İletişim", + description: + "Projeniz hakkında konuşmak için bize ulaşın. İzmit Sanayi Sitesi, Kocaeli.", + }); +} export default function ContactPage() { return ( diff --git a/app/(site)/layout.tsx b/app/(site)/layout.tsx new file mode 100644 index 0000000..1d42f3a --- /dev/null +++ b/app/(site)/layout.tsx @@ -0,0 +1,16 @@ +import { Header } from "@/components/header"; +import { Footer } from "@/components/footer"; + +export default function SiteLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> +
+
{children}
+