Updated on 20 Aug 2024
This Pro tutorial provides the steps for users (post authors) to delete their posts from the front end in Bricks.

We shall set the delete buttons to only be output for posts that the current user has authored (published).
While it is not mandatory, we shall use BricksExtras‘ Nestable Table element for quickly generating a dynamic HTML table of posts (can be of those of any post type) using a Bricks’ query loop.
Update 1: Added instructions near the end on how to delete the rows without a page reload.
Update 2: Added instructions near the end on how to make the delete buttons appear and work for post authors AND users that can delete others’ posts (admins and editors by default).
We are going to
1. Create a custom REST API endpoint:
- Register a new route for post deletion
- Implement permission checks
- Handle the deletion process
2. Implement the server-side logic:
- Create a callback function to process the deletion request
- Move the deleted post to the Trash
- Return appropriate responses for success or failure
3. Set up the client-side JavaScript:
- Enqueue a custom JavaScript file
- Localize the script with necessary data (API root URL and nonce)
4. Implement the client-side deletion process:
- Add delete buttons to posts in the query loop
- Attach event listeners to the delete buttons
- Send a DELETE request to the custom API endpoint when a button is clicked
- Handle the API response (success or error)
5. Manage the user interface after deletion:
- Implement a page reload to refresh the content
6. Take care of accessibility:
- For the delete button’s HTML structure
- Add appropriate ARIA attributes
- Ensure keyboard navigability
- Provide text alternatives for icons
7. Security considerations:
- Implement proper permission checks on the server-side
- Use nonces for API requests
- Ensure only post authors can delete their own posts
8. Error handling and user feedback:
- Implement error catching in the JavaScript
- Provide user-friendly error messages
- Confirm deletion action with the user before proceeding
Step 1
Install and activate BricksExtras.
Enable ‘Nestable Element’ element in its settings.
Step 2
Edit your Page/template with Bricks in which you’d like to output the posts table.
Copy the fully-built Section from our dev site via this JSON and paste.

For the delete button a Code element is being used.
PHP & HTML:
<button class="delete-post" data-post-id="8383" aria-label="Delete post">
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24" fill="#5f6368" aria-hidden="true" focusable="false">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</svg>
<span class="sr-only">Delete</span>
</button>
CSS:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
Click ‘Sign code’.
This dynamic data condition has been applied on this Code element:

