Tailwind v4 migration, massive SEO optimization, and fixes
- Migrate to Tailwind CSS v4 with @theme, @layer, @plugin syntax - Upgrade to Next.js 16.1, React 19.2, TypeScript 5.9.3 - Add 8 technology landing pages (Next.js, React, TypeScript, Tailwind CSS, Claude AI, Python, n8n, KI-Agenten) - Expand SEO keywords across all locales (de/en/sr) for home, layout, cities, services - Add tech-specific keywords to all 22 city pages - Add FAQ schema + seoKeywords to service detail pages - Add CollectionPage JSON-LD to blog listing - Fix city page: dynamic region, BASE_URL canonicals, x-default hreflang - Rewrite terms page with full German AGB, English and Serbian translations - Remove dead crypto import and unused CSRF middleware (Edge Runtime fix) - Remove crawlDelay from robots.ts - Update sitemap with technology pages (~198 indexed URLs) - Remove deprecated tailwind.config.js Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { ArrowLeft, ArrowRight, CheckCircle, Brain, Mic, Workflow, Cloud, Code, Bot, Building, ShoppingCart, Factory } from 'lucide-react';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { allServices, getServiceBySlug, getAllServiceSlugs } from '@/data/services';
|
||||
import { BreadcrumbJsonLd, ServiceJsonLd } from '@/components/seo';
|
||||
import { BreadcrumbJsonLd, ServiceJsonLd, FAQJsonLd } from '@/components/seo';
|
||||
import { type Locale, locales } from '@/i18n/config';
|
||||
|
||||
type Props = {
|
||||
@@ -51,15 +51,17 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
|
||||
const localePath = locale === 'de' ? `/leistungen/${slug}` : `/${locale}/leistungen/${slug}`;
|
||||
|
||||
// Enhanced keywords for English
|
||||
const enhancedKeywords = locale === 'en'
|
||||
? [...service.technologies, 'Remote AI Developer', 'Hire AI Developer', 'AI Consultant USA', 'AI Developer UK'].join(', ')
|
||||
: service.technologies.join(', ');
|
||||
// Use seoKeywords if available, otherwise fall back to technologies
|
||||
const seoKw = service.seoKeywords?.[locale as Locale] || [];
|
||||
const allKeywords = [...service.technologies, ...seoKw];
|
||||
if (locale === 'en') {
|
||||
allKeywords.push('Remote AI Developer', 'Hire AI Developer', 'AI Consultant USA', 'AI Developer UK');
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
description: service.description[locale as Locale] || service.description.de,
|
||||
keywords: enhancedKeywords,
|
||||
keywords: allKeywords.join(', '),
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}${localePath}`,
|
||||
languages: {
|
||||
@@ -76,6 +78,14 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
type: 'website',
|
||||
locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US',
|
||||
alternateLocale: ['de_DE', 'en_US', 'sr_RS'].filter(l => l !== (locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US')),
|
||||
images: [
|
||||
{
|
||||
url: `${BASE_URL}/images/og-image.avif`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: `${service.name[locale as Locale]} - Damjan Savić`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -101,6 +111,48 @@ export default async function ServicePage({ params }: Props) {
|
||||
// Get related services (excluding current)
|
||||
const relatedServices = allServices.filter((s) => s.slug !== slug).slice(0, 3);
|
||||
|
||||
// FAQ items for this service
|
||||
const serviceFaqItems = locale === 'de' ? [
|
||||
{
|
||||
question: `Was kostet ${service.name.de}?`,
|
||||
answer: `Die Kosten für ${service.name.de} variieren je nach Projektumfang und Komplexität. Ich erstelle gerne ein individuelles Angebot nach einem kostenlosen Erstgespräch.`,
|
||||
},
|
||||
{
|
||||
question: `Welche Technologien nutzen Sie für ${service.name.de}?`,
|
||||
answer: `Für ${service.name.de} setze ich auf bewährte Technologien: ${service.technologies.join(', ')}. Die genaue Auswahl hängt von Ihren Anforderungen ab.`,
|
||||
},
|
||||
{
|
||||
question: `Wie lange dauert ein typisches ${service.name.de}-Projekt?`,
|
||||
answer: `Die Dauer eines ${service.name.de}-Projekts hängt vom Umfang ab. Einfache Projekte können in 2-4 Wochen abgeschlossen werden, komplexere Lösungen in 6-12 Wochen.`,
|
||||
},
|
||||
] : locale === 'en' ? [
|
||||
{
|
||||
question: `How much does ${service.name.en} cost?`,
|
||||
answer: `The cost of ${service.name.en} varies depending on project scope and complexity. I'm happy to provide a custom quote after a free initial consultation.`,
|
||||
},
|
||||
{
|
||||
question: `What technologies do you use for ${service.name.en}?`,
|
||||
answer: `For ${service.name.en}, I use proven technologies: ${service.technologies.join(', ')}. The exact selection depends on your requirements.`,
|
||||
},
|
||||
{
|
||||
question: `How long does a typical ${service.name.en} project take?`,
|
||||
answer: `The duration of a ${service.name.en} project depends on scope. Simple projects can be completed in 2-4 weeks, more complex solutions in 6-12 weeks.`,
|
||||
},
|
||||
] : [
|
||||
{
|
||||
question: `Koliko košta ${service.name.sr}?`,
|
||||
answer: `Troškovi za ${service.name.sr} variraju u zavisnosti od obima i kompleksnosti projekta. Rado ću vam dati individualizovanu ponudu nakon besplatnog inicijalnog razgovora.`,
|
||||
},
|
||||
{
|
||||
question: `Koje tehnologije koristite za ${service.name.sr}?`,
|
||||
answer: `Za ${service.name.sr} koristim proverene tehnologije: ${service.technologies.join(', ')}. Tačan izbor zavisi od vaših zahteva.`,
|
||||
},
|
||||
{
|
||||
question: `Koliko traje tipičan ${service.name.sr} projekat?`,
|
||||
answer: `Trajanje ${service.name.sr} projekta zavisi od obima. Jednostavni projekti mogu se završiti za 2-4 nedelje, kompleksnija rešenja za 6-12 nedelja.`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
|
||||
@@ -110,6 +162,7 @@ export default async function ServicePage({ params }: Props) {
|
||||
url={`/${locale}/leistungen/${slug}`}
|
||||
locale={locale as Locale}
|
||||
/>
|
||||
<FAQJsonLd items={serviceFaqItems} locale={locale as Locale} />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="py-24">
|
||||
@@ -226,6 +279,32 @@ export default async function ServicePage({ params }: Props) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="py-16">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-2xl font-bold text-white text-center mb-8">
|
||||
{locale === 'de'
|
||||
? `Häufige Fragen zu ${service.name.de}`
|
||||
: locale === 'sr'
|
||||
? `Česta pitanja o ${service.name.sr}`
|
||||
: `FAQ about ${service.name.en}`}
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{serviceFaqItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-zinc-800/30 border border-zinc-800 rounded-lg p-6"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
{item.question}
|
||||
</h3>
|
||||
<p className="text-zinc-400">{item.answer}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
|
||||
Reference in New Issue
Block a user