feat: add full SEO (generateMetadata, OG, Twitter Card, JSON-LD, sitemap, robots, llms.txt)
This commit is contained in:
+126
-5
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Playfair_Display, Lato } from "next/font/google";
|
import { Playfair_Display, Lato } from "next/font/google";
|
||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
|
import { prisma } from "@/lib/db";
|
||||||
|
|
||||||
const playfair = Playfair_Display({
|
const playfair = Playfair_Display({
|
||||||
variable: "--font-playfair",
|
variable: "--font-playfair",
|
||||||
@@ -13,15 +14,91 @@ const lato = Lato({
|
|||||||
weight: ["300", "400", "700", "900"],
|
weight: ["300", "400", "700", "900"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Moy Beach Akyaka | Lüks & Huzur",
|
|
||||||
description: "Akyaka'nın kalbinde doğayla iç içe lüks bir beach, restoran ve otel deneyimi.",
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
return [{ lang: 'tr' }, { lang: 'en' }];
|
return [{ lang: 'tr' }, { lang: 'en' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata(
|
||||||
|
{ params }: { params: Promise<{ lang: string }> }
|
||||||
|
): Promise<Metadata> {
|
||||||
|
const { lang } = await params;
|
||||||
|
const settings = await prisma.siteSettings.findFirst();
|
||||||
|
|
||||||
|
const isTr = lang === 'tr';
|
||||||
|
|
||||||
|
const title = isTr
|
||||||
|
? (settings?.metaTitleTr || 'Moy Beach Akyaka | Lüks & Huzur')
|
||||||
|
: (settings?.metaTitleEn || 'Moy Beach Akyaka | Luxury & Serenity');
|
||||||
|
|
||||||
|
const description = isTr
|
||||||
|
? (settings?.metaDescTr || "Akyaka'nın kalbinde doğayla iç içe lüks bir beach, restoran ve otel deneyimi.")
|
||||||
|
: (settings?.metaDescEn || 'A luxury beach, restaurant and hotel experience immersed in nature at the heart of Akyaka.');
|
||||||
|
|
||||||
|
const keywords = isTr
|
||||||
|
? (settings?.metaKeywordsTr || 'moy beach, akyaka, beach, restoran, otel, muğla, deniz, tatil')
|
||||||
|
: (settings?.metaKeywordsEn || 'moy beach, akyaka, beach, restaurant, hotel, mugla, sea, holiday');
|
||||||
|
|
||||||
|
const siteUrl = settings?.siteUrl || 'https://moybeach.com';
|
||||||
|
const ogImage = settings?.ogImage || `${siteUrl}/moy-beach.jpg`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
keywords,
|
||||||
|
authors: [{ name: 'Moy Beach' }],
|
||||||
|
creator: 'Moy Beach',
|
||||||
|
publisher: 'Moy Beach',
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
'max-video-preview': -1,
|
||||||
|
'max-image-preview': 'large',
|
||||||
|
'max-snippet': -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${lang}`,
|
||||||
|
languages: {
|
||||||
|
'tr': `${siteUrl}/tr`,
|
||||||
|
'en': `${siteUrl}/en`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
locale: isTr ? 'tr_TR' : 'en_US',
|
||||||
|
alternateLocale: isTr ? 'en_US' : 'tr_TR',
|
||||||
|
url: `${siteUrl}/${lang}`,
|
||||||
|
siteName: 'Moy Beach',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: ogImage,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: title,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
images: [ogImage],
|
||||||
|
...(settings?.twitterHandle ? { creator: `@${settings.twitterHandle}`, site: `@${settings.twitterHandle}` } : {}),
|
||||||
|
},
|
||||||
|
verification: {
|
||||||
|
...(settings?.googleVerify ? { google: settings.googleVerify } : {}),
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
'theme-color': '#1a1a2e',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
params,
|
params,
|
||||||
@@ -30,9 +107,53 @@ export default async function RootLayout({
|
|||||||
params: Promise<{ lang: string }>;
|
params: Promise<{ lang: string }>;
|
||||||
}>) {
|
}>) {
|
||||||
const { lang } = await params;
|
const { lang } = await params;
|
||||||
|
const settings = await prisma.siteSettings.findFirst();
|
||||||
|
const siteUrl = settings?.siteUrl || 'https://moybeach.com';
|
||||||
|
|
||||||
|
// JSON-LD Structured Data
|
||||||
|
const jsonLd = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'LocalBusiness',
|
||||||
|
'@id': siteUrl,
|
||||||
|
name: 'Moy Beach',
|
||||||
|
url: siteUrl,
|
||||||
|
logo: settings?.logoUrl || `${siteUrl}/logo.png`,
|
||||||
|
image: settings?.ogImage || `${siteUrl}/moy-beach.jpg`,
|
||||||
|
description: lang === 'tr'
|
||||||
|
? (settings?.metaDescTr || "Akyaka'nın kalbinde doğayla iç içe lüks bir beach, restoran ve otel deneyimi.")
|
||||||
|
: (settings?.metaDescEn || 'A luxury beach, restaurant and hotel experience immersed in nature at the heart of Akyaka.'),
|
||||||
|
telephone: settings?.phone || '',
|
||||||
|
email: settings?.email || '',
|
||||||
|
address: {
|
||||||
|
'@type': 'PostalAddress',
|
||||||
|
streetAddress: settings?.addressText1 || '',
|
||||||
|
addressLocality: 'Akyaka',
|
||||||
|
addressRegion: 'Muğla',
|
||||||
|
addressCountry: 'TR',
|
||||||
|
},
|
||||||
|
openingHours: settings?.workingHours || '',
|
||||||
|
sameAs: [
|
||||||
|
settings?.instagramUrl,
|
||||||
|
settings?.facebookUrl,
|
||||||
|
settings?.youtubeUrl,
|
||||||
|
].filter(Boolean),
|
||||||
|
servesCuisine: ['Turkish', 'Mediterranean', 'Seafood'],
|
||||||
|
priceRange: '₺₺₺',
|
||||||
|
geo: {
|
||||||
|
'@type': 'GeoCoordinates',
|
||||||
|
latitude: 37.0437,
|
||||||
|
longitude: 28.3249,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={lang} className="scroll-smooth">
|
<html lang={lang} className="scroll-smooth">
|
||||||
|
<head>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
<body className={`${lato.variable} ${playfair.variable} antialiased`}>
|
<body className={`${lato.variable} ${playfair.variable} antialiased`}>
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+136
-10
@@ -1,7 +1,7 @@
|
|||||||
import { prisma } from '@/lib/db';
|
import { prisma } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation'
|
||||||
import { Save, Upload } from 'lucide-react';
|
import { Save } from 'lucide-react';
|
||||||
import { updateSiteSettings } from '../actions';
|
import { updateSiteSettings } from '../actions';
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import { uploadImage } from '@/lib/cloudinary';
|
import { uploadImage } from '@/lib/cloudinary';
|
||||||
@@ -32,6 +32,16 @@ export default async function SettingsAdminPage() {
|
|||||||
facebookUrl: formData.get('facebookUrl') as string,
|
facebookUrl: formData.get('facebookUrl') as string,
|
||||||
youtubeUrl: formData.get('youtubeUrl') as string,
|
youtubeUrl: formData.get('youtubeUrl') as string,
|
||||||
whatsappNumber: formData.get('whatsappNumber') as string,
|
whatsappNumber: formData.get('whatsappNumber') as string,
|
||||||
|
// SEO fields
|
||||||
|
siteUrl: formData.get('siteUrl') as string,
|
||||||
|
metaTitleTr: formData.get('metaTitleTr') as string,
|
||||||
|
metaTitleEn: formData.get('metaTitleEn') as string,
|
||||||
|
metaDescTr: formData.get('metaDescTr') as string,
|
||||||
|
metaDescEn: formData.get('metaDescEn') as string,
|
||||||
|
metaKeywordsTr: formData.get('metaKeywordsTr') as string,
|
||||||
|
metaKeywordsEn: formData.get('metaKeywordsEn') as string,
|
||||||
|
twitterHandle: formData.get('twitterHandle') as string,
|
||||||
|
googleVerify: formData.get('googleVerify') as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const logoFile = formData.get('logo') as File;
|
const logoFile = formData.get('logo') as File;
|
||||||
@@ -39,19 +49,22 @@ export default async function SettingsAdminPage() {
|
|||||||
data.logoUrl = await uploadImage(logoFile);
|
data.logoUrl = await uploadImage(logoFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ogImageFile = formData.get('ogImage') as File;
|
||||||
|
if (ogImageFile && ogImageFile.size > 0) {
|
||||||
|
data.ogImage = await uploadImage(ogImageFile);
|
||||||
|
}
|
||||||
|
|
||||||
await updateSiteSettings(data);
|
await updateSiteSettings(data);
|
||||||
revalidatePath('/', 'layout');
|
revalidatePath('/', 'layout');
|
||||||
redirect('/admin/settings');
|
redirect('/admin/settings');
|
||||||
}} className="space-y-8">
|
}} className="space-y-10">
|
||||||
|
|
||||||
{/* Logo Section */}
|
{/* Logo Section */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-xl font-bold mb-4 border-b pb-2">Logo Ayarları</h2>
|
<h2 className="text-xl font-bold mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">Logo Ayarları</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Yeni Logo Yükle</label>
|
||||||
Yeni Logo Yükle
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
name="logo"
|
name="logo"
|
||||||
@@ -73,7 +86,7 @@ export default async function SettingsAdminPage() {
|
|||||||
|
|
||||||
{/* Contact Section */}
|
{/* Contact Section */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-xl font-bold mb-4 border-b pb-2">İletişim Bilgileri</h2>
|
<h2 className="text-xl font-bold mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">İletişim Bilgileri</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Telefon</label>
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Telefon</label>
|
||||||
@@ -89,7 +102,7 @@ export default async function SettingsAdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">WhatsApp Numarası</label>
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">WhatsApp Numarası</label>
|
||||||
<input type="text" name="whatsappNumber" defaultValue={settings?.whatsappNumber || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="905344656235 (Sadece rakam, + olmadan)" />
|
<input type="text" name="whatsappNumber" defaultValue={settings?.whatsappNumber || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="905344656235" />
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Adres Satırı 1</label>
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Adres Satırı 1</label>
|
||||||
@@ -104,7 +117,7 @@ export default async function SettingsAdminPage() {
|
|||||||
|
|
||||||
{/* Social Media */}
|
{/* Social Media */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-xl font-bold mb-4 border-b pb-2">Sosyal Medya Linkleri</h2>
|
<h2 className="text-xl font-bold mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">Sosyal Medya Linkleri</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Instagram URL</label>
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Instagram URL</label>
|
||||||
@@ -121,6 +134,119 @@ export default async function SettingsAdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* SEO Section */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-xl font-bold mb-1 border-b border-slate-200 dark:border-slate-700 pb-2 flex items-center gap-2">
|
||||||
|
🔍 SEO & Metadata Ayarları
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-500 dark:text-slate-400 mb-6">
|
||||||
|
Arama motorları ve sosyal medya paylaşımları için meta bilgileri.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Site URL */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Site URL <span className="text-slate-400 font-normal">(canonical & sitemap için)</span>
|
||||||
|
</label>
|
||||||
|
<input type="url" name="siteUrl" defaultValue={settings?.siteUrl || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="https://moybeach.com" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Meta Titles */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Meta Başlık — Türkçe
|
||||||
|
</label>
|
||||||
|
<input type="text" name="metaTitleTr" defaultValue={settings?.metaTitleTr || ""} maxLength={60} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="Moy Beach Akyaka | Lüks & Huzur" />
|
||||||
|
<p className="text-xs text-slate-400 mt-1">Max 60 karakter önerilir</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Meta Başlık — İngilizce
|
||||||
|
</label>
|
||||||
|
<input type="text" name="metaTitleEn" defaultValue={settings?.metaTitleEn || ""} maxLength={60} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="Moy Beach Akyaka | Luxury & Serenity" />
|
||||||
|
<p className="text-xs text-slate-400 mt-1">Max 60 characters recommended</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Meta Descriptions */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Meta Açıklama — Türkçe
|
||||||
|
</label>
|
||||||
|
<textarea name="metaDescTr" rows={3} defaultValue={settings?.metaDescTr || ""} maxLength={160} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 resize-none" placeholder="Akyaka'nın kalbinde doğayla iç içe lüks bir beach, restoran ve otel deneyimi." />
|
||||||
|
<p className="text-xs text-slate-400 mt-1">Max 160 karakter önerilir</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Meta Açıklama — İngilizce
|
||||||
|
</label>
|
||||||
|
<textarea name="metaDescEn" rows={3} defaultValue={settings?.metaDescEn || ""} maxLength={160} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 resize-none" placeholder="A luxury beach, restaurant and hotel experience in the heart of Akyaka." />
|
||||||
|
<p className="text-xs text-slate-400 mt-1">Max 160 characters recommended</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Keywords */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Anahtar Kelimeler — Türkçe
|
||||||
|
</label>
|
||||||
|
<input type="text" name="metaKeywordsTr" defaultValue={settings?.metaKeywordsTr || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="moy beach, akyaka, beach, restoran, otel, muğla" />
|
||||||
|
<p className="text-xs text-slate-400 mt-1">Virgülle ayırın</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Keywords — English
|
||||||
|
</label>
|
||||||
|
<input type="text" name="metaKeywordsEn" defaultValue={settings?.metaKeywordsEn || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="moy beach, akyaka, beach, restaurant, hotel, mugla" />
|
||||||
|
<p className="text-xs text-slate-400 mt-1">Separate with commas</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OG Image */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
OG / Paylaşım Görseli
|
||||||
|
<span className="text-slate-400 font-normal ml-1">(1200×630px önerilir)</span>
|
||||||
|
</label>
|
||||||
|
<input type="file" name="ogImage" accept="image/*" className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" />
|
||||||
|
</div>
|
||||||
|
{settings?.ogImage && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Mevcut OG Görseli</label>
|
||||||
|
<div className="rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700 w-full max-w-xs">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img src={settings.ogImage} alt="OG Image" className="w-full h-auto object-cover" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Twitter & Google Verify */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Twitter/X Handle
|
||||||
|
<span className="text-slate-400 font-normal ml-1">(@işareti olmadan)</span>
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 font-medium">@</span>
|
||||||
|
<input type="text" name="twitterHandle" defaultValue={settings?.twitterHandle || ""} className="w-full pl-8 pr-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="moybeachakyaka" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Google Search Console Doğrulama Kodu
|
||||||
|
</label>
|
||||||
|
<input type="text" name="googleVerify" defaultValue={settings?.googleVerify || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="google-site-verification kodu" />
|
||||||
|
<p className="text-xs text-slate-400 mt-1">Google Search Console → Doğrulama → HTML Meta Tag'deki content değeri</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="pt-4 flex justify-end">
|
<div className="pt-4 flex justify-end">
|
||||||
<button type="submit" className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg transition-colors font-medium">
|
<button type="submit" className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg transition-colors font-medium">
|
||||||
<Save className="w-5 h-5" />
|
<Save className="w-5 h-5" />
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { prisma } from '@/lib/db';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const [settings, services] = await Promise.all([
|
||||||
|
prisma.siteSettings.findFirst(),
|
||||||
|
prisma.service.findMany({ orderBy: { createdAt: 'asc' } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const siteUrl = settings?.siteUrl || 'https://moybeach.com';
|
||||||
|
const siteName = 'Moy Beach';
|
||||||
|
|
||||||
|
const serviceList = services.length > 0
|
||||||
|
? services.map(s => `- **${s.title}**: ${s.description}`).join('\n')
|
||||||
|
: '- Beach hizmetleri\n- Restoran\n- Otel';
|
||||||
|
|
||||||
|
const contactLines = [
|
||||||
|
settings?.phone ? `- Telefon: ${settings.phone}` : '',
|
||||||
|
settings?.email ? `- E-posta: ${settings.email}` : '',
|
||||||
|
settings?.addressText1 ? `- Adres: ${settings.addressText1}${settings.addressText2 ? ', ' + settings.addressText2 : ''}` : '',
|
||||||
|
settings?.workingHours ? `- Çalışma Saatleri: ${settings.workingHours}` : '',
|
||||||
|
].filter(Boolean).join('\n');
|
||||||
|
|
||||||
|
const socialLines = [
|
||||||
|
settings?.instagramUrl ? `- Instagram: ${settings.instagramUrl}` : '',
|
||||||
|
settings?.facebookUrl ? `- Facebook: ${settings.facebookUrl}` : '',
|
||||||
|
settings?.youtubeUrl ? `- YouTube: ${settings.youtubeUrl}` : '',
|
||||||
|
settings?.whatsappNumber ? `- WhatsApp: https://wa.me/${settings.whatsappNumber}` : '',
|
||||||
|
].filter(Boolean).join('\n');
|
||||||
|
|
||||||
|
const content = `# ${siteName}
|
||||||
|
|
||||||
|
> ${settings?.metaDescTr || "Akyaka'nın kalbinde doğayla iç içe lüks bir beach, restoran ve otel deneyimi."}
|
||||||
|
|
||||||
|
${siteName} is a luxury beach, restaurant and hotel located in Akyaka, Muğla, Turkey.
|
||||||
|
Situated at the heart of the Akyaka coast, it offers a unique blend of natural beauty,
|
||||||
|
fine dining, and premium hospitality.
|
||||||
|
|
||||||
|
## Important Links
|
||||||
|
|
||||||
|
- Homepage (TR): ${siteUrl}/tr
|
||||||
|
- Homepage (EN): ${siteUrl}/en
|
||||||
|
- Sitemap: ${siteUrl}/sitemap.xml
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
${serviceList}
|
||||||
|
|
||||||
|
## Contact Information
|
||||||
|
|
||||||
|
${contactLines || '- İletişim bilgileri için siteyi ziyaret edin.'}
|
||||||
|
|
||||||
|
## Social Media
|
||||||
|
|
||||||
|
${socialLines || '- Sosyal medya bilgileri için siteyi ziyaret edin.'}
|
||||||
|
|
||||||
|
## Location
|
||||||
|
|
||||||
|
- Şehir: Akyaka, Muğla, Türkiye
|
||||||
|
- Bölge: Ege Bölgesi, Türkiye
|
||||||
|
- Koordinatlar: 37.0437° N, 28.3249° E
|
||||||
|
|
||||||
|
## Language Support
|
||||||
|
|
||||||
|
This website is available in Turkish (TR) and English (EN).
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
${siteName} offers an exclusive beachside experience in one of Turkey's most
|
||||||
|
pristine coastal towns — Akyaka. Known for its crystal-clear waters,
|
||||||
|
fresh Ula wind, and untouched nature, Akyaka is a hidden gem on the
|
||||||
|
Aegean coast. ${siteName} brings luxury and comfort to this natural paradise.
|
||||||
|
|
||||||
|
---
|
||||||
|
Generated: ${new Date().toISOString()}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return new NextResponse(content, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
|
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { MetadataRoute } from 'next';
|
||||||
|
import { prisma } from '@/lib/db';
|
||||||
|
|
||||||
|
export default async function robots(): Promise<MetadataRoute.Robots> {
|
||||||
|
const settings = await prisma.siteSettings.findFirst();
|
||||||
|
const siteUrl = settings?.siteUrl || 'https://moybeach.com';
|
||||||
|
|
||||||
|
return {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
userAgent: '*',
|
||||||
|
allow: '/',
|
||||||
|
disallow: ['/admin/', '/api/'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sitemap: `${siteUrl}/sitemap.xml`,
|
||||||
|
host: siteUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { MetadataRoute } from 'next';
|
||||||
|
import { prisma } from '@/lib/db';
|
||||||
|
|
||||||
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
|
const settings = await prisma.siteSettings.findFirst();
|
||||||
|
const siteUrl = settings?.siteUrl || 'https://moybeach.com';
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/tr`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'weekly',
|
||||||
|
priority: 1,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
tr: `${siteUrl}/tr`,
|
||||||
|
en: `${siteUrl}/en`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/en`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'weekly',
|
||||||
|
priority: 1,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
tr: `${siteUrl}/tr`,
|
||||||
|
en: `${siteUrl}/en`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ model HeroMedia {
|
|||||||
|
|
||||||
model SiteSettings {
|
model SiteSettings {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
// Genel
|
||||||
logoUrl String?
|
logoUrl String?
|
||||||
phone String?
|
phone String?
|
||||||
email String?
|
email String?
|
||||||
@@ -60,6 +61,17 @@ model SiteSettings {
|
|||||||
facebookUrl String?
|
facebookUrl String?
|
||||||
youtubeUrl String?
|
youtubeUrl String?
|
||||||
whatsappNumber String?
|
whatsappNumber String?
|
||||||
|
// SEO
|
||||||
|
siteUrl String?
|
||||||
|
metaTitleTr String?
|
||||||
|
metaTitleEn String?
|
||||||
|
metaDescTr String?
|
||||||
|
metaDescEn String?
|
||||||
|
metaKeywordsTr String?
|
||||||
|
metaKeywordsEn String?
|
||||||
|
ogImage String?
|
||||||
|
twitterHandle String?
|
||||||
|
googleVerify String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user