<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Tobbe Lundberg's place on teh Intarwebs]]></title><description><![CDATA[Tobbe Lundberg's place on teh Intarwebs]]></description><link>https://tlundberg.com</link><generator>RSS for Node</generator><lastBuildDate>Tue, 21 Apr 2026 22:51:29 GMT</lastBuildDate><atom:link href="https://tlundberg.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Hosting a Verdaccio NPM Registry on Hetzner Cloud Part 4: Verdaccio]]></title><description><![CDATA[With nginx configured (see Part 3) we're now ready for what we all came here to do – to install and set up Verdaccio!
NodeJS and fnm
Verdaccio is built using NodeJS, so we need to install node. I know I'll want to run other node based apps as well, a...]]></description><link>https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-4-verdaccio</link><guid isPermaLink="true">https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-4-verdaccio</guid><category><![CDATA[verdaccio]]></category><category><![CDATA[systemd]]></category><category><![CDATA[nginx]]></category><category><![CDATA[Nginx configuration guide]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Sat, 27 Apr 2024 07:08:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/x1CTeuCNhSI/upload/484d2c0e289682b4d0c9992c621d8db6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With nginx configured (<a target="_blank" href="https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-3-nginx">see Part 3</a>) we're now ready for what we all came here to do – to install and set up <a target="_blank" href="https://verdaccio.org">Verdaccio</a>!</p>
<h2 id="heading-nodejs-and-fnm">NodeJS and fnm</h2>
<p><a target="_blank" href="https://verdaccio.org">Verdaccio</a> is built using NodeJS, so we need to install node. I know I'll want to run other node based apps as well, and they might require a different version of node. So I'll install a node version manager first, and then use that to install node.</p>
<p>I'll be using <a target="_blank" href="https://github.com/Schniz/fnm">Fast Node Manager</a> <code>fnm</code>, which requires <code>unzip</code> to be available for its installation script to work</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo apt install unzip
</code></pre>
<p>It's a pretty common practice on Linux to have specific users for specific apps/services. We'll be doing the same thing here, so let's create a separate user for Verdaccio.</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo adduser --system --gecos "" --group verdaccio
</code></pre>
<p>This isn't a "normal" user that should be able to log in to the server, so we're creating a system user here. And we're skipping all the prompts for Full Name, Room Number etc by specifying <code>--gecos ""</code>. Finally we're also creating a separate group for this user that we also name "verdaccio".</p>
<p>Let's switch over to the new user</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo su -s /bin/bash verdaccio
</code></pre>
<p>Since verdaccio is a system user with no login switching is a little different in that we have to specify that we want to run the bash shell. Let's follow up by running <code>cd</code> to get to verdaccio's home directory.</p>
<p>Now we can install <code>fnm</code> by running its install script</p>
<pre><code class="lang-plaintext">verdaccio@verdaccio:~$ curl -fsSL https://fnm.vercel.app/install | bash
</code></pre>
<p>Notice that we're <em>not</em> using <code>sudo</code> here because we want to install this as the verdaccio user into directories that verdaccio (the user) has full access to.</p>
<p>Follow up by sourcing <code>.bashrc</code> to load <code>fnm</code> into our current shell (another option would be to log out and then log back in again)</p>
<pre><code class="lang-plaintext">verdaccio@verdaccio:~$ source /home/verdaccio/.bashrc
</code></pre>
<p>With <code>fnm</code> loaded we can now install node. I like using the latest LTS version, which is node 20 right now.</p>
<pre><code class="lang-plaintext">verdaccio@verdaccio:~$ fnm install 20
</code></pre>
<h2 id="heading-verdaccio">Verdaccio</h2>
<p>Finally ready to install Verdaccio!</p>
<pre><code class="lang-plaintext">verdaccio@verdaccio:~$ npm install -g verdaccio
</code></pre>
<p>We install it globally, which just means that it's "global" to the verdaccio user, i.e. not tied to any particular nodejs project.</p>
<p>Make sure it's installed, and also print its installation directory</p>
<pre><code class="lang-plaintext">verdaccio@verdaccio:~$ npm ls -g --depth 0
/home/verdaccio/.local/share/fnm/node-versions/v20.12.2/installation/lib
├── corepack@0.25.2
├── npm@10.5.0
└── verdaccio@5.30.3
</code></pre>
<p>That's looking great! Let's run it 🙂</p>
<pre><code class="lang-plaintext">verdaccio@verdaccio:~$ verdaccio
 info --- config file  - /home/verdaccio/verdaccio/config.yaml
 info --- the "crypt" algorithm is deprecated consider switch to "bcrypt" in the configuration file. Read the documentation for additional details
 info --- using htpasswd file: /home/verdaccio/verdaccio/htpasswd
 info --- plugin successfully loaded: verdaccio-htpasswd
 info --- plugin successfully loaded: verdaccio-audit
 warn --- http address - http://localhost:4873/ - verdaccio/5.30.3
</code></pre>
<p>And now stop it again by pressing Ctrl+C. When we just now ran Verdaccio for the first time it created the directory <code>/home/verdaccio/verdaccio</code>. It contains Verdaccio's config file and a directory it'll use for storing packages it downloads or someone publishes to it. For now we want to focus on the config file. There are a few settings we want to tweak.</p>
<pre><code class="lang-plaintext">verdaccio@verdaccio:~$ vim verdaccio/config.yaml
</code></pre>
<p>As you might have seen when you ran Verdaccio it said something about switching to <code>bcrypt</code>. Do that by finding the commented out <code># algorithm: bcrypt</code> line and removing the comment (<code>#</code>) from the start of that line. Also uncomment the <code>rounds</code> setting a couple of lines down. This is what you want that section of the config file to look like</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># https://verdaccio.org/docs/configuration#authentication</span>
<span class="hljs-attr">auth:</span>
  <span class="hljs-attr">htpasswd:</span>
    <span class="hljs-attr">file:</span> <span class="hljs-string">./htpasswd</span>
    <span class="hljs-comment"># Maximum amount of users allowed to register, defaults to "+inf".</span>
    <span class="hljs-comment"># You can set this to -1 to disable registration.</span>
    <span class="hljs-comment"># max_users: 1000</span>
    <span class="hljs-comment"># Hash algorithm, possible options are: "bcrypt", "md5", "sha1", "crypt".</span>
    <span class="hljs-attr">algorithm:</span> <span class="hljs-string">bcrypt</span> <span class="hljs-comment"># by default is crypt, but is recommended use bcrypt for new installations</span>
    <span class="hljs-comment"># Rounds number for "bcrypt", will be ignored for other algorithms.</span>
    <span class="hljs-attr">rounds:</span> <span class="hljs-number">10</span>
</code></pre>
<p>Next up is logging. By default it just logs to stdout, but for our purposes it's better if it logs to a file</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># https://verdaccio.org/docs/logger</span>
<span class="hljs-comment"># log settings</span>
<span class="hljs-comment"># log: { type: stdout, format: pretty, level: http }</span>
<span class="hljs-attr">log:</span> { <span class="hljs-attr">type:</span> <span class="hljs-string">file</span>, <span class="hljs-attr">path:</span> <span class="hljs-string">verdaccio/verdaccio.log</span>, <span class="hljs-attr">level:</span> <span class="hljs-string">info</span> }
</code></pre>
<p>Because we're running Verdaccio behind a reverse proxy we should configure <code>trustProxy</code>. If we don't configure it you won't see proper IP addresses in the Verdaccio logs, you'll only see the IP address the reverse proxy is forwarding all requests to (that was <code>::1</code> in my case).</p>
<pre><code class="lang-yaml"><span class="hljs-attr">server:</span>
  <span class="hljs-attr">keepAliveTimeout:</span> <span class="hljs-number">60</span>
  <span class="hljs-comment"># Allow `req.ip` to resolve properly when Verdaccio is behind a proxy or load-balancer</span>
  <span class="hljs-comment"># See: https://expressjs.com/en/guide/behind-proxies.html</span>
  <span class="hljs-attr">trustProxy:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>We're going to publish a test package later. To avoid trying to also publish to the upstream npmjs.org registry we're disabling proxying for packages in our own scope (I chose @tobbe.dev as my scope name, you're of course free to choose whatever you like)</p>
<pre><code class="lang-yaml"><span class="hljs-attr">packages:</span>
  <span class="hljs-string">'@tobbe.dev/*'</span><span class="hljs-string">:</span>
    <span class="hljs-comment"># our own scoped packages</span>
    <span class="hljs-attr">access:</span> <span class="hljs-string">$all</span>
    <span class="hljs-attr">publish:</span> <span class="hljs-string">$authenticated</span>
    <span class="hljs-attr">unpublish:</span> <span class="hljs-string">$authenticated</span>
</code></pre>
<p>(Without this config all scoped packages would use the <code>'@*/*'</code> rules, which includes <code>proxy: npmjs</code> which would also proxy any publish requests to the main npmjs.org registry.)</p>
<h2 id="heading-nginx">nginx</h2>
<p>Now that Verdaccio is installed it's time to go back to configuring nginx again. nginx keeps all the config for the different (sub-)domains it serves in <code>/etc/nginx/sites-available/</code> so let's go there (exit out of the verdaccio user shell if you're still in there first, so you get back to pistachio, <code>exit</code>)</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ cd /etc/nginx/sites-available/
</code></pre>
<p>Make a copy of <code>default</code> and call it <code>pistachio.tobbe.dev</code> for the subdomain we'll be using for Verdaccio. And then open it up in your editor of choice (vim, of course! 😄)</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:/etc/nginx/sites-available$ sudo cp default pistachio.tobbe.dev
pistachio@verdaccio:/etc/nginx/sites-available$ sudo vim pistachio.tobbe.dev
</code></pre>
<p>Get rid of most of the comments and make sure everything is nicely formated so it's easy to read. This is what you want it to look like.</p>
<pre><code class="lang-nginx"><span class="hljs-section">server</span> {
    <span class="hljs-attribute">server_name</span> pistachio.tobbe.dev; <span class="hljs-comment"># managed by Certbot</span>

    <span class="hljs-attribute">listen</span> [::]:<span class="hljs-number">443</span> ssl ipv6only=<span class="hljs-literal">on</span>; <span class="hljs-comment"># managed by Certbot</span>
    <span class="hljs-attribute">listen</span> <span class="hljs-number">443</span> ssl; <span class="hljs-comment"># managed by Certbot</span>

    <span class="hljs-attribute">location</span> / {
        <span class="hljs-attribute">proxy_pass</span> http://localhost:4873/;
        <span class="hljs-attribute">proxy_set_header</span> X-Real-IP <span class="hljs-variable">$remote_addr</span>;
        <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
        <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
        <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
        <span class="hljs-attribute">proxy_set_header</span> X-NginX-Proxy <span class="hljs-literal">true</span>;
    }

    <span class="hljs-attribute">ssl_certificate</span> /etc/letsencrypt/live/pistachio.tobbe.dev/fullchain.pem; <span class="hljs-comment"># managed by Certbot</span>
    <span class="hljs-attribute">ssl_certificate_key</span> /etc/letsencrypt/live/pistachio.tobbe.dev/privkey.pem; <span class="hljs-comment"># managed by Certbot</span>
    <span class="hljs-attribute">include</span> /etc/letsencrypt/options-ssl-nginx.conf; <span class="hljs-comment"># managed by Certbot</span>
    <span class="hljs-attribute">ssl_dhparam</span> /etc/letsencrypt/ssl-dhparams.pem; <span class="hljs-comment"># managed by Certbot</span>
}

<span class="hljs-section">server</span> {
    <span class="hljs-attribute">server_name</span> pistachio.tobbe.dev;

    <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;
    <span class="hljs-attribute">listen</span> [::]:<span class="hljs-number">80</span>;

    <span class="hljs-attribute">if</span> (<span class="hljs-variable">$host</span> = pistachio.tobbe.dev) {
        <span class="hljs-attribute">return</span> <span class="hljs-number">301</span> https://<span class="hljs-variable">$host</span><span class="hljs-variable">$request_uri</span>;
    } <span class="hljs-comment"># managed by Certbot</span>

    <span class="hljs-attribute">return</span> <span class="hljs-number">404</span>; <span class="hljs-comment"># managed by Certbot</span>
}
</code></pre>
<p>To activate this configuration we put a symlink to it in <code>/etc/nginx/sites-enabled</code></p>
<pre><code class="lang-plaintext">pistachio@verdaccio:/etc/nginx/sites-available$ sudo ln -s /etc/nginx/sites-available/pistachio.tobbe.dev ../sites-enabled/pistachio.tobbe.dev
</code></pre>
<p>Now that we've moved the verdaccio server config to its own file, we should clean up the <code>default</code> config</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:/etc/nginx/sites-available$ sudo vim default
</code></pre>
<pre><code class="lang-nginx"><span class="hljs-section">server</span> {
    <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span> default_server;
    <span class="hljs-attribute">listen</span> [::]:<span class="hljs-number">80</span> default_server;
    <span class="hljs-attribute">server_name</span> _;
    <span class="hljs-attribute">return</span> <span class="hljs-number">410</span>;
}
</code></pre>
<p>That config will make nginx reply with HTTP status 410 (GONE) for all sub domains that are not configured</p>
<p>Verify that your config is sound</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
</code></pre>
<p>Reload the nginx unit</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo systemctl reload nginx
</code></pre>
<p>Time to log back into the verdaccio user account</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo su -s /bin/bash verdaccio
verdaccio@verdaccio:/home/pistachio$ cd
</code></pre>
<p>Run Verdaccio temporarily in the foreground, just to make sure everything is working</p>
<pre><code class="lang-plaintext">verdaccio@verdaccio:~$ verdaccio
</code></pre>
<p>Open another terminal, SSH to your vps and look at the verdaccio logs to see that it started, and to be able to troubleshoot any problems</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo tail -f /home/verdaccio/verdaccio/verdaccio.log
[sudo] password for pistachio:
 info --- config file  - /home/verdaccio/verdaccio/config.yaml
 info --- using htpasswd file: /home/verdaccio/verdaccio/htpasswd
 info --- plugin successfully loaded: verdaccio-htpasswd
 info --- plugin successfully loaded: verdaccio-audit
 warn --- http address - http://127.0.0.1:4873/ - verdaccio/5.30.3
</code></pre>
<p>Now try to access Verdaccio in your browser by going to your configured (sub-)domain. For me that's <a target="_blank" href="https://pistachio.tobbe.dev">https://pistachio.tobbe.dev</a></p>
<p>You should see something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713942906991/e9712a47-ab33-4793-bd43-429917316a79.png" alt class="image--center mx-auto" /></p>
<p>Go ahead and follow the instructions:<br />On your local computer, in your terminal, run the following command (but substituting your own URL, of course)</p>
<pre><code class="lang-plaintext">❯ npm adduser --registry https://pistachio.tobbe.dev
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713943164316/b6e80528-8953-4e12-a0d4-a575f55653ff.png" alt /></p>
<p>Going back to the terminal where you're tailing your verdaccio logs you should now see</p>
<pre><code class="lang-plaintext"> info &lt;-- 217.201.48.96 requested 'POST /-/v1/login'
 info &lt;-- 217.201.48.96 requested 'PUT /-/user/org.couchdb.user:tobbe'
 info --- the user tobbe has been added
</code></pre>
<p>Now let's try publishing a package!</p>
<p>On your local computer</p>
<pre><code class="lang-plaintext">❯ mkdir -p ~/tmp/v-test
❯ cd ~/tmp/v-test
❯ npm init -y
</code></pre>
<p>Add a <code>bin</code> to <code>package.json</code> and update some values. The most important value to update is the <code>name</code>, to make sure it also includes our scope. Otherwise publishing will fail.</p>
<pre><code class="lang-plaintext">❯ vim package.json
</code></pre>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"@tobbe.dev/v-test"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Saying Hello World! from a Verdaccio package"</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"bin"</span>: {
    <span class="hljs-attr">"v-test"</span>: <span class="hljs-string">"index.js"</span>
  },
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"keywords"</span>: [],
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">"Tobbe Lundberg"</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>
}
</code></pre>
<p>Add an index.js file</p>
<pre><code class="lang-plaintext">❯ vim index.js
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-meta">#!/usr/bin/env node</span>

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Hello World!"</span>);
</code></pre>
<p>Now we're ready to publish our test package</p>
<pre><code class="lang-plaintext">❯ npm publish --registry https://pistachio.tobbe.dev/
</code></pre>
<p>Hopefully that goes well. If not – check your logs (both nginx and verdaccio) – to try to figure out what went wrong</p>
<p>Let's make sure it's in Verdaccio's storage. <code>tree</code> is a very useful Unix tool to visualize directory structures. So I recommend installing that first. This is what it looks like for me after a successful publish.</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo apt install tree
pistachio@verdaccio:~$ sudo tree /home/verdaccio/verdaccio/storage
/home/verdaccio/verdaccio/storage
└── @tobbe.dev
    └── v-test
        ├── package.json
        └── v-test-1.0.0.tgz

2 directories, 2 files
</code></pre>
<p>Now that we know it's all working there's another tool we should configure – systemd – to make sure Verdaccio starts on server start and restarts if it ever crashes.</p>
<h2 id="heading-systemd">systemd</h2>
<p>Verdaccio comes with a basic systemd unit file. We just need to copy it to a place where systemd will read it</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo cp /home/verdaccio/.local/share/fnm/node-versions/v20.12.2/installation/lib/node_modules/verdaccio/systemd/verdaccio.service /etc/systemd/system/
</code></pre>
<p>You might be asking how I figured out that long path. And that's a good question! 🙂 You can run <code>which verdaccio</code> as the verdaccio user and you'll get something like <code>/home/verdaccio/.local/state/fnm_multishells/27702_1713942027155/bin/verdaccio</code> back. But because of how <code>fnm</code> works that's just a symlink and it's not guaranteed to always be available. To figure out where it <em>really</em> is we can combine that with <code>realpath</code>. So the final command is <code>realpath `which verdaccio` </code> and that'll give you <code>/home/verdaccio/.local/share/fnm/node-versions/v20.12.2/installation/lib/node_modules/verdaccio/bin/verdaccio</code>. So that's the full path to the <code>bin</code>. Removing the last two parts of that path gives us the base directory for Verdaccio. And then we can just add on <code>systemd/verdaccio.service</code> to get the full path to the systemd unit file 😅</p>
<p>Now I like to make some modifications to the default unit</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo vim /etc/systemd/system/verdaccio.service
</code></pre>
<pre><code class="lang-ini"><span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=Verdaccio lightweight npm proxy registry
<span class="hljs-attr">After</span>=network-<span class="hljs-literal">on</span>line.target

<span class="hljs-section">[Service]</span>
<span class="hljs-attr">Type</span>=simple
<span class="hljs-attr">Restart</span>=<span class="hljs-literal">on</span>-failure
<span class="hljs-attr">RestartSec</span>=<span class="hljs-number">5</span>
<span class="hljs-attr">User</span>=verdaccio
<span class="hljs-attr">WorkingDirectory</span>=/home/verdaccio
<span class="hljs-attr">ExecStart</span>=/home/verdaccio/.local/share/fnm/node-versions/v20.<span class="hljs-number">12.2</span>/installation/bin/node /home/verdaccio/.local/share/fnm/node-versions/v20.<span class="hljs-number">12.2</span>/installation/lib/node_modules/verdaccio/bin/verdaccio

