Step-by-Step Guide to building a custom database optimizer portal block for Gutenberg using REST API custom routes
Setting Up the WordPress REST API for Custom Data Access
To build a dynamic Gutenberg block that interacts with a custom database optimization portal, we first need to expose our optimization data via the WordPress REST API. This involves creating custom endpoints that our block can query. We’ll leverage WordPress’s built-in REST API registration functions to achieve this.
The core of this process lies in the `register_rest_route` function. This function allows us to define new API endpoints, specify the HTTP methods they respond to (GET, POST, etc.), and associate callback functions that will handle the requests and return data. For our database optimizer portal, we’ll likely need endpoints to fetch optimization statistics, retrieve specific optimization tasks, and potentially trigger new optimizations.
Registering a Custom REST API Endpoint
Let’s start by creating a simple endpoint to retrieve a list of available optimization tasks. This code should be placed within your plugin’s main PHP file or an included file.
<?php
/**
* Register custom REST API routes for the optimizer portal.
*/
function register_optimizer_api_routes() {
register_rest_route( 'optimizer/v1', '/tasks', array(
'methods' => 'GET',
'callback' => 'get_optimizer_tasks',
'permission_callback' => function () {
// Basic permission check: ensure user is logged in and has sufficient capabilities.
// In a production environment, you'd want more robust checks.
return current_user_can( 'manage_options' );
},
) );
register_rest_route( 'optimizer/v1', '/task/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => 'get_optimizer_task_details',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
'args' => array(
'id' => array(
'validate_callback' => function( $param, $request, $key ) {
return is_numeric( $param );
}
),
),
) );
// Add more routes here for POST, PUT, DELETE operations as needed.
}
add_action( 'rest_api_init', 'register_optimizer_api_routes' );
/**
* Callback function to retrieve optimizer tasks.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST | WP_Error Response object on success, or WP_Error object on failure.
*/
function get_optimizer_tasks( WP_REST_Request $request ) {
// In a real-world scenario, this would query your custom database tables
// or a more complex data source for optimization tasks.
$tasks = array(
array( 'id' => 1, 'name' => 'Optimize Image Sizes', 'status' => 'completed', 'last_run' => '2023-10-27 10:00:00' ),
array( 'id' => 2, 'name' => 'Clean Up Database Transients', 'status' => 'pending', 'last_run' => null ),
array( 'id' => 3, 'name' => 'Minify CSS/JS', 'status' => 'running', 'last_run' => '2023-10-27 09:30:00' ),
);
return new WP_REST_Response( $tasks, 200 );
}
/**
* Callback function to retrieve details for a specific optimizer task.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST | WP_Error Response object on success, or WP_Error object on failure.
*/
function get_optimizer_task_details( WP_REST_Request $request ) {
$task_id = $request['id'];
// Fetch specific task details based on $task_id.
// This is a placeholder. Replace with actual data retrieval logic.
$task_details = array(
'id' => $task_id,
'name' => "Task {$task_id} Details",
'description' => 'Detailed information about optimization task ' . $task_id . '.',
'progress' => rand(0, 100) . '%', // Example dynamic data
'last_run_log' => 'Log entry 1...\nLog entry 2...',
);
if ( empty( $task_details ) ) {
return new WP_Error( 'optimizer_task_not_found', 'Optimizer task not found', array( 'status' => 404 ) );
}
return new WP_REST_Response( $task_details, 200 );
}
?>
In this snippet:
- We hook into the
rest_api_initaction to register our routes. register_rest_routeis used to define the endpoint/optimizer/v1/tasksfor GET requests. The first argument,'optimizer/v1', is the namespace.- The
callbackparameter points to the PHP function (get_optimizer_tasks) that will execute when this endpoint is hit. - A basic
permission_callbackis included to ensure only users with the ‘manage_options’ capability can access these endpoints. This is crucial for security. - We also define a route for individual task details, using a regex parameter
(?P<id>\d+)to capture the task ID. - The callback functions (
get_optimizer_tasksandget_optimizer_task_details) simulate fetching data. In a production system, these would interact with your custom database tables or other data sources. WP_REST_Responseis used to return data in a structured JSON format, along with an appropriate HTTP status code.WP_Erroris used to signal issues, such as a task not being found.
Developing the Gutenberg Block for the Optimizer Portal
Now that our API endpoints are ready, we can build the Gutenberg block. This block will serve as the user interface within the WordPress editor for interacting with our optimizer portal. We’ll use JavaScript (specifically React, as used by Gutenberg) to fetch data from our custom REST API endpoints and display it dynamically.
Block Registration and Editor Script
First, we need to register our block type and enqueue the necessary JavaScript and CSS files for the editor. This is typically done in your plugin’s main PHP file.
<?php
/**
* Enqueue block editor assets.
*/
function enqueue_optimizer_block_assets() {
// Register the block script.
wp_register_script(
'optimizer-block-editor-script',
plugin_dir_url( __FILE__ ) . 'build/index.js', // Path to your compiled JS
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-api-fetch' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
);
// Register the block style.
wp_register_style(
'optimizer-block-editor-style',
plugin_dir_url( __FILE__ ) . 'build/index.css', // Path to your compiled CSS
array( 'wp-edit-blocks' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
);
// Register the block.
register_block_type( 'optimizer/portal', array(
'editor_script' => 'optimizer-block-editor-script',
'editor_style' => 'optimizer-block-editor-style',
'render_callback' => 'render_optimizer_block_frontend', // For frontend rendering
) );
}
add_action( 'init', 'enqueue_optimizer_block_assets' );
/**
* Server-side rendering callback for the block.
* This is optional if your block is fully dynamic via JS, but good for SEO and initial load.
*
* @param array $attributes Block attributes.
* @return string HTML output.
*/
function render_optimizer_block_frontend( $attributes ) {
// This function would typically fetch data server-side or render a static placeholder.
// For a fully dynamic block, it might just return an empty div or a loading indicator.
return '<div class="optimizer-portal-frontend">Loading Optimizer Data...</div>';
}
?>
Explanation:
wp_register_scriptandwp_register_styleare used to enqueue our block’s assets. The dependencies includewp-api-fetch, which is essential for making requests to the WordPress REST API from the client-side JavaScript.register_block_typeregisters our block with the name'optimizer/portal'.editor_scriptandeditor_stylepoint to our registered assets for the block editor.render_callbackis defined for server-side rendering. While our block will be largely dynamic via JavaScript, a server-side render can provide initial content or be used for static blocks.
Client-Side JavaScript (React) for the Block
This is where the magic happens. We’ll use React components to build the block’s UI in the editor. The wp.apiFetch utility will be used to query our custom REST API endpoints.
Assuming you have a build process set up (e.g., using `@wordpress/scripts` or a custom Webpack configuration) that compiles your React/JSX code into build/index.js and build/index.css.
// src/index.js (or your main block JS file)
const { registerBlockType } = wp.blocks;
const { Component, Fragment } = wp.element;
const { InspectorControls, RichText } = wp.editor;
const { PanelBody, Button, TextControl, Spinner, Table, TableRow, TableCell, TableHead, TableBody } = wp.components;
const apiFetch = wp.apiFetch;
// Define the block attributes
const blockAttributes = {
title: {
type: 'string',
default: 'Database Optimizer Status',
},
};
class OptimizerPortalBlock extends Component {
constructor(props) {
super(props);
this.state = {
tasks: [],
loading: true,
error: null,
selectedTaskId: null,
taskDetails: null,
detailsLoading: false,
};
}
componentDidMount() {
this.fetchTasks();
}
fetchTasks() {
this.setState({ loading: true, error: null });
apiFetch({ path: '/optimizer/v1/tasks' })
.then(tasks => {
this.setState({ tasks, loading: false });
})
.catch(error => {
console.error('Error fetching tasks:', error);
this.setState({ error: error.message || 'Failed to load tasks.', loading: false });
});
}
fetchTaskDetails(taskId) {
this.setState({ selectedTaskId: taskId, taskDetails: null, detailsLoading: true, error: null });
apiFetch({ path: `/optimizer/v1/task/${taskId}` })
.then(details => {
this.setState({ taskDetails: details, detailsLoading: false });
})
.catch(error => {
console.error(`Error fetching task ${taskId} details:`, error);
this.setState({ error: error.message || `Failed to load details for task ${taskId}.`, detailsLoading: false });
});
}
handleTaskClick(taskId) {
if (this.state.selectedTaskId === taskId) {
// Collapse if already selected
this.setState({ selectedTaskId: null, taskDetails: null });
} else {
this.fetchTaskDetails(taskId);
}
}
render() {
const { attributes, setAttributes } = this.props;
const { title } = attributes;
const { tasks, loading, error, selectedTaskId, taskDetails, detailsLoading } = this.state;
return (
<Fragment>
<InspectorControls>
<PanelBody title="Block Settings">
<TextControl
label="Block Title"
value={ title }
onChange={ ( newTitle ) => setAttributes( { title: newTitle } ) }
/>
<Button isPrimary onClick={ () => this.fetchTasks() }>
Refresh Tasks
</Button>
</PanelBody>
</InspectorControls>
<div className="optimizer-portal-block">
<RichText
tagName="h2"
value={ title }
onChange={ ( newTitle ) => setAttributes( { title: newTitle } ) }
placeholder="Enter block title..."
/>
{loading && <Spinner />}
{error && <p style={{ color: 'red' }}>{error}</p>}
{!loading && !error && tasks.length === 0 && (
<p>No optimization tasks found.</p>
)}
{!loading && !error && tasks.length > 0 && (
<Table>
<TableHead>
<TableRow>
<TableCell>Task Name</TableCell>
<TableCell>Status</TableCell>
<TableCell>Last Run</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tasks.map(task => (
<Fragment key={ task.id }>
<TableRow
className={ `task-row ${selectedTaskId === task.id ? 'selected' : ''}` }
onClick={ () => this.handleTaskClick(task.id) }
style={{ cursor: 'pointer' }}
>
<TableCell>{task.name}</TableCell>
<TableCell>{task.status}</TableCell>
<TableCell>{task.last_run || 'Never'}</TableCell>
</TableRow>
{selectedTaskId === task.id && (
<TableRow>
<TableCell colSpan="3">
{detailsLoading && <Spinner />}
{taskDetails && !detailsLoading && (
<div className="task-details">
<h4>Details for {task.name}</h4>
<p><strong>Progress:</strong> {taskDetails.progress}</p>
<pre>{taskDetails.last_run_log}</pre>
</div>
)}
{!taskDetails && !detailsLoading && !error && (
<p>Click a task to view details.</p>
)}
</TableCell>
</TableRow>
)}
</Fragment>
))}
</TableBody>
</Table>
)}
</div>
</Fragment>
);
}
}
// Register the block
registerBlockType( 'optimizer/portal', {
title: 'Optimizer Portal',
icon: 'performance', // Choose an appropriate Dashicon
category: 'widgets', // Or 'common', 'layout', etc.
attributes: blockAttributes,
edit: OptimizerPortalBlock,
save: () => {
// The save function should return null for dynamic blocks
// or the static HTML that will be rendered server-side.
// Since we have a render_callback, we can return null here.
return null;
},
} );
Key aspects of the JavaScript code:
- We import necessary components from
@wordpress/blocks,@wordpress/element,@wordpress/editor,@wordpress/components, and@wordpress/api-fetch. - The
OptimizerPortalBlockclass is a React component that extendsComponent. componentDidMountis used to fetch the initial list of tasks when the block is loaded in the editor.apiFetch({ path: '/optimizer/v1/tasks' })makes a GET request to our custom REST API endpoint.- The state manages the
tasks,loadingstatus, and any potentialerror. - We use
InspectorControlsto add settings to the block sidebar, like a title input and a refresh button. - The main block area displays the title (using
RichTextfor inline editing) and a table of tasks. - Clicking a task row triggers
fetchTaskDetailsto load more information for that specific task via the/optimizer/v1/task/:idendpoint. - The
savefunction returnsnullbecause this is a dynamic block; its content will be rendered on the frontend by therender_callbackdefined in PHP.
Styling the Block
You’ll also need some CSS for both the editor and the frontend. Create a src/style.scss (or similar) and import it into your build process.
/* src/style.scss */
.optimizer-portal-block {
border: 1px solid #ddd;
padding: 15px;
background-color: #f9f9f9;
border-radius: 4px;
h2 {
margin-top: 0;
font-size: 1.5em;
color: #333;
}
.task-row {
transition: background-color 0.2s ease;
&:hover {
background-color: #eef;
}
&.selected {
background-color: #dde;
font-weight: bold;
}
}
.task-details {
padding: 10px;
background-color: #fff;
border-top: 1px solid #eee;
margin-top: 5px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #555;
}
pre {
white-space: pre-wrap; /* Allows text to wrap */
word-wrap: break-word; /* Breaks long words */
background-color: #f0f0f0;
padding: 8px;
border-radius: 3px;
font-size: 0.9em;
max-height: 150px;
overflow-y: auto;
}
}
}
/* Frontend specific styles if needed */
.optimizer-portal-frontend {
/* Styles for the block on the actual website */
border: 1px solid #ccc;
padding: 20px;
background-color: #f0f0f0;
}
Ensure your build process (e.g., Webpack) is configured to compile this SCSS into build/index.css and that the enqueue_optimizer_block_assets function correctly points to it.
Deployment and Testing
To deploy this solution:
- Place the PHP code in your plugin’s main file or an included file.
- Organize your JavaScript and SCSS files within a
srcdirectory. - Set up a build process (e.g., using
@wordpress/scripts) to compile yoursrcassets into abuilddirectory. Runnpm run buildoryarn build. - Activate your plugin in WordPress.
- Navigate to the WordPress editor (for a post or page) and add the “Optimizer Portal” block.
- Verify that the tasks are loaded and that clicking on them displays details.
- Test the REST API endpoints directly using tools like Postman or by navigating to
your-site.com/wp-json/optimizer/v1/tasksin your browser (you’ll need to be logged in with sufficient permissions).
Advanced Considerations for Production
For a production-ready system, consider the following:
- Robust Error Handling: Implement more detailed error logging and user-friendly error messages on both the client and server sides.
- Security: The current permission callback is basic. Implement granular capabilities and nonce verification for POST/PUT/DELETE requests. Sanitize all input data rigorously.
- Data Source: Replace the placeholder data arrays with actual database queries to your custom tables or optimized data stores. Use WordPress’s DB abstraction layer (
$wpdb) for security and portability. - Performance: For large datasets, implement pagination for the REST API endpoints. Cache API responses where appropriate. Optimize database queries.
- Frontend Rendering: While the block is dynamic via JS, consider if some data could be pre-rendered server-side for better initial load performance and SEO.
- Internationalization (i18n): Use WordPress i18n functions (
__,_e, etc.) for all user-facing strings in both PHP and JavaScript. - Build Process: A robust build process is essential for managing dependencies, transpiling modern JavaScript, and optimizing assets.