Collect Local oEmbed
A simple gutenberg block that lets you pick local posts (with a search) and show them like a list of embeds
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/gin0115/collect-local-oembed/releases/download/0.1.0/Collect-Local-oEmbed.zipReadme
Collect Local oEmbed
A Gutenberg block that lets editors pick local posts (any public post type, including WooCommerce products) and renders them on the frontend as cards that look and behave like a native WordPress oEmbed iframe — without the iframe.
For users
What it does
Add the Collect Local oEmbed block to a page or post. Open the block's picker, search and filter for posts you want to feature, and click to add them. Each selected post renders as a card matching WordPress's native embed look:
- Title, excerpt, featured image (positioned automatically based on the image's aspect ratio — square images float left of the excerpt, very wide images render full-width above the title)
- Site icon + site name footer (with the WordPress "W" logo fallback when no site icon is configured)
- Comments link (for non-product posts)
- Share button that opens a dialog with the post's URL + an HTML embed snippet, just like the native embed
WooCommerce support
If a selected post is a WooCommerce product, the card automatically shows:
- The product price (from
get_price_html()) - "Buy now" and "Read more" buttons (rendered via WC's own
WC_Embed::product_buttons(), so styling matches WC's native embed)
The comments link is hidden on product cards — same as WC's native product embeds do.
Installation
- Upload the plugin folder to
wp-content/plugins/, or install via the WordPress admin. - Activate it.
- In any post or page, add the Collect Local oEmbed block.
Requirements
- WordPress 6.2 or higher
- PHP 7.4 or higher
WooCommerce integration is automatic if WooCommerce is active — no extra configuration required.
For developers
Architecture overview
The plugin is intentionally small and follows one principle: piggyback on core WordPress's own embed template machinery wherever possible so the rendered output stays consistent with native embeds as core evolves.
Rendering
CLOE_Render::render() loops the selected post IDs, sets $GLOBALS['post']
- calls
setup_postdata()per card, and uses the same action / function calls as core'swp-includes/theme-compat/embed-content.php:
the_post_thumbnail()
the_title()
the_excerpt_embed()
do_action( 'embed_content' ) ← WC and other plugins hook here
the_embed_site_title()
do_action( 'embed_content_meta' ) ← comments + share buttons
print_embed_sharing_dialog()
The wrapper element is <li class="cloe-card wp-embed"> with post_class()
extras, so the cards form a valid <ul>.
Thumbnail shape
The featured image shape detection mirrors core exactly: loop every
registered thumbnail size, pick the size with the widest aspect ratio,
classify rectangular if ≥ 1.75 else square. Rectangular images render
above the heading; square images render after the heading and float left
of the excerpt.
Product cards (WooCommerce)
WC_Embed::init() gates every product-embed hook behind
is_embedded_product(), which calls is_embed() && is_product().
is_embed() is only true inside a real oEmbed iframe — so all of WC's hooks
return early on our inline render. The plugin works around this by:
- Outputting the price markup directly (
<p><span class="wc-embed-price"> {price_html}</span></p>) before the excerpt - Calling
WC_Embed::product_buttons()directly after the excerpt (this method is public static with nois_embedded_productgate, so it works outside the iframe) - Removing
print_embed_comments_buttonfromembed_content_metafor the product card's iteration, then re-adding it (carefully — see notes on preserving priority bucket order in the render file)
Editor preview
src/edit.js renders a React preview that mirrors the frontend markup
exactly. It uses the same wp-embed class names, so the same CSS rules apply
to the preview, and the editor stores the full payload from
CLOE_Render::build_post_payload() on the block attributes for each picked
post.
The block.json editorStyle is an array including both the editor-specific
styles and the frontend style-index.css, so the editor canvas inherits the
full embed styling.
CSS
src/style.scss is a port of wp-includes/css/wp-embed-template.css,
scoped under the block selector so the original's top-level body { … }
rules don't leak onto the host page. SVG dashicon backgrounds are included
inline so the plugin doesn't depend on the dashicons font being loaded.
Two deliberate deviations from the faithful port (clearly marked in the
file): a margin: 0 override on the share button. When core's
wp-embed-template.css is updated upstream, re-port the source section but
keep the deviations block.
Share dialog interaction
src/view.js is a small vanilla JS file (no dependencies). Core's
wp-embed-template.js is hardcoded for the iframe context (single dialog
per page, postMessage to parent), so it can't be reused for our multi-card
inline render. Instead, view.js uses delegated click/keydown handlers
scoped to each card's .wp-embed ancestor — opening, closing, tab
switching, escape-to-close, and arrow-key tab navigation all work
independently per card.
REST endpoint
CLOE_REST exposes a single route:
GET /wp-json/collect-local-oembed/v1/search
Used by the editor picker. Params:
| Param | Type | Notes |
|---|---|---|
search |
string | Free-text search |
post_type |
array | Post type slugs to include |
tax_filters |
string | JSON-encoded object: {tax_slug: [term_ids]} |
per_page |
int | 1–50, default 20 |
page |
int | Page index (1-based) |
context |
string | picker (minimal payload) or view (full payload) |
include |
array | Post IDs to fetch — used to grab full payload by ID |
Responses include the standard X-WP-Total and X-WP-TotalPages headers so
the picker can drive the "Load more" UI.
Permission requires current_user_can('edit_posts') AND a valid X-WP-Nonce
header. The nonce check is also done implicitly by WP REST for
cookie-authenticated requests; the explicit check is defence in depth.
File layout
collect-local-oembed.php Plugin bootstrap, registers the block
includes/class-cloe-render.php Server-side render + canonical payload
includes/class-cloe-rest.php Editor picker search endpoint
src/block.json Block metadata (compiled to build/)
src/edit.js Editor (picker UI + live preview)
src/index.js Block registration entry
src/view.js Frontend share dialog JS
src/style.scss Scoped port of core's embed CSS + WC
src/editor.scss Editor-specific overrides (minimal)
HANDOVER.md Detailed implementation notes
Build
The plugin uses @wordpress/scripts for the build pipeline:
npm install
npm run build # production build
npm run start # watch mode
npm run plugin-zip # bundle for distribution
The compiled output lives in build/ and register_block_type() reads
build/block.json. The build/ directory is gitignored — regenerate from
src/ after pulling.
Testing the picker pagination
PICKER_PER_PAGE at the top of src/edit.js controls the
picker's page size. Setting it to a low number (e.g. 2) makes the "Load
more" flow easy to verify end-to-end against any size dataset.
Re-porting from core
When WP core releases a new version that touches the embed template, follow
the checklist in HANDOVER.md (Re-port checklist when core updates) to refresh the scoped CSS port + the image shape detection.
Known gaps / future work
- The editor preview does not show product prices (no JavaScript equivalent
of
get_price_html()). The "Buy now" / "Read more" buttons do appear, so product cards still read as products. - The editor preview's comment count is always rendered as
0— fetching the real count would require a separate REST call per card. - The transient cache infrastructure mentioned in earlier versions has been removed; live rendering uses object cache + page cache. If you need to serve cards from a stale cache, wrap the render output yourself.
License
GPL-2.0-or-later.
Read the full README on GitHub →