Metadata
| Field | Value |
|---|---|
| Article type | Recipe (Developer) |
| Plugin slug | mam-main |
| Applies to plugin version | 2.1.11+ |
| Category | Extending MAM Suite |
| Audience | PHP developer |
| Estimated time | 30–60 minutes |
| Last verified | 2026-07-20 |
Goal
Supply your own markers — each with a latitude, longitude, icon, and snippet — to a Map screen in a MAM app, instead of (or alongside) the built-in WP-category or GeoDirectory marker sources.
By the end you’ll have a small sibling plugin that injects markers into the mobile JSON payload, with the markers shaped the way the native map view expects, and you’ll know the two main ways markers go missing from a map.
How the Map class actually works
It helps to know what you’re extending before you write code.
The Map content class (local_app_map, source includes/content-classes/local-app-map-class.php) is a thin nav-button registrar. It declares a Map content type with vc_type map, and it is what an admin picks in Mobile App Manager → App Setup when they add a map button. Its get_data_for_app() is a pass-through and its content-source form returns No settings required. In other words, the class itself does not build markers.
Markers reach the native map view through the mobile JSON payload — the response from the local_app_get_phone_data AJAX action. Sibling plugins inject marker data into that payload by subscribing to the mam_get_phone_data_before_send filter. mam-geodirectory contributes its own listings/events to the map through this same phone-data pipeline (see content-classes/local-app-geodirectory-v2-class.php). The native map view (Apple Maps / Google Maps depending on platform and config) does the rendering; your job server-side is to produce correctly-shaped markers.
So “add a custom marker source” means: subscribe to mam_get_phone_data_before_send and inject markers in the frozen marker shape.
Note: the top-level key the native map view reads for a custom marker source is coordinated with the mobile client — it must match the key the app (or the Map button’s custom marker source) is configured to read. The examples below use a plugin-prefixed placeholder,
my_plugin_map_markers; substitute the key your app build actually reads.
The marker shape (frozen)
Every marker is an object with this shape:
{
"id": "post_123",
"title": "Venue name",
"lat": "40.7128",
"lon": "-74.0060",
"marker_icon": "https://example.com/icon.png",
"snippet": "Short description shown in the callout"
}
Rules that are frozen — older mobile parsers break if you ignore them:
latandlonare strings, not numbers. Emit"40.7128", never40.7128. Older mobile parsers reject numeric coordinates.idis a string even when it’s a numeric post id ("123", not123).marker_iconis a full URL to the icon image. If you omit it, the app falls back to its default marker.snippetis the short text shown when a marker is tapped.
See Mobile JSON shape for the full set of payload conventions.
Prerequisites
- A sibling plugin with its own slug (don’t add this to a theme’s
functions.phpfor production). mam-mainactive — the Map class and the phone-data pipeline live there.- A Map button already placed in the app via App Setup, so you have a screen to render onto.
- Your marker data available server-side (a custom post type, an external dataset you’ve already cached locally, etc.).
- Coordination with the mobile client team: the app must know which payload key to read your markers from.
Steps
1. Build your markers in the frozen shape
Produce an array of marker objects. Cast coordinates and ids to strings:
private function build_markers(): array {
$markers = array();
foreach ( $this->get_my_locations() as $loc ) {
// Skip anything without real coordinates — see "exclude-from-map" gotchas.
if ( empty( $loc['lat'] ) || empty( $loc['lon'] ) ) {
continue;
}
$markers[] = array(
'id' => (string) $loc['id'],
'title' => $loc['name'],
'lat' => (string) $loc['lat'], // string, not float
'lon' => (string) $loc['lon'], // string, not float
'marker_icon' => $loc['icon_url'], // full URL, or '' for default
'snippet' => $loc['blurb'],
);
}
return $markers;
}
2. Inject the markers via mam_get_phone_data_before_send
Register a subscriber at the default content-provider priority (10). Prefix your top-level key with your plugin slug so it can’t collide with another plugin’s key:
add_filter( 'mam_get_phone_data_before_send', function ( array $data_array ): array {
// Replace 'my_plugin_map_markers' with the key the native map view reads.
$data_array['my_plugin_map_markers'] = $this->build_markers();
return $data_array;
}, 10 );
Priority 10 is the default cohort for content providers. Do not register at 1000 — that slot is reserved for home_cats. See Hook: mam_get_phone_data_before_send for the full priority table.
3. Respect anonymous vs. signed-in users
The filter fires for every request, including anonymous app launches. If your markers are user-scoped, gate on the current user instead of building for everyone:
add_filter( 'mam_get_phone_data_before_send', function ( array $data_array ): array {
if ( ! mam_user_id() ) {
return $data_array; // anonymous — skip user-scoped markers
}
$data_array['my_plugin_map_markers'] = $this->build_markers();
return $data_array;
}, 10 );
4. Cache if your marker build is expensive
This filter is on the hot path — it runs on every phone-data request, and a slow subscriber adds latency to every app launch. If build_markers() runs heavy queries or (worse) an HTTP call, wrap it in your own cache so unchanged data isn’t rebuilt on every request:
add_filter( 'mam_get_phone_data_before_send', function ( array $data_array ): array {
$markers = get_transient( 'my_plugin_map_markers' );
if ( false === $markers ) {
$markers = $this->build_markers(); // expensive: heavy query / HTTP
set_transient( 'my_plugin_map_markers', $markers, HOUR_IN_SECONDS );
}
$data_array['my_plugin_map_markers'] = $markers;
return $data_array;
}, 10 );
When your underlying location data changes, clear your cache and bump the global JSON cursor so clients know to refetch:
delete_transient( 'my_plugin_map_markers' );
JSON_Cursor_Manager::reset_cursor();
JSON_Cursor_Manager::get_cursor() returns a single global cache token (the mam_json_cursor option) and takes no per-user argument; reset_cursor() rolls it to a fresh value, which is what forces every client to pull a new payload.
Advanced: the mobile contract also allows a section-root
nullas a “no change since cursor” signal — distinct from a field-levelnull, whichmam_replace_null_with_empty_string()converts during output shaping (so never putnullinside an individual marker’s fields). Emitting a section-rootnullcorrectly depends on the platform’s cursor-cache internals; see Cursor cache mechanism before relying on it, and default to the transient cache above.
5. Verify the markers land in the payload
Snapshot the phone-data JSON before and after activating your plugin and diff it:
curl 'https://example.com/wp-admin/admin-ajax.php?action=local_app_get_phone_data' > before.json
# activate your plugin, then:
curl 'https://example.com/wp-admin/admin-ajax.php?action=local_app_get_phone_data' > after.json
diff before.json after.json
Confirm your key appears at the top level, that lat/lon are quoted strings, and that no marker has a null field. Then open the Map screen in the Previewer / a device build and confirm the pins render where you expect.
Exclude-from-map gotchas
Markers most often go missing for one of these reasons. Check them before assuming the injection itself failed.
- Missing or empty coordinates. A marker with no
lat/lon(or withnull/""after output shaping) can’t be placed and is effectively excluded. Filter these out inbuild_markers()(Step 1) rather than shipping empty coordinates. lat/lonshipped as numbers. If a marker silently doesn’t appear, check that the coordinates are strings. Numeric coordinates are rejected by older parsers — the rest of the payload may still load, so the failure looks like “just that map is empty.”- The GeoDirectory events exclusion. If mam-geodirectory is active and also contributing to a map, the filter
mam_gd_events_exclude_from_map(defaultfalse) controls whethergd_eventposts appear as markers. A__return_trueanywhere on the site removes events from every map request site-wide — there is no per-screen override through that hook alone. If your custom markers share a map with GeoDirectory events and the events vanish, look for this filter. (It does not touch your injected key — it only affects GeoDirectory’s own event markers.) - Default coordinates are global. The map’s fallback center (
default_lat/default_lon) is a global setting, not per-button. If a map opens “in the ocean,” your markers may be fine but the initial center/zoom is pointing elsewhere. - You overwrote a key you don’t own. Two subscribers writing the same top-level key produce last-writer-wins. Keep your plugin-prefixed key and never write into another plugin’s marker key.
What’s next
- Hook: mam_get_phone_data_before_send — the full signature, priority table, and caching contract for the extension point you used here.
- Recipe: Inject custom data into the mobile JSON — the general-purpose version of Steps 2–5.
- Content class: Map — the screen-reference for
local_app_map, its settings, and the marker JSON shape. - Hook: mam_gd_events_exclude_from_map — details on the GeoDirectory events exclusion called out above.
- Mobile JSON shape — the frozen payload conventions (string ids,
yes/noflags,nullhandling).
