Merck corporate logo in dark charcoal on a cream background

Set up a simple contact form without a plugin

WordPress already has the core tools for a small enquiry form: pages, a public request endpoint and email delivery through wp_mail(). A child theme and a short PHP handler can collect contact details and notify the business without adding a forms plugin.

This approach suits Australian sole traders, studios and local service firms receiving occasional enquiries. The trade-off is responsibility: spam prevention, secure input handling, dependable email and accessible status messages need attention. Replace the default welcome content with a proper Contact page once the setup is tested.

Plan the form before writing code

Start with the smallest useful fields: name, email and message. Make an optional phone number available for urgent local enquiries, and add a service or suburb field only when it helps the business respond. Do not request passwords, card details, health information or identity documents through a basic message field.

Australian operating details should shape the copy. Display business hours and the relevant time zone, such as AEST or AEDT in Sydney and Melbourne and AWST in Perth. Accept +61 as well as familiar local prefixes such as 03 for Melbourne and 07 for Brisbane. Publish a plain privacy notice covering purpose, storage and contact details; when the Privacy Act 1988 and Australian Privacy Principles apply, ensure it meets those obligations. Keep marketing consent separate from responding to an enquiry because commercial electronic messages are governed by the Spam Act 2003.

Decide what happens after submission. State a realistic response window, such as one business day, and identify public holidays when they regularly affect support hours. Provide a direct email or phone fallback for urgent matters, and explain whether submissions are monitored during weekends or school holidays.

Write labels in plain Australian English and explain why each optional field helps. Ask for a suburb when distance affects a quote, but skip date of birth or an exact street address unless the business genuinely needs them. This keeps the form proportionate for sole traders and small firms, where customers often make an enquiry before deciding whether to meet.

Place the form in a child theme

In the WordPress dashboard, use Pages > Add New Page to create a page named Contact with the slug contact. Use Appearance > Themes > Add New > Child Theme, then add a shortcode to its functions.php. The shortcode works with the block-based default theme: insert a Shortcode block on the Contact page containing [simple_contact_form]. A classic child theme can place the same markup in a page template instead.

The following function goes in functions.php. It creates a nonce, labels each field and includes a honeypot for basic bot filtering:

add_shortcode('simple_contact_form', 'simple_contact_form_markup');

function simple_contact_form_markup() {
    ob_start();
    ?>
    <form action="<?php echo esc_url(admin_url('admin-post.php')); ?>" method="post">
        <?php wp_nonce_field('simple_contact', 'contact_nonce'); ?>
        <input type="hidden" name="action" value="simple_contact">

        <p>
            <label for="contact-name">Name</label>
            <input id="contact-name" name="name" type="text" autocomplete="name" required>
        </p>
        <p>
            <label for="contact-email">Email</label>
            <input id="contact-email" name="email" type="email" autocomplete="email" required>
        </p>
        <p>
            <label for="contact-phone">Phone (optional)</label>
            <input id="contact-phone" name="phone" type="tel" autocomplete="tel">
        </p>
        <p>
            <label for="contact-message">Message</label>
            <textarea id="contact-message" name="message" rows="6" maxlength="2000" required></textarea>
        </p>
        <div aria-hidden="true" style="position:absolute;left:-9999px;">
            <label for="contact-website">Leave this field empty</label>
            <input id="contact-website" name="website" type="text" tabindex="-1" autocomplete="off">
        </div>
        <p>
            <label>
                <input type="checkbox" name="privacy_acknowledgement" value="yes" required>
                I have read the privacy notice and agree to my details being used to answer this enquiry.
            </label>
        </p>
        <p><button type="submit">Send enquiry</button></p>
    </form>
    <?php
    return ob_get_clean();
}

The hidden website field is a honeypot that people cannot see but naive bots may fill. Replace the privacy wording with the site’s actual notice. If full-page caching is enabled, exclude the form page or refresh cached nonces, because an expired nonce rejects a valid submission.

Process and validate submissions

Add the handler to the same functions.php. Its two actions cover visitors who are logged in and those who are not. The handler verifies the nonce, checks the honeypot, sanitises every field, validates required information and redirects so a page refresh does not repeat the request.

add_action('admin_post_nopriv_simple_contact', 'simple_contact_handle');
add_action('admin_post_simple_contact', 'simple_contact_handle');

