From d0a7205f902d8b1dba01769ecd6b24c00485e841 Mon Sep 17 00:00:00 2001 From: AyrisAI Date: Sat, 16 May 2026 00:55:42 +0300 Subject: [PATCH] feat: implement programmatic SEO infrastructure with localized service pages --- app/actions.ts | 35 +++++ app/services/[slug]/[location]/page.tsx | 175 ++++++++++++++++++++++++ app/services/page.tsx | 7 +- components/ServicesClient.tsx | 36 ++++- setup_locations.mjs | 50 +++++++ 5 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 app/services/[slug]/[location]/page.tsx create mode 100644 setup_locations.mjs diff --git a/app/actions.ts b/app/actions.ts index 8cd6c62..4a72bfe 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -97,4 +97,39 @@ export async function getProjectBySlug(slug: string) { return null; } } +export async function getServiceBySlug(slug: string) { + try { + const services = await sql`SELECT * FROM services WHERE slug = ${slug} LIMIT 1`; + return services[0] || null; + } catch (error) { + console.error('Error fetching service:', error); + return null; + } +} +export async function getLocationBySlug(slug: string) { + try { + const locations = await sql`SELECT * FROM locations WHERE slug = ${slug} LIMIT 1`; + return locations[0] || null; + } catch (error) { + console.error('Error fetching location:', error); + return null; + } +} + +export async function getProjectsByService(serviceName: string) { + try { + // Search projects where the serviceName is in the title or categories + // serviceName is expected to be something like "Drone Çekimi" + return await sql` + SELECT * FROM projects + WHERE title ILIKE ${'%' + serviceName + '%'} + OR categories::text ILIKE ${'%' + serviceName + '%'} + ORDER BY created_at DESC + LIMIT 4 + `; + } catch (error) { + console.error('Error fetching projects by service:', error); + return []; + } +} diff --git a/app/services/[slug]/[location]/page.tsx b/app/services/[slug]/[location]/page.tsx new file mode 100644 index 0000000..0ca806a --- /dev/null +++ b/app/services/[slug]/[location]/page.tsx @@ -0,0 +1,175 @@ +import { getServiceBySlug, getLocationBySlug, getProjectsByService, getSettings } from "@/app/actions"; +import Navbar from "@/components/Navbar"; +import Footer from "@/components/Footer"; +import Image from "next/image"; +import Link from "next/link"; +import { ArrowRight, CheckCircle2 } from "lucide-react"; +import { Metadata } from "next"; +import { notFound } from "next/navigation"; + +interface Props { + params: Promise<{ + slug: string; + location: string; + }>; +} + +export async function generateMetadata({ params }: Props): Promise { + const { slug, location: locationSlug } = await params; + const [service, location] = await Promise.all([ + getServiceBySlug(slug), + getLocationBySlug(locationSlug) + ]); + + if (!service || !location) return {}; + + const title = `${location.name} ${service.title} | Muğla Dijital`; + const description = `${location.name} bölgesinde profesyonel ${service.title.toLowerCase()} hizmetleri. Muğla Dijital Medya Ajansı ile markanızı zirveye taşıyın.`; + + return { + title, + description, + alternates: { + canonical: `/services/${slug}/${locationSlug}`, + } + }; +} + +export default async function ServiceLocationPage({ params }: Props) { + const { slug, location: locationSlug } = await params; + const [service, location, settings] = await Promise.all([ + getServiceBySlug(slug), + getLocationBySlug(locationSlug), + getSettings() + ]); + + if (!service || !location) { + notFound(); + } + + const projects = await getProjectsByService(service.title); + + return ( +
+ + + {/* Hero Section */} +
+
+ + {location.name} / {service.title} + +

+ {location.name}
+ {service.title}
+ Çözümleri. +

+

+ {location.name} bölgesindeki işletmeniz için profesyonel {service.title.toLowerCase()} hizmetleri sunuyoruz. + Markanızın dijital varlığını {location.name} ruhuna uygun, estetik ve stratejik bir dille inşa ediyoruz. +

+
+
+ + {/* Content & Features */} +
+
+
+
+

Neler Sunuyoruz?

+
+ {[ + "Profesyonel Ekipman", + "Yüksek Çözünürlük", + "Hızlı Teslimat", + "Stratejik Planlama", + "Yaratıcı Kurgu", + "Müşteri Odaklılık" + ].map((feature) => ( +
+ + {feature} +
+ ))} +
+
+ +
+

+ {location.name}, Muğla'nın en değerli bölgelerinden biri olarak kendine has bir kimliğe sahip. + Muğla Dijital olarak biz, bu bölgedeki rekabetin farkındayız ve markanızı öne çıkaracak + {service.title.toLowerCase()} stratejilerini hayata geçiriyoruz. +

+

+ {service.description || "Hizmetimiz hakkında detaylı bilgi için bizimle iletişime geçebilirsiniz."} +

+
+
+ +
+ {`${location.name} +
+
+
+ + {/* Related Projects */} + {projects.length > 0 && ( +
+
+
+
+ Referanslarımız +

+ Örnek Çalışmalar +

+
+
+ +
+ {projects.map((project: any) => ( + +
+
+ {project.title} +
+

+ {project.title} +

+
+ + ))} +
+
+
+ )} + + {/* CTA */} +
+
+

+ {location.name} İçin
Strateji Geliştirelim. +

+

+ {location.name} bölgesindeki projeniz için profesyonel destek almaya hazır mısınız? +

+ + Teklif Al + + +
+
+ +
+
+ ); +} diff --git a/app/services/page.tsx b/app/services/page.tsx index e65403a..0a5cd7b 100644 --- a/app/services/page.tsx +++ b/app/services/page.tsx @@ -18,7 +18,10 @@ export async function generateMetadata(): Promise { } export default async function ServicesPage() { - const services = await sql`SELECT * FROM services ORDER BY display_order ASC`; + const [services, locations] = await Promise.all([ + sql`SELECT * FROM services ORDER BY display_order ASC`, + sql`SELECT * FROM locations ORDER BY display_order ASC` + ]); const servicesSchema = { "@context": "https://schema.org", @@ -44,7 +47,7 @@ export default async function ServicesPage() { type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(servicesSchema) }} /> - + ); } \ No newline at end of file diff --git a/components/ServicesClient.tsx b/components/ServicesClient.tsx index 029f6b5..70dbf37 100644 --- a/components/ServicesClient.tsx +++ b/components/ServicesClient.tsx @@ -21,8 +21,9 @@ const processSteps = [ { icon: Zap, title: "Raporlama", desc: "Sonuçları ölçüyor ve optimize ediyoruz." } ]; -export default function ServicesClient({ services: initialServices }: { services: any[] }) { +export default function ServicesClient({ services: initialServices, locations: initialLocations }: { services: any[], locations?: any[] }) { const [services] = useState(initialServices); + const [locations] = useState(initialLocations || []); const DynamicIcon = ({ name, className }: { name: string, className?: string }) => { const IconComponent = (LucideIcons as any)[name] || Layers; @@ -102,6 +103,39 @@ export default function ServicesClient({ services: initialServices }: { services + {/* Locations Section (pSEO Hub) */} + {locations.length > 0 && ( +
+
+
+ Operasyon Bölgelerimiz +

Muğla'nın Her
Noktasındayız.

+
+ +
+ {locations.map((loc) => ( +
+

{loc.name}

+
    + {services.slice(0, 3).map((service) => ( +
  • + + {loc.name} {service.title} + + +
  • + ))} +
+
+ ))} +
+
+
+ )} + {/* CTA Section */}
diff --git a/setup_locations.mjs b/setup_locations.mjs new file mode 100644 index 0000000..fe20f70 --- /dev/null +++ b/setup_locations.mjs @@ -0,0 +1,50 @@ +import postgres from 'postgres'; +import dotenv from 'dotenv'; + +dotenv.config({ path: '.env.local' }); + +const sql = postgres(process.env.DATABASE_URL); + +async function setup() { + try { + console.log('Creating locations table...'); + await sql` + CREATE TABLE IF NOT EXISTS locations ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + slug VARCHAR(255) UNIQUE NOT NULL, + description TEXT, + display_order INTEGER DEFAULT 0 + ) + `; + + const locations = [ + { name: 'Bodrum', slug: 'bodrum', description: 'Bodrum profesyonel drone çekimi ve dijital medya çözümleri.' }, + { name: 'Marmaris', slug: 'marmaris', description: 'Marmaris otel çekimleri ve sosyal medya yönetimi.' }, + { name: 'Fethiye', slug: 'fethiye', description: 'Fethiye turizm odaklı video prodüksiyon hizmetleri.' }, + { name: 'Datça', slug: 'datca', description: 'Datça butik işletmeler için dijital pazarlama.' }, + { name: 'Dalaman', slug: 'dalaman', description: 'Dalaman kurumsal tanıtım ve drone hizmetleri.' }, + { name: 'Milas', slug: 'milas', description: 'Milas sanayi ve tarım odaklı prodüksiyon çözümleri.' }, + { name: 'Menteşe', slug: 'mentese', description: 'Muğla merkez Menteşe kurumsal medya yönetimi.' } + ]; + + console.log('Inserting locations...'); + for (const loc of locations) { + await sql` + INSERT INTO locations (name, slug, description) + VALUES (${loc.name}, ${loc.slug}, ${loc.description}) + ON CONFLICT (slug) DO UPDATE SET + name = EXCLUDED.name, + description = EXCLUDED.description + `; + } + + console.log('Locations setup complete!'); + process.exit(0); + } catch (err) { + console.error('Error setting up locations:', err); + process.exit(1); + } +} + +setup();