Изменение дизайна страниц
Цвета и стилизация позволяют перекрасить весь сайт без кода. Когда вам нужно, чтобы у страницы были другой макет или разметка — а не только другие цвета, — вы меняете её представление (view). Эта страница показывает, как.
Идея простыми словами
Каждая страница в Pano состоит из двух частей:
- Логика — загрузка данных, обработка входов, запуск плагинов. Этим владеет движок, и вы к этому никогда не прикасаетесь.
- Представление (view) — то, как эта страница выглядит: разметка и макет. Это ваше, чтобы менять.
Поскольку эти две части разделены, вы можете взять внешний вид любой страницы под свой контроль, не трогая её логику. Данные по-прежнему приходят, плагины по-прежнему работают, входы по-прежнему происходят — вы лишь заново стилизуете представление.
Есть 57 представлений движка, которые вы можете взять под контроль: 26 представлений страниц и макетов, по одному на каждый вид страницы (главная, вход, регистрация, профиль и так далее), и 31 часть поменьше (навбар, шапка, подвал, карточка поста, боковые панели и так далее), плюс все представления, которые предлагают установленные плагины.
Шаг 1 — посмотрите, что доступно
Выведите список каждого представления, которое можно переопределить, вместе с данными, которые получает каждое:
bunx @panomc/theme-core list-viewsЧтобы увидеть их отрисованными на тестовых данных, откройте <адрес вашего Pano>/__pano/views, пока тема работает в режиме разработки. Это каталог представлений: все представления движка и плагинов на одной странице.
Шаг 2 — возьмите представление под свой контроль
Чтобы взять представление под контроль, извлеките (eject) его. Извлечение копирует версию по умолчанию из движка в вашу собственную папку src/views/ и регистрирует её в theme.config.js:
bunx @panomc/theme-core eject-view HomeViewПосле этого у вас будет рабочий файл src/views/HomeView.svelte, который вы можете свободно редактировать.
TIP
Извлечённые файлы начинаются как рабочие копии настоящего файла по умолчанию — а не как пустая страница. Вы редактируете существующий дизайн, а не пишете его с нуля. Начните с изменения мелочей и обновления страницы.
Шаг 3 — прочитайте заголовок («материалы, которые вам даны»)
Каждое извлечённое представление начинается с комментария-заголовка, который документирует каждый проп (prop) — данные и функции, которые движок передаёт вашему представлению. Считайте это списком материалов, с которыми вам предстоит работать. Вот реальный фрагмент из HomeView темы blaze-theme:
<!--
@view HomeView (blaze override)
Controller: $pano/lib/pages/HomePage.svelte
Props:
data.posts array — записи текущей страницы
data.postCount number — общее число записей
data.page number — номер текущей страницы
data.totalPages number — общее число страниц для пагинации
themeSettings object — настройки темы из контекста
onPageClick function(data, page) — обработчик пагинации
-->Хранилища (stores) приходят как объекты-хранилища (читайте их с префиксом $, например $_), а действия (actions) приходят как функции, которые вы вызываете. Всё, что перечислено в заголовке, — это то, что у вас есть; вам не нужно знать, откуда это берётся.
Разобранный пример — переработка главной страницы
Давайте действительно это сделаем. После eject-view HomeView ваш файл src/views/HomeView.svelte выглядит так (немного сокращён для удобства чтения):
<div class="vstack gap-3">
<Hook name="page:home:top" />
<!-- Posts -->
<Posts posts={data.posts} />
<!-- Pagination -->
{#if data.postCount > 0}
<Pagination
page={data.page}
totalPages={data.totalPages}
on:pageLinkClick={(event) => onPageClick(data, event.detail.page)} />
{/if}
</div>
<script>
import { _ } from "svelte-i18n";
import Hook from "$pano/lib/components/Hook.svelte";
import Pagination from "$pano/lib/components/Pagination.svelte";
import Posts from "$pano/lib/components/Posts.svelte";
export let data;
export let themeSettings;
export let onPageClick;
</script>Читайте сверху вниз: область плагинов (<Hook>), список записей и пагинация. Это вся главная страница. Теперь давайте изменим её, по одной небольшой правке за раз.
Правка 1 — добавьте свою собственную разметку
Всё, что вы пишете в разметке, просто появляется на странице. Добавьте приветственный баннер над записями:
<div class="vstack gap-3">
<Hook name="page:home:top" />
<div class="welcome-banner">
<h1>Welcome, adventurer!</h1>
<p>Grab your pickaxe — the server awaits.</p>
</div>
<!-- Posts -->
<Posts posts={data.posts} />
...Сохраните, обновите → баннер на вашей главной странице. Стилизуйте .welcome-banner в SCSS вашей темы, как любой другой CSS-класс. В этом и заключается большая часть работы над темой: обычные HTML и CSS, написанные внутри представления.
Правка 2 — используйте данные, которые вам даны
Заголовок сказал нам, что data.posts — это массив записей. Вам не обязательно использовать готовый компонент <Posts> — вы можете расположить записи по-своему с помощью цикла {#each}:
<!-- Posts — replaced with our own card grid -->
<div class="post-grid">
{#each data.posts as post}
<a class="post-card" href="/post/{post.url}">
<h3>{post.title}</h3>
</a>
{/each}
</div>Сохраните, обновите → те же записи, совершенно другой макет, и вы владеете каждым его пикселем. Движок по-прежнему загружает данные, по-прежнему делает пагинацию, по-прежнему запускает плагины — вы лишь решили, как выглядит запись.
Как узнать, что внутри post?
Два простых способа: посмотрите, как это использовала разметка по умолчанию, или на минутку вставьте <pre>{JSON.stringify(post, null, 2)}</pre> внутрь цикла — он выведет весь объект на страницу. Удалите его, когда закончите.
Правка 3 — реагируйте на настройку
themeSettings хранит то, что владелец сайта настроил в панели. Используйте это, чтобы сделать части вашего дизайна необязательными:
{#if themeSettings.welcomeBannerVisible !== false}
<div class="welcome-banner">
<h1>Welcome, adventurer!</h1>
</div>
{/if}Теперь баннер можно отключить из панели — смотрите ниже раздел Собственные настройки темы о том, как объявить ключ, чтобы он корректно сохранялся.
Вот и весь цикл
Каждое представление работает именно так, какой бы ни была страница: eject → прочитайте заголовок, чтобы увидеть свои материалы → отредактируйте разметку → обновите. Страница входа, профиль, детали записи — тот же рецепт, другие пропы. Когда что-то ломается, отмените последнюю правку; когда сомневаетесь, сравните с представлением движка по умолчанию (оно всегда доступно в node_modules/@panomc/theme-core/src/lib/views/).
API плагинов внутри ваших представлений
Установленные плагины появляются на странице через маркеры, которые находятся внутри представлений. Их два вида:
Маркеры
<Hook>— именованные области, куда плагины могут вставлять свои собственные компоненты. Хук выглядит как<Hook name="page:home:top" />в разметке. Сегодня представления движка несут следующие имена хуков:Имя хука Где появляются плагины theme:topВ самом верху каждой страницы page:topВверху содержимого каждой страницы page:home:topВверху главной страницы theme:post-detail:bottomПод содержимым записи theme:support:contentВнутри страницы поддержки Слоты
<ViewComponent>— места, где представление отрисовывает список компонентов, зарегистрированных плагинами, например дополнительные способы входа на странице входа или дополнительные строки на карточке профиля. Они приходят через пропы, документированные в заголовке представления (хранилища, такие какcontentItemsилиaltMethods), и отрисовываются через<ViewComponent component={item.component} … />.
Что нельзя удалять ни в коем случае
WARNING
Когда вы перерабатываете представление, сохраняйте каждый <Hook> и каждый слот <ViewComponent>, которые были в оригинале — перемещайте их, меняйте стили вокруг них, оборачивайте их в свою собственную разметку, но не удаляйте. Если вы уберёте хоть один, любой плагин, полагавшийся на него, незаметно исчезнет с сайтов ваших пользователей. Кроме того, имя хука должно присутствовать только в одном представлении одновременно — размещение одного и того же хука в двух местах отрисует каждый плагин там дважды.
Вам не нужно отслеживать это вручную: bun run check завершается с ошибкой, если переопределённое представление потеряло точку подключения или имя хука подключено дважды, поэтому инструмент защищает вас, прежде чем вы сможете отправить сломанную тему.
Добавление собственных точек подключения
Вы не ограничены встроенными хуками — ваша тема может расширять API плагинов, добавляя свои собственные новые области-хуки. В любом месте представления, которым вы владеете, поставьте новый маркер со свежим именем:
<script>
import Hook from "$pano/lib/components/Hook.svelte";
</script>
<Hook name="my-theme:hero:bottom" />Любой плагин, который зарегистрирует компонент для my-theme:hero:bottom, теперь будет отрисован там. Два правила делают это безопасным:
- Используйте пространство имён для ваших имён. Начинайте их с
idвашей темы (my-theme:…), чтобы они никогда не могли столкнуться с хуками движка или другой темы. - Не переназначайте существующие имена. У встроенных имён из таблицы выше есть фиксированное значение, на которое полагаются плагины — добавляйте новые имена вместо повторного использования старых где-то ещё.
После публикации относитесь к своим пользовательским хукам как к обещанию: плагины могут начать полагаться на них, поэтому сохраняйте их в будущих версиях вашей темы так же, как встроенные.
SSR и загрузка плагинов — откуда берутся данные плагинов
Содержимое плагинов не прикручивается к странице позже в браузере — оно является частью серверного рендеринга (SSR): когда страница рендерится на сервере, компоненты плагинов, смонтированные в хуках, рендерятся вместе с ней, так что посетители (и поисковые системы) получают полную страницу в первом же ответе.
За кулисами это обеспечивают два API плагинов, и оба запускаются контроллерами движка — ваша тема никогда их не вызывает, но полезно знать, что они существуют:
- Функции
load()хуков. Компонент плагина, смонтированный в хуке, может экспортировать собственную функциюload(); движок выполняет её во время загрузки страницы (на сервере для SSR, на клиенте при навигации) и автоматически передаёт результаты компоненту какhookProps— возможно, вы заметилиhookPropsвdataзаголовков некоторых представлений. Это происходит без каких-либо действий с вашей стороны. - События жизненного цикла. Плагины также могут подписываться на события времени загрузки, которые движок вызывает при подготовке данных страницы —
theme:app:load,theme:navbar:load,theme:profile:load,theme:post-detail:load,theme:support:load,theme:tickets:load,theme:settings:loadи подобные. Именно так, например, плагины добавляют элементы в навбар достаточно рано, чтобы те появлялись в отрендеренном на сервере HTML, а не возникали после загрузки страницы.
Что это значит для вас как автора темы:
- Ничего настраивать не нужно — пока ваши переопределённые представления сохраняют точки монтирования, всё вышеперечисленное продолжает работать, включая SSR.
- Одна честная оговорка о пользовательских хуках: серверный конвейер
load()работает только для встроенных имён хуков. Плагин, смонтированный в добавленном вами пользовательском хуке (например,my-theme:hero:bottom), всё равно рендерится — включая SSR — но его данныеload()не готовятся движком, поэтому такие плагины обычно загружают свои данные на клиенте.
Перерисовка представления плагина
Пользовательский интерфейс плагина на сайте это набор именованных представлений. Id представления: <ns>:<Name> (например market:ProductCard); у представлений движка остаются простые имена (Navbar). Тема может перерисовать любое из них так же, как представление движка:
bunx @panomc/theme-core eject-view market:ProductCard
bunx @panomc/theme-core eject-view 'market:*' --pagesКоманда копирует читаемый исходник плагина в src/views/market/ProductCard.svelte, убирает из него load и view, переписывает импорты других представлений плагина в pluginView("market:PriceTag"), копирует нужные вспомогательные файлы и записывает запись в theme.config.js:
views: {
Navbar: () => import("./src/views/Navbar.svelte"),
"market:ProductCard": {
contract: 2,
controllers: ["market/cart"],
component: () => import("./src/views/market/ProductCard.svelte"),
},
},Два правила защищают обе стороны:
- Данные остаются у плагина. Страницы, инъекции и блоки сохраняют собственный
loadплагина; ваш файл даёт только разметку. Экспортированный из переопределенияloadигнорируется. - Ваша тема никогда не блокирует обновление плагина. Переопределение используется, только пока его
contract(и версия каждого закреплённого контроллера) совпадает с плагином. Иначе отрисовывается собственное представление плагина, а панель показывает его как откатившееся. Чтобы обновить, снова выполнитеeject-view market:ProductCard(рядом появится файл.new), объедините правки, затемbunx @panomc/theme-core accept market:ProductCard.
Логика плагина, не являющаяся разметкой (корзина, сессия), это контроллер с именем <ns>/<name>. Используйте его в своих представлениях и страницах: plugin('market').use('cart') из @panomc/sdk/controllers. bunx @panomc/theme-core check --fix сам запишет закрепление версий.
Блоки, захваты и слоты
Блоки. Разместите представление плагина в любом месте разметки через <PluginBlock>. Данные загружаются на сервере для каждого размещения, так что две сетки с разными props получают два набора данных. Props должны быть литералами:
<script>
import { PluginBlock } from "@panomc/sdk/components/theme";
</script>
<PluginBlock id="market:ProductGrid" limit={8} category="vip" />Неизвестный id или не установленный плагин ничего не отрисует.
Захваты (claims). Элемент навигации, виджет боковой панели или хук, которые плагин монтирует сам, это инъекция. Если вы сами разместите то же представление через <PluginBlock id="market:NavCart" />, автоматическая копия исчезнет: ваша тема захватила его. theme.config.js может задать это вручную (false сохраняет автоматическую копию):
claims: { "market:NavCart": true, "market:CartOffcanvas": false },Слоты. Представление плагина может открыть слот для других плагинов через <PluginSlot id="market:checkout:payment" />. Переопределяя такое представление, сохраните все <PluginSlot> версии по умолчанию.
Когда плагин установлен или нет
Три способа, от самого трудоёмкого к самому лёгкому:
- Поведение и раскладка:
hasPlugin.siteInfo.pluginsперечисляет установленные и запущенные плагины по их id.hasPlugin(siteInfo, "market")(из$pano/lib/plugins.js) отвечает на вопрос; основной вариант это полный id"pano-plugin-market", короткий namespace тоже работает.pluginInfo(siteInfo, "market")возвращает{ version, uiHash, dependencies }илиnull. Функция не хранит состояние, поэтому безопасна на сервере. Вload()беритеsiteInfoиз(await parent()).session, в компоненте используйте$session.siteInfo:
<Hero cta={hasPlugin($session.siteInfo, "market") ? "store" : "register"} />- Показать один блок или другой. Если плагина нет,
<PluginBlock>рисует сниппетfallback:<PluginBlock id="market:GoalWidget">{#snippet fallback()}<p>Присоединяйтесь!</p>{/snippet}</PluginBlock>. - Переопределениям и CSS плагина защита не нужна. Без плагина они просто не используются, а панель показывает плагин как «не установлен», а не как ошибку.
Не вызывайте API или контроллер плагина без проверки и не прописывайте в меню пункт со ссылкой на страницу плагина вручную.
Маршруты
theme.config.js может добавлять, отключать и переименовывать маршруты. Канонические пути в файлах маршрутов и на страницах плагинов не меняются; конфиг меняет только то, что видят посетители:
routes: {
add: { "/staff-team": "./src/pages/StaffTeam.svelte" },
disable: ["/rules"],
rename: { "/store": "/shop", "/store/[slug]": "/shop/[slug]" },
},Путь, от которого отказались переименованием, отвечает 308 на новый, отключённый путь даёт 404, а обе стороны переименования должны иметь одинаковые имена [param]. В своей разметке пишите href={route("/store")}, чтобы ссылки следовали карте. Письма и редиректы Pano тоже следуют переименованию. Ничто не может указывать на /posts или /__pano*.
Главная страница
Админ выбирает главную страницу из списка в панели; варианты объявляете вы:
home: {
default: "landing",
options: {
posts: { label: "Posts" },
landing: { label: { "en-US": "Landing", tr: "Acilis" }, page: "./src/pages/Landing.svelte" },
store: { label: "Store", path: "/store" },
custom: { label: "Custom page", path: "*" },
},
},Без ключа home варианты это лента записей, каждая установленная страница плагина, предлагающая себя как главную, и произвольный путь. Выбор, который нельзя показать (плагин удалён), откатывается к default, затем к ленте записей.
Темы без Bootstrap
Все официальные темы поставляются с Bootstrap и Font Awesome, а представления плагинов по умолчанию написаны под них. Тема со своим CSS сообщает об этом:
provides: { bootstrap: false, fontawesome: false },Создайте такую командой bunx @panomc/theme-core new my-theme --bare. Движок подключает ограниченную по области резервную таблицу стилей для каждого неперекрытого представления плагина по умолчанию; она работает на переменных --pano-* из раздела Кастомизация. check предупреждает о теме с bootstrap: false, которая всё ещё импортирует Bootstrap. Резервным стилям нужны Chrome 118, Safari 17.4 или Firefox 146; ниже представление плагина по умолчанию в теме без Bootstrap остаётся без стилей. Дочерний комбинатор CSS (.a > .b) не может пересекать обёртку, которую движок ставит вокруг представления каждого плагина по умолчанию.
Собственные настройки темы
Если ваше переработанное представление добавляет новые опции, которые владелец сайта должен иметь возможность менять (скажем, заголовок hero на главной странице), эти опции нужно объявить, чтобы панель могла их сохранять и сбрасывать. Это делается в theme.config.js в разделе settingsSchema.
Правила просты: записи только добавляют (additive) — ваши ключи добавляются во вкладку (новая вкладка создаётся, если её нет), и вы не можете удалить или переместить базовый ключ. defaultTab необязателен; задавайте его, только если ваше представление не показывает базовую вкладку по умолчанию. Вот компактный пример в стиле blaze, добавляющий ключи hero во вкладку header:
// theme.config.js
export default {
views: {
HomeView: () => import("./src/views/HomeView.svelte"),
},
settingsSchema: {
tabs: {
header: ["heroSubtitle", "heroSubtitleVisibility"],
},
defaultTab: "logo",
},
};Без этого ваши новые поля будут отображаться в панели, но никогда не будут сохраняться на самом деле. Ключу, который вы только читаете в разметке (без поля ввода в представлении настроек), запись здесь не нужна.
Проверка
bunx @panomc/theme-core check # предупреждения и ошибки
bunx @panomc/theme-core check --strict # предупреждения считаются ошибкамиЧестное замечание
Этот уровень требует базового Svelte — языка шаблонов, на котором написаны представления. Если вы никогда его не использовали, официальный учебник по Svelte короткий и интерактивный и охватывает всё, что использует представление.
Помните: вы никогда не начинаете с пустой страницы. Каждое извлечённое представление — это рабочая копия настоящего дизайна: вы редактируете, обновляете и повторяете.
Что дальше?
Когда ваша тема выглядит так, как вы хотите, Сборка и упаковка описывает проверку, сборку и упаковку, а Публикация и Premium — выпуск.