Сапожник обулся: как я делал портфолио-сайт после 3 лет на клиентов
Три года я делал сайты, лендинги и приложения клиентам. Заказы приходили через сарафан, никаких отказов от нехватки витрины не было, и я долго откладывал. В марте 2026 я наконец сел и сделал meermost.site. Это разбор изнутри: что выбрал, почему именно так, и где я сэкономил бы себе неделю, если бы делал заново.
Почему три года не было сайта
Честный ответ: не нужно было. Клиенты приходили через знакомых, чаты разработчиков и через Telegram-канал. Каждый новый заказ был результатом конкретного разговора, а не «пришёл с сайта».
Что изменилось:
- Заказов стало больше, чем времени. Перестало хватать ручного режима «расскажу по созвону». Нужна была точка, на которую можно отправить человека до созвона, чтобы он сам отсёк нерелевантное.
- Свои продукты выросли до объёма, который не помещается в одно сообщение. MeerGuard, MeerBot, Meerno, плюс десяток клиентских кейсов. Нужно было место, где это лежит как портфолио.
- Появилась команда видеопродакшна. Видеоуслуги нужно показывать примерами, а не описаниями.
- YouTube стал основным каналом привлечения. Ролики ведут на сайт, и сайт должен конвертировать в заявку.
Решение откладывать дальше стало дороже, чем решение сделать.
Стек
Next.js 15 (App Router) + React 18 + TypeScript. Был соблазн взять Tilda или Framer, собрать за выходные. Я выбрал код. Причин три:
- Контроль над SEO. Канониклы, hreflang, structured data, динамические sitemap и robots на конструкторах это либо отсутствует, либо в платных тарифах с ограничениями.
- Калькуляторы. Я знал, что главные SEO-страницы это калькулятор стоимости приложения и калькулятор видео. На Tilda пошаговый wizard с формулой на клиенте не соберёшь.
- Это часть портфолио. Сам сайт показывает, что я умею собирать. Конструктор бы это сломал.
Tailwind CSS. Здесь без размышлений. Для одностраничного портфолио с пятнадцатью страницами и сорока компонентами это просто работает. Никаких CSS-in-JS, никаких отдельных стилевых файлов.
Vercel для деплоя. Бесплатный hobby-тариф закрывает всё, что мне нужно. Previews на каждый коммит, edge-функции, аналитика, cron-задачи. На отдельный VPS я бы потратил полдня в месяц на обновления и мониторинг, здесь это просто работает.
Архитектура контента
Сайт состоит из пяти типов сущностей:
| Сущность | Где живёт | Формат |
|---|---|---|
| Переводы | lib/i18n/en.ts, lib/i18n/ru.ts | TypeScript-объекты с типом-источником на английском |
| Проекты | lib/projects.ts | TypeScript-массив с метаданными |
| Кейсы (case studies) | lib/case-studies.ts | TypeScript-объекты с двуязычным контентом |
| Блог | content/blog/<slug>.<locale>.md | Markdown с frontmatter |
| SEO-схемы | lib/seo/schema.ts, lib/seo/metadata.ts | TypeScript-билдеры |
Здесь главное решение это двуязычность через типы. Английская версия переводов является source of truth, тип Translations выводится из неё. Русская версия типизируется как Translations. Если я добавляю ключ в английский, TypeScript ругается на русский, пока я не дополню. Это лучше любой системы переводов с YAML или JSON. Забыть перевод физически нельзя, проект не соберётся.
i18n без библиотеки
Я не взял next-intl, next-translate и подобные библиотеки. Вместо этого простая структура:
app/[locale]/.... Все страницы под параметром locale.lib/i18n/index.ts.locales = ['en', 'ru'] as const,defaultLocale = 'ru'.- Компоненты принимают
localeиt(translations) пропсами.
Почему так:
- Меньше зависимостей. Любая i18n-библиотека приносит свой подход к роутингу, к namespace, к плюрализации. Для двух языков это overkill.
- Полный контроль над SEO.
hreflang,canonical, локализованные URL пишутся руками в одном файлеlib/seo/metadata.ts. Я знаю, что отдаётся в<head>на каждой странице. - Сервер-компоненты по умолчанию. Перевод это обычный объект, не контекст React. На сервере он рендерится в HTML без гидратации.
Минус один. Нет автоматического обнаружения языка по Accept-Language. Я этот минус закрыл вручную через middleware.ts с редиректом на defaultLocale.
Блог на markdown
Блог построен на статических markdown-файлах с frontmatter:
content/blog/cobbler-finally-got-shoes.ru.md
content/blog/cobbler-finally-got-shoes.en.md
Один файл, одна статья на одном языке. Парсинг через gray-matter, рендер через react-markdown + remark-gfm. В lib/blog/articles.ts лежит логика чтения файлов, валидация frontmatter, сортировка по дате, поиск похожих статей по тегам.
Frontmatter:
title: "..."
description: "..."
date: "2026-06-10"
category: development | ai | business | marketing
tags: [...]
videoId: "..." # опционально, для встраивания YouTube
draft: false
Почему markdown, а не CMS вроде Notion / Strapi / Contentful:
- Git как версионирование. Каждое изменение это коммит. Можно откатить, посмотреть историю, сделать ветку под черновик.
- Локальное редактирование. Я пишу в обычном редакторе кода, без переключения в браузер. Это важно для длинных текстов.
- Деплой проще. Markdown это часть репозитория. Изменил, запушил, Vercel пересобрал. Никаких webhook’ов из CMS, никаких задержек.
- Не плачу за инструмент. Notion-as-CMS это абонентка плюс ограничения на API-запросы.
Видео-формат блога заслуживает отдельной заметки. Если в frontmatter есть videoId, статья рендерится с YouTube-embed в начале и текстом-транскрибацией внизу. Это нужно для статей, которые я делаю из своих YouTube-роликов. Ролик сам по себе закрывает потребность, а текст работает на SEO и для тех, кто читает быстрее, чем смотрит.
Калькуляторы как главные SEO-страницы
Калькулятор стоимости приложения и калькулятор видео это не фичи сайта, это лендинги. На них я планирую гнать основной трафик из Google и Яндекса по запросам «сколько стоит приложение», «сколько стоит видео для бизнеса».
Архитектурно это пошаговые wizard’ы. Пять шагов, формула на клиенте, никакого бэкенда для расчётов. Курс рубля захардкожен, потому что для оценки приложения погрешность в 2-3% некритична, а накладные расходы на запрос к API курса валют это лишняя точка отказа.
Главная находка. Автоматический показ финального экрана отправляет анонимный лид в Telegram. Не дожидаясь, когда пользователь заполнит форму. Если человек дошёл до конца калькулятора, я об этом узнаю сразу, и могу заранее подготовиться к диалогу.
Дедупликация через sessionStorage, чтобы один и тот же пользователь не генерировал десять одинаковых уведомлений за сессию. Защита от мгновенных рекалькуляций это простой debounce.
SEO-инфраструктура
В lib/seo/:
metadata.ts.pageMetadata()принимает страницу и локаль, возвращает Next.js Metadata-объект с canonical, hreflang, OG-картинками, Twitter Card.schema.ts. Билдеры JSON-LD для Organization, Person, BlogPosting, Service, Product.
В app/:
sitemap.ts. Динамический sitemap.xml: страницы, проекты, статьи блога. Двуязычные URL автоматически.robots.ts. Динамический robots.txt с актуальным domain.
Никаких ручных правок sitemap при добавлении статьи или проекта. Добавил .md-файл, он автоматически попадает в sitemap. Добавил проект в lib/projects.ts, то же самое. Это снимает целый класс ошибок, который мучает сайты на конструкторах.
Домен meermost.site. Изначально был meermost.vercel.app. Купил .site вместо .com потому, что .com свободного с фамилией не оказалось, а .site работает с SEO ровно так же.
Telegram-интеграция
Все формы и лиды идут в один Telegram-чат через lib/telegram/send.ts. Шесть источников:
| Источник | Триггер |
|---|---|
| Контактная форма | Submit на /contact |
| Калькулятор приложений | Показ финала калькулятора |
| Калькулятор видео | Показ финала VideoCalculator |
| Vercel deploy | Webhook от Vercel при deployment.succeeded / deployment.error |
| Cron CapCut | 17-го числа каждого месяца в 12:00 МСК, напоминание оплатить подписку |
| Демо MeerBot | Через калькулятор demo-форм |
Вся аналитика и весь оперативный контакт идёт в одно место. Для соло-режима это лучше расщепления на email, CRM и Slack. Ноль переключений.
Что я бы сделал по-другому
Раньше начать с лендингов под трафик, а не с портфолио. Я начал с главной страницы, потом про себя, потом проекты. Калькуляторы и видео-лендинг сделал в конце. Логичнее было начать с них, они приносят заявки, а портфолио только подтверждает компетенцию.
Сразу два языка. Я делал сначала русский, потом добавил английский. В итоге пришлось переделать половину компонентов, чтобы они принимали locale и t. Если бы я с первого дня закладывал i18n, это бы заняло меньше времени.
Не пытаться сделать всё за один заход. Я сделал портфолио, услуги, блог, видеопродакшн, калькуляторы за один длинный спринт. Лучше было разрезать на две волны: сначала портфолио + контакты + калькулятор приложения, потом всё остальное. Тогда бы я не сжёг две недели, а получал бы фидбэк по ходу.
Контент-план до сайта, а не после. Сейчас я задним числом расписываю статьи и услуги. Если бы я сделал контент-план первым, структура сайта была бы под него, а не под мою фантазию о том, как должна быть устроена студия.
Чему меня научил собственный сайт
Собственный продукт всегда сложнее клиентского. Когда делаешь клиенту, есть бриф, дедлайн, чужое мнение, граница допустимого. Когда делаешь себе, все границы можно сдвинуть. И ты их сдвигаешь, неделя за неделей, пока не упрёшься в реальность.
Решение оказалось простым. Запретить себе ширину и зафиксировать конкретные пункты. Получился сайт с понятной структурой, который работает на заявки, а не висит как «портфолио для красоты».
Что дальше
Хотите свой сайт или лендинг, который работает на заявки, а не существует ради факта существования? Прикиньте бюджет или напишите мне, обсудим конкретику.
Похожие статьи
Меня уже 6 раз отклоняли в App Store. Вот что я понял про категорию VPN
Шесть реджектов в App Store по VPN-приложениям. Какие guideline срабатывают, что Apple делает с иконкой-щитом и словом VPN в названии, что помогает пройти ревью.
Что я подчеркнул в Чёрном лебеде Талеба, глазами фаундера
Книгу Талеба разбирали тысячу раз. Я разбираю её через свои проекты: что я перестал делать, какие решения не принял благодаря этой рамке мышления.
Как мы автоматизировали посуточную аренду: AmoCRM, RealtyCalendar и Telegram
Реальный кейс автоматизации бизнеса посуточной аренды. Цепочка: агрегаторы, RealtyCalendar, AmoCRM, бэкенд, WhatsApp и Telegram через Salesbot/Pact.