Goal
Add a section of your own to the GeoDirectory listing-detail screen without forking mam-geodirectory and without touching mam-main. By the end you will have a new section that:
- shows up in the app’s listing-detail
content_sections[], in a position the admin can reorder; - appears in the App Settings → Layouts → {Content type} → Detail UI alongside the seven sections
mam-geodirectoryalready contributes; - is built from your own data, with your own visibility rules.
The detail screen is “owned” by mam-geodirectory in the sense that it registers the screen’s content class and its default sections. But the screen is assembled through public filters, so you can extend it from a small companion plugin and survive upgrades to mam-geodirectory.
This article is a worked example for the GeoDirectory detail screen, but the same two-filter pattern extends any MAM screen that composes a content_sections[] array.
Prerequisites
- A working MAM Suite install with
mam-mainandmam-geodirectoryactive. - Comfort writing a small WordPress plugin (a single PHP file with
add_filter()calls is enough). Do not put this code insidemam-geodirectory— keep it in your own plugin so upgrades don’t overwrite it. - Familiarity with two reference articles, because this how-to combines them:
- Hook: mam_get_phone_data_before_send — the primary mobile-API extension point, the filter every sibling plugin uses to inject data into the phone-data response.
- Content class elements reference and Listing detail content sections — how a screen’s
content_sections[]array is shaped and built.
- A way to identify the data you want to show. This example assumes you already have per-listing data keyed by the listing’s post ID (post meta, an option, a custom table, an API response you cache, etc.).
⚠️
mam_get_phone_data_before_sendis a frozen contract with roughly seventy subscribers across ~2K customer sites. You are subscribing to it, which is safe and expected. Do not rename it, and do not overwrite top-level keys you don’t own.
How the detail screen is assembled (just enough background)
mam-geodirectory builds the listing-detail content sections in two layers:
-
A catalogue of available sections. Its content class declares the seven default sections (check-in/favorite, post content, address, business hours, social media, events, event details) in
default_content_sections(), then runs that array through themam_content_sectionsfilter. This is what populates the Layouts → Detail admin UI, where each section can be enabled, reordered, and re-titled. -
A builder per section. For each enabled section, the app-settings layout renderer in
mam-mainfires amam_content_section_<id>filter — keyed on the section’s registeredid— that returns the actual payload appended to the screen’scontent_sections[]array. Each builder is registered at priority10with4args and receives$sections, $data_array, $show_title, $title, returning the (possibly extended)$sections.
So extending the screen “in context” means doing both: register your section in the catalogue so the admin can place it, and provide a builder that emits its payload when it’s enabled. The whole thing is glued together inside the listing-detail leg of mam_get_phone_data_before_send.
A content section payload has a stable shape — see Content class elements reference:
[
'title' => 'My section',
'content' => '...', // string, HTML, or a structured array depending on 'type'
'type' => 'my_section', // discriminator the app uses to pick a renderer
'show_title' => 'no', // 'yes' | 'no'
]
The
typevalue tells the mobile app which renderer to use. If you invent a brand-newtype, the app has to know how to render it. For a first pass, reuse atypethe app already understands — for example the raw-HTML element (typehtml) — so your section renders without any app-side change. Introduce a newtypeonly when you’re also shipping app support for it.
Steps
1. Register your section in the catalogue
Hook the same mam_content_sections filter the content class applies, and append your section definition. This makes it appear in App Settings → Layouts → {GeoDirectory content type} → Detail so an admin can enable it, reorder it, and override its title.
add_filter( 'mam_content_sections', function ( array $sections, $content_type ) {
// Only add to GeoDirectory detail screens, not every content type.
if ( 'local_app_geodirectory' !== $content_type ) {
return $sections;
}
$sections[] = array(
'id' => 'my_section',
'title' => 'My Section', // default admin-facing label; admin can override
'type' => 'my_section',
);
return $sections;
}, 10, 2 );
The second filter argument is the content-type identifier. For GeoDirectory detail screens
mam-geodirectorypasses the literal string'local_app_geodirectory'— its canonical public content-type id, not the PHP class name (static::classwould resolve tomam_geodirectory_content, which is deliberately avoided so downstream registrants gate on the stable public id). The exact-match guard above keeps your section off screens it doesn’t belong on.
2. Provide a builder for your section type
Each enabled section is built through a mam_content_section_<id> filter, where <id> matches the id you registered in step 1 (not the payload’s type). Subscribe to mam_content_section_my_section and append your payload. Keep the four-argument signature so your builder composes the same way the built-in ones do.
add_filter( 'mam_content_section_my_section', function (
array $sections,
array $data_array,
string $show_title = 'no',
string $title = 'My Section'
) {
$listing_id = $data_array['id'] ?? 0;
if ( ! $listing_id ) {
return $sections;
}
$body = my_plugin_build_section_html( $listing_id );
// Visibility rule: omit the section when there's nothing to show.
if ( '' === $body ) {
return $sections;
}
$sections[] = array(
'title' => $title, // honor the admin's title override
'content' => $body,
'type' => 'html', // reuse an app-known renderer (see note above)
'show_title' => $show_title,
);
return $sections;
}, 10, 4 );
Notes that mirror how the built-in sections behave (see Listing detail content sections):
- Honor
$titleand$show_title. They carry the admin’s per-section overrides from the Layouts UI. Don’t hard-code your own. - Omit, don’t emit empty. Several built-in sections (business hours, social media, events) skip themselves when their data is empty rather than rendering a blank block. Do the same.
- Keep it pure. The built-in builders only append to
$sections; they never mutate the listing’s fields. Stay in that lane so you don’t fight other subscribers.
3. Apply visibility / permission rules (optional)
The detail screen is built per request, so you can vary your section by user — exactly how check_in_fav hides itself for a listing’s owner, manager, or staff. If you need the same owner/manager/staff awareness, resolve roles from the listing ID inside your builder rather than re-deriving them; the section list is recomputed on the next data fetch whenever the user’s relationship to the listing changes.
$is_privileged = my_plugin_user_manages_listing( $listing_id, mam_user_id() );
if ( $is_privileged ) {
return $sections; // e.g. hide a "claim this listing" prompt from people who already own it
}
If your visibility rule depends only on listing data (not the user), prefer to keep the check in the builder where it’s easy to reason about per request.
4. Make sure the section’s data is current (cache awareness)
The detail data flows through mam_get_phone_data_before_send, which is cursor-cached per user. If your section reads data that changes over time, follow the cache-aware pattern from Hook: mam_get_phone_data_before_send: when the underlying data changes, bump a “changed at” timestamp so the per-user cache invalidates and your new content is sent. Don’t make uncached HTTP calls from the builder — it runs on a hot path that fires on every app launch.
5. Let the admin place the section
Activate your companion plugin, then go to App Settings → Layouts → {your GeoDirectory content type} → Detail. Your section appears in the list (from step 1). Enable it, drag it into position, and optionally rename it. Save.
The default ordering of the built-in sections is the order they’re registered (check_in_fav, post_content, address_section, business_hours, social_media, events, event_details); your section drops in wherever the admin places it relative to those.
6. Verify on a real listing
Open the listing-detail screen for a listing that has the data your section needs. Confirm:
- the section renders in the position the admin chose;
- it is absent for listings where your visibility rule says to skip it (step 2/3);
- the title reflects any admin override;
- updating the underlying data and refreshing the app shows the change (step 4).
If the section never appears, check in this order: the section is enabled in Layouts; your mam_content_sections guard actually matched the content class; your type in step 1 matches the mam_content_section_<type> filter name in step 2; your builder isn’t returning early because it thinks the data is empty.
Why this beats forking
- Upgrade-safe. Your code lives in your own plugin. Upgrading
mam-geodirectoryormam-maindoesn’t touch it, and you’re using the same public filters the suite uses internally. - Admin-controllable. Because you registered through
mam_content_sections, a non-developer can reposition, rename, or disable your section without touching code — the same as any built-in section. - Composable. Other suite plugins (chat, special offers) add their own detail sections the same way. Yours coexists with them instead of conflicting.
Gotchas
typeis an app contract. Reusing an app-knowntype(likehtml) renders with zero app changes. A newtypevalue needs a matching renderer shipped in the app, or it won’t display. See Content class elements reference.- Match the filter name to the registered
idexactly. A section registered withidmy_sectionis built bymam_content_section_my_section— the fired filter name comes from the section’sid, not its payloadtype. A mismatch silently produces a registered-but-never-built section. - Don’t overwrite keys you don’t own. Append to
$sections; never replace the array or stomp other top-level$data_arraykeys. Last-writer-wins bugs here are hard to trace across ~70 subscribers. - The section is recomputed per request. Good for per-user variation; it also means a slow builder slows every app launch for every user. Keep it cheap and cache external data.
- Guard on the content-type id. The check in step 1 matches
'local_app_geodirectory'— the idmam-geodirectorypasses for every GeoDirectory detail screen (listings, admin approvals, off-directory, map, calendar all share it). Match it exactly so your section doesn’t leak onto unrelated screens (or fail to appear on the one you want).
Related
- Hook: mam_get_phone_data_before_send — the injection point this whole pattern hangs off.
- Listing detail content sections — the seven sections
mam-geodirectoryships and how each is built (the template this example follows). - Content class elements reference — the reusable section builders and the
content_sections[]payload shape. - Content classes overview — what “owns” a screen and how per-screen handlers are registered.
What’s next
- Ship an app-side renderer for a custom
typewhen raw HTML isn’t enough. - Apply the same two-filter pattern (
mam_content_sections+mam_content_section_<type>) to a different screen’s content class. - If your section needs its own admin settings, see Recipe: register a content class for how screen handlers and their settings are wired.
