Skip to content

Departures daily, 6:00 AM – 11:45 PM

EasyShuttle

Documentation

WordPress theme · Manual

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.

12Option panels
178Settings
23Sections
23Shortcodes
19Hooks
12Starter pages

Using the theme

For whoever runs the site

How 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

PluginStatusWhy
Magepeople Shuttle BookingRequiredRoutes, timetables, fares and the booking flow all come from this plugin. The theme displays them.
Shuttle Booking ProNot releasedAdds PDF tickets, calendar export, booking chat and extra payment gateways.
WooCommerceOptionalOptional. Only needed if you take payment through WooCommerce rather than the plugin's own checkout.
ElementorOptionalOptional. 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.

  1. 1
    PluginsThe booking engine
  2. 2
    PagesHome, routes, booking
  3. 3
    MenuHeader navigation
  4. 4
    Demo contentSample routes
  5. 5
    FinishStyle 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.

PageSlugTemplate
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 panels

Theme 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 fields

Identity and the global look: what the header shows, the five colours every section inherits, page width, corner radius and motion.

FieldStored asTypeWhat it does
Logobrand.logo_idimage
Site name overridebrand.site_nametextUsed in branding and structured data. Defaults to the WordPress site title.
Logo mark textbrand.mark_texttextTwo or three characters, shown when no logo image is set.
Wordmark, first partbrand.name_leadtext
Wordmark, highlighted partbrand.name_accenttext
Taglinebrand.taglinemulti-lineShown under the logo in the footer.
Primary (forest green)colors.primarycolour
Primary darkcolors.primary_darkcolour
Accent (orange)colors.accentcolour
Highlight (amber)colors.highlightcolour
Page backgroundcolors.backgroundcolour
Content widthlayout.content_widthnumberPixels. The design is drawn for 1200.
Section spacinglayout.spacingselectOptions: Compact · Comfortable · Spacious
Corner roundinglayout.radiusselectOptions: Square · Rounded · Soft
Motionlayout.motionselectVisitors who ask their system for reduced motion always get it, whatever is chosen here. Options: Full animation · Reduced

Header

8 fields

The top bar and the navigation row, including the click-to-call number and the booking button.

FieldStored asTypeWhat it does
Top barheader.topbartoggle
Top bar noteheader.topbar_notetext
Phone numberheader.phonetextShown in the top bar as a click-to-call link.
Sticky headerheader.stickytoggle
Header button labelheader.cta_labeltext
Header button linkheader.cta_urlURLLeave empty to link to the booking page.
Booking lookup linkheader.show_lookuptoggle
Lookup link labelheader.lookup_labeltext

Homepage

22 fields

Which of the homepage bands appear, and the copy inside each one.

FieldStored asTypeWhat it does
Hero background imagehomepage.hero_image_idimage
Hero eyebrowhomepage.hero_eyebrowtext
Headline, first parthomepage.hero_title_beforetext
Headline, highlighted parthomepage.hero_title_accenttext
Headline, last parthomepage.hero_title_aftertext
Hero leadhomepage.hero_leadmulti-line
Hero reassurance listhomepage.hero_metamulti-lineOne item per line.
Hero button labelhomepage.hero_cta_labeltextLeave empty to hide the hero button.
Hero button linkhomepage.hero_cta_urlURL
Departure board titlehomepage.board_titletext
Departure board notehomepage.board_notetext
Departures to showhomepage.board_limitnumber
Search card eyebrowhomepage.search_eyebrowtext
Search card titlehomepage.search_titletext
Search card subtitlehomepage.search_subtitlemulti-line
Booking searchhomepage.show_searchtoggle
Popular routeshomepage.show_routestoggle
How booking workshomepage.show_stepstoggle
Stats bandhomepage.show_statstoggle
Fleet sectionhomepage.show_fleettoggle
Testimonialshomepage.show_testimonialstoggle
CTA bandhomepage.show_ctatoggle

Routes & listing

10 fields

The route cards — on the homepage, the listing page and anywhere the shortcode is used.

FieldStored asTypeWhat it does
Eyebrowroutes.eyebrowtext
Titleroutes.titletext
Introroutes.intromulti-line
Routes on the homepageroutes.limitnumber
Columnsroutes.columnsselectOptions: Two · Three · Four
Featured imagesroutes.show_imagestoggle
"See all" buttonroutes.show_buttontoggle
Button labelroutes.button_labeltext
Routes on the listing pageroutes.archive_limitnumberUse -1 for every published shuttle.
Listing filtersroutes.show_filterstoggle