<span class="hljs-section">[Install]</span>
<span class="hljs-attr">WantedBy</span>=multi-user.target
</code></pre>
<p>I added the <code>After=...</code> option, because we need network to be online before we can start Verdaccio. I also added <code>RestartSec=5</code> to give the system a few seconds to recover if something goes wrong (like if something crashed). <code>WorkingDirectory=/home/verdaccio</code> is also new, and finally I changed <code>ExecStart</code> to run node and pass it the main entry point for Verdaccio. To figure out the node path I used <code>realpath `which node` </code> (just as I showed for <code>verdaccio</code> earlier in this guide). Just remember to run it as the verdaccio user.</p>
<p>To make sure the systemd unit is working make sure you don't have Verdaccio running anywhere. Close or at least log out of all terminals that are connected to your VPS except for one, where you're logged in as the normal user (<code>pistachio</code> in my case). Then reload systemd to read all new units and finally enable the Verdaccio one</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo systemctl daemon-reload
pistachio@verdaccio:~$ sudo systemctl enable --now verdaccio
</code></pre>
<p>After running that you should once again be able to access the Verdaccio web interface at your configured (sub-)domain. And now you should see your published package there!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713947913312/adbc3570-b5a2-486f-aff5-ec7081839ed0.png" alt /></p>
<h2 id="heading-final-test">Final test</h2>
<p>To really make sure everything is working properly we're going to do two things:</p>
<ol>
<li><p>Reboot the server</p>
</li>
<li><p>Make sure you can <code>npm install</code> the newly created package without manually touching anything on the server</p>
</li>
</ol>
<p>Let's get to it!</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo reboot
</code></pre>
<p>Wait a couple of minutes while the server reboots, and then try accessing the Verdaccio web interface. When that's up you will be able to also install the package you published.</p>
<pre><code class="lang-plaintext">❯ npm i -g --registry https://pistachio.tobbe.dev @tobbe.dev/v-test

added 1 package in 451ms
</code></pre>
<p>And now try to run it:</p>
<pre><code class="lang-plaintext">❯ v-test
Hello World!
</code></pre>
<p>It works! 🎉 Now go celebrate 🎉 🥳 🪅</p>
<p>When you're back it's time to clean up (the boring work you have to do after any party 🙁) because you probably don't want that test package to be cluttering up your system, right? At least I didn't!</p>
<pre><code class="lang-plaintext">❯ cd ~/tmp/v-test
❯ npm uninstall -g @tobbe.dev/v-test
❯ npm unpublish --force --registry https://pistachio.tobbe.dev
npm WARN using --force Recommended protections disabled.
- @tobbe.dev/v-test@1.0.0
❯ cd
❯ rm -fr ~/tmp/v-test
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Lot's of things going on in this part of the guide! We started with installing Node by using fnm. Then we installed and configured Verdaccio. After that we went back to nginx and did some major changes to its config to support our own (sub-)domain. To make Verdaccio more resilient to crashes and have it running automatically after server reboots we added a systemd unit for it. Finally we tested it all out by publishing a package, rebooting the server and then installing the package we had published.</p>
<p>You now have Verdaccio configured and running on a Hetzner Cloud server. Congratulations! What you want to do from here on is up to you 🙂 In the next (and final, for now at least) part of this guide I'll show you some things I needed to do for what I wanted to use this for.</p>
]]></content:encoded></item><item><title><![CDATA[Hosting a Verdaccio NPM Registry on Hetzner Cloud Part 3: nginx]]></title><description><![CDATA[To follow along here you're going to need a (sub-)domain you want to use to access your Verdaccio NPM Registry. I already have a tobbe.dev domain, so I used a subdomain for this, pistachio.tobbe.dev, and pointed it to my new server IP.
Login and upda...]]></description><link>https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-3-nginx</link><guid isPermaLink="true">https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-3-nginx</guid><category><![CDATA[Hetzner]]></category><category><![CDATA[nginx]]></category><category><![CDATA[Nginx configuration guide]]></category><category><![CDATA[npm]]></category><category><![CDATA[SSL]]></category><category><![CDATA[certbot]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Fri, 26 Apr 2024 14:04:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1714140067175/66268fe0-b48b-4f4f-ad1e-9445c85a370a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>To follow along here you're going to need a (sub-)domain you want to use to access your <a target="_blank" href="https://verdaccio.org">Verdaccio</a> NPM Registry. I already have a tobbe.dev domain, so I used a subdomain for this, <a target="_blank" href="https://pistachio.tobbe.dev">pistachio.tobbe.dev</a>, and pointed it to my new server IP.</p>
<h2 id="heading-login-and-update">Login and update</h2>
<p>If you've followed along with Part 1 and Part 2 your server should be secure. Let's make sure it's also up to date!</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo apt-get update &amp;&amp; sudo apt-get upgrade
</code></pre>
<p>Answer Yes if/when it asks if you want to continue</p>
<p>If it asks about your sshd_config file, you want to keep your locally modified version. Press Tab to highlight &lt;Ok&gt; and then press Enter to continue</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713876132307/33e5d8c0-c962-47a5-9bf0-f8a2adae26f4.png" alt /></p>
<p>Following that it might ask about daemons using outdated libraries. Again, press Tab to highlight &lt;Ok&gt; and then press Enter to continue.</p>
<h2 id="heading-nginx-installation">nginx installation</h2>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo apt install nginx
</code></pre>
<p>Again, it might ask you about daemons using outdated libraries. Select all of them and then continue</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713876354167/db66ae85-149a-41a8-86dd-4adaaa7d9c3c.png" alt /></p>
<p>Remember that firewall we configured before? It's blocking access to nginx, so we need to update its config.</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo ufw app list
Available applications:
  Nginx Full
  Nginx HTTP
  Nginx HTTPS
  OpenSSH
</code></pre>
<p>You might be tempted to go with HTTPS only, but we need HTTP to be able to verify our SSL setup later. Plus we will tell nginx to redirect all HTTP traffic to HTTPS, so we're going to allow "Nginx Full".</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo ufw allow 'Nginx Full'
Rule added
Rule added (v6)
</code></pre>
<p>Check the firewall status</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
Nginx Full                 ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
Nginx Full (v6)            ALLOW       Anywhere (v6)
</code></pre>
<p>You should now be able to access your web server in your browser by going to http://&lt;IP-address&gt;</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713876568300/806c8b60-4531-4e67-bbb4-bb7f675cb6d4.png" alt /></p>
<p>Depending on your domain/DNS you might also be able to access it using <a target="_blank" href="http://your.domain.tld">http://your.domain.tld</a> but since I'm using a .dev top level domain (TLD) I can't, because they require SSL</p>
<h2 id="heading-ssl">SSL</h2>
<p>Certbot will manage our SSL certificates.</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo apt install certbot python3-certbot-nginx
</code></pre>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo certbot --nginx -d pistachio.tobbe.dev
</code></pre>
<p>And this is the output you want</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713877093188/bd433e47-9b92-4d25-83a9-5b9b773d9858.png" alt /></p>
<p>Let's take a look at the default nginx config</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo less /etc/nginx/sites-available/default
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713908429323/d6478867-cd8d-4232-b4fb-bf96f5fb0964.png" alt /></p>
<p>You'll see a bunch of lines with <code># managed by Certbot</code> at the end. Those were added thanks to the <code>--nginx</code> command line option we used when we ran the <code>certbot</code> command. The file is a bit messy with commented config, indentation a bit all over the place etc. But we'll create our own config soon, and make sure to clean it up so it's easier to follow along with what's going on.</p>
<p>For now we should just make sure it all works by going to <a target="_blank" href="https://verdaccio.tobbe.dev">https://pistachio.tobbe.dev</a> in our web browser.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This was a pretty short part. We installed nginx and configured it to use SSL. We also verified that we could now access our web server using https. We'll come back to the nginx config, but first we need to install <a target="_blank" href="https://verdaccio.org">Verdaccio</a>, and that's exactly what we'll do in <a target="_blank" href="https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-4-verdaccio">the next part of this guide</a>.</p>
<hr />
<p>Cover Photo by <a target="_blank" href="https://unsplash.com/@tvick?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Taylor Vick</a> on <a target="_blank" href="https://unsplash.com/photos/cable-network-M5tzZtFCOfs?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Hosting a Verdaccio NPM Registry on Hetzner Cloud Part 2: Adding a User and Securing the Server]]></title><description><![CDATA[In Part 1 we bought a server and added an SSH key to the root user. If you skipped that part because you already had a server, please make sure you also have your public SSH key added to the root user's authorized_keys file as this part of the guide ...]]></description><link>https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-2-adding-a-user-and-securing-the-server</link><guid isPermaLink="true">https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-2-adding-a-user-and-securing-the-server</guid><category><![CDATA[verdaccio]]></category><category><![CDATA[Hetzner]]></category><category><![CDATA[ssh]]></category><category><![CDATA[ssh-keys]]></category><category><![CDATA[firewall]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Wed, 24 Apr 2024 18:02:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1713981539798/3822daf2-13a1-4c2a-8b67-44ca37f8cb1f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In <a target="_blank" href="https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-1-buying-a-vps">Part 1</a> we bought a server and added an SSH key to the root user. If you skipped <a target="_blank" href="https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-1-buying-a-vps">that part</a> because you already had a server, please make sure you also have your public SSH key added to the root user's <code>authorized_keys</code> file as this part of the guide will assume that's already set up.</p>
<h2 id="heading-adding-a-user">Adding a User</h2>
<p>As a rule of thumb you want to use the root user for as few things as possible, so let's go ahead and create a regular user account we can use instead. I'll name mine "pistachio". Make sure you give the user a strong password. You can skip all the other fields by just pressing Enter on all of them.</p>
<pre><code class="lang-plaintext">root@verdaccio:~# adduser pistachio
Adding user `pistachio' ...
Adding new group `pistachio' (1000) ...
Adding new user `pistachio' (1000) with group `pistachio' ...
Creating home directory `/home/pistachio' ...
Copying files from `/etc/skel' ...
New password:
Retype new password:
passwd: password updated successfully
Changing the user information for pistachio
Enter the new value, or press ENTER for the default
    Full Name []:
    Room Number []:
    Work Phone []:
    Home Phone []:
    Other []:
Is the information correct? [Y/n]
root@verdaccio:~#
</code></pre>
<p>Next thing we'll do is add the user to the <code>sudo</code> group</p>
<pre><code class="lang-plaintext">root@verdaccio:~# usermod -aG sudo pistachio
</code></pre>
<p>You can test sudo access by switching over to the new user, try to run a command that requires root privileges and then running again with sudo.</p>
<pre><code class="lang-plaintext">root@verdaccio:~# su - pistachio
</code></pre>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ ls -la /root
ls: cannot open directory '/root': Permission denied
</code></pre>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo ls -la /root
[sudo] password for pistachio:
</code></pre>
<p>Enter the password you selected for your user. If everything works correctly the contents of the <code>/root</code> directory should now be printed to the screen, like <code>.bashrc</code>, <code>.profile</code>, etc</p>
<p>Something like this is what you should see</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713867719878/714929c6-78bf-4025-a87a-216d0efe1742.png" alt /></p>
<p>Run <code>exit</code> to go back to your root shell. One more thing we want to set up is ssh key login for the new user. We already have the info we need on the root user, so we can just copy that over and then make sure all owners and permissions are correct</p>
<pre><code class="lang-plaintext">root@verdaccio:~# cp -r ~/.ssh /home/pistachio
root@verdaccio:~# chown -R pistachio:pistachio /home/pistachio/.ssh
</code></pre>
<p>When that's done you can run <code>exit</code> again to log out of your ssh session.</p>
<p>Now let's try to ssh in as your new user <code>ssh -i &lt;path to private key&gt; &lt;username&gt;@&lt;IP-address&gt;</code>. With my example values that becomes</p>
<pre><code class="lang-plaintext">❯ ssh -i ~/.ssh/id_ed25519-hetzner-verdaccio pistachio@78.47.20.27
</code></pre>
<p>That should log you in with your ssh key, so without requiring the pistachio user password.</p>
<h2 id="heading-firewall">Firewall</h2>
<p>Once you're in we're going to continue making our server more secure. Next step is to enable the <a target="_blank" href="https://ubuntu.com/server/docs/firewalls">Uncomplicated Firewall</a> <code>ufw</code>.</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo ufw allow OpenSSH
[sudo] password for pistachio:
Rules updated
Rules updated (v6)
</code></pre>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
</code></pre>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
</code></pre>
<p>All connections, except SSH, are now blocked. When you install new applications you want to be able to connect to you need to activate them in the firewall.</p>
<h2 id="heading-ssh-server-sshd-configuration">SSH Server (sshd) Configuration</h2>
<p>To enhance the security of your server even more, we'll deactivate password based logins, and not allow the root user to login using ssh at all</p>
<p>Open the sshd config file using your preferred editor. I use vim</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo vim /etc/ssh/sshd_config
</code></pre>
<p>Find the commented out line that says<br /><code>#PasswordAuthentication yes</code><br />and change it to<br /><code>PasswordAuthentication no</code></p>
<p>Also find<br /><code>#PermitRootLogin prohibit-password</code><br />and change that to<br /><code>PerminRootLogin no</code></p>
<p><code>:wq</code> to write the file and quit vim</p>
<p>Restart the sshd service</p>
<pre><code class="lang-plaintext">pistachio@verdaccio:~$ sudo systemctl restart ssh
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Any computer connected to the Internet is subject to attacks. Common attack vectors are poorly configured SSH servers and (default) users with weak passwords. In this part of the guide we locked down the server as much as we could by enabling the <a target="_blank" href="https://ubuntu.com/server/docs/firewalls">Uncomplicated Firewall</a> (<code>ufw</code>), making sure no one can login in remotely using only a username plus password combination and we blocked root login even over SSH.</p>
<p><a target="_blank" href="https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-3-nginx">Next up, in Part 3</a>, is to configure a reverse proxy with SSL certificates to have encrypted communication with the Verdaccio NPM registry. We'll be using the nginx reverse proxy and web server for this.</p>
<hr />
<p>Cover Photo by <a target="_blank" href="https://unsplash.com/@superadmins?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Sammyayot254</a> on <a target="_blank" href="https://unsplash.com/photos/man-in-blue-sweater-using-silver-macbook-vIQDv6tUHYk?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Hosting a Verdaccio NPM Registry on Hetzner Cloud Part 1: Buying a VPS]]></title><description><![CDATA[As the title suggest, we're going to host this on a Hetzner cloud server, a Virtual Private Server (VPS). If you already have a VPS you want to use you can skip ahead to Part 2 where we start configuring the server.
Start by going to https://www.hetz...]]></description><link>https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-1-buying-a-vps</link><guid isPermaLink="true">https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-1-buying-a-vps</guid><category><![CDATA[Hetzner]]></category><category><![CDATA[verdaccio]]></category><category><![CDATA[vps]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Wed, 24 Apr 2024 17:43:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1713981264687/eb1a42d1-97e3-483b-b460-3f37f30923ac.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As the title suggest, we're going to host this on a <a target="_blank" href="https://www.hetzner.com">Hetzner</a> cloud server, a Virtual Private Server (VPS). If you already have a VPS you want to use you can <a target="_blank" href="https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-2-adding-a-user-and-securing-the-server">skip ahead to Part 2</a> where we start configuring the server.</p>
<p>Start by going to <a target="_blank" href="https://www.hetzner.com/cloud/">https://www.hetzner.com/cloud/</a> and signing up</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713861833399/78d225a6-07fe-4b05-81da-d30d337dcd47.png" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713861966613/2dff3836-7c9e-4b42-a1bb-f18173071649.png" alt /></p>
<p>Click the red "Sign Up" button, or "Login" in the top navigation bar and then "Cloud". Both buttons should take you to the screen below where you need to click "Register Now" and then fill in your email etc. It'll also ask for a credit card number, but you won't be charged yet.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713861881777/0e0982a3-5cd9-4d13-b95e-4d587bcfa925.png" alt /></p>
<p>Once you're through the registration process you should see something like this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713862093377/3e5337af-e993-4619-a7a7-414e404630c7.png" alt /></p>
<p>Click "Create Server" and chose your desired location. Then pick Ubuntu 22.04 as your desired OS Image. For "Type" the cheapest option is absolutely good enough. So I went with a Shared Intel vCPU with 2 GB or RAM, 20 GB of disk space (SSD) and 20 TB of traffic (I wish I had that much traffic!).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713862440916/67938794-57ca-4535-996b-cd37e80a9f50.png" alt class="image--center mx-auto" /></p>
<p>To simplify things, and make sure my registry can be used by as many as possible, I also chose to pay for an IPv4 address. And I suggest you do the same.</p>
<p>Now it's time to add an SSH key. You don't have to, but as they say – it is recommended for security reasons. If you already have one you can go ahead and add it, otherwise you can generate a new one by pasting the text below into your terminal.</p>
<pre><code class="lang-bash">❯ ssh-keygen -t ed25519 -C <span class="hljs-string">"hetzner-verdaccio"</span>
</code></pre>
<p>The output will look something like this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713863609869/0d7315ac-66c7-469e-ae9d-fdfa562404b8.png" alt class="image--center mx-auto" /></p>
<p>As you can see I also gave my key a custom name that matches the comment (<code>-C</code>) I passed to <code>ssh-keygen</code>, just to make it easier to identify. And you probably should use a passphrase, but for convenience I didn't this time 😬</p>
<p>Copy your public key. If you're on a MacOS computer you can do it like this:</p>
<pre><code class="lang-bash">❯ cat ~/.ssh/id_ed25519-hetzner-verdaccio.pub | pbcopy
</code></pre>
<p>Click "Add SSH key" on the server creation page and then paste your key into the form.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713864565926/87f9fe5b-4b33-4958-9c13-4527ca2b1ffe.png" alt class="image--center mx-auto" /></p>
<p>It'll parse the key for you and populate the Name field too. Choose "Set as default key" if you want and then click "Add SSH key". You should now see this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713864834491/0de0c822-697b-4a21-baee-024a3e59f1d5.png" alt class="image--center mx-auto" /></p>
<p>That's almost all we have to do. Now just scroll to the bottom and give your server a more descriptive name, like "verdaccio"</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713864940012/51e90e68-bae2-4b45-8da0-f916fd9329a7.png" alt /></p>
<p>Finally click "Create &amp; Buy now"</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713865030791/e4ed5755-a545-4055-b5fa-5f37ca34818f.png" alt /></p>
<p>After a few seconds you should see something like this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713865099264/c8cfaf81-34a8-4d61-8a0f-4cd6437d0ac3.png" alt class="image--center mx-auto" /></p>
<p>Copy your server's IP address by clicking on it. You'll need it to connect to your server from the terminal.</p>
<p>So open up a new terminal and enter <code>ssh -i ~/.ssh/id_ed25519-hetzner-verdaccio root@&lt;IP-address&gt;</code> where you replace <code>&lt;IP-address&gt;</code> with the address you copied from the Hetzner web interface. It'll ask you to confirm the fingerprint. Type <code>yes</code> and press Enter.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713866391916/4d0b8fd1-2155-45bd-8a9a-8a63a661ddc9.png" alt /></p>
<p>You're now logged into your very own Hetzner Cloud VPS!</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've now bought a cloud computer on <a target="_blank" href="https://www.hetzner.com">Hetzner</a>. It's a very cheap Virtual Private Server (VPS) more than capable of hosting your own NPM registry and a few other things if you want. During setup we added an SSH key, so that you can login to the server and get root access without relying on potentially insecure passwords.</p>
<p><a target="_blank" href="https://tlundberg.com/hosting-a-verdaccio-npm-registry-on-hetzner-cloud-part-2-adding-a-user-and-securing-the-server">Next up in Part 2</a> we're going to create a regular user (not root) and make the server more secure.</p>
<hr />
<p>Cover Photo by <a target="_blank" href="https://unsplash.com/@matthieu_cabri?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Matthieu Beaumont</a> on <a target="_blank" href="https://unsplash.com/photos/a-very-large-array-of-electronic-equipment-in-a-room-iYnpYeyu57k?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[WebSockets in RedwoodJS]]></title><description><![CDATA[Realtime/WebSocket support in Redwood has been requested since forever*. For most people, using something like https://pusher.com/channels or https://supabase.com/docs/guides/realtime is the best choice. But I wanted to see if I could roll my own. (*...]]></description><link>https://tlundberg.com/websockets-in-redwoodjs</link><guid isPermaLink="true">https://tlundberg.com/websockets-in-redwoodjs</guid><category><![CDATA[RedwoodJS]]></category><category><![CDATA[websockets]]></category><category><![CDATA[React context]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Sat, 25 Feb 2023 14:54:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/P787-xixGio/upload/62a078279c370b395c74f8d10bfe97a0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677193124191/bcaa5dff-e565-4bf1-b0e7-c8c0af2668dc.png" alt class="image--center mx-auto" /></p>
<p>Realtime/WebSocket support in Redwood <a target="_blank" href="https://community.redwoodjs.com/t/how-would-you-implement-realtime-websockets-in-redwoodjs/644">has been requested</a> since forever*. For most people, using something like <a target="_blank" href="https://pusher.com/channels">https://pusher.com/channels</a> or <a target="_blank" href="https://supabase.com/docs/guides/realtime">https://supabase.com/docs/guides/realtime</a> is the best choice. But I wanted to see if I could roll my own. (*beginning of 2020 😄)</p>
<p>I decided to build a simple multiplayer card game that would use WebSockets to sync the game state for all players (what cards they have on hand, what cards they've played etc). That's what you can see in the screenshot at the top of the page.</p>
<p>EDIT 2023-02-27: A lot of the setup shown in this blog post can now be done for you by just running <code>yarn dlx rw-setup-ws</code>. Read more here <a target="_blank" href="https://community.redwoodjs.com/t/experiment-websockets-with-redwoodjs/4648">https://community.redwoodjs.com/t/experiment-websockets-with-redwoodjs/4648</a></p>
<p>This blog post will show you how I set it up. But to keep it simple I'll just focus on a tiny part of the game: the score. If you look at the screenshot above you can see a score next to each player's name. I kept it super basic by just syncing all of the text inputs. You can just write whatever number you want in any of them and it'll sync to all players. We'll build something similar. An input field for a player's name and another one for the player's score. When the score is updated it'll be synced to all other players. But in this version, unlike in my actual game, you can only change your own score. (Not strictly true, but I'll let you figure that out as you play around with the code 😉)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677335376266/313eede2-666d-4435-8e94-f4ce87227a30.png" alt class="image--center mx-auto" /></p>
<p>Create a new Redwood project <code>yarn create redwood-app --ts --git rw-ws-score</code></p>
<p>Redwood uses Fastify for its API side web server. To get it to understand WebSockets we need to install a plugin: <code>yarn workspace api add @fastify/websocket</code></p>
<p>That's all we have to do as far as setup goes. Now let's write some code! This is where it really shows that Redwood wasn't designed for realtime or WebSocket support – we'll be writing all of the ws code on the api side in what is basically just meant to be a configuration file. But if you want ws support let us (the RW core team) know, and if there's enough demand we'll try to make the experience better!</p>
<p>Open up <code>api/server.config.js</code> and register the ws plugin.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> configureFastify = <span class="hljs-keyword">async</span> (fastify, options) =&gt; {
  <span class="hljs-keyword">if</span> (options.side === <span class="hljs-string">'api'</span>) {
    fastify.log.info({ <span class="hljs-attr">custom</span>: { options } }, <span class="hljs-string">'Configuring api side'</span>)

    fastify.register(<span class="hljs-built_in">require</span>(<span class="hljs-string">'@fastify/websocket'</span>))
  }

  <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>This lets Fastify know about WebSockets. Now we need to add a route that'll handle the ws traffic.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> configureFastify = <span class="hljs-keyword">async</span> (fastify, options) =&gt; {
  <span class="hljs-keyword">if</span> (options.side === <span class="hljs-string">'api'</span>) {
    fastify.log.info({ <span class="hljs-attr">custom</span>: { options } }, <span class="hljs-string">'Configuring api side'</span>)

    fastify.register(<span class="hljs-built_in">require</span>(<span class="hljs-string">'@fastify/websocket'</span>))

    fastify.register(<span class="hljs-function">(<span class="hljs-params">fastify</span>) =&gt;</span> {
      fastify.get(<span class="hljs-string">'/ws'</span>, { <span class="hljs-attr">websocket</span>: <span class="hljs-literal">true</span> }, <span class="hljs-function">(<span class="hljs-params">connection</span>) =&gt;</span> {
        connection.socket.on(<span class="hljs-string">'message'</span>, <span class="hljs-function">(<span class="hljs-params">message</span>) =&gt;</span> {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`/ws message: <span class="hljs-subst">${message}</span>`</span>)
        })

        connection.socket.on(<span class="hljs-string">'close'</span>, <span class="hljs-function">() =&gt;</span> {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Client disconnected'</span>)
        })
      })
    })
  }

  <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>Let's test our code! Run <code>yarn rw dev api</code> in a terminal to start just the api side. In another terminal you can run <code>npx -y wscat -c ws://localhost:8911/ws</code>. It should open a connection to your server and give you a prompt. Type something, like <code>hello world</code>, and you should see it printed in the terminal that's running the RW api server.</p>
<p>Do you see the messages you type echoed in the api server terminal? If you do: Congratulations! You have a WebSocket web server running! If you don't: Try adding some <code>console.log</code>s, then restart the api server and try again. If you still can't get it to work, create a post on <a target="_blank" href="https://community.redwoodjs.com">the RW forums</a> and ping me there. We'll all do our best to help you.</p>
<p>Now that you've confirmed that the api side is working, let's add some code to the web side too. I'll over-engineer this a little bit for our simple example, just to give you a better foundation to stand on when you build this out with more functionality.</p>
<p>I'll put the websocket code in a React Context so that you can access it from anywhere in your app.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// web/components/WsContext/WsContext.tsx</span>

<span class="hljs-keyword">import</span> { useCallback, useContext, useEffect, useRef, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>

<span class="hljs-keyword">interface</span> WsContextProps {
  players: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;
  setScore: <span class="hljs-function">(<span class="hljs-params">playerId: <span class="hljs-built_in">string</span>, score: <span class="hljs-built_in">string</span></span>) =&gt;</span> <span class="hljs-built_in">void</span>
}

<span class="hljs-keyword">const</span> WsContext = React.createContext&lt;WsContextProps | <span class="hljs-literal">undefined</span>&gt;(<span class="hljs-literal">undefined</span>)

<span class="hljs-keyword">interface</span> Props {
  children: React.ReactNode
}

<span class="hljs-keyword">const</span> WsContextProvider: React.FC&lt;Props&gt; = <span class="hljs-function">(<span class="hljs-params">{ children }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> [players, setPlayers] = useState&lt;Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;&gt;({})

  <span class="hljs-keyword">const</span> ws = useRef&lt;WebSocket&gt;()

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> socket = <span class="hljs-keyword">new</span> WebSocket(<span class="hljs-string">'ws://localhost:8911/ws'</span>)

    socket.onopen = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'socket open'</span>, event)
    }
    socket.onclose = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'socket close'</span>, event)
    }
    socket.onerror = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'socket error'</span>, event)
    }
    socket.onmessage = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'socket message'</span>, event.data)

      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> players = <span class="hljs-built_in">JSON</span>.parse(event.data)
        setPlayers(players)
      } <span class="hljs-keyword">catch</span> (e) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'JSON.parse error'</span>, e)
      }
    }

    ws.current = socket

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
      socket.close()
    }
  }, [])

  <span class="hljs-keyword">const</span> setScore = useCallback(<span class="hljs-function">(<span class="hljs-params">playerId: <span class="hljs-built_in">string</span>, score: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    ws.current?.send(<span class="hljs-built_in">JSON</span>.stringify({ playerId, score }))
  }, [])

  <span class="hljs-keyword">return</span> (
    &lt;WsContext.Provider value={{ players, setScore }}&gt;
      {children}
    &lt;/WsContext.Provider&gt;
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useWsContext</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> context = useContext(WsContext)

  <span class="hljs-keyword">if</span> (!context) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(
      <span class="hljs-string">'useWsContext must be used within a WsContextProvider'</span>
    )
  }

  <span class="hljs-keyword">return</span> context
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> WsContextProvider
</code></pre>
<p>Basically what's going on here is I create a React Context with a <code>useEffect</code> that will only be called after the very first render thanks to its empty dependency array. The <code>useEffect</code> function sets up a new WebSocket connection to our api server, registers a bunch of callbacks and returns a clean-up function that will close the web socket connection when the context unmounts. Notice that we use the <code>ws</code> protocol when we specify the server address. The main socket callback is <code>socket.onmessage</code> which will receive all the messages from the WebSocket server. The other callbacks are mainly there to show you what's available and to help you debug any errors you might run into. On the context you can access all players and their scores plus a method to update the score for a player. It might look weird to have the score be a <code>string</code> instead of <code>number</code>, but that's just to make it easier to handle the score input later on. It makes it easier to handle empty input fields. WebSockets can only communicate using text, that's why we use <code>JSON.stringify</code> before sending an updated score for a player. Finally, there's a custom React hook to make it more convenient to use the context.</p>
<p>To make this context available to the entire app I place it in <code>App.tsx</code></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> App = <span class="hljs-function">() =&gt;</span> (
  &lt;FatalErrorBoundary page={FatalErrorPage}&gt;
    &lt;RedwoodProvider titleTemplate=<span class="hljs-string">"%PageTitle | %AppTitle"</span>&gt;
      &lt;RedwoodApolloProvider&gt;
        &lt;WsContextProvider&gt;
          &lt;Routes /&gt;
        &lt;/WsContextProvider&gt;
      &lt;/RedwoodApolloProvider&gt;
    &lt;/RedwoodProvider&gt;
  &lt;/FatalErrorBoundary&gt;
)
</code></pre>
<p>The final piece of the web-side puzzle is to actually use the context. Let's create a page for this.</p>
<pre><code class="lang-bash">yarn rw g page home /
</code></pre>
<p>Here's the base component code</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>

