/
ncit
/
studiosite
Обзор
Документация
Войти
/
ncit
/
studiosite
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
server.js
199 строк
7 KB
ncit
SEO: deepen service page content, add FAQ schema, fix duplicate-content redirect
05 июн 2026, 10:18
05 июн 2026, 10:18
686ce4d
Код
Авторство
О чём код?
const http = require('http'); const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); const PORT = process.env.PORT || 3000; const DATA_DIR = path.join(__dirname, 'data'); const MIME = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.xml': 'application/xml', '.txt': 'text/plain', }; // Compressible types (text-based) const COMPRESSIBLE = new Set([ 'text/html', 'text/css', 'application/javascript', 'application/json', 'image/svg+xml', 'application/xml', 'text/plain', ]); // Static assets get long cache, HTML gets short cache function getCacheHeader(ext) { if (['.png', '.jpg', '.jpeg', '.webp', '.svg', '.ico', '.woff2'].includes(ext)) { return 'public, max-age=31536000, immutable'; // 1 year } if (['.js', '.css'].includes(ext)) { return 'public, max-age=604800'; // 1 week } return 'public, max-age=3600'; // 1 hour for HTML/XML/TXT } function parseMultipart(buf, boundary) { const parts = {}; const sep = Buffer.from('--' + boundary); let start = buf.indexOf(sep) + sep.length; while (start < buf.length) { const nextSep = buf.indexOf(sep, start); if (nextSep === -1) break; const part = buf.slice(start, nextSep); const headerEnd = part.indexOf('\r\n\r\n'); if (headerEnd === -1) { start = nextSep + sep.length; continue; } const headers = part.slice(0, headerEnd).toString(); let body = part.slice(headerEnd + 4); if (body.length >= 2 && body[body.length - 2] === 13 && body[body.length - 1] === 10) { body = body.slice(0, -2); } const nameMatch = headers.match(/name="([^"]+)"/); const fileMatch = headers.match(/filename="([^"]*)"/); if (nameMatch) { if (fileMatch && fileMatch[1]) { parts[nameMatch[1]] = { filename: fileMatch[1], data: body }; } else { parts[nameMatch[1]] = body.toString(); } } start = nextSep + sep.length; } return parts; } const server = http.createServer((req, res) => { // Handle form submission if (req.method === 'POST' && req.url === '/submit') { const chunks = []; req.on('data', chunk => chunks.push(chunk)); req.on('end', () => { try { const contentType = req.headers['content-type'] || ''; const boundaryMatch = contentType.match(/boundary=(.+)/); if (!boundaryMatch) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid content type' })); return; } const buf = Buffer.concat(chunks); const parts = parseMultipart(buf, boundaryMatch[1]); // Create submission folder: data/2026-03-07_21-30-15/ const now = new Date(); const ts = now.toISOString().replace(/T/, '_').replace(/:/g, '-').replace(/\..+/, ''); const dir = path.join(DATA_DIR, ts); fs.mkdirSync(dir, { recursive: true }); // Build txt content const contact = parts.contact || ''; const services = parts.services || 'Не указаны'; const budget = parts.budget || 'Не указан'; const description = parts.description || ''; let txt = `Дата: ${now.toLocaleString('ru-RU', { timeZone: 'Europe/Moscow' })}\n`; txt += `Контакт: ${contact}\n`; txt += `\nУслуги: ${services}\n`; txt += `Бюджет: ${budget}\n`; txt += `\nОписание:\n${description || '(не указано)'}\n`; // Save attached file if (parts.file && parts.file.filename) { const safeName = parts.file.filename.replace(/[^a-zA-Zа-яА-ЯёЁ0-9._-]/g, '_'); fs.writeFileSync(path.join(dir, safeName), parts.file.data); txt += `\nПриложенный файл: ${parts.file.filename}\n`; } fs.writeFileSync(path.join(dir, 'заявка.txt'), txt, 'utf8'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true })); } catch (e) { console.error('Submit error:', e); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid data' })); } }); return; } // Serve static files const urlPath = req.url.split('?')[0]; // Avoid duplicate content: the homepage is canonically "/", so 301 any // direct hit on /pixelforge.html (or /index.html) back to "/". if (urlPath === '/pixelforge.html' || urlPath === '/index.html') { res.writeHead(301, { 'Location': '/', 'Cache-Control': 'public, max-age=86400' }); res.end(); return; } let filePath = path.join(__dirname, urlPath === '/' ? 'pixelforge.html' : urlPath); const ext = path.extname(filePath); const contentType = MIME[ext] || 'application/octet-stream'; fs.readFile(filePath, (err, content) => { if (err) { res.writeHead(404); res.end('Not found'); return; } const headers = { 'Content-Type': contentType, 'Cache-Control': getCacheHeader(ext), 'X-Content-Type-Options': 'nosniff', }; // Gzip compression for text-based content const acceptEncoding = req.headers['accept-encoding'] || ''; if (COMPRESSIBLE.has(contentType) && content.length > 1024) { if (acceptEncoding.includes('br')) { headers['Content-Encoding'] = 'br'; headers['Vary'] = 'Accept-Encoding'; zlib.brotliCompress(content, (err, compressed) => { if (err) { res.writeHead(200, headers); res.end(content); } else { res.writeHead(200, headers); res.end(compressed); } }); return; } if (acceptEncoding.includes('gzip')) { headers['Content-Encoding'] = 'gzip'; headers['Vary'] = 'Accept-Encoding'; zlib.gzip(content, (err, compressed) => { if (err) { res.writeHead(200, headers); res.end(content); } else { res.writeHead(200, headers); res.end(compressed); } }); return; } } res.writeHead(200, headers); res.end(content); }); }); server.listen(PORT, () => { console.log(`КодЭкспресс server running at http://localhost:${PORT}`); });