Extend an existing screen in context (geodirectory detail worked example)

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-geodirectory already 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-main and mam-geodirectory active.
  • Comfort writing a small WordPress plugin (a single PHP file with add_filter() calls is enough). Do not put this code inside mam-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_send is 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:

  1. 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 the mam_content_sections filter. This is what populates the Layouts → Detail admin UI, where each section can be enabled, reordered, and re-titled.

  2. A builder per section. For each enabled section, the app-settings layout renderer in mam-main fires a mam_content_section_<id> filter — keyed on the section’s registered id — that returns the actual payload appended to the screen’s content_sections[] array. Each builder is registered at priority 10 with 4 args 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 type value tells the mobile app which renderer to use. If you invent a brand-new type, the app has to know how to render it. For a first pass, reuse a type the app already understands — for example the raw-HTML element (type html) — so your section renders without any app-side change. Introduce a new type only 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-geodirectory passes the literal string 'local_app_geodirectory' — its canonical public content-type id, not the PHP class name (static::class would resolve to mam_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 $title and $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-geodirectory or mam-main doesn’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

  • type is an app contract. Reusing an app-known type (like html) renders with zero app changes. A new type value 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 id exactly. A section registered with id my_section is built by mam_content_section_my_section — the fired filter name comes from the section’s id, not its payload type. 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_array keys. 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 id mam-geodirectory passes 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).

  • Hook: mam_get_phone_data_before_send — the injection point this whole pattern hangs off.
  • Listing detail content sections — the seven sections mam-geodirectory ships 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 type when 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.
Was this article helpful?
Contents

    Need Support?

    Can't find the answer you're looking for? Don't worry we're here to help!