<span class="hljs-keyword">import</span> { useWsContext } <span class="hljs-keyword">from</span> <span class="hljs-string">'src/components/WsContext/WsContext'</span>

<span class="hljs-keyword">const</span> HomePage = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [name, setName] = useState(<span class="hljs-string">''</span>)
  <span class="hljs-keyword">const</span> [score, setScore] = useState(<span class="hljs-string">''</span>)
  <span class="hljs-keyword">const</span> ws = useWsContext()

  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;label&gt;
        Name:{<span class="hljs-string">' '</span>}
        &lt;input
          value={name}
          onChange={<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
            setName(event.target.value)
          }}
        /&gt;
      &lt;/label&gt;
      &lt;br /&gt;
      &lt;br /&gt;
      &lt;label&gt;
        Score:{<span class="hljs-string">' '</span>}
        &lt;input
          value={score}
          onChange={<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
            <span class="hljs-keyword">const</span> score = event.target.value

            <span class="hljs-comment">// Set the score in component state to make the input</span>
            <span class="hljs-comment">// value update immediately</span>
            setScore(score)

            <span class="hljs-comment">// Send to the server to update all clients (including</span>
            <span class="hljs-comment">// this one)</span>
            ws.setScore(name, score)
          }}
        /&gt;
      &lt;/label&gt;
      &lt;hr /&gt;
      &lt;ul&gt;
        {<span class="hljs-built_in">Object</span>.entries(ws.players).map(<span class="hljs-function">(<span class="hljs-params">[playerId, playerScore]</span>) =&gt;</span> (
          &lt;li key={playerId}&gt;
            {playerId}: {playerScore}
          &lt;/li&gt;
        ))}
      &lt;/ul&gt;
    &lt;/&gt;
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> HomePage
</code></pre>
<p>Unless you're super new to React the code should be pretty straightforward. A couple of input fields with state bound to them and then the context we created earlier is used to get a list of players to map over to display their names/ids and scores. The one thing that might look a little unusual is how we both keep our own score in the local <code>score</code> state as well as send it to the web socket using <code>ws.setScore</code>. But I hope the code comments are clear enough in describing what's going on and why I do it like that. With this code in place, we can start the RW dev server again, but this time we start both sides <code>yarn rw dev</code>. Enter your name and a score and you should see something like <code>/ws message: {"playerId":"Tobbe","score":"5"}</code> in your dev server terminal output.</p>
<p>Now it's time to focus on the API side code again. It needs to keep track of all players and their scores and broadcast it to everyone. So I add two global variables to the Fastify server config file: <code>players</code> for all the players and their scores, and <code>wsConnections</code> to keep track of all connections I should broadcast the new scores on. Both those variables are used in the <code>socket.on('message'</code> handler function. The code for that function now looks like this</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">`/ws message: <span class="hljs-subst">${message}</span>`</span>)

