Bukas Hours: A Free WordPress Plugin That Tells Customers If You're Open Right Now
Before driving to a shop, clinic or municipal office, most people check the website first. Too often they find a static list of hours that ignores the noon break, the holiday and page caching, and they arrive to a locked door. In this breakdown I walk through how I built Bukas Hours, a free WordPress plugin that tells visitors in real time whether a business is open, closing soon or closed.
Bukas is Filipino for open. The plugin handles split shifts, Philippine holidays and special hours, prints Google-ready opening-hours schema, and stays accurate even when a caching plugin serves a page generated hours earlier.
Try It: Drag the Clock
This is what the plugin renders on a sample bakery's Contact page. Move the time slider or tap a preset and watch the badge, the door sign and the highlighted day change.
Build Footprint & Quality Checks
Small-business sites often run on cheap shared hosting, so I kept the plugin light. These are the measured numbers from the v1.0.0 build:
Technology Stack & Architecture Layers
WordPress 6.3+ APIs
Settings API for the admin screen, Shortcode API for three shortcodes, and Block API v3 for a dynamic block.
PHP 7.4+ DateTimeImmutable
Timezone-correct schedule engine that turns weekly hours and holiday rows into exact open and close times.
Vanilla JS & Intl
A 3 KB script recalculates the badge every minute with Intl.DateTimeFormat in the business timezone.
Schema.org JSON-LD
LocalBusiness markup with openingHoursSpecification and specialOpeningHoursSpecification for holidays.
Key Architectural Decisions
Cache-Safe Status
PHP embeds the next eight days of opening ranges as timestamps. The browser compares them to the clock, so a page cached at 11:55 AM still says "Closed" at 3:00 AM.
Sanitize In, Escape Out
Strict HH:MM and date validation, allow-listed timezones and business types, nonces via the Settings API, and late escaping on every output.
One Pure Logic Class
All time math lives in Bukas_Hours_Schedule, which has no output. Shortcodes, the block and the schema just render its results.
No Build Step
The block is written against WordPress globals and rendered by PHP, so the plugin zip installs as-is. Assets load only on pages that show hours.
Core Status Logic, Live Badge & Shortcodes
Here are the three pieces that make the badge work: the PHP status check, the browser-side recalculation, and the shortcodes you place on a page.
// includes/class-bukas-hours-schedule.php
public function status( ?DateTimeImmutable $now = null ) {
$now = $now ? $now : $this->now();
$soon = max( 0, (int) $this->settings['closing_soon'] ) * MINUTE_IN_SECONDS;
$ts = $now->getTimestamp();
foreach ( $this->ranges_for( $now ) as $r ) {
if ( $ts >= $r[0]->getTimestamp() && $ts < $r[1]->getTimestamp() ) {
$left = $r[1]->getTimestamp() - $ts;
return array(
'state' => ( $left <= $soon ) ? 'closing_soon' : 'open',
'next_change' => $r[1],
);
}
}
return array(
'state' => 'closed',
'next_change' => $this->next_opening( $now ),
);
}
// assets/js/bukas-hours.js — runs in the visitor's browser
/** Pure status calculation; exported on window for testing. */
function compute( cfg, nowTs ) {
var soon = ( cfg.soon || 0 ) * 60;
for ( var i = 0; i < cfg.ranges.length; i++ ) {
var r = cfg.ranges[ i ];
if ( nowTs >= r[ 0 ] && nowTs < r[ 1 ] ) {
return { state: r[ 1 ] - nowTs <= soon ? 'closing_soon' : 'open', next: r[ 1 ] };
}
}
for ( var j = 0; j < cfg.ranges.length; j++ ) {
if ( cfg.ranges[ j ][ 0 ] > nowTs ) {
return { state: 'closed', next: cfg.ranges[ j ][ 0 ] };
}
}
return { state: 'closed', next: null };
}
<!-- Header: live badge -->
[bukas_status]
<!-- Contact page: weekly hours, today highlighted -->
[bukas_hours]
<!-- Footer or notice: next 5 holidays / special hours -->
[bukas_closures limit="5"]
<!-- Block editor: add the "Bukas Hours" block (Widgets category)
and choose Badge, Hours, Badge + Hours, or All -->
// Optional: extend the LocalBusiness schema from your theme
add_filter( 'bukas_hours_schema', function ( $data ) {
$data['priceRange'] = '₱₱';
return $data;
} );
Development Roadmap & Milestones
Version 1.0.0 is released. The next milestones come straight from the project's roadmap:
v1.0.0: Core Plugin (Released)
Weekly hours with noon breaks, holidays and special hours, PH holiday helper, live cache-safe badge, three shortcodes, a dynamic block and LocalBusiness schema.
Next: Overnight Shifts & Notices
Support hours that run past midnight (6 PM – 2 AM) and a temporary "Closed today for inventory" banner with an expiry date.
Planned: Status REST API
A /wp-json/bukas-hours/v1/status endpoint so chatbots, Messenger auto-replies and n8n workflows can answer "Bukas pa ba kayo?"
Planned: Branches & WooCommerce
Multiple locations with their own hours, and an option to show a notice or pause checkout while the store is closed.
User Interface & Feature Showcase
Project Links, Code & Documentation
Download the Plugin
Get bukas-hours.zip and install it in wp-admin under Plugins → Add New → Upload Plugin. Then open Settings → Bukas Hours.
GitHub Repository
Browse the source, open an issue or suggest a feature. The code is free and open source under GPL-2.0-or-later.
Tutorial & Docs
A beginner-friendly walkthrough of every file, plus deployment steps for LocalWP, wp-env and GitHub releases.
Small Plugin, Real Problem Solved
A business-hours widget sounds simple until you add lunch breaks, holidays, timezones and page caching. Bukas Hours handles all four so customers stop arriving to a locked door. Want it set up on your business website, or a custom version for multiple branches?
The sample bakery in the demo is fictional. The holiday helper adds only fixed-date regular holidays, so check the year's official proclamation for movable and special non-working days.

Comments
Post a Comment