Shuttle page

15 fields

The single shuttle page: which panels appear beside the fare box.

FieldStored asTypeWhat it does
About panel titlesingle.about_titletext
About panel leadsingle.about_leadmulti-line
Timetable panel titlesingle.timetable_titletext
Timetable panel leadsingle.timetable_leadmulti-line
On-board panelsingle.show_amenitiestoggle
On-board panel titlesingle.amenity_titletext
On-board panel leadsingle.amenity_leadmulti-line
Amenitiessingle.amenitiesmulti-lineOne per line. The booking plugin stores no amenity data, so these are set here and shown on every route page.
Route mapsingle.show_maptoggle
Questions panelsingle.show_faqtoggle
Questions panel titlesingle.faq_titletext
Questions panel leadsingle.faq_leadmulti-line
Fare sidebar notesingle.sidebar_notetext
Counter card titlesingle.counter_titletext
Counter card textsingle.counter_textmulti-line

Sections

60 fields

Copy for the standing pages — fleet, about, story, team, FAQ, testimonials, stats and the CTA band. The largest panel by far.

FieldStored asTypeWhat it does
How booking works
Eyebrowsteps.eyebrowtext
Titlesteps.titletext
Introsteps.intromulti-line
Stepssteps.itemsrepeaterLeave a row empty to hide it. Columns: Title · Text · 4 rows
Stats band
Statsstats.itemsrepeaterLeave a row empty to hide it. Columns: Value · Label · 4 rows
Fleet
Eyebrowfleet.eyebrowtext
Titlefleet.titletext
Body copyfleet.textmulti-line
Checklistfleet.checklistmulti-lineOne item per line.
Fleet photofleet.image_idimage
Plate labelfleet.plate_labeltext
Plate textfleet.plate_texttext
Button labelfleet.button_labeltext
Button linkfleet.button_urlURL
Image firstfleet.fliptoggle
Testimonials
Eyebrowtestimonials.eyebrowtext
Titletestimonials.titletext
Quotestestimonials.itemsrepeaterLeave a row empty to hide it. Columns: Quote · Name · Role · Photo · 3 rows
Call to action
Eyebrowcta.eyebrowtext
Titlecta.titletext
Textcta.textmulti-line
Primary buttoncta.button_labeltext
Primary linkcta.button_urlURL
Secondary buttoncta.button2_labeltext
Secondary linkcta.button2_urlURL
About page
Eyebrowabout.eyebrowtext
Titleabout.titletext
Body copyabout.textmulti-lineLeave a blank line between paragraphs.
Checklistabout.checklistmulti-lineOne item per line.
About photoabout.image_idimage
Plate labelabout.plate_labeltext
Plate textabout.plate_texttext
Stats bandabout.show_statstoggle
Timelineabout.show_storytoggle
Teamabout.show_teamtoggle
FAQabout.show_faqtoggle
Timeline
Eyebrowstory.eyebrowtext
Titlestory.titletext
Introstory.intromulti-line
Milestonesstory.itemsrepeaterLeave a row empty to hide it. Columns: Year · Title · Text · 6 rows
Team
Eyebrowteam.eyebrowtext
Titleteam.titletext
Peopleteam.itemsrepeaterLeave a row empty to hide it. Columns: Name · Role · Photo · 4 rows
FAQ
Eyebrowfaq.eyebrowtext
Titlefaq.titletext
Questionsfaq.itemsrepeaterLeave a row empty to hide it. Columns: Question · Answer · 6 rows
News
Eyebrowblog.eyebrowtext
Titleblog.titletext
Introblog.intromulti-line
Columnsblog.columnsselectOptions: Two · Three

Contact

21 fields

The contact page: the form, the details card, opening hours and the map.

FieldStored asTypeWhat it does
Details card titlecontact.eyebrowtext
Form titlecontact.form_titletext
Form leadcontact.form_leadmulti-line
Enquiry topicscontact.topicsmulti-lineOne per line.
Counter addresscontact.addressmulti-lineShown on the contact card, and the map is built from it. Leave this and Map location both empty and no map is drawn.
Phonecontact.phonetext
Public emailcontact.email
Counter hours summarycontact.hourstext
Corporate desk hourscontact.corporatetext
Opening hours card titlecontact.hours_titletext
Mondaycontact.hours_montext
Tuesdaycontact.hours_tuetext
Wednesdaycontact.hours_wedtext
Thursdaycontact.hours_thutext
Fridaycontact.hours_fritext
Saturdaycontact.hours_sattext
Sundaycontact.hours_suntext
Map locationcontact.map_querytextAn 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 labelcontact.map_labeltext
Help cardscontact.show_featurestoggle
Map loadingcontact.map_loadingselectAsking first keeps Google from setting cookies before a visitor opts in. Options: Ask before loading (recommended) · Load immediately

