Read in:
Русский

Одна папка, десять дверей: как собрано колесо позиционирования

О чём это: колесо позиционирования — страница на весь экран, где каждая вершина — один способ пользоваться trip2g. У неё нет отдельного бэкенда и нет сборки. Это папка заметок и один файл макета. Здесь — как она устроена и где система макетов закончилась.

У trip2g нет одной фразы для лифта, и я перестал её искать. Поэтому вместо фразы мне нужна была страница, где двери стоят рядом: сайт из Obsidian, блог в Telegram, платная база эксперта, память для агентов и остальное. Выбираешь вершину, колесо поворачивает её наверх, в центре — объяснение.

Одна заметка — одна точка

Каждая вершина — markdown-заметка в ru/circle/, а её английский двойник лежит в en/circle/:

title: "Блог в Telegram из Obsidian"
label: "блог в telegram"
order: 3
motion_visualization: channel
link: "[[ru/user/telegram-blog-from-obsidian]]"
related:
  - "[[ru/user/telegram]]"
lang: ru
lang_redirect: "[[en/circle/channel]]"

Тело заметки — те два-четыре предложения, что видны в центре. link — главная статья, related — мелкие ссылки под ней. lang и lang_redirect связывают двойников, как у любой двуязычной заметки на этом сайте.

Добавить точку — значит добавить заметку. Убрать — удалить файл. У многоугольника столько сторон, сколько заметок. Переходы с восьми точек на девять и с девяти на десять не потребовали ничего менять в колесе; только каждой новой анимации понадобилась своя строка со скриптом (об этом ниже).

Макет, который перечисляет свою папку

Заметка страницы ru/circle/_index.md говорит layout: mesh/circle. Макет берёт папку у самой заметки и спрашивает точки в ней у запроса к заметкам:

{{ points := noteQueryRows("queries/circle_points.md", map("dir", note.Dir())) }}

Запрос — несколько строк Jsonnet в заметке: заметки прямо в этой папке, у которых есть label, по порядку order, и от каждой только version_id. У заметки страницы label нет, поэтому вершиной она не становится. Для каждой строки макет зовёт row.Note() и получает заметку-точку целиком: заголовок, текст, frontmatter. Ссылки ищутся по пути и печатаются с заголовком целевой заметки, так что сменил заголовок статьи — сменилась и подпись ссылки на колесе.

Текст для поисковиков, скрипт для колеса

Все десять описаний есть в HTML, который отдаёт сервер. Скрипт только переключает, какое из них видно, и двигает вершины. Без JavaScript страница — обычная стопка разделов, и именно это читает поисковик. Сами заметки-точки помечены noindex, а их канонический адрес указывает на колесо. Открыть такую, например /ru/circle/channel, — значит увидеть колесо, повёрнутое к этой точке.

Визуализации — ассеты макета

Каждая точка может назвать маленькую зацикленную анимацию. Каждая анимация — один файл рядом с макетом, circle/viz_channel.js и так далее, и каждая — чистая функция времени:

window.trip2gCircleViz.channel = function (ctx, t, w, h, colors) { … }

Раз картинка зависит только от t, «уменьшенное движение» достаётся даром: нарисовать один кадр и остановиться. Файлы уезжают на сервер обычной синхронизацией хранилища, как любой ассет макета.

Где упёрлось

  • Скрипт нельзя выбрать по имени во время работы. asset() регистрирует только статические строки. Поэтому в макете по строке <script> на каждую анимацию. Новая точка со старой анимацией — это просто заметка. Новая анимация — новый файл и новая строка в макете.
  • Нет dirname. Сначала я получал папку, разрезав путь по / и отрезав последний кусок. Работало, но это три строки, которые должны быть одной функцией. Теперь она есть: note.Dir() даёт ru/circle/.
  • Выборка в шаблоне не умела фильтровать по полю frontmatter. Поэтому фильтровал внутри цикла через if. Естественным фильтром был бы .Public(), но он отбрасывает noindex-заметки, а заметки-точки как раз такие. noteQuery мы сначала не нашли: заметка-запрос фильтрует по любому полю. Но и он отдавал только JSON, так что макету пришлось бы копировать в запрос каждое поле, которое он показывает, а готовый текст скопировать вообще нельзя. Теперь запрос может вернуть одни номера, а noteQueryRows возвращает по каждому заметку через .Note(), заново проверив доступ читателя. В цикле не осталось ни одного if.
  • Оболочка лендинга не подошла. Общая оболочка темы mesh жёстко ставила языковые ссылки на главную, поэтому эта страница писала свой <head>. Теперь оболочка строит canonical, hreflang и og:url по странице, которую ей передали, и её языковым двойникам (note.LangAlternative()), и колесо пользуется ею. Тонкая верхняя полоса — часть самой страницы, она осталась.
  • Подписи внутри анимаций живут в скрипте. Трёхактная анимация про компанию подписывает акты по-русски или по-английски, глядя на язык страницы. Эти строки — в JavaScript, а не в заметке, и это единственное место, где правило «всё — заметка» гнётся.
  • Одну CSS-ловушку пришлось поискать. Плавное появление текста анимировало transform. С animation-fill-mode: both трансформация оставалась применённой и тихо становилась опорой для позиционирования холста внутри, и на телефонах анимация прыгала в середину текста. Помогло анимировать только прозрачность.

Зачем это всё

Страница — демонстрация того, о чём она рассказывает. Позиционирование живёт в заметках, которые я правлю в Obsidian, сервер их рендерит, а макет — несколько сотен строк в том же хранилище. Захочу завтра одиннадцатую дверь — это ещё одна заметка.