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