<span class="hljs-keyword">try</span> {
  <span class="hljs-keyword">const</span> player = <span class="hljs-built_in">JSON</span>.parse(message)
  wsConnections[player.playerId] = connection
  players[player.playerId] = player.score

  <span class="hljs-built_in">Object</span>.values(wsConnections).forEach(<span class="hljs-function">(<span class="hljs-params">wsConnection</span>) =&gt;</span> {
    wsConnection.socket.send(<span class="hljs-built_in">JSON</span>.stringify(players))
  })
} <span class="hljs-keyword">catch</span> (e) {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Could not parse input'</span>, message)
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'error object:'</span>, e)
}
</code></pre>
<p>Start the dev server again if it isn't still running and go to http://localhost:8910 in two different browser windows. Enter a name and a score in both of them and notice that the score immediately is synced between both windows 🎉</p>
<p>There are a few things I've left as an exercise for the reader 😁</p>
<ul>
<li><p>If you update your name you're seen as a new player and your old name with your old score is still kept around. See if you can make that work a little better</p>
</li>
<li><p>You're allowed to enter text in the score input fields. Should probably only be allowed to enter actual numbers (except for the empty string (''), which you might still want to allow)</p>
</li>
<li><p>Maybe you don't want to include yourself in the list of players with their scores. So maybe filter out your own player id.</p>
</li>
<li><p>Make it so you can't "steal" someone else's name.</p>
</li>
</ul>
<p>This should work pretty well running on your computer using the dev server. If you want to deploy it there are a few more hoops you have to jump through. I'll write another blog post about that.</p>
<p>The full code for the example can be found here <a target="_blank" href="https://github.com/Tobbe/rw-ws-score/">https://github.com/Tobbe/rw-ws-score/</a></p>
<p>Thanks for reading and don't hesitate to reach out if you have any questions.</p>
]]></content:encoded></item><item><title><![CDATA[Using RedwoodJS as a Static Site Generator]]></title><description><![CDATA[My PR for Cell Prerendering recently got merged into the RedwoodJS codebase. It makes it possible to 1. pre-render pages that need URL path parameters, and 2. pre-render pages that uses cells. This combo is super powerful!
Let's say you have an e-com...]]></description><link>https://tlundberg.com/using-redwoodjs-as-a-static-site-generator</link><guid isPermaLink="true">https://tlundberg.com/using-redwoodjs-as-a-static-site-generator</guid><category><![CDATA[RedwoodJS]]></category><category><![CDATA[ssg]]></category><category><![CDATA[pre rendering]]></category><category><![CDATA[Gatsby]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Sun, 31 Jul 2022 15:19:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1659280922543/pm6WgNxnT.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My PR for Cell Prerendering recently got merged into the RedwoodJS codebase. It makes it possible to 1. pre-render pages that need URL path parameters, and 2. pre-render pages that uses cells. This combo is super powerful!</p>
<p>Let's say you have an e-commerce storefront with URLs like <a target="_blank" href="https://shop.redwoodjs.com/collections/shirts/products/redwoodjs-lapel-pin">/products/redwoodjs-lapel-pin</a>, <a target="_blank" href="https://shop.redwoodjs.com/collections/shirts/products/redwoodjs-skater-hat">/products/redwoodjs-skater-hat</a> and <a target="_blank" href="https://shop.redwoodjs.com/collections/shirts/products/redwoodjs-logo-shirt">products/redwoodjs-logo-shirt</a>. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1659274307989/HWMGYtnaz.png" alt="Screenshot of e-commerce storefront showing the products/redwoodjs-logo-shirt page" />
<em>The RedwoodJS Shop is not actually built with the technique I'm demonstrating here, I just used it as an example of what one <strong>could</strong> do</em></p>
<p>You have probably defined the route that uses those URLs like this: <code>&lt;Route path="/products/{sku}" page={ProductPage} name="product" prerender /&gt;</code>. The page probably passes the <code>sku</code> from the URL on to a <code>&lt;ProductCell sku={sku} /&gt;</code> to fetch details about the product from your database. So you'd need to be able to tell Redwood what sku(s) to pre-render for, and then Redwood has to know how to execute the GraphQL query in the Cell, with the given sku, connect to your database, return the data, and finally render it all on a page. If you use the latest canary version of Redwood all this is now possible!</p>
<p>As you know, Redwood uses React for its frontend. And the way pre-rendering works with React (and most other JavaScript frontend frameworks) is you generate a static html page and send that to the user's browser. This html page has a <code>&lt;script&gt;</code> tag (or several) that downloads the React runtime plus all the javascript logic that you have written for this particular page. It then replaces the static html on the page with dynamic html rendered by the React runtime. When this is done the page is finally ready for user interaction. And now, when you click on a link on the page to navigate somewhere else, this new page you're navigating to will be fully rendered by React. So no matter if you go to another page that is also pre-rendered or not, the existing html that's stored on the server will never be sent to the end user. This is great, because the page stays interactive, and only parts of the page that actually changes when you navigate (i.e. not your side menu, top navigation bar, footer etc) will update. The user gets a smoother experience, and you don't need such a beefy server as you offload a lot of the rendering to the user's client instead.</p>
<p>But what if your page is pretty much all static, and overall very basic? You don't need the interactivity that React brings. And it wouldn't be any rendering work that needed to be done on the server - it would just have to serve static html. In this case you might wonder - why use React at all? And that's a valid question. But let's ignore that for now, or I wouldn't have anything to blog about 😛 (In reality the argument is probably that you have some pages that are very basic, and/or have to be super fast, and you have other pages that are highly dynamic, and you want to use the same tech stack for all of them.) In this case it might make sense to just render pure html pages and statically serve them straight from your web-server like it was 1995 allover again.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1659272925029/-fvyVh-LP.webp" alt="Opera 2.0 web browser" />
<em>Image from https://thehistoryoftheweb.com/a-mini-browser-for-the-masses/</em></p>
<p>Redwood's current pre-render implementation is optimized for the most common use case of fast and SEO-friendly first page loads, and then letting React take over from there to get the interactivity and smooth page transitions you can get with a JS framework.</p>
<p>So of course I had to go and experiment with making it work for that other scenario with 100% static, basic, html pages 😁</p>
<p>I wanted to build out a basic blog that uses markdown files for the blog posts. So the api side of the blog will not read from a database, but rather read files from the file system. This wouldn't even work on Netlify or other JAM-Stack focused hosting providers, because we don't bundle any files like that into the lambdas/serverless functions. So there's nothing to read from the filesystem. But still, it's a pretty common use case to have plog posts as markdown files and then render html pages from those. And locally it works just fine, so let's start there!</p>
<p>I made a <code>/blog</code> folder at the root of my project and placed three markdown files in there</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1659274808417/S-F1bTKUX.png" alt="Screenshot of VSCode showing my /blog folder" /></p>
<p>I then went and added a <code>BlogPost</code> model to <code>schema.prisma</code>. You might wonder why I'm adding a database model, when I said I wasn't going to read from a database. But that's only because a lot of Redwood's generators needs a model in there to work with. When everything is generated we can just remove it. We never need to actually migrate our database to include it.</p>
<pre><code class="lang-prisma">model BlogPost {
  slug      String    @id
  author    String
  createdAt DateTime
  updatedAt DateTime?
  title     String
  body      String
}
</code></pre>
<p>With the model in place I could run <code>yarn rw g sdl BlogPost</code>, <code>yarn rw g cell BlogPost</code> and <code>yarn rw g page BlogPost</code>. I installed <a target="_blank" href="https://www.npmjs.com/package/front-matter"><code>front-matter</code></a> on my api side to parse the <code>.md</code> files, and <a target="_blank" href="https://www.npmjs.com/package/markdown-it"><code>markdown-it</code></a> on the web side to render the markdown to html.</p>
<p>The <code>blogPosts</code> service reads a markdown file (with frontmatter metadata) from the file system, and returns it. Redwood takes that and sends it to the web side using graphql. A Cell gets it, renders the title, the author etc to specific elements, and puts the markdown formatted body into a single <code>&lt;section&gt;</code></p>
<p>I also added a layout with a cell that fetches all the blog posts to build out a navigation menu.</p>
<p>This all works great in my dev environment, both as it is like a normal client/sever React app. But also as pre-rendered pages. Trying to deploy it though, and you'll run into a few problems. This is still just a canary release, so deploying to Netlify (and Vercel, maybe others) is straight up broken. We can work around that on our own though. I have <a target="_blank" href="https://github.com/redwoodjs/redwood/issues/6088">an issue open</a> for it and a PR with a potential solution that's waiting for the rest of the Redwood team to take a look at it. You can read the issue and the linked PR for more detailed background info. But the solution for now is to update your build command in <code>netlify.toml</code></p>
<pre><code class="lang-toml"><span class="hljs-section">[build]</span>
<span class="hljs-attr">command</span> = <span class="hljs-string">"yarn rw prisma generate &amp;&amp; yarn rw build --verbose"</span>
</code></pre>
<p>Compared to the standard build command we're generating the prisma client first, before building anything and that's the key to solving the problem. I also don't do any prisma migrations or data migrations because we don't need any of that since we're not using a database.</p>
<p>Now it should build and deploy successfully.</p>
<p>Going to the page it'll quickly flash the content you want to see, but then just show this 🙁</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1659277582992/U2hL392jw.png" alt="Screenshot showing the red text &quot;Something went wrong&quot;" /></p>
<p>What gives? </p>
<p>There are a couple of things going on here. First the web server serves the pre-rendered html page, and as soon as the web browser has that page it will start downloading the JavaScript bundles for the page. While it's downloading you see the pre-rendered page. But then when it's finished downloading and renders the React pages and components the BlogPostCell will fire off a gql request to the api side. It'll show its loading state ("Loading...") while the gql does its roundtrip to the api side. And now we'll run into the second problem. As I said at the beginning, when deploying to a serverless environment we're not bundling any extra files with the lambdas. So when the <code>blogPost</code> service tries to read the markdown file on the filesystem it fails, because the files aren't there. And so some random error page is returned to the Cell and we get the Cell's error state.</p>
<p>So what's the solution here? Get rid of the JavaScript that replaces our perfectly fine pre-rendered page, and just keep that! We're probably going to add this functionality into Redwood itself. But not yet. So for now we'll have to hack around it (and I ❤️ it!)</p>
<p>To solve this we're going to use one of RW's most underrated features - its scripts! Generate a new script <code>yarn rw g script ssg</code> and then execute it as part of the build process</p>
<pre><code class="lang-toml"><span class="hljs-section">[build]</span>
<span class="hljs-attr">command</span> = <span class="hljs-string">"yarn rw prisma generate &amp;&amp; yarn rw build web --verbose &amp;&amp; yarn rw exec ssg"</span>
</code></pre>
<p>I also went ahead and told it to only build the web side. Because there won't be any javascript triggering any calls to the api side we don't even need it at all anymore.</p>
<p>The <code>ssg</code> script goes through all the generated html pages and does a string replace to remove all <code>&lt;script&gt;</code> tags that download the React framework code and our app-specific code.</p>
<p>And there we have it! Redwood as a static site generator generating purely static html pages and serving them up on Netlify. Gatsby we're coming for you! (Just kidding, we love Gatsby and all other web frameworks too 😀)</p>
<p>The demo page is deployed here: https://rw-static-site-generator.netlify.app.
And the code for it is on GitHub: https://github.com/Tobbe/rw-static-site-generator.</p>
<p>Go visit the page. Navigate around and you'll notice that it does a full page reload for each page you go to. Normally this isn't all that great. But since the pages are all so small, and just basically text files on some server somewhere, it actually works out great in the end anyway! And now, turn off JavaScript and you'll notice... No difference! Because we ain't using no JS for these pages anymore 😄</p>
<hr />
<p>Cover photo by <a href="https://unsplash.com/@rgaleriacom?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Ricardo Gomez Angel</a> on <a href="https://unsplash.com/s/photos/concreto?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Visualizing your Prisma schema in a RedwoodJS project]]></title><description><![CDATA[Do you want to have a visualization of your database models? Using Redwood to build your fullstack application? Then this guide is for you! I'm going to show how easy it is to generate an ER diagram, and keep it updated!
We'll be using keonik/prisma-...]]></description><link>https://tlundberg.com/visualizing-your-prisma-schema-in-a-redwoodjs-project</link><guid isPermaLink="true">https://tlundberg.com/visualizing-your-prisma-schema-in-a-redwoodjs-project</guid><category><![CDATA[RedwoodJS]]></category><category><![CDATA[prisma]]></category><category><![CDATA[Databases]]></category><category><![CDATA[database]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Thu, 07 Jul 2022 23:00:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1657231144093/DLh5UF2uv.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Do you want to have a visualization of your database models? Using Redwood to build your fullstack application? Then this guide is for you! I'm going to show how easy it is to generate an ER diagram, and keep it updated!</p>
<p>We'll be using <a target="_blank" href="https://github.com/keonik/prisma-erd-generator">keonik/prisma-erd-generator</a> which is an npm package that you can use like a kind of plugin to prisma to generate an svg ER diagram of your prisma schema.</p>
<p>First thing you need to do is to install it. In the root of your RW project, run this command (yes, we're not specifying any workspace here):</p>
<pre><code class="lang-zsh">yarn add -D prisma-erd-generator @mermaid-js/mermaid-cli
</code></pre>
<p>You'll then need to add a bit of config to the top of your schema file (<code>api/prisma/schema.prisma</code>), right below the generator config for client (<code>generate client { ... }</code>)</p>
<pre><code class="lang-prisma">generator erd {
  provider = "prisma-erd-generator"
  output   = "./ERD.svg"
}
</code></pre>
<p>Save the file and now you're ready to generate the diagram. Run this command from the root of your project (it can take a <em>long</em> time the first time you run it). (If you're trying this on a fresh project, make sure you've ran at least one migration before trying to add and run the erd generator.)</p>
<pre><code class="lang-bash">yarn rw prisma generate
</code></pre>
<p>That should have produced an SVG file at <code>api/prisma/ERD.svg</code>. Open it up (for example using your web browser) and you should see all your models and relations from your schema file.</p>
<p>What's great here is that whenever prisma runs <code>generate</code> you'll get a new diagram. So,  for example, whenever you generate a new migration you'll also get an up-to-date visual representation of your database model. How great is that?!</p>
<p>For a basic schema like this:</p>
<pre><code class="lang-prisma">model Post {
  id        Int      @id @default(autoincrement())
  title     String
  body      String
  createdAt DateTime @default(now())
  userId    Int?
  user      User?    @relation(fields: [userId], references: [id])
}

model Contact {
  id        Int      @id @default(autoincrement())
  name      String
  email     String
  message   String
  createdAt DateTime @default(now())
}

model User {
  id                  Int       @id @default(autoincrement())
  email               String    @unique
  hashedPassword      String
  salt                String
  resetToken          String?
  resetTokenExpiresAt DateTime?
  roles               String?
  posts               Post[]
}
</code></pre>
<p>You'll get a diagram like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657234005994/WKAhd5xZT.png" alt="erd_simple.png" /></p>
]]></content:encoded></item><item><title><![CDATA[Migrating data with prisma migrate]]></title><description><![CDATA[prisma migrate is mainly used for schema migrations, and it's great for doing that. But sometimes you also need or want to migrate your data along with your schema. Turns out prisma migrate can do that as well!
I'm using RedwoodJS, and Redwood has it...]]></description><link>https://tlundberg.com/migrating-data-with-prisma-migrate</link><guid isPermaLink="true">https://tlundberg.com/migrating-data-with-prisma-migrate</guid><category><![CDATA[RedwoodJS]]></category><category><![CDATA[Databases]]></category><category><![CDATA[SQL]]></category><category><![CDATA[migration]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Sun, 28 Nov 2021 15:23:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1638104820837/nnP_EeWzM.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><code>prisma migrate</code> is mainly used for schema migrations, and it's great for doing that. But sometimes you also need or want to migrate your data along with your schema. Turns out prisma migrate can do that as well!</p>
<p>I'm using RedwoodJS, and Redwood has <a target="_blank" href="https://redwoodjs.com/docs/data-migrations">its own data migration solution</a>. But I wanted to do it all with prisma for various reasons. You should decide for your particular scenario what works best for you.</p>
<p>Prisma migrations are just standard .sql files, so if you can express your data migration as one or more sql statements you're good to go.</p>
<p>As I said, I'm using RedwoodJS, so first I edit my <code>api/db/schema.prisma</code> file to do my schema migration. In this case I have a "Products" model with a soft/implicit self-reference. I wanted to make that a proper Prisma one-to-many relation. The self-reference is used to model product variations. Like you have a main "Product" that's a shirt. Then you have variants for the different colors of the shirt. So you could have three variants for "red", "green" and "blue" shirts.</p>
<p>This is the diff for my <code>schema.prisma</code>-file</p>
<pre><code class="lang-diff">diff --git a/api/db/schema.prisma b/api/db/schema.prisma
index d29fc77..40ffc59 100644
<span class="hljs-comment">--- a/api/db/schema.prisma</span>
<span class="hljs-comment">+++ b/api/db/schema.prisma</span>
@@ -45,6 +45,9 @@ model Product {
   stock               Int                 @default(0)
   images              String?
   parent_v_p_id       Int?
<span class="hljs-addition">+  parentId            String?             @db.Uuid</span>
<span class="hljs-addition">+  parentProduct       Product?            @relation("ParentProduct", fields: [parentId], references: [id])</span>
<span class="hljs-addition">+  variants            Product[]           @relation("ParentProduct")</span>
   ean                 String?
   attrib1name         String?
   attrib1val          String?
</code></pre>
<p>Next up I run <code>yarn rw prisma migrate dev --create-only</code>. The key here is the <code>--create-only</code> flag. It creates a .sql file with the migration instruction in it, but doesn't actually apply it. This gives us a chance to modify the SQL statements the migration will run.</p>
<p>This is what the generated file looks like</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- AlterTable</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-string">"Product"</span> <span class="hljs-keyword">ADD</span> <span class="hljs-keyword">COLUMN</span>     <span class="hljs-string">"parentId"</span> <span class="hljs-keyword">UUID</span>;

<span class="hljs-comment">-- AddForeignKey</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-string">"Product"</span> <span class="hljs-keyword">ADD</span> <span class="hljs-keyword">FOREIGN</span> <span class="hljs-keyword">KEY</span> (<span class="hljs-string">"parentId"</span>) <span class="hljs-keyword">REFERENCES</span> <span class="hljs-string">"Product"</span>(<span class="hljs-string">"id"</span>) <span class="hljs-keyword">ON</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">SET</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">UPDATE</span> <span class="hljs-keyword">CASCADE</span>;
</code></pre>
<p>As you can see the Prisma relation is made up of three fields, but only one is actually created in the database. The <code>parentId</code> field in this case. I'm using the database-native datatype <code>UUID</code> here to match with the data type of my <code>id</code> field.</p>
<p>I mentioned I had a soft or implicit self-reference for product variants. Half of it is the <code>parent_v_p_id</code> field you can see in the diff output above. The other half is a <code>variable_product_id</code> field. I call products that have different variants for "variable" products. (And products without variants for "simple" products.) Now however I decided to just use the <code>id</code> that all products have as the relationship reference. No need for a specific field when I already have a perfectly good id to use.</p>
<p>Now that we know what we want to do, let's write the SQL for it.</p>
<p>I'm not great at SQL, but I do know the basics, so after a few minutes on Google I came up with this statement*</p>
<pre><code class="lang-sql"><span class="hljs-keyword">UPDATE</span> <span class="hljs-string">"Product"</span>
<span class="hljs-keyword">SET</span> <span class="hljs-string">"parentId"</span> = p.id
<span class="hljs-keyword">FROM</span> (
  <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">id</span>,
         variable_product_id <span class="hljs-keyword">AS</span> v_p_id
  <span class="hljs-keyword">FROM</span> <span class="hljs-string">"Product"</span>
) <span class="hljs-keyword">AS</span> p
<span class="hljs-keyword">WHERE</span> parent_v_p_id <span class="hljs-keyword">IS</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
  <span class="hljs-keyword">AND</span> p.v_p_id = parent_v_p_id;
</code></pre>
<p>(* Not really true. I came up with something similar, but had to do some additional tweaks to arrive at what you see above)</p>
<p>Modifying the DB, and especially editing the data in it, is scary. So I wanted a way to test my SQL before making it part of the migration. This is where I turned to my db admin tool of choice, DBeaver. I'm sure TablePlus or something like that would work great as well. Just need a way to run custom SQL statements. The trick I used to be able to test my code was to wrap it in a transaction that I always roll back at the end. That way I can iterate on the sql without doing any permanent changes to the database.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">BEGIN</span>;

<span class="hljs-comment">-- [migration statements]...</span>

<span class="hljs-keyword">ROLLBACK</span>;
</code></pre>
<p>I did have to make a few tweaks to my original SQL. When the entire transaction completed without errors I was ready to try it on my test database. So I just copied the statements between the <code>BEGIN;</code> and <code>ROLLBACK;</code> lines and replaced all the content of my migration .sql file with the copied code.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- 20211128123650_parentproduct_relation/migration.sql</span>

<span class="hljs-comment">-- AlterTable</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-string">"Product"</span>
<span class="hljs-keyword">ADD</span> <span class="hljs-keyword">COLUMN</span> <span class="hljs-string">"parentId"</span> <span class="hljs-keyword">UUID</span>;

<span class="hljs-comment">-- AddForeignKey</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-string">"Product"</span>
<span class="hljs-keyword">ADD</span> <span class="hljs-keyword">FOREIGN</span> <span class="hljs-keyword">KEY</span> (<span class="hljs-string">"parentId"</span>)
  <span class="hljs-keyword">REFERENCES</span> <span class="hljs-string">"Product"</span>(<span class="hljs-string">"id"</span>)
  <span class="hljs-keyword">ON</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">SET</span> <span class="hljs-literal">NULL</span>
  <span class="hljs-keyword">ON</span> <span class="hljs-keyword">UPDATE</span> <span class="hljs-keyword">CASCADE</span>;

<span class="hljs-keyword">UPDATE</span> <span class="hljs-string">"Product"</span>
<span class="hljs-keyword">SET</span> <span class="hljs-string">"parentId"</span> = p.id
<span class="hljs-keyword">FROM</span> (
  <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">id</span>,
         variable_product_id <span class="hljs-keyword">AS</span> v_p_id
  <span class="hljs-keyword">FROM</span> <span class="hljs-string">"Product"</span>
) <span class="hljs-keyword">AS</span> p
<span class="hljs-keyword">WHERE</span> parent_v_p_id <span class="hljs-keyword">IS</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
  <span class="hljs-keyword">AND</span> p.v_p_id = parent_v_p_id;
</code></pre>
<p>One final precaution to take (depending on how much you care about your test database) is to dump your database to a file. I usually do that with this command</p>
<pre><code class="lang-bash">pg_dump \
  --username=username \
  --password \
  --format=plain \
  --inserts \
  --no-owner \
  --no-privileges \
  db_name &gt; db_name_dump_$(date +<span class="hljs-string">"%Y%m%dT%H%M%S"</span>).sql
</code></pre>
<p>It results in a big file that's slow to restore. But it's also a file that's easy read (and understand) and that will work across different versions of PostgreSQL and maybe even between different DB engines. I think it's worth the tradeoffs for when working with my test db. For backing up your prod db you might want to use something else. But now we're off on a tangent. Let's get back to data migrations... </p>
<p>With the migration script updated it's time to apply it. Running <code>yarn rw prisma migrate dev</code> again, but without any extra flags this time, will apply the migration to your test database. </p>
<p>After running that it's obviously a good idea to verify that everything looks alright in the database. Again DBeaver (or TablePlus etc) comes in handy. Finally you should spin up your app to make sure it runs as expected. Hopefully the db survived the migration and your data (and schema) is properly migrated.</p>
<p>That's it 🙂 Thanks for reading!</p>
<hr />
<p>Cover photo by <a href="https://unsplash.com/@groove328?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Andrey Novik</a> on <a href="https://unsplash.com/s/photos/prisma?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Adding Google Maps to your RedwoodJS App]]></title><description><![CDATA[I wanted to show the location of a local business on their About page. The most common solution to this is to embed Google Maps with a pin showing the location. Since Redwood uses React I went and looked for a Google Maps React component.
Looking for...]]></description><link>https://tlundberg.com/adding-google-maps-to-your-redwoodjs-app</link><guid isPermaLink="true">https://tlundberg.com/adding-google-maps-to-your-redwoodjs-app</guid><category><![CDATA[RedwoodJS]]></category><category><![CDATA[google maps]]></category><category><![CDATA[React]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Wed, 13 Oct 2021 18:29:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634149458872/MT_OhSh8h.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I wanted to show the location of a local business on their About page. The most common solution to this is to embed Google Maps with a pin showing the location. Since Redwood uses React I went and looked for a Google Maps React component.</p>
<p>Looking for existing guides on how to integrate Google Maps I found mention of two React components, https://github.com/tomchentw/react-google-maps and https://github.com/fullstackreact/google-maps-react. They both seem fairly popular on npm too, with 144k and 60k weekly downloads respectively. A quick check on <a target="_blank" href="https://bundlephobia.com/package/google-maps-react@2.0.6">bundlephobia</a> showed that google-maps-react was about 1/3 the size of react-google-maps. So even though it wasn't as popular I went with the smaller option. Only to find out their docs are outdated and their TypeScript support is broken 🙁</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1634132926737/v8OclApo4.png" alt="image.png" /></p>
<p>Turns out both project are pretty much abandoned too, with lots of open issues without any replies. So back to Google to find other options.</p>
<p>I then found <a target="_blank" href="https://github.com/JustFly1984/react-google-maps-api">@react-google-maps/api</a> which is a rewrite of react-google-maps, the most popular of the packages I evaluated earlier. This rewrite is made for modern versions of React with hooks and function components 💯 Nice! To be honest their docs could use some work (like a total overhaul), but after a couple of tries I got something working!</p>
<p>While writing this blog post I also found https://github.com/google-map-react/google-map-react which is the most popular option I've found so far with 215k weekly downloads. But that library seems more focused on rendering your own custom markers on the map. I just wanted the default Google Maps pin, which is super easy with the package I used.</p>
<h2 id="implementation">Implementation</h2>
<p>Begin by adding the package to your app</p>
<pre><code><span class="hljs-attribute">yarn</span> workspace web <span class="hljs-variable">@react</span>-google-maps/api
</code></pre><p>You'll need a Google Maps API key, so while the package is downloading and installing you can go to https://developers.google.com/maps/documentation/javascript/get-api-key and set everything up. You don't need to set up billing to use it, but you'll have an ugly "for development only" overlay and lots of watermarks on your map until you do.</p>
<p>When you have your API key you should go ahead and paste it in your .env file. (If you don't have a .env file, look for a .env.defaults file and create an empty .env file next to it.) I'm going to use the key name GOOGLE_MAPS_API_KEY. So place this on a row of its own</p>
<pre><code><span class="hljs-attr">GOOGLE_MAPS_API_KEY</span>=AIezaChaosFml69ggpW7YlQHnndx-QBesos
</code></pre><p>It should look something like the above. To be able to use it in your front-end code you also have to whitelist it in redwood.toml</p>
<pre><code><span class="hljs-section">[web]</span>
  <span class="hljs-attr">includeEnvironmentVariables</span> = [
    <span class="hljs-string">'GOOGLE_MAPS_API_KEY'</span>,
  ]
</code></pre><p>After you can created a <code>Map</code> component to use on your About page (or wherever you want, obv).</p>
<pre><code>yarn rw g component <span class="hljs-built_in">Map</span>
</code></pre><p>Open up <code>Map.tsx</code> and replace everything inside with this</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { GoogleMap, Marker, useJsApiLoader } <span class="hljs-keyword">from</span> <span class="hljs-string">'@react-google-maps/api'</span>

<span class="hljs-keyword">const</span> containerStyle = {
  width: <span class="hljs-string">'100%'</span>,
  height: <span class="hljs-string">'400px'</span>,
  marginTop: <span class="hljs-string">'16px'</span>,
}

<span class="hljs-keyword">const</span> center = {
  lat: <span class="hljs-number">37.419857</span>,
  lng: <span class="hljs-number">-122.078827</span>,
}

<span class="hljs-keyword">const</span> <span class="hljs-built_in">Map</span> = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> { isLoaded, loadError } = useJsApiLoader({
    googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY,
  })

  <span class="hljs-keyword">if</span> (loadError) {
    <span class="hljs-comment">// This would be a great place to show just a static image of a map as a</span>
    <span class="hljs-comment">// fallback option</span>
    <span class="hljs-keyword">return</span> &lt;p&gt;<span class="hljs-built_in">Error</span> loading map&lt;/p&gt;
  }

  <span class="hljs-keyword">return</span> isLoaded ? (
    &lt;GoogleMap
      zoom={<span class="hljs-number">10</span>}
      center={center}
      mapContainerStyle={containerStyle}
    &gt;
      &lt;Marker position={center} /&gt;
    &lt;/GoogleMap&gt;
  ) : (
    &lt;p&gt;Loading...&lt;/p&gt;
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> React.memo(<span class="hljs-built_in">Map</span>)
</code></pre>
<p>It's important to note that you need to give the container some dimensions. Otherwise the map won't show up.</p>
<p>And now we're ready to use it on our About page</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> <span class="hljs-built_in">Map</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'src/components/Map'</span>

<span class="hljs-keyword">const</span> AboutPage = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">address</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span>Googleplex<span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">br</span> /&gt;</span>
        1600 Amphitheatre Pkwy
        <span class="hljs-tag">&lt;<span class="hljs-name">br</span> /&gt;</span>
        Mountain View
        <span class="hljs-tag">&lt;<span class="hljs-name">br</span> /&gt;</span>
        CA 94043
        <span class="hljs-tag">&lt;<span class="hljs-name">br</span> /&gt;</span>
        United States
      <span class="hljs-tag">&lt;/<span class="hljs-name">address</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Map</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> AboutPage
</code></pre>
<p>This is what it'll look like (it's just a screenshot, so it's not interactive like the real map would be)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1634149283746/bbjzg5c84.png" alt="image.png" /></p>
<hr />
<p>Cover photo by <a href="https://unsplash.com/@thomaskinto?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Thomas Kinto</a> on <a href="https://unsplash.com/s/photos/map?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Moving to hashnode from my custom Gatsby blog]]></title><description><![CDATA[I'm moving my blog to hashnode. I had it on a Gatsby site that I hosted using GitHub pages. Even had a sweet automated workflow 🤖 set up where I could just push new content and it would go live automatically 🪄. 
Gatsby is great! It gives you a lot ...]]></description><link>https://tlundberg.com/moving-to-hashnode-from-my-custom-gatsby-blog</link><guid isPermaLink="true">https://tlundberg.com/moving-to-hashnode-from-my-custom-gatsby-blog</guid><category><![CDATA[Blogging]]></category><category><![CDATA[Gatsby]]></category><category><![CDATA[Hashnode]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Wed, 13 Oct 2021 12:27:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634127765344/sLEiLtxcU.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm moving my blog to hashnode. I had it on a Gatsby site that I hosted using GitHub pages. Even had a sweet automated workflow 🤖 set up where I could just push new content and it would go live automatically 🪄. </p>
<p>Gatsby is great! It gives you a lot of features and creative freedom. The thing is though - I didn't use barely any of it. I just want a place to dump my thoughts, and hashnode seems a great place to do that. Hopefully it'll help a bit with reach as well.</p>
<p>Here are three great features of hashnode that they highlight on their front page</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1634126750776/VI-WDLicF.png" alt="image.png" /></p>
<p>Writing this post I can add another thing that's really nice, and that's their online editor! Even let me just copy-paste any image and it's automatically uploaded to their CDN and linked in my post 🏞</p>
<p>What I would like to see though, is better docs on how to migrate your existing blog to hashnode. If the docs are there, I couldn't find them. And while they have a pretty prominent link to instructions for linking your own domain to hashnode there was really no mention of what to expect once it was linked. All I got was this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1634127120241/55V6HfDFQ.png" alt="image.png" /></p>
<p>That doesn't look very nice. Some hints on what to do to prepare for linking would have been nice. Like "move your content over to us first, link your domain after", if that's indeed the case. TBD.</p>
<p>Update: A bit later I got this, which obviously isn't perfect, but at least it's 100x better!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1634127275816/5GSBrsbcf.png" alt="image.png" /></p>
<p>I've also found an import tool</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1634127386401/kniOEWuSw.png" alt="image.png" /></p>
<p>I already have my posts as .md markdown files, so hopefully the Bulk Markdown importer should be helpful (it takes a .zip-file with a single .md-file for each post). Wish me luck 🤞!</p>
<hr />
<p>Cover photo by <a href="https://unsplash.com/@robinson?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Robinson Greig</a> on <a href="https://unsplash.com/s/photos/moving?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Deploying a RedwoodJS app on Dokku]]></title><description><![CDATA[This is a guide on how to self host a Redwood project using Dokku. Some familiarity with Linux and the command line is required. If you're just getting started with Redwood and want something easy and free I recommend deploying to Netlify or Vercel, ...]]></description><link>https://tlundberg.com/deploying-a-redwoodjs-app-on-dokku</link><guid isPermaLink="true">https://tlundberg.com/deploying-a-redwoodjs-app-on-dokku</guid><category><![CDATA[RedwoodJS]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Devops]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Mon, 29 Mar 2021 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634193916140/e4sKKzFz1.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a guide on how to self host a Redwood project using <a target="_blank" href="https://dokku.com/">Dokku</a>. Some familiarity with Linux and the command line is required. If you're just getting started with Redwood and want something easy and free I recommend deploying to <a target="_blank" href="https://netlify.com">Netlify</a> or <a target="_blank" href="https://vercel.com">Vercel</a>, both of which have first-class Redwood support.</p>
<p>If you don't know what Dokku is, the short version is that it's like a self-hosted version of Heroku. It uses the same buildpacks, procfiles and deployment process as Heroku. There are other, more advanced, options as well, if the Heroku stuff is too limiting for you, but I won't go into that here. Since it's like Heroku, that also means you could host your database in Dokku if you wanted. But that's also not covered by this guide.</p>
<p>A big shoutout to <a target="_blank" href="https://www.brentjanderson.com/">Brent Anderson</a> for his <a target="_blank" href="https://gist.github.com/brentjanderson/dcb59c46023c67c44eb12492b038ff84">gist</a> on getting RW running on Heroku. I could not have written this guide without that gist.</p>
<p>Beware that it's you, yourself, who is responsible for keeping your server secure and up-to-date. You have to figure out backups. What do you do if your page becomes super popular? How do you handle scaling? There are a thousand and one reasons to <strong>not</strong> self-host. <a target="_blank" href="https://begin.com">Begin</a> has a whole page dedicated to why you shouldn't do it. It's a pretty fun, and eye-opening read. Have a look: https://begin.com/learn/shit-youre-not-doing-with-begin</p>
<p>With that out of the way: let's get started!</p>
<p>To follow along you'll need an Ubuntu 20.04 box (or a recent version of Debian) where you have root access. The server needs to have at least 1 GB of RAM. You'll also need a domain you can configure DNS for. And finally, a suitable Redwood project to deploy. I have a small cloud server at <a target="_blank" href="https://www.hetzner.com">Hetzner</a> and a domain that I bought through <a target="_blank" href="https://porkbun.com">Porkbun</a>. If you have a DigitalOcean droplet cloud server, or a Microsoft Azure server there are specific guides on Dokku's web page <a target="_blank" href="https://dokku.com/docs/getting-started/install/digitalocean/">1</a> <a target="_blank" href="https://dokku.com/docs/getting-started/install/azure/">2</a></p>
<p>First thing to do is configure your domain to point to the IP address of your server (you need a wildcard record). Make sure it's working by SSHing into your server. If this is a freshly installed OS, create a new user (do <strong>not</strong> name it "dokku") and make sure it's allowed to use the <code>sudo</code> command. From now on, use this user to execute all commands.</p>
<p>Install Dokku</p>
<pre><code><span class="hljs-attribute">wget</span> https://raw.githubusercontent.com/dokku/dokku/v<span class="hljs-number">0</span>.<span class="hljs-number">24</span>.<span class="hljs-number">3</span>/bootstrap.sh
<span class="hljs-attribute">sudo</span> DOKKU_TAG=v<span class="hljs-number">0</span>.<span class="hljs-number">24</span>.<span class="hljs-number">3</span> bash bootstrap.sh
</code></pre><p>Go to yourdomain.com and finish the setup in your browser.</p>
<p>Create a new Dokku application (replace <code>my-redwood-app</code> with the actual name of your app).</p>
<pre><code>dokku apps:<span class="hljs-keyword">create</span> my-redwood-app
</code></pre><p>If you have a database connected to your app you'll want to set the <code>DATABASE_URL</code> environment variable.</p>
<pre><code>dokku config:<span class="hljs-keyword">set</span> my-redwood-app DATABASE_URL=postgresql:<span class="hljs-comment">//asldfjsldf</span>
</code></pre><p>Now it's time to prepare your Redwood app for deploying to Dokku. You need to do two things:</p>
<ol>
<li>Set <code>apiProxyPath = "/api"</code> in redwood.toml</li>
<li>Create a new file in the root of your project called <code>.buildpacks</code>. Add this content to that file<pre><code><span class="hljs-attribute">https</span>:<span class="hljs-comment">//github.com/tobbe/dokku-buildpack-redwood-init.git</span>
<span class="hljs-attribute">https</span>:<span class="hljs-comment">//github.com/heroku/heroku-buildpack-nodejs.git</span>
<span class="hljs-attribute">https</span>:<span class="hljs-comment">//github.com/heroku/heroku-buildpack-nginx.git</span>
<span class="hljs-attribute">https</span>:<span class="hljs-comment">//github.com/tobbe/dokku-buildpack-redwood-finish.git</span>
</code></pre></li>
</ol>
<p>Now your app is ready. For the next step you'll need to have an ssh agent running, with your key loaded. If you're on Windows you might have to do this manually. These two commands worked for me in git-bash.</p>
<pre><code><span class="hljs-built_in">eval</span> <span class="hljs-string">`ssh-agent -s`</span>
ssh-add ~<span class="hljs-regexp">/.ssh/i</span>d_rsa
</code></pre><p>Now you can add dokku as a new remote to git, and push to deploy! 🚀</p>
<pre><code>git remote add dokku dokku@yourdomain.com:<span class="hljs-keyword">my</span>-redwood-app
git <span class="hljs-keyword">push</span> dokku main
</code></pre><p>This will take a few minutes as it downloads and installs node.js, nginx, redwood and your other project dependencies etc. But in the end, when it's ready, you should be able to go to my-redwood-app.yourdomain.com and see your site live in your browser!</p>
<p>If you made it this far, and it works, congratulations! 🎉🏁</p>
<p>I bet a few of you who read this wonder what those github hosted buildpacks do that we added to the <code>.buildpacks</code> file. The first one is written by me, and it adds a few files to your Redwood app. Then we add nodejs and nginx. One of the files added in the first step is a config file for nginx. That's why the "redwood-init" buildpack needs to run first. Finally there's another Redwood specific buildpack that installs a few needed packages, builds Redwood and finally starts nginx and Redwood's api server.</p>
<p>🔒 Now that your Redwood app is live, I highly recommend setting up SSL for it, to make it more secure. I'm just going to link to <a target="_blank" href="https://dokku.com/docs/deployment/application-deployment/#setting-up-ssl">Dokku's SSL guide</a>. It's really easy to get Let's Encrypt setup.</p>
<p><span>(Header photo by <a href="https://unsplash.com/@some_random_guy?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Alex Duffy</a> on <a href="https://unsplash.com/s/photos/docks-containers?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a>)</span></p>
]]></content:encoded></item><item><title><![CDATA[Building a RedwoodJS Signup Form with Validation]]></title><description><![CDATA[I recently wrote a signup form and wanted to share how I did it. It shows how to use some of the more advanced features of React-Hook-Form with RedwoodJS. Redwood has its own @redwoodjs/form package. Under the hood it's using react-hook-form. For a l...]]></description><link>https://tlundberg.com/building-a-redwoodjs-signup-form-with-validation</link><guid isPermaLink="true">https://tlundberg.com/building-a-redwoodjs-signup-form-with-validation</guid><category><![CDATA[RedwoodJS]]></category><category><![CDATA[React]]></category><category><![CDATA[forms]]></category><category><![CDATA[Validation]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Sat, 06 Mar 2021 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634193921804/78tvbK_DU.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently wrote a signup form and wanted to share how I did it. It shows how to use some of the more advanced features of React-Hook-Form with RedwoodJS. Redwood has its own <code>@redwoodjs/form</code> package. Under the hood it's using react-hook-form. For a lot of use cases using what's available through the RW package will be all you need. But if you need more control, you can import from the react-hook-form package directly and use everything available there.</p>
<p>This is what we'll build</p>
<p><img src="https://tobbe.github.io/assets/rw_signup_form.png" alt="Screenshot of signup form" /></p>
<p>Let's start with a new Redwood project and generate a signup page to add our form to.</p>
<pre><code class="lang-terminal">yarn create redwood-app signup-example
cd signup-example/
yarn rw g page signup
</code></pre>
<p>Open up <code>SignupPage.js</code> in your code editor of choice and change it to look like this</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> {
  Form,
  TextField,
  EmailField,
  PasswordField,
  Label,
  Submit,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@redwoodjs/forms"</span>;

<span class="hljs-keyword">const</span> SignupPage = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> onSubmit = <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Submitted form with data"</span>, data);
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Form</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{onSubmit}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span>&gt;</span>Name:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">TextField</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span> /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span>&gt;</span>Email:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">EmailField</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span> /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span>&gt;</span>Password:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">PasswordField</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span> /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password_confirm"</span>&gt;</span>Confirm Password:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">PasswordField</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password_confirm"</span> /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Submit</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">Submit</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Form</span>&gt;</span></span>
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> SignupPage;
</code></pre>
<p>And just to make it display a tiny bit better we'll add some CSS rules to <code>index.css</code></p>
<pre><code class="lang-css"><span class="hljs-selector-tag">label</span>,
<span class="hljs-selector-tag">button</span>,
<span class="hljs-selector-tag">input</span> {
  <span class="hljs-attribute">display</span>: block;
}

<span class="hljs-selector-tag">label</span>,
<span class="hljs-selector-tag">button</span> {
  <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">1em</span>;
}

<span class="hljs-selector-tag">span</span> {
  <span class="hljs-attribute">color</span>: red;
}
</code></pre>
<p>Now you have a basic form you can use to let users signup on your website. Enter some data and press Submit and you'll see everything printed to the web browser console.</p>
<p>Next thing I wanted to do was to add some validation to the form. We want to make all fields required.</p>
<p>For this we can use the regular html5 <code>required</code> attribute, like so</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">return</span> (
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Form</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{onSubmit}</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span>&gt;</span>Name:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">TextField</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span> <span class="hljs-attr">required</span> /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span>&gt;</span>Email:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">EmailField</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">required</span> /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span>&gt;</span>Password:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">PasswordField</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">required</span> /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password_confirm"</span>&gt;</span>Confirm Password:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">PasswordField</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password_confirm"</span> <span class="hljs-attr">required</span> /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">Submit</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">Submit</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">Form</span>&gt;</span></span>
);
</code></pre>
<p>Now, if you try to submit the form without filling in all the fields we'll see something like this</p>
<p><img src="https://tobbe.github.io/assets/rw_signup_required.png" alt="Screenshot of html5 required field" /></p>
<p>But Redwood can do much better than that! Let's switch over to <code>@redwood/forms</code>' custom validation. One gotcha here is that you have to switch all fields over, you can't just add custom validation to one field, to try it out. If you do, the form will get confused. So you have to pick one way or the other for all your form fields.</p>
<p>Change your code to look like this to get "required" validation with custom messages.</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">return</span> (
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Form</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{onSubmit}</span> <span class="hljs-attr">noValidate</span> <span class="hljs-attr">validation</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">mode:</span> "<span class="hljs-attr">onBlur</span>" }}&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span>&gt;</span>Name:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">TextField</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span> <span class="hljs-attr">validation</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">required:</span> "<span class="hljs-attr">Name</span> <span class="hljs-attr">is</span> <span class="hljs-attr">required</span>" }} /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">FieldError</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span> /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span>&gt;</span>Email:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">EmailField</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">validation</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">required:</span> "<span class="hljs-attr">Email</span> <span class="hljs-attr">is</span> <span class="hljs-attr">requried</span>" }} /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">FieldError</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span> /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span>&gt;</span>Password:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">PasswordField</span>
      <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span>
      <span class="hljs-attr">validation</span>=<span class="hljs-string">{{</span>
        <span class="hljs-attr">required:</span> "<span class="hljs-attr">You</span> <span class="hljs-attr">must</span> <span class="hljs-attr">choose</span> <span class="hljs-attr">a</span> <span class="hljs-attr">password</span>",
      }}
    /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">FieldError</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span> /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password_confirm"</span>&gt;</span>Confirm Password:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">PasswordField</span>
      <span class="hljs-attr">name</span>=<span class="hljs-string">"password_confirm"</span>
      <span class="hljs-attr">validation</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">required:</span> "<span class="hljs-attr">You</span> <span class="hljs-attr">have</span> <span class="hljs-attr">to</span> <span class="hljs-attr">confirm</span> <span class="hljs-attr">your</span> <span class="hljs-attr">password</span>" }}
    /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">FieldError</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password_confirm"</span> /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">Submit</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">Submit</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">Form</span>&gt;</span></span>
);
</code></pre>
<p>As you can see we removed <code>required</code> and added <code>validation</code> instead. <code>required</code> is a regular html attribute, and <code>validation</code> is a prop from <code>@redwoodjs/forms</code>. As I mentioned earlier <code>@redwoodjs/forms</code> uses react-hook-form under the hood, and that <code>validation</code> syntax we used here would have looked something like this if done with r-h-f instead: <code>ref={register({ required: 'Name is required' })}</code>. This can be good to know when reading the r-h-f docs to try to figure out how to do something more advanced. We also added <code>noValidate</code> to the form, because we don't want the html5 validation now that we've added our own. Also configured to validation to trigger on blur.</p>
<p>The email is really important, so I wanted to add a bit more strict verification for that field. It's obviously not bullet-proof, but it's better than nothing.</p>
<pre><code class="lang-jsx">&lt;EmailField
  name=<span class="hljs-string">"email"</span>
  validation={{
    <span class="hljs-attr">required</span>: <span class="hljs-string">'Email is required'</span>,
    <span class="hljs-attr">pattern</span>: {
      <span class="hljs-attr">value</span>: <span class="hljs-regexp">/[^@]+@[^.]+\..{2,}/</span>,
      message: <span class="hljs-string">'Please enter a valid email address'</span>,
    },
  }}
/&gt;
</code></pre>
<p>I decided to keep my password validation rules really simple. All I require is that the passwords are at least 10 characters long.</p>
<pre><code class="lang-jsx">&lt;PasswordField
  name=<span class="hljs-string">"password"</span>
  validation={{
    <span class="hljs-attr">required</span>: <span class="hljs-string">"Password is required"</span>,
    <span class="hljs-attr">minLength</span>: {
      <span class="hljs-attr">value</span>: <span class="hljs-number">10</span>,
      <span class="hljs-attr">message</span>: <span class="hljs-string">"Password must be at least 10 characters long"</span>,
    },
  }}
/&gt;
</code></pre>
<p>The final thing I wanted to add is probably the most interesting and that's validation to make sure "Password" and "Confirm Password" matches. For this we need to use the <code>watch</code> method from react-hook-form. It's available as part of what you get back from the <code>useForm()</code> hook.</p>
<p>These are the parts you need to set it up.</p>
<p>First a couple of new imports.</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { useRef } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
<span class="hljs-keyword">import</span> { useForm } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-hook-form'</span>
</code></pre>
<p>Here we use the imports from above. <code>useForm()</code> is from react-hook-form and gives us full control over the form. Normally <code>@redwoodjs/forms</code> set this up for us, but now we need our own, to set up a watch on the password. We store the watched value in a ref we get from React's <code>useRef()</code>. When you add a <code>watch</code> you're going to introduce more re-renders. But only when that watched field changes, not when any other field in the form changes.</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> formMethods = useForm()
<span class="hljs-keyword">const</span> password = useRef()
password.current = formMethods.watch(<span class="hljs-string">'password'</span>, <span class="hljs-string">''</span>)
</code></pre>
<p>As I said, we're now using our own <code>formMethods</code>, so we have to let Redwood know to use that one, and not create one of its own.</p>
<pre><code class="lang-jsx">&lt;Form
  onSubmit={onSubmit}
  noValidate
  validation={{ <span class="hljs-attr">mode</span>: <span class="hljs-string">'onBlur'</span> }}
  formMethods={formMethods}
&gt;
</code></pre>
<p>Finally, for the validation, we add a custom <code>validate</code> method to the <code>validation</code> object. These custom <code>validate</code> methods receive the current field value as a parameter, and should return <code>true</code> when the field is valid, and <code>false</code> or a string when the field is not valid. The string, if that's what you return, will be the error message displayed for that field.</p>
<p>You can read more about <code>validate</code> and all other validation possibilities in the r-h-f docs: https://react-hook-form.com/api#register</p>
<pre><code class="lang-jsx">&lt;PasswordField
  name=<span class="hljs-string">"password_confirm"</span>
  validation={{
    <span class="hljs-attr">required</span>: <span class="hljs-string">'You must confirm your password'</span>,
    <span class="hljs-attr">validate</span>: <span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span>
      value === password.current || <span class="hljs-string">'Passwords must match'</span>,
  }}
/&gt;
</code></pre>
<p>There is one more thing I wanted to add. Since this is a signup page where the user should pick a new password I want the browser to show the password suggestion box.</p>
<p><img src="https://tobbe.github.io/assets/rw_signup_suggest_password.png" alt="Screenshot of passwrod suggestion box" /></p>
<p>The browser will try to figure out by context if it should show that box or not. And often it does the right thing. But there's no need for us to leave it to chance. If we tell the browser this is a <code>new-password</code> field it knows to show that ui. It's also helpful to tell the browser what field is the user name. We do this by setting <code>autoComplete="new-password"</code> and <code>autoComplete="username"</code> respectivly. Read more about these and many more options at <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete">MDN</a></p>
<p>So this is the password field, notice the <code>autoComplete</code> attribute at the end</p>
<pre><code class="lang-jsx">&lt;PasswordField
  name=<span class="hljs-string">"password"</span>
  validation={{
    <span class="hljs-attr">required</span>: <span class="hljs-string">'Password is required'</span>,
    <span class="hljs-attr">minLength</span>: {
      <span class="hljs-attr">value</span>: <span class="hljs-number">10</span>,
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Password must be at least 10 characters long'</span>,
    },
  }}
  autoComplete=<span class="hljs-string">"new-password"</span>
/&gt;
</code></pre>
<p>Wrapping it all up, here's the full <code>SignupPage.js</code></p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { useRef } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
<span class="hljs-keyword">import</span> { useForm } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-hook-form'</span>
<span class="hljs-keyword">import</span> {
  Form,
  TextField,
  EmailField,
  PasswordField,
  Label,
  Submit,
  FieldError,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@redwoodjs/forms'</span>

<span class="hljs-keyword">const</span> SignupPage = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> formMethods = useForm()
  <span class="hljs-keyword">const</span> password = useRef()
  password.current = formMethods.watch(<span class="hljs-string">'password'</span>, <span class="hljs-string">''</span>)

  <span class="hljs-keyword">const</span> onSubmit = <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Submitted form with data'</span>, data)
  }

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Form</span>
      <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{onSubmit}</span>
      <span class="hljs-attr">noValidate</span>
      <span class="hljs-attr">validation</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">mode:</span> '<span class="hljs-attr">onBlur</span>' }}
      <span class="hljs-attr">formMethods</span>=<span class="hljs-string">{formMethods}</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span>&gt;</span>Name:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">TextField</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span> <span class="hljs-attr">validation</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">required:</span> '<span class="hljs-attr">Name</span> <span class="hljs-attr">is</span> <span class="hljs-attr">required</span>' }} /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">FieldError</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span> /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span>&gt;</span>Email:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">EmailField</span>
        <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span>
        <span class="hljs-attr">validation</span>=<span class="hljs-string">{{</span>
          <span class="hljs-attr">required:</span> '<span class="hljs-attr">Email</span> <span class="hljs-attr">required</span>',
          <span class="hljs-attr">pattern:</span> {
            <span class="hljs-attr">value:</span> /[^@]+@[^<span class="hljs-attr">.</span>]+\<span class="hljs-attr">..</span>{<span class="hljs-attr">2</span>,}/,
            <span class="hljs-attr">message:</span> '<span class="hljs-attr">Please</span> <span class="hljs-attr">enter</span> <span class="hljs-attr">a</span> <span class="hljs-attr">valid</span> <span class="hljs-attr">email</span> <span class="hljs-attr">address</span>',
          },
        }}
        <span class="hljs-attr">autoComplete</span>=<span class="hljs-string">"username"</span>
      /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">FieldError</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span> /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span>&gt;</span>Password:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">PasswordField</span>
        <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span>
        <span class="hljs-attr">validation</span>=<span class="hljs-string">{{</span>
          <span class="hljs-attr">required:</span> '<span class="hljs-attr">Password</span> <span class="hljs-attr">is</span> <span class="hljs-attr">required</span>',
          <span class="hljs-attr">minLength:</span> {
            <span class="hljs-attr">value:</span> <span class="hljs-attr">10</span>,
            <span class="hljs-attr">message:</span> '<span class="hljs-attr">Password</span> <span class="hljs-attr">must</span> <span class="hljs-attr">be</span> <span class="hljs-attr">at</span> <span class="hljs-attr">least</span> <span class="hljs-attr">10</span> <span class="hljs-attr">characters</span> <span class="hljs-attr">long</span>',
          },
        }}
        <span class="hljs-attr">autoComplete</span>=<span class="hljs-string">"new-password"</span>
      /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">FieldError</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span> /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Label</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password_confirm"</span>&gt;</span>Confirm Password:<span class="hljs-tag">&lt;/<span class="hljs-name">Label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">PasswordField</span>
        <span class="hljs-attr">name</span>=<span class="hljs-string">"password_confirm"</span>
        <span class="hljs-attr">validation</span>=<span class="hljs-string">{{</span>
          <span class="hljs-attr">required:</span> '<span class="hljs-attr">You</span> <span class="hljs-attr">must</span> <span class="hljs-attr">confirm</span> <span class="hljs-attr">your</span> <span class="hljs-attr">password</span>',
          <span class="hljs-attr">validate:</span> (<span class="hljs-attr">value</span>) =&gt;</span>
            value === password.current || 'Passwords must match',
        }}
        autoComplete="new-password"
      /&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">FieldError</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password_confirm"</span> /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Submit</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">Submit</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Form</span>&gt;</span></span>
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> SignupPage
</code></pre>
<p><span>(Header photo by <a href="https://unsplash.com/@paulius005?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Paulius Dragunas</a> on <a href="https://unsplash.com/s/photos/password?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a>)</span></p>
]]></content:encoded></item><item><title><![CDATA[Using Github Flavored Markdown in the Netlify CMS Preview Pane]]></title><description><![CDATA[Document editing mode on Netlify CMS gives you two panes, one with the editor, and one with a preview. By using the global CMS object you can customize the preview. What we're going to do here is to make it render the markdown we write in the editor ...]]></description><link>https://tlundberg.com/using-github-flavored-markdown-in-the-netlify-cms-preview-pane</link><guid isPermaLink="true">https://tlundberg.com/using-github-flavored-markdown-in-the-netlify-cms-preview-pane</guid><category><![CDATA[Netlify]]></category><category><![CDATA[cms]]></category><category><![CDATA[markdown]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Thu, 11 Feb 2021 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634193893122/gYS4xjuTs.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Document editing mode on Netlify CMS gives you two panes, one with the editor, and one with a preview. By using the global <code>CMS</code> object you can customize the preview. What we're going to do here is to make it render the markdown we write in the editor with <a target="_blank" href="https://github.com/remarkjs/react-markdown"><code>react-markdown</code></a> which we extend with the <a target="_blank" href="https://github.com/remarkjs/remark-gfm"><code>remark-gfm</code></a> plugin to add support for Github Flavored Markdown (tables, strikethrough, etc).</p>
<p><img src="https://tobbe.github.io/assets/gfm_screenshot.png" alt="Screenshot of Netlify CMS with GFM enabled preview" /></p>
<p>The way the preview customization works is you tell <code>CMS</code> what collection to use the custom preview for, and you give it a React component to use. You can only write old-school class components, and you have to use the <code>createClass</code> and <code>h</code> helper functions provided by NetlifyCMS.</p>
<p>Below is an example that creates a <code>DocPreview</code> React component and tells <code>CMS</code> to use that whenever it's showing a preview for a document in the <code>"pages"</code> category. The <code>render()</code> function uses the <code>widgetFor()</code> helper function (read more in the <a target="_blank" href="https://www.netlifycms.org/docs/customization/#registerpreviewtemplate">NetifyCMS docs</a>) to get the default widget used to display the <code>'body'</code> field, and nothing else. By default the preview will show all fields, like "title" etc, but here we're stripping those out.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Netlify CMS Example<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://unpkg.com/netlify-cms@^2.10.0/dist/netlify-cms.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
      <span class="hljs-keyword">const</span> DocPreview = createClass({
        <span class="hljs-attr">render</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
          <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.props.widgetFor(<span class="hljs-string">'body'</span>);
        },
      });

      <span class="hljs-built_in">window</span>.CMS.registerPreviewTemplate(<span class="hljs-string">"pages"</span>, DocPreview);
    </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Another thing we can do is take the plain text from the "body" field and dump it in a <code>&lt;div&gt;</code> with the <code>h()</code> function, which is basically just an alias for <a target="_blank" href="https://reactjs.org/docs/react-api.html#createelement">React's <code>createElement()</code></a>.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Netlify CMS Example<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://unpkg.com/netlify-cms@^2.10.0/dist/netlify-cms.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
      <span class="hljs-keyword">const</span> DocPreview = createClass({
        <span class="hljs-attr">render</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
          <span class="hljs-keyword">const</span> bodyText = <span class="hljs-built_in">this</span>.props.entry.getIn([<span class="hljs-string">"data"</span>, <span class="hljs-string">"body"</span>]);

          <span class="hljs-keyword">return</span> h(<span class="hljs-string">"div"</span>, {}, bodyText);
        },
      });

      <span class="hljs-built_in">window</span>.CMS.registerPreviewTemplate(<span class="hljs-string">"pages"</span>, DocPreview);
    </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Since we're writing all the code in a basic <code>index.html</code> file without any kind of build tooling to support us we can't just do <code>npm install</code> and then <code>include</code>-ing the package. All we have to work with is the <code>&lt;script&gt;</code> tag. So whatever we want to use has to be in AMD or UMD module format. Thankfully <code>react-markdown</code> provides an umd build we can use straight from <a target="_blank" href="https://unpkg.com">unpkg.com</a>.</p>
