feat(blog): CMS cover images via MinIO, instant cache purge, drop embedded posts #121

Merged
root merged 1 commits from blog-cms-assets into main 2026-06-10 00:22:51 +03:00
4 changed files with 86 additions and 413 deletions

View File

@@ -4,6 +4,7 @@ import {
Controller,
Get,
Headers,
HttpCode,
Param,
Post,
ServiceUnavailableException,
@@ -13,6 +14,25 @@ import { Public } from "../common/decorators/public.decorator";
import type { CreateBlogPost } from "./blog.dto";
import { BlogService } from "./blog.service";
// Bearer-token check shared by the automation endpoints (n8n pipeline and the
// Directus cache-purge Flow). Mirrors the changelog/internal token pattern.
function assertAutomationToken(authHeader: string | undefined): void {
const token = process.env.BLOG_AUTOMATION_TOKEN;
if (!token) {
throw new ServiceUnavailableException("Blog automation endpoint is not configured");
}
if (!authHeader || !authHeader.startsWith("Bearer ")) {
throw new UnauthorizedException("Missing or invalid Authorization header");
}
const provided = authHeader.slice(7);
if (
provided.length !== token.length ||
!timingSafeEqual(Buffer.from(provided), Buffer.from(token))
) {
throw new UnauthorizedException("Invalid token");
}
}
@Controller("blog")
export class BlogController {
constructor(private readonly blogService: BlogService) {}
@@ -30,30 +50,27 @@ export class BlogController {
}
// Automation endpoint — published by the Süper Panel content pipeline via n8n.
// Mirrors the changelog/internal Bearer-token pattern.
@Post("posts/internal")
@Public()
async createInternal(
@Headers("authorization") authHeader: string,
@Body() body: CreateBlogPost,
) {
const token = process.env.BLOG_AUTOMATION_TOKEN;
if (!token) {
throw new ServiceUnavailableException("Blog automation endpoint is not configured");
}
if (!authHeader || !authHeader.startsWith("Bearer ")) {
throw new UnauthorizedException("Missing or invalid Authorization header");
}
const provided = authHeader.slice(7);
if (
provided.length !== token.length ||
!timingSafeEqual(Buffer.from(provided), Buffer.from(token))
) {
throw new UnauthorizedException("Invalid token");
}
assertAutomationToken(authHeader);
if (!body?.title || !body?.body_markdown) {
throw new UnauthorizedException("title and body_markdown are required");
}
return this.blogService.create(body);
}
// Called by a Directus Flow whenever a post is created/updated/deleted, so
// edits made in the CMS admin appear immediately instead of after cache TTL.
@Post("cache/purge")
@Public()
@HttpCode(200)
async purgeCache(@Headers("authorization") authHeader: string) {
assertAutomationToken(authHeader);
await this.blogService.purgeListCache();
return { purged: true };
}
}

View File

@@ -18,6 +18,13 @@ const DIRECTUS_URL = process.env.DIRECTUS_URL?.replace(/\/+$/, "");
const DIRECTUS_TOKEN = process.env.DIRECTUS_TOKEN;
const DIRECTUS_PROJECT = process.env.DIRECTUS_PROJECT ?? "sase";
// Files uploaded in the Directus admin land in the shared MinIO `blog-assets`
// bucket (public read), served from the MinIO domain — NOT from Directus,
// which is Tailscale-only and unreachable for site visitors.
const BLOG_ASSETS_BASE_URL = (
process.env.BLOG_ASSETS_BASE_URL ?? "https://storage.sase.tr/blog-assets"
).replace(/\/+$/, "");
// Row shape of the shared `posts` collection in Directus.
interface DirectusPost {
id: string;
@@ -27,6 +34,7 @@ interface DirectusPost {
body_markdown: string;
tags: string[] | null;
cover_image: string | null;
cover_file: { filename_disk: string } | null;
status: string;
source: string;
published_at: string;
@@ -34,6 +42,16 @@ interface DirectusPost {
date_updated: string | null;
}
// cover_image (external URL, e.g. from the content pipeline) wins over
// cover_file (admin upload, resolved to its public MinIO URL).
function resolveCover(row: Pick<DirectusPost, "cover_image" | "cover_file">): string | null {
if (row.cover_image) return row.cover_image;
if (row.cover_file?.filename_disk) {
return `${BLOG_ASSETS_BASE_URL}/${row.cover_file.filename_disk}`;
}
return null;
}
// URL-safe slug, Turkish-aware. The content_blog prompt already emits a clean
// slug, but we slugify defensively (and to derive one if it's missing).
function slugify(input: string): string {
@@ -91,7 +109,7 @@ export class BlogService {
metaDescription: row.meta_description,
bodyMarkdown: row.body_markdown,
tags: row.tags ?? [],
coverImage: row.cover_image,
coverImage: resolveCover(row),
status: row.status as BlogPost["status"],
source: row.source,
publishedAt: row.published_at,
@@ -100,6 +118,12 @@ export class BlogService {
};
}
// Called by the Directus Flow webhook when a post changes, so both envs
// drop their 30-minute list cache immediately.
async purgeListCache(): Promise<void> {
await this.redis.del(LIST_CACHE_KEY);
}
async findAllPublished(): Promise<BlogPostListItem[]> {
const cached = await this.redis.getJson<BlogPostListItem[]>(LIST_CACHE_KEY);
if (cached) return cached;
@@ -112,7 +136,7 @@ export class BlogService {
"filter[project][_eq]": DIRECTUS_PROJECT,
"filter[status][_eq]": "published",
sort: "-published_at",
fields: "slug,title,meta_description,tags,cover_image,published_at",
fields: "slug,title,meta_description,tags,cover_image,cover_file.filename_disk,published_at",
limit: "-1",
}),
);
@@ -127,7 +151,7 @@ export class BlogService {
title: r.title,
metaDescription: r.meta_description,
tags: r.tags ?? [],
coverImage: r.cover_image,
coverImage: resolveCover(r),
publishedAt: r.published_at,
}));
@@ -144,6 +168,7 @@ export class BlogService {
"filter[project][_eq]": DIRECTUS_PROJECT,
"filter[slug][_eq]": slug,
"filter[status][_eq]": "published",
fields: "*,cover_file.filename_disk",
limit: "1",
}),
);

