• CMSnpm version

    • Overview
    • ContentBuilder
    • InviteUserForm
    • LoginForm
    • RenewPasswordForm
    • WysiwygInput
  • UInpm version

    • Overview
    • Accordion
    • AlertBubble
    • AnnouncementBar
    • Avatar
    • AvatarFileInput
    • Badge
    • Button
    • ButtonGroup
    • ButtonList
    • Calendar
    • Checkbox
    • CheckboxButton
    • CheckboxInput
    • CheckboxList
    • Chip
    • ColorRadio
    • ColorRadioGroup
    • Combobox
    • DatePicker
    • DatePickerInput
    • DateRangePicker
    • DateRangePickerInput
    • DatetimePicker
    • DatetimePickerInput
    • Dialog
    • Dropdown
    • Dropzone
    • ErrorMessage
    • FileInput
    • FlashMessages
    • FormComponent
    • Icon
    • IconButton
    • ImageGallery
    • InfoBox
    • Input
    • Label
    • Layout
    • Lightbox
    • ListItem
    • Loader
    • Lozenge
    • Menu
    • Message
    • Modal
    • ✅ ModalDialog
    • ✅ ModalHeader
    • MultiCombobox
    • MultiSelect
    • Pagination
    • Paper
    • Popover
    • Radio
    • RadioGroup
    • RasterImage
    • Select
    • Switch
    • ✅ Tabs
    • TextInput
    • TextLink
    • Textarea
    • TimePicker
    • TimePickerInput
    • Toggle
    • Tooltip
    • Typography
  • Formnpm version

    • Overview
    • AvatarFileInput
    • CheckboxButton
    • CheckboxInput
    • CheckboxList
    • ColorRadioGroup
    • Combobox
    • DatePickerInput
    • DateRangePickerInput
    • DatetimePickerInput
    • Dropzone
    • FileInput
    • Form
    • FormRenderer
    • GpsInput
    • MoneyInput
    • MultiCombobox
    • MultiSelect
    • NumberInput
    • PasswordInput
    • RadioGroup
    • Select
    • TextInput
    • Textarea
    • TimePickerInput
    • Toggle
  • DataGridnpm version

    • Overview
    • DataGrid
    • DataGridCustomExample
    • ExportButton
    • FilterList
    • Filters
    • FiltersButton
    • FulltextInput
    • HiddenColumns
    • HiddenColumnsButton
    • Pagination
    • RowCounts
    • RowsPerPageSelect
    • SelectedRowsToolbar
    • TableV2
    • ToolbarControl
    • ToolbarCustoms
    • ToolbarTabs
  • Wysiwygnpm version

    • Overview
  • Resizernpm version

    • Overview
  • Routernpm version

    • Overview
  • Corenpm version

    • Overview
  • Core-Reactnpm version

    • Overview
  • DnDnpm version

    • Overview
  • Page-Buildernpm version

    • Overview
  • Page-Builder-CMSnpm version

    • Overview
  • Page-Builder-Webnpm version

    • Overview
  • Sentrynpm version

    • Overview
  • Stylesnpm version

    • Overview
  • Localizenpm version

    • Overview
  • Analyticsnpm version

    • Overview
  • Datepickernpm version

    • Overview
  • Icons-generatornpm version

    • Overview
  • Smart-addressnpm version

    • Overview
  • E2Enpm version

    • Overview
  • E2E-Playwrightnpm version

    • Overview

@uxf/page-builder-cms

The backoffice half of the UXF Page Builder: the block editor and the visual editor, section forms generated from the section schemas, the page and shared section detail (settings, history, publishing and approval) and the list helpers (row actions, grid cells).

It runs on top of @uxf/cms (UI, forms, modals, flash messages) and talks to the uxf/page-builder Symfony bundle.

Install in a project

yarn add @uxf/page-builder-cms @uxf/page-builder

Peer dependencies: @uxf/cms, @uxf/core, @uxf/core-react, @uxf/dnd, @uxf/form, @uxf/ui, next, react, react-dom, react-hook-form, swr (see package.json). The API side is the uxf/page-builder bundle.

Setup

Wrap the page builder routes in the provider and pass the project's config:

import { PageBuilderCmsProvider, type PageBuilderCmsConfig } from "@uxf/page-builder-cms";

const config: PageBuilderCmsConfig = {
    registry, // generated: the project's registry (`uxf-page-builder-generator` of @uxf/page-builder)
    sectionSchemas, // generated: the section schemas, from the PHP section requests
    sectionForms: {}, // hand-written forms replacing the generated one of a section type
    sectionLayouts, // optional field arrangement per section type
    sectionInputs, // optional project inputs by schema `input` name
    api, // PageBuilderApi
    sharedSections, // SharedSectionsApi
    webUrl: "https://web.example.com", // the web app in the editor iframe; null on the same origin
    uploadImage: async (file) => "/path/of/stored/file",
    // images in rich text are stored as the file (FileResponse), as @uxf/wysiwyg renders them
    wysiwygImage: { uploadImage: async (file) => uploadFile(file, "page-builder"), getImageUrl: getFileUrl },
    links, // PageBuilderLinks: URLs of the project's routes (below)
    Layout, // the project's page frame: title, subtitle, breadcrumbs, actions, children
};

<PageBuilderCmsProvider config={config}>{children}</PageBuilderCmsProvider>;

Routes the project defines

