Page constructor
@gravity-ui/page-constructor ·

Page constructor
Page-constructor (конструктор страниц) — это библиотека, которая позволяет отрисовывать веб-страницы или их части на основе данных в формате JSON (в дальнейшем будет добавлена поддержка формата YAML).
При создании страниц используется компонентный подход: страница составляется из набора готовых блоков, которые могут быть расположены в произвольном порядке. Каждому блоку соответствует определенный тип и набор параметров во входных данных.
Формат входных данных и список доступных блоков можно посмотреть в документации.
Установка
npm install @gravity-ui/page-constructor
Быстрый старт
Для начала нам понадобится проект на react и какой-нибудь сервер. Например, можно сделать react проект с использованием Vite и сервер на express или можно создать Next.js приложение - в нем сразу будет клиентская и серверная часть.
Устанавливаем необходимые зависимости:
npm install @gravity-ui/page-constructor @diplodoc/transform @gravity-ui/uikit
Подключаем PageConstructor на страницу. Для корректной работы его необходимо обернуть в PageConstructorProvider:
import {PageConstructor, PageConstructorProvider} from '@gravity-ui/page-constructor';
import '@gravity-ui/page-constructor/styles/styles.scss';
const App = () => {
const content = {
blocks: [
{
type: 'header-block',
title: 'Hello world',
background: {color: '#f0f0f0'},
description:
'**Congratulations!** Have you built a [page-constructor](https://github.com/gravity-ui/page-constructor) into your website',
},
],
};
return (
<PageConstructorProvider>
<PageConstructor content={content} />
</PageConstructorProvider>
);
};
export default App;
Это был самый простой пример подключения. Чтобы заработала YFM разметка, нужно обрабатывать content на сервере и получать его на клиенте.
Если ваш сервер это отдельное приложение, то нужно установить page-constructor:
npm install @gravity-ui/page-constructor
Для обработки YFM во всех базовых блоках вызовите contentTransformer и передайте туда контент и опции:
const express = require('express');
const app = express();
const {contentTransformer} = require('@gravity-ui/page-constructor/server');
const content = {
blocks: [
{
type: 'header-block',
title: 'Hello world',
background: {color: '#f0f0f0'},
description:
'**Congratulations!** Have you built a [page-constructor](https://github.com/gravity-ui/page-constructor) into your website',
},
],
};
app.get('/content', (req, res) => {
res.send({content: contentTransformer({content, options: {lang: 'en'}})});
});
app.listen(3000);
На клиенте добавляем вызов эндпоинта для получения контента:
import {PageConstructor, PageConstructorProvider} from '@gravity-ui/page-constructor';
import '@gravity-ui/page-constructor/styles/styles.scss';
import {useEffect, useState} from 'react';
const App = () => {
const [content, setContent] = useState();
useEffect(() => {
(async () => {
const response = await fetch('http://localhost:3000/content').then((r) => r.json());
setContent(response.content);
})();
}, []);
return (
<PageConstructorProvider>
<PageConstructor content={content} />
</PageConstructorProvider>
);
};
export default App;
Готовый шаблон
Чтобы начачать новый проект с нуля можно использовать готовый шаблон на Next.js который мы подготовили.
Сборщик статических сайтов
Page Constructor Builder - утилита командной строки для создания статических страниц на основе YAML-конфигураций с использованием @gravity-ui/page-constructor
Документация
Параметры
interface PageConstructorProps {
content: PageContent; //Blocks data in JSON format.
shouldRenderBlock?: ShouldRenderBlock; // A function that is invoked when rendering each block and lets you set conditions for its display.
custom?: Custom; //Custom blocks (see `Customization`).
renderMenu?: () => React.ReactNode; //A function that renders the page menu with navigation (we plan to add rendering for the default menu version).
navigation?: NavigationData; // Navigation data for using navigation component in JSON format
}
interface PageConstructorProviderProps {
isMobile?: boolean; //A flag indicating that the code is executed in mobile mode.
locale?: LocaleContextProps; //Info about the language and domain (used when generating and formatting links).
location?: Location; //API of the browser or router history, the page URL.
analytics?: AnalyticsContextProps; // function to handle analytics event
ssrConfig?: SSR; //A flag indicating that the code is run on the server side.
theme?: 'light' | 'dark'; //Theme to render the page with.
mapsContext?: MapsContextType; //Params for map: apikey, type, scriptSrc, nonce
}
export interface PageContent extends Animatable {
blocks: Block[];
menu?: Menu;
background?: MediaProps;
}
interface Custom {
blocks?: CustomItems;
subBlocks?: CustomItems;
headers?: CustomItems;
loadable?: LoadableConfig;
}
type ShouldRenderBlock = (block: Block, blockKey: string) => Boolean;
interface Location {
history?: History;
search?: string;
hash?: string;
pathname?: string;
hostname?: string;
}
interface Locale {
lang?: Lang;
tld?: string;
}
interface SSR {
isServer?: boolean;
}
interface NavigationData {
logo: NavigationLogo;
header: HeaderData;
}
interface NavigationLogo {
icon: ImageProps;
text?: string;
url?: string;
}
interface HeaderData {
leftItems: NavigationItem[];
rightItems?: NavigationItem[];
}
interface NavigationLogo {
icon: ImageProps;
text?: string;
url?: string;
}
Серверные утилиты
Пакет включает набор серверных утилит для преобразования контента.
const {fullTransform} = require('@gravity-ui/page-constructor/server');
const {html} = fullTransform(content, {
lang,
extractTitle: true,
allowHTML: true,
path: __dirname,
plugins,
});
Для преобразования Yandex Flavored Markdown в HTML используется пакет diplodoc/transfrom, который входит в peer-зависимости.
Эти утилиты можно также использовать в кастомных компонентах или других частях проекта.
const {
typografToText,
typografToHTML,
yfmTransformer,
} = require('@gravity-ui/page-constructor/server');
const post = {
title: typografToText(title, lang),
content: typografToHTML(content, lang),
description: yfmTransformer(lang, description, {plugins}),
};
Список других доступных утилит можно найти в этом разделе.
Подробная инструкция по подготовке данных, использованию кастомных трансформеров находится в дополнительном разделе документации.
Пользовательские блоки
Конструктор страниц поддерживает возможность работы с блоками, определенными пользователем в его приложении. Блоки представляют собой обычные React-компоненты.
Для того чтобы передать в конструктор свои блоки, нужно:
-
Создать у себя в приложении блок.
-
В коде создать объект, в котором ключом будет тип блока (строка), а значением – импортированный компонент блока.
-
Передать созданный объект в параметр
custom.blocks,custom.headersилиcustom.subBlocksкомпонентаPageConstructor(вcustom.headersуказываются блоки заголовков, которые должны рендериться отдельно над общим контентом). -
Теперь во входных данных (параметр
content) можно использовать созданный блок, указав его тип и данные.
Для того чтобы при создании собственных блоков использовать миксины и стилевые переменные конструктора, нужно в своем файле стилей добавить импорт:
@import '~@gravity-ui/page-constructor/styles/styles.scss';
Динамически загружаемые блоки
Иногда нужно, чтобы блок рендерился на основе загружаемых данных. В таких случаях применяются динамически загружаемые (loadable) блоки.
Для того чтобы добавить пользовательские loadable блоки, нужно передать в PageConstructor свойство custom.loadable, в котором ключами являются названия источников данных (строка) для компонента, а значением — объект.
export interface LoadableConfigItem {
fetch: FetchLoadableData; // data loading method
component: React.ComponentType; //blog to pass loaded data
}
type FetchLoadableData<TData = any> = (blockKey: string) => Promise<TData>;
Сетка
PageConstructor использует сетку bootstrap и ее реализацию на основе React-компонентов, которую можно использовать в проекте (в том числе отдельно от конструктора).
Пример использования:
import {Grid, Row, Col} from '@gravity-ui/page-constructor';
const Page = ({children}: React.PropsWithChildren<PageProps>) => (
<Grid>
<Row>
<Col sizes={{lg: 4, sm: 6, all: 12}}>{children}</Col>
</Row>
</Grid>
);
Навигация
Элемент навигации по страницам также может быть реализован отдельно от конструктора:
import {Navigation} from '@gravity-ui/page-constructor';
const Page = ({data, logo}: React.PropsWithChildren<PageProps>) => <Navigation data={data} logo={logo} />;
Блоки
Каждый из блоков представляет собой неделимый верхнеуровневый компонент. Все блоки хранятся в директории src/units/constructor/blocks.
Саб-блоки
Саб-блоки — это компоненты, которые могут использоваться в свойстве children основного блока. В конфигурации указывается список дочерних компонентов из саб-блоков, которые после рендеринга будут переданы в блок как children.
Как добавить новый блок в page-constructor
-
В директории
src/blocksилиsrc/sub-blocksсоздайте папку с кодом блока или саб-блока. -
Добавьте название блока или саб-блока в перечисление
BlockTypeилиSubBlockTypeи опишите его свойства в файлеsrc/models/constructor-items/blocks.tsилиsrc/models/constructor-items/sub-blocks.tsпо аналогии с уже существующими. -
Добавьте экспорт блока в файле
src/blocks/index.tsили саб-блока в файлеsrc/sub-blocks/index.ts. -
Добавьте новый компонент или блок в маппинг в файле
src/constructor-items.ts. -
Добавьте валидатор для нового блока:
- Добавьте файл
schema.tsв директорию блока или саб-блока. В этом файле опишите валидатор параметров для данного компонента в форматеjson-schema. - Экспортируйте его в файл
schema/validators/blocks.tsилиschema/validators/sub-blocks.ts. - Добавьте его в
enumилиselectCasesв файлеschema/index.ts.
- Добавьте файл
-
В директории блока добавьте файл
README.mdс описанием входных параметров. -
В директории блока добавьте демо-Storybook в папку
__stories__. Весь демо-контент для компонентаStoryпомещается в файлdata.jsonв соответствующей директории. КомпонентStoryдолжен принимать тип свойств блока, иначе в Storybook будут отображаться некорректные данные. -
Добавьте шаблон данных блока в папку
src/editor/data/templates/. Имя файла должно соответствовать типу блока. -
При необходимости добавьте иконку превью блока в папку
src/editor/data/templates/. Имя файла должно соответствовать типу блока.
Темы
PageConstructor поддерживает использование тем: для отдельных свойств блоков можно задавать разные значения в зависимости от выбранной в приложении темы.
Для добавления темы в свойство блока:
-
В файле
models/blocks.tsопределите тип нужного свойства блока через обобщениеThemeSupporting<T>, гдеT— тип данного свойства. -
В файле с React-компонентом блока получите значение свойства с темой через хук
getThemedValueиuseTheme(примеры можно посмотреть в блокеMediaBlock.tsx). -
Добавьте поддержку темы в валидатор свойства: в файле
schema.tsблока оберните данное свойство вwithTheme.
i18n
Библиотека page-constructor основана на UIKit и работает с ее экземпляром i18n. Для настройки интернационализации используйте configure из UIKit:
import {configure} from '@gravity-ui/uikit';
configure({
lang: 'ru',
});
Карты
Для использования карт необходимо указать тип карты, scriptSrc и apiKey в поле mapContext в PageConstructorProvider.
Для режима разработки переменные окружения можно определить в файле .env.development в корне проекта.
Например, STORYBOOK_GMAP_API_KEY содержит значение apiKey Google Maps.
Аналитика
Инициализация
Для начала работы с аналитикой передайте обработчик в конструктор. Такой обработчик создаётся на стороне проекта и получает события трёх классов:
- События по умолчанию — универсальные события Page Constructor для кнопок, ссылок, навигации и контролов. Для их отправки установите
autoEvents.enabled: true. - Расширенные события — зарегистрированные события, которые передаёт библиотека-композиция. Наличие
autoEvents.extendedEventsвключает их независимо отenabledи позволяет добавить префикс и счётчик. - Пользовательские события задаются потребителями через
analyticsEvents. Конфигурация автоматических событий их не изменяет.
Предпочтительный формат конфигурации — объект:
function sendEvents(events: MyEventType []) {
...
}
<PageConstructorProvider
...
analytics={{
sendEvents,
autoEvents: {
enabled: true,
extendedEvents: {
prefix: 'LIBRARY_',
counter: 'secondary',
},
},
}}
...
/>
type ExtendedEventsConfig = {
prefix?: string;
counter?: string;
};
type AutoEventsConfig = {
enabled: boolean;
extendedEvents?: ExtendedEventsConfig;
};
Для обратной совместимости поддерживается прежний булев формат: true эквивалентно {enabled: true}, а false — {enabled: false}. Если autoEvents не указан, события по умолчанию и расширенные события выключены. Объект extendedEvents включает переданные расширенные события, даже если enabled равен false.
Расширенное событие должно иметь type: 'extended-event'. Префикс объединяется с именем без изменений регистра, разделителей или пробелов. Если указан counter, он задаёт counters.include расширенного события:
// Переданное событие
{name: 'REGISTERED_CLICK', type: 'extended-event'}
// Событие, которое получит sendEvents с конфигурацией выше
{
name: 'LIBRARY_REGISTERED_CLICK',
type: 'extended-event',
counters: {include: ['secondary']},
}
Порядок отправки: сначала созданное событие по умолчанию (если оно включено), затем переданные расширенные и пользовательские события в исходном порядке. Если extendedEvents не настроен, расширенные события пропускаются. Дополнительный контекст конкретного взаимодействия в последнюю очередь объединяется с каждым отправляемым событием.
У объекта события есть только одно обязательное поле — name. Также он включает предопределенные поля для управления сложной логикой. Например, counter.include позволяет отправлять событие в конкретный счетчик, если в проекте используются несколько аналитических систем.
type AnalyticsEvent<T = {}> = T & {
name: string;
type?: string;
counters?: AnalyticsCounters;
context?: string;
};
Тип события для проекта можно настроить.
type MyEventType = AnalyticsEvent<{
[key: string]?: string; // only a 'string' type is supported
}>;
Селектор счетчика
Событие можно привязать к конкретной аналитической системе.
type AnalyticsCounters = {
include?: string[]; // array of analytics counter ids that will be applied
exclude?: string[]; // array of analytics counter ids that will not be applied
};
Параметр context
Используйте параметр context для определения точки вызова события.
Используйте предложенный ниже селектор или создайте логику под нужды проекта.
// analyticsHandler.ts
if (isCounterAllowed(counterName, counters)) {
analyticsCounter.reachGoal(counterName, name, parameters);
}
Зарезервированные типы событий
Ряд предопределенных типов событий применяется для обозначения автоматически настроенных событий. Их можно использовать, например, для фильтрации событий по умолчанию.
enum PredefinedEventTypes {
Default = 'default-event', // default events which fire on every button click
Extended = 'extended-event', // events supplied by a composing library
Play = 'play', // React player event
Stop = 'stop', // React player event
}
Разработка
npm ci
npm run dev
Работа с Vite
import react from '@vitejs/plugin-react-swc';
import dynamicImport from 'vite-plugin-dynamic-import';
export default defineConfig({
plugins: [
react(),
dynamicImport({
filter: (id) => id.includes('/node_modules/@gravity-ui/page-constructor'),
}),
],
});
Для работы с Vite необходимо установить плагин vite-plugin-dynamic-import и настроить конфигурацию для поддержки динамических импортов.
Процесс релиза
В стандартной практике используются два основных вида коммитов:
Fix— тип коммита для исправления багов в базе кода (соответствуетPATCHв семантическом версионировании).Feat— тип коммита для добавления новых функций в базу кода (соответствуетMINORв семантическом версионировании).BREAKING CHANGE— тип коммита с подвалом «BREAKING CHANGE:» или знаком «!» после типа/области; указывает на критические изменения в API (соответствуетMAJORв семантическом версионировании). Может быть частью любого типа коммита.- Для ручной настройки версии релизного пакета укажите
Release-As: <version>в сообщении коммита, например:
git commit -m 'chore: bump release
Release-As: 1.2.3'
Всю необходимую информацию можно найти здесь.
После того как пулл-реквест (PR) будет одобрен владельцами кода и пройдет все проверки, выполните следующие шаги:
- Проверьте наличие PR для релиза от робота (например,
chore(main): release 0.0.0) с изменениями другого контрибьютора. Если такой PR есть, выясните, почему он не был слит. Если контрибьютор согласен на релиз общей версии, переходите к следующему шагу. Если нет, попросите его выпустить свою версию, а затем переходите к следующему шагу. - Объедините свои изменения в один коммит (
squash) и выполните слияние (merge) PR. Важно, чтобы новая версия была выпущена с помощью Github-Actions. - Подождите, пока робот создаст пулл-реквест с новой версией пакета и внесет информацию об изменениях в
CHANGELOG.md. Следите за процессом на вкладке Actions. - Проверьте внесенные изменения в
CHANGELOG.mdи одобрите PR робота. - Выполните
squashиmergeвашего PR. Следите за процессом релиза на вкладке Actions.
Релиз альфа-версий
Если необходимо выпустить альфа-версию пакета из ветки, это можно сделать вручную:
- Перейдите на вкладку Actions.
- В меню слева выберите рабочий процесс Release alpha version.
- Справа появится кнопка Run workflow с возможностью выбрать ветку.
- Рядом будет доступно поле для ручного ввода версии. При первом релизе альфа-версии из ветки это поле можно оставить пустым. Последующие релизы потребуют указания новой версии вручную, так как мы не изменяем
package.jsonна случай, если ветка скоро будет удалена. Убедитесь, что в версии используется префиксalpha, иначе возникнет ошибка. - Нажмите на кнопку Run workflow и дождитесь завершения процесса. Выпускать новые версии можно по мере необходимости, но не слишком часто. В остальных случаях используйте
npm pack.
Релиз бета-версий новой мажорной версии
Для релиза новой стабильной мажорной версии, скорее всего, потребуется сначала выпустить несколько бета-версий. Для этого выполните следующие действия:
-
Создайте или обновите ветку
beta. -
Добавьте необходимые изменения в эту ветку.
-
При готовности к выпуску новой бета-версии выполните ручной релиз. Для этого создайте пустой коммит или в подвале последнего коммита добавьте следующее сообщение:
git commit -m 'fix: last commit Release-As: 3.0.0-beta.0' --allow-empty -
После релиза робот создаст новый PR в ветку
beta, который будет включать обновленныйCHANGELOG.mdи новую версию пакета. -
Этот процесс можно повторять неограниченное количество раз. При готовности к выпуску стабильной мажорной версии без бета-метки создайте PR из ветки
betaв веткуmain. Следует учесть, что версия пакета будет содержать бета-метку. Система автоматически определит и изменит версию на соответствующую. Например, версия3.0.0-beta.0будет преобразована в3.0.0.
Процесс релиза предыдущих мажорных версий
Если необходимо выпустить новую версию предыдущей мажорной версии после коммита в main, выполните следующие шаги:
- Обновите ветку, которая относится к предыдущей мажорной версии:
version-1.x.x/fixes— для версии 1.x.x.version-2.x.x— для версии 2.x.x.
- Создайте новую ветку от соответствующей ветки предыдущей мажорной версии.
- Выборочно перенесите (
cherry-pick) свой коммит из веткиmainв созданную ветку. - Создайте PR и после одобрения слейте его в ветку предыдущей мажорной версии.
- Объедините свои изменения в один коммит (
squash) и выполните слияние (merge) PR. Важно, чтобы новая версия была выпущена с помощью Github-Actions. - Подождите, пока робот создаст пулл-реквест с новой версией пакета и внесет информацию об изменениях в
CHANGELOG.md. Следите за процессом на вкладке Actions. - Проверьте внесенные изменения в
CHANGELOG.mdи одобрите PR робота. - Выполните
squashиmergeвашего PR. Следите за процессом релиза на вкладке Actions.
Редактор конструктора страниц
Пользовательский интерфейс редактора позволяет управлять содержимым страницы с функцией предпросмотра в реальном времени.
Пример использования:
import {Editor} from '@gravity-ui/page-constructor/editor';
interface MyAppEditorProps {
initialContent: PageContent;
transformContent: ContentTransformer;
onChange: (content: PageContent) => void;
}
export const MyAppEditor = ({initialContent, onChange, transformContent}: MyAppEditorProps) => (
<Editor content={initialContent} onChange={onChange} transformContent={transformContent} />
);
Memory Bank (Банк памяти)
Этот проект включает в себя комплексный Memory Bank (Банк памяти) — коллекцию файлов документации в формате Markdown, которые предоставляют подробную информацию об архитектуре проекта, компонентах и паттернах использования. Memory Bank особенно полезен при работе с AI-агентами, поскольку содержит структурированную информацию о:
- Обзор проекта: Основные требования, цели и контекст
- Документация компонентов: Подробные руководства по использованию всех компонентов
- Архитектура системы: Технические паттерны и проектные решения
- Прогресс разработки: Текущий статус и детали реализации
Использование Memory Bank
Memory Bank расположен в директории memory-bank/ и состоит из обычных Markdown-файлов, которые можно читать как любую другую документацию:
projectbrief.md- Основополагающий документ с ключевыми требованиямиproductContext.md- Назначение проекта и цели пользовательского опытаsystemPatterns.md- Архитектура и технические решенияtechContext.md- Технологии, настройка и ограниченияactiveContext.md- Текущий фокус работы и последние измененияprogress.md- Статус реализации и известные проблемыusage/- Документация по использованию конкретных компонентовstorybookComponents.md- Детали интеграции со Storybook
Для AI-агентов
При работе с AI-агентами над этим проектом Memory Bank служит как комплексная база знаний, которая помогает агентам понять:
- Структуру проекта и паттерны
- API компонентов и примеры использования
- Рабочие процессы разработки и лучшие практики
- Текущий статус реализации и следующие шаги
AI-агенты могут читать эти файлы, чтобы быстро разобраться в контексте проекта и принимать более обоснованные решения относительно изменений кода и реализации.
Тесты
Полный комплект документации можно найти по этой ссылке.