<p>So just put https://unpkg.com/react-markdown@5.0.3/react-markdown.min.js in a <code>&lt;script&gt;</code> tag, and you'll have <code>window.ReactMarkdown</code> available, which is a React component you can use inside your custom <code>render</code> method.</p>
<p>With this you should be back to something that works the same as when we used <code>widgetFor()</code> except now we're using our own Markdown renderer, and not Netlify CMS's.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Netlify CMS Example<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://unpkg.com/netlify-cms@^2.10.0/dist/netlify-cms.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://unpkg.com/react-markdown@5.0.3/react-markdown.min.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
      <span class="hljs-keyword">const</span> DocPreview = createClass({
        <span class="hljs-attr">render</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
          <span class="hljs-keyword">const</span> bodyText = <span class="hljs-built_in">this</span>.props.entry.getIn([<span class="hljs-string">"data"</span>, <span class="hljs-string">"body"</span>]);

          <span class="hljs-keyword">return</span> h(<span class="hljs-built_in">window</span>.ReactMarkdown, {}, bodyText);
        },
      });

      <span class="hljs-built_in">window</span>.CMS.registerPreviewTemplate(<span class="hljs-string">"pages"</span>, DocPreview);
    </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>The final step is to add support for Github Flavored Markdown, so we can do tables, strikethrough text and other things. The <code>window.ReactMarkdown</code> component takes a <code>plugin</code> prop that lets you augment it with different plugins, one which is <a target="_blank" href="https://github.com/remarkjs/remark-gfm"><code>remark-gfm</code></a>. Unfortunately that package doesn't have a UMD build for us to use, it's only available in CJS (CommonJS) format. There is a tool called Browserify that takes CJS modules and converts them to modules that can be used by the browser. But it's a command line tool that you'd add to your tooling. Which, we don't have here. Thankfully someone turned it into a web service we can use! https://wzrd.in takes any npm package and tries to run it through Browserify and serves up the built package for you. First time it's run it takes a while, but after that it's cached and then it's much faster. Here's the link for the Browserified version of remark-gfm: https://wzrd.in/standalone/remark-gfm@1.0.0. Add that to a script tag and you'll have the plugin available at <code>window.remarkGfm</code>. Add that as a plugin to react-markdown and you're done!</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Netlify CMS Example<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://unpkg.com/netlify-cms@^2.10.0/dist/netlify-cms.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://unpkg.com/react-markdown@5.0.3/react-markdown.min.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://wzrd.in/standalone/remark-gfm@1.0.0"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
      <span class="hljs-keyword">const</span> DocPreview = createClass({
        <span class="hljs-attr">render</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
          <span class="hljs-keyword">const</span> bodyText = <span class="hljs-built_in">this</span>.props.entry.getIn([<span class="hljs-string">"data"</span>, <span class="hljs-string">"body"</span>]);

          <span class="hljs-keyword">return</span> h(<span class="hljs-built_in">window</span>.ReactMarkdown, { <span class="hljs-attr">plugins</span>: [<span class="hljs-built_in">window</span>.remarkGfm] }, bodyText);
        },
      });

      <span class="hljs-built_in">window</span>.CMS.registerPreviewTemplate(<span class="hljs-string">"pages"</span>, DocPreview);
    </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Try writing some markdown jazzed up with a bit of gfm syntax to make sure everything works.</p>