The Pages Router needs the route files, so the project keeps them; each renders one screen of the package inside the provider and its own access check. The screens are client-only: render them behind the client gate (e.g. the access check), never during SSR. links tells the screens how to link between them (without the base path):

Route (uxf-base path)ScreenParams
/page-builder/pagesproject list (generated grid) with the package's cells and row actions–
/page-builder/pages/[page]PageDetailScreenpageId, revisionId (older revision, read-only), sectionId (?section=, open section form)
/page-builder/preview/[pageRevision]PagePreviewScreenpageRevisionId, pageId (?page=, the detail „Editovat“ returns to), sectionId (one section)
/page-builder/visual-editor/[pageRevision]VisualEditorScreenpageRevisionId, pageId (?page=, the page detail the back button returns to)
/page-builder/shared-sectionsproject list with the package's cells and row actions–
/page-builder/shared-sections/[sharedSection]SharedSectionDetailScreensharedSectionId, revisionId
/page-builder/shared-section-preview/[sharedSectionRevision]SharedSectionPreviewScreenrevisionId, sharedSectionId
function Page() {
    const [query] = useQueryParamsStatic("page-builder/page-detail");
    if (!query) {
        return <Loader />;
    }
    return (
        <ProjectPageBuilderProvider>
            <PageDetailScreen
                pageId={query.page}
                revisionId={query.revision ?? null}
                sectionId={query.section ?? null}
            />
        </ProjectPageBuilderProvider>
    );
}

The block editor keeps the open section in ?section= itself (Back closes it), so the page detail route must accept that query param, and links.pageDetail must put sectionId under it (PAGE_DETAIL_SECTION_PARAM). A project test can pin it:

const url = new URL(links.pageDetail({ pageId: 1, sectionId: 5 }), "https://admin.test");
expect(url.searchParams.get(PAGE_DETAIL_SECTION_PARAM)).toBe("5");

Generated code

What the config takes from the API side is generated, never written by hand:

  • sectionSchemas – bin/console uxf:page-builder:schema-gen reads the project's section requests (#[SectionField]) and writes a TypeScript module with the schema of every section type: field names, inputs, labels, validation rules. The forms, their validation and the inline edits are driven by it.
  • registry – the project's registry JSON (layouts, groups, section types with their example, languages), the file the bundle validates placements against, kept in api/config/page-builder/. uxf:page-builder:schema-gen (uxf/page-builder 3.88+) writes it with the schemas as JSON into each app (gen.destinations), and uxf-page-builder-generator of @uxf/page-builder turns them into registry.ts (registry, ProjectSectionType) and section-schemas.ts, like the grid and form schemas.
  • Types of the section requests and responses – the project's uxf:gen hook area that covers /api/cms writes them (with SWR hooks the package does not need; the screens use createPageBuilderApi).

Run the generators from the project's make gen in api/ and commit the output, like the other generated code.

API client

createPageBuilderApi({ baseUrl }) and createSharedSectionsApi({ baseUrl }) call the bundle's REST API with the browser's cookies (credentials: "include"). baseUrl is the API origin, "" when the backoffice proxies /api. Failed calls reject with PageBuilderApiError (status, info.validationErrors); isVersionConflict (409) and isForbidden (403) recognise the two the screens handle.

Translations

The package's texts go through useUxfTranslation of @uxf/core-react, namespace uxf-page-builder-cms. It ships the dictionaries at @uxf/page-builder-cms/translations/{cs,de,en,sk}.json (the source is @uxf/page-builder-cms/translations/translations); merge them into the translation function you pass to UiContextProvider, next to the @uxf/cms dictionaries. Without them the screens show the keys.

The list helpers are plain functions, so they take the translation function: runPageRowAction and runSharedRowAction need t (from useUxfTranslation()) in their deps, and the row action labels are keys, t(PAGE_ROW_ACTION_LABEL_KEYS[action]) and t(SHARED_ROW_ACTION_LABEL_KEYS[action]).

Styling

  • Tailwind with the @uxf/cms preset (presets: [require("@uxf/cms/utils/tailwind.config")]).
  • Add the package to Tailwind content: "./node_modules/@uxf/page-builder-cms/**/*.js".
  • The wysiwyg field is @uxf/cms's WysiwygInput (Plate). It is loaded only when a wysiwyg field renders (client only, a short empty slot while the chunk loads), so the package entry stays importable in Jest without transforming Plate's ESM. Register the @uxf/wysiwyg translations in the app like any @uxf/cms project.

Icons

PAGE_BUILDER_CMS_ICONS lists the icons the package renders. Each must be in the project's icons.config.js: the @uxf/cms icon set and the listed Font Awesome Pro icons (faPro.adapter(["regular.trash-can", …])). A project test can check it:

expect(PAGE_BUILDER_CMS_ICONS.filter((name) => !(name in iconsConfig.icons))).toEqual([]);

Environment

The project passes the web app's URL as webUrl (e.g. NEXT_PUBLIC_WEB_FRONTEND_URL); null when the web runs on the backoffice's origin. The web app must allow the backoffice origin in its editor pages' frame-ancestors (NEXT_PUBLIC_EDITOR_SHELL_ORIGIN of @uxf/page-builder-web); locally, where the two run on different ports, set both, otherwise the canvas stays empty.

The add section drawer shows a section type's registry example in an iframe of the web's SectionExample page (/_edit/section-example/[type], @uxf/page-builder-web), so the web must define that route too.

License

MIT