/* 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 (
// интерактивный стек
Кликните по технологии — покажу пример
С чем работаю каждый день. Слева — стек, справа — конкретный кусок кода из реальной задачи и понятным языком что это даёт бизнесу.
);
}
/* 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;