feat: implement programmatic SEO infrastructure with localized service pages
This commit is contained in:
@@ -97,4 +97,39 @@ export async function getProjectBySlug(slug: string) {
|
|||||||
return null;
|
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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
175
app/services/[slug]/[location]/page.tsx
Normal file
175
app/services/[slug]/[location]/page.tsx
Normal file
@@ -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<Metadata> {
|
||||||
|
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 (
|
||||||
|
<main className="min-h-screen bg-[#f5f5f0] text-black pt-24">
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="pt-24 pb-16 px-6 md:px-12 border-b border-black/10">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-6">
|
||||||
|
{location.name} / {service.title}
|
||||||
|
</span>
|
||||||
|
<h1 className="editorial-headline text-4xl md:text-6xl lg:text-[5.5rem] text-black uppercase leading-tight">
|
||||||
|
{location.name} <br />
|
||||||
|
<span className="text-primary">{service.title}</span> <br />
|
||||||
|
Çözümleri.
|
||||||
|
</h1>
|
||||||
|
<p className="text-black/40 text-[14px] max-w-2xl leading-relaxed mt-10">
|
||||||
|
{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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Content & Features */}
|
||||||
|
<section className="py-24 px-6 md:px-12">
|
||||||
|
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
|
||||||
|
<div className="space-y-12">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-light uppercase tracking-widest mb-6">Neler Sunuyoruz?</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
{[
|
||||||
|
"Profesyonel Ekipman",
|
||||||
|
"Yüksek Çözünürlük",
|
||||||
|
"Hızlı Teslimat",
|
||||||
|
"Stratejik Planlama",
|
||||||
|
"Yaratıcı Kurgu",
|
||||||
|
"Müşteri Odaklılık"
|
||||||
|
].map((feature) => (
|
||||||
|
<div key={feature} className="flex items-center gap-3 text-[12px] text-black/60">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-primary" />
|
||||||
|
{feature}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="prose prose-sm text-black/60 leading-relaxed max-w-none">
|
||||||
|
<p>
|
||||||
|
{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
|
||||||
|
<strong> {service.title.toLowerCase()}</strong> stratejilerini hayata geçiriyoruz.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{service.description || "Hizmetimiz hakkında detaylı bilgi için bizimle iletişime geçebilirsiniz."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative aspect-square bg-black/5 border border-black/10 overflow-hidden group">
|
||||||
|
<Image
|
||||||
|
src={service.image_url || "https://images.unsplash.com/photo-1473968512647-3e447244af8f?q=80&w=800"}
|
||||||
|
alt={`${location.name} ${service.title}`}
|
||||||
|
fill
|
||||||
|
className="object-cover grayscale group-hover:grayscale-0 transition-all duration-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Related Projects */}
|
||||||
|
{projects.length > 0 && (
|
||||||
|
<section className="py-24 px-6 md:px-12 bg-white/30 border-t border-black/10">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="flex justify-between items-end mb-12">
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-4">Referanslarımız</span>
|
||||||
|
<h2 className="editorial-headline text-3xl md:text-4xl text-black">
|
||||||
|
Örnek <span className="text-primary">Çalışmalar</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
{projects.map((project: any) => (
|
||||||
|
<Link key={project.id} href={`/works/${project.slug}`} className="group block">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="relative aspect-video overflow-hidden border border-black/10 bg-black/5">
|
||||||
|
<Image
|
||||||
|
src={project.hero_image}
|
||||||
|
alt={project.title}
|
||||||
|
fill
|
||||||
|
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-[12px] uppercase tracking-wider group-hover:text-primary transition-colors">
|
||||||
|
{project.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<section className="py-24 px-6 text-center border-t border-black/10">
|
||||||
|
<div className="max-w-4xl mx-auto space-y-8">
|
||||||
|
<h2 className="editorial-headline text-3xl md:text-5xl text-black uppercase">
|
||||||
|
{location.name} İçin <br /><span className="text-primary">Strateji Geliştirelim.</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-black/40 text-[13px] tracking-[0.1em]">
|
||||||
|
{location.name} bölgesindeki projeniz için profesyonel destek almaya hazır mısınız?
|
||||||
|
</p>
|
||||||
|
<Link href="/contact" className="button-primary mx-auto inline-flex items-center gap-2">
|
||||||
|
Teklif Al
|
||||||
|
<ArrowRight className="w-3 h-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,7 +18,10 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function ServicesPage() {
|
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 = {
|
const servicesSchema = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -44,7 +47,7 @@ export default async function ServicesPage() {
|
|||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(servicesSchema) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(servicesSchema) }}
|
||||||
/>
|
/>
|
||||||
<ServicesClient services={services || []} />
|
<ServicesClient services={services || []} locations={locations || []} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -21,8 +21,9 @@ const processSteps = [
|
|||||||
{ icon: Zap, title: "Raporlama", desc: "Sonuçları ölçüyor ve optimize ediyoruz." }
|
{ 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<any[]>(initialServices);
|
const [services] = useState<any[]>(initialServices);
|
||||||
|
const [locations] = useState<any[]>(initialLocations || []);
|
||||||
|
|
||||||
const DynamicIcon = ({ name, className }: { name: string, className?: string }) => {
|
const DynamicIcon = ({ name, className }: { name: string, className?: string }) => {
|
||||||
const IconComponent = (LucideIcons as any)[name] || Layers;
|
const IconComponent = (LucideIcons as any)[name] || Layers;
|
||||||
@@ -102,6 +103,39 @@ export default function ServicesClient({ services: initialServices }: { services
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Locations Section (pSEO Hub) */}
|
||||||
|
{locations.length > 0 && (
|
||||||
|
<section className="py-24 px-6 md:px-12 border-b border-black/10 bg-white/30">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="mb-16">
|
||||||
|
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-4">Operasyon Bölgelerimiz</span>
|
||||||
|
<h2 className="editorial-headline text-3xl md:text-5xl text-black uppercase">Muğla'nın Her <br /><span className="text-primary">Noktasındayız.</span></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-x-12 gap-y-16">
|
||||||
|
{locations.map((loc) => (
|
||||||
|
<div key={loc.id} className="space-y-6">
|
||||||
|
<h3 className="text-lg font-bold uppercase tracking-widest border-b border-black/10 pb-2">{loc.name}</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{services.slice(0, 3).map((service) => (
|
||||||
|
<li key={service.id}>
|
||||||
|
<Link
|
||||||
|
href={`/services/${service.slug}/${loc.slug}`}
|
||||||
|
className="text-[11px] text-black/40 hover:text-primary transition-colors flex items-center justify-between group"
|
||||||
|
>
|
||||||
|
{loc.name} {service.title}
|
||||||
|
<ArrowRight className="w-2 h-2 opacity-0 group-hover:opacity-100 transition-all -translate-x-1 group-hover:translate-x-0" />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<section className="py-24 px-6 text-center">
|
<section className="py-24 px-6 text-center">
|
||||||
<div className="max-w-4xl mx-auto space-y-12">
|
<div className="max-w-4xl mx-auto space-y-12">
|
||||||
|
|||||||
50
setup_locations.mjs
Normal file
50
setup_locations.mjs
Normal file
@@ -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();
|
||||||
Reference in New Issue
Block a user