post.md
23 окт. 2026 г. · 3 мин чтения
Как устроен этот сайт
Краткий обзор архитектуры cmkrw.com: от терминального интерфейса до Markdown, графиков и отчётов об ошибках.
- nextjs
- design
Содержание
Этот сайт намеренно небольшой: одна страница, один блог и как можно меньше JavaScript. Вот как всё устроено — на случай, если вы захотите сделать что-то похожее.
Терминал — это и есть макет
Сайт построен как терминальное приложение. Вверху — панель вкладок, внизу — строка состояния, а всё между ними — панели: рамки с заголовком, встроенным в верхнюю границу. Все панели рисует один небольшой компонент Pane.
Блог в виде «список — содержимое»: список остаётся на месте, пока меняется запись.
Навигация следует той же идее. В блоге список записей остаётся слева, а запись открывается справа, поэтому для перехода между записями не нужна ссылка «назад». Сочетания клавиш работают так же, как в терминале, а эту таблицу можно отсортировать щелчком по заголовку столбца:
| 1 | Открыть главную | везде |
| 2 | Открыть блог | везде |
| j | Прокрутить вниз | везде |
| k | Прокрутить вверх | везде |
| g g | В начало | везде |
| G | В конец | везде |
| n | Более старая запись | блог |
| p | Более новая запись | блог |
| d | Сменить тему | везде |
| ? | Показать сочетания | везде |
Анимация — в основном CSS. Команда в шапке печатается анимацией steps(), панели всплывают по мере прокрутки, а линия под панелью вкладок и процент в строке состояния следуют за положением прокрутки. При prefers-reduced-motion всё это отключается.
Как пишутся записи
Записи — это обычные Markdown-файлы в content/blog/<slug>/<locale>.md, которые отображает TanStack Markdown. Если перевода нет, используется английский файл, а draft: true не пускает запись в продакшен-сборку.
Остальное делают три инструмента TanStack:
- Markdown разбирает запись и отображает её как элементы React — без промежуточной HTML-строки.
- Highlight раскрашивает блоки кода. В сборку попадают только зарегистрированные мной языки, а цвета берутся из тех же CSS-переменных, что и вся тема.
- Charts и Table встраиваются однострочным комментарием с именем JSON-файла рядом с записью.
У блоков кода может быть заголовок с именем файла:
export function createEmbeds(slug: string, locale: string) {
return {
chart: ({ data = "", kind = "bar", x = "x", y = "y", title = "Chart" }) => (
<TuiChart
rows={getPostDataset(slug, data, locale)}
kind={kind}
x={x}
y={y}
label={title}
/>
),
}
}Обычные таблицы Markdown тоже работают и получают тот же стиль:
| Слой | Папка | Ответственность |
|---|---|---|
| Маршруты | app/ | Страницы, метаданные, значки и изображения |
| Интерфейс | components/tui/ | Панели, верхняя панель, строка состояния, сочетания клавиш |
| Функции | features/ | Разделы главной страницы и блог |
| Утилиты | lib/, hooks/ | Метаданные, наблюдаемость, ввод с клавиатуры |
Вот сколько файлов исходного кода лежит в каждой папке на момент написания:
Показать данные
| folder | files |
|---|---|
| app | 17 |
| components | 16 |
| lib | 16 |
| features | 15 |
| i18n | 4 |
| hooks | 3 |
| types | 1 |
Сгенерированные значки и карточки для соцсетей
Next.js 16 создаёт изображения метаданных из файлов в каталоге app/:
icon.tsxиapple-icon.tsxрисуют фавикон и значок для iOS из одного SVG —assets/logo-icon.svg.opengraph-image.tsxрисует карточку главной страницы, а ещё один файл — карточку для каждой записи.
Файла favicon.tsx нет. В документации прямо сказано, что фавикон сгенерировать нельзя, поэтому эту работу делает icon.tsx. Всё это опирается на небольшой общий набор примитивов, так что смена знака бренда меняет фавикон и все карточки сразу.
Инструментирование
За наблюдаемость отвечают два файла. instrumentation.ts работает на сервере и сообщает об ошибках запросов. instrumentation-client.ts запускается до гидратации и отслеживает ошибки клиента, веб-метрики и смену маршрутов. Оба передают события репортёру, который рассылает их по получателям:
export const serverReporter = createReporter([
consoleSink,
...(endpoint ? [endpointSink(endpoint)] : []),
])События клиента отправляются на /api/observability, где записываются в журнал. Чтобы позже подключить настоящий сборщик, достаточно добавить одного получателя, не трогая ни один из входных файлов.
Переводы
Сайт доступен на десяти языках, включая арабский и иврит, которые читаются справа налево. Все строки лежат в файле сообщений для каждого языка; если ключа вдруг не хватает, снизу подмешиваются сообщения языка по умолчанию, поэтому страница никогда не остаётся пустой. Записи работают так же: у каждого языка свой файл, а при его отсутствии используется английский.