estebanforge / hypermedia-api-wordpress
Add an API endpoint to WordPress to support Hypermedia libraries like HTMX, Alpine Ajax, and Datastar.
Installs: 8
Dependents: 0
Suggesters: 0
Security: 0
Stars: 80
Watchers: 9
Forks: 8
Open Issues: 0
Language:JavaScript
Type:wordpress-plugin
Requires
- php: >=8.1
- jeffreyvanrossum/wp-settings: ^1.2
- starfederation/datastar-php: ^1.0
README
An unofficial WordPress plugin that enables the use of HTMX, Alpine AJAX, Datastar and other hypermedia libraries on your WordPress site, theme, and/or plugins. Intended for software developers.
Adds a new endpoint /wp-html/v1/
from which you can load any hypermedia template.
Hypermedia what?
Hypermedia is a "new" concept that allows you to build modern web applications, even SPAs, without the need to write a single line of JavaScript. A forgotten concept that was popular in the 90s and early 2000s, but has been forgotten by newer generations of software developers.
HTMX, Alpine Ajax and Datastar are JavaScript libraries that allows you to access AJAX, WebSockets, and Server-Sent Events directly in HTML using attributes, without writing any JavaScript.
Unless you're trying to build a Google Docs clone or a competitor, Hypermedia allows you to build modern web applications, even SPAs, without the need to write a single line of JavaScript.
For a better explanation and demos, check the following video:
Why mix it with WordPress?
Because I share the same sentiment as Carson Gross, the creator of HTMX, that the software stack used to build the web today has become too complex without good reason (most of the time). And, just like him, I also want to see the world burn.
(Seriously) Because Hypermedia is awesome, and WordPress is awesome (sometimes). So, why not?
I'm using this in production for a few projects, and it's working great, stable, and ready to use. So, I decided to share it with the world.
I took this idea out of the tangled mess it was inside a project and made it into a standalone plugin that should work for everyone.
It might have some bugs, but the idea is to open it up and improve it over time.
So, if you find any bugs, please report them.
Installation
Install it directly from the WordPress.org plugin repository. On the plugins install page, search for: Hypermedia API
Or download the zip from the official plugin repository and install it from your WordPress plugins install page.
Activate the plugin. Configure it to your liking on Settings > Hypermedia API.
Installation via Composer
If you want to use this plugin as a library, you can install it via Composer. This allows you to use hypermedia libraries in your own plugins or themes, without the need to install this plugin.
composer require estebanforge/hypermedia-api-wordpress
This plugin/library will determine which instance of itself is the newer one when WordPress is loading. Then, it will use the newer instance between all competing plugins or themes. This is to avoid conflicts with other plugins or themes that may be using the same library for their Hypermedia implementation.
How to use
After installation, you can use hypermedia templates in any theme.
This plugin will include the active hypermedia library by default, locally from the plugin folder. Libraries like HTMX, Alpine.js, Hyperscript, and Datastar are supported.
The plugin has an opt-in option, not enforced, to include these third-party libraries from a CDN (using the unpkg.com service). You must explicitly enable this option for privacy and security reasons.
Create a hypermedia
folder in your theme's root directory. This plugin includes a demo folder that you can copy to your theme. Don't put your templates inside the demo folder located in the plugin's directory, because it will be deleted when you update the plugin.
Inside your hypermedia
folder, create as many templates as you want. All files must end with .hm.php
.
For example:
hypermedia/live-search.hm.php
hypermedia/related-posts.hm.php
hypermedia/private/author.hm.php
hypermedia/private/author-posts.hm.php
Check the demo template at hypermedia/demo.hm.php
to see how to use it.
Then, in your theme, use your Hypermedia library to GET/POST to the /wp-html/v1/
endpoint corresponding to the template you want to load, without the file extension:
/wp-html/v1/live-search
/wp-html/v1/related-posts
/wp-html/v1/private/author
/wp-html/v1/private/author-posts
Helper Functions
The plugin provides a comprehensive set of helper functions for developers to interact with hypermedia templates and manage responses. All functions are designed to work with HTMX, Alpine Ajax, and Datastar.
URL Generation Functions
hm_get_endpoint_url(string $template_path = ''): string
Generates the full URL for your hypermedia templates. Automatically adds the /wp-html/v1/
prefix and applies proper URL formatting.
// Basic usage echo hm_get_endpoint_url('live-search'); // Output: http://your-site.com/wp-html/v1/live-search // With subdirectories echo hm_get_endpoint_url('admin/user-list'); // Output: http://your-site.com/wp-html/v1/admin/user-list // With namespaced templates (plugin/theme specific) echo hm_get_endpoint_url('my-plugin:dashboard/stats'); // Output: http://your-site.com/wp-html/v1/my-plugin:dashboard/stats
hm_endpoint_url(string $template_path = ''): void
Same as hm_get_endpoint_url()
but echoes the result directly. Useful for template output.
// HTMX usage <div hx-get="<?php hm_endpoint_url('search-results'); ?>"> Loading... </div> // Datastar usage <div data-on-click="@get('<?php hm_endpoint_url('user-profile'); ?>')"> Load Profile </div> // Alpine Ajax usage <div @click="$ajax('<?php hm_endpoint_url('dashboard-stats'); ?>')"> Refresh Stats </div>
Response Management Functions
hm_send_header_response(array $data = [], string $action = null): void
Sends hypermedia-compatible header responses for non-visual actions. Automatically validates nonces and terminates execution. Perfect for "noswap" templates that perform backend actions without returning HTML.
// Success response (works with HTMX/Alpine Ajax) hm_send_header_response([ 'status' => 'success', 'message' => 'User saved successfully', 'user_id' => 123 ], 'save_user'); // Error response hm_send_header_response([ 'status' => 'error', 'message' => 'Invalid email address' ], 'save_user'); // Silent success (no user notification) hm_send_header_response([ 'status' => 'silent-success', 'data' => ['updated_count' => 5] ]); // For Datastar SSE endpoints, use the ds helpers instead: // hypermedia/save-user-sse.hm.php // Get user data from Datastar signals $signals = hm_ds_read_signals(); $user_data = $signals; // Signals contain the form data $result = save_user($user_data); if ($result['success']) { // Update UI with success state hm_ds_patch_elements('<div class="success">User saved!</div>', ['selector' => '#message']); hm_ds_patch_signals(['user_saved' => true, 'user_id' => $result['user_id']]); } else { // Show error message hm_ds_patch_elements('<div class="error">Save failed: ' . $result['error'] . '</div>', ['selector' => '#message']); hm_ds_patch_signals(['user_saved' => false, 'error' => $result['error']]); }
hm_die(string $message = '', bool $display_error = false): void
Terminates template execution with a 200 status code (allowing hypermedia libraries to process the response) and sends error information via headers.
// Die with hidden error message hm_die('Database connection failed'); // Die with visible error message hm_die('Please fill in all required fields', true);
Security & Validation Functions
hm_validate_request(array $hmvals = null, string $action = null): bool
Validates hypermedia requests by checking nonces and optionally validating specific actions. Supports both new (hmapi_nonce
) and legacy (hxwp_nonce
) nonce formats.
Note: This function is designed for traditional HTMX/Alpine Ajax requests. For Datastar SSE endpoints, nonce validation works differently since SSE connections don't follow the same request pattern. Consider alternative security measures for SSE endpoints (user capability checks, rate limiting, etc.).
// Basic nonce validation (works for all hypermedia libraries) if (!hm_validate_request()) { hm_die('Security check failed'); } // Validate specific action if (!hm_validate_request($_REQUEST, 'delete_post')) { hm_die('Invalid action'); } // Validate custom data array $custom_data = ['action' => 'save_settings', '_wpnonce' => $_POST['_wpnonce']]; if (!hm_validate_request($custom_data, 'save_settings')) { hm_die('Validation failed'); } // Datastar SSE endpoint with real-time validation // hypermedia/validate-form.hm.php $signals = hm_ds_read_signals(); $email = $signals['email'] ?? ''; $password = $signals['password'] ?? ''; // Validate email in real-time if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) { hm_ds_patch_elements('<div class="error">Valid email required</div>', ['selector' => '#email-error']); hm_ds_patch_signals(['email_valid' => false]); } else { hm_ds_remove_elements('#email-error'); hm_ds_patch_signals(['email_valid' => true]); } // Validate password strength if (strlen($password) < 8) { hm_ds_patch_elements('<div class="error">Password must be 8+ characters</div>', ['selector' => '#password-error']); hm_ds_patch_signals(['password_valid' => false]); } else { hm_ds_remove_elements('#password-error'); hm_ds_patch_signals(['password_valid' => true]); }
Library Detection Functions
hm_is_library_mode(): bool
Detects whether the plugin is running as a WordPress plugin or as a Composer library. Useful for conditional functionality.
if (hm_is_library_mode()) { // Running as composer library - no admin interface // Configure via filters only add_filter('hmapi/default_options', function($defaults) { $defaults['active_library'] = 'htmx'; return $defaults; }); } else { // Running as WordPress plugin - full functionality available add_action('admin_menu', 'my_admin_menu'); } // Datastar-specific library mode configuration if (hm_is_library_mode()) { // Configure Datastar for production use as library add_filter('hmapi/default_options', function($defaults) { $defaults['active_library'] = 'datastar'; $defaults['load_from_cdn'] = false; // Use local files for reliability $defaults['load_datastar_backend'] = true; // Enable in wp-admin return $defaults; }); // Register custom SSE endpoints for the plugin using this library add_filter('hmapi/register_template_path', function($paths) { $paths['my-plugin'] = plugin_dir_path(__FILE__) . 'datastar-templates/'; return $paths; }); } else { // Plugin mode - users can configure via admin interface // Add custom Datastar functionality only when running as main plugin add_action('wp_enqueue_scripts', function() { if (get_option('hmapi_active_library') === 'datastar') { wp_add_inline_script('datastar', 'console.log("Datastar ready for SSE!");'); } }); }
Datastar Helper Functions
These functions provide direct integration with Datastar's Server-Sent Events (SSE) capabilities for real-time updates.
hm_ds_sse(): ?ServerSentEventGenerator
Gets or creates the ServerSentEventGenerator instance. Returns null
if Datastar SDK is not available.
$sse = hm_ds_sse(); if ($sse) { // SSE is available, proceed with real-time updates $sse->patchElements('<div id="status">Connected</div>'); }
hm_ds_read_signals(): array
Reads signals sent from the Datastar client. Returns an empty array if Datastar SDK is not available.
// Read client signals $signals = hm_ds_read_signals(); $user_input = $signals['search_query'] ?? ''; $page_number = $signals['page'] ?? 1; // Use signals for processing if (!empty($user_input)) { $results = search_posts($user_input, $page_number); hm_ds_patch_elements($results_html, ['selector' => '#results']); }
hm_ds_patch_elements(string $html, array $options = []): void
Patches HTML elements into the DOM via SSE. Supports various patching modes and view transitions.
// Basic element patching hm_ds_patch_elements('<div id="message">Hello World</div>'); // Advanced patching with options hm_ds_patch_elements( '<div class="notification">Task completed</div>', [ 'selector' => '.notifications', 'mode' => 'append', 'useViewTransition' => true ] );
hm_ds_remove_elements(string $selector, array $options = []): void
Removes elements from the DOM via SSE.
// Remove specific element hm_ds_remove_elements('#temp-message'); // Remove with view transition hm_ds_remove_elements('.expired-items', ['useViewTransition' => true]);
hm_ds_patch_signals(mixed $signals, array $options = []): void
Updates Datastar signals on the client side. Accepts JSON string or array.
// Update single signal hm_ds_patch_signals(['user_count' => 42]); // Update multiple signals hm_ds_patch_signals([ 'loading' => false, 'message' => 'Data loaded successfully', 'timestamp' => time() ]); // Only patch if signal doesn't exist hm_ds_patch_signals(['default_theme' => 'dark'], ['onlyIfMissing' => true]);
hm_ds_execute_script(string $script, array $options = []): void
Executes JavaScript code on the client via SSE.
// Simple script execution hm_ds_execute_script('console.log("Server says hello!");'); // Complex client-side operations hm_ds_execute_script(' document.querySelector("#progress").style.width = "100%"; setTimeout(() => { location.reload(); }, 2000); ');
hm_ds_location(string $url): void
Redirects the browser to a new URL via SSE.
// Redirect after processing hm_ds_location('/dashboard'); // Redirect to external URL hm_ds_location('https://example.com/success');
hm_ds_is_rate_limited(array $options = []): bool
Checks if current request is rate limited for Datastar SSE endpoints to prevent abuse and protect server resources. Uses WordPress transients for persistence across requests.
// Basic rate limiting (10 requests per 60 seconds) if (hm_ds_is_rate_limited()) { hm_die(__('Rate limit exceeded', 'api-for-htmx')); } // Custom rate limiting configuration if (hm_ds_is_rate_limited([ 'requests_per_window' => 30, // Allow 30 requests 'time_window_seconds' => 120, // Per 2 minutes 'identifier' => 'search_' . get_current_user_id(), // Custom identifier 'error_message' => __('Search rate limit exceeded. Please wait.', 'api-for-htmx'), 'error_selector' => '#search-errors' ])) { // Rate limit exceeded - SSE error already sent to client return; } // Strict rate limiting without SSE feedback if (hm_ds_is_rate_limited([ 'requests_per_window' => 10, 'time_window_seconds' => 60, 'send_sse_response' => false // Don't send SSE feedback ])) { hm_die(__('Too many requests', 'api-for-htmx')); } // Different rate limits for different actions $action = hm_ds_read_signals()['action'] ?? ''; switch ($action) { case 'search': $rate_config = ['requests_per_window' => 20, 'time_window_seconds' => 60]; break; case 'upload': $rate_config = ['requests_per_window' => 5, 'time_window_seconds' => 300]; break; default: $rate_config = ['requests_per_window' => 30, 'time_window_seconds' => 60]; } if (hm_ds_is_rate_limited($rate_config)) { return; // Rate limited }
Rate Limiting Options:
requests_per_window
(int): Maximum requests allowed per time window. Default: 10time_window_seconds
(int): Time window in seconds. Default: 60identifier
(string): Custom identifier for rate limiting. Default: IP + user IDsend_sse_response
(bool): Send SSE error response when rate limited. Default: trueerror_message
(string): Custom error message. Default: translatable 'Rate limit exceeded...'error_selector
(string): CSS selector for error display. Default: '#rate-limit-error'
Complete SSE Example
Here's a practical example combining multiple Datastar helpers:
// hypermedia/process-upload.hm.php <?php // Apply strict rate limiting for uploads (5 uploads per 5 minutes) if (hm_ds_is_rate_limited([ 'requests_per_window' => 5, 'time_window_seconds' => 300, 'identifier' => 'file_upload_' . get_current_user_id(), 'error_message' => __('Upload rate limit exceeded. You can upload 5 files every 5 minutes.', 'api-for-htmx'), 'error_selector' => '#upload-errors' ])) { return; // Rate limited - error sent via SSE } // Initialize SSE $sse = hm_ds_sse(); if (!$sse) { hm_die('SSE not available'); } // Show progress hm_ds_patch_elements('<div id="status">Processing upload...</div>'); hm_ds_patch_signals(['progress' => 0]); // Simulate file processing for ($i = 1; $i <= 5; $i++) { sleep(1); hm_ds_patch_signals(['progress' => $i * 20]); hm_ds_patch_elements('<div id="status">Processing... ' . ($i * 20) . '%</div>'); } // Complete hm_ds_patch_elements('<div id="status" class="success">Upload complete!</div>'); hm_ds_patch_signals(['progress' => 100, 'completed' => true]); // Redirect after 2 seconds hm_ds_execute_script('setTimeout(() => { window.location.href = "/dashboard"; }, 2000);'); ?>
Complete Datastar Integration Example
Here's a complete frontend-backend example showing how all helper functions work together in a real Datastar application:
Frontend HTML:
<!-- Live search with real-time validation --> <div data-signals-query="" data-signals-results="[]" data-signals-loading="false"> <h3>User Search</h3> <!-- Search input with live validation --> <input type="text" data-bind-query data-on-input="@get('<?php hm_endpoint_url('search-users-validate'); ?>')" placeholder="Search users..." /> <!-- Search button --> <button data-on-click="@get('<?php hm_endpoint_url('search-users'); ?>')" data-bind-disabled="loading" > <span data-show="!loading">Search</span> <span data-show="loading">Searching...</span> </button> <!-- Results container --> <div id="search-results" data-show="results.length > 0"> <!-- Results will be populated via SSE --> </div> <!-- No results message --> <div data-show="results.length === 0 && !loading && query.length > 0"> No users found </div> </div>
Backend Template - Real-time Validation (hypermedia/search-users-validate.hm.php):
<?php // Apply rate limiting if (hm_ds_is_rate_limited()) { return; // Rate limited } // Get search query from signals $signals = hm_ds_read_signals(); $query = trim($signals['query'] ?? ''); // Validate query length if (strlen($query) < 2 && strlen($query) > 0) { hm_ds_patch_elements( '<div class="validation-error">Please enter at least 2 characters</div>', ['selector' => '#search-validation'] ); hm_ds_patch_signals(['query_valid' => false]); } elseif (strlen($query) >= 2) { hm_ds_remove_elements('#search-validation .validation-error'); hm_ds_patch_signals(['query_valid' => true]); // Show search suggestion hm_ds_patch_elements( '<div class="search-hint">Press Enter or click Search to find users</div>', ['selector' => '#search-validation'] ); } ?>
Backend Template - Search Execution (hypermedia/search-users.hm.php):
<?php // Apply rate limiting for search operations if (hm_ds_is_rate_limited([ 'requests_per_window' => 20, 'time_window_seconds' => 60, 'identifier' => 'user_search_' . get_current_user_id(), 'error_message' => __('Search rate limit exceeded. Please wait before searching again.', 'api-for-htmx'), 'error_selector' => '#search-errors' ])) { // Rate limit exceeded - error already sent to client via SSE return; } // Get search parameters $signals = hm_ds_read_signals(); $query = sanitize_text_field($signals['query'] ?? ''); // Set loading state hm_ds_patch_signals(['loading' => true, 'results' => []]); hm_ds_patch_elements('<div class="loading">Searching users...</div>', ['selector' => '#search-results']); // Simulate search delay usleep(500000); // 0.5 seconds // Perform user search (example with WordPress users) $users = get_users([ 'search' => '*' . $query . '*', 'search_columns' => ['user_login', 'user_email', 'display_name'], 'number' => 10 ]); // Build results HTML $results_html = '<div class="user-results">'; $results_data = []; foreach ($users as $user) { $results_data[] = [ 'id' => $user->ID, 'name' => $user->display_name, 'email' => $user->user_email ]; $results_html .= sprintf( '<div class="user-item" data-user-id="%d"> <strong>%s</strong> (%s) <button data-on-click="@get(\'%s\', {user_id: %d})">View Details</button> </div>', $user->ID, esc_html($user->display_name), esc_html($user->user_email), hm_get_endpoint_url('user-details'), $user->ID ); } $results_html .= '</div>'; // Update UI with results if (count($users) > 0) { hm_ds_patch_elements($results_html, ['selector' => '#search-results']); hm_ds_patch_signals([ 'loading' => false, 'results' => $results_data, 'result_count' => count($users) ]); // Show success notification hm_ds_execute_script(" const notification = document.createElement('div'); notification.className = 'notification success'; notification.textContent = 'Found " . count($users) . " users'; document.body.appendChild(notification); setTimeout(() => notification.remove(), 3000); "); } else { hm_ds_patch_elements('<div class="no-results">No users found for \"' . esc_html($query) . '\"</div>', ['selector' => '#search-results']); hm_ds_patch_signals(['loading' => false, 'results' => []]); } ?>
This example demonstrates:
- Frontend: Datastar signals, reactive UI, and SSE endpoint integration
- Backend: Real-time feedback, progressive enhancement, and signal processing
- Helper Usage:
hm_ds_read_signals()
,hm_get_endpoint_url()
, and allhm_ds_*
functions - Security: Input sanitization and validation, plus rate limiting for SSE endpoints
- UX: Loading states, real-time validation, and user feedback
Rate Limiting Integration Example
Here's a complete example showing how to integrate rate limiting with user feedback:
Frontend HTML:
<!-- Rate limit aware interface --> <div data-signals-rate_limited="false" data-signals-requests_remaining="30"> <h3>Real-time Chat</h3> <!-- Rate limit status display --> <div id="rate-limit-status" data-show="rate_limited"> <div class="warning">Rate limit reached. Please wait before sending more messages.</div> </div> <!-- Requests remaining indicator --> <div class="rate-info" data-show="!rate_limited && requests_remaining <= 10"> <small>Requests remaining: <span data-text="requests_remaining"></span></small> </div> <!-- Chat input --> <input type="text" data-bind-message data-on-keyup.enter="@get('<?php hm_endpoint_url('send-message'); ?>')" data-bind-disabled="rate_limited" placeholder="Type your message..." /> <!-- Send button --> <button data-on-click="@get('<?php hm_endpoint_url('send-message'); ?>')" data-bind-disabled="rate_limited" > Send Message </button> <!-- Error display area --> <div id="chat-errors"></div> <!-- Messages area --> <div id="chat-messages"></div> </div>
Backend Template (hypermedia/send-message.hm.php):
<?php // Apply rate limiting for chat messages (10 messages per minute) if (hm_ds_is_rate_limited([ 'requests_per_window' => 10, 'time_window_seconds' => 60, 'identifier' => 'chat_' . get_current_user_id(), 'error_message' => __('Message rate limit exceeded. You can send 10 messages per minute.', 'api-for-htmx'), 'error_selector' => '#chat-errors' ])) { // Rate limit exceeded - user is notified via SSE // The rate limiting helper automatically updates signals and shows error return; } // Get message from signals $signals = hm_ds_read_signals(); $message = trim($signals['message'] ?? ''); // Validate message if (empty($message)) { hm_ds_patch_elements( '<div class="error">' . esc_html__('Message cannot be empty', 'api-for-htmx') . '</div>', ['selector' => '#chat-errors'] ); return; } if (strlen($message) > 500) { hm_ds_patch_elements( '<div class="error">' . esc_html__('Message too long (max 500 characters)', 'api-for-htmx') . '</div>', ['selector' => '#chat-errors'] ); return; } // Clear any errors hm_ds_remove_elements('#chat-errors .error'); // Save message (example) $user = wp_get_current_user(); $chat_message = [ 'user' => $user->display_name, 'message' => esc_html($message), 'timestamp' => current_time('H:i:s') ]; // Add message to chat $message_html = sprintf( '<div class="message"> <strong>%s</strong> <small>%s</small><br> %s </div>', $chat_message['user'], $chat_message['timestamp'], $chat_message['message'] ); hm_ds_patch_elements($message_html, [ 'selector' => '#chat-messages', 'mode' => 'append' ]); // Clear input field hm_ds_patch_signals(['message' => '']); // Show success feedback hm_ds_execute_script(" // Scroll to bottom of chat const chatMessages = document.getElementById('chat-messages'); chatMessages.scrollTop = chatMessages.scrollHeight; // Brief success indicator const input = document.querySelector('[data-bind-message]'); input.style.borderColor = '#28a745'; setTimeout(() => { input.style.borderColor = ''; }, 1000); "); // The rate limiting helper automatically updates the requests_remaining signal // So the frontend will show the updated count automatically ?>
This rate limiting example shows:
- Intuitive Function Naming:
hm_ds_is_rate_limited()
returns true when blocked - Proactive Rate Limiting: Applied before processing the request
- Automatic User Feedback: Rate limit helper sends SSE responses with error messages
- Dynamic UI Updates: Frontend reacts to rate limit signals automatically
- Resource Protection: Prevents abuse of SSE endpoints
- User Experience: Clear feedback about rate limits and remaining requests
Backward Compatibility
For backward compatibility, the following deprecated functions are still available but should be avoided in new development:
hxwp_api_url()
→ Usehm_get_endpoint_url()
insteadhxwp_send_header_response()
→ Usehm_send_header_response()
insteadhxwp_die()
→ Usehm_die()
insteadhxwp_validate_request()
→ Usehm_validate_request()
instead
How to pass data to the template
You can pass data to the template using URL parameters (GET/POST). For example:
/wp-html/v1/live-search?search=hello
/wp-html/v1/related-posts?category_id=5
All of those parameters (with their values) will be available inside the template as an array named: $hmvals
.
No Swap response templates
Hypermedia libraries allow you to use templates that don't return any HTML but perform some processing in the background on your server. These templates can still send a response back (using HTTP headers) if desired. Check Swapping for more info.
For this purpose, and for convenience, you can use the noswap/
folder/endpoint. For example:
/wp-html/v1/noswap/save-user?user_id=5&name=John&last_name=Doe
/wp-html/v1/noswap/delete-user?user_id=5
In this examples, the save-user
and delete-user
templates will not return any HTML, but will do some processing in the background. They will be loaded from the hypermedia/noswap
folder.
hypermedia/noswap/save-user.hm.php
hypermedia/noswap/delete-user.hm.php
You can pass data to these templates in the exact same way as you do with regular templates.
Nothing stops you from using regular templates to do the same thing or using another folder altogether. You can mix and match or organize your templates in any way you want. This is mentioned here just as a convenience feature for those who want to use it.
Choosing a Hypermedia Library
This plugin comes with HTMX, Alpine Ajax and Datastar already integrated and enabled.
You can choose which library to use in the plugin's options page: Settings > Hypermedia API.
In the case of HTMX, you can also enable any of its extensions in the plugin's options page: Settings > Hypermedia API.
Local vs CDN Loading
The plugin includes local copies of all libraries for privacy and offline development. You can choose to load from:
- Local files (default): Libraries are served from your WordPress installation
- CDN: Optional CDN loading from jsdelivr.net. Will always load the latest version of the library.
Datastar Usage
Datastar can be used to implement Server-Sent Events (SSE) to push real-time updates from the server to the client. Here is an example of how to implement a simple SSE endpoint within a hypermedia template:
// In your hypermedia template file, e.g., hypermedia/my-sse-endpoint.hm.php // Apply rate limiting for SSE endpoint if (hm_ds_is_rate_limited()) { return; // Rate limited } // Initialize SSE (headers are sent automatically) $sse = hm_ds_sse(); if (!$sse) { hm_die('SSE not available'); } // Read client signals $signals = hm_ds_read_signals(); $delay = $signals['delay'] ?? 0; $message = 'Hello, world!'; // Stream message character by character for ($i = 0; $i < strlen($message); $i++) { hm_ds_patch_elements('<div id="message">' . substr($message, 0, $i + 1) . '</div>'); // Sleep for the provided delay in milliseconds usleep($delay * 1000); } // Script will automatically exit and send the SSE stream
On the frontend, you can create an HTML structure to consume this SSE endpoint. The following is a minimal example adapted from the official Datastar SDK companion:
<!-- Container for the Datastar component --> <div data-signals-delay="400"> <h1>Datastar SDK Demo</h1> <p>SSE events will be streamed from the backend to the frontend.</p> <div> <label for="delay">Delay in milliseconds</label> <input data-bind-delay id="delay" type="number" step="100" min="0" /> </div> <button data-on-click="@get('<?php echo hm_get_endpoint_url('my-sse-endpoint'); ?>')"> Start </button> </div> <!-- Target element for SSE updates --> <div id="message">Hello, world!</div>
This example demonstrates how to:
- Set initial signal values with
data-signals-delay
. - Bind signals to form inputs with
data-bind-delay
. - Trigger the SSE stream with a button click using
data-on-click
.
The server will receive the delay
signal and use it to control the stream speed, while the #message
div is updated in real-time.
Managing Frontend Libraries
For developers, the plugin includes npm scripts to download the latest versions of all libraries locally:
# Update all libraries npm run update-all # Update specific library npm run update-htmx npm run update-alpinejs npm run update-hyperscript npm run update-datastar npm run update-all
This ensures your local development environment stays in sync with the latest library versions.
Using Hypermedia Libraries in your plugin
You can definitely use hypermedia libraries and this Hypermedia API for WordPress in your plugin. You are not limited to using it only in your theme.
The plugin provides the filter: hmapi/register_template_path
This filter allows you to register a new template path for your plugin or theme. It expects an associative array where keys are your chosen namespaces and values are the absolute paths to your template directories.
For example, if your plugin slug is my-plugin
, you can register a new template path like this:
add_filter( 'hmapi/register_template_path', function( $paths ) { // Ensure YOUR_PLUGIN_PATH is correctly defined, e.g., plugin_dir_path( __FILE__ ) // 'my-plugin' is the namespace. $paths['my-plugin'] = YOUR_PLUGIN_PATH . 'hypermedia/'; return $paths; });
Assuming YOUR_PLUGIN_PATH
is already defined and points to your plugin's root directory, the above code registers the my-plugin
namespace to point to YOUR_PLUGIN_PATH/hypermedia/
.
Then, you can use the new template path in your plugin like this, using a colon :
to separate the namespace from the template file path (which can include subdirectories):
// Loads the template from: YOUR_PLUGIN_PATH/hypermedia/template-name.hm.php echo hm_get_endpoint_url( 'my-plugin:template-name' ); // Loads the template from: YOUR_PLUGIN_PATH/hypermedia/parts/header.hm.php echo hm_get_endpoint_url( 'my-plugin:parts/header' );
This will output the URL for the template from the path associated with the my-plugin
namespace. If the namespace is not registered, or the template file does not exist within that registered path (or is not allowed due to sanitization rules), the request will result in a 404 error. Templates requested with an explicit namespace do not fall back to the theme's default hypermedia
directory.
For templates located directly in your active theme's hypermedia
directory (or its subdirectories), you would call them without a namespace:
// Loads: wp-content/themes/your-theme/hypermedia/live-search.hm.php echo hm_get_endpoint_url( 'live-search' ); // Loads: wp-content/themes/your-theme/hypermedia/subfolder/my-listing.hm.php echo hm_get_endpoint_url( 'subfolder/my-listing' );
Using as a Composer Library (Programmatic Configuration)
If you require this project as a Composer dependency, it will automatically be loaded. The library-load.php
file is registered in composer.json
and ensures that the plugin's bootstrapping logic is safely included only once, even if multiple plugins or themes require it. You do not need to manually require
or include
any file.
Detecting Library Mode
The plugin exposes a helper function hm_is_library_mode()
to detect if it is running as a library (not as an active plugin). This is determined automatically based on whether the plugin is in the active plugins list and whether it is running in the admin area.
When in library mode, the plugin will not register its admin options/settings page in wp-admin.
Programmatic Configuration via Filters
You can configure the plugin programmatically using WordPress filters instead of using the admin interface. This is particularly useful when the plugin is used as a library or when you want to force specific configurations.
All plugin settings can be controlled using the hmapi/default_options
filter. This filter allows you to override any default option value:
add_filter('hmapi/default_options', function($defaults) { // General Settings $defaults['active_library'] = 'htmx'; // 'htmx', 'alpinejs', or 'datastar' $defaults['load_from_cdn'] = false; // `true` to use CDN, `false` for local files // HTMX Core Settings $defaults['load_hyperscript'] = true; $defaults['load_alpinejs_with_htmx'] = false; $defaults['set_htmx_hxboost'] = false; $defaults['load_htmx_backend'] = false; // Alpine Ajax Settings $defaults['load_alpinejs_backend'] = false; // Datastar Settings $defaults['load_datastar_backend'] = false; // HTMX Extensions - Enable by setting to `true` $defaults['load_extension_ajax-header'] = false; $defaults['load_extension_alpine-morph'] = false; $defaults['load_extension_class-tools'] = false; $defaults['load_extension_client-side-templates'] = false; $defaults['load_extension_debug'] = false; $defaults['load_extension_disable-element'] = false; // Note: key is 'disable-element' $defaults['load_extension_event-header'] = false; $defaults['load_extension_head-support'] = false; $defaults['load_extension_include-vals'] = false; $defaults['load_extension_json-enc'] = false; $defaults['load_extension_loading-states'] = false; $defaults['load_extension_method-override'] = false; $defaults['load_extension_morphdom-swap'] = false; $defaults['load_extension_multi-swap'] = false; $defaults['load_extension_path-deps'] = false; $defaults['load_extension_preload'] = false; $defaults['load_extension_remove-me'] = false; $defaults['load_extension_response-targets'] = false; $defaults['load_extension_restored'] = false; $defaults['load_extension_sse'] = false; $defaults['load_extension_ws'] = false; return $defaults; });
Common Configuration Examples
Complete HTMX Setup with Extensions:
add_filter('hmapi/default_options', function($defaults) { $defaults['active_library'] = 'htmx'; $defaults['load_from_cdn'] = false; // Use local files $defaults['load_hyperscript'] = true; $defaults['set_htmx_hxboost'] = true; // Progressive enhancement $defaults['load_htmx_backend'] = true; // Use in admin too // Enable commonly used HTMX extensions $defaults['load_extension_debug'] = true; $defaults['load_extension_loading-states'] = true; $defaults['load_extension_preload'] = true; $defaults['load_extension_sse'] = true; return $defaults; });
Alpine Ajax Setup:
add_filter('hmapi/default_options', function($defaults) { $defaults['active_library'] = 'alpinejs'; $defaults['load_from_cdn'] = true; // Use CDN for latest version $defaults['load_alpinejs_backend'] = true; return $defaults; });
Datastar Configuration:
add_filter('hmapi/default_options', function($defaults) { $defaults['active_library'] = 'datastar'; $defaults['load_from_cdn'] = false; $defaults['load_datastar_backend'] = true; return $defaults; });
Production-Ready Configuration (CDN with specific extensions):
add_filter('hmapi/default_options', function($defaults) { $defaults['active_library'] = 'htmx'; $defaults['load_from_cdn'] = true; // Better performance $defaults['load_hyperscript'] = true; $defaults['set_htmx_hxboost'] = true; // Enable production-useful extensions $defaults['load_extension_loading-states'] = true; $defaults['load_extension_preload'] = true; $defaults['load_extension_response-targets'] = true; return $defaults; });
Register Custom Template Paths
Register custom template paths for your plugin or theme:
add_filter('hmapi/register_template_path', function($paths) { $paths['my-plugin'] = plugin_dir_path(__FILE__) . 'hypermedia/'; $paths['my-theme'] = get_template_directory() . '/custom-hypermedia/'; return $paths; });
Customize Sanitization
Modify the sanitization process for parameters:
// Customize parameter key sanitization add_filter('hmapi/sanitize_param_key', function($sanitized_key, $original_key) { // Custom sanitization logic return $sanitized_key; }, 10, 2); // Customize parameter value sanitization add_filter('hmapi/sanitize_param_value', function($sanitized_value, $original_value) { // Custom sanitization logic for single values return $sanitized_value; }, 10, 2); // Customize array parameter value sanitization add_filter('hmapi/sanitize_param_array_value', function($sanitized_array, $original_array) { // Custom sanitization logic for array values return array_map('esc_html', $sanitized_array); }, 10, 2);
Customize Asset Loading
For developers who need fine-grained control over where JavaScript libraries are loaded from, the plugin provides filters to override asset URLs for all libraries. These filters work in both plugin and library mode, giving you complete flexibility.
Available Asset Filters:
hmapi/assets/htmx_url
- Override HTMX library URLhmapi/assets/htmx_version
- Override HTMX library versionhmapi/assets/hyperscript_url
- Override Hyperscript library URLhmapi/assets/hyperscript_version
- Override Hyperscript library versionhmapi/assets/alpinejs_url
- Override Alpine.js library URLhmapi/assets/alpinejs_version
- Override Alpine.js library versionhmapi/assets/alpine_ajax_url
- Override Alpine Ajax library URLhmapi/assets/alpine_ajax_version
- Override Alpine Ajax library versionhmapi/assets/datastar_url
- Override Datastar library URLhmapi/assets/datastar_version
- Override Datastar library versionhmapi/assets/htmx_extension_url
- Override HTMX extension URLshmapi/assets/htmx_extension_version
- Override HTMX extension versions
Filter Parameters:
Each filter receives the following parameters:
$url
- Current URL (CDN or local)$load_from_cdn
- Whether CDN loading is enabled$asset
- Asset configuration array withlocal_url
andlocal_path
$is_library_mode
- Whether running in library mode
For HTMX extensions, additional parameters:
$ext_slug
- Extension slug (e.g., 'loading-states', 'sse')
Common Use Cases:
Load from Custom CDN:
// Use your own CDN for all libraries add_filter('hmapi/assets/htmx_url', function($url, $load_from_cdn, $asset, $is_library_mode) { return 'https://your-cdn.com/js/htmx@2.0.3.min.js'; }, 10, 4); add_filter('hmapi/assets/datastar_url', function($url, $load_from_cdn, $asset, $is_library_mode) { return 'https://your-cdn.com/js/datastar@1.0.0.min.js'; }, 10, 4);
Custom Local Paths for Library Mode:
// Override asset URLs when running as library with custom vendor structure add_filter('hmapi/assets/htmx_url', function($url, $load_from_cdn, $asset, $is_library_mode) { if ($is_library_mode) { // Load from your custom assets directory return content_url('plugins/my-plugin/assets/htmx/htmx.min.js'); } return $url; }, 10, 4); add_filter('hmapi/assets/datastar_url', function($url, $load_from_cdn, $asset, $is_library_mode) { if ($is_library_mode) { return content_url('plugins/my-plugin/assets/datastar/datastar.min.js'); } return $url; }, 10, 4);
Version-Specific Loading:
// Force specific versions for compatibility add_filter('hmapi/assets/alpinejs_url', function($url, $load_from_cdn, $asset, $is_library_mode) { return 'https://cdn.jsdelivr.net/npm/alpinejs@3.13.0/dist/cdn.min.js'; }, 10, 4); add_filter('hmapi/assets/alpinejs_version', function($version, $load_from_cdn, $asset, $is_library_mode) { return '3.13.0'; }, 10, 4);
Conditional Loading Based on Environment:
// Different sources for different environments add_filter('hmapi/assets/datastar_url', function($url, $load_from_cdn, $asset, $is_library_mode) { if (wp_get_environment_type() === 'production') { return 'https://your-production-cdn.com/datastar.min.js'; } elseif (wp_get_environment_type() === 'staging') { return 'https://staging-cdn.com/datastar.js'; } else { // Development - use local file return $asset['local_url']; } }, 10, 4);
HTMX Extensions from Custom Sources:
// Override specific HTMX extensions add_filter('hmapi/assets/htmx_extension_url', function($url, $ext_slug, $load_from_cdn, $is_library_mode) { // Load SSE extension from custom source if ($ext_slug === 'sse') { return 'https://your-custom-cdn.com/htmx-extensions/sse.js'; } // Load all extensions from your CDN return "https://your-cdn.com/htmx-extensions/{$ext_slug}.js"; }, 10, 4);
Library Mode with Custom Vendor Directory Detection:
// Handle non-standard vendor directory structures add_filter('hmapi/assets/htmx_url', function($url, $load_from_cdn, $asset, $is_library_mode) { if ($is_library_mode && empty($url)) { // Custom detection for non-standard paths $plugin_path = plugin_dir_path(__FILE__); if (strpos($plugin_path, '/vendor-custom/') !== false) { $custom_url = str_replace(WP_CONTENT_DIR, WP_CONTENT_URL, $plugin_path); return $custom_url . 'assets/js/libs/htmx.min.js'; } } return $url; }, 10, 4);
Complete Asset Override Example:
// Override all hypermedia library URLs for a custom setup function my_plugin_override_hypermedia_assets() { $base_url = 'https://my-custom-cdn.com/hypermedia/'; // HTMX add_filter('hmapi/assets/htmx_url', function() use ($base_url) { return $base_url . 'htmx@2.0.3.min.js'; }); // Hyperscript add_filter('hmapi/assets/hyperscript_url', function() use ($base_url) { return $base_url . 'hyperscript@0.9.12.min.js'; }); // Alpine.js add_filter('hmapi/assets/alpinejs_url', function() use ($base_url) { return $base_url . 'alpinejs@3.13.0.min.js'; }); // Alpine Ajax add_filter('hmapi/assets/alpine_ajax_url', function() use ($base_url) { return $base_url . 'alpine-ajax@1.3.0.min.js'; }); // Datastar add_filter('hmapi/assets/datastar_url', function() use ($base_url) { return $base_url . 'datastar@1.0.0.min.js'; }); // HTMX Extensions add_filter('hmapi/assets/htmx_extension_url', function($url, $ext_slug) use ($base_url) { return $base_url . "htmx-extensions/{$ext_slug}.js"; }, 10, 2); } // Apply overrides only in library mode add_action('plugins_loaded', function() { if (function_exists('hm_is_library_mode') && hm_is_library_mode()) { my_plugin_override_hypermedia_assets(); } });
These filters provide maximum flexibility for developers who need to:
- Host libraries on their own CDN for performance/security
- Use custom builds or versions
- Handle non-standard vendor directory structures
- Implement environment-specific loading strategies
- Ensure asset availability in complex deployment scenarios
Disable Admin Interface Completely
If you want to configure everything programmatically and hide the admin interface, define the HMAPI_LIBRARY_MODE
constant in your wp-config.php
or a custom plugin file. This will prevent the settings page from being added.
// In wp-config.php or a custom plugin file define('HMAPI_LIBRARY_MODE', true); // You can then configure the plugin using filters as needed add_filter('hmapi/default_options', function($defaults) { // Your configuration here. See above for examples. return $defaults; });
Security
Every call to the wp-html
endpoint will automatically check for a valid nonce. If the nonce is not valid, the call will be rejected.
The nonce itself is auto-generated and added to all Hypermedia requests automatically.
If you are new to Hypermedia, please read the security section of the official documentation. Remember that Hypermedia requires you to validate and sanitize any data you receive from the user. This is something developers used to do all the time, but it seems to have been forgotten by newer generations of software developers.
If you are not familiar with how WordPress recommends handling data sanitization and escaping, please read the official documentation on Sanitizing Data and Escaping Data.
REST Endpoint
The plugin will perform basic sanitization of calls to the new REST endpoint, wp-html
, to avoid security issues like directory traversal attacks. It will also limit access so you can't use it to access any file outside the hypermedia
folder within your own theme.
The parameters and their values passed to the endpoint via GET or POST will be sanitized with sanitize_key()
and sanitize_text_field()
, respectively.
Filters hmapi/sanitize_param_key
and hmapi/sanitize_param_value
are available to modify the sanitization process if needed. For backward compatibility, the old filters hxwp/sanitize_param_key
and hxwp/sanitize_param_value
are still supported but deprecated.
Do your due diligence and ensure you are not returning unsanitized data back to the user or using it in a way that could pose a security issue for your site. Hypermedia requires that you validate and sanitize any data you receive from the user. Don't forget that.
Examples
Check out the showcase/demo theme at EstebanForge/Hypermedia-Theme-WordPress.
Suggestions, Support
Please, open a discussion.
Bugs and Error reporting
Please, open an issue.
FAQ
Changelog
Contributing
You are welcome to contribute to this plugin.
If you have a feature request or a bug report, please open an issue on the GitHub repository.
If you want to contribute with code, please open a pull request.
License
This plugin is licensed under the GPLv2 or later.
You can find the full license text in the license.txt
file.