<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Dusty Candland</title>
  <subtitle>I'm a Ruby on Rails engineer who helps B2B SaaS startup founders bring their ideas to life. Unlike my competitors, I understand startups; the importance of moving quickly and iterating.</subtitle>
  <link href="https://candland.net/feed.xml" rel="self"/>
  <link href="https://candland.net"/>
  <updated>2026-06-08T15:19:34.899Z</updated>
  <id>https://candland.net</id>
  <author>
    <name>Dusty Candland</name>
    <email>candland@gmail.com</email>
  </author>
  <entry>
    <id>https://candland.net/posts/2021/01-14-ruby-ncurses-notes/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2021/01-14-ruby-ncurses-notes/"/>
    <updated>2026-06-08T15:19:23.695Z</updated><category term="post"/>
    <summary type="html"><![CDATA[
      null
    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>https://www.2n.pl/blog/basics-of-curses-library-in-ruby-make-awesome-terminal-apps
https://atevans.com/2017/08/02/ruby-curses-for-terminal-apps.html
https://stac47.github.io/ruby/curses/tutorial/2014/01/21/ruby-and-curses-tutorial.html
https://www.rubydoc.info/gems/curses/toplevel
https://github.com/piotrmurach/pastel Doesn't work with Curses</p>
<p>other options:
https://ttytoolkit.org/components/</p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2026/making-the-leap-name-change/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2026/making-the-leap-name-change/"/>
    <updated>2026-04-09T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="wpsupporthq"/>
    <summary type="html"><![CDATA[
      null
    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>I started WP Support HQ in 2014, I picked the name b/c I thought it was descriptive enough and that it
didn't matter too much. Never really liked the name, but stuck with it for 12 years now!</p>
<p>It's so generic and unmemorable. And turns out isn't as descriptive and obvious as I thought. People
don't know anything about what I do, maybe if they are familiar with WordPress, they get that part, but
that's fairly rare.</p>
<p>WP was allowed to be used in names, but in 2025, it seems like WordPress might be changing tone. The
thought of a name change popped back up.</p>
<p>I've always pushed past the name, saying I just need more marketing, more advertising, I'm just procrastinating; all true.
But I think the bigger issue has always been that I don't like the name. It says nothing about me, the brand I'm trying
to build, or anything really.</p>
<p>So, it's time for a change. It's scary. I'm worried the new name will be bad. That nobody will get it.
That it's dumb. I guess it might be any of those.</p>
<p>Besides all that, it's going to be a lot of work to change. Need to update the legal stuff, bank accounts,
notify customers. Hope customers see and remember the name changed! Update the website, socials, documents, and
a bunch of other stuff I'm not thinking of.</p>
<p>They say you should do stuff you're scared of. It helps you grow... New name coming soon!</p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2025/02-13-restic-backups-ubuntu/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2025/02-13-restic-backups-ubuntu/"/>
    <updated>2025-02-13T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="restic"/>
    <category term="linux"/>
    <category term="ubuntu"/>
    <category term="backups"/>
    <summary type="html"><![CDATA[
      null
    ]]></summary>
    <content type="html"><![CDATA[
      <p><p><a href="https://restic.net/">Restic</a> is a backup solution for Linux and other systems. I'm using it for
automated backups to an external drive, for now. Will also add an offsite solution soon. For now,
I'm mostly relying on Dropbox and GitHub.</p>
<h1>Install Restic</h1>
<p>I downloaded the pre-build binary and installed to <code>/usr/bin/</code>.</p>
<h1>Setup the repository</h1>
<p>I used <a href="https://mauricius.dev/basic-guide-on-how-to-use-restic-for-local-backups/">Basic guide on how to use restic for local backups</a>
and <code>restic -h</code> to set up the repository on my external drive.</p>
<p>This creates the repository for Restic to use for backups.</p>
<pre class="language-sh"><code class="language-sh">restic init <span class="token parameter variable">--repo</span> /media/dcandland/framework-backup/backups</code></pre>
<h1>Automate the backups</h1>
<p>Following the post from Bithive <a href="https://blog.bithive.space/post/automatic-backups-with-restic/">Automatic Backups with Restic</a>,
and adopting it to my setup, I set up the following in <code>/etc/backup</code>.</p>
<h2>Restic</h2>
<p><em>backup_env</em></p>
<pre class="language-sh"><code class="language-sh"><span class="token builtin class-name">export</span> <span class="token assign-left variable">RESTIC_REPOSITORY</span><span class="token operator">=</span><span class="token string">"/media/dcandland/framework-backup/backups"</span>
<span class="token builtin class-name">export</span> <span class="token assign-left variable">RESTIC_PASSWORD_FILE</span><span class="token operator">=</span><span class="token string">"/etc/restic/pw.txt"</span></code></pre>
<p><em>backup-excludes.txt</em></p>
<pre><code>.cache/*
.local/share/[Tt]rash*
<em>.backup</em>
.dropbox*
*.log
node_modules
*<em>/.git/</em>
*<em>/_build/</em>
*<em>/build/</em>
*<em>/.svelte-kit/</em>
*<em>/wire/</em>
*<em>/.vscode/</em>
*<em>/.terraform/</em>
*<em>/tmp/</em>
</code></pre>
<p>Add your repository password to <code>/etc/restic/pw.txt</code></p>
<p>This script runs the backups from systemd. Adjust as needed.</p>
<p><em>auto_backup.sh</em></p>
<pre class="language-sh"><code class="language-sh"><span class="token shebang important">#!/usr/bin/env bash</span>
<span class="token comment"># This script is intended to be run by a systemd timer</span></p>
<p><span class="token comment"># Exit on failure or pipefail</span>
<span class="token builtin class-name">set</span> <span class="token parameter variable">-e</span> <span class="token parameter variable">-o</span> pipefail</p>
<p><span class="token comment"># Set this to any location you like</span>
<span class="token assign-left variable">BACKUP_PATHS</span><span class="token operator">=</span><span class="token string">"/home/dcandland/"</span></p>
<p><span class="token assign-left variable">BACKUP_TAG</span><span class="token operator">=</span>systemd.timer</p>
<p><span class="token comment"># How many backups to keep.</span>
<span class="token assign-left variable">RETENTION_DAYS</span><span class="token operator">=</span><span class="token number">14</span>
<span class="token assign-left variable">RETENTION_WEEKS</span><span class="token operator">=</span><span class="token number">16</span>
<span class="token assign-left variable">RETENTION_MONTHS</span><span class="token operator">=</span><span class="token number">6</span>
<span class="token assign-left variable">RETENTION_YEARS</span><span class="token operator">=</span><span class="token number">1</span></p>
<p><span class="token builtin class-name">source</span> /etc/backup/backup_env</p>
<p><span class="token comment"># Remove locks in case other stale processes kept them in</span>
restic unlock <span class="token operator">&</span>
<span class="token function">wait</span> <span class="token variable">$!</span></p>
<p><span class="token comment"># Do the backup</span>
restic backup <span class="token punctuation"></span>
<span class="token parameter variable">--verbose</span> <span class="token punctuation"></span>
--exclude-file /etc/backup/backup-excludes.txt <span class="token punctuation"></span>
--one-file-system <span class="token punctuation"></span>
<span class="token parameter variable">--tag</span> <span class="token variable">$BACKUP_TAG</span> <span class="token punctuation"></span>
<span class="token variable">$BACKUP_PATHS</span> <span class="token operator">&</span>
<span class="token function">wait</span> <span class="token variable">$!</span></p>
<p><span class="token comment"># Remove old Backups</span>
restic forget <span class="token punctuation"></span>
<span class="token parameter variable">--verbose</span> <span class="token punctuation"></span>
<span class="token parameter variable">--tag</span> <span class="token variable">$BACKUP_TAG</span> <span class="token punctuation"></span>
<span class="token parameter variable">--prune</span> <span class="token punctuation"></span>
--keep-daily <span class="token variable">$RETENTION_DAYS</span> <span class="token punctuation"></span>
--keep-weekly <span class="token variable">$RETENTION_WEEKS</span> <span class="token punctuation"></span>
--keep-monthly <span class="token variable">$RETENTION_MONTHS</span> <span class="token punctuation"></span>
--keep-yearly <span class="token variable">$RETENTION_YEARS</span> <span class="token operator">&</span>
<span class="token function">wait</span> <span class="token variable">$!</span></p>
<p><span class="token comment"># Check if everything is fine</span>
restic check <span class="token operator">&</span>
<span class="token function">wait</span> <span class="token variable">$!</span></p>
<p><span class="token builtin class-name">echo</span> <span class="token string">"Backup done!"</span></code></pre>
<h2>Systemd</h2>
<p>I'm not very familiar with Systemd timers, but I resisted using 'cron' so I could learn more about them.</p>
<p>The timer:</p>
<p><em>/etc/systemd/system/backup.timer</em></p>
<pre class="language-sh"><code class="language-sh"><span class="token punctuation">[</span>Unit<span class="token punctuation">]</span>
<span class="token assign-left variable">Description</span><span class="token operator">=</span>Backup on schedule</p>
<p><span class="token punctuation">[</span>Timer<span class="token punctuation">]</span>
<span class="token assign-left variable">OnCalendar</span><span class="token operator">=</span>daily
<span class="token assign-left variable">Persistent</span><span class="token operator">=</span>true</p>
<p><span class="token punctuation">[</span>Install<span class="token punctuation">]</span>
<span class="token assign-left variable">WantedBy</span><span class="token operator">=</span>timers.target</code></pre>
<p>The service:</p>
<p><em>/etc/systemd/system/backup.service</em></p>
<pre class="language-sh"><code class="language-sh"><span class="token punctuation">[</span>Unit<span class="token punctuation">]</span>
<span class="token assign-left variable">Description</span><span class="token operator">=</span>Backup with restic</p>
<p><span class="token punctuation">[</span>Service<span class="token punctuation">]</span>
<span class="token assign-left variable">Type</span><span class="token operator">=</span>simple
<span class="token assign-left variable">Nice</span><span class="token operator">=</span><span class="token number">10</span>
<span class="token assign-left variable">ExecStart</span><span class="token operator">=</span>/etc/backup/auto_backup.sh
<span class="token comment">#$HOME must be set for restic to find /root/.cache/restic/</span>
<span class="token assign-left variable">Environment</span><span class="token operator">=</span><span class="token string">"HOME=/root"</span></code></pre>
<p>Add to Systemd</p>
<pre class="language-sh"><code class="language-sh">systemctl <span class="token builtin class-name">enable</span> backup.timer <span class="token parameter variable">--now</span></code></pre>
<h1>Usage</h1>
<p>Manually run:</p>
<pre class="language-sh"><code class="language-sh">systemctl start backup</code></pre>
<p>View the results:</p>
<pre class="language-sh"><code class="language-sh">journalctl <span class="token parameter variable">-u</span> backup.service</code></pre>
<p>Mount the latest snapshot to view backup:</p>
<pre class="language-sh"><code class="language-sh"><span class="token function">sudo</span> <span class="token function">mkdir</span> /media/dcandland/snapshot
<span class="token function">sudo</span> restic <span class="token function">mount</span> <span class="token parameter variable">-r</span> /media/dcandland/framework-backup/backups /media/dcandland/snapshot</code></pre></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2024/11-11-to-paint-the-devil/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2024/11-11-to-paint-the-devil/"/>
    <updated>2024-11-11T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="thoughts"/>
    <summary type="html"><![CDATA[
      null
    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>The saying <a href="https://idioms.thefreedictionary.com/paint+the+devil+on+the+wall">'to paint the devil on the wall'</a> is a German saying that's often used
to tell people they're being too pessimistic.</p>
<p>When I first heard it, it struck me as the Stoic idea for removing fear by
contemplating the worst case, <a href="https://dailystoic.com/premortem/">Premeditatio Malorum</a>.</p>
<p>I'm reading <a href="https://www.amazon.com/Outwitting-Devil-Reproduced-Manuscript-Publication/dp/1640952225/">Outwitting the Devil by Napolian Hill</a>.
In his interview with the Devil, the devil says he's negative energy and that's how he gains control over people.
Fear being one of his best weapons.</p>
<p>Using fear, he stops people from thinking for themselves.
And they become drifters.
Doing nothing. Accomplishing nothing.</p>
<p>I had a teacher, I wish I could remember who, that said 'don't be a jellyfish'.
She meant the same thing.
Don't just float along with the currents.
Choose a path.</p>
<p>Should we paint the devil on the wall?</p>
<p>I'm going to.</p>
<p>Then pick it apart.
Think through the worst cases.
Dispel the fears and negative energy.
And finally act accordingly.</p>
<p>Will it work?
Is it real?
Who knows.
I know negative visualization has been helpful for me in the past.
Old sayings sometimes offer great advise.
Negative energy and fear are a idea of the Devil I can understand.</p>
<p>Who cares what I think... What do YOU think?</p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2024/06-03-hacking-the-query-loop/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2024/06-03-hacking-the-query-loop/"/>
    <updated>2024-06-03T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="wordpress"/>
    <category term="queryloop"/>
    <category term="wpsupporthq"/>
    <summary type="html"><![CDATA[
      <p>I've been loving building sites with the block editor and more recently sites with full site editing (FSE).</p>
<p>One thing I've run into a few times is the need to hack the Query Loop block with some additional filtering and sorting.</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>I've been loving building sites with the block editor and more recently sites with full site editing (FSE).</p>
<p>One thing I've run into a few times is the need to hack the Query Loop block with some additional filtering and sorting.</p>
<p>Both time were event related. For the latest site, we created an event type using Pods Framework, storing the fields as postmeta.</p>
<p>For events, you'll want to sort by a date, like start_date and sometimes files past events.</p>
<p>The query loop supports a search field. I've been using that to hack the loop with sorting and filtering. The basic idea is to intercept the query and split it into other supported query options.</p>
<p>For example: <code>order:asc|orderby:start_date|meta_key:start_date|meta:start_date;$today;<</code></p>
<p>That command, will set the <code>order</code>, <code>orderby</code>, and <code>meta_key</code> for sorting. Then the meta key, will do a <code>meta_query</code> filtering for <code>start_date</code> less than <code>$today</code>.</p>
<p><code>$today</code> is a special value that gets today formatted as <code>yyyy-mm-dd</code>.</p>
<p>Add the following to the <code>functions.php</code> file in the child theme.</p>
<pre class="language-php"><code class="language-php"><span class="token function">add_action</span><span class="token punctuation">(</span> <span class="token string single-quoted-string">'pre_get_posts'</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span> <span class="token class-name class-name-fully-qualified type-declaration"><span class="token punctuation"></span>WP_Query</span> <span class="token variable">$query</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token variable">$query</span><span class="token operator">-></span><span class="token function">is_search</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token function">str_contains</span><span class="token punctuation">(</span> <span class="token variable">$query</span><span class="token operator">-></span><span class="token function">get</span><span class="token punctuation">(</span> <span class="token string single-quoted-string">'s'</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token string double-quoted-string">":"</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// Editor html encodes <</span>
<span class="token variable">$args</span> <span class="token operator">=</span> <span class="token function">html_entity_decode</span><span class="token punctuation">(</span><span class="token variable">$query</span><span class="token operator">-></span><span class="token function">get</span><span class="token punctuation">(</span> <span class="token string single-quoted-string">'s'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">''</span> <span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// echo "Args: " . $args . "<br>";</span></p>
<pre><code>	<span class="token comment">// Clear search query</span>
	<span class="token variable">$query</span><span class="token operator">-></span><span class="token function">set</span><span class="token punctuation">(</span> <span class="token string single-quoted-string">'s'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">''</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>

	<span class="token variable">$commands</span> <span class="token operator">=</span> <span class="token function">preg_split</span><span class="token punctuation">(</span> <span class="token string double-quoted-string">"/\|/"</span><span class="token punctuation">,</span> <span class="token variable">$args</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>

	<span class="token comment">// Loop through filters</span>
	<span class="token comment">// order, orderby, s, post_type, posts_per_page, paged, meta_key</span>
	<span class="token keyword">foreach</span> <span class="token punctuation">(</span> <span class="token variable">$commands</span> <span class="token keyword">as</span> <span class="token variable">$command</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span>
		<span class="token comment">// Split filter into key and value</span>
		<span class="token variable">$command</span> <span class="token operator">=</span> <span class="token function">preg_split</span><span class="token punctuation">(</span> <span class="token string double-quoted-string">"/:/"</span><span class="token punctuation">,</span> <span class="token variable">$command</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
		<span class="token variable">$key</span> <span class="token operator">=</span> <span class="token variable">$command</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
		<span class="token variable">$value</span> <span class="token operator">=</span> <span class="token variable">$command</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span>

		<span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token variable">$key</span> <span class="token operator">===</span> <span class="token string single-quoted-string">'meta'</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span>
			<span class="token variable">$params</span> <span class="token operator">=</span> <span class="token function">preg_split</span><span class="token punctuation">(</span> <span class="token string double-quoted-string">"/;/"</span><span class="token punctuation">,</span> <span class="token variable">$value</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
			<span class="token variable">$param_key</span> <span class="token operator">=</span> <span class="token variable">$params</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
			<span class="token variable">$param_value</span> <span class="token operator">=</span> <span class="token variable">$params</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
			<span class="token variable">$param_compare</span> <span class="token operator">=</span> <span class="token variable">$params</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">;</span>

			<span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token variable">$param_value</span> <span class="token operator">==</span> <span class="token string single-quoted-string">'$today'</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span>
				<span class="token variable">$param_value</span> <span class="token operator">=</span> <span class="token function">date</span><span class="token punctuation">(</span> <span class="token string single-quoted-string">'Y-m-d'</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
			<span class="token punctuation">}</span>

			<span class="token comment">// echo "Adding: " . $param_key . " " . $param_compare . " " . $param_value . "&lt;br>";</span>
			<span class="token variable">$query</span><span class="token operator">-></span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'meta_query'</span><span class="token punctuation">,</span> <span class="token keyword">array</span><span class="token punctuation">(</span>
				<span class="token keyword">array</span><span class="token punctuation">(</span>
					<span class="token string single-quoted-string">'key'</span> <span class="token operator">=></span> <span class="token variable">$param_key</span><span class="token punctuation">,</span>
					<span class="token string single-quoted-string">'value'</span> <span class="token operator">=></span> <span class="token variable">$param_value</span><span class="token punctuation">,</span>
					<span class="token string single-quoted-string">'compare'</span> <span class="token operator">=></span> <span class="token variable">$param_compare</span><span class="token punctuation">,</span>
				<span class="token punctuation">)</span><span class="token punctuation">,</span>
			<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
		<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
			<span class="token comment">// Add command to query</span>
			<span class="token variable">$query</span><span class="token operator">-></span><span class="token function">set</span><span class="token punctuation">(</span> <span class="token variable">$key</span><span class="token punctuation">,</span> <span class="token variable">$value</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
			<span class="token comment">// echo "Adding: " . $key . " => " . $value . "&lt;br>";</span>
		<span class="token punctuation">}</span>
	<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<p><span class="token punctuation">}</span> <span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Definitely not exhaustive or super intuitive, but it gets the job done and is usable for any Query Loop.</p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2024/04-11-adding-sitepress-to-rails/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2024/04-11-adding-sitepress-to-rails/"/>
    <updated>2024-04-11T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="rails"/>
    <category term="sitepress"/>
    <category term="seo"/>
    <summary type="html"><![CDATA[
      null
    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>I've been using Proofreader.io and thought it's about time I fixed some of the issues
that Proofreader is designed to surface. Like SEO & social meta tags, sitemaps, robots.txt.</p>
<p>I choose Sitepress to handle the content side of things, which make doing a lot of this
easier. Here's a walk though.</p>
<h2>Install and setup Sitepress</h2>
<p><a href="https://sitepress.cc/">Sitepress</a> - "Build ambitious content websites in Rails"</p>
<p>Add the gems. You can skip <code>markdown-rails</code> and <code>rouge</code> if you don't want Markdown support or syntax
highlighting.</p>
<pre class="language-ruby"><code class="language-ruby">gem <span class="token string-literal"><span class="token string">"sitepress-rails"</span></span>
gem <span class="token string-literal"><span class="token string">"markdown-rails"</span></span>
gem <span class="token string-literal"><span class="token string">"rouge"</span></span></code></pre>
<p>Run the installers. They will create the basic initializers and structure for the content. Content
ends up in <code>app/content</code>, along with helpers and layouts.</p>
<pre class="language-bash"><code class="language-bash">bin/rails generate sitepress:install
bin/rails generate markdown_rails:install</code></pre>
<p>Since these pages will be cached in production, we want to make sure they're signed out when generated.
I don't actually know if this is needed, but I want it to work that way in development regardless.</p>
<p>I ended up creating a sub directory for the website controllers, <code>app/controllers/website/</code>.</p>
<p>I'm also using <a href="https://github.com/candland/pundit_can">Pundit Can</a> which requires that I skip some filters.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">class</span> <span class="token class-name">Website</span><span class="token double-colon punctuation">::</span>WebsiteController <span class="token operator"><</span> Sitepress<span class="token double-colon punctuation">::</span>SiteController
layout <span class="token string-literal"><span class="token string">"website"</span></span></p>
<p>skip_scoped_check <span class="token symbol">:show</span>
skip_authorized_check <span class="token symbol">:show</span></p>
<p><span class="token keyword">protected</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">signed_in</span></span><span class="token operator">?</span>
<span class="token boolean">false</span>
<span class="token keyword">end</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">current_user</span></span>
<span class="token keyword">nil</span>
<span class="token keyword">end</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">current_account</span></span>
<span class="token keyword">nil</span>
<span class="token keyword">end</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">setup_current</span></span>
Current<span class="token punctuation">.</span>user <span class="token operator">=</span> <span class="token keyword">nil</span>
Current<span class="token punctuation">.</span>true_user <span class="token operator">=</span> <span class="token keyword">nil</span>
Current<span class="token punctuation">.</span>account <span class="token operator">=</span> <span class="token keyword">nil</span>
Current<span class="token punctuation">.</span>ip <span class="token operator">=</span> request<span class="token punctuation">.</span>remote_ip
<span class="token keyword">end</span>
<span class="token keyword">end</span></code></pre>
<p>Next update the routes so they user the overriden controller. I also removed <code>sitepress_root</code> in favor
of using the <code>root: true</code> parameter.</p>
<pre class="language-ruby"><code class="language-ruby">sitepress_pages root<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token symbol">controller</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"website/website"</span></span></code></pre>
<p>For some reason, <a href="https://github.com/sitepress/sitepress/issues/58">Slim isn't loaded correctly</a>.
I added an initialer for it.</p>
<p><code>config/initializers/sitepress.rb</code></p>
<pre class="language-ruby"><code class="language-ruby">ActionView<span class="token double-colon punctuation">::</span>Template<span class="token double-colon punctuation">::</span>Handlers<span class="token punctuation">.</span>extensions <span class="token operator"><<</span> <span class="token symbol">:slim</span></code></pre>
<p>Restart the server</p>
<p>Now Sitepress should be working and serving pages!</p>
<h2>SEO & Social</h2>
<p>Turns out <code>url_for</code> doesn't add anything when passed a string, so we need this helper.
Add to <code>app/content/helpers/page_helper.rb</code>.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">module</span> <span class="token class-name">PageHelper</span>
<span class="token comment"># <code>url_for</code> doesn't create a url when a string is passed</span>
<span class="token keyword">def</span> <span class="token method-definition"><span class="token function">path_url</span></span> path
<span class="token constant">URI</span><span class="token punctuation">.</span>const_get<span class="token punctuation">(</span>request<span class="token punctuation">.</span>scheme<span class="token punctuation">.</span>upcase<span class="token punctuation">)</span><span class="token punctuation">.</span>build<span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token symbol">host</span><span class="token operator">:</span> Rails<span class="token punctuation">.</span>application<span class="token punctuation">.</span>routes<span class="token punctuation">.</span>default_url_options<span class="token punctuation">.</span>fetch<span class="token punctuation">(</span><span class="token symbol">:host</span><span class="token punctuation">,</span> request<span class="token punctuation">.</span>host<span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token symbol">port</span><span class="token operator">:</span> Rails<span class="token punctuation">.</span>application<span class="token punctuation">.</span>routes<span class="token punctuation">.</span>default_url_options<span class="token punctuation">.</span>fetch<span class="token punctuation">(</span><span class="token symbol">:port</span><span class="token punctuation">,</span> request<span class="token punctuation">.</span>port<span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token symbol">path</span><span class="token operator">:</span> path
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span>to_s
<span class="token keyword">end</span>
<span class="token keyword">end</span></code></pre>
<h3>Meta Tags</h3>
<p>Next up, meta tags. These are used by the search engines and social sites to get a better
understanding of the page. <a href="https://ogp.me/">Open Graph</a> for Facebook, Twitter has some specifice tags,
and keywords & description for search engines.</p>
<p>This definitely could be done without a gem, but I liked the
<a href="https://github.com/kpumuk/meta-tags">Meta Tags</a> gem and used that.</p>
<p>Add to the Gemfile and bundle.</p>
<pre class="language-ruby"><code class="language-ruby">gem <span class="token string-literal"><span class="token string">"meta-tags"</span></span></code></pre>
<p>Install it. This adds a config, but I didn't change anything there.</p>
<pre class="language-bash"><code class="language-bash">rails generate meta_tags:install</code></pre>
<p>I want to use the frontmatter from Sitepress to override data for the meta tags. This maps the frontmatter
to a hash for the <code>set_meta_tags</code> method.</p>
<p>Add this to <code>app/content/helpers/page_helper.rb</code></p>
<pre class="language-ruby"><code class="language-ruby"><span class="token operator">...</span>
<span class="token keyword">def</span> <span class="token method-definition"><span class="token function">to_meta_tags</span></span> page
<span class="token punctuation">{</span>
<span class="token symbol">title</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>title<span class="token punctuation">,</span>
<span class="token symbol">description</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>description<span class="token punctuation">,</span>
<span class="token symbol">keywords</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>keywords<span class="token punctuation">,</span>
<span class="token symbol">image_src</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>image<span class="token punctuation">,</span>
<span class="token symbol">canonical</span><span class="token operator">:</span> canonical<span class="token punctuation">(</span>page<span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token symbol">noindex</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>noindex<span class="token punctuation">,</span>
<span class="token symbol">index</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>index<span class="token punctuation">,</span>
<span class="token symbol">nofollow</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>nofollow<span class="token punctuation">,</span>
<span class="token symbol">follow</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>follow<span class="token punctuation">,</span>
<span class="token symbol">noarchive</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>noarchive<span class="token punctuation">,</span>
<span class="token symbol">prev</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>prev<span class="token punctuation">,</span>
<span class="token symbol">next</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span><span class="token keyword">next</span><span class="token punctuation">,</span>
<span class="token symbol">og</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>og<span class="token punctuation">,</span>
<span class="token symbol">twitter</span><span class="token operator">:</span> page<span class="token punctuation">.</span>data<span class="token punctuation">.</span>twitter
<span class="token punctuation">}</span><span class="token punctuation">.</span>compact
<span class="token keyword">end</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">canonical</span></span> page
path_url<span class="token punctuation">(</span>page<span class="token punctuation">.</span>request_path<span class="token punctuation">.</span>gsub<span class="token punctuation">(</span><span class="token regex-literal"><span class="token regex">/index.html\z/</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">""</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>gsub<span class="token punctuation">(</span><span class="token regex-literal"><span class="token regex">/.html\z/</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">""</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>gsub<span class="token punctuation">(</span><span class="token regex-literal"><span class="token regex">//\z/</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">""</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token keyword">end</span>
<span class="token operator">...</span></code></pre>
<p>The <code>canonical</code> method strips <code>index.html</code>, <code>.html</code>, and tailing slashes to get a normalized URL. Also uses
the other helper method, <code>path_url</code> we added above.</p>
<p>Sitepress has some layout support, but I used the build in Rails layouts.
This is where we connect Sitepress to meta_tags with the <code>set_meta_tags</code> method.</p>
<p><code>app/views/layouts/website.html.slim</code></p>
<pre class="language-slim"><code class="language-slim">- set_meta_tags to_meta_tags(current_page)</p>
<ul>
<li>content_for :content do
= yield</li>
</ul>
<p>= render template: "layouts/application"</code></pre>
<p>Add the <code>display_meta_tags</code> to the application layout, or <code>head.html.slim</code> in my case. Here is where I set
the defaults for tags that might not be set.</p>
<pre class="language-slim"><code class="language-slim">= display_meta_tags site: "Proofreader.io",
reverse: true,
description: "Automates monitoring the technical details to make sure your content shines!",
image_src: image_url("manolo-chretien-252147-mono.jpg"),
og: { title: :title,
description: :description,
site_name: :site,
url: :canonical,
image: :image_src,
type: "website",
},
twitter: { card: "summary", site: "@proofreaderio" }</code></pre>
<h3>Sitemap</h3>
<p>It's a good idea to have a <a href="https://www.sitemaps.org/protocol.html">sitemap</a>. To set up we'll
need a route, controller, and view.</p>
<p>The route.</p>
<pre class="language-ruby"><code class="language-ruby">get <span class="token string-literal"><span class="token string">'/sitemap.xml'</span></span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">'sitemaps#index'</span></span><span class="token punctuation">,</span> <span class="token symbol">defaults</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token symbol">format</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">'xml'</span></span> <span class="token punctuation">}</span></code></pre>
<p>The controller.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">class</span> <span class="token class-name">Website</span><span class="token double-colon punctuation">::</span>SitemapsController <span class="token operator"><</span> ApplicationController
layout <span class="token boolean">false</span></p>
<p>skip_authorized_check <span class="token symbol">:index</span>
skip_scoped_check <span class="token symbol">:index</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">index</span></span>
render formats<span class="token operator">:</span> <span class="token symbol">:xml</span>
<span class="token keyword">end</span>
<span class="token keyword">end</span></code></pre>
<p>The view. Here we use Sitepress and <code>path_url</code> to create a map of pages. I'm removing any
that have <code>noindex</code> frontmatter.</p>
<pre class="language-ruby"><code class="language-ruby">xml<span class="token punctuation">.</span>instruct<span class="token operator">!</span> <span class="token symbol">:xml</span><span class="token punctuation">,</span> <span class="token symbol">version</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"1.0"</span></span>
xml<span class="token punctuation">.</span>tag<span class="token operator">!</span> <span class="token string-literal"><span class="token string">"urlset"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"xmlns"</span></span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">"http://www.sitemaps.org/schemas/sitemap/0.9"</span></span> <span class="token keyword">do</span>
site <span class="token operator">=</span> Sitepress<span class="token double-colon punctuation">::</span><span class="token class-name">Site</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token symbol">root_path</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"app/content"</span></span><span class="token punctuation">)</span>
site<span class="token punctuation">.</span>resources<span class="token punctuation">.</span>reject<span class="token punctuation">{</span>_1<span class="token punctuation">.</span>data<span class="token punctuation">.</span>noindex<span class="token punctuation">}</span><span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>resource<span class="token operator">|</span>
xml<span class="token punctuation">.</span>tag<span class="token operator">!</span> <span class="token string-literal"><span class="token string">"url"</span></span> <span class="token keyword">do</span>
xml<span class="token punctuation">.</span>tag<span class="token operator">!</span> <span class="token string-literal"><span class="token string">"loc"</span></span><span class="token punctuation">,</span> path_url<span class="token punctuation">(</span>resource<span class="token punctuation">.</span>request_path<span class="token punctuation">)</span>
xml<span class="token punctuation">.</span>lastmod resource<span class="token punctuation">.</span>asset<span class="token punctuation">.</span>updated_at<span class="token punctuation">.</span>strftime<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"%F"</span></span><span class="token punctuation">)</span>
<span class="token keyword">end</span>
<span class="token keyword">end</span>
<span class="token keyword">end</span></code></pre>
<h3>Breadcrumbs</h3>
<p>Not huge fan of breadcrumbs, but they are helpful for SEO and large content sites. Again we
can the Sitepress meta data to create them. <code>parents</code> is based on the file/folder stucture of the
content and is exactly what I needed.</p>
<p>In the <code>app/views/layouts/website.html.slim</code> file, I added this:</p>
<pre class="language-slim"><code class="language-slim">/ ...</p>
<ul>
<li>
<p>content_for :content do</p>
<ul>
<li>current_page.parents.any?
menu.flex.flex-row.gap-3.text-sm.text-gray-700.mb-4
<ul>
<li>current_page.parents.reverse.each_with_index do |parent, idx|
li
a href=parent.request_path = parent.data.title
<ul>
<li>if idx < current_page.parents.size - 1
li <br>
/ ...</code></pre>
<h3>Robots.txt</h3>
<p>Very similar to the sitemap, we can have a more dynamic <code>robots.txt</code>. You could also use the one from
the <code>public</code> directory, but I liked the idea of having a bit more control by moving into Rails.</p>
<p>The route.</p>
<pre class="language-ruby"><code class="language-ruby">get <span class="token string-literal"><span class="token string">"/robots.txt"</span></span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">"website/robots#index"</span></span><span class="token punctuation">,</span> <span class="token symbol">:defaults</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token symbol">format</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"text"</span></span><span class="token punctuation">}</span></code></pre>
<p>The controller.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">class</span> <span class="token class-name">Website</span><span class="token double-colon punctuation">::</span>RobotsController <span class="token operator"><</span> ApplicationController
layout <span class="token boolean">false</span></li>
</ul>
</li>
</ul>
</li>
</ul>
<p>skip_authorized_check <span class="token symbol">:index</span>
skip_scoped_check <span class="token symbol">:index</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">index</span></span>
render format<span class="token operator">:</span> <span class="token symbol">:text</span>
<span class="token keyword">end</span>
<span class="token keyword">end</span></code></pre>
<p>The view. Used ERB for this one. <code>index.text.erb</code>. Also added a reference to the sitemap.</p>
<pre class="language-erb"><code class="language-erb"><span class="token erb language-erb"><span class="token delimiter punctuation"><%</span><span class="token ruby language-ruby"> <span class="token keyword">if</span> Rails<span class="token punctuation">.</span>env<span class="token punctuation">.</span>production<span class="token operator">?</span> </span><span class="token delimiter punctuation">%></span></span>
User-Agent: *
Disallow:
<span class="token erb language-erb"><span class="token delimiter punctuation"><%</span><span class="token ruby language-ruby"> <span class="token keyword">else</span> </span><span class="token delimiter punctuation">%></span></span>
User-Agent: *
Noindex: /
<span class="token erb language-erb"><span class="token delimiter punctuation"><%</span><span class="token ruby language-ruby"> <span class="token keyword">end</span> </span><span class="token delimiter punctuation">%></span></span></p>
</li>
</ul>
<h2>Sitemap: <span class="token erb language-erb"><span class="token delimiter punctuation"><%=</span><span class="token ruby language-ruby"> path_url<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"/sitemap.xml"</span></span><span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span></code></pre>
<p>Remove the on in <code>public/</code></p>
<pre class="language-bash"><code class="language-bash"><span class="token function">rm</span> public/robots.txt</code></pre>
<h3>Error pages, 404, 422, 500</h3>
<p>Not sure if this actually helps with SEO, but I like the idea of having nicer error pages. Of course,
there are some cases where the app is actually broken and we'll end up with the Nginx default error pages.
Even still, I moved into Rails.</p>
<p>Remove the defaults</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">rm</span> public/<span class="token punctuation">{</span><span class="token number">404,422</span>,400<span class="token punctuation">}</span>.html</code></pre>
<p>Tell Rails to use our routes for the exceptions in <code>config/application.rb</code>.</p>
<pre class="language-ruby"><code class="language-ruby">    config<span class="token punctuation">.</span>exceptions_app <span class="token operator">=</span> routes</code></pre>
<p>Here's the 404 page in <code>app/content/pages/404.html</code>. I'm using the Sitepress meta data to
list some suggestions that be helpful. Also note the <code>noindex</code> frontmatter.</p>
<pre class="language-slim"><code class="language-slim">---
title: Oops, we couldn't find what you're looking for - 404
noindex: true</h2>
<p>.container.mx-auto.prose.lg:prose-lg.py-20.px-4'
h1 = current_page.data.title</p>
<h2>h2 Maybe something here?
menu.flex.flex-col.gap-1
- PageModel.all.reject{_1.data.noindex}.each do |page|
li : a href=page.request_path = page.data.title
</code></pre>
<p>And the 422 page.</p>
<pre class="language-slim"><code class="language-slim">---
title: The change you wanted was rejected - 422
noindex: true</h2>
<p>.container.mx-auto.prose.lg:prose-lg.py-20.px-4'
h1 = current_page.data.title</p>
<h2>p Maybe you tried to change something you didn't have access to.</code></pre>
<p>Lastly, the 500 page.</p>
<pre class="language-slim"><code class="language-slim">---
title: We're sorry, but something went wrong - 500
noindex: true</h2>
<p>.container.mx-auto.prose.lg:prose-lg.py-20.px-4'
h1 = current_page.data.title</p>
<p>p We've been notified of the error and will investigate it shortly.</p>
<p>p Feel free to contact us if you have any questions!
p
a href="/contact" Contact Us</code></pre>
<h3>Force HTTPS</h3>
<p>This is built in! Probably already handled by Nginx.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token comment"># config/environments/production.rb</span></p>
<p>Rails<span class="token punctuation">.</span>application<span class="token punctuation">.</span>configure <span class="token keyword">do</span>
<span class="token comment"># ...</span>
<span class="token comment"># force HTTPS on production</span>
config<span class="token punctuation">.</span>force_ssl <span class="token operator">=</span> <span class="token boolean">true</span>
<span class="token comment">#...</span>
<span class="token keyword">end</span></code></pre>
<h3>Redirect www to non-www</h3>
<p>Generally I prefer without <code>www</code>. Added this to the top of <code>config/routes.rb</code>. Any request
starting with <code>www.</code> will be redirected without it.</p>
<pre class="language-ruby"><code class="language-ruby">Rails<span class="token punctuation">.</span>application<span class="token punctuation">.</span>routes<span class="token punctuation">.</span>draw <span class="token keyword">do</span>
constraints<span class="token punctuation">(</span><span class="token symbol">host</span><span class="token operator">:</span> <span class="token regex-literal"><span class="token regex">/\Awww./i</span></span><span class="token punctuation">)</span> <span class="token keyword">do</span>
get <span class="token string-literal"><span class="token string">"(*any)"</span></span> <span class="token operator">=></span> redirect <span class="token punctuation">{</span> <span class="token operator">|</span>params<span class="token punctuation">,</span> request<span class="token operator">|</span>
<span class="token constant">URI</span><span class="token punctuation">.</span>parse<span class="token punctuation">(</span>request<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">.</span>tap <span class="token punctuation">{</span> <span class="token operator">|</span>uri<span class="token operator">|</span> uri<span class="token punctuation">.</span>host <span class="token operator">=</span> uri<span class="token punctuation">.</span>host<span class="token punctuation">.</span>gsub<span class="token punctuation">(</span><span class="token regex-literal"><span class="token regex">/\Awww./</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">""</span></span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">.</span>to_s
<span class="token punctuation">}</span>
<span class="token keyword">end</span></p>
<p><span class="token comment">#...</span>
<span class="token keyword">end</span></code></pre>
<h4>Testing</h4>
<p>Making the above change will break tests b/c the defaults host is <code>www.example.com</code>. Here's how
to default to <code>example.com</code>.</p>
<p>Add to <code>test/test_helper.rb</code>.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token comment">#...</span>
<span class="token keyword">class</span> <span class="token class-name">ActionDispatch</span><span class="token double-colon punctuation">::</span>IntegrationTest
before <span class="token punctuation">{</span> host<span class="token operator">!</span> <span class="token string-literal"><span class="token string">"example.com"</span></span> <span class="token punctuation">}</span>
<span class="token keyword">end</span></code></pre>
<h3>Redirects</h3>
<p>As the application grows, there will probably be cases where we want to redirect old URLs. I setup
a new routes file for them in <code>config/routes/redirects.rb</code>.</p>
<pre class="language-ruby"><code class="language-ruby">Rails<span class="token punctuation">.</span>application<span class="token punctuation">.</span>routes<span class="token punctuation">.</span>draw <span class="token keyword">do</span>
<span class="token comment">#</span>
<span class="token comment"># Redirects: https://guides.rubyonrails.org/routing.html#redirection</span>
<span class="token comment">#</span>
<span class="token comment"># get '/stories', to: redirect('/articles')</span>
<span class="token comment"># get '/stories/:name', to: redirect('/articles/%{name}')</span>
<span class="token keyword">end</span></code></pre>
<p>We need to tell Rails about those additional routes files in <code>config/application.rb</code>. This also
allows us to <a href="https://dev.to/buddyreno/organizing-routes-in-rails-1hd3">organize the routes</a>.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token comment">#...</span>
config<span class="token punctuation">.</span>paths<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"config/routes.rb"</span></span><span class="token punctuation">]</span><span class="token punctuation">.</span>concat <span class="token builtin">Dir</span><span class="token punctuation">[</span>Rails<span class="token punctuation">.</span>root<span class="token punctuation">.</span>join<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"config/routes/*.rb"</span></span><span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token comment">#...</span></code></pre>
<h2>References</h2>
<p>A lot of this was adapted and updated from <a href="https://medium.com/la-revanche-des-sites/seo-ruby-on-rails-the-comprehensive-guide-2018-b4101cc51b78">SEO & Ruby On Rails : the comprehensive guide 2018</a></p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2024/03-11-using-fail2ban-to-stop-an-attack/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2024/03-11-using-fail2ban-to-stop-an-attack/"/>
    <updated>2024-03-11T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="wordpress"/>
    <category term="fail2ban"/>
    <category term="nginx"/>
    <summary type="html"><![CDATA[
      <p>Had two attacks this week on different WordPress sites. One seems like a DDoS and the other an
enumeration attack. Fail2Ban stopped both by quickly adding new jails.</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>Had two attacks this week on different WordPress sites. One seems like a DDoS and the other an
enumeration attack. Fail2Ban stopped both by quickly adding new jails.</p>
<h2>Attack one, DDoS maybe</h2>
<p>The first attack had been going on for a while. It wasn't enough to take the server down (or many that wasn't the poing)
I'm not sure. Either way, it was clearly not user traffic.</p>
<p>Watching the server logs there was tons of HEAD & GET requests going to this page. With different sets of values for tag_ids
and cat_ids.</p>
<pre class="language-text"><code class="language-text">/training/webinars/action~week/tag_ids~388,601,614,273,315,521,390,311,131,348,314,195,157,616,522,169,611,376,547,600,70,144,613,146,172,375,617,171,168,408,274,130,495,497,525,275,559,387,148,104,312,380,514/request_format~html/</code></pre>
<p>That page does return a list of webinars, some of the tag_ids / cat_ids match, but I'm assuming they'd match on a lot
of sites.</p>
<p>Given the above, I created the following filter.</p>
<p><em>filters.d/nginx-someclientdomainname.conf</em></p>
<pre class="language-text"><code class="language-text">[Definition]</p>
<p>failregex = ^<HOST> -.* "(GET|HEAD) /training/webinars/action~week/.*</p>
<p>ignoreregex =</code></pre>
<p>And then added to the local jail config.</p>
<p><em>jail.local</em></p>
<pre class="language-text"><code class="language-text">#...</p>
<p>[nginx-someclientdomainname]
enabled = True
port = http,https
logpath = %(nginx_access_log)s
maxretry = 3
findtime = 60
bantime  = 172800
action   = iptables-multiport[name=nginx-shorter, port="http,https", protocol=tcp]</code></pre>
<blockquote>
<p>Don't use long names for the filters / jails. It does warn you, but it's also easy to miss, which I did. And that's
why I needed the action config</p>
</blockquote>
<p>Then restart Fail2Ban with <code>fail2ban-client restart</code>. I suggest tailing the Fail2Ban log, but it can be hard to see
what's happening when there is a lot of bad traffic.</p>
<p>All the IP I checked from this attack where from China</p>
<h2>Attack two, admin Ajax enumeration</h2>
<p>This was just one IP in the US. They were POST'ing to /wp-admin/admin-ajax.php. I'm not logging the POST body, so
not excatly sure what the point was. This was also enough traffic to take the site down a couple of times.</p>
<p><em>filters.d/nginx-wp-ajax.conf</em></p>
<pre class="language-text"><code class="language-text">[Definition]</p>
<p>failregex = ^<HOST> -.* "POST /+wp-admin/admin-ajax.php .*</p>
<p>ignoreregex =</code></pre>
<p><em>jail.local</em></p>
<pre class="language-text"><code class="language-text"># ...</p>
<h1>[nginx-wp-ajax]
enabled = True
port = http,https
logpath = %(nginx_access_log)s
maxretry = 20
findtime = 60
bantime  = 172800</code></pre>
<h2>Testing new filters</h2>
<p>Fail2Ban has a tool for testing filters, which is awesome so you know if the filter is working.</p>
<p>I first grabbed some sample data.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">tail</span> <span class="token parameter variable">-n</span> <span class="token number">5000</span> /var/log/nginx/access.log <span class="token operator">></span> sample.log</code></pre>
<p>Then you can test the filter with <code>fail2ban-regex</code>.</p>
<pre class="language-bash"><code class="language-bash">fail2ban-regex sample-2.log nginx-wp-ajax</code></pre>
<pre class="language-text"><code class="language-text">Running tests</h1>
<p>Use   failregex filter file : nginx-wp-ajax, basedir: /etc/fail2ban
Use         log file : sample-2.log
Use         encoding : UTF-8</p>
<h1>Results</h1>
<p>Failregex: 213 total
|-  #) [# of hits] regular expression
|   1) [213] ^<HOST> -.* "POST /+wp-admin/admin-ajax.php .*
`-</p>
<p>Ignoreregex: 0 total</p>
<p>Date template hits:
|- [# of hits] date format
|  [1000] Day(?P<_sep>[-/])MON(?P=_sep)ExYear[ :]?24hour:Minute:Second(?:.Microseconds)?(?: Zone offset)?
`-</p>
<p>Lines: 1000 lines, 0 ignored, 213 matched, 787 missed
[processed in 0.27 sec]</p>
<p>Missed line(s): too many to print.  Use --print-all-missed to print all 787 lines</code></pre>
<h2>Who's in jail</h2>
<p>Here're the commands for seeing IPs that are blocked.</p>
<p><code>fail2ban-client banned</code> will show all currently banned IPs.</p>
<p><code>fail2ban-client status <JAIL></code> will show details about a specific jail.</p>
<p><code>iptables -S | grep f2b</code> show's the IPs that currently banned in the firewall.</p>
<h2>Summarize the fail2ban logs</h2>
<h4>Reporting on today's activity:</h4>
<pre class="language-bash"><code class="language-bash"><span class="token function">grep</span> <span class="token string">"Ban "</span> /var/log/fail2ban.log <span class="token punctuation"></span>
<span class="token operator">|</span> <span class="token function">awk</span> -F<span class="token punctuation">[</span><span class="token punctuation"></span> <span class="token punctuation"></span>:<span class="token punctuation">]</span> <span class="token string">'{print $19,$17}'</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token operator">|</span> <span class="token function">uniq</span> <span class="token parameter variable">-c</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token parameter variable">-n</span> <span class="token operator">|</span></code></pre>
<h4>Group by IP address and Fail2Ban section:</h4>
<pre class="language-bash"><code class="language-bash"><span class="token function">grep</span> <span class="token string">"Ban "</span> /var/log/fail2ban.log <span class="token punctuation"></span>
<span class="token operator">|</span> <span class="token function">grep</span> <span class="token variable"><span class="token variable">$(</span><span class="token function">date</span> +%Y-%m-%d<span class="token variable">)</span></span> <span class="token punctuation"></span>
<span class="token operator">|</span> <span class="token function">awk</span> <span class="token string">'{print $NF}'</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token punctuation"></span>
<span class="token operator">|</span> <span class="token function">awk</span> <span class="token string">'{print $1,"("$1")"}'</span> <span class="token punctuation"></span>
<span class="token operator">|</span> <span class="token function">uniq</span> <span class="token parameter variable">-c</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token parameter variable">-n</span></code></pre>
<h4>Grouping by Date and Fail2Ban section</h4>
<pre class="language-bash"><code class="language-bash">zgrep <span class="token parameter variable">-h</span> <span class="token string">"Ban "</span> /var/log/fail2ban.log* <span class="token punctuation"></span>
<span class="token operator">|</span> <span class="token function">awk</span> <span class="token string">'{print $6,$1}'</span> <span class="token punctuation"></span>
<span class="token operator">|</span> <span class="token function">sort</span> <span class="token operator">|</span> <span class="token function">uniq</span> <span class="token parameter variable">-c</span></code></pre>
<p>From: <a href="https://www.the-art-of-web.com/system/fail2ban-log/">The Art of Web - Fail2Ban log</a></p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2024/02-25-weebly-export-to-wp-xml/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2024/02-25-weebly-export-to-wp-xml/"/>
    <updated>2024-02-25T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="wordpress"/>
    <category term="weebly"/>
    <category term="code"/>
    <category term="ruby"/>
    <summary type="html"><![CDATA[
      <p>Needed to migrate a Weebly site to WordPress. You can export all the web pages and images, but not the blog
content. This script exports into a WordPress XML file that can be imported.</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>Needed to migrate a Weebly site to WordPress. You can export all the web pages and images, but not the blog
content. This script exports into a WordPress XML file that can be imported.</p>
<h2>Export the blog</h2>
<p>You need to change the blog URL and add any categories you want to tag the posts with.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token comment">#! /usr/bin ruby</span></p>
<p><span class="token keyword">require</span> <span class="token string-literal"><span class="token string">"http"</span></span>
<span class="token keyword">require</span> <span class="token string-literal"><span class="token string">"nokogiri"</span></span></p>
<p><span class="token keyword">class</span> <span class="token class-name">Export</span>
attr_reader <span class="token symbol">:categories</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span> url<span class="token operator">:</span><span class="token punctuation">,</span> <span class="token symbol">categories</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
<span class="token variable">@url</span> <span class="token operator">=</span> url
<span class="token variable">@categories</span> <span class="token operator">=</span> categories
<span class="token keyword">end</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">call</span></span>
builder <span class="token operator">=</span> Nokogiri<span class="token double-colon punctuation">::</span><span class="token constant">XML</span><span class="token double-colon punctuation">::</span><span class="token class-name">Builder</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token symbol">encoding</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"UTF-8"</span></span><span class="token punctuation">)</span> <span class="token keyword">do</span> <span class="token operator">|</span>xml<span class="token operator">|</span>
xml<span class="token punctuation">.</span>rss<span class="token punctuation">(</span><span class="token symbol">:version</span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">"2.0"</span></span><span class="token punctuation">,</span>
<span class="token string-literal"><span class="token string">"xmlns:excerpt"</span></span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">"http://wordpress.org/export/1.2/excerpt/"</span></span><span class="token punctuation">,</span>
<span class="token string-literal"><span class="token string">"xmlns:content"</span></span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">"http://purl.org/rss/1.0/modules/content/"</span></span><span class="token punctuation">,</span>
<span class="token string-literal"><span class="token string">"xmlns:wfw"</span></span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">"http://wellformedweb.org/CommentAPI/"</span></span><span class="token punctuation">,</span>
<span class="token string-literal"><span class="token string">"xmlns:dc"</span></span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">"http://purl.org/dc/elements/1.1/"</span></span><span class="token punctuation">,</span>
<span class="token string-literal"><span class="token string">"xmlns:wp"</span></span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">"http://wordpress.org/export/1.2/"</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
xml<span class="token punctuation">.</span>channel <span class="token punctuation">{</span>
xml<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"wp"</span></span><span class="token punctuation">]</span><span class="token punctuation">.</span>wxr_version <span class="token string-literal"><span class="token string">"1.2"</span></span></p>
<pre><code>      xml<span class="token punctuation">.</span>title <span class="token string-literal"><span class="token string">"Weebly Blog"</span></span>
      xml<span class="token punctuation">.</span>link <span class="token variable">@url</span><span class="token punctuation">.</span>to_s
      xml<span class="token punctuation">.</span>description <span class="token string-literal"><span class="token string">""</span></span>
      xml<span class="token punctuation">.</span>language <span class="token string-literal"><span class="token string">"en"</span></span>
      xml<span class="token punctuation">.</span>pubDate <span class="token builtin">Time</span><span class="token punctuation">.</span>now<span class="token punctuation">.</span>to_s
      xml<span class="token punctuation">.</span>lastBuildDate <span class="token builtin">Time</span><span class="token punctuation">.</span>now<span class="token punctuation">.</span>to_s
      parse_page url<span class="token operator">:</span> <span class="token variable">@url</span><span class="token punctuation">,</span> <span class="token symbol">xml</span><span class="token operator">:</span> xml
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token keyword">end</span>

<span class="token builtin">File</span><span class="token punctuation">.</span>write<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"rss.xml"</span></span><span class="token punctuation">,</span> builder<span class="token punctuation">.</span>to_xml<span class="token punctuation">)</span>
</code></pre>
<p><span class="token keyword">end</span></p>
<p><span class="token keyword">private</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">parse_page</span></span> url<span class="token operator">:</span><span class="token punctuation">,</span> <span class="token symbol">xml</span><span class="token operator">:</span>
puts <span class="token string-literal"><span class="token string">"parse: </span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content">url</span><span class="token delimiter punctuation">}</span></span><span class="token string">"</span></span>
body <span class="token operator">=</span> get<span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">.</span>to_s</p>
<pre><code>html <span class="token operator">=</span> Nokogiri<span class="token double-colon punctuation">::</span><span class="token constant">HTML</span><span class="token punctuation">.</span>parse<span class="token punctuation">(</span>body<span class="token punctuation">)</span>

posts <span class="token operator">=</span> html<span class="token punctuation">.</span>css<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">".blog-post"</span></span><span class="token punctuation">)</span>

posts<span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>post<span class="token operator">|</span>
  title <span class="token operator">=</span> post<span class="token punctuation">.</span>css<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">".blog-title .blog-title-link"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text<span class="token punctuation">.</span>strip
  link <span class="token operator">=</span> post<span class="token punctuation">.</span>css<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">".blog-title .blog-title-link"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>attribute<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"href"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>value
  link <span class="token operator">=</span> absolute_url<span class="token punctuation">(</span>link<span class="token punctuation">)</span>
  date <span class="token operator">=</span> post<span class="token punctuation">.</span>css<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">".blog-date .date-text"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text<span class="token punctuation">.</span>strip
  time <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>strptime<span class="token punctuation">(</span>date<span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"%m/%d/%Y"</span></span><span class="token punctuation">)</span>
  read_more <span class="token operator">=</span> post<span class="token punctuation">.</span>css<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">".blog-read-more a"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>attribute<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"href"</span></span><span class="token punctuation">)</span>
  content <span class="token operator">=</span> <span class="token keyword">if</span> read_more<span class="token punctuation">.</span><span class="token keyword">nil</span><span class="token operator">?</span>
    post<span class="token punctuation">.</span>css<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">".blog-content"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>inner_html
  <span class="token keyword">else</span>
    get_post_page_content<span class="token punctuation">(</span><span class="token symbol">url</span><span class="token operator">:</span> read_more<span class="token punctuation">.</span>value<span class="token punctuation">)</span>
  <span class="token keyword">end</span>

  content <span class="token operator">=</span> fix_links<span class="token punctuation">(</span>content<span class="token punctuation">)</span>

  puts <span class="token string-literal"><span class="token string">"Title: </span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content">title</span><span class="token delimiter punctuation">}</span></span><span class="token string">: </span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content">time</span><span class="token delimiter punctuation">}</span></span><span class="token string">: </span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content">content<span class="token punctuation">.</span>length</span><span class="token delimiter punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content">link</span><span class="token delimiter punctuation">}</span></span><span class="token string">"</span></span>
  xml<span class="token punctuation">.</span>item <span class="token punctuation">{</span>
    xml<span class="token punctuation">.</span>title title
    xml<span class="token punctuation">.</span>description
    xml<span class="token punctuation">.</span>link link
    xml<span class="token punctuation">.</span>guid<span class="token punctuation">(</span><span class="token symbol">isPermaLink</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"false"</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> xml<span class="token punctuation">.</span>text<span class="token punctuation">(</span>link<span class="token punctuation">)</span> <span class="token punctuation">}</span>
    categories<span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>id<span class="token punctuation">,</span> name<span class="token operator">|</span>
      xml<span class="token punctuation">.</span>category<span class="token punctuation">(</span><span class="token symbol">domain</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"category"</span></span><span class="token punctuation">,</span> <span class="token symbol">nicename</span><span class="token operator">:</span> id<span class="token punctuation">)</span> <span class="token punctuation">{</span> xml<span class="token punctuation">.</span>cdata<span class="token punctuation">(</span>name<span class="token punctuation">)</span> <span class="token punctuation">}</span>
    <span class="token keyword">end</span>
    xml<span class="token punctuation">.</span>pubDate time<span class="token punctuation">.</span>to_s
    xml<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"dc"</span></span><span class="token punctuation">]</span><span class="token punctuation">.</span>creator <span class="token string-literal"><span class="token string">"admin"</span></span>
    xml<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"wp"</span></span><span class="token punctuation">]</span><span class="token punctuation">.</span>post_date time<span class="token punctuation">.</span>to_s
    xml<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"wp"</span></span><span class="token punctuation">]</span><span class="token punctuation">.</span>status <span class="token string-literal"><span class="token string">"publish"</span></span>
    xml<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"wp"</span></span><span class="token punctuation">]</span><span class="token punctuation">.</span>post_type <span class="token string-literal"><span class="token string">"post"</span></span>
    xml<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"excerpt"</span></span><span class="token punctuation">]</span><span class="token punctuation">.</span>encoded <span class="token punctuation">{</span> xml<span class="token punctuation">.</span>cdata<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">""</span></span><span class="token punctuation">)</span> <span class="token punctuation">}</span>
    xml<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"content"</span></span><span class="token punctuation">]</span><span class="token punctuation">.</span>encoded <span class="token punctuation">{</span> xml<span class="token punctuation">.</span>cdata<span class="token punctuation">(</span>content<span class="token punctuation">)</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token keyword">end</span>

previous <span class="token operator">=</span> html<span class="token punctuation">.</span>css<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">".blog-page-nav-previous .blog-link"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>attribute<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"href"</span></span><span class="token punctuation">)</span>
<span class="token keyword">if</span> <span class="token operator">!</span>previous<span class="token punctuation">.</span><span class="token keyword">nil</span><span class="token operator">?</span>
  <span class="token comment"># puts "Previous: #{absolute_url(previous)}"</span>
  parse_page<span class="token punctuation">(</span><span class="token symbol">url</span><span class="token operator">:</span> absolute_url<span class="token punctuation">(</span>previous<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token symbol">xml</span><span class="token operator">:</span> xml<span class="token punctuation">)</span>
<span class="token keyword">end</span>
</code></pre>
<p><span class="token keyword">end</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">fix_links</span></span> content
html <span class="token operator">=</span> Nokogiri<span class="token double-colon punctuation">::</span><span class="token constant">HTML</span><span class="token punctuation">.</span>parse<span class="token punctuation">(</span>content<span class="token punctuation">)</span></p>
<pre><code>html<span class="token punctuation">.</span>css<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"a"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>a<span class="token operator">|</span>
  href <span class="token operator">=</span> a<span class="token punctuation">.</span>attribute<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"href"</span></span><span class="token punctuation">)</span>
  <span class="token keyword">if</span> <span class="token operator">!</span>href<span class="token punctuation">.</span><span class="token keyword">nil</span><span class="token operator">?</span>
    href<span class="token punctuation">.</span>value <span class="token operator">=</span> absolute_url<span class="token punctuation">(</span>href<span class="token punctuation">.</span>value<span class="token punctuation">)</span>
  <span class="token keyword">end</span>
<span class="token keyword">end</span>

html<span class="token punctuation">.</span>css<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"img"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>img<span class="token operator">|</span>
  src <span class="token operator">=</span> img<span class="token punctuation">.</span>attribute<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"src"</span></span><span class="token punctuation">)</span>
  <span class="token keyword">if</span> <span class="token operator">!</span>src<span class="token punctuation">.</span><span class="token keyword">nil</span><span class="token operator">?</span>
    src<span class="token punctuation">.</span>value <span class="token operator">=</span> absolute_url<span class="token punctuation">(</span>src<span class="token punctuation">.</span>value<span class="token punctuation">)</span>
  <span class="token keyword">end</span>
<span class="token keyword">end</span>

html<span class="token punctuation">.</span>to_s
</code></pre>
<p><span class="token keyword">end</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">get</span></span> url
<span class="token constant">HTTP</span><span class="token punctuation">.</span>get<span class="token punctuation">(</span>url<span class="token punctuation">)</span>
<span class="token keyword">end</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">get_post_page_content</span></span> url<span class="token operator">:</span>
url <span class="token operator">=</span> absolute_url<span class="token punctuation">(</span>url<span class="token punctuation">)</span>
puts <span class="token string-literal"><span class="token string">"Reading from </span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content">url</span><span class="token delimiter punctuation">}</span></span><span class="token string">"</span></span>
body <span class="token operator">=</span> get<span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">.</span>to_s
html <span class="token operator">=</span> Nokogiri<span class="token double-colon punctuation">::</span><span class="token constant">HTML</span><span class="token punctuation">.</span>parse<span class="token punctuation">(</span>body<span class="token punctuation">)</span>
html<span class="token punctuation">.</span>css<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">".blog-post .blog-content"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>inner_html
<span class="token keyword">end</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">absolute_url</span></span> url
<span class="token constant">URI</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span>root_url<span class="token punctuation">,</span> url<span class="token punctuation">)</span><span class="token punctuation">.</span>to_s
<span class="token keyword">end</span></p>
<p><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">root_url</span></span>
<span class="token variable">@root_url</span> <span class="token operator">||=</span> <span class="token constant">URI</span><span class="token punctuation">.</span>parse<span class="token punctuation">(</span><span class="token variable">@url</span><span class="token punctuation">)</span><span class="token punctuation">.</span>tap <span class="token keyword">do</span> <span class="token operator">|</span>uri<span class="token operator">|</span>
uri<span class="token punctuation">.</span>path <span class="token operator">=</span> <span class="token string-literal"><span class="token string">""</span></span>
uri<span class="token punctuation">.</span>query <span class="token operator">=</span> <span class="token keyword">nil</span>
uri<span class="token punctuation">.</span>fragment <span class="token operator">=</span> <span class="token keyword">nil</span>
uri<span class="token punctuation">.</span>scheme <span class="token operator">=</span> <span class="token string-literal"><span class="token string">"https"</span></span>
<span class="token keyword">end</span>
<span class="token keyword">end</span>
<span class="token keyword">end</span></p>
<p><span class="token class-name">Export</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token symbol">url</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"https://www.example.org/outing-summaries-blog"</span></span><span class="token punctuation">,</span> <span class="token symbol">categories</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token string-literal"><span class="token string">"category-slug"</span></span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">"Category name"</span></span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span>call</code></pre>
<h3>Images</h3>
<p>For images, I used the website export to pull the image uploads, then ran a search and replace
on the posts to have the uploads match the location in WordPress.</p>
<p>I just uploaded them all to the wp-content directoy.</p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2024/01-25-dokku-postrgess-backsup/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2024/01-25-dokku-postrgess-backsup/"/>
    <updated>2024-01-25T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="dokku"/>
    <category term="digitalocean"/>
    <category term="javascript"/>
    <category term="postgres"/>
    <summary type="html"><![CDATA[
      <p>I'm using <a href="https://dokku.com/">Dokku</a> for my side projects and needed to make sure I had database backups.
My Dokku instance is on <a href="https://digitalocean.com/">DigitalOcean</a>, so I wanted the backups to be there too.</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>I'm using <a href="https://dokku.com">Dokku</a> for my side projects and needed to make sure I had database backups.
My Dokku instance is on <a href="https://digitalocean.com">DigitalOcean</a>, so I wanted the backups to be there too.</p>
<h2>Create a new Spaces bucket on DO</h2>
<p>I'm using a bucket per project, but you don't need to. So create bucket and a set of
access keys.</p>
<h2>Next setup Dokku</h2>
<p>You can do some of this through SSH, but it's easier to just do it all on the server.</p>
<p><code>proofreader_production</code> = the Dokku Postgres service name
<code>proofreader-backups</code> = the DigitalOcean bucket</p>
<p>We'll setup the authentication, then do a test backup, then schedule daily backups.</p>
<pre class="language-bash"><code class="language-bash">dokku postgres:backup-auth proofreader_production XXXXXXXXXXXXXXXXXXXX xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx us s3v4 https://sfo3.digitaloceanspaces.com</p>
<p><span class="token comment"># RUN</span>
dokku postgres:backup proofreader_production proofreader-backups</p>
<p><span class="token comment"># SCHEULDE 9:04 AM UTC</span>
dokku postgres:backup-schedule proofreader_production <span class="token string">"14 9 * * *"</span> proofreader-backups</code></pre>
<p><a href="https://github.com/dokku/dokku-postgres">Dokku Postgres Docs</a></p>
<h2>Setup lifecycle rules to clean up old backups</h2>
<p>I don't want to keep backups forever, 30 days seems good.
You can't add these rules using the DigitalOcean web UI, but you can with code.
Below is the script I'm using, modified from <a href="https://www.codemzy.com/blog/digitalocean-spaces-lifecycle-rules">How to delete DigitalOcean Spaces files after X days with lifecycle rules</a></p>
<p>I changed it to take the bucket name from the command line and to get the configured rules after.</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// https://www.codemzy.com/blog/digitalocean-spaces-lifecycle-rules</span></p>
<p><span class="token keyword">const</span> <span class="token punctuation">{</span>
S3Client<span class="token punctuation">,</span>
GetBucketLifecycleConfigurationCommand<span class="token punctuation">,</span>
PutBucketLifecycleConfigurationCommand<span class="token punctuation">,</span>
<span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"@aws-sdk/client-s3"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></p>
<p><span class="token comment">// connect to spaces</span>
<span class="token keyword">const</span> s3 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">S3Client</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">endpoint</span><span class="token operator">:</span> <span class="token string">"https://sfo3.digitaloceanspaces.com"</span><span class="token punctuation">,</span>
<span class="token literal-property property">forcePathStyle</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token literal-property property">region</span><span class="token operator">:</span> <span class="token string">"us"</span><span class="token punctuation">,</span>
<span class="token literal-property property">credentials</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">accessKeyId</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">DO_KEY</span><span class="token punctuation">,</span>
<span class="token literal-property property">secretAccessKey</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">DO_SECRET</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></p>
<p><span class="token keyword">const</span> bucketName <span class="token operator">=</span> process<span class="token punctuation">.</span>argv<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">;</span></p>
<p><span class="token comment">// create the lifecycle policy</span>
<span class="token keyword">const</span> putConfigCommand <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">PutBucketLifecycleConfigurationCommand</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">Bucket</span><span class="token operator">:</span> bucketName<span class="token punctuation">,</span>
<span class="token literal-property property">LifecycleConfiguration</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">Rules</span><span class="token operator">:</span> <span class="token punctuation">[</span>
<span class="token punctuation">{</span>
<span class="token constant">ID</span><span class="token operator">:</span> <span class="token string">"autodelete_rule"</span><span class="token punctuation">,</span>
<span class="token literal-property property">Expiration</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">Days</span><span class="token operator">:</span> <span class="token number">30</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">Status</span><span class="token operator">:</span> <span class="token string">"Enabled"</span><span class="token punctuation">,</span>
<span class="token literal-property property">Prefix</span><span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span> <span class="token comment">// Unlike AWS in DO this parameter is required</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></p>
<p><span class="token keyword">const</span> getConfigCommand <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">GetBucketLifecycleConfigurationCommand</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">Bucket</span><span class="token operator">:</span> bucketName<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></p>
<p><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code></span><span class="token string">Enabling lifecycle policy for </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bucketName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string"></code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">await</span> s3<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span>putConfigCommand<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Lifecycle policy enabled!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></p>
<p><span class="token keyword">try</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code></span><span class="token string">Getting lifecycle policy for </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bucketName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string"></code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> s3<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span>getConfigCommand<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>result<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></p>
<p><span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Run with</p>
<pre class="language-bash"><code class="language-bash"><span class="token assign-left variable">DO_KEY</span><span class="token operator">=</span>XXXX <span class="token assign-left variable">DO_SECRET</span><span class="token operator">=</span>xxxx <span class="token function">node</span> index.js your-bucket-name</code></pre>
<p>That's it! Daily Postgres backups from Dokku to DigitalOcean with a rolling 30 day window.</p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2023/12-26-gowebly-go-and-htmx/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2023/12-26-gowebly-go-and-htmx/"/>
    <updated>2023-12-26T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="go"/>
    <category term="golang"/>
    <category term="htmx"/>
    <category term="hyperscript"/>
    <category term="gowebly"/>
    <category term="templ"/>
    <summary type="html"><![CDATA[
      <p>I took a look at Go and <a href="https://htmx.org/">HTMX</a> using a web generation tool, <a href="https://gowebly.org/">Gowebly</a>.</p>
<blockquote>
<p>A next-generation CLI tool for building amazing web apps.
Go, htmx & hyperscript, modern CSS frameworks!</p>
</blockquote>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>I took a look at Go and <a href="https://htmx.org/">HTMX</a> using a web generation tool, <a href="https://gowebly.org/">Gowebly</a>.</p>
<blockquote>
<p>A next-generation CLI tool for building amazing web apps.
Go, htmx & hyperscript, modern CSS frameworks!</p>
</blockquote>
<h2>The Good</h2>
<p>Gowebly generates the start of web projects using a number of popular projects for
the backend and frontent.</p>
<p>It get's the project started quickly!
I started with <a href="https://github.com/gofiber/fiber">Fiber</a>, Templ, and <a href="https://tailwindcss.com/">TailwindCSS</a>. Templ is new to be, but I like the idea.
Basically JSX for Go. Write HTML & Go together and that get compiled to actual Go.</p>
<p>Gowebly provides a runner to run the compilation step in the background.
It also runs a <code>watch</code> script with Node or Bun.
TailwindCSS is compiled through that.</p>
<p>The only really opinionated aspect is the use of HTMX and <a href="https://hyperscript.org/docs/">HyperScript</a>.
I wanted to checkout HTMX, so that was a perfect fit.</p>
<h2>The Bad</h2>
<p>Not bad, but room for improvements?</p>
<p>The runner does recompile the CSS and Templ files, but doesn't hot reload the Go application.
A restart is still needed to see any updated changes outside of CSS.</p>
<p>It would be cool to be able to generate more parts of the application.
Maybe a new handler, which would scaffold the handler code and Templ files?</p>
<h2>Learning</h2>
<p>Templ is pretty cool, once I got the LSP and syntax setup in Vim.
Couldn't find string interpolation, which would be nice, but also didn't need it too much.</p>
<p>Example Templ for a podcast list item.</p>
<pre class="language-go"><code class="language-go">templ <span class="token function">PodcastItem</span><span class="token punctuation">(</span>p api<span class="token punctuation">.</span>Podcast<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token operator"><</span>li class<span class="token operator">=</span><span class="token string">"flex flex-col md:flex-row gap-4"</span><span class="token operator">></span>
<span class="token operator"><</span>div class<span class="token operator">=</span><span class="token string">"flex-1 min-w-32"</span><span class="token operator">></span>
<span class="token operator"><</span>img class<span class="token operator">=</span><span class="token string">"w-full"</span> src<span class="token operator">=</span><span class="token punctuation">{</span> p<span class="token punctuation">.</span>Images<span class="token punctuation">.</span>Thumbnail <span class="token punctuation">}</span> alt<span class="token operator">=</span><span class="token punctuation">{</span> fmt<span class="token punctuation">.</span><span class="token function">Sprintf</span><span class="token punctuation">(</span><span class="token string">"%s cover image"</span><span class="token punctuation">,</span> p<span class="token punctuation">.</span>Title<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token operator">/</span><span class="token operator">></span>
<span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span>
<span class="token operator"><</span>div class<span class="token operator">=</span><span class="token string">"flex flex-col gap-4"</span><span class="token operator">></span>
<span class="token operator"><</span>h2 class<span class="token operator">=</span><span class="token string">"text-lg font-medium"</span><span class="token operator">></span><span class="token punctuation">{</span> p<span class="token punctuation">.</span>Title <span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>h2<span class="token operator">></span>
<span class="token operator"><</span>div class<span class="token operator">=</span><span class="token string">"flex flex-row gap-4 text-gray-600"</span><span class="token operator">></span>
<span class="token operator"><</span>span<span class="token operator">></span><span class="token punctuation">{</span> p<span class="token punctuation">.</span>PublisherName <span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>span<span class="token operator">></span><span class="token operator"><</span>span<span class="token operator">></span><span class="token punctuation">{</span> p<span class="token punctuation">.</span>CategoryName <span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>span<span class="token operator">></span>
<span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span>
<span class="token operator"><</span>p class<span class="token operator">=</span><span class="token string">"text-gray-800"</span><span class="token operator">></span><span class="token punctuation">{</span> p<span class="token punctuation">.</span>Description <span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>p<span class="token operator">></span>
<span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span>
<span class="token operator"><</span><span class="token operator">/</span>li<span class="token operator">></span>
<span class="token punctuation">}</span></code></pre>
<p>I like HTMX so far.
It was super easy to get lazy loading, instant search, and paging setup.
I'd like to read a larger project that's using a lot of HTMX to see how it scales.</p>
<p>Example HTMX for the next page button.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span>
<span class="token attr-name">hx-get</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/api/podcasts?page=2<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#podcasts<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-swap</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>innerHTML show:top<span class="token punctuation">"</span></span>
<span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded<span class="token punctuation">"</span></span>
<span class="token punctuation">></span></span>Next<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span></code></pre>
<p>Didn't do anything with HyperScript.
Looks a little like <a href="https://alpinejs.dev/">AlpineJS</a>, maybe?</p>
<p>I really wanted hot reloading, so I setup a project called <a href="https://github.com/cosmtrek/air">Air</a>, that is made specifically for that.
And combined that with <a href="https://github.com/DarthSim/overmind">Overmind</a>, since I use that already.
I didn't get it configured perfectly, but it did the job.
Templ and JS have there own watchers, so those ran as Overmind jobs, with Air being the third.</p>
<p>Air setup for hot reloading.</p>
<p>Procfile.dev</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">web</span><span class="token punctuation">:</span> air
<span class="token key atrule">templ</span><span class="token punctuation">:</span> templ generate <span class="token punctuation">-</span><span class="token punctuation">-</span>watch
<span class="token key atrule">frontend</span><span class="token punctuation">:</span> npm run watch</code></pre>
<pre class="language-toml"><code class="language-toml"><span class="token comment"># .air.toml</span>
<span class="token key property">root</span> <span class="token punctuation">=</span> <span class="token string">"."</span>
<span class="token key property">tmp_dir</span> <span class="token punctuation">=</span> <span class="token string">"tmp"</span>
<span class="token punctuation">[</span><span class="token table class-name">build</span><span class="token punctuation">]</span>
<span class="token key property">cmd</span> <span class="token punctuation">=</span> <span class="token string">"go build -o ./tmp/main"</span>
<span class="token key property">bin</span> <span class="token punctuation">=</span> <span class="token string">"./tmp/main"</span>
<span class="token key property">delay</span> <span class="token punctuation">=</span> <span class="token number">1000</span> <span class="token comment"># ms</span>
<span class="token key property">exclude_dir</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"assets"</span><span class="token punctuation">,</span> <span class="token string">"tmp"</span><span class="token punctuation">,</span> <span class="token string">"vendor"</span><span class="token punctuation">,</span> <span class="token string">"node_modules"</span><span class="token punctuation">]</span>
<span class="token key property">include_ext</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"go"</span><span class="token punctuation">,</span> <span class="token string">"tpl"</span><span class="token punctuation">,</span> <span class="token string">"tmpl"</span><span class="token punctuation">,</span> <span class="token string">"html"</span><span class="token punctuation">]</span>
<span class="token key property">exclude_regex</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"_test\.go"</span><span class="token punctuation">]</span></code></pre>
<p>Go is fast and I like coding in Go.
Not sure it's the best for web applications, unless of course, speed is super important.</p>
<p>Another cool advantage, you could compile in the generated CSS and have a single executable web app.</p>
<p>As I've said before, I think HTMX and similar libraries will become the norm.</p>
<p>Check out the code: <a href="https://github.com/candland/gowebly-podcast-example">https://github.com/candland/gowebly-podcast-example</a></p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2023/12-05-zero-downtime-deploys/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2023/12-05-zero-downtime-deploys/"/>
    <updated>2023-12-05T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="infrastructure"/>
    <category term="processes"/>
    <summary type="html"><![CDATA[
      <p><strong>How we implemented zero downtime deploys to increase team productivity</strong></p>
<p>We had moved our API out of Rails into a Clojure service.
The service was running great, but deploys were a pain.
We couldn't have downtime when deploying; so we were taking servers offline, updating them, and then bringing them back online.
Using the typical rolling deployment pattern.</p>
<p>That worked, but it was slow and took a lot of manual time.
Being slow meant longer times to fix bugs.
Taking a lot of manual time meant wasted time and a hesitancy to release often.
Both are bad for the team and the business.</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p><strong>How we implemented zero downtime deploys to increase team productivity</strong></p>
<p>We had moved our API out of Rails into a Clojure service.
The service was running great, but deploys were a pain.
We couldn't have downtime when deploying; so we were taking servers offline, updating them, and then bringing them back online.
Using the typical rolling deployment pattern.</p>
<p>That worked, but it was slow and took a lot of manual time.
Being slow meant longer times to fix bugs.
Taking a lot of manual time meant wasted time and a hesitancy to release often.
Both are bad for the team and the business.</p>
<p>After a long day of pushing fixes and feeling frustrated with the deployment process, we decided we needed a better way.</p>
<p>Coming from Rails, the Unicorn web server had a way to restart its self using Unix signals.
I liked the way that worked and wanted our Clojure service to do the same thing.</p>
<p>I put together a quick proof of concept.
It worked, but there were some issues to figure out.</p>
<p>First, the service had to be able to shut itself down gracefully.
We implemented the Component system to ensure the service could shut down.</p>
<p>Secondly, we don't want to shut down until the new service was started.
This presented an issue since the web service was listening on a given port, the new version couldn't listen on the port until the first version shut down.</p>
<p>Lastly, since Clojure runs on the JVM, the startup time was significant.</p>
<p>To accomplish the last two requirements, the service would listen for the restart signal.
Once received, it would start the service in a new process.
The new process would start everything but the web server.
Once it got to that point, it would send a kill signal to the current instance and wait for the port to become available.
Then it would start its web server.</p>
<p>That a lot! But it made sure the new version could start and allowed <em>almost</em> zero downtime restarts.
The almost part (milliseconds) was handled by Nginx, so the net results was zero downtime deploys!</p>
<p>This allowed the team to have more confidence in deploys.
Reduced the number of servers we needed to have running.
Removed the need to manually deploy and reduced the deployment time from 30-40 minutes to a few minutes.</p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2023/11-17-ghost-on-dokku-with-sqlite/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2023/11-17-ghost-on-dokku-with-sqlite/"/>
    <updated>2023-11-17T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="ghost"/>
    <category term="dokku"/>
    <category term="sqlite"/>
    <summary type="html"><![CDATA[
      <p>I'm using the <a href="https://ghost.org/">Ghost</a> modern publishing platform for my daily newsletter.
It has a great UI, email support, and it's self hosted.
I'm using Dokku for the hosting and Sqlite for the DB.</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>I'm using the <a href="https://ghost.org">Ghost</a> modern publishing platform for my daily newsletter.
It has a great UI, email support, and it's self hosted.
I'm using Dokku for the hosting and Sqlite for the DB.</p>
<h2>Setup</h2>
<p>Setup the <a href="https://dokku.com">Dokku</a> appplication to use <a href="https://www.sqlite.org/index.html">Sqlite</a> (not actually supported), and install via <a href="https://www.docker.com">Docker</a> image.</p>
<pre class="language-bash"><code class="language-bash">dokku apps:create candland-cms</p>
<p>dokku domains:add cms.candland.net</p>
<p>dokku config:set --no-restart <span class="token assign-left variable">database__client</span><span class="token operator">=</span>sqlite3 <span class="token assign-left variable">database__connection__filename</span><span class="token operator">=</span>/var/lib/ghost/content/data/ghost.db <span class="token assign-left variable">database__useNullAsDefault</span><span class="token operator">=</span>true <span class="token assign-left variable">database__debug</span><span class="token operator">=</span>false</p>
<p>dokku config:set --no-restart <span class="token assign-left variable">NODE_ENV</span><span class="token operator">=</span>production</p>
<p>dokku git:from-image ghost:latest</p>
<p>dokku proxy:ports-add http:80:2368</p>
<p>dokku proxy:ports-remove http:2368:2368</code></pre>
<p>Setup pesistent storage for the database and content files.</p>
<pre class="language-bash"><code class="language-bash">dokku storage:ensure-directory</p>
<p>dokku storage:mount /var/lib/dokku/data/storage/candland-cms:/var/lib/ghost/content</p>
<p>dokku ps:rebuild</code></pre>
<p>Setup the domain and SSL</p>
<pre class="language-bash"><code class="language-bash">dokku config:set --no-restart <span class="token assign-left variable">DOKKU_LETSENCRYPT_EMAIL</span><span class="token operator">=</span>dusty@domain.net</p>
<p>dokku letsencrypt</p>
<p>dokku config:set <span class="token assign-left variable">url</span><span class="token operator">=</span>https://cms.candland.com</code></pre>
<h3>Database backups</h3>
<blockquote>
<p>Setup <a href="https://github.com/axelthegerman/dokku-litestream">Litestream</a> on the server</p>
</blockquote>
<pre class="language-bash"><code class="language-bash">dokku plugin:install https://github.com/axelthegerman/dokku-litestream litestream</p>
<p>dokku litestream:auth</p>
<p>dokku litestream:create candland-cms /var/lib/dokku/data/storage/candland-cms/data ghost.db s3://candland-cms-litestream.sfo3.digitaloceanspaces.com/</p>
<p>dokku litestream:start candland-cms</code></pre>
<h2>Email Setup</h2>
<p>I set up Mailgun for the newsletter sending, so I'm going to use that for the transactional email config too.</p>
<pre class="language-bash"><code class="language-bash">dokku config:set <span class="token assign-left variable">mail__transport</span><span class="token operator">=</span>SMTP <span class="token punctuation"></span>
<span class="token assign-left variable">mail__options__service</span><span class="token operator">=</span>Mailgun <span class="token punctuation"></span>
<span class="token assign-left variable">mail__options__host</span><span class="token operator">=</span>smtp.mailgun.org <span class="token punctuation"></span>
<span class="token assign-left variable">mail__options__port</span><span class="token operator">=</span><span class="token number">465</span> <span class="token punctuation"></span>
<span class="token assign-left variable">mail__options__secure</span><span class="token operator">=</span>true <span class="token punctuation"></span>
<span class="token assign-left variable">mail__options__auth__user</span><span class="token operator">=</span>user@domain.net <span class="token punctuation"></span>
<span class="token assign-left variable">mail__options__auth__pass</span><span class="token operator">=</span>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX <span class="token punctuation"></span>
<span class="token assign-left variable">mail__from</span><span class="token operator">=</span>dusty@domain.net</p>
<p>dokku ps:rebuild</code></pre>
<p>Check out the final result <a href="https://news.candland.net">https://news.candland.net</a> (yeah, I changed the subdomain).</p>
<h2>Upgrading</h2>
<blockquote>
<p>I'm not sure export/import are needed for minor version upgrades.</p>
</blockquote>
<ol>
<li>
<p>Export everything</p>
</li>
<li>
<p>Upgrade docker image</p>
</li>
</ol>
<pre class="language-bash"><code class="language-bash">dokku git:from-image ghost:latest</code></pre>
<ol start="3">
<li>Import everything</li>
</ol>
<h2>References</h2>
<p><a href="https://hub.docker.com/_/ghost/">Latest docker images</a></p>
<p><a href="https://bitkidd.dev/posts/deploy-ghost-using-dokku">https://bitkidd.dev/posts/deploy-ghost-using-dokku</a></p>
<p><a href="https://github.com/AxelTheGerman/dokku-litestream">https://github.com/AxelTheGerman/dokku-litestream</a></p>
<p><a href="https://github.com/zutrinken/attila">https://github.com/zutrinken/attila</a></p>
<p><a href="https://dani.gg/en/blog/create-a-ghost-theme-with-tailwind-css/">https://dani.gg/en/blog/create-a-ghost-theme-with-tailwind-css/</a></p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2023/10-21-margin-for-error/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2023/10-21-margin-for-error/"/>
    <updated>2023-10-21T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="coding"/>
    <summary type="html"><![CDATA[
      null
    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>Very early in my career, I was written up for a JavaScript bug.
I don't remember what bug, but I remember the analogy in the write-up.
It bothered me for a long while.</p>
<p>The analogy was basically:</p>
<blockquote>
<p>You drive to work every day and don't get into an accident, you should be able to write code every day that works.</p>
</blockquote>
<p>At first, I dismissed it as simply a bad analogy.</p>
<p>But why don't most people get into accidents more often? Bugs and accidents both seem like things that will happen.
Is coding simply harder than driving?</p>
<p>At some point I realized, it's because there is a margin for error built into driving, and you can increase that margin, if you want.
You can allow more room ahead of you.
And make sure you have an exit plan if something happens ahead.</p>
<p>You don't drive inches from the car ahead.
Lanes are not the exact with of cars.
You can veer a little and not have a huge accident.</p>
<p>Code generally doesn't have a margin for error built in... at least not much of one.
Compilers add some margin, but generally, there isn't a lot.</p>
<p>However, you, as an engineer, can add to the margin.
And realizing this changed the way I think about coding.</p>
<p>How? You might ask.</p>
<p>Compilers are one way.
Opting for a compiled language, increases the margin a little bit, depending on the language.</p>
<p>Tests increase the margin for error.
Assuming they're automated, unit, integration, and system tests all help.</p>
<p>Defensive programming can increase it, by making sure you're working with the data you think you are.</p>
<p>Capability checking, specifically with JavaScript, helps.
JavaScript is kinda unique since there are so many runtimes that might run the code.
Granted, this is so much better now!</p>
<p>Tools like linters, formatters, and static code analysis all increase the margin for error.</p>
<p>Small merges and deploys increase the margin for error. Using source control.</p>
<p>And in the code itself, you can check for errors and take some action.
Maybe retry a network request? Reset some state and try again? Try some backup path?</p>
<p>Message queues, RAID disks, replicated data, even TCP all increase the margin for error.
Some of which you don't even think about.</p>
<p>All these years later, I still think about that analogy and increasing my margin for error.
The experience wasn't awesome, but the lesson was.</p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2023/09-01-on-becoming-a-10x-engineer/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2023/09-01-on-becoming-a-10x-engineer/"/>
    <updated>2023-09-01T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="coding"/>
    <summary type="html"><![CDATA[
      <p>I know, 10x engineer is click bait-y...</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>I know, 10x engineer is click bait-y...</p>
<blockquote>
<p>A 10x developer will have insights and find solutions that would never occur to an average programmer; they will avoid entire categories of problems that eat up enormous amounts of time amongst average programmers. 10 engineers writing the <em>wrong</em> code could definitely be out performed by a single engineer writing the <em>right</em> code.
-- Yevgeniy Brikman <a href="https://www.ybrikman.com/writing/2013/09/29/the-10x-developer-is-not-myth/">The 10x developer is NOT a myth</a></p>
</blockquote>
<p>Yevgeniy also says 10x developers are rare. I agree on both.</p>
<p>But, can you become a 10x engineer... or at least an 8 or 9x?</p>
<p>I don't know, but these traits will make you a better engineer.</p>
<h2>Be a craftsman</h2>
<p>Take pride in your work and constantly improve. Always be learning more about software and your tools.</p>
<p>Understand higher level concepts like design patterns, OOP, FP, common database structures. Learn other programming languages. Experiment and try new ways of doing something. Build an application in two styles or two different stacks.</p>
<p>Know your tools. Take the time to learn your editor, its shortcuts, and what it helps you do. Spend time on the command line and learn the tools available there. Use linters and formatters.</p>
<p>Take responsibility for your work and learn from your mistakes.</p>
<h2>Be creative</h2>
<p>Look for simple solutions. Simple solutions are quicker to develop. They're easier to understand and debug. And they cheaper to maintain. Adding constraints to a problem can help find simple solutions.</p>
<p>Think of different ways to solve a problem. Having a few potential solutions can help you think about the problem from different angles and spot problems you might not of found until later. It also gives you a plan B or C in case something makes plan A unusable.</p>
<p>Use metaphors in your projects. This helps others get a quick idea of what the code or project does. It also helps with naming, which we all know is the hardest part of programming! I forget where I found this, but I definitely like it.</p>
<h2>Be a fixer</h2>
<p>I was reading about consulting, and different roles that consults play. It's by Jonathan Stark, <a href="https://jonathanstark.com/the-cab-ride">The Cab Ride</a>. It's short, worth the read, and inspired this post.</p>
<p>Understand the why, the motivation for doing something. What is the actual goal, the expected outcome. Dig deeper.</p>
<p>Often, someone will propose a solution, but nothing says it's the best solution. You will have insights in the code that could result in a better solution, or a faster solution, but only if you ask and understand the goal.</p>
<p>View problems from another perspective. Be empathetic.</p>
<h2>Be an enabler</h2>
<p>Help others do their best work.</p>
<p>Mentor other developers. You'll learn something along the way and they'll become better. Which makes the whole team better. Pair with people when they're stuck, but try to help they figure it out, don't just do it for them. Review PRs with the intent of providing helpful feedback. It doesn't all need to be implemented / fixed in the PR, it can just be notes or related topics to check out.</p>
<p>Build tools that help the whole team. If there is a lot of boilerplate code, write a generator to create it. Look for things that are repetitive and automatable, then automate them.</p>
<p>Streamline processes and systems. Adding code linters to the CI process, for example. This saves time in PRs and leads to consistent code.</p>
<p>Take the shit work and let others push themselves. This helps the team grow and understand more of the codebase. Plus, it gives you an opportunity to fix or optimize the shit work.</p>
<p><a href="https://www.billeager.com/the-true-10x-engineer/">The true 10x engineer</a> by Bill Eager expands on this idea.</p>
<h2>Don't be the brilliant asshole</h2>
<p>Their contribution is limited, at best. They're detrimental as the team grows. They don't help others, they're not humble, and what ever personal productivity gains they have won't compensate for the damage they do the overall team.</p>
<p><a href="https://thenewstack.io/10x-programmer-just-jerk/">Are you a 10x Programmer, or Just a Jerk?</a></p>
<p>Here are some other good posts I found while researching this.</p>
<ul>
<li><a href="https://blog.developerpurpose.com/7-daily-practices-of-top-developers-510777a0a486">7 daily practices of top developers</a></li>
<li><a href="https://blog.codegiant.io/how-to-become-a-10x-engineer/">How to become a 10x engineer</a></li>
<li><a href="https://machine-learning-made-simple.medium.com/become-a-10x-software-engineer-with-bayesian-thinking-220a8b065b16">Become a 10x Software Engineer with Bayesian Thinking</a></li>
<li><a href="https://levelup.gitconnected.com/how-to-be-a-10-engineer-be-a-10x-engineer-first-f670a9bca968">How to Be a 10% Engineer: Be a 10x Engineer First</a></li>
</ul>
<p>Maybe you want to go the other way and <a href="https://www.youtube.com/watch?v=pHJmmTivG1k">Become a -10x engineer</a></p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2023/07-10-ai-llm/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2023/07-10-ai-llm/"/>
    <updated>2023-07-10T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="ai"/>
    <category term="llm"/>
    <summary type="html"><![CDATA[
      <p>A list of resources for AI and LLM.</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>A list of resources for AI and LLM.</p>
<h2>Articles</h2>
<p>These cover a lot of the same ground about building LLMs into applications, mostly by creating agent systems.</p>
<p><a href="https://a16z.com/2023/06/20/emerging-architectures-for-llm-applications/">Emerging Architectures for LLM Applications</a> - Good overview of the current landscape for agents and the companies working in the space.</p>
<p><a href="https://huyenchip.com/2023/04/11/llm-engineering.html">Building LLM applications for production</a> - Includes some interesting potential use cases.</p>
<p><a href="https://medium.com/@simon_attard/leveraging-large-language-models-in-your-software-applications-9ea520fb2f34">Leveraging Large Language Models in your Software Applications</a> - Good overview of the components used to leverage LLMs in software.</p>
<p><a href="https://lilianweng.github.io/posts/2023-06-23-agent/">LLM Powered Autonomous Agents</a> - Technical and in-depth overview of agent systems; planning, memory, and tools.</p>
<p><a href="https://www.promptingguide.ai/">Prompt Engineering Guide</a> - Covers different approaches to prompting.
These are mostly for use in applications.</p>
<h2>Software</h2>
<p>I learned a lot about agents by reading the code for these projects.</p>
<p><a href="https://github.com/tmc/langchaingo">Langchaingo</a> - Golang implementation of the Langchain agent system. Not sure how closely this matches the python version.</p>
<p><a href="https://github.com/andreibondarev/langchainrb">Langchainrb</a> - Ruyby implementation based on the Langchain agent system. This is starting to diverge from the python version.</p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2023/06-29-dokku-upgrade-1-by-1/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2023/06-29-dokku-upgrade-1-by-1/"/>
    <updated>2023-06-29T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="dokku"/>
    <category term="ubuntu"/>
    <summary type="html"><![CDATA[
      <p>I've used the <code>dokku-update</code> app in the past, but version 0.30.0 has a breaking change, so I needed to upgrade to 0.29.x first.
It doesn't look like <code>dokku-update</code> app doesn't support specific versions.
Here's what I did to upgrade manually.</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>I've used the <code>dokku-update</code> app in the past, but version 0.30.0 has a breaking change, so I needed to upgrade to 0.29.x first.
It doesn't look like <code>dokku-update</code> app doesn't support specific versions.
Here's what I did to upgrade manually.</p>
<p>I'm running Ubuntu 20.04 LTS.</p>
<h2>The Process</h2>
<p>This is all pieced together from the <a href="https://dokku.com/docs/getting-started/upgrading/">Dokku upgrade guide</a>, which is very helpful.</p>
<pre class="language-bash"><code class="language-bash">dokku ps:stop <span class="token parameter variable">--all</span></p>
<p><span class="token comment"># update your local apt cache</span>
<span class="token function">apt-get</span> update <span class="token parameter variable">-qq</span></p>
<p><span class="token comment"># list available versions</span>
<span class="token function">apt-cache</span> madison dokku</p>
<p><span class="token comment"># update dokku and its dependencies</span>
<span class="token function">apt-get</span> <span class="token parameter variable">-y</span> --no-install-recommends <span class="token function">install</span> <span class="token assign-left variable">dokku</span><span class="token operator">=</span><span class="token number">0.27</span>.10 <span class="token parameter variable">-V</span>
<span class="token function">apt-get</span> <span class="token parameter variable">-y</span> --no-install-recommends <span class="token function">install</span> <span class="token assign-left variable">dokku</span><span class="token operator">=</span><span class="token number">0.28</span>.4 <span class="token parameter variable">-V</span></code></pre>
<p>On version 0.29.4 I ran into an error about missing the <code>docker-compose-plugin</code>.</p>
<h3>Fix missing docker-compose-plugin</h3>
<p>Follow this to install: <a href="https://docs.docker.com/engine/install/ubuntu/#set-up-the-repository">Docker GPG key/repository</a>.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">apt-get</span> update
<span class="token function">apt-get</span> <span class="token function">install</span> docker-compose-plugin</p>
<p><span class="token function">apt-get</span> <span class="token parameter variable">-y</span> --no-install-recommends <span class="token function">install</span> <span class="token assign-left variable">dokku</span><span class="token operator">=</span><span class="token number">0.29</span>.4 <span class="token parameter variable">-V</span></p>
<p><span class="token function">apt-get</span> <span class="token parameter variable">-y</span> --no-install-recommends <span class="token function">install</span> herokuish sshcommand plugn gliderlabs-sigil dokku-update dokku-event-listener</code></pre>
<p>After getting to version 0.29.4 I wanted to rebuild everything to make sure it was all okay.
Not sure if it needed or not.</p>
<pre class="language-bash"><code class="language-bash">dokku ps:rebuild <span class="token parameter variable">--all</span></code></pre>
<h3>Finishing up</h3>
<p>Everything worked! Now to get to the latest version, and update everything else.</p>
<pre class="language-bash"><code class="language-bash">dokku ps:stop <span class="token parameter variable">--all</span>
<span class="token function">apt</span> upgrade
<span class="token function">reboot</span></code></pre>
<p>After the reboot, restart everything</p>
<pre class="language-bash"><code class="language-bash">dokku ps:start <span class="token parameter variable">--all</span>
<span class="token function">apt</span> autoremove</code></pre>
<h2>References</h2>
<ul>
<li><a href="https://linuxopsys.com/topics/install-specific-version-package-apt">Find a specific version for apt package</a></li>
<li><a href="https://dokku.com/docs/getting-started/upgrading/">Upgrading Dokku</a></li>
</ul></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2023/06-25-setup-pgvector-on-dokku/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2023/06-25-setup-pgvector-on-dokku/"/>
    <updated>2023-06-25T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="postgres"/>
    <category term="pgvector"/>
    <category term="dokku"/>
    <category term="route285"/>
    <summary type="html"><![CDATA[
      <p>I'm starting to experiment with AI on Route285 and need a vector database.
Since I'm already using Postgres and Dokku, I'm adding pgvector.
Pgvector isn't installed by the main Postgres images.</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>I'm starting to experiment with AI on Route285 and need a vector database.
Since I'm already using Postgres and Dokku, I'm adding pgvector.
Pgvector isn't installed by the main Postgres images.</p>
<ol>
<li>Install a new version of Postgres with pgvector. <a href="https://hub.docker.com/r/ankane/pgvector/tags">Pgvector Docker Image</a></li>
<li>Migrate the data to the new instance.</li>
<li>Switch the app to the new instance.</li>
<li>Connect and install pgvector.</li>
</ol>
<h2>Setup Postgres with pgvector</h2>
<p>Creates a new service using the pgvector docker image.</p>
<pre class="language-bash"><code class="language-bash">dokku postgres:create route285-db-15 <span class="token parameter variable">--image</span> <span class="token string">"ankane/pgvector"</span> --image-version <span class="token string">"v0.4.4"</span></code></pre>
<h2>Migrate to the new database</h2>
<p>Migration process from <a href="https://github.com/dokku/dokku-postgres#upgrade-service--to-the-specified-versions">Upgrade service to the specified versions</a></p>
<pre class="language-bash"><code class="language-bash">dokku ps:stop route285-cms</p>
<p>dokku postgres:export route285-db <span class="token operator">></span> route285-db.sql</p>
<p>dokku postgres:import route285-db-15 <span class="token operator"><</span> route285-db.sql</p>
<p>dokku postgres:unlink route285-db route285-cms
-----<span class="token operator">></span> Unsetting DATABASE_URL</p>
<p>dokku postgres:link route285-db-15 route285-cms
-----<span class="token operator">></span> Setting config vars
DATABASE_URL:  postgres://postgres<span class="token punctuation">..</span>.</p>
<p>dokku ps:start route285-cms</code></pre>
<h2>Install the pgvector extension</h2>
<p>Connect to the new database.</p>
<pre class="language-bash"><code class="language-bash">dokku postgres:connect route285-db-15</code></pre>
<p>Install the extension. Also make sure the imported data looks good.</p>
<pre class="language-sql"><code class="language-sql">route285_db_15<span class="token operator">=</span><span class="token comment"># CREATE EXTENSION vector;</span>
<span class="token keyword">CREATE</span> EXTENSION</code></pre>
<h2>Cleanup</h2>
<pre class="language-bash"><code class="language-bash">dokku postgres:stop route285-db
dokku postgres:delete route285-db</code></pre>
<h2>References</h2>
<ul>
<li><a href="https://github.com/dokku/dokku-postgres">Dokku Postgres</a></li>
<li><a href="https://github.com/pgvector/pgvector">pgvector</a></li>
<li><a href="https://hub.docker.com/r/ankane/pgvector/tags">pgvector Docker Image</a></li>
</ul></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2023/06-04-premailer-webpacker-full-paths/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2023/06-04-premailer-webpacker-full-paths/"/>
    <updated>2023-06-04T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="rails"/>
    <category term="premailer"/>
    <category term="webpack"/>
    <category term="shakapacker"/>
    <summary type="html"><![CDATA[
      <p>I've ran into this a couple of times and it's taken forever to figure out! I don't know how
I fixed it before, but this is how I fixed it this time.</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>I've ran into this a couple of times and it's taken forever to figure out! I don't know how
I fixed it before, but this is how I fixed it this time.</p>
<h2>The Problem</h2>
<p>When using premailer-rails with webpacker, I run into this error:</p>
<pre><code>ArgumentError: not an HTTP URI
</code></pre>
<p>After some digging I found the issue is with the <code>premailer-rails</code> gem. It's trying to
request the stylesheet using a full path; <code>file://....</code>. This path is coming from the webpack
manifest file. I'm not sure why it's using a full path, but it is.</p>
<h2>The Solution</h2>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token punctuation">{</span> webpackConfig<span class="token punctuation">,</span> merge <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'shakapacker'</span><span class="token punctuation">)</span></p>
<p><span class="token keyword">const</span> customConfig <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token literal-property property">output</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">publicPath</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">[</span><span class="token string">"RAILS_ENV"</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">"test"</span> <span class="token operator">?</span> <span class="token string">"/packs-test/"</span> <span class="token operator">:</span> webpackConfig<span class="token punctuation">.</span>output<span class="token punctuation">.</span>publicPath<span class="token punctuation">,</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></p>
<p>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token function">merge</span><span class="token punctuation">(</span>webpackConfig<span class="token punctuation">,</span> customConfig<span class="token punctuation">)</span></code></pre>
<p>This forces webapck to use a relative path instead of a full path.</p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2023/05-24-wordpress-verify-checksums/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2023/05-24-wordpress-verify-checksums/"/>
    <updated>2023-05-24T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="wordpress"/>
    <category term="commandwp"/>
    <category term="security"/>
    <category term="wp-cli"/>
    <summary type="html"><![CDATA[
      null
    ]]></summary>
    <content type="html"><![CDATA[
      <p><p>I recently implemented verifying checksums in <a href="https://commandwp.com">CommandWP</a> and found some interesting results. 65% of sites had checksums that didn't verify! Here's a look at what I found.</p>
<h2>What changed?</h2>
<p><code>wp-admin/.rnd</code>  - Random seed files left over from some process. <a href="https://wordpress.org/support/topic/unknown-file-in-wordpress-core-wp-admin-rnd-type-file/">Ref</a></p>
<p><code>wp-config.transfer.php</code> - My fault, left over from migrations, (this was a lot of the sites).</p>
<p><code>readme.html</code> - File is missing.</p>
<p><code>shortcodes-ultimate/freemius/assets/img/tickera-event-calendar.png</code>
<code>shortcodes-ultimate/freemius/assets/img/tickera-events-listing.png</code> - Added, but doesn't seem worth figuring out why.</p>
<p><code>wp-includes/file.php</code> - PHP File Manager. Seems bad. <a href="https://github.com/alexantr/filemanager">GitHub</a>. This same file was in three locations. Feels like a forgotten back door, but guessing it's left over from an inexperienced developer.</p>
<p><code>all-in-one-event-calendar</code> - One interesting thing, their SVN doesn't have tags, so we can't compare with that version. Secondly, it looks like the files are cached and created later. The files in trunk exist, but are 0 bytes.</p>
<p><code>quick-pagepost-redirect-plugin</code> - Seems this plugin might have been taken over by bad developers? <a href="https://wordpress.org/support/plugin/quick-pagepost-redirect-plugin/">Ref</a>. I removed it.</p>
<p><code>wp-futures-4.3.1.php</code> - Not sure what this is, removed it.</p>
<p><code>wp-includes/css/modules.php</code> - Looks like a Joomla?! Printed out the current directory.</p>
<p><code>wordpress-automatic-image-hotlink-protection</code> - This plugin also doesn't have tags, and so source is to trunk. Seems the plugin is abandoned, changed owners, and is no longer available. <a href="https://plugins.svn.wordpress.org/wordpress-automatic-image-hotlink-protection/trunk/readme.txt">Plugin readme.txt</a>.</p>
<h2>Check your site</h2>
<p>If you want to check them on your site, you can use <a href="https://wp-cli.org">WP-CLI</a>.</p>
<pre class="language-bash"><code class="language-bash">wp core verify-checksums --skip-plugins
wp plugin verify-checksums <span class="token parameter variable">--all</span></code></pre>
<p>These will grab the expected file checksums from WordPress and check them against the files you have locally.</p>
<p>Secondly, premium plugins aren't checked, which sucks but understandable since the source isn't available.</p>
<h3>What changed?</h3>
<p>So here's an example of a file that didn't verify and the steps to see what's changed.</p>
<pre class="language-bash"><code class="language-bash"><span class="token operator">></span> wp plugin verify-checksums <span class="token parameter variable">--all</span></p>
<p>+-------------------------------------+----------------------------------------+-------------------------+
<span class="token operator">|</span> plugin_name                         <span class="token operator">|</span> <span class="token function">file</span>                                   <span class="token operator">|</span> message                 <span class="token operator">|</span>
+-------------------------------------+----------------------------------------+-------------------------+
<span class="token operator">|</span> essential-addons-for-elementor-lite <span class="token operator">|</span> includes/Traits/Login_Registration.php <span class="token operator">|</span> Checksum does not match <span class="token operator">|</span>
+-------------------------------------+----------------------------------------+-------------------------+</code></pre>
<p>Okay, let's view the file and see if anything looks malicious.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">cat</span> wp-content/plugins/essential-addons-for-elementor-lite/includes/Traits/Login_Registration.php</code></pre>
<p>Turns out this is a huge file, 1448 lines! On a quick look, I don't see anything that looks bad. I could grep for some typical PHP methods used in malware.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">cat</span> wp-content/plugins/essential-addons-for-elementor-lite/includes/Traits/Login_Registration.php <span class="token operator">|</span> <span class="token function">grep</span> <span class="token builtin class-name">eval</span></p>
<p><span class="token function">cat</span> wp-content/plugins/essential-addons-for-elementor-lite/includes/Traits/Login_Registration.php <span class="token operator">|</span> <span class="token function">grep</span> base64</code></pre>
<p>Nothing. That's good! But, is different then?</p>
<p>We need the file from source. We have the slug from above, but need the current version.</p>
<pre class="language-bash"><code class="language-bash">wp plugin status essential-addons-for-elementor-lite</p>
<p>Plugin essential-addons-for-elementor-lite details:
Name: Essential Addons <span class="token keyword">for</span> Elementor
Status: Active
Version: <span class="token number">5.7</span>.2 <span class="token punctuation">(</span>Update available<span class="token punctuation">)</span>
Author: WPDeveloper
Description: The Essential plugin you <span class="token function">install</span> after Elementor<span class="token operator">!</span> Packed with <span class="token number">40</span>+ stunning <span class="token function">free</span> elements including Advanced Data Table, Event Calendar, Filterable Gallery, WooCommerce, and many more.</code></pre>
<p>Now we can find the source file for this plugin and version.</p>
<pre><code>https://plugins.svn.wordpress.org/essential-addons-for-elementor-lite/tags/5.7.2/includes/Traits/Login_Registration.php
</code></pre>
<p>Now we can diff the file with our local file. This is how to do it without saving the remote file to disk.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">diff</span> <span class="token parameter variable">-u</span> wp-content/plugins/essential-addons-for-elementor-lite/includes/Traits/Login_Registration.php <span class="token operator"><</span><span class="token punctuation">(</span><span class="token function">curl</span> https://plugins.svn.wordpress.org/essential-addons-for-elementor-lite/tags/5.7.2/includes/Traits/Login_Registration.php<span class="token punctuation">)</span></p>
<p>--- wp-content/plugins/essential-addons-for-elementor-lite/includes/Traits/Login_Registration.php       <span class="token number">2023</span>-05-17 <span class="token number">12</span>:15:00.642412832 +0000
+++ /dev/fd/63  <span class="token number">2023</span>-05-24 <span class="token number">18</span>:13:16.100527581 +0000
@@ -49,7 +49,7 @@
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> isset<span class="token punctuation">(</span> <span class="token variable">$_POST</span><span class="token punctuation">[</span><span class="token string">'eael-lostpassword-submit'</span><span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token variable">$this</span>-<span class="token operator">></span>send_password_reset<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> isset<span class="token punctuation">(</span> <span class="token variable">$_POST</span><span class="token punctuation">[</span><span class="token string">'eael-resetpassword-submit'</span><span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span></p>
<ul>
<li>
<pre><code>                  if<span class="token punctuation">(</span>isset<span class="token punctuation">(</span><span class="token variable">$_COOKIE</span><span class="token punctuation">[</span><span class="token string">'91c73d6f'</span><span class="token punctuation">]</span><span class="token punctuation">))</span><span class="token punctuation">{</span><span class="token variable">$this</span>-<span class="token operator">></span>reset_password<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span>
</code></pre>
</li>
</ul>
<ul>
<li>
<pre><code>                  <span class="token variable">$this</span>-<span class="token operator">></span>reset_password<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
          <span class="token punctuation">}</span>
          do_action<span class="token punctuation">(</span> <span class="token string">'eael/login-register/after-processing-login-register'</span>, <span class="token variable">$_POST</span> <span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
</code></pre>
</li>
</ul>
<p><p>Hard to know how or why that was changed, but we're going to replace our file with the file from SVN.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> https://plugins.svn.wordpress.org/essential-addons-for-elementor-lite/tags/5.7.2/includes/Traits/Login_Registration.php <span class="token operator">></span> wp-content/plugins/essential-addons-for-elementor-lite/includes/Traits/Login_Registration.php</code></pre>
<p>Then run <code>verify-checksums</code> again and make sure everything matches!</p>
<p>That's a lot of work for one file!</p>
<h2>What's next for CommandWP?</h2>
<p>Currently, we verify checksum each night, and log the results. If any are high priority, we send an alert email. This is a good start, but doesn't allow any action to be taken.</p>
<p>Some files shouldn't exist. Depending on the file, this may or may not be an issue. Most of the time, it was a leftover file from migrating the site. Generally, those file should be removed and that's easy enough.</p>
<p>But...</p>
<p>What about the other issues?</p>
<p>Files that are missing file are easy to fix, in theory. But, using WP-CLI, requires deleting and re-downloading the plugin. Seems nicer to be able to just download the missing file. What about abandoned plugins?</p>
<p>Secondly, files that are different... What's different? Is it bad? Why is it different?</p>
<p>This gets a little trickier. You can easily view the local file, but getting the remote file to do a diff is a pain. It's available, but you don't have the URL available, and then you need to make sure to delete the downloaded file.</p>
<p>What if the file is simply a text file or CSS file? Do you care? These issues should probably be a warning and not an error.</p>
<p>So we need a way to compare, download, remove, and ignore these files. That's what I'm going to implement next in <a href="https://commandwp.com">CommandWP</a>.</p></p>

    ]]></content>
  </entry>
  
  <entry>
    <id>https://candland.net/posts/2023/05-04-bridgetown-with-dynamic-routes-on-dokku/</id>
    <title></title>
    <link rel="alternate" href="https://candland.net/posts/2023/05-04-bridgetown-with-dynamic-routes-on-dokku/"/>
    <updated>2023-05-04T14:00:00.000Z</updated><category term="post"/>
    <category term="post"/>
    <category term="proofreader"/>
    <category term="bridgetown"/>
    <category term="ruby"/>
    <category term="dokku"/>
    <category term="jamstack"/>
    <category term="turbo"/>
    <category term="hotwire"/>
    <summary type="html"><![CDATA[
      <p><a href="https://www.bridgetownrb.com/">Bridgetown</a> is a Ruby based static site generator. Being Ruby is awesome already, but with the new <a href="https://www.bridgetownrb.com/docs/routes">dynamic routes</a> feature and <a href="https://www.bridgetownrb.com/">support for Turbo</a> it could be a really powerful site builder. Static for most things, and dynamic content provided by <a href="https://turbo.hotwired.dev/">Turbo</a>!</p>

    ]]></summary>
    <content type="html"><![CDATA[
      <p><p><a href="https://www.bridgetownrb.com">Bridgetown</a> is a Ruby based static site generator. Being Ruby is awesome already, but with the new <a href="https://www.bridgetownrb.com/docs/routes">dynamic routes</a> feature and <a href="https://www.bridgetownrb.com">support for Turbo</a> it could be a really powerful site builder. Static for most things, and dynamic content provided by <a href="https://turbo.hotwired.dev/">Turbo</a>!</p>
<p>To test things out, I converted an older Jeklly based site, <a href="https://proofreader.io">Proofreader.io</a>.</p>
<p>I'm assuming you have <a href="https://dokku.com/docs/">Dokku</a> already setup and a Bridgetown site ready to deploy. From the projects root directory, these are the things we need.</p>
<p>Add a post build script for Dokku to build static files and assets.</p>
<p><em>package.json</em></p>
<pre class="language-javascript"><code class="language-javascript"><span class="token punctuation">{</span>
<span class="token operator">...</span>
<span class="token string-property property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token operator">...</span>
<span class="token string-property property">"heroku-postbuild"</span><span class="token operator">:</span> <span class="token string">"bin/bridgetown deploy"</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token operator">...</span>
<span class="token punctuation">}</span></code></pre>
<p>Next, create a proc file for Dokku to run. We need to change the port to 5000, the default for Dokku.</p>
<p><em>Procfile</em></p>
<pre><code>web: bin/bridgetown start -B tcp://0.0.0.0:5000
</code></pre>
<p>Then, add the build packs we need, Ruby & JavaScript.</p>
<p><em>.buildpacks</em></p>
<pre><code>https://github.com/heroku/heroku-buildpack-ruby
https://github.com/heroku/heroku-buildpack-nodejs
</code></pre>
<p>Add the Linux platform for bundler.</p>
<pre class="language-bash"><code class="language-bash">bundle lock --add-platform x86_64-linux</code></pre>
<h2>Deploy to Dokku</h2>
<p>I set my DOKKU_HOST with <a href="https://direnv.net/">Direnv</a> on a project by project basis.</p>
<p><em>.envrc</em></p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">export</span> <span class="token assign-left variable">DOKKU_HOST</span><span class="token operator">=</span>dokku.host.com</code></pre>
<p>Don't forget to add <code>.envrc</code> to <code>.gitignore</code>!</p>
<p>Create a Dokku app and add some required environment vars, add your domains, and setup TLS.</p>
<pre class="language-bash"><code class="language-bash">dokku apps:create proofreader
dokku config:set
dokku config:set --no-restart <span class="token assign-left variable">BRIDGETOWN_ENV</span><span class="token operator">=</span>production <span class="token assign-left variable">DOKKU_LETSENCRYPT_EMAIL</span><span class="token operator">=</span><span class="token operator"><</span>your@email<span class="token operator">></span>
dokku domains:add proofreader.io
dokku domains:add www.proofreader.io
dokku letsencrypt
<span class="token function">git</span> push dokku main</code></pre>
<p>Assuming all goes well, we'll have a Bridgetown site up and running, supporting dynamic routes!</p>
<h3>References</h3>
<p>https://dokku.com/docs/deployment/application-deployment/
https://www.bridgetownrb.com/docs/deployment
https://edge.bridgetownrb.com/docs/routes</p></p>

    ]]></content>
  </entry>
  
</feed>
