Skip to content

getWidgetContent() ​

The static getWidgetContent() method on the widget class is where widgets load dynamic data — e.g. blog articles, products, menu structures, or user-specific content. Pure form-driven widgets simply return null.

Signature ​

php
class myWidget_PageBuilderPlugin
{
    public static function getWidgetContent($widget_array, $backend = false)
    {
        // Return value: null | array
    }
}
ParameterTypePurpose
$widget_arrayarrayForm values + meta (IDs, folder path, class)
$backendbooltrue when rendering in Pagebuilder preview mode, false in the public frontend

The return value ends up in the Vue template under widget.content. null means: no extra data, Vue only sees widget.data.

When to implement? ​

SituationgetWidgetContent()
Widget shows only form values (title, text, image, link)return null;
Widget loads content from the DB (blog, shop, menu)Query + return array
Widget shows login status or user-specific contentRead session, return array
Widget renders sub-items (repeater)Load items from page_widget_item
Widget behaves differently between preview and liveif ($backend) branch

$widget_array keys ​

Form values are exposed with a widget_ prefix — not under the row name directly.

row in JSONKey in $widget_array
titlewidget_title
subtitlewidget_subtitle
textwidget_html (legacy!)
linkwidget_link
imagewidget_image
gallerywidget_gallery
logowidget_logo
colorwidget_color
pricewidget_price
videowidget_video
locationwidget_location
vcardwidget_vcard
short_textwidget_short_text
mapwidget_map
accordionwidget_accordion
pagebuilderwidget_pagebuilder
formwidget_form

row: "text" maps to key widget_html

The only mapping that doesn't follow the widget_{row} schema: "row": "text" becomes $widget_array['widget_html']. If you read $widget_array['widget_text'], you get null and wonder why. The remap happens in PageService.php — the DB column is called text, but the key name widget_html makes the HTML-content semantic explicit.

$widget_array also contains system keys:

KeyPurpose
item_id / base_idWidget base ID — for item queries against page_widget_item
widget_idConcrete widget record ID (one per placement on a page)
widget_folderAbsolute path to the widget folder
widget_classWidget template name (myWidget)

sale_status and sliderrevolution exist as DB columns but are not mapped into $widget_array — if you need them, load them directly from page_row_col_widget using $widget_id.

Pattern: return null (pure form widget) ​

For purely static widgets that only render form values:

php
class image_PageBuilderPlugin
{
    public static function getVersion() { return "1.0.0"; }
    public static function getName()    { return "Image"; }

    public static function getWidgetContent($widget_array, $backend = false)
    {
        return null;
    }
}

The real image widget from widgetsBase/widgets/image/bootstrap.php — it doesn't get shorter. The Vue template then only sees widget.data.*.

Pattern: load DB data ​

Typical pattern: the widget has a select field (e.g. a product ID or category ID), and the data gets loaded dynamically from it.

php
class productTeaser_PageBuilderPlugin
{
    public static function getVersion() { return "1.0.0"; }
    public static function getName()    { return "Product Teaser"; }