<p>That's it. Hope this guide was helpful!</p>
<p><span>(Header photo by <a href="https://unsplash.com/@richygreat?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Richy Great</a> on <a href="https://unsplash.com/s/photos/github?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a>)</span></p>
]]></content:encoded></item><item><title><![CDATA[Switching to Another GraphQL Client in RedwoodJS]]></title><description><![CDATA[RedwoodJS ships with Apollo Client as its default GraphQL client. With the 0.22.0 release of Redwood it's now possible to use another GraphQL client if you want. The key is the new <GraphQLHooksProvider> component where you can plug in whatever useQu...]]></description><link>https://tlundberg.com/switching-to-another-graphql-client-in-redwoodjs</link><guid isPermaLink="true">https://tlundberg.com/switching-to-another-graphql-client-in-redwoodjs</guid><category><![CDATA[RedwoodJS]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[Apollo GraphQL]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Mon, 18 Jan 2021 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634193910081/FCxjsY3N4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>RedwoodJS ships with Apollo Client as its default GraphQL client. With the 0.22.0 release of Redwood it's now possible to use another GraphQL client if you want. The key is the new <code>&lt;GraphQLHooksProvider&gt;</code> component where you can plug in whatever <code>useQuery</code> and <code>useMutation</code> hooks you want, as long as they have the correct function signature.</p>
<p>By default when you create a new RedwoodJS app this is what you get in your <code>index.js</code> file:</p>
<pre><code class="lang-jsx">ReactDOM.render(
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">FatalErrorBoundary</span> <span class="hljs-attr">page</span>=<span class="hljs-string">{FatalErrorPage}</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">AuthProvider</span> <span class="hljs-attr">client</span>=<span class="hljs-string">{netlifyIdentity}</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"netlify"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">RedwoodProvider</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Routes</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">RedwoodProvider</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">AuthProvider</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">FatalErrorBoundary</span>&gt;</span></span>,
  <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'redwood-app'</span>)
)
</code></pre>
<p>The interesting bit is <code>&lt;RedwoodProvider&gt;</code>. Looking at the source for Redwood we see this:</p>
<pre><code class="lang-js"><span class="hljs-keyword">export</span> { RedwoodApolloProvider <span class="hljs-keyword">as</span> RedwoodProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/RedwoodApolloProvider'</span>
</code></pre>
<p>and this:</p>
<pre><code class="lang-tsx">import {
  ApolloProvider,
  ApolloClientOptions,
  ApolloClient,
  InMemoryCache,
  useQuery,
  useMutation,
} from '@apollo/client'

// Other imports...

const ApolloProviderWithFetchConfig: React.FunctionComponent&lt;{
  config?: Omit&lt;ApolloClientOptions&lt;InMemoryCache&gt;, 'cache'&gt;
}&gt; = ({ config = {}, children }) =&gt; {
  const { uri, headers } = useFetchConfig()

  const client = new ApolloClient({
    cache: new InMemoryCache(),
    uri,
    headers,
    ...config,
  })

  return &lt;ApolloProvider client={client}&gt;{children}&lt;/ApolloProvider&gt;
}

export const RedwoodApolloProvider: React.FunctionComponent&lt;{
  graphQLClientConfig?: Omit&lt;ApolloClientOptions&lt;InMemoryCache&gt;, 'cache'&gt;
  useAuth: () =&gt; AuthContextInterface
}&gt; = ({ graphQLClientConfig, useAuth, children }) =&gt; {
  return (
    &lt;FetchConfigProvider useAuth={useAuth}&gt;
      &lt;ApolloProviderWithFetchConfig config={graphQLClientConfig}&gt;
        &lt;GraphQLHooksProvider useQuery={useQuery} useMutation={useMutation}&gt;
          &lt;FlashProvider&gt;{children}&lt;/FlashProvider&gt;
        &lt;/GraphQLHooksProvider&gt;
      &lt;/ApolloProviderWithFetchConfig&gt;
    &lt;/FetchConfigProvider&gt;
  )
}
</code></pre>
<p>So <code>&lt;RedwoodProvider&gt;</code> is a renamed export of <code>&lt;RedwoodApolloProvider&gt;</code> that wrapps the <code>&lt;ApolloProvider&gt;</code> context around its children, and passes <code>useQuery</code> and <code>useMutation</code> from <code>@apollo/client</code> to <code>&lt;GraphQLHooksProvider&gt;</code>.</p>
<p>The new powerful thing is that we can remove <code>&lt;RedwoodProvider&gt;</code> from our code and do what it does on our own instead — and that gives us the ability to pass in other <code>useQuery</code> and <code>useMutation</code> hooks from some other GraphQL client. For Apollo Client it's super easy. (It's almost as if Redwood was built for usage with Apollo Client 😜) All you have to do is import <code>useQuery</code> and <code>useMutation</code> and pass them straight into <code>&lt;GraphQLHooksProvider&gt;</code>. For any other graphql client you are probably going to have to write some adapter code to make it all work.</p>
<p>The other thing we need to do is to create our graphql client. And the client will need to know what headers to send and what url to talk to. For this we have the <code>useFetchConfig()</code> hook. Again, it's super straightforward to use with Apollo Client, but should be fairly easy to use with your client of choice as well.</p>
<p>This is an example of how it can be done when wiring up <a target="_blank" href="https://github.com/nearform/graphql-hooks">graphql-hooks</a></p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> useQueryAdapter = <span class="hljs-function">(<span class="hljs-params">query, options</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> useQuery(print(query), options)
}

<span class="hljs-keyword">const</span> useMutationAdapter = <span class="hljs-function">(<span class="hljs-params">query, options</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> useMutation(print(query), options)
}

<span class="hljs-keyword">const</span> GraphqlHooksClientProvider = <span class="hljs-function">(<span class="hljs-params">{ children }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> { <span class="hljs-attr">uri</span>: url, headers } = useFetchConfig()

  <span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> GraphQLClient({ url, headers })

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">ClientContext.Provider</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{client}</span>&gt;</span>{children}<span class="hljs-tag">&lt;/<span class="hljs-name">ClientContext.Provider</span>&gt;</span></span>
  )
}

