WP Plugins by AI

AI generated WP plugins (Opus + ChatGPT)

oEmbed JS

A plugin that lets you define your own oEmbeds for WordPress. It also works in page builders like BricksBuilder and Divi.

oEmbed JS

oEmbed JS (v1.2.0) — Documentation

Update 1.3.0: You can now change the order of providers and disable WordPress's built-in oEmbed.

Introduction

oEmbed JS is a WordPress plugin that transforms plain URLs in your content into rich embedded media — videos, audio players, social media posts, images, and more — entirely using client-side JavaScript. Unlike WordPress's built-in oEmbed system, which renders embeds during page generation on the server, oEmbed JS operates in the browser. This makes it particularly well-suited for sites built with page builders like Bricks Builder, Elementor, Divi, and others where you may not have direct control over how post content is rendered by PHP.

The plugin ships with 15 pre-configured providers including YouTube, Vimeo, Spotify, SoundCloud, X (Twitter), TikTok, and more. You can add any oEmbed-compatible provider manually or import from the official oEmbed directory, which contains over 300 providers.


What is oEmbed?

oEmbed is an open standard that allows websites to display embedded representations of a URL. Instead of requiring you to copy and paste complex <iframe> code, oEmbed lets a consuming site (yours) ask a provider site (like YouTube) for the embed code automatically.