Step 3
Install and activate the Bricks child theme if it is not already active. You can download the Bricks child theme directly from your Bricks account.
This can be done at any stage, even if your site is already built with the parent Bricks theme, and will not have any negative impact.
Create a directory called assets in the child theme.
Create a directory called js inside assets.
Create a file named say, delete-post.js in the above js directory having:
// Wait for the DOM to be fully loaded before executing the script
document.addEventListener('DOMContentLoaded', () => {
// Select all elements with the class 'delete-post'
const deleteButtons = document.querySelectorAll('.delete-post');
// Attach click event listeners to each delete button
deleteButtons.forEach(button => {
button.addEventListener('click', async e => {
// Prevent the default action of the button (not really needed in this case, but it's good practice)
e.preventDefault();
// Ask for user confirmation before proceeding with deletion
if (!confirm('Are you sure you want to delete this post?')) {
return; // If user cancels, exit the function
}
// Get the post ID from the button's data attribute
const postId = button.dataset.postId;
try {
// Send a DELETE request to the custom REST API endpoint
const response = await fetch(
`${blDeletePost.root}bl/v1/delete-post/${postId}`,
{
method: 'DELETE',
headers: {
'X-WP-Nonce': blDeletePost.nonce, // WordPress nonce for security
'Content-Type': 'application/json',
},
}
);
// Parse the JSON response
const data = await response.json();
if (data.success) {
// If deletion was successful, reload the page to reflect changes
window.location.reload();
} else {
// If deletion failed, log the error and show an alert to the user
console.error('Failed to delete post:', data.message);
alert('Failed to delete post. Please try again.');
}
} catch (error) {
// If an error occurs during the fetch operation, log it and alert the user
console.error('Error:', error);
alert('An error occurred while deleting the post. Please try again.');
}
});
});
});
Step 4
Add this in child theme’s functions.php near the end:
<?php
// Register a custom REST API endpoint for deleting posts
add_action( 'rest_api_init', function () {
register_rest_route(
'bl/v1', // Namespace
'/delete-post/(?P<id>d+)', // Route
[
'methods' => 'DELETE', // This endpoint responds to DELETE requests
'callback' => 'bl_delete_post_callback', // Function to handle the request
'permission_callback' => 'bl_delete_post_permissions_check', // Function to check if user has permission
]
);
} );
/**
* Check if the current user has permission to delete the specified post
*
* @param WP_REST_Request $request The request object
* @return bool True if user has permission, false otherwise
*/
function bl_delete_post_permissions_check( WP_REST_Request $request ): bool {
$post_id = (int) $request['id']; // Get the post ID from the request
$post = get_post( $post_id ); // Fetch the post object
if ( ! $post ) {
return false; // Post doesn't exist, so no permission
}
// Check if user can delete this post AND if they are the post author
return current_user_can( 'delete_post', $post_id ) && get_current_user_id() === (int) $post->post_author;
}
/**
* Handle the post deletion request
*
* @param WP_REST_Request $request The request object
* @return WP_REST_Response The response object
*/
function bl_delete_post_callback( WP_REST_Request $request ): WP_REST_Response {
$post_id = (int) $request['id']; // Get the post ID from the request
$result = wp_delete_post( $post_id ); // Attempt to delete the post (moves to trash by default)
if ( $result && ! is_null( $result ) ) {
// Deletion successful
return new WP_REST_Response( [ 'success' => true, 'message' => 'Post moved to Trash' ], 200 );
} else {
// Deletion failed
return new WP_REST_Response( [ 'success' => false, 'message' => 'Failed to move post to Trash' ], 500 );
}
}
// Enqueue the JavaScript file for post deletion
add_action( 'wp_enqueue_scripts', function (): void {
// If this is not the 'sample-page', don't enqueue the script i.e., abort
if ( ! is_page( 'sample-page' ) ) {
return;
}
wp_enqueue_script(
'bl-delete-post', // Handle
get_stylesheet_directory_uri() . '/assets/js/delete-post.js',
[], // No dependencies
'1.0.0', // Version number
[
'in_footer' => true, // Load in footer
'strategy' => 'defer', // Use defer loading strategy
]
);
// Localize the script with necessary data
wp_localize_script( 'bl-delete-post', 'blDeletePost', [
'root' => esc_url_raw( rest_url() ), // URL for the REST API
'nonce' => wp_create_nonce( 'wp_rest' ), // Nonce for authentication
] );
} );
Inside the wp_enqueue_scripts hook’s handler function we currently have
// If this is not the 'sample-page', don't enqueue the script i.e., abort
if ( ! is_page( 'sample-page' ) ) {
return;
}
You would need to edit the if condition to match where your posts output is. Help reference.
Ex.: if the posts table is on the homepage, you’d change it to
// If this is not the homepage, don't enqueue the script i.e., abort
if ( ! is_front_page() ) {
return;
}
If you are unsure about this, you could ask in the comments section.
That’s it! Test on the front end.
Summary of our implementation:
- Creates a custom REST API endpoint for deleting posts.
- Checks permissions to ensure only the post author can delete their own posts.
- Adds a delete button to posts in the query loop, visible only to the post author.
- Implements JavaScript to handle the delete action via the REST API.
- Removes the deleted post from the DOM upon successful deletion and reloads the page.
Code Explanation
The 'strategy' => 'defer' option tells the browser to download the script while HTML parsing continues, but to only execute it after parsing is complete. This can improve page load performance.
The 2nd argument of register_rest_route() is
'/delete-post/(?P<id>d+)'
This is the route for our custom endpoint. It defines the specific path and any parameters.
/delete-post/: This is the base path for this particular endpoint.(?P<id>d+): This is a regex pattern that defines a named capture group:?: starts the special syntaxP: indicates that this is a Python-style named capture group?P<id>: this names the capture groupid. It will be accessible in your callback function as$request['id'].d+: this is the actual regex pattern, meaning “one or more digits”.
So, this route will match URLs like:
/delete-post/123
/delete-post/456789
But it won’t match:
/delete-post/abc (because it’s not digits)
/delete-post/ (because there are no digits after the slash)
By combining the first two arguments (1st being the namespace), we’re effectively saying:
“Create an endpoint in the ‘bl/v1’ namespace that responds to requests to ‘/delete-post/’ followed by one or more digits. The digits should be captured and made available as the ‘id’ parameter.”
Update 1: How to delete the rows without a page reload
Replace the bl_delete_post_callback() definition with:
/**
* Callback function to handle post deletion via REST API
*
* @param WP_REST_Request $request The request object containing the post ID to delete
* @return WP_REST_Response The response object with the result of the deletion attempt
*/
function bl_delete_post_callback( WP_REST_Request $request ): WP_REST_Response {
// Extract the post ID from the request and ensure it's an integer
$post_id = (int) $request['id'];
// Attempt to delete the post
// wp_delete_post() moves the post to trash by default, unless force delete is set to true
$result = wp_delete_post( $post_id );
// Check if the deletion was successful
// wp_delete_post() returns the deleted post object on success, or null if the post doesn't exist
if ( $result && ! is_null( $result ) ) {
// Deletion successful
return new WP_REST_Response(
[
'success' => true,
'message' => 'Post moved to Trash',
'deletedPostId' => $post_id // Include the deleted post ID in the response
],
200 // HTTP status code for success
);
} else {
// Deletion failed
return new WP_REST_Response(
[
'success' => false,
'message' => 'Failed to move post to Trash'
],
500 // HTTP status code for server error
);
}
}
Replace delete-post.js with
// Wait for the DOM to be fully loaded before executing the script
document.addEventListener('DOMContentLoaded', () => {
// Select all delete buttons in the document
const deleteButtons = document.querySelectorAll('.delete-post');
// Attach click event listeners to each delete button
deleteButtons.forEach(button => {
button.addEventListener('click', async e => {
// Prevent the default action of the button (e.g., form submission)
e.preventDefault();
// Ask for user confirmation before proceeding with deletion
if (!confirm('Are you sure you want to delete this post?')) {
return; // If user cancels, exit the function
}
// Get the post ID from the button's data attribute
const postId = button.dataset.postId;
try {
// Send a DELETE request to the custom REST API endpoint
const response = await fetch(
`${blDeletePost.root}bl/v1/delete-post/${postId}`,
{
method: 'DELETE',
headers: {
'X-WP-Nonce': blDeletePost.nonce, // WordPress nonce for security
'Content-Type': 'application/json',
},
}
);
// Parse the JSON response
const data = await response.json();
if (data.success) {
// If deletion was successful, find the deleted post's row by using the button that was clicked
const deletedPost = button.closest('tr');
// Apply fade-out and shrink animation
// Set transition properties for smooth animation
deletedPost.style.transition =
'opacity 0.5s ease, max-height 0.5s ease';
deletedPost.style.opacity = '0'; // Fade out
deletedPost.style.maxHeight = '0'; // Shrink
// Remove the element from the DOM after the animation completes
setTimeout(() => {
deletedPost.remove();
}, 500); // 500ms matches the transition duration
} else {
// If deletion failed, log the error and show an alert to the user
console.error('Failed to delete post:', data.message);
alert('Failed to delete post. Please try again.');
}
} catch (error) {
// If an error occurs during the fetch operation, log it and alert the user
console.error('Error:', error);
alert('An error occurred while deleting the post. Please try again.');
}
});
});
});
If your query loop-enabled element’s HTML tag is li, replace
// If deletion was successful, find the deleted post's row by using the button that was clicked
const deletedPost = button.closest('tr');
with
// If deletion was successful, find the deleted post's container element
const deletedPost = document.querySelector(`[data-id="${data.deletedPostId}"]`).closest('li');
Add this CSS:
tr[data-id] {
/* Set up transitions for smooth animation */
transition: opacity 0.5s ease, max-height 0.5s ease;
/* Hide overflowing content during height animation */
overflow: hidden;
}
If your query loop-enabled element’s HTML tag is li, instead add:
li[data-id] {
/* Set up transitions for smooth animation */
transition: opacity 0.5s ease, max-height 0.5s ease;
/* Hide overflowing content during height animation */
overflow: hidden;
}
How to set up the delete buttons for post authors AND users that can delete others’ posts (admins and editors by default)
Child theme’s functions.php:
<?php
// Register a custom REST API endpoint for deleting posts
add_action( 'rest_api_init', function () {
register_rest_route(
'bl/v1', // Namespace
'/delete-post/(?P<id>d+)', // Route
[
'methods' => 'DELETE', // This endpoint responds to DELETE requests
'callback' => 'bl_delete_post_callback', // Function to handle the request
'permission_callback' => 'bl_delete_post_permissions_check', // Function to check if user has permission
]
);
} );
/**
* Check if the current user has permission to delete the specified post
*
* @param WP_REST_Request $request The request object
* @return bool True if user has permission, false otherwise
*/
function bl_delete_post_permissions_check( WP_REST_Request $request ): bool {
$post_id = (int) $request['id']; // Get the post ID from the request
$post = get_post( $post_id ); // Fetch the post object
if ( ! $post ) {
return false; // Post doesn't exist, so no permission
}
// Check if user can delete this post
return current_user_can( 'delete_post', $post_id );
}
/**
* Callback function to handle post deletion via REST API
*
* @param WP_REST_Request $request The request object containing the post ID to delete
* @return WP_REST_Response The response object with the result of the deletion attempt
*/
function bl_delete_post_callback( WP_REST_Request $request ): WP_REST_Response {
// Extract the post ID from the request and ensure it's an integer
$post_id = (int) $request['id'];
// Attempt to delete the post
// wp_delete_post() moves the post to trash by default, unless force delete is set to true
$result = wp_delete_post( $post_id );
// Check if the deletion was successful
// wp_delete_post() returns the deleted post object on success, or null if the post doesn't exist
if ( $result && ! is_null( $result ) ) {
// Deletion successful
return new WP_REST_Response(
[
'success' => true,
'message' => 'Post moved to Trash',
'deletedPostId' => $post_id // Include the deleted post ID in the response
],
200 // HTTP status code for success
);
} else {
// Deletion failed
return new WP_REST_Response(
[
'success' => false,
'message' => 'Failed to move post to Trash'
],
500 // HTTP status code for server error
);
}
}
// Enqueue the JavaScript file for post deletion
add_action( 'wp_enqueue_scripts', function (): void {
// If this is not the 'sample-page', don't enqueue the script i.e., abort
if ( ! is_page( 'sample-page' ) ) {
return;
}
wp_enqueue_script(
'bl-delete-post', // Handle
get_stylesheet_directory_uri() . '/assets/js/delete-post.js',
[], // No dependencies
'1.0.0', // Version number
[
'in_footer' => true, // Load in footer
'strategy' => 'defer', // Use defer loading strategy
]
);
// Localize the script with necessary data
wp_localize_script( 'bl-delete-post', 'blDeletePost', [
// The root URL for the WordPress REST API
// esc_url_raw() ensures the URL is properly escaped for security
'root' => esc_url_raw( rest_url() ),
// A nonce (number used once) for the WordPress REST API
// This adds a layer of security to prevent CSRF attacks
'nonce' => wp_create_nonce( 'wp_rest' ),
// The ID of the currently logged-in user
// Returns 0 for non-logged-in users
'currentUserId' => get_current_user_id(),
// Boolean indicating if the current user can delete posts by other users
// Typically true for administrators and editors
'canDeleteAnyPost' => current_user_can( 'delete_others_posts' ),
] );
} );
The change is in the 3rd argument sent to the wp_localize_script() function.
assets/js/delete-post.js:
// Wait for the DOM to be fully loaded before executing the script
document.addEventListener('DOMContentLoaded', () => {
// Select all delete buttons in the document
const deleteButtons = document.querySelectorAll('.delete-post');
// Iterate over each delete button
deleteButtons.forEach(button => {
// Get the post author ID from the button's data attribute and parse it as an integer
const postAuthorId = parseInt(button.dataset.authorId, 10);
// Determine if the current user can delete this post
// They can delete if they can delete any post OR if they are the post author
const canDeleteThisPost =
blDeletePost.canDeleteAnyPost ||
postAuthorId === parseInt(blDeletePost.currentUserId, 10);
if (canDeleteThisPost) {
// If the user can delete this post, make the delete button visible
button.style.display = 'inline-block';
// Add a click event listener to the delete button
button.addEventListener('click', async e => {
// Prevent the default action of the button (e.g., form submission)
e.preventDefault();
// Ask for user confirmation before proceeding with deletion
if (!confirm('Are you sure you want to delete this post?')) {
return; // If user cancels, exit the function
}
// Get the post ID from the button's data attribute
const postId = button.dataset.postId;
try {
// Send a DELETE request to the custom REST API endpoint
const response = await fetch(
`${blDeletePost.root}bl/v1/delete-post/${postId}`,
{
method: 'DELETE',
headers: {
'X-WP-Nonce': blDeletePost.nonce, // WordPress nonce for security
'Content-Type': 'application/json',
},
}
);
// Parse the JSON response
const data = await response.json();
if (data.success) {
// If deletion was successful, find the table row containing the deleted post
const deletedPost = button.closest('tr');
// Apply fade-out and shrink animation
deletedPost.style.transition =
'opacity 0.5s ease, max-height 0.5s ease';
deletedPost.style.opacity = '0'; // Fade out
deletedPost.style.maxHeight = '0'; // Shrink
// Remove the table row from the DOM after the animation completes
setTimeout(() => {
deletedPost.remove();
}, 500); // 500ms matches the transition duration
// Log success message (can be replaced with more specific UI updates)
console.log('Post successfully deleted');
} else {
// If deletion failed, log the error and show an alert to the user
console.error('Failed to delete post:', data.message);
alert('Failed to delete post. Please try again.');
}
} catch (error) {
// If an error occurs during the fetch operation, log it and alert the user
console.error('Error:', error);
alert('An error occurred while deleting the post. Please try again.');
}
});
} else {
// If the user can't delete this post, hide the delete button
button.style.display = 'none';
}
});
});
This still applies: https://d.pr/i/mdVOBu