    public static function getWidgetContent($widget_array, $backend = false)
    {
        $productId = (int) ($widget_array['widget_subtitle'] ?? 0);
        if ($productId === 0) return null;

        $currentLanguage = $GLOBALS['current_language'] ?? 'de';
        $lang = real_escape_string($currentLanguage);

        $q = query("SELECT id, title, price, image, url_rewrite
                    FROM s_products
                    WHERE id = $productId AND language_short LIKE '$lang'
                    LIMIT 1");
        if (num_rows($q) === 0) return null;

        $row = fetch_assoc($q);
        $row['image'] = files::loadURL($row['image']);

        return ['product' => $row];
    }
}

In the Vue template:

vue
<template>
  <div v-if="widget.content?.product" class="product-teaser">
    <NuxtImg :src="widget.content.product.image" :alt="widget.content.product.title" />
    <h3>{{ widget.content.product.title }}</h3>
    <span>{{ widget.content.product.price }} EUR</span>
  </div>
</template>

Pattern: load items (repeater) ​

When the widget has an items field, the sub-data lives in page_widget_item. base_id comes from $widget_array['item_id']:

php
public static function getWidgetContent($widget_array, $backend = false)
{
    $widgetBaseId = (int) ($widget_array['item_id'] ?? 0);
    $language     = real_escape_string($GLOBALS['current_language'] ?? 'de');

    $q = query("SELECT * FROM page_widget_item
                WHERE widget_id = $widgetBaseId
                  AND language_short LIKE '$language'
                ORDER BY position ASC");

    $items = [];
    while ($row = fetch_assoc($q)) {
        $row['image'] = files::loadURL($row['image']);
        $items[] = $row;
    }

    return ['items' => $items];
}

Details on the items system: Widget items / repeater.

Pattern: frontend vs. backend rendering ​

Some widgets load data in the live frontend but want to show only placeholders in the Pagebuilder preview (to save DB calls or avoid disrupting preview rendering):

php
public static function getWidgetContent($widget_array, $backend = false)
{
    if ($backend) {
        return ['items' => []];   // placeholder for live preview
    }

    // Only on the real frontend: expensive query
    $items = [];
    $q = query("SELECT * FROM blog WHERE visible = 1 ORDER BY postdate DESC LIMIT 10");
    while ($row = fetch_assoc($q)) {
        $items[] = $row;
    }
    return ['items' => $items];
}

The real blog widget uses exactly this pattern: if (!$backend) { ...DB queries... }. In preview mode it loads nothing, because blog entries only make sense in the live context with URL parameters (?p=2, ?c=news, ?s=query).

Pattern: session-dependent data ​

For user-specific widgets (user center, my courses, personalized recommendations):

php
public static function getWidgetContent($widget_array, $backend = false)
{
    $userId = (int) ($_SESSION['userid'] ?? 0);
    if ($userId === 0) {
        return ['loggedIn' => false];
    }

    $q = query("SELECT id, title FROM elearning_purchases p
                JOIN elearning_courses c ON c.id = p.course_id
                WHERE p.user_id = $userId");
    return [
        'loggedIn' => true,
        'courses'  => fetch_all($q),
    ];
}

Session-dependent widgets bypass the cache

The response cache (APCu) only kicks in for anonymous GETs. Widgets that read $_SESSION are re-rendered on every request — that's correct behavior, but useful to know for performance tuning.

Pattern: multi-language with fallback ​

For multi-language content: try the current language first, fall back to the base language on an empty result.

php
public static function getWidgetContent($widget_array, $backend = false)
{
    $currentLang = real_escape_string($GLOBALS['current_language'] ?? 'de');
    $baseLang    = real_escape_string($GLOBALS['baseLanguage']     ?? 'de');

    $q = query("SELECT * FROM blog
                WHERE visible = 1 AND language_short LIKE '$currentLang'
                ORDER BY postdate DESC LIMIT 10");

    if (num_rows($q) === 0 && $currentLang !== $baseLang) {
        $q = query("SELECT * FROM blog
                    WHERE visible = 1 AND language_short LIKE '$baseLang'
                    ORDER BY postdate DESC LIMIT 10");
    }

    return ['items' => fetch_all($q)];
}

Return shape ​

The returned array is completely free — the Vue template decides which keys it expects. By convention:

KeyContents
itemsArray for list widgets (blog, shop, gallery)
product / post / courseSingle object for teaser/detail widgets
loggedIn + userAuth status for personalized widgets
configPlugin config for settings widgets
pagesPagination data

Error handling ​

getWidgetContent() runs during normal page rendering. Errors should not escalate to an HTTP 500 — that would block rendering of the whole page.

php
public static function getWidgetContent($widget_array, $backend = false)
{
    try {
        // DB operations
        return ['items' => $items];
    } catch (Throwable $e) {
        error_log('Widget xyz: ' . $e->getMessage());
        return ['items' => []];   // silent fallback
    }
}

The Vue component then shows an empty state — much better than a broken page.

Performance ​

  • No heavy queries without an index — widgets run on every page load. Add indexes on filter columns.
  • Always set LIMIT — even if the dataset is "small", it can grow.
  • Use the $backend guard — the Pagebuilder preview doesn't need real data.
  • The response cache kicks in for anonymous GETs — but only if the widget doesn't read the session.

Common issues ​

$widget_array['widget_text'] for row: "text"

As mentioned above: the key is widget_html, not widget_text. You get null instead of the entered text.

Non-static method

getWidgetContent() must be public static. The core calls it via ClassName::getWidgetContent($widget_array, $backend). An instance method is neither found nor executed.

Forgetting real_escape_string

For string parameters coming out of $widget_array (e.g. category names, slugs) that end up in SQL queries: always escape them with real_escape_string(). Use (int) for numeric values. Widgets run in the frontend context with user input from URL parameters — SQL-injection-relevant.

Language globals

The active language lives at $GLOBALS['current_language'] (snake_case), the main language at $GLOBALS['baseLanguage'] (camelCase — historically inconsistent). Both variables depend on the calling context; when in doubt, null-coalesce with a fallback: $GLOBALS['current_language'] ?? 'de'.

No URL resolution for images

The form value widget_image is JSON with a media ID, not a URL. Without files::loadURL() or equivalent resolution, the Vue template gets the raw ID and can't display the image.

See also ​