oEmbed Provider Generator
Registers an oEmbed provider WordPress does not know about, with the URL pattern, the regex flag people get wrong, and a note on where the result is cached.
<?php
/**
* oEmbed providers.
*
* Adds services WordPress does not ship a provider for, so a bare URL on its own
* line becomes an embed.
*
* WordPress can also find an endpoint by itself: it fetches the page and looks for
* a <link rel="alternate" type="application/json+oembed"> tag. That is discovery,
* it is on by default, and it means a provider entry is only needed when the
* service does not advertise an endpoint, when discovery has been turned off, or
* when you want the embed to work without the extra request.
*/
defined( 'ABSPATH' ) || exit;
add_action( 'init', 'my_plugin_register_oembed_providers' );
/**
* Registers the providers.
*
* On init rather than at file load: wp_oembed_add_provider() writes into the
* oEmbed singleton, and calling it too early is how a provider silently goes
* missing.
*
* @return void
*/
function my_plugin_register_oembed_providers() {
// Loom.
wp_oembed_add_provider(
'https://www.loom.com/share/*',
'https://www.loom.com/v1/oembed',
false
);
}
add_action( 'init', 'my_plugin_maybe_clear_oembed_cache' );
/**
* Clears cached oEmbed results after a provider change.
*
* Results are cached twice: in post meta, as _oembed_<hash> on the post that
* contains the URL, and in a transient for the HTTP response. Changing a provider
* does not touch either, which is why an edit often appears to do nothing at all.
*
* Bump the version constant below to clear them once.
*
* @return void
*/
function my_plugin_maybe_clear_oembed_cache() {
$version = '1';
if ( get_option( 'my_plugin_oembed_version' ) === $version ) {
return;
}
global $wpdb;
// Post meta first: these are what the editor and the front end read.
$wpdb->query(
"DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '\_oembed\_%'"
);
update_option( 'my_plugin_oembed_version', $version );
}
Output is valid and updates as you type.
Fix the highlighted fields to update the output.
WordPress turns a bare URL on its own line into an embed when it knows the service.
wp_oembed_add_provider() teaches it one it does not know.
Two things about it are worth knowing before you write the call. WordPress can often find
the endpoint by itself: it fetches the page and looks for a
<link rel="alternate" type="application/json+oembed"> tag, which is discovery, and it is
on by default. So a provider entry is only needed when the service does not advertise an
endpoint, when discovery has been turned off, or when you want to skip the extra request.
And the third argument decides how the pattern is read. Off, * is a wildcard. On, the
whole string is a regular expression, delimiters included. Passing a regex with the flag off
matches nothing and produces no error.
How to use
- Set a function prefix.
- Add a row per service: the URL pattern people will paste, and the service’s oEmbed endpoint from its documentation.
- Tick the regex box only if you are passing a real expression.
- Paste the file into your plugin, or into a site-specific plugin.
Example
add_action( 'init', 'my_plugin_register_oembed_providers' );
function my_plugin_register_oembed_providers() {
// Loom.
wp_oembed_add_provider(
'https://www.loom.com/share/*',
'https://www.loom.com/v1/oembed',
false
);
}
Registering on init rather than at file load is deliberate: wp_oembed_add_provider()
writes into the oEmbed singleton, and calling it before WordPress has built that is how a
provider goes quietly missing.
Pitfalls
Cached results do not change when the provider does. The HTML is cached twice: as
_oembed_<hash> post meta on the post containing the URL, and in a transient for the HTTP
response. Edit a provider and the old embed keeps rendering, which looks like the change did
nothing. The generated file includes a one-shot clearing helper for that reason.
The regex flag is all or nothing. With it off, only * is special. With it on, your
string is a full pattern and needs delimiters: #https?://video\.example\.com/(\d+)#i. A
pattern that works in a regex tester and fails here is usually missing them.
The endpoint is the API, not the page. WordPress appends ?url= and &format=json
itself. Passing a page URL produces HTML where JSON is expected, and the embed falls back to
a plain link.
A failed embed is silent on the front end. You get a link instead of an embed. To see
why, check the response from the endpoint by hand and look for a WP_Error from
wp_remote_get, since a provider on a host that blocks outbound requests will never work.
The returned HTML goes through wp_kses for users without unfiltered_html, which
strips iframes for ordinary editors. That is a sensible default. The option to skip it is in
the generated file and it means trusting the provider with your page: an XSS at their end
becomes one at yours.
Some services need an access token. oEmbed endpoints for Facebook and Instagram have required one since 2020, which is why those embeds broke in core and now need an app. A provider entry alone will not bring them back.
A provider is site-wide. Adding one in a theme means the embeds stop working when the theme changes. A small plugin is the right home.
Compatibility
Everything runs in the browser: nothing is uploaded and nothing is stored.
wp_oembed_add_provider() has been in WordPress since 2.9 and is unchanged. The init
hook is the conventional place; anything before plugins_loaded is too early.
The generated cache-clearing helper deletes _oembed_% post meta with one query and records
a version in an option so it runs once. That is deliberately blunt: there is no supported API
for clearing oEmbed caches selectively, and the alternative is waiting for the transient to
expire, which is a day by default and can be much longer for the post meta.
Every generated file was checked with php -l, including inputs containing quotes, */ and
?>, since a snippet that does not parse is worse than no snippet. Names go through a
comment filter, keys through an identifier filter, and values through single-quoted string
escaping.
Discovery, the <link rel="alternate"> route, is controlled by the oembed_discovery and
related filters. If embeds work on one site and not another with the same code, that is the
first thing to check.
Frequently asked questions
Do I need a provider at all?
Where do I find a service’s endpoint?
/oembed or /v1/oembed.Why does my embed work in the editor and not on the front end?
wp_kses stripping the iframe for a non-administrator
author. Clear the post meta and test as the role that will actually be viewing.Can I change how an embed is rendered?
oembed_dataparse gives you the response and the HTML before it is returned, and
embed_oembed_html wraps the final markup. Wrapping for a responsive aspect ratio is the
common case.