View File

@@ -1,7 +1,6 @@
import { usePageMeta } from "@/hooks/use-page-meta";
import { SiteHeader } from "@/components/site-header";
import { useBlogPosts } from "@/hooks/use-blog";
import { Button } from "@sase/ui";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
@@ -9,41 +8,6 @@ export const Route = createFileRoute("/blog")({
component: BlogPage,
});
type PostCard = { title: string; description: string; date: string; slug: string };
// Original hand-authored posts. Kept so they never disappear; auto-published
// posts from the content pipeline are merged in from the API below.
const staticPosts: PostCard[] = [
{
title: "Şase Numarası (VIN) Nedir? Nasıl Okunur?",
description:
"17 haneli VIN kodunun her bir bölümünün ne anlama geldiğini, araç geçmişini nasıl ortaya çıkarabileceğinizi adım adım anlatıyoruz.",
date: "2026-02-10",
slug: "vin-nedir-nasil-okunur",
},
{
title: "Orijinal Parça mı, Muadil Parça mı?",
description:
"OEM ve aftermarket parçalar arasındaki farkları, avantaj ve dezavantajlarını karşılaştırmalı olarak inceliyoruz.",
date: "2026-01-28",
slug: "orijinal-mi-muadil-mi",
},
{
title: "Yedek Parça Aramada Dijital Dönüşüm",
description:
"Geleneksel katalog yöntemlerinden dijital platformlara geçiş sürecini ve sektöre etkilerini ele alıyoruz.",
date: "2026-01-15",
slug: "dijital-donusum",
},
{
title: "Sase.tr ile Doğru Parçayı İlk Seferde Bulun",
description:
"Platform özelliklerini kullanarak VIN bazlı arama, şema görüntüleme ve parça eşleştirme rehberi.",
date: "2026-01-05",
slug: "dogru-parcayi-bulun",
},
];
function BlogPage() {
usePageMeta({
title: "Blog — Sase.tr | Şase & Yedek Parça Rehberi",
@@ -51,21 +15,17 @@ function BlogPage() {
canonical: "https://sase.tr/blog",
});
const { data: apiPosts } = useBlogPosts();
// All posts come from the central Directus CMS through the API.
const { data: apiPosts, isLoading } = useBlogPosts();
// Merge static + API posts, dedupe by slug (static wins), newest first.
const bySlug = new Map<string, PostCard>();
for (const p of staticPosts) bySlug.set(p.slug, p);
for (const p of apiPosts ?? []) {
if (bySlug.has(p.slug)) continue;
bySlug.set(p.slug, {
const posts = (apiPosts ?? [])
.map((p) => ({
slug: p.slug,
title: p.title,
description: p.metaDescription ?? "",
date: p.publishedAt.slice(0, 10),
});
}
const posts = [...bySlug.values()].sort((a, b) => b.date.localeCompare(a.date));
}))
.sort((a, b) => b.date.localeCompare(a.date));
return (
<div className="min-h-screen">
@@ -77,6 +37,10 @@ function BlogPage() {
Yedek parça sektörü, araç bakımı ve Sase.tr hakkında güncel yazılar.
</p>
{isLoading && posts.length === 0 && (
<p className="mt-12 text-muted-foreground">Yükleniyor</p>
)}
<div className="mt-12 grid gap-6 sm:grid-cols-2">
{posts.map((post) => (
<Link key={post.slug} to="/blog/$slug" params={{ slug: post.slug }}>

View File

@@ -1,352 +1,13 @@
import { SiteHeader } from "@/components/site-header";
import { useBlogPost } from "@/hooks/use-blog";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Button } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
// ─── BLOG POST DATA ───────────────────────────────────────────────────────────
interface BlogPost {
slug: string;
title: string;
description: string;
date: string;
content: React.ReactNode;
}
const POSTS: Record<string, BlogPost> = {
"vin-nedir-nasil-okunur": {
slug: "vin-nedir-nasil-okunur",
title: "Şase Numarası (VIN) Nedir? Nasıl Okunur?",
description:
"17 haneli VIN kodunun her bir bölümünün ne anlama geldiğini, araç geçmişini nasıl ortaya çıkarabileceğinizi adım adım anlatıyoruz.",
date: "2026-02-10",
content: (
<div className="space-y-6 leading-relaxed text-muted-foreground">
<p>
VIN (Vehicle Identification Number), yani Araç Tanımlama Numarası, her motorlu taşıta
üretim aşamasında atanan 17 karakterlik benzersiz bir koddur. Türkiye'de "şase numarası"
olarak da bilinen bu kod, aracın üretiminden imhasına kadar tüm yaşam döngüsünü takip
etmeye yarar.
</p>
<h2 className="text-xl font-semibold text-foreground">VIN Nereden Okunur?</h2>
<p>
VIN numarasına birçok yerden ulaşabilirsiniz: ön camın sol alt köşesindeki plaka, sürücü
kapısının iç kısmındaki etiket, motor bölmesi veya araç ruhsatı ve fatura bunların başında
gelir. Bazı araçlarda bagaj kapısı iç kısmında da bulunur.
</p>
<h2 className="text-xl font-semibold text-foreground">17 Karakterin Anlamı</h2>
<p>VIN üç ana bölüme ayrılır:</p>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong className="text-foreground">
WMI (1-3. karakterler) — Dünya Üretici Kodu:
</strong>{" "}
Aracın hangi ülkede ve hangi fabrikada üretildiğini gösterir. Örneğin "WVW" Volkswagen
Almanya, "ZFA" Fiat İtalya demektir.
</li>
<li>
<strong className="text-foreground">
VDS (4-9. karakterler) — Araç Tanımlayıcı Bölüm:
</strong>{" "}
Model, kasa tipi, motor hacmi, yakıt türü ve güvenlik donanımları hakkında bilgi içerir.
9. karakter her zaman kontrol karakteridir ve matematiksel bir doğrulama amacı taşır.
</li>
<li>
<strong className="text-foreground">
VIS (10-17. karakterler) — Araç Tanımlama Bölümü:
</strong>{" "}
Model yılı (10. karakter), üretim fabrikası (11. karakter) ve üretim sıra numarası
(12-17. karakterler) bilgilerini içerir.
</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">Örnek: WVWZZZ1JZ3W597935</h2>
<p>
Bu VIN'i inceleyelim: <strong className="font-mono text-foreground">WVW</strong>
Volkswagen Almanya, <strong className="font-mono text-foreground">ZZZ</strong> pazar
tanımlayıcı, <strong className="font-mono text-foreground">1J</strong> Golf modeli,{" "}
<strong className="font-mono text-foreground">Z</strong> motor tipi,{" "}
<strong className="font-mono text-foreground">3</strong> model yılı 2003,{" "}
<strong className="font-mono text-foreground">W</strong> Wolfsburg fabrikası,{" "}
<strong className="font-mono text-foreground">597935</strong> sıra numarası.
</p>
<h2 className="text-xl font-semibold text-foreground">VIN ile Ne Öğrenebilirsiniz?</h2>
<ul className="list-disc space-y-2 pl-6">
<li>Aracın üretim yılı ve fabrikası</li>
<li>Motor hacmi, yakıt türü ve şanzıman tipi</li>
<li>Kasa tipi ve donanım paketi</li>
<li>Kaza ve hasar geçmişi (resmi kayıtlarda)</li>
<li>Kilometre manipülasyonu şüphesi</li>
<li>Orijinal parça uyumluluğu</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">VIN ile Doğru Parça Bulma</h2>
<p>
VIN'in en kritik kullanım alanlarından biri yedek parça aramadır. Aynı model arabada bile
üretim yılı, motor tipi veya donanım paketine göre farklı parçalar kullanılmış olabilir.
VIN numarasıyla sorgulama yaparak yalnızca aracınıza tam uyumlu OEM parçalara ulaşabilir,
yanlış parça sipariş etme riskini sıfıra indirebilirsiniz.
</p>
<p>
Sase.tr platformunda VIN numaranızı girerek saniyeler içinde aracınızın tam teknik
bilgilerine ve orijinal parça kataloğuna erişebilirsiniz. 50'den fazla markayı destekleyen
platformumuzla yanlış parça siparişlerine son verin.
</p>
</div>
),
},
"orijinal-mi-muadil-mi": {
slug: "orijinal-mi-muadil-mi",
title: "Orijinal Parça mı, Muadil Parça mı?",
description:
"OEM ve aftermarket parçalar arasındaki farkları, avantaj ve dezavantajlarını karşılaştırmalı olarak inceliyoruz.",
date: "2026-01-28",
content: (
<div className="space-y-6 leading-relaxed text-muted-foreground">
<p>
Araç bakımı ve onarımında en sık sorulan sorulardan biri şudur: "Orijinal mi alsam, muadil
mi?" Bu soru, hem maliyet hem de güvenlik ısından kritik öneme sahiptir. İki seçeneği
tüm boyutlarıyla inceleyelim.
</p>
<h2 className="text-xl font-semibold text-foreground">OEM Parça Nedir?</h2>
<p>
OEM (Original Equipment Manufacturer), aracın üretiminde kullanılan ya da araç
üreticisinin onayladığı orijinal parçalardır. Bu parçalar araç fabrikasında kullanılan
parçalarla aynı spesifikasyonlara sahiptir ve genellikle aynı tedarikçilerden gelir.
Üzerinde araç markasının logosu bulunabilir ya da yalnızca parça numarasıyla satılabilir.
</p>
<h2 className="text-xl font-semibold text-foreground">Aftermarket (Muadil) Parça Nedir?</h2>
<p>
Muadil parçalar, araç üreticisinden bağımsız üçüncü taraf firmalar tarafından üretilir.
Kalite seviyeleri geniş bir yelpazede değişir: premium muadil parçalar OEM kalitesine
yaklaşırken, düşük kaliteli kopya ürünler araç güvenliğini tehlikeye atabilir.
</p>
<h2 className="text-xl font-semibold text-foreground">OEM Parçaların Avantajları</h2>
<ul className="list-disc space-y-2 pl-6">
<li>Araca tam uyum garantisi montaj sorunu yaşanmaz</li>
<li>Fabrika spesifikasyonlarına uygunluk</li>
<li>Araç garantisini etkilemez</li>
<li>Standart kalite tutarlılığı</li>
<li>Orijinal parça numarasıyla kolay temin</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">OEM Parçaların Dezavantajları</h2>
<ul className="list-disc space-y-2 pl-6">
<li>Genellikle muadile göre %20-50 daha pahalı</li>
<li>Her markada ve her bölgede stok bulunmayabilir</li>
<li>Bazı modeller için yüksek bekleme süresi</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">Muadil Parçaların Avantajları</h2>
<ul className="list-disc space-y-2 pl-6">
<li>Genellikle daha düşük fiyat</li>
<li>Daha kolay erişilebilirlik</li>
<li>Bazı premium muadil markalar üstün performans sunar</li>
<li>Üretimi durdurulmuş eski araçlarda tek seçenek olabilir</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">Muadil Parçaların Riskleri</h2>
<ul className="list-disc space-y-2 pl-6">
<li>Kalite tutarsızlığı marka ve fiyata göre büyük farklar</li>
<li>Uyumsuzluk riski görünürde aynı ama teknik farklılıklar olabilir</li>
<li>Araç garantisini geçersiz kılabilir</li>
<li>Güvenlik sistemlerini olumsuz etkileyebilir</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">
Hangi Durumlarda Orijinal, Hangisinde Muadil?
</h2>
<p>
Fren sistemi, hava yastığı, direksiyon ve motor parçaları gibi güvenlik kritik
bileşenlerde kesinlikle OEM tercih edilmelidir. Kaporta, döşeme veya aksesuar
niteliğindeki parçalarda güvenilir bir muadil marka değerlendirilebilir.
</p>
<h2 className="text-xl font-semibold text-foreground">Doğru Parçayı Nasıl Bulursunuz?</h2>
<p>
İster OEM ister muadil tercih edin, en önemli adım doğru OEM parça numarasını bilmektir.
Sase.tr ile araç VIN numaranızdan yola çıkarak orijinal parça kodlarına ulaşın. Bu kodu
elinizde bulundurmak, hem servisle hem de tedarikçiyle iletişimi kolaylaştırır, yanlış
parça siparişini önler.
</p>
</div>
),
},
"dijital-donusum": {
slug: "dijital-donusum",
title: "Yedek Parça Aramada Dijital Dönüşüm",
description:
"Geleneksel katalog yöntemlerinden dijital platformlara geçiş sürecini ve sektöre etkilerini ele alıyoruz.",
date: "2026-01-15",
content: (
<div className="space-y-6 leading-relaxed text-muted-foreground">
<p>
Türkiye'nin otomotiv yedek parça sektörü onlarca yıldır basılı kataloglar, telefon
görüşmeleri ve tecrübeye dayalı bilgiyle ayakta durdu. Ancak 2020'lerin ortasına
gelindiğinde bu yöntemler artık yetersiz kalmaktadır. Dijital dönüşüm, sektörde kaçınılmaz
bir zorunluluk hâline gelmiştir.
</p>
<h2 className="text-xl font-semibold text-foreground">Geleneksel Yöntemlerin Sorunları</h2>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong className="text-foreground">Zaman kaybı:</strong> Tek bir parça için birden
fazla katalog taramak ortalama 15-20 dakika sürebilir.
</li>
<li>
<strong className="text-foreground">Hata oranı:</strong> Elle yapılan arama ve
karşılaştırma işlemlerinde yanlış parça sipariş riski yüksektir.
</li>
<li>
<strong className="text-foreground">Güncellik sorunu:</strong> Basılı kataloglar yeni
model güncellemelerini yavaş takip eder.
</li>
<li>
<strong className="text-foreground">Ölçeklenemezlik:</strong> Büyüyen filo veya artan
müşteri sayısıyla geleneksel yöntemler çökmektedir.
</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">Dijital Dönüşümün Faydaları</h2>
<p>Dijital platformlar, yedek parça arama sürecini kökten değiştirmektedir:</p>
<ul className="list-disc space-y-2 pl-6">
<li>VIN bazlı anlık araç tanımlama saniyeler içinde doğru araç tespiti</li>
<li>Çoklu katalog çapraz sorgulama tek arayüzde birden fazla kaynak</li>
<li>Otomatik parça eşleştirme insan hatasını minimuma indirir</li>
<li>Gerçek zamanlı fiyat karşılaştırma</li>
<li>Sipariş geçmişi ve parça takibi</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">Sektörde Sayısal Dönüşüm</h2>
<p>Dijital platforma geçiş yapan işletmelerin deneyimlerine göre:</p>
<ul className="list-disc space-y-2 pl-6">
<li>Parça arama süresi ortalama %85 kısalmaktadır</li>
<li>Yanlış parça iade oranları %60-80 düşmektedir</li>
<li>Müşteri memnuniyeti belirgin biçimde artmaktadır</li>
<li>Personel kapasitesi daha katma değerli işlere yönlendirilebilmektedir</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">
Küçük ve Orta Ölçekli İşletmeler İçin Fırsatlar
</h2>
<p>
Dijital dönüşüm artık yalnızca büyük zincirlerin ayrıcalığı değildir. Aylık sabit
maliyetli abonelik modelleri sayesinde küçük oto yedek parçacılar ve servisler de kurumsal
araçlara erişebilmektedir. Bu durum, rekabet eşitliğini kısmen sağlamaktadır.
</p>
<h2 className="text-xl font-semibold text-foreground">Sase.tr'nin Rolü</h2>
<p>
Sase.tr, Türkiye'nin yedek parça sektörüne özgü geliştirilen bu dijital dönüşümün
öncüsüdür. VIN/şase numarası sorgulama, çoklu katalog entegrasyonu ve interaktif şema
görüntüleme özellikleriyle geleneksel yapış biçimlerini dönüştürmektedir. Platform, 27
marka ve 1 milyondan fazla OEM parça numarasıyla sektörün en kapsamlı dijital kataloğunu
sunmaktadır.
</p>
</div>
),
},
"dogru-parcayi-bulun": {
slug: "dogru-parcayi-bulun",
title: "Sase.tr ile Doğru Parçayı İlk Seferde Bulun",
description:
"Platform özelliklerini kullanarak VIN bazlı arama, şema görüntüleme ve parça eşleştirme rehberi.",
date: "2026-01-05",
content: (
<div className="space-y-6 leading-relaxed text-muted-foreground">
<p>
Yanlış parça sipariş etmek hem zaman hem para kaybettirdiği gibi, müşteri memnuniyetini de
olumsuz etkiler. Sase.tr, bu sorunu kökten çözmek için tasarlanmıştır. Bu rehberde
platformu nasıl en verimli şekilde kullanacağınızı adım adım anlatıyoruz.
</p>
<h2 className="text-xl font-semibold text-foreground">Adım 1: VIN Numarasını Girin</h2>
<p>
Ana sayfadaki arama kutusuna aracın 17 haneli şase numarasını (VIN) girin. Numara
girilirken anlık doğrulama çubuğu dolmaya başlar 17. karaktere ulaştığınızda sistem
otomatik olarak araç bilgilerini çeker.
</p>
<p>
Sistem saniyeler içinde araç markasını, modelini, yılını ve motor bilgilerini gösterir.
Bilgileri doğrulayın ve "Parça Kataloğuna Devam Et" butonuna tıklayın.
</p>
<h2 className="text-xl font-semibold text-foreground">Adım 2: Kategori Seçin</h2>
<p>
Araç tanımlandıktan sonra karşınıza o araca özgü parça kategorileri çıkar. Motor, Şasi &
Süspansiyon, Elektrik, Karoseri, Klima & Isıtma gibi ana kategorilerden ihtiyacınız olan
bölümü seçin.
</p>
<p>
Her ana kategorinin altında detaylı alt kategoriler bulunur. Örneğin "Motor" altında
Silindir Kapağı, Krank Mili, Piston, Yağ Pompası gibi bölümler yer alır.
</p>
<h2 className="text-xl font-semibold text-foreground">
Adım 3: İnteraktif Şemayı Kullanın
</h2>
<p>
Kategori seçildikten sonra o bölgеnin teknik çizimi interaktif şema ekranda ılır.
Şema üzerindeki parçalara tıklayarak OEM kodunu, ıklamasını ve gerekli miktarını
görebilirsiniz.
</p>
<p>
Şemalar zoom ve pan desteğiyle büyütülebilir, yatay veya dikey kaydırılabilir. Karmaşık
motor veya şasi bölgelerinde parça konumunu görsel olarak tespit etmek iade oranlarını
ciddi ölçüde düşürmektedir.
</p>
<h2 className="text-xl font-semibold text-foreground">Adım 4: OEM Kodunu Kopyalayın</h2>
<p>
Doğru parçayı bulduktan sonra OEM kodunu panoya kopyalayın. Bu kodu tedarikçinize, servis
faturanıza veya online sipariş formunuza yapıştırarak yanlış parça riskini tamamen ortadan
kaldırın.
</p>
<h2 className="text-xl font-semibold text-foreground">İpuçları</h2>
<ul className="list-disc space-y-2 pl-6">
<li>
Birden fazla araç için sorgu yapıyorsanız, her birini VIN ile kaydedin; geçmiş
aramalarınıza hızla dönebilirsiniz.
</li>
<li>
Aynı OEM kodu birden fazla katalogda farklı fiyatlarla listelenebilir fiyat
karşılaştırma özelliğini kullanın.
</li>
<li>
Parça bulamadığınızda kategori ağacında bir seviye yukarı çıkarak daha geniş bir arama
yapabilirsiniz.
</li>
<li>27 markanın tamamına erişmek için Full Paket aboneliği en avantajlı seçenektir.</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">Sonuç</h2>
<p>
Sase.tr ile tek bir yanlış parça iadesinden tasarruf ettiğiniz para, aylık abonelik
ücretini karşılar. 30 günlük ücretsiz deneme süresiyle platformu bugün deneyin kredi
kartı gerektirmez.
</p>
</div>
),
},
};
// ─── MARKDOWN RENDERING (API-backed posts) ─────────────────────────────────────
// Maps markdown to the same prose styling the hand-authored posts use.
// ─── MARKDOWN RENDERING ───────────────────────────────────────────────────────
// Posts live in the central Directus CMS and arrive as markdown via the API.
const mdComponents = {
h2: (props: React.ComponentProps<"h2">) => (
@@ -383,12 +44,11 @@ export const Route = createFileRoute("/blog_/$slug")({
function BlogPostPage() {
const { slug } = Route.useParams();
const staticPost = POSTS[slug];
const { data: apiPost, isLoading, isError } = useBlogPost(slug, !staticPost);
const { data: post, isLoading, isError } = useBlogPost(slug);
const title = staticPost?.title ?? apiPost?.title ?? "Blog";
const description = staticPost?.description ?? apiPost?.metaDescription ?? "";
const date = staticPost?.date ?? apiPost?.publishedAt?.slice(0, 10) ?? "";
const title = post?.title ?? "Blog";
const description = post?.metaDescription ?? "";
const date = post?.publishedAt?.slice(0, 10) ?? "";
usePageMeta({
title: `${title} | Sase.tr Blog`,
@@ -397,8 +57,7 @@ function BlogPostPage() {
});
let body: React.ReactNode;
if (staticPost) body = staticPost.content;
else if (apiPost) body = <MarkdownBody markdown={apiPost.bodyMarkdown} />;
if (post) body = <MarkdownBody markdown={post.bodyMarkdown} />;
else if (isLoading) body = <p className="text-muted-foreground">Yükleniyor</p>;
else
body = (
@@ -424,6 +83,14 @@ function BlogPostPage() {
<h1 className="mt-3 text-3xl font-bold leading-tight sm:text-4xl">{title}</h1>
{description && <p className="mt-4 text-lg text-muted-foreground">{description}</p>}
{post?.coverImage && (
<img
src={post.coverImage}
alt={title}
className="mt-8 w-full rounded-lg border object-cover"
/>
)}
<div className="mt-10">{body}</div>
</article>