How to build custom FSE Block Themes extensions utilizing modern Rewrite API custom endpoints schemas
Leveraging the Rewrite API for Custom FSE Block Theme Endpoints
Full Site Editing (FSE) in WordPress has revolutionized theme development, shifting the paradigm towards a block-based, data-driven approach. While FSE empowers users to customize sites visually, developers often need to extend this functionality by integrating custom data sources or performing complex operations. This post details how to build custom REST API endpoints within your FSE block theme, specifically focusing on utilizing the WordPress Rewrite API for clean, user-friendly URLs and defining custom schemas for robust data exchange.
Registering Custom REST API Endpoints
The foundation of integrating custom data or functionality into WordPress lies in registering custom REST API endpoints. For FSE themes, it’s often beneficial to register these endpoints within the theme’s PHP files, typically in the `functions.php` file or a dedicated plugin. We’ll use the `register_rest_route` function, a core WordPress API for this purpose.
Let’s define an endpoint to fetch custom “project” data. This data could originate from a custom post type, an external API, or a database table. For this example, we’ll simulate fetching data.
Basic Endpoint Registration
The `register_rest_route` function requires a namespace, a route, and an array of arguments defining the endpoint’s behavior. The namespace helps prevent conflicts with other plugins or core endpoints.
Example: Fetching a List of Projects
This code snippet registers a route under the namespace `mytheme/v1` for fetching a list of projects.
<?php
/**
* Register custom REST API routes for the theme.
*/
function mytheme_register_custom_routes() {
register_rest_route( 'mytheme/v1', '/projects', array(
'methods' => WP_REST_Server::READABLE, // Equivalent to 'GET'
'callback' => 'mytheme_get_projects',
'permission_callback' => '__return_true', // For simplicity, allow public access. Adjust as needed.
) );
}
add_action( 'rest_api_init', 'mytheme_register_custom_routes' );
/**
* Callback function to fetch project data.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
function mytheme_get_projects( WP_REST_Request $request ) {
// Simulate fetching project data. In a real scenario, this would query a CPT, DB, or external API.
$projects = array(
array(
'id' => 1,
'title' => 'Project Alpha',
'description' => 'This is the first project.',
'status' => 'completed',
),
array(
'id' => 2,
'title' => 'Project Beta',
'description' => 'This is the second project.',
'status' => 'in-progress',
),
);
return new WP_REST_Response( $projects, 200 );
}
?>
With this code in place (e.g., in your theme’s `functions.php`), you can access this endpoint via `your-site.com/wp-json/mytheme/v1/projects`. The `permission_callback` is crucial for security; `__return_true` is used here for demonstration, but you should implement proper checks (e.g., `current_user_can()`).
Defining Custom Schemas
To ensure data consistency and provide clear documentation for your API consumers (like your FSE block theme’s JavaScript), it’s best practice to define JSON Schemas for your custom endpoints. WordPress’s REST API includes schema registration capabilities.
Schema Registration for the Projects Endpoint
We can add a `schema` argument to our `register_rest_route` call. This schema describes the structure and types of the data returned by the endpoint.
<?php
/**
* Register custom REST API routes and their schemas.
*/
function mytheme_register_custom_routes_with_schema() {
// Project Schema Definition
$project_schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'project',
'description' => 'A WordPress project object.',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => esc_html__( 'Unique identifier for the project.', 'mytheme' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'title' => array(
'description' => esc_html__( 'The title of the project.', 'mytheme' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => esc_html__( 'A short description of the project.', 'mytheme' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'status' => array(
'description' => esc_html__( 'The current status of the project.', 'mytheme' ),
'type' => 'string',
'enum' => array( 'pending', 'in-progress', 'completed', 'on-hold', 'cancelled' ),
'context' => array( 'view', 'edit' ),
),
),
);
// Register the /projects endpoint
register_rest_route( 'mytheme/v1', '/projects', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'mytheme_get_projects',
'permission_callback' => '__return_true',
'schema' => array( $project_schema ), // Pass the schema here
) );
// Register the /projects/(?P<id>[\d]+) endpoint for single project
register_rest_route( 'mytheme/v1', '/projects/(?P<id>[\d]+)', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'mytheme_get_project',
'permission_callback' => '__return_true',
'args' => array(
'id' => array(
'description' => esc_html__( 'Unique identifier for the project.', 'mytheme' ),
'type' => 'integer',
'validate_callback' => function( $param, $request, $key ) {
return is_numeric( $param ) && $param > 0;
},
),
),
'schema' => array( $project_schema ), // Re-use the same schema
) );
}
add_action( 'rest_api_init', 'mytheme_register_custom_routes_with_schema' );
/**
* Callback function to fetch a single project.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
function mytheme_get_project( WP_REST_Request $request ) {
$project_id = $request['id'];
// Simulate fetching a single project.
$projects_data = array(
1 => array( 'id' => 1, 'title' => 'Project Alpha', 'description' => 'This is the first project.', 'status' => 'completed' ),
2 => array( 'id' => 2, 'title' => 'Project Beta', 'description' => 'This is the second project.', 'status' => 'in-progress' ),
);
if ( isset( $projects_data[ $project_id ] ) ) {
return new WP_REST_Response( $projects_data[ $project_id ], 200 );
} else {
return new WP_Error( 'rest_not_found', esc_html__( 'Project not found.', 'mytheme' ), array( 'status' => 404 ) );
}
}
// Ensure mytheme_get_projects is defined as before, or update it to use the schema.
// For simplicity, we'll assume it's defined elsewhere or can be adapted.
// If you want to use the schema for the collection endpoint, you'd typically return an array
// where each item conforms to the schema.
function mytheme_get_projects( WP_REST_Request $request ) {
$projects_data = array(
1 => array( 'id' => 1, 'title' => 'Project Alpha', 'description' => 'This is the first project.', 'status' => 'completed' ),
2 => array( 'id' => 2, 'title' => 'Project Beta', 'description' => 'This is the second project.', 'status' => 'in-progress' ),
);
// The schema expects an array of objects, so we return the values.
return new WP_REST_Response( array_values( $projects_data ), 200 );
}
?>
Now, when you visit `your-site.com/wp-json/mytheme/v1/projects` or `your-site.com/wp-json/mytheme/v1/projects/1`, you’ll not only get the data but also the schema definition. This is invaluable for documentation and for tools that can auto-generate API clients or validate responses.
Integrating with the Rewrite API for Custom Endpoints
While `wp-json` URLs are standard, sometimes you need cleaner, more human-readable URLs, especially if these endpoints are meant to be consumed directly by users or integrated into front-end routing. The WordPress Rewrite API allows us to map custom URLs to our existing REST API endpoints.
Adding Rewrite Rules
We can hook into `rewrite_rules_array` to add our custom rules. These rules will tell WordPress how to interpret a URL and which query variables to set, which can then be used by `register_rest_route` if configured correctly, or to trigger a specific template or handler.
Example: Mapping a Clean URL to the Projects Endpoint
Let’s say we want to access our projects list at `/projects/` instead of `/wp-json/mytheme/v1/projects/`. We can achieve this by adding a rewrite rule that maps `/projects/` to a query that WordPress can understand, and then have our REST API endpoint logic pick it up.
<?php
/**
* Add custom rewrite rules for cleaner URLs.
*
* @param array $rules Existing rewrite rules.
* @return array Modified rewrite rules.
*/
function mytheme_add_rewrite_rules( $rules ) {
$new_rules = array(
// Map /projects/ to the REST API endpoint for listing projects.
// We use a query variable that our REST API callback can potentially use,
// or that can be intercepted by WordPress to load specific content.
// For REST API, it's more about ensuring the WP_Rewrite object is flushed
// and the query is correctly parsed.
'projects/?$' => 'index.php?rest_route=/mytheme/v1/projects',
// Map /projects/([0-9]+)/?$ to the REST API endpoint for a single project.
'projects/([0-9]+)/?$' => 'index.php?rest_route=/mytheme/v1/projects/$1',
);
// Merge new rules with existing rules.
return array_merge( $new_rules, $rules );
}
add_filter( 'rewrite_rules_array', 'mytheme_add_rewrite_rules' );
/**
* Add custom query variables.
*
* @param array $vars Existing query variables.
* @return array Modified query variables.
*/
function mytheme_add_query_vars( $vars ) {
$vars[] = 'rest_route'; // Ensure 'rest_route' is a recognized query variable.
// If you were mapping to a custom template, you'd add custom vars here.
// For example: $vars[] = 'mytheme_view';
return $vars;
}
add_filter( 'query_vars', 'mytheme_add_query_vars' );
/**
* Flush rewrite rules on theme activation/deactivation.
* This is crucial for the new rules to take effect.
*/
function mytheme_activate_rewrite_rules() {
mytheme_add_rewrite_rules( array() ); // Call the function to generate rules
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'mytheme_activate_rewrite_rules' ); // If in a plugin
// If in functions.php, you might need to manually flush rules after adding this code.
// A common approach is to add a transient or check a flag to flush only once.
// For theme functions.php, you might add a note for the user to visit Settings -> Permalinks.
/**
* Hook to ensure rewrite rules are flushed when the theme is activated.
* This is typically done in a plugin's activation hook. If this code is in
* functions.php, the user will need to visit Settings -> Permalinks to flush.
*/
// Example for functions.php:
// add_action('after_switch_theme', 'flush_rewrite_rules');
// Note: This flushes ALL rewrite rules, which can be performance-intensive.
// A more targeted approach is preferred for production.
?>
After adding this code, you must flush your WordPress rewrite rules. If this code is in a plugin, the `register_activation_hook` will handle it. If it’s in your theme’s `functions.php`, you’ll need to manually visit the “Settings” -> “Permalinks” page in the WordPress admin to trigger the flush. Once flushed, you should be able to access your projects at `your-site.com/projects/` and individual projects at `your-site.com/projects/1/`.
Consuming Custom Endpoints in FSE Block Themes
With your custom endpoints registered and accessible via clean URLs, you can now consume this data within your FSE block theme’s JavaScript. This typically involves using the `fetch` API or a library like `axios` within your custom blocks or theme JavaScript files.
Example: Fetching Projects in a Custom Block
Imagine you have a custom block that displays a list of projects. You would use JavaScript to fetch the data from your custom endpoint.
// Assuming this is part of your block's edit or save function, or a separate JS module.
// This example uses the Fetch API.
const fetchProjects = async () => {
const apiUrl = '/projects/'; // Use the clean URL mapped by the Rewrite API
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const projects = await response.json();
console.log('Fetched Projects:', projects);
// Now you can use this data to render your block's content.
// For example, update the block's attributes or render dynamic content.
return projects;
} catch (error) {
console.error('Error fetching projects:', error);
return [];
}
};
// Example usage within a React component (common in Gutenberg blocks):
// In your block's edit component:
// useEffect(() => {
// fetchProjects().then(data => {
// // Update block state with fetched data
// setAttributes({ projects: data });
// });
// }, []);
// In your block's save component (for static rendering, or dynamic rendering via PHP):
// If rendering dynamically, the PHP would fetch the data.
// If rendering statically, you'd typically fetch data in the editor and save it.
It’s important to note that for FSE themes, dynamic rendering of blocks is often handled server-side by PHP. If your custom block needs to display dynamic data fetched from your custom endpoint, you would typically create a server-side rendering callback for your block. This callback would then use PHP to fetch the data (e.g., via `wp_remote_get` to your own endpoint or by directly calling your data retrieval functions) and render the block’s HTML.
Server-Side Rendering with Custom Endpoints
For blocks that require dynamic data that should be present on initial page load (essential for SEO and performance), server-side rendering is the way to go. Here’s how a block’s PHP `render_callback` could fetch data from your custom endpoint.
<?php
/**
* Registers the custom 'projects-list' block.
*/
function mytheme_register_projects_list_block() {
register_block_type( 'mytheme/projects-list', array(
'editor_script' => 'mytheme-projects-list-editor-script',
'render_callback' => 'mytheme_render_projects_list_block',
'attributes' => array(
// Attributes to control the block, e.g., number of projects to display.
'count' => array(
'type' => 'number',
'default' => 5,
),
),
) );
}
add_action( 'init', 'mytheme_register_projects_list_block' );
/**
* Server-side rendering callback for the 'projects-list' block.
*
* @param array $attributes Block attributes.
* @return string HTML output for the block.
*/
function mytheme_render_projects_list_block( $attributes ) {
$count = isset( $attributes['count'] ) ? intval( $attributes['count'] ) : 5;
// Fetch data from our custom REST API endpoint.
// We can use wp_remote_get to call our own endpoint.
// Ensure the URL is correct, considering potential site URL changes.
$api_url = home_url( '/projects/' ); // Use the clean URL
$response = wp_remote_get( $api_url );
if ( is_wp_error( $response ) ) {
// Handle error, maybe return an error message or empty string.
return '<p>' . esc_html__( 'Error loading projects.', 'mytheme' ) . '</p>';
}
$projects_data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $projects_data ) ) {
// Handle invalid data.
return '<p>' . esc_html__( 'Invalid project data received.', 'mytheme' ) . '</p>';
}
// Limit the number of projects if specified.
$projects_to_display = array_slice( $projects_data, 0, $count );
if ( empty( $projects_to_display ) ) {
return '<p>' . esc_html__( 'No projects found.', 'mytheme' ) . '</p>';
}
// Build the HTML output.
$output = '<ul class="wp-block-mytheme-projects-list">';
foreach ( $projects_to_display as $project ) {
$output .= '<li>';
$output .= '<h3>' . esc_html( $project['title'] ) . '</h3>';
$output .= '<p>' . esc_html( $project['description'] ) . '</p>';
$output .= '<span>Status: ' . esc_html( $project['status'] ) . '</span>';
$output .= '</li>';
}
$output .= '</ul>';
return $output;
}
?>
This server-side rendering approach ensures that the project data is available immediately when the page loads, benefiting both users and search engines. The use of `wp_remote_get` to call your own custom endpoint is a robust way to decouple data fetching logic and leverage the work already done in registering the endpoint and its schema.
Advanced Considerations and Best Practices
When building custom FSE extensions with custom endpoints, keep these points in mind:
- Security: Always implement proper `permission_callback` functions for your REST API routes. Never expose sensitive data publicly. Use `current_user_can()` or custom capability checks.
- Error Handling: Implement comprehensive error handling in both your PHP callbacks and JavaScript consumers. Return meaningful error codes and messages.
- Data Validation: Use the `args` parameter in `register_rest_route` to validate incoming data for endpoints that accept input (e.g., POST, PUT requests).
- Caching: For performance, consider caching responses from your custom endpoints, especially if the data doesn’t change frequently. WordPress’s Transients API or object caching can be useful here.
- Internationalization: Ensure all user-facing strings in your API responses and block output are translatable using WordPress’s i18n functions (e.g., `esc_html__`, `esc_html_e`).
- REST API Discovery: The registered schema helps with API discovery. Tools like Swagger/OpenAPI can be integrated if you need more formal API documentation.
- Theme vs. Plugin: While registering endpoints in a theme’s `functions.php` is possible, for reusability and maintainability, consider creating a small, dedicated plugin for your custom API endpoints and schemas. This decouples the functionality from the theme itself.
By combining the power of the WordPress REST API, custom schemas, and the Rewrite API, you can build sophisticated, data-driven extensions for your FSE block themes, offering a seamless experience for both developers and end-users.