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.
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.
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>;
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) | Screen | Params |
|---|---|---|
/page-builder/pages | project list (generated grid) with the package's cells and row actions | – |
/page-builder/pages/[page] | PageDetailScreen | pageId, revisionId (older revision, read-only), sectionId (?section=, open section form) |
/page-builder/preview/[pageRevision] | PagePreviewScreen | pageRevisionId, pageId (?page=, the detail „Editovat“ returns to), sectionId (one section) |
/page-builder/visual-editor/[pageRevision] | VisualEditorScreen | pageRevisionId, pageId (?page=, the page detail the back button returns to) |
/page-builder/shared-sections | project list with the package's cells and row actions | – |
/page-builder/shared-sections/[sharedSection] | SharedSectionDetailScreen | sharedSectionId, revisionId |
/page-builder/shared-section-preview/[sharedSectionRevision] | SharedSectionPreviewScreen | revisionId, 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");
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.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.
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.
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]).
@uxf/cms preset (presets: [require("@uxf/cms/utils/tailwind.config")]).content: "./node_modules/@uxf/page-builder-cms/**/*.js".@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.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([]);
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.
MIT