Media Providers
How the plugin gets media, and how to add a source of your own.
A provider is where a gallery's items come from. The Media Library is one. YouTube, Vimeo, TikTok, Wistia and plain external URLs are others. Adding another is one class and one registration call.
The contract
Every provider answers the same questions:
| Method | Returns |
|---|---|
get_id() | A stable slug, stored on each item. |
get_label() | The name shown in the picker's source rail. |
get_icon() | Its glyph. |
is_available() | Whether it can run right now — credentials present, extension loaded. |
is_pro() | Whether it belongs to the premium build. |
fetch() | Media, as normalised items. |
to_array() | Its own description, for the picker. |
The normalised item
fetch() returns items in one shape, whatever the source:
| Key | Holds |
|---|---|
id | A stable identifier — an attachment id, or the URL for an external item. |
type | image or video. |
url | What the tile displays. |
thumb | A small version. |
full | The full-size version, used by the lightbox. |
width · height | Intrinsic dimensions. The measured layouts need these. |
alt · title · caption · description | Text. |
link | A destination for the custom URL click action. |
provider | Your provider's id. |
width and height matter more than they look
Justified and Masonry balance rows and pack columns from real proportions. A provider that returns items without dimensions will render, but those two layouts can't do their job.
Why this shape exists
The renderer only ever sees normalised items. It has no idea whether an item came from the Media Library or a third-party service. Neither does the React grid, the REST layer or the CSS.
That's what makes a new provider a genuinely additive change: one class, one registration, and it works in every layout, in the lightbox, with categories, with filtering and with pagination — with no change to anything downstream.
Registering one
add_action(
'shaped_gallery_register_providers',
function ( $registry ) {
$registry::register( new My_Provider() );
}
);$registry is the provider registry class name. Registration happens during the plugin's own bootstrap, so the action is the right place — not init.
Appearing in the picker
The picker's source rail is built from the registry, so a registered, available provider shows up on its own. Two things decide whether yours appears:
is_available()returning true. If your provider needs an API key, return false until one is saved — a source that can't work shouldn't be offered.is_pro()— which build it belongs to.
Things to get right
Sanitize everything you bring in. Anything from a remote service is untrusted. URLs go through esc_url_raw(), text through sanitize_text_field(), numbers get a real cast. Don't forward an unknown key — drop it.
Cast numbers, never absint() them. absint( -3 ) is 3, which turns nonsense into a plausible value. A bogus id that looks valid is worse than one that's obviously wrong.
Use wp_safe_remote_* with TLS verification on. An outbound request carrying a customer's credential isn't a place to switch verification off.
Cache your fetches, and cache failures too. Without a negative cache, a service that's down is re-dialled on every editor load, and each attempt costs the author a long timeout.
Read the response defensively. json_decode() of an HTML error page is null, and reading a property off it is a fatal error on PHP 8.
What providers can't do
Two features need a real WordPress attachment behind an item, so they don't apply to external providers:
- Watermarking — the mark is composited into a new file derived from an attachment.
- Resolution and retina candidates — these are registered WordPress image sizes.
Everything else — every layout, the lightbox, categories, filtering, search, pagination, click actions, hover animations — works for any provider.
Per-gallery data and the re-fetch
Some of what an item carries belongs to the gallery, not the source: its categories, and its text overrides. When items are re-resolved from a provider at render time, that per-gallery data is carried across rather than being re-fetched — a provider could never know it.
If you're extending the resolution path, that's the rule to preserve. Dropping category membership silently empties every filter; dropping the text overrides means the source's own values quietly win over what the author typed.
Contributing categories from elsewhere
If you want memberships to come from a taxonomy or a provider's own albums rather than from the Edit Gallery modal, there's a filter for exactly that:
add_filter(
'shaped_gallery_item_categories',
function ( $categories, $item, $vocabulary ) {
// Return category ids from wherever you like.
// They're allow-listed against the gallery's own vocabulary.
return $categories;
},
10,
3
);No renderer, CSS or runtime change is needed — filtering, counts and pagination all follow.
Where to go next
- Adding Media — the user-facing view of the same thing
- Hooks · Block Attributes