Goal
You have a button type that shows up correctly in the Mobile App Manager builder — admins can add it to a tab bar or left menu, configure it, and save it — but on a real device the button either renders blank, shows nothing, or silently falls back to placeholder content. This article walks you through diagnosing the most common cause: a gap in the duck-typed content-class interface, where the class either omits get_data_for_app() or returns a payload the device-side JSON pipeline can’t use.
This is a developer task. It assumes you can read PHP and reach the mam-main source on the affected site.
Background: why the builder and the device disagree
Every button a customer can place in the no-code builder is backed by a content class — one file in mam-main/includes/content-classes/. There are roughly 21 of them today (Login, Map, Web URL, Phone Call, WP Post Category, and so on). Each file does two things:
- Registers itself in the global
$local_app_content[]array withcontent_type,class, andvc_typemetadata. - Defines a PHP class that implements a duck-typed interface — a set of methods that are expected by convention, not enforced by a formal
interfacedeclaration.
The builder UI and the device payload are produced by different methods on that class:
- The builder uses
app_settings_categories(),app_settings(),get_blank_content_html(), andget_content_type_form(). If those work, the button appears and is configurable in the admin. - The device uses
get_data_for_app($source, ...). This is the method that produces the mobile-app payload — the data the app actually receives.
Because there is no formal interface enforcing all methods, a class can ship with a perfectly working admin side and a broken or missing device side. It will “break only at the moment the method is called” — which is during JSON assembly for the phone, not during admin save. That is exactly the failure you’re chasing.
Prerequisites
- Filesystem or repo access to
mam-mainon the affected install. - The content_type label of the offending button as it appears in the builder (for example, “Web URL”, “Phone Call”). You’ll use this to find the registration.
- Ability to read the JSON the device receives — either via the app-connect endpoint response, or by logging inside the build pipeline. (You do not need a physical device; the payload is the source of truth.)
Steps
1. Map the builder label to its content class
Open mam-main/includes/content-classes/ and find the file whose registration matches the builder label. The registration block at the top of each file looks like this:
$local_app_content['Web_URL'] = array(
'content_type' => 'Web URL', // user-visible label in the builder
'class' => 'local_app_web_url',
'vc_type' => 'url',
);
The content_type value is the label admins see. The class value is the PHP class that must implement the interface. The vc_type value matters more than it looks — keep it in mind for Step 4.
Confirm the file is actually loaded: it must be in the require_once chain in content-class-manager.php. A file that exists on disk but was never added to that loader will not register at all (and usually the button wouldn’t appear in the builder either — but a partial merge can leave you in a confusing in-between state).
2. Check that get_data_for_app() exists on the class
In the class body, confirm there is a method named exactly:
public function get_data_for_app( $source ) {
// ...
}
If it is missing entirely, you have found the problem. Per the content-class contract: a content class that omits get_data_for_app will appear in the admin UI but render nothing on the device. Because the interface is not enforced, nothing warns you at save time — the omission only surfaces here.
If the method exists, continue — the shape of what it returns is the next suspect.
3. Trace how the pipeline calls the method
The device payload is assembled in mam-main/includes/app-connect/main-json-manager.php, in mam_build_nav_item(). The relevant call is branched on vc_type, and this is where a wrong shape silently fails:
$content_obj = new $content_class_name();
if ( 'html' === $this_button['vc_type'] ) {
// HTML buttons: the method receives the button array and must RETURN the whole button
$this_button = $content_obj->get_data_for_app( $value['source'], $this_button );
} else {
// Everything else: the method's return value is placed under ['data']
$this_button['data'] = $content_obj->get_data_for_app( $value['source'], false, $title );
if ( 'list' === $this_button['vc_type'] ) {
$this_button['use_subcategories'] = 'yes';
}
}
Two things to notice:
- For
vc_type === 'html', the return value replaces the entire button array. If yourget_data_for_app()returns just a fragment (ornull), you wipe outtitle,icon, and everything else. The button effectively disappears. - For every other
vc_type, the return value is dropped into['data']. If it returns nothing useful, the next block kicks in.
Right after that call, the pipeline has a fallback:
if ( ! $this_button['data'] ) {
if ( 'list' === $this_button['vc_type'] || 'offDirectory' === $this_button['vc_type'] ) {
$this_button['data'] = array( 'nodata' => 1 );
$this_button['nodata'] = array( 'nodata' => 1 );
} else {
$this_button['data'] = ' ';
}
}
This is why the symptom is usually “renders nothing” rather than a crash. An empty or falsy return gets papered over with a nodata marker or a single space — so the app receives a structurally valid but empty button.
4. Verify the method’s signature matches the branch it lands in
The signature your class needs depends on which branch in Step 3 it falls into, and that is decided by the vc_type you registered in Step 1:
vc_typeishtml— the method is called asget_data_for_app( $source, $this_button ). Your method must accept that second argument, enrich it, and return the full button array. The simple per-button classes (Web URL, Phone Call) instead useget_data_for_app( $source )and are not HTML-typed — copying one of their signatures into an HTML-typed registration is a classic mismatch.- Any other
vc_type— the method is called asget_data_for_app( $source, false, $title ). Your return value becomes thedatapayload. Returning a scalar where the app expects an array (or vice versa) is the typical wrong-shape failure here.
So: line up the vc_type in the registration against the method signature and return type. A mismatch between the two is the single most common cause once get_data_for_app() is present but the button still renders blank.
5. Inspect the actual payload
Capture the JSON the device receives for this button and look for the tells:
"data": " "(a single space) — your method returned something falsy and hit the non-list fallback."data": {"nodata": 1}or a sibling"nodata"key — falsy return on alist/offDirectorybutton.- Missing
title/iconon an HTML button — yourhtml-typed method returned a fragment instead of the whole button array.
Any of these confirms the gap is in get_data_for_app(), not in the builder, the icon, or the placement.
6. Fix the class and re-verify
- If the method was missing, add it with the signature that matches your
vc_type(Step 4), and return a payload shaped like a working sibling class of the samevc_type. - If the method returned the wrong shape, correct the return to match the branch. For HTML buttons, return the mutated
$this_button; for data buttons, return the array the app expects underdata. - Re-pull the device payload and confirm the
nodata/ single-space tells are gone and real content is present.
Note the frozen-name constraint while you’re in here: do not rename the content class if customers may have it saved in serialized button arrays (local-app-button-array). Renaming, say, local_app_login_button orphans every site’s saved button of that type. Fix the method; leave the class name alone unless you ship a migration.
If it still renders nothing
If get_data_for_app() exists, has the right signature for its vc_type, and returns a real array — but the button is still blank — widen the search beyond the content class:
- Registration not loaded. Confirm the file is in the
require_oncechain incontent-class-manager.php, and that any code reading$local_app_contentruns after that manager has loaded (it’s a global populated at include time). vc_typeconfusion.vc_typeis a Visual Composer leftover that is mostly inert, but downstream code — including the branching above — still reads it. A button registered with an unexpectedvc_typecan route into the wrong branch.- Enrichment hook. Per-button data is further massaged by subscribers to
mam_tab_manager(dozens of subscribers across the suite). A subscriber stripping or overwriting yourdatawould produce the same empty-button symptom from outside the class.
Related
- content-classes — Class Interface (Duck-Typed) — the full list of the seven expected methods and what each returns. Source of record:
mam-main/includes/content-classes/README.md. - Adding a new content class — the four-step recipe (create file, add to loader, register in
$local_app_content, implement the interface) the README documents. - App-connect JSON pipeline — how
mam_build_nav_item()assembles the per-button payload the device consumes.
What’s next
Once you’ve confirmed the fix in the payload, add a regression check: this class of bug is invisible until get_data_for_app() is actually called during a build, so the safest guard is a test that assembles the JSON for a button of this type and asserts data is neither the single-space fallback nor a nodata marker. A formal interface declaration for content classes is a known refactor target that would catch the missing-method variant at load time.
