Книжная полка в Quartz

Книжная полка на сайте Smirnoff нужна не для того, чтобы спрятать BOOK-LIBRARY внутри Quartz. Её задача проще и полезнее: дать читателю понятную публичную витрину — с категориями, обложками и описаниями — прямо рядом с заметками и проектами сайта.

При этом роли не смешиваются. Quartz остаётся статическим сайтом и отвечает за показ. BOOK-LIBRARY остаётся источником истины: там живут книги, категории, обложки, админка, импорт, переводы и скачивание через backend.

Такое разделение сделано специально. Сайт не превращается в файловый склад, не берёт на себя задачи backend библиотеки и при этом может аккуратно показать, что находится на полке.

Страница content/library.md

В content/library.md почти нет тела страницы:

---
title: "Библиотека"
description: "Категории и книги из BOOK-LIBRARY..."
publish: true
---

На первый взгляд это выглядит слишком пусто, но здесь пустота работает на архитектуру. Slug library перехватывается в quartz.layout.ts: для него подключается LibraryPage, а обычные заголовок, meta, TOC и Backlinks отключаются.

Markdown-файл остаётся маленькой, но важной точкой входа. Он задаёт title, description, publish-флаг и URL страницы, а сам интерфейс библиотеки собирается уже компонентами Quartz.

LibraryPage

quartz/components/LibraryPage.tsx создаёт каркас интерфейса, то есть всё, что читатель видит до загрузки конкретных данных:

  • hero-блок библиотеки;
  • status-сообщение загрузки;
  • контейнер категорий;
  • hover preview;
  • modal для подробной карточки книги.

Компонент получает настройки из bookshelf.json и прокидывает их в data-атрибуты:

  • data-library-backend-base-url;
  • data-library-catalog-path;
  • data-library-preview-description-length.

Так страница остаётся статической в сборке, но получает достаточно контекста, чтобы на клиенте выбрать источник данных и корректно отрисовать полку. Дальше всё оживляет libraryPage.inline.ts.

bookshelf.json

Настройки библиотеки лежат в quartz/static/data/bookshelf.json:

{
  "schemaVersion": 1,
  "backendBaseUrl": "https://book.slavx.ru",
  "catalogPath": "/static/generated/bookshelf-catalog.json",
  "previewDescriptionLength": 260
}

backendBaseUrl говорит, где искать live API BOOK-LIBRARY. catalogPath указывает на статический fallback-каталог. previewDescriptionLength ограничивает длину описания в hover preview, чтобы подсказка оставалась компактной и не превращалась в отдельную статью поверх интерфейса.

Live API

На клиенте библиотека сначала пытается загрузить свежие данные из BOOK-LIBRARY:

  • категории: /api/categories;
  • книги категории: /api/categories/:id/books?limit=24&offset=0.

Книги грузятся лениво. Первая категория запрашивается сразу, чтобы страница быстро получила видимое наполнение. Остальные подтягиваются при приближении к viewport через IntersectionObserver. Это снижает количество запросов на первом экране и не заставляет посетителя ждать всю библиотеку сразу.

Static fallback

Если live API не ответил, скрипт пробует загрузить static catalog по catalogPath. Это файл quartz/static/generated/bookshelf-catalog.json.

Fallback важен не как техническая страховка «на всякий случай», а как способ не оставлять читателя перед пустой страницей:

  • сайт остаётся рабочим, даже если backend временно недоступен;
  • GitHub Pages продолжает отдавать статическую витрину;
  • можно показать curated-снимок библиотеки без запроса к API;
  • часть проблем CORS не ломает страницу полностью.

Static catalog не заменяет backend. Это запасной слой для публичной витрины: достаточно самостоятельный, чтобы страница не развалилась, но не претендующий на роль главной базы библиотеки.

Build-time generation

generateBookshelfStaticAssets находится в quartz/util/bookshelfCatalog.ts. Он вызывается из quartz.layout.ts и может во время сборки:

  1. сходить в backendBaseUrl;
  2. получить категории;
  3. получить книги по каждой категории;
  4. скачать обложки;
  5. записать JSON-каталог;
  6. сохранить mirrored covers в quartz/static/generated/book-library-covers.

Путь к каталогу вычисляется из catalogPath. Если он начинается с /static/generated/, файл пишется внутрь generated-директории Quartz.

В практическом смысле это превращает живую библиотеку в статический снимок, который Quartz умеет отдать без обращения к backend. Для публичного сайта это удобная граница: данные подготовлены заранее, а страница всё ещё остаётся частью обычной статической сборки.

Mirrored covers

У каждой книги может быть coverUrl из BOOK-LIBRARY. Генератор пытается скачать обложку, определить расширение по URL или content-type и сохранить файл как статический asset.

После этого в catalog у книги появляется coverSrc, например:

{
  "coverSrc": "/static/generated/book-library-covers/10.webp"
}

На странице это даёт быстрые локальные обложки и уменьшает зависимость витрины от внешнего ответа в момент просмотра. Если картинка не загрузилась, LibraryPage показывает fallback с первой буквой названия — не так красиво, как обложка, зато карточка остаётся читаемой и не ломает ряд.

Rails, preview и modal

Интерфейс библиотеки сделан как набор горизонтальных rails. У каждой категории есть строка обложек и собственный scrollbar, поэтому полка ощущается как витрина: можно быстро пробежать глазами по разделу и остановиться на книге, которая заинтересовала.

На десктопе hover или focus показывает preview: категория, формат, название, автор и короткое описание. Это быстрый слой знакомства без лишнего клика. Если нужно больше контекста, клик открывает modal.

В модальном окне больше места, поэтому туда выводятся обложка, номер книги, формат, автор и полное описание. Escape и кнопка закрытия возвращают пользователя назад.

Почему Quartz — витрина

Quartz хорош как публичный слой:

  • его легко деплоить как статику;
  • он связывает библиотеку с заметками и проектами;
  • он умеет показывать curated-контент;
  • он не требует backend для каждой страницы.

Но Quartz не должен решать задачи BOOK-LIBRARY:

  • хранение файлов книг;
  • временные ссылки на скачивание;
  • импорт;
  • переводы;
  • админские операции;
  • контроль публикации.

Если смешать эти роли, сайт станет сложнее и небезопаснее. Поэтому BOOK-LIBRARY остаётся источником истины, а Quartz показывает аккуратную публичную полку. Для читателя это выглядит как единая страница библиотеки, но внутри ответственность разделена там, где ей и место.

generated — не ручной источник

quartz/static/generated нельзя воспринимать как место для ручного редактирования каталога. Это результат генерации. Если нужно поменять книгу, категорию или обложку, править нужно BOOK-LIBRARY или конфигурацию интеграции, а не generated JSON.

Для Markdown-задач особенно важно не запускать build без необходимости: он может обновить generated catalog и covers, даже если вы меняли только статьи. Иначе рядом с правкой текста легко получить шумные изменения в сгенерированных данных.

Итог

Книжная полка в Quartz — это статическая витрина с live API и fallback. Она показывает читателю категории, обложки и описания, но не забирает на себя роль библиотеки. Static catalog и mirrored covers делают эту витрину устойчивее, а fallback к BOOK-LIBRARY сохраняет связь с настоящим источником данных. Такая граница делает проект проще: Quartz отвечает за сайт и связи, BOOK-LIBRARY — за книги и данные.