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
class myWidget_PageBuilderPlugin
{
public static function getWidgetContent($widget_array, $backend = false)
{
// Return value: null | array
}
}| Parameter | Type | Purpose |
|---|---|---|
$widget_array | array | Form values + meta (IDs, folder path, class) |
$backend | bool | true 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?
| Situation | getWidgetContent() |
|---|---|
| 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 content | Read session, return array |
| Widget renders sub-items (repeater) | Load items from page_widget_item |
| Widget behaves differently between preview and live | if ($backend) branch |
$widget_array keys
Form values are exposed with a widget_ prefix — not under the row name directly.
row in JSON | Key in $widget_array |
|---|---|
title | widget_title |
subtitle | widget_subtitle |
text | widget_html (legacy!) |
link | widget_link |
image | widget_image |
gallery | widget_gallery |
logo | widget_logo |
color | widget_color |
price | widget_price |
video | widget_video |
location | widget_location |
vcard | widget_vcard |
short_text | widget_short_text |
map | widget_map |
accordion | widget_accordion |
pagebuilder | widget_pagebuilder |
form | widget_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:
| Key | Purpose |
|---|---|
item_id / base_id | Widget base ID — for item queries against page_widget_item |
widget_id | Concrete widget record ID (one per placement on a page) |
widget_folder | Absolute path to the widget folder |
widget_class | Widget 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:
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.
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:
<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']:
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):
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):
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.
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:
| Key | Contents |
|---|---|
items | Array for list widgets (blog, shop, gallery) |
product / post / course | Single object for teaser/detail widgets |
loggedIn + user | Auth status for personalized widgets |
config | Plugin config for settings widgets |
pages | Pagination 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.
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
$backendguard — 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
- Widget Anatomy — signature of the
_PageBuilderPluginclass - widget_menu field types — which
rowvalues exist - Widget items / repeater — loading
page_widget_item - Custom CSS convention — CSS wrapper per widget
- User-center items — Vue-level pattern for user widgets