ReactDOM.render(
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">FatalErrorBoundary</span> <span class="hljs-attr">page</span>=<span class="hljs-string">{FatalErrorPage}</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">AuthProvider</span> <span class="hljs-attr">client</span>=<span class="hljs-string">{netlifyIdentity}</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"netlify"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">FetchConfigProvider</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">GraphqlHooksClientProvider</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">GraphQLHooksProvider</span>
            <span class="hljs-attr">useQuery</span>=<span class="hljs-string">{useQueryAdapter}</span>
            <span class="hljs-attr">useMutation</span>=<span class="hljs-string">{useMutationAdapter}</span>
          &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">FlashProvider</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">Routes</span> /&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">FlashProvider</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">GraphQLHooksProvider</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">GraphqlHooksClientProvider</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">FetchConfigProvider</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">AuthProvider</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">FatalErrorBoundary</span>&gt;</span></span>,
  <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'redwood-app'</span>)
)
</code></pre>
<p>The adaptors for the hooks are simple. Only change we had to do was to transform the graphql queries that come as GQL ASTs in to plain strings. We use the <code>print</code> function for this. Setting up the client using <code>useFetchConfig()</code> is also easy, just have to rename <code>uri</code> to <code>url</code> for graphql-hooks to be happy.</p>
<p>You can see a full implementation in this GitHub repo: https://github.com/Tobbe/redwood-graphql-hooks (But there really isn't much more to it than what I've shown here.)</p>
<p>So, why do we have to let Redwood know about our <code>useQuery</code> and <code>useMutation</code> hooks in the first place? <code>useQuery</code> is used internally by Redwood with Cells, in its <code>withCellHOC</code>. <code>useMutation</code> technically wouldn't be necessary. But having it there allows the generators to generate code that runs and is valid. Without it, generated code like this would never be valid: <code>import { useMutation, useFlash } from '@redwoodjs/web'</code>. (That line is from the <code>EditNameCell.js.template</code> file.)</p>
<p><span>(Header photo by <a href="https://unsplash.com/@armand_khoury?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Armand Khoury</a> on <a href="https://unsplash.com/s/photos/graph?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a>)</span></p>
]]></content:encoded></item><item><title><![CDATA[Using RedwoodJS to download protected files from an Amazon S3 bucket]]></title><description><![CDATA[Recently I had to download files from a RedwoodJS Function (AWS Lambda serverless function). The files contained sensitive information, so I needed to host them somewhere where I could control who could download them. Easiest for me was to put them i...]]></description><link>https://tlundberg.com/using-redwoodjs-to-download-protected-files-from-an-amazon-s3-bucket</link><guid isPermaLink="true">https://tlundberg.com/using-redwoodjs-to-download-protected-files-from-an-amazon-s3-bucket</guid><category><![CDATA[RedwoodJS]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Amazon S3]]></category><category><![CDATA[lambda]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Sat, 28 Nov 2020 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634193898541/vi_pd5QRj.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently I had to download files from a RedwoodJS Function (AWS Lambda serverless function). The files contained sensitive information, so I needed to host them somewhere where I could control who could download them. Easiest for me was to put them in an Amazon AWS S3 Bucket, and then create an IAM policy to give a single user read-only access to the files.</p>
<p>Here's a step-by-step guide or tutorial on how I did it.</p>
<h2 id="aws-s3-bucket">AWS S3 Bucket</h2>
<p>The first step is going to be to set up our storage. Go to https://aws.amazon.com/ and sign in to the AWS Management Console (or create an account if you don't have one already).</p>
<p><img src="https://tobbe.github.io/assets/aws_signin.png" alt="Screenshot of AWS Management drop-down item and signup button" /></p>
<p>Choose to sign in as "Root user" and then you'll find S3 under "Storage" to the left. Here's a direct link that you might be able to use https://s3.console.aws.amazon.com/s3/home?region=us-east-1. Click "Create bucket" and give it a unique name. All other options can be left with their default values, so just scroll down and click "Create bucket"</p>
<p><img src="https://tobbe.github.io/assets/aws_new_bucket.png" alt="Truncated screenshot showing creation of new bucket" /></p>
<p>Now that you have a new bucket created, it's time to set up a user with a policy that lets it access the files in the bucket. In the upper left corner of the screen you click on "Services" and then you can search for "iam" and click the first and only result to go to "Identity and Access Management". </p>
<p><img src="https://tobbe.github.io/assets/aws_services_iam.png" alt="Screenshot showing the menu option to got to IAM" /></p>
<p>Click "Policies" in the left menu and then the blue "Create policy" button up top.</p>
<p><img src="https://tobbe.github.io/assets/aws_create_policy_button.png" alt="Screenshot of blue Create policy button" /></p>
<p>When creating your new policy you should select the "S3" service, choose the "GetObject" Read action, add the ARN (Amazon Resource Name) for the files you want to give access to by typing in the name of your newly created bucket and clicking "Any" on the object name to allow access to all files in the bucket, and finally you can leave the "Request conditions" as it is.</p>
<p>This is what it looked like for me when I created the policy</p>
<p><img src="https://tobbe.github.io/assets/aws_visual_policy.png" alt="Screenshot of visual policy editor" /></p>
<p>Click "Review policy" (bottom right), give your policy a name, like "example-secure-bucket-tlundberg-com-policy" and finally click the blue "Create policy" button.</p>
<p>Now, with the policy created, we'll go ahead and create a user that we'll connect this policy to. So click "Users" in the left menu and the the blue "Add user" button. Give the user a name, like "example-secure-bucket-tlundberg-com-user" and select the "Programatic access" access type. This will let you use this user with the AWS SDK and APIs.</p>
<p><img src="https://tobbe.github.io/assets/aws_new_user.png" alt="New user screenshot" /></p>
<p>Now click "Next: Permissions", and on the new page you chose "Attach existing policies directly" (it's the last box in the row up top). Filter the list of policies to find the one you created previously and click the checkbox next to it.</p>
<p><img src="https://tobbe.github.io/assets/aws_new_user_policy.png" alt="New user policy screenshot" /></p>
<p>This was the last step where we had to do anything, so just click "Next: Tags", "Next: Review" and finally "Create user".</p>
<p>Now it's important you save the secret access key for this user, because you will not be able to find it again. Either download the .csv file, or copy/paste the values to somewhere safe. After you've saved the credentials you can close the "Add user" wizard. Should you lose the Access key ID and/or the Access Secret you can come back here to generate new ones.</p>
<p>You're finally done with AWS and it's time to move on to RedwoodJS stuff.</p>
<h2 id="redwoodjs-function">RedwoodJS Function</h2>
<p>Create a new Redwood project if you don't have one already, and add a new file in <code>api/src/functions/</code>. I called mine <code>s3download.js</code>. Add this code to the file</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> AWS = <span class="hljs-built_in">require</span>(<span class="hljs-string">'aws-sdk'</span>)

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> s3 = <span class="hljs-keyword">new</span> AWS.S3({
    <span class="hljs-attr">accessKeyId</span>: process.env.S3_KEY_ID,
    <span class="hljs-attr">secretAccessKey</span>: process.env.S3_SECRET,
    <span class="hljs-attr">region</span>: <span class="hljs-string">'us-east-1'</span>,
  })

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> s3Result = <span class="hljs-keyword">await</span> s3
      .getObject({
        <span class="hljs-attr">Bucket</span>: process.env.S3_BUCKET,
        <span class="hljs-attr">Key</span>: <span class="hljs-string">'my_s3_file.txt'</span>,
      })
      .promise()

    <span class="hljs-keyword">const</span> s3File = s3Result.Body.toString(<span class="hljs-string">'utf-8'</span>)

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'file contents'</span>, s3File)
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>, <span class="hljs-attr">body</span>: <span class="hljs-string">'File downloaded successfully'</span> }
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(err)
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>, <span class="hljs-attr">body</span>: err.message }
  }
}
</code></pre>
<p>We have to install the <code>aws-sdk</code> package to use this function. Do that by running <code>yarn workspace api add aws-sdk</code>.</p>
<p>As you can see the function uses three environment variables. Under no circumstances should you put your bucket credential directly in your source code, because it will (probably) be pushed to GitHub, where other people could see it. Even if it's a private repo it's just yet anohter place your credentials could be compromised. So instead we use environment variables. Let's add them to the <code>.env</code> file</p>
<pre><code><span class="hljs-attr">S3_KEY_ID</span>=KYSA9EKSDFHCK88194UK
<span class="hljs-attr">S3_SECRET</span>=dsli5lsi92klsjdf120sdfGsiSDDKSKS3sdflkjS
<span class="hljs-attr">S3_BUCKET</span>=example-secure-bucket-tlundberg-com
</code></pre><p>Those are just made-up values to show you what it should look like, use your values instead. Also make sure the <code>region</code> in the code matches the region you have your bucket in.</p>
<p>Before we can test this we need to upload the <code>my_s3_file.txt</code> file to the s3 bucket. Easiest is to just drag-and-drop it in the AWS web interface. So go ahead and do that. </p>
<p>It's finally time to try it all out! Run <code>yarn rw dev</code> and you should be able to access your function at http://localhost:8911/s3download. You should see the message "File downloaded successfully" in your browser, and if you switch over to your console you should see the content of the file.</p>
<p>Did it work? Congratulations! All the AWS setup is not easy. Thankfully it's pretty easy to use the SDK once everything is set up correctly. </p>
<p>Thanks for reading! </p>
<p><span>(Header photo by <a href="https://unsplash.com/@jdjohnston?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Jessica Johnston</a> on <a href="https://unsplash.com/s/photos/buckets?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a>)</span></p>
]]></content:encoded></item><item><title><![CDATA[Redwood Pagination Tutorial]]></title><description><![CDATA[This tutorial will show you one way to implement pagination in an app built using RedwoodJS. It builds ontop of the official RedwoodJS tutorial and I'll assume you have a folder with the code from the tutorial that you can continue working on. (If yo...]]></description><link>https://tlundberg.com/redwood-pagination-tutorial</link><guid isPermaLink="true">https://tlundberg.com/redwood-pagination-tutorial</guid><category><![CDATA[RedwoodJS]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[full stack]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Tue, 15 Sep 2020 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634193904461/JkPJ-Qga0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This tutorial will show you one way to implement pagination in an app built using RedwoodJS. It builds ontop of <a target="_blank" href="https://redwoodjs.com/tutorial/welcome-to-redwood">the official RedwoodJS tutorial</a> and I'll assume you have a folder with the code from the tutorial that you can continue working on. (If you don't, you can clone this repo: https://github.com/thedavidprice/redwood-tutorial-test)</p>
<p><img src="https://tobbe.github.io/assets/redwood_pagination.png" alt="Screenshot of pagination" /></p>
<p>The screenshot above shows what we're building. See the pagination at the bottom? The styling is up to you to fix.</p>
<p>So you have a blog, and probably only a few short posts. But as the blog grows bigger you'll soon need to paginate all your posts. So, go ahead and create a bunch of posts to make this pagination worthwhile. We'll display five posts per page, so begin with creating at least six posts, to get two pages.</p>
<p>We'll begin by updating the SDL. To our Query type a new query is added to get just a single page of posts. We'll pass in the page we want, and when returning the result we'll also include the total number of posts as that'll be needed when building our pagination component.</p>
<pre><code class="lang-graphql"># api/src/graphql/posts.sdl.js

type PostPage {
  posts: [Post!]!
  count: Int!
}

type Query {
  postPage(page: Int): PostPage
  posts: [Post!]!
  post(id: Int!): Post!
}
</code></pre>
<p>You might have noticed that we made the page optional. That's because we want to be able to default to the first page if no page is provided.</p>
<p>Now we need to add a resolver for this new query to our posts service.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// api/src/services/posts/posts.js</span>

<span class="hljs-keyword">const</span> POSTS_PER_PAGE = <span class="hljs-number">5</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> postPage = <span class="hljs-function">(<span class="hljs-params">{ page = <span class="hljs-number">1</span> }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> offset = (page - <span class="hljs-number">1</span>) * POSTS_PER_PAGE

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">posts</span>: db.post.findMany({
      <span class="hljs-attr">take</span>: POSTS_PER_PAGE,
      <span class="hljs-attr">skip</span>: offset,
      <span class="hljs-attr">orderBy</span>: { <span class="hljs-attr">createdAt</span>: <span class="hljs-string">'desc'</span> },
    }),
    <span class="hljs-attr">count</span>: db.post.count(),
  }
}
</code></pre>
<p>So now we can make a GraphQL request (using <a target="_blank" href="https://www.apollographql.com/">Apollo</a>) for a specific page of our blog posts. And the resolver we just updated will use <a target="_blank" href="https://www.prisma.io/">Prisma</a> to fetch the correct posts from our database.</p>
<p>With these updates to the API side of things done, it's time to move over to the web side. It's the BlogPostsCell component that makes the gql query to display the list of blog posts on the HomePage of the blog, so let's update that query. </p>
<pre><code class="lang-javascript"><span class="hljs-comment">// web/src/components/BlogPostsCell/BlogPostsCell.js</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> QUERY = gql<span class="hljs-string">`
  query BlogPostsQuery($page: Int) {
    postPage(page: $page) {
      posts {
        id
        title
        body
        createdAt
      }
      count
    }
  }