The protocol works through a simple request-response cycle:

  1. Discovery: Your site recognizes that a URL belongs to a specific provider (e.g., https://www.youtube.com/watch?v=dQw4w9WgXcQ belongs to YouTube).

  2. Request: Your site sends a request to the provider's oEmbed endpoint — a special API URL — asking "How should I display this URL?" The request looks something like:

    https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ&format=json
    
  3. Response: The provider returns a JSON object containing metadata and embed code:

    {
      "type": "video",
      "title": "Rick Astley - Never Gonna Give You Up",
      "html": "<iframe width=\"480\" height=\"270\" src=\"https://www.youtube.com/embed/dQw4w9WgXcQ\" ...></iframe>",
      "width": 480,
      "height": 270,
      "provider_name": "YouTube"
    }
    
  4. Rendering: Your site takes the html field from the response and inserts it into the page, replacing the original link.

The oEmbed response type field can be one of four values:

Why a Proxy is Needed

Modern browsers enforce a security policy called CORS (Cross-Origin Resource Sharing). When JavaScript running on your site tries to fetch data from a different domain (like youtube.com/oembed), the browser blocks the response unless the remote server explicitly allows it with CORS headers. Many oEmbed providers do not include these headers.

oEmbed JS solves this by routing oEmbed requests through your own WordPress server. The frontend JavaScript sends its request to a proxy endpoint on your site (/wp-json/oembed-js/v1/proxy), which then fetches the data from the provider server-to-server, where CORS restrictions do not apply, and passes the response back to the browser.


How oEmbed JS Works

The plugin operates in two distinct phases:

Phase 1: Configuration (Admin)

When you visit Settings → oEmbed JS in your WordPress admin, the plugin reads its configuration from a JSON file stored in the plugin directory. The admin page is a single-page application that communicates with the WordPress REST API to save settings, reset defaults, and browse the oEmbed directory. No settings are stored in the WordPress database — everything lives in a config.json file.

Phase 2: Embedding (Frontend)

When a visitor loads a page on your site, the frontend script (included via the snippet you paste into your page builder) performs the following sequence:

  1. Fetches configuration from the public REST endpoint (/wp-json/oembed-js/v1/public-config), which returns only the enabled providers and display settings.

  2. Compiles URL patterns from the provider list into regular expressions.

  3. Scans the DOM for <a> (anchor) elements within the CSS selectors you have configured (e.g., .entry-content, .brxe-text).

  4. Matches each link against the compiled provider patterns to determine if it should be embedded.

  5. Replaces matching links with embed wrappers and fetches the oEmbed data through the server-side proxy.

  6. Renders the embed HTML returned by the provider, applying your display settings (responsive wrapping, dimensions, lazy loading, etc.).

  7. Executes embedded scripts if the provider's HTML includes <script> tags (required by Twitter, Instagram, TikTok, and similar services that use JavaScript-based embed rendering).


Installation

  1. Upload the oembedjs_1.2.0 folder to your /wp-content/plugins/ directory, or install through the WordPress plugin installer.
  2. Activate the plugin through the Plugins menu in WordPress.
  3. Navigate to Settings → oEmbed JS to configure the plugin.
  4. Copy the generated snippet and paste it into your page builder's footer scripts section.

Requirements


Getting Started

After activation, the basic setup requires two steps:

  1. Define CSS Selectors: Tell the plugin which parts of your pages contain links that should be converted to embeds. This is done in the "CSS Selectors to Scan" field under Global Display Settings.

  2. Install the Snippet: Copy the snippet from the top of the settings page and paste it into your page builder's footer scripts area.

Once these two steps are complete, any plain link in your content that matches a configured provider will automatically be replaced with a rich embed when visitors view the page.


The Snippet

At the top of the settings page, you will find a Page Builder Snippet box containing a <script> tag. This is the frontend loader that makes embedding work. It looks like this:

<script src="https://yoursite.com/wp-content/plugins/oembedjs_1.2.0/frontend/oembed-js-front.js" data-oembed-api="https://yoursite.com/wp-json/oembed-js/v1/public-config" defer></script>

How the Snippet Works

Where to Place It

Place this snippet in your page builder's footer scripts section. The exact location depends on your builder:

The snippet does not need to be wrapped in additional <script> tags — it is already a complete script tag.

Use the Copy Snippet button to copy the text to your clipboard.


Global Display Settings

These settings control the default appearance and behavior of all embeds. Individual providers can override some of these values.

Maximum Width

Sets the CSS max-width property on embed wrappers. Accepts any valid CSS length value.

This constrains the embed to never exceed the specified width while allowing it to be smaller when the container is narrower.

Maximum Height

Sets the CSS max-height property on embed wrappers. Content exceeding this height will be clipped.

Leave empty to allow embeds to take their natural height.

Fixed Width

When set, applies a CSS width (not max-width) to the embed wrapper. This overrides the Maximum Width setting.

Use this when you want all embeds to be exactly a specific width regardless of their container.

Fixed Height

When set, applies a CSS height (not max-height) to the embed wrapper. This overrides the Maximum Height setting.

Important: When fixed dimensions are set, the responsive aspect-ratio wrapping is disabled because you are explicitly controlling the size.

Wrapper CSS Class

The CSS class applied to the <div> element that wraps each embed.

You can target this class in your theme's CSS to style all embeds globally. For example:

.jsemb {
  margin: 20px auto;
  border-radius: 8px;
  overflow: hidden;
}

CSS Selectors to Scan

A comma-separated list of CSS selectors. The frontend script will only scan for links inside elements matching these selectors.

This is a critical security and performance measure. By restricting scanning to specific containers, the plugin avoids converting links in navigation menus, sidebars, footers, or other areas where you would not want embeds to appear.

If this field is empty, no embeds will be generated. This is by design — the plugin requires you to explicitly define where embeds should appear.

To find the correct selectors for your theme, use your browser's developer tools (right-click an element → Inspect) to identify the class or ID of the container that holds your post or page content.

Loading Strategy

Controls when embed content (particularly iframes) is loaded:

Responsive Embeds

When enabled (default), the plugin wraps iframes in an aspect-ratio-preserving container using the "padding-bottom percentage" technique. This ensures video embeds scale fluidly to fill their container width while maintaining the correct height ratio (typically 16:9).

The technique works as follows:

  1. The plugin reads the width and height attributes from the iframe (or from the oEmbed response data).
  2. It calculates the aspect ratio: (height / width) × 100 to get a percentage.
  3. It creates a wrapper <div> with position: relative, height: 0, and padding-bottom set to that percentage.
  4. The iframe is positioned absolutely within this wrapper to fill it completely.

This approach is widely used across the web for responsive video embeds and works in all modern browsers.

When fixed width or fixed height values are set, responsive wrapping is automatically disabled because explicitly sized embeds should not scale.


Providers

Providers are the services whose URLs the plugin recognizes and converts to embeds. Each provider entry has the following properties:

Name

A human-readable label displayed in the admin interface. This has no effect on functionality.

URL Pattern

A text pattern used to match URLs in your content against this provider. This can be either a regular expression or a plain text substring, controlled by the Regex checkbox.

As a regular expression (Regex checked):

https?://(?:www\.)?youtube\.com/watch\?v=([a-zA-Z0-9_-]+)

This uses standard JavaScript regex syntax. The i (case-insensitive) flag is automatically applied. The pattern is tested against the full href value of each anchor tag.

As a plain pattern (Regex unchecked):

youtube.com/watch

The plugin checks if the URL contains this substring anywhere within it.

Regular expressions are more precise and prevent false matches. All pre-configured providers use regex patterns.

Endpoint URL

The oEmbed API URL for the provider. This is where the plugin sends requests (via the server-side proxy) to retrieve embed data.

For example, YouTube's endpoint is https://www.youtube.com/oembed. When the plugin encounters a matching YouTube URL, it constructs a request like:

https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ&format=json

Regex

A checkbox indicating whether the URL Pattern should be treated as a JavaScript regular expression (true) or a plain substring match (false).

Enabled

A checkbox to quickly enable or disable a provider without deleting it. Disabled providers are excluded from the public configuration served to the frontend and are not checked by the proxy's allowlist validation.

Embed Type

A classification hint for the provider:

This field is informational and used by the admin interface. The frontend rendering logic primarily checks for the presence of html in the oEmbed response, the type field, and the url field for photos, rather than relying on this admin-side classification.

Managing Providers

Remember: After making any changes to providers or settings, you must click Save Settings at the bottom of the page.


The oEmbed Directory Browser

The plugin includes a built-in browser for the official oEmbed provider directory hosted at oembed.com. This directory contains over 300 providers maintained by the oEmbed community.

Accessing the Directory

Click the 📂 Browse oEmbed Directory button in the Providers section. A modal window opens with a searchable list of all available providers.

How It Works

  1. When you first open the directory, the plugin fetches the provider list from https://oembed.com/providers.json through your WordPress server (via the REST endpoint /wp-json/oembed-js/v1/oembed-directory).

  2. The server transforms each entry from the official format into the plugin's internal format. This transformation includes:

    • Converting oEmbed wildcard scheme patterns (which use * as a wildcard) into regular expressions (replacing * with .+).
    • Normalizing endpoint URLs by replacing {format} placeholders with json.
    • Generating stable IDs from provider names.
  3. The transformed data is cached as a WordPress transient for 24 hours, so subsequent opens of the directory do not re-fetch from oembed.com.

  4. The directory list is displayed in the browser. Providers that you have already added (matched by endpoint URL) are visually dimmed and labeled "Already added." By default, already-added providers are hidden; uncheck the "Hide already added" checkbox to show them.

Importing Providers

  1. Search or scroll through the list to find providers you want.
  2. Click on a provider row (or its checkbox) to select it. You can select multiple providers.
  3. Click Import Selected to add the selected providers to your configuration.
  4. The modal closes and you will see the imported providers in your provider list.
  5. Click Save Settings to persist the changes.

Imported providers are pre-configured with the URL patterns and endpoints from the official directory. You may need to adjust patterns or settings for specific use cases.


Per-Provider Display Overrides

Each provider has an "overrides" section that lets you customize display settings for that specific provider, overriding the global values. Any override field left empty falls back to the corresponding global setting.

Available overrides:

This is useful when different content types need different treatment. For example, you might want Twitter embeds to be narrower than YouTube videos:


How the Frontend Script Works

The frontend script at frontend/oembed-js-front.js is the core of the plugin's client-side functionality. Here is a detailed walkthrough of its operation:

1. Self-Discovery

The script finds its own <script> tag in the DOM by querying for script[data-oembed-api]. It reads the data-oembed-api attribute to determine the URL of the configuration endpoint. It also derives the proxy URL by replacing public-config with proxy in the API URL path.

2. Configuration Fetch

The script makes a fetch() request to the public configuration endpoint. This endpoint returns only the data the frontend needs: the display settings and the list of enabled providers. Disabled providers are filtered out on the server side.

3. Pattern Compilation

Each provider's URL pattern is compiled into a JavaScript RegExp object (if it is a regex pattern) or stored as a plain string for substring matching. Invalid regex patterns are caught and logged as warnings without crashing the script. This compilation happens once and the compiled patterns are reused for all URL checks.

4. DOM Scanning

The script queries the DOM using the CSS selectors defined in your settings. Within each matched container, it finds all <a> elements with an href attribute. Each anchor is processed exactly once — a data-oejs-processed attribute is set on each processed link to prevent duplicate handling.

5. URL Matching

For each anchor, the script tests the href against all compiled provider patterns in order. The first match wins, and the corresponding provider configuration is used.

6. Wrapper Creation

A <div> element is created with the configured wrapper class and dimension styles. This wrapper replaces the original <a> element in the DOM. Depending on the loading strategy:

7. oEmbed Fetching

The script requests embed data through the server-side proxy:

/wp-json/oembed-js/v1/proxy?endpoint=<provider_endpoint>&url=<page_url>

Responses are cached in an in-memory JavaScript object keyed by the full proxy URL. If the same URL appears multiple times on a page, only one network request is made.

8. Rendering

Based on the oEmbed response:

After HTML insertion:

9. Script Execution

Some providers (Twitter, Instagram, TikTok) return HTML that includes <script> tags. These scripts are responsible for rendering the embed's visual appearance. Since scripts inserted via innerHTML do not execute automatically in browsers, the plugin explicitly re-creates each <script> element:

The original <script> elements are removed from the embed wrapper.


The Server-Side Proxy

The proxy endpoint at /wp-json/oembed-js/v1/proxy is a critical component that bridges the gap between browser security restrictions and oEmbed providers.

Request Flow

Browser → Your WordPress Server (/wp-json/oembed-js/v1/proxy) → Provider's oEmbed Endpoint → Response flows back

Security: Allowlist Validation

To prevent your server from being used as an open proxy (which could be abused to make arbitrary web requests through your server), the proxy validates every request against your configured providers. Specifically:

  1. The proxy receives endpoint and url parameters.
  2. It reads the current plugin configuration.
  3. It compares the requested endpoint against every enabled provider's endpoint URL (normalized: trailing slashes removed, case-insensitive comparison, query parameters stripped before comparison).
  4. If no match is found, the request is rejected with a 403 Forbidden error.

This means only endpoints that you have explicitly configured and enabled in the plugin can be accessed through the proxy.

Caching

The proxy sets a Cache-Control: public, max-age=3600 response header, instructing browsers and intermediate caches to cache the oEmbed response for one hour. This reduces the number of requests to provider endpoints for frequently viewed pages.

Error Handling

The proxy uses a 15-second timeout for requests to provider endpoints.


Configuration Storage

oEmbed JS stores all configuration in a single file: config.json, located in the plugin's root directory alongside defaults.json.

Why a JSON File Instead of the Database?

This design choice has several implications:

File Permissions

The web server process (typically running as www-data, apache, or nginx) must have write permission to the plugin directory to create and update config.json. If the file cannot be written, save operations will fail with an error message indicating the file path.

How Configuration Flows

  1. Admin page load: PHP reads config.json and passes it to the JavaScript admin interface via wp_localize_script() as the OEJS.config object.

  2. Admin save: The JavaScript gathers all form values, constructs a JSON object, and POSTs it to /wp-json/oembed-js/v1/config. The PHP handler writes the data to config.json and returns the saved configuration for verification.

  3. Frontend load: The frontend script fetches /wp-json/oembed-js/v1/public-config, which reads config.json and returns a filtered version containing only enabled providers.

defaults.json

The defaults.json file is a read-only template containing the factory default configuration. It is used when:

This file should never be edited. It ships with the plugin and is overwritten on updates.


Resetting to Defaults

Clicking Reset to Defaults in the admin interface:

  1. Displays a confirmation dialog.
  2. Sends a POST request to /wp-json/oembed-js/v1/reset.
  3. The server copies the contents of defaults.json over config.json.
  4. The admin page re-populates all fields with the default values.

This action cannot be undone. Any custom providers or settings modifications will be lost.


Uninstallation

When you delete the plugin through the WordPress admin (not just deactivate — actually delete):

  1. WordPress executes the uninstall.php file.
  2. The plugin deletes the oejs_oembed_directory transient from the WordPress database (the cached oEmbed directory data).
  3. The plugin's files, including config.json, are removed by WordPress's standard plugin deletion process.

Since the plugin stores no other data in the WordPress database (no options, no custom tables), uninstallation is clean and complete.

Note: Deactivating the plugin (without deleting) preserves all files including your config.json, so reactivating will restore your settings.


Troubleshooting

Embeds are not appearing

  1. Check CSS Selectors: The most common issue. If the "CSS Selectors to Scan" field is empty, no links will be processed. Open your browser's developer tools, inspect the element containing your links, and note the CSS class of the content container. Enter it in the selectors field.

  2. Verify the snippet is installed: View your page source (Ctrl+U in most browsers) and search for oembed-js-front.js. If it is not present, the snippet has not been correctly added to your page builder.

  3. Check the browser console: Open the developer tools console (F12 → Console tab). The plugin logs informational messages:

    • "oEmbed JS: No CSS selectors configured." — The selectors field is empty.
    • "oEmbed JS: Failed to load config." — The public-config endpoint is unreachable.
    • "oEmbed JS: Invalid regex for ..." — A provider has a malformed URL pattern.
    • "oEmbed JS: Failed to fetch embed for ..." — The oEmbed request failed.
  4. Check that the link is a plain anchor: The plugin only processes <a> elements with an href attribute. If your page builder wraps URLs in other elements or the links are inside iframes, they will not be detected.

  5. Check provider is enabled: In the admin, verify the checkbox next to the provider name is checked.

"Error saving settings" message

The web server cannot write to the config.json file. Check that the plugin directory has appropriate write permissions. On most Linux servers:

chown -R www-data:www-data /path/to/wp-content/plugins/oembedjs_1.2.0/
chmod 755 /path/to/wp-content/plugins/oembedjs_1.2.0/
chmod 644 /path/to/wp-content/plugins/oembedjs_1.2.0/config.json

Instagram embeds require a Facebook App token

Instagram's oEmbed endpoint (graph.facebook.com/v18.0/instagram_oembed) requires an access_token parameter linked to a Facebook App. The pre-configured Instagram provider will not work without this token. You need to create a Facebook App, obtain an access token, and append it to the endpoint URL:

https://graph.facebook.com/v18.0/instagram_oembed?access_token=YOUR_TOKEN

Embeds appear but look broken or unstyled

Some providers (Twitter, Instagram, TikTok) rely on their own JavaScript libraries to render embeds. If your site has a Content Security Policy (CSP) that blocks scripts from third-party domains, these embeds will not render correctly. Check your browser console for CSP violation warnings.

Embeds load slowly


Technical Reference

REST API Endpoints

All endpoints are registered under the namespace oembed-js/v1.

Endpoint Method Auth Required Description
/config POST Yes (manage_options) Save the full plugin configuration
/reset POST Yes (manage_options) Reset configuration to defaults
/public-config GET No Retrieve display settings and enabled providers
/proxy GET No Proxy oEmbed requests to provider endpoints
/oembed-directory GET Yes (manage_options) Fetch and transform the oembed.com directory

File Structure

oembedjs_1.2.0/
├── oembed-js.php          — Main plugin file, PHP logic, REST endpoints
├── defaults.json          — Factory default configuration (read-only)
├── config.json            — Active configuration (created at runtime)
├── providers.json         — Local copy of the oembed.com directory
├── uninstall.php          — Cleanup on plugin deletion
├── admin/
│   ├── admin-page.php     — Admin page HTML template
│   ├── admin.js           — Admin interface JavaScript
│   └── admin.css          — Admin interface styles
└── frontend/
    └── oembed-js-front.js — Frontend embedding script

Configuration Schema

The config.json (and defaults.json) file follows this structure:

{
  "settings": {
    "max_width": "100%",
    "max_height": "",
    "fixed_width": "",
    "fixed_height": "",
    "wrapper_class": "jsemb",
    "css_selectors": ".entry-content, .post-content",
    "loading_strategy": "lazy",
    "responsive": true
  },
  "providers": [
    {
      "id": "youtube",
      "name": "YouTube",
      "url_pattern": "https?://(?:www\\.)?youtube\\.com/watch\\?v=...",
      "endpoint": "https://www.youtube.com/oembed",
      "regex": true,
      "enabled": true,
      "embed_type": "iframe",
      "override": {
        "max_width": "",
        "max_height": "",
        "fixed_width": "",
        "fixed_height": "",
        "wrapper_class": ""
      }
    }
  ]
}

Pre-Configured Providers

The plugin ships with the following 15 providers enabled by default:

Provider Endpoint Embed Type
YouTube youtube.com/oembed iframe
YouTube Shorts youtube.com/oembed iframe
Vimeo vimeo.com/api/oembed.json iframe
Dailymotion dailymotion.com/services/oembed iframe
Spotify open.spotify.com/oembed iframe
SoundCloud soundcloud.com/oembed rich
Mixcloud app.mixcloud.com/oembed/ rich
X (Twitter) publish.twitter.com/oembed rich
Instagram graph.facebook.com/v18.0/instagram_oembed rich
TikTok tiktok.com/oembed rich
CodePen codepen.io/api/oembed rich
Flickr flickr.com/services/oembed/ photo
Reddit reddit.com/oembed rich
SlideShare slideshare.net/api/oembed/2 rich
Giphy giphy.com/services/oembed photo

WordPress Hooks

The plugin uses the following WordPress hooks:

Browser Compatibility

The frontend script uses fetch(), document.querySelectorAll(), Element.closest(), and classList. These are supported in all modern browsers (Chrome, Firefox, Safari, Edge). Internet Explorer is not supported.

oEmbed JS

Skill: oEmbed JS Provider Configuration Assistant

Purpose: When a user wants to add a new oEmbed provider to the oEmbed JS WordPress plugin, help them determine the correct values for each field based on whatever information they provide (a URL, an iframe embed code, a provider website, an API documentation link, or just a service name).

Context: The oEmbed JS plugin allows users to configure custom oEmbed providers. Each provider has the following fields that need to be filled in:

Field Description Expected Format Required
Name Human-readable name of the provider (e.g., "YouTube", "Spotify") Plain text Yes
URL Pattern A regex pattern that matches URLs from this provider that should be converted to embeds Regular expression (without delimiters), case-insensitive matching is applied automatically Yes
Endpoint URL The oEmbed API endpoint URL where the plugin sends requests to retrieve embed data A full HTTPS URL, must return JSON Yes
Regex Whether the URL Pattern field is a regular expression Checkbox (true/false). Almost always true. Yes
Enabled Whether this provider is active Checkbox (true/false) Yes
Embed Type The type of embed this provider returns One of: "iframe" (for video/audio players), "rich" (for HTML embeds like tweets, posts), or "photo" (for images) Yes
Max Width (override) Per-provider maximum width override CSS value (e.g., "640px", "100%") or leave empty to use global setting No
Max Height (override) Per-provider maximum height override CSS value (e.g., "480px") or leave empty to use global setting No
Fixed Width (override) Per-provider fixed width override CSS value or leave empty No
Fixed Height (override) Per-provider fixed height override CSS value or leave empty No
Wrapper Class (override) Per-provider CSS class override for the embed wrapper CSS class name or leave empty to use global setting No

How to derive the values from user input:

  1. If the user provides a URL they want to embed (e.g., "https://www.example.com/video/12345"):

    • Identify the provider/service from the domain.
    • Search for whether this provider has a known oEmbed endpoint. Check oembed.com/providers.json or the provider's documentation.
    • Construct a regex URL pattern that matches all embeddable URLs from this provider. Use capturing groups and character classes as needed. Escape dots with \\. in the regex.
    • If no oEmbed endpoint exists, inform the user that this provider does not support oEmbed and suggest alternatives (e.g., using an iframe embed directly via a custom solution, or checking if the provider offers oEmbed through a third-party service).
  2. If the user provides an iframe embed code (e.g., <iframe src="https://player.example.com/embed/12345">):

    • Extract the iframe src URL to understand the embed URL structure.
    • Work backwards from the embed/player URL to identify the provider.
    • Look up whether the provider has an oEmbed endpoint.
    • Note: The oEmbed endpoint URL is NOT the iframe src. The endpoint is an API URL that accepts a content URL and returns embed HTML (which typically contains the iframe). These are different things.
    • If the provider has an oEmbed API, determine the correct content URL pattern (the URL a user would share, not the embed URL) and the API endpoint.
  3. If the user provides just a service name (e.g., "Vimeo"):

    • Look up the provider in the known oEmbed providers directory.
    • Provide the standard configuration.
  4. If the user provides a provider's API documentation link:

    • Read the documentation to extract the oEmbed endpoint URL and supported URL schemes.
    • Convert the URL schemes to regex patterns.

Converting oEmbed scheme patterns to regex:

Many providers document their supported URLs using wildcard patterns like https://www.example.com/video/*. Convert these to regex:

Choosing the Embed Type:

Response format: Present the recommended configuration in a clear table. Example:

For a user who says: "I want to embed Vimeo videos"

Field Value
Name Vimeo
URL Pattern https?://(?:www\.)?vimeo\.com/(\d+)
Endpoint URL https://vimeo.com/api/oembed.json
Regex ✅ Checked
Enabled ✅ Checked
Embed Type iframe
Max Width (leave empty — uses global setting)
Max Height (leave empty — uses global setting)
Fixed Width (leave empty)
Fixed Height (leave empty)
Wrapper Class (leave empty — uses global setting)

Additional guidance to provide the user:

When oEmbed is not available:

If the provider does not offer an oEmbed endpoint, inform the user clearly and suggest:

  1. Check if a third-party oEmbed proxy service (like Iframely or Embedly) supports the provider.
  2. The provider may support oEmbed discovery — look for <link type="application/json+oembed"> in the HTML head of content pages.
  3. If none of these work, this plugin cannot embed content from that provider, as it relies on the oEmbed protocol.

BricksBuilder Plugins

BricksBuilder Plugins

The Complete Guide to Developing Plugins & Extensions for Bricks Builder

Bricks is a visual, theme‑based site builder for WordPress. Unlike some other builders, Bricks ships with a genuinely developer‑friendly PHP/JS API. You can extend it by registering custom elements, adding controls to existing elements, creating dynamic data tags, and hooking into dozens of action and filter hooks. This guide walks through every major extension point, with working code, best practices, and distribution advice.

Everything below is based on the official Bricks Academy Developer documentation (academy.bricksbuilder.io/developer).


1. How Bricks Extensibility Works (Mental Model)

Before writing code, understand the three "worlds" your code lives in:

Context What runs there Notes
Builder panel (the editing UI) Vue.js + your PHP set_controls() / set_control_groups() definitions Controls are defined in PHP but rendered by Vue. Optionally you can supply a Vue x-template for live preview.
Canvas (live preview inside builder) Your render() output + frontend scripts The iframe that shows the rendered page while editing.
Frontend (the public page) Your render() output + enqueue_scripts() What visitors actually see.

A Bricks element is a PHP class that extends \Bricks\Element. It's conceptually very similar to a WordPress widget: you declare properties, define settings ("controls"), and implement a render() method that echoes HTML.

Your extension code can live in one of two places:

⚠️ Golden rule: Never edit the Bricks parent theme files directly — updates will wipe your changes. Always use a child theme, a code‑snippets plugin, or your own plugin.


2. Prerequisites


3. Two Ways to Ship Your Code

Option A — Bricks Child Theme (quick start)

  1. Download bricks-child.zip from your Bricks account (my.bricksbuilder.io).
  2. Appearance → Themes → Add New → Upload, then activate Bricks Child Theme.
  3. The child theme already includes a demo custom element and a working functions.php to learn from.

The child theme's functions.php is loaded in addition to (not instead of) the parent theme's, right before it.

Option B — Standalone Plugin (recommended for add‑ons)

A plugin is portable across themes/sites and is how commercial Bricks add‑ons (e.g., Bricksforge, Advanced Themer) are shipped. A minimal plugin scaffold:

my-bricks-addon/
├── my-bricks-addon.php          ← main plugin file (header + bootstrap)
├── elements/
│   └── element-test.php         ← one file per custom element
├── assets/
│   ├── js/
│   │   └── test-element.js
│   └── css/
│       └── test-element.css
└── includes/
    └── dynamic-tags.php

Main plugin file (my-bricks-addon.php):

<?php
/**
 * Plugin Name:       My Bricks Add-on
 * Description:       Custom elements and dynamic data for Bricks Builder.
 * Version:           1.0.0
 * Author:            Your Name
 * Requires PHP:      7.4
 * Text Domain:       my-bricks-addon
 */

if ( ! defined( 'ABSPATH' ) ) exit; // No direct access

define( 'MBA_PATH', plugin_dir_path( __FILE__ ) );
define( 'MBA_URL',  plugin_dir_url( __FILE__ ) );
define( 'MBA_VER',  '1.0.0' );

/**
 * Only boot our Bricks integration if the Bricks theme is active.
 */
add_action( 'init', function () {

    // Bail early if Bricks isn't present.
    if ( ! class_exists( '\Bricks\Elements' ) ) {
        return;
    }

    // Register our custom elements.
    $element_files = [
        MBA_PATH . 'elements/element-test.php',
    ];

    foreach ( $element_files as $file ) {
        \Bricks\Elements::register_element( $file );
    }

    // Load other integrations (dynamic tags, etc.).
    require_once MBA_PATH . 'includes/dynamic-tags.php';

}, 11 ); // Priority 11 => after Bricks registers its own elements.

The key API call is identical whether you use a child theme or a plugin:

\Bricks\Elements::register_element( $file, $name = null, $class = null );

register_element() accepts up to 3 arguments:

The class_exists( '\Bricks\Elements' ) guard is what makes a plugin safe: if someone deactivates Bricks or switches themes, your plugin won't fatal‑error.


4. Anatomy of a Custom Element

Custom elements follow a pattern very similar to WordPress widgets: extend \Bricks\Element, fill in properties, define controls, implement render().

4.1 The blank skeleton

Create elements/element-test.php:

<?php
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

class Prefix_Element_Test extends \Bricks\Element {

    // ---- Element properties ----
    public $category     = '';
    public $name         = '';
    public $icon         = '';
    public $css_selector = '';
    public $scripts      = [];
    public $nestable     = false; // true => nestable (@since 1.5)

    // ---- Builder-specific methods ----
    public function get_label() {}
    public function get_keywords() {}
    public function set_control_groups() {}
    public function set_controls() {}

    // ---- Frontend-specific methods ----
    public function enqueue_scripts() {}
    public function render() {}
}

4.2 Element properties reference

Property Required Purpose
$category Lowercase, no spaces. Use a built‑in category (general, media, etc.) or your own. If custom, provide a translatable label via the bricks/builder/i18n filter.
$name Unique identifier, lowercase, no spaces. Always prefix to avoid clashes (e.g. prefix-test). Bricks adds a CSS class .brxe-{name} to your element.
$icon Icon font CSS class shown in the builder panel. Bricks bundles Font Awesome 6, Ionicons 4, and Themify Icons (e.g. ti-bolt-alt, fas fa-anchor).
$css_selector By default, style controls target the element wrapper. Set this to redirect the default CSS selector to a child element.
$nestable false for plain elements; true to allow the element to contain other elements via drag & drop.
$scripts Array of JS function names to run when the element renders on the frontend or updates in the builder. Prefix them (e.g. prefixElementTest).

4.3 Builder methods reference

Method Required Purpose
get_label() Return the localized element name shown in the panel.
get_keywords() Array of search keywords so users can find the element.
set_control_groups() Group your controls into collapsible sections under the Content/Style tabs.
set_controls() Define the element's settings (controls).
enqueue_scripts() Enqueue element‑specific CSS/JS — loaded only on pages using the element (great for performance).
render() Echo the element's HTML.

4.4 Built‑in render helpers

These helper methods (inherited from \Bricks\Element) are the backbone of a clean render():

Helper What it does
set_attribute( $key, $attribute, $value ) Register an HTML attribute for a tag. $key is a unique identifier for that tag ('_root' is special — it holds the element ID/classes). $value can be a string or array.
render_attributes( $key ) Output all attributes previously set for $key.
render_dynamic_data_tag( $tag, $context, $args ) Resolve a single dynamic tag such as {post_title} with the correct post context.
render_dynamic_data( $content ) Resolve any dynamic tags contained inside a string, using the correct post ID for the current render context.

5. A Complete, Working Element (Fully Populated)

Here's the full example from the docs, an "alert box"‑style element with a content field and a type selector:

<?php
if ( ! defined( 'ABSPATH' ) ) exit;

class Prefix_Element_Test extends \Bricks\Element {

    // Element properties
    public $category     = 'general';                // Predefined category
    public $name         = 'prefix-test';            // Always prefix!
    public $icon         = 'ti-bolt-alt';            // Themify icon
    public $css_selector = '.prefix-test-wrapper';   // Default CSS target
    public $scripts      = [ 'prefixElementTest' ];  // JS to run on render

    // Localized label
    public function get_label() {
        return esc_html__( 'Test element', 'bricks' );
    }

    // Search keywords
    public function get_keywords() {
        return [ 'alert', 'notice', 'message', 'box' ];
    }

    // Control groups (sections in the panel)
    public function set_control_groups() {
        $this->control_groups['text'] = [
            'title' => esc_html__( 'Text', 'bricks' ),
            'tab'   => 'content', // "content" or "style"
        ];

        $this->control_groups['settings'] = [
            'title' => esc_html__( 'Settings', 'bricks' ),
            'tab'   => 'content',
        ];
    }

    // Controls (the actual settings)
    public function set_controls() {

        $this->controls['content'] = [
            'tab'     => 'content',
            'group'   => 'text',
            'label'   => esc_html__( 'Content', 'bricks' ),
            'type'    => 'text',
            'default' => esc_html__( 'Content goes here ..', 'bricks' ),
        ];

        $this->controls['type'] = [
            'tab'         => 'content',
            'group'       => 'settings',
            'label'       => esc_html__( 'Type', 'bricks' ),
            'type'        => 'select',
            'options'     => [
                'info'    => esc_html__( 'Info', 'bricks' ),
                'success' => esc_html__( 'Success', 'bricks' ),
                'warning' => esc_html__( 'Warning', 'bricks' ),
                'danger'  => esc_html__( 'Danger', 'bricks' ),
                'muted'   => esc_html__( 'Muted', 'bricks' ),
            ],
            'inline'      => true,
            'clearable'   => false,
            'pasteStyles' => false,
            'default'     => 'info',
        ];
    }

    // Enqueue this element's assets (only on pages that use it)
    public function enqueue_scripts() {
        wp_enqueue_script( 'prefix-test-script' );
    }

    // Render the HTML output
    public function render() {

        $root_classes[] = 'prefix-test-wrapper';

        if ( ! empty( $this->settings['type'] ) ) {
            $root_classes[] = "color-{$this->settings['type']}";
        }

        // Attach classes to the special '_root' tag
        $this->set_attribute( '_root', 'class', $root_classes );

        // '_root' is REQUIRED (contains element ID, classes, etc.)
        echo "<div {$this->render_attributes( '_root' )}>";

            if ( ! empty( $this->settings['content'] ) ) {
                echo "<div>{$this->settings['content']}</div>";
            }

        echo '</div>';
    }
}

Key things to notice


6. Element Controls In Depth

Controls are defined in set_controls(). Each control is an array keyed by a unique control ID.

6.1 Universal control arguments (apply to every control type)

Argument Type Default Description
tab string content content or style.
group string Which control group to display under.
label string Localized label.
type string The control type (see list below).
inline bool false Label and input on the same line.
small bool false Narrow (60px) input.
css array CSS rule(s) auto‑generated from the value. Each rule needs property + selector (and optional important).
default string/array Default value (shape depends on control type).
pasteStyles bool true Set false for content‑producing controls so they're excluded from "Paste Styles".
description string Help text under the control.
required array Conditional display based on another control's value.

6.2 The css argument — auto CSS generation

Many controls can generate CSS for you without touching render(). Example of a color control that writes color onto .content:

$this->controls['testColor'] = [
    'tab'         => 'content',
    'group'       => 'settings',
    'label'       => esc_html__( 'Text color', 'bricks' ),
    'type'        => 'color',
    'inline'      => true,
    'small'       => true,
    'css'         => [
        [
            'property'  => 'color',
            'selector'  => '.content',
            'important' => true, // optional
        ],
    ],
    'default'     => [
        'rgb' => 'rgba(158, 158, 158, .8)',
        'hex' => '#9e9e9e',
    ],
    'pasteStyles' => false,
    'description' => esc_html__( 'Define the content color.', 'bricks' ),
    'required'    => [ 'showText', '!=', '' ],
];

6.3 The required argument — conditional controls

required shows/hides a control based on another control's value:

'required' => ['layout', '=', ['list', 'grid']],
// Show this control only when the "layout" control equals "list" OR "grid".

Comparison operators: =, !=, >, <, >=, <=.

6.4 Available control types

Content‑producing, CSS‑producing, or both:

Control Output
apply, info Panel only
text, textarea, editor, code, link, icon, svg, image, image-gallery, audio, datepicker, repeater, posts Content
color, background, gradient, border, box-shadow, text-shadow, filters, dimensions, direction, typography, align-items, justify-content, text-align, text-decoration, text-transform CSS
number, select, image, checkbox Content and/or CSS/Conditional

Each type has extra type‑specific arguments documented under academy.bricksbuilder.io/developer/controls/.


7. Adding Frontend JavaScript to Your Element

For interactive elements, register scripts referenced by $scripts and enqueue them in enqueue_scripts().

In the element class:

public $scripts = [ 'prefixElementTest' ]; // JS function name

public function enqueue_scripts() {
    wp_enqueue_script( 'prefix-test-script' );
    wp_enqueue_style(  'prefix-test-style'  );
}

Register the assets (in your plugin bootstrap, hooked to wp_enqueue_scripts):

add_action( 'wp_enqueue_scripts', function () {
    wp_register_script(
        'prefix-test-script',
        MBA_URL . 'assets/js/test-element.js',
        [ 'bricks-scripts' ], // depend on Bricks frontend if needed
        MBA_VER,
        true
    );
    wp_register_style(
        'prefix-test-style',
        MBA_URL . 'assets/css/test-element.css',
        [],
        MBA_VER
    );
} );

The JS file — the function named in $scripts is invoked by Bricks per element instance. A common, robust pattern:

// assets/js/test-element.js
function prefixElementTest() {
  document.querySelectorAll('.brxe-prefix-test').forEach((el) => {
    // Initialize each instance of the element here
    el.addEventListener('click', () => el.classList.toggle('is-open'));
  });
}

// Run on frontend load AND when Bricks re-renders in the builder
document.addEventListener('DOMContentLoaded', prefixElementTest);

Builder vs frontend gotcha: When enqueuing CSS that shouldn't leak into the builder UI, guard with bricks_is_builder_main():

add_action( 'wp_enqueue_scripts', function () {
    if ( ! bricks_is_builder_main() ) {
        wp_enqueue_style( 'my-frontend-only', /* ... */ );
    }
} );

8. Nestable Elements (containers that hold other elements)

Since Bricks 1.5, elements can be nestable — meaning users can drag other elements inside them (like the Accordion, Slider, and Tabs elements). This is what you want for cards, sliders, layout wrappers, etc.

8.1 Enable nesting

public $nestable = true;

This tells Bricks to use the nestable render path and enable drag & drop inside the element.

8.2 Define the default inner structure with get_nestable_children()

Return an array of element definitions that populate the element when it's first dropped in:

public function get_nestable_children() {
    return [
        [
            'name'     => 'block',
            'label'    => esc_html__( 'Slide', 'bricks' ) . ' {item_index}',
            'settings' => [
                '_hidden' => [
                    '_cssClasses' => 'hidden-class', // CSS class, hidden from UI
                ],
            ],
            'children' => [
                [
                    'name'     => 'heading',
                    'settings' => [
                        'text' => esc_html__( 'Slide', 'bricks' ) . ' {item_index}',
                    ],
                ],
                [
                    'name'     => 'button',
                    'settings' => [
                        'text'  => esc_html__( 'I am a button', 'bricks' ),
                        'size'  => 'lg',
                        'style' => 'primary',
                    ],
                ],
            ],
        ],
    ];
}

8.3 Render children in PHP

Use the Frontend::render_children() helper, passing $this:

public function render() {
    $output  = "<div {$this->render_attributes( '_root' )}>";
    $output .= \Bricks\Frontend::render_children( $this ); // renders nested items
    $output .= '</div>';
    echo $output;
}

8.4 (Optional) Live builder preview with a Vue x-template

For a live preview inside the builder, output a Vue template that includes <bricks-element-children>:

public function render_builder() { ?>
    <script type="text/x-template" id="tmpl-bricks-element-custom-nestable">
        <component :is="tag">
            <h2>Title before nestable children</h2>
            <bricks-element-children :element="element"/>
            <p>Text node after nestable children</p>
        </component>
    </script>
<?php }

8.5 (Optional) Repeater for same‑level items

If your nestable structure is a list of same‑level items (like accordion items), add a repeater control bound to children:

public function set_controls() {
    $this->controls['_children'] = [
        'type'          => 'repeater',
        'titleProperty' => 'label',
        'items'         => 'children', // @since 1.5
    ];
}

9. Extending Existing Bricks Elements (without replacing them)

You don't always need a brand‑new element. Since Bricks 1.3.2, you can inject controls into any core element with the filter bricks/elements/{element_name}/controls:

add_filter( 'bricks/elements/posts/controls', function ( $controls ) {
    $controls['ignoreStickyPosts'] = [
        'tab'   => 'content',
        'group' => 'query',
        'label' => esc_html__( 'Ignore Sticky Posts', 'my-bricks-addon' ),
        'type'  => 'checkbox',
    ];
    return $controls;
} );

10. Creating Custom Dynamic Data Tags

Dynamic Data tags (like {post_title}) let content update automatically. You can register your own, e.g. {my_dd_tag}.

Step 1 — Register the tag in the builder UI

add_filter( 'bricks/dynamic_tags_list', function ( $tags ) {
    $tags[] = [
        'name'  => '{my_dd_tag}',            // prefix to keep unique
        'label' => 'My Dynamic Data',
        'group' => 'My Dynamic Data Group',
    ];
    return $tags;
} );

Step 2 — Resolve a single tag with bricks/dynamic_data/render_tag

add_filter( 'bricks/dynamic_data/render_tag', 'get_my_tag_value', 20, 3 );

function get_my_tag_value( $tag, $post, $context = 'text' ) {
    if ( ! is_string( $tag ) ) {
        return $tag;
    }

    $clean_tag = str_replace( [ '{', '}' ], '', $tag );

    if ( $clean_tag !== 'my_dd_tag' ) {
        return $tag; // not ours; pass through untouched
    }

    return run_my_dd_tag_logic();
}

function run_my_dd_tag_logic() {
    return 'My dynamic data value';
}

Step 3 — Resolve tags embedded in larger strings

bricks/dynamic_data/render_content and bricks/frontend/render_data fire when a string may contain multiple tags mixed with HTML:

add_filter( 'bricks/dynamic_data/render_content', 'render_my_tag', 20, 3 );
add_filter( 'bricks/frontend/render_data',        'render_my_tag', 20, 2 );

function render_my_tag( $content, $post, $context = 'text' ) {
    if ( strpos( $content, '{my_dd_tag}' ) === false ) {
        return $content;
    }
    $value = run_my_dd_tag_logic();
    return str_replace( '{my_dd_tag}', $value, $content );
}

Tags with arguments (e.g. {my_dd_tag:foo})

Parse arguments after the colon:

add_filter( 'bricks/dynamic_data/render_tag', function ( $tag, $post, $context = 'text' ) {
    if ( ! is_string( $tag ) ) return $tag;

    $clean = str_replace( [ '{', '}' ], '', $tag );
    if ( strpos( $clean, 'my_dd_tag:' ) !== 0 ) return $tag;

    $argument = str_replace( 'my_dd_tag:', '', $clean );
    return run_my_dd_tag_logic( $argument );
}, 20, 3 );

For content strings containing argument‑style tags, use a regex to find and replace each occurrence:

preg_match_all( '/{(my_dd_tag:[^}]+)}/', $content, $matches );
foreach ( $matches[1] as $i => $match ) {
    $full  = $matches[0][ $i ];
    $value = get_my_tag_value( $match, $post, $context );
    $content = str_replace( $full, $value, $content );
}

11. Hooks Reference (Actions & Filters)

Bricks exposes a large hook API. Below are the most useful ones for plugin developers, grouped by purpose.

11.1 Action hooks (structure & lifecycle)

Hook Fires…
bricks_before_site_wrapper / bricks_after_site_wrapper Around the whole site wrapper.
bricks_before_header / bricks_after_header Around the header template.
bricks_before_footer / bricks_after_footer Around the footer template.
bricks_body Just inside <body>.
bricks_meta_tags In the <head> for meta output.
bricks/load_elements/before / bricks/load_elements/after Around element registration — handy for late registration.
bricks/frontend/before_render_data / after_render_data Around content rendering.
bricks/query/before_loop / after_loop Around query‑loop iterations.
bricks/form/custom_action Run custom logic on a Form element submission.
bricks/dynamic_data/tags_registered After all dynamic tags register.

11.2 Filter hooks (behavior & data)

Filter Use it to…
bricks/builder/i18n Provide a translatable label for a custom element category.
bricks/elements/{name}/controls Add controls to an existing element.
bricks/elements/{name}/control_groups Add control groups to an existing element.
bricks/element/settings Modify element settings before render.
bricks/element/render / bricks/frontend/render_element Filter rendered element HTML.
bricks/element/render_attributes Modify HTML attributes.
bricks/dynamic_tags_list Register a custom dynamic data tag.
bricks/dynamic_data/render_tag / render_content Resolve custom tags.
bricks/dynamic_data/register_providers Register a whole dynamic data provider class.
bricks/posts/query_vars / bricks/{type}s/query_vars Modify query‑loop queries.
bricks/query/result / result_count Alter query results.
bricks/code/allow_execution / disable_execution / disallow_keywords Control the Code element's PHP execution (security).
bricks/svg/allowed_tags / allowed_attributes / bypass_sanitization Control SVG sanitization.
bricks/builder/supported_post_types Enable Bricks editing on custom post types.
bricks/conditions/groups / options / result Extend the conditional‑visibility system.
bricks/setup/control_options Add reusable global control option sets.

The full documented list contains 200+ filters and ~35 actions. Browse them at academy.bricksbuilder.io/developer/hooks/.

11.3 Registering a custom element category with a translatable label

If you use a non‑built‑in $category, register its label:

add_filter( 'bricks/builder/i18n', function ( $i18n ) {
    $i18n['myaddon'] = esc_html__( 'My Add-on', 'my-bricks-addon' );
    return $i18n;
} );

Then set public $category = 'myaddon'; on your element.


12. Reading & Generating Bricks Data (Advanced)

Bricks stores page structure as an array of element definitions in post meta. The Schema section of the docs describes this data model. This matters when you:

Each element node generally looks like:

[
    'id'       => 'abc123',          // unique element id
    'name'     => 'heading',         // element type
    'parent'   => 'parentId',        // parent element id (0 for top-level)
    'children' => [ /* child ids */ ],
    'settings' => [ /* control values */ ],
]

Helper functions worth knowing (documented under /developer/functions/):


13. Best Practices, Security & Conventions

Naming & prefixing

Internationalization

Security

Performance

Compatibility & robustness

Don't edit core


14. Testing & Debugging Workflow

  1. Enable debugging in wp-config.php:
    define( 'WP_DEBUG', true );
    define( 'WP_DEBUG_LOG', true );
    define( 'SCRIPT_DEBUG', true ); // load unminified assets
    
  2. Verify registration: open the builder, search your element by label/keywords in the elements panel.
  3. Check both contexts: confirm the element renders correctly in the canvas and on the live frontend (they can differ).
  4. Inspect the wrapper: confirm your root tag has the .brxe-{name} class and unique ID — if not, you likely forgot render_attributes( '_root' ).
  5. Watch the console for JS errors when your $scripts function runs.
  6. Study the source: the Bricks theme's own element files (accordion-nested.php, slider-nested.php, tabs-nested.php, etc.) are the best reference for advanced patterns. The child theme also ships a demo element.

15. Packaging & Distributing a Bricks Add‑on


Quick‑Reference Checklist

Task API / Hook
Register a custom element \Bricks\Elements::register_element( $file, $name, $class ) on init (priority 11)
Base class to extend \Bricks\Element
Required methods get_label(), set_controls(), render()
Required render output render_attributes( '_root' ) on the root tag
Group controls set_control_groups()
Auto‑generate CSS from a control css argument on the control
Conditional control required argument
Make element a container $nestable = true + get_nestable_children() + Frontend::render_children($this)
Add controls to core elements bricks/elements/{name}/controls
Custom dynamic tag bricks/dynamic_tags_list + bricks/dynamic_data/render_tag + render_content
Custom element category label bricks/builder/i18n
Detect builder for asset loading bricks_is_builder_main()
Resolve dynamic data in a string bricks_render_dynamic_data() / $this->render_dynamic_data()

Official Resources

With the element API, the controls system, nestable containers, dynamic data tags, and the extensive hook library, you have everything needed to build a full‑featured, distributable Bricks Builder add‑on. Start from the child‑theme demo element, graduate to a standalone plugin structure, and layer in nestable behavior, dynamic data, and hooks as your add‑on grows.