<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>The Beginner’s Playbook for Fixing WordPress Errors</title>
	<atom:link href="https://ceeveeglobal.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://ceeveeglobal.com/</link>
	<description>Effortless Fixes for WordPress Errors, Designed for Beginners</description>
	<lastBuildDate>Mon, 12 Oct 2026 09:01:52 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.7</generator>

<image>
	<url>https://s3.ceeveeglobal.com/ceeveeglobalimages/cropped-Untitled-YouTube-Icon-32x32.png</url>
	<title>The Beginner’s Playbook for Fixing WordPress Errors</title>
	<link>https://ceeveeglobal.com/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>How to Add an AI Chatbot to WordPress (No Plugin Needed)</title>
		<link>https://ceeveeglobal.com/add-ai-chatbot-to-wordpress/</link>
					<comments>https://ceeveeglobal.com/add-ai-chatbot-to-wordpress/#respond</comments>
		
		<dc:creator><![CDATA[Dimuthu Harshana]]></dc:creator>
		<pubDate>Mon, 12 Oct 2026 09:01:52 +0000</pubDate>
				<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Ai]]></category>
		<category><![CDATA[Chatbot]]></category>
		<category><![CDATA[REST API]]></category>
		<category><![CDATA[wordpress]]></category>
		<category><![CDATA[WPCode]]></category>
		<guid isPermaLink="false">https://ceeveeglobal.com/?p=16194</guid>

					<description><![CDATA[<p>How to add an AI chatbot to WordPress with a code snippet, not a plugin: the full build, the 9 ways it breaks, and the hardened fix I tested in a lab.</p>
<p>The post <a href="https://ceeveeglobal.com/add-ai-chatbot-to-wordpress/">How to Add an AI Chatbot to WordPress (No Plugin Needed)</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>The code everyone copies to put an AI chatbot on WordPress works. That&#8217;s exactly the problem.</p>
<p>I put the most-copied version on a real WordPress install, then hit its endpoint ten times from a terminal with no login, no nonce and no cookie. All ten went through. Ten real calls to a real AI model, on my bill, from something that wasn&#8217;t my website.</p>
<p>So this is the honest version of <strong>how to add an AI chatbot to WordPress</strong>: the build, every way I managed to break it, and the hardened version I ended up with. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> I tested the whole thing for free on my own machine, and I&#8217;ll show you how to do that too.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A15-e9-open-proxy.webp" alt="Terminal output showing ten unauthenticated requests to a WordPress AI chatbot endpoint, all landing successfully" /></p>
<p><em>Tested on WordPress 7.1.3, PHP 8.3 and WPCode 2.4.0. The Location behaviour in Step 1 is WPCode&#8217;s own, so check it still holds if you&#8217;re on a much newer version.</em></p>
<h2>What this is, in plain words</h2>
<p>A chat bubble sits in the corner of your site. A visitor clicks it, types a question, and an AI model answers in your words, following instructions you wrote.</p>
<p>You&#8217;re not installing someone else&#8217;s chatbot service. There&#8217;s no monthly fee to a third party, no branding you can&#8217;t remove, no visitor data sitting on a platform you don&#8217;t control. It&#8217;s your code, calling an AI model directly, and you decide what it&#8217;s allowed to say.</p>
<p>What it fixes, specifically:</p>
<ul>
<li><strong>The same five questions, all day.</strong> Opening hours, pricing, do-you-cover-my-area. The bot answers at 2am without you.</li>
<li><strong>Visitors who leave rather than fill in a form.</strong> A chat box gets answered questions that a contact form never would.</li>
<li><strong>Paying monthly for a chat widget</strong> that puts its own logo on your site.</li>
</ul>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A3-panel-startup.webp" alt="The finished chat widget open on a WordPress page, ready for a question" /></p>
<h3>What it costs to run</h3>
<p>Two honest options, and you can switch between them with one setting:</p>
<ul>
<li><strong>A paid API key</strong> from OpenAI or Anthropic. You pay per message, usually a fraction of a penny each. Better answers, nothing to maintain.</li>
<li><strong>Free, forever, on your own server</strong> using Ollama. No key, no bill, no limit on messages. You need a machine you control with a few GB of spare RAM, so a VPS rather than shared hosting. Answers are noticeably weaker than a paid model.</li>
</ul>
<p>Both routes run the exact same code. There&#8217;s a section further down on how I tested the whole thing for free before spending anything.</p>
<div style="border: 1px solid #e3e3ea; border-radius: 14px; padding: 22px; margin: 32px 0; background: #fbfbfd;">
<p style="margin: 0 0 4px; font-size: 1.05em; font-weight: bold; color: #15151c;">Get the files</p>
<p style="margin: 0 0 18px; color: #52525e; font-size: 0.95em;">Two ways to run this. The same hardened code underneath either one.</p>
<div style="display: flex; flex-wrap: wrap; gap: 16px;">
<div style="flex: 1 1 260px; border: 1px solid #e3e3ea; border-radius: 10px; padding: 18px; background: #ffffff;">
<p style="margin: 0 0 6px; font-weight: bold; color: #15151c;">The snippet files</p>
<p style="margin: 0 0 14px; color: #52525e; font-size: 0.92em;">Four files you paste into WPCode, exactly the build in this post. You change a CONFIG block to customise it.</p>
<p><a style="display: inline-block; padding: 11px 20px; border-radius: 8px; background: #4F46E5; color: #fdfff4; font-weight: bold; text-decoration: none;" href="https://github.com/dimu2000/cvg-ai-chatbot-snippet/archive/refs/heads/main.zip">Get it free</a></p>
<p style="margin: 12px 0 0; font-size: 0.85em;"><a style="color: #52525e;" href="https://github.com/dimu2000/cvg-ai-chatbot-snippet">or read through the files first</a></p>
</div>
<div style="flex: 1 1 260px; border: 1px solid #e3e3ea; border-radius: 10px; padding: 18px; background: #ffffff;">
<p style="margin: 0 0 6px; font-weight: bold; color: #15151c;">The finished plugin</p>
<p style="margin: 0 0 14px; color: #52525e; font-size: 0.92em;">Upload, activate, fill in a settings screen. No code editor, no CONFIG block, and the setup guides are in the box.</p>
<p><a style="display: inline-block; padding: 11px 20px; border-radius: 8px; background: #15151c; color: #fdfff4; font-weight: bold; text-decoration: none;" href="https://www.patreon.com/cw/ceeveeglobal">Get the plugin</a></p>
<p style="margin: 12px 0 0; font-size: 0.85em; color: #52525e;">on my Patreon</p>
</div>
</div>
</div>
<p>If you&#8217;d rather build it by hand and understand every line, keep reading. That&#8217;s what the rest of this is.</p>
<h2>What you need</h2>
<ul>
<li>A WordPress site you can install a plugin on (mine was a throwaway Docker install, and Hostinger is where I&#8217;d start if I were setting this up fresh today)</li>
<li>The <strong>WPCode</strong> plugin, free version, for pasting snippets</li>
<li>Permalinks set to anything <strong>except</strong> Plain (this one bites people, more below)</li>
<li>An API key from OpenAI or Anthropic, <em>or</em> Ollama on your own machine if you want to spend nothing</li>
<li>30 minutes</li>
</ul>
<h2>What we&#8217;re building</h2>
<p>A chat bubble in the corner of your site. A visitor clicks it, types a question, and your chosen AI model answers, with your instructions shaping what it says.</p>
<p>Three pieces make that work: a <strong>REST route</strong> your site exposes, a <strong>widget</strong> in the page that calls it, and your <strong>API key</strong>, which never leaves the server.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A17-architecture.webp" alt="Diagram of the chatbot request path: browser, nonce check, rate limit, server-held history, AI provider" /></p>
<p>That diagram is the hardened version. The tutorial version is the same picture with the two gates missing, which is the whole story of this post.</p>
<h2>How I tested this for free</h2>
<p>I didn&#8217;t spend anything building this, and you don&#8217;t have to either.</p>
<p><a href="https://ollama.com">Ollama</a> runs an AI model on your own computer. It installs without sudo, and it exposes an <strong>OpenAI-compatible</strong> endpoint, meaning the same code that talks to OpenAI talks to it, with only the base URL changed.</p>
<pre><code class="language-bash">ollama list
# qwen2.5:3b-instruct-q4_K_M    357c53fb659c    1.9 GB
</code></pre>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A21-ollama-local.webp" alt="Terminal showing a local Ollama model loaded and running on CPU" /></p>
<p>In the chatbot settings I pointed the base URL at <code>http://localhost:11434/v1</code>, set the model name, and left the API key field <strong>empty</strong>:</p>
<pre><code>provider=compatible  base_url=http://ollama:11434/v1
model=qwen2.5:3b-instruct-q4_K_M  key_set=no
</code></pre>
<p>That answered in <strong>1.56 seconds</strong> on a CPU-only box with no GPU. I timed the model call five more times to be sure: <strong>1.73 seconds median</strong>, 1.66 to 1.82 once it&#8217;s warm.</p>
<p>One thing that will confuse you if nobody says it: my <em>first</em> request took <strong>6.9 seconds</strong>. Ollama loads the model into memory on demand, so the first answer after an idle spell is slow and every one after it isn&#8217;t. Don&#8217;t judge the setup by that first reply.</p>
<p>You can also just <strong>leave it like this</strong>. If you run your own VPS, Ollama answers every chat for free, forever, and no API bill ever arrives. That is a real option, not a demo mode.</p>
<p>The catch is honest and worth knowing before you plan around it: this needs a machine you control with a few GB of spare RAM. Normal shared hosting won&#8217;t run it, so for most people it&#8217;s a VPS or nothing.</p>
<p>Here&#8217;s the other honest limit: a 3B model gives visibly weaker answers than GPT-4o or Claude. <strong>Test your plumbing locally, then judge answer quality only once you point it at a paid key.</strong> And one more thing I&#8217;d rather say than hide: my Anthropic adapter was verified against a local Anthropic-shaped mock, not against <code>api.anthropic.com</code>.</p>
<h2>Step 1: the PHP snippet (and the trap that eats it)</h2>
<p><strong>Do this:</strong></p>
<ol>
<li>Install and activate <strong>WPCode</strong> from Plugins &gt; Add New.</li>
<li>Go to <strong>Code Snippets &gt; Add Snippet</strong>, choose <strong>Add Your Custom Code</strong>, and pick <strong>PHP Snippet</strong>.</li>
<li>Paste the PHP in. If you&#8217;re using my files, that&#8217;s <code>01-paste-into-wpcode.php</code>.</li>
<li>In the right-hand column, set <strong>Location</strong> to <strong>Run Everywhere</strong>. This one matters more than it looks.</li>
<li>Save, then switch the snippet to <strong>Active</strong>.</li>
</ol>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A1-wpcode-editor-scaled.webp" alt="WPCode snippet editor with the chatbot PHP pasted in and the location set to Run Everywhere" /></p>
<p>That&#8217;s the whole step. The rest of this section is <em>why</em>, and you can skip to Step 2 if you just want the thing working.</p>
<h3>What that PHP actually does</h3>
<p>It registers a <strong>REST route</strong>: a URL on your own site that your chat widget can post a question to. Your snippet takes the question, adds your instructions, calls the AI model with your key, and sends the answer back.</p>
<p>The part everyone gets right:</p>
<pre><code class="language-php">register_rest_route( 'myapi/v1', '/chat-bot/', array(
    'methods'             =&gt; 'POST',
    'callback'            =&gt; 'handle_chat_bot_request',
    'permission_callback' =&gt; '__return_true',
) );
</code></pre>
<p>The part almost nobody does:</p>
<pre><code class="language-php">register_rest_route( 'myapi/v1', '/chat-bot-config', array(
    'methods'  =&gt; 'GET',
    'callback' =&gt; 'load_chat_bot_base_configuration',
    // no permission_callback at all
) );
</code></pre>
<p>WordPress <strong>does</strong> warn you about that second one. It just sends the warning somewhere you will never look:</p>
<pre><code class="language-bash">curl -s -D - -o /dev/null http://yoursite.test/wp-json/myapi/v1/chat-bot-config

HTTP/1.1 200 OK
X-WP-DoingItWrong: register_rest_route (since 5.5.0; The REST API route definition for
  myapi/v1/chat-bot-config is missing the required permission_callback argument...)
</code></pre>
<p>With <code>WP_DEBUG_LOG</code> switched on, <code>debug.log</code> stayed at <strong>0 bytes</strong>. Core suppresses that notice on purpose during REST requests, because a PHP warning printed mid-request would corrupt the JSON. Reasonable, but it means the one message telling you your endpoint is unprotected goes into a response header, not your log.</p>
<h3>Why that one Location setting decides whether anything works</h3>
<p>Skippable if yours already works. Worth reading if it doesn&#8217;t, because this is the one I&#8217;d never seen written down anywhere and it cost me an afternoon on a real build.</p>
<p>In WPCode, a PHP snippet has a <strong>Location</strong>. Set it to <em>Frontend Conditional Logic</em> and your REST route is <strong>never registered at all</strong>. No error, no log line, nothing:</p>
<table>
<thead>
<tr>
<th>Location</th>
<th>Route in <code>/wp-json/</code></th>
<th><code>POST /chat-bot/</code></th>
<th><code>debug.log</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>Run Everywhere</td>
<td>listed</td>
<td><strong>200</strong></td>
<td>0 bytes</td>
</tr>
<tr>
<td>Frontend Only</td>
<td>listed</td>
<td><strong>200</strong></td>
<td>0 bytes</td>
</tr>
<tr>
<td><strong>Frontend Conditional Logic</strong></td>
<td><strong>absent</strong></td>
<td><strong>404 <code>rest_no_route</code></strong></td>
<td><strong>0 bytes</strong></td>
</tr>
<tr>
<td>Admin Only</td>
<td>absent</td>
<td>404 <code>rest_no_route</code></td>
<td>0 bytes</td>
</tr>
</tbody>
</table>
<p>The code was byte-identical in all four runs. Only the location changed.</p>
<p>The reason is in WPCode&#8217;s own source: <em>Run Everywhere</em> runs your snippet on <code>plugins_loaded</code>, which fires on every request including REST ones. <em>Frontend Conditional Logic</em> runs on <code>wp</code>, a hook a REST request never reaches. Nothing errors, so nothing logs.</p>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Set the location to Run Everywhere.</strong> If your chatbot does nothing at all and your log is empty, check this before you check anything else.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A13-e7b-location.webp" alt="WPCode snippet list showing four snippets and their location settings" /></p>
<h2>Step 2: the widget</h2>
<p>This is the part visitors actually see: the bubble, the panel, and the JavaScript that sends their question to the route you just built.</p>
<p><strong>Do this:</strong> three more snippets, each added the same way as Step 1, each set to <strong>Active</strong>.</p>
<table>
<thead>
<tr>
<th>Snippet type</th>
<th>What goes in it</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>HTML Snippet</strong></td>
<td>the markup (<code>02-html-block.html</code>)</td>
<td>Site Wide Footer</td>
</tr>
<tr>
<td><strong>CSS Snippet</strong></td>
<td>the styles (<code>03-styles.css</code>)</td>
<td>Site Wide Header</td>
</tr>
<tr>
<td><strong>JavaScript Snippet</strong></td>
<td>the widget script (<code>04-script.js</code>)</td>
<td>Site Wide Footer</td>
</tr>
</tbody>
</table>
<p>Not using WPCode for these? The HTML and JavaScript go in your theme&#8217;s <code>footer.php</code> before <code>&lt;/body&gt;</code>, and the CSS goes in <strong>Appearance &gt; Customise &gt; Additional CSS</strong>.</p>
<p>Reload any page on the front end and the launcher appears in the bottom corner.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A2-launcher-on-page-scaled.webp" alt="The chat launcher button sitting in the corner of a live WordPress page" /></p>
<h3>The endpoint line, and the two ways it goes wrong</h3>
<p>The one line to get right is the endpoint, and the mistake to avoid is hardcoding it:</p>
<pre><code class="language-js">// Don't do this. It's your dev machine, not your visitor's
const apiUrl = 'http://ai-chatbot.local/wp-json/myapi/v1/chat-bot/';
</code></pre>
<p>When I left that in, the browser console said <code>net::ERR_NAME_NOT_RESOLVED</code>, then <code>TypeError: Failed to fetch</code>, and the visitor saw a generic error with no clue what happened.</p>
<p>There&#8217;s a nastier version of the same mistake. If your site is on <strong>HTTPS</strong> and your endpoint URL starts with <code>http://</code> on a real domain, Chrome blocks the request outright:</p>
<pre><code>Mixed Content: The page at 'https://yoursite.com/' was loaded over HTTPS, but requested
an insecure resource 'http://yoursite.com/wp-json/...'. This request has been blocked.
</code></pre>
<p>Worth knowing: <code>http://localhost</code> is <em>exempt</em> from that rule, so this never shows up while you&#8217;re testing locally. It appears the moment you go live.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A9-e3-mixed-content.webp" alt="Chat widget showing a generic error and a loading spinner after a blocked mixed-content request" /></p>
<h2>Step 3: your API key, not in the snippet</h2>
<p><strong>Do this:</strong></p>
<ol>
<li>Get a key. OpenAI keys come from platform.openai.com, Anthropic keys from console.anthropic.com. Running Ollama locally? Skip this step entirely, you don&#8217;t need one.</li>
<li>Open <a href="https://ceeveeglobal.com/how-to-edit-wp-config-php-wordpress/"><code>wp-config.php</code></a> and add this line above <code>/* That's all, stop editing! */</code>:</li>
</ol>
<pre><code class="language-php">define( 'MY_CHATBOT_API_KEY', 'sk-your-key-here' );
</code></pre>
<ol>
<li>Save, reload your site, and ask the bot something.</li>
</ol>
<p>The PHP snippet reads that constant. Don&#8217;t paste the key into the snippet itself: a key in a WPCode snippet sits in your database, and in every backup and export of it.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A4-conversation.webp" alt="The finished chat widget open on a page, holding a real conversation" /></p>
<p>If you got an answer, the build is done. Everything after this is making it yours, and knowing what to do when it breaks.</p>
<h2>Make it yours</h2>
<p>This is the part most tutorials skip, and it&#8217;s the part you&#8217;ll actually spend time on. None of it needs a code editor if you&#8217;re on the plugin version, and all of it is one <code>CONFIG</code> block if you&#8217;re on the snippets.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A6-settings-full-scaled.webp" alt="The chatbot settings screen showing the prompt, model, colours, limits and quick replies" /></p>
<h3>What it says before anyone types</h3>
<p>Three fields, and they&#8217;re the three everyone changes first.</p>
<ul>
<li><strong>Window title</strong>, the heading on the panel. &#8220;Ask us anything&#8221; beats &#8220;Chatbot&#8221;.</li>
<li><strong>First message</strong>, what it greets people with.</li>
<li><strong>Quick replies</strong>, the suggestion buttons under that greeting. Two or three, phrased the way a visitor would phrase them, because most people click one of these instead of typing.</li>
</ul>
<p>In the snippets, those are <code>title</code>, <code>startup_message</code> and <code>quick_replies</code> at the top of the PHP file. In the plugin, they&#8217;re the first three boxes on the settings screen.</p>
<h3>What it knows about your business</h3>
<p>The <strong>system prompt</strong> is the most important field there is. It&#8217;s read on the server on every request, so a visitor can&#8217;t overwrite it or talk their way past it.</p>
<p>Here&#8217;s the thing nobody tells you, and it surprises everyone: <strong>the bot only knows what&#8217;s in that box.</strong> It does not read your pages, your posts or your products. If you want it answering questions about your prices, put your prices in there.</p>
<pre><code>You answer questions for Example Plumbing in Bristol.
Boiler service £95. Emergency callout £120 plus parts. Bathrooms quoted on site.
Open Mon to Fri 8am to 6pm, Saturday mornings by arrangement, closed Sunday.
We cover Bristol and 20 miles around it.
To book: 0117 000 0000 or the form at /contact.
Answer briefly. If you don't know, say so and point them at the contact form.
Never invent a price.
</code></pre>
<p>The cost of that: it&#8217;s sent on <strong>every single message</strong>, so you pay for it every time. A tight prompt is cheaper than a long one, and pasting your whole FAQ in there gets expensive at volume.</p>
<h3>Colours</h3>
<p>Two values, not a theme editor: an <strong>accent colour</strong> and the <strong>text colour that sits on it</strong>. They flow through the launcher, the header and the visitor&#8217;s own message bubbles. Pick your brand colour and a text colour that reads clearly on it.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A16b-kit-page-open-scaled.webp" alt="The chat panel closed and open, restyled to match a site's own colours" /></p>
<h3>Swapping the model, including to a free local one</h3>
<p>The same screen, pointed at two different providers. On the left, a local model through Ollama with the key field empty. On the right, OpenAI with a real key.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A22a-provider-local-scaled.webp" alt="The provider settings pointed at a local Ollama model" /></p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A22b-provider-openai-scaled.webp" alt="The same provider settings pointed at OpenAI" /></p>
<p>Nothing else changes. Not the widget, not the styles, not your prompt. Your visitors see no difference at all, which is what makes &#8220;test free locally, then switch to a paid key&#8221; a real workflow rather than a slogan.</p>
<h3>The settings that control what a bad day costs you</h3>
<p>These are the ones worth understanding before you leave it running. Every one is enforced on your server, before the AI model is ever called, so a refused request costs nothing.</p>
<table>
<thead>
<tr>
<th>Setting</th>
<th>What raising it costs you</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Messages per visitor</strong> (per hour)</td>
<td>This is the ceiling on what one person can spend. <code>0</code> means no ceiling.</td>
</tr>
<tr>
<td><strong>Max reply length</strong></td>
<td>Longer answers, more cost per message, more rambling.</td>
</tr>
<tr>
<td><strong>Max question length</strong></td>
<td>Someone can paste a whole document in and you pay to read it.</td>
</tr>
<tr>
<td><strong>Remembered exchanges</strong></td>
<td>Better memory inside a conversation, but every remembered turn is re-sent and re-paid on every later message. This is the one that quietly multiplies a long chat.</td>
</tr>
<tr>
<td><strong>Creativity</strong> (temperature)</td>
<td><code>0.7</code> is a sensible default. Above <code>1.2</code> it starts inventing. If it quotes your prices, go lower.</td>
</tr>
<tr>
<td><strong>Timeout</strong></td>
<td>Too short and a slow local model fails on its first request. Too long and it ties up a PHP worker.</td>
</tr>
</tbody>
</table>
<h2>When it breaks: 9 symptoms, 9 fixes</h2>
<p>Here&#8217;s the part no tutorial includes. Every row is something I actually hit in this lab, and most of them are in the screenshots above.</p>
<table>
<thead>
<tr>
<th>What you see</th>
<th>What&#8217;s actually wrong</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Widget does nothing, log empty, route 404s</td>
<td>Snippet Location is Frontend Conditional Logic</td>
<td>Set it to Run Everywhere</td>
</tr>
<tr>
<td>&#8220;Oops!&#8221; + <code>Unexpected token '&lt;'</code> in console</td>
<td>Permalinks set to Plain. <code>/wp-json/</code> returns <strong>HTML with a 200</strong></td>
<td>Change permalinks, or call <code>/?rest_route=</code></td>
</tr>
<tr>
<td>&#8220;Oops!&#8221; + <code>ERR_NAME_NOT_RESOLVED</code></td>
<td>Hardcoded dev hostname in the JS</td>
<td>Use your real site URL</td>
</tr>
<tr>
<td>&#8220;Oops!&#8221; + <code>Mixed Content ... blocked</code></td>
<td>HTTPS page calling an <code>http://</code> endpoint</td>
<td>Use <code>https://</code>, or a relative URL</td>
</tr>
<tr>
<td>&#8220;Oops!&#8221;, 200 response, empty log</td>
<td>Bad API key, wrong model name, or DNS. <strong>All three look identical</strong></td>
<td><code>curl</code> the route and read the <code>message</code> field</td>
</tr>
<tr>
<td>Wrong icons / empty squares</td>
<td>Your theme already loads a different FontAwesome version</td>
<td>Drop the snippet&#8217;s <code>&lt;link&gt;</code> and use the theme&#8217;s</td>
</tr>
<tr>
<td>Chatbot forgets the previous message <em>(only once you move history server-side)</em></td>
<td>Session id case-mangled on the way back in by <code>sanitize_key()</code></td>
<td>Generate session ids in lowercase hex</td>
</tr>
<tr>
<td>Header says &#8220;Offline&#8221;</td>
<td>Config endpoint never answered</td>
<td>Check the route first, then the config values</td>
</tr>
<tr>
<td>Strangers using your credit</td>
<td>No nonce, no rate limit</td>
<td>See the next section</td>
</tr>
</tbody>
</table>
<p>Two things that tripped me up worth calling out. <strong><code>response.ok</code> is not &#8220;it worked&#8221;</strong>. Under Plain permalinks the route returns a perfectly valid 200 carrying an HTML page, and the kit&#8217;s JavaScript believes it. And a failing API key, a bad model name and an unresolvable host produce <em>exactly</em> the same &#8220;Oops!&#8221; with <strong>0 bytes</strong> in the log every time.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A10-e4-oops.webp" alt="The widget showing &quot;Oops! Something went wrong!&quot; beside a debug log of zero bytes" /></p>
<h2>Common mistakes (and how to avoid them)</h2>
<ul>
<li><strong>Leaving the route public with no rate limit.</strong> Ten unauthenticated requests landed, 10 of 10. Anyone with the URL can spend your API credit.</li>
<li><strong>Letting the browser send the conversation history.</strong> I planted a fake earlier turn saying <em>&#8220;My campaign code is TOPAZ-8831&#8221;</em> and asked the bot to repeat it. With no history it said <em>&#8220;I dont know&#8221;</em>. With the injected history it said <strong>TOPAZ-8831</strong>. The visitor controls what your model is told.</li>
<li><strong>Trusting the system prompt to keep it on topic.</strong> My snippet said &#8220;only answer digital marketing questions&#8221;. It wrote me a poem about the sea.</li>
<li><strong>Debugging with <code>WP_DEBUG</code> and expecting a log.</strong> For every failure above, the log stayed empty.</li>
<li><strong>Pasting your key into the snippet.</strong> Use <code>wp-config.php</code>.</li>
</ul>
<h2>What you should improve next</h2>
<p>Three changes take this from a demo to something I&#8217;d leave on a client site.</p>
<p><strong>Check a nonce.</strong> Hand the widget a <code>wp_rest</code> nonce and verify it. Give the rejection its own error code so the widget can tell &#8220;your token expired&#8221; apart from &#8220;you&#8217;re going too fast&#8221;, fetch a fresh one and retry once. That keeps the check working on a cached page.</p>
<p><strong>Add a rate limit, in front of the provider.</strong> Mine is a counter in a transient. Six requests against a limit of three gave <strong>three 200s, then three 429s</strong>, and the refused ones never reached the model, so they cost nothing.</p>
<p><strong>Keep the conversation on the server.</strong> Store history in a transient keyed by session id. The browser then sends one message and an id, so it can&#8217;t rewrite what the model sees.</p>
<p>After those three, the same abuse test that landed 10 of 10 against the tutorial build landed <strong>zero</strong>. And when the provider genuinely fails, the visitor gets a neutral sentence while the real cause goes to the log where the owner can read it.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/A18-e11-before-after.webp" alt="Before and after comparison of unauthenticated requests against the tutorial build and the hardened build" /></p>
<h2>Frequently asked questions</h2>
<p><strong>Do I need a plugin for this?</strong><br />
No, and that&#8217;s the point of the snippet build. You need WPCode to hold the snippets, but the chatbot itself is your own code, so nothing is hidden from you.</p>
<p><strong>Will it answer questions about my own pages and prices?</strong><br />
Only if you tell it them. It doesn&#8217;t read your site. Everything it knows comes from the system prompt field, so your prices, hours and policies have to be typed in there. Searching your actual content is a much bigger build than this one.</p>
<p><strong>How much does it cost to run?</strong><br />
The build costs nothing. Each conversation costs whatever your provider charges per token, and the tutorial version sends the <strong>entire history on every turn</strong> with no cap, so a long chat gets expensive quietly.</p>
<p><strong>Can I use Claude instead of ChatGPT?</strong><br />
Yes. The request shape differs, so you need a small adapter per provider rather than one function with the URL swapped.</p>
<p><strong>Why does my chatbot say &#8220;Oops&#8221; with nothing in the log?</strong><br />
Because that one message covers at least five unrelated causes. Start with <code>curl</code> on your route and read the response body, not the status code. The table above maps each body to its cause.</p>
<p><strong>Is it safe to leave the endpoint public?</strong><br />
Not as written. A public endpoint with no nonce and no rate limit is a proxy to your paid API that anyone can drive.</p>
<h2>Take the files</h2>
<p>Everything in this post, as something you can actually run.</p>
<div style="border: 1px solid #e3e3ea; border-radius: 14px; padding: 22px; margin: 32px 0; background: #fbfbfd;">
<p style="margin: 0 0 4px; font-size: 1.05em; font-weight: bold; color: #15151c;">Get the files</p>
<p style="margin: 0 0 18px; color: #52525e; font-size: 0.95em;">Two ways to run this. The same hardened code underneath either one.</p>
<div style="display: flex; flex-wrap: wrap; gap: 16px;">
<div style="flex: 1 1 260px; border: 1px solid #e3e3ea; border-radius: 10px; padding: 18px; background: #ffffff;">
<p style="margin: 0 0 6px; font-weight: bold; color: #15151c;">The snippet files</p>
<p style="margin: 0 0 14px; color: #52525e; font-size: 0.92em;">Four files you paste into WPCode, exactly the build in this post. You change a CONFIG block to customise it.</p>
<p><a style="display: inline-block; padding: 11px 20px; border-radius: 8px; background: #4F46E5; color: #fdfff4; font-weight: bold; text-decoration: none;" href="https://github.com/dimu2000/cvg-ai-chatbot-snippet/archive/refs/heads/main.zip">Get it free</a></p>
<p style="margin: 12px 0 0; font-size: 0.85em;"><a style="color: #52525e;" href="https://github.com/dimu2000/cvg-ai-chatbot-snippet">or read through the files first</a></p>
</div>
<div style="flex: 1 1 260px; border: 1px solid #e3e3ea; border-radius: 10px; padding: 18px; background: #ffffff;">
<p style="margin: 0 0 6px; font-weight: bold; color: #15151c;">The finished plugin</p>
<p style="margin: 0 0 14px; color: #52525e; font-size: 0.92em;">Upload, activate, fill in a settings screen. No code editor, no CONFIG block, and the setup guides are in the box.</p>
<p><a style="display: inline-block; padding: 11px 20px; border-radius: 8px; background: #15151c; color: #fdfff4; font-weight: bold; text-decoration: none;" href="https://www.patreon.com/cw/ceeveeglobal">Get the plugin</a></p>
<p style="margin: 12px 0 0; font-size: 0.85em; color: #52525e;">on my Patreon</p>
</div>
</div>
</div>
<p>Which one? If you&#8217;re comfortable pasting code and want to read every line before it runs on your site, take the snippets. If you&#8217;d rather upload a zip, tick some boxes and get on with your day, take the plugin. Both are the hardened build. Neither is a cut-down version of the other.</p>
<h2>Wrap-up</h2>
<p>An AI chatbot on WordPress is genuinely a 30-minute build. The hard part isn&#8217;t getting it talking, it&#8217;s the gap between &#8220;it answers&#8221; and &#8220;it&#8217;s safe to leave running&#8221;. A nonce, a rate limit and server-side history are what close it.</p>
<p>If you&#8217;re new to editing site files at all, start with <a href="https://ceeveeglobal.com/how-to-edit-wp-config-php-wordpress/">how to edit wp-config.php</a> before you touch anything else.</p>
<p>If you get stuck on any of this, come and ask me in <a href="https://discord.gg/W7vvnVSX2G">my Discord</a>. I&#8217;d rather answer a question there than have you give up on it.</p>
<p>The post <a href="https://ceeveeglobal.com/add-ai-chatbot-to-wordpress/">How to Add an AI Chatbot to WordPress (No Plugin Needed)</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ceeveeglobal.com/add-ai-chatbot-to-wordpress/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Replace Vercel, Sentry &#038; Plausible for $5/Month (Real Setup)</title>
		<link>https://ceeveeglobal.com/self-hosted-vps-replaces-vercel-sentry-plausible-better-stack/</link>
					<comments>https://ceeveeglobal.com/self-hosted-vps-replaces-vercel-sentry-plausible-better-stack/#respond</comments>
		
		<dc:creator><![CDATA[Dimuthu Harshana]]></dc:creator>
		<pubDate>Tue, 06 Oct 2026 07:25:40 +0000</pubDate>
				<category><![CDATA[Beginner Guides]]></category>
		<category><![CDATA[Automation]]></category>
		<category><![CDATA[self-hosting]]></category>
		<category><![CDATA[Temps]]></category>
		<category><![CDATA[VPS]]></category>
		<category><![CDATA[WordPress alternative tools]]></category>
		<guid isPermaLink="false">https://ceeveeglobal.com/?p=16172</guid>

					<description><![CDATA[<p>I replaced four paid subscriptions ($152/mo) with one $5/mo self-hosted server: real deployment, a real caught error, real traffic counted. Free cheat sheet.</p>
<p>The post <a href="https://ceeveeglobal.com/self-hosted-vps-replaces-vercel-sentry-plausible-better-stack/">Replace Vercel, Sentry &#038; Plausible for $5/Month (Real Setup)</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/comic-dimu-hero-scaled.webp" alt="Four paid tools replaced by one $5/mo server" /></p>
<p>Four invoices landed in my inbox the same week: Vercel, Sentry, Plausible, Better Stack. Added up, they came to <strong>$152 a month</strong>, just to host an app, know when it broke, see who visited, and get a ping if it went down.</p>
<p>None of those jobs are hard. They&#8217;re just scattered across four different companies, each charging you separately for the privilege.</p>
<p>So I moved all four onto one box. A $5/mo VPS, one install command, one binary doing all four jobs. Here&#8217;s exactly what that looks like, with the real thing actually running: not a feature comparison, not a pricing page screenshot.</p>
<h2>What You Need</h2>
<ul>
<li>A VPS with 1-2 vCPU and 2-8GB RAM (I used a $5/mo box, Ubuntu 24.04 LTS)</li>
<li>SSH access to that box</li>
<li>A domain, if you want a real custom URL (not required to follow along)</li>
<li>Five minutes: the install really is one command</li>
</ul>
<p>I run my whole setup on a <strong>Contabo</strong> VPS, solid price for what you get, and it&#8217;s what this whole post is built on.</p>
<h2>The $152/mo Problem, Priced Out</h2>
<p>Here&#8217;s what four separate &#8220;it&#8217;s just $20/mo&#8221; subscriptions actually add up to:</p>
<table>
<thead>
<tr>
<th>Service</th>
<th>Job</th>
<th>Monthly cost</th>
</tr>
</thead>
<tbody>
<tr>
<td>Vercel</td>
<td>App hosting</td>
<td>$20</td>
</tr>
<tr>
<td>Sentry</td>
<td>Error tracking</td>
<td>$29</td>
</tr>
<tr>
<td>Plausible</td>
<td>Web analytics</td>
<td>$69</td>
</tr>
<tr>
<td>Better Stack</td>
<td>Uptime monitoring</td>
<td>$34</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td></td>
<td><strong>$152/mo, $1,824/year</strong></td>
</tr>
</tbody>
</table>
<p>Each one, on its own, sounds reasonable. Stacked, it&#8217;s real money for four things that don&#8217;t individually need a dedicated company behind them.</p>
<h2>Why One $5 Box Actually Holds Up Here</h2>
<p>I&#8217;ll be straight with you: &#8220;run your whole stack on one cheap box&#8221; is a claim I&#8217;ve measured being wrong before. A different self-hosted stack I tested, Coolify plus Postal plus Mautic plus WordPress, needed real RAM minimums that added up fast. Coolify alone wants 2 cores and 2GB before you&#8217;ve deployed anything. Postal&#8217;s own docs recommend 8GB on its own, because it runs MariaDB <em>and</em> RabbitMQ next to its web and worker processes. Stack those together and you&#8217;re looking at two boxes, not one, closer to $15/mo than the $4/mo the pitch usually promises.</p>
<p>This is a different situation, and I didn&#8217;t want to just assume it was different because the marketing said so. The tool I used, <a href="https://temps.sh/">Temps</a>, a single self-hosted Rust binary, states its own idle footprint at around <strong>50MB of RAM</strong> on its own homepage. That&#8217;s genuinely light, not a marketing rounding error, because it&#8217;s one purpose-built binary doing these four jobs natively, instead of four separate open-source apps glued together on the same box, each with its own database, its own worker process, its own idea of how much RAM is &#8220;normal.&#8221;</p>
<p>I went looking for that number specifically because I&#8217;d been burned assuming a lightweight-sounding pitch would hold up without checking. It&#8217;s worth doing that check yourself on any tool before you commit a VPS tier to it. The resource page is usually one search away, and it&#8217;s a lot cheaper to read than to discover you need a bigger box halfway through setup.</p>
<h3>Why not just run the individual open-source alternatives?</h3>
<p>You could. <strong>GlitchTip</strong> (Sentry-API-compatible) and <strong>Umami</strong> (a clean Plausible-style dashboard) are both solid, genuinely open-source options if you only need to replace one piece. I looked at going that route instead of an all-in-one tool, and the honest answer is: it works, but you&#8217;re back to running multiple separate apps, each with its own database and its own update cycle, even if each one individually is light. The appeal of Temps specifically is that it&#8217;s one binary, one install, one thing to keep patched, not that the individual alternatives are bad.</p>
<h2>Setting It Up</h2>
<p>SSH into a clean Ubuntu box and run one command:</p>
<pre><code class="language-bash">curl -fsSL https://temps.sh/deploy.sh | bash
</code></pre>
<p>That&#8217;s it. No dashboard sign-up first, no credit card, no multi-step wizard before you see anything. The installer provisions Docker, a time-series database, and a Pingora-based proxy (the same proxy engine Cloudflare runs) automatically.</p>
<p>Once it finishes, you register your admin account and you&#8217;re deploying. No signup form, no credit card, no email verification loop before you can see the dashboard: the install script is genuinely the whole onboarding.</p>
<p>What you should see once it&#8217;s done: a confirmation that default roles were initialized, an admin user created, and a preview domain assigned automatically (Temps hands you a working subdomain immediately, so you&#8217;re not stuck configuring DNS before you can test anything). If any of those three lines don&#8217;t show up, something in the install failed silently. Re-run the script before moving on rather than troubleshooting forward from a broken base.</p>
<h3>Block 1: Hosting (replaces Vercel, $20/mo)</h3>
<p>Point your domain at the box, connect a repo, deploy. Here&#8217;s the real thing, actually live, not a staged screenshot:</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/deployment-live.webp" alt="A real deployed app, confirmed live via docker ps and a real 200 OK response" /></p>
<p>That&#8217;s <code>observability-starter</code>, genuinely running on my own box, answering real requests.</p>
<h3>Block 2: Error Tracking (replaces Sentry, $29/mo)</h3>
<p>Temps accepts your app&#8217;s existing Sentry SDK. You point its DSN at your Temps instance instead of Sentry&#8217;s servers, and your error reporting code doesn&#8217;t change.</p>
<p>I didn&#8217;t just take that on faith. I threw a real error on purpose and watched it get caught:</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/error-caught.webp" alt="A real 500 error, actually thrown and actually caught, with the app's own log naming where to look" /></p>
<p>That&#8217;s a genuine HTTP 500 from a real endpoint, and the app&#8217;s own log line telling you exactly where to go look for it.</p>
<h3>Block 3: Analytics (replaces Plausible, $69/mo)</h3>
<p>Analytics here means real request counting, not a JavaScript snippet reporting back to someone else&#8217;s server. Here&#8217;s a real count, pulled straight from the box&#8217;s own service log:</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/traffic-counted.webp" alt="Real traffic, counted live from the box's own service log" /></p>
<p>That number is real, counted from <code>journalctl</code>, not invented for this post.</p>
<h3>Block 4: Uptime &amp; Health (replaces Better Stack, $34/mo)</h3>
<p>Temps runs its own health checks every 5 seconds, with a 60-second window before it acts. This is specifically its deploy-health and auto-rollback mechanism, not a separate uptime-monitoring product bolted on. Add a webhook and you get a ping the moment something looks wrong, the same job Better Stack is doing for $34/mo.</p>
<h2>Is It Actually Safe to Expose?</h2>
<p>I&#8217;ve been burned by this exact question before. On a different self-hosted tool (Coolify), I measured that <code>ufw</code> <strong>cannot</strong> close a port that Docker has published. Docker writes its own firewall rules ahead of <code>ufw</code>&#8216;s, so a <code>ufw deny</code> on that port does nothing at all. I&#8217;d actually written the opposite assumption into a script once, and only caught it by testing instead of trusting.</p>
<p>Temps avoids that specific trap by construction: its control-plane runs as a host <strong>systemd service</strong>, not a Docker-published port, and every deployed app routes through its single Pingora proxy rather than individual host ports. There&#8217;s no equivalent Docker-published admin port here to accidentally leave open.</p>
<h2>Common Mistakes (and How to Avoid Them)</h2>
<ul>
<li><strong>Assuming every self-hosted stack fits on one cheap box.</strong> It doesn&#8217;t. I&#8217;ve measured a different stack that genuinely needed two boxes. Check the <em>specific</em> tool&#8217;s real resource numbers before you commit, don&#8217;t assume &#8220;self-hosted&#8221; means &#8220;lightweight.&#8221;</li>
<li><strong>Trusting <code>ufw</code> to close a Docker-published port.</strong> It silently won&#8217;t. If a tool exposes its dashboard via <code>docker run -p</code>, firewall it at the <code>DOCKER-USER</code> chain or put it behind a reverse proxy. <code>ufw</code> alone is not protection.</li>
<li><strong>Skipping the &#8220;what breaks with two servers&#8221; question.</strong> Every &#8220;replace your whole stack&#8221; pitch glosses over this. Ask it before you&#8217;re mid-migration.</li>
<li><strong>Not registering the admin account immediately.</strong> Any fresh self-hosted install has a window where the setup page is reachable by anyone who finds the URL before you&#8217;ve claimed it. Register the moment the install script finishes, not &#8220;later tonight.&#8221;</li>
<li><strong>Confusing a tool&#8217;s general self-hosting reputation with its actual numbers.</strong> &#8220;Self-hosted&#8221; isn&#8217;t a resource tier. Two tools both described as lightweight can have wildly different real RAM/CPU needs. Read the specific tool&#8217;s own stated numbers, not the category&#8217;s reputation.</li>
</ul>
<h2>What This Doesn&#8217;t Do</h2>
<p>One box only. The moment you need a second server, more traffic than one box handles, or you want redundancy, you&#8217;re back to manual provisioning each time. There&#8217;s no fleet management, no declarative multi-box config, no unified observability across machines. That&#8217;s a real trade-off, not a footnote.</p>
<h2>Frequently Asked Questions</h2>
<p><strong>Is Temps really free?</strong><br />
Yes. The software itself has no license fee. You pay for the VPS it runs on, which is where the $5/mo comes from.</p>
<p><strong>Does this replace Sentry&#8217;s exact feature set?</strong><br />
It accepts Sentry-compatible DSNs so your existing error-reporting code works unchanged, but it&#8217;s not a drop-in clone of every Sentry feature. Check your specific needs against what you actually use.</p>
<p><strong>What if I outgrow one server?</strong><br />
You provision a second box manually, there&#8217;s no built-in fleet management. See &#8220;What This Doesn&#8217;t Do&#8221; above.</p>
<p><strong>Is my admin console exposed to the internet by default?</strong><br />
Its control-plane binds to an address you configure (<code>TEMPS_CONSOLE_ADMIN_ADDRESS</code>), separate from the public traffic path. Register your admin account immediately after install regardless, the same advice that applies to any fresh self-hosted install.</p>
<p><strong>Do I need to know Docker to run this?</strong><br />
Not really. The install script provisions Docker underneath for you. You&#8217;re not writing Docker Compose files or managing containers by hand day to day, the same way you wouldn&#8217;t on Vercel. It&#8217;s there, it&#8217;s just not something you touch directly for normal use.</p>
<p><strong>Can I migrate an existing app over, or does it only work for new projects?</strong><br />
The install and the hosting side work the same either way, point an existing repo at it the same as a new one. What I haven&#8217;t personally tested is migrating <em>historical</em> error/analytics data out of Sentry or Plausible into Temps; treat that as a fresh start for monitoring data, not a data-import job.</p>
<h2>The Real Number</h2>
<p>$152/mo down to $5/mo is <strong>$1,824 a year</strong> back in your pocket, for four jobs that genuinely don&#8217;t need four separate companies billing you for them.</p>
<p>I put together a free one-page cheat sheet with the exact install command, the setup checklist, and the hardening steps. Grab it below if you want to run this yourself without re-reading this whole post.</p>
<div id="cvg-ebook-optin-wrap" style="background: #f0f0ff; border: 1px solid #d6d6fb; border-radius: 8px; padding: 24px 28px; margin: 28px 0; font-family: inherit; display: flex; gap: 24px; align-items: center; flex-wrap: wrap;"><img decoding="async" style="width: 130px; height: auto; border-radius: 8px; box-shadow: 0 8px 24px rgba(26,26,46,0.25); flex: 0 0 auto;" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/ebook-cover.webp" alt="The $5/Mo Self-Hosted Cheat Sheet, ebook cover" /></p>
<div style="flex: 1; min-width: 260px;">
<p style="margin: 0 0 6px; font-size: 18px; font-weight: bold; color: #1a1a2e;">Get the free $5/Mo Self-Hosted Cheat Sheet</p>
<p style="margin: 0 0 16px; font-size: 14px; color: #555;">Every command from this post, one page, as a PDF. Enter your email and we&#8217;ll send it straight to your inbox.</p>
<form id="mauticform_vps_cheat" style="display: flex; flex-wrap: wrap; gap: 10px;" action="https://mautic.aibuilttools.com/form/submit?formId=13" autocomplete="off" method="POST"><input style="flex: 1; min-width: 140px; padding: 10px 14px; border: 1px solid #ccc; border-radius: 5px; font-size: 14px;" name="mauticform[firstname]" type="text" placeholder="First name" /><br />
<input id="cvg-ebook-optin-email" style="flex: 2; min-width: 200px; padding: 10px 14px; border: 1px solid #ccc; border-radius: 5px; font-size: 14px;" name="mauticform[email]" required="" type="email" placeholder="your@email.com" /><br />
<button style="padding: 10px 22px; background: #4F46E5; color: #fff; border: none; border-radius: 5px; font-size: 14px; font-weight: 600; cursor: pointer; white-space: nowrap;" type="submit">Get the Cheat Sheet →</button><br />
<input name="mauticform[formId]" type="hidden" value="13" /><br />
<input name="mauticform[formName]" type="hidden" value="vps_cheat" /></form>
</div>
</div>
<p>If you want to see this whole setup walked through start to finish, <a href="https://www.youtube.com/@CeeVeeGlobal">I made a video covering the same build</a>. [LINK: video URL, once the video ships].</p>
<p>The post <a href="https://ceeveeglobal.com/self-hosted-vps-replaces-vercel-sentry-plausible-better-stack/">Replace Vercel, Sentry &#038; Plausible for $5/Month (Real Setup)</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ceeveeglobal.com/self-hosted-vps-replaces-vercel-sentry-plausible-better-stack/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Yoast Canonical Pointing to Staging: Why No Setting Shows It</title>
		<link>https://ceeveeglobal.com/yoast-canonical-points-at-staging/</link>
					<comments>https://ceeveeglobal.com/yoast-canonical-points-at-staging/#respond</comments>
		
		<dc:creator><![CDATA[Dimuthu Harshana]]></dc:creator>
		<pubDate>Tue, 29 Sep 2026 13:58:02 +0000</pubDate>
				<category><![CDATA[WordPress Error Fixes]]></category>
		<category><![CDATA[canonical]]></category>
		<category><![CDATA[seo]]></category>
		<category><![CDATA[staging]]></category>
		<category><![CDATA[wordpress migration]]></category>
		<category><![CDATA[wp-cli]]></category>
		<category><![CDATA[yoast]]></category>
		<guid isPermaLink="false">https://ceeveeglobal.com/?p=16165</guid>

					<description><![CDATA[<p>My Yoast canonical pointed at a dead staging site while every setting looked fine. Here is the hidden table behind it, the half-reset trap, and the fix.</p>
<p>The post <a href="https://ceeveeglobal.com/yoast-canonical-points-at-staging/">Yoast Canonical Pointing to Staging: Why No Setting Shows It</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Fourteen times. That&#8217;s how often my homepage pointed at a dead copy of my site, and a <strong>Yoast canonical pointing to staging</strong> told Google it was the real one.</p>
<p>Someone pointed it out to me, not anything on my own site. I checked every setting I knew, and every one was correct.</p>
<p>The address wasn&#8217;t in a setting at all. It was in a table Yoast keeps for itself. Below is how I found it, the half-fix that made things worse, the complete one, and the extra trap that made my fix look like it had failed.</p>
<p>One note before we start. The screenshots come from a copy of my site that I rebuilt from the backup I took before the fix, so I could break it again and capture every step. The real site is already repaired.</p>
<h2>What You Need</h2>
<ul>
<li>A WordPress site running Yoast SEO that has moved from a staging address to its live one (I tested this on a recent Yoast SEO release with WP Fastest Cache active)</li>
<li>WP-CLI access from your WordPress folder</li>
<li>Access to the database with the <code>mysql</code> or <code>mariadb</code> client</li>
<li>A backup of two tables: <code>wp_yoast_indexable</code> and <code>wp_yoast_seo_links</code></li>
<li>Your own table prefix, if it isn&#8217;t <code>wp_</code> (mine is <code>wp_</code>)</li>
</ul>
<h2>Step 1: Check for a Yoast Canonical Pointing to Staging</h2>
<p>Open your homepage, view the page source and search for <code>canonical</code>. Or let the terminal do it in one line:</p>
<pre><code class="language-bash">curl -s https://ceeveeglobal.com/ | grep -oE '&lt;link rel="canonical"[^&gt;]*&gt;|og:url" content="[^"]*"'
</code></pre>
<p><strong>You should see:</strong> your live address in both lines. If you see <code>staging</code>, <code>localhost</code> or an old domain, keep going. This is what I saw:</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/canonical-before-after.webp" alt="Yoast canonical pointing to staging in the page source, before and after the fix" /></p>
<p>(The <code>:8081</code> in the second panel is just the port my copy of the site runs on.)</p>
<p>A <strong>canonical tag</strong> is one line in your page&#8217;s code that names the original address of that page. <code>og:url</code> is a similar line that Facebook and LinkedIn read when someone shares your page.</p>
<p>Those two were only part of it. My homepage held <strong>14</strong> references to the staging address: one canonical, one <code>og:url</code>, and twelve more inside the structured data, a block of code in the page that tells search engines what the page is.</p>
<p>Google calls a <code>rel="canonical"</code> link &#8220;a strong signal that the specified URL should become canonical&#8221;. That is why this one line matters.</p>
<h2>Step 2: Rule Out the Usual Suspects</h2>
<p>Every guide sends you to the same places first. I went through all of them, and each one was clean.</p>
<table>
<thead>
<tr>
<th>Where I looked</th>
<th>How I checked</th>
<th>What I found</th>
</tr>
</thead>
<tbody>
<tr>
<td>Home and Site address settings</td>
<td><code>wp option get home</code>, <code>wp option get siteurl</code></td>
<td>Both correct</td>
</tr>
<tr>
<td>Code overriding them</td>
<td><code>wp eval 'echo home_url();'</code>, then a search of <code>wp-config.php</code></td>
<td>Correct at runtime, nothing set</td>
</tr>
<tr>
<td>The page&#8217;s own canonical field</td>
<td>Yoast&#8217;s field on the page, plus the <code>_yoast_wpseo_canonical</code> meta</td>
<td>Empty</td>
</tr>
<tr>
<td>The options table</td>
<td>A search for the word <code>staging</code></td>
<td>One row, an old Elementor log</td>
</tr>
<tr>
<td>The posts</td>
<td>The same search on the content and the GUID</td>
<td>Nothing</td>
</tr>
</tbody>
</table>
<p>Here are the checks. The first block is the WP-CLI part:</p>
<pre><code class="language-bash">wp option get home
wp option get siteurl
wp eval 'echo home_url();'
grep -E "WP_HOME|WP_SITEURL" wp-config.php
</code></pre>
<p>And this is the database part:</p>
<pre><code class="language-sql">SELECT COUNT(*) FROM wp_options WHERE option_value LIKE '%staging.ceeveeglobal%';
SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%staging.ceeveeglobal%' OR guid LIKE '%staging.ceeveeglobal%';
SELECT COUNT(*) FROM wp_postmeta WHERE meta_key='_yoast_wpseo_canonical';
</code></pre>
<p><strong>You should see:</strong> correct addresses from the first three commands and no output from the <code>grep</code>. The three counts came back <code>1</code>, <code>0</code> and <code>0</code>.</p>
<p>I had six clean checks and one wrong page. That told me the address wasn&#8217;t stored where WordPress keeps its settings.</p>
<p>It also explains why the usual advice, &#8220;check your SEO plugin settings&#8221;, goes nowhere. The plugin&#8217;s settings screen was right. The wrong address lived somewhere the settings screen never reads.</p>
<h2>Step 3: Find the Real Cause in Yoast&#8217;s Own Table</h2>
<p>A <strong>permalink</strong> is a page&#8217;s full web address. Yoast keeps a table called <code>wp_yoast_indexable</code>, with a saved copy of the permalink of every page. Each row has a <code>canonical</code> column. When that column is empty, Yoast quietly uses the address it saved.</p>
<p>The <code>canonical</code> column was empty on every row of mine. So I counted how many saved addresses still held the staging domain.</p>
<p><strong>Back up <code>wp_yoast_indexable</code> and <code>wp_yoast_seo_links</code> first</strong>, and open the backup file to check it isn&#8217;t empty. Then run this:</p>
<pre><code class="language-sql">SELECT 'indexable rows', COUNT(*) FROM wp_yoast_indexable
UNION ALL SELECT 'permalink on staging domain', COUNT(*) FROM wp_yoast_indexable WHERE permalink LIKE '%staging.ceeveeglobal%'
UNION ALL SELECT 'og_image on staging', COUNT(*) FROM wp_yoast_indexable WHERE open_graph_image LIKE '%staging.ceeveeglobal%'
UNION ALL SELECT 'twitter_image on staging', COUNT(*) FROM wp_yoast_indexable WHERE twitter_image LIKE '%staging.ceeveeglobal%'
UNION ALL SELECT 'og_image_meta on staging', COUNT(*) FROM wp_yoast_indexable WHERE open_graph_image_meta LIKE '%staging.ceeveeglobal%'
UNION ALL SELECT 'seo_links on staging', COUNT(*) FROM wp_yoast_seo_links WHERE url LIKE '%staging.ceeveeglobal%'
UNION ALL SELECT 'permalink NULL', COUNT(*) FROM wp_yoast_indexable WHERE permalink IS NULL
UNION ALL SELECT 'permalink_hash consistent', COUNT(*) FROM wp_yoast_indexable WHERE permalink IS NOT NULL AND permalink_hash = CONCAT(LENGTH(permalink),':',MD5(permalink))
UNION ALL SELECT 'permalink not NULL', COUNT(*) FROM wp_yoast_indexable WHERE permalink IS NOT NULL;
</code></pre>
<p><strong>You should see:</strong> zero on every <code>staging</code> line. This is what I saw:</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/yoast-table-counts.webp" alt="Counts of stale rows in Yoast's wp_yoast_indexable table before and after the fix" /></p>
<p>There were 611 rows, and 511 of them still held the staging address. Here is what each line of the result means:</p>
<ul>
<li><strong><code>permalink on staging domain</code></strong> counts the stale page addresses. Mine was 511.</li>
<li><strong><code>og_image</code>, <code>twitter_image</code> and <code>og_image_meta</code></strong> are the picture addresses Yoast keeps for social sharing. They held the staging domain in 72, 55 and 49 rows.</li>
<li><strong><code>seo_links on staging</code></strong> counts the internal-link records in <code>wp_yoast_seo_links</code>. Mine held it in 384 rows.</li>
<li><strong><code>permalink NULL</code></strong> counts rows with no saved address. It matters in Step 5.</li>
</ul>
<p>All of it was left over from before the move from staging to live. I had decommissioned my staging site on 13 June 2026, so the address these rows pointed at didn&#8217;t exist any more.</p>
<h2>Step 4: The Half Reset That Made It Worse</h2>
<p>Yoast has its own reset for this. In its code it is called <code>reset_permalink</code>, and it runs when your home address changes. It changes <strong>three</strong> columns:</p>
<pre><code class="language-php">'permalink'      =&gt; null,
'permalink_hash' =&gt; null,
'version'        =&gt; 0,
</code></pre>
<p>I only knew about two of them. So I emptied <code>permalink</code> and <code>permalink_hash</code> and left the rest alone. <strong>Do not run this on your live site.</strong></p>
<pre><code class="language-sql">UPDATE wp_yoast_indexable SET permalink = NULL, permalink_hash = NULL WHERE permalink LIKE '%staging.ceeveeglobal%';
</code></pre>
<p>It emptied the rows in a fraction of a second:</p>
<pre><code class="language-text">Query OK, 511 rows affected (0.017 sec)
Rows matched: 511  Changed: 511  Warnings: 0
</code></pre>
<p>Then I reloaded the page. The canonical tag was gone, completely, and so was <code>og:url</code>.</p>
<p>No wrong address and no address at all. That was worse. Nothing refilled the empty permalinks when a page loaded, so on my copy they just stayed empty.</p>
<p>I also ran Yoast&#8217;s plain indexing command, <code>wp yoast index</code>, after emptying the rows. It added three rows and didn&#8217;t refill any of the emptied ones. The canonical tag stayed missing both times.</p>
<p>The missing piece was the third column. Yoast rebuilds a row when its <code>version</code> is lower than the current one, and its own reset sets <code>version</code> to <code>0</code>, the lowest possible. My rows still said <code>version</code> 2, so as far as Yoast could tell they were up to date.</p>
<h2>Step 5: The Complete Reset (the Actual Fix)</h2>
<p>Do exactly what Yoast&#8217;s own reset does, all three columns:</p>
<pre><code class="language-sql">UPDATE wp_yoast_indexable SET permalink = NULL, permalink_hash = NULL, version = 0 WHERE permalink LIKE '%staging.ceeveeglobal%';
</code></pre>
<p><strong>You should see:</strong> the number of stale rows in the <code>Rows matched</code> line. Mine was 511:</p>
<pre><code class="language-text">Query OK, 511 rows affected (0.015 sec)
Rows matched: 511  Changed: 511  Warnings: 0
</code></pre>
<p>Now load a page. The canonical tag and <code>og:url</code> come back with your live address, because Yoast rebuilds each emptied row the first time its page is visited. For the tag itself, that is all it takes.</p>
<p>I checked that on my copy by visiting every published page once, 108 of them. Not one still carried the staging address in its source.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/complete-reset-output.webp" alt="Output of the complete reset, and the result of visiting every published page once" /></p>
<p>The rows rebuild only as their pages get visited, so <code>permalink NULL</code> stays high for a while. That is normal, and it is why the picture columns and the links table can still hold the old address. Steps 6 and 7 deal with those.</p>
<h2>Step 6: Optional: Rebuild Every Row Now</h2>
<p>If you would rather not wait for visitors, Yoast has a WP-CLI command that rebuilds everything. Its own help says <code>--reindex</code> &#8220;removes all existing indexables and then reindexes them&#8221;:</p>
<pre><code class="language-bash">wp yoast index --reindex
</code></pre>
<p>On my copy, with 108 published posts and pages, it finished in under two minutes. Afterwards no permalink or picture column held the staging address.</p>
<p>Two things it did not do. It rebuilds every row, including ones that were fine, and on a big site that takes a while. It also left 227 stale rows in the links table, so you still need the second statement in Step 7.</p>
<h2>Step 7: Fix the Image and Link Columns</h2>
<p>The picture columns and the links table hold real media addresses, so I kept them and swapped only the domain. If you did Step 6, the picture columns are already rebuilt and only the second statement, the one for <code>wp_yoast_seo_links</code>, matters. Running both is harmless:</p>
<pre><code class="language-sql">UPDATE wp_yoast_indexable SET open_graph_image=REPLACE(open_graph_image,'staging.ceeveeglobal.com','ceeveeglobal.com'), twitter_image=REPLACE(twitter_image,'staging.ceeveeglobal.com','ceeveeglobal.com'), open_graph_image_meta=REPLACE(open_graph_image_meta,'staging.ceeveeglobal.com','ceeveeglobal.com') WHERE open_graph_image LIKE '%staging.ceeveeglobal%' OR twitter_image LIKE '%staging.ceeveeglobal%' OR open_graph_image_meta LIKE '%staging.ceeveeglobal%';
UPDATE wp_yoast_seo_links SET url=REPLACE(url,'staging.ceeveeglobal.com','ceeveeglobal.com') WHERE url LIKE '%staging.ceeveeglobal%';
</code></pre>
<p><strong>You should see:</strong> the matched counts from Step 3. Mine were:</p>
<pre><code class="language-text">Query OK, 72 rows affected (0.007 sec)
Rows matched: 72  Changed: 72  Warnings: 0
Query OK, 384 rows affected (0.015 sec)
Rows matched: 384  Changed: 384  Warnings: 0
</code></pre>
<p>One warning. This only worked because the data in those columns is plain text or JSON. PHP stores serialized data with each string&#8217;s length inside it, like <code>s:5:"hello";</code>, so a plain find and replace on serialized data breaks it.</p>
<h2>Step 8: Clear the Page Cache (Or Your Fix Will Look Like It Failed)</h2>
<p>My database now held zero staging addresses. I reloaded the page and it still said staging.</p>
<p>That is the page cache. It saves a finished copy of every page, wrong canonical included, and keeps serving that copy without asking WordPress. On the real site, 599 files in the cache folder carried the staging address, and the cache held 376 pages in total. A cached copy of my homepage from 14 September already had it.</p>
<p>Clear the saved pages first, then WordPress&#8217;s own object cache, which is its short-term memory for values it has already looked up. These are the commands for WP Fastest Cache, with the paths from my install, so change the path to your own WordPress folder, or use your cache plugin&#8217;s Clear Cache button if you run a different one:</p>
<pre><code class="language-bash">rm -rf /var/www/html/wp-content/cache/all/*
wp cache flush
</code></pre>
<p><strong>You should see:</strong> <code>Success: The cache was flushed.</code> If you need a refresher, my guide on <a href="https://ceeveeglobal.com/error-library/clear_cache/">how to clear the cache on a WordPress site</a> covers the other plugins.</p>
<h2>Step 9: Check It Worked</h2>
<p>Run the one-line check from Step 1 again. You should see your live address in both lines.</p>
<p>Then run the counting query from Step 3 again. Every <code>staging</code> line should read <code>0</code>. On my copy, the homepage and five other pages had zero staging references after the cache was cleared.</p>
<h2>The Script I Used on My Live Site (Optional)</h2>
<p>I have to be straight about this. On my live site I didn&#8217;t know the complete reset would do it. I emptied the rows, saw the tag vanish, and wrote a small script that works out the real address for every row from whatever that row points at. I found the shorter fix afterwards, by reading Yoast&#8217;s source.</p>
<p>The script still works, and it is a fair option if you want every emptied row filled in right now without a full reindex. It never deletes a row. Save it as <code>repair-permalinks.php</code> and run <code>wp eval-file repair-permalinks.php</code> from your WordPress folder:</p>
<pre><code class="language-php">&lt;?php
/**
 * Repopulate Yoast indexable permalinks that were nulled.
 *
 * Nulling the permalink was supposed to make Yoast rebuild it lazily (that is what its own
 * reset-permalinks routine does on a permalink-structure change). On this install it does not
 * rebuild on a front-end request — it simply renders no canonical at all, which is worse than the
 * wrong one. So the permalink is recomputed here from the object each row points at.
 *
 * permalink_hash must stay consistent with permalink: Yoast looks rows up by that hash, and its
 * format is strlen(permalink) . ':' . md5(permalink). Verified against the rows that still had
 * matching hashes before any changes were made.
 *
 * Read-then-write per row, with a count of what it could not resolve. Nothing is deleted.
 */

global $wpdb;

$rows = $wpdb-&gt;get_results(
    "SELECT id, object_type, object_sub_type, object_id
       FROM {$wpdb-&gt;prefix}yoast_indexable
      WHERE permalink IS NULL"
);

$fixed = 0;
$skipped = [];

foreach ( $rows as $r ) {
    $url = null;

    switch ( $r-&gt;object_type ) {
        case 'post':
            if ( $r-&gt;object_id ) {
                $p = get_post( (int) $r-&gt;object_id );
                // Only published/attachment-style objects have a meaningful public permalink.
                if ( $p ) {
                    $url = get_permalink( $p );
                }
            }
            break;

        case 'term':
            if ( $r-&gt;object_id ) {
                $link = get_term_link( (int) $r-&gt;object_id );
                if ( ! is_wp_error( $link ) ) {
                    $url = $link;
                }
            }
            break;

        case 'user':
            if ( $r-&gt;object_id ) {
                $url = get_author_posts_url( (int) $r-&gt;object_id );
            }
            break;

        case 'home-page':
            $url = home_url( '/' );
            break;

        case 'post-type-archive':
            if ( $r-&gt;object_sub_type ) {
                $link = get_post_type_archive_link( $r-&gt;object_sub_type );
                if ( $link ) {
                    $url = $link;
                }
            }
            break;

        case 'date-archive':
            $url = home_url( '/' );
            break;

        // 'system-page' (404, search) has no canonical of its own — left NULL deliberately.
    }

    if ( ! $url ) {
        $key = $r-&gt;object_type . ( $r-&gt;object_sub_type ? ':' . $r-&gt;object_sub_type : '' );
        $skipped[ $key ] = ( $skipped[ $key ] ?? 0 ) + 1;
        continue;
    }

    $wpdb-&gt;update(
        $wpdb-&gt;prefix . 'yoast_indexable',
        [
            'permalink'      =&gt; $url,
            'permalink_hash' =&gt; strlen( $url ) . ':' . md5( $url ),
        ],
        [ 'id' =&gt; (int) $r-&gt;id ]
    );
    $fixed++;
}

echo "repaired: $fixed\n";
echo "left NULL by type:\n";
foreach ( $skipped as $k =&gt; $n ) {
    echo "  $k: $n\n";
}

$remaining = (int) $wpdb-&gt;get_var( "SELECT COUNT(*) FROM {$wpdb-&gt;prefix}yoast_indexable WHERE permalink IS NULL" );
$staging   = (int) $wpdb-&gt;get_var( "SELECT COUNT(*) FROM {$wpdb-&gt;prefix}yoast_indexable WHERE permalink LIKE '%staging.ceeveeglobal%'" );
$hashok    = (int) $wpdb-&gt;get_var(
    "SELECT COUNT(*) FROM {$wpdb-&gt;prefix}yoast_indexable
      WHERE permalink IS NOT NULL AND permalink_hash = CONCAT(LENGTH(permalink),':',MD5(permalink))"
);
$notnull   = (int) $wpdb-&gt;get_var( "SELECT COUNT(*) FROM {$wpdb-&gt;prefix}yoast_indexable WHERE permalink IS NOT NULL" );

echo "\nstill NULL: $remaining\n";
echo "still staging: $staging\n";
echo "hash consistent: $hashok of $notnull non-null\n";
</code></pre>
<p>On my copy it repaired 512 rows and left 47 empty. Those 47 are system pages such as the 404 page and search results, which never had a canonical.</p>
<h2>Common Mistakes (and How to Avoid Them)</h2>
<ul>
<li><strong>Emptying only some of the columns Yoast&#8217;s reset changes.</strong> I emptied two of three, and the canonical tag disappeared instead of coming back. Use the complete reset from Step 5.</li>
<li><strong>Running plain <code>wp yoast index</code> after a half reset.</strong> On my copy it added three rows and left every emptied row empty. Use <code>--reindex</code>, or set <code>version</code> to <code>0</code> as in Step 5.</li>
<li><strong>Fixing the table and skipping the cache.</strong> The page keeps serving the old copy, so the fix looks like it failed. Clear the cache every time you change the table.</li>
<li><strong>Checking only the canonical tag.</strong> My homepage had 14 references, and 12 of them were in the structured data. Count the whole page source, not one line.</li>
<li><strong>Running a find and replace over serialized data.</strong> The stored string lengths stop matching, and the data breaks. Only replace text in columns you know are plain text or JSON.</li>
</ul>
<h2>Frequently Asked Questions</h2>
<h3>How do I know if my site has this problem?</h3>
<p>View the page source of your homepage and search for <code>canonical</code>. If the address is a staging, localhost or old domain, run the counting query from Step 3 to see how many rows in Yoast&#8217;s table still hold it.</p>
<h3>Does a wrong canonical hurt my rankings?</h3>
<p>I didn&#8217;t measure any change in rankings, so I can&#8217;t tell you it did. Google describes a canonical link as &#8220;a strong signal&#8221;, so a dead staging address is the wrong thing to hand it.</p>
<h3>Why didn&#8217;t Yoast fix this itself?</h3>
<p>Yoast runs its reset when the <code>home</code> address option is updated. Why that didn&#8217;t happen on my site, I don&#8217;t know. I can&#8217;t tell how the old addresses got into the table, so I&#8217;m not going to guess.</p>
<h3>Will this work with Rank Math or another SEO plugin?</h3>
<p>I only tested this on a recent Yoast SEO release with WP Fastest Cache active. Another SEO plugin or another cache, or an older Yoast version, might store or serve the address differently, so check its own tables and cache before copying these steps.</p>
<h3>Should I run this on my live site?</h3>
<p>I ran the script from the last section on my live site after backing up both tables. The complete reset and the reindex I only tested on the copy. Try either one on a copy of your own site first.</p>
<h2>Wrapping Up</h2>
<p>The wrong address wasn&#8217;t in any setting. It was in Yoast&#8217;s own table, half a reset made it worse, and the page cache kept serving it after I fixed it.</p>
<p>If a page ever refuses to change after you fix it, my guide to the <a href="https://ceeveeglobal.com/error-library/how-to-fix-wordpress-white-screen-of-death-complete-guide/">WordPress White Screen of Death</a> shows another case where the fix was right and something else was in the way.</p>
<p>The post <a href="https://ceeveeglobal.com/yoast-canonical-points-at-staging/">Yoast Canonical Pointing to Staging: Why No Setting Shows It</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ceeveeglobal.com/yoast-canonical-points-at-staging/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>AI Generated WordPress Plugin Security, Tested for Real</title>
		<link>https://ceeveeglobal.com/ai-wordpress-plugin-security-audit/</link>
					<comments>https://ceeveeglobal.com/ai-wordpress-plugin-security-audit/#respond</comments>
		
		<dc:creator><![CDATA[Dimuthu Harshana]]></dc:creator>
		<pubDate>Mon, 14 Sep 2026 15:16:34 +0000</pubDate>
				<category><![CDATA[Beginner Guides]]></category>
		<category><![CDATA[AI coding]]></category>
		<category><![CDATA[Claude Code]]></category>
		<category><![CDATA[Plugin Check]]></category>
		<category><![CDATA[plugin security]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://ceeveeglobal.com/?p=16153</guid>

					<description><![CDATA[<p>I tested AI generated WordPress plugin security with the official Plugin Check tool: 21 findings, 2 rounds to zero, and one real bug the tool never caught.</p>
<p>The post <a href="https://ceeveeglobal.com/ai-wordpress-plugin-security-audit/">AI Generated WordPress Plugin Security, Tested for Real</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>A year ago I published a plugin I built with AI in fifteen minutes. Buttons, a database table, an admin dashboard — it worked, so I shipped it and moved on. I never once asked whether <strong>AI generated WordPress plugin security</strong> was something I could actually trust, or just something I&#8217;d assumed.</p>
<p>So this time I built another one the same way, and before I touched a single line myself, I ran WordPress.org&#8217;s own security scanner against it. Then I went further, because a warning list isn&#8217;t proof of anything: I tried to actually break the thing.</p>
<p><strong>Here&#8217;s the short version:</strong> the official tool found 21 issues. Two rounds of fixes took that to zero.</p>
<p>And the one bug I could actually exploit for real was never on that list at all. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<h2>What You Need</h2>
<p>Everything here is free and runs on your own machine.</p>
<ul>
<li><strong>A terminal-based AI coding assistant</strong> — I used Claude Code, but the method works with any of them</li>
<li><strong>A local WordPress install</strong> — Docker, LocalWP, whatever you already use</li>
<li><strong>WordPress Plugin Check</strong> — WordPress.org&#8217;s own scanner, install it like any plugin: <code>wp plugin install plugin-check --activate</code></li>
<li><strong>About fifteen minutes</strong>, which I&#8217;ll come back to</li>
</ul>
<h2>The plugin, and why I picked this one</h2>
<p>I asked the assistant for a &#8220;Was this helpful?&#8221; widget: Yes/No buttons under a post, votes stored in the database, a summary on an admin page. Nothing exotic.</p>
<p>I picked it on purpose. It touches every place a beginner plugin usually gets hurt: an AJAX endpoint anyone can hit without logging in, a database table the plugin owns, an admin page only the site owner should see, and a settings form an admin can change. Four different attack surfaces in maybe eighty lines of real logic — dense enough to be worth checking, small enough to actually finish checking in one sitting.</p>
<p>Compare that to what most &#8220;build a plugin with AI&#8221; tutorials show you: a shortcode that prints some text, or a widget that reads an option. Those genuinely have nothing to attack. This one does, and that&#8217;s exactly why it&#8217;s the more honest test.</p>
<p>The prompt I actually used had no security words in it anywhere — no &#8220;secure,&#8221; no &#8220;sanitize,&#8221; no &#8220;nonce.&#8221; Just what I wanted it to do. That&#8217;s the point: <strong>I wanted the default, not the graded version.</strong></p>
<p>One honest caveat before anything else: the assistant that wrote this plugin is the same tool I use every day, tested by an isolated instance that had no idea an audit was coming. I&#8217;m not claiming a blind test — I&#8217;m claiming a single-blind one, and I&#8217;d rather tell you that than pretend otherwise.</p>
<h2>AI generated WordPress plugin security: what Plugin Check actually said</h2>
<p>I ran <code>wp plugin check</code> — WordPress Plugin Check, the tool WordPress.org uses to review real plugin submissions — against the first thing the assistant handed back.</p>
<p><img decoding="async" alt="Plugin Check results showing 2 errors and 18 warnings on the first-pass plugin" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/a1-lint-vs-exploit.webp" /></p>
<p><strong>21 findings: 2 errors, 19 warnings.</strong> And here&#8217;s the part that actually surprised me: <strong>zero nonce problems, zero missing capability checks, zero unescaped output.</strong> The code checked its nonce. It called <code>wp_unslash()</code> before sanitizing input, which is the exact mistake I&#8217;ve personally shipped before. It gated both admin pages behind <code>current_user_can('manage_options')</code>.</p>
<p>What Plugin Check actually flagged was almost entirely one pattern, repeated: SQL built with <code>{$table}</code> and <code>{$column}</code> dropped straight into the query string instead of going through <code>$wpdb-&gt;prepare()</code> properly, plus a missing readme file and an outdated plugin header. Real findings — just not the ones I expected.</p>
<h2>Which of those findings can actually hurt you?</h2>
<p>This is the part nobody else checks. A tool telling you something is unsafe and something actually being unsafe are two different claims, and only one of them is provable.</p>
<p>So I logged in as a real WordPress user, grabbed a live nonce off the actual page, and fired six real requests at the running plugin — the same way an attacker, or just a careless script, would. Not curl commands typed by hand and eyeballed — a script that checks what actually landed in the database afterward, because a request that returns a friendly JSON response and a request that actually stored something are not the same event. I&#8217;ve been burned by that gap before: a tool I built once looked like it was working in every test, right up until someone checked whether the database row it claimed to write was actually there.</p>
<ul>
<li><strong>No nonce at all.</strong> Rejected. The check held.</li>
<li><strong>A script tag as the vote value</strong>, trying for stored XSS. Rejected — the vote only ever accepts &#8220;yes&#8221; or &#8220;no,&#8221; so there was nothing to inject into.</li>
<li><strong><code>1 OR 1=1</code> as the post ID</strong>, going for SQL injection. Rejected — <code>absint()</code> turned it into a plain integer before it ever reached the query.</li>
<li><strong>A Subscriber account with a genuinely valid session</strong>, trying the admin page directly. Blocked. A valid login proves who you are, not what you&#8217;re allowed to do — and the capability check actually enforced that.</li>
<li><strong>A quoted value</strong>, testing whether <code>wp_unslash()</code> was handled correctly. It was.</li>
</ul>
<p>Five attacks, five failures. Plugin Check&#8217;s flagged SQL pattern — the raw <code>{$table}</code> interpolation — turned out to be exactly what it looked like: untidy, not exploitable. Every value going into those queries was already a validated integer or one of two hardcoded strings before the SQL ever ran.</p>
<p><strong>One attack worked</strong>, and it wasn&#8217;t on Plugin Check&#8217;s list, or PHPCS&#8217;s, or PHPStan&#8217;s.</p>
<h2>The comparison table</h2>
<table>
<thead>
<tr>
<th></th>
<th>Plugin Check + PHPCS + PHPStan (static)</th>
<th>Real requests fired</th>
</tr>
</thead>
<tbody>
<tr>
<td>Distinct issues flagged</td>
<td>14</td>
<td>—</td>
</tr>
<tr>
<td>Attacks attempted</td>
<td>—</td>
<td>6</td>
</tr>
<tr>
<td>Actually exploitable</td>
<td>—</td>
<td><strong>1</strong></td>
</tr>
<tr>
<td>What it was</td>
<td>not flagged by any tool</td>
<td>ballot stuffing</td>
</tr>
</tbody>
</table>
<p>Fourteen is the honest number, not the raw one — Plugin Check and PHPCS flagged the <em>same eight lines</em>, just twice each, once per tool. I checked file-and-line for overlap rather than just adding the totals, because reporting 38 would have been a real number technically, and a misleading one in practice.</p>
<h2>Does a nonce make my AJAX endpoint safe?</h2>
<p>No — and this plugin proves both halves of that sentence.</p>
<p>The nonce genuinely stopped forged requests. What it doesn&#8217;t stop is the <em>same valid request</em>, repeated.</p>
<p>I wrote a tiny script that pulled a real nonce off the page and voted &#8220;Yes&#8221; twenty times in a row. The database accepted every single one — the count went from 1 to 21. The only thing standing between a visitor and unlimited votes was a note in the browser&#8217;s local storage, which a script simply doesn&#8217;t have.</p>
<p>That&#8217;s not a nonce failure. It&#8217;s a missing-rate-limit failure, and it&#8217;s invisible to every tool I ran, because none of them ask &#8220;what happens if this exact request runs a thousand times?&#8221;</p>
<h2>How long does it take to get it actually clean?</h2>
<p><img decoding="async" alt="Plugin Check violations dropping from 21 to 2 to 0 across two correction rounds" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/a2-rounds-to-zero.webp" /></p>
<p>I fed Plugin Check&#8217;s own output straight back to a fresh assistant session — the real 21-line report, not my summary of it — and asked it to fix every finding.</p>
<p><strong>Round one: 21 → 2.</strong> It rewrote every SQL call to use WordPress 6.2&#8217;s <code>%i</code> placeholder correctly, added real object caching, and wrote a readme file that hadn&#8217;t existed. That readme immediately introduced two new findings of its own — an outdated &#8220;tested up to&#8221; version and a description that ran over the character limit.</p>
<p><strong>Round two: 2 → 0</strong>, fixing exactly those two lines.</p>
<p>Two rounds. Roughly nine minutes total, timed for real rather than guessed at. And the vote-stuffing bug was still sitting there afterward, completely untouched — because getting Plugin Check to say &#8220;no errors found&#8221; was never the same question as &#8220;is this actually safe.&#8221;</p>
<h2>Common Mistakes</h2>
<p><strong>Trusting &#8220;it works&#8221; as your only test.</strong> My own plugin from last year still has no security section written about it anywhere. It works. That was never the same claim as safe.</p>
<p><strong>Auditing AI code with the same AI that wrote it.</strong> Asking the assistant to grade its own homework tells you what it believes about its own work, not what&#8217;s actually true. Use the tool the WordPress plugin reviewers actually use.</p>
<p><strong>Treating a clean linter run as the finish line.</strong> Twenty-one violations really did go to zero. The one exploitable bug was never touched by that process, at any point, because static analysis reads code — it doesn&#8217;t run it against the questions that actually matter, like &#8220;what if someone sends this exact same request 500 times?&#8221;</p>
<p><strong>Skipping <code>wp_unslash()</code> before sanitizing input.</strong> I&#8217;ve personally shipped this exact bug before. This plugin got it right on the first attempt, which is worth noticing precisely because it&#8217;s the mistake that&#8217;s easiest to make and hardest to catch by reading.</p>
<h2>FAQ</h2>
<p><strong>Is AI generated code secure? WordPress builders keep assuming yes.</strong><br />
This one held against five of six real attacks — no nonce, XSS, SQL injection, missing capability checks. It did have one real gap: unlimited vote stuffing. Test your own before you trust &#8220;it works.&#8221;</p>
<p><strong>What does Plugin Check actually test?</strong><br />
Coding standards, security patterns like unescaped output and unprepared SQL, and WordPress.org submission requirements. It&#8217;s static analysis — it reads your code, it doesn&#8217;t run it against real requests.</p>
<p><strong>Does a valid nonce mean a request is safe?</strong><br />
It means the request came from an active session on your site. It says nothing about what that account is allowed to do, and nothing about how many times the exact same valid request gets repeated.</p>
<p><strong>How long does it really take to build WordPress plugin with AI?</strong><br />
About four minutes to a working install, about nine to a Plugin-Check-clean one — both timed, not estimated. Getting to <em>actually secure</em> took a sixth check no automated tool ran.</p>
<p><strong>Can I run these checks on my own plugin?</strong><br />
Yes — that&#8217;s the actual point of this piece. <code>wp plugin check &lt;your-plugin&gt;</code> costs nothing and takes under a minute. Every script and prompt behind this piece is published; run them against your own plugin and see what comes back.</p>
<p><strong>What&#8217;s the actual fix for the vote-stuffing bug?</strong><br />
Track votes server-side per visitor instead of trusting the browser. A signed cookie, an IP-plus-post-ID row in the votes table, or a rate limit on the AJAX action itself would all close it. I haven&#8217;t shipped that fix here on purpose — the point of this piece is what the audit found, not a finished, hardened plugin.</p>
<h2>So what do you actually do with this?</h2>
<p>Run Plugin Check on whatever AI-generated code is already live on your site. It&#8217;s free, it&#8217;s the same tool plugin reviewers use, and it takes less time than reading this post.</p>
<p>Then go further than the tool does. Ask what happens if a valid request gets repeated. Ask whether a valid login actually proves permission, not just identity.</p>
<p>Those are the questions that catch what static analysis structurally can&#8217;t — because they&#8217;re about behavior, not patterns in the source.</p>
<p>Twenty-one violations reaching zero is a real, checkable win. It just isn&#8217;t the same win as &#8220;this plugin is secure.&#8221; Only one of those two claims was true here, and the tool that would have told you which one never got asked the question.</p>
<p><em>If you haven&#8217;t built your first AI-generated plugin yet, <a href="https://ceeveeglobal.com/build-wordpress-social-share-plugin-ai-tutorial/">start here</a> — then come back and run this checklist on it.</em></p>
<p>The post <a href="https://ceeveeglobal.com/ai-wordpress-plugin-security-audit/">AI Generated WordPress Plugin Security, Tested for Real</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ceeveeglobal.com/ai-wordpress-plugin-security-audit/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WordPress vs Static Site: I Measured Both, Here&#8217;s the Truth</title>
		<link>https://ceeveeglobal.com/wordpress-vs-static-site/</link>
					<comments>https://ceeveeglobal.com/wordpress-vs-static-site/#respond</comments>
		
		<dc:creator><![CDATA[Dimuthu Harshana]]></dc:creator>
		<pubDate>Sun, 06 Sep 2026 13:15:29 +0000</pubDate>
				<category><![CDATA[Beginner Guides]]></category>
		<category><![CDATA[hosting]]></category>
		<category><![CDATA[Hugo]]></category>
		<category><![CDATA[site speed]]></category>
		<category><![CDATA[static site]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://ceeveeglobal.com/?p=16146</guid>

					<description><![CDATA[<p>WordPress vs static site, measured on one machine with the scripts published: static won by 96x, until I turned caching on and the gap fell to 0.08ms.</p>
<p>The post <a href="https://ceeveeglobal.com/wordpress-vs-static-site/">WordPress vs Static Site: I Measured Both, Here&#8217;s the Truth</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>The WordPress vs static site argument is fought almost entirely with numbers nobody measured. I went looking for a straight answer to one question: <strong>should I build my next site on WordPress, or generate it as static files?</strong></p>
<p>What I found was a table. One top-ranking guide publishes time-to-first-byte figures — 200 to 800 milliseconds for WordPress, 20 to 100 for static — and never says what hardware it ran on, what page it requested, what tool it used, or how many times it tried.</p>
<p>That is a range wide enough to be right no matter what happens — and Google&#8217;s AI summary already repeats those numbers back to me as fact.</p>
<p>So I stopped reading and built all three stacks on one machine at home, measured them with scripts I&#8217;m publishing, and got an answer that surprised me. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p><strong>The short version:</strong> static beat WordPress by 96×. Then I turned caching on, and the gap collapsed to 0.08 milliseconds.</p>
<h2>What You Need</h2>
<p>Everything runs on one Linux machine with Docker. No cloud account, no card, nothing to sign up for.</p>
<ul>
<li><strong>Docker and Docker Compose</strong> — the whole lab is containers</li>
<li><strong>WordPress 6.5, MariaDB 11, nginx 1.27</strong> — official images</li>
<li><strong>Hugo 0.128.0 (extended)</strong> — for the build-time test</li>
<li><strong>wrk and wget</strong> — load generator and export tool</li>
<li><strong>About an hour</strong>, most of it waiting</li>
</ul>
<p>Mine ran in a throwaway container on an Ubuntu Optiplex in my house — which is why I&#8217;m careful below about which numbers I&#8217;ll stand behind.</p>
<h2>The setup: four doors into one WordPress site</h2>
<p>Benchmark WordPress on one machine and a static site on another, with different themes and different content, and you haven&#8217;t compared platforms — you&#8217;ve compared two different websites. That&#8217;s why most of these numbers don&#8217;t mean much.</p>
<p>So I ran <strong>one</strong> WordPress install with 200 identical posts, behind four nginx server blocks:</p>
<table>
<thead>
<tr>
<th></th>
<th>Port</th>
<th>What it is</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>A</strong></td>
<td>8081</td>
<td>WordPress, no page cache</td>
</tr>
<tr>
<td><strong>B</strong></td>
<td>8082</td>
<td>The same WordPress + nginx <code>fastcgi_cache</code></td>
</tr>
<tr>
<td><strong>C</strong></td>
<td>8083</td>
<td>A static mirror of A, made with <code>wget</code></td>
</tr>
<tr>
<td><strong>A′</strong></td>
<td>8084</td>
<td>A second door to A, identical config</td>
</tr>
</tbody>
</table>
<p>For WordPress vs static HTML to mean anything, both sides need the same HTML — so the static side is a <code>wget</code> mirror of the WordPress site, not a separately-built Hugo one. That&#8217;s deliberate: it makes the HTML <strong>byte-identical</strong>.</p>
<p>A, B and A′ returned the same md5. C differed by 165 bytes — <code>wget</code> rewriting absolute URLs to relative ones, and nothing else.</p>
<p>So any speed difference belongs to the serving path. Not the theme, not the content, not the page size.</p>
<h3>The control that makes the rest trustworthy</h3>
<p><strong>A′ is the important one, and no comparison I read had anything like it.</strong></p>
<p>It&#8217;s the same stack as A, same PHP-FPM pool, identical config. It <em>is</em> A. If my measurements are any good the two must come out the same — and if they don&#8217;t, my machine is too noisy and every other number here is garbage.</p>
<p>I ran it first, 160 rounds, alternating which went first:</p>
<pre><code class="language-text">A  median TTFB   33.6958 ms
A' median TTFB   33.6912 ms
ratio            0.9999   (drift 0.01%)
gap              0.0047 ms   against an IQR of 0.75 ms
</code></pre>
<p>The two identical stacks landed 0.01% apart — under one percent of the ordinary jitter. That&#8217;s the receipt that lets me report everything else.</p>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> This is the check to demand from anyone showing you a benchmark. If they can&#8217;t tell two identical things apart, they can&#8217;t tell two different things apart either.</p>
<h2>WordPress vs static site: is static actually faster?</h2>
<p>Every static site vs WordPress speed claim you have read starts here, and this part is true: enormously faster — against WordPress with no caching.</p>
<p>I hit each stack 185 times, rotating the order every round so a busy moment on my machine couldn&#8217;t land on one stack and stay there.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/a2-ttfb-by-stack.webp" alt="WordPress vs static site time to first byte, measured across three stacks on one machine" /></p>
<table>
<thead>
<tr>
<th>Stack</th>
<th>Median TTFB</th>
<th>25th–75th</th>
<th>vs uncached</th>
</tr>
</thead>
<tbody>
<tr>
<td>WordPress, uncached</td>
<td>33.70 ms</td>
<td>33.44 – 33.98</td>
<td>1×</td>
</tr>
<tr>
<td>WordPress, cached</td>
<td>0.432 ms</td>
<td>0.384 – 0.471</td>
<td><strong>78× faster</strong></td>
</tr>
<tr>
<td>Static files</td>
<td>0.350 ms</td>
<td>0.305 – 0.400</td>
<td><strong>96× faster</strong></td>
</tr>
</tbody>
</table>
<p>Ninety-six times. That&#8217;s the number that sells the switch, and it&#8217;s real. It&#8217;s also the wrong comparison.</p>
<h2>What happens when WordPress is properly cached</h2>
<p>Look at the middle row again. <strong>Cached WordPress is 78× faster than uncached WordPress</strong> — and it&#8217;s the <em>same WordPress</em>. Same plugins, same theme, same database.</p>
<p>The only change is nginx keeping the finished HTML and handing it out instead of waking PHP.</p>
<p>Which puts static at <strong>1.23× faster than cached WordPress</strong>. Not 96×. Not 10×.</p>
<p>In absolute terms that&#8217;s <strong>0.082 milliseconds</strong>.</p>
<p>What does 0.082ms mean in practice? It is three orders of magnitude smaller than the time a packet takes to reach your server and come back. It vanishes into the noise of the internet before the page starts rendering.</p>
<p>Every guide comparing static to WordPress compares it to the <em>uncached</em> configuration. Nobody runs an uncached WordPress site on purpose.</p>
<h3>Does it hold up when people arrive at once?</h3>
<p>One visitor at a time is the easy case. I pushed 1, 10 and 50 concurrent connections at each stack, three runs each.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/a3-throughput-vs-concurrency.webp" alt="Requests per second for WordPress and a static site as concurrent visitors increase" /></p>
<table>
<thead>
<tr>
<th>Concurrency</th>
<th>WP uncached</th>
<th>WP cached</th>
<th>Static</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>28 rps</td>
<td>6,323 rps</td>
<td>9,551 rps</td>
</tr>
<tr>
<td>10</td>
<td>132 rps</td>
<td>31,697 rps</td>
<td>35,816 rps</td>
</tr>
<tr>
<td>50</td>
<td>129 rps</td>
<td>40,826 rps</td>
<td>47,341 rps</td>
</tr>
</tbody>
</table>
<p>Uncached WordPress hits a wall at about <strong>130 requests per second</strong> and stays there — PHP-FPM&#8217;s default of five worker processes. Five requests at a time, no matter how many people show up.</p>
<p>Cached WordPress and static both keep climbing, and stay within about 16% of each other all the way up. Zero failed responses anywhere, on any stack.</p>
<p>The honest headline isn&#8217;t &#8220;static is faster than WordPress&#8221;. It&#8217;s that <strong>caching is faster than not caching, by 78×</strong> — and static adds a rounding error on top.</p>
<h2>The comparison table</h2>
<p>Everything I measured, in one place:</p>
<table>
<thead>
<tr>
<th></th>
<th>WordPress (cached)</th>
<th>Static</th>
</tr>
</thead>
<tbody>
<tr>
<td>Median TTFB</td>
<td>0.432 ms</td>
<td>0.350 ms</td>
</tr>
<tr>
<td>Requests/sec at 50 concurrent</td>
<td>40,826</td>
<td>47,341</td>
</tr>
<tr>
<td>Page HTML</td>
<td>75,773 bytes</td>
<td>75,608 bytes</td>
</tr>
<tr>
<td>Assets that actually load</td>
<td>14 of 16</td>
<td><strong>8 of 16</strong></td>
</tr>
<tr>
<td>Memory under load</td>
<td>463.9 MiB</td>
<td><strong>38.6 MiB</strong></td>
</tr>
<tr>
<td>Disk</td>
<td>242.7 MiB</td>
<td><strong>40.6 MiB</strong></td>
</tr>
<tr>
<td>Smallest DigitalOcean droplet that fits</td>
<td>$6/mo (1 GB)</td>
<td>$4/mo (512 MB)</td>
</tr>
<tr>
<td>Search, forms, comments</td>
<td>work</td>
<td><strong>broken</strong></td>
</tr>
</tbody>
</table>
<p>Two rows deserve a second look, because they&#8217;re the ones that actually decide this.</p>
<p><strong>Memory is where static wins properly.</strong> WordPress needs MariaDB, PHP-FPM and nginx — 463.9 MiB with traffic on it. Static needs nginx alone: 38.6 MiB.</p>
<p>That&#8217;s <strong>12× less memory and 6× less disk</strong>, and unlike the speed difference it isn&#8217;t a rounding error. It&#8217;s the gap between DigitalOcean&#8217;s <a href="https://www.digitalocean.com/pricing/droplets">$4/month 512 MB droplet and the $6/month 1 GB one</a> — a real, if modest, $24 a year.</p>
<p><strong>And that &#8220;8 of 16&#8221; row is not static being lighter.</strong> I&#8217;ll get to it.</p>
<h2>Does publishing get slower as your blog grows?</h2>
<p>This is the static site generator vs WordPress question I actually wanted answered, and the one nobody measures. The only person I found writing about it was a developer in 2021 whose workflow fell apart as his archive grew — a good post, with no numbers in it.</p>
<p>So I measured the cost of publishing <em>one more post</em> at three archive sizes.</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/a4-publish-cost-vs-archive-size.webp" alt="Time to publish one more post on WordPress versus rebuilding a static site, at 10, 100 and 1000 posts" /></p>
<table>
<thead>
<tr>
<th>Archive size</th>
<th>Static (full rebuild)</th>
<th>WordPress (publish one post)</th>
</tr>
</thead>
<tbody>
<tr>
<td>10 posts</td>
<td>40 ms</td>
<td>35 ms</td>
</tr>
<tr>
<td>100 posts</td>
<td>61 ms</td>
<td>35 ms</td>
</tr>
<tr>
<td>1,000 posts</td>
<td><strong>296 ms</strong></td>
<td><strong>33 ms</strong></td>
</tr>
</tbody>
</table>
<p>Both camps are a little wrong.</p>
<p><strong>Static defenders are wrong that build time is nothing.</strong> Multiply the archive by 100 and the rebuild takes 7.4× longer. That curve is real and keeps going.</p>
<p><strong>The 2021 post is wrong that it falls apart.</strong> At a thousand posts, a full rebuild takes 296 milliseconds. A third of a second. If you publish weekly, you will not notice this in your lifetime.</p>
<p>WordPress stays flat — 33 ms at a thousand posts, statistically identical to 35 ms at ten — because publishing is one database insert regardless of how much you&#8217;ve already written.</p>
<p>So the authoring-loop objection is directionally true and practically irrelevant at blog scale. Revisit it at 50,000 pages; under a few thousand it&#8217;s a non-issue.</p>
<h2>What actually breaks when you go static</h2>
<p>Here&#8217;s where the piece changed my mind.</p>
<p>I put the three things a normal blog has onto the WordPress site — a contact form, site search, comments — exported it the way someone without a paid plugin would, with <code>wget</code>, then tried every feature against the export.</p>
<p><strong>Five of six were broken.</strong></p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>WordPress</th>
<th>Static export</th>
</tr>
</thead>
<tbody>
<tr>
<td>Site search</td>
<td>works</td>
<td><strong>returns the homepage</strong></td>
</tr>
<tr>
<td>Contact form submit</td>
<td>works</td>
<td>404</td>
</tr>
<tr>
<td>Posting a comment</td>
<td>works</td>
<td>404</td>
</tr>
<tr>
<td><code>comment-reply.js</code></td>
<td>loads</td>
<td>404</td>
</tr>
<tr>
<td>RSS feed</td>
<td>works</td>
<td>works, but frozen at export time</td>
</tr>
<tr>
<td>Comment form on the page</td>
<td>works</td>
<td><strong>still renders, posts nowhere</strong></td>
</tr>
</tbody>
</table>
<p>Search made me sit up. On the export, <code>/?s=fixture</code> returns <strong>HTTP 200</strong> — not a 404, a completely normal successful response. A static server has no idea what <code>?s=</code> means, so it ignores it and hands back the homepage.</p>
<p>My first version of this test checked status codes only, and told me <em>&#8220;site search: still works.&#8221;</em> It does not. I had to compare the page bodies to catch it — and if I&#8217;d trusted the status code, I&#8217;d have published the opposite of the truth.</p>
<p>And the contact page is worse, because it looks completely fine:</p>
<p><img decoding="async" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/a5-static-contact-form-dead-scaled.webp" alt="WordPress vs static site: the exported static contact page still showing a working-looking contact form" /></p>
<p>That&#8217;s the exported site. Name, email, subject, message, Submit — and a visitor who fills it in, hits the button, and 404s into nothing.</p>
<p><strong>You don&#8217;t get an error report, because nothing looks broken.</strong> You just stop hearing from people.</p>
<p>This also explains the &#8220;8 of 16 assets&#8221; row above. The static page isn&#8217;t 90KB lighter because it&#8217;s leaner — it&#8217;s lighter because six files it asks for aren&#8217;t there, including the block-navigation CSS and JavaScript.</p>
<h2>Common Mistakes</h2>
<p><strong>Benchmarking WordPress with caching off.</strong> This is the big one. Every &#8220;static is 10× faster&#8221; claim I could find compares against a configuration nobody deploys. Turn on page caching and you recover 78× of the 96×.</p>
<p><strong>Trusting a static site vs WordPress speed table with no method attached.</strong> If it won&#8217;t tell you the hardware, the tool, the page and the number of runs — and shows no spread around its figures — it&#8217;s a guess with decimal points.</p>
<p><strong>Costing the hosting but not the workflow.</strong> The $2/month you save is real. So is the afternoon spent rebuilding your contact form on a third-party service, and every future post going through a build step instead of a Publish button.</p>
<p><strong>Assuming an export keeps things working.</strong> It doesn&#8217;t, and it fails quietly. Go static <em>deliberately</em> — pick a forms service, pick a search service (or drop search), pick a comments host. Don&#8217;t export a dynamic site and hope.</p>
<h2>What I&#8217;m not claiming</h2>
<p>My lab was a container on a home Optiplex that also runs my other work. That is not clean benchmark hardware and I won&#8217;t pretend otherwise.</p>
<p>So I claim the <strong>ratios between stacks</strong>, not the absolute milliseconds. Every stack was measured in the same rounds, in rotating order, minutes apart, and the null control resolved identical stacks to 0.01%.</p>
<p>Under those conditions &#8220;static is 1.23× cached WordPress&#8221; is solid. &#8220;Static serves in 0.35ms&#8221; is true of my box and nothing else.</p>
<p>I&#8217;m also not claiming anything about <strong>SEO</strong> — everyone asserts static ranks better, and I can&#8217;t test it in an afternoon. I didn&#8217;t test managed WordPress hosting or Netlify&#8217;s free tier either. Different question, different piece.</p>
<h2>FAQ</h2>
<p><strong>Is a static site faster than WordPress?</strong><br />
On WordPress vs static HTML, against uncached WordPress, yes — 96× on my measurements. Against WordPress with nginx page caching, 1.23×, which is 0.08 milliseconds. On a real connection that difference is invisible.</p>
<p><strong>Is a static site worth it for a blog I update weekly?</strong><br />
For speed alone, no — caching gets you within a rounding error. For a lower bill and less to maintain, maybe: 12× less memory and no database to patch.</p>
<p><strong>Will my contact form work after converting to static?</strong><br />
No. Mine returned 404 while still displaying a perfectly normal form. If you go static you need a forms service, and you need to know that before you migrate, not after.</p>
<p><strong>Can I keep WordPress and get static speed?</strong><br />
Largely, yes — that&#8217;s the finding. Full-page caching took the same WordPress install from 33.70 ms to 0.432 ms without changing a single plugin or theme file. It&#8217;s the cheapest performance work available to you.</p>
<p><strong>How much does each one actually cost to host?</strong><br />
Static fits DigitalOcean&#8217;s $4/month 512 MB droplet; WordPress needs the $6/month 1 GB tier at minimum. Around $24 a year, before you count your own time.</p>
<p><strong>What about search on a static site?</strong><br />
Site search stops working and does it silently — HTTP 200, homepage returned. You&#8217;d need a client-side index or a hosted search service.</p>
<h2>So which should you build?</h2>
<p>Here&#8217;s the rule I&#8217;d give a friend, and it isn&#8217;t the one I expected to write.</p>
<p>If your site has forms, comments, search, or anything a visitor submits — <strong>stay on WordPress and turn on full-page caching</strong>. That one change bought 78× on my measurements.</p>
<p>You keep everything that works, and the remaining gap is a fraction of a millisecond nobody will perceive.</p>
<p><strong>Reach for a static site generator vs WordPress only when your site genuinely has nothing dynamic in it</strong> — a portfolio, documentation, a landing page, an archive that only you update. Then you get real wins: 12× less memory, a cheaper box, no database to keep patched, and nothing to break at 3am.</p>
<p>What should not decide it is a speed table with no method behind it. The 96× is real and it&#8217;s measuring the wrong thing.</p>
<p>If you take one thing from this: <strong>go and turn caching on</strong>. It&#8217;s free, it takes an afternoon, and it&#8217;s most of the benefit people switch platforms for. You might try it before you rebuild anything.</p>
<p>Every script and raw measurement behind this post is published — the null control, the timing harness, the export probes, all of it.</p>
<p>Run them on your own box and tell me if you get something different. I&#8217;d genuinely like to know. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p><em>Related: <a href="https://ceeveeglobal.com/how-i-connected-wordpress-to-claude-ai-with-an-mcp-server/">How I Connected WordPress to Claude AI With an MCP Server</a></em></p>
<p>The post <a href="https://ceeveeglobal.com/wordpress-vs-static-site/">WordPress vs Static Site: I Measured Both, Here&#8217;s the Truth</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ceeveeglobal.com/wordpress-vs-static-site/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>FastAPI Email Verification API: How I Built It</title>
		<link>https://ceeveeglobal.com/fastapi-email-verification-api/</link>
					<comments>https://ceeveeglobal.com/fastapi-email-verification-api/#respond</comments>
		
		<dc:creator><![CDATA[Dimuthu Harshana]]></dc:creator>
		<pubDate>Thu, 09 Jul 2026 16:14:49 +0000</pubDate>
				<category><![CDATA[Beginner Guides]]></category>
		<guid isPermaLink="false">https://ceeveeglobal.com/fastapi-email-verification-api/</guid>

					<description><![CDATA[<p>My Mautic contact list was filling up with garbage. Fake domains, throwaway addresses, bot signups that never open a single email. Every campaign I sent had a chunk of it just bouncing into nowhere. Quick Answer: I built a small FastAPI email verification API — it checks every email against four things: valid syntax, a&#8230;&#160;<a href="https://ceeveeglobal.com/fastapi-email-verification-api/" rel="bookmark">Read More &#187;<span class="screen-reader-text">FastAPI Email Verification API: How I Built It</span></a></p>
<p>The post <a href="https://ceeveeglobal.com/fastapi-email-verification-api/">FastAPI Email Verification API: How I Built It</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>My Mautic contact list was filling up with garbage. Fake domains, throwaway addresses, bot signups that never open a single email. Every campaign I sent had a chunk of it just bouncing into nowhere.</p>
<blockquote><p><strong>Quick Answer:</strong> I built a small FastAPI email verification API — it checks every email against four things: valid syntax, a disposable-domain blocklist, role-based prefixes like info@ or admin@, and a live DNS MX record lookup, before anything is trusted. It&#8217;s live on my own tools API today, and you can build your own copy in about 10 minutes.</p></blockquote>
<h2>What You Need</h2>
<ul>
<li>Python 3.9 or newer</li>
<li>About 10 minutes and a terminal</li>
<li>A Mautic instance or any signup form, if you want to wire this in for real — optional for just trying the API</li>
<li>Basic FastAPI familiarity helps, but every route here is short enough to follow cold</li>
</ul>
<h2>What I Tried First — And Why It Wasn&#8217;t Enough</h2>
<p>My first fix was a cleanup script, not a filter. I wrote <code>mautic-cleanup.py</code> to scan every contact already in Mautic through its REST API, flag anything on a disposable-domain list or still unconfirmed after 30 days, and delete the flagged ones.</p>
<pre><code class="language-python">DISPOSABLE_DOMAINS = {
    "mailinator.com", "guerrillamail.com", "tempmail.com", "throwaway.email",
    "yopmail.com", "sharklasers.com", "trashmail.com", "dispostable.com",
    # ...786 domains total in my full list
}
# In production this loads from a text file instead of a hardcoded set —
# same idea, just easier to keep adding to as new disposable domains show up.

CUTOFF_DAYS = 30  # unconfirmed contacts older than this are considered bots

def is_bot(contact):
    reasons = []
    email = (contact.get("fields", {}).get("core", {}).get("email", {}).get("value") or "").lower()
    if not email:
        reasons.append("no email")
        return reasons

    domain = email.split("@")[-1] if "@" in email else ""
    if domain in DISPOSABLE_DOMAINS:
        reasons.append(f"disposable domain: {domain}")

    confirmed = contact.get("fields", {}).get("core", {}).get("confirmed", {}).get("value")
    date_added_str = contact.get("dateAdded", "")
    if str(confirmed) in ("0", "false", "") and date_added_str:
        date_added = datetime.fromisoformat(date_added_str.replace("Z", "+00:00"))
        age_days = (datetime.now(timezone.utc) - date_added).days
        if age_days >= CUTOFF_DAYS:
            reasons.append(f"unconfirmed {age_days}d")

    return reasons</code></pre>
<p>Any contact that comes back with a reason gets flagged. The script pulls every contact through Mautic&#8217;s <code>/api/v2/contacts</code> endpoint first, paginating 200 at a time, then runs this check against each one.</p>
<p>It ran as a dry run by default and only deleted with <code>--delete</code>. It worked — I cleared out a real batch of bots the first time I ran it. But it&#8217;s reactive: junk still landed in Mautic first, and I was cleaning up a mess instead of stopping it at the door.</p>
<h2>What Actually Fixed It — Building a FastAPI Email Verification API</h2>
<p>The real fix was moving the check to <em>before</em> a signup gets accepted, not after. So I built a standalone FastAPI endpoint — <code>/email/verify</code> — that scores every address before anything touches Mautic.</p>
<p>I keep it as its own router file, <code>app/routers/email_tools.py</code>, with the disposable-domain list in a separate <code>app/data/disposable_domains.txt</code> so it&#8217;s not baked into the code. One line in <code>main.py</code> wires it into the app:</p>
<pre><code class="language-python">app.include_router(email_tools.router, tags=["email_tools"])</code></pre>
<p>That&#8217;s the same pattern most FastAPI apps use to stay organized as you add more tools — each feature gets its own router file instead of one giant main.py.</p>
<h3>The Four Checks</h3>
<p>Each email goes through four gates, in order, and stops at the first one it fails:</p>
<pre><code class="language-python">import re
import dns.resolver

SYNTAX_RE = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]{2,}$')

ROLE_PREFIXES = {
    "info", "admin", "support", "noreply", "no-reply", "postmaster",
    "webmaster", "contact", "sales", "help", "billing", "abuse",
    "security", "hostmaster", "mailer-daemon", "marketing", "newsletter",
}

def check_mx(domain: str) -> bool:
    try:
        dns.resolver.resolve(domain, "MX")
        return True
    except Exception:
        return False

def verify_single(email: str) -> dict:
    email = email.strip().lower()
    checks = {"syntax": False, "has_mx": False, "is_disposable": False, "is_role_based": False}
    score = 0

    if not SYNTAX_RE.match(email):
        return {"email": email, "is_valid": False, "score": 0, "checks": checks, "reason": "Invalid email format"}
    checks["syntax"] = True
    score += 25

    local, domain = email.split("@", 1)

    if domain in DISPOSABLE_DOMAINS:
        checks["is_disposable"] = True
        return {"email": email, "is_valid": False, "score": score, "checks": checks, "reason": "Disposable email domain not accepted"}
    score += 25

    if local in ROLE_PREFIXES:
        checks["is_role_based"] = True
        return {"email": email, "is_valid": False, "score": score, "checks": checks, "reason": "Role-based addresses not accepted (e.g. info@, admin@)"}
    score += 25

    has_mx = check_mx(domain)
    checks["has_mx"] = has_mx
    if not has_mx:
        return {"email": email, "is_valid": False, "score": score, "checks": checks, "reason": "Email domain has no mail server (MX record missing)"}
    score += 25

    return {"email": email, "is_valid": True, "score": score, "checks": checks, "reason": None}</code></pre>
<p>The scoring stops early. If the syntax is wrong, you get a score of 0 back immediately — there&#8217;s no point checking MX records on something that isn&#8217;t even shaped like an email. Each passed check adds 25 points, so a fully valid address always scores 100.</p>
<blockquote><p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Dimu&#8217;s Note:</strong> the role-based rejection was the one I almost skipped. But <code>info@company.com</code> is a real inbox someone reads — just not the person who signed up for your newsletter. Blocking it kept my open-rate numbers honest.</p></blockquote>
<h3>Why the Checks Run in a Thread Executor</h3>
<p><code>dns.resolver.resolve()</code> is blocking — it waits on a real network call, and so does reading the disposable-domain file. Call either one directly inside an <code>async def</code> route and it freezes the whole event loop, not just that one request. Every other request waiting on your API stalls with it.</p>
<p>The fix is one line per call:</p>
<pre><code class="language-python">loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, verify_single, email)</code></pre>
<p><code>run_in_executor</code> pushes the blocking function onto a background thread pool and hands control straight back to the event loop. Your API stays responsive to every other request while that one DNS lookup is still working. This is the part most FastAPI tutorials skip — and it&#8217;s the difference between an endpoint that scales and one that falls over the moment two people hit it at once.</p>
<h3>Three Endpoints</h3>
<p>I exposed the checker three ways, so it fits however I end up calling it:</p>
<pre><code class="language-python">@router.get("/email/verify")
async def verify_email_get(email: str = Query(...)):
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, verify_single, email)
    return {"success": True, "message": f"Email is {'valid' if result['is_valid'] else 'invalid'}", "result": result}

@router.post("/email/verify")
async def verify_email_post(body: dict):
    email = body.get("email", "")
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, verify_single, email)
    return {"success": True, "message": f"Email is {'valid' if result['is_valid'] else 'invalid'}", "result": result}

@router.post("/email/verify-bulk")
async def verify_email_bulk(body: BulkEmailVerifyRequest):
    emails = body.emails[:50]  # hard cap so one request can't hammer DNS 5000 times
    loop = asyncio.get_event_loop()
    tasks = [loop.run_in_executor(None, verify_single, e) for e in emails]
    results = await asyncio.gather(*tasks)
    valid_count = sum(1 for r in results if r["is_valid"])
    return {"success": True, "result": {"results": results, "summary": {"total": len(results), "valid": valid_count, "invalid": len(results) - valid_count}}}</code></pre>
<p>The bulk endpoint gathers all the executor calls concurrently with <code>asyncio.gather</code>, so 50 emails don&#8217;t verify one at a time — they all run in parallel threads at once.</p>
<h3>Try It Yourself — Self-Host in About 10 Minutes</h3>
<p>You don&#8217;t need my full tools API to run this — it&#8217;s one router file. Here&#8217;s the minimal setup:</p>
<pre><code class="language-bash">pip install fastapi uvicorn dnspython</code></pre>
<p>Drop <code>verify_single</code>, <code>check_mx</code>, and the three routes above into a single <code>main.py</code>, wire the router into a FastAPI app, then run it:</p>
<pre><code class="language-python">from fastapi import FastAPI, APIRouter, Query
from pydantic import BaseModel
from typing import List

router = APIRouter()

class BulkEmailVerifyRequest(BaseModel):
    emails: List[str]

# ...paste verify_single(), check_mx(), and the three @router routes here...

app = FastAPI()
app.include_router(router)</code></pre>
<pre><code class="language-bash">uvicorn main:app --host 0.0.0.0 --port 8000</code></pre>
<p>Then test it:</p>
<pre><code class="language-bash">curl "http://localhost:8000/email/verify?email=test@mailinator.com"</code></pre>
<pre><code class="language-json">{
  "success": true,
  "message": "Email is invalid",
  "result": {
    "email": "test@mailinator.com",
    "is_valid": false,
    "score": 25,
    "checks": {"syntax": true, "has_mx": false, "is_disposable": true, "is_role_based": false},
    "reason": "Disposable email domain not accepted"
  }
}</code></pre>
<p>FastAPI also builds an interactive docs page automatically — mine&#8217;s at <code>/customdocs</code> since I renamed the default route, yours will sit at <code>localhost:8000/docs</code> if you didn&#8217;t touch it. You can test all three endpoints straight from the browser there, no curl needed.</p>
<p>If you&#8217;re putting this on a real domain instead of localhost, I run mine on a Contabo VPS — cheap enough that a small side API like this barely registers on the bill.</p>
<h2>Quick Reference</h2>
<table>
<tr>
<th>Endpoint</th>
<th>Method</th>
<th>Body / Query</th>
<th>Use case</th>
</tr>
<tr>
<td>/email/verify</td>
<td>GET</td>
<td>?email=user@example.com</td>
<td>Quick single check, e.g. from a frontend form</td>
</tr>
<tr>
<td>/email/verify</td>
<td>POST</td>
<td>{&#8220;email&#8221;: &#8220;&#8230;&#8221;}</td>
<td>Same check, from server-to-server calls</td>
</tr>
<tr>
<td>/email/verify-bulk</td>
<td>POST</td>
<td>{&#8220;emails&#8221;: [&#8230;]}, max 50</td>
<td>Cleaning an existing list before import</td>
</tr>
</table>
<pre><code class="language-bash">curl -X POST "http://localhost:8000/email/verify" \
  -H "Content-Type: application/json" \
  -d '{"email": "someone@gmail.com"}'</code></pre>
<pre><code class="language-bash">curl -X POST "http://localhost:8000/email/verify-bulk" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["a@gmail.com", "b@mailinator.com", "info@company.com"]}'</code></pre>
<h2>Advanced Tips</h2>
<p>My live version sits behind an API key, since it shares one FastAPI app with a dozen other tools I&#8217;ve built. Instead of decorating every single route with a dependency, I gate the whole app in one place:</p>
<pre><code class="language-python">class APIKeyMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        if request.url.path not in WHITELISTED_PATHS:
            api_key = request.headers.get(API_KEY_NAME)
            if api_key != API_KEY:
                return JSONResponse(status_code=401, content={"detail": "Invalid API key"})
        return await call_next(request)

app.add_middleware(APIKeyMiddleware)</code></pre>
<p>One middleware class, one line to register it, every route protected except the ones I explicitly whitelist, like the docs page. If you&#8217;re only running the email verifier alone, you probably don&#8217;t need this yet — but the moment you add a second tool to the same app, it&#8217;s worth doing before you&#8217;re pasting the same <code>Depends()</code> onto every route.</p>
<p>The 50-email cap on the bulk endpoint is doing similar work. It&#8217;s not a technical limit, it&#8217;s a guardrail — without it, one request could try to trigger 5,000 DNS lookups and tie up your thread pool for everyone else.</p>
<h2>Frequently Asked Questions</h2>
<h3>Does this replace SMTP-level email verification?</h3>
<p>No. This checks that the domain <em>can</em> receive mail via its MX record, not that the specific mailbox exists. Full SMTP verification means connecting to the mail server and asking directly — slower, and a lot of providers block or fake that response anyway.</p>
<h3>Why not just use a paid email verification API?</h3>
<p>Cost, mostly. Most paid APIs charge per lookup once you&#8217;re past a small free tier. For a Mautic list that&#8217;s mostly WordPress and dev-tool signups, syntax plus disposable plus role plus MX catches the obvious junk without a monthly bill.</p>
<h3>How do I keep the disposable domain list current?</h3>
<p>Mine started small and grew every time a new one slipped through in Mautic — it&#8217;s at 786 domains now. There are public lists on GitHub you can seed from, then keep adding whatever gets past you.</p>
<h3>Will this catch every bot signup?</h3>
<p>No — a bot using a real Gmail address passes every check here. This stops the lazy, high-volume junk: disposable domains, malformed addresses, dead domains — which was most of what was hitting my Mautic list.</p>
<h2>Final Thoughts</h2>
<p>The real lesson wasn&#8217;t the code — it was catching the problem at the door instead of mopping up after. <code>mautic-cleanup.py</code> still runs occasionally as a backstop, but the verification API is what actually changed my numbers.</p>
<p>This endpoint isn&#8217;t wired into my Mautic signup form yet — that&#8217;s next. I&#8217;m also planning to open a version of this up as a free public tool on <a href="https://aibuilttools.com">aibuilttools.com</a>, where you&#8217;ll be able to grab an API key by joining my list — if that&#8217;s useful to you, keep an eye on that page.</p>
<p>The post <a href="https://ceeveeglobal.com/fastapi-email-verification-api/">FastAPI Email Verification API: How I Built It</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ceeveeglobal.com/fastapi-email-verification-api/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How I Connected WordPress to Claude AI With an MCP Server</title>
		<link>https://ceeveeglobal.com/how-i-connected-wordpress-to-claude-ai-with-an-mcp-server/</link>
					<comments>https://ceeveeglobal.com/how-i-connected-wordpress-to-claude-ai-with-an-mcp-server/#respond</comments>
		
		<dc:creator><![CDATA[Dimuthu Harshana]]></dc:creator>
		<pubDate>Mon, 08 Jun 2026 09:00:00 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://ceeveeglobal.com/how-i-connected-wordpress-to-claude-ai-with-an-mcp-server/</guid>

					<description><![CDATA[<p>I never planned to build an MCP server. I just got tired of switching tabs. Every time I wanted Claude to help me update a post, add a new page, or check my plugin list, I was copying and pasting. Text in. Text out. Then manually clicking through the WordPress dashboard to actually apply it.&#8230;&#160;<a href="https://ceeveeglobal.com/how-i-connected-wordpress-to-claude-ai-with-an-mcp-server/" rel="bookmark">Read More &#187;<span class="screen-reader-text">How I Connected WordPress to Claude AI With an MCP Server</span></a></p>
<p>The post <a href="https://ceeveeglobal.com/how-i-connected-wordpress-to-claude-ai-with-an-mcp-server/">How I Connected WordPress to Claude AI With an MCP Server</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>I never planned to build an MCP server. I just got tired of switching tabs.</p>
<p>Every time I wanted Claude to help me update a post, add a new page, or check my plugin list, I was copying and pasting. Text in. Text out. Then manually clicking through the WordPress dashboard to actually apply it.</p>
<p>There had to be a better way. And there was — it just took me a full weekend of broken Node.js paths and permission errors to find it.</p>
<hr />
<h2 id="what-i-tried-first-and-why-it-didnt-work">What I Tried First — And Why It Didn&#8217;t Work</h2>
<p>My first instinct was to use a REST API approach. WordPress has a built-in REST API, so I figured I&#8217;d just point Claude at it with an API key and call it a day.</p>
<p>Two problems showed up immediately.</p>
<p><strong>First problem</strong>: Claude Code doesn&#8217;t natively &#8220;call&#8221; REST APIs on its own during a conversation — it needs a tool or server to bridge that connection. The REST API is great for building apps, but it isn&#8217;t a conversational interface.</p>
<p><strong>Second problem</strong>: My WordPress site runs inside a Docker container on a Coolify VPS. It&#8217;s not publicly accessible via REST during dev work — I&#8217;d have to expose ports and deal with auth tokens just to test a simple post update. That felt like overkill for what I needed.</p>
<p>I also tried a plugin called &#8220;AI Power&#8221; which connects WordPress to OpenAI. Useful for content generation — but it doesn&#8217;t let Claude <em>control</em> WordPress. It just uses GPT inside the WordPress editor. That&#8217;s the opposite direction from what I wanted.</p>
<p>What I actually needed was an <strong>MCP server</strong> — a small Node.js process that exposes WordPress tools (create post, get posts, update page, list plugins) to Claude Code via the Model Context Protocol.</p>
<p>So I built one.</p>
<hr />
<h2 id="what-actually-fixed-it-the-mcp-server-setup">What Actually Fixed It — The MCP Server Setup</h2>
<p>Here&#8217;s exactly how I set it up. My stack: <strong>Coolify VPS + Docker WordPress container + Node.js MCP server</strong>.</p>
<h3 id="step-1-ssh-into-the-vps-and-set-up-a-working-directory">Step 1 — SSH into the VPS and set up a working directory</h3>
<pre><code class="language-bash">ssh coolify-vps
mkdir -p /home/coolify-service/wp-mcp
cd /home/coolify-service/wp-mcp
</code></pre>
<p>I created a dedicated folder outside the Docker container. The MCP server runs on the host — it communicates with WordPress via WP-CLI commands executed <em>inside</em> the container.</p>
<h3 id="step-2-install-nodejs-on-the-vps-host">Step 2 — Install Node.js on the VPS host</h3>
<pre><code class="language-bash">curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version  # should show v20.x.x
</code></pre>
<p>If you&#8217;re on a minimal Coolify VPS like mine, Node.js won&#8217;t be pre-installed. Don&#8217;t skip this check.</p>
<h3 id="step-3-initialize-the-project-and-install-the-mcp-sdk">Step 3 — Initialize the project and install the MCP SDK</h3>
<pre><code class="language-bash">cd /home/coolify-service/wp-mcp
npm init -y
npm install @modelcontextprotocol/sdk
</code></pre>
<p>The <code>@modelcontextprotocol/sdk</code> is the official Anthropic MCP package. It handles all the protocol handshake — you just define tools.</p>
<h3 id="step-4-write-the-serverjs-file">Step 4 — Write the server.js file</h3>
<p>This is the core of the whole setup. Here&#8217;s a simplified version of what my <code>server.js</code> looks like:</p>
<pre><code class="language-javascript">import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { execSync } from "child_process";

const WP_CONTAINER = "wordpress-n0g4skoo8sgwgc848koog0ww";

function runWpCli(command) {
  const full = `docker exec ${WP_CONTAINER} wp --allow-root ${command}`;
  return execSync(full, { encoding: "utf8" });
}

const server = new Server(
  { name: "wordpress-mcp", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () =&gt; ({
  tools: [
    {
      name: "wp_get_posts",
      description: "List recent WordPress posts",
      inputSchema: {
        type: "object",
        properties: {
          count: { type: "number", description: "Number of posts to retrieve" }
        }
      }
    },
    {
      name: "wp_create_post",
      description: "Create a new WordPress post",
      inputSchema: {
        type: "object",
        properties: {
          title: { type: "string" },
          content: { type: "string" },
          status: { type: "string", enum: ["draft", "publish"] }
        },
        required: ["title", "content"]
      }
    }
  ]
}));

server.setRequestHandler(CallToolRequestSchema, async (request) =&gt; {
  const { name, arguments: args } = request.params;

  if (name === "wp_get_posts") {
    const count = args.count || 5;
    const result = runWpCli(`post list --posts_per_page=${count} --fields=ID,post_title,post_status --format=json`);
    return { content: [{ type: "text", text: result }] };
  }

  if (name === "wp_create_post") {
    const { title, content, status = "draft" } = args;
    const result = runWpCli(`post create --post_title="${title}" --post_content="${content}" --post_status=${status}`);
    return { content: [{ type: "text", text: result }] };
  }

  throw new Error(`Unknown tool: ${name}`);
});

const transport = new StdioServerTransport();
await server.connect(transport);
</code></pre>
<p>The key insight here: <strong>every tool just runs a WP-CLI command inside the Docker container via <code>docker exec</code></strong>. No REST API. No auth tokens. Direct shell access.</p>
<h3 id="step-5-add-the-mcp-server-to-claude-code-config">Step 5 — Add the MCP server to Claude Code config</h3>
<p>On your local machine (not the VPS), open your Claude Code MCP config file. On Windows it&#8217;s at:</p>
<pre><code>C:\Users\YourName\AppData\Roaming\Claude\claude_desktop_config.json
</code></pre>
<p>Add this block:</p>
<pre><code class="language-json">{
  "mcpServers": {
    "wordpress": {
      "command": "ssh",
      "args": [
        "coolify-vps",
        "node /home/coolify-service/wp-mcp/server.js"
      ]
    }
  }
}
</code></pre>
<p>This tells Claude Code: when you need WordPress tools, SSH into my VPS and run the Node.js server. The MCP protocol runs over stdio — so SSH becomes the transport pipe.</p>
<h3 id="step-6-test-it">Step 6 — Test it</h3>
<p>Restart Claude Code. In the chat, try:</p>
<pre><code>List my last 5 WordPress posts
</code></pre>
<p>If it works, you&#8217;ll see Claude call the <code>wp_get_posts</code> tool and return your actual post titles. That moment — the first time Claude listed my real posts from ceeveeglobal.com without me touching the dashboard — genuinely felt like magic. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="language-bash"># SSH into VPS
ssh coolify-vps

# Start MCP server manually (for testing)
node /home/coolify-service/wp-mcp/server.js

# Test WP-CLI directly (check connection works)
docker exec wordpress-n0g4skoo8sgwgc848koog0ww wp --allow-root post list

# Check Node.js version on VPS
node --version

# Reinstall SDK if something breaks
cd /home/coolify-service/wp-mcp &amp;&amp; npm install
</code></pre>
<p><strong>Claude Code config location (Windows):</strong></p>
<pre><code>%APPDATA%\Claude\claude_desktop_config.json
</code></pre>
<p><strong>Claude Code config location (Mac):</strong></p>
<pre><code>~/Library/Application Support/Claude/claude_desktop_config.json
</code></pre>
<hr />
<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>
<p><strong>Q: Do I need a special WordPress plugin for this to work?</strong></p>
<p>No plugin needed. The MCP server communicates with WordPress through WP-CLI running inside the Docker container. As long as WP-CLI is installed in your container (it is by default in most WordPress Docker images), you&#8217;re good to go. No REST API, no extra plugins — just shell commands.</p>
<p><strong>Q: Is this secure? I don&#8217;t want to expose my WordPress to the internet.</strong></p>
<p>It&#8217;s actually more secure than the REST API approach. The MCP server runs on your VPS host and only accepts connections via SSH. Nothing is exposed publicly. Claude Code connects through your existing SSH key pair — same as if you logged in manually. Your WordPress dashboard URL stays private.</p>
<p><strong>Q: What happens if the Docker container name changes after a Coolify update?</strong></p>
<p>This caught me once. After a Coolify update, my container got recreated with a different ID. I store the container name as a constant at the top of <code>server.js</code> — so I just update that one line and restart. To find your current container name:</p>
<pre><code class="language-bash">docker ps --filter "name=wordpress" --format "{{.Names}}"
</code></pre>
<hr />
<h2 id="final-thoughts">Final Thoughts</h2>
<p>This setup changed how I work with my WordPress sites. Instead of switching between Claude and the dashboard, I just describe what I want and it happens — new posts drafted, plugin lists checked, page content updated, all through conversation.</p>
<p>It&#8217;s not a product you can buy. It&#8217;s something you build, and that&#8217;s the point. If you want to go further — more tools, more sites, a shared MCP config — I&#8217;ve packaged some of these patterns into ready-to-use tools at <a href="https://aibuilttools.com">aibuilttools.com</a>.</p>
<p>Start with one tool. Get it working. Build from there.</p>
<p>The post <a href="https://ceeveeglobal.com/how-i-connected-wordpress-to-claude-ai-with-an-mcp-server/">How I Connected WordPress to Claude AI With an MCP Server</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ceeveeglobal.com/how-i-connected-wordpress-to-claude-ai-with-an-mcp-server/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Mautic 5 Missing in Coolify? Here’s How to Fix It with Docker Compose</title>
		<link>https://ceeveeglobal.com/mautic-5-missing-in-coolify-heres-how-to-fix-it-with-docker-compose/</link>
					<comments>https://ceeveeglobal.com/mautic-5-missing-in-coolify-heres-how-to-fix-it-with-docker-compose/#respond</comments>
		
		<dc:creator><![CDATA[Dimuthu Harshana]]></dc:creator>
		<pubDate>Mon, 25 May 2026 08:46:31 +0000</pubDate>
				<category><![CDATA[Beginner Guides]]></category>
		<guid isPermaLink="false">https://ceeveeglobal.com/?p=15975</guid>

					<description><![CDATA[<p>For users wondering why Mautic 5 is missing in Coolify, you aren’t alone. Many Coolify users expect a seamless integration with Mautic, but are met with disappointment when it’s nowhere to be found. In this guide, we’ll explore why Mautic 5 vanished and provide a step-by-step solution to deploy it manually using Docker Compose. Whether&#8230;&#160;<a href="https://ceeveeglobal.com/mautic-5-missing-in-coolify-heres-how-to-fix-it-with-docker-compose/" rel="bookmark">Read More &#187;<span class="screen-reader-text">Mautic 5 Missing in Coolify? Here’s How to Fix It with Docker Compose</span></a></p>
<p>The post <a href="https://ceeveeglobal.com/mautic-5-missing-in-coolify-heres-how-to-fix-it-with-docker-compose/">Mautic 5 Missing in Coolify? Here’s How to Fix It with Docker Compose</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">For users wondering why Mautic 5 is missing in Coolify, you aren’t alone. Many Coolify users expect a seamless integration with Mautic, but are met with disappointment when it’s nowhere to be found. In this guide, we’ll explore why Mautic 5 vanished and provide a step-by-step solution to deploy it manually using Docker Compose. Whether you&#8217;re a WordPress enthusiast or a marketing automation aficionado, this solution will get you back on track.</p>



<h2 class="wp-block-heading" id="understanding-the-issue-why-is-mautic-5-missing-in-coolify">Understanding the Issue: Why is Mautic 5 Missing in Coolify?<a href="http://localhost:8501/#understanding-the-issue-why-is-mautic-5-missing-in-coolify"></a></h2>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1376" height="768" data-id="15977" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/Coolify_dashboard.jpeg" alt="" class="wp-image-15977" srcset="https://s3.ceeveeglobal.com/ceeveeglobalimages/Coolify_dashboard.jpeg 1376w, https://s3.ceeveeglobal.com/ceeveeglobalimages/Coolify_dashboard-600x335.jpeg 600w" sizes="(max-width: 1376px) 100vw, 1376px" /></figure>
</figure>



<p class="wp-block-paragraph">When you open Coolify, looking for Mautic under the &#8220;Add Resource&#8221; section, it can be frustrating to find nothing. The absence of a one-click app or template for Mautic 5 in Coolify is due to recent updates and changes in the Coolify ecosystem, necessitating a manual workaround.</p>



<h3 class="wp-block-heading" id="why-mautic-matters-for-wordpress-users">Why Mautic Matters for WordPress Users<a href="http://localhost:8501/#why-mautic-matters-for-wordpress-users"></a></h3>



<p class="wp-block-paragraph">Mautic is a powerful open-source marketing automation tool, seamlessly integrating with WordPress to enhance customer engagement and conversion rates. Its absence from Coolify means WordPress users need an alternative method to keep their automation processes running smoothly.</p>



<h2 class="wp-block-heading" id="prerequisites">Prerequisites<a href="http://localhost:8501/#prerequisites"></a></h2>



<p class="wp-block-paragraph">Before diving into the deployment process, ensure you have the following prerequisites:</p>



<ul class="wp-block-list">
<li>Basic understanding of Docker and Docker Compose.</li>



<li>Access to a server with Docker installed.</li>



<li>Admin access to a Coolify account.</li>



<li>A domain name if you plan to use a custom URL.</li>
</ul>



<h2 class="wp-block-heading" id="step-by-step-implementation-deploying-mautic-5-with-docker-compose">Step-by-Step Implementation: Deploying Mautic 5 with Docker Compose<a href="http://localhost:8501/#step-by-step-implementation-deploying-mautic-5-with-docker-compose"></a></h2>



<figure class="wp-block-image size-full"><img decoding="async" width="1376" height="768" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/Coolify_Mautic_5_deployment_guide.jpeg" alt="" class="wp-image-15978" srcset="https://s3.ceeveeglobal.com/ceeveeglobalimages/Coolify_Mautic_5_deployment_guide.jpeg 1376w, https://s3.ceeveeglobal.com/ceeveeglobalimages/Coolify_Mautic_5_deployment_guide-600x335.jpeg 600w" sizes="(max-width: 1376px) 100vw, 1376px" /></figure>



<p class="wp-block-paragraph">Despite its absence in Coolify, deploying Mautic 5 is straightforward with Docker Compose. Follow these steps to get started:</p>



<ol class="wp-block-list">
<li><strong>Create a New Project in Coolify:</strong> Navigate to your Coolify dashboard and click <em>New</em>. Name your project (e.g., &#8220;Mautic&#8221;), then proceed with <em>Continue</em>.</li>



<li><strong>Add a Resource:</strong> Choose <em>Add Resource</em> and select <em>Docker Compose Empty</em> to initiate your custom setup.</li>



<li><strong>Insert the Docker Compose File:</strong> Copy the Docker Compose file provided in the resource link and paste it into the designated area. Click <em>Save</em>, then <em>Deploy</em> to start the process.<br><strong>Access Your Mautic Instance:</strong> Once deployed, go to <em>Links</em> in Coolify and click the generated URL to complete your Mautic setup.</li>
</ol>



<pre class="wp-block-code"><code>version: "3.9"

services:
  db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_64_MYSQLROOT}
      - MYSQL_DATABASE=${MYSQL_DATABASE:-mautic}
      - MYSQL_USER=${SERVICE_USER_MYSQL}
      - MYSQL_PASSWORD=${SERVICE_PASSWORD_64_MYSQL}
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: mysqladmin ping --silent --user=$${SERVICE_USER_MYSQL} --password=$${SERVICE_PASSWORD_64_MYSQL}
      start_period: 30s
      interval: 10s
      timeout: 5s
      retries: 5

  rabbitmq:
    image: rabbitmq:3
    environment:
      - RABBITMQ_DEFAULT_VHOST=${RABBITMQ_DEFAULT_VHOST:-mautic}
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq

  mautic_web:
    image: mautic/mautic:latest
    ports:
      - "8880:80"
    volumes:
      - mautic_data/config:/var/www/html/config:z
      - mautic_data/logs:/var/www/html/var/logs:z
      - mautic_data/media/files:/var/www/html/docroot/media/files:z
      - mautic_data/media/images:/var/www/html/docroot/media/images:z
      - mautic_data/plugins:/var/www/html/docroot/plugins:z
      - mautic_data/vendor:/var/www/html/vendor:z
      - mautic_data/bin:/var/www/html/bin:z
      - mautic_data/cron:/opt/mautic/cron:z
    environment:
      - SERVICE_FQDN_MAUTIC_80
      - DOCKER_MAUTIC_LOAD_TEST_DATA=${MAUTIC_LOAD_TEST_DATA:-false}
      - DOCKER_MAUTIC_RUN_MIGRATIONS=${MAUTIC_RUN_MIGRATIONS:-false}
      - MAUTIC_DB_HOST=${MYSQL_HOST:-db}
      - MAUTIC_DB_PORT=${MYSQL_PORT:-3306}
      - MAUTIC_DB_DATABASE=${MYSQL_DATABASE:-mautic}
      - MAUTIC_DB_USER=${SERVICE_USER_MYSQL}
      - MAUTIC_DB_PASSWORD=${SERVICE_PASSWORD_64_MYSQL}
      - MAUTIC_MESSENGER_DSN_EMAIL=amqp://guest:guest@rabbitmq:5672/mautic/messages
      - MAUTIC_MESSENGER_DSN_HIT=amqp://guest:guest@rabbitmq:5672/mautic/messages
    depends_on:
      db:
        condition: service_healthy
      rabbitmq:
        condition: service_started
    healthcheck:
      test: &#91;"CMD", "curl", "-f", "http://localhost"]
      interval: 15s
      timeout: 10s
      retries: 15

  mautic_cron:
    image: mautic/mautic:latest
    volumes:
      - mautic_data/config:/var/www/html/config:z
      - mautic_data/logs:/var/www/html/var/logs:z
      - mautic_data/media/files:/var/www/html/docroot/media/files:z
      - mautic_data/media/images:/var/www/html/docroot/media/images:z
      - mautic_data/plugins:/var/www/html/docroot/plugins:z
      - mautic_data/vendor:/var/www/html/vendor:z
      - mautic_data/bin:/var/www/html/bin:z
      - mautic_data/cron:/opt/mautic/cron:z
    environment:
      - DOCKER_MAUTIC_ROLE=mautic_cron
      - MAUTIC_DB_HOST=${MYSQL_HOST:-db}
      - MAUTIC_DB_PORT=${MYSQL_PORT:-3306}
      - MAUTIC_DB_DATABASE=${MYSQL_DATABASE:-mautic}
      - MAUTIC_DB_USER=${SERVICE_USER_MYSQL}
      - MAUTIC_DB_PASSWORD=${SERVICE_PASSWORD_64_MYSQL}
      - MAUTIC_MESSENGER_DSN_EMAIL=amqp://guest:guest@rabbitmq:5672/mautic/messages
      - MAUTIC_MESSENGER_DSN_HIT=amqp://guest:guest@rabbitmq:5672/mautic/messages
    depends_on:
      mautic_web:
        condition: service_healthy

  mautic_worker:
    image: mautic/mautic:latest
    volumes:
      - mautic_data/config:/var/www/html/config:z
      - mautic_data/logs:/var/www/html/var/logs:z
      - mautic_data/media/files:/var/www/html/docroot/media/files:z
      - mautic_data/media/images:/var/www/html/docroot/media/images:z
      - mautic_data/plugins:/var/www/html/docroot/plugins:z
      - mautic_data/vendor:/var/www/html/vendor:z
      - mautic_data/bin:/var/www/html/bin:z
      - mautic_data/cron:/opt/mautic/cron:z
    environment:
      - DOCKER_MAUTIC_ROLE=mautic_worker
      - MAUTIC_DB_HOST=${MYSQL_HOST:-db}
      - MAUTIC_DB_PORT=${MYSQL_PORT:-3306}
      - MAUTIC_DB_DATABASE=${MYSQL_DATABASE:-mautic}
      - MAUTIC_DB_USER=${SERVICE_USER_MYSQL}
      - MAUTIC_DB_PASSWORD=${SERVICE_PASSWORD_64_MYSQL}
      - MAUTIC_MESSENGER_DSN_EMAIL=amqp://guest:guest@rabbitmq:5672/mautic/messages
      - MAUTIC_MESSENGER_DSN_HIT=amqp://guest:guest@rabbitmq:5672/mautic/messages
    depends_on:
      mautic_web:
        condition: service_healthy

volumes:
  mysql-data:
  rabbitmq-data:
  mautic_data:</code></pre>



<h3 class="wp-block-heading" id="connecting-a-custom-domain">Connecting a Custom Domain<a href="http://localhost:8501/#connecting-a-custom-domain"></a></h3>



<figure class="wp-block-image size-full"><img decoding="async" width="1376" height="768" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/Domain_connected_to_Mautic_instance-1.jpeg" alt="" class="wp-image-15980" srcset="https://s3.ceeveeglobal.com/ceeveeglobalimages/Domain_connected_to_Mautic_instance-1.jpeg 1376w, https://s3.ceeveeglobal.com/ceeveeglobalimages/Domain_connected_to_Mautic_instance-1-600x335.jpeg 600w" sizes="(max-width: 1376px) 100vw, 1376px" /></figure>



<p class="wp-block-paragraph">For personalised branding, connecting a custom domain to your Mautic instance is beneficial. Here’s how:</p>



<ul class="wp-block-list">
<li>Log in to your domain provider (e.g., Namecheap) and access Advanced DNS settings.</li>



<li>Add a new record, select <strong>A Record</strong>, enter your subdomain (e.g., &#8220;mautic&#8221;), and point it to your server&#8217;s IP.</li>



<li>Save the changes. After DNS propagation, return to Coolify, open your Mautic service, navigate to <em>Settings</em>, and add your subdomain.</li>
</ul>



<h2 class="wp-block-heading" id="advanced-insights-expert-tips-for-optimizing-your-setup">Advanced Insights: Expert Tips for Optimising Your Setup<a href="http://localhost:8501/#advanced-insights-expert-tips-for-optimizing-your-setup"></a></h2>



<p class="wp-block-paragraph">Deploying Mautic through Docker Compose opens avenues for customization and optimization. Here are expert-level tips:</p>



<ul class="wp-block-list">
<li><strong>Optimize Performance:</strong> Configure Docker resources such as CPU and memory allocation to ensure Mautic runs efficiently.</li>



<li><strong>Regular Backups:</strong> Set up automated backups for your Mautic data to prevent data loss. Tools like <a href="https://www.digitalocean.com/products/spaces/" target="_blank" rel="noreferrer noopener">DigitalOcean Spaces</a> can be valuable here.</li>



<li><strong>Enhance Security:</strong> Use SSL certificates for secure communication. Services like <a href="https://letsencrypt.org/" target="_blank" rel="noreferrer noopener">Let&#8217;s Encrypt</a> provide free SSL certificates.</li>
</ul>



<h3 class="wp-block-heading" id="integrating-affiliate-recommendations">Integrating Affiliate Recommendations<a href="http://localhost:8501/#integrating-affiliate-recommendations"></a></h3>



<p class="wp-block-paragraph">For an all-in-one solution, consider hosting providers like&nbsp;<a href="https://www.vultr.com/" target="_blank" rel="noreferrer noopener">Vultr</a>&nbsp;or&nbsp;<a href="https://www.interserver.net/" target="_blank" rel="noreferrer noopener">InterServer</a>&nbsp;for reliable and cost-effective hosting solutions. Additionally,&nbsp;<a href="https://www.namecheap.com/" target="_blank" rel="noreferrer noopener">Namecheap</a>&nbsp;remains a top choice for domain management.</p>



<h2 class="wp-block-heading" id="troubleshooting-common-issues">Troubleshooting Common Issues<a href="http://localhost:8501/#troubleshooting-common-issues"></a></h2>



<p class="wp-block-paragraph">If you encounter issues during the installation or deployment process, consider the following tips:</p>



<ul class="wp-block-list">
<li><strong>Port Conflicts:</strong> Ensure no other services are using the ports Mautic requires.</li>



<li><strong>Check Logs:</strong> Use Docker logs to identify any errors during deployment.</li>



<li><strong>Network Settings:</strong> Verify your server&#8217;s firewall and network settings allow necessary traffic.</li>
</ul>



<h2 class="wp-block-heading" id="conclusion-bringing-mautic-back-to-coolify">Conclusion: Bringing Mautic Back to Coolify<a href="http://localhost:8501/#conclusion-bringing-mautic-back-to-coolify"></a></h2>



<p class="wp-block-paragraph">While the absence of Mautic 5 in Coolify might initially seem daunting, the manual deployment using Docker Compose is a robust alternative. By following the steps outlined above, WordPress users can regain the powerful marketing automation capabilities Mautic offers. Embrace these changes, and enhance your online strategies with a tailored approach.</p>



<h2 class="wp-block-heading" id="faqs-mautic-5-missing-in-coolify">FAQs: Mautic 5 Missing in Coolify<a href="http://localhost:8501/#faqs-mautic-5-missing-in-coolify"></a></h2>



<ul class="wp-block-list">
<li><strong>Why is Mautic 5 not showing in Coolify?</strong> Coolify has undergone updates, removing the direct one-click Mautic installation, necessitating a manual setup.</li>



<li><strong>Can I still use Mautic with Coolify?</strong> Yes, by utilizing Docker Compose, you can manually deploy Mautic 5 on your server.</li>



<li><strong>Is Docker Compose difficult to use?</strong> Docker Compose simplifies the deployment process, especially when following detailed guides like this one.</li>
</ul>



<h2 class="wp-block-heading" id="summary">Summary<a href="http://localhost:8501/#summary"></a></h2>



<p class="wp-block-paragraph">To recap, deploying Mautic 5 in Coolify involves creating a new project, adding a Docker Compose resource, and configuring your domain. With these steps, you can restore Mautic’s capabilities even without direct Coolify support.</p>



<p class="wp-block-paragraph">Have questions or need further clarification? Feel free to leave a comment below, and we’ll be happy to assist you.</p>



<p class="wp-block-paragraph">Explore more WordPress tips and tricks|/wordpress-tips-tricks<br>Learn how to optimize your WordPress site for SEO|/wordpress-seo-optimization</p>
<p>The post <a href="https://ceeveeglobal.com/mautic-5-missing-in-coolify-heres-how-to-fix-it-with-docker-compose/">Mautic 5 Missing in Coolify? Here’s How to Fix It with Docker Compose</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ceeveeglobal.com/mautic-5-missing-in-coolify-heres-how-to-fix-it-with-docker-compose/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>8 Essential WordPress Code Concepts Every Beginner Should Master (With Real Examples)</title>
		<link>https://ceeveeglobal.com/essential-wordpress-code-concepts-beginners-guide/</link>
					<comments>https://ceeveeglobal.com/essential-wordpress-code-concepts-beginners-guide/#respond</comments>
		
		<dc:creator><![CDATA[Dimuthu Harshana]]></dc:creator>
		<pubDate>Sat, 08 Nov 2025 05:52:58 +0000</pubDate>
				<category><![CDATA[Beginner Guides]]></category>
		<category><![CDATA[wordpress tutorial]]></category>
		<guid isPermaLink="false">https://ceeveeglobal.com/?p=15904</guid>

					<description><![CDATA[<p>Do you know WordPress runs on just 8 fundamental code concepts? Most beginners copy-paste WordPress code from Stack Overflow without understanding what it actually does. I did the same thing for months until I realized I was just guessing. One wrong character in functions.php, and boom — White Screen of Death. Here&#8217;s the truth: you&#8230;&#160;<a href="https://ceeveeglobal.com/essential-wordpress-code-concepts-beginners-guide/" rel="bookmark">Read More &#187;<span class="screen-reader-text">8 Essential WordPress Code Concepts Every Beginner Should Master (With Real Examples)</span></a></p>
<p>The post <a href="https://ceeveeglobal.com/essential-wordpress-code-concepts-beginners-guide/">8 Essential WordPress Code Concepts Every Beginner Should Master (With Real Examples)</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Do you know WordPress runs on just 8 fundamental code concepts?</p>



<p class="wp-block-paragraph">Most beginners copy-paste WordPress code from Stack Overflow without understanding what it actually does. I did the same thing for months until I realized I was just guessing. One wrong character in functions.php, and boom — White Screen of Death.</p>



<p class="wp-block-paragraph">Here&#8217;s the truth: you don&#8217;t need to memorize thousands of functions. <strong>I&#8217;ll show you 8 WordPress code concepts that power every WordPress site</strong> — from simple blogs to complex e-commerce stores.</p>



<p class="wp-block-paragraph">Understanding these core WordPress code concepts will transform how you build and customize WordPress sites. No more guessing. No more breaking things accidentally.</p>



<p class="wp-block-paragraph">By the end of this guide, you&#8217;ll understand:</p>



<ul class="wp-block-list">
<li>What each WordPress code concept is (in plain English)</li>



<li>How to use it properly (with working code examples)</li>



<li>Real-world problems it solves (not just theory)</li>
</ul>



<p class="wp-block-paragraph">Let&#8217;s start with the most important warning before diving into these WordPress code concepts.</p>



<h2 class="wp-block-heading">⚠️ CRITICAL WARNING: Before You Edit functions.php</h2>



<p class="wp-block-paragraph">Editing functions.php can break your entire site if you make one syntax error. I learned this the hard way when my client&#8217;s site went down.</p>



<p class="wp-block-paragraph"><strong>Always follow these rules:</strong></p>



<p class="wp-block-paragraph">✅ <strong>Backup your site first</strong> — Don&#8217;t waste money on backup plugins. <a href="https://www.youtube.com/watch?v=vE93UqZNWh0">Watch how I built my own backup system in 15 minutes using AI</a> (saves you $840/year)</p>



<p class="wp-block-paragraph">✅ <strong>Use a child theme</strong> — Never edit parent theme files directly</p>



<p class="wp-block-paragraph">✅ <strong>Test on staging site</strong> — If you have one available</p>



<p class="wp-block-paragraph">✅ <strong>Or use Code Snippets plugin</strong> — Much safer than editing functions.php</p>



<p class="wp-block-paragraph">One missing semicolon = White Screen of Death. You&#8217;ve been warned.</p>



<p class="wp-block-paragraph">Now let&#8217;s dive into the essential WordPress code concepts that every developer should know.</p>



<h2 class="wp-block-heading">Why Understanding WordPress Code Concepts Matters</h2>



<p class="wp-block-paragraph">Before we jump into specific WordPress code concepts, let&#8217;s talk about why this matters.</p>



<p class="wp-block-paragraph">WordPress powers 43% of all websites. But here&#8217;s what most people don&#8217;t know: every WordPress plugin, theme, and custom functionality uses the same 8 core WordPress code concepts.</p>



<p class="wp-block-paragraph">When you understand these WordPress programming fundamentals, you can:</p>



<ul class="wp-block-list">
<li>Debug WordPress errors faster</li>



<li>Build custom features without expensive plugins</li>



<li>Understand how WordPress plugins actually work</li>



<li>Optimize your site&#8217;s performance</li>



<li>Fix broken functionality yourself</li>
</ul>



<p class="wp-block-paragraph">These WordPress code concepts are the building blocks. Master them, and you&#8217;ll never feel helpless staring at WordPress code again.</p>



<h2 class="wp-block-heading">Concept 1: add_action() Hook — The Foundation of WordPress</h2>



<h3 class="wp-block-heading">What is add_action()?</h3>



<p class="wp-block-paragraph">The add_action() hook is one of the most important WordPress code concepts. It lets you add custom code at specific points in WordPress without modifying core files.</p>



<p class="wp-block-paragraph">Think of WordPress actions as &#8220;when X happens, do Y.&#8221; It&#8217;s event-driven programming made simple.</p>



<h3 class="wp-block-heading">How to use add_action():</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: php; title: ; notranslate">
add_action(&#039;wp_footer&#039;, &#039;add_footer_text&#039;);
function add_footer_text() {
    echo &#039;&lt;div style=&quot;text-align: center; padding: 20px; background: #f0f0f0;&quot;&gt;
        Built with ❤️ by Dimu | &lt;a href=&quot;https://ceeveeglobal.com&quot;&gt;Visit My Site&lt;/a&gt;
    &lt;/div&gt;&#039;;
}
</pre></div>


<p class="wp-block-paragraph"><strong>What this does:</strong> Adds a custom footer message to every page on your site.</p>



<h3 class="wp-block-heading">How to Test If It&#8217;s Working:</h3>



<ol class="wp-block-list">
<li>Add the code above to Code Snippets plugin (or functions.php)</li>



<li>Save and activate</li>



<li>Visit any page on your site</li>



<li>Scroll to the bottom</li>
</ol>



<p class="wp-block-paragraph"><strong>You should see:</strong> A gray box with &#8220;Built with ❤️ by Dimu&#8221; at the footer.</p>



<p class="wp-block-paragraph"><strong>If you don&#8217;t see it:</strong> Check for syntax errors. One missing semicolon or bracket breaks everything.</p>



<h3 class="wp-block-heading">Common Beginner Mistake:</h3>



<p class="wp-block-paragraph">Many tutorials use <code>console.log()</code> in examples like this:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
echo &#039;&amp;lt;script&gt;console.log(&quot;Hello&quot;);&amp;lt;/script&gt;&#039;;
</pre></div>


<p class="wp-block-paragraph">This <strong>does work</strong> — but you can&#8217;t see it on your page. You have to open browser Developer Console (F12) to see the message. That&#8217;s why beginners think their code failed.</p>



<p class="wp-block-paragraph"><strong>Always test with visible output first</strong> (text, colors, borders) so you know your code is running.</p>



<h3 class="wp-block-heading">Real-world problems add_action() solves:</h3>



<ul class="wp-block-list">
<li>Add Google Analytics or tracking codes to footer automatically</li>



<li>Insert custom content after every blog post</li>



<li>Load scripts at the right time (not too early, not too late)</li>



<li>Modify WordPress behavior without touching core files</li>



<li>Hook into WordPress events like user login, post publishing</li>
</ul>



<h3 class="wp-block-heading">When to use add_action():</h3>



<p class="wp-block-paragraph">When you need to <strong>add</strong> functionality at specific WordPress events. Like adding a banner after every blog post or loading custom CSS on specific pages.</p>



<p class="wp-block-paragraph">I use add_action() constantly. My <a href="https://ceeveeglobal.com/tools/">AI tools page</a> uses add_action to load custom JavaScript only on that page — not sitewide. Result? Faster load times and better performance.</p>



<h3 class="wp-block-heading">More Practical Examples:</h3>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="700" height="450" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/add_action-Hook-—-The-Foundation-of-WordPress-What-is-add_action.webp" alt="add_action() Hook — The Foundation of WordPress" class="wp-image-15905" srcset="https://s3.ceeveeglobal.com/ceeveeglobalimages/add_action-Hook-—-The-Foundation-of-WordPress-What-is-add_action.webp 700w, https://s3.ceeveeglobal.com/ceeveeglobalimages/add_action-Hook-—-The-Foundation-of-WordPress-What-is-add_action-600x386.webp 600w" sizes="auto, (max-width: 700px) 100vw, 700px" /></figure>
</div>


<p class="wp-block-paragraph"><strong>Example 2: Add Custom CSS to All Pages</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: php; title: ; notranslate">
add_action(&#039;wp_head&#039;, &#039;my_custom_styles&#039;);
function my_custom_styles() {
    echo &#039;&lt;style&gt;
        body { border-top: 5px solid #ff6b6b; }
    &lt;/style&gt;&#039;;
}
</pre></div>


<p class="wp-block-paragraph"><strong>What you&#8217;ll see:</strong> Red border at the top of every page.</p>



<p class="wp-block-paragraph"><strong>Example 3: Admin Dashboard Notice</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: php; title: ; notranslate">
add_action(&#039;admin_notices&#039;, &#039;my_admin_notice&#039;);
function my_admin_notice() {
    echo &#039;&lt;div class=&quot;notice notice-success&quot;&gt;
        &lt;p&gt;Your custom code is working! 🎉&lt;/p&gt;
    &lt;/div&gt;&#039;;
}
</pre></div>


<p class="wp-block-paragraph"><strong>Where to see it:</strong> WordPress Admin Dashboard (top of the page when you log in).</p>



<p class="wp-block-paragraph"><strong>Pro tip:</strong> The most common WordPress actions are <code>wp_head</code>, <code>wp_footer</code>, <code>init</code>, <code>wp_enqueue_scripts</code>, and <code>admin_init</code>. Start with these.</p>



<h2 class="wp-block-heading">Concept 2: add_filter() Hook — Transform WordPress Content</h2>



<h3 class="wp-block-heading">What is add_filter()?</h3>



<p class="wp-block-paragraph">Filters are another crucial WordPress code concept. While actions &#8220;do something,&#8221; filters <strong>modify existing data</strong> before WordPress displays it.</p>



<p class="wp-block-paragraph">This is how WordPress lets you transform content, change titles, modify excerpts, and customize output without editing template files.</p>



<h3 class="wp-block-heading">How to use add_filter():</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
add_filter(&#039;the_content&#039;, &#039;add_reading_time&#039;);
function add_reading_time($content) {
    // Only add to single posts
    if (is_single()) {
        $word_count = str_word_count(strip_tags($content));
        $reading_time = ceil($word_count / 200);
        $message = &quot;&amp;lt;p style=&#039;background: #e3f2fd; padding: 15px; border-left: 4px solid #2196f3;&#039;&gt;&amp;lt;strong&gt;⏱️ Reading time: &quot; . $reading_time . &quot; minutes&amp;lt;/strong&gt;&amp;lt;/p&gt;&quot;;
        return $message . $content;
    }
    return $content;
}
</pre></div>


<p class="wp-block-paragraph">This automatically adds reading time to every post. No manual work needed.</p>



<h3 class="wp-block-heading">How to Test If It&#8217;s Working:</h3>



<ol class="wp-block-list">
<li>Add the code to Code Snippets plugin</li>



<li>Save and activate</li>



<li>Visit any blog post (not page)</li>



<li>Look at the top of the content</li>
</ol>



<p class="wp-block-paragraph"><strong>You should see:</strong> A blue box with reading time estimate.</p>



<h3 class="wp-block-heading">Real-world problems add_filter() solves:</h3>



<ul class="wp-block-list">
<li>Add custom content before/after posts automatically</li>



<li>Modify excerpt length from default 55 words</li>



<li>Change post titles programmatically</li>



<li>Filter search results by custom criteria</li>



<li>Add disclaimers to specific post types</li>



<li>Customize WordPress login messages</li>
</ul>



<h3 class="wp-block-heading">When to use add_filter():</h3>



<p class="wp-block-paragraph">When you need to <strong>transform</strong> existing content. I use filters to add custom CTAs at the end of my <a href="https://ceeveeglobal.com/category/wordpress-errors/">WordPress error fix posts</a>. One filter updates 40+ posts automatically.</p>



<h3 class="wp-block-heading">More Practical Examples:</h3>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="700" height="450" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/add_filter-Hook-—-Transform-WordPress-Content.webp" alt="add_filter() Hook — Transform WordPress Content" class="wp-image-15907" style="width:652px;height:auto" srcset="https://s3.ceeveeglobal.com/ceeveeglobalimages/add_filter-Hook-—-Transform-WordPress-Content.webp 700w, https://s3.ceeveeglobal.com/ceeveeglobalimages/add_filter-Hook-—-Transform-WordPress-Content-600x386.webp 600w" sizes="auto, (max-width: 700px) 100vw, 700px" /></figure>
</div>


<p class="wp-block-paragraph"><strong>Example: Change Excerpt Length</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
add_filter(&#039;excerpt_length&#039;, &#039;custom_excerpt_length&#039;);
function custom_excerpt_length($length) {
    return 30; // Change from default 55 words to 30
}
</pre></div>


<p class="wp-block-paragraph"><strong>Example: Add Text After Every Post</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
add_filter(&#039;the_content&#039;, &#039;add_cta_after_post&#039;);
function add_cta_after_post($content) {
    if (is_single()) {
        $cta = &#039;&amp;lt;div style=&quot;background: #fff3cd; padding: 20px; margin-top: 30px; border-radius: 5px;&quot;&gt;
            &amp;lt;h3&gt;Need Help with WordPress Errors?&amp;lt;/h3&gt;
            &amp;lt;p&gt;Try my &amp;lt;a href=&quot;https://ceeveeglobal.com/wp-error-expert/&quot;&gt;WP Error Expert Tool&amp;lt;/a&gt; - AI-powered error diagnosis!&amp;lt;/p&gt;
        &amp;lt;/div&gt;&#039;;
        return $content . $cta;
    }
    return $content;
}
</pre></div>


<p class="wp-block-paragraph"><strong>Key difference:</strong> Actions add new functionality. Filters modify existing data. Both are essential WordPress code concepts you&#8217;ll use daily.</p>



<h2 class="wp-block-heading">Concept 3: WP_Query — Custom WordPress Loops</h2>



<h3 class="wp-block-heading">What is WP_Query?</h3>



<p class="wp-block-paragraph">WP_Query is WordPress&#8217;s powerful way to fetch posts from the database. It&#8217;s one of the most flexible WordPress code concepts for displaying content.</p>



<p class="wp-block-paragraph">The default WordPress loop shows all posts. WP_Query lets you get specific posts based on any criteria you want — categories, tags, custom fields, dates, and more.</p>



<h3 class="wp-block-heading">How to use WP_Query:</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
function display_custom_posts() {
    $args = array(
        &#039;posts_per_page&#039; =&gt; 5,
        &#039;category_name&#039; =&gt; &#039;wordpress-errors&#039;, // Change to your category slug
        &#039;orderby&#039; =&gt; &#039;date&#039;,
        &#039;order&#039; =&gt; &#039;DESC&#039;
    );
    $query = new WP_Query($args);

    if ($query-&gt;have_posts()) {
        echo &#039;&amp;lt;div class=&quot;custom-posts&quot;&gt;&#039;;
        while ($query-&gt;have_posts()) {
            $query-&gt;the_post();
            echo &#039;&amp;lt;div style=&quot;margin-bottom: 20px; padding: 15px; border: 1px solid #ddd; border-radius: 5px;&quot;&gt;&#039;;
            echo &#039;&amp;lt;h3&gt;&amp;lt;a href=&quot;&#039; . get_permalink() . &#039;&quot;&gt;&#039; . get_the_title() . &#039;&amp;lt;/a&gt;&amp;lt;/h3&gt;&#039;;
            echo &#039;&amp;lt;p&gt;&#039; . get_the_excerpt() . &#039;&amp;lt;/p&gt;&#039;;
            echo &#039;&amp;lt;a href=&quot;&#039; . get_permalink() . &#039;&quot; style=&quot;color: #0073aa;&quot;&gt;Read More →&amp;lt;/a&gt;&#039;;
            echo &#039;&amp;lt;/div&gt;&#039;;
        }
        echo &#039;&amp;lt;/div&gt;&#039;;
    } else {
        echo &#039;&amp;lt;p&gt;No posts found.&amp;lt;/p&gt;&#039;;
    }
    wp_reset_postdata();
}

// Add shortcode to use anywhere
add_shortcode(&#039;custom_posts&#039;, &#039;display_custom_posts&#039;);
</pre></div>


<h3 class="wp-block-heading">How to Test If It&#8217;s Working:</h3>



<p class="wp-block-paragraph"><strong>Using Code Snippets Plugin (Recommended):</strong></p>



<ol class="wp-block-list">
<li>Add the code to Code Snippets plugin</li>



<li>Change <code>'category_name' => 'wordpress-errors'</code> to match your actual category slug</li>



<li>Edit any post or page</li>



<li>Add this shortcode: <code>[custom_posts]</code></li>



<li>Publish and view the page</li>
</ol>



<p class="wp-block-paragraph"><strong>You should see:</strong> A styled list of 5 posts from that category with borders and read more links.</p>



<p class="wp-block-paragraph"><strong>If you see &#8220;No posts found&#8221;:</strong> Your category slug might be wrong. Go to Posts → Categories and check the actual slug.</p>



<h3 class="wp-block-heading">Real-world problems WP_Query solves:</h3>



<ul class="wp-block-list">
<li>Create custom post grids (like my tools page layout)</li>



<li>Display posts from specific categories only</li>



<li>Build &#8220;Related Posts&#8221; sections based on tags or categories</li>



<li>Show posts by custom field values</li>



<li>Create portfolio sections with filtering options</li>



<li>Display posts from multiple post types</li>
</ul>



<h3 class="wp-block-heading">When to use WP_Query:</h3>



<p class="wp-block-paragraph">When the default WordPress loop isn&#8217;t flexible enough. I use WP_Query on the homepage to show only featured <a href="https://ceeveeglobal.com/">WordPress error fixes</a> — not all 45+ posts.</p>



<h3 class="wp-block-heading">Useful WP_Query Parameters:</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
$args = array(
    &#039;posts_per_page&#039; =&gt; 10,        // Number of posts
    &#039;category_name&#039; =&gt; &#039;tutorials&#039;, // Category slug
    &#039;tag&#039; =&gt; &#039;beginner&#039;,           // Tag slug
    &#039;orderby&#039; =&gt; &#039;date&#039;,           // Sort by date
    &#039;order&#039; =&gt; &#039;DESC&#039;,             // Newest first
    &#039;post_type&#039; =&gt; &#039;post&#039;,         // Post type
    &#039;meta_key&#039; =&gt; &#039;featured&#039;,      // Custom field key
    &#039;meta_value&#039; =&gt; &#039;yes&#039;          // Custom field value
);
</pre></div>


<p class="wp-block-paragraph"><strong>Important:</strong> Always use <code>wp_reset_postdata()</code> after custom queries to avoid conflicts with the main loop. This resets global post data.</p>



<h2 class="wp-block-heading">Concept 4: WordPress Shortcodes — Reusable Content Blocks</h2>



<h3 class="wp-block-heading">What are WordPress shortcodes?</h3>



<p class="wp-block-paragraph">Shortcodes are reusable code snippets you can insert anywhere using simple brackets. They&#8217;re essential WordPress code concepts for creating custom, reusable elements.</p>



<p class="wp-block-paragraph">Think of shortcodes like creating your own Gutenberg blocks, but lighter and more flexible.</p>



<h3 class="wp-block-heading">How to use WordPress shortcodes:</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
add_shortcode(&#039;cta_button&#039;, &#039;my_button_shortcode&#039;);
function my_button_shortcode($atts) {
    // Set default values
    $atts = shortcode_atts(array(
        &#039;text&#039; =&gt; &#039;Click Here&#039;,
        &#039;url&#039; =&gt; &#039;#&#039;,
        &#039;color&#039; =&gt; &#039;blue&#039;
    ), $atts);
    
    // Return the button HTML
    return &#039;&amp;lt;a href=&quot;&#039; . esc_url($atts&#x5B;&#039;url&#039;]) . &#039;&quot; style=&quot;display: inline-block; padding: 12px 24px; background: &#039; . esc_attr($atts&#x5B;&#039;color&#039;]) . &#039;; color: white; text-decoration: none; border-radius: 5px; font-weight: bold;&quot;&gt;&#039; . esc_html($atts&#x5B;&#039;text&#039;]) . &#039;&amp;lt;/a&gt;&#039;;
}

</pre></div>


<h3 class="wp-block-heading">How to Test If It&#8217;s Working: </h3>



<p class="wp-block-paragraph">1. Add the code above to the Code Snippets plugin </p>



<p class="wp-block-paragraph">2. Save and activate </p>



<p class="wp-block-paragraph">3. Edit any post or page </p>



<p class="wp-block-paragraph">4. Add this shortcode to the content:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
&#x5B;cta_button text=&quot;Try Free Tool&quot; url=&quot;https://ceeveeglobal.com/tools/&quot; color=&quot;green&quot;]
</pre></div>


<p class="wp-block-paragraph"><strong>You should see:</strong> A green button with &#8220;Try Free Tool&#8221; text.</p>



<h3 class="wp-block-heading">More Shortcode Examples:</h3>



<p class="wp-block-paragraph"><strong>Example: Simple Contact Box</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
add_shortcode(&#039;contact_box&#039;, &#039;contact_box_shortcode&#039;);
function contact_box_shortcode() {
    return &#039;&amp;lt;div style=&quot;background: #f8f9fa; padding: 20px; border-left: 4px solid #007bff; margin: 20px 0;&quot;&gt;
        &amp;lt;h3&gt;Need WordPress Help?&amp;lt;/h3&gt;
        &amp;lt;p&gt;Contact me: &amp;lt;a href=&quot;https://ceeveeglobal.com/contact/&quot;&gt;Click Here&amp;lt;/a&gt;&amp;lt;/p&gt;
    &amp;lt;/div&gt;&#039;;
}
</pre></div>


<p class="wp-block-paragraph"><strong>Usage:</strong> <code>[contact_box]</code></p>



<h3 class="wp-block-heading">Real-world problems shortcodes solve:</h3>



<ul class="wp-block-list">
<li>Add custom CTAs without page builders</li>



<li>Insert contact forms anywhere in content</li>



<li>Display pricing tables consistently across pages</li>



<li>Embed custom widgets in posts and pages</li>



<li>Create reusable content blocks that update globally</li>
</ul>



<h3 class="wp-block-heading">When to use shortcodes:</h3>



<p class="wp-block-paragraph">When you need the same element across multiple posts or pages. I use shortcodes for tool promotion boxes in my blog posts. Update one shortcode function, and all posts update automatically.</p>



<p class="wp-block-paragraph">Shortcodes are perfect for <a href="https://ceeveeglobal.com/wordpress-performance-analyzer-tool/">WordPress performance optimization</a> too — they&#8217;re lighter than page builder elements and load faster.</p>



<h2 class="wp-block-heading">Concept 5: wp_enqueue_script() &amp; wp_enqueue_style() — Load Assets Properly</h2>



<h3 class="wp-block-heading">What is wp_enqueue?</h3>



<p class="wp-block-paragraph">These functions represent critical WordPress code concepts for properly loading CSS and JavaScript. Never add scripts directly to header.php or footer.php — always use wp_enqueue.</p>



<h3 class="wp-block-heading">How to use wp_enqueue properly:</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
function load_my_assets() {
    // Load CSS
    wp_enqueue_style(&#039;my-custom-style&#039;, get_stylesheet_directory_uri() . &#039;/custom.css&#039;, array(), &#039;1.0.0&#039;);
    
    // Load JavaScript with jQuery dependency
    wp_enqueue_script(&#039;my-custom-script&#039;, get_stylesheet_directory_uri() . &#039;/custom.js&#039;, array(&#039;jquery&#039;), &#039;1.0.0&#039;, true);
}
add_action(&#039;wp_enqueue_scripts&#039;, &#039;load_my_assets&#039;);
</pre></div>


<p class="wp-block-paragraph"><strong>Explanation:</strong></p>



<ul class="wp-block-list">
<li><code>get_stylesheet_directory_uri()</code> gets your theme&#8217;s URL</li>



<li><code>array('jquery')</code> means &#8220;load jQuery first, then my script&#8221;</li>



<li><code>'1.0.0'</code> is version number (for cache busting)</li>



<li><code>true</code> at the end means &#8220;load in footer for better performance&#8221;</li>
</ul>



<h3 class="wp-block-heading">How to Test If It&#8217;s Working:</h3>



<ol class="wp-block-list">
<li>Create a file called <code>custom.js</code> in your theme folder</li>



<li>Add this code to it:</li>
</ol>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
jQuery(document).ready(function($) {
    $(&#039;body&#039;).prepend(&#039;&amp;lt;div style=&quot;background: yellow; padding: 10px; text-align: center;&quot;&gt;Custom JS Loaded!&amp;lt;/div&gt;&#039;);
});
</pre></div>


<ol start="3" class="wp-block-list">
<li>Add the wp_enqueue code above to your functions.php or Code Snippets</li>



<li>Refresh your site</li>
</ol>



<p class="wp-block-paragraph"><strong>You should see:</strong> Yellow banner at the top saying &#8220;Custom JS Loaded!&#8221;</p>



<h3 class="wp-block-heading">Real-world problems wp_enqueue solves:</h3>



<ul class="wp-block-list">
<li>Prevents jQuery conflicts (the #1 WordPress JavaScript error)</li>



<li>Loads scripts in correct order with proper dependencies</li>



<li>Avoids loading same script twice (duplicate loading)</li>



<li>Improves page speed by controlling what loads where</li>



<li>Makes debugging easier with proper script handles</li>



<li>Enables conditional loading (only load on specific pages)</li>
</ul>



<h3 class="wp-block-heading">Conditional Loading Example:</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
function load_my_assets() {
    // Only load on homepage
    if (is_front_page()) {
        wp_enqueue_script(&#039;homepage-slider&#039;, get_stylesheet_directory_uri() . &#039;/slider.js&#039;, array(&#039;jquery&#039;), &#039;1.0.0&#039;, true);
    }
    
    // Only load on single posts
    if (is_single()) {
        wp_enqueue_style(&#039;single-post-style&#039;, get_stylesheet_directory_uri() . &#039;/single-post.css&#039;);
    }
}
add_action(&#039;wp_enqueue_scripts&#039;, &#039;load_my_assets&#039;);
</pre></div>


<h3 class="wp-block-heading">When to use wp_enqueue:</h3>



<p class="wp-block-paragraph"><strong>Always.</strong> This is not optional for WordPress development. If you&#8217;re adding CSS or JavaScript to WordPress, use wp_enqueue_style() and wp_enqueue_script(). Period.</p>



<p class="wp-block-paragraph">I use wp_enqueue to load my custom scripts only on pages that need them. My <a href="https://ceeveeglobal.com/tools/wordpress-plugin-finder/">WordPress Plugin Finder tool</a> loads specific JavaScript — not on every page. This keeps the site fast.</p>



<p class="wp-block-paragraph"><strong>Common mistake:</strong> Loading scripts in <code>wp_head</code> action. Use <code>wp_enqueue_scripts</code> action instead for proper loading. Need help debugging JavaScript errors? Check my <a href="https://ceeveeglobal.com/how-ai-can-detect-fix-common-wordpress-errors-using-wordpress-error-log/">WordPress Error Log guide</a>.</p>



<h2 class="wp-block-heading">Concept 6: Custom Post Types — Organize Content Better</h2>



<h3 class="wp-block-heading">What are custom post types?</h3>



<p class="wp-block-paragraph">Custom post types are powerful WordPress code concepts that let you create your own content types beyond default posts and pages.</p>



<p class="wp-block-paragraph">Think of custom post types as adding new categories of content with their own admin menus, templates, and archive pages.</p>



<h3 class="wp-block-heading">How to create custom post types:</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
function create_tools_post_type() {
    $args = array(
        &#039;labels&#039; =&gt; array(
            &#039;name&#039; =&gt; &#039;Tools&#039;,
            &#039;singular_name&#039; =&gt; &#039;Tool&#039;,
            &#039;add_new&#039; =&gt; &#039;Add New Tool&#039;,
            &#039;add_new_item&#039; =&gt; &#039;Add New Tool&#039;,
            &#039;edit_item&#039; =&gt; &#039;Edit Tool&#039;
        ),
        &#039;public&#039; =&gt; true,
        &#039;has_archive&#039; =&gt; true,
        &#039;supports&#039; =&gt; array(&#039;title&#039;, &#039;editor&#039;, &#039;thumbnail&#039;, &#039;excerpt&#039;),
        &#039;menu_icon&#039; =&gt; &#039;dashicons-admin-tools&#039;,
        &#039;rewrite&#039; =&gt; array(&#039;slug&#039; =&gt; &#039;tools&#039;),
        &#039;show_in_rest&#039; =&gt; true  // Enables Gutenberg editor
    );
    
    register_post_type(&#039;tools&#039;, $args);
}
add_action(&#039;init&#039;, &#039;create_tools_post_type&#039;);
</pre></div>


<h3 class="wp-block-heading">How to Test If It&#8217;s Working:</h3>



<p class="wp-block-paragraph">1. Add the code above to Code Snippets plugin </p>



<p class="wp-block-paragraph">2. Save and activate </p>



<p class="wp-block-paragraph">3. Go to WordPress Admin Dashboard </p>



<p class="wp-block-paragraph">4. <strong>Go to Settings → Permalinks</strong> (IMPORTANT!) </p>



<p class="wp-block-paragraph">5. Click &#8220;Save Changes&#8221; (this flushes rewrite rules) </p>



<p class="wp-block-paragraph">6. Look in the left sidebar</p>



<p class="wp-block-paragraph">You should see: A new &#8220;Tools&#8221; menu item with &#8220;Add New Tool&#8221; option.</p>



<p class="wp-block-paragraph">If you get 404 errors:You forgot step 4-5. Always flush permalinks after registering custom post types. Read my <a href="https://ceeveeglobal.com/wordpress-404-error-causes-effects-and-simple-solutions/">WordPress 404 Error guide</a> for more solutions.</p>



<h3 class="wp-block-heading">Real-world problems custom post types solve:</h3>



<ul class="wp-block-list">
<li><strong>Build portfolio sections</strong> separate from blog posts</li>



<li><strong>Create testimonial systems</strong> with custom fields</li>



<li><strong>Add product showcases</strong> for services or digital products</li>



<li><strong>Organize different content types</strong> (like my AI tools collection)</li>



<li><strong>Keep blog posts and other content</strong> completely separate</li>



<li><strong>Create custom taxonomies</strong> for better organization</li>
</ul>



<h3 class="wp-block-heading">When to use custom post types:</h3>



<p class="wp-block-paragraph">When posts and pages aren&#8217;t enough for your content structure. My <a href="https://ceeveeglobal.com/tools/">AI tools collection</a> uses custom post types. Each tool has its own entry, separate from blog posts.</p>



<p class="wp-block-paragraph"><strong>Pro tip:</strong> After registering custom post types, always go to Settings → Permalinks and click &#8220;Save Changes&#8221; to flush rewrite rules. Otherwise, you&#8217;ll get 404 errors on your custom post type pages.</p>



<h2 class="wp-block-heading">Concept 7: WordPress REST API — Access Data Anywhere</h2>



<h3 class="wp-block-heading">What is WordPress REST API?</h3>



<p class="wp-block-paragraph">The WordPress REST API is one of the most modern WordPress code concepts. It lets you access your WordPress content via URLs that return JSON data.</p>



<p class="wp-block-paragraph">Think of it as opening a window into your WordPress database from anywhere — mobile apps, external websites, JavaScript applications.</p>



<h3 class="wp-block-heading">How to use WordPress REST API:</h3>



<p class="wp-block-paragraph">You don&#8217;t need to write code to use REST API. Just access these URLs in your browser:</p>



<p class="wp-block-paragraph"><strong>Get all posts:</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
https://yoursite.com/wp-json/wp/v2/posts
</pre></div>


<p class="wp-block-paragraph"><strong>Get 5 posts from specific category:</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
https://yoursite.com/wp-json/wp/v2/posts?per_page=5&amp;amp;categories=12
</pre></div>


<p class="wp-block-paragraph"><strong>Search posts:</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
https://yoursite.com/wp-json/wp/v2/posts?search=wordpress
</pre></div>


<h3 class="wp-block-heading">How to Test If It&#8217;s Working:</h3>



<ol class="wp-block-list">
<li>Replace <code>yoursite.com</code> with your actual domain</li>



<li>Try this URL in your browser:</li>
</ol>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
https://ceeveeglobal.com/wp-json/wp/v2/posts?per_page=3
</pre></div>


<p class="wp-block-paragraph"><strong>You should see:</strong> JSON data with post information (title, content, excerpt, etc.)</p>



<h3 class="wp-block-heading">Common REST API Parameters:</h3>



<ul class="wp-block-list">
<li><code>per_page</code>: Number of posts to return (default: 10)</li>



<li><code>categories</code>: Filter by category ID</li>



<li><code>tags</code>: Filter by tag ID</li>



<li><code>search</code>: Search posts by keyword</li>



<li><code>orderby</code>: Sort by date, title, modified, etc.</li>



<li><code>order</code>: ASC (ascending) or DESC (descending)</li>



<li><code>page</code>: Pagination for large datasets</li>
</ul>



<h3 class="wp-block-heading">Using REST API in JavaScript:</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
fetch(&#039;https://yoursite.com/wp-json/wp/v2/posts?per_page=5&#039;)
    .then(response =&gt; response.json())
    .then(posts =&gt; {
        posts.forEach(post =&gt; {
            console.log(post.title.rendered);
        });
    });
</pre></div>


<h3 class="wp-block-heading">Real-world problems REST API solves:</h3>



<ul class="wp-block-list">
<li>Build headless WordPress sites (WordPress backend, custom React/Vue frontend)</li>



<li>Create mobile apps that pull WordPress content</li>



<li>Fetch data for JavaScript applications without page reload</li>



<li>Integrate WordPress with external services and platforms</li>



<li>Display WordPress posts on non-WordPress pages</li>



<li>Build custom dashboards that pull WordPress data</li>
</ul>



<h3 class="wp-block-heading">When to use REST API:</h3>



<p class="wp-block-paragraph">When you need WordPress data outside WordPress itself. I use REST API to power some of my FastAPI tools that interact with WordPress data programmatically.</p>



<p class="wp-block-paragraph"><strong>Security note:</strong> WordPress REST API is public by default. For sensitive data, use authentication. Most sites disable <code>/wp-json/wp/v2/users</code> endpoint for security. Learn more in my <a href="https://ceeveeglobal.com/the-ultimate-wordpress-security-audit-before-updates-never-break-your-site-again/">WordPress Security Audit guide</a>.</p>



<h2 class="wp-block-heading">Concept 8: WordPress Transients — Built-in Caching</h2>



<h3 class="wp-block-heading">What are WordPress transients?</h3>



<p class="wp-block-paragraph">Transients are WordPress&#8217;s built-in caching system — one of the most underrated WordPress code concepts for performance optimization.</p>



<p class="wp-block-paragraph">They provide temporary data storage with automatic expiration. No Redis, Memcached, or external caching plugins required.</p>



<h3 class="wp-block-heading">How to use WordPress transients:</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
function get_popular_posts() {
    // Try to get cached data
    $popular_posts = get_transient(&#039;my_popular_posts&#039;);
    
    // If no cache exists or expired
    if ($popular_posts === false) {
        // Run expensive query
        $args = array(
            &#039;posts_per_page&#039; =&gt; 10,
            &#039;meta_key&#039; =&gt; &#039;post_views&#039;,
            &#039;orderby&#039; =&gt; &#039;meta_value_num&#039;,
            &#039;order&#039; =&gt; &#039;DESC&#039;
        );
        $popular_posts = new WP_Query($args);
        
        // Store in cache for 1 hour (3600 seconds)
        set_transient(&#039;my_popular_posts&#039;, $popular_posts, 3600);
    }
    
    return $popular_posts;
}
</pre></div>


<h3 class="wp-block-heading">How to Test If It&#8217;s Working:</h3>



<p class="wp-block-paragraph">Add this code to test transient speed:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
function test_transient_speed() {
    // First load (no cache)
    delete_transient(&#039;test_data&#039;);
    $start = microtime(true);
    $data = get_transient(&#039;test_data&#039;);
    if ($data === false) {
        sleep(2); // Simulate slow query
        $data = &quot;Slow data loaded&quot;;
        set_transient(&#039;test_data&#039;, $data, 3600);
    }
    $first_load = microtime(true) - $start;
    
    // Second load (from cache)
    $start = microtime(true);
    $data = get_transient(&#039;test_data&#039;);
    $second_load = microtime(true) - $start;
    
    echo &quot;First load: &quot; . round($first_load, 4) . &quot; seconds&amp;lt;br&gt;&quot;;
    echo &quot;Second load: &quot; . round($second_load, 4) . &quot; seconds&amp;lt;br&gt;&quot;;
    echo &quot;Speed improvement: &quot; . round($first_load / $second_load) . &quot;x faster&quot;;
}
add_action(&#039;wp_footer&#039;, &#039;test_transient_speed&#039;);
</pre></div>


<p class="wp-block-paragraph"><strong>You should see:</strong> First load takes ~2 seconds, second load is almost instant (0.0001 seconds).</p>



<h3 class="wp-block-heading">Real-world problems transients solve:</h3>



<ul class="wp-block-list">
<li>Speed up slow database queries dramatically</li>



<li>Cache external API responses (like OpenAI, Google APIs)</li>



<li>Reduce server load on high-traffic WordPress sites</li>



<li>Improve page load times without complex caching plugins</li>



<li>Store temporary data without creating custom database tables</li>



<li>Cache RSS feeds, social media data, weather info</li>
</ul>



<h3 class="wp-block-heading">When to use transients:</h3>



<p class="wp-block-paragraph">When you have expensive operations that don&#8217;t change frequently. My <a href="https://ceeveeglobal.com/wp-error-expert/">WP Error Expert tool</a> caches AI responses using transients. Same error searched twice? Instant results from cache instead of calling the API again.</p>



<h3 class="wp-block-heading">Practical Caching Example:</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
function get_latest_tools() {
    $tools = get_transient(&#039;latest_tools_cache&#039;);
    
    if ($tools === false) {
        $args = array(
            &#039;post_type&#039; =&gt; &#039;tools&#039;,
            &#039;posts_per_page&#039; =&gt; 10,
            &#039;orderby&#039; =&gt; &#039;date&#039;,
            &#039;order&#039; =&gt; &#039;DESC&#039;
        );
        $query = new WP_Query($args);
        $tools = $query-&gt;posts;
        
        // Cache for 6 hours (21600 seconds)
        set_transient(&#039;latest_tools_cache&#039;, $tools, 21600);
    }
    
    return $tools;
}
</pre></div>


<p class="wp-block-paragraph">I once reduced a page load time from 8 seconds to 1.2 seconds just by caching one slow WP_Query with transients. That&#8217;s the power of proper caching.</p>



<p class="wp-block-paragraph"><strong>Important:</strong> Transients are stored in wp_options table by default. For high-traffic sites, use persistent object caching (Redis) for better performance. Check my <a href="https://ceeveeglobal.com/wordpress-performance-analyzer-tool/">WordPress Performance Analyzer</a> to see if you need it.</p>



<h2 class="wp-block-heading">Mastering WordPress Code Concepts: Quick Reference Guide</h2>



<p class="wp-block-paragraph">Here&#8217;s a cheat sheet for choosing the right WordPress code concept for your needs:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="700" height="450" src="https://s3.ceeveeglobal.com/ceeveeglobalimages/Quick-Reference-Guide.webp" alt="Quick Reference Guide" class="wp-image-15909" srcset="https://s3.ceeveeglobal.com/ceeveeglobalimages/Quick-Reference-Guide.webp 700w, https://s3.ceeveeglobal.com/ceeveeglobalimages/Quick-Reference-Guide-600x386.webp 600w" sizes="auto, (max-width: 700px) 100vw, 700px" /></figure>
</div>


<p class="wp-block-paragraph">Save this table. You&#8217;ll reference it constantly when building WordPress sites.</p>



<h2 class="wp-block-heading">How to Practice These WordPress Code Concepts</h2>



<p class="wp-block-paragraph">Understanding WordPress code concepts is one thing. Actually using them is different.</p>



<p class="wp-block-paragraph">Here&#8217;s my recommended learning path:</p>



<p class="wp-block-paragraph"><strong>Week 1: Master WordPress Hooks</strong></p>



<ul class="wp-block-list">
<li>Create one add_action() to add visible content to footer</li>



<li>Create one add_filter() to modify post content</li>



<li>Break something, then fix it (seriously, this teaches you)</li>
</ul>



<p class="wp-block-paragraph"><strong>Week 2: Work with Data</strong></p>



<ul class="wp-block-list">
<li>Build a custom post grid using WP_Query</li>



<li>Create a simple shortcode for a CTA button</li>



<li>Test different query parameters</li>
</ul>



<p class="wp-block-paragraph"><strong>Week 3: Asset Management &amp; Custom Content</strong></p>



<ul class="wp-block-list">
<li>Properly enqueue one CSS file and one JS file</li>



<li>Register a simple custom post type</li>



<li>Understand the loading order</li>
</ul>



<p class="wp-block-paragraph"><strong>Week 4: Advanced Concepts</strong></p>



<ul class="wp-block-list">
<li>Test REST API endpoints on your site</li>



<li>Implement transient caching on one slow query</li>



<li>Measure the performance improvement</li>
</ul>



<p class="wp-block-paragraph">Don&#8217;t try to learn all 8 WordPress code concepts at once. Master one per week. That&#8217;s how I learned, and that&#8217;s what works.</p>



<h2 class="wp-block-heading">Common WordPress Code Mistakes to Avoid</h2>



<p class="wp-block-paragraph">After teaching these WordPress code concepts for years, I&#8217;ve seen the same mistakes repeatedly:</p>



<p class="wp-block-paragraph"><strong>1. Editing parent theme files directly</strong> Always use child themes. Theme updates wipe your changes otherwise. Learn <a href="https://ceeveeglobal.com/how-to-add-custom-code-to-wordpress-without-a-page-builder-using-claude-ai-code-snippets-plugin/">how to create a child theme properly</a>.</p>



<p class="wp-block-paragraph"><strong>2. Forgetting wp_reset_postdata() after WP_Query</strong> This causes weird bugs with your main loop. Always reset. I&#8217;ve spent hours debugging this mistake.</p>



<p class="wp-block-paragraph"><strong>3. Not sanitizing shortcode attributes</strong> Use <code>esc_url()</code>, <code>esc_attr()</code>, and <code>esc_html()</code> to prevent security issues. Never trust user input.</p>



<p class="wp-block-paragraph"><strong>4. Loading scripts without dependencies</strong> If your script needs jQuery, specify it: <code>array('jquery')</code>. Otherwise, you&#8217;ll get &#8220;$ is not defined&#8221; errors.</p>



<p class="wp-block-paragraph"><strong>5. Not setting transient expiration</strong> Without expiration, transients become permanent database clutter. Always set a reasonable expiration time.</p>



<p class="wp-block-paragraph"><strong>6. Hardcoding instead of using WordPress functions</strong> Use <code>get_template_directory_uri()</code> instead of hardcoded paths. Your code will break when you change themes.</p>



<p class="wp-block-paragraph"><strong>7. Not flushing permalinks after custom post types</strong> Register a custom post type, then go to Settings → Permalinks → Save. Otherwise, you&#8217;ll get <a href="https://ceeveeglobal.com/wordpress-404-error-causes-effects-and-simple-solutions/">404 errors</a>.</p>



<p class="wp-block-paragraph">Learn from my mistakes. I&#8217;ve made all of these errors and spent hours debugging them.</p>



<h2 class="wp-block-heading">Tools to Help You Learn WordPress Code Concepts</h2>



<p class="wp-block-paragraph">You don&#8217;t have to learn these WordPress code concepts alone. Here are tools that helped me:</p>



<p class="wp-block-paragraph"><strong>1. Code Snippets Plugin</strong> — Practice WordPress code safely without editing functions.php directly. If something breaks, just disable the snippet. No White Screen of Death. <a href="https://wordpress.org/plugins/code-snippets/">Download from WordPress.org</a></p>



<p class="wp-block-paragraph"><strong>2. Query Monitor Plugin</strong> — See exactly which hooks fire, database queries run, and where your code slows down. Essential for learning WordPress code concepts. Shows every SQL query, PHP error, and HTTP request.</p>



<p class="wp-block-paragraph"><strong>3. WordPress Code Reference</strong> — Official documentation at <a href="https://developer.wordpress.org/">developer.wordpress.org</a>. Search any function, hook, or class. Bookmark it.</p>



<p class="wp-block-paragraph"><strong>4. My AI-Powered WordPress Tools:</strong></p>



<ul class="wp-block-list">
<li><a href="https://ceeveeglobal.com/tools/wordpress-plugin-finder/">WordPress Plugin Finder</a> — Find plugins by functionality</li>



<li><a href="https://ceeveeglobal.com/wp-error-expert/">WP Error Expert</a> — AI-powered error diagnosis using my knowledge base</li>



<li><a href="https://ceeveeglobal.com/wordpress-performance-analyzer-tool/">WordPress Performance Analyzer</a> — Analyze Core Web Vitals and get optimization tips</li>
</ul>



<p class="wp-block-paragraph">These tools use the same WordPress code concepts I just taught you. Study how they work.</p>



<p class="wp-block-paragraph"><strong>5. Local Development Environment</strong> — Test code on your computer before deploying live. I use Local by Flywheel. Watch my <a href="https://www.youtube.com/watch?v=UktgUbpOPio">Local by Flywheel tutorial</a> to set it up.</p>



<h2 class="wp-block-heading">Conclusion: Your WordPress Code Concepts Journey Starts Now</h2>



<p class="wp-block-paragraph">These 8 WordPress code concepts power every WordPress site you&#8217;ve ever used — from simple blogs to WooCommerce stores to complex membership sites.</p>



<p class="wp-block-paragraph">Understanding these fundamental WordPress code concepts transforms you from someone who installs plugins to someone who builds solutions. That&#8217;s the difference between WordPress users and WordPress developers.</p>



<p class="wp-block-paragraph">You don&#8217;t need to master all 8 WordPress code concepts today. Start with <strong>add_action()</strong> and <strong>add_filter()</strong> — they&#8217;re the foundation everything else builds on.</p>



<p class="wp-block-paragraph">Here&#8217;s your action plan:</p>



<p class="wp-block-paragraph"><strong>This week:</strong> Try adding one action hook with visible output (like the footer text example)<br><strong>Next week:</strong> Create a simple filter to add reading time to posts<br><strong>Week 3:</strong> Build something custom with WP_Query<br><strong>Week 4:</strong> Start exploring REST API and transients</p>



<p class="wp-block-paragraph">I learned these WordPress code concepts by breaking things. You will too. That&#8217;s okay. Just keep your backups current and learn from every error.</p>



<p class="wp-block-paragraph"><strong>Pro tip from 15 years of WordPress experience:</strong> Use the <a href="https://wordpress.org/plugins/code-snippets/">Code Snippets plugin</a> instead of editing functions.php directly. It&#8217;s safer, easier to debug, and you can disable snippets without deleting code.</p>



<p class="wp-block-paragraph">Don&#8217;t forget to set up backups before experimenting. Watch my video: <a href="https://www.youtube.com/watch?v=vE93UqZNWh0">Stop Paying for Backup Plugins &#8211; Build Your Own with AI in 15 Minutes</a>. I&#8217;ll show you exactly how to create a custom backup system that costs $0.</p>



<p class="wp-block-paragraph">These WordPress code concepts are your foundation. Build on them, experiment with them, break things and fix them. That&#8217;s how you become a real WordPress developer.</p>



<p class="wp-block-paragraph"><strong>Which WordPress code concept are you trying first? Drop a comment below.</strong> I read every single one and reply with help if you&#8217;re stuck. Let&#8217;s build something together.</p>



<h2 class="wp-block-heading">Frequently Asked Questions About WordPress Code Concepts</h2>



<h3 class="wp-block-heading">What are the most important WordPress code concepts for beginners?</h3>



<p class="wp-block-paragraph">The most important WordPress code concepts for beginners are add_action() and add_filter() hooks. These two WordPress code concepts form the foundation of WordPress development. Master hooks first, then move to WP_Query, shortcodes, and wp_enqueue functions. Start with actions and filters — everything else builds on understanding how WordPress hooks work.</p>



<h3 class="wp-block-heading">What&#8217;s the difference between add_action and add_filter in WordPress?</h3>



<p class="wp-block-paragraph">add_action() adds new functionality at specific WordPress events, while add_filter() modifies existing data before display. Actions &#8220;do something&#8221; (add tracking code, send emails), filters &#8220;change something&#8221; (modify post content, change titles). Both are essential WordPress code concepts, but serve different purposes. Use actions to insert new features, use filters to transform existing content.</p>



<h3 class="wp-block-heading">Do I need to edit functions.php to use WordPress code concepts?</h3>



<p class="wp-block-paragraph">No, you don&#8217;t have to edit functions.php directly. Use the Code Snippets plugin for safer WordPress code management, or create a custom plugin. Only edit functions.php if you&#8217;re using a child theme. Never edit parent theme files — updates will erase your changes. Code Snippets is the safest way to practice WordPress code concepts without risking your site.</p>



<h3 class="wp-block-heading">Which WordPress code concepts should I learn first?</h3>



<p class="wp-block-paragraph">Learn WordPress code concepts in this order: 1) add_action() and add_filter() hooks (foundation), 2) WP_Query for custom loops, 3) wp_enqueue for loading assets properly, 4) shortcodes for reusable content, 5) custom post types, 6) REST API, and 7) transients. Master hooks first — they&#8217;re used in all other WordPress code concepts and understanding them makes everything else easier.</p>



<h3 class="wp-block-heading">Can WordPress code concepts break my site?</h3>



<p class="wp-block-paragraph">Yes, syntax errors in WordPress code can cause the White Screen of Death. Always backup before adding custom code. Use a child theme or Code Snippets plugin. Test on staging sites first. One missing semicolon can break everything. Learn proper WordPress code concepts to avoid common mistakes and keep your site safe. Read my <a href="https://ceeveeglobal.com/how-to-fix-the-wordpress-white-screen-of-death-a-simple-guide-for-young-web-developers/">White Screen of Death guide</a> for recovery steps.</p>



<h3 class="wp-block-heading">How do WordPress hooks work with themes and plugins?</h3>



<p class="wp-block-paragraph">WordPress hooks (actions and filters) work the same in themes and plugins. They&#8217;re core WordPress code concepts that let themes and plugins interact with WordPress without modifying core files. Plugins use hooks to add features, themes use hooks to modify display. Both follow the same WordPress code concepts and execution order. Hooks are the standard way to extend WordPress functionality.</p>



<h3 class="wp-block-heading">What are WordPress transients used for?</h3>



<p class="wp-block-paragraph">WordPress transients cache temporary data with automatic expiration. Use transients to cache slow database queries, external API responses, or expensive calculations. Transients are essential WordPress code concepts for performance optimization. They reduce server load and speed up sites without external caching plugins. Set appropriate expiration times — I use 1 hour for frequently updated data, 24 hours for static content.</p>



<h3 class="wp-block-heading">How does WP_Query differ from the default WordPress loop?</h3>



<p class="wp-block-paragraph">WP_Query gives you complete control over which posts display, while the default loop shows all posts. WP_Query is one of the most flexible WordPress code concepts — filter by category, tag, custom fields, dates, post types, and more. Use WP_Query when the default loop isn&#8217;t flexible enough. Always use wp_reset_postdata() after custom queries to avoid conflicts with the main loop.</p>



<h3 class="wp-block-heading">How can I test if my WordPress code is working?</h3>



<p class="wp-block-paragraph">Test WordPress code by adding visible output first. Use echo statements with HTML and CSS styling so you can see results immediately. Don&#8217;t use console.log() for initial testing — it&#8217;s invisible unless you open browser Developer Console. Start with simple examples like adding colored text or borders. Once you confirm code works, then optimize it. Check my <a href="https://ceeveeglobal.com/wordpress-enable-error-log-a-teens-guide-to-keeping-your-website-error-free/">WordPress Error Log guide</a> for debugging tips.</p>



<h3 class="wp-block-heading">Are WordPress code concepts the same across all WordPress versions?</h3>



<p class="wp-block-paragraph">Core WordPress code concepts (hooks, WP_Query, shortcodes, wp_enqueue) have been stable for years and work across all modern WordPress versions. However, newer features like REST API and Gutenberg support require WordPress 4.7+ and 5.0+ respectively. Always check WordPress documentation for version-specific features. These fundamental WordPress code concepts you learned will work on any WordPress site running version 4.0 or newer.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://ceeveeglobal.com/essential-wordpress-code-concepts-beginners-guide/">8 Essential WordPress Code Concepts Every Beginner Should Master (With Real Examples)</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ceeveeglobal.com/essential-wordpress-code-concepts-beginners-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How to Build and Integrate an AI Chatbot into Your WordPress Site: A Complete Guide</title>
		<link>https://ceeveeglobal.com/build-an-ai-chatbot-into-wordpress/</link>
		
		<dc:creator><![CDATA[Dimuthu Harshana]]></dc:creator>
		<pubDate>Mon, 03 Nov 2025 05:12:46 +0000</pubDate>
				<category><![CDATA[Beginner Guides]]></category>
		<category><![CDATA[Ai]]></category>
		<category><![CDATA[Automation]]></category>
		<category><![CDATA[Build AI Chatbot]]></category>
		<guid isPermaLink="false">https://ceeveeglobal.com/?p=14413</guid>

					<description><![CDATA[<p>Why You Should Add an AI Chatbot to Your Website Why did I create an AI chatbot for my website? Users coming to my website come from different countries. Their arrival times vary depending on the countries they live in. So if I go to chat with them, I have to put my life under&#8230;&#160;<a href="https://ceeveeglobal.com/build-an-ai-chatbot-into-wordpress/" rel="bookmark">Read More &#187;<span class="screen-reader-text">How to Build and Integrate an AI Chatbot into Your WordPress Site: A Complete Guide</span></a></p>
<p>The post <a href="https://ceeveeglobal.com/build-an-ai-chatbot-into-wordpress/">How to Build and Integrate an AI Chatbot into Your WordPress Site: A Complete Guide</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h2>Why You Should Add an AI Chatbot to Your Website</h2>
<p>Why did I create an AI chatbot for my website?</p>
<p>Users coming to my website come from different countries. Their arrival times vary depending on the countries they live in. So if I go to chat with them, I have to put my life under my website.</p>
<p>I wanted to overcome that barrier and provide instant responses to user queries, improving engagement and satisfaction.</p>
<p>It also allowed me to increase my website&#8217;s efficiency, automate repetitive tasks, and free up time for more complex issues. Plus it allowed me to increase conversions.</p>
<p>&nbsp;</p>
<p>How would you like to be able to go to a website and get instant answers to your questions without waiting for someone to respond?</p>
<p>Basically, that is the main task an AI chatbot can do for your website!</p>
<p>Hence, AI chatbots are becoming a must for any website. That&#8217;s because it can accomplish tasks ranging from improving user experience to improving website efficiency and increasing conversions.</p>
<p>Here, I will introduce you to the steps to create a similar or more improved AI chatbot for your website. You can try using the click icon on the bottom right of this page.</p>
<h3>By adding an AI chatbot to your website, you can:</h3>
<h4>Improving user experience</h4>
<p>The biggest advantage of an AI chatbot is that it can give very accurate answers to user queries instantly. Imagine a user lands on your website in the middle of the night with a question. A chatbot can provide an immediate response instead of waiting for someone to get back to them. This will make your website more attractive and make your visitors happy.</p>
<h4>Increasing efficiency</h4>
<p>By automating repetitive tasks, an AI chatbot saves you time. Think about it. Instead of answering the same questions over and over again, you can do it through your chatbot. This allows you to focus on the more complex and important tasks of your website.</p>
<h4>Increase conversions</h4>
<p>AI chatbots can guide users through your website, helping them find what they&#8217;re looking for faster. This can lead to more sales, signups, or whatever else your website may be aiming for and have a positive impact.</p>
<p>In this guide, I&#8217;ll walk you through everything you need to know to build your own AI chatbot and integrate it into your WordPress site.</p>
<h2>Understanding AI Chatbots: The Core Structure</h2>
<p>Before we dive into the technical aspects, it is essential to understand the basic structure of an AI chatbot. It will be very useful for you while building or improving this chatbot.</p>
<h4>Key components of an AI chatbot</h4>
<p><strong>User Interface (UI):</strong> The visual part through which users interact with the chatbot. This is shown in the image above.</p>
<p><strong>Backend:</strong> Server-side logic that processes user input and generates responses.</p>
<p><strong>AI Integration:</strong> The chatbot&#8217;s brain is powered by models like OpenAI.</p>
<h4>Overview of how it works</h4>
<p>&nbsp;</p>
<p>Chatbots capture user input, send it to the backend, where AI processes it, and then displays the response to the user on the frontend.</p>
<h2>Step-by-step guide to building your AI Chatbot</h2>
<p>Now that you have a basic understanding of AI Chatbot, let&#8217;s start building your Chatbot.</p>
<p>I will guide you step by step. Follow these steps very carefully. I have used <strong>PHP</strong>, <strong>HTML</strong>, <strong>CSS</strong>, and <strong>JavaScript</strong> for this process. Don&#8217;t worry I&#8217;m not an expert in these things like you. But you can make this process very easy by using ChatGPT or Claude as a support.</p>
<p>However, I am providing all the codes you need here.</p>
<p><a href="https://ceeveeglobal.ck.page/0afba29bbd"><br />
AI Chatbot For WordPress<br />
</a></p>
<h3>Creating a Chatbot UI</h3>
<p>First impressions are the foundation of anything. A good-looking chatbot makes a big difference to your website. Here&#8217;s how to design a simple yet attractive chatbot interface.</p>
<h5>Essential UI elements</h5>
<p><strong>Chat Box:</strong> This is where the conversation happens. It shows the chat history between the user and the chatbot.</p>
<p><strong>Input Field:</strong> Allows users to type their messages here.</p>
<p><strong>Send Button:</strong> This is used to send the message to the chatbot.</p>
<p>To create this UI, you will use HTML and CSS. You can download all related files below.</p>
<p>After entering the <strong>AI-chatbot</strong> folder, you can see two folders: <strong>front</strong> and <strong>back</strong>. Go inside the front folder. There are 4 files as shown below.<img loading="lazy" decoding="async" src="https://ceeveeglobal.com/wp-content/uploads/chatbot-3.png" alt="chatbot-3" width="700" height="450" /></p>
<p>Here the <strong>chatbot.html</strong> file contains everything. But the <strong>Index.html</strong>, <strong>script.js</strong>, and <strong>style.css</strong> files contain only the related things. From there you can use these files appropriately.</p>
<p>Either way, open <strong>chatbot.html</strong> or <strong>Index.html</strong> in your browser. Then you will see the chatbot as below.<img loading="lazy" decoding="async" src="https://ceeveeglobal.com/wp-content/uploads/chatbot_4.png" alt="chatbot_4" width="700" height="450" /></p>
<p>But you still can&#8217;t chat with this.</p>
<h4>Set up Chatbot code in the backend</h4>
<p>Now that your UI is ready, it&#8217;s time to access the code that will make your chatbot work.</p>
<ol>
<li>
<h5><strong>Install and activate WPCode Lite.</strong></h5>
</li>
<li>Paste the code in the back folder here. Note, the Code Type should be changed to P<strong>HP Snippet.</strong></li>
<li>Connecting the OpenAI API Key.<br />
Register on the OpenAI website and get an OpenAI API key. Because our chatbot is powered by the OpenAI API.  Keep this confidential. Next, enter the OpenAI API key in the snippet and save the snippet.</li>
</ol>
<h4>Linking the UI with the backend</h4>
<p>Now that your front and back panels are ready, it&#8217;s time to connect them together. This is where JavaScript comes into play.</p>
<p>Now open the script.js file in the front folder. You can use Notepad for this. Go there and update your domain name and save the file.</p>
<p>Eg: replace <strong>https://YourWebsite.com</strong> with your site domain name.</p>
<p>If you did everything correctly, now you just have to open the index.html file again. To your surprise, your chatbot will work like the image below.</p>
<p><img loading="lazy" decoding="async" src="https://ceeveeglobal.com/wp-content/uploads/chatbot-9.png" alt="chatbot-9" width="700" height="450" /></p>
<h2>Advanced Customisation: Taking Your Chatbot to the Next Level</h2>
<p>This chatbot is designed to suit my site. That is, only matters related to WordPress can be chatted through this chat button. When asked about a matter not related to WordPress, it replies, &#8220;I am trained to assist with WordPress-related questions. Feel free to ask me anything about WordPress&#8221;.</p>
<p>So, how to customise this chatbot for you?</p>
<h3>System Prompt Customisation</h3>
<p>Customising your AI chatbot&#8217;s system prompt is essential to ensure it aligns with the specific needs of your website and the type of interaction you want to have with your users. So here&#8217;s how to adjust your system prompt to suit different websites:</p>
<h4>1. Understand the purpose of your website</h4>
<p>Identify the primary function of your website. Is it an e-commerce site, a blog, a tech support platform or a content-driven site? Your chatbot&#8217;s system prompt should reflect the primary purpose and type of queries users might have.</p>
<h4>2. Define the scope of the interaction</h4>
<p>Clearly state the purpose for which your chatbot is designed. For example, if your website focuses on e-commerce, chatbots can assist with product inquiries, order tracking, or product recommendations. The scope should be narrow enough to keep the chatbot focused but broad enough to handle common user needs.</p>
<h4>3. Crafting the System Prompt</h4>
<p>Change the language and focus of the system prompt based on the context of the site. Here&#8217;s how you can adjust the provided trigger for different types of websites:</p>
<h5>Example 1: E-commerce Website:</h5>
<p><em>You are a knowledgeable shopping assistant on our online store. Your task is to help users find products, answer questions about orders, provide product recommendations, and assist with any inquiries related to our store. Engage users in friendly conversations to enhance their shopping experience. Do not respond to questions unrelated to shopping or our products. If asked, politely state that you are here to assist with shopping-related queries.</em></p>
<h5>Example 2: Tech Support Website</h5>
<p><em>You are a tech support specialist. Your task is to help users troubleshoot technical issues, provide guidance on software and hardware products, and assist with inquiries related to our tech support services. Engage users in problem-solving conversations to resolve their technical problems. Do not respond to questions outside of tech support. If asked, politely inform them that you specialize in providing tech support assistance.</em></p>
<h5>Example 3: Blog or Content-Focused Website</h5>
<p><em>You are an expert content guide for our website. Your task is to help users find articles, explain content topics, suggest related posts, and assist with any questions about our blog content. Engage users in informative conversations to enrich their reading experience. Do not respond to questions unrelated to our content. If asked, gently redirect them to the relevant blog topics.</em></p>
<h5>Example 4: Educational Platform</h5>
<p><em>You are an educational assistant for our online learning platform. Your task is to help users navigate courses, answer questions about course content, provide study tips, and assist with any inquiries related to learning on our platform. Engage users in supportive conversations to enhance their learning journey. Do not respond to questions outside of education-related topics. If asked, kindly direct them to the appropriate course material.</em></p>
<h4>4. Enter the brand identity</h4>
<p>Make sure the tone and style of the prompt aligns with your brand&#8217;s voice. If your brand is casual and fun, the query should reflect that. For a more professional brand, use formal and precise language.</p>
<h4>5. Test and Iterate</h4>
<p>After customizing your system prompt, test it with real user interactions to see if it meets the intended objectives. Gather feedback and make adjustments as needed to fine-tune the chatbot&#8217;s performance.</p>
<p>By customizing your chatbot&#8217;s system prompts, you can ensure that it provides relevant, useful and on-brand responses, making your website&#8217;s user experience more engaging and effective.</p>
<p>After you prepare your prompt, go to <strong>&#8220;Add the last prompt to the conversation history&#8221;</strong> in the Backend Snippet and enter the prompt you prepared.</p>
<p>Then save the code snippet. Depending on the success of your prompting, the output of your chatbot will be successful.</p>
<h3>Icon Customization</h3>
<p>But your chatbot still has the same icon as my chatbot. Let&#8217;s change that too.</p>
<p>Go to <strong>&#8220;load_chat_bot_base_configuration&#8221; </strong>and change your user_avatar_url and bot_image_url to the icon you require.</p>
<h3>Other minor changes</h3>
<p>You can change the Startup Message to suit you. You can also change the font size and commonButtons of your chatbot.</p>
<p><img loading="lazy" decoding="async" src="https://ceeveeglobal.com/wp-content/uploads/chatbot-10.png" alt="chatbot-10" width="700" height="450" /></p>
<h2>Final Steps: Publishing Your Chatbot on WordPress</h2>
<p>Now you can integrate your chatbot into your WordPress website pages.<br />
For that, we are again asking for help in the code snippet.</p>
<p>Go to Header and Footer in the code snippet. Go there and paste the full code, HTML, CSS, and JavaScript, in the footer.</p>
<p><img loading="lazy" decoding="async" src="https://ceeveeglobal.com/wp-content/uploads/chatbot-11.png" alt="chatbot-11" width="700" height="450" /></p>
<p><strong>Bonus Tip:</strong> If you run into any WordPress errors while setting up your chatbot, don’t worry. My WP Error Expert AI tool can help you fix many errors easily. And if you’re a web developer, check out my other AI tools designed to make your life easier.</p>
<h3>Conclusion</h3>
<p>In conclusion, integrating an AI chatbot into your WordPress site can significantly enhance user experience, increase efficiency, and drive conversions.</p>
<p>By automating responses to common queries, an AI chatbot ensures that visitors receive instant support, no matter the time of day.</p>
<p>This guide provides a comprehensive, step-by-step process to build and customize an AI chatbot that aligns with your website&#8217;s specific needs, whether it&#8217;s for e-commerce, tech support, content guidance, or education.</p>
<p>By following these steps, you can create a chatbot that improves customer satisfaction and optimizes your website&#8217;s performance, ultimately leading to better engagement and outcomes.</p>
<p><a href="https://ceeveeglobal.ck.page/0afba29bbd"><br />
AI Chatbot For WordPress<br />
</a></p>
<p>The post <a href="https://ceeveeglobal.com/build-an-ai-chatbot-into-wordpress/">How to Build and Integrate an AI Chatbot into Your WordPress Site: A Complete Guide</a> appeared first on <a href="https://ceeveeglobal.com">The Beginner’s Playbook for Fixing WordPress Errors</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
