• 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-web

The web half of the UXF Page Builder:

  • PageRenderer renders a page's sections with the project's section components: a published page from the public page API (PublicPageData of @uxf/page-builder) as well as a revision in the editor;
  • the visual editor's iframe: the bridge to the backoffice (@uxf/page-builder protocol), the selection overlay, drag and drop, and inline editing (InlineText, InlineRichText);
  • the edit and preview pages: ready-made for the Pages Router, and the components and data fetchers an App Router route is composed of.

Entry points

ImportWhatWhere it runs
@uxf/page-builder-webPageRenderer, SectionElement, InlineText, InlineRichText, useIsInlineEditing, PageBuilderWebProvider, normaliseOrigin, PAGE_BUILDER_WEB_ICONS, typesimportable from a Server Component; the components and the hook are client components ("use client")
@uxf/page-builder-web/pagescreatePageBuilderWebPages (Pages Router) and the page components (EditPageRevisionPage, SharedPreviewPage, …)the page components are client components
@uxf/page-builder-web/serverfetchPageRender, fetchSharedSectionRender, fetchSharedPreview, fetchSectionExample, fetchPublicPageByUrl, publicPageMetadata, PageRenderNotFoundError, getEditorPageHeaders, getPageBuilderRouteHeaders, SHARED_PREVIEW_HEADERSserver only: Server Components, route handlers, getServerSideProps; no React

Install in a project

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

Peer dependencies: @uxf/core, @uxf/core-react, @uxf/wysiwyg, next, react, react-dom (see package.json).

Setup

import { normaliseOrigin, PageBuilderWebProvider, type PageBuilderWebConfig } from "@uxf/page-builder-web";

const config: PageBuilderWebConfig = {
    registry, // generated: `uxf-page-builder-generator` of @uxf/page-builder, from uxf:page-builder:schema-gen
    sectionComponents, // { hero: HeroSection, … } by section type
    shellOrigin: normaliseOrigin(process.env.NEXT_PUBLIC_EDITOR_SHELL_ORIGIN),
};

Section components receive { content }, the API's public output of their own section type, and may use InlineText / InlineRichText for the fields the editor edits in place; outside the editor they render plain text. Type the map with SectionComponents and the union of the public outputs the API generator writes (uxf:gen, e.g. PageBuilderSectionContentPublicOutput): a section type without a component, or a component of another type's content, then fails the type check instead of rendering nothing.

import type { SectionComponents } from "@uxf/page-builder-web";
import type { PageBuilderSectionContentPublicOutput } from "@generated/page-builder-api";

export const sectionComponents: SectionComponents<PageBuilderSectionContentPublicOutput> = {
    hero: HeroSection, // (props: { content: PageBuilderHeroSectionPublicResponse }) => ReactNode
    faq: FaqSection,
};

App Router

The root entry works in the App Router (the default target). PageRenderer reads the config from PageBuilderWebProvider, so both are client components: a Server Component may render <PageRenderer layout={page.layout} sections={page.sections} /> with the data it fetched, as long as the provider sits above it. The config holds the section components (functions), which a Server Component cannot pass as props, so render the provider from the project's own client module:

// src/page-builder/page-builder-provider.tsx
"use client";

import { PageBuilderWebProvider } from "@uxf/page-builder-web";
import { ReactNode } from "react";
import { pageBuilderWebConfig } from "./page-builder-config";

export function PageBuilderProvider(props: { children: ReactNode }) {
    return <PageBuilderWebProvider config={pageBuilderWebConfig}>{props.children}</PageBuilderWebProvider>;
}

wrapSection of PageRenderer is a function too and can only be passed from a client component.

Routes the project defines

The backoffice opens these paths (WEB_ROUTES in @uxf/page-builder).

Pages Router

createPageBuilderWebPages from @uxf/page-builder-web/pages builds the pages once; the project's route files only re-export them:

// src/zones/page-builder-zone/pages.ts
import { createPageBuilderWebPages } from "@uxf/page-builder-web/pages";