function simple_contact_handle() {
    $redirect = home_url('/contact/');

    if (!empty($_POST['website'])) {
        wp_safe_redirect(add_query_arg('contact', 'sent', $redirect));
        exit;
    }

    $nonce = isset($_POST['contact_nonce']) ? sanitize_text_field(wp_unslash($_POST['contact_nonce'])) : '';
    if (!wp_verify_nonce($nonce, 'simple_contact')) {
        wp_safe_redirect(add_query_arg('contact', 'invalid', $redirect));
        exit;
    }

    $name = isset($_POST['name']) && is_string($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : '';
    $email = isset($_POST['email']) && is_string($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';
    $phone = isset($_POST['phone']) && is_string($_POST['phone']) ? sanitize_text_field(wp_unslash($_POST['phone'])) : '';
    $message = isset($_POST['message']) && is_string($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : '';
    $consent = !empty($_POST['privacy_acknowledgement']);

    if ('' === $name || !is_email($email) || '' === $message || !$consent) {
        wp_safe_redirect(add_query_arg('contact', 'invalid', $redirect));
        exit;
    }

    $body = "Name: {$name}\nEmail: {$email}\nPhone: {$phone}\n\nMessage:\n{$message}";
    $headers = array('Reply-To: ' . $email);
    $sent = wp_mail(get_option('admin_email'), 'Website enquiry from ' . $name, $body, $headers);
    $status = $sent ? 'sent' : 'error';

    wp_safe_redirect(add_query_arg('contact', $status, $redirect));
    exit;
}

Browser-side required fields improve convenience, but the PHP checks provide the real protection. Serve the site over HTTPS so submitted details are encrypted in transit. A nonce helps prevent cross-site request forgery; it is not a CAPTCHA and will not stop a determined spam bot. Rate limits, host firewall rules or a CAPTCHA service can add another layer, depending on the server and the site’s exposure.

Delivery depends on the web host’s PHP mail configuration. Ask the provider whether wp_mail() uses authenticated SMTP mail, then add SPF and DKIM records for the domain and review DMARC reports. Those DNS records sit outside the form code, but they materially improve whether Australian customer enquiries reach the business inbox instead of a spam folder.

Show accessible status messages

After the redirect, use the query value to display the result. Add this PHP inside the shortcode callback directly after ob_start(); and before the existing ?> that precedes the form:

if (isset($_GET['contact']) && is_string($_GET['contact'])) {
    $status = sanitize_key(wp_unslash($_GET['contact']));
    if ('sent' === $status) {
        echo '<p role="status">Thanks. Your enquiry has been sent. We aim to reply within one business day.</p>';
    } elseif ('invalid' === $status) {
        echo '<p role="alert">Please check the required fields and try again.</p>';
    } elseif ('error' === $status) {
        echo '<p role="alert">We could not send your enquiry. Please try again or use the email address listed on this page.</p>';
    }
}

Keep the message close to the form, ensure it is not communicated by colour alone, and allow keyboard and screen-reader users to move directly to the next action. This simple handler asks visitors to re-enter a rejected enquiry; sites with longer messages can restore submitted values temporarily and safely. Quote response times against the business’s actual Australian working hours, including weekends and selected public holidays.

Compare methods and test before launch

A quick comparison shows where each approach fits:

Method Setup effort Spam protection Message delivery Best fit
Core PHP form with wp_mail() Medium Nonce, honeypot, optional server rules Uses host mail; supports domain DNS records Owners comfortable maintaining a child theme
mailto: link Low Very limited Opens the visitor’s email application Occasional enquiries and simple sites
Hosted form service Low to medium Provider rate limits and CAPTCHA options Provider handles routing and email Sites needing file uploads or CRM integration

The core PHP route gives control over markup and data handling without a recurring plugin fee, but it needs testing when WordPress or PHP changes. A mailto: link exposes the address to harvesting. A hosted service reduces custom code but requires disclosure of the processor, storage location and subscription cost for Australian businesses.

Before launch, send submissions from a desktop and phone, check server logs, confirm success and error paths, and inspect SPF, DKIM and DMARC results. Test long names, international email addresses and screen readers without placing sensitive information in the route. Review the child theme after major platform updates. A valid enquiry in the business’s Australian inbox and a clear, accessible response for the visitor form the launch standard.