feat: SEO altyapısı + admin editör/favicon/menü düzeltmeleri

Admin & site:
- @tailwindcss/typography ekle → editör ve yayın içeriği prose stilleriyle düzgün render
- Favicon: logo.png'den kare app/icon.png + apple-icon.png, varsayılan favicon.ico kaldırıldı
- SEO keyword: seo_settings.default_keywords + seo_pages.keywords + buildMetadata birleştirme
- Menü düzeni admin'den yönetilebilir (site_settings.nav_items, /admin/menu, header & mobile-menu refactor)

SEO:
- app/sitemap.ts (statik + blog/hizmet/çözüm/proje/sektör dinamik)
- app/robots.ts (sitemap ref + /admin,/api disallow)
- app/llms.txt/route.ts (AI/LLM rehberi)
- BlogPosting/Service/FAQ/Article JSON-LD wire (json-ld bileşenleri bağlandı)
- buildMetadata: blog/proje OG görseli + type article + keywords birleştirme düzeltmesi
- blog tags → keyword
This commit is contained in:
egecankomur
2026-06-04 07:15:18 +03:00
parent a321ac5c9b
commit d49c9aa225
26 changed files with 780 additions and 151 deletions
+119
View File
@@ -0,0 +1,119 @@
// Üst menü (header + mobil) düzeni. Öğeler sabit bir kayıttan gelir; admin
// panelinden yalnızca SIRA, GÖRÜNÜRLÜK ve (opsiyonel) ETİKET düzenlenir.
// Bu modül hem sunucu (header) hem istemci (admin formu) tarafında import
// edilebilir — bu yüzden "server-only" YOK.
export type NavKey =
| "home"
| "services"
| "solutions"
| "projects"
| "blog"
| "about"
| "contact";
export interface NavRegistryEntry {
key: NavKey;
label: string; // varsayılan etiket
href: string;
mega?: boolean; // Hizmetler — mega menü olarak render edilir
}
export const NAV_REGISTRY: Record<NavKey, NavRegistryEntry> = {
home: { key: "home", label: "Anasayfa", href: "/" },
services: { key: "services", label: "Hizmetler", href: "/hizmetler", mega: true },
solutions: { key: "solutions", label: "Çözümler", href: "/cozumler" },
projects: { key: "projects", label: "Projeler", href: "/projeler" },
blog: { key: "blog", label: "Blog", href: "/blog" },
about: { key: "about", label: "Hakkımızda", href: "/hakkimizda" },
contact: { key: "contact", label: "İletişim", href: "/iletisim" },
};
export const DEFAULT_NAV_ORDER: NavKey[] = [
"home",
"services",
"solutions",
"projects",
"blog",
"about",
"contact",
];
export interface NavItem {
key: NavKey;
label: string; // çözülmüş etiket (override veya varsayılan)
href: string;
mega: boolean;
visible: boolean;
}
interface StoredNavItem {
key: string;
visible?: boolean;
label?: string | null;
}
/**
* site_settings.nav_items içindeki JSON'ı kayıt ile birleştirir.
* - Kayıtlı sıra önceliklidir, geçersiz/silinmiş key'ler atlanır.
* - Kayıtta olmayan (yeni eklenen) öğeler varsayılan sırayla sona eklenir.
* - JSON yoksa/bozuksa tam varsayılan menü döner.
*/
export function resolveNavItems(navItemsJson?: string | null): NavItem[] {
let stored: StoredNavItem[] = [];
if (navItemsJson) {
try {
const parsed = JSON.parse(navItemsJson);
if (Array.isArray(parsed)) stored = parsed as StoredNavItem[];
} catch {
/* bozuk JSON — varsayılanlara düş */
}
}
const seen = new Set<NavKey>();
const ordered: NavItem[] = [];
for (const item of stored) {
const reg = NAV_REGISTRY[item.key as NavKey];
if (!reg || seen.has(reg.key)) continue;
seen.add(reg.key);
ordered.push({
key: reg.key,
label: item.label?.trim() || reg.label,
href: reg.href,
mega: !!reg.mega,
visible: item.visible !== false,
});
}
for (const key of DEFAULT_NAV_ORDER) {
if (seen.has(key)) continue;
const reg = NAV_REGISTRY[key];
ordered.push({
key: reg.key,
label: reg.label,
href: reg.href,
mega: !!reg.mega,
visible: true,
});
}
return ordered;
}
/** Admin formundan gelen düzeni depolanacak kompakt JSON'a çevirir. */
export function serializeNavItems(
items: { key: NavKey; visible: boolean; label?: string }[],
): string {
return JSON.stringify(
items.map((i) => {
const reg = NAV_REGISTRY[i.key];
const out: StoredNavItem = { key: i.key, visible: i.visible };
// Sadece varsayılandan farklıysa etiketi sakla
if (i.label && i.label.trim() && i.label.trim() !== reg.label) {
out.label = i.label.trim();
}
return out;
}),
);
}