Email

12 fields

Where contact messages go and how they are sent, including the Brevo API path.

FieldStored asTypeWhat it does
Delivery methodemail.providerselectBrevo sends over HTTPS, which survives hosts that block outbound SMTP. Options: WordPress mail · Brevo API
Brevo API keyemail.brevo_api_keytextPaste the v3 API key from your Brevo account.
Send messages tocontact.form_toDefaults to the public email, then the WordPress admin address.
Sender nameemail.sender_nametext
Sender addressemail.sender_email
Notification subjectemail.admin_subjecttextPlaceholders: {name}, {topic}, {site_name}.
Confirmation emailemail.customer_confirmationtoggle
Confirmation subjectemail.customer_subjecttext
Confirmation messageemail.customer_messagemulti-linePlaceholders: {name}, {topic}, {site_name}.
Messages per visitoremail.rate_limit_clientnumber
Messages per addressemail.rate_limit_recipientnumber
Rate limit window (minutes)email.rate_limit_windownumber

SEO

10 fields

Titles, descriptions, the share card and structured data.

FieldStored asTypeWhat it does
Built-in SEOseo.enabletoggle
Structured dataseo.output_schematoggle
Hide search resultsseo.noindex_searchtoggle
Homepage titleseo.home_titletextLeave empty to use the site title and tagline.
Homepage descriptionseo.home_descriptionmulti-lineAround 150 characters.
Title separatorseo.title_separatortext
Default sharing imageseo.share_image_idimage
X / Twitter handleseo.twitter_sitetext
Price rangeseo.price_rangetextShown in structured data, for example "$$".
Business typeseo.business_typeselectOptions: Local business · Taxi service · Travel agency · Transportation service

Pages

2 fields

Which page holds the booking flow, when it cannot be detected automatically.

FieldStored asTypeWhat it does
Booking pagepages.booking_pageselectThe 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 pagestextMy bookings: not found — the plugin creates this page itself.Find my booking: not found — the plugin creates this page itself.

Builders

0 fields

Switch 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 it

Architecture

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.

SectionShortcodeFieldsRenders
Hero[easyshuttle_hero]13Dark hero with the headline and a live departure board pulled from your shuttles.
Booking search[easyshuttle_search]6The Shuttle Booking plugin's own search and booking flow, restyled to match the theme.
Popular routes[easyshuttle_routes]11Route cards built from your published shuttles: stops, timetable, seats and fare.
Shuttle listing[easyshuttle_shuttle_list]9The full route network with keyword, service type, vehicle and fare filters.
How booking works[easyshuttle_steps]5Numbered steps that mirror the order of the booking form.
Stats band[easyshuttle_stats]1Four large numbers on the dark green band.
Fleet split[easyshuttle_fleet]11Image panel beside copy and a checklist, with the caption plate.
Testimonials[easyshuttle_testimonials]4Rider quotes with initials or a photo.
CTA band[easyshuttle_cta_band]7Wide dark call-to-action band with up to two buttons.
Page hero[easyshuttle_page_hero]5Dark inner-page header with breadcrumbs. Falls back to the page title.
About intro[easyshuttle_about_intro]9The story split panel with a checklist and caption plate.
Timeline[easyshuttle_story]5Dated milestones down a vertical rule.
Team[easyshuttle_team]4Staff cards with a photo or initials.
FAQ[easyshuttle_faq]4Accordion of common questions.
Feature cards[easyshuttle_features]2Three short icon cards, used on the contact page.
Contact form[easyshuttle_contact_form]3Accessible enquiry form. Sends through WordPress mail or Brevo.
Contact details[easyshuttle_contact_cards]2Dark details card plus the map panel.
Opening hours[easyshuttle_business_hours]1Counter hours, with consecutive matching days grouped.
Latest posts[easyshuttle_blog_posts]6Recent posts as cards.
Booking flow[easyshuttle_booking_flow]1The plugin's full booking flow inside the theme's booking shell.
Service page flow[easyshuttle_service_flow]7One shuttle type end to end: a search form locked to it, and the departures still running today.
Find my booking[easyshuttle_find_booking]5The plugin's booking lookup, in the theme's search card.
My bookings[easyshuttle_my_bookings]3The 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 panel field of all tells 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.

