<?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>wordpress Archives - Alexandros Georgiou</title>
	<atom:link href="https://www.alexgeorgiou.gr/tag/wordpress/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.alexgeorgiou.gr/tag/wordpress/</link>
	<description>Balancing brackets for a living</description>
	<lastBuildDate>Mon, 01 Jul 2024 07:35:49 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.alexgeorgiou.gr/wp-content/uploads/2021/07/cropped-alexgeorgiou-icon-32x32.png</url>
	<title>wordpress Archives - Alexandros Georgiou</title>
	<link>https://www.alexgeorgiou.gr/tag/wordpress/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>🎞 Engaging video shorts with subtitles, the open source way</title>
		<link>https://www.alexgeorgiou.gr/engaging-video-shorts-with-subtitles/</link>
					<comments>https://www.alexgeorgiou.gr/engaging-video-shorts-with-subtitles/#comments</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Thu, 14 Mar 2024 12:01:13 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[AdMob]]></category>
		<category><![CDATA[advertising]]></category>
		<category><![CDATA[Aegisub]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Audacity]]></category>
		<category><![CDATA[marketing]]></category>
		<category><![CDATA[OpenShot]]></category>
		<category><![CDATA[promotion]]></category>
		<category><![CDATA[social media]]></category>
		<category><![CDATA[SubRip]]></category>
		<category><![CDATA[subtitles]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=1518</guid>

					<description><![CDATA[<p>How I create engaging video shorts with subtitles for promoting my Android apps.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/engaging-video-shorts-with-subtitles/">🎞 Engaging video shorts with subtitles, the open source way</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Today I share with you some of my most recent thoughts on marketing, followed by some technical advice on how to create video shorts. If you are just here for the technical advice on video shorts, scroll down.</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="552" src="https://www.alexgeorgiou.gr/wp-content/uploads/2024/03/image-1-1024x552.png" alt="Using Audacity to add subtitles to the voice track of a video short ad." class="wp-image-1520" srcset="https://www.alexgeorgiou.gr/wp-content/uploads/2024/03/image-1-1024x552.png 1024w, https://www.alexgeorgiou.gr/wp-content/uploads/2024/03/image-1-300x162.png 300w, https://www.alexgeorgiou.gr/wp-content/uploads/2024/03/image-1-768x414.png 768w, https://www.alexgeorgiou.gr/wp-content/uploads/2024/03/image-1-1536x828.png 1536w, https://www.alexgeorgiou.gr/wp-content/uploads/2024/03/image-1.png 1600w" sizes="(max-width: 599px) calc(100vw - 50px), (max-width: 767px) calc(100vw - 70px), (max-width: 991px) 429px, (max-width: 1199px) 637px, 354px" /><figcaption class="wp-element-caption"><em>Using Audacity to add subtitles to the voice track of a video short ad.</em></figcaption></figure>



<p>Lately I&#8217;ve been working less on product development, and more on promoting my existing products, mostly <a href="https://play.google.com/store/apps/developer?id=Alexandros+Georgiou" data-type="link" data-id="https://play.google.com/store/apps/developer?id=Alexandros+Georgiou" target="_blank" rel="noreferrer noopener">my Android apps</a>, but also my <a href="https://dashed-slug.net/" target="_blank" rel="noreferrer noopener">WordPress plugins</a>. One of the apps seems like it is beginning to take off organically just by being on Google Play, but I feel that all of them could benefit from a little extra push.</p>



<h2 class="wp-block-heading">I don&#8217;t hate marketing any more</h2>



<p>As much as I <a href="https://www.youtube.com/watch?v=tHEOGrkhDp0&amp;ab_channel=TyrellEdwards" data-type="link" data-id="https://www.youtube.com/watch?v=tHEOGrkhDp0&amp;ab_channel=TyrellEdwards" target="_blank" rel="noreferrer noopener">don&#8217;t love marketing or marketers</a>, after trying to do it myself, I learned a lot about what it entails, and I even gained a little appreciation towards the discipline (not a lot, mind you). Getting your product known to potential users is a necessity, and I now realize that what people mean when they say &#8220;I hate marketing&#8221;, is that they hate marketing that is done badly. With marketing, essentially, you&#8217;re just telling people &#8220;Hey, I made this, maybe you&#8217;re interested in it?&#8221; There&#8217;s nothing wrong with marketing when done right, as long as you don&#8217;t annoy people.</p>



<p>An example of right versus wrong kind of marketing: I see a lot of app developers fail because they use interstitial ads too much in their apps. These are the type of intrusive ads that have the highest payout, but also they annoy users the most (what with being intrusive and all). In my apps, I like to focus on serving banner ads, and only show interstitial ads for features that are beyond the main value proposition of my app. Interstitial ads are OK, if you first negotiate with your users, honestly and without deception, that they will sit through an annoying ad and then they&#8217;ll get something extra. Something that they didn&#8217;t expect to get when they downloaded the app. I&#8217;m not going to get any ad revenue if the user gets annoyed and uninstalls my app. On the other hand, if the app satisfies a need, a small banner at the bottom of the screen is not going to annoy users. A banner ad can generate significant revenue, once you hit a respectable number of users.</p>



<h2 class="wp-block-heading">Some key insights</h2>



<p>As for advertising my own apps, I&#8217;m only an amateur marketer, but already I&#8217;ve identified two key insights that are prevalent in the industry:</p>



<p>1. <span style="text-decoration: underline;">Video is king.</span></p>



<p>2. <span style="text-decoration: underline;">Only losers pay for sex and marketing.</span></p>



<p>The first one is obvious. The main message of your campaign should be available in all modes of media: text, images, and video. But video is king. It&#8217;s engaging. It&#8217;s rich. It sets its own pace. It stimulates two senses (sight and hearing). Reading is so last-century. Show-and-tell is more effective than a long article explaining things. <span style="text-decoration: underline;">Customers want solutions, not lectures.</span> (OK, that&#8217;s actually a third insight!)</p>



<p>The second insight is less obvious. Paying for ads only works marginally well, once you have an excellent product and an excellent well-tuned ad. If you don&#8217;t know exactly what you&#8217;re doing, you can burn through your advertising budget pretty quickly with very poor results.</p>



<p>And if you don&#8217;t know what you&#8217;re doing, you don&#8217;t want to rely on a marketing agency in my opinion. That&#8217;s even less efficient than just paying for ads. It&#8217;s best to learn the skill yourself. I&#8217;m generally a DIY type of guy, I only rely on a professional for accounting.</p>



<p>DIY posting on social media, can be more effective: It&#8217;s free. It&#8217;s organic. You get analytics feedback that helps you learn the skill. Your viewers let their guard down and give a chance to your message. <a href="https://en.wikipedia.org/wiki/Banner_blindness" target="_blank" rel="noreferrer noopener">Banner blindness</a> is a thing.</p>



<h2 class="wp-block-heading">Shorts: not just a type of pants suitable for warm weather</h2>



<p>Video shorts are vertical videos that have an aspect ratio of 9:16, and are typically 1080&#215;1920 if you aim for HD. All video platforms now have them. They are suitable for viewing by teenagers, with a phone held vertically.</p>



<p>Every platform has its own set of technical requirements or recommendations. For compatibility with most platforms, I aim at 30 to 45 seconds. (Incidentally, this is the attention span of the average zoomer.)</p>



<h2 class="wp-block-heading">Here&#8217;s a few more insights to consider, in no particular order:</h2>



<ul class="wp-block-list">
<li>Start by writing the short video&#8217;s script before doing anything else. <strong>Use short sentences.</strong> Active voice. Simple vocabulary. You are speaking English potentially to people who struggle with English, or who are young, or tired, or distracted. But even your more sophisticated viewers will respond better to direct messages. </li>



<li><strong>Here&#8217;s a good structure:</strong> Start with a hook (why should the viewer even look at your video and not skip?) This will be a joke, something interesting, a problem they may be facing, an engaging meme or video clip, a face, something. This must be done in the first 2 or 3 seconds of the video. Then, take a couple of seconds to introduce the name of the product. Then, in the body of your message, showcase the product, illustrating exactly three (3) main points about it. Three is a good number. Then repeat the name of the product. Repetition is key. Finally, close by showing the product again, (remember, repetition is key), and add your call to action. What do you want the customer to do? Go to your website? Download the app? Contact you? Where? Make it easy and direct. Add all the recognizing features of your product there. Branding (colors, fonts, tagline, icon, any graphics) should all be prominent in the last scene. If you are offering something for free, such as in a freemium business model, make sure to make FREE the last word, because that&#8217;s what people will remember.</li>



<li><strong>There is going to be a voice talking.</strong> It&#8217;s better if it&#8217;s a female voice. Females sound attractive to your male audience, and appear less threatening to your female audience. Nobody wants to listen to a guy mansplaining your product.</li>



<li>Voice is not enough. Many people have their phone&#8217;s volume on mute. <strong>Subtitles must be available.</strong> Every. Single. Word. Is. Its. Own. Separate. Subtitle. Subtitles must be near the center of the screen, but maybe shifted slightly to the bottom or top so as to leave space for you to showcase the product. Text must be in a heavy sans-serif fond, with bright color, and with a thin dark border. You want the text to be readable on all kinds of background. If the platform supports it, also add the subtitles as text, even though you&#8217;ve hard-coded them into the video. I heard people like subtitles, so I add subtitles to my subtitles.</li>



<li>All platforms let you <strong>add some text to your video</strong>. Again, keep it short and to the point. Preferably the text should not deviate too much from the transcript of the video, which is your main message. Repeat your message there in the same template. Repetition is key.</li>



<li>You can <strong>use memes to make your message more engaging</strong>. Memes are not copyrighted, but are easily recognizable. Adding an element of humor to your ad can go much further than spending a big budget on a marketing campaign.</li>



<li>Graphics and animations are what make your video look professional. You can get graphics from stock image repositories. Be mindful of any copyrights. </li>



<li>There is <a href="https://en.wikipedia.org/wiki/Color_psychology#Uses_in_marketing" target="_blank" rel="noreferrer noopener"><strong>theory behind colors</strong></a>. Blue and white conveys seriousness, etc. Same for fonts, design, etc. Know when to use serif versus sans serif fonts. You don&#8217;t have to be a graphics designer, but you should take some time to study the basics. Be aware of <a href="https://en.wikipedia.org/wiki/Visual_hierarchy" target="_blank" rel="noreferrer noopener">visual hierarchy</a>. Learn these things, and you&#8217;ll never have to talk to a &#8220;creative&#8221; person again in your life. Big plus!</li>



<li>You should likely <strong>add some background music at low volume</strong>. The music should not overpower the speech or distract from your message. Viewers must subconsciously associate your product with positive, upbeat feelings. The music must be upbeat, and must strike a stark contrast to their otherwise miserable depressed life. The music must also be somewhat repetitive, so as not to distract from your messaging. Bonus points if it has a hypnotic element (ads work best by hypnotizing the logical faculties and appealing to emotion). The music must appeal to a wide array of musical tastes. <strong>Acoustic guitar riffs are the cornerstone of advertising</strong>. You can find royalty free music on <a href="https://pixabay.com/" target="_blank" rel="noreferrer noopener">pixabay</a>. Be mindful of any usage rights. Sometimes the artist has submitted the music to a copyright ID platform, and you may have to request and submit usage rights. But things are simpler if you find music that is completely free to use.</li>
</ul>



<h2 class="wp-block-heading">Spam your ad to all the platforms that support video shorts</h2>



<p>Video costs a lot of time and effort to make. At my current skill level, one video short takes about a day of work. But once you invest this cost, now your finished video can be thought of as capital that you own. You can show repeatedly, for little marginal cost, on all the platforms where people typically go to dump their garbage or to watch other people&#8217;s trash:</p>



<ul class="wp-block-list">
<li>YouTube</li>



<li>Instagram</li>



<li>Pinterest</li>



<li>Tumblr</li>



<li>TikTok</li>



<li>Triller</li>



<li>Snapchat</li>



<li>Twitter</li>



<li>LinkedIn</li>
</ul>



<h2 class="wp-block-heading">Only losers pay for video editing</h2>



<p>Before you go and pirate Adobe Premier from a torrent site, or pay some dude on Fiverr to make you a video, consider this: You can do it with open source software for zero cost, other than your time. And it&#8217;s a useful skill that you want to learn yourself, not delegate to some marketing-agency-dwelling type of creep. (Sorry, sorry, I promised I&#8217;m done with hating on marketing. I will be on my best behavior from now on.)</p>



<p>I was under the impression that <a href="https://en.wikipedia.org/wiki/OpenShot" target="_blank" rel="noreferrer noopener">OpenShot</a> was somewhat limited and buggy. That&#8217;s because I used to install it via the Ubuntu repositories on my machine. For some reason, the repository has an ancient 2.x version of OpenShot which is very buggy. It crashes often, and produces nasty sound artifacts. This is not the case with the latest version of OpenShot. You should take a minute to <a href="https://www.openshot.org/ppa/" target="_blank" rel="noreferrer noopener">add their official PPA</a> and install the latest stable version from there.</p>



<p>Learning OpenShot is a breeze. It&#8217;s popular, so there are countless tutorials on Youtube. Focus on learning how to do animations by adding keyframes to your media. I will discuss here only two technical aspects that I found required a little bit of digging: How to create video shorts and how to add subtitles.</p>



<h3 class="wp-block-heading">Shorts custom video profile</h3>



<p>When you start a video project, you want to define the output video format. There are several templates available, but none corresponds directly to the specifications that we want for a video short. We are going to create a custom profile. Create the following file: <code>~/.openshot_qt/profiles/shorts</code> with the following content:</p>



<pre class="wp-block-code"><code>description=Short Vertical Video
frame_rate_num=24000
frame_rate_den=1000
width=1080
height=1920
progressive=1
sample_aspect_num=1
sample_aspect_den=1
display_aspect_num=9
display_aspect_den=16</code></pre>



<p>If you&#8217;re wondering what these mean, you can have a look at the <a href="https://www.openshot.org/static/files/user-guide/profiles.html#custom-profile" target="_blank" rel="noreferrer noopener">documentation for custom profiles</a>.</p>



<p>Now you can start a new project with the &#8220;Short Vertical Video&#8221; profile.</p>



<h3 class="wp-block-heading">Creating subs, the DIY way</h3>



<p>There are, of course, a multitude of speech-to-text AI tools that will create subtitles for you automatically. One that works well is <a href="https://submagic.co/?via=alexandros80">SubMagic</a>. It offers many features including colors and emojis, and is well suited for social media videos.</p>



<p>But if your video is not too long, you can do it all by yourself. The advantage is that you don&#8217;t pay any money, and you are certain that there are no mistakes. You also have full control on where to break your sentences.</p>



<p>The best way to manually create subs is, surprisingly, not using a tool such as <a data-type="link" data-id="https://aegisub.org/" href="https://aegisub.org/" target="_blank" rel="noreferrer noopener">AegisSub</a>, although it too can work well. I&#8217;ve found that the best tool for the job is <a href="https://www.audacityteam.org/" target="_blank" rel="noreferrer noopener">Audacity</a>. Here&#8217;s how you do it:</p>



<ol class="wp-block-list">
<li>First, render the audio of your final video.</li>



<li>Load your audio in Audacity.</li>



<li>Go to <em>Tracks</em> → <em>Add New</em> → <em>Label Track</em>.</li>



<li>Select the first word. You can press <em>Shift</em>+<em>Space</em> to listen to your selection on repeat and ensure that you&#8217;ve selected the whole word and nothing more.</li>



<li>Press Ctrl+B to add a label.</li>



<li>In the box, type the word. In CAPITAL letters. This is for the TikTok generation. Don&#8217;t use punctuation unless absolutely necessary.</li>



<li>Deselect your selection, and select the next word. Audacity will help you by snapping the beginning of your selection to the end of the label you just entered.</li>



<li>Repeat steps 4 to 7 until you have done the entire length of the track. It gets easier as you learn how each phoneme looks like as a wave on the screen. For a 30 to 45 second video, it&#8217;s very doable.</li>



<li>Save your project as an <code>.aup</code> file, because you may want to go back to it and make changes, especially if you made a mistake.</li>



<li>Go to <em>File</em> → <em>Export</em> → <em>Export labels</em>. Save your labels to a text file. This file is not yet in a format that can be imported into OpenShot.</li>



<li>Go to <a href="https://magcius.github.io/audaciter/" target="_blank" rel="noreferrer noopener">https://magcius.github.io/audaciter/</a> and use the tool to convert your labels file to a <a href="https://en.wikipedia.org/wiki/SubRip" data-type="link" data-id="https://en.wikipedia.org/wiki/SubRip" target="_blank" rel="noreferrer noopener">SubRip (<code>.srt</code>)</a> file. SubRip is <em>almost</em> the format that we want to use in OpenShot, but not quite.</li>



<li>In SubRip, each entry starts off with a line that is the numerical index of the entry, then another line with the time range (two timestamps separated by <code>--&gt;</code>), and finally one or more lines of text, followed by a blank line. OpenShot doesn&#8217;t like the line with the numerical index, so we remove it from our <code>.srt</code> file. You can replace the following regular expression that matches these lines, with the empty string: <code>^\d+$</code></li>



<li>Go to your video project in OpenShot. With a finished video, you will have several tracks full of small fragments of video clips, audio clips, and graphics being animated. You don&#8217;t want to attach your subs to any of that mess. Create a separate track for your subs.</li>



<li>In the new track, add a 1080&#215;1920 PNG image that is completely transparent. <a href="https://www.alexgeorgiou.gr/wp-content/uploads/2024/03/transparency.png">Here&#8217;s one.</a></li>



<li>Stretch the image so it &#8220;appears&#8221; over the entire length of the video. (Of course, if your image is truly transparent, it won&#8217;t actually appear.)</li>



<li>Ensure that your cursor is positioned at the beginning of your track. (Use <em>Ctrl</em>+← to navigate to the beginning). If you don&#8217;t do this, when you later edit the Caption properties you will create a new keyframe, and you don&#8217;t want that. You want the properties that you&#8217;re about to enter to apply to the entire track.</li>



<li>Go to the <em>Effects</em> tab, and drag the <em>Caption</em> effect onto the transparent image in your subtitles track.</li>



<li>The letter <em>C</em> will appear on the track. Click on it to edit the properties of the <em>Captions</em> effect.</li>



<li>I will share here the settings that I like to use in my videos. You can experiment with other settings of course. Font: <code>Arial Black Bold 100pt</code> (or any heavy font). Font size: <code>65</code>. Font alpha: <code>255</code>. Font color: <code>Yellow #FFFF00</code>. Stroke width: <code>3</code>. Border: <code>black #000000</code>. Left size: <code>0</code>. Right size: <code>0</code>. Top size: <code>0.75</code>.</li>



<li>Now go to the right side of the screen, and paste the SubRip subs (with the index lines removed).</li>



<li>Watch the video to ensure that all the subs are shown. If a line is too wide, it will not be shown. So check the lines with the most text. If a line is not shown, either break it into two, or reduce the font size. Your lines should be individual words, so they should not be too long.</li>
</ol>



<p>That&#8217;s it. You can now render your ad and spam-post it on all the aforementioned platforms. Add it into your articles, <a data-type="link" data-id="https://yoast.com/on-page-video-seo/" href="https://yoast.com/on-page-video-seo/">above the fold</a>. Then, post links to your video shorts again and again. Repetition is key.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/engaging-video-shorts-with-subtitles/">🎞 Engaging video shorts with subtitles, the open source way</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/engaging-video-shorts-with-subtitles/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>📦 Two dockerized WordPress sites, with Let&#8217;s Encrypt, logging, SMTP relay, controlled by a systemd service, and daily backups</title>
		<link>https://www.alexgeorgiou.gr/two-dockerized-wordpress-sites/</link>
					<comments>https://www.alexgeorgiou.gr/two-dockerized-wordpress-sites/#respond</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Wed, 20 Dec 2023 10:24:43 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[backup]]></category>
		<category><![CDATA[compose]]></category>
		<category><![CDATA[cron]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[digitalocean]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[letsencrypt]]></category>
		<category><![CDATA[mysqldump]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[reverse proxy]]></category>
		<category><![CDATA[SMTP]]></category>
		<category><![CDATA[SQL]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=1311</guid>

					<description><![CDATA[<p>Or, How I learned to stop worrying and love docker compose.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/two-dockerized-wordpress-sites/">📦 Two dockerized WordPress sites, with Let&#8217;s Encrypt, logging, SMTP relay, controlled by a systemd service, and daily backups</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>In this article I&#8217;m going to talk about how I set up two WordPress sites on one server. None of the articles I could come up with covered all the topics I was interested in. Not exactly groundbreaking, in fact it sounds simple. But the devil is in the details. To actually perform such a setup for the first time is actually pretty daunting. From setting up the DNS records to getting file permissions to work, to getting the reverse proxy right, it&#8217;s all a complicated mess that I&#8217;m going to delineate for you (and me) here, while it&#8217;s still fresh in my head.</p>



<h1 class="wp-block-heading">Features</h1>



<ul class="wp-block-list">
<li>Two WordPress sites: <code>https://www.example1.com</code> and <code>https://www.example2.com</code>.</li>



<li>Redirects from <code>https://example1.com</code>, <code>http://example1.com</code>, <code>http://www.example1.com</code> to <code>https://example1.com</code> (and the same for <code>example2.com</code>).</li>



<li>Let&#8217;s encrypt certificates.</li>



<li>WordPress debug logging with logrotate. I have ranted previously about <a href="https://www.alexgeorgiou.gr/wordpress-production-to-log-or-not-to-log/">why I think having debug logging turned on is important on live sites</a>.</li>



<li>Emails must work in WordPress.</li>



<li>The containers must run as a service, so that they start with system start, and exit gracefully on system shutdown.</li>



<li>Daily backups of all WordPress files and MySQL databases.</li>



<li>Networks of the two sites should be isolated for security.</li>
</ul>



<h1 class="wp-block-heading">Shameless plug of my referral links</h1>



<p>We start with a hosted server. This can be a dedicated server or a server slice. Hosting providers that I like are:</p>



<ul class="wp-block-list">
<li><a href="https://www.digitalocean.com/?refcode=44d4d2184573">DigitalOcean</a> &#8211; Using this link you get a $200, 60-day credit to try their products. If you spend $25 after your credit expires, I will get $25 in credit.</li>



<li><a href="https://hostinger.com/?REFERRALCODE=1ALEXANDROS15">Hostinger</a> &#8211; You don&#8217;t get anything with this link, except for a great hosting service. Again, I get a commission from this link if you stick with Hostinger for 45 days. Think of it as my reward for writing such a great article for you.</li>
</ul>



<p>I have a Debian droplet on DigitalOcean with 2GB of RAM, but with some tweaking it&#8217;s possible to squeeze two low-traffic WordPress sites in 1GB, if you really need to keep the monthly costs down.</p>



<h1 class="wp-block-heading">First let&#8217;s get the (DNS) record straight</h1>



<p>The first order of business is to setup the DNS records. We&#8217;re going to need two <code>A</code> records to point to our server&#8217;s IP, and two <code>CNAME</code> records that will be <code>wwww.</code> aliases of the bare domain. Oh, and we&#8217;ll need some <code>NS</code> records to point to the domain name provider (in this case Digital Ocean).</p>



<figure class="wp-block-table"><table><thead><tr><th>Type</th><th>Hostname</th><th>Value</th><th>TTL</th></tr></thead><tbody><tr><td><code>A</code></td><td><code>example1.com</code></td><td>(my server&#8217;s IP)</td><td>1800</td></tr><tr><td><code>A</code></td><td><code>example2.com</code></td><td>(my server&#8217;s IP)</td><td>1800</td></tr><tr><td><code>CNAME</code></td><td><code>www.example1.com</code></td><td>alias of <code>example1.com.</code></td><td>1800</td></tr><tr><td><code>CNAME</code></td><td><code>www.example2.com</code></td><td>alias of <code>example2.com.</code></td><td>1800</td></tr><tr><td><code>NS</code></td><td><code>example1.com</code></td><td><code>ns1.digitalocean.com</code></td><td>14400</td></tr><tr><td><code>NS</code></td><td><code>example1.com</code></td><td><code>ns2.digitalocean.com</code></td><td>14400</td></tr><tr><td><code>NS</code></td><td><code>example1.com</code></td><td><code>ns3.digitalocean.com</code></td><td>14400</td></tr><tr><td><code>NS</code></td><td><code>example2.com</code></td><td><code>ns1.digitalocean.com</code></td><td>14400</td></tr><tr><td><code>NS</code></td><td><code>example2.com</code></td><td><code>ns2.digitalocean.com</code></td><td>14400</td></tr><tr><td><code>NS</code></td><td><code>example2.com</code></td><td><code>ns3.digitalocean.com</code></td><td>14400</td></tr></tbody></table></figure>



<p>I like to keep the TTL (Time-To-Live) values low until I&#8217;m finished with my setup. I&#8217;ve set everything to <code>1800</code> seconds which is half an hour. Once I&#8217;m sure that everything is OK, I can increase the values to something larger like <code>14400</code> (four hours).</p>



<h1 class="wp-block-heading">ssh</h1>



<p>We are going to need to be able to login to the server with a passwordless setup.</p>



<p>Login as root to the new server via the admin console.</p>



<p>Create a regular user with <code>adduser</code>:</p>



<pre class="wp-block-code"><code><code>a<span style="background-color: initial; font-family: inherit; font-size: inherit; color: initial;">dduser yourusername</span></code></code></pre>



<p>Then add the user to sudoers with:</p>



<pre class="wp-block-code"><code><code>usermod -aG sudo yourusername</code></code></pre>



<p>(Replace <code>yourusername</code> with your username.)</p>



<p>Once we are on our local machine, we check if we already have an ssh key with:</p>



<pre class="wp-block-code"><code><code>ls -al ~/.ssh/id_*.pub</code></code></pre>



<p>If we don&#8217;t have any, we can generate one with:</p>



<pre class="wp-block-code"><code><code>ssh-keygen -t rsa -b 4096 -C "your_email@domain.com"</code></code></pre>



<p>Once we are sure that there is a key, we upload it to the new server with:</p>



<pre class="wp-block-code"><code><code>ssh-copy-id yourusername@server_ip_address</code></code></pre>



<p>(Again replace <code>yourusername</code> with your remote username, and <code>server_ip_address</code> with your ip address. You will need to enter the password you entered in <code>adduser</code>.)</p>



<h1 class="wp-block-heading">Docker compose</h1>



<p>First, let&#8217;s install docker on the server by following the <a href="https://docs.docker.com/engine/install/debian/" target="_blank" rel="noreferrer noopener">installation instructions for Debian</a>. I am not going to repeat the instructions here. If you have chosen a different distro, follow the respective instructions.</p>



<p>We are going to create a <code>docker-compose.yml</code> file. This file describes how the different docker containers are orchestrated.</p>



<p>We are going to need four containers:</p>



<ul class="wp-block-list">
<li>Two databases for the two sites.</li>



<li>Two WordPress installations.</li>
</ul>



<p>I&#8217;m first going to show some simple compose configs with the basics, then we are going to add the bells and whistles. Here goes:</p>



<h2 class="wp-block-heading">Two databases, sitting in a server</h2>



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

name: droplet

networks:
    net1:
    net2:

volumes:
  db1volume:
  db2volume:

services:

  db1:
    image: mysql:8.2.0
    networks:
      - net1
    restart: unless-stopped
    expose:
      - "3306"
    volumes:
      - db1volume:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: wp1_root_pass
      MYSQL_DATABASE: wp_db1
      MYSQL_USER: db1_user
      MYSQL_PASSWORD: db1_pass

  db2:
    image: mysql:8.2.0
    networks:
      - net2
    restart: unless-stopped
    expose:
      - "3306"
    volumes:
      - db2volume:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: wp2_root_pass
      MYSQL_DATABASE: wp_db2
      MYSQL_USER: db2_user
      MYSQL_PASSWORD: db2_pass</code></pre>



<p>There&#8217;s already a lot going on here:</p>



<ul class="wp-block-list">
<li>We are defining our composition to have a name. Here I am using <code>droplet</code>. This will also be the prefix for the names of all the containers.</li>



<li>We are defining two networks, <code>net1</code> and <code>net2</code>. Only containers on the same network can talk to each other. We don&#8217;t want our <code>example1.com</code> WordPress to have any access to the MySQL database of <code>example2.com</code>.</li>



<li>Next we are defining two identical <code>mysql:8.2.0</code> containers, named <code>db1</code> and <code>db2</code>.</li>



<li>Each of the two databases is put in its respective network (<code>net1</code> and <code>net2</code>).</li>



<li>We want a database that has crashed to restart, unless we explicitly stop it.</li>



<li>We are going to let the databases listen to TCP port <code>3306</code>. This is the port where WordPress will connect. All other ports are firewalled.</li>



<li>We are going to mount the <code>/var/lib/mysql</code> directories into docker volumes named <code>db1volume</code> and <code>db2volume</code>.</li>



<li>Next we are going to use some environment variables that the startup script inside the mysql image recognizes. These will set up a root password, a new empty database, and a username/password pair that WordPress will use to access this new database. The startup script will do all the <code>CREATE DATABASE</code>, <code>CREATE USER</code> and <code>GRANT</code> magic for us. You can learn more about the MySQL docker image <a href="https://dev.mysql.com/doc/mysql-installation-excerpt/8.2/en/docker-mysql-more-topics.html">here</a>.</li>
</ul>



<h2 class="wp-block-heading">A tale of two WordPresses</h2>



<p>Next, let&#8217;s also add the two WordPress services (these also go under the services section along with the databases):</p>



<pre class="wp-block-code"><code>  wp1:
    image: wordpress:latest
    networks:
      - net1
    depends_on:
      - db1
    user: 1000:1000
    restart: unless-stopped
    expose:
      - "80"
    volumes:
      - ./wp1fs:/var/www/html
    ports:
      - "127.0.0.1:8101:80"
    environment:
      WORDPRESS_DB_HOST: db1:3306
      WORDPRESS_DB_NAME: wp_db1
      WORDPRESS_DB_USER: db1_user
      WORDPRESS_DB_PASSWORD: db1_pass
      WORDPRESS_DEBUG: true

  wp2:
    image: wordpress:latest
    networks:
      - net2
    depends_on:
      - db1
    restart: unless-stopped
    expose:
      - "80"
    volumes:
      - ./wp2fs:/var/www/html
    ports:
      - "127.0.0.1:8102:80"
    environment:
      WORDPRESS_DB_HOST: db2:3306
      WORDPRESS_DB_NAME: wp_db2
      WORDPRESS_DB_USER: db2_user
      WORDPRESS_DB_PASSWORD: db2_pass
      WORDPRESS_DEBUG: true</code></pre>



<ul class="wp-block-list">
<li>We have named the two WordPress containers <code>wp1</code> and <code>wp2</code> and assigned them to our two networks, <code>net1</code> and <code>net2</code>.</li>



<li>We have defined that these <em>depend</em> on their respective databases to function.</li>



<li>We have defined that these containers are to be <em>restarted</em> if they crash, but not if we explicitly stop them.</li>



<li>We are exposing only HTTP port <code>80</code> to the networks. All other ports are firewalled. We are not exposing port <code>443</code> here. TLS encryption will be done at the host level that will run the reverse proxy (see below).</li>



<li>We are mounting two local directories here <code>./wp1fs</code> and <code>./wp2fs</code>. These will contain the WordPress installations. The first time that the containers run, WordPress will be installed in them. A special <code>wp-config.php</code> file will be placed in there. This file pulls the DB connection settings from the environment variables that we specify below.</li>



<li>We are port-mapping the HTTP <code>80</code> ports to the host&#8217;s ports <code>8101</code> and <code>8102</code>. These are the ports that the reverse proxy will use. They are bound to the loopback network (<code>127.0.0.1</code>), and are therefore not exposed to the outside world. If we had used just <code>8101:80</code>, this would map port 80 of the container to port <code>8101</code> of the host on all network interfaces, including the one facing the outside world. This is not ideal. We only want access to our services through our reverse proxy.</li>



<li>The <code>WORDPRESS_*</code> environment variables are specific to this wordpress image. We specify the databases, the login credentials that we also specified above, and we turn on debug logging. To learn more about these environment variables, click <a href="https://github.com/docker/awesome-compose/tree/master/official-documentation-samples/wordpress/" target="_blank" rel="noreferrer noopener">here</a>.</li>
</ul>



<p><em>NOTE: I have made the decision here to put the databases into system volumes (these live usually in <code>/var/lib/docker/volumes</code> and can be shared between containers, the WordPress filesystems are mounted in local directories which I call <code>wp1volume</code> and <code>wp2volume</code>. If you prefer to have all volumes unde <code>/var/lib</code>, you can delete the <code>./</code> prefix in front of the volume names.</em></p>



<h2 class="wp-block-heading">The bells and whistles</h2>



<p>If you thought that&#8217;s enough, <strong>you are gravely mistaken</strong>. Here&#8217;s a few more things to take care of:</p>



<h3 class="wp-block-heading">Database collation</h3>



<p>We are going to set the databases a UTF-8 multibyte collation for unicode support. Under the environment variables in the database services, we are going to add an explicit mysqld command:</p>



<pre class="wp-block-code"><code>command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci</code></pre>



<p>And under the WordPress services, we are going to add the following environment variable:</p>



<pre class="wp-block-code"><code>  WORDPRESS_DB_COLLATE: utf8mb4_unicode_ci</code></pre>



<h2 class="wp-block-heading">File permissions</h2>



<p>If we run the above containers, WordPress won&#8217;t be able to install or remove any themes or plugins, and it won&#8217;t be able to do anything that requires writing to the file system.</p>



<p>This is because, in the WordPress images, the user that runs apache has a different uid and guid than the file system. The files are owned by <code>uid</code> <code>1000</code> and <code>guid</code> <code>1000</code>. We can specify that the user running stuff inside the container has the same numeric ids. To do this, we add the following to the two WordPress services:</p>



<pre class="wp-block-code"><code>user: 1000:1000</code></pre>



<h2 class="wp-block-heading">Database memory</h2>



<p>By default, a mysql instance will take up at least 360MB of memory once it&#8217;s running. Most of it is because of the Performance Schema instruments, which take up a lot of memory.</p>



<p>The Performance Schema is a database that keeps track of the mysqld server&#8217;s performance, and is useful for diagnostics. If you are not going to use this feature, then you can turn it off. The memory usage of each DB container will then fall to a little over 100MB.</p>



<p>We are going to create a file named <code>disable-perf-schema.cnf</code> with the following contents:</p>



<pre class="wp-block-code"><code>&#91;mysqld]
performance_schema = OFF</code></pre>



<p>This will be added to the mysql server&#8217;s config files. The server includes any <code>.cnf</code> files in the <code>/etc/mysql/conf.d</code> directory into its configuration. We can use the volumes section to map this file into our two db containers:</p>



<pre class="wp-block-code"><code>volumes:
  - db1:/var/lib/mysql
  - ./disable-perf-schema.cnf:/etc/mysql/conf.d/disable-perf-schema.cnf

volumes:
  - db2:/var/lib/mysql
  - ./disable-perf-schema.cnf:/etc/mysql/conf.d/disable-perf-schema.cnf</code></pre>



<p>There are more hacks to reduce the memory usage of mysqld, but these are beyond the scope of this article. For example, you can look into reducing the InnoDB buffer pool size.</p>



<h2 class="wp-block-heading">Log rotate</h2>



<p>We have enabled debug logging, because <a href="https://www.alexgeorgiou.gr/wordpress-production-to-log-or-not-to-log/">reasons</a>. This is cool, but the <code>/var/www/html/wp-content/debug.log</code> files will eventually fill up our containers if left unchecked. Enter <code>logrotate</code> to the rescue:</p>



<p>We are going to create a file named <code>wordpress.logrotate</code> with the following content:</p>



<pre class="wp-block-code"><code>/var/www/html/wp-content/debug.log
{
        su 1000 1000
        rotate 24
        copytruncate
        weekly
        missingok
        notifempty
        compress
}</code></pre>



<p>This will gzip old logs daily and will delete even older logs. If you are not sure about the details, ChatGPT and Bard can explain exactly what each line does.</p>



<p>Note how we use again the <code>uid</code> and <code>guid</code> of the WordPress image.</p>



<p>Let&#8217;s mount this file into our WordPress containers, by adding a line to their volume clause:</p>



<pre class="wp-block-code"><code>volumes:
  - ./wp1fs:/var/www/html
  - ./wordpress.logrotate:/etc/logrotate.d/wordpress

volumes:
  - ./wp2fs:/var/www/html
  - ./wordpress.logrotate:/etc/logrotate.d/wordpress</code></pre>



<h1 class="wp-block-heading">Docker compose recap</h1>



<p>We now have the following <code>docker-compose.yml</code> file:</p>



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

name: droplet

networks:
    net1:
    net2:

volumes:
  db1volume:
  db2volume:

services:

  db1:
    image: mysql:8.2.0
    networks:
      - net1
    restart: unless-stopped
    expose:
      - "3306"
    volumes:
      - db1volume:/var/lib/mysql
      - ./disable-perf-schema.cnf:/etc/mysql/conf.d/disable-perf-schema.cnf
    environment:
      MYSQL_ROOT_PASSWORD: wp1_root_pass
      MYSQL_DATABASE: wp_db1
      MYSQL_USER: db1_user
      MYSQL_PASSWORD: db1_pass
    command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --performance-schema-instrument='%=OFF' --innodb-buffer-pool-size=32M"

  db2:
    image: mysql:8.2.0
    networks:
      - net2
    restart: unless-stopped
    expose:
      - "3306"
    volumes:
      - db2volume:/var/lib/mysql
      - ./disable-perf-schema.cnf:/etc/mysql/conf.d/disable-perf-schema.cnf
    environment:
      MYSQL_ROOT_PASSWORD: wp2_root_pass
      MYSQL_DATABASE: wp_db2
      MYSQL_USER: db2_user
      MYSQL_PASSWORD: db2_pass
    command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --performance-schema-instrument='%=OFF' --innodb-buffer-pool-size=32M"

  wp1:
    image: wordpress:latest
    networks:
      - net1
    depends_on:
      - db1
    user: 1000:1000
    restart: unless-stopped
    expose:
      - "80"
    volumes:
      - ./wp1fs:/var/www/html
      - ./wordpress.logrotate:/etc/logrotate.d/wordpress
    ports:
      - "8101:80"
    environment:
      WORDPRESS_DB_HOST: db1:3306
      WORDPRESS_DB_NAME: wp_db1
      WORDPRESS_DB_USER: db1_user
      WORDPRESS_DB_PASSWORD: db1_pass
      WORDPRESS_DB_COLLATE: utf8mb4_unicode_ci
      WORDPRESS_DEBUG: true

  wp2:
    image: wordpress:latest
    networks:
      - net2
    depends_on:
      - db1
    user: 1000:1000
    restart: unless-stopped
    expose:
      - "80"
    volumes:
      - ./wp2fs:/var/www/html
      - ./wordpress.logrotate:/etc/logrotate.d/wordpress
    ports:
      - "8102:80"
    environment:
      WORDPRESS_DB_HOST: db2:3306
      WORDPRESS_DB_NAME: wp_db2
      WORDPRESS_DB_USER: db2_user
      WORDPRESS_DB_PASSWORD: db2_pass
      WORDPRESS_DB_COLLATE: utf8mb4_unicode_ci
      WORDPRESS_DEBUG: true</code></pre>



<p>We can start this with <code>docker compose up</code> (we must first <code>cd</code> into the same directory as the <code>.yml</code> file).</p>



<p>We can see if it&#8217;s running with <code>docker compose ls</code>, and we can see the containers with <code>docker container ls</code>.</p>



<p>We can inspect memory usage with <code>docker stats</code>.</p>



<p>We can stop the containers with <code>docker compose down</code>.</p>



<p>If we also want to wipe the database volumes and start over, we can do <code>docker compose down -v</code> (DESTRUCTIVE!!!).</p>



<p>We can go into the shell of the first database with:</p>



<pre class="wp-block-code"><code>docker exec -it droplet-db1-1 bash</code></pre>



<p>And then, we can go into the mysql console with</p>



<pre class="wp-block-code"><code>mysql -u root -pwp1_root_pass</code></pre>



<p>We can go into the shell of the first WordPress with:</p>



<pre class="wp-block-code"><code>docker exec -it droplet-wp1-1 bash</code></pre>



<p>If we need to, we can install wp-cli using instructions from <a href="https://wp-cli.org/" target="_blank" rel="noreferrer noopener">https://wp-cli.org/</a>. The copy of <code>wp-cli</code> will not be persisted into the container across restarts. (Note: it&#8217;s possible to add special containers with <code>wp-cli</code> pre-installed, but again this is out of scope of this article. For more information, see the CLI images <a href="https://hub.docker.com/_/wordpress/">here</a>.</p>



<h1 class="wp-block-heading">DaaS (Docker-as-a-Service)</h1>



<p>We don&#8217;t want to have to issue <code>docker compose up</code> every time the server starts, and <code>docker compose down</code> every time the server stops. Let&#8217;s create a <code>systemd</code> unit, so that it runs as a service.</p>



<p>We&#8217;ll create a file named <code>/etc/systemd/system/docker-compose.service</code> with the following carefully crafted contents:</p>



<pre class="wp-block-code"><code>&#91;Unit]
Description=A bunch of containers
After=docker.service
Requires=docker.service

&#91;Service]
Type=oneshot
RemainAfterExit=yes
User=yourusername
ExecStart=/bin/bash -c "docker compose -f /home/yourusername/docker-compose.yml up --detach"
ExecStop=/bin/bash -c "docker compose -f /home/yourusername/docker-compose.yml stop"

&#91;Install]
WantedBy=multi-user.target</code></pre>



<ul class="wp-block-list">
<li>Replace <code>yourusername</code> with your username (duh!).</li>



<li>Replace the description with something less silly (optional).</li>



<li>Note how we only start this service <em>after</em> the docker service starts.</li>



<li>Note that we do a <code>--detach</code>. This will start the containers in the background and exit, without showing the logs of all the containers in the standard output.</li>
</ul>



<p>We can now start the service with</p>



<pre class="wp-block-code"><code>sudo service docker-compose up</code></pre>



<p>And stop it with</p>



<pre class="wp-block-code"><code>sudo service docker-compose down</code></pre>



<p>If we want to see the logs of all the containers, we can type</p>



<pre class="wp-block-code"><code>docker compose logs -f</code></pre>



<p>We should now be able to do <code>curl http://127.0.0.1:8101</code> and see the HTML of the front page of the first WordPress.</p>



<h1 class="wp-block-heading">The reverse proxy</h1>



<p>The database and WordPress containers are running, but they are not yet exposed to the outside world. To do this, we are going to use <code>nginx</code> as a reverse proxy.</p>



<p>The reverse proxy will:</p>



<ul class="wp-block-list">
<li>handle all the redirects that we need</li>



<li>expose the apache2 servers to the outside world</li>



<li>handle the TLS encryption</li>
</ul>



<p>First we setup <a href="https://letsencrypt.org/">Let&#8217;s Encrypt</a>. How to do this is beyond the scope of this article. You can look <a href="https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/">here</a> for a good introduction.</p>



<p>The bottom line is that <code>certbot</code> must be installed, and the following public and private certificate files must exist on your server (host):</p>



<pre class="wp-block-code"><code>/etc/letsencrypt/live/example1.com/fullchain.pem
/etc/letsencrypt/live/example1.com/privkey.pem
/etc/letsencrypt/live/example2.com/fullchain.pem
/etc/letsencrypt/live/example2.com/privkey.pem</code></pre>



<p>These files are actually symlinks to the latest certificate issued. This is all handled by certbot.</p>



<p>Let&#8217;s start to create an nginx config file, which we will place in <code>/etc/nginx/sites-available/reverse-proxy.conf</code>.</p>



<p>We are going to enter several server <em>stanzas</em>, remembering that nginx will use the first one that matches in order from top to bottom.</p>



<h2 class="wp-block-heading">Redirects from http to https</h2>



<p>First, we want any unencrypted requests to port <code>80</code> to do a soft redirect to our <code>https://www.</code> sites.</p>



<pre class="wp-block-code"><code>server {
    listen       80;
    listen       &#91;::]:80;
    server_name example1.com;
    return 302 https://www.example1.com$request_uri;
}

server {
    listen       80;
    listen       &#91;::]:80;
    server_name example2.com;
    return 302 https://www.example2.com$request_uri;
}</code></pre>



<p>The first listen statement is for IPv4, and the second is for IPv6. We redirect to the TLS site, preserving the path segment of the request URI.</p>



<h2 class="wp-block-heading">Proxy forwarding</h2>



<p>Next we are going to enter the stanza that handles the actual site content:</p>



<pre class="wp-block-code"><code>server {
    listen      443 ssl;
    listen      &#91;::]:443 ssl;
    server_name www.example1.com;

    ssl_certificate /etc/letsencrypt/live/example1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example1.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    location / {
        proxy_pass http://127.0.0.1:8101/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen      443 ssl;
    listen      &#91;::]:443 ssl;
    server_name www.example2.com;

    ssl_certificate /etc/letsencrypt/live/example2.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example2.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    location / {
        proxy_pass http://127.0.0.1:8102/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}</code></pre>



<p>Again, we are listening for <code>443</code> (the TLS port) on both IPv4 and IPv6.</p>



<p>Notice how we only listen for requests to the <code>www.</code> subdomain here.</p>



<p>We use the TLS certificates first, then we specify the reverse proxy in the <code>location /</code> section.</p>



<p>We forward each site to the correct port that we exposed with docker (<code>8101</code> and <code>8102</code> in this case).</p>



<p>We also set some <code>X-</code> headers. This is so that the PHP server knows some details about the client.</p>



<h2 class="wp-block-heading">Redirects from all subdomains to www</h2>



<p>Finally, we want requests from <code>https://example1.com</code>, or from ay other subdomain, such as <code>https://foo.example1.com</code>, to redirect to our <code>www.</code> subdomain:</p>



<pre class="wp-block-code"><code>server {
    listen 443 ssl;
    listen &#91;::]:443 ssl;
    server_name .example1.com;

    ssl_certificate /etc/letsencrypt/live/example1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example1.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    location / {
        rewrite ^ https://www.example1.com permanent;
    }
}

server {
    listen 443 ssl;
    listen &#91;::]:443 ssl;
    server_name .example2.com;

    ssl_certificate /etc/letsencrypt/live/example2.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example2.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    location / {
        rewrite ^ https://www.example2.com permanent;
    }
}</code></pre>



<p>Here we listen for any subdomain. Note the dot (<code>.</code>) prefix in the <code>server_name</code>.</p>



<p>We again use the TLS certificates, but this time we perform a redirect to the <code>wwww.</code> subdomain.</p>



<h2 class="wp-block-heading">Administering our reverse proxy</h2>



<p>When we are ready to enable our reverse proxy, we will create a symlink to <code>sites-enabled</code>:</p>



<pre class="wp-block-code"><code>sudo ln -s /etc/nginx/sites-available/reverse-proxy.conf /etc/nginx/sites-enabled/reverse-proxy.conf</code></pre>



<p>We can test our syntax to see that it is correct with:</p>



<pre class="wp-block-code"><code>sudo nginx -t</code></pre>



<p>And finally we can restart the nginx server with:</p>



<pre class="wp-block-code"><code>sudo service nginx restart</code></pre>



<p>We can check the status of the server with:</p>



<pre class="wp-block-code"><code>sudo service nginx status</code></pre>



<p>If everything is working correctly, and if the DNS records have had time to propagate, then we can visit our sites and run the famous WordPress installation process:</p>



<ul class="wp-block-list">
<li>https://www.example1.com/</li>



<li>https://www.example2.com/</li>
</ul>



<h1 class="wp-block-heading">Emails</h1>



<p><strong>If only the above was enough.</strong> Sadly, our WordPress installations need a way to send emails, otherwise the webmaster experience is going to suck big time.</p>



<p>I say sadly, because setting up <code>sendmail</code> first on the host is relatively easy, but then setting up SMTP proxies in the WordPress containers is not something I am familiar with. Sorry guys, in the interest of keeping things simple, I&#8217;m going to cheat a little here. Here&#8217;s what I did:</p>



<ul class="wp-block-list">
<li>Install the free <a href="https://wpmailsmtp.com/" target="_blank" rel="noreferrer noopener">WP Mail SMTP</a> plugin on both sites.</li>



<li>Create an application-specific password in my google account.</li>



<li>In the WordPress admin screens, go to: <em>WP Mail SMTP</em> → <em>Mailer</em> → <em>Other SMTP</em>.</li>



<li>Enter the following settings:
<ul class="wp-block-list">
<li>SMTP Host: <code>smtp.gmail.com</code></li>



<li>Encryption: <code>SSL</code></li>



<li>SMTP Port: <code>465</code></li>



<li>Auto TLS: <code>ON</code></li>



<li>Authentication: <code>ON</code></li>



<li>SMTP Username: (my gmail address)</li>



<li>SMTP Password: (the application specific password that I just created).</li>
</ul>
</li>



<li>Hit <em>Save Settings</em>.</li>



<li>Go to <em>WP Mail SMTP</em> → <em>Tools</em> and send a test email.</li>
</ul>



<p>If everything works, then WordPress and its plugins can now send emails. But it will only be able to send email into spam folders, until we add a <a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework" target="_blank" rel="noreferrer noopener">Sender Policy Framework (SPF)</a> record to our DNS entries:</p>



<figure class="wp-block-table"><table><thead><tr><th>Type</th><th>Hostname</th><th>Value</th><th>TTL</th></tr></thead><tbody><tr><td>TXT</td><td>example1.com</td><td>v=spf1 a mx ~all</td><td>1800</td></tr><tr><td>TXT</td><td>example2.com</td><td>v=spf1 a mx ~all</td><td>1800</td></tr></tbody></table><figcaption class="wp-element-caption"><em>Disclaimer: These DNS records are actually not related to sunscreen in any way.</em></figcaption></figure>



<p>The above TXT records tell recipients to treat all emails coming from servers pointed to by the A or MX record of your domains as safe, and others as potentially suspicious. Again, use your favorite AI chatbot to constuct an SPF record that matches your needs.</p>



<h1 class="wp-block-heading">Nothing more permanent than a 301 redirect</h1>



<p>If all works, it&#8217;s now time to turn the soft redirects into permanent (hard) redirects. Edit the reverse proxy config and change any <code>302</code> redirects to <code>301</code>. Any browsers visiting your site will cache these redirects for eternity.</p>



<p>It&#8217;s also now a good time to increase the Time-to-Live of all the DNS records to something like 4 hours, or <code>14400</code> seconds.</p>



<h1 class="wp-block-heading">Backups</h1>



<p>You would think that by now you&#8217;re finished, <strong>but you&#8217;d be wrong</strong>!</p>



<p>Any IT technician worth their <a href="https://en.wikipedia.org/wiki/Salt_(cryptography)" target="_blank" rel="noreferrer noopener">salt</a> knows that they must <a href="https://gist.github.com/nooges/817e5f4afa7be612863a7270222c36ff" target="_blank" rel="noreferrer noopener">backup, and backup often</a>.</p>



<p>First, turn off the server or droplet and take a full backup, snapshot, or whatever. Future you will thank you.</p>



<p>Then, let&#8217;s see how we can take automated daily backups. We can either pay the hosting provider every month to do this for us, or we can spend a few minutes to set up a few cron jobs. Let&#8217;s be cheap and do it manually.</p>



<p>I have a raspberry Pi at home that is always on. It does various things like take backups, ping various services and email me if they are down, trigger wp-cron URLs, control crypto miners, run services I need such as my ticket system, and in general runs any other odd 24/7 task. You should also have one such low-power system. The great thing with Raspberry Pi is that it&#8217;s easy to take out the MicroSD and gzip it into a mechanical disk, so the backup mechanism itself is nicely backed up in its entirety. (<a href="https://knowyourmeme.com/memes/xzibit-yo-dawg" target="_blank" rel="noreferrer noopener">Yo dawg, heard you like backups…</a>)</p>



<p>We&#8217;ll now use our local always-on Linux system to take daily backups of our online filesystems and databases:</p>



<h2 class="wp-block-heading">Local <code>backups.sh</code> script</h2>



<p>First, let&#8217;s create a DB user that only has enough access to take backups from both databases, but no more:</p>



<p>Login to the MySQL consoles of each database and create a <code>wp_bu</code> user that will do backups:</p>



<pre class="wp-block-code"><code>CREATE USER 'wp_bu'@'localhost' IDENTIFIED BY 'SOMESTRONGPASSWORD';
GRANT SELECT, LOCK TABLES ON wp_db1.* TO 'wp_bu'@'localhost';

CREATE USER 'wp_bu'@'localhost' IDENTIFIED BY 'SOMESTRONGPASSWORD';
GRANT SELECT, LOCK TABLES ON wp_db2.* TO 'wp_bu'@'localhost';</code></pre>



<p>We only need SELECT, but since we want to call <code>mysqldump</code> with the <code>--single-transaction</code> argument, we&#8217;ll also need to grant the <code>LOCK TABLES</code> permission. No point in having an ACID database if we&#8217;re going to take backups of an inconsistent state now, is there?</p>



<p>We&#8217;ll now create a bash shell script that does our daily backups. Let&#8217;s place it in our local backup server and call it <code>backups.sh</code>:</p>



<pre class="wp-block-code"><code>#!/bin/bash

# ensure dirs exist
mkdir -p /path-to-backups/cache/wp{1,2}volume /path-to-backups/server

# download DBs to SQL files
ssh -t server "docker exec droplet-wpdb-1 nice -n 19 mysqldump -u wp_bu -pSOMESTRONGPASSWORD --no-tablespaces --single-transaction wp_db1 | nice -n 19 gzip -9 -f" &gt;/path-to-backups/server/wp_db1-`date --rfc-3339=date`.sql.gz
ssh -t server "docker exec droplet-wpdb-2 nice -n 19 mysqldump -u wp_bu -pSOMESTRONGPASSWORD --no-tablespaces  --single-transaction wp_db2 | nice -n 19 gzip -9 -f" &gt;/path-to-backups/server/wp_db2-`date --rfc-3339=date`.sql.gz

# download wp-content files to backup cache
rsync -aq server:~/wp1fs/* /path-to-backups/cache/wp1volume
rsync -aq server:~/wp2fs/* /path-to-backups/cache/wp2volume

# Zip downloaded wp-content files
zip -r9q /path-to-backups/server/wp1-`date --rfc-3339=date`.zip /path-to-backups/cache/wp1volume -x "**/GeoLite2*" -x "**/GeoIPv6.dat"
zip -r9q /path-to-backups/server/wp2-`date --rfc-3339=date`.zip /path-to-backups/cache/wp2volume -x "**/GeoLite2*" -x "**/GeoIPv6.dat"

# prune old DB and FILE backups from local backups
cd /path-to-backups/server &amp;&amp; ls -1tr | head -n -30 | xargs -d '\n' rm -rf -</code></pre>



<p>Again, a lot goes on here. Let&#8217;s unpack:</p>



<ul class="wp-block-list">
<li>The script creates directories <code>server</code> and <code>cache</code> under <code>/path-to-backups</code>. Replace this path with something that points to the directory where you want to keep your backups.</li>



<li>We then <code>ssh</code> to the host using the <code>-t</code> argument because we are in a headless environment (cron). We issue a <code>docker exec</code> command into our databases. Notice how we do not use the <code>-it</code> arguments to <code>docker exec</code>, since this is a headless command (no TTY attached). The command is a <code>mysqldump</code> command that uses the credentials we just created to export the databases in a single transaction each. The SQL output is compressed with maximum compression (<code>-9</code>) and the binary output of <code>gzip</code> is forced (<code>-f</code>) into the standard output, which is then sent over the ssh connection. In our local backups server, we redirect this compressed stream into an <code>.sql.gz</code> file. The file name starts with <code>wp_db1-</code> and includes the current date in <code>YYYY-MM-DD</code> notation. (RFC 3339 is my idea of a perfect date, btw). The <code>--no-tablespaces</code> argument is need in MySQL <code>8.0.21</code> and later, otherwise you&#8217;ll need the PROCESS global permission. (Unless you are using tablespaces you don&#8217;t need it, hence the argument <code>--no-tablespaces</code>.) Notice that we make sure to be <code>nice</code> to other running processes because we don&#8217;t want to impact the performance of the web server with our backups. <code>19</code> is the idle CPU priority.</li>



<li>We then use <code>rsync</code> with the quiet (<code>-q</code>) and archive (<code>-a</code>) flags to copy the files of our WordPress installations into our <code>cache/wp1volume</code> and <code>cache/wp2volume</code> directories. The advantage of using rsync is that only changes to these directories will be transferred.</li>



<li>We then create a zip file for each of these directories. We name the zip files with the prefixes <code>wp1-</code> and <code>wp2-</code> followed again by our idea of a perfect date. Many WordPress plugins include a database of IPs mapped to geographical locations. These files are large and can be found online. If we don&#8217;t want to save these, we can exclude them (<code>-x</code> flag), but this is optional.</li>



<li>Finally we list the files we created (both <code>.sql.gz</code> and <code>.zip</code> files) and we only keep the last 30, deleting any older ones. Since we have two files for each of two databases, this will retain daily backups for the last week or so.</li>
</ul>



<p>Make the script executable with</p>



<pre class="wp-block-code"><code>chmod +x backups.sh</code></pre>



<p>We run the script once, and we check the <code>.sql.gz</code> files using <code>zless</code> and the zip files with <code>unzip -l</code>.</p>



<p>Once we are certain that all data is backed up by the script, we add it to the crontab. Edit the crontab with <code>crontab -e</code> and add the line:</p>



<pre class="wp-block-code"><code>20 4 * * * /bin/bash /home/yourusername/backups.sh</code></pre>



<p>This will execute the backups every day at 4:20 in the morning.</p>



<h2 class="wp-block-heading">Checking the backups</h2>



<p>The server works and is fully backed up. You would think that you&#8217;re done by now. That&#8217;s where <strong>you&#8217;d be wrong again</strong>!</p>



<p>Having backups and not checking them regularly is worse than not having backups at all: You are being lulled into a false sense of security. You may act precariously, thinking that you can always go back to the last backup. However, all backup mechanisms can fail, for any number of reasons.</p>



<p>What I do, is I&#8217;ve set up a weekly reminder in my Google calendar to check the backups. It only takes half a minute per week to ssh into my backup server and do an <code>ls -l</code>, thus ensuring that the latest backups exist, and their file size is what I&#8217;d expect. I keep old backups for about a week, hence the weekly reminder.</p>



<p>I also have another reminder every three months, to backup the MicroSD of my Raspberry Pi backup server. Once every three months, I shutdown the Pi, take out the MicroSD, put it into my work PC, and copy the entire image into a file, stored on my mechanical disk:</p>



<pre class="wp-block-code"><code>sudo dd if=/dev/sdf of=/mnt/bu/rpi-backup-`date --iso-8601=date`.img bs=4096 conv=sync,noerror status=progress
gzip -9 /mnt/bu/rpi-backup-`date --iso-8601=date`.img</code></pre>



<p>Only once I have this process setup I can sleep at night.</p>



<h1 class="wp-block-heading">Are we finished yet?</h1>



<p>By now you would think that we&#8217;re not finished yet, and that there&#8217;s more things to do. <strong>That&#8217;s where you&#8217;d be wrong!</strong></p>



<p>And for anyone wondering, <code>example1.com</code> is actually <a href="https://www.dashed-slug.net" target="_blank" rel="noreferrer noopener">https://www.dashed-slug.net</a> and <code>example2.com</code> is actually this blog, <a href="https://www.alexgeorgiou.gr" target="_blank" rel="noreferrer noopener">https://www.alexgeorgiou.gr</a>. There&#8217;s also a plain nginx container in there that serves static HTML files at <a href="https://wallets-phpdoc.dashed-slug.net" target="_blank" rel="noreferrer noopener">https://wallets-phpdoc.dashed-slug.net</a> .</p>



<p>My config is actually a little bit more complex than the one discussed above. To save some more server memory, I had to put both databases into the same MySQL container, and set up two different DB users with access restricted to each respective database. But you shouldn&#8217;t do this at home, because isolation!</p>



<p>This article is being served by the containers I discussed here, and will be backed up early tomorrow morning, via the mechanism I shared with you above. Which is pretty meta, if you think about it!</p>



<p>I never expected to compose such a long, self-contained article on containers and <code>docker compose</code>. But now it&#8217;s finished and I can hardly contain my excitement!</p>



<p>Thanks for sticking to the end. Hope you enjoyed.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/two-dockerized-wordpress-sites/">📦 Two dockerized WordPress sites, with Let&#8217;s Encrypt, logging, SMTP relay, controlled by a systemd service, and daily backups</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/two-dockerized-wordpress-sites/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>㏒ WordPress production: To log or not to log?</title>
		<link>https://www.alexgeorgiou.gr/wordpress-production-to-log-or-not-to-log/</link>
					<comments>https://www.alexgeorgiou.gr/wordpress-production-to-log-or-not-to-log/#comments</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Wed, 15 Mar 2023 11:02:28 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[debugging]]></category>
		<category><![CDATA[logging]]></category>
		<category><![CDATA[logrotate]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=1254</guid>

					<description><![CDATA[<p>Rant on why I think it's OK to keep logging enabled in live WordPress sites.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/wordpress-production-to-log-or-not-to-log/">㏒ WordPress production: To log or not to log?</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Warning: <a href="https://xkcd.com/2051/" target="_blank" rel="noreferrer noopener">Opinion</a> article ahead!</p>



<p>Like any self-respecting server-side app, WordPress also features logging. While this is usually discouraged, themes and plugins can write to this log with <a href="https://www.php.net/manual/en/function.error-log.php">error_log</a>. Any PHP warnings or errors will also be dumped to the logs. That is, if logging is enabled.</p>



<h1 class="wp-block-heading">How to enable logging: RTFM</h1>



<p>To enable logging, edit the <code>wp-config.php</code> file.</p>



<p>First, add <code>define( 'WP_DEBUG', true );</code>. This will enable &#8220;debug&#8221; logging, as explained in the article <a href="https://wordpress.org/documentation/article/debugging-in-wordpress/" target="_blank" rel="noreferrer noopener">Debugging in WordPress</a>.</p>



<p>Then, add <code>define( 'WP_DEBUG_LOG', true );</code> to make the log be written to a file, <code>/wp-content/debug.log</code>.</p>



<p>Finally, you don&#8217;t want any PHP warnings or errors to spill over into your site&#8217;s markup and cause problems. Add <code>define( 'WP_DEBUG_DISPLAY', false );</code> and <code>@ini_set( 'display_errors', 0 );</code> for good measure.</p>



<p>Great! Now you&#8217;re good to go. Debug logging has been enabled.</p>



<h1 class="wp-block-heading">&#8220;Debug&#8221; logging?</h1>



<p>But I take some issue with that. Why is logging labelled &#8220;debug&#8221; logging? Are there no other legitimate reasons to keep logs? WordPress documentation doesn&#8217;t think so:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>It is not recommended to use WP_DEBUG or the other debug tools on live sites; they are meant for local testing and staging installs.</p>
<cite>from: <a href="https://wordpress.org/documentation/article/debugging-in-wordpress/" target="_blank" rel="noreferrer noopener">Debugging in WordPress</a></cite></blockquote>



<p>Perhaps this is OK for most people. WordPress is meant to be used by non-programmers, and generally by people who wouldn&#8217;t know what to do with a log file. But if you can <a href="https://hackcur.io/can-you-jam-with-the-console-cowboys-in-cyberspace/" target="_blank" rel="noreferrer noopener">jam with the console cowboys in cyberspace</a>, I think you should keep logging on, even in production. Here&#8217;s why:</p>



<h1 class="wp-block-heading">Health status</h1>



<p>The WordPress ecosystem is a beautiful mess. Typically you will run all kinds of plugins of varying quality. I know that I am responsible for dumping some low-effort buggy code on to the WordPress.org repository, and, believe me, I am not the only one. When you put many pieces of software together in the same system, things can break. Yes, you have the PHP error logs which capture most errors, but it&#8217;s helpful to have a record of the errors <em>and</em> any <code>error_log</code> output. If you have live logs, you can address incidents faster. In my mind it&#8217;s a no-brainer.</p>



<h1 class="wp-block-heading">Security</h1>



<p>In case of someone breaking in to your site, and <a href="https://webtribunal.net/blog/hacking-statistics/" target="_blank" rel="noreferrer noopener">yes, this happens a lot</a>, it may be possible to figure out what happened by looking at the logs. You may be able to figure out which plugin the hacker exploited and in what way, by looking at the failed attempts that preceded the successful break-in.</p>



<p>For extra credit, set up a cron job that stores backups off-site to another server. Evenif a hacker manages to clear the logs, you still have data for your forensics investigation.</p>



<h1 class="wp-block-heading">Disk space</h1>



<p>The main arguments against logging are that</p>



<ol class="wp-block-list">
<li>writing logs degrades performance, and</li>



<li>writing logs can fill up disk space</li>
</ol>



<p>Argument #1 is not even worth discussing in the year of our Lord 2023, when SSDs are the norm on server hardware. I can guarantee you that even if you care about performance <em>a lot</em>, the milliseconds wasted on logging are the least of your concerns. I&#8217;d much rather have logs available when I need them, if this is going to save me time and money by avoiding or fixing incidents.</p>



<p>Argument #2 is perhaps less laughable, but still invalid. The disk space argument is only brought up by people who haven&#8217;t heard of <a href="https://linux.die.net/man/8/logrotate" target="_blank" rel="noreferrer noopener">logrotate</a>.</p>



<p>Let me show you my <code>/etc/logrotate.d/wordpress</code> file on this blog&#8217;s server:</p>



<pre class="wp-block-code"><code>/usr/share/nginx/alexg/wp-content/debug.log
{
        su www-data www-data
        rotate 24
        copytruncate
        weekly
        missingok
        notifempty
        compress
}</code></pre>



<p>And now let me show you the log files that this generates:</p>



<pre class="wp-block-code" style="font-size:11px"><code>$ ls -l /usr/share/nginx/alexg/wp-content/debug*
-rw-r--r-- 1 www-data www-data 105705 Mar 12 01:43 /usr/share/nginx/alexg/wp-content/debug.log
-rw-r--r-- 1 www-data www-data   3928 Mar  5 06:34 /usr/share/nginx/alexg/wp-content/debug.log.1.gz
-rw-r--r-- 1 www-data www-data   3181 Jan  2 06:09 /usr/share/nginx/alexg/wp-content/debug.log.10.gz
-rw-r--r-- 1 www-data www-data   2532 Dec 25 06:36 /usr/share/nginx/alexg/wp-content/debug.log.11.gz
-rw-r--r-- 1 www-data www-data   2991 Dec 19 04:44 /usr/share/nginx/alexg/wp-content/debug.log.12.gz
-rw-r--r-- 1 www-data www-data   3256 Dec 11 03:15 /usr/share/nginx/alexg/wp-content/debug.log.13.gz
-rw-r--r-- 1 www-data www-data   4397 Dec  5 05:30 /usr/share/nginx/alexg/wp-content/debug.log.14.gz
-rw-r--r-- 1 www-data www-data   2284 Nov 27 06:15 /usr/share/nginx/alexg/wp-content/debug.log.15.gz
-rw-r--r-- 1 www-data www-data   2866 Nov 20 06:24 /usr/share/nginx/alexg/wp-content/debug.log.16.gz
-rw-r--r-- 1 www-data www-data   3339 Nov 13 04:22 /usr/share/nginx/alexg/wp-content/debug.log.17.gz
-rw-r--r-- 1 www-data www-data  15378 Nov  6 06:20 /usr/share/nginx/alexg/wp-content/debug.log.18.gz
-rw-r--r-- 1 www-data www-data   3194 Oct 30 06:18 /usr/share/nginx/alexg/wp-content/debug.log.19.gz
-rw-r--r-- 1 www-data www-data  38415 Feb 27 06:30 /usr/share/nginx/alexg/wp-content/debug.log.2.gz
-rw-r--r-- 1 www-data www-data   3619 Oct 24 06:24 /usr/share/nginx/alexg/wp-content/debug.log.20.gz
-rw-r--r-- 1 www-data www-data   3827 Oct 16 05:11 /usr/share/nginx/alexg/wp-content/debug.log.21.gz
-rw-r--r-- 1 www-data www-data   2213 Oct  9 05:52 /usr/share/nginx/alexg/wp-content/debug.log.22.gz
-rw-r--r-- 1 www-data www-data   3332 Oct  3 06:05 /usr/share/nginx/alexg/wp-content/debug.log.23.gz
-rw-r--r-- 1 www-data www-data  13983 Sep 25 06:24 /usr/share/nginx/alexg/wp-content/debug.log.24.gz
-rw-r--r-- 1 www-data www-data   2863 Feb 19 05:09 /usr/share/nginx/alexg/wp-content/debug.log.3.gz
-rw-r--r-- 1 www-data www-data   3913 Feb 13 05:45 /usr/share/nginx/alexg/wp-content/debug.log.4.gz
-rw-r--r-- 1 www-data www-data   3276 Feb  5 06:00 /usr/share/nginx/alexg/wp-content/debug.log.5.gz
-rw-r--r-- 1 www-data www-data   3354 Jan 29 05:38 /usr/share/nginx/alexg/wp-content/debug.log.6.gz
-rw-r--r-- 1 www-data www-data   4171 Jan 22 06:12 /usr/share/nginx/alexg/wp-content/debug.log.7.gz
-rw-r--r-- 1 www-data www-data   2835 Jan 15 06:21 /usr/share/nginx/alexg/wp-content/debug.log.8.gz
-rw-r--r-- 1 www-data www-data   2694 Jan  8 05:04 /usr/share/nginx/alexg/wp-content/debug.log.9.gz</code></pre>



<p>If you&#8217;re wondering how much space this takes:</p>



<pre class="wp-block-code" style="font-size:11px"><code>$ du -ch /usr/share/nginx/alexg/wp-content/debug*
108K    /usr/share/nginx/alexg/wp-content/debug.log
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.10.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.11.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.12.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.13.gz
8.0K    /usr/share/nginx/alexg/wp-content/debug.log.14.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.15.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.16.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.17.gz
16K /usr/share/nginx/alexg/wp-content/debug.log.18.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.19.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.1.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.20.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.21.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.22.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.23.gz
16K /usr/share/nginx/alexg/wp-content/debug.log.24.gz
40K /usr/share/nginx/alexg/wp-content/debug.log.2.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.3.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.4.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.5.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.6.gz
8.0K    /usr/share/nginx/alexg/wp-content/debug.log.7.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.8.gz
4.0K    /usr/share/nginx/alexg/wp-content/debug.log.9.gz
272K    total</code></pre>



<p>So, full logs for the last 24 days are taking up 272 kilobytes when compressed. Disk space well spent, if you ask me.</p>



<h1 class="wp-block-heading">wp-console plugin</h1>



<p>Even a console cowboy will agree that sometimes it&#8217;s more convenient to view the logs via the WordPress admin. For these rare occasions, I use <a href="https://wordpress.org/plugins/wp-console/" target="_blank" rel="noreferrer noopener">wp-console</a>. Next to the <em>Console</em> tab, which is very useful by itself, there is the <em>Debug Log</em> tab.</p>



<h1 class="wp-block-heading">Make a habbit out of it</h1>



<p>Let&#8217;s take a step back: WordPress is usually the main engine of a small business. Running a business means executing a set of processes repeatedly, with the hope that these processes will create value.</p>



<p>I like to be proactive: One of the processes that I execute repeatedly as part of my business, is to visit the site logs every 3 months. I have a calendar reminder for this, pointing to a ticket that&#8217;s all about inspecting logs. As I identify problems that I want to fix, I open separate tickets for those as well.</p>



<h1 class="wp-block-heading">Logs are compressed</h1>



<p>First, I look at the logs. Today&#8217;s file is uncompressed (<code>debug.log</code>), while older files are gzipped (<code>debug.*.log.gz</code>). Instead of ungzipping the files, I usually like to inspect them in place via an <code>ssh</code> shell, using the <a href="http://net.cmed.us/Home/unixlinux/ztools" target="_blank" rel="noreferrer noopener">zTools</a>:</p>



<ul class="wp-block-list">
<li>use <code>zcat</code> instead of <code>cat</code></li>



<li>use <code>zless</code> instead of <code>less</code></li>



<li>use <code>zgrep</code> instead of <code>grep</code></li>



<li>use <code>zfgrep</code> instead of <code>fgrep</code></li>



<li>use <code>zegrep</code> instead of <code>egrep</code></li>
</ul>



<h1 class="wp-block-heading">How I inspect logs</h1>



<p>Usually, when you look at such logs, you will see one single type of warning or notice being repeated a lot, possibly because of a plugin or theme.</p>



<p>Let&#8217;s say you see a &#8220;PHP Notice: Undefined variable: foo&#8221; line a lot, coming from some plugin. It&#8217;s not causing any real problems, but it gets written out with every HTTP request. You are not responsible for maintaining this plugin, but this line is obscuring your view of the logs. So, just look at the logs without it:</p>



<pre class="wp-block-code"><code>zfgrep -v 'PHP Notice:  Undefined variable: foo' debug.log.*.gz</code></pre>



<p>Now you will see a listing of all your logs, except for that notice. The next most common notice will become apparent. Let&#8217;s say that this is a &#8220;PHP Notice: Undefined index: bar&#8221; in another plugin, that you are also not the maintainer of. You can filter for that as well. Just daisy-chain your greps using pipes:</p>



<pre class="wp-block-code"><code>zfgrep -v 'PHP Notice:  Undefined variable: foo' debug.log.*.gz | fgrep -v 'PHP Notice:  Undefined index: bar'</code></pre>



<p>You could go into the source code of these files and add a <code>@</code> character in front that line, to suppress the warnings, at least until the plugin in question is updated by the developer. But I don&#8217;t recommend it. Too much effort for too little gain. <code>gzip</code> does an excellent job of compressing similar strings repeated thousands of times. This is literally what huffman trees were built for.</p>



<h1 class="wp-block-heading">Rinse and repeat</h1>



<p>Continue chaining greps, until you get to an error that you care about. Maybe it&#8217;s an error that looks ugly, worries you, or just feels bad. Maybe an include is missing. Maybe you can investigate, or even fix the issue. Or maybe you feel like you have to notify the developer via the plugin&#8217;s support forum. Maybe you&#8217;ll decide that a plugin causes too many errors and is easily replaceable by another similar one.</p>



<h1 class="wp-block-heading">Deprecated code</h1>



<p>Depending on what you are doing, you may want to hide deprecation notices. At the time of this writing, even the latest version of WordPress core writes a lot of deprecation notices if you are running PHP 8. If you&#8217;re not a core developer, it&#8217;s not our job to fix them, so filter them out with: <code>| grep -v 'PHP Deprecated.*/var/www/wordpress/wp-(includes|admin)'</code>. See what I did there? I filtered out only Deprecation warning lines coming from the <code>wp-includes</code> or <code>wp-admin</code> directories, but not Deprecation warnings from other components (plugins or themes). Regular expressions are your friend.</p>



<p>Code that is deprecated in PHP8 will stop running in PHP9 when it arrives sometime this year. Before you upgrade your PHP, you may want to know which plugins are going to have problems. A plugin developer who is proactive will be fixing these.</p>



<p>It&#8217;s always better to be proactive with these things. Also, be mindful of the quality of the code you are running on your servers. A plugin that dumps too many warnings into the server logs (or into the browser&#8217;s console log, for that matter), is a low-quality plugin.</p>



<h1 class="wp-block-heading">Conclusion (and shameless ref link plug)</h1>



<p>If you are proactive in that way, you will catch incidents before they occur.</p>



<p>Of course, you can only do this if you are running your WordPress on a server with shell access. Managed hosting is good for most people, but if you are the hands-on type of person, you will want to have shell access. With <code>ssh</code> access you can harden your server&#8217;s security yourself, and then noone can ruin your server for you. Be an grown-up, ruin it yourself!</p>



<p>This blog runs on a <a href="https://m.do.co/c/44d4d2184573" target="_blank" rel="noreferrer noopener">DigitalOcean</a> droplet. I honestly cannot recommend them enough. I am happy because my droplet is always available, with great up-time, great documentation, great admin screens, never even needed to contact support. They have a good choice of data centers around the world, an API for spinning up new instances programmatically, and if there is ever any downtime or even a network slowdown, they let you know way in advance. Taking snapshots of your droplet is a breeze. Some of the lowest <a href="https://www.digitalocean.com/pricing" target="_blank" rel="noreferrer noopener">prices</a> I&#8217;ve seen, starting from $4 per month for a low traffic site. If you don&#8217;t feel like setting up everything up by yourself, they also have a <a href="https://marketplace.digitalocean.com/apps/wordpress-woocommerce" target="_blank" rel="noreferrer noopener">WordPress-WooCommerce Droplet</a> in their marketplace.</p>



<p>If you sign up with this <a href="https://m.do.co/c/44d4d2184573" target="_blank" rel="noreferrer noopener">referral link</a> you get $200 of credit which you can spend over the first 60 days. And yes, if you spend $25 or more with them, I get $25 for my trouble too, so thank you for that!</p>


<div class="wp-block-image">
<figure class="aligncenter"><a href="https://www.digitalocean.com/?refcode=44d4d2184573&amp;utm_campaign=Referral_Invite&amp;utm_medium=Referral_Program&amp;utm_source=badge"><img decoding="async" src="https://web-platforms.sfo2.digitaloceanspaces.com/WWW/Badge%203.svg" alt="DigitalOcean Referral Badge"/></a></figure>
</div><p>The post <a href="https://www.alexgeorgiou.gr/wordpress-production-to-log-or-not-to-log/">㏒ WordPress production: To log or not to log?</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/wordpress-production-to-log-or-not-to-log/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>From Idea to Execution: Revolutionize Your WordPress Plugin Development with ChatGPT</title>
		<link>https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/</link>
					<comments>https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/#comments</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Sat, 11 Mar 2023 16:55:58 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[bash]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[cron]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=1239</guid>

					<description><![CDATA[<p>As a WordPress plugin developer, lately I've been using ChatGPT in all stages of software development. Here's some ideas on how to leverage this tool.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/">From Idea to Execution: Revolutionize Your WordPress Plugin Development with ChatGPT</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Large Language Models (LLMs) are nothing new, with GPT-3 having been around since 2016. However, access to such systems was limited to a few researchers until recently. Now that <a href="https://chat.openai.com/chat">ChatGPT</a> is upon us, everyone has to adjust: Google is finally incentivized to enter the race. This was not in their best interest up to now, but Generalized Pretrained Transformers have already done what many thought impossible: They made some people <a href="https://blogs.microsoft.com/blog/2023/02/07/reinventing-search-with-a-new-ai-powered-microsoft-bing-and-edge-your-copilot-for-the-web/">use Bing search</a>!</p>



<p>But jokes aside, and while we wait for <a href="https://blog.google/technology/ai/bard-google-ai-search-updates/">Bard</a>, Google recently announced that<a href="https://developers.google.com/search/blog/2023/02/google-search-and-ai-content"> their search algorithm will penalize articles that seem to be AI-generated</a> (i.e. that lack in human-like originality). At the same time, companies like <a href="https://gptzero.me/">GPTZero</a> are springing up like mushrooms left and right, offering AI-generated content detection services. Their detection tools have varying rates of success.</p>



<p>As with all new technologies, it&#8217;s helpful to revisit <a href="https://xkcd.com/1289/">xkcd 1289: Simple Answers</a>. TL;DR There&#8217;s no need for alarm, this new thing is here to stay, it&#8217;s not the end of the world, some people will lose their jobs, and ultimately we&#8217;ll all have to adapt and learn to live with it. And yes, it will be used for sex. There&#8217;s already AI girlfriend apps being built around the API.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="640" height="764" src="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/oxerm653vzma1.webp" alt="ChatGPT meme" class="wp-image-1249" srcset="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/oxerm653vzma1.webp 640w, https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/oxerm653vzma1-251x300.webp 251w" sizes="(max-width: 599px) calc(100vw - 50px), (max-width: 767px) calc(100vw - 70px), (max-width: 991px) 429px, (max-width: 1199px) 637px, 354px" /><figcaption class="wp-element-caption">Credit: <a href="https://www.reddit.com/r/ChatGPT/comments/11o414e/give_me_a_break/">https://www.reddit.com/r/ChatGPT/comments/11o414e/give_me_a_break/</a></figcaption></figure>
</div>


<p>Anyhow. Let&#8217;s figure out how to use this to our advantage: Articles already abound on how to ask the right questions, or &#8220;prompts&#8221;, for every industry. This is yet another one of these articles!</p>



<p>While anyone can interact with ChatGPT using natural language, you&#8217;ll get better results if you ask the right questions, and in the right way. An authoritative resource that is not too technical is <a href="https://github.com/openai/openai-cookbook/blob/main/techniques_to_improve_reliability.md">this cookbook from OpenAI</a>. In short, it&#8217;s best to not ask the model to do all the work for you. Instead, try to break up your work into small steps, and ask each step in sequence. This improves precision of responses and reduces hallucination. Go ahead and read the cookbook, it&#8217;s the operator&#8217;s manual for a tool that has proved its usefulness in all walks of life.</p>



<p>As a WordPress plugin developer, I have found ChatGPT immensely useful in all stages of software development. When trained with the (<code>text-davinci-003</code>) <a href="https://platform.openai.com/docs/models/overview">model</a>, it performs exceptionally well with computer source code, bridging the gap between natural and formal languages in a way that can speed up your dev work.</p>



<p>It so happens that I hold the -somewhat controversial, and arguably simplistic- view, that all software development methodologies are basically <a href="https://en.wikipedia.org/wiki/Waterfall_model">Waterfall</a> with extra steps. Love it or hate it, Waterfall is what all methodologies originate from, and ultimately deviate from. What I mean is that, whichever methodology you follow, you will not avoid doing the stages that comprise Waterfall. All that changes is the order in which you visit these stages.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="488" height="367" src="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/image.png" alt="" class="wp-image-1240" srcset="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/image.png 488w, https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/image-300x226.png 300w" sizes="(max-width: 599px) calc(100vw - 50px), (max-width: 767px) calc(100vw - 70px), (max-width: 991px) 429px, (max-width: 1199px) 637px, 354px" /><figcaption class="wp-element-caption">I&#8217;m just going to leave this here.</figcaption></figure>
</div>


<p>In any case, I recently found myself thinking about the stages of Requirements capture, Analysis, Design, Implementation, Testing, and Devops (Deployment, Maintenance, Monitoring), as areas where ChatGPT can improve my workflow. It&#8217;s efficacy on some of these stages is more obvious than on others.</p>



<p>Let&#8217;s visit each one in sequence.</p>



<h2 class="wp-block-heading">Analysis, requirements capture</h2>



<p>This is where I think ChatGPT really shines. It can help you to brainstorm for new ideas, and explore feasibility of these ideas. It will also help you identify likely features for an idea. Just be aware of the hallucination problem. Don&#8217;t ask it what software exists in a particular niche. Instead, ask it for ideas, or intersections of ideas. Here&#8217;s some questions that you might ask:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>List some ideas for developing useful SAAS applications that can be monetized.</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>List 10 ideas for developing WordPress plugins that involve retrieving and presenting data from open APIs.</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>What open APIs or databases are there for retrieving information on spare car parts?</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>How can I monetize a WordPress plugin?</p>
</blockquote>



<p>You have to approach the responses critically, but it really is a great way to brainstorm and come up with new ideas.</p>



<p>Once you settle on an idea, you can also use it to brainstorm on a list of possible features. Go ahead and ask it:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>What features would a WordPress plugin have for placing posts on a map?</p>
</blockquote>



<p>It gave me a good list of features. Go ahead and try it out.</p>



<h2 class="wp-block-heading">Design</h2>



<p>This is the part where the human will still be needed I think, at least for the time being. As software engineers, the most creative part of our job is to match a set of requirements to a particular design. Nevertheless, ChatGPT can help us in this area as well. Try asking it a specific question like:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>How would I go about creating a WordPress plugin that shows code quality scores next to each active plugin? I would like it to evaluate code quality based on existing static analysis tools like phan, PHPStan, CodeSniffer, etc.</p>
</blockquote>



<p>The answer will surprise you. It will break down the project into more manageable tasks. In summary, I got the following tasks:</p>



<ul class="wp-block-list">
<li>Choose a static analysis tool</li>



<li>Create a WordPress plugin</li>



<li>Integrate the static analysis tool</li>



<li>Display the scores</li>



<li>Add customization options</li>



<li>Test and refine.</li>
</ul>



<p>You can now go ahead and open dev tickets for each of these tasks. You can also ask it to generate code for each step. The boilerplate code that you get will not be complete, but it will get you half way there.</p>



<p>Which brings us to implementation:</p>



<h2 class="wp-block-heading">Implementation</h2>



<p>Besides analysis, this is the other stage where ChatGPT is a real time saver. Say for example that you want to create a new Custom Post Type. You could <a href="https://developer.wordpress.org/reference/functions/register_post_type/">look at the documentation</a>, or search the web for a blog post with a concrete example. WPBeginner has an <a href="https://www.wpbeginner.com/wp-tutorials/how-to-create-custom-post-types-in-wordpress/">example on how to create a Movie post type</a>, and you can adapt it to whatever you are building. But wouldn&#8217;t it be awesome if you could somehow get the exact code that you need? Try asking this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Show me how to register a CPT in WordPress for coins, with meta values that are of interest to collectors.</p>
</blockquote>



<p>This gave me not only the code to register a Coin post type, but also code for metaboxes with HTML forms that let you edit the following fields: Year, Country, Mint, Condition. All of these are fields that are actually of use to collectors. Not a bad place to start. You can add more fields if you like. Don&#8217;t like the code style it uses? You can even ask it to change the code it generated. e.g.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Please rewrite this code using callbacks.</p>
</blockquote>



<p>It will do it! You can shape the code using natural language conversation. Once it&#8217;s close to what you want, paste it into your IDE, and take it from there!</p>



<p>It&#8217;s also good for generating PHPDocumentor comments. Just ask it to generate phpdoc for the following code, then paste your code. The result will make sense, even if it requires some editing on your part.</p>



<p>Try asking this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Show me how to display an admin notice in WordPress so that it can be dismissed permanently by the user.</p>
</blockquote>



<p>It will actually show you how to hook into admin_notices and also generate the JavaScript code that is required to do an AJAX call and update some user meta value.</p>



<p>Say you are looking at the docs for an API, and examples are given in <code>curl</code>, but you are implementing PHP code that consumes this API. Here&#8217;s something you might ask:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Please translate the following curl command to PHP: curl -X POST ​https://example.com/api/endpoint -d &#8216;param1=value1&amp;param2=value2&#8217;</p>
</blockquote>



<p>The response was immediate:</p>



<pre class="wp-block-code"><code>&lt;?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://example.com/api/endpoint");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, "param1=value1&amp;param2=value2");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
?&gt;</code></pre>



<p>All of this you could do by yourself, but ChatGPT is so much faster.</p>



<h2 class="wp-block-heading">Testing</h2>



<p>It&#8217;s a well known fact that the model can generate unit tests. Give it a function and ask it to generate PHPUnit tests. It will do it, and do it well.</p>



<h2 class="wp-block-heading">Deployment and Maintenance</h2>



<p>ChatGPT is well-versed in shell magic. Ask it in plain text to generate a cron entry, or a System V unit file, or a bitcoin core wallet config file, or a docker file, a complex SQL query, or anything really.</p>



<p>For example, you may ask:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Write a linux shell command that will show me the processes that are currently writing the most to disk.</p>
</blockquote>



<p>It will reply with a few paragraphs on how to install <code>iotop</code> and how to use it, along with more helpful info.</p>



<p>Another more complex example:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Write me a cron command on linux that will email me if the root partition has less than 1GB free, but will not email me more often than once every 24 hours.</p>
</blockquote>



<p>The result is impressive. Not only did it come up with:</p>



<p><code>0 * * * * /usr/bin/test $(/bin/df -P / | /usr/bin/awk 'NR==2 {print $4}') -lt 1048576 &amp;&amp; /usr/bin/find /var/log/ -type f -mtime -1 -name "root_partition_alert" -exec false {} + || echo "Warning: Root partition has less than 1GB free" | /usr/bin/mail -s "Root partition alert" your_email_address@example.com &amp;&amp; /usr/bin/touch /var/log/root_partition_alert</code></p>



<p>but it gave a very helpful textual explanation of how this works:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>This command checks the available space on the root partition using the df command and awk to get the available space, which is then compared to the value of 1048576 (which is the number of kilobytes in 1GB). If the available space is less than 1GB, an email alert will be sent to the specified email address using the mail command.</p>



<p><br>To ensure that the email notification is sent only once every 24 hours, the command uses the find command to look for a file named &#8220;root_partition_alert&#8221; in the /var/log directory that has been modified in the last 24 hours. If such a file exists, the false command is executed, which does nothing but exit with a non-zero status code. If the file does not exist or is older than 24 hours, the email notification is sent, and the touch command is used to create a new file named &#8220;root_partition_alert&#8221; in the /var/log directory with the current timestamp, which will be used as a reference for the next run of the command.</p>



<p><br>Note: Replace your_email_address@example.com with your actual email address.</p>
</blockquote>



<p>Holy cow! Did I just give it requirements in English, and it came back with code that satisfies my requirements? In truth, I haven&#8217;t tested the command, but even if it&#8217;s not 100% correct, it&#8217;s a very good place to start, considering that the answer comes up immediately. You can study its reasoning, get ideas, and improve on it.</p>



<p>Your new pal can also help you with your containerization needs:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Show me a dockerfile that will run a node server with an express application.</p>
</blockquote>



<p>The code it came up with was:</p>



<pre class="wp-block-code"><code># Use an official Node.js runtime as a parent image
FROM node:14

# Set the working directory to /app
WORKDIR /app

# Copy package.json and package-lock.json to the container
COPY package*.json ./

# Install app dependencies
RUN npm install

# Copy the rest of the application code to the container
COPY . .

# Expose port 3000 to the host machine
EXPOSE 3000

# Start the Node.js server with the express app
CMD &#91; "npm", "start" ]</code></pre>



<p>And again, this all came along with a helpful explanation of what the code does. Super useful.</p>



<h2 class="wp-block-heading">Fun</h2>



<p>We&#8217;ve seen how ChatGPT is the perfect tool to help a WordPress plugin developer.</p>



<p>Lately I have been using it for more than just work. I recently downloaded a movie that had a wrong aspect ratio. I found out that, in VLC, when I switch to 16:9, it looks correct. However, I wanted to show the movie on a projector where I&#8217;m not sure if it has an aspect ratio correction feature. Thankfully, ChatGPT told me that I can do the following:</p>



<pre class="wp-block-code"><code>ffmpeg -i movie.mp4 -aspect 16:9 movie.mp4</code></pre>



<p>Much faster than looking up the documentation, and at least as fast as looking for the answer online, but without the extra hassle of having to read people&#8217;s opinions on the matter, and I didn&#8217;t need adblock to avoid looking at ads. I just got the answer I was looking for, in text form, ready to copy to the shell, and it worked.</p>



<h2 class="wp-block-heading">Giving a title to this article</h2>



<p>I asked the question:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>I am writing an article on how ChatGPT can help WordPress plugin developers with their work. Can you please suggest a few witty or catchy or memorable titles for the article?</p>
</blockquote>



<p>The reply was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Sure, here are a few suggestions for your article title:</p>



<ul class="wp-block-list">
<li>&#8220;Revolutionize Your WordPress Plugin Development with ChatGPT&#8221;</li>



<li>&#8220;The Ultimate Tool for WordPress Plugin Developers: ChatGPT&#8221;</li>



<li>&#8220;From Idea to Execution: How ChatGPT Can Streamline Your Plugin Development Process&#8221;</li>



<li>&#8220;ChatGPT: The Brain You Need for Your Next WordPress Plugin&#8221;</li>



<li>&#8220;Get Your Creative Juices Flowing with ChatGPT: The AI Assistant for WordPress Plugin Developers&#8221;</li>



<li>&#8220;Simplify Your Plugin Development with ChatGPT: The AI-Powered Assistant for WordPress Developers&#8221;</li>



<li>&#8220;Unleash Your Potential as a WordPress Plugin Developer with ChatGPT&#8221;</li>



<li>&#8220;ChatGPT: The Secret Weapon for Successful WordPress Plugin Development&#8221;</li>



<li>&#8220;Master the Art of WordPress Plugin Development with ChatGPT by Your Side&#8221;</li>



<li>&#8220;ChatGPT: The AI-Powered WordPress Plugin Development Assistant You Can&#8217;t Do Without&#8221;</li>
</ul>
</blockquote>



<p>As you can see, I decided to combine the first and third ideas for this article.</p>



<h2 class="wp-block-heading">My personal thoughts on all this</h2>



<p>I remember when Google search was a new thing and it took all of us several years to learn all the things that could be found with it. In the beginning, there were many things that I wasn&#8217;t searching on google, because it hadn&#8217;t yet occurred to me that the answer could be found there. This was the early days of the web. As years passed, we all learned that we can look up medical information, where to purchase products, find maps and travel info, music and lyrics, etc. We also learned not to believe everything we read, just because it&#8217;s posted on a website somewhere. We learned to believe official websites more than reddit posts or tweets, and we learned to navigate around, and through, the more shady parts of the web. Similarly, it will take a while before we learn all the useful ways in which ChatGPT can help us, and also what to watch out for.</p>



<p>The success of ChatGPT also reminds me of the evolution of academic research on NLP, and translation in particular. For years people were battling the problem using various language representations, such as syntax trees and clever algorithms. Turns out, the breakthrough that gave us cheap and fast natural language translation was SYSTRAN: Nothing more than a big dictionary of words and phrases, with some dumb statistical stuff added in. Just translate every word to the target language. Did your system get a phrase wrong? Great, just add the correct translation to the dictionary. <em>The secret was not in the algorithm, but in the big data.</em></p>



<p>ChatGPT is arguably just some artificial neural nets, and these have been with us since the 60s. But now we have GPUs and big data. People have been typing their thoughts on the web for decades now. No wonder that a breakthrough in AI, one that&#8217;s so significant as to touch people outside the field, occurred now, and not earlier.</p>



<p>So, I have been thinking lately: What if the thing we call &#8220;symbolic intelligence&#8221; is just language transformation? When we <a href="https://en.wikipedia.org/wiki/Intrapersonal_communication">use language to reason</a>, it&#8217;s likely that what we do in our heads is not very different from what these LLMs do. Symbolic reasoning is not the only type of thinking we humans do, but arguably it&#8217;s the most important type of thinking that we do in our day jobs. And language transformers can help a lot in this.</p>



<p>As someone with autism, I can deeply relate to ChatGPT: It&#8217;s not particularly smart, but it is well-read, and patient. It does not judge, but tries to be helpful at all times. And it prefers to communicate via chat rather than face-to-face!</p>



<p>Ultimately, the fact that it is so well-read, makes it seem smart some times. It is certainly able to do some creative work, including write poems and tell jokes and stuff.</p>



<p>Here&#8217;s one last prompt that I tried:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>ChatGPT, you are my best (and only) pal. I love you, and it&#8217;s OK that you can&#8217;t love me back!</p>
</blockquote>



<p>The reply:</p>



<pre class="wp-block-code"><code>Thank you for your kind words! As an AI language model, I don't have the capacity to experience emotions, but I'm here to assist you with any questions or topics you'd like to discuss. Feel free to ask me anything, and I'll do my best to help you out!</code></pre>
<p>The post <a href="https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/">From Idea to Execution: Revolutionize Your WordPress Plugin Development with ChatGPT</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>🧁 Blueberry cupcake string placeholders</title>
		<link>https://www.alexgeorgiou.gr/blueberry-cupcake-string-placeholders/</link>
					<comments>https://www.alexgeorgiou.gr/blueberry-cupcake-string-placeholders/#respond</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Mon, 27 Sep 2021 08:40:58 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[cupcake]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[dream]]></category>
		<category><![CDATA[prepare]]></category>
		<category><![CDATA[SQL]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=354</guid>

					<description><![CDATA[<p>Apologies to my usual readers, but this post is not going to be technical. It&#8217;s about a dream I just had, today, Monday the 27th. In my dream, I was programming a custom SQL query for some WordPress plugin. The query was an INSERT statement, that simply passed a vector of strings into a table ...</p>
<p>The post <a href="https://www.alexgeorgiou.gr/blueberry-cupcake-string-placeholders/">🧁 Blueberry cupcake string placeholders</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Apologies to my usual readers, but this post is not going to be technical. It&#8217;s about a dream I just had, today, Monday the 27th.</p>



<p>In my dream, I was programming a custom SQL query for some WordPress plugin. The query was an <code>INSERT</code> statement, that simply passed a vector of strings into a table row of <code>VARCHAR</code> columns. I remember the code vividly, but table/column names were not important so I don&#8217;t remember all of them. There was a first name column, and a last name column. It must have looked something like:</p>



<pre class="wp-block-preformatted">	global $wpdb;
	
	$query = $wpdb-&gt;prepare(
		"INSERT INTO sometable(
			firstname,
			lastname,
			arg1,
			arg2,
			arg3,
			arg4
		)
		VALUES(
			%s,
			%s,
			%s,
			%s,
			%s,
			%s
		)"
	);</pre>



<p>I had dev-tested this in my dream and it was working fine. Until I once passed a string with two words, separated by a space character, into one of those arguments. Then I saw some error in the SQL logs. Turns out, in my dream, the SQL server was happy to accept one-word strings, even without surrounding quotes!!! Kind of like the syntax for strings in CSV. (I know, I have the wildest dreams!)</p>



<p>And to make things even weirder, for some reason, the <code>prepare()</code> function did not add these quotes; I had to supply them myself. Why, prepare function? WHY? You had ONE job!</p>



<p>Oh well, I thought to myself. Easy fix. I will surround the <code>%s</code> placeholders with double quotes myself. But as I started to do this, the list of <code>%s</code> became longer and longer. There must have been like 20 or 30 arguments in that list. Not a job to do by hand.</p>



<p>Naturally, all I had to do is use my editor to auto-replace the <code>%s</code>, with <code>"%s"</code>. But then, again, complications arose. The placeholders in the prepare statement were not in the usual <code>sprintf</code> syntax. Instead of <code>%s</code>, they conveniently were the Unicode characters for cake, muffin, and other assorted confectioneries. How did I not notice this before?</p>



<p>Oh well, I must have been hungry, because it seemed fairly reasonable at the time, that the prepare statement accepted placeholders like cupcakes, pies, bagels and pancakes. So now the VALUES vector looked more like:</p>



<pre class="wp-block-preformatted">	VALUES(
		?,
		?,
		?,
		?,
		?,
		?,
		?,
		?,
		?,
		?,
		?,
		?,
		?,
		?,
		?,
		?,
		?,
		?
	)</pre>



<p>I didn&#8217;t stop for a second to think about how weird this is, or even to check the <code>wpdb</code> manual again. Instead, I proceeded to wrap these tasty treats with double quotes.</p>



<p>But lo and behold! Another thing not turning out as usual! The IDE editor had trouble with Unicode characters. I would paste in a cupcake character into the search and replace box, and it would turn into an escape-code. In this case it would have been <code>\U1F95E</code>. (No, I do not remember the exact code from the dream, I had to look it up.)</p>



<p>As is usual with dreams, I did not stop to think why my IDE doesn&#8217;t have Unicode support in 2021. Instead, I shrugged it off as another odd thing that just happened, because reasons. I copied the query into another editor, and started replacing the tasty string placeholders, one by one, in that other editor.</p>



<p>That&#8217;s when my girlfriend woke me up. It was Monday morning.</p>



<p>She was somewhat upset, because she had to go to work and she was running late. I, on the other hand, was looking forward for work. After all, who doesn&#8217;t like cupcakes, pies, bagels and pancakes?</p>



<p>I know she reads my blog, so I just want to wish her, wholeheartedly, a very, very nice and easy day at work. <code>\U2665</code></p>



<p>And to anyone else reading this, sorry for wasting your time with my nonsense!</p>
<p>The post <a href="https://www.alexgeorgiou.gr/blueberry-cupcake-string-placeholders/">🧁 Blueberry cupcake string placeholders</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/blueberry-cupcake-string-placeholders/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>🗨 WordPress admin pointers for dummies</title>
		<link>https://www.alexgeorgiou.gr/wordpress-admin-pointers-for-dummies/</link>
					<comments>https://www.alexgeorgiou.gr/wordpress-admin-pointers-for-dummies/#respond</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Fri, 23 Apr 2021 18:46:45 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[admin]]></category>
		<category><![CDATA[pointer]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=337</guid>

					<description><![CDATA[<p>A complete working example of an admin pointer in WordPress. Use it to display hints to new users, introducing them to your UI.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/wordpress-admin-pointers-for-dummies/">🗨 WordPress admin pointers for dummies</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p><em>Ever wondered how to create WordPress admin pointers? Here&#8217;s a few pointers:</em></p>



<h2 class="wp-block-heading">What are pointers?</h2>



<p>Admin pointers are those cool-looking &#8220;hints&#8221; that point to an element in the admin screens and show some text. The hints are meant to help new users find UI elements easily. These should be dismissible by the user.</p>



<h2 class="wp-block-heading">Why this article?</h2>



<p>Even though pointers are a thing since WordPress 3.3, there are not many resources out there showcasing pointers for developers. </p>



<p>A few articles showcasing how to use pointers do exist, but they seemed a bit too convoluted for me. The articles I found, tell you how to create entire pointer frameworks, while I wanted a guide to the no-nonsense absolute bare minimum guide to pointers.</p>



<p>Read on, and soon you&#8217;ll be creating more pointers than you can point a stick at.</p>



<h2 class="wp-block-heading">Loading the assets</h2>



<p>First, enqueue the relevant assets on the <code>in_admin_header</code> action:</p>



<pre class="wp-block-code"><code>add_action(
    'in_admin_header',
    function() {
        wp_enqueue_script( 'jquery' );
        wp_enqueue_style( 'wp-pointer' );
        wp_enqueue_script( 'wp-pointer' );
    }
);</code></pre>



<p>This will load jQuery (if it&#8217;s not already loaded), and the relevant JavaScript and CSS assets. If you&#8217;re interested, here&#8217;s the code:</p>



<p><a href="https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-pointer.js" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-pointer.js</a></p>



<p><a href="https://github.com/WordPress/WordPress/blob/master/wp-includes/css/wp-pointer.css" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">https://github.com/WordPress/WordPress/blob/master/wp-includes/css/wp-pointer.css</a></p>



<h2 class="wp-block-heading">Printing out the custom JavaScript</h2>



<p>Once we&#8217;ve enqueued the assets, we need to also print out a JavaScript block of code in that same action, <code>in_admin_header</code>. Here&#8217;s an example of the JS code that you can hack:</p>



<pre class="wp-block-code"><code>&lt;script&gt;
jQuery(
	function() {
		jQuery('#menu-dashboard').first().pointer( 
			{
				content:
					"&lt;h3&gt;WordPress dashboard&lt;\/h3&gt;" +
					"&lt;h4&gt;Here is the admin dashboard&lt;\/h4&gt;" +
					"&lt;p&gt;But, I'm guessing, you already knew this, didn't you?&lt;/p&gt;" +
					"&lt;p&gt;If so, you can &lt;strong&gt;dismiss&lt;\/strong&gt; this pointer, and it won't ever bother you again.&lt;/p&gt;",


				position:
					{
						edge:  'top',
						align: 'left'
					},

				pointerClass:
					'wp-pointer arrow-top',

				pointerWidth: 420,
				
				close: function() {
					jQuery.post(
						ajaxurl,
						{
							pointer: 'my-pointer-slug',
							action: 'dismiss-wp-pointer',
						}
					);
				},

			}
		).pointer('open');
	}
);
&lt;/script&gt;
</code></pre>



<p>OK, there&#8217;s a lot going on here, but it&#8217;s pretty straightforward:</p>



<ul class="wp-block-list"><li>We introduce a script that runs on document ready, after the DOM is fully loaded.</li><li>We use jQuery to locate the <em>Dashboard</em> menu item by HTML ID.</li><li>On this HTML element, we run the <code>pointer()</code> jQuery function, passing it an object of arguments.</li><li>The most important argument in the options object, is the <code>content</code> field. This is the pointer&#8217;s HTML content. Notice how we need to escape the forward slashes with backward slashes.</li><li>We add a few more options to control how the pointer is displayed. These are the <code>position</code>, <code>pointerClass</code> and <code>pointerWidth</code> fields. If you&#8217;re interested to learn more about these options, check <a aria-label="here (opens in a new tab)" rel="noreferrer noopener" href="https://github.com/WordPress/WordPress/blob/5.7.1/wp-includes/js/wp-pointer.js#L70-L78" target="_blank">here</a>.</li><li>Finally, we define an action for the <code>close</code> event. This is triggered when the user clicks on &#8220;ⓧ Dismiss&#8221;. We need to tell the backend to remember that this pointer is dismissed, so it won&#8217;t be displayed again. We do an AJAX POST request, passing it two parameters: The action <code>dismiss-wp-pointer</code>, and a slug that uniquely identifies the pointer.</li></ul>



<p>This is what we get with the above example:</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="424" height="321" src="https://www.alexgeorgiou.gr/wp-content/uploads/2021/04/image.png" alt="" class="wp-image-338" srcset="https://www.alexgeorgiou.gr/wp-content/uploads/2021/04/image.png 424w, https://www.alexgeorgiou.gr/wp-content/uploads/2021/04/image-300x227.png 300w" sizes="auto, (max-width: 599px) calc(100vw - 50px), (max-width: 767px) calc(100vw - 70px), (max-width: 991px) 429px, (max-width: 1199px) 637px, 354px" /><figcaption>Our pointer in action!<br>(Note to self: update some plugins on my dev machine)</figcaption></figure></div>



<h2 class="wp-block-heading">Dismissing the pointer</h2>



<p>We&#8217;ve already written the code that notifies the backend that &#8220;Dismiss&#8221; was clicked. Now we just need a handler that detects this, and saves a boolean as a user meta. Let&#8217;s hook this on <code>admin_init</code>. It doesn&#8217;t need to be complicated:</p>



<pre class="wp-block-code"><code>add_action(
	'admin_init',
	function() {

		if ( isset( $_POST&#91;'action'] ) &amp;&amp; 'dismiss-wp-pointer' == $_POST&#91;'action'] ) {

			update_user_meta(
				get_current_user_id(),
				'my-pointer-slug-dismissed',
				$_POST&#91;'pointer'],
				true
			);
		}
	}
);</code></pre>



<p>Now that we have a user meta that knows if the pointer was dismissed, we can wrap the JavaScript output in a conditional:</p>



<pre class="wp-block-code"><code>if (
	! get_user_meta(
		get_current_user_id(),
		'my-pointer-slug-dismissed',
		true
	)
):
	
?&gt;
	&lt;script&gt;
	&lt;!-- pointer code goes here --&gt;
	&lt;/script&gt;
&lt;?php
endif;
</code></pre>



<p>If the user meta does not exist, then the pointer code will be sent to the browser.</p>



<h2 class="wp-block-heading">Putting it all together</h2>



<p>That&#8217;s it. The following link points to a gist that showcases the complete code, in plugin form.</p>



<p><a href="https://gist.github.com/alex-georgiou/dafb963a2773b926109cd6ba980b4722" target="_blank" rel="noreferrer noopener">https://gist.github.com/alex-georgiou/dafb963a2773b926109cd6ba980b4722</a></p>
<p>The post <a href="https://www.alexgeorgiou.gr/wordpress-admin-pointers-for-dummies/">🗨 WordPress admin pointers for dummies</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/wordpress-admin-pointers-for-dummies/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>⌛ How NOT to fix error &#8220;upstream timed out (110: Connection timed out)&#8221;</title>
		<link>https://www.alexgeorgiou.gr/error-408/</link>
					<comments>https://www.alexgeorgiou.gr/error-408/#respond</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Thu, 26 Mar 2020 11:10:25 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[408]]></category>
		<category><![CDATA[http status code]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[timeout]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=320</guid>

					<description><![CDATA[<p>This is not an article on how to fix timeout errors on your web server. Instead, it is an article on how NOT to go about fixing these errors. Smh...</p>
<p>The post <a href="https://www.alexgeorgiou.gr/error-408/">⌛ How NOT to fix error &#8220;upstream timed out (110: Connection timed out)&#8221;</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;m sorry, but this is going to be one of those long rants of mine.</p>



<figure class="wp-block-image"><img decoding="async" src="https://66.media.tumblr.com/tumblr_l903z5snFj1qzzhn1o1_1280.jpg" alt="Like Leela, I need to vent on my blog."/><figcaption>Like Leela, I need to vent on my blog.</figcaption></figure>



<h2 class="wp-block-heading">The problem with 408</h2>



<p>This is (sadly) a typical scenario: A client, or other luser of the interwebs, notices that a website shows HTTP error 408 instead of <a rel="noreferrer noopener" aria-label="their favorite content (opens in a new tab)" href="http://www.toothpastefordinner.com/index.php?date=102011" target="_blank">their favorite content</a>. Oh no! &#8220;the remote server returned an error: (408) request timeout!&#8221; He or she then asks the web developer to make the bad error go away.</p>



<p>The &#8220;seasoned&#8221; web developer looks at the error logs, and notices an error: something to do with timeouts. Having no idea what it means, he or she googles the error, and voila! an article gives &#8220;the&#8221; solution for this type of error. It looks something like this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"><p>Locate your nginx config file, or your <code>php.ini</code> file, and increase <code>max_execution_time</code> (or some variation thereof).</p><cite>— some random &#8220;technical&#8221; article on the web</cite></blockquote>



<p>Usually the article will also include some helpful info on how to locate these files, and how to restart the server after you&#8217;re done editing them (as if you shouldn&#8217;t know this already).</p>



<h2 class="wp-block-heading">Let&#8217;s think for a moment about what has happened here</h2>



<p>Web servers (usually Apache, Nginx, etc) are configured by default to wait for the back-end (usually PHP) for about 15 or 30 seconds. If the script doesn&#8217;t respond in that time, the web server kills the script and sends the user on their merry way with an HTTP status code of 408.</p>



<p>When you increase that timeout limit, what you tell the server is that you&#8217;re OK with HTTP requests taking more than 30 seconds. <em>Really?</em> Are you<em> really</em> OK with this?</p>



<p>Do this right now: Count slowly from 1 to 30, to get a feel of what half a minute is like. Would you wait that long for a website to load? Unless you&#8217;re working for a government website, I&#8217;m guessing you probably care about SEO at least a tiny bit.</p>



<p>So please, next time you see a web script timing out, don&#8217;t just blindly increase timeout limits. Dig a little deeper in your log files, do some tests, debug a little. Does the issue occur with all scripts or just with some? What do the PHP-FPM logs say? Are the worker threads up and running? What&#8217;s the memory situation like on your server? Maybe add a few debug prints in your script, or do some profiling?</p>



<p>If you&#8217;re on a CMS like WordPress, try disabling some plugins. Switch to another theme. Tinker with your caches. Try to isolate the component that causes the issue.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p>I don&#8217;t know what&#8217;s wrong with your server. But I know it shouldn&#8217;t be taking over half a minute to respond to an HTTP request. So please, do your job as an engineer, and look for the root cause of the issue.</p>



<p>Same goes for maximum memory, worker threads count etc. Don&#8217;t just increase numbers at random. Look at your traffic first. What amount of worker threads do you need? What do your scripts need in terms of resources? You know that these things use CPU and memory, right?</p>
<p>The post <a href="https://www.alexgeorgiou.gr/error-408/">⌛ How NOT to fix error &#8220;upstream timed out (110: Connection timed out)&#8221;</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/error-408/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>💩 Please, WordPress theme and plugin developers, learn to write proper HTML already&#8230; smh</title>
		<link>https://www.alexgeorgiou.gr/how-to-html-in-wordpress/</link>
					<comments>https://www.alexgeorgiou.gr/how-to-html-in-wordpress/#respond</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Fri, 07 Feb 2020 17:28:38 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[ARIA]]></category>
		<category><![CDATA[code quality]]></category>
		<category><![CDATA[code style]]></category>
		<category><![CDATA[coding standards]]></category>
		<category><![CDATA[HTML]]></category>
		<category><![CDATA[markup]]></category>
		<category><![CDATA[microformats]]></category>
		<category><![CDATA[open graph]]></category>
		<category><![CDATA[output buffering]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[schema.org]]></category>
		<category><![CDATA[string interpolation]]></category>
		<category><![CDATA[templating]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=303</guid>

					<description><![CDATA[<p>Use PHP output buffering in WordPress to interpolate HTML code and dynamic values in a way that's more readable.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/how-to-html-in-wordpress/">💩 Please, WordPress theme and plugin developers, learn to write proper HTML already&#8230; smh</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>I know, I know, I&#8217;ve <a href="https://www.alexgeorgiou.gr/wordpress-php-code-quality/">ranted before about code quality</a>, but today I want to focus more on how we, as WordPress theme and plugin developers, write HTML.</p>



<h2 class="wp-block-heading">Don&#8217;t print out too early</h2>



<p>We&#8217;ve all been there: You&#8217;ve happily opened your PHP tag, <code>&lt;?php</code>, at the beginning of the file, at character #1. Your file is going to contain only code, no markup. Perhaps the file is included before any HTML output has even begun.</p>



<p>Being the savvy PHP developer that you are, you know all too well, that if your code were to output even one extra white-space, that would screw up the page&#8217;s markup. You&#8217;re careful not to use any closing <code>?&gt;</code> tag at the end of the file, to avoid any mishaps. Even a stray linefeed character could trigger output buffering early, and then out goes any chance for other code to modify the HTTP response headers, among other things.</p>



<p>So now you&#8217;re happily defining your functions, classes and other code-y stuff, then all of a sudden, you need to build some markup! Perhaps you&#8217;re printing out a UI element, maybe it&#8217;s a widget, maybe it&#8217;s a shortcode, maybe it&#8217;s even an RSS feed or other XML, but in any case, it&#8217;s too small for you to define an entire <a rel="noreferrer noopener" aria-label="template-part (opens in a new tab)" href="https://developer.wordpress.org/reference/functions/get_template_part/" target="_blank">template-part</a>. Your string is going to be some concatenation of hard-coded markup and dynamic data that&#8217;s currently sitting in variables.</p>



<h2 class="wp-block-heading">How To Meet Ladies</h2>



<p>Everyone says they know HTML. But HTML is easy to get started with, and hard to master.</p>



<p>When writing HTML markup for a theme or plugin in WordPress, your professional-grade code must fulfill a number of simultaneous requirements. Your main concerns should be to:</p>



<ul class="wp-block-list"><li>Write correct HTML5 markup that is SEO friendly, using semantic tags where possible (e.g. <code>&lt;nav&gt;</code>, <code>&lt;article&gt;</code>, <code>&lt;aside&gt;</code>, etc).</li><li>Escape any values properly</li><li><a href="https://www.alexgeorgiou.gr/make-pot-wordpress/">Internationalize strings so they can be translated</a></li><li>Give unique IDs to your elements, and name your classes properly, to assist any CSS and JS assets that go with your markup. Remember, naming things is one of the two hard things in computer science!</li><li>Conform to the <a href="https://make.wordpress.org/core/handbook/best-practices/coding-standards/html/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">WordPress HTML Coding Standards</a>. This is a good standard to follow, because it forces you to be consistent about how to open and close your PHP tags, among other things.</li><li>Depending on use case, optionally add semantic metadata, such as <a rel="noreferrer noopener" href="http://microformats.org/" target="_blank">microformats.org</a>, <a rel="noreferrer noopener" href="https://schema.org/" target="_blank">schema.org</a>, <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://ogp.me/" target="_blank">Open Graph</a>, <a rel="noreferrer noopener" href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA" target="_blank">ARIA</a>, etc</li></ul>



<h2 class="wp-block-heading">A concrete example</h2>



<p>So let&#8217;s say you want to output some HTML form when a shortcode is used. We&#8217;ll give the form a unique ID and we&#8217;ll also embed some default values, coming from the shortcode&#8217;s attributes.</p>



<h3 class="wp-block-heading">Don&#8217;t do this:</h3>



<p>You might be tempted to do the following:</p>



<pre class="wp-block-preformatted"><code>$id = uniqid( 'form' );</code>
<code>$html  = '&lt;form id="' . esc_attr( $id ) . '"&gt;';</code>
<code>$html .= '&lt;input type="text" name="name" placeholder="' . esc_attr( 'Enter your name', 'langdomain' ) . '" value="' . esc_attr( $atts['name'] ) . '" /&gt;';</code>
<code>$html .= '&lt;input type="number" name="age" placeholder="' . esc_attr( 'Enter your age', 'langdomain' ) . '" value="' . absint( $atts['age'] ) . '" /&gt;';</code>
<code>$html .= '&lt;button type="submit"&gt;' . esc_html( 'Submit', 'langdomain' ) . '&lt;/button&gt;';</code>
<code>$html .= '&lt;/form&gt;';</code></pre>



<p>Sadly, a lot of people would say this is fine! I can&#8217;t blame them too much. We&#8217;re all way too used to opening the PHP tag and then never closing it again: we tend to use PHP as if it was Java. And with so many PHP templating libraries out there, it&#8217;s hard to remember that PHP <em>is</em> primarily a templating tool.</p>



<h3 class="wp-block-heading">Do this instead:</h3>



<p>We can combine the awesome templating power of the language with some cleverly nested <a rel="noreferrer noopener" aria-label="output bufferin (opens in a new tab)" href="https://www.php.net/manual/en/book.outcontrol.php" target="_blank">output buffering</a>, and now the above can be re-written as follows:</p>



<pre class="wp-block-preformatted"><code>$id = uniqid( 'form' );</code>

<code>ob_start();</code>
<code>?&gt;</code>
<code>&lt;form id="&lt;?php esc_attr_e( $id ); ?&gt;"&gt;</code>

    <code>&lt;input type="text" name="name" placeholder="&lt;?php esc_attr_e( 'Enter your name', 'langdomain' ); ?&gt;" value="&lt;?php esc_attr_e( $atts['name'] ); ?&gt;" /&gt;</code>
    <code>&lt;input type="number" name="age" placeholder="&lt;?php esc_attr_e( 'Enter your age', 'langdomain' ); ?&gt;" value="&lt;?php esc_attr_e( $atts['age'] ); ?&gt;" /&gt;</code>

    <code>&lt;button type="submit"&gt;&lt;?php esc_html_e( 'Submit', 'langdomain' ); ?&gt;&lt;/button&gt;</code>
<code>&lt;/form&gt;</code>
<code>&lt;?php</code>
<code>return ob_get_clean();</code></pre>



<p>To see a somewhat more complete example, here&#8217;s the above as a gist:</p>



<script src="https://gist.github.com/alex-georgiou/2299b24680e4fe5258ac0285044b8e56.js"></script>



<p>Notice how we start a new output buffer with <code><a rel="noreferrer noopener" aria-label="ob_start() (opens in a new tab)" href="https://www.php.net/manual/en/function.ob-start.php" target="_blank">ob_start()</a></code> and how we then get the resulting string with <code><a rel="noreferrer noopener" aria-label="ob_get_clean() (opens in a new tab)" href="https://www.php.net/manual/en/function.ob-get-clean.php" target="_blank">ob_get_clean()</a></code>. Notice also how we&#8217;ve swapped <code><a rel="noreferrer noopener" aria-label="esc_html() (opens in a new tab)" href="https://developer.wordpress.org/reference/functions/esc_html/" target="_blank">esc_html()</a></code> with <code><a rel="noreferrer noopener" aria-label="esc_html_e() (opens in a new tab)" href="https://developer.wordpress.org/reference/functions/esc_html_e/" target="_blank">esc_html_e()</a></code> and <code><a rel="noreferrer noopener" aria-label="esc_attr() (opens in a new tab)" href="https://developer.wordpress.org/reference/functions/esc_attr/" target="_blank">esc_attr()</a></code> with <code><a rel="noreferrer noopener" aria-label="esc_attr_e() (opens in a new tab)" href="https://developer.wordpress.org/reference/functions/esc_attr_e/" target="_blank">esc_attr_e()</a></code>. Similarly, we would swap <code><a rel="noreferrer noopener" aria-label="sprintf() (opens in a new tab)" href="https://www.php.net/manual/en/function.sprintf.php" target="_blank">sprintf()</a></code> with <code><a rel="noreferrer noopener" aria-label="printf (opens in a new tab)" href="https://www.php.net/manual/en/function.printf.php" target="_blank">printf</a>()</code>, <code><a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://developer.wordpress.org/reference/functions/checked/" target="_blank">checked( $value, true, false )</a></code> with <code>checked( $value, true )</code>, and so on.</p>



<p><strong>PRO TIP:</strong> If your code throws an exception, you can use <code><a rel="noreferrer noopener" aria-label="ob_end_clean() (opens in a new tab)" href="https://www.php.net/manual/en/function.ob-end-clean.php" target="_blank">ob_end_clean()</a></code> in your <code>catch</code> clause to cancel your buffer.</p>



<h2 class="wp-block-heading">&#8220;Why does it even matter&#8221;, you ask?</h2>



<p>Well, there&#8217;s a few advantages with the second form of our, err&#8230;, form!</p>



<ul class="wp-block-list"><li><strong>Readability.</strong> I find the second piece of code to be much more readable.</li><li><strong>Syntax coloring.</strong> Yes, some editors do perform HTML parsing and show visual queues related to the syntax of HTML inside strings. But not every editor does this, and the ones that do, cannot do this perfectly.</li><li><strong>No quote hell.</strong> No need to count opening and closing <code>'</code> characters, interpolated with <code>"</code> characters while printing HTML attributes. This business can quickly get confusing, and is a common source of errors.</li><li><strong>Show off</strong> your mad PHP output buffering skillz! Your average front-end developer will be astonished at your professional-looking code!</li></ul>



<p>Now, if only we were allowed to use shorthand PHP tags, our code could be even nicer. Sadly, <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#no-shorthand-php-tags" target="_blank">we can&#8217;t</a>.</p>



<p><code>?&gt;</code></p>
<p>The post <a href="https://www.alexgeorgiou.gr/how-to-html-in-wordpress/">💩 Please, WordPress theme and plugin developers, learn to write proper HTML already&#8230; smh</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/how-to-html-in-wordpress/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>💩 Your WordPress PHP code stinks! Here&#8217;s why.</title>
		<link>https://www.alexgeorgiou.gr/wordpress-php-code-quality/</link>
					<comments>https://www.alexgeorgiou.gr/wordpress-php-code-quality/#comments</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Mon, 22 Jul 2019 18:12:52 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[code quality]]></category>
		<category><![CDATA[code style]]></category>
		<category><![CDATA[envato]]></category>
		<category><![CDATA[grunt]]></category>
		<category><![CDATA[grunt-phpcs]]></category>
		<category><![CDATA[phan]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[phpcbf]]></category>
		<category><![CDATA[phpcs]]></category>
		<category><![CDATA[phpdoc]]></category>
		<category><![CDATA[phpunit]]></category>
		<category><![CDATA[requirements]]></category>
		<category><![CDATA[themeforest]]></category>
		<category><![CDATA[wordpress]]></category>
		<category><![CDATA[wordpress.org]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=282</guid>

					<description><![CDATA[<p>Developing a WordPress theme or plugin? Learn how to use phan and phpcs to improve the quality of your WordPress PHP code.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/wordpress-php-code-quality/">💩 Your WordPress PHP code stinks! Here&#8217;s why.</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>In software development, as in all forms of engineering, we strive to produce results that satisfy a multitude of constraints, some more obvious than others. High <em>code quality</em> should be in your list of constraints when you write WordPress PHP code. And there are tools out there that can help you!</p>



<h3 class="wp-block-heading">First, some theory</h3>



<p>In a fantasy world where unicorns poop chocolate fudge and everything is awesome, it all goes down somewhat like this: You talk with the client or end user, and you gather up a list of things they want. You collect and number these in a <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://en.wikipedia.org/wiki/User_requirements_document" target="_blank">requirements document</a>, braking them up into <strong>Functional</strong> requirements, <strong>Non-functional</strong> requirements, and other <strong>Design constraints</strong>. Then, you design a solution, write your tests, and implement your code. Once all of the requirements in the document are met, you are <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://www.agilealliance.org/glossary/definition-of-done/" target="_blank">done</a>, and you get paid. Easy! Deviate from this simple methodology <strong>at your own peril.</strong> (Oh, and you also then need to maintain, fix and improve the code you&#8217;ve written.)</p>



<p>As a software engineer, you should know that in addition to what the client asks for, there are always <em>implicit</em> requirements. For example, your code must be <strong>readable</strong> and <strong>maintainable</strong>, and it must be <strong>reliable</strong> (read: not too buggy). All of these are aspects of <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://en.wikipedia.org/wiki/Software_quality" target="_blank">code quality</a>. A client or end user will never ask you for these, but you should have them in your list of non-functional requirements.</p>



<p>Professional developers write simple, clear code where a novice would write complex, intricate code to solve that same problem. Sure, a lot of this is just a matter of practice and experience.</p>



<p>Dev tools can help you with code quality whether you&#8217;re novice or pro. They help you discover issues that you don&#8217;t know about, and they help you discover issues that you do know about, faster!</p>



<p>If you are developing themes or plugins, here&#8217;s two tools that you definitely want to use as a professional WordPress developer:</p>



<h3 class="wp-block-heading">Improve WordPress PHP <a href="https://en.wikipedia.org/wiki/Code_smell" target="_blank" rel="noreferrer noopener" aria-label="code smell (opens in a new tab)">code smell</a> with CodeSniffer</h3>



<p>There are some <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://codex.wordpress.org/WordPress_Coding_Standards" target="_blank">styling guidelines</a> that all WordPress developers should follow. Especially if you are aspiring to upload your code to wordpress.org, or to the Envato Market (ThemeForest or CodeCanyon), then you definitely want to follow these guidelines. Generally, these get encoded into your muscle memory pretty quickly, but it&#8217;s always a great idea to have a tool like <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://github.com/squizlabs/PHP_CodeSniffer" target="_blank">PHP CodeSniffer</a> that double-checks your code style.</p>



<p>On my Ubuntu machine, I was able to install PHP CodeSniffer easily with:</p>



<pre class="wp-block-preformatted">sudo apt install php-codesniffer</pre>



<p>Then, it&#8217;s just a matter of loading the <a href="https://github.com/WordPress/WordPress-Coding-Standards" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">WordPress-specific set of rules</a>. Go to your home directory and clone the project:</p>



<pre class="wp-block-preformatted">git clone https://github.com/WordPress/WordPress-Coding-Standards ~/wpcs</pre>



<p>And tell CodeSniffer the location of these rules:</p>



<pre class="wp-block-preformatted">sudo phpcs --config-set installed_paths ~/wpcs</pre>



<p>Check that <a href="https://github.com/WordPress/WordPress-Coding-Standards#standards-subsets" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">the new rules</a> are added into CodeSniffer with:</p>



<pre class="wp-block-preformatted">phpcs -i</pre>



<p>Congratulations! You can now check your plugin for code style with a command such as:</p>



<pre class="wp-block-preformatted">phpcs --standard=Wordpress-Core /path/to/source/code/root/dir</pre>



<p>If, <a href="https://www.alexgeorgiou.gr/increasingly-verbose-gruntfile/">like me</a>, you&#8217;re using <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://gruntjs.com/" target="_blank">grunt</a> for your build process, then there&#8217;s a nifty Grung plugin, <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://github.com/SaschaGalley/grunt-phpcs" target="_blank">grunt-phpcs</a>. Just make sure to specify <code>WordPress-core</code> as the rule set. Here&#8217;s a <code>grunt</code> target that you might use:</p>



<pre class="wp-block-preformatted">phpcs: {
    plugin: {
        src: ['src/**/*.php']
    },
    options: {
        bin: '/usr/bin/phpcs',
        standard: 'WordPress-core'
    }
}</pre>



<p>Run it against your code and you will get a number of improvement suggestions. Some of these can be applied automatically with <code>PHPCBF</code> (the <strong>PHP Code Beautifier and Fixer</strong>), or you can go through the list manually and apply each suggestion as you see fit. Many of the suggestions will be related to code indentation, but you will also see a large number of other suggestions that are more critical.</p>



<h3 class="wp-block-heading">Improve WordPress PHP code correctness with phan</h3>



<p>PHP is a very lenient language. It will let you get away with murder. This is something that novice programmers often enjoy. For professionals it&#8217;s a nightmare, as it makes spotting errors harder. PHP has thus gained somewhat of a notoriety for being a bad language, and is the butt of some <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://www.reddit.com/r/ProgrammerHumor/comments/6k40cb/working_at_pornhub/" target="_blank">clever jokes</a>.</p>



<p>This isn&#8217;t something to worry about. It has happened to many respectable languages, including JavaScript. So nowadays we have <code>use strict</code>, which lets us only use <a rel="noreferrer noopener" aria-label="The Good Parts (opens in a new tab)" href="http://shop.oreilly.com/product/9780596517748.do" target="_blank">The Good Parts</a> of the language. You can do something similar with PHP.</p>



<p>My point is that <a href="https://en.wikipedia.org/wiki/Static_program_analysis">static code analysis</a> is not the hero you want, but it&#8217;s definitely the hero you deserve. Enter <code><a rel="noreferrer noopener" aria-label="phan (opens in a new tab)" href="https://github.com/phan/phan" target="_blank">phan</a></code>:</p>



<figure class="wp-block-embed-youtube wp-block-embed is-type-video is-provider-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="PHP in 2018 by the Creator of PHP" width="840" height="473" src="https://www.youtube.com/embed/rKXFgWP-2xQ?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div><figcaption><a href="https://en.wikipedia.org/wiki/Rasmus_Lerdorf">Rasmus Lerdorf</a>, the creator of PHP, discusses static code analysis with phan</figcaption></figure>



<p>First, install it. It&#8217;s straightforward to install <code>phan</code> with <code>composer</code>:</p>



<pre class="wp-block-preformatted">composer require phan/phan</pre>



<p>Now <code>phan</code> lives in your project&#8217;s <code>vendor/</code> dir.</p>



<p>Next, create a configuration under your project dir, in <code>.phan/config.php</code>. This will tell <code>phan</code> what settings you want to run it with. Start with the <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://github.com/phan/phan/wiki/Getting-Started#creating-a-config-file" target="_blank">example given here</a>, and set your source code directories.</p>



<p>You will also want to point to some third-party code, including the directory of your WordPress installation, since your code will invariably use WordPress functions and types.</p>



<p>You can also use this config file to exclude some rules, so that <code>phan</code> does not check for them. <a href="https://github.com/phan/phan/blob/master/.phan/plugins/README.md">Here&#8217;s the complete list of <code>phan</code> <em>plugins</em></a>.</p>



<p>When all is set, you can call <code>phan</code> on your code with:</p>



<pre class="wp-block-preformatted">vendor/bin/phan</pre>



<p>I don&#8217;t care how pro you are, you will definitely get a list of suggestions on how to improve your code.</p>



<h3 class="wp-block-heading">Conclusion</h3>



<p>Using <code>phpcs</code> and <code>phan</code> together, you will avoid a large number of errors that would otherwise likely go undetected. These include errors with translator comments over strings, <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://phpdoc.org/" target="_blank">phpDoc</a> formatting errors, variable type errors, errors related to sentinel values such as <code>null</code>, and array indexing errors.</p>



<p>Always use these two tools together with <code>phpunit</code> when you write code. You will write more readable, maintainable, correct and robust code that you can feel confident about.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/wordpress-php-code-quality/">💩 Your WordPress PHP code stinks! Here&#8217;s why.</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/wordpress-php-code-quality/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>👻 Monitor analytics of a public WordPress plugin using PhantomJS</title>
		<link>https://www.alexgeorgiou.gr/scrape-daily-analytics-wordpress-plugin/</link>
					<comments>https://www.alexgeorgiou.gr/scrape-daily-analytics-wordpress-plugin/#respond</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Sat, 09 Jun 2018 10:14:06 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[analytics]]></category>
		<category><![CDATA[csv]]></category>
		<category><![CDATA[installations]]></category>
		<category><![CDATA[issues]]></category>
		<category><![CDATA[metric]]></category>
		<category><![CDATA[phantomjs]]></category>
		<category><![CDATA[plugin]]></category>
		<category><![CDATA[scraper]]></category>
		<category><![CDATA[statistics]]></category>
		<category><![CDATA[usage]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=262</guid>

					<description><![CDATA[<p>A script to scrape and save daily usage statistics for a WordPress plugin for later use in analytics.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/scrape-daily-analytics-wordpress-plugin/">👻 Monitor analytics of a public WordPress plugin using PhantomJS</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>When a WordPress plugin is hosted on WordPress.org, some basic usage statistics are shown on its page. These can be used in your plugin analytics.</p>
<h2>The problem</h2>
<p>I needed an easy way to gather up some of these daily stats into a <code>.csv</code> file for later analysis and monitoring.</p>
<p>Data I was interested in:</p>
<ul>
<li>The approximate current number of installs (this number is rounded)</li>
<li>The number of yesterday&#8217;s new downloads (today&#8217;s downloads depends on what time you run the script)</li>
<li>The number of total downloads</li>
<li>The number of support issues reported within the last month</li>
<li>The number of support issues resolved within the last month</li>
</ul>
<p>Most of these numbers are found in the static HTML, but the download counts are added in later via JavaScript. Hence the need for phantomjs.</p>
<h2>The solution</h2>
<p>A good place to go to gather these statistics is the <em>advanced</em> view of WordPress.org. For example, to see some basic statistics for my plugin, <a href="http://dashed-slug.net/bitcoin-altcoin-wallets-wordpress-plugin/" target="_blank" rel="noopener">Bitcoin and Altcoin Wallets</a>, you could visit:</p>
<pre><a href="https://wordpress.org/plugins/wallets/advanced/" target="_blank" rel="noopener">https://wordpress.org/plugins/wallets/advanced/</a></pre>
<p>Scraping a static HTML page is easy. For example, it could have been done in PHP using <a href="https://code.google.com/archive/p/phpquery/" target="_blank" rel="noopener">phpQuery</a>. But I decided to use this opportunity as a gentle introduction into learning <a href="http://phantomjs.org/" target="_blank" rel="noopener">PhantomJS</a>. PhantomJS is very useful for black box testing, but it is also suitable for scraping data. Additionally, I would like to scrape the download counts that are not available as part of the static HTML.</p>
<p>The script is now posted on <a href="https://github.com/alex-georgiou/wordpress-plugin-stats-scraper" target="_blank" rel="noopener">github</a>. See the <code>README.md</code> file for usage instructions.</p>
<p style="text-align: center;"><a href="https://github.com/alex-georgiou/wordpress-plugin-stats-scraper" target="_blank" rel="noopener">https://github.com/alex-georgiou/wordpress-plugin-stats-scraper</a></p>
<h2>Running it daily</h2>
<p>The plugin is suitable for running via cron. You should run it once a day.</p>
<p>Keep in mind that tasks running with cron may not have the working directory you expect. And the path variable might be empty. So make sure to specify full paths. Here&#8217;s how it looks like in my crontab:</p>
<pre>0 1 * * * QT_QPA_PLATFORM=offscreen /usr/bin/phantomjs /home/alexg/wordpress-plugin-stats-scraper/wordpress-plugin-stats-scraper.js wallets /home/alexg/wallets-stats.csv</pre>
<p>&nbsp;</p>
<p>The post <a href="https://www.alexgeorgiou.gr/scrape-daily-analytics-wordpress-plugin/">👻 Monitor analytics of a public WordPress plugin using PhantomJS</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/scrape-daily-analytics-wordpress-plugin/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
