<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[NodeThemes]]></title><description><![CDATA[NodeThemes]]></description><link>https://nodethemes.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 04:13:45 GMT</lastBuildDate><atom:link href="https://nodethemes.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What a booking system on WordPress really has to handle (lessons from building one)]]></title><description><![CDATA["Let people pick a time" sounds like a one-day feature. We build WordPress themes at NodeThemes, and when we added appointment booking and a class schedule…]]></description><link>https://nodethemes.hashnode.dev/what-a-booking-system-on-wordpress-really-has-to-handle-lessons-from-building-one</link><guid isPermaLink="true">https://nodethemes.hashnode.dev/what-a-booking-system-on-wordpress-really-has-to-handle-lessons-from-building-one</guid><category><![CDATA[WordPress]]></category><category><![CDATA[PHP]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[nodethemescom]]></dc:creator><pubDate>Tue, 03 Nov 2026 20:09:34 GMT</pubDate><content:encoded><![CDATA[<p>"Let people pick a time" sounds like a one-day feature. We build WordPress themes at NodeThemes, and when we added appointment booking and a class schedule to our theme <strong>Avelyn</strong> (for coaches, beauty and yoga studios), the calendar was the easy part. Here is what actually took the time, in case you are building or choosing one.</p>
<blockquote>
<p>Disclosure: we make Avelyn. The full, non-technical guide for business owners is on our blog: <a href="https://nodethemes.com/online-booking-wordpress-coaches-studios/">Online booking on WordPress for coaches and studios</a>.</p>
</blockquote>
<h2>1. A free time is not just "not booked"</h2>
<p>A start time is offered only when all of these are true:</p>
<ul>
<li>it lies inside the opening hours of that weekday (with an optional second range for a lunch break),</li>
<li>the date is not in the closed-dates list,</li>
<li>it is later than <em>now + minimum notice</em> and earlier than <em>today + days ahead</em>,</li>
<li>the service length <strong>plus the break after it</strong> fits before the range ends,</li>
<li>it does not overlap another booking, including that booking's break.</li>
</ul>
<p>In code that is an interval check, not an equality check:</p>
<pre><code class="language-php">// $start / $end in Unix time; $buffer = break after, in seconds.
function overlaps( $start, $end, $buffer, $booked ) {
    foreach ( $booked as $b ) {
        if ( $start &lt; $b['end'] + $b['buffer'] &amp;&amp; $b['start'] &lt; $end + $buffer ) {
            return true;
        }
    }
    return false;
}
</code></pre>
<h2>2. Two people, one slot, same second</h2>
<p>Two clients can click the last free time at the same moment. Re-checking availability just before saving is not enough. You need a short lock around "check + insert" so the second request sees the first booking. We use an option-based lock with a short expiry, which works on shared hosting without extra database tables.</p>
<h2>3. Time zones</h2>
<p>Everything is stored in UTC and shown in the site time zone from <strong>Settings → General → Timezone</strong>. Choose a <em>city</em>, not a UTC offset, so summer time is handled. The booking widget shows the time zone label next to the times; coaches with international clients get far fewer "is that my time or yours?" e-mails.</p>
<h2>4. Calendar files and links</h2>
<p>Every confirmation e-mail has:</p>
<table>
<thead>
<tr>
<th>What</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td>An <code>.ics</code> file (RFC 5545)</td>
<td>Opens in Apple Calendar, Outlook and Google Calendar</td>
</tr>
<tr>
<td>An "Add to Google Calendar" link</td>
<td>Most clients use Google Calendar on their phone</td>
</tr>
<tr>
<td>A "Manage booking" link with a secret token</td>
<td>Cancel online without an account</td>
</tr>
</tbody></table>
<p>The cancel link respects a limit (for example 24 hours before), and the client gets a reminder e-mail a set number of hours before the start.</p>
<h2>5. Classes are a different problem</h2>
<p>A class is a fixed weekly time with a capacity, so the questions change:</p>
<ul>
<li>How many spots are left right now?</li>
<li>Has this e-mail address already reserved this exact class time?</li>
<li>When the class is full, can people join a <strong>waitlist</strong>?</li>
<li>When someone cancels, the first person on the waitlist is confirmed automatically and gets an e-mail.</li>
</ul>
<p>We also keep a small "My bookings" list in the visitor's browser (localStorage), so people can see which classes they reserved on that device without creating an account.</p>
<h2>6. The boring parts that matter</h2>
<ul>
<li><strong>Spam:</strong> a hidden honeypot field plus a rate limit per visitor on every public endpoint.</li>
<li><strong>E-mail delivery:</strong> many hosts do not deliver mail sent by WordPress. The docs tell users to install an SMTP plugin, and the first test booking proves it works.</li>
<li><strong>Privacy:</strong> bookings are registered with WordPress's personal data exporter and eraser, so a "delete my data" request covers them too.</li>
<li><strong>Payments:</strong> we deliberately do not charge at booking time. Packages, passes and gift cards are WooCommerce products. It keeps booking simple and lets each business choose its payment provider.</li>
</ul>
<h2>7. Settings people can actually find</h2>
<p>Our first version used the WordPress Site Editor for page design. Business owners wanted one place for everything, so version 1.1 moved to a single settings panel: business details, colors and fonts, header and footer, and the sections, texts and photos of every page, next to the booking rules.</p>
<h2>Checklist if you are choosing a booking tool</h2>
<ul>
<li>Breaks between appointments and two ranges per day</li>
<li>Minimum notice and "days ahead"</li>
<li>Time zone shown on the booking page</li>
<li><code>.ics</code> file, Google Calendar link and an online cancel link</li>
<li>Waitlist for classes</li>
<li>No double bookings under load</li>
<li>Works with WordPress's privacy tools</li>
</ul>
<p>The long version, written for coaches and studio owners rather than developers, is here: <strong><a href="https://nodethemes.com/online-booking-wordpress-coaches-studios/">Online booking on WordPress for coaches and studios</a></strong>.</p>
]]></content:encoded></item><item><title><![CDATA[Sending WooCommerce orders to n8n without losing any]]></title><description><![CDATA[Connecting WooCommerce to n8n is easy: point a webhook at an n8n Webhook node and every new order starts a workflow. Keeping it reliable is harder. Your…]]></description><link>https://nodethemes.hashnode.dev/sending-woocommerce-orders-to-n8n-without-losing-any</link><guid isPermaLink="true">https://nodethemes.hashnode.dev/sending-woocommerce-orders-to-n8n-without-losing-any</guid><category><![CDATA[woocommerce]]></category><category><![CDATA[n8n]]></category><category><![CDATA[webhooks]]></category><category><![CDATA[WordPress]]></category><dc:creator><![CDATA[nodethemescom]]></dc:creator><pubDate>Thu, 29 Oct 2026 08:44:52 GMT</pubDate><content:encoded><![CDATA[<p>Connecting WooCommerce to n8n is easy: point a webhook at an n8n Webhook node and every new order starts a workflow. Keeping it reliable is harder. Your n8n server restarts, a workflow throws an error, a deploy takes it offline for ten minutes, and the orders that arrive in that window are simply gone.</p>
<p>This post covers what to check so that every order reaches your automation exactly once.</p>
<h2>What WooCommerce gives you out of the box</h2>
<p>WooCommerce has built-in webhooks under <strong>WooCommerce → Settings → Advanced → Webhooks</strong>. For each one you choose a topic (for example <em>Order created</em>), a delivery URL and a secret. WooCommerce then signs every request with an <code>X-WC-Webhook-Signature</code> header: a base64 HMAC-SHA256 of the body, made with your secret.</p>
<p>That covers the basics, with three things to watch:</p>
<ol>
<li><strong>Failed deliveries add up.</strong> After a number of consecutive failures (5 by default, set by the <code>woocommerce_max_webhook_delivery_failures</code> filter) WooCommerce disables the webhook. Nothing tells you, and new orders stop flowing until someone notices.</li>
<li><strong>"Order created" is not "order paid".</strong> An order is created before the payment is confirmed. If your workflow sends a welcome e-mail or creates an invoice, trigger it on the paid status, and make sure it runs once even if the status changes again.</li>
<li><strong>Debugging is blind.</strong> When something fails you want the exact body that was sent and the exact answer your server gave.</li>
</ol>
<h2>A checklist for reliable delivery</h2>
<ul>
<li><strong>Send in the background.</strong> The webhook should never slow down checkout. Queue the request and send it after the order is saved.</li>
<li><strong>Retry with a delay.</strong> A short outage should mean "delivered a few minutes later", not "lost".</li>
<li><strong>Never switch an endpoint off silently.</strong> If deliveries keep failing, alert someone instead.</li>
<li><strong>Fire "paid" once per order.</strong> Store a flag on the order so a second status change does not run the workflow again.</li>
<li><strong>Log every attempt</strong> with request body, response code and response body, and keep a "resend" button.</li>
<li><strong>Verify the signature in n8n</strong> so nobody can post fake orders to your public webhook URL.</li>
</ul>
<h2>Verifying a signature in n8n</h2>
<p>You do not need any code for this. As an example, the open-source <a href="https://github.com/nodethemes/nodethemes-webhooks">NodeThemes Webhooks</a> plugin, which I maintain, sends an <code>X-NT-Signature</code> header: <code>sha256=</code> followed by an HMAC-SHA256 of the timestamp and the body, so an old request cannot simply be replayed. In n8n:</p>
<ol>
<li><strong>Webhook</strong> node: receives the order.</li>
<li><strong>Crypto</strong> node: Action = <em>Hmac</em>, Type = <em>SHA256</em>, Encoding = <em>hex</em>, Secret = your endpoint secret, and as Value:</li>
</ol>
<pre><code class="language-txt">{{ $json.headers["x-nt-timestamp"] }}.{{ JSON.stringify($json.body) }}
</code></pre>
<ol start="3">
<li><strong>If</strong> node: continue only when <code>"sha256=" + </code> the Crypto result equals <code>{{ $json.headers["x-nt-signature"] }}</code>.</li>
</ol>
<p>The plugin sends the body in a form that <code>JSON.stringify</code> reproduces exactly, which is what makes step 2 work. For other senders, check this first: if the bytes differ, the signature will never match. Two more habits help:</p>
<ul>
<li><strong>Reject old timestamps</strong>, for example anything older than five minutes.</li>
<li><strong>Deduplicate.</strong> A retry or a manual resend carries the same event ID (<code>X-NT-Event-Id</code>). Store the IDs you have handled and skip repeats, so a retry never sends a second SMS or creates a second invoice.</li>
</ul>
<p>If you would rather skip signatures, an authentication header checked by n8n's <em>Header Auth</em> is a simpler alternative.</p>
<p>For WooCommerce's own webhooks the idea is the same, but the signature is the base64 HMAC of the body only, in the <code>X-WC-Webhook-Signature</code> header.</p>
<h2>How the plugin handles the rest</h2>
<p>The free plugin covers the checklist above for up to three endpoints:</p>
<ul>
<li>events for order created, order paid (once per order), status changed, completed and cancelled,</li>
<li>sending in the background with Action Scheduler, so checkout never waits,</li>
<li>three automatic retries over about 15 minutes; an endpoint is never switched off by itself,</li>
<li>an HMAC-SHA256 signature on every request, plus an optional authentication header,</li>
<li>a delivery log with the exact body, the answer and the error, and one-click resend,</li>
<li>HPOS and block checkout support.</li>
</ul>
<p>Whether you use it or WooCommerce's built-in webhooks, test the failure case on purpose: stop n8n, place a test order, start n8n again, and check that the order still arrives.</p>
<p>The full step-by-step guide, with screenshots of the n8n workflow, is on the NodeThemes blog: <a href="https://nodethemes.com/woocommerce-n8n-integration-step-by-step-guide/">WooCommerce n8n Integration: Step-by-Step Guide</a>.</p>
]]></content:encoded></item></channel></rss>