HookKindDeclared in
easyshuttle_auto_service_submenufilterinc/helpers.php:432
easyshuttle_breadcrumb_trailfilterinc/helpers.php:737
easyshuttle_contact_delivery_failedactioninc/class-easyshuttle-email-delivery.php:546
easyshuttle_contact_rate_limitsfilterinc/class-easyshuttle-email-delivery.php:573
easyshuttle_default_artfilterinc/defaults.php:503
easyshuttle_email_delivery_configfilterinc/class-easyshuttle-email-delivery.php:85
easyshuttle_email_palettefilterinc/class-easyshuttle-email-delivery.php:310
easyshuttle_needs_plugin_assetsfilterinc/setup.php:286
easyshuttle_section_attsfilterinc/sections.php:716
easyshuttle_section_htmlfilterinc/sections.php:818
easyshuttle_sectionsfilterinc/sections.php:633
easyshuttle_seo_defer_to_pluginfilterinc/class-easyshuttle-seo.php:257
easyshuttle_seo_descriptionfilterinc/class-easyshuttle-seo.php:1056
easyshuttle_seo_enabledfilterinc/class-easyshuttle-seo.php:231
easyshuttle_seo_meta_post_typesfilterinc/class-easyshuttle-seo-meta.php:51
easyshuttle_seo_schema_graphfilterinc/class-easyshuttle-seo.php:674
easyshuttle_shuttlefilterinc/shuttle-data.php:502
easyshuttle_suppressed_onboarding_transientsfilterinc/class-easyshuttle-setup-wizard.php:295
easyshuttle_use_builder_contentfilterinc/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.

/** * Rail navigation for the generated manual. * * One source for all three places the manual appears — the standalone file, the * admin screen and the public page. In WordPress the manual's body is passed * through wp_kses_post(), which strips , so the behaviour has to arrive * as an enqueued asset rather than inline markup. * * @package EasyShuttle */ /* * Mark the rail entry for the section being read. * * Position maths rather than IntersectionObserver: the observer needs a * rootMargin tuned to the viewport, marks nothing until the first scroll, and * silently marks nothing at all when the margins collapse the root box. This * always resolves to exactly one entry, at any height, including on load. */ ( function () { var links = Array.prototype.slice.call( document.querySelectorAll( '.rail a[href^="#"]' ) ); if ( ! links.length ) { return; } var entries = []; links.forEach( function ( link ) { var el = document.getElementById( decodeURIComponent( link.getAttribute( 'href' ).slice( 1 ) ) ); if ( el ) { entries.push( { link: link, el: el } ); } } ); if ( ! entries.length ) { return; } var current = null; /** * How far below the top a jumped-to heading lands. * * Read from the same --jump custom property the scroll offset uses, so a * page with a tall sticky header does not mark the section above the one * the reader is looking at. A little slack covers rounding. */ function threshold() { var raw = getComputedStyle( entries[ 0 ].el ).scrollMarginTop; var n = parseInt( raw, 10 ); return ( isNaN( n ) ? 28 : n ) + 12; } function update() { var pick = entries[ 0 ]; var limit = threshold(); for ( var i = 0; i < entries.length; i++ ) { // The last heading that has passed the top of the viewport. if ( entries[ i ].el.getBoundingClientRect().top = document.body.scrollHeight - 2 ) { pick = entries[ entries.length - 1 ]; } if ( pick.link === current ) { return; } if ( current ) { current.classList.remove( 'is-current' ); } pick.link.classList.add( 'is-current' ); current = pick.link; } /* * Called straight from the scroll handler rather than deferred into a * requestAnimationFrame. A "already queued" flag that is only cleared * inside the rAF callback sticks on forever the moment one callback does * not run — a background tab, a throttled frame — and every later scroll * is then ignored. Reading a couple of dozen rects is cheap enough that * the guard cost more than the work. */ window.addEventListener( 'scroll', update, { passive: true } ); window.addEventListener( 'resize', update ); // A page opened on an anchor has not finished jumping when this runs, and // following a rail link changes the hash without scrolling past anything. window.addEventListener( 'load', update ); window.addEventListener( 'hashchange', update ); update(); }() );