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:
@@ -1,67 +0,0 @@
|
||||
import sharp from 'sharp'
|
||||
import { readFileSync } from 'fs'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const publicDir = resolve(__dirname, '../public')
|
||||
|
||||
// og-image: SVG → PNG 1200×630
|
||||
const svgBuffer = readFileSync(resolve(publicDir, 'og-image.svg'))
|
||||
await sharp(svgBuffer)
|
||||
.resize(1200, 630)
|
||||
.png({ quality: 90 })
|
||||
.toFile(resolve(publicDir, 'og-image.png'))
|
||||
console.log('✓ og-image.png (1200×630)')
|
||||
|
||||
// apple-touch-icon: logo.png → 180×180
|
||||
await sharp(resolve(publicDir, 'logo.png'))
|
||||
.resize(180, 180, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 1 } })
|
||||
.png()
|
||||
.toFile(resolve(publicDir, 'apple-touch-icon.png'))
|
||||
console.log('✓ apple-touch-icon.png (180×180)')
|
||||
|
||||
// favicon.ico: logo.png → multi-size ICO (16, 32, 48)
|
||||
import { writeFileSync } from 'fs'
|
||||
|
||||
const icoSizes = [16, 32, 48]
|
||||
const pngBuffers = await Promise.all(
|
||||
icoSizes.map(size =>
|
||||
sharp(resolve(publicDir, 'logo.png'))
|
||||
.resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.png()
|
||||
.toBuffer()
|
||||
)
|
||||
)
|
||||
|
||||
// Build ICO: header + directory + image data
|
||||
const count = icoSizes.length
|
||||
const headerSize = 6
|
||||
const dirEntrySize = 16
|
||||
const dirSize = count * dirEntrySize
|
||||
let offset = headerSize + dirSize
|
||||
|
||||
const header = Buffer.alloc(headerSize)
|
||||
header.writeUInt16LE(0, 0) // reserved
|
||||
header.writeUInt16LE(1, 2) // type: 1 = ICO
|
||||
header.writeUInt16LE(count, 4)
|
||||
|
||||
const dirs = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const dir = Buffer.alloc(dirEntrySize)
|
||||
const size = icoSizes[i]
|
||||
dir.writeUInt8(size === 256 ? 0 : size, 0) // width
|
||||
dir.writeUInt8(size === 256 ? 0 : size, 1) // height
|
||||
dir.writeUInt8(0, 2) // color count
|
||||
dir.writeUInt8(0, 3) // reserved
|
||||
dir.writeUInt16LE(1, 4) // color planes
|
||||
dir.writeUInt16LE(32, 6) // bits per pixel
|
||||
dir.writeUInt32LE(pngBuffers[i].length, 8)
|
||||
dir.writeUInt32LE(offset, 12)
|
||||
offset += pngBuffers[i].length
|
||||
dirs.push(dir)
|
||||
}
|
||||
|
||||
const ico = Buffer.concat([header, ...dirs, ...pngBuffers])
|
||||
writeFileSync(resolve(publicDir, 'favicon.ico'), ico)
|
||||
console.log('✓ favicon.ico (16×16, 32×32, 48×48)')
|
||||
@@ -1,34 +0,0 @@
|
||||
import { readFileSync, writeFileSync, rmSync, readdirSync } from 'fs'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const root = resolve(__dirname, '..')
|
||||
const buildDir = process.env.BUILD_DIR || 'dist'
|
||||
|
||||
// Import SSR bundle (always built to dist/server inside project)
|
||||
const { render } = await import('../dist/server/entry-server.js')
|
||||
|
||||
let html = readFileSync(resolve(root, buildDir, 'index.html'), 'utf-8')
|
||||
|
||||
// Inject prerendered HTML
|
||||
const appHtml = render()
|
||||
html = html.replace('<div id="root"></div>', `<div id="root">${appHtml}</div>`)
|
||||
|
||||
// Fix preload font: find actual cyrillic-700 woff2 hash in dist/assets
|
||||
const assetsDir = resolve(root, buildDir, 'assets')
|
||||
const fontFile = readdirSync(assetsDir).find(f => f.match(/manrope-cyrillic-700-normal-.+\.woff2/))
|
||||
if (fontFile) {
|
||||
html = html.replace(
|
||||
/href="\/assets\/manrope-cyrillic-700-normal-[^"]+\.woff2"/,
|
||||
`href="/assets/${fontFile}"`
|
||||
)
|
||||
console.log(`✓ Preload font updated: ${fontFile}`)
|
||||
}
|
||||
|
||||
writeFileSync(resolve(root, buildDir, 'index.html'), html)
|
||||
|
||||
// Cleanup SSR bundle
|
||||
rmSync(resolve(root, 'dist/server'), { recursive: true, force: true })
|
||||
|
||||
console.log('✓ Prerendered index.html')
|
||||
Reference in New Issue
Block a user