<?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>Nigel M Rodgers</title>
	<atom:link href="https://nigelmrodgers.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://nigelmrodgers.com</link>
	<description>WordPress specialist in Harare. I fix, secure and look after WordPress sites.</description>
	<lastBuildDate>Mon, 09 Nov 2026 09:53:08 +0000</lastBuildDate>
	<language>en-GB</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>
	<item>
		<title>I kept WordPress and removed the server</title>
		<link>https://nigelmrodgers.com/moved-wordpress-site-to-static/</link>
		
		<dc:creator><![CDATA[Nigel M Rodgers]]></dc:creator>
		<pubDate>Mon, 09 Nov 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[Uncategorised]]></category>
		<guid isPermaLink="false">https://nigelmrodgers.com/C:/Program%20Files/Git/moved-wordpress-site-to-static/</guid>

					<description><![CDATA[Trimming assets by 22% did nothing for a 1.3 second first byte. Taking the origin out of the path got it to 0.3 seconds.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Nziyo is a Shona hymn library with about 2,600 pages. These hymns are sung in Zimbabwe. Its server was in the US.</p>



<p class="wp-block-paragraph">On a cached page, the first byte took about 1.3 seconds to arrive here. I spent time on the usual fixes. I minified everything, and stopped Contact Form 7 and its reCAPTCHA from loading on hymn pages. Combined assets went from 113,998 bytes to 88,953, a 22% cut.</p>



<p class="wp-block-paragraph">The first byte took 1.31 seconds before that work and 1.28 to 1.34 seconds after it.</p>



<h2 class="wp-block-heading">Distance doesn&#8217;t minify</h2>



<p class="wp-block-paragraph">About 890 ms of that time was round trips to the US. No plugin setting can change where a server is. The options were a CDN in front of WordPress, or removing the origin from the visitor&#8217;s path altogether.</p>



<p class="wp-block-paragraph">Almost nothing on a hymn site changes between requests, so I took the second option.</p>



<h2 class="wp-block-heading">How it works now</h2>



<p class="wp-block-paragraph">WordPress runs only on my own PC. I edit there exactly as before. A Python script asks WordPress for every public URL, saves the HTML, rewrites the links, copies the assets and deploys the result to Cloudflare Pages.</p>



<p class="wp-block-paragraph">Two things needed replacing, because static files can&#8217;t run PHP:</p>



<ul class="wp-block-list">

<li><strong>Search</strong> became a Pagefind index, built during export.</li>


<li><strong>Forms and comments</strong> became Cloudflare Pages Functions. Comments wait in a D1 database and I approve them from a link in an email. The next publish imports them into WordPress and clears them from the queue.</li>

</ul>



<h2 class="wp-block-heading">The result</h2>



<p class="wp-block-paragraph">The first byte now takes about 0.3 seconds from the same connection. There&#8217;s also no public <code>wp-login.php</code> for bots to hit, and nothing on the server to patch.</p>



<h2 class="wp-block-heading">What it costs you</h2>



<p class="wp-block-paragraph">Publishing is now a step you have to run, and scheduled posts won&#8217;t appear by themselves. The only full copy of the site is on one computer, which makes a nightly backup essential. And this approach suits a site that&#8217;s mostly read. A shop with carts and accounts needs something else.</p>



<p class="wp-block-paragraph">For a brochure site, a blog or a reference library, it&#8217;s worth asking whether your visitors need PHP at all.</p>


]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>update_post_meta does not fire save_post</title>
		<link>https://nigelmrodgers.com/update-post-meta-does-not-fire-save-post/</link>
		
		<dc:creator><![CDATA[Nigel M Rodgers]]></dc:creator>
		<pubDate>Mon, 09 Nov 2026 09:30:00 +0000</pubDate>
				<category><![CDATA[Uncategorised]]></category>
		<guid isPermaLink="false">https://nigelmrodgers.com/C:/Program%20Files/Git/update-post-meta-does-not-fire-save-post/</guid>

					<description><![CDATA[A lookup table that rebuilt on save_post went stale the moment a script changed the data. Why, and the two ways to handle it.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I built a feature that links the same hymn across different hymn books. It keeps a lookup table in an option and rebuilds it on the <code>save_post</code> hook, so the table is always current.</p>



<p class="wp-block-paragraph">Then I corrected some lyrics with a script, and the table stopped being current.</p>



<h2 class="wp-block-heading">The catch</h2>



<p class="wp-block-paragraph">The lyrics live in post meta. My script called <code>update_post_meta()</code>, which writes the meta and fires its own meta hooks. It doesn&#8217;t fire <code>save_post</code>, because the post itself wasn&#8217;t saved.</p>



<p class="wp-block-paragraph">Edit a post on the screen and everything works, since the editor saves the post. Change the same value from a script, an import or WP-CLI, and anything hanging off <code>save_post</code> never hears about it.</p>



<h2 class="wp-block-heading">What to do about it</h2>



<p class="wp-block-paragraph">If your code derives something from post meta, you have two options.</p>



<p class="wp-block-paragraph">Hook the meta write as well:</p>



<pre class="wp-block-code"><code>add_action( &#039;updated_post_meta&#039;, function ( $meta_id, $post_id, $key ) {
    if ( &#039;hymn&#039; === $key ) {
        my_rebuild_map();
    }
}, 10, 3 );</code></pre>



<p class="wp-block-paragraph">That fires once per changed row, so on a bulk import you&#8217;d rebuild thousands of times. For a bulk job it&#8217;s cheaper to do what I did and call the rebuild once, at the end of the script.</p>



<p class="wp-block-paragraph">Either way, the rule is worth remembering. <code>save_post</code> means a post was saved, and it doesn&#8217;t mean the data changed.</p>


]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>The cache purge that said it worked</title>
		<link>https://nigelmrodgers.com/litespeed-purge-said-it-worked/</link>
		
		<dc:creator><![CDATA[Nigel M Rodgers]]></dc:creator>
		<pubDate>Mon, 09 Nov 2026 09:00:00 +0000</pubDate>
				<category><![CDATA[Uncategorised]]></category>
		<guid isPermaLink="false">https://nigelmrodgers.com/C:/Program%20Files/Git/litespeed-purge-said-it-worked/</guid>

					<description><![CDATA[wp litespeed-purge all printed Success, and 19,189 stale pages kept being served. Where the cache really lived, and how to check a purge.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I added a tracking tag to a site. The tag appeared in freshly generated pages and on no real URL. So I purged the cache.</p>



<pre class="wp-block-code"><code>wp litespeed-purge all
Success: Purged All!</code></pre>



<p class="wp-block-paragraph">The pages kept coming back with <code>x-litespeed-cache: hit</code> and the old HTML.</p>



<h2 class="wp-block-heading">Where the cache really was</h2>



<p class="wp-block-paragraph">The page cache on that host lived in server-level storage outside the WordPress folder, in a directory called <code>lscache</code>. It held 19,189 pages. The <code>wp-content/litespeed</code> folder that I&#8217;d assumed was the cache only held about 3 MB of optimized assets.</p>



<p class="wp-block-paragraph">The WP-CLI purge signal never reached the server&#8217;s storage. The command had no way of knowing that, so it reported success.</p>



<p class="wp-block-paragraph">Purging from wp-admin worked, because that request goes through the web server. On the command line, this did the job:</p>



<pre class="wp-block-code"><code>find ~/lscache -mindepth 2 -type f -delete</code></pre>



<p class="wp-block-paragraph">It&#8217;s safe because it&#8217;s only a cache and rebuilds itself on the next request. <code>-mindepth 2</code> leaves the top-level control file alone.</p>



<h2 class="wp-block-heading">Check the page, not the message</h2>



<p class="wp-block-paragraph">With a seven-day cache lifetime, a theme or plugin change on that site could look broken for a week. Any fix would seem to have done nothing, and you&#8217;d go hunting for a bug that wasn&#8217;t there.</p>



<p class="wp-block-paragraph">So after any purge, I fetch the page twice:</p>



<pre class="wp-block-code"><code>curl -s -D- https://example.com/ -o page.html | grep -i x-litespeed-cache</code></pre>



<p class="wp-block-paragraph">The first fetch should be a <code>miss</code> and the second a <code>hit</code>, with your new markup in the file. Desktop and mobile are cached separately, so check both.</p>



<p class="wp-block-paragraph">A success message tells you that a command finished. It says nothing about whether the thing you wanted actually happened.</p>


]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
