Documentation
EasyShuttle
A shuttle-booking theme that presents data it does not own. Routes, timetables, fares and the booking flow all belong to the Magepeople Shuttle Booking plugin; everything about how they look lives here. This manual covers both halves — the settings a site owner touches, and the structure a developer extends.
Using the theme
For whoever runs the siteHow it fits together
Three things share the work, and knowing which owns what saves a lot of hunting:
- The plugin owns the data. Shuttles, routes, stops, timetables, fares, seats, bookings and payment all come from Magepeople Shuttle Booking. If a price is wrong or a departure is missing, it is fixed in the plugin, not here.
- The theme owns the presentation. Every colour, heading, band and layout on the public site is a theme setting.
- WordPress owns the pages. The theme creates eight of them for you and assigns the right template to each.
The theme degrades honestly. With the plugin inactive the site still renders — booking sections simply say what is missing rather than erroring. That is deliberate, so a lapsed licence never takes the whole site down.
Plugins it knows about
| Plugin | Status | Why |
|---|---|---|
| Magepeople Shuttle Booking | Required | Routes, timetables, fares and the booking flow all come from this plugin. The theme displays them. |
| Shuttle Booking Pro | Not released | Adds PDF tickets, calendar export, booking chat and extra payment gateways. |
| WooCommerce | Optional | Optional. Only needed if you take payment through WooCommerce rather than the plugin's own checkout. |
| Elementor | Optional | Optional. Every theme section is also an Elementor widget, so pages can be rebuilt visually. |
Guided setup
On first activation the theme opens EasyShuttle → Guided Setup. It runs in order,
and every step is safe to leave half-done — each one checks the site and reports what is already
in place rather than repeating work.
- 1PluginsThe booking engine
- 2PagesHome, routes, booking
- 3MenuHeader navigation
- 4Demo contentSample routes
- 5FinishStyle your site
Finished steps lock themselves. Once the pages exist or the menu is assigned, that step shows a green note and its button is disabled — the same work cannot run twice. Delete what it made and the step unlocks again on its own.
The pages
Step two creates these. An existing page with the same slug is adopted, never overwritten, so running setup on a live site is safe.
| Page | Slug | Template |
|---|---|---|
| Home | /home/ | Default page template |
| Shuttles | /shuttles/ | page-templates/template-shuttles.php |
| Book a shuttle | /booking/ | page-templates/template-booking.php |
| About | /about/ | page-templates/template-about.php |
| Contact | /contact/ | page-templates/template-contact.php |
| Find my booking | /find-my-booking/ | page-templates/template-find-booking.php |
| My bookings | /my-bookings/ | page-templates/template-account.php |
| News | /news/ | Default page template |
| Documentation | /documentation/ | page-templates/template-docs.php |
| Privacy Policy | /privacy-policy/ | page-templates/template-legal.php |
| Terms & Conditions | /terms/ | page-templates/template-legal.php |
| Refund Policy | /refunds/ | page-templates/template-legal.php |
Build modes
Under Theme Options → Page builders, choose how pages are assembled:
- Theme layouts — pages render from the templates and their Theme Options copy. Nothing to edit visually; fastest and the least that can break.
- Blocks — each section is also a block, so pages are editable in the block editor.
- Elementor — every section is an Elementor widget too.
Switching only rewrites pages the theme created and has not seen edited, and it backs up the previous state first. Pages you authored yourself are skipped.
Every setting
178 fields across 12 panelsTheme Options lives at EasyShuttle → Theme Options. Panels switch
instantly — everything is on one page and saves together, so a half-typed value in one panel
survives a look at another. Stored as is the dot path the value is kept under, which is
also what easyshuttle_option() takes.
Brand & layout
15 fieldsIdentity and the global look: what the header shows, the five colours every section inherits, page width, corner radius and motion.
| Field | Stored as | Type | What it does |
|---|---|---|---|
| Logo | brand.logo_id | image | — |
| Site name override | brand.site_name | text | Used in branding and structured data. Defaults to the WordPress site title. |
| Logo mark text | brand.mark_text | text | Two or three characters, shown when no logo image is set. |
| Wordmark, first part | brand.name_lead | text | — |
| Wordmark, highlighted part | brand.name_accent | text | — |
| Tagline | brand.tagline | multi-line | Shown under the logo in the footer. |
| Primary (forest green) | colors.primary | colour | — |
| Primary dark | colors.primary_dark | colour | — |
| Accent (orange) | colors.accent | colour | — |
| Highlight (amber) | colors.highlight | colour | — |
| Page background | colors.background | colour | — |
| Content width | layout.content_width | number | Pixels. The design is drawn for 1200. |
| Section spacing | layout.spacing | select | Options: Compact · Comfortable · Spacious |
| Corner rounding | layout.radius | select | Options: Square · Rounded · Soft |
| Motion | layout.motion | select | Visitors who ask their system for reduced motion always get it, whatever is chosen here. Options: Full animation · Reduced |
Header
8 fieldsThe top bar and the navigation row, including the click-to-call number and the booking button.
| Field | Stored as | Type | What it does |
|---|---|---|---|
| Top bar | header.topbar | toggle | — |
| Top bar note | header.topbar_note | text | — |
| Phone number | header.phone | text | Shown in the top bar as a click-to-call link. |
| Sticky header | header.sticky | toggle | — |
| Header button label | header.cta_label | text | — |
| Header button link | header.cta_url | URL | Leave empty to link to the booking page. |
| Booking lookup link | header.show_lookup | toggle | — |
| Lookup link label | header.lookup_label | text | — |
Homepage
22 fieldsWhich of the homepage bands appear, and the copy inside each one.
| Field | Stored as | Type | What it does |
|---|---|---|---|
| Hero background image | homepage.hero_image_id | image | — |
| Hero eyebrow | homepage.hero_eyebrow | text | — |
| Headline, first part | homepage.hero_title_before | text | — |
| Headline, highlighted part | homepage.hero_title_accent | text | — |
| Headline, last part | homepage.hero_title_after | text | — |
| Hero lead | homepage.hero_lead | multi-line | — |
| Hero reassurance list | homepage.hero_meta | multi-line | One item per line. |
| Hero button label | homepage.hero_cta_label | text | Leave empty to hide the hero button. |
| Hero button link | homepage.hero_cta_url | URL | — |
| Departure board title | homepage.board_title | text | — |
| Departure board note | homepage.board_note | text | — |
| Departures to show | homepage.board_limit | number | — |
| Search card eyebrow | homepage.search_eyebrow | text | — |
| Search card title | homepage.search_title | text | — |
| Search card subtitle | homepage.search_subtitle | multi-line | — |
| Booking search | homepage.show_search | toggle | — |
| Popular routes | homepage.show_routes | toggle | — |
| How booking works | homepage.show_steps | toggle | — |
| Stats band | homepage.show_stats | toggle | — |
| Fleet section | homepage.show_fleet | toggle | — |
| Testimonials | homepage.show_testimonials | toggle | — |
| CTA band | homepage.show_cta | toggle | — |
Routes & listing
10 fieldsThe route cards — on the homepage, the listing page and anywhere the shortcode is used.
| Field | Stored as | Type | What it does |
|---|---|---|---|
| Eyebrow | routes.eyebrow | text | — |
| Title | routes.title | text | — |
| Intro | routes.intro | multi-line | — |
| Routes on the homepage | routes.limit | number | — |
| Columns | routes.columns | select | Options: Two · Three · Four |
| Featured images | routes.show_images | toggle | — |
| "See all" button | routes.show_button | toggle | — |
| Button label | routes.button_label | text | — |
| Routes on the listing page | routes.archive_limit | number | Use -1 for every published shuttle. |
| Listing filters | routes.show_filters | toggle | — |
Shuttle page
15 fieldsThe single shuttle page: which panels appear beside the fare box.
| Field | Stored as | Type | What it does |
|---|---|---|---|
| About panel title | single.about_title | text | — |
| About panel lead | single.about_lead | multi-line | — |
| Timetable panel title | single.timetable_title | text | — |
| Timetable panel lead | single.timetable_lead | multi-line | — |
| On-board panel | single.show_amenities | toggle | — |
| On-board panel title | single.amenity_title | text | — |
| On-board panel lead | single.amenity_lead | multi-line | — |
| Amenities | single.amenities | multi-line | One per line. The booking plugin stores no amenity data, so these are set here and shown on every route page. |
| Route map | single.show_map | toggle | — |
| Questions panel | single.show_faq | toggle | — |
| Questions panel title | single.faq_title | text | — |
| Questions panel lead | single.faq_lead | multi-line | — |
| Fare sidebar note | single.sidebar_note | text | — |
| Counter card title | single.counter_title | text | — |
| Counter card text | single.counter_text | multi-line | — |
Sections
60 fieldsCopy for the standing pages — fleet, about, story, team, FAQ, testimonials, stats and the CTA band. The largest panel by far.
| Field | Stored as | Type | What it does |
|---|---|---|---|
| How booking works | |||
| Eyebrow | steps.eyebrow | text | — |
| Title | steps.title | text | — |
| Intro | steps.intro | multi-line | — |
| Steps | steps.items | repeater | Leave a row empty to hide it. Columns: Title · Text · 4 rows |
| Stats band | |||
| Stats | stats.items | repeater | Leave a row empty to hide it. Columns: Value · Label · 4 rows |
| Fleet | |||
| Eyebrow | fleet.eyebrow | text | — |
| Title | fleet.title | text | — |
| Body copy | fleet.text | multi-line | — |
| Checklist | fleet.checklist | multi-line | One item per line. |
| Fleet photo | fleet.image_id | image | — |
| Plate label | fleet.plate_label | text | — |
| Plate text | fleet.plate_text | text | — |
| Button label | fleet.button_label | text | — |
| Button link | fleet.button_url | URL | — |
| Image first | fleet.flip | toggle | — |
| Testimonials | |||
| Eyebrow | testimonials.eyebrow | text | — |
| Title | testimonials.title | text | — |
| Quotes | testimonials.items | repeater | Leave a row empty to hide it. Columns: Quote · Name · Role · Photo · 3 rows |
| Call to action | |||
| Eyebrow | cta.eyebrow | text | — |
| Title | cta.title | text | — |
| Text | cta.text | multi-line | — |
| Primary button | cta.button_label | text | — |
| Primary link | cta.button_url | URL | — |
| Secondary button | cta.button2_label | text | — |
| Secondary link | cta.button2_url | URL | — |
| About page | |||
| Eyebrow | about.eyebrow | text | — |
| Title | about.title | text | — |
| Body copy | about.text | multi-line | Leave a blank line between paragraphs. |
| Checklist | about.checklist | multi-line | One item per line. |
| About photo | about.image_id | image | — |
| Plate label | about.plate_label | text | — |
| Plate text | about.plate_text | text | — |
| Stats band | about.show_stats | toggle | — |
| Timeline | about.show_story | toggle | — |
| Team | about.show_team | toggle | — |
| FAQ | about.show_faq | toggle | — |
| Timeline | |||
| Eyebrow | story.eyebrow | text | — |
| Title | story.title | text | — |
| Intro | story.intro | multi-line | — |
| Milestones | story.items | repeater | Leave a row empty to hide it. Columns: Year · Title · Text · 6 rows |
| Team | |||
| Eyebrow | team.eyebrow | text | — |
| Title | team.title | text | — |
| People | team.items | repeater | Leave a row empty to hide it. Columns: Name · Role · Photo · 4 rows |
| FAQ | |||
| Eyebrow | faq.eyebrow | text | — |
| Title | faq.title | text | — |
| Questions | faq.items | repeater | Leave a row empty to hide it. Columns: Question · Answer · 6 rows |
| News | |||
| Eyebrow | blog.eyebrow | text | — |
| Title | blog.title | text | — |
| Intro | blog.intro | multi-line | — |
| Columns | blog.columns | select | Options: Two · Three |
Contact
21 fieldsThe contact page: the form, the details card, opening hours and the map.
| Field | Stored as | Type | What it does |
|---|---|---|---|
| Details card title | contact.eyebrow | text | — |
| Form title | contact.form_title | text | — |
| Form lead | contact.form_lead | multi-line | — |
| Enquiry topics | contact.topics | multi-line | One per line. |
| Counter address | contact.address | multi-line | Shown on the contact card, and the map is built from it. Leave this and Map location both empty and no map is drawn. |
| Phone | contact.phone | text | — |
| Public email | contact.email | — | |
| Counter hours summary | contact.hours | text | — |
| Corporate desk hours | contact.corporate | text | — |
| Opening hours card title | contact.hours_title | text | — |
| Monday | contact.hours_mon | text | — |
| Tuesday | contact.hours_tue | text | — |
| Wednesday | contact.hours_wed | text | — |
| Thursday | contact.hours_thu | text | — |
| Friday | contact.hours_fri | text | — |
| Saturday | contact.hours_sat | text | — |
| Sunday | contact.hours_sun | text | — |
| Map location | contact.map_query | text | An address or place name. Defaults to the counter address. With both empty there is nothing to map, so the map panel is left out. |
| Map pin label | contact.map_label | text | — |
| Help cards | contact.show_features | toggle | — |
| Map loading | contact.map_loading | select | Asking first keeps Google from setting cookies before a visitor opts in. Options: Ask before loading (recommended) · Load immediately |
Where contact messages go and how they are sent, including the Brevo API path.
| Field | Stored as | Type | What it does |
|---|---|---|---|
| Delivery method | email.provider | select | Brevo sends over HTTPS, which survives hosts that block outbound SMTP. Options: WordPress mail · Brevo API |
| Brevo API key | email.brevo_api_key | text | Paste the v3 API key from your Brevo account. |
| Send messages to | contact.form_to | Defaults to the public email, then the WordPress admin address. | |
| Sender name | email.sender_name | text | — |
| Sender address | email.sender_email | — | |
| Notification subject | email.admin_subject | text | Placeholders: {name}, {topic}, {site_name}. |
| Confirmation email | email.customer_confirmation | toggle | — |
| Confirmation subject | email.customer_subject | text | — |
| Confirmation message | email.customer_message | multi-line | Placeholders: {name}, {topic}, {site_name}. |
| Messages per visitor | email.rate_limit_client | number | — |
| Messages per address | email.rate_limit_recipient | number | — |
| Rate limit window (minutes) | email.rate_limit_window | number | — |
SEO
10 fieldsTitles, descriptions, the share card and structured data.
| Field | Stored as | Type | What it does |
|---|---|---|---|
| Built-in SEO | seo.enable | toggle | — |
| Structured data | seo.output_schema | toggle | — |
| Hide search results | seo.noindex_search | toggle | — |
| Homepage title | seo.home_title | text | Leave empty to use the site title and tagline. |
| Homepage description | seo.home_description | multi-line | Around 150 characters. |
| Title separator | seo.title_separator | text | — |
| Default sharing image | seo.share_image_id | image | — |
| X / Twitter handle | seo.twitter_site | text | — |
| Price range | seo.price_range | text | Shown in structured data, for example "$$". |
| Business type | seo.business_type | select | Options: Local business · Taxi service · Travel agency · Transportation service |
Pages
2 fieldsWhich page holds the booking flow, when it cannot be detected automatically.
| Field | Stored as | Type | What it does |
|---|---|---|---|
| Booking page | pages.booking_page | select | The page holding the booking flow. Route buttons and the header CTA link here. Left on automatic, the theme finds the first page containing the booking shortcode. Options: — Detect automatically — · Sample Page |
| Plugin pages | — | text | My bookings: not found — the plugin creates this page itself.Find my booking: not found — the plugin creates this page itself. |
Builders
0 fieldsSwitch between the theme layouts, blocks and Elementor, and read the shortcode reference.
No stored settings — this panel is a switcher and a reference table.
Under the hood
For whoever extends itArchitecture
Load order matters and is fixed in functions.php: defaults before helpers,
helpers before anything that reads options, and the shuttle data layer before the renderers that
use it.
easyshuttle/
├── functions.php load order, theme supports, enqueues
├── inc/
│ ├── defaults.php every option's default + easyshuttle_option()
│ ├── shuttle-data.php reads the plugin: shuttles, routes, fares
│ ├── helpers.php presentation helpers used by templates
│ ├── sections.php the section registry (schema for all 23)
│ ├── section-renderers.php one render function per section
│ ├── setup.php starter pages, asset gating, contact handler
│ ├── class-easyshuttle-theme-options.php the admin screen
│ ├── class-easyshuttle-setup-wizard.php guided setup
│ ├── class-easyshuttle-seo.php titles, cards, schema
│ ├── class-easyshuttle-email-delivery.php contact form delivery
│ └── builders/ shortcodes, blocks, Elementor, Divi, build mode
├── page-templates/ 8 page templates
├── template-parts/ shared partials
└── assets/ css, js, fonts, img, admin
The section system
This is the core idea worth understanding. A section is declared once in
easyshuttle_sections() — label, description, icon and its fields. From that single
declaration the theme derives, automatically:
- a shortcode
- a block, registered per section with its attributes
- an Elementor widget
- a Divi module
- the Theme Options fields that feed it, where a field declares an
option
Add a section to that array and all five follow. There is no second place to register it.
| Section | Shortcode | Fields | Renders |
|---|---|---|---|
| Hero | [easyshuttle_hero] | 13 | Dark hero with the headline and a live departure board pulled from your shuttles. |
| Booking search | [easyshuttle_search] | 6 | The Shuttle Booking plugin's own search and booking flow, restyled to match the theme. |
| Popular routes | [easyshuttle_routes] | 11 | Route cards built from your published shuttles: stops, timetable, seats and fare. |
| Shuttle listing | [easyshuttle_shuttle_list] | 9 | The full route network with keyword, service type, vehicle and fare filters. |
| How booking works | [easyshuttle_steps] | 5 | Numbered steps that mirror the order of the booking form. |
| Stats band | [easyshuttle_stats] | 1 | Four large numbers on the dark green band. |
| Fleet split | [easyshuttle_fleet] | 11 | Image panel beside copy and a checklist, with the caption plate. |
| Testimonials | [easyshuttle_testimonials] | 4 | Rider quotes with initials or a photo. |
| CTA band | [easyshuttle_cta_band] | 7 | Wide dark call-to-action band with up to two buttons. |
| Page hero | [easyshuttle_page_hero] | 5 | Dark inner-page header with breadcrumbs. Falls back to the page title. |
| About intro | [easyshuttle_about_intro] | 9 | The story split panel with a checklist and caption plate. |
| Timeline | [easyshuttle_story] | 5 | Dated milestones down a vertical rule. |
| Team | [easyshuttle_team] | 4 | Staff cards with a photo or initials. |
| FAQ | [easyshuttle_faq] | 4 | Accordion of common questions. |
| Feature cards | [easyshuttle_features] | 2 | Three short icon cards, used on the contact page. |
| Contact form | [easyshuttle_contact_form] | 3 | Accessible enquiry form. Sends through WordPress mail or Brevo. |
| Contact details | [easyshuttle_contact_cards] | 2 | Dark details card plus the map panel. |
| Opening hours | [easyshuttle_business_hours] | 1 | Counter hours, with consecutive matching days grouped. |
| Latest posts | [easyshuttle_blog_posts] | 6 | Recent posts as cards. |
| Booking flow | [easyshuttle_booking_flow] | 1 | The plugin's full booking flow inside the theme's booking shell. |
| Service page flow | [easyshuttle_service_flow] | 7 | One shuttle type end to end: a search form locked to it, and the departures still running today. |
| Find my booking | [easyshuttle_find_booking] | 5 | The plugin's booking lookup, in the theme's search card. |
| My bookings | [easyshuttle_my_bookings] | 3 | The plugin's customer booking dashboard, restyled. |
Options API
Everything is stored in one option, easyshuttle_theme_options, read by dot path:
easyshuttle_option( 'header.sticky' ); // one value, with its default
easyshuttle_option( 'brand.site_name', 'Fallback' );
easyshuttle_get_options(); // the whole merged tree
Three behaviours are worth knowing before you touch the save path:
- Defaults are merged, not copied. A key absent from the database falls back
to
easyshuttle_default_options(), so upgrades that add a setting need no migration. - Saving is atomic across panels. All panels post together; a hidden
panelfield ofalltells the sanitiser every checkbox was on screen, which is how unticking one records as off. - A stale form is refused. Each form carries a revision hash; if the options changed elsewhere while you were editing, the save is rejected rather than clobbering.
Reading in the same request you save.
easyshuttle_get_options() memoises in a static, so a save and a read inside one PHP
process disagree. In normal use options.php redirects, so this only bites in tests and CLI.
Hooks
The theme's own extension points. Everything else is standard WordPress.
| Hook | Kind | Declared in |
|---|---|---|
easyshuttle_auto_service_submenu | filter | inc/helpers.php:432 |
easyshuttle_breadcrumb_trail | filter | inc/helpers.php:737 |
easyshuttle_contact_delivery_failed | action | inc/class-easyshuttle-email-delivery.php:546 |
easyshuttle_contact_rate_limits | filter | inc/class-easyshuttle-email-delivery.php:573 |
easyshuttle_default_art | filter | inc/defaults.php:503 |
easyshuttle_email_delivery_config | filter | inc/class-easyshuttle-email-delivery.php:85 |
easyshuttle_email_palette | filter | inc/class-easyshuttle-email-delivery.php:310 |
easyshuttle_needs_plugin_assets | filter | inc/setup.php:286 |
easyshuttle_section_atts | filter | inc/sections.php:716 |
easyshuttle_section_html | filter | inc/sections.php:818 |
easyshuttle_sections | filter | inc/sections.php:633 |
easyshuttle_seo_defer_to_plugin | filter | inc/class-easyshuttle-seo.php:257 |
easyshuttle_seo_description | filter | inc/class-easyshuttle-seo.php:1056 |
easyshuttle_seo_enabled | filter | inc/class-easyshuttle-seo.php:231 |
easyshuttle_seo_meta_post_types | filter | inc/class-easyshuttle-seo-meta.php:51 |
easyshuttle_seo_schema_graph | filter | inc/class-easyshuttle-seo.php:674 |
easyshuttle_shuttle | filter | inc/shuttle-data.php:502 |
easyshuttle_suppressed_onboarding_transients | filter | inc/class-easyshuttle-setup-wizard.php:295 |
easyshuttle_use_builder_content | filter | inc/helpers.php:579 |
SEO module
On by default and self-effacing: easyshuttle_seo_defer_to_plugin lets it stand
down when Yoast, Rank Math or similar is active, so tags are never printed twice. When it owns
the output it emits the title, meta description, canonical, Open Graph and Twitter cards, and a
JSON-LD graph of Organization, WebSite, WebPage and BreadcrumbList — plus route data on shuttle
pages.
- Titles cap at 70 characters, descriptions at 160.
- Search results and 404s are noindexed; per-post overrides live in the editor sidebar.
- Hidden WooCommerce products — the ones the booking plugin mints per shuttle to carry a price — are kept out of both the sitemap and the index. Without that they are submitted to search engines as thin, machine-named pages.
Extending
Add a section
Declare it, then write its renderer. Nothing else is needed for the shortcode, block, Elementor and Divi versions to appear.
add_filter( 'easyshuttle_sections', function ( $sections ) {
$sections['notice'] = array(
'label' => 'Service notice',
'description' => 'A single line above the fold.',
'icon' => 'eicon-alert',
'fields' => array(
'text' => easyshuttle_field( 'text', 'Message' ),
),
);
return $sections;
} );
function easyshuttle_section_notice( array $a ) {
printf( '<p class="es-notice">%s</p>', esc_html( $a['text'] ) );
}
Replace the stand-in artwork
Empty image slots fall back to bundled SVGs rather than an empty panel. Swap them site-wide without touching a section:
add_filter( 'easyshuttle_default_art', function ( $url, $name ) {
return 'fleet' === $name ? get_stylesheet_directory_uri() . '/my-fleet.jpg' : $url;
}, 10, 2 );
Tests
The theme ships its own checks. Run them after any change:
php wp-content/themes/easyshuttle/tests/static-checks.php # no WordPress needed
wp eval-file wp-content/themes/easyshuttle/tests/wp-integration.php
The static suite asserts the things that break quietly — escaping on output, asset paths that exist, the section registry matching its renderers, and the admin behaviours that have regressed before. The integration suite is read-only apart from an options round trip it restores.