[{"data":1,"prerenderedAt":456},["ShallowReactive",2],{"project-case-dobro":3},{"blocks":4,"person":395},[5,15,47,106,122,139,165,326,358,382,388],{"component":6,"data":7},"ProjectCaseHeader",{"index":8,"subtitle":9,"name":10,"headline":11,"description":12,"url":13,"tone":14},"01","ПЛАТФОРМА ПОЖЕРТВОВАНИЙ · ЦИФРОВОЙ РУБЛЬ","Dobro.rf","В 13 раз больше запросов в секунду","Платформа пожертвований с проверенными фондами, цифровым рублём и кабинетами НКО. Генеральный партнёр — Альфа-Банк.","https:\u002F\u002Fblago.dobro.ru\u002F","secondary",{"component":16,"data":17},"ProjectCaseCover",{"image":18,"imageMobile":33,"imageAlt":45,"frame":46},{"src":19,"width":20,"height":21,"variants":22},"\u002F_nuxt\u002Fpreview.DZaiDPpT.webp",974,609,[23,26,29,32],{"src":24,"width":25},"\u002F_nuxt\u002Fpreview.fnBXH0Fp.webp",480,{"src":27,"width":28},"\u002F_nuxt\u002Fpreview.DcugL7dT.webp",720,{"src":30,"width":31},"\u002F_nuxt\u002Fpreview.B9WkH9O7.webp",960,{"src":19,"width":20},{"src":34,"width":35,"height":36,"variants":37},"\u002F_nuxt\u002Fpreview-mobile.0TDrCJp_.webp",600,1298,[38,41,44],{"src":39,"width":40},"\u002F_nuxt\u002Fpreview-mobile.Bntv-01a.webp",160,{"src":42,"width":43},"\u002F_nuxt\u002Fpreview-mobile.D3P3CKjh.webp",300,{"src":34,"width":35},"Главная страница платформы Добро.рф","devices",{"component":48,"data":49},"ProjectCaseMeta",{"tags":50,"stack":54},[51,52,53],"Платежи","Кабинеты","Тимлид",[55,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104],{"label":56,"own":57},"Next.js 16",true,{"label":59,"own":57},"React 19",{"label":61,"own":57},"TypeScript",{"label":63,"own":57},"Tailwind CSS 4",{"label":65,"own":57},"Zustand",{"label":67,"own":57},"SWR",{"label":69,"own":57},"ofetch",{"label":71,"own":57},"openapi-typescript",{"label":73,"own":57},"react-hook-form",{"label":75,"own":57},"zod",{"label":77,"own":57},"maska",{"label":79,"own":57},"Vitest",{"label":81,"own":57},"React Testing Library",{"label":83,"own":57},"Storybook",{"label":85,"own":57},"autocannon",{"label":87,"own":57},"0x",{"label":89,"own":57},"prom-client",{"label":91,"own":57},"Husky",{"label":93,"own":57},"lint-staged",{"label":95},"Lexical",{"label":97},"GSAP",{"label":99},"@use-gesture",{"label":101},"TanStack Virtual",{"label":103,"own":57},"Docker",{"label":105},"GitHub Actions",{"component":107,"data":108},"ProjectCaseFacts",{"facts":109},[110,113,116,119],{"key":111,"value":112},"role","Frontend Team Lead",{"key":114,"value":115},"team","15 человек: 5 frontend, 4 backend, 2 дизайнера, 2 QA, PM и DevOps",{"key":117,"value":118},"duration","С марта 2026",{"key":120,"value":121},"status","В разработке, запуск запланирован на I квартал 2027",{"component":123,"data":124},"ProjectCaseRole",{"id":111,"name":125,"items":126},"Что сделал я",[127,130,133,136],{"title":128,"text":129},"Код","Кодовая база — 37 страниц, из них 20 в кабинетах, и 110 переиспользуемых компонентов, покрытых тестами. Сам написал UI-кит и поля форм, кабинеты НКО и доноров, страницы НКО и сбора, главную, каталог, шапку и подвал, раздел модерации.",{"title":131,"text":132},"Команда","Ставил задачи и приоритеты четырём фронтенд-разработчикам, ревьюил и сливал PR. Написал регламенты и онбординг-документ, чтобы новые разработчики быстрее входили в проект и писали код в одном стиле.",{"title":134,"text":135},"Архитектура и стек","Выбрал стек и заложил архитектуру фронтенда. React задал заказчик, а Next.js поверх него отдаёт страницы сборов и НКО готовыми с сервера: их находят поисковики, а донор видит сбор сразу, без ожидания загрузки. TypeScript в строгом режиме, без any и as, ловит ошибки до релиза, а это важно там, где через сайт проходят деньги. Компоненты разложены по слоям ui → form → base → страницы, Server Components по умолчанию, на каждый компонент есть тесты.",{"title":137,"text":138},"Производительность","Сайт выдерживает в 13 раз больше запросов в секунду, а сервер готовит страницу за 69 мс вместо 919. Для благотворительной платформы это значит, что сайт не тормозит, когда на него разом приходит много доноров, например после рассылки или в большую акцию. Всю оптимизацию провёл сам, от замеров до отчёта.",{"component":140,"data":141},"ProjectCaseChapter",{"id":142,"name":143,"blocks":144},"context","Контекст",[145,154],{"component":140,"data":146},{"id":147,"name":148,"blocks":149},"business","Бизнес",[150],{"component":151,"data":152},"ProjectCaseText",{"text":153},"Добро.рф (dobro.ru) — платформа прозрачной благотворительности, генеральный партнёр — Альфа-Банк. Доноры делают разовые и регулярные пожертвования, собирают корзину из нескольких сборов и смотрят историю. НКО и частные организаторы проходят онбординг, загружают документы, создают сборы и следят за статистикой. Модераторы проверяют сборы.",{"component":140,"data":155},{"id":156,"name":157,"blocks":158},"situation","Ситуация",[159,162],{"component":151,"data":160},{"text":161},"У заказчика уже была платформа, но за основу её не брали. Заказали новую с нуля: с новыми техническими требованиями и полностью новым дизайном. Поэтому архитектуру, дизайн-систему и интеграции строили заново, без наследия старого кода. Разработка идёт с марта 2026 по ТЗ заказчика.",{"component":151,"data":163},{"text":164},"Стек тоже задал заказчик: React. До проекта во фронтенд-команде на React писал только я, поэтому команде нужно было быстро освоить новый стек. При этом интеграций много: цифровой рубль, оплата в модальном окне прямо на сайте, Альфа-Банк, многоступенчатый онбординг НКО.",{"component":140,"data":166},{"id":167,"name":168,"blocks":169},"process","Процесс, архитектура и решения",[170,194,223,251,259,288,296],{"component":140,"data":171},{"id":172,"name":173,"blocks":174},"flow","Путь пожертвования",[175,178],{"component":151,"data":176},{"text":177},"Донор может поддержать сразу несколько сборов одной оплатой: деньги распределяются автоматически, вся история остаётся в кабинете.",{"component":179,"data":180},"ProjectCaseSteps",{"steps":181},[182,185,188,191],{"title":183,"text":184},"Каталог","Фильтры по городу, направлению помощи, реестру СО НКО и приёму цифровых рублей. В разработке: каталог с кешем запросов, справочник фильтров вынесен из HTML, чтобы страница грузилась быстрее",{"title":186,"text":187},"Сбор","Прогресс, «осталось дней», отчёты и журнал транзакций. В разработке: страница в ISR, она отдаётся готовой и обновляется сама",{"title":189,"text":190},"Корзина","Несколько сборов, одна оплата, распределение автоматически. В разработке: состояние корзины в Zustand, одна форма оплаты на все сборы",{"title":192,"text":193},"Оплата","Карта в модальном окне на сайте, СБП или цифровой рубль. В разработке: способы оплаты приходят с бэкенда, данные карты вводятся в защищённые поля платёжного шлюза",{"component":140,"data":195},{"id":196,"name":197,"blocks":198},"onboarding","Онбординг НКО",[199,202],{"component":151,"data":200},{"text":201},"Организация попадает на платформу через мастер из шести шагов. Замечания модератора показываются ошибками полей и не дают отправить анкету, пока их не исправят.",{"component":179,"data":203},{"steps":204},[205,208,211,214,217,220],{"title":206,"text":207},"Организация","Вход через Госуслуги, реквизиты с подсказками. В разработке: подсказки DaData через серверный роут, ключ не попадает в браузер",{"title":209,"text":210},"Анкета","Сведения об организации. В разработке: формы на react-hook-form и zod, замечания модератора выводятся ошибками полей",{"title":212,"text":213},"Счёт","Вход через Alfa ID и открытие счёта. В разработке: подключение Alfa ID и возврат в мастер на тот же шаг",{"title":215,"text":216},"Документы","Загрузка PDF. В разработке: загрузка файлов с проверкой формата",{"title":218,"text":219},"Подписание","Подписание документов. В разработке: шаг недоступен, пока не загружены все документы",{"title":221,"text":222},"Завершение","Итог проверки, в том числе отказ. В разработке: отдельный экран для каждого итога проверки",{"component":140,"data":224},{"id":225,"name":226,"blocks":227},"architecture","Архитектура",[228],{"component":229,"data":230},"ProjectCaseArchitecture",{"items":231},[232,236,239,247],{"label":233,"title":234,"text":235},"STOREFRONT","Публичная витрина","Главная, каталог сборов, страница сбора, организации, документы. Главная в статике, сборы и НКО в ISR, каталог и организации — динамика с кешем запросов. Публичные страницы отдаются готовыми с сервера: их находят поисковики, а донор быстро открывает сбор по ссылке из соцсетей или рассылки.",{"label":237,"title":52,"text":238},"ACCOUNTS","Кабинеты доноров и НКО, раздел модерации. Данные грузятся на клиенте: SWR и Zustand, формы на react-hook-form и zod. Кабинеты личные и поисковикам не нужны, поэтому данные грузятся в браузере и не нагружают сервер, а проверка форм не даёт отправить анкету с ошибкой.",{"label":240,"title":241,"text":242,"code":243},"SERVER","Серверные роуты Next.js","Прокси к API, подсказки DaData с ключом только на сервере, метрики приложения. Ключи платных сервисов не попадают в браузер, поэтому их нельзя украсть и потратить лимит, а метрики показывают рост нагрузки раньше, чем его заметят пользователи.",[244,245,246],"app\u002Fapi\u002Fbackend","app\u002Fapi\u002Fdadata","app\u002Fmetrics",{"label":248,"title":249,"text":250},"API","REST бэкенда","Типы ответов генерируются из OpenAPI, поэтому фронтенд узнаёт об изменении контракта на typecheck. Расхождения с бэкендом ловятся до релиза, а не на странице оплаты у донора.",{"component":140,"data":252},{"id":253,"name":254,"blocks":255},"design-system","Дизайн-система",[256],{"component":151,"data":257},{"text":258},"Макеты рисовали два дизайнера команды в Figma. По ним фронтенд собрал свою дизайн-систему: токены темы, CSS-компоненты кнопок, карточек и полей, 110 переиспользуемых компонентов. Каждый переиспользуемый компонент принимается через Storybook.",{"component":140,"data":260},{"id":261,"name":262,"blocks":263},"integrations","Интеграции",[264,282,285],{"component":265,"data":266},"ProjectCaseIntegrations",{"items":267},[268,270,272,274,276,278,280],{"label":269},"Банковские карты · защищённые поля шлюза",{"label":271},"СБП",{"label":273},"Цифровой рубль",{"label":275,"term":57},"Alfa ID",{"label":277},"Госуслуги",{"label":279,"term":57},"DaData",{"label":281},"Яндекс Метрика",{"component":151,"data":283},{"text":284},"Интеграциями на фронтенде руководил я: по каждой выбрал подход и инструменты, поставил задачи группе и принимал результат на ревью.",{"component":151,"data":286},{"text":287},"Оплата — одна из самых сложных частей платформы: три способа, и у каждого свои правила. Картой донор платит прямо на сайте, в модальном окне, без перехода на страницу банка. Поля для номера карты встраивает платёжный шлюз, поэтому сайт данные карты не видит и не хранит. Через СБП донор подтверждает платёж в приложении своего банка, а сайт дожидается ответа и показывает результат. Цифровой рубль — новая форма денег от Центробанка, и им можно оплатить только разовое пожертвование, а корзину и ежемесячные пожертвования — картой или через СБП. Какие способы доступны, решает бэкенд для каждого сбора и вида пожертвования, и сайт показывает донору только подходящие.",{"component":140,"data":289},{"id":290,"name":291,"blocks":292},"security","Безопасность данных",[293],{"component":151,"data":294},{"text":295},"Ключ DaData живёт только на сервере, запросы идут через серверный роут. HTTP-клиент не отправляет cookie в кешируемых запросах, поэтому персональный ответ не может попасть в общий кеш. Для частных сборов есть согласие на публикацию персональных данных.",{"component":140,"data":297},{"id":298,"name":299,"blocks":300},"quality","Качество и релизы",[301,304,320,323],{"component":151,"data":302},{"text":303},"Vitest и React Testing Library: 48 файлов, около 450 тест-кейсов, базовые тесты на каждый компонент. Отдельно покрыты швы производительности: cookie и кеш в HTTP-клиенте, кеширование публичных ручек, клиентская загрузка справочников. E2E на Playwright пишет другой разработчик: корзина и пожертвование, авторизация, онбординг НКО.",{"component":305,"data":306},"ProjectCasePipeline",{"stages":307},[308,311,314,317],{"name":309,"next":310},"husky","линт и юнит-тесты на пуше",{"name":312,"next":313},"pull request","ревью и CI",{"name":315,"next":316},"develop","dev-стенд",{"name":318,"next":319},"preprod","prod",{"component":151,"data":321},{"text":322},"Проверки на пуше настроил я, GitHub Actions, Docker и деплой — DevOps команды.",{"component":151,"data":324},{"text":325},"Работаем недельными спринтами с демо в конце каждого. Задачи в Jira, ветка на задачу, PR с ревью. Для новых разработчиков есть онбординг-документ: слои, нейминг, паттерны, формы, тесты, Storybook. Для AI-агентов — правила и скиллы проекта: ревью ветки, тесты, токены темы.",{"component":140,"data":327},{"id":328,"name":329,"blocks":330},"challenges","Сложности",[331],{"component":332,"data":333},"ProjectCaseChallenges",{"items":334},[335,348],{"id":336,"name":337,"rows":338},"rps","20 rps на любой странице",[339,342,345],{"key":340,"text":341},"problem","Корневой layout на каждый запрос ждал два ответа API, до секунды: проверку авторизации и данные шапки, из которых использовалось одно поле. Из-за чтения cookie все 39 маршрутов были динамическими, кеша не было нигде.",{"key":343,"text":344},"solution","Замерил оба варианта и перенёс авторизацию в клиент: 182 rps против 15,5 у серверного с кешем шапки. Публичные ручки пометил на кеширование с тегами, главную перевёл в статику, страницы сбора и НКО — в ISR, справочник фильтров на 130 КБ вынес из HTML, кабинеты перевёл на клиентскую загрузку, 109 PNG — в webp.",{"key":346,"text":347},"result","17 маршрутов: 340 → 4552 rps суммарно, медиана ответа 919 → 69 мс. Статических маршрутов стало 17 и ещё 2 в ISR, HTML каталога похудел с 254 до 169 КБ, картинки в сборке — с 13 до 1,4 МБ.",{"id":349,"name":350,"rows":351},"empty-page","Ускорение, которое оказалось пустой страницей",[352,354,356],{"key":340,"text":353},"После перевода страницы сбора в ISR замер показал рост в 15 раз. На деле сервер отдавал пустую оболочку: useSearchParams() ронял страницу в клиентский рендер, в HTML осталось 659 символов текста вместо 2398. Ни typecheck, ни 444 теста этого не поймали.",{"key":343,"text":355},"Нашёл это сам на ревью своей ветки. Чтение параметра суммы вынесли в эффект, а в регламент добавили проверку маркеров клиентского рендера и объёма видимого текста после перевода маршрута в статику.",{"key":346,"text":357},"Страница осталась в ISR и держит 196 rps уже с полным контентом.",{"component":140,"data":359},{"id":360,"name":361,"blocks":362},"results","Метрики и результаты",[363,379],{"component":364,"data":365},"ProjectCaseResults",{"stats":366},[367,370,373,376],{"value":368,"label":369},"×13","больше запросов в секунду: 340 → 4552 rps",{"value":371,"label":372},"69 мс","медиана ответа вместо 919 мс",{"value":374,"label":375},"100","Lighthouse в Accessibility, Best Practices и SEO, Performance 90+",{"value":377,"label":378},"37","страниц, из них 20 в кабинетах",{"component":151,"data":380},{"text":381},"Нагрузку мерил autocannon на prod-сборке, 20 соединений. Lighthouse — Unlighthouse по всем страницам сайта. Бизнес-метрик пока нет: платформа ещё не вышла в релиз.",{"component":383,"data":384},"ProjectCaseCta",{"id":385,"title":386,"text":387,"url":13},"cta","Нужен большой проект на Next.js или команда, которая быстро освоит новый стек?","Заложу архитектуру и регламенты, выстрою работу фронтенд-команды и разгоню сайт под нагрузку.",{"component":389,"data":390},"ProjectCaseNext",{"slug":391,"name":392,"index":393,"tone":394},"hector","Hector","02","primary",{"name":396,"alternateNames":397,"role":400,"skills":401},"Михаил Малахов",[398,399],"Michael Malakhov","Mikhail Malakhov","Fullstack Developer",[402,61,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,103,426,427,105,428,429,430,431,432,433,434,79,435,436,437,83,438,439,440,441,442,443,65,67,444,445,446,447,448,449,450,451,452,85,453,454,455],"JavaScript (ES6+)","HTML5","CSS3","Sass","Vue.js (Vue 2\u002FVue 3, Composition API)","Nuxt 3\u002F4","React","Next.js","Vue Router","Tailwind CSS","Bootstrap","SPA","SSR","MPA","SEO","Chrome DevTools","Node.js","NestJS","Express","Laravel","Rust","PostgreSQL","Redis","REST API","Nginx","CI\u002FCD","Vite","Webpack","Git","GitHub","GitLab","Figma","Redux","Jest","Tauri","Prometheus","Jira","Claude Code","Codex","Gemini","MCP","Atlassian Rovo","TanStack Query","Pinia","Vuex","React Hook Form","Zod","Capacitor","GraphQL","WebSocket","Clinic.js","Lighthouse","GitHub Copilot","AI agents",1793309466836]