<?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/"
	
	xmlns:georss="http://www.georss.org/georss"
	xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
	>

<channel>
	<title>Ricard Torres dev</title>
	<atom:link href="https://ricard.dev/feed/" rel="self" type="application/rss+xml" />
	<link>https://ricard.dev/</link>
	<description>HTML, CSS and JavaScript</description>
	<lastBuildDate>Wed, 21 Dec 2022 09:10:11 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.1.1</generator>
	<atom:link rel='hub' href='https://ricard.dev/?pushpress=hub'/>
<site xmlns="com-wordpress:feed-additions:1">12590159</site>	<item>
		<title>Scaling Mastodon: moving media assets to Object Storage</title>
		<link>https://ricard.dev/scaling-mastodon-moving-media-assets-to-object-storage/</link>
					<comments>https://ricard.dev/scaling-mastodon-moving-media-assets-to-object-storage/#comments</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Mon, 19 Dec 2022 07:10:59 +0000</pubDate>
				<category><![CDATA[Mastodon]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9616</guid>

					<description><![CDATA[<p>When I installed my private personal Mastodon instance y quickly noticed Mastodon has storage issues, so I tried to improve it by setting up a bunch of cron jobs to purge cached files. All the commands used are out of the box, no hacking involved but it's something that's not really highlighted in the installation [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/scaling-mastodon-moving-media-assets-to-object-storage/">Scaling Mastodon: moving media assets to Object Storage</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p><a href="https://ricard.dev/hosting-my-own-mastodon-instance/">When I installed my private personal Mastodon instance</a> y quickly noticed Mastodon has storage issues, so <a href="https://ricard.dev/improving-mastodons-disk-usage/">I tried to improve it</a> by setting up a bunch of cron jobs to purge cached files.</p>



<p>All the commands used are out of the box, no hacking involved but it's something that's not really highlighted in the installation guides and it's quite important.</p>



<p>I've been battling with a steady reduction of disk space for the last month. Not terrible but not great.</p>



<p>After reading a lot from other users that the actual costs of moving the cached files with a CDN in front were so low I deiced to make the jump. External object storage provider was the next step.</p>



<figure class="wp-block-image size-full"><img decoding="async" width="1440" height="1766" src="https://ricard.dev/wp-content/uploads/2022/12/backblaze-B2-S3.jpg" alt="" class="wp-image-9640" srcset="https://ricard.dev/wp-content/uploads/2022/12/backblaze-B2-S3.jpg 1440w, https://ricard.dev/wp-content/uploads/2022/12/backblaze-B2-S3-768x942.jpg 768w, https://ricard.dev/wp-content/uploads/2022/12/backblaze-B2-S3-1252x1536.jpg 1252w" sizes="(max-width: 1440px) 100vw, 1440px" /></figure>



<h2>Why?</h2>



<p>Disk space, obviously 😄 The reality is that VPS providers don't give you that much disk space. My VPS provider Linode, offers great shared CPU plans such as:</p>



<ul>
<li>5$ for 1 CPU, 1GB of RAM and 25 GB of disk space</li>
</ul>



<p>It's quite cheap, if you ask me 💜</p>



<p>But that's not enough for Mastodon's media usage. Nowadays the space should be cheap, and it actually is, but you have to use object storage. Regular SSD disk space was not meant for this use case.</p>



<p>Linode also offers object storage, nice. In fact (at the time of the writing) they offer 250GB for only 5$ extra. Five dollars is not much but it's a flat fee 😢 If you go with AWS or BackBlaze the price starts from 0$ 🤩 Unless you already have two hundred GB of media, you're probably better off with a dynamically priced provider to start with just like I did 😉</p>



<h2>What's my setup?</h2>



<ul>
<li><a href="https://www.backblaze.com/">BackBlaze</a>: the object storage provider with lower prices than Amazon's famous S3 but it has a compatible S3 API (be mindful when creating an account, select the correct region US vs EU). It has partnership with <a href="https://www.cloudflare.com/">Cloudflare</a> for very cheap close to free CDN. You need however to move your domain's DNS to Cloudflare if you want to use their CDN.</li>



<li><a href="https://aws.amazon.com/cloudfront/pricing/">CloudFront</a> from Amazon Web Services: I didn't move my domain's DNS to Cloudflare so I used AWS. It has an absurdly high free tier which I can't possibly make use of. My CDN forecasted usage is less than 2% of the free tier 🤭</li>



<li><a href="https://aws.amazon.com/cli/">AWS CLI</a>: I've used to do the initial sync between my disk and the S3 bucket in BackBlaze. It takes a while, sit tight and grab a coffee.</li>
</ul>



<h2>Configuring your .env.production file</h2>



<p>From your Mastodon folder <code>~/live/.env.production</code></p>



<p>Here's what I had to add, customize the values based on your S3 and URL's:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-1" data-shcb-language-name="Bash" data-shcb-language-slug="bash"><div><code class="hljs language-bash">S3_ENABLED=<span class="hljs-literal">true</span>
S3_PROTOCOL=https
S3_ENDPOINT=https://XXXXX.backblazeb2.com
S3_HOSTNAME=XXXX.backblazeb2.com
S3_BUCKET=XXXX
AWS_ACCESS_KEY_ID=XXXX
AWS_SECRET_ACCESS_KEY=XXXX
S3_ALIAS_HOST=media.ricard.social</code></div><small class="shcb-language" id="shcb-language-1"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">Bash</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">bash</span><span class="shcb-language__paren">)</span></small></pre>


<h2>What's the Mastodon retention policy?</h2>



<p>I've set up the purging of attachments and previews to <strong>4 days</strong> running every night.</p>



<p>Before doing the S3 migration I had it to one day but considering how cheap it is, seven feels like a nice number.</p>



<h2>How cheap is the whole thing?</h2>



<p>I'm honestly surprised how low the cost of the whole move is. For the past <strong>2 weeks</strong>:</p>



<ol>
<li><strong>BackBlaze S3 bucket</strong>: 
<ul>
<li>💾 Disk usage (38.7 GB): 0.04€</li>



<li>📈 Transactions: 0.08€</li>



<li>📉 Downloaded GB: 0€</li>



<li>💰 Cost: <strong>0.13€</strong></li>
</ul>
</li>



<li><strong>Amazon Web Services CloudFront CDN</strong>: 
<ul>
<li>📈 Transactions: 1% from the free tier</li>



<li>📉 Downloaded GB: 1.2% from the free tier</li>



<li>💰 Monthly cost: <strong>free</strong></li>
</ul>
</li>
</ol>



<h2>Conclusion</h2>



<p>There is no doubt a move to object storage should be your default if you attempt to host a Mastodon instance. It doesn't matter if for a private single-user, 10 users, or 10.000 users. The cost of hosting the media files in a S3-compatible bucket will be far cheaper than locally on the same server.</p>



<p><strong>Have you moved your files to an object storage?</strong> <strong>Have you encountered any issues?</strong></p>



<p><strong>Any other cloud storage provider you recommend?</strong></p>
<p>The post <a rel="nofollow" href="https://ricard.dev/scaling-mastodon-moving-media-assets-to-object-storage/">Scaling Mastodon: moving media assets to Object Storage</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/scaling-mastodon-moving-media-assets-to-object-storage/feed/</wfw:commentRss>
			<slash:comments>37</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9616</post-id>	</item>
		<item>
		<title>How to add web mentions to your WordPress site</title>
		<link>https://ricard.dev/how-to-add-web-mentions-to-your-wordpress-site/</link>
					<comments>https://ricard.dev/how-to-add-web-mentions-to-your-wordpress-site/#comments</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Tue, 29 Nov 2022 07:39:29 +0000</pubDate>
				<category><![CDATA[Mastodon]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9609</guid>

					<description><![CDATA[<p>A Webmention enables enables cross-site conversations. Letting site A know that site B is mentioning them (be it via link, a like, a re-post... there are many times of mentions). One of the cool use cases is to link your Fediverse account (Mastodon, in my case) with your blog. So every time you share a [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-add-web-mentions-to-your-wordpress-site/">How to add web mentions to your WordPress site</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>A <a href="https://indieweb.org/Webmention">Webmention</a> enables enables cross-site conversations. Letting site A know that site B is mentioning them (be it via link, a like, a re-post... there are many times of mentions).</p>



<p>One of the cool use cases is to link your Fediverse account (<a href="https://ricard.social/@ricard">Mastodon, in my case</a>) with your blog. So every time you share a link from your blog and people react to it, these reactions (comments, or likes) will show up as native comments in WordPress. Pretty cool, huh ? 😁</p>



<h2>Requirement: Enable pingbacks</h2>



<p>There are 2 things you should enable in Settings &gt; Discussion</p>



<ol>
<li>Attempt to notify any blogs linked to from the post</li>



<li>Allow link notifications from other blogs (pingbacks and trackbacks) on new posts</li>
</ol>



<p>Otherwise even if webmentions hit your site, you wouldn't see them.</p>



<h2>Requirement: Install the Webmention plugin</h2>



<figure class="wp-block-embed is-type-wp-embed is-provider-plugin-directory wp-block-embed-plugin-directory"><div class="wp-block-embed__wrapper">
<blockquote class="wp-embedded-content" data-secret="3J27yIhGf1"><a href="https://wordpress.org/plugins/webmention/">Webmention</a></blockquote><iframe class="wp-embedded-content" sandbox="allow-scripts" security="restricted" title="&#8220;Webmention&#8221; &#8212; Plugin Directory" src="https://wordpress.org/plugins/webmention/embed/#?secret=qUIc4PTjIX#?secret=3J27yIhGf1" data-secret="3J27yIhGf1" width="500" height="282" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe>
</div></figure>



<h2>Requirement: Bridgy</h2>



<p>This free service is used to ping your site when, in my case my Mastodon account, makes a post linking to one of my articles. The likes and mentions from that initial post on Mastodon will be pinged back to my WordPress site.</p>



<p><a href="https://brid.gy/">https://brid.gy/</a></p>



<h2>Requirement: Install Semantic-Linkbacks</h2>



<p>It modifies the way your comments are printed, adding the mentions, likes in a nice way. It includes CSS and JS for these rich features.</p>



<figure class="wp-block-embed is-type-wp-embed is-provider-plugin-directory wp-block-embed-plugin-directory"><div class="wp-block-embed__wrapper">
<blockquote class="wp-embedded-content" data-secret="pwh65gBufk"><a href="https://wordpress.org/plugins/semantic-linkbacks/">Semantic-Linkbacks</a></blockquote><iframe class="wp-embedded-content" sandbox="allow-scripts" security="restricted" title="&#8220;Semantic-Linkbacks&#8221; &#8212; Plugin Directory" src="https://wordpress.org/plugins/semantic-linkbacks/embed/#?secret=6pOUDJoC2m#?secret=pwh65gBufk" data-secret="pwh65gBufk" width="500" height="282" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe>
</div></figure>



<h2>Conclusion</h2>



<p>It might need a little tweaking to show the comments to your liking, if the out of the box doesn't please you. A part from that, quite straightforward.</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-add-web-mentions-to-your-wordpress-site/">How to add web mentions to your WordPress site</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/how-to-add-web-mentions-to-your-wordpress-site/feed/</wfw:commentRss>
			<slash:comments>12</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9609</post-id>	</item>
		<item>
		<title>Improving Mastodon&#8217;s disk usage</title>
		<link>https://ricard.dev/improving-mastodons-disk-usage/</link>
					<comments>https://ricard.dev/improving-mastodons-disk-usage/#comments</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Tue, 08 Nov 2022 13:44:24 +0000</pubDate>
				<category><![CDATA[Mastodon]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9204</guid>

					<description><![CDATA[<p>Mastodon's built-in CLI gives you the availability to clean attachments and previews from remote accounts, purging the disk cache. This is fantastic and you couldn't possible survive with out it. My current crontab looks something like this: I basically run these 3 jobs at 3AM, 4AM and 5AM every day to keep the single-user instance [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/improving-mastodons-disk-usage/">Improving Mastodon&#8217;s disk usage</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p><a href="https://docs.joinmastodon.org/admin/tootctl/">Mastodon's built-in CLI</a> gives you the availability to clean attachments and previews from remote accounts, purging the disk cache. <strong>This is fantastic</strong> and you couldn't possible survive with out it.</p>



<p>My current <code>crontab</code> looks something like this:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-2" data-shcb-language-name="Bash" data-shcb-language-slug="bash"><div><code class="hljs language-bash">0 3 * * * RAILS_ENV=production /home/mastodon/live/bin/tootctl media remove --days 1
0 4 * * * RAILS_ENV=production /home/mastodon/live/bin/tootctl preview_cards remove --days 1
0 5 * * * RAILS_ENV=production /home/mastodon/live/bin/tootctl statuses remove --days 1</code></div><small class="shcb-language" id="shcb-language-2"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">Bash</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">bash</span><span class="shcb-language__paren">)</span></small></pre>


<p>I basically run these 3 jobs at 3AM, 4AM and 5AM every day to keep the single-user instance thin. It works, the size is kept down to something like this.</p>


<pre class="wp-block-code" aria-describedby="shcb-language-3" data-shcb-language-name="plaintext" data-shcb-language-slug="plaintext"><div><code class="hljs language-plaintext">Attachments:	494 MB (0 Bytes local)
Custom emoji:	43.4 MB (0 Bytes local)
Preview cards:	13.3 MB</code></div><small class="shcb-language" id="shcb-language-3"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">plaintext</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">plaintext</span><span class="shcb-language__paren">)</span></small></pre>


<p><strong>Q: Why do you run the commands cleaning 1 day old media?</strong><br>A: My server doesn't have much space and I want to keep it as lean as possible.</p>



<h2>How about avatars and headers?</h2>



<p>Here's where things get dissapointing, a <a href="https://github.com/mastodon/mastodon/issues/9567">GitHub issue from 2018</a> outlines the problem of caching avatars and headers for the the users from outside your instance. Yes, it makes sense that these are downloaded to your instance but it doesn't make sense that are kept forever (?).</p>



<p>There should be a way to purge them, to check if these are accounts you're interacting with or not. In a way you have:</p>


<pre class="wp-block-code"><div><code class="hljs">tootctl accounts refresh --all</code></div></pre>


<p>Which does:</p>



<blockquote class="wp-block-quote">
<p>Refetch remote user data and files for one or multiple accounts.</p>
</blockquote>



<p>But this is <strong>not</strong> good enough ❌</p>



<p>You have yet another command at your disposal:</p>


<pre class="wp-block-code"><div><code class="hljs">tootctl accounts cull</code></div></pre>


<blockquote class="wp-block-quote">
<p>Remove remote accounts that no longer exist. Queries every single remote account in the database to determine if it still exists on the origin server, and if it doesn't, then remove it from the database. Accounts that have had confirmed activity within the last week are excluded from the checks, in case the server is just down.</p>
</blockquote>



<h3>Again, this is not good enough ❌</h3>



<p>I refuse to believe that after running these commands I'm still stuck with:</p>



<blockquote class="wp-block-quote">
<p></p>
</blockquote>


<pre class="wp-block-code" aria-describedby="shcb-language-4" data-shcb-language-name="plaintext" data-shcb-language-slug="plaintext"><div><code class="hljs language-plaintext">Avatars:	920 MB (61.3 KB local)
Headers:	1.95 GB (101 KB local)</code></div><small class="shcb-language" id="shcb-language-4"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">plaintext</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">plaintext</span><span class="shcb-language__paren">)</span></small></pre>


<p>I couldn't care less for user headers 🤷‍♂️</p>



<h3>💡Idea: replace all headers with 1x1 pixel </h3>



<blockquote class="wp-block-quote">
<p><strong>Update</strong>: I've ended up simply removing all the files inside the <code>accounts/headers</code> folder. Nothing seems to break and it's just more fast and clean ✅</p>
<cite>Edited: November 14th 2022</cite></blockquote>



<p>What if we replace all header files (jpg, png, jpeg, webp...) with the smallest image file possible? A 1x1 pixel file.</p>



<p>Yes, you would still have thousands of files but taking way less space.</p>



<p>Well, that's exactly what I did. A small <strong>bash script</strong> (I'm no expert!) that loops through all the image files inside the <code>cache/headers</code> folder and replaces them with a pixel.</p>


<pre class="wp-block-code" aria-describedby="shcb-language-5" data-shcb-language-name="Bash" data-shcb-language-slug="bash"><div><code class="hljs language-bash"><span class="hljs-meta">#!/bin/bash
</span>
<span class="hljs-keyword">for</span> file <span class="hljs-keyword">in</span> $(find /home/mastodon/live/public/system/cache/accounts/headers -<span class="hljs-built_in">type</span> f \( -iname \*.jpg -o -iname \*.jpeg -o -iname \*.png -o -iname \*.webp \) -<span class="hljs-built_in">type</span> f); <span class="hljs-keyword">do</span>
  rm <span class="hljs-string">"<span class="hljs-variable">$file</span>"</span>

  <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">${file: -5}</span>"</span> == <span class="hljs-string">".jpeg"</span> ]
  <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"This is a JPEG! <span class="hljs-variable">$file</span>"</span>
    SOURCE=<span class="hljs-string">"pixel.jpeg"</span>
  <span class="hljs-keyword">fi</span>

  <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">${file: -4}</span>"</span> == <span class="hljs-string">".jpg"</span> ]
  <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"This is a JPG! <span class="hljs-variable">$file</span>"</span>
    SOURCE=<span class="hljs-string">"pixel.jpg"</span>
  <span class="hljs-keyword">fi</span>

  <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">${file: -4}</span>"</span> == <span class="hljs-string">".png"</span> ]
  <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"This is a PNG! <span class="hljs-variable">$file</span>"</span>
    SOURCE=<span class="hljs-string">"pixel.png"</span>
  <span class="hljs-keyword">fi</span>

  <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">${file: -5}</span>"</span> == <span class="hljs-string">".webp"</span> ]
  <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"This is a webP! <span class="hljs-variable">$file</span>"</span>
    SOURCE=<span class="hljs-string">"pixel.webp"</span>
  <span class="hljs-keyword">fi</span>

  cp <span class="hljs-string">"<span class="hljs-variable">$SOURCE</span>"</span> <span class="hljs-string">"<span class="hljs-variable">$file</span>"</span>
<span class="hljs-keyword">done</span></code></div><small class="shcb-language" id="shcb-language-5"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">Bash</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">bash</span><span class="shcb-language__paren">)</span></small></pre>


<p><a href="https://github.com/quicoto/mastodon-cache-buster">GitHub repo with the code ready for you to checkout</a></p>



<h4>Did it work?</h4>



<p>Yes! The <code>headers</code> folder has considerably reduced (for a single-user instance):</p>


<pre class="wp-block-code"><div><code class="hljs">2GB ➡️ 250MB</code></div></pre>


<p>Nothing seems to be broken. The user profiles load, the images load, the only thing is that they're the replaced 1 pixel images instead of the originally cached header.</p>



<p>⚠️ The only thing is that Mastodon's built in usage check still thinks I have 2GB in headers. My guess is the size of the images must be stored in the database (?)</p>


<pre class="wp-block-code"><div><code class="hljs">tootctl media usage</code></div></pre>


<h3>💡 Re-compress instead of replace</h3>



<p>We can also re-compress the files, if you don't want to delete the headers or avatars. Personally I run the following on the avatars cache:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-6" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript">find -name <span class="hljs-string">'*.jpg'</span> -print0 | xargs <span class="hljs-number">-0</span> jpegoptim --verbose --preserve --threshold=<span class="hljs-number">1</span> --max=<span class="hljs-number">45</span>
find -name <span class="hljs-string">'*.jpeg'</span> -print0 | xargs <span class="hljs-number">-0</span> jpegoptim --verbose --preserve --threshold=<span class="hljs-number">1</span> --max=<span class="hljs-number">45</span>
find -name <span class="hljs-string">'*.png'</span> -print0 | xargs <span class="hljs-number">-0</span> pngquant --verbose --ext=.png --force --speed <span class="hljs-number">10</span> --quality <span class="hljs-number">45</span><span class="hljs-number">-50</span> --skip-<span class="hljs-keyword">if</span>-larger</code></div><small class="shcb-language" id="shcb-language-6"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h2>Conclusion</h2>



<p>Keeping the size of a single-user Mastodon is not trivial.</p>



<p>Had I known this before getting started I would have probably installed a <a href="https://pleroma.social/">Pleroma</a> or <a href="https://akkoma.social/">Akkoma</a> instance instead. Which are way more lightweight. Granted <strong>the UI is not as good</strong> (if you want multi-column layout) but maybe you don't need all Mastodon's features. I am currently too invested to switch but I would highly encourage you to check out Pleroma and Akkoma (a fork) before installing Mastodon.</p>



<p><strong>Do you have any other tips on how to keep a Mastodon instance lean?</strong></p>



<p class="has-black-background-color has-background">⚠️ Don't miss the follow up post: <a href="https://ricard.dev/scaling-mastodon-moving-media-assets-to-object-storage/">Scaling Mastodon: moving media assets to Object Storage</a></p>
<p>The post <a rel="nofollow" href="https://ricard.dev/improving-mastodons-disk-usage/">Improving Mastodon&#8217;s disk usage</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/improving-mastodons-disk-usage/feed/</wfw:commentRss>
			<slash:comments>35</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9204</post-id>	</item>
		<item>
		<title>Hosting my own Mastodon instance</title>
		<link>https://ricard.dev/hosting-my-own-mastodon-instance/</link>
					<comments>https://ricard.dev/hosting-my-own-mastodon-instance/#comments</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Sun, 23 Oct 2022 10:34:18 +0000</pubDate>
				<category><![CDATA[Mastodon]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9163</guid>

					<description><![CDATA[<p>⚠️ Read this before deep diving I've been dealing with Mastodon's high disk usage for the last week. Have a read before installing your own Mastodon instance. I would recommend checking out Pleroma and Akkoma before taking a decision. They have way less system requirements than Mastodon. EDIT November 8th Self-hosting That's right, I know [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/hosting-my-own-mastodon-instance/">Hosting my own Mastodon instance</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2>⚠️ Read this before deep diving</h2>



<blockquote class="wp-block-quote">
<p>I've been dealing with <a href="https://ricard.dev/improving-mastodons-disk-usage/">Mastodon's high disk usage</a> for the last week. Have a read before installing your own Mastodon instance. I would recommend checking out <a href="https://pleroma.social/">Pleroma</a> and <a href="https://akkoma.social/">Akkoma</a> before taking a decision. They have way less system requirements than Mastodon.</p>
<cite><strong>EDIT November 8th</strong></cite></blockquote>



<h2>Self-hosting</h2>



<p>That's right, I know have <a href="https://ricard.social/">ricard.social</a> as my Mastodon home. Here's how this went down...</p>



<p>(I will assume that you're at least a little bit familiar with <a href="https://joinmastodon.org/">Mastodon</a> and the <a href="https://en.wikipedia.org/wiki/Fediverse">Fediverse</a>. If not, have a quick read before diving into my post 🤓)</p>



<h2>🛑 mastodon.technology is shutting down</h2>



<p>This great instance of 24.000 users is shutting down. The owner can't maintain it anymore, but that is fine! The beauty of the Fediverse is that you can move somewhere else, take your followers and continue sharing, reading, communicating...</p>



<p>At this point I thought I'd try to host my own Mastodon instance. I did try in the past and failed 🫠 I had hopes now it would be different. Lucky me, it was 🍀</p>



<p>😁 I'm proud to say that all the setup was done from my hotel room, in Busan 🇰🇷 (the big city at the boom of South Korea) ✈️ 👨‍💻</p>



<h2>🤷‍♂️ Why host your own instance?</h2>



<ul>
<li>Because we can. Unlike with Twitter.</li>



<li>Complete control over what you see, who can talk to you, etc. You could block entire instances, mute people and so on with a regular account in someone else's instance. Having your own just gives you more super powers.</li>



<li>Hopefully by self-hosting I won't have to move again.</li>



<li>Self-hosting gives you extra authority. As anybody could create an account with your username in any instance, by having my own instance with my domain, it's easier to know who is behind this account.</li>
</ul>



<h2>👨‍💻 Was it hard to have it up and running?</h2>



<p>A little bit, yes. I'm not a black belt in system engineering but I know my way around NGINX and basic Ubuntu administration.</p>



<ul>
<li>The official documentation is good. A great step by step that in most cases should be enough. It wasn't for me.</li>



<li>The official mastodon forum is helpful as many people post there issues and you end up there after googling your error.</li>



<li>Still, I ended up in a state in which I saw no error and didn't know what to do. I slept on it and finally understood I had low memory issues. </li>



<li>I had to double my server capacity to 2GB RAM. Truth is, it already was at its limit. I'm self hosting a dozen sites and services. So adding a Mastodon instance on top was too much for it. Even tring to add <em>swap</em> space wasn't possible as I was at near full disk.</li>
</ul>



<p>As soon as I upgraded it all worked out.</p>



<p>I'm very happy with the process and the end result. I hope to be more engaged on Mastodon now. I mean, <em>now</em> I really have to use it 😁</p>



<p><strong>Do you usually self-host services?</strong></p>



<p><strong>Have you tried self-hosting a Mastodon instance?</strong></p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://ricard.social/@ricard"><img decoding="async" loading="lazy" width="2180" height="2034" src="https://ricard.dev/wp-content/uploads/2022/10/ricard-social-mastodon.png" alt="" class="wp-image-9166" srcset="https://ricard.dev/wp-content/uploads/2022/10/ricard-social-mastodon.png 2180w, https://ricard.dev/wp-content/uploads/2022/10/ricard-social-mastodon-768x717.png 768w, https://ricard.dev/wp-content/uploads/2022/10/ricard-social-mastodon-1536x1433.png 1536w, https://ricard.dev/wp-content/uploads/2022/10/ricard-social-mastodon-2048x1911.png 2048w" sizes="(max-width: 2180px) 100vw, 2180px" /></a></figure></div><p>The post <a rel="nofollow" href="https://ricard.dev/hosting-my-own-mastodon-instance/">Hosting my own Mastodon instance</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/hosting-my-own-mastodon-instance/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9163</post-id>	</item>
		<item>
		<title>Migrating from Gatsby to custom Node.js script</title>
		<link>https://ricard.dev/migrating-from-gatsby-to-custom-node-js-script/</link>
					<comments>https://ricard.dev/migrating-from-gatsby-to-custom-node-js-script/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Tue, 30 Aug 2022 08:00:00 +0000</pubDate>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Node.js]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9142</guid>

					<description><![CDATA[<p>Two years ago I created a small site to track what movies and shows I watch. I did it with Gatbsy because well, it was a new thing for me and I wanted to experiment with Jamstack. The idea is to create a new Markdown file for each movie or tv episode watched. A TV [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/migrating-from-gatsby-to-custom-node-js-script/">Migrating from Gatsby to custom Node.js script</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="2250" height="2002" src="https://ricard.dev/wp-content/uploads/2022/08/reviews-homepage.png" alt="" class="wp-image-9144" srcset="https://ricard.dev/wp-content/uploads/2022/08/reviews-homepage.png 2250w, https://ricard.dev/wp-content/uploads/2022/08/reviews-homepage-768x683.png 768w, https://ricard.dev/wp-content/uploads/2022/08/reviews-homepage-1536x1367.png 1536w, https://ricard.dev/wp-content/uploads/2022/08/reviews-homepage-2048x1822.png 2048w" sizes="(max-width: 2250px) 100vw, 2250px" /></figure>



<p>Two years ago I created <a href="https://quicoto.github.io/reviews/">a small site to track what movies and shows I watch</a>. I did it with <a href="https://www.gatsbyjs.com/">Gatbsy</a> because well, it was a new thing for me and I wanted to experiment with <a href="https://jamstack.org/">Jamstack</a>.</p>



<p>The idea is to create a new Markdown file for each movie or tv episode watched. A TV show season has 10 episodes, then I have 10 files. Using GraphQL I would query the files and show those on the site. Neat!</p>



<p>Using GitHub Actions, of course, I would simply build the Gatbsy site and send it to GitHub Pages. No issues. Life was good.</p>



<p>The site never needed much update. It was working fine. Until I noticed it wasn't that fast, it was taking a while to build, around 2 minutes on GH Actions. The naive version of me thought: "<em>oh; i'll try to upgrade the NPM dependencies see if the newer versions speed things up</em>" 🤭</p>



<h2>🤦‍♂️ Dependency hell</h2>



<p>You might have experienced this. The pain of trying to upgrade an old build system, the deprecated packages, the incompatibilities of the old versions with the newer and more secure versions of Node and NPM.</p>



<p>It was painful. Nothing worked anymore. Gatsby would no longer compile of course. Upgrading to breaking change versions was bound to break stuff. I did try to fix but I eventually gave up.</p>



<p>I'm just fed up with this nonsense. Having dozens of packages in your <code>package.json</code> is a recipe for headache if you're not constantly upgrading it, checking the changelogs, etc. For work projects it's fine, but for a side project with 0 maintenance... no good.</p>



<h2>🤔 What's the alternative?</h2>



<p>Custom Node script with just 1 dependency. It feels good, I just feel clean! 🥰</p>



<p>That's right. The only thing I really needed for this site is a way to convert Markdown files into HTML. Well, there's just a package to do that. A single one, your <code>package.json</code> will never look better.</p>



<p>Have a look at <a href="https://github.com/markdown-it/markdown-it">markdown-it/markdown-it</a> if you're interested.</p>


<pre class="wp-block-code" aria-describedby="shcb-language-7" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-keyword">const</span> MarkdownIt = <span class="hljs-built_in">require</span>(<span class="hljs-string">'markdown-it'</span>);

<span class="hljs-keyword">const</span> md = <span class="hljs-keyword">new</span> MarkdownIt();

<span class="hljs-keyword">const</span> result = md.render(<span class="hljs-string">'# markdown-it rulezz!'</span>);</code></div><small class="shcb-language" id="shcb-language-7"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h2>🤓 Yeah, yeah... but how?</h2>



<p>Using what Node already gives you functions to read folders, to read files and to write files. What else do you need?</p>



<p>Sure, you won't have all the featues Gatbsy gives you out of the box, that's obvious. But I don't really needed them for this site anyway. This is a very boiled down version but has all the essentials:</p>



<ul><li>Read a folder to get all the files in it.</li><li>Read each of the files inside a folder.</li><li>Process the Markdown into HTML.</li><li>Save a new HTML file with the processed content.</li></ul>


<pre class="wp-block-code" aria-describedby="shcb-language-8" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-keyword">const</span> movies = <span class="hljs-keyword">await</span> fs.promises.readdir(<span class="hljs-string">`my/folder`</span>);

movies.forEach(<span class="hljs-function">(<span class="hljs-params">movie</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> movieContent = fs.readFileSync(<span class="hljs-string">`source/folder/<span class="hljs-subst">${movie}</span>/index.md`</span>, { <span class="hljs-attr">encoding</span>: <span class="hljs-string">'utf8'</span>, <span class="hljs-attr">flag</span>: <span class="hljs-string">'r'</span> });

  <span class="hljs-keyword">const</span> md = <span class="hljs-keyword">new</span> MarkdownIt();
  
  <span class="hljs-keyword">const</span> html = md.render(movieContent);
  
  fs.writeFileSync(<span class="hljs-string">`new-file-html`</span>, html);
});</code></div><small class="shcb-language" id="shcb-language-8"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<p>The final <code>main.js</code> is surely a little bit more complex but that's because I wanted to add the extra features. <a href="https://github.com/quicoto/reviews/blob/master/backend/main.js">Have a look here</a>.</p>



<h2>Final thoughts</h2>



<p>Running through close to 6.000 (thousand!) Markdown files and creating the new HTML takes on GitHub actions takes about 20 seconds 😮 The whole build is 45 seconds 🏎</p>



<p><strong>Am I going to be in dependency hell in the future?</strong></p>



<p>Very doubtful. There just 2 things I need to take care of, the main dependency and the Node.js version which has very long LTS versions. I should be fine for a couple of years. </p>



<p><strong>I'm very happy with the result.</strong> Not only to learn more about file handling in Node.js but to speed things up. Taking less build time and resources is just a win-win for me, the data-center running the build, and the environment 💜</p>



<h3>Repository</h3>



<p><a href="https://github.com/quicoto/reviews">https://github.com/quicoto/reviews</a></p>
<p>The post <a rel="nofollow" href="https://ricard.dev/migrating-from-gatsby-to-custom-node-js-script/">Migrating from Gatsby to custom Node.js script</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/migrating-from-gatsby-to-custom-node-js-script/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9142</post-id>	</item>
		<item>
		<title>How to dynamically generate images with Node.js</title>
		<link>https://ricard.dev/how-to-dynamically-generate-images-with-node-js/</link>
					<comments>https://ricard.dev/how-to-dynamically-generate-images-with-node-js/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Wed, 11 May 2022 13:14:01 +0000</pubDate>
				<category><![CDATA[Node.js]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9117</guid>

					<description><![CDATA[<p>🚫 Problem Having to manually 😴 create unique images for Social Media (Open Graph) for your new blog posts. ✅ Solution Let's use Node.js to engineer a script to take care of this for us 😎 ❤️ Generated image This is the final result, a dynamically generated image: 🤔 How does this work? A part [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-dynamically-generate-images-with-node-js/">How to dynamically generate images with Node.js</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2>🚫 Problem</h2>



<p>Having to manually 😴 create unique images for Social Media (<a href="https://ogp.me/">Open Graph</a>) for your new blog posts.</p>



<h2>✅ Solution</h2>



<p>Let's use Node.js to engineer a script to take care of this for us 😎</p>



<h2>❤️ Generated image</h2>



<p>This is the final result, a dynamically generated image:</p>



<div class="wp-block-image"><figure class="aligncenter size-full"><img decoding="async" loading="lazy" width="1200" height="630" src="https://ricard.dev/wp-content/uploads/2022/05/featured-image.png" alt="" class="wp-image-9118" srcset="https://ricard.dev/wp-content/uploads/2022/05/featured-image.png 1200w, https://ricard.dev/wp-content/uploads/2022/05/featured-image-768x403.png 768w" sizes="(max-width: 1200px) 100vw, 1200px" /></figure></div>



<h2>🤔 How does this work?</h2>



<p>A part from playing with the <a href="https://nodejs.org/api/fs.html">file system (fs) module</a> from Node.js, the actual image creation will be done using canvas:</p>



<p><a href="https://www.npmjs.com/package/canvas">https://www.npmjs.com/package/canvas</a></p>



<p>I assume you're comfortable adding NPM packages to your <code>package.json</code> file.</p>



<h4>Require all functions needed from canvas</h4>



<p>We'll start our <code>index.js</code> file by requiring this, as well as the file system module:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-9" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);
<span class="hljs-keyword">const</span> {
  createCanvas,
  loadImage,
  registerFont,
} = <span class="hljs-built_in">require</span>(<span class="hljs-string">'canvas'</span>);</code></div><small class="shcb-language" id="shcb-language-9"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h4>Creating a canvas</h4>



<p>To define the area we will be working with, define it like so:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-10" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-keyword">const</span> imageCanvas = createCanvas(<span class="hljs-number">1200</span>, <span class="hljs-number">630</span>);
<span class="hljs-keyword">const</span> context = imageCanvas.getContext(<span class="hljs-string">'2d'</span>);</code></div><small class="shcb-language" id="shcb-language-10"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h4>Loading a custom font (optional)</h4>



<p>In case the system you're creating the images in does not have the fonts you want, simply include the <code>.ttf</code> files and register the custom font like so:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-11" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript">registerFont(<span class="hljs-string">'comicsans.ttf'</span>, { <span class="hljs-attr">family</span>: <span class="hljs-string">'Comic Sans'</span> })</code></div><small class="shcb-language" id="shcb-language-11"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<p><a href="https://github.com/Automattic/node-canvas/#registerfont">Read more on the registerFont function</a>.</p>



<h4>Adding a background color to the canvas</h4>



<p>You'll notice the <code>width</code> and <code>height</code> match the canvas size we defined earlier.</p>


<pre class="wp-block-code" aria-describedby="shcb-language-12" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript">context.fillStyle = <span class="hljs-string">'#343a40'</span>;
context.fillRect(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1200</span>, <span class="hljs-number">630</span>);</code></div><small class="shcb-language" id="shcb-language-12"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h4>Adding text to the canvas</h4>



<p>Use these 3 properties to define properties of the text, and then add the text in the position you want relative to the canvas.</p>


<pre class="wp-block-code" aria-describedby="shcb-language-13" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript">context.fillStyle = <span class="hljs-string">'#fff'</span>;
context.font = <span class="hljs-string">'bold 30pt Menlo'</span>;
context.fillText(<span class="hljs-string">'ricard.dev'</span>, <span class="hljs-number">580</span>, <span class="hljs-number">520</span>);</code></div><small class="shcb-language" id="shcb-language-13"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h4>Writing to disk</h4>



<p>The final step is to simply write a file with the canvas as PNG format:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-14" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript">fs.writeFileSync(
  <span class="hljs-string">`./images/myImage.png`</span>,
  imageCanvas.toBuffer(<span class="hljs-string">'image/png'</span>),
);</code></div><small class="shcb-language" id="shcb-language-14"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<p>At this point you would be done 👏. You now know how to create images with text with canvas from within Node. You don't have to continue if you don't use WordPress. Just jump to the end to grab the final code.</p>



<p><strong>What improvements can be done?</strong></p>



<ul><li>Use a <code>database.txt</code>  file to list all the images you want to create (this can be found in the final code).</li><li>Create this on the fly. You could even serve the image on the fly rather than storing it to disk (not covered in this blog post).</li></ul>



<h2>WordPress (optional)</h2>



<p>If you're using WordPress here are small snippets you'll be needing:</p>



<h3>Generate the database</h3>



<p>List of post and their ID's:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-15" data-shcb-language-name="PHP" data-shcb-language-slug="php"><div><code class="hljs language-php">$path = preg_replace(<span class="hljs-string">'/wp-content.*$/'</span>,<span class="hljs-string">''</span>,<span class="hljs-keyword">__DIR__</span>);

<span class="hljs-keyword">include</span>($path.<span class="hljs-string">'wp-load.php'</span>);

$args = <span class="hljs-keyword">array</span>(
  <span class="hljs-string">'numberposts'</span> =&gt; <span class="hljs-number">-1</span>
);

$posts = get_posts( $args );

<span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;pre&gt;"</span>;

<span class="hljs-keyword">if</span> ( $posts ) {
  <span class="hljs-keyword">foreach</span> ( $posts <span class="hljs-keyword">as</span> $post ) {
    print_r($post-&gt;ID . <span class="hljs-string">"|"</span> . $post-&gt;post_title . <span class="hljs-string">"\n"</span>);
  }
}

<span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;/pre&gt;"</span>;</code></div><small class="shcb-language" id="shcb-language-15"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">PHP</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">php</span><span class="shcb-language__paren">)</span></small></pre>


<p>It will output something like this:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-16" data-shcb-language-name="plaintext" data-shcb-language-slug="plaintext"><div><code class="hljs language-plaintext">9099|How to list recently edited pages in Hugo
9072|Moving from Docusaurus to Hugo</code></div><small class="shcb-language" id="shcb-language-16"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">plaintext</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">plaintext</span><span class="shcb-language__paren">)</span></small></pre>


<h4>Loop the database</h4>



<p>From the database you just created, it would only make sense to loop it while creating the images.</p>


<pre class="wp-block-code" aria-describedby="shcb-language-17" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-keyword">const</span> databaseContent = fs.readFileSync(<span class="hljs-string">'./database.txt'</span>, { <span class="hljs-attr">encoding</span>: <span class="hljs-string">'utf8'</span>, <span class="hljs-attr">flag</span>: <span class="hljs-string">'r'</span> });
<span class="hljs-keyword">const</span> lines = databaseContent.split(<span class="hljs-regexp">/\r?\n/</span>)

lines.forEach(<span class="hljs-function">(<span class="hljs-params">line</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> content = line.split(<span class="hljs-string">'|'</span>);
  <span class="hljs-keyword">const</span> id = content[<span class="hljs-number">0</span>];
  <span class="hljs-keyword">const</span> text = content[<span class="hljs-number">1</span>];

  <span class="hljs-comment">// Do stuff with the canvas</span>
  <span class="hljs-comment">// Add text, colors, etc</span>
  <span class="hljs-comment">// ...</span>
});</code></div><small class="shcb-language" id="shcb-language-17"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h3>Overwrite <a href="https://wordpress.org/plugins/wordpress-seo/">Yoast SEO</a> plugin output</h3>



<p>By default the plugin will get the actual featured image from the post, you want to customize that output. Should you have placed the script inside your theme, you will also want this in your <code>functions.php</code> file</p>


<pre class="wp-block-code" aria-describedby="shcb-language-18" data-shcb-language-name="PHP" data-shcb-language-slug="php"><div><code class="hljs language-php"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">wp_open_graph_overwrite_YOAST</span><span class="hljs-params">( $url )</span> </span>{
  <span class="hljs-keyword">if</span> (!is_singular(<span class="hljs-string">'post'</span>)) {
    <span class="hljs-keyword">return</span> $url;
  }

  <span class="hljs-comment">// Check if the file exists on disk</span>
  $image = <span class="hljs-string">'/image-generator/images/'</span> . get_the_ID() . <span class="hljs-string">".png"</span>;
  $imageURL = get_stylesheet_directory_uri() . $image;
  $imagePath = get_stylesheet_directory() . $image;

  <span class="hljs-keyword">if</span> (file_exists($imagePath)) {
    <span class="hljs-keyword">return</span> $imageURL;
  }

  <span class="hljs-keyword">return</span> $url;
}

add_filter( <span class="hljs-string">'wpseo_opengraph_image'</span>, <span class="hljs-string">'wp_open_graph_overwrite_YOAST'</span> );</code></div><small class="shcb-language" id="shcb-language-18"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">PHP</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">php</span><span class="shcb-language__paren">)</span></small></pre>


<h2>👨‍💻 Grab the complete code</h2>



<p>Check out the GitHub repository:</p>



<p><a href="https://github.com/quicoto/image-generator">https://github.com/quicoto/image-generator</a></p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-dynamically-generate-images-with-node-js/">How to dynamically generate images with Node.js</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/how-to-dynamically-generate-images-with-node-js/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9117</post-id>	</item>
		<item>
		<title>Defer CSS loading with WordPress</title>
		<link>https://ricard.dev/defer-css-loading-with-wordpress/</link>
					<comments>https://ricard.dev/defer-css-loading-with-wordpress/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Thu, 03 Mar 2022 09:51:05 +0000</pubDate>
				<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Web Performance]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9105</guid>

					<description><![CDATA[<p>So you're learning about Critical Rendering Path, you have already optimized your WordPress theme's stylesheet but still there are WordPress core CSS files that you want to lazy load. I have a present for you 🎁 One of several techniques is to load the stylesheet using media="print" which tells the browser this file is meant [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/defer-css-loading-with-wordpress/">Defer CSS loading with WordPress</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>So you're learning about <a href="https://developers.google.com/web/fundamentals/performance/critical-rendering-path">Critical Rendering Path</a>, you have already optimized your WordPress theme's stylesheet but still there are WordPress core CSS files that you want to lazy load. I have a present for you 🎁</p>



<p>One of <strong>several techniques</strong> is to load the stylesheet using <code>media="print"</code> which tells the browser this file is meant for print view and not when regular browsing the page.</p>



<p>The trick is to use JavaScript to tell the browser that after finishing loading change that stylesheet from <code>print</code> to <code>all</code>, thus loading it for the regular view.</p>



<p>This will effectively <strong>defer the CSS loading</strong> until the end. In my case I want to defer the WordPress Block Library, I don't use it a lot and I saw a reduction of render time by applying this change:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-19" data-shcb-language-name="PHP" data-shcb-language-slug="php"><div><code class="hljs language-php"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">custom_use_print_block_library</span><span class="hljs-params">( $html, $handle )</span> </span>{
  $handles = <span class="hljs-keyword">array</span>( <span class="hljs-string">'wp-block-library'</span> );
  <span class="hljs-keyword">if</span> ( in_array( $handle, $handles ) ) {
    $html = str_replace( <span class="hljs-string">'media=\'all\''</span>, <span class="hljs-string">'media=\'print\' onload="this.onload=null;this.media=\'all\'"'</span>, $html );
  }
  <span class="hljs-keyword">return</span> $html;
}
add_filter( <span class="hljs-string">'style_loader_tag'</span>, <span class="hljs-string">'custom_use_print_block_library'</span>, <span class="hljs-number">10</span>, <span class="hljs-number">2</span> );</code></div><small class="shcb-language" id="shcb-language-19"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">PHP</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">php</span><span class="shcb-language__paren">)</span></small></pre>


<p>To be placed in your <code>functions.php</code> theme's file.</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/defer-css-loading-with-wordpress/">Defer CSS loading with WordPress</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/defer-css-loading-with-wordpress/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9105</post-id>	</item>
		<item>
		<title>How to list recently edited pages in Hugo</title>
		<link>https://ricard.dev/how-to-list-recently-edited-pages-in-hugo/</link>
					<comments>https://ricard.dev/how-to-list-recently-edited-pages-in-hugo/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Sun, 20 Feb 2022 15:59:09 +0000</pubDate>
				<category><![CDATA[Hugo]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9099</guid>

					<description><![CDATA[<p>The following snippet will list your Hugo site pages ordered by last modification date. You simply have to reverse it and limit to your liking. Shortcode themes/YOUR_THEME/layouts/shortcodes/last-edited-pages.html How to use?</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-list-recently-edited-pages-in-hugo/">How to list recently edited pages in Hugo</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>The following snippet will list your Hugo site pages ordered by last modification date. You simply have to reverse it and limit to your liking.</p>



<h2 id="shortcode">Shortcode</h2>



<p><code>themes/YOUR_THEME/layouts/shortcodes/last-edited-pages.html</code></p>


<pre class="wp-block-code" aria-describedby="shcb-language-20" data-shcb-language-name="HTML, XML" data-shcb-language-slug="xml"><div><code class="hljs language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Recently Edited<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>

{{ $byLastMod :=  .Site.Pages.ByLastmod  }}
{{ $recent := ($byLastMod | last 5).Reverse }}

<span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span>
{{ range  $recent }}
  <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"{{ .Permalink }}"</span>&gt;</span>{{ .Title }}<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
{{ end }}
<span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
</code></div><small class="shcb-language" id="shcb-language-20"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">HTML, XML</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">xml</span><span class="shcb-language__paren">)</span></small></pre>


<h2 id="how-to-use">How to use?</h2>


<pre class="wp-block-code" aria-describedby="shcb-language-21" data-shcb-language-name="HTML, XML" data-shcb-language-slug="xml"><div><code class="hljs language-xml">{{<span class="hljs-tag">&lt; <span class="hljs-attr">last-edited-pages</span> &gt;</span>}}</code></div><small class="shcb-language" id="shcb-language-21"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">HTML, XML</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">xml</span><span class="shcb-language__paren">)</span></small></pre><p>The post <a rel="nofollow" href="https://ricard.dev/how-to-list-recently-edited-pages-in-hugo/">How to list recently edited pages in Hugo</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/how-to-list-recently-edited-pages-in-hugo/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9099</post-id>	</item>
		<item>
		<title>Moving from Docusaurus to Hugo</title>
		<link>https://ricard.dev/moving-from-docusaurus-to-hugo/</link>
					<comments>https://ricard.dev/moving-from-docusaurus-to-hugo/#comments</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Tue, 15 Feb 2022 08:00:00 +0000</pubDate>
				<category><![CDATA[Hugo]]></category>
		<category><![CDATA[Docusaurus]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9072</guid>

					<description><![CDATA[<p>For static generated sites I'm loving more everyday Hugo. It's speed and zero JavaScript dependency (unlike Gatsby) is just what the doctor ordered. I have a private wiki for my, well, not public notes. The first version was done with WordPress (I even open sourced the theme). It worked okay, nice writing experience, one click [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/moving-from-docusaurus-to-hugo/">Moving from Docusaurus to Hugo</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>For static generated sites I'm loving more everyday <a href="https://gohugo.io">Hugo</a>. It's speed and zero JavaScript dependency (unlike <a href="https://www.gatsbyjs.com/">Gatsby</a>) is just what the doctor ordered.</p>



<p>I have a private wiki for my, well, not public notes. The first version was done with WordPress (<a href="https://github.com/quicoto/wiki-theme">I even open sourced the theme</a>). It worked okay, <strong>nice writing experience</strong>, one click publish/update. My problem with WordPress (and I do like WordPress) is <strong>security</strong> (of the PHP files, the database, etc) as well as maintenance of them. Plus in the end <strong>I just want to write plain text files.</strong> <br>I then moved to <a href="https://docusaurus.io/">Docusaurus</a> because I wanted to <strong>get rid of the database</strong> and increase speed (no cache plugins, no PHP cache, CPU load on each HTTP request to query the database and process the PHP files... Don't get me started!). <strong>Setup simplicity</strong> overall at the cost of writing experience. </p>



<p>Now, I'm about to change it again, to hopefully a even better setup. You tell me. My reasons? The same I had before. <strong>I want more simplicity and speed</strong>.</p>



<h2 id="why-move-to-hugo">🤔 Why move to Hugo?</h2>



<p>It's pointless to compare the size of the two systems. Hugo, running in <strong>Go</strong> and installed globally on your machine vs <strong>hundreds if not thousands of NPM packages</strong> with Docusaurus. It's a far cry from a simple HTML and CSS file we used back in the good ol' days.</p>



<p>My pain points:</p>



<ul><li><strong>Dependency maintenance</strong>: I hate having to maintain NPM dependencies, because of security reasons or when I want to update to latest versions/features. You end up with a big <code>package.json</code> (and even bigger <code>node_modules</code> folder) that sometimes takes puzzle work to match dependencies with NodeJS and NPM versions, without breaking a feature.<br>With Hugo, if it works, you don't ever need to update the Hugo binaries. Just build the site, it works and it's fast, like really fast.</li><li><strong>Build speed</strong>: might not be a fair comparison as the final generated product is different, but it's important to illustrate my use case.<ul><li>Local macOS:<ul><li><strong>Docusaurus:</strong> <ul><li>Develop: 8 seconds</li></ul><ul><li>Production: 23 seconds</li></ul></li><li><strong>Hugo</strong>: <ul><li>Develop: 1 second</li><li>Production: 1 second </li></ul></li></ul></li></ul><ul><li>On GitHub Actions, build for production:<ul><li><strong>Docusaurus:</strong> average of 1.5 minutes.</li><li><strong>Hugo</strong>: 45 seconds.</li></ul></li></ul></li></ul>



<h2 id="content">📝 Content</h2>



<h3 id="categories">🗂 Categories</h3>



<p>Docusaurus handles post categories with a <code>_category_.json</code> which has a content similar to:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-22" data-shcb-language-name="JSON / JSON with Comments" data-shcb-language-slug="json"><div><code class="hljs language-json">{
  <span class="hljs-attr">"label"</span>: <span class="hljs-string">"🎸 Guitar"</span>,
  <span class="hljs-attr">"collapsed"</span>: <span class="hljs-literal">true</span>
}</code></div><small class="shcb-language" id="shcb-language-22"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JSON / JSON with Comments</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">json</span><span class="shcb-language__paren">)</span></small></pre>


<p>This need to go. With Hugo you create a <code>_index.md</code> file under the folder. I like this better because with this file you can already include content for the category, before creating the child pages under it. You end up with something like:</p>


<pre class="wp-block-code"><div><code class="hljs">/content/guitar/_index.md
/content/guitar/songs.md</code></div></pre>


<h3 id="pages">📄 Pages</h3>



<p>Regarding your content, your Markdown files, the "only" change is to add <a href="https://gohugo.io/content-management/front-matter/">Front Matter</a> to each <code>.md</code> file</p>



<p>I didn't have many files so I did it manually but on a high content site you might need a bash script or similar to automate the process. This would be the bare minimum:</p>


<pre class="wp-block-code"><div><code class="hljs">---
title: Hello World
---</code></div></pre>


<h2 id="the-need-for-speed">🏎 The need for speed?</h2>



<p>With Hugo, as mentioned, you have full control over what JS and CSS you include in your theme.</p>



<p><a href="https://ricard.dev/how-to-add-purgecss-to-hugo/">Why not use PostCSS to remove unused CSS from your theme?</a> 😁</p>



<h2 id="conclusion">👨‍💻 Conclusion</h2>



<p>The move was quite painless. Since you'r content is practically the same, Markdown files, you can easily switch to another CMS if needed.</p>



<p>The most work, as you can imagine, is creating a theme in the new system. Less work if you get an open sourced theme, I forked one and stripped down everything I didn't need to make it lean.</p>



<p>I'm sure there are plenty of valid use cases for Docusaurus, it's actively developed and it looks promising. For my particular simple use case, I didn't need the added complexity (even if they claim: "<em>Build optimized websites quickly, <strong>focus on your content</strong></em>").</p>



<p><strong>What's your take? Which one do you use? What's your use case?</strong></p>
<p>The post <a rel="nofollow" href="https://ricard.dev/moving-from-docusaurus-to-hugo/">Moving from Docusaurus to Hugo</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/moving-from-docusaurus-to-hugo/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9072</post-id>	</item>
		<item>
		<title>Generating book lists from my Goodreads profile</title>
		<link>https://ricard.dev/generating-book-lists-from-my-goodreads-profile/</link>
					<comments>https://ricard.dev/generating-book-lists-from-my-goodreads-profile/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Wed, 09 Feb 2022 12:03:41 +0000</pubDate>
				<category><![CDATA[Node.js]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9060</guid>

					<description><![CDATA[<p>If you read books you have perhaps heard about Goodreads.com, the site has book reviews, season awards, and more. The most interesting part is that you can track you read, currently reading and want to read books. Yes, you could use a notepad or a spreadsheet 🤷‍♂️ but using the site is nice. You can [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/generating-book-lists-from-my-goodreads-profile/">Generating book lists from my Goodreads profile</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>If you read books you have perhaps heard about <a href="https://www.goodreads.com/">Goodreads.com</a>, the site has book reviews, season awards, and more. The most interesting part is that <strong>you can track</strong> you read, currently reading and want to read books.</p>



<p>Yes, you could use a notepad or a spreadsheet 🤷‍♂️ but using the site is nice. You can review the books you read, rate them and so on. </p>



<h2 id="owning-your-data">🔒 Owning your data</h2>



<p>They key thing is not to give your data away with risk of losing it. I want to create a backup / export of my data, should Goodreads eventually go away. Simple, don't lose your data 😉</p>



<h2 id="rss-xml">🤓 RSS XML</h2>



<p>The good folks at Goodreads are nice enough to offer RSS XML files for your profile ❤️ For each of the mentioned shelves (Currently Reading, Read, and Want to Read) 💯</p>



<p>We can use these 3 XML to store the output in <code>.txt</code> files. The text files can be used (regardless of the RSS XML availability) to generate a nice HTML output (should you want to showcase the lists in your site).</p>


<pre class="wp-block-code" aria-describedby="shcb-language-23" data-shcb-language-name="HTML, XML" data-shcb-language-slug="xml"><div><code class="hljs language-xml">https://www.goodreads.com/review/list_rss/YOUR_PROFILE_ID?key=&amp;shelf=currently-reading
https://www.goodreads.com/review/list_rss/YOUR_PROFILE_ID?key=&amp;shelf=to-read
https://www.goodreads.com/review/list_rss/YOUR_PROFILE_ID?key=&amp;shelf=read</code></div><small class="shcb-language" id="shcb-language-23"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">HTML, XML</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">xml</span><span class="shcb-language__paren">)</span></small></pre>


<h2 id="fetching-the-rss">📚 Fetching the RSS</h2>



<p>While we wait for Node 17.5 to have native <code>fetch()</code> support we need to find a workaround. This nice NPM package <a href="https://www.npmjs.com/package/node-fetch">node-fetch</a> will provide what we need ✅</p>



<p>Additionally we need to parse de XML, <a href="https://www.npmjs.com/package/arraybuffer-xml-parser">arraybuffer-xml-parser</a> is the other package we make use of ✅</p>



<p>With these two we can do something like:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-24" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript">fetch(url)
  .then(<span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> response.text())
  .then(<span class="hljs-function"><span class="hljs-params">data</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> xmlData = encoder.encode(data);
    <span class="hljs-keyword">const</span> object = parse(xmlData);
    <span class="hljs-keyword">const</span> items = object.rss.channel.item;

    items.forEach(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(item.title)
    })
  });</code></div><small class="shcb-language" id="shcb-language-24"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h2 id="creating-files">🆕 Creating files</h2>



<p>Ok, so we've fetched the RSS files and after parsing them we need to create a file. With Node is easy:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-25" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">'fs'</span>;

fs.writeFile(fileName, data, (err) =&gt; {
  <span class="hljs-keyword">if</span> (!err) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'File created: '</span> + fileName);
  }
});</code></div><small class="shcb-language" id="shcb-language-25"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<p>The content of the <code>.txt</code> files look like this:</p>


<pre class="wp-block-code"><div><code class="hljs">book_id|year|Book Name|URL
book_id|year|Book Name|URL
book_id|year|Book Name|URL</code></div></pre>


<h2 id="reading-files">👓 Reading files</h2>



<p>Reading your files with Node is trivial:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-26" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">'fs'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">readFile</span>(<span class="hljs-params">shelve</span>) </span>{
  <span class="hljs-keyword">return</span> fs.readFileSync(<span class="hljs-string">`./database/<span class="hljs-subst">${shelve}</span>.txt`</span>, { <span class="hljs-attr">encoding</span>:<span class="hljs-string">'utf8'</span>, <span class="hljs-attr">flag</span>:<span class="hljs-string">'r'</span>});
}</code></div><small class="shcb-language" id="shcb-language-26"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h3 id="reading-the-lines-in-the-file">Reading the lines in the file</h3>



<p>Once you read the file with <code>fs</code> you can get the lines with a simple <code>split</code>:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-27" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getLines</span>(<span class="hljs-params">content</span>) </span>{
  <span class="hljs-keyword">return</span> content.split(<span class="hljs-regexp">/\r?\n/</span>);
}</code></div><small class="shcb-language" id="shcb-language-27"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h2 id="github-actions">🔥 GitHub Actions</h2>



<p>You know me, I'm a fan of Github Actions 🤩 We'll use this simple action to run our Node.js script and deploy the output to a different branch 🚀</p>



<p>The idea is to have our scripts create the files in a the <code>public</code> folder (or whatever name you want it to be!). Then after running the script copy that folder to another branch, for persistence, history, etc.</p>



<p>Create the file in your repo: <code>.github/workflows/build.yml</code></p>


<pre class="wp-block-code" aria-describedby="shcb-language-28" data-shcb-language-name="YAML" data-shcb-language-slug="yaml"><div><code class="hljs language-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">and</span> <span class="hljs-string">Deploy</span>
<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build-and-deploy:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">🛎️</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>
        <span class="hljs-attr">with:</span>
            <span class="hljs-attr">persist-credentials:</span> <span class="hljs-literal">false</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">and</span> <span class="hljs-string">Build</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          npm ci
          npm run build
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">🚀</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">JamesIves/github-pages-deploy-action@releases/v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">BRANCH:</span> <span class="hljs-string">output</span> <span class="hljs-comment"># The branch the action should deploy to.</span>
          <span class="hljs-attr">FOLDER:</span> <span class="hljs-string">public</span> <span class="hljs-comment"># The folder the action should deploy.</span></code></div><small class="shcb-language" id="shcb-language-28"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">YAML</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">yaml</span><span class="shcb-language__paren">)</span></small></pre>


<p>🤔 One <strong>possible improvement</strong> could be to have the action do a commit to the <code>main</code> branch with the changes. Otherwise I have to manually copy the updated files from the <code>output</code> branch to the <code>main</code> branch. Small price to pay, though.</p>



<h2 id="conclusion">👨‍💻 Conclusion</h2>



<p>Now you know how to easily fetch, write files to disk and read them with NodeJS.</p>



<p>The rest is up to you know to build your app. Make use of the free and powerful GitHub Actions and own your data!</p>



<h2 id="live">😎 Live</h2>



<p>See it live here <a href="https://ricard.blog/books/">ricard.blog/books</a></p>



<h2 id="github">🍴 GitHub</h2>



<p>Play with it, modify it, fork it!</p>



<p><a href="https://github.com/quicoto/goodreads-profile-generator">github.com/quicoto/goodreads-profile-generator</a></p>
<p>The post <a rel="nofollow" href="https://ricard.dev/generating-book-lists-from-my-goodreads-profile/">Generating book lists from my Goodreads profile</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/generating-book-lists-from-my-goodreads-profile/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9060</post-id>	</item>
		<item>
		<title>How to create a news digest using RSS and GitHub Actions</title>
		<link>https://ricard.dev/how-to-create-a-news-digest-using-rss-and-github-actions/</link>
					<comments>https://ricard.dev/how-to-create-a-news-digest-using-rss-and-github-actions/#comments</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Thu, 03 Feb 2022 14:54:48 +0000</pubDate>
				<category><![CDATA[Node.js]]></category>
		<category><![CDATA[Github Actions]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9039</guid>

					<description><![CDATA[<p>One of my daily morning routines is to scan several news sites and read headlines to see if anything piques my interest. Or at least to have a vague recollection of topics happening nowadays. I go to Hacker News, The Verge, The Guardian… and others. A mixed bag, really of tech and world news. Because [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-create-a-news-digest-using-rss-and-github-actions/">How to create a news digest using RSS and GitHub Actions</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>One of my daily morning routines is to scan several news sites and read headlines to see if anything piques my interest. Or at least to have a vague recollection of topics happening nowadays.</p>



<p>I go to <a href="https://news.ycombinator.com/">Hacker News</a>, <a href="https://www.theverge.com/">The Verge</a>, <a href="https://www.theguardian.com/">The Guardian</a>… and others. A mixed bag, really of tech and world news.</p>



<p>Because of the mind of an engineer… I wanted to optimize the time spent doing this. <strong>How?</strong> Glad you asked 🌝 Using the magic of RSS 🥰 I love RSS, Google tried to end RSS with the killing of Google Reader but it is still going strong. I use it to follow the blogs I like and now, the news I want to consume.</p>



<h2 id="idea">💡 Idea</h2>



<p>Have a simply HTML page of containing news headlines with links to the articles from different sites (RSS feeds). For instance two of the feeds I use:</p>



<ul><li><a href="https://www.theverge.com/rss/index.xml">The Verge RSS</a> </li><li><a href="https://hnrss.org/best">Hacker News RSS</a></li></ul>



<p>Scraping the sites would be another approach but it would complicate things so much more 👎 If they ever change classes or markup, your selectors might stop working. With RSS you know what you're getting. As long as they keep updating the content on their feeds, we're good! 😌</p>



<h3 id="i-don-t-want-a-backend">🙅‍♂️ I don't want a backend</h3>



<p>I don't want any backend, database or API service. Just give me the stuff on a plain HTML page. Fast.</p>



<p>To do that we need to use a CI to build this HTML, so we don't have a server to pull on demand. Rather, why don't we schedule the build of this HTML before I wake up?</p>



<h2 id="nodejs-and-github-actions">🤔 NodeJS and GitHub Actions</h2>



<p>We will use NodeJS because I know JavaScript but you could use another language. As long as you can fetch the RSS feeds and parse them, you're good.</p>



<p>With NodeJS I used this cool <a href="https://github.com/rbren/rss-parser">NPM package</a>:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-29" data-shcb-language-name="Bash" data-shcb-language-slug="bash"><div><code class="hljs language-bash">npm install --save rss-parser</code></div><small class="shcb-language" id="shcb-language-29"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">Bash</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">bash</span><span class="shcb-language__paren">)</span></small></pre>


<p>Then something similar to this:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-30" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-keyword">let</span> Parser = <span class="hljs-built_in">require</span>(<span class="hljs-string">'rss-parser'</span>);
<span class="hljs-keyword">let</span> parser = <span class="hljs-keyword">new</span> Parser();

<span class="hljs-function">(<span class="hljs-params"><span class="hljs-keyword">async</span> (</span>) =&gt;</span> {

  <span class="hljs-keyword">let</span> feed = <span class="hljs-keyword">await</span> parser.parseURL(<span class="hljs-string">'url-of-the-feed.xml'</span>);
  <span class="hljs-built_in">console</span>.log(feed.title);

  feed.items.forEach(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(item.title + <span class="hljs-string">':'</span> + item.link)
  });

})();</code></div><small class="shcb-language" id="shcb-language-30"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<p>In my case I will loop through my RSS feed URLs which I have stored in a JSON file so I can easily add/remove items without changing the actual implementation.</p>



<p>I'm building an <strong>array of promises</strong> and using <code>Promise.all</code> I'll parse all the contents and build up an HTML page. A list of links, really.</p>



<h3 id="github-actions">GitHub Actions</h3>



<p>I'm in love with the simplicity of GitHub Actions, so easy to implement, so little friction, and yet so powerful.</p>



<p>I'm going to create an action to run my NodeJS script, take the generated HTML and deploy it to GitHub Pages. Which will simply host HTML and CSS.</p>



<p>In my <code>package.json</code> I have a single command <code>build</code>:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-31" data-shcb-language-name="JSON / JSON with Comments" data-shcb-language-slug="json"><div><code class="hljs language-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"news-digest"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"A single place to check all my daily news sites in 1 go"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"rss-parser"</span>: <span class="hljs-string">"^3.12.0"</span>
  },
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"node index.js"</span>
  },
  <span class="hljs-attr">"keywords"</span>: [],
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>
}</code></div><small class="shcb-language" id="shcb-language-31"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JSON / JSON with Comments</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">json</span><span class="shcb-language__paren">)</span></small></pre>


<p id="block-d50772d7-c3f7-47bd-b920-7d65a38de90e">Then on the Github Action workflow:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-32" data-shcb-language-name="YAML" data-shcb-language-slug="yaml"><div><code class="hljs language-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">and</span> <span class="hljs-string">Deploy</span>
<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
  <span class="hljs-attr">schedule:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">cron:</span> <span class="hljs-string">"0 6 * * *"</span>
<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build-and-deploy:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">🛎️</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>
        <span class="hljs-attr">with:</span>
            <span class="hljs-attr">persist-credentials:</span> <span class="hljs-literal">false</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">and</span> <span class="hljs-string">Build</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          npm ci
          npm run build
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">🚀</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">JamesIves/github-pages-deploy-action@releases/v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">BRANCH:</span> <span class="hljs-string">gh-pages</span> <span class="hljs-comment"># The branch the action should deploy to.</span>
          <span class="hljs-attr">FOLDER:</span> <span class="hljs-string">dist</span> <span class="hljs-comment"># The folder the action should deploy.</span></code></div><small class="shcb-language" id="shcb-language-32"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">YAML</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">yaml</span><span class="shcb-language__paren">)</span></small></pre>


<p>Don't forget to setup the Github Pages on the repository settings. You have to set it to the <code>gh-pages</code> branch.</p>



<h2 id="see-it-in-action">😎 See it in action</h2>



<p><a href="https://quicoto.github.io/news-digest/">Here's the live site</a> which I check every morning ☕️ 🥱 <a href="https://github.com/quicoto/news-digest">Have also a look on GitHub</a></p>



<p><strong>Would you do this differently? If so, how?</strong></p>



<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1116" height="906" src="https://ricard.dev/wp-content/uploads/2022/02/news-digest-screenshot.png" alt="" class="wp-image-9044" srcset="https://ricard.dev/wp-content/uploads/2022/02/news-digest-screenshot.png 1116w, https://ricard.dev/wp-content/uploads/2022/02/news-digest-screenshot-768x623.png 768w" sizes="(max-width: 1116px) 100vw, 1116px" /></figure>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-create-a-news-digest-using-rss-and-github-actions/">How to create a news digest using RSS and GitHub Actions</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/how-to-create-a-news-digest-using-rss-and-github-actions/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9039</post-id>	</item>
		<item>
		<title>How to defer scripts in WordPress</title>
		<link>https://ricard.dev/how-to-defer-scripts-in-wordpress/</link>
					<comments>https://ricard.dev/how-to-defer-scripts-in-wordpress/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Mon, 16 Aug 2021 07:00:00 +0000</pubDate>
				<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9022</guid>

					<description><![CDATA[<p>You don't need me explaining you how important it is to defer scripts for performance. If your application doesn't need render-blocking scripts you should simply defer them. Most of the WordPress blogs/sites do not need render blocking scripts, for toggling menu visibility, swapping emojis for SVG's, embed media... All these can be deferred no problem. [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-defer-scripts-in-wordpress/">How to defer scripts in WordPress</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>You don't need me explaining you how important it is to defer scripts for performance. If your application doesn't need render-blocking scripts you should simply defer them.</p>



<p>Most of the WordPress blogs/sites do not need render blocking scripts, for toggling menu visibility, swapping emojis for SVG's, embed media... All these can be deferred no problem.</p>



<p>The script below will handle all non-Dashboard (WordPress admin) scripts as defer.</p>



<p>You might want to have an allow-list array using the <code>$handle</code> param. In my case I want everything, to speed up the page render time as much as possible.</p>


<pre class="wp-block-code" aria-describedby="shcb-language-33" data-shcb-language-name="PHP" data-shcb-language-slug="php"><div><code class="hljs language-php"><span class="hljs-comment">/*
 * Add defer attribute to enqueued scripts
 *
 * <span class="hljs-doctag">@param</span> string $tag
 * <span class="hljs-doctag">@param</span> string $handle
 * <span class="hljs-doctag">@param</span> string $src
 * <span class="hljs-doctag">@return</span> void
 */</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">defer_scripts</span><span class="hljs-params">( $tag, $handle, $src )</span> </span>{
  <span class="hljs-keyword">if</span> (is_admin()) <span class="hljs-keyword">return</span> $tag;

  <span class="hljs-keyword">return</span> <span class="hljs-string">'&lt;script type="text/javascript" src="'</span> . $src . <span class="hljs-string">'" defer="defer"&gt;&lt;/script&gt;'</span>;
}

add_filter( <span class="hljs-string">'script_loader_tag'</span>, <span class="hljs-string">'defer_scripts'</span>, <span class="hljs-number">10</span>, <span class="hljs-number">3</span> );</code></div><small class="shcb-language" id="shcb-language-33"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">PHP</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">php</span><span class="shcb-language__paren">)</span></small></pre><p>The post <a rel="nofollow" href="https://ricard.dev/how-to-defer-scripts-in-wordpress/">How to defer scripts in WordPress</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/how-to-defer-scripts-in-wordpress/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9022</post-id>	</item>
		<item>
		<title>How to create custom commands for Mac&#8217;s Spotlight</title>
		<link>https://ricard.dev/how-to-create-custom-commands-for-macs-spotlight/</link>
					<comments>https://ricard.dev/how-to-create-custom-commands-for-macs-spotlight/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Fri, 06 Aug 2021 13:02:07 +0000</pubDate>
				<category><![CDATA[JavaScript]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9005</guid>

					<description><![CDATA[<p>Out of the box doesn't seem to be possible 🙁 BUT! I've found a workaround that works almost as nicely 🎉 There are paid apps out there, but we as developers should do it ourselves 👨🏻‍💻 To learn from it and to feel like a hacker. 💡 Create a bash script I want to able [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-create-custom-commands-for-macs-spotlight/">How to create custom commands for Mac&#8217;s Spotlight</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Out of the box doesn't seem to be possible 🙁 <strong>BUT!</strong> I've found a workaround that works almost as nicely 🎉 <br>There are paid apps out there, but we as developers should do it ourselves 👨🏻‍💻 To learn from it and to feel like a hacker.</p>



<h2>💡 Create a bash script</h2>



<p>I want to able to go quickly to a site from anywhere on my Mac. Sure that's what bookmarks are for, but you need to focus on Firefox, find the bookmark and click on it. That's just too much 😁</p>



<p>Imagine just typing a command in spotlight and be done with it ✅ The bash script below does exactly that.</p>



<ol><li>Opens Firefox Developer Edition</li><li>Goes to the URL</li><li>Terminal closes itself (I have configured Terminal to not ask for confirmation when closing with open processes).</li></ol>


<pre class="wp-block-code" aria-describedby="shcb-language-34" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-meta">#! /bin/bash</span>

open -a Firefox\ Developer\ Edition -g https:<span class="hljs-comment">//ricard.dev/wp-admin/</span>
osascript -e <span class="hljs-string">'tell application "Terminal" to quit'</span>
</code></div><small class="shcb-language" id="shcb-language-34"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<p>As you know you can do practically anything with a bash script, so just let loose! It doesn't have to be my browser use case 😎</p>



<h2>🤔 Easy find/write syntax</h2>



<p>I've created my own syntax for naming the files <code><strong>c</strong>XZY<strong>.command</strong></code></p>



<p>In the example below: <code>cdev.command</code> will open my dev blog Dashboard in Firefox.</p>



<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1584" height="1084" src="https://ricard.dev/wp-content/uploads/2021/08/Screenshot-2021-08-06-at-14.51.28.png" alt="" class="wp-image-9007" srcset="https://ricard.dev/wp-content/uploads/2021/08/Screenshot-2021-08-06-at-14.51.28.png 1584w, https://ricard.dev/wp-content/uploads/2021/08/Screenshot-2021-08-06-at-14.51.28-768x526.png 768w, https://ricard.dev/wp-content/uploads/2021/08/Screenshot-2021-08-06-at-14.51.28-1536x1051.png 1536w" sizes="(max-width: 1584px) 100vw, 1584px" /></figure>



<p>This syntax might not work for your brain just come up with something unique so Spotlight does not give you the wrong result.</p>



<h2>Known issues</h2>



<p>If I'm watching a video with VLC and run the command it will open Firefox but it will not focus on that app. I've tried to use <code>activate</code> but doesn't work. <strong>Ideas?</strong></p>


<pre class="wp-block-code" aria-describedby="shcb-language-35" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript">osascript -e <span class="hljs-string">'tell application "Firefox\ Developer\ Edition" to activate'</span></code></div><small class="shcb-language" id="shcb-language-35"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h2>🐙 Github</h2>



<p>I've created a repo for them: <a href="https://github.com/quicoto/shortcut-commands">https://github.com/quicoto/shortcut-commands</a></p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-create-custom-commands-for-macs-spotlight/">How to create custom commands for Mac&#8217;s Spotlight</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/how-to-create-custom-commands-for-macs-spotlight/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9005</post-id>	</item>
		<item>
		<title>How to do multiple StaticQuery in Gatsby</title>
		<link>https://ricard.dev/how-to-do-multiple-staticquery-in-gatsby/</link>
					<comments>https://ricard.dev/how-to-do-multiple-staticquery-in-gatsby/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Thu, 05 Aug 2021 08:00:00 +0000</pubDate>
				<category><![CDATA[Gatsby]]></category>
		<category><![CDATA[GraphQL]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=9001</guid>

					<description><![CDATA[<p>If you need to do more than one GraphQL StaticQuery in Gatbsy you can assign each query to a name. Then simply access data.yourName and you're ready to go. Here's a full example of how I use it on my reviews site:</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-do-multiple-staticquery-in-gatsby/">How to do multiple StaticQuery in Gatsby</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>If you need to do more than one GraphQL <strong>StaticQuery</strong> in <a href="https://ricard.dev/category/gatsbyjs/">Gatbsy</a> you can assign each query to a name.</p>



<p>Then simply access <code>data.yourName</code> and you're ready to go.</p>



<p>Here's a full example of how I use it on <a href="https://github.com/quicoto/reviews">my reviews site</a>:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-36" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>
<span class="hljs-keyword">import</span> { StaticQuery, graphql } <span class="hljs-keyword">from</span> <span class="hljs-string">"gatsby"</span>

<span class="hljs-keyword">const</span> TotalTime = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> (
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">StaticQuery</span>
    <span class="hljs-attr">query</span>=<span class="hljs-string">{graphql</span>`
      {
        <span class="hljs-attr">movies:</span> <span class="hljs-attr">allMarkdownRemark</span>(<span class="hljs-attr">filter:</span> { <span class="hljs-attr">frontmatter:</span> { <span class="hljs-attr">type:</span> { <span class="hljs-attr">eq:</span> "<span class="hljs-attr">movie</span>" } } }) {
          <span class="hljs-attr">edges</span> {
            <span class="hljs-attr">node</span> {
              <span class="hljs-attr">id</span>
            }
          }
        }
        <span class="hljs-attr">series:</span> <span class="hljs-attr">allMarkdownRemark</span>(<span class="hljs-attr">filter:</span> { <span class="hljs-attr">frontmatter:</span> { <span class="hljs-attr">type:</span> { <span class="hljs-attr">eq:</span> "<span class="hljs-attr">series</span>" } } }) {
          <span class="hljs-attr">edges</span> {
            <span class="hljs-attr">node</span> {
              <span class="hljs-attr">id</span>
            }
          }
        }
      }
    `}
    <span class="hljs-attr">render</span>=<span class="hljs-string">{data</span> =&gt;</span> {
      let movies = JSON.parse(JSON.stringify(data.movies, null, 4)).edges;
      let series = JSON.parse(JSON.stringify(data.series, null, 4)).edges;

      return <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
          Do something...
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    }}
  &gt;<span class="hljs-tag">&lt;/<span class="hljs-name">StaticQuery</span>&gt;</span></span>
)

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> TotalTime
</code></div><small class="shcb-language" id="shcb-language-36"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre><p>The post <a rel="nofollow" href="https://ricard.dev/how-to-do-multiple-staticquery-in-gatsby/">How to do multiple StaticQuery in Gatsby</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/how-to-do-multiple-staticquery-in-gatsby/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">9001</post-id>	</item>
		<item>
		<title>How to remove client-side JavaScript from Gatsby</title>
		<link>https://ricard.dev/how-to-remove-client-side-javascript-from-gatsby/</link>
					<comments>https://ricard.dev/how-to-remove-client-side-javascript-from-gatsby/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Sat, 31 Jul 2021 15:49:17 +0000</pubDate>
				<category><![CDATA[Gatsby]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=8993</guid>

					<description><![CDATA[<p>I build my reviews site using Gatsby. It's cool, the graphql is powerful but there's something that was buggering me. Why does my static site need client-side JavaScript? There's no reason for it. Gatsby will argue about faster route changing this way, sure. For a high-traffic site where users visit multiple pages on the same [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-remove-client-side-javascript-from-gatsby/">How to remove client-side JavaScript from Gatsby</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>I build <a href="https://quicoto.github.io/reviews/">my reviews site</a> using <a href="https://www.gatsbyjs.com/">Gatsby</a>. It's cool, the graphql is powerful but there's something that was buggering me.</p>



<p><strong>Why does my static site need client-side JavaScript?</strong></p>



<p>There's no reason for it. <a href="https://www.gatsbyjs.com/">Gatsby</a> will argue about faster route changing this way, sure. For a high-traffic site where users visit multiple pages on the same visit, it makes sense. Not my case.</p>



<p>Astonishingly Gatsby does not provide an out-of-the-box setting or flag to amend this issue. I would've loved for a boolean in the config static if I need JS. To it builds a a truly static site, which can be done with <a href="https://jekyllrb.com/">Jekyll</a>, <a href="https://gohugo.io/">Hugo</a> or <a href="https://www.11ty.dev/">Eleventy</a>.</p>



<h2>No JavaScript plugin</h2>



<p>I found <a href="https://github.com/itmayziii/gatsby-plugin-no-javascript">this plugin</a> but it doesn't seem to work with sites under a subfolder such as <code>example.com/mysite</code></p>



<p>Then I encountered <a href="https://gist.github.com/joaocarloscabral/1173496642a15772d13815126ee1dc73">this Gist</a> with the small amendment needed so it works in a subdirectory installation. It's based on the plugin but you don't need the plugin installed to work.</p>



<p>Create a <code>gatsby-ssr.js</code> file in the root directory of your site. it will remove all JavaScript from the final build (not when running the develop task).</p>



<p>⚠️ It did not fully work in my Gatsby <code>2.25.3</code>, it needed a small optional chaining around the polyfill condition.</p>



<p>Here's my <strong>updated</strong> version:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-37" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-keyword">let</span> pageScripts
<span class="hljs-comment">/*
 * The "scripts" variable is not documented by Gatsby, https://www.gatsbyjs.org/docs/ssr-apis/#onRenderBody, and that is probably for a good
 * reason. The variable contains the scripts the Gatsby internals, https://github.com/gatsbyjs/gatsby/blob/d9cf5a21403c474846ebdf7a0508902b9b8a2ea9/packages/gatsby/cache-dir/static-entry.js#L270-L283,
 * puts into the head and post body. We will be relying on this undocumented variable until it does not work anymore as the alternative is
 * to read the webpack.stats.json file and parse it ourselves.
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onRenderBody</span>(<span class="hljs-params">{ scripts }</span>) </span>{
  <span class="hljs-keyword">if</span> (process.env.NODE_ENV !== <span class="hljs-string">"production"</span>) {
    <span class="hljs-comment">// During a gatsby development build (gatsby develop) we do nothing.</span>
    <span class="hljs-keyword">return</span>
  }
  <span class="hljs-comment">// TODO maybe we should not even wait and see if Gatsby removes this internal "script" variable and code around the issue if the variable is not there.</span>
  <span class="hljs-keyword">if</span> (!scripts) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(
      <span class="hljs-string">"gatsby-plugin-no-javascript: Gatsby removed an internal detail that this plugin relied upon, please submit this issue to https://www.github.com/itmayziii/gatsby-plugin-no-javascript."</span>
    )
  }
  pageScripts = scripts
}
<span class="hljs-comment">// Here we rely on the fact that onPreRenderHTML is called after onRenderBody so we have access to the scripts Gatsby inserted into the HTML.</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onPreRenderHTML</span>(<span class="hljs-params">
  {
    getHeadComponents,
    pathname,
    replaceHeadComponents,
    getPostBodyComponents,
    replacePostBodyComponents,
  },
  pluginOptions
</span>) </span>{
  <span class="hljs-keyword">if</span> (
    process.env.NODE_ENV !== <span class="hljs-string">"production"</span> ||
    checkPathExclusion(pathname, pluginOptions)
  ) {
    <span class="hljs-comment">// During a gatsby development build (gatsby develop) we do nothing.</span>
    <span class="hljs-keyword">return</span>
  }
  replaceHeadComponents(
    getHeadComponentsNoJS(getHeadComponents(), pluginOptions)
  )
  replacePostBodyComponents(
    getPostBodyComponentsNoJS(getPostBodyComponents(), pluginOptions)
  )
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getHeadComponentsNoJS</span>(<span class="hljs-params">headComponents, pluginOptions</span>) </span>{
  <span class="hljs-keyword">return</span> headComponents.filter(<span class="hljs-function"><span class="hljs-params">headComponent</span> =&gt;</span> {
    <span class="hljs-comment">// Not a react component and therefore not a &lt;script&gt;.</span>
    <span class="hljs-keyword">if</span> (!isReactElement(headComponent)) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
    }

    <span class="hljs-keyword">if</span> (
      pluginOptions.excludeFiles &amp;&amp;
      headComponent.props.href &amp;&amp;
      <span class="hljs-built_in">RegExp</span>(pluginOptions.excludeFiles).test(headComponent.props.href)
    ) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
    }
    <span class="hljs-comment">// Gatsby puts JSON files in the head that should also be removed if javascript is removed, all these Gatsby files are in the</span>
    <span class="hljs-comment">// "/static" or /page-data directories.</span>
    <span class="hljs-keyword">if</span> (
      headComponent.props.href &amp;&amp;
      (headComponent.props.href.startsWith(<span class="hljs-string">`<span class="hljs-subst">${__PATH_PREFIX__}</span>/static/`</span>) ||
        headComponent.props.href.startsWith(<span class="hljs-string">`<span class="hljs-subst">${__PATH_PREFIX__}</span>/page-data/`</span>))
    ) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
    }

    <span class="hljs-keyword">return</span> (
      pageScripts.find(<span class="hljs-function"><span class="hljs-params">script</span> =&gt;</span> {
        <span class="hljs-keyword">return</span> (
          headComponent.props.as === <span class="hljs-string">"script"</span> &amp;&amp;
          <span class="hljs-string">`<span class="hljs-subst">${__PATH_PREFIX__}</span>/<span class="hljs-subst">${script.name}</span>`</span> === headComponent.props.href &amp;&amp;
          script.rel === headComponent.props.rel
        )
      }) === <span class="hljs-literal">undefined</span>
    )
  })
}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getPostBodyComponentsNoJS</span>(<span class="hljs-params">postBodyComponents, pluginOptions</span>) </span>{
  <span class="hljs-keyword">return</span> postBodyComponents.filter(<span class="hljs-function"><span class="hljs-params">postBodyComponent</span> =&gt;</span> {
    <span class="hljs-comment">// Not a react component and therefore not a &lt;script&gt;.</span>
    <span class="hljs-keyword">if</span> (!isReactElement(postBodyComponent)) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
    }
    <span class="hljs-keyword">if</span> (
      pluginOptions.excludeFiles &amp;&amp;
      postBodyComponent.props.src &amp;&amp;
      <span class="hljs-built_in">RegExp</span>(pluginOptions.excludeFiles).test(postBodyComponent.props.src)
    ) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
    }
    <span class="hljs-comment">// These are special Gatsby files we are calling out specifically.</span>
    <span class="hljs-keyword">if</span> (
      postBodyComponent.props.id &amp;&amp;
      (postBodyComponent.props.id === <span class="hljs-string">"gatsby-script-loader"</span> ||
        postBodyComponent.props.id === <span class="hljs-string">"gatsby-chunk-mapping"</span>)
    ) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
    }

    <span class="hljs-keyword">if</span> (postBodyComponent.props.src?.indexOf(<span class="hljs-string">"polyfill"</span>) &gt; <span class="hljs-number">-1</span>) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
    }

    <span class="hljs-keyword">return</span> (
      pageScripts.find(<span class="hljs-function"><span class="hljs-params">script</span> =&gt;</span> {
        <span class="hljs-keyword">return</span> (
          postBodyComponent.type === <span class="hljs-string">"script"</span> &amp;&amp;
          <span class="hljs-string">`<span class="hljs-subst">${__PATH_PREFIX__}</span>/<span class="hljs-subst">${script.name}</span>`</span> === postBodyComponent.props.src
        )
      }) === <span class="hljs-literal">undefined</span>
    )
  })
}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isReactElement</span>(<span class="hljs-params">reactNode</span>) </span>{
  <span class="hljs-keyword">return</span> reactNode.props !== <span class="hljs-literal">undefined</span>
}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkPathExclusion</span>(<span class="hljs-params">pathname, pluginOptions</span>) </span>{
  <span class="hljs-keyword">if</span> (!pluginOptions.excludePaths) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">RegExp</span>(pluginOptions.excludePaths).test(pathname)
}
</code></div><small class="shcb-language" id="shcb-language-37"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h2>Performance</h2>



<ul><li>Reduced from about 35 requests to less than 10.</li><li>The site finishes loading in about 0.7 seconds comparted to 1.5 seconds when using the JavaScript (and JSON) files.</li></ul>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-remove-client-side-javascript-from-gatsby/">How to remove client-side JavaScript from Gatsby</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/how-to-remove-client-side-javascript-from-gatsby/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8993</post-id>	</item>
		<item>
		<title>How to set docs as homepage for Docusaurus</title>
		<link>https://ricard.dev/how-to-set-docs-as-homepage-for-docusaurus/</link>
					<comments>https://ricard.dev/how-to-set-docs-as-homepage-for-docusaurus/#comments</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Thu, 22 Jul 2021 09:54:55 +0000</pubDate>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Docusaurus]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=8970</guid>

					<description><![CDATA[<p>If you'd prefer to show the Docusaurus docs section as a homepage instead of the regular landing page, you can use the snippet below. There are several ways to do this. From a simply server redirect, to the a hacky JavaScript redirect to the much desired official approach down below. Official solution: configure the docs [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-set-docs-as-homepage-for-docusaurus/">How to set docs as homepage for Docusaurus</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>If you'd prefer to show the <a href="https://docusaurus.io/">Docusaurus</a> docs section as a homepage instead of the regular landing page, you can use the snippet below.</p>



<p>There are several ways to do this. From a simply server redirect, to the a hacky JavaScript redirect to the much desired official approach down below.</p>



<h2>Official solution: configure the docs plugin</h2>



<p>Follow these 3 steps:</p>



<h3>1. Set a document with a slug /</h3>



<p>Create the markdown file you want as homepage inside <code>/docs/ </code>and set the slug to <code>/</code></p>


<pre class="wp-block-code"><div><code class="hljs">---
id: my-home-doc
slug: /
---

My home doc content....</code></div></pre>


<h3>2. Set the docs plugin to /</h3>



<p>In your <code>docusaurus.config.js</code> add <code>routeBasePath: '/'</code></p>


<pre class="wp-block-code" aria-describedby="shcb-language-38" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript">presets: [
  [
    <span class="hljs-string">'@docusaurus/preset-classic'</span>,
    {
      <span class="hljs-attr">docs</span>: {
        <span class="hljs-attr">routeBasePath</span>: <span class="hljs-string">'/'</span>,</code></div><small class="shcb-language" id="shcb-language-38"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h3>3. Delete the single.js file</h3>



<p>Find the following file and delete it:</p>


<pre class="wp-block-code"><div><code class="hljs">./src/pages/index.js</code></div></pre>


<p>Check out the <a href="https://docusaurus.io/docs/docs-introduction/#docs-only-mode">documentation for Docs-only mode</a>.</p>



<h2>Hack: use a redirect</h2>



<p>The initial version of this post I pointed out that you could replace the file contents of <code>src/pages/index.js</code> with this:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-39" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span>  { Redirect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Redirect</span> <span class="hljs-attr">to</span>=<span class="hljs-string">'/docs/intro'</span> /&gt;</span></span>;
}</code></div><small class="shcb-language" id="shcb-language-39"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<p>While this works it's an unnecessary redirect given the out of the box solution described above.</p>



<p><em>This was tested on Docusaurus 2.0.0</em></p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-set-docs-as-homepage-for-docusaurus/">How to set docs as homepage for Docusaurus</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/how-to-set-docs-as-homepage-for-docusaurus/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8970</post-id>	</item>
		<item>
		<title>Stop blindly preloading fonts</title>
		<link>https://ricard.dev/stop-blindly-preloading-fonts/</link>
					<comments>https://ricard.dev/stop-blindly-preloading-fonts/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Wed, 21 Jul 2021 10:31:42 +0000</pubDate>
				<category><![CDATA[HTML]]></category>
		<category><![CDATA[Web Performance]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=8961</guid>

					<description><![CDATA[<p>For a while now I thought that preloading your custom fonts was the way to go. In my defense in the past that was the recommended approach. Well, no longer 👋 I did a quick test with my cat's site Haku.cat and the result is clear: What exactly happened here? The fonts stopped being a [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/stop-blindly-preloading-fonts/">Stop blindly preloading fonts</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>For a while now I thought that preloading your custom fonts was the way to go. In my defense in the past that was the recommended approach.</p>


<pre class="wp-block-code" aria-describedby="shcb-language-40" data-shcb-language-name="HTML, XML" data-shcb-language-slug="xml"><div><code class="hljs language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"preload"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/fonts/cookie-v12-latin-regular.woff2"</span> <span class="hljs-attr">as</span>=<span class="hljs-string">"font"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"font/woff2"</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"preload"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/fonts/raleway-v18-latin-regular.woff2"</span> <span class="hljs-attr">as</span>=<span class="hljs-string">"font"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"font/woff2"</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"preload"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/fonts/raleway-v18-latin-800.woff2"</span> <span class="hljs-attr">as</span>=<span class="hljs-string">"font"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"font/woff2"</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"preload"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/fonts/raleway-v18-latin-italic.woff2"</span> <span class="hljs-attr">as</span>=<span class="hljs-string">"font"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"font/woff2"</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>/&gt;</span>
</code></div><small class="shcb-language" id="shcb-language-40"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">HTML, XML</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">xml</span><span class="shcb-language__paren">)</span></small></pre>


<h2>Well, no longer 👋</h2>



<p>I did a quick test with my cat's site <a href="https://www.haku.cat/">Haku.cat</a> and the result is clear:</p>



<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="2824" height="1422" src="https://ricard.dev/wp-content/uploads/2021/07/remove-preload-fonts.png" alt="" class="wp-image-8962" srcset="https://ricard.dev/wp-content/uploads/2021/07/remove-preload-fonts.png 2824w, https://ricard.dev/wp-content/uploads/2021/07/remove-preload-fonts-768x387.png 768w, https://ricard.dev/wp-content/uploads/2021/07/remove-preload-fonts-1536x773.png 1536w, https://ricard.dev/wp-content/uploads/2021/07/remove-preload-fonts-2048x1031.png 2048w" sizes="(max-width: 2824px) 100vw, 2824px" /><figcaption>Webpagetest.org screenshot</figcaption></figure>



<h2>What exactly happened here?</h2>



<p>The fonts stopped being a render blocking resource and moved down the waterfall allowing the browser to download the image before the font.</p>



<p>💡 This, might be something you don't want. Sure. Perhaps instead of 5 preloads you could only add the font preload for the most prominent text? Main body text? First heading? Menu?</p>



<p>In my case, <strong>I'd rather have the image load faster than the font.</strong> So removing the preload makes sense:</p>



<div class="wp-block-image"><figure class="aligncenter size-full"><img decoding="async" loading="lazy" width="2170" height="1160" src="https://ricard.dev/wp-content/uploads/2021/07/waterfall-before.png" alt="" class="wp-image-8963" srcset="https://ricard.dev/wp-content/uploads/2021/07/waterfall-before.png 2170w, https://ricard.dev/wp-content/uploads/2021/07/waterfall-before-768x411.png 768w, https://ricard.dev/wp-content/uploads/2021/07/waterfall-before-1536x821.png 1536w, https://ricard.dev/wp-content/uploads/2021/07/waterfall-before-2048x1095.png 2048w" sizes="(max-width: 2170px) 100vw, 2170px" /><figcaption>Before</figcaption></figure></div>



<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="2108" height="1140" src="https://ricard.dev/wp-content/uploads/2021/07/waterfall-after.png" alt="" class="wp-image-8964" srcset="https://ricard.dev/wp-content/uploads/2021/07/waterfall-after.png 2108w, https://ricard.dev/wp-content/uploads/2021/07/waterfall-after-768x415.png 768w, https://ricard.dev/wp-content/uploads/2021/07/waterfall-after-1536x831.png 1536w, https://ricard.dev/wp-content/uploads/2021/07/waterfall-after-2048x1108.png 2048w" sizes="(max-width: 2108px) 100vw, 2108px" /><figcaption>After</figcaption></figure>
<p>The post <a rel="nofollow" href="https://ricard.dev/stop-blindly-preloading-fonts/">Stop blindly preloading fonts</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/stop-blindly-preloading-fonts/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8961</post-id>	</item>
		<item>
		<title>How to DELETE with LIMIT and OFFSET in MySQL</title>
		<link>https://ricard.dev/how-to-delete-with-limit-and-offset-in-mysql/</link>
					<comments>https://ricard.dev/how-to-delete-with-limit-and-offset-in-mysql/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Mon, 12 Jul 2021 13:45:36 +0000</pubDate>
				<category><![CDATA[MySQL]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=8957</guid>

					<description><![CDATA[<p>It has been ages since I needed to write MySQL queries. I barely remember the basics. I found myself perplexed after learning DELETE does not allow you to specify a limit. I thought it simply acted as a SELECT but the results instead of being returned where deleted. It seems you need to nest 2 [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/how-to-delete-with-limit-and-offset-in-mysql/">How to DELETE with LIMIT and OFFSET in MySQL</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>It has been ages since I needed to write MySQL queries. I barely remember the basics.</p>



<p>I found myself perplexed after learning DELETE does not allow you to specify a limit. I thought it simply acted as a SELECT but the results instead of being returned where deleted.</p>



<p><a href="https://stackoverflow.com/questions/7142097/mysql-delete-statement-with-limit">It seems</a> you need to nest 2 queries in order to fool MySQL. This seems overkill but it does the job.</p>


<pre class="wp-block-code" aria-describedby="shcb-language-41" data-shcb-language-name="SQL (Structured Query Language)" data-shcb-language-slug="sql"><div><code class="hljs language-sql"><span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">FROM</span> <span class="hljs-string">`my_table`</span> <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> 
  <span class="hljs-keyword">IN</span> (<span class="hljs-keyword">select</span> <span class="hljs-keyword">id</span> <span class="hljs-keyword">from</span> 
      (<span class="hljs-keyword">select</span> <span class="hljs-keyword">id</span> <span class="hljs-keyword">FROM</span> <span class="hljs-string">`my_table`</span> 
       <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> <span class="hljs-string">`my_field`</span> <span class="hljs-keyword">DESC</span> 
       <span class="hljs-keyword">LIMIT</span> <span class="hljs-number">20</span>, <span class="hljs-number">50</span>) 
  x)</code></div><small class="shcb-language" id="shcb-language-41"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">SQL (Structured Query Language)</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">sql</span><span class="shcb-language__paren">)</span></small></pre><p>The post <a rel="nofollow" href="https://ricard.dev/how-to-delete-with-limit-and-offset-in-mysql/">How to DELETE with LIMIT and OFFSET in MySQL</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/how-to-delete-with-limit-and-offset-in-mysql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8957</post-id>	</item>
		<item>
		<title>Creating a custom RSS Reader with PHP</title>
		<link>https://ricard.dev/creating-custom-rss-reader/</link>
					<comments>https://ricard.dev/creating-custom-rss-reader/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Fri, 28 May 2021 07:35:00 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Vue.js]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=8941</guid>

					<description><![CDATA[<p>Before you eye roll me 🙄 after saying PHP, hear me out. 💬 What's your current RSS reader? Please tell me you still use RSS 🙏, you should, everybody should. I used to use Feedly and there was nothing wrong with it. At least for my use: Open it from time to time. Get my [&#8230;]</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/creating-custom-rss-reader/">Creating a custom RSS Reader with PHP</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Before you eye roll me 🙄 after saying PHP, hear me out.</p>



<h2> 💬 What's your current RSS reader?</h2>



<p>Please tell me you still use RSS 🙏, you should, everybody should. </p>



<p>I used to use <a href="https://feedly.com/">Feedly</a> and there was nothing wrong with it. At least for my use:</p>



<ul><li>Open it from time to time.</li><li>Get my unread items.</li><li>Read items.</li></ul>



<p>That's it. I don't really use categories, sharing or other features.</p>



<h2>🤓 Why build my own?</h2>



<p>First because I can and perhaps more importantly because <strong>privacy</strong>. It's a small step but now there's one less company that knows the sites I read, what I like, don't like, etc.</p>



<h2>🤷‍♂️ How do you build an RSS Reader?</h2>



<p>First thing you do is do a Github search see if there's any open source project with proper license you can fork. Turns out there is, I took <a href="https://github.com/travisred/bottomfeeder">a very simply implementation</a> and refactored it to fit my needs.</p>



<p>I built it with PHP and MySQL, it's easy and straightfoward. You can use many other tools that will also get the job done. I'm somewhat confortable with PHP, I remember enough of MySQL and my VPS already has both installed. So not much fricction.</p>



<p>Because I'm a Front-end developer I had to drop in a bunch of node packages 😁 Well, I'm using <a href="https://vuejs.org/">Vue</a> for this <a href="https://github.com/quicoto/RSS-Reader">RSS Reader</a>.</p>



<figure class="wp-block-image size-large"><img decoding="async" loading="lazy" width="1066" height="694" src="https://ricard.dev/wp-content/uploads/2021/05/rss-reader.png" alt="" class="wp-image-8942" srcset="https://ricard.dev/wp-content/uploads/2021/05/rss-reader.png 1066w, https://ricard.dev/wp-content/uploads/2021/05/rss-reader-768x500.png 768w" sizes="(max-width: 1066px) 100vw, 1066px" /></figure>



<h2>🤔 What does my RSS Reader do?</h2>



<ul><li>Add feeds</li><li>Delete feeds</li><li>See unread items</li><li>Mark items as read (when you click the link or hit the read button)</li><li>Mark all items as read</li><li>Star items</li></ul>



<p>That's it. You could have categories and other features, I don't need them. Sure I don't have "inline preview" as I would have in Feedly but I prefer to see the post in its original context. Not everybody likes that, some just want the regular pain text version. If that's what you want, this RSS Reader is not for you.</p>



<h2>simplexml_load_file()</h2>



<p>This function is the key of the whole thing. This allows you to read the XML feed files.</p>



<p><a href="https://www.php.net/manual/en/function.simplexml-load-file.php">https://www.php.net/manual/en/function.simplexml-load-file.php</a></p>



<p>Then it's just a matter of querying the MySQL database to store the results.</p>



<h2>PHP and MySQL work well together</h2>



<p>Look at the snippet below, it queries the database to update an item's field. Not too complicated:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-42" data-shcb-language-name="PHP" data-shcb-language-slug="php"><div><code class="hljs language-php">$mysqli-&gt;query(<span class="hljs-string">"update "</span>. $table_items . <span class="hljs-string">" set is_starred="</span> . $_GET[<span class="hljs-string">'is_starred'</span>] . <span class="hljs-string">" where id="</span> . $_GET[<span class="hljs-string">'id'</span>]);</code></div><small class="shcb-language" id="shcb-language-42"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">PHP</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">php</span><span class="shcb-language__paren">)</span></small></pre>


<h2>🏎 Vue CLI + BootstrapVue</h2>



<p>To create the Vue app I used the <a href="https://cli.vuejs.org/">Vue CLI</a> which is fantastic for this kind of projects. You don't have to think about build systems and whatnot.</p>



<p>Then for the components <a href="https://bootstrap-vue.org/">BoostrapVue</a> is a wonderful choice to get a bunch of components plug and play, ready to use.</p>



<h3>Fetch API</h3>



<p>From the Vue components I simply use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch API</a> to call my PHP and query the database. </p>


<pre class="wp-block-code" aria-describedby="shcb-language-43" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript">fetch(<span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">window</span>.rss_reader.apiUrl}</span><span class="hljs-subst">${endpoints.items}</span><span class="hljs-subst">${params}</span>`</span>)
  .then(<span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> response.json())
  .then(<span class="hljs-function"><span class="hljs-params">items</span> =&gt;</span> {
    <span class="hljs-keyword">this</span>.items = items;
    <span class="hljs-keyword">this</span>.showLoading = <span class="hljs-literal">false</span>;
  });</code></div><small class="shcb-language" id="shcb-language-43"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h2>💡 VPS</h2>



<p>I'm hosting this in an NGINX protected path in my VPS and I've set 2 cron jobs:</p>



<ul><li>Fetch feeds every hour.</li><li>Delete feed items older than 1 week (to keep my database light).</li></ul>



<h3>👨‍💻 Grab the code</h3>



<p><a href="https://github.com/quicoto/RSS-Reader">https://github.com/quicoto/RSS-Reader</a></p>
<p>The post <a rel="nofollow" href="https://ricard.dev/creating-custom-rss-reader/">Creating a custom RSS Reader with PHP</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/creating-custom-rss-reader/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8941</post-id>	</item>
		<item>
		<title>Creating a Kanban board with WordPress</title>
		<link>https://ricard.dev/creating-kanban-board-wordpress/</link>
					<comments>https://ricard.dev/creating-kanban-board-wordpress/#respond</comments>
		
		<dc:creator><![CDATA[Ricard Torres]]></dc:creator>
		<pubDate>Tue, 27 Apr 2021 08:48:12 +0000</pubDate>
				<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Vue.js]]></category>
		<guid isPermaLink="false">https://ricard.dev/?p=8921</guid>

					<description><![CDATA[<p>How to create a WordPress theme that loads a Kanban board built with Vue.js app which uses the WP REST API...</p>
<p>The post <a rel="nofollow" href="https://ricard.dev/creating-kanban-board-wordpress/">Creating a Kanban board with WordPress</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" loading="lazy" width="2810" height="1588" src="https://ricard.dev/wp-content/uploads/2021/04/kanban-board-wordpress.png" alt="" class="wp-image-8923" srcset="https://ricard.dev/wp-content/uploads/2021/04/kanban-board-wordpress.png 2810w, https://ricard.dev/wp-content/uploads/2021/04/kanban-board-wordpress-768x434.png 768w, https://ricard.dev/wp-content/uploads/2021/04/kanban-board-wordpress-1536x868.png 1536w, https://ricard.dev/wp-content/uploads/2021/04/kanban-board-wordpress-2048x1157.png 2048w" sizes="(max-width: 2810px) 100vw, 2810px" /></figure>



<p>You know me, I love to use WordPress not only as a blog but as a full fledged CMS and lately as an app backend using the <a href="https://developer.wordpress.org/rest-api/">WordPress REST API</a>. So today I want to share a proof of concept creating a <a href="https://vuejs.org/">Vue.js</a> app to create a Kanban board. </p>



<p><strong>Why?</strong></p>



<p>I want to keep track of my weekly tasks at work and for personal use. At the end of the week I usually have to report highlights of the past days, this is a great tool to do that.</p>



<p>I could've used Trello, or similar, but what's the fun in that? 😁 Plus, owning your data is nice for privacy 🕵️‍♂️</p>



<p><strong>What's the plan?</strong></p>



<p>The plan today is to create a WordPress Theme that has a single page Vue.js app which talks to the WordPress REST API to pull and create data. WordPress already has out of the box authentication (<code>/wp-admin</code>), so as long as you're logged in you're good go. You wouldn't know you're inside a WordPress instance.</p>



<h2>👷‍♂️ How will I structure the data?</h2>



<p>Easy. I'll use a <a href="https://developer.wordpress.org/plugins/post-types/">Custom Post Type</a> and a Custom Taxonomy to create my data structure.</p>



<ul><li>Post type: <strong>item</strong> (title, content and custom meta data)</li><li>Taxonomy: <strong>board</strong></li></ul>



<p>I'll have N items assigned to 1 or more boards. Each item will have a custom metadata field called <code>status</code> to know in what column (to do, in progress or done) the item is in.</p>



<p>Here's the quick code to create both post type and taxonomy:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-44" data-shcb-language-name="HTML, XML" data-shcb-language-slug="xml"><div><code class="hljs language-xml"><span class="php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">custom_post_type</span><span class="hljs-params">()</span> </span>{

	$labels = <span class="hljs-keyword">array</span>(
		...
	);
	$args = <span class="hljs-keyword">array</span>(
		<span class="hljs-string">'label'</span>                 =&gt; __( <span class="hljs-string">'Item'</span>, <span class="hljs-string">'text_domain'</span> ),
		<span class="hljs-string">'description'</span>           =&gt; __( <span class="hljs-string">'Item Description'</span>, <span class="hljs-string">'text_domain'</span> ),
		<span class="hljs-string">'labels'</span>                =&gt; $labels,
		<span class="hljs-string">'supports'</span>              =&gt; <span class="hljs-keyword">array</span>( <span class="hljs-string">'title'</span>, <span class="hljs-string">'editor'</span>, <span class="hljs-string">'custom-fields'</span> ),
		<span class="hljs-string">'taxonomies'</span>            =&gt; <span class="hljs-keyword">array</span>(),
		<span class="hljs-string">'hierarchical'</span>          =&gt; <span class="hljs-keyword">false</span>,
		<span class="hljs-string">'public'</span>                =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'show_ui'</span>               =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'show_in_menu'</span>          =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'menu_position'</span>         =&gt; <span class="hljs-number">5</span>,
		<span class="hljs-string">'menu_icon'</span>             =&gt; <span class="hljs-string">'dashicons-clipboard'</span>,
		<span class="hljs-string">'show_in_admin_bar'</span>     =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'show_in_nav_menus'</span>     =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'show_in_rest'</span>          =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'can_export'</span>            =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'has_archive'</span>           =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'exclude_from_search'</span>   =&gt; <span class="hljs-keyword">false</span>,
		<span class="hljs-string">'publicly_queryable'</span>    =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'capability_type'</span>       =&gt; <span class="hljs-string">'post'</span>,
	);
	register_post_type( <span class="hljs-string">'item'</span>, $args );

}
add_action( <span class="hljs-string">'init'</span>, <span class="hljs-string">'custom_post_type'</span>, <span class="hljs-number">0</span> );

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">custom_taxonomy</span><span class="hljs-params">()</span> </span>{

	$labels = <span class="hljs-keyword">array</span>(
		...
	);
	$args = <span class="hljs-keyword">array</span>(
		<span class="hljs-string">'labels'</span>                     =&gt; $labels,
		<span class="hljs-string">'hierarchical'</span>               =&gt; <span class="hljs-keyword">false</span>,
		<span class="hljs-string">'public'</span>                     =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'show_ui'</span>                    =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'show_admin_column'</span>          =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'show_in_nav_menus'</span>          =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'show_in_rest'</span>               =&gt; <span class="hljs-keyword">true</span>,
		<span class="hljs-string">'show_tagcloud'</span>              =&gt; <span class="hljs-keyword">true</span>,
	);
	register_taxonomy( <span class="hljs-string">'board'</span>, <span class="hljs-keyword">array</span>( <span class="hljs-string">'item'</span> ), $args );

}
add_action( <span class="hljs-string">'init'</span>, <span class="hljs-string">'custom_taxonomy'</span>, <span class="hljs-number">0</span> );
</span></code></div><small class="shcb-language" id="shcb-language-44"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">HTML, XML</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">xml</span><span class="shcb-language__paren">)</span></small></pre>


<h2>🏗 Create the Vue app</h2>



<p>One of the fastest ways to get you up and running without worrying about the build, setup, local server, production bundles... is <a href="https://cli.vuejs.org/">Vue CLI</a>. Done ✅ If you haven't checked it out yet, do it. It's so easy to use and powerful, yet it allows for customization.</p>



<h2>🤔 How to let the app know the REST API url?</h2>



<p>WordPress provides <code>rest_url()</code> which <a href="https://developer.wordpress.org/reference/functions/rest_url/">retrieves the URL to a REST endpoint.</a></p>



<p>I've searched for a ways to set a data attribute the app <code>div</code> but no luck (disclaimer, I'm no Vue.js expert. Could it be done?)</p>



<p>Well, this works so I'll go with it. Just adding a global variable with my rest URL and accessing it within the Vue app later:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-45" data-shcb-language-name="HTML, XML" data-shcb-language-slug="xml"><div><code class="hljs language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
  <span class="hljs-built_in">window</span>.kanban_ = { <span class="hljs-attr">restUrl</span>: <span class="hljs-string">'&lt;?=rest_url()?&gt;'</span> }
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"app"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></code></div><small class="shcb-language" id="shcb-language-45"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">HTML, XML</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">xml</span><span class="shcb-language__paren">)</span></small></pre>


<h2>😍 Create custom API endpoints</h2>



<p>Here's the fun part. You can create any endpoint you want. Here's what I created, get all the boards with items, create an item, update an item... Everything I need to fire POST/GET requests from the FE to the WordPress backend. Here's a snippet of the <a href="https://github.com/quicoto/WordPress-Kanban/blob/main/inc/custom-api.php">full code</a>:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-46" data-shcb-language-name="PHP" data-shcb-language-slug="php"><div><code class="hljs language-php">add_action( <span class="hljs-string">'rest_api_init'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">()</span> </span>{
  register_rest_route( <span class="hljs-string">'kanban/v1'</span>, <span class="hljs-string">'/all-boards'</span>, <span class="hljs-keyword">array</span>(
    <span class="hljs-string">'methods'</span> =&gt; <span class="hljs-string">'GET'</span>,
    <span class="hljs-string">'callback'</span> =&gt; <span class="hljs-string">'get_all_boards'</span>,
  ) );

  register_rest_route( <span class="hljs-string">'kanban/v1'</span>, <span class="hljs-string">'/board/(?P&lt;id&gt;\d+)'</span>, <span class="hljs-keyword">array</span>(
    <span class="hljs-string">'methods'</span> =&gt; <span class="hljs-string">'GET'</span>,
    <span class="hljs-string">'callback'</span> =&gt; <span class="hljs-string">'get_board'</span>
  ) );

  register_rest_route( <span class="hljs-string">'kanban/v1'</span>, <span class="hljs-string">'/create-item'</span>, <span class="hljs-keyword">array</span>(
    <span class="hljs-string">'methods'</span> =&gt; <span class="hljs-string">'POST'</span>,
    <span class="hljs-string">'callback'</span> =&gt; <span class="hljs-string">'create_item'</span>,
  ) );

  register_rest_route( <span class="hljs-string">'kanban/v1'</span>, <span class="hljs-string">'/update-item-status'</span>, <span class="hljs-keyword">array</span>(
    <span class="hljs-string">'methods'</span> =&gt; <span class="hljs-string">'POST'</span>,
    <span class="hljs-string">'callback'</span> =&gt; <span class="hljs-string">'update_item_status'</span>,
  ) );

  register_rest_route( <span class="hljs-string">'kanban/v1'</span>, <span class="hljs-string">'/update-item'</span>, <span class="hljs-keyword">array</span>(
    <span class="hljs-string">'methods'</span> =&gt; <span class="hljs-string">'POST'</span>,
    <span class="hljs-string">'callback'</span> =&gt; <span class="hljs-string">'update_item'</span>,
  ) );
} );</code></div><small class="shcb-language" id="shcb-language-46"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">PHP</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">php</span><span class="shcb-language__paren">)</span></small></pre>


<h3>📖 Creating, querying, updating...</h3>



<p>Nothing new really, just regular WordPress functions in the callbacks defined above. Such as using <a href="https://developer.wordpress.org/reference/functions/wp_insert_post/">wp_insert_post</a> function and using the POST data sent from the Vue app:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-47" data-shcb-language-name="PHP" data-shcb-language-slug="php"><div><code class="hljs language-php"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">create_item</span> <span class="hljs-params">($data)</span> </span>{
  $post = <span class="hljs-keyword">array</span>(
    <span class="hljs-string">'post_title'</span>    =&gt; wp_strip_all_tags( $data[<span class="hljs-string">'post_title'</span>] ),
    <span class="hljs-string">'post_content'</span>  =&gt; $data[<span class="hljs-string">'post_content'</span>],
    <span class="hljs-string">'post_status'</span>   =&gt; <span class="hljs-string">'publish'</span>,
    <span class="hljs-string">'post_type'</span> =&gt; <span class="hljs-string">'item'</span>
  );

  $post_id = wp_insert_post( $post );
 
  wp_set_object_terms(
    $post_id,
    <span class="hljs-keyword">array</span>($data[<span class="hljs-string">'board'</span>]),
    <span class="hljs-string">'board'</span>
  );
 
  add_post_meta($post_id, <span class="hljs-string">'status'</span>, $data[<span class="hljs-string">'status'</span>], <span class="hljs-keyword">true</span> );
}</code></div><small class="shcb-language" id="shcb-language-47"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">PHP</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">php</span><span class="shcb-language__paren">)</span></small></pre>


<p>💡<a href="https://ricard.dev/how-to-add-custom-meta-data-to-wordpress-rest-api/">How to add custom meta data to WordPress REST API</a></p>



<h2>🔥 Fire fetch requests from Vue</h2>



<p>Super easy and straight forward to do this, no much Vue involved but vanilla JS.</p>



<p>Here's the GET request to <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch API</a> all (or a specific) Kanban board:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-48" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript">fetchResources: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">let</span> endpoint = <span class="hljs-built_in">window</span>.kanban_.restUrl

  <span class="hljs-keyword">if</span> ( <span class="hljs-keyword">this</span>.$route.params.board_id ) {
    endpoint += <span class="hljs-string">`<span class="hljs-subst">${endpoints.board}</span>/<span class="hljs-subst">${<span class="hljs-keyword">this</span>.$route.params.board_id}</span>`</span>
  } <span class="hljs-keyword">else</span> {
    endpoint += endpoints.allBoards
  }

  <span class="hljs-keyword">this</span>.showLoading = <span class="hljs-literal">true</span>;

  fetch(endpoint)
    .then(<span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> response.json())
    .then(<span class="hljs-function"><span class="hljs-params">boards</span> =&gt;</span> {
      <span class="hljs-keyword">this</span>.boards = boards;
      <span class="hljs-keyword">this</span>.showLoading = <span class="hljs-literal">false</span>;

      <span class="hljs-keyword">if</span> ( <span class="hljs-keyword">this</span>.$route.params.board_id ) {
        <span class="hljs-keyword">const</span> metaTitle = <span class="hljs-keyword">this</span>.boards[<span class="hljs-number">0</span>].name
        <span class="hljs-built_in">document</span>.title = <span class="hljs-built_in">document</span>.title.replace(<span class="hljs-string">'%board%'</span>, metaTitle);
      }
    });
}</code></div><small class="shcb-language" id="shcb-language-48"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<p>Doing a POST request is as trivial as:</p>


<pre class="wp-block-code" aria-describedby="shcb-language-49" data-shcb-language-name="JavaScript" data-shcb-language-slug="javascript"><div><code class="hljs language-javascript">createItem: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{  
  <span class="hljs-keyword">const</span> data = {
    <span class="hljs-attr">board</span>: <span class="hljs-keyword">this</span>.newItem.board,
    <span class="hljs-attr">post_title</span>: <span class="hljs-keyword">this</span>.newItem.title,
    <span class="hljs-attr">post_content</span>: <span class="hljs-keyword">this</span>.newItem.content,
    <span class="hljs-attr">status</span>: <span class="hljs-number">1</span>
  };

  <span class="hljs-keyword">const</span> options = {
    <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
    <span class="hljs-attr">headers</span>: {
      <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>,
    },
    <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(data),
  };

  fetch(<span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">window</span>.kanban_.restUrl}</span><span class="hljs-subst">${endpoints.createItem}</span>`</span>, options)
    .then(<span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
      <span class="hljs-comment">// Refresh everything</span>
      <span class="hljs-keyword">this</span>.fetchResources();
    });
}</code></div><small class="shcb-language" id="shcb-language-49"><span class="shcb-language__label">Code language:</span> <span class="shcb-language__name">JavaScript</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">javascript</span><span class="shcb-language__paren">)</span></small></pre>


<h2>🛀 What's left?</h2>



<p>Just the UI for the app. Icons, components, just plain layout prototyping. The most complicated part creating the API and doing HTTP requests is done.</p>



<p>I usually rely on <a href="https://bootstrap-vue.org/">BootstrapVue</a> when I don't need anything custom, it provides a lot out of the box for these type of projects.</p>



<h2>👇 Grab the code </h2>



<p>Head over to <strong>Github</strong> and grab the theme and enjoy!<br><a href="https://github.com/quicoto/WordPress-Kanban">https://github.com/quicoto/WordPress-Kanban</a></p>
<p>The post <a rel="nofollow" href="https://ricard.dev/creating-kanban-board-wordpress/">Creating a Kanban board with WordPress</a> appeared first on <a rel="nofollow" href="https://ricard.dev">Ricard Torres dev</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://ricard.dev/creating-kanban-board-wordpress/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8921</post-id>	</item>
	</channel>
</rss>
