WP Manifestindependent plugin directory
manifest / editor / collect-local-oembed

Collect Local oEmbed

A simple gutenberg block that lets you pick local posts (with a search) and show them like a list of embeds

by Glynn Quelch · github.com/gin0115/collect-local-oembed

0stars
1release downloads
0forks

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.zip

Readme

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

  1. Upload the plugin folder to wp-content/plugins/, or install via the WordPress admin.
  2. Activate it.
  3. 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's wp-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 no is_embedded_product gate, so it works outside the iframe)
  • Removing print_embed_comments_button from embed_content_meta for 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 →

Releases

TagPublishedAssetDownloads
0.1.0 May 14, 2026 Collect-Local-oEmbed.zip 0
0.1.0-RC1 May 14, 2026 Collect-Local-oEmbed.zip 1