export const pageBuilderPages = createPageBuilderWebPages({
    config,
    apiUrl: process.env.NEXT_PUBLIC_FRONTEND_URL ?? "",
});

// src/pages/_edit/page-revision/[id].tsx
export const getServerSideProps = pageBuilderPages.EditPageRevision.getServerSideProps;
export default pageBuilderPages.EditPageRevision.default;
PathPage
/_edit/page-revision/[id]EditPageRevision: the revision in the visual editor's iframe; ?preview=1 for the preview window
/_edit/shared-section-revision/[id]EditSharedSectionRevision: one shared section revision for its preview
/_preview/[token]SharedPreview: a preview opened through a shared link, without login
/_edit/section-example/[type]SectionExample: a section type with its registry example, for its card in the add section drawer

apiUrl is the base the SSR fetches call the API through (usually the web's own URL, which proxies /api). The edit pages forward the visitor's cookie, so the API sees the backoffice login; a revision the visitor may not see answers 404. They also send frame-ancestors with the shell origin, X-Robots-Tag: noindex and Cache-Control: no-store. The shared preview sends X-Robots-Tag: noindex, Cache-Control: no-store and Referrer-Policy: no-referrer on every answer, its 404 and error pages included.

Errors of the fetchers name the endpoint, never the URL: a shared preview's token does not end up in logs or Sentry.

App Router

There is no factory for the App Router yet (follow-up). A project composes each route from @uxf/page-builder-web/server and the page components of @uxf/page-builder-web/pages, inside its PageBuilderProvider (see Setup):

// src/app/%5Fedit/page-revision/[id]/page.tsx (`%5F`: a folder starting with `_` is private in the App Router)
import { getDefaultLanguage, readRouteLang } from "@uxf/page-builder";
import { EditPageRevisionPage, getPreviewMode, getPreviewSections } from "@uxf/page-builder-web/pages";
import { fetchPageRender, PageRenderNotFoundError } from "@uxf/page-builder-web/server";
import { cookies } from "next/headers";
import { notFound } from "next/navigation";
import { registry } from "@generated/page-builder/registry"; // generated by uxf-page-builder-generator

export default async function Page(props: PageProps<"/_edit/page-revision/[id]">) {
    const { id } = await props.params;
    const query = await props.searchParams;
    try {
        const render = await fetchPageRender({
            baseUrl: process.env.NEXT_PUBLIC_FRONTEND_URL ?? "",
            cookie: (await cookies()).toString(),
            pageRevisionId: Number(id),
            lang: readRouteLang(query, getDefaultLanguage(registry)),
        });
        const preview = getPreviewMode(query);
        if (preview !== null && getPreviewSections(render.sections, preview) === null) {
            notFound();
        }
        return <EditPageRevisionPage preview={preview} render={render} />;
    } catch (error) {
        if (error instanceof PageRenderNotFoundError) {
            notFound();
        }
        throw error;
    }
}

A Server Component cannot set response headers, so set them in next.config. getPageBuilderRouteHeaders(shellOrigin) returns them for every route above, in the shape headers() expects: the edit routes get getEditorPageHeaders(shellOrigin) (frame-ancestors with the shell origin, X-Robots-Tag: noindex, Cache-Control: no-store; only the origin of the configured URL is used, so the value cannot add another source or directive), /_preview/[token] gets SHARED_PREVIEW_HEADERS.

// next.config.ts
import { getPageBuilderRouteHeaders } from "@uxf/page-builder-web/server";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
    async headers() {
        return getPageBuilderRouteHeaders(process.env.NEXT_PUBLIC_EDITOR_SHELL_ORIGIN ?? null);
    },
};

export default nextConfig;

A project that already returns headers of its own spreads them into the same list. In middleware/proxy, set getEditorPageHeaders(shellOrigin) or SHARED_PREVIEW_HEADERS on the response of the matching path instead.

fetchSharedPreview rejects a value that cannot be a token with PageRenderNotFoundError without calling the API, and every fetcher gives up after 10 seconds with an error that names the endpoint.

Public pages

