Перейти к содержимому

post.md

23 окт. 2026 г. · 3 мин чтения
Анимированный ASCII-арт: катамаран Nacra 17 плывёт по океану на закате

Как устроен этот сайт

Краткий обзор архитектуры 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-файла рядом с записью.

У блоков кода может быть заголовок с именем файла:

src/features/blog/embeds/index.tsx
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/Метаданные, наблюдаемость, ввод с клавиатуры

Вот сколько файлов исходного кода лежит в каждой папке на момент написания:

Файлы исходного кода по папкам
Показать данные
folderfiles
app17
components16
lib16
features15
i18n4
hooks3
types1

Сгенерированные значки и карточки для соцсетей

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 запускается до гидратации и отслеживает ошибки клиента, веб-метрики и смену маршрутов. Оба передают события репортёру, который рассылает их по получателям:

src/lib/observability/server.ts
export const serverReporter = createReporter([
  consoleSink,
  ...(endpoint ? [endpointSink(endpoint)] : []),
])

События клиента отправляются на /api/observability, где записываются в журнал. Чтобы позже подключить настоящий сборщик, достаточно добавить одного получателя, не трогая ни один из входных файлов.

Переводы

Сайт доступен на десяти языках, включая арабский и иврит, которые читаются справа налево. Все строки лежат в файле сообщений для каждого языка; если ключа вдруг не хватает, снизу подмешиваются сообщения языка по умолчанию, поэтому страница никогда не остаётся пустой. Записи работают так же: у каждого языка свой файл, а при его отсутствии используется английский.

NORMAL~/blog/how-this-site-is-built
0%

Сочетания клавиш

12
Основная навигация
jk
Прокрутка вниз / вверх
g gG
В начало / в конец
np
Более новая / старая запись
d
Сменить тему
:
Открыть палитру команд
?
Показать или скрыть справку
esc