feat(kvkk): çerez politikası, ölçümü kapatma ve ilk ziyaret bilgi bandı
All checks were successful
Deploy / deploy (push) Successful in 6m27s

- /cerez-politikasi: çerez aydınlatma metni (ad, amaç, süre, taraf),
  Rybbit ölçümünün kapsamı ve hukuki dayanağı (KVKK md. 5/2-f,
  Çerez Rehberi Temmuz 2025 §5.9)
- OlcumTercihi: Rybbit'i tarayıcıda kapatan düğme (disable-rybbit)
- CerezBilgi: ilk ziyarette bir kez görünen bilgilendirme (onay
  penceresi değil; rıza gerektiren çerez yok)
- gizlilik: analitik bölümü kodla eşitlendi (e-posta/ad gönderilmiyor)
- alt bilgi ve sitemap'e sayfa eklendi
- docs: hukuki analiz ve meşru menfaat denge testi

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 10:11:38 +03:00
parent ff86176d9e
commit 9e4c5608eb
8 changed files with 629 additions and 13 deletions

View File

@@ -0,0 +1,80 @@
"use client";
// İlk ziyarette bir kez görünen çerez bilgilendirmesi. Onay penceresi DEĞİL:
// sitede açık rıza gerektiren çerez yok (zorunlu çerezler + kendi
// sunucumuzdaki ölçüm; bkz. /cerez-politikasi). KVKK Çerez Rehberi (Temmuz
// 2025) §9, veri işleme şartından bağımsız olarak siteye girişte
// bilgilendirme bekliyor; bu bant onu karşılar. Kabul/ret düğmesi yoktur,
// çünkü sorulan bir rıza yoktur — ölçümü kapatma anahtarı politika
// sayfasındadır. Kapatma kalıcıdır (localStorage).
import { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Button } from "@/components/ui/button";
import { ViewportPortal } from "@/components/viewport-portal";
import { kromGizliMi } from "@/components/header-gate";
const GORULDU_KEY = "kolaytercih.cerez-bilgi";
export function CerezBilgi() {
const pathname = usePathname();
// SSR/ilk boyada gizli başla; durum mount'ta okunur.
const [goruldu, setGoruldu] = useState(true);
useEffect(() => {
try {
// eslint-disable-next-line react-hooks/set-state-in-effect
setGoruldu(Boolean(localStorage.getItem(GORULDU_KEY)));
} catch {
// Depolama kapalıysa "görüldü" saklanamaz; her sayfada çıkmasın.
setGoruldu(true);
}
}, []);
if (goruldu) return null;
// Giriş/ödeme gibi kromsuz akışlarda ve politikanın kendisinde gösterilmez.
if (pathname && kromGizliMi(pathname)) return null;
if (pathname === "/cerez-politikasi") return null;
function kapat() {
setGoruldu(true);
try {
localStorage.setItem(GORULDU_KEY, "1");
} catch {}
}
return (
<ViewportPortal>
{/* z-45: KaydetBanner'ın (z-40) üstünde, modalların (z-50) altında */}
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-45 px-4 print:hidden">
<section
aria-label="Çerez bilgilendirmesi"
className="pointer-events-auto mx-auto flex max-w-xl flex-wrap items-center gap-3 rounded-2xl border border-slate-200 bg-card p-4 shadow-xl sm:flex-nowrap sm:gap-4"
>
<p className="min-w-0 flex-1 text-sm leading-snug text-slate-700">
Bu sitede reklam ya da izleme çerezi yok. Yalnız oturumun ve listen
için gerekli çerezleri ve kendi sunucumuzda çalışan çerezsiz
ziyaret ölçümünü kullanıyoruz.{" "}
<Link
href="/cerez-politikasi"
onClick={kapat}
className="font-semibold text-primary underline"
>
Ayrıntılar ve ölçümü kapatma
</Link>
</p>
<Button
type="button"
variant="outline"
size="lg"
onClick={kapat}
className="w-full cursor-pointer px-6 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97] sm:w-auto"
>
Tamam
</Button>
</section>
</div>
</ViewportPortal>
);
}

View File

@@ -0,0 +1,72 @@
"use client";
// Kullanım ölçümünü (Rybbit) bu tarayıcıda kapatan/açan anahtar — KVKK
// md. 11 itiraz hakkının arayüzü, /cerez-politikasi sayfasında durur.
//
// Rybbit betiği yüklenirken localStorage'da `disable-rybbit` anahtarını
// görürse hiçbir şey göndermez (betiğin kendi vazgeçme kapısı). Betik
// yüklendikten sonra durdurulamadığı için tercih değişince sayfa yenilenir.
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
const KAPALI_KEY = "disable-rybbit";
// Rybbit'in bu tarayıcıya yazdığı kimlikler; ölçüm kapanınca silinir.
const RYBBIT_KIMLIK_KEYLERI = ["rybbit-user-id", "rybbit-visitor-id"];
export function OlcumTercihi() {
// null = henüz okunmadı (SSR/ilk boya)
const [kapali, setKapali] = useState<boolean | null>(null);
const [hata, setHata] = useState(false);
useEffect(() => {
try {
// eslint-disable-next-line react-hooks/set-state-in-effect
setKapali(localStorage.getItem(KAPALI_KEY) !== null);
} catch {
setKapali(false);
}
}, []);
function degistir() {
try {
if (kapali) {
localStorage.removeItem(KAPALI_KEY);
} else {
localStorage.setItem(KAPALI_KEY, "1");
for (const k of RYBBIT_KIMLIK_KEYLERI) localStorage.removeItem(k);
}
} catch {
setHata(true);
return;
}
window.location.reload();
}
return (
<div className="mt-3 flex flex-wrap items-center gap-3 rounded-2xl border border-slate-200 bg-card p-4 sm:flex-nowrap sm:gap-4">
<div className="min-w-0 flex-1">
<p className="font-heading text-base font-bold text-slate-900">
{kapali ? "Ölçüm bu tarayıcıda kapalı" : "Ölçüm bu tarayıcıda açık"}
</p>
<p className="text-sm leading-snug text-slate-500" role="status">
{hata
? "Tarayıcın bu tercihi saklamaya izin vermiyor."
: kapali
? "Ziyaretlerin sayılmıyor. Site aynı şekilde çalışır."
: "Kapatırsan ziyaretlerin sayılmaz; site aynı şekilde çalışır."}
</p>
</div>
<Button
type="button"
variant="outline"
size="lg"
disabled={kapali === null}
onClick={degistir}
className="w-full cursor-pointer px-6 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97] sm:w-auto"
>
{kapali ? "Ölçümü aç" : "Ölçümü kapat"}
</Button>
</div>
);
}

View File

@@ -180,6 +180,12 @@ export function SiteFooter() {
>
Gizlilik & KVKK
</Link>
<Link
href="/cerez-politikasi"
className="hover:text-slate-700 hover:underline"
>
Çerez politikası
</Link>
<Link
href="/kosullar"
className="hover:text-slate-700 hover:underline"