PageRenderer renders a published page's sections in the groups of its layout. fetchPublicPageByUrl asks the public read API for the page published at a URL (GET /api/app/page-builder/public/page-by-url, uxf/page-builder 3.88+): its layout, meta data and sections, or PageRenderNotFoundError for a URL without a published page. publicPageMetadata turns the meta data into App Router metadata. A catch-all route of the web composes them:

// src/app/(web)/[[...slug]]/page.tsx (inside a layout with the project's PageBuilderProvider)
import { PageRenderer } from "@uxf/page-builder-web";
import { fetchPublicPageByUrl, PageRenderNotFoundError, publicPageMetadata } from "@uxf/page-builder-web/server";
import { notFound } from "next/navigation";

async function loadPage(props: PageProps<"/[[...slug]]">) {
    const { slug } = await props.params;
    try {
        return await fetchPublicPageByUrl({
            baseUrl: process.env.NEXT_PUBLIC_FRONTEND_URL ?? "",
            url: `/${(slug ?? []).join("/")}`,
            lang: "cs",
            revalidate: 60, // published changes show within a minute
        });
    } catch (error) {
        if (error instanceof PageRenderNotFoundError) {
            notFound();
        }
        throw error;
    }
}

export async function generateMetadata(props: PageProps<"/[[...slug]]">) {
    return publicPageMetadata(await loadPage(props));
}

export default async function Page(props: PageProps<"/[[...slug]]">) {
    const page = await loadPage(props);
    return <PageRenderer layout={page.layout} sections={page.sections} />;
}

Next.js deduplicates the two identical fetches of one request. The project's own routes take precedence over the catch-all; a multilingual web takes the language from its own routing (prefix, domain) instead of "cs". Publishing does not purge Next's cache: revalidate is how long an outdated page may be served.

Environment

VariableWhereValue
NEXT_PUBLIC_EDITOR_SHELL_ORIGINwebOrigin of the backoffice. The edit pages allow it in frame-ancestors and the bridge accepts messages only from it. Empty when backoffice and web share an origin.
NEXT_PUBLIC_FRONTEND_URLwebThe web's own URL: the base of the SSR fetches (apiUrl / baseUrl), which reach the API through the web's /api proxy.
NEXT_PUBLIC_WEB_FRONTEND_URLbackoffice (webUrl)URL of the web the visual editor and the previews open in their iframes.

Locally the web and the backoffice run on different ports, so they are different origins: set NEXT_PUBLIC_EDITOR_SHELL_ORIGIN to the backoffice's local origin (e.g. http://localhost:3011) and the backoffice's webUrl to the local web, otherwise the canvas stays empty (the browser refuses the frame). On a review or production stage where the backoffice runs on the web's host (e.g. under /admin), leave it empty.

The SSR fetches of the edit pages go to NEXT_PUBLIC_FRONTEND_URL and forward only the visitor's cookie. Behind HTTP basic auth (UXF review environments) check that this request is not refused; point apiUrl at an address the server reaches without it if it is.

Local development against fe-monorepo

When a project uses a local checkout of the packages (file: dependencies, never committed), add them to transpilePackages in next.config (["@uxf/page-builder", "@uxf/page-builder-web"]): the dev server then bundles the rebuilt files instead of keeping the first ones it required from node_modules.

Translations

The editor's texts (overlay, inline editing, section placeholders, the shared preview bar) go through useUxfTranslation of @uxf/core-react, namespace uxf-page-builder-web. The package ships the dictionaries at @uxf/page-builder-web/translations/{cs,de,en,sk}.json (the source is @uxf/page-builder-web/translations/translations); merge them into the translation function you pass to UiContextProvider. Without them the editor shows the keys.

Styling

  • Add the package to Tailwind content: "./node_modules/@uxf/page-builder-web/**/*.js".
  • Inline rich text uses @uxf/wysiwyg: import @uxf/wysiwyg/styles/styles.css and register its translations.

Icons

PAGE_BUILDER_WEB_ICONS lists the icons the package needs in the project's icons.config.js. It is empty today.

License

MIT