feat(seo): implement SEO audit remediations

- Add dynamic sitemap.ts, robots.ts, and llms.txt route
- Add metadataBase and JSON-LD Organization schema to layout
- Set canonical alternates for all routes
- Migrate raw img tags to next/image for LCP optimization
- Add SEO dictionary strings to tr.json and en.json
This commit is contained in:
mstfyldz
2026-06-04 12:29:19 +03:00
parent 4996ec38e6
commit e1d8bae5a4
17 changed files with 233 additions and 28 deletions
+5 -3
View File
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { motion, type Variants } from "framer-motion";
import Link from "next/link";
import { Utensils, Umbrella, Home, Wind, Hotel, Star, MapPin, ArrowRight } from "lucide-react";
@@ -289,11 +290,12 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
className={`flex flex-col ${i % 2 === 1 ? "md:flex-row-reverse" : "md:flex-row"} gap-10 items-center`}
>
{/* Image */}
<div className="w-full md:w-[55%] group relative overflow-hidden shadow-[var(--shadow-xl)]">
<img
<div className="w-full md:w-[55%] h-[420px] group relative overflow-hidden shadow-[var(--shadow-xl)]">
<Image
src={venue.img}
alt={venue.title}
className="w-full h-[420px] object-cover transition-transform duration-700 group-hover:scale-105"
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className={`absolute inset-0 bg-gradient-to-t ${venue.color} to-transparent opacity-50`} />
<Badge variant="light" className="absolute bottom-6 left-6 shadow-md">
+8 -4
View File
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion";
import { X, ZoomIn } from "lucide-react";
import { Badge } from "@/app/components/ui/badge";
@@ -75,10 +76,11 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any }
className="group relative aspect-square cursor-pointer overflow-hidden"
onClick={() => setLightbox(img)}
>
<img
<Image
src={img.src}
alt={img.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
fill
className="object-cover transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-[var(--color-moy-dark)]/0 group-hover:bg-[var(--color-moy-dark)]/50 transition-all duration-300 flex flex-col items-center justify-center gap-3">
<ZoomIn
@@ -125,10 +127,12 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any }
className="max-w-4xl w-full"
onClick={(e) => e.stopPropagation()}
>
<img
<Image
src={lightbox.src}
alt={lightbox.title}
className="w-full max-h-[80vh] object-contain"
width={1200}
height={800}
className="w-full h-auto max-h-[80vh] object-contain"
/>
<p className="text-center text-white font-serif text-xl mt-4">{lightbox.title}</p>
</motion.div>
+11 -3
View File
@@ -1,9 +1,17 @@
import { getDictionary, Locale } from "../../dictionaries";
import GaleriClient from "./GaleriClient";
export const metadata = {
title: "Galeri | Moy Group",
};
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const dict = await getDictionary(lang as Locale);
return {
title: dict.seo.gallery.title,
description: dict.seo.gallery.description,
alternates: {
canonical: `/${lang}/galeri`,
},
};
}
export default async function GaleriPage({
params,
+12
View File
@@ -32,6 +32,18 @@ const LOCATIONS = [
},
];
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const dict = await getDictionary(lang as Locale);
return {
title: dict.seo.contact.title,
description: dict.seo.contact.description,
alternates: {
canonical: `/${lang}/iletisim`,
},
};
}
export default async function IletisimPage({
params,
}: {
+11 -3
View File
@@ -1,8 +1,16 @@
import { getDictionary, Locale } from "../../dictionaries";
export const metadata = {
title: "Kurumsal | Moy Group",
};
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const dict = await getDictionary(lang as Locale);
return {
title: dict.seo.corporate.title,
description: dict.seo.corporate.description,
alternates: {
canonical: `/${lang}/kurumsal`,
},
};
}
export default async function KurumsalPage({
params,
+31 -4
View File
@@ -17,10 +17,18 @@ const montserrat = Montserrat({
weight: ["300", "400", "500", "600", "700"],
});
export const metadata: Metadata = {
title: "Moy Group | Ege'nin Ruhunu Hisset",
description: "Akyaka, Akçapınar ve Datça'da restoran, beach, bungalov ve kitesurf hizmetleriyle doğa ile iç içe premium tatil deneyimi.",
};
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params;
const dict = await getDictionary(lang as Locale);
return {
metadataBase: new URL("https://moygroup.com.tr"),
title: {
template: "%s | Moy Group",
default: dict.seo.home.title,
},
description: dict.seo.home.description,
};
}
export default async function RootLayout({
children,
@@ -33,11 +41,30 @@ export default async function RootLayout({
const locale = lang as Locale;
const dict = await getDictionary(locale);
const jsonLd = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Moy Group",
"url": "https://moygroup.com.tr",
"logo": "https://moygroup.com.tr/logo.png",
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+90-534-465-62-35",
"contactType": "customer service"
}
};
return (
<html
lang={locale}
className={`${cormorant.variable} ${montserrat.variable} h-full antialiased`}
>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body className="min-h-full flex flex-col">
<Navbar lang={locale} dict={dict.navigation} />
<main className="flex-1">{children}</main>
+11 -3
View File
@@ -2,9 +2,17 @@ import { SectionHeader } from "@/app/components/ui/section-header";
import { Badge } from "@/app/components/ui/badge";
import { getDictionary, Locale } from "../../dictionaries";
export const metadata = {
title: "Dijital Menü | Moy Group",
};
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const dict = await getDictionary(lang as Locale);
return {
title: dict.seo.menu.title,
description: dict.seo.menu.description,
alternates: {
canonical: `/${lang}/menu`,
},
};
}
export default async function MenuPage({
params,
+12 -1
View File
@@ -1,6 +1,18 @@
import { getDictionary, Locale } from "../dictionaries";
import HomeClient from "./HomeClient";
export async function generateMetadata({ params }: { params: Promise<{ lang: Locale }> }) {
const { lang } = await params;
const dict = await getDictionary(lang);
return {
title: dict.seo.home.title,
description: dict.seo.home.description,
alternates: {
canonical: `/${lang}`,
},
};
}
export default async function Page({
params,
}: {
@@ -8,6 +20,5 @@ export default async function Page({
}) {
const { lang } = await params;
const dict = await getDictionary(lang);
return <HomeClient lang={lang} dict={dict.home} />;
}
+11 -3
View File
@@ -1,9 +1,17 @@
import { getDictionary, Locale } from "../../dictionaries";
import SSSClient from "./SSSClient";
export const metadata = {
title: "SSS | Moy Group",
};
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const dict = await getDictionary(lang as Locale);
return {
title: dict.seo.faq.title,
description: dict.seo.faq.description,
alternates: {
canonical: `/${lang}/sss`,
},
};
}
export default async function SSSPage({
params,
+2 -1
View File
@@ -1,4 +1,5 @@
import Link from "next/link";
import Image from "next/image";
import { MapPin, Phone, Mail } from "lucide-react";
import { Locale } from "../dictionaries";
@@ -9,7 +10,7 @@ export default function Footer({ lang, dict }: { lang: Locale; dict: any }) {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-12">
{/* Brand */}
<div className="space-y-4">
<img src="/logo.png" alt="Moy Group Logo" className="h-12 w-auto mb-6" />
<Image src="/logo.png" alt="Moy Group Logo" width={180} height={48} className="h-12 w-auto mb-6" />
<p className="text-gray-400 text-sm leading-relaxed">
{dict.description}
</p>
+2 -1
View File
@@ -2,6 +2,7 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
import { Menu, X, Globe } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
@@ -53,7 +54,7 @@ export default function Navbar({ lang, dict }: { lang: Locale; dict: any }) {
<div className="flex justify-between items-center">
{/* Logo */}
<Link href={`/${lang}`} className="flex-shrink-0">
<img src="/logo.png" alt="Moy Group Logo" className="h-10 w-auto" />
<Image src="/logo.png" alt="Moy Group Logo" width={160} height={40} className="h-10 w-auto" priority />
</Link>
{/* Desktop Nav */}
+5 -2
View File
@@ -1,5 +1,7 @@
"use client";
import * as React from "react"
import Image from "next/image"
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
@@ -43,10 +45,11 @@ export function ImageCard({ src, alt, title, subtitle, badge, className }: Image
return (
<Card className={cn("group cursor-pointer", className)}>
<div className="relative h-64 overflow-hidden">
<img
<Image
src={src}
alt={alt}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-108"
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-400" />
{badge && (
+25
View File
@@ -0,0 +1,25 @@
import { NextResponse } from 'next/server'
export async function GET() {
const content = `# Moy Group - llms.txt
Moy Group operates premium tourism venues in Akyaka, Akçapınar, and Datça, Turkey.
Our main venues include:
- Moy Beach (Akyaka) - A premium beach and gastronomy experience.
- Kite Beach Hotel (Akçapınar) - A boutique hotel right by the kiteboard beach.
- Volkswagen Kite Surfing (Akçapınar) - Professional kitesurf training and equipment rental.
We offer sustainable and nature-respectful holiday experiences with local Aegean flavors and excellent accommodation.
Website: https://moygroup.com.tr
Locations: Akyaka, Datça, Akçapınar (Muğla, Turkey)
`
return new NextResponse(content, {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
})
}
+11
View File
@@ -0,0 +1,11 @@
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: 'https://moygroup.com.tr/sitemap.xml',
}
}
+24
View File
@@ -0,0 +1,24 @@
import { MetadataRoute } from 'next'
import { i18n } from './dictionaries'
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://moygroup.com.tr'
// Public pages defined in the project
const pages = ['', '/kurumsal', '/galeri', '/iletisim', '/menu', '/sss']
const sitemapEntries: MetadataRoute.Sitemap = []
for (const locale of i18n.locales) {
for (const page of pages) {
sitemapEntries.push({
url: `${baseUrl}/${locale}${page}`,
lastModified: new Date(),
changeFrequency: page === '' ? 'weekly' : 'monthly',
priority: page === '' ? 1 : 0.8,
})
}
}
return sitemapEntries
}