feat: migrate to Next.js 15 App Router with i18n and multi-page SEO

- Replace Vite+React SPA with Next.js 15, TypeScript, App Router
- Static export (output: 'export'), deploy via npm run deploy
- i18n routing: /ru/... and /en/... with generateStaticParams
- New pages: /uslugi/ (catalog), /uslugi/[slug]/ (8 service pages), /faq/, /kontakty/
- SEO: generateMetadata on all pages, JSON-LD Service schema, hreflang alternates
- ContactForm: 'use client', Turnstile + POST to /api/contact.php
- Fonts: Manrope via next/font/google (CSS variable)
- Remove: vite.config.js, entry-server.jsx, prerender scripts, LanguageContext

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 21:19:18 +03:00
parent 18fff2e3f6
commit a0ebac1544
46 changed files with 1454 additions and 1960 deletions

View File

@@ -0,0 +1,38 @@
'use client'
import { useState } from 'react'
import { ChevronDown } from 'lucide-react'
import type { Dictionary } from '@/lib/i18n'
function FaqItem({ q, a }: { q: string; a: string }) {
const [open, setOpen] = useState(false)
return (
<div className="border border-slate-200 rounded-xl overflow-hidden">
<button onClick={() => setOpen(!open)}
className="w-full flex items-center justify-between px-6 py-4 text-left bg-white hover:bg-slate-50 transition-colors">
<span className="font-semibold text-slate-800 pr-4">{q}</span>
<ChevronDown size={18} className={`text-slate-400 flex-shrink-0 transition-transform duration-200 ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<div className="px-6 py-4 bg-slate-50 border-t border-slate-200 text-slate-600 text-sm leading-relaxed">{a}</div>
)}
</div>
)
}
export default function FaqSection({ d, standalone }: { d: Dictionary; standalone?: boolean }) {
return (
<section id="faq" className={`${standalone ? 'py-24' : 'py-24 border-t border-slate-100'} bg-white`}>
<div className="max-w-3xl mx-auto px-4 sm:px-6">
<div className="text-center mb-12">
<h2 className="text-3xl sm:text-4xl font-bold text-slate-900 mb-4">{d.faq.title}</h2>
<p className="text-slate-500 text-lg">{d.faq.subtitle}</p>
</div>
<div className="flex flex-col gap-3">
{d.faq.items.map((item, i) => (
<FaqItem key={i} q={item.q} a={item.a} />
))}
</div>
</div>
</section>
)
}