Case Study: Greensun Hotel Theme V2
A complete WordPress theme rebuild for a Makati hotel — custom Gutenberg blocks, multi-step booking wizard, and bidirectional Aiosell integration for real-time room inventory.

Role: Lead Developer
Timeline: July–September 2026
Stack: WordPress, PHP 8.1, React, Tailwind CSS, Advanced Custom Fields, Aiosell API
Live Site: https://greensun.com.ph/
Repo: https://github.com/maumaun30/greensun-hotel-theme-v2
Overview
Green Sun Hotel needed to replace their aging v1 theme with a redesign that matched a new Figma spec, supported sophisticated inventory management through an external channel manager, and gave staff control over rooms, promos, and careers without touching code. The result is a 50+ custom Gutenberg block library, three public content types, a wizard-driven booking flow, and full integration with Aiosell's reservation platform.
The theme supports both mock mode (for development) and live mode (bidirectional sync with Aiosell), with a standalone fallback when the API is unreachable. Every state change — booking attempts, webhook deliveries, inventory pushes — is logged to an audit table.
The Challenge
The hotel operates in a competitive Makati market where direct bookings carry higher margins than OTA placements. The site had to:
- Present 130+ rooms with real-time availability across seven room types
- Let guests search dates, pick rooms, and submit reservations in a single flow without page reloads
- Sync with Aiosell's channel manager: push inventory changes from WordPress, receive confirmed bookings via webhook
- Support event inquiries for seven venues (The Eye, Axon, Focus Room, plus smaller spaces)
- Allow staff to publish promos and job postings without developer intervention
- Work offline (standalone mode) when the API is unavailable, with a clear admin indicator of the current state
The original theme was a monolithic PHP template with hardcoded sections. The redesign demanded a composable block-based architecture so marketing could iterate layouts without deploying code.
What I Built
Block-Based Architecture
I built 50+ server-rendered Gutenberg blocks in React, each living in its own folder under assets/js/blocks/<slug>/ with a block.json, edit.js, render.php, and optional style.css. A scan-and-register system in functions.php auto-loads every block at runtime — adding a new block means creating the folder, no manual registration.
Blocks fall into seven categories:
- Home page:
home-hero(crossfade carousel),home-about,home-amenities,home-events,home-location - Booking:
booking-bar(mini search form),booking-flow(five-step wizard: search → pick room → add-ons → contact details → summary) - Rooms:
rooms-preview(card grid),rooms-archive(alternating rows),room-detail(hero + sticky reserve card),room-tabs(amenities, policies) - Events:
venue-detail,venue-showcase,event-inquiry,occasion-cards,pricing-packages - Careers:
careers-list,job-detail(with inline application form) - Blog & search:
blog-list,post-detail(prose + share bar + sticky CTA sidebar),search-results(grouped by type) - Content:
page-hero,prose,feature-split,about-story,values-grid,team-grid,cta-section,contact-form,gallery-grid,reviews
Each block is built independently by a custom script that wraps @wordpress/scripts, producing per-block bundles instead of a single monolith.
Custom Post Types
I created six CPTs:
- Rooms (
room): public, REST-enabled, ACF fields for capacity, pricing, Aiosell room code, amenities. Powers both the rooms archive and the booking wizard's room picker. - Promos (
promo): public, ordered bymenu_order, ACF fields for discount amount, validity period, featured image. - Jobs (
job): public, classic editor for prose job descriptions, ACF fields for department, location, employment type. Own capability set so the Careers Manager role can post jobs without site-wide edit rights. - Reservations (
gs_reservation): private, written by the booking flow and Aiosell webhooks. Stores guest details, selected rooms, dates, total amount, confirmation code. - Submissions (
gs_submission): private, captures contact form leads. - Applications (
gs_application): private, attached to jobs, stores applicant details and a private résumé file.
Rooms and Jobs both use the classic editor (TinyMCE) instead of Gutenberg for the body field. I restored prose typography inside the editor with editor-classic.css, undoing Tailwind's aggressive preflight resets.
Aiosell Integration
The booking system has three modes:
- Mock: No API calls; returns canned availability and accepts all bookings. Used in development.
- Standalone: Local inventory table mirroring room counts and blocked dates. Runs when Aiosell is unreachable.
- Live: Bidirectional sync with Aiosell.
I built Greensun_Aiosell_Client as a mode-aware HTTP client that switches behavior based on settings. In live mode:
- Outbound: WordPress pushes room inventory changes (new room, capacity update, manual block) to Aiosell via
POST /roomsandPOST /availability. - Inbound: Aiosell delivers confirmed bookings to
/wp-json/greensun/v1/aiosell/webhook. The handler validates the HMAC signature, creates ags_reservationpost, sends guest and staff emails, and returns a 200. - Search: The booking wizard calls Aiosell's search endpoint with arrival, departure, and guest count; the API returns available room types with live pricing.
Every state change — push attempt, webhook delivery, mode switch — is written to a gs_audit_log table with microsecond timestamps. The Activity Log screen (admin/activity-log.php) surfaces this as a filterable table.
Payload assembly lives in api/aiosell-payload.php as pure functions (WordPress-free) so I could unit-test them in isolation under tests/aiosell/. The client, store, and REST routes are WordPress-coupled.
Booking Flow
The booking-flow block renders a five-step wizard:
- Search: Arrival, departure, rooms, guests. Submits to Aiosell search or standalone inventory.
- Select Room: Grid of available room types with pricing. Multi-select when booking multiple rooms.
- Add-ons: Optional breakfast, late checkout, extra bed (checkboxes that update the total in real time).
- Contact Details: Name, email, phone, special requests. Client-side validation.
- Summary: Line-item breakdown, terms acceptance, submit button.
On submit, booking-flow.js POSTs to /wp-json/greensun/v1/booking. The endpoint:
- Validates input
- Checks availability again (anti-race)
- Creates a
gs_reservationpost with statuspending - In live mode, pushes the reservation to Aiosell and waits for a confirmation code
- Sends guest confirmation email and staff notification
- Returns success + reservation ID
The wizard then swaps to a success state with the confirmation code and a "View Booking" link.
In standalone mode, the endpoint decrements the local inventory table and skips the Aiosell push.
Email System
I built a template registry in inc/emails/ with a renderer that composes HTML emails from reusable header/footer partials and per-template bodies. Templates are editable at Greensun → Email Templates (ACF options page), with Mustache-style placeholders like {{guest_name}} and {{confirmation_code}}.
The system supports:
- Booking confirmations (guest + staff)
- Contact form submissions
- Job applications (applicant + hiring manager)
- Promo announcements (optional manual sends)
Emails are sent via Brevo (formerly Sendinblue) with credentials stored at Greensun → Email (Brevo). The sender falls back to wp_mail() when Brevo is unconfigured.
Admin Experience
I created a top-level Greensun menu with:
- Dashboard: Summary of pending reservations, recent submissions, active promos, open jobs.
- Activity Log: Filterable audit table (booking attempts, API calls, webhook deliveries).
- Booking (Aiosell): Mode switch, API credentials, webhook secret.
- Email (Brevo): SMTP credentials, sender name/email.
- Email Templates: Editable templates with live placeholders.
- Theme Settings: Global CTAs, footer text, social links.
Reservations, Submissions, and Applications appear as submenus under Greensun. Rooms, Promos, and Careers get their own top-level menus.
An admin bar badge shows the current mode (Mock / Standalone / Live) and payment gateway (Maya Sandbox / Maya Live) at a glance, with color coding (gray / yellow / green). A dismissible admin notice appears when not in production mode.
I added a "Hide from search" checkbox to all post types (custom meta box) so staff can exclude pages, posts, rooms, or promos from site search without unpublishing them.
Technical Highlights
CSS pipeline: I inlined critical.min.css (above-the-fold styles: header, footer, focus rings, reveal animations) into <head> via a wp_head hook at priority 1, eliminating a render-blocking request. main.min.css loads as a separate file with cache-busting. Both are compiled from a Tailwind source with custom @layer components for buttons, cards, and form controls.
Block editing restrictions: I locked the Gutenberg inserter on CPT edit screens and "layout" pages (those tagged with a layout-page term) to a curated subset of blocks — no core/paragraph on the home page, no home-hero on a room detail. The system reads an allow-list per post type from inc/block-editing.php.
Search engine: The main search query (inc/search.php) targets titles, excerpts, post content, and ACF text fields across posts, pages, rooms, promos, and jobs. Results are grouped by type in the search-results block, with a "Did you mean" suggestion parser for common misspellings.
Performance: Fonts (Recoleta + Lekton) are self-hosted under assets/fonts/ and preloaded via <link rel="preload">. Lenis smooth scroll loads from CDN, deferred. All block scripts are deferred. The home page's hero carousel and testimonials carousel load only when their container intersects the viewport.
Deployment: GitHub Actions (deploy.yml, deploy-staging.yml) run npm run build and rsync the built theme to Cloudways on every push to main or staging. Secrets (DEPLOY_HOST, DEPLOY_PATH, SSH_PRIVATE_KEY) are stored in the repo settings.
Outcome
[add outcome]
Screenshots

