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:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}: {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,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 */}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user