I sat down with Google Gemini to try and write a WordPress plugin to allow site visitors to suggest spelling corrections or other corrections.
The TL;DR: it helped a lot, but started to make errors that were hard (for me) to solve shortly after starting. As the plugin got more complex, the help it offered was mostly useless, compared to what I could find on the web, with some careful searching.
I used the plugin development Docker, and it worked great!
The code is incomplete, and the plugin isn’t fully functional, but it has the following features:
- Shortcodes.
- A custom post type.
- An admin menu.
- An admin screen listing the posts.
- A notification.
The code is way down at the bottom of the page. You might want to open this page in two windows, so you can read one, and scroll around the code in the other.
Overall Design
I made this an “old style” shortcode that will accept a form submission from the front end. These are considered obsolete, but far easier to do than a Gutenberg Block.
The plugin creates two shortcodes. One to generate a “Submit a Correction” link, and another to display a form to submit, and receive the form post.
Gemini created the submission link correctly.
Gemini also did an acceptable job on the form. The code weirdness where the form handler is broken out, is my modification. I was having trouble handling the form.
The form handling didn’t work intially, because it used the field names “email” and “correction”, and also had some redundant non-features, with duplicate post IDs.
Both those field names are problems. “email” is reserved by WordPress. “correction” matches the name of the CPT, and WordPress will use it to search for matching corrections. (It’s the query var for the CPT.)
Fixing these took a long time.
Gemini couldn’t answer questions correctly, so I mostly did the usual web searches, and documentation searches.
I ended up mucking around, checking that permalinks were enabled, because I thought that getting the page ID out of the URL would help with sending the POST. That’s why there’s correction_init(). That’s my cruft. That said, I do like putting all the init code in one place.
The Custom Post Type
It made a reasonable CPT. However, the code had a bug. The CPT was defined in the plugin activation function, rather than running it every time the page is loaded.
So that code had to be moved out, and hooked to the ‘init’ action.
Form Submission Screen
It did a perfect job creating a Page to hold the form. It even checks to see if the page already exists.
Admin Screens
The CPT it created didn’t have the args for the admin screens set, so I took the code, pasted it into a prompt, and got it to fix the code. It did a good job, and I think it worked without modification.
I asked it to create an admin menu, and it made a submenu for a nonexistent menu. So I had to change that. Then I asked it to rewrite the code, and it did so.
It produced a page listing corrections, but the code didn’t work correctly. I had to spend at least an hour figuring out this bug.
The problem was that the get_posts function needed a ‘post_status’ argument set to ‘any’ or ‘pending’.
The screen it produced wasn’t quite what I wanted, but it was easy to modify.
Conclusion
I spent a few hours at this. It felt like a long time, but it wasn’t too bad. I’ve made a few plugins, and even one that showed a list of things in the admin, but I hadn’t used CPTs that much. So I’m a beginner level plugin writer. I think Gemini performed acceptably for me, even with all the errors.
Looking up information to fix the bugs, however, was pretty difficult.
The WordPress Codex was woefully inadequate at explaining my type of plugin. The web didn’t have that much information that I could find, either.
No wonder Gemini started giving bad advice to fix its imperfect code: the information is barely out there, buried among a mountain of beginner and intermediate level tutorial pages.
WordPress has a few sophisticated plugins to make forms, and accept submissions. WordPress also has a few plugins to make custom post types, and the admin screens come along with those. All I really needed was create a form, and a CPT, and then write some code to copy the form data into a CPT.
Update: I was digging around, and found a bunch of add-ons like an extension for Gravity Forms that saves to CPTs, the ACF acf_form() function, the CMB2 framework, TypeRocket framework, other frameworks. So this problem has been partially solved by visual tools and code-centered tools.
Aside
I have done more WordPress work with my own tables, than with CPTs. Mainly, I found CPTs hard to understand and program. I was familiar with SQL.
I have decided to finally change my ways. For simple database publishing, CPTs are better. WordPress is your GUI, and you write some code to do all the database magic. The output is SEO friendly.
I’ll finish this plugin over a few more posts. The story continues at Gemini-Assisted WordPress Plugin: Corrections 2/2
<?php
/*
Plugin Name: Correction Feedback Plugin
Description: A very basic plugin example.
Version: 1.0
Author: Your Name
*/
// Shortcode to display the link
function correction_link_shortcode() {
global $post;
if (!$post) {
return '';
}
$form_url = add_query_arg(array(
'correction_form' => 'true',
'post_id' => $post->ID,
), get_permalink(get_option('correction_form_page_id')));
$icon = '[]';
$link_text = 'Submit a Correction ';
$link = '<a href="' . esc_url($form_url) . '" target="_blank">' . esc_html($link_text) . $icon . '</a>';
return $link;
}
// Shortcode to display the form
function correction_form_shortcode() {
if (isset($_GET['correction_form']) && $_GET['correction_form'] == 'true') {
ob_start();
$post_id = isset($_GET['post_id']) ? intval($_GET['post_id']) : 0;
if ($post_id <= 0) {
echo "<p>Invalid post ID.</p>";
return ob_get_clean();
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
echo '<form method="post">';
echo '<input type="hidden" name="post_id" value="' . esc_attr($post_id) . '">';
echo '<label for="original_sentence">Original Sentence (Paste Here):</label><br>';
echo '<textarea name="original_sentence" id="original_sentence" rows="4" cols="50" required></textarea><br><br>';
echo '<label for="correction">Your Correction:</label><br>';
echo '<textarea name="correction-text" id="correction" rows="4" cols="50" required></textarea><br><br>';
echo '<label for="email">Your Email:</label><br>';
echo '<input type="email" name="correction-email" id="email"><br><br>';
echo '<input type="submit" value="Submit">';
echo '</form>';
return ob_get_clean();
} else {
return correction_form_handler();
}
}
}
// Handle the submission
function correction_form_handler() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
ob_start();
$original_sentence = isset($_POST['original_sentence']) ? sanitize_textarea_field($_POST['original_sentence']) : '';
$correction = isset($_POST['correction-text']) ? sanitize_textarea_field($_POST['correction-text']) : '';
$email = isset($_POST['correction-email']) ? sanitize_email($_POST['correction-email']) : '';
$post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
if (empty($original_sentence) || empty($correction) || empty($email)) {
echo '<p>Please fill in all required fields.</p>';
return ob_get_clean();
}
// Create a new correction post
$correction_post = array(
'post_type' => 'correction',
'post_status' => 'pending',
'post_title' => 'Correction for Post ID: ' . $post_id,
'post_content' => "Original Sentence:\n" . $original_sentence . "\n\nCorrection:\n" . $correction . "\n\nEmail: " . $email,
'meta_input' => array(
'original_post_id' => $post_id,
'original_sentence' => $original_sentence,
'correction' => $correction,
'email' => $email,
),
);
$result = wp_insert_post($correction_post, true);
// Send email notification to the administrator
$admin_email = get_option('admin_email');
$subject = 'New Correction Submitted';
$message = "A new correction has been submitted for post ID: {$post_id}\n\n";
$message .= "Original Sentence:\n{$original_sentence}\n\n";
$message .= "Correction:\n{$correction}\n\n";
$message .= "Email: {$email}\n";
// wp_mail($admin_email, $subject, $message);
echo '<p>Thank you for your correction! It has been submitted.</p>';
echo "<p>ID: $result</p>";
return ob_get_clean();
}
}
// Plugin Activation Function
function correction_plugin_activate() {
$page_title = 'Submit a Correction';
$page_content = '[correction_form]';
$page_exists = get_page_by_title($page_title);
if (!$page_exists) {
$page_id = wp_insert_post(array(
'post_title' => $page_title,
'post_content' => $page_content,
'post_status' => 'publish',
'post_type' => 'page',
));
if ($page_id) {
update_option('correction_form_page_id', $page_id);
}
} else {
update_option('correction_form_page_id', $page_exists->ID);
}
flush_rewrite_rules();
}
register_activation_hook(__FILE__, 'correction_plugin_activate');
// Plugin Deactivation Function
function correction_plugin_deactivate() {
unregister_post_type('correction');
delete_option('correction_form_page_id');
flush_rewrite_rules();
}
register_deactivation_hook(__FILE__, 'correction_plugin_deactivate');
// Custom post type
function register_correction_post_type() {
register_post_type(
'correction', // $post_type
array(
'labels' => array(
'name' => __('Corrections', 'textdomain'),
'singular_name' => __('Correction', 'textdomain'),
'menu_name' => __('Corrections', 'textdomain'),
'all_items' => __('All Corrections', 'textdomain'),
'add_new' => __('Add New', 'textdomain'),
'add_new_item' => __('Add New Correction', 'textdomain'),
'edit_item' => __('Edit Correction', 'textdomain'),
'new_item' => __('New Correction', 'textdomain'),
'view_item' => __('View Correction', 'textdomain'),
'search_items' => __('Search Corrections', 'textdomain'),
'not_found' => __('No corrections found', 'textdomain'),
'not_found_in_trash' => __('No corrections found in Trash', 'textdomain'),
),
'public' => true, // Show in admin menu
'show_ui' => true, // Show admin UI
'show_in_menu' => true, //show in the admin menu.
'capability_type' => 'correction',
'hierarchical' => false,
'rewrite' => array('slug' => 'correction'),
'query_var' => true,
'supports' => array('title', 'editor'),
'menu_position' => 30,
'has_archive' => false // No archive page
)
);
}
function correction_admin_menu() {
add_menu_page(
'Corrections', // Page title
'Corrections', // Menu title
'edit_posts', // Capability
'correction', // Menu slug
'correction_admin_page', // Callback function
'dashicons-edit',
30
);
}
// Admin page content
function correction_admin_page() {
?>
<div class="wrap">
<h2>Corrections</h2>
<table class="wp-list-table widefat fixed striped posts">
<thead>
<tr>
<th>Post ID</th>
<th>Original Sentence</th>
<th>Correction</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php
$corrections = get_posts($args = array(
'post_type' => 'correction',
'post_status' => 'any',
'numberposts' => 10
));
foreach ($corrections as $correction) {
$post_id = get_post_meta($correction->ID, 'original_post_id', true);
$original_sentence = get_post_meta($correction->ID, 'original_sentence', true);
$corrected_sentence = get_post_meta($correction->ID, 'correction', true);
$email = get_post_meta($correction->ID, 'email', true);
$status = $correction->post_status;
?>
<tr>
<td><?php echo esc_html($post_id); ?></td>
<td><?php echo esc_html($original_sentence); ?></td>
<td><?php echo esc_html($corrected_sentence); ?></td>
<td><?php echo esc_html($email); ?></td>
<td>
<a href="<?php echo esc_url(add_query_arg(array('action' => 'delete_correction', 'correction_id' => $correction->ID))); ?>">Delete</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php
}
// Handle delete actions
function handle_correction_actions() {
if (isset($_GET['action']) && $_GET['action'] === 'delete_correction' && isset($_GET['correction_id'])) {
$correction_id = intval($_GET['correction_id']);
if ($correction_id) {
wp_delete_post($correction_id, true); // true = force delete
wp_redirect(admin_url('edit.php?post_type=correction'));
exit;
}
}
}
// INIT SECTION
// Admin notice error shown when permalinks are off.
function correction_permalink_notice() {
?>
<div class="notice notice-error">
<p><?php _e('The Correction Plugin requires using permalinks. Please enable permalinks in your WordPress settings.', 'textdomain'); ?></p>
</div>
<?php
}
function correction_init() {
global $wp_rewrite;
if ( ! $wp_rewrite->using_permalinks() ) {
add_action('admin_notices', 'correction_permalink_notice');
} else {
add_shortcode('correction_link', 'correction_link_shortcode');
add_shortcode('correction_form', 'correction_form_shortcode');
// add_action('init', 'correction_form_handler');
add_action('init', 'register_correction_post_type');
add_action('admin_menu', 'correction_admin_menu');
add_action('admin_init', 'handle_correction_actions');
}
}
add_action('init', 'correction_init', 5);