/* Interactive tech stack — click an item, see code example + plain-language outcome */ function getPublicContact() { return window.__PUBLIC_DATA__?.contact || {}; } const contact = getPublicContact(); const STACK = [ { id: 'php', abbr: 'PHP', color: '#8993be', name: 'PHP / MySQL', tag: 'формы, админки, старые сайты', badge: 'починю что есть', headline: 'Чиню формы, оптимизирую старые сайты', sub: 'Самописные PHP-сайты на десять лет — это норма. Прихожу в существующий код, нахожу причину, чиню без переписывания всего.', lang: 'php', snippet: `isSMTP(); $mail->Host = getenv('SMTP_HOST'); $mail->SMTPAuth = true; $mail->Username = getenv('SMTP_USER'); $mail->Password = getenv('SMTP_PASS'); $mail->setFrom('noreply@your-site.ru', 'Заявки'); $mail->addAddress($manager); $mail->Subject = 'Новая заявка'; $mail->Body = htmlspecialchars($message); if (!$mail->send()) { error_log("mail: " . $mail->ErrorInfo); return ['ok' => false, 'reason' => 'mailer']; }`, outcomes: [ 'Заявки снова доходят до менеджера', 'Можно понять, на каком шаге форма ломается', 'Логи говорят, что пошло не так', 'Спам-фильтры перестают резать письма', ], }, { id: 'python', abbr: 'PY', color: '#79b8ff', name: 'Python · FastAPI', tag: 'backend, скрипты, автоматизация', badge: 'автоматизирую рутину', headline: 'Backend-сервисы и автоматизация', sub: 'FastAPI для небольших backend-сервисов, скрипты-парсеры, выгрузки прайсов, обработка заявок. Всё, что можно делать руками — пусть делает скрипт.', lang: 'python', snippet: `# FastAPI-эндпоинт принимает заявки с сайта и шлёт в Telegram + CRM from fastapi import FastAPI, BackgroundTasks from pydantic import BaseModel, EmailStr app = FastAPI() class Lead(BaseModel): name: str contact: str message: str @app.post("/lead") async def lead(l: Lead, bg: BackgroundTasks): bg.add_task(send_telegram, l) bg.add_task(push_to_crm, l) return {"ok": True}`, outcomes: [ 'Заявки мгновенно прилетают в Telegram', 'Прайс обновляется сам каждую ночь', 'Менеджер не копирует данные руками', 'Понятный API под мобильное приложение', ], }, { id: 'docker', abbr: 'DK', color: '#2496ed', name: 'Docker · Compose', tag: 'один файл — весь проект', badge: 'упакую проект', headline: 'Один docker-compose — весь проект', sub: 'Сайт, база, кеш, фоновые задачи — описаны в одном файле. Перенос на другой сервер занимает 10 минут, а не неделю.', lang: 'yaml', snippet: `# docker-compose.yml — сайт + база + nginx, всё в одной команде services: app: build: . restart: unless-stopped env_file: .env depends_on: [db] db: image: postgres:16-alpine volumes: ['./pgdata:/var/lib/postgresql/data'] environment: POSTGRES_PASSWORD: \${DB_PASS} nginx: image: nginx:alpine ports: ['80:80', '443:443'] volumes: ['./nginx.conf:/etc/nginx/nginx.conf:ro']`, outcomes: [ 'Переезд на другой сервер — за один вечер', 'Локальная копия для тестов — одной командой', 'Никаких "у меня работало"', 'Бэкапы и обновления — предсказуемые', ], }, { id: 'nginx', abbr: 'NX', color: '#009639', name: 'Nginx · SSL', tag: 'reverse proxy, HTTPS', badge: 'настрою сервер', headline: 'Reverse proxy, HTTPS и редиректы', sub: 'Сайт по HTTPS, www → без www, старые URL → новые, кеш статики, защита админки по IP. Всё в одном понятном конфиге.', lang: 'nginx', snippet: `# nginx — HTTPS + редиректы + защита админки server { listen 443 ssl http2; server_name your-site.ru; ssl_certificate /etc/letsencrypt/live/your-site.ru/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-site.ru/privkey.pem; location /admin/ { allow 203.0.113.0/24; # офис deny all; proxy_pass http://app:8000; } location / { proxy_pass http://app:8000; proxy_set_header X-Forwarded-For $remote_addr; } }`, outcomes: [ 'Сайт открывается по HTTPS, замочек в браузере', 'Старые ссылки в Яндексе не теряются', 'Админка закрыта для посторонних', 'Картинки и CSS грузятся быстрее', ], }, { id: 'seo', abbr: 'SE', color: '#f7df1e', name: 'schema.org · SEO', tag: 'микроразметка, индексация', badge: 'покажу поисковику', headline: 'Микроразметка и техническое SEO', sub: 'JSON-LD для статей, услуг, организации и хлебных крошек. robots.txt, sitemap, canonical и редиректы — чтобы поисковик увидел все нужные страницы.', lang: 'json', snippet: `{ "@context": "https://schema.org", "@type": "Service", "serviceType": "Технический SEO-аудит", "provider": { "@type": "Person", "name": "${contact.specialist_name || ''}", "url": "https://app.winetlab.site/" }, "areaServed": "RU", "offers": { "@type": "Offer", "price": "2000", "priceCurrency": "RUB" } }`, outcomes: [ 'Яндекс и Google понимают, что у вас за бизнес', 'В выдаче появляются хлебные крошки, цены, рейтинг', 'Лишние страницы перестают попадать в индекс', 'Понятно, что чинить в первую очередь', ], }, { id: 'api', abbr: 'API', color: '#c590e0', name: 'API-интеграции', tag: 'CRM, мессенджеры, выгрузки', badge: 'свяжу системы', headline: 'Связываю системы между собой', sub: 'Заявки с сайта → в CRM. Прайсы поставщика → в каталог. Уведомления → в Telegram. Без танцев с экспортом-импортом руками.', lang: 'python', snippet: `# заявки с сайта летят в Telegram и amoCRM одновременно async def push_to_crm(lead: Lead): async with httpx.AsyncClient() as c: r = await c.post( f"{AMO_URL}/api/v4/leads", headers={"Authorization": f"Bearer {AMO_TOKEN}"}, json=[{ "name": lead.name, "price": 0, "custom_fields_values": [ {"field_code": "PHONE", "values": [{"value": lead.contact}]}, {"field_code": "DESC", "values": [{"value": lead.message}]}, ], }], ) r.raise_for_status()`, outcomes: [ 'Менеджер видит заявки в CRM, а не в почте', 'Прайс обновляется без копи-пасты', 'Уведомления — там, где их прочитают', 'Возвраты, статусы, чеки — автоматом', ], }, ]; function Stack() { const [active, setActive] = React.useState(STACK[0].id); const item = STACK.find(s => s.id === active); return (
// интерактивный стек Кликните по технологии — покажу пример С чем работаю каждый день. Слева — стек, справа — конкретный кусок кода из реальной задачи и понятным языком что это даёт бизнесу.
{STACK.map(s => ( ))}
{item.badge} ~ /examples/{item.id}.{item.lang === 'python' ? 'py' : item.lang === 'php' ? 'php' : item.lang === 'yaml' ? 'yml' : item.lang === 'nginx' ? 'conf' : 'json'}

{item.headline}

{item.sub}

                
              
{item.outcomes.map((o, i) => (
{o}
))}
); } /* Lightweight syntax highlighter Uses sentinel chars (\x01 \x02 \x03) as markers so subsequent regex passes never re-scan injected markup. Final pass converts sentinels to real spans. */ function CodeHighlight({ code, lang }) { const KW = { php: /\b(use|new|return|if|else|getenv|true|false|null|public|function|foreach|as|require|require_once|namespace)\b/g, python: /\b(from|import|async|def|return|if|else|elif|None|True|False|with|as|await|for|in|raise|try|except)\b/g, nginx: /\b(server|listen|server_name|ssl_certificate|ssl_certificate_key|location|allow|deny|proxy_pass|proxy_set_header|root|index)\b/g, yaml: /^(\s*)([a-z_-]+):/gmi, json: /"([^"]+)":/g, }; const html = React.useMemo(() => { // 1) escape HTML first let s = code .replace(/&/g, '&') .replace(//g, '>'); // 2) wrap matches with sentinels — \x01\x02\x03 const wrap = (cls) => (m) => `\x01${cls}\x02${m}\x03`; // Comments first (so strings inside comments aren't double-wrapped) if (lang === 'python' || lang === 'yaml' || lang === 'nginx') { s = s.replace(/(^|\n)(\s*)(#[^\n]*)/g, (_, a, b, c) => a + b + `\x01term-comment\x02${c}\x03`); } if (lang === 'php') { s = s.replace(/(\/\/[^\n]*)/g, wrap('term-comment')); } // Strings s = s.replace(/("(?:[^"\\]|\\.)*")/g, wrap('term-str')); s = s.replace(/('(?:[^'\\]|\\.)*')/g, wrap('term-str')); // Numbers (word boundary so sentinels — which are control chars — won't match) s = s.replace(/\b(\d+(?:\.\d+)?)\b/g, wrap('term-num')); // Keywords const kw = KW[lang]; if (kw) s = s.replace(kw, wrap('term-key')); // 3) convert sentinels to real spans. Open and close are independent — // nesting is preserved naturally because wraps always emit balanced pairs. s = s.replace(/\x01([a-z-]+)\x02/g, ''); s = s.replace(/\x03/g, ''); return s; }, [code, lang]); return ; } window.Stack = Stack;