`</span>
</code></pre>
<p>The <code>Success</code> component in the same file also needs a bit of an update to handle the new gql query result structure.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// web/src/components/BlogPostsCell/BlogPostsCell.js</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Success = <span class="hljs-function">(<span class="hljs-params">{ postPage }</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> postPage.posts.map(<span class="hljs-function">(<span class="hljs-params">post</span>) =&gt;</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">BlogPost</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{post.id}</span> <span class="hljs-attr">post</span>=<span class="hljs-string">{post}</span> /&gt;</span></span>)
}
</code></pre>
<p>Now we need a way to pass a value for the <code>page</code> parameter to the query. To do that we'll take advantage of a little RedwoodJS magic. Remember from the tutorial how you made the post id part of the route path (<code>&lt;Route path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" /&gt;</code>) and that id was then sent as a prop to the BlogPostPage component? We'll do something similar here for the page number, but instead of making it a part of the url path, we'll make it a url query string. These, too, are magically passed as a prop to the relevant page component. And you don't even have to update the route to make it work! Let's update <code>HomePage.js</code> to handle the prop.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// web/src/pages/HomePage/HomePage.js</span>

<span class="hljs-keyword">const</span> HomePage = <span class="hljs-function">(<span class="hljs-params">{ page = <span class="hljs-number">1</span> }</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">BlogLayout</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">BlogPostsCell</span> <span class="hljs-attr">page</span>=<span class="hljs-string">{page}</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">BlogLayout</span>&gt;</span></span>
  )
}
</code></pre>
<p>So now if someone navigates to https://awesomeredwoodjsblog.com?page=2 (and the blog was actually hosted on awesomeredwoodjsblog.com), then <code>HomePage</code> would have its <code>page</code> prop set to <code>"2"</code>, and we then pass that value along to <code>BlogPostsCell</code>. If no <code>?page=</code> query parameter is provided <code>page</code> will default to <code>1</code></p>
<p>Going back to <code>BlogPostsCell</code> there is one me thing to add before the query parameter work.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// web/src/components/BlogPostsCell/BlogPostsCell.js</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> beforeQuery = <span class="hljs-function">(<span class="hljs-params">{ page }</span>) =&gt;</span> {
  page = page ? <span class="hljs-built_in">parseInt</span>(page, <span class="hljs-number">10</span>) : <span class="hljs-number">1</span>

  <span class="hljs-keyword">return</span> { <span class="hljs-attr">variables</span>: { page } }
}
</code></pre>
<p>The query parameter is passed to the component as a string, so we need to parse it into a number.</p>
<p>If you run the project with <code>yarn rw dev</code> on the default port 8910 you can now go to http://localhost:8910 and you should only see the first five posts. Change the URL to http://localhost:8910?page=2 and you should see the next five posts (if you have that many, if you only have six posts total you should now see just one post).</p>
<p>The final thing to add is a page selector, or pagination component, to the end of the list of posts to be able to click and jump between the different pages.</p>
<p>Generate a new component with <code>yarn rw g component Pagination</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// web/src/components/Pagination/Pagination.js</span>

<span class="hljs-keyword">import</span> { Link, routes } <span class="hljs-keyword">from</span> <span class="hljs-string">'@redwoodjs/router'</span>

<span class="hljs-keyword">const</span> POSTS_PER_PAGE = <span class="hljs-number">5</span>

<span class="hljs-keyword">const</span> Pagination = <span class="hljs-function">(<span class="hljs-params">{ count }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> items = []

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-built_in">Math</span>.ceil(count / POSTS_PER_PAGE); i++) {
    items.push(
      <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">li</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{i}</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Link</span> <span class="hljs-attr">to</span>=<span class="hljs-string">{routes.home({</span> <span class="hljs-attr">page:</span> <span class="hljs-attr">i</span> + <span class="hljs-attr">1</span> })}&gt;</span>
          {i + 1}
        <span class="hljs-tag">&lt;/<span class="hljs-name">Link</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span></span>
    )
  }

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Pagination<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span>{items}<span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Pagination
</code></pre>
<p>Keeping with the theme of the official RedwoodJS tutorial we're not adding any css, but if you wanted the pagination to look a little nicer it'd be easy to remove the bullets from that list, and make it horizontal instead of vertical.</p>
<p>Finally let's add this new component to the end of <code>BlogPostsCell</code>. Don't forget to <code>import</code> it at the top as well.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// web/src/components/BlogPostsCell/BlogPostsCell.js</span>

<span class="hljs-keyword">import</span> Pagination <span class="hljs-keyword">from</span> <span class="hljs-string">'src/components/Pagination'</span>

<span class="hljs-comment">// ...</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Success = <span class="hljs-function">(<span class="hljs-params">{ postPage }</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;&gt;</span>
      {postPage.posts.map((post) =&gt; <span class="hljs-tag">&lt;<span class="hljs-name">BlogPost</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{post.id}</span> <span class="hljs-attr">post</span>=<span class="hljs-string">{post}</span> /&gt;</span>)}

      <span class="hljs-tag">&lt;<span class="hljs-name">Pagination</span> <span class="hljs-attr">count</span>=<span class="hljs-string">{postPage.count}</span> /&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
  )
}
</code></pre>
<p>And there you have it! You have now added a functioning, but somewhat ugly, pagination to your redwood blog. One techincal limitation to the current implementation is that it doesn't handle too many pages very gracefully. Just imagine what that list of pages would look like if you had 100 pages! I'll write another blog post in the future with a more fully featured pagination component. </p>
<p>Most of the code in this tutorial was copy/pasted from the <a target="_blank" href="https://github.com/redwoodjs/example-blog">"Hammer Blog" RedwoodJS example</a></p>
<p>If you want to learn more about pagination with Prisma and Apollo they both have excelent docs on the topic. https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/pagination https://www.apollographql.com/docs/react/data/pagination/</p>
]]></content:encoded></item><item><title><![CDATA[Running specific tests in RedwoodJS core]]></title><description><![CDATA[Running the test suite for the RedwoodJS Framework is pretty straighforward. yarn install followed by yarn test is all that's needed. You will probably see some warnings about missing peer dependencies when installing, but those can safely be ignored...]]></description><link>https://tlundberg.com/running-specific-tests-in-redwoodjs-core</link><guid isPermaLink="true">https://tlundberg.com/running-specific-tests-in-redwoodjs-core</guid><category><![CDATA[RedwoodJS]]></category><category><![CDATA[Testing]]></category><category><![CDATA[TDD (Test-driven development)]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Sun, 21 Jun 2020 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634193927173/sSXsXHra4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Running the test suite for the RedwoodJS Framework is pretty straighforward. <code>yarn install</code> followed by <code>yarn test</code> is all that's needed. You will probably see some warnings about missing peer dependencies when installing, but those can safely be ignored.</p>
<p>Running all the tests takes a little while however, so when working on a specific feature it's somethimes helpful to be able to just run the tests for a specific package, or even just a specific test. So that's what I want to show here.</p>
<p>Let's say for example you want to run the tests for the <code>cli</code> package. First, go in to the cli directory; <code>cd packages/cli</code>. Then run all the tests by issuing <code>yarn test</code> (or <code>yarn jest</code>).</p>
<p>For more granularity you can run a single test-file by passing the path to it to jest. Here's the command and example output from running the page generator tests.</p>
<pre><code>$ yarn test src/commands/generate/page/__tests__/page.test.js
yarn run v1<span class="hljs-number">.22</span><span class="hljs-number">.4</span>
$ jest src/commands/generate/page/__tests__/page.test.js
 FAIL  src/commands/generate/page/__tests__/page.test.js (<span class="hljs-number">5.467</span>s)
  √ <span class="hljs-keyword">returns</span> exactly <span class="hljs-number">2</span> files (<span class="hljs-number">4</span>ms)
  × creates a page component (<span class="hljs-number">4</span>ms)
  × creates a page test
  × creates a page component (<span class="hljs-number">1</span>ms)
  × creates a page test (<span class="hljs-number">2</span>ms)
  √ creates a single-word route <span class="hljs-type">name</span> (<span class="hljs-number">1</span>ms)
  √ creates a camelCase route <span class="hljs-type">name</span> <span class="hljs-keyword">for</span> multiple word names (<span class="hljs-number">1</span>ms)
  √ creates a <span class="hljs-type">path</span> equal <span class="hljs-keyword">to</span> passed <span class="hljs-type">path</span>
</code></pre><p>To run just one of the testcases, open up the test-file in your code editor, find the test definition and add <code>.only</code> to it. E.g. changing it from <code>test('creates a page test', () =&gt; {</code> to <code>test.only('creates a page test', () =&gt; {</code>. Run the test file again, and only the selected test case will execute. You can add <code>.only</code> to however many tests you like, and all the selected tests will run. The other tests in the file will be skipped.</p>
<p>Example output when selecting three testcases to run</p>
<pre><code><span class="hljs-string">$</span> <span class="hljs-string">yarn</span> <span class="hljs-string">test</span> <span class="hljs-string">src/commands/generate/page/__tests__/page.test.js</span>
<span class="hljs-string">yarn</span> <span class="hljs-string">run</span> <span class="hljs-string">v1.22.4</span>
<span class="hljs-string">$</span> <span class="hljs-string">jest</span> <span class="hljs-string">src/commands/generate/page/__tests__/page.test.js</span>
 <span class="hljs-string">PASS</span>  <span class="hljs-string">src/commands/generate/page/__tests__/page.test.js</span> <span class="hljs-string">(5.598s)</span>
  <span class="hljs-string">√</span> <span class="hljs-string">creates</span> <span class="hljs-string">a</span> <span class="hljs-string">page</span> <span class="hljs-string">test</span> <span class="hljs-string">(3ms)</span>
  <span class="hljs-string">√</span> <span class="hljs-string">creates</span> <span class="hljs-string">a</span> <span class="hljs-string">page</span> <span class="hljs-string">test</span> <span class="hljs-string">(1ms)</span>
  <span class="hljs-string">√</span> <span class="hljs-string">creates</span> <span class="hljs-string">a</span> <span class="hljs-string">page</span> <span class="hljs-string">component</span> <span class="hljs-string">with</span> <span class="hljs-string">a</span> <span class="hljs-string">plural</span> <span class="hljs-string">word</span> <span class="hljs-string">for</span> <span class="hljs-string">name</span>
  <span class="hljs-string">○</span> <span class="hljs-string">skipped</span> <span class="hljs-string">returns</span> <span class="hljs-string">exactly</span> <span class="hljs-number">2</span> <span class="hljs-string">files</span>
  <span class="hljs-string">○</span> <span class="hljs-string">skipped</span> <span class="hljs-string">creates</span> <span class="hljs-string">a</span> <span class="hljs-string">page</span> <span class="hljs-string">component</span>
  <span class="hljs-string">○</span> <span class="hljs-string">skipped</span> <span class="hljs-string">creates</span> <span class="hljs-string">a</span> <span class="hljs-string">page</span> <span class="hljs-string">component</span>
  <span class="hljs-string">○</span> <span class="hljs-string">skipped</span> <span class="hljs-string">creates</span> <span class="hljs-string">a</span> <span class="hljs-string">single-word</span> <span class="hljs-string">route</span> <span class="hljs-string">name</span>
  <span class="hljs-string">○</span> <span class="hljs-string">skipped</span> <span class="hljs-string">creates</span> <span class="hljs-string">a</span> <span class="hljs-string">camelCase</span> <span class="hljs-string">route</span> <span class="hljs-string">name</span> <span class="hljs-string">for</span> <span class="hljs-string">multiple</span> <span class="hljs-string">word</span> <span class="hljs-string">names</span>
  <span class="hljs-string">○</span> <span class="hljs-string">skipped</span> <span class="hljs-string">creates</span> <span class="hljs-string">a</span> <span class="hljs-string">path</span> <span class="hljs-string">equal</span> <span class="hljs-string">to</span> <span class="hljs-string">passed</span> <span class="hljs-string">path</span>

<span class="hljs-attr">Test Suites:</span> <span class="hljs-number">1</span> <span class="hljs-string">passed,</span> <span class="hljs-number">1</span> <span class="hljs-string">total</span>
<span class="hljs-attr">Tests:</span>       <span class="hljs-number">6</span> <span class="hljs-string">skipped,</span> <span class="hljs-number">3</span> <span class="hljs-string">passed,</span> <span class="hljs-number">9</span> <span class="hljs-string">total</span>
<span class="hljs-attr">Snapshots:</span>   <span class="hljs-number">0</span> <span class="hljs-string">total</span>
<span class="hljs-attr">Time:</span>        <span class="hljs-number">7.</span><span class="hljs-string">306s</span>
<span class="hljs-string">Ran</span> <span class="hljs-string">all</span> <span class="hljs-string">test</span> <span class="hljs-string">suites</span> <span class="hljs-string">matching</span> <span class="hljs-string">/src\\commands\\generate\\page\\__tests__\\page.test.js/i.</span>
<span class="hljs-string">Done</span> <span class="hljs-string">in</span> <span class="hljs-number">9.</span><span class="hljs-string">28s.</span>
</code></pre>]]></content:encoded></item><item><title><![CDATA[Installing rsync on Windows]]></title><description><![CDATA[This guide will help you install rsync on Windows 10. It is assumed that you already have Git for Windows installed.

If you didn't already know, Git for Windows and its Git Bash environment is built using msys2, but it doesn't include all the binari...]]></description><link>https://tlundberg.com/installing-rsync-on-windows</link><guid isPermaLink="true">https://tlundberg.com/installing-rsync-on-windows</guid><category><![CDATA[Windows]]></category><category><![CDATA[Git]]></category><category><![CDATA[Bash]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Mon, 15 Jun 2020 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634193888096/fRxBW33_2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This guide will help you install rsync on Windows 10. It is assumed that you already have <a target="_blank" href="https://gitforwindows.org/">Git for Windows</a> installed.</p>
<p><img src="https://tobbe.github.io/assets/rsync_windows.png" alt="Screenshot of rsync running in PowerShell" /></p>
<p>If you didn't already know, Git for Windows and its Git Bash environment is built using <a target="_blank" href="http://msys2.org">msys2</a>, but it doesn't include all the binaries from that project. One of the binaries that exists, but that isn't included, is rsync. So what we need to do is to download the msys2 rsync binary, and place it somewhere Git Bash can find it.</p>
<ol>
<li>Go to http://repo.msys2.org/msys/x86_64/ and download the latest version of rsync (not rsync2). At the time of this writing that is rsync-3.1.3-1-x86_64.pkg.tar.xz</li>
<li>Extract the downloaded archive. I'm using Total Commander with a .xz plugin, but 7-zip is also a great option. Download and install from https://www.7-zip.org/ if you need to.</li>
<li>Copy the contents of the extracted archive (sub-folders and all) to where you have Git for Windows installed. For me that's <code>C:\Program Files\Git\</code>. (The archive contains a <code>\usr</code> folder, and so does the git installation directory. What you want is for everything inside of the <code>\usr</code> folder in the archive to end up in the <code>\usr</code> folder in the git installation directory, ultimately ending up with, among other files, <code>C:\Program Files\Git\usr\bin\rsync.exe</code>)</li>
</ol>
<p>That's it. You now have rsync installed. You can test your installation by opening up a Git Bash command line window and running <code>rsync --version</code>. You should see it print out version information.</p>
<p>Now, if you want to use rsync from the Windows Command Prompt, or from PowerShell, there is one more step.</p>
<p>Create a new <code>.bat</code> file with the following content (adjust the path to match your environment)</p>
<pre><code class="lang-batch">"C:\Program Files\Git\usr\bin\rsync.exe" %*
</code></pre>
<p>Name the file <code>rsync.bat</code> and place it somewhere in your %PATH%. I placed mine in <code>C:\Windows\</code>. Press <span class="nowrap"><kbd>Win</kbd> + <kbd>R</kbd></span> and enter <code>cmd</code>. In the Command Prompt window that you just launched, enter <code>rsync --version</code> and it will find your <code>.bat</code>-file and run it, passing all arguments (that's what <code>%*</code> does in the command above) off to your newly installed rsync.exe</p>
<p>The first three steps above are based on https://serverfault.com/questions/310337/using-rsync-from-msysgit-for-binary-files/872557#872557 where you can also find instructions for setting up Pageant for SSH, if that's something you need.</p>
<p>I hope this short tutorial was useful to you. Happy rsyncing!</p>
]]></content:encoded></item><item><title><![CDATA[Getting something new and fresh up]]></title><description><![CDATA[After having my old page, which was pretty much nothing, I found myself wanting to have a proper blog here. So I went from this

to what you see here right now. Quite the improvement, if I may say so myself!
For nostalgia's sake, I'll include that sa...]]></description><link>https://tlundberg.com/getting-something-new-and-fresh-up</link><guid isPermaLink="true">https://tlundberg.com/getting-something-new-and-fresh-up</guid><category><![CDATA[Meta]]></category><category><![CDATA[personal]]></category><dc:creator><![CDATA[Tobbe Lundberg]]></dc:creator><pubDate>Sat, 13 Jun 2020 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634193882828/P1BrWzb9T.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After having my old page, which was pretty much nothing, I found myself wanting to have a proper blog here. So I went from this</p>
<p><img src="https://tobbe.github.io/assets/tlundberg_com.png" alt="Screenshot of how my webpage used to look, with just a link to tetris" /></p>
<p>to what you see here right now. Quite the improvement, if I may say so myself!</p>
<p>For nostalgia's sake, I'll include that same link to Tetris right here. <a target="_blank" href="https://tobbe.github.io/tetris.html">Play Tetris</a></p>
<p>So, what should I do with this new blog of mine? The idea is like most every other blog on the Internet to just have a braindump. Whenever I find something interesting, or make something worth sharing, I will put it here. The first thing I want to share is how I got this page to where it is right now. So that will be my next blog post. Other ideas I have for the future is to write "addons" or "extensions" or whatever you want to call them, to the main <a target="_blank" href="https://redwoodjs.com/tutorial/welcome-to-redwood">RedwoodJS tutorial</a>. Something that picks up where that tutorial ends.</p>
]]></content:encoded></item></channel></rss>