WP Manifestindependent plugin directory
manifest / events / cpj-appointment-scheduler

CPJ Appointment Scheduler

WordPress Plugin for appointment scheduling.

★ 0stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/cpj9251/cpj-appointment-scheduler/archive/refs/heads/main.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

A WordPress plugin for booking appointments: visitors pick a date and time on a front-end calendar, submit their contact info, and both they and the site admin get a confirmation email. Built as a Gutenberg block + React front end on top of a small, customer-centric REST API.

  • Author: Paul Jarvis (cpauljarvis.com)
  • Requires: PHP 8.2+, WordPress with the block editor
  • Status: v1 beta — see Roadmap for what's intentionally not built yet

How it works

  1. A page or post has the CPJ Appointment Scheduler block added to it.
  2. A visitor picks a date on the calendar (only dates with open slots are selectable), then a time, then enters their name/email/phone.
  3. On submit, the plugin finds-or-creates a Customer record (matched by email, so repeat visitors don't get duplicated) and creates a linked Appointment record.
  4. The visitor gets a confirmation email; the admin gets a notification email. Both are template-driven and editable from the settings screen.
  5. Admins manage everything from wp-admin: the Appointments and Customers post-list screens, each with a real Gutenberg block (not a classic meta box) for editing the underlying data.

The block editor preview for the scheduler block renders the exact same React component tree as the live front end — there's no separate "editor preview" to keep in sync.

Data model

Customer-centric, not appointment-centric: a customer can accumulate many appointments over time without their info being re-entered or duplicated.

cpj_customer (post)                cpj_appointment (post)
├─ meta: first_name                ├─ meta: customer_id ──┐
├─ meta: last_name                 ├─ meta: date          │
├─ meta: email                     ├─ meta: time          │
├─ meta: phone                     ├─ meta: duration_minutes
├─ meta: address                   ├─ meta: status (confirmed|cancelled|completed)
└─ meta: preferred_contact         └─ meta: notes
        ▲                                    │
        └────────────────────────────────────┘

Both post types register their meta via register_post_meta (validated, show_in_rest) and template-lock a dedicated Gutenberg block onto their editor screen, so wp-admin shows real form controls instead of the classic "Custom Fields" box. Because the customer-facing booking flow creates these posts anonymously (no logged-in user), post creation deliberately bypasses wp_kses for that one call — otherwise WordPress strips the block markup right back out of post_content before it's saved.

REST API

Namespace: cpj-appt-sched/v1. Availability/booking routes are public (anonymous) by design; the customer-appointments lookup is admin-only.

Method Route Auth Purpose
GET /availability?month=YYYY-MM Public Which dates in a month have at least one open slot
GET /availability/times?date=YYYY-MM-DD Public Open time slots for one date
POST /appointments Public Book an appointment (honeypot + rate-limited)
GET /customers/{id}/appointments edit_posts An admin-screen lookup for a customer's appointment history

Settings are read/written through WordPress's own /wp/v2/settings endpoint rather than a custom route — see below.

Abuse protection on POST /appointments: a hidden honeypot field (website) silently no-ops the request if filled in, and submissions are rate-limited per IP (5 requests / 10 minutes) via transients. There's no CAPTCHA in v1 — see Roadmap.

Settings

Stored as a single option, cpj_appt_scheduler_settings, registered with show_in_rest so it's readable/writable at /wp/v2/settings. Configurable from Settings → Appointment Scheduler:

Field Description
appointmentDurationMinutes Length of one appointment slot
minimumNoticeMinutes How far in advance a booking must be made
maximumAdvanceDays How far out bookings are allowed
weeklyAvailability Recurring open times per weekday
dateOverrides Per-date exceptions — close a whole day or block specific times
notifyEmail Admin address for new-booking notifications
customerEmailSubject / customerEmailBody Confirmation email sent to the customer
adminEmailSubject / adminEmailBody Notification email sent to the admin

Email bodies support {first_name}, {last_name}, {date}, and {time} placeholders, substituted at send time.

Project structure

cpj-appointment-scheduler.php   Plugin bootstrap
src/
├─ Plugin.php                   Wires everything up on load
├─ Blocks/SchedulerBlock.php    Registers + conditionally enqueues the booking block
├─ CustomPostTypes/             Appointments, Customers
├─ MetaFields/                  register_post_meta + block-template wiring per CPT
├─ Rest/                        Route registration + request handling
├─ Services/                    Business logic: Availability, Appointment, Email
└─ Settings/Settings.php        Options schema, defaults, sanitization
assets/src/
├─ block/                       Scheduler block registration + editor preview
├─ frontend/                    The booking flow itself (calendar → time → details → confirmation)
├─ metafields/                  Appointment CPT's editor block
├─ customer-metafields/         Customer CPT's editor block
└─ settings/                    Settings screen React app

assets/build/ is compiled output (gitignored). prev_version/ is the original procedural/jQuery plugin, kept for reference only — nothing in the active plugin loads it.

Development setup

composer install
npm install

npm run start     # webpack watch mode
npm run build     # production build (required before deploying)

Linting

Both linters are configured against shared rulesets and are expected to pass clean before anything ships:

composer run lint       # phpcs
composer run lint:fix   # phpcbf — re-run `lint` after, it doesn't catch everything

npm run lint             # eslint
npm run lint:fix         # eslint --fix

Note: phpcbf can occasionally mangle complex compound conditionals (e.g. a Yoda-condition fix inside an || expression) into something that no longer parses. Always run php -l on anything it touches before trusting the auto-fix.

Roadmap

Deliberately out of scope for v1 beta:

  • Payment collection at booking time.
  • A custom admin dashboard — v1 uses the default WordPress post-list screens (with a few extra sortable columns) rather than a bespoke React admin UI.
  • CAPTCHA/third-party anti-spam — the honeypot + rate limit is judged sufficient for now.
  • Plugin uninstall/cleanup routine — no uninstall.php yet; deactivating the plugin does not remove its data or options.