Posts tagged code

Next.js and Tumblr as a CMS

Thought I would do a series of posts on moving my blog from a Tumblr-hosted and themed site to a Tumblr powered Next.js app.

You might reasonably ask, “Why would anyone want to do that when there are so many actual CMS options available?”.

In my case, it’s mostly sentimental. Way back when I was deciding whether to switch careers and try coding full time, I took a lot of freelance jobs converting PSDs to Tumblr themes and I’ve been using it ever since.

But, I finally wanted a little more control over my blog and a good excuse to experiment more with Next.js so I decided to mix it up just a little. Let’s dig into some of the decision making, adventures in data fetching, and general idiosyncrasies that come with using Tumblr as a CMS.


How To Set It Up

Starting at the beginning, the very first question was how to set everything up. Where should the site and the code live? How do you share the parts that need to be shared? It seems simple but there are a lot of options.

Keeping with the original Tumblr setup, I decided to create a new app for my blog instead of trying to integrate it with the rest of my site (which was already using Next.js). I wanted to use Vercel for hosting which meant I would just need to update my subdomain records once everything was up and running.

Since Tumblr templates are basically HTML files that include Tumblr’s custom blocks and variables, I’d never really shared much code between my blog and the rest of my site even though they look pretty similar. I had a small script to generate stylesheets but that was it. Using Next.js for both opened up the possibility of sharing React components, variables, and utilities in addition to CSS.

To figure out how to do that, I followed one article (6 Ways to Share React Components in 2020) to another (4 Git Submodules Alternatives You Should Know) and finally landed at Git subtree: the alternative to Git submodule for step-by-step instructions.

Any solution that required publishing components, individually or as a monorepo, seemed like overkill for a smallish, one dev project. Git subtrees provided a way to nest one repo as a subdirectory in others which was exactly what I wanted. I split out shared components like the header, footer, form elements, etc. into a separate repo, ran the commands from the tutorial above in my blog and site repos to hook it all up, and voila. No copying and pasting duplicate code between sections and no publishing to NPM or other third-parties.

So that’s the foundation, stay tuned for the next installment for data fetching and some actual code!

React Inner Image Zoom Version 3.0.0

React Inner Image Zoom version 3.0.0 went out earlier this week with a handful of bug fixes, test and build improvements, and one major change.

What’s the big thing to look out for? By popular demand, the imgAttributes prop was added to pass down (almost) any valid React img attributes in a single object instead of as individual props. That means scrSet, sizes, alt, and title are gone but in exchange you get all the data attributes and event handlers you could want. I haven’t submitted updated type definitions to DefinitelyTyped yet but I’ll try to get that done in the next few days.

This release also included a handy new Changelog so I would be remiss not include the official record here:

Changed

  • Replaced srcSet, sizes, alt, and title props with imgAttributes to set the original image’s attributes.
  • Show close button when moveType is set to “drag” on all breakpoints.
  • Switched from setTimeout to onTransitionEnd to check that zoomed image has finished fading out.

Added

  • This handy CHANGELOG.

Fixed

  • Added stopPropagation on touchmove to prevent events below fullscreen modal.

If you run into any bugs, please let me know in the GitHub issues.

Vue Inner Image Zoom v2

As promised, now that migrating is easier I’ve updated my Vue Inner Image Zoom component to support Vue 3. If you’re still on Vue 2 and want to use the component, just make sure to install it as vue-inner-image-zoom@1.1.1.

I also updated the demos site to remove the lazy loading example since vue-lazyload isn’t compatible with Vue 3 (I’m open to any suggestions for replacements) and switched from vue-slick-carousel to Swiper both for compatibility and because it’s my preferred carousel library.

If I broke anything and you run into any new bugs, please report them on the GitHub issues page.

The Mysterious Case of Emotion and “exports is not defined”

Thought I’d share a bug I ran into a while back that sent me on a Poirot style investigation full of red herrings and unexpected culprits.

This is tangentially related to my recent page speed woes at work. We’d started using Emotion for CSS-in-JS in our component library and, combined with lazy and conditional component loading, it helped with some of the “Reduce unused CSS” warnings we were seeing in Lighthouse.

So adding Emotion as a styling option in our main codebase seemed like an obvious choice. We’d already installed @emotion/core (v10) when we started importing from our component library which meant it should be a quick, two-step process: 1) running yarn add @emotion/babel-preset-css-prop and 2) adding it to our babel.config.js presets after @babel/preset-react. I followed those steps, ran Webpack, and promptly got the error “ReferenceError: exports is not defined”.

Weird.

Continue Reading

Moving a Node App from Google Compute Engine to DigitalOcean

This site can

There’s nothing like trying to deploy a minor site update and having your VM hang while SSHing 75% of the time, give up mid-build the other 25%, and then fall apart completely when you try to restart it. Since all I remember about setting up a Google Compute Engine Node instance in the first place is that it took me a while and wasn’t particularly intuitive, this seemed like a good opportunity to try a new host. I’m running a commercial Next.js site with a custom server so after browsing around DigitalOcean seemed like the best option.

And it was actually pretty painless!

Of course, it would be even quicker with a step-by-step guide so here’s a reference for my future self or anyone else who wants it.

My guide for migrating a Node app to DigitalOcean (this may vary a little depending on your site):

  1. Sign up for DigitalOcean and create a Droplet using NodeJS 1-Click App. Poking around the DigitalOcean Community questions, I ran into some talk about additional installations for production but my Droplet came with Nginx, PM2, and optional Let’s Encrypt for SSL out of the box so those discussions may be a little out of date.
  2. Add the new IPv4 address to MongoDB Atlas. DigitalOcean provides instructions for getting started with your Droplet but I wound up skipping around a bit. Since I already had a MongoDB database set up, I completely ignored their section on creating a new one. To use an existing database, just remember to log in to MongoDB Atlas and add your new IP address (using port 22) to the IP Address List under Network Access.
  3. Clone your repo. Using ssh root@0.0.0.0 (replacing the zeros with your IP address), ssh into your Droplet and clone your repo.
  4. Copy environmental variable files, assets, etc. From outside your Droplet, use scp -r /path/on/computer root@0.0.0.0:/path/on/droplet (replacing the paths and zeros) to copy any necessary files that weren’t included in the repo.
  5. Start your app. Back in the Droplet, start with a good old yarn install. Then, for a Next.js site, run yarn build followed by sudo NODE_ENV=production -u nodejs pm2 start server.js to start a custom server. This is also a good time to jump back to the DigitalOcean instructions for shutting down the starter app with sudo -u nodejs pm2 delete hello and saving your PM2 process list with sudo -u nodejs pm2 save.
  6. Create an Nginx configuration. I added “read up on everything Nginx can do” to my todo list but for now I have a pretty simple config with basic asset caching in my /etc/nginx/sites-enabled/ directory. You can view the file in this Gist but keep in mind it’s pretty tailored to my SSL setup. Whether you alter my example, the DigitalOcean default, or start fresh, make sure to 1) switch the root setting to something like /root/your-site/.next/pages; for Next.js and 2) update the proxy_pass to use your app’s port (8443 in my example). Restart with sudo systemctl restart nginx.
  7. Point your domain to the new IPv4 address. How you do this will depend on your DNS host but you’ll want to complete the migration by updating your A name to point to your new IP address.

There it is in seven steps. The first run through took a bit of trial and error but hopefully this will speed things up next time.

Preloading Images in a Responsive, WebP World

Watch out for giant images

It’s time for another Google Core Web Vitals themed TIL. This one concerns image loading. Specifically, huge, above the fold image preloading.

The site I work on uses a lot of super wide to full width hero images. We’d done some previous optimizations around serving images in WebP format (Serve images in modern formats) and using picture and srcset for responsive images (Properly size images). I won’t go into details since those topics already have a ton of tutorials that explain things better than I ever could. In fact, Smashing Magazine alone has you covered with Using WebP Image Format Today and Responsive Images Done Right: A Guide To picture And srcset.

Preload critical assets to improve loading speed.

The one downside to our earlier work? It made things a little more confusing when we started looking into preloading images.

Preloading assets that aren’t actually used isn’t particularly helpful (Chrome will even warn you if you do it!). If you’re serving a WebP image but preloading the fallback or preloading a mobile image on a desktop browser, you aren’t getting any benefits out of it.

Let’s say you have a picture block that selects an image based on browser support, screen resolution, and breakpoint:

<picture>
  <source srcset="large-image.webp, large-image-2x.webp 2x" media="(min-width: 768px)" type="image/webp" />
  <source srcset="large-image.jpg, large-image-2x.jpg 2x" media="(min-width: 768px)" />
  <source srcset="small-image.webp, small-image-2x.webp 2x" type="image/webp" />
  <img src="fallback-image.jpg" srcset="small-image.jpg, small-image-2x.jpg 2x" />
</picture>

The first question is how to handle different image formats. Looking at the Can I use entries for Webp image formats and preload, it seems pretty unlikely for a browser to support preloading but not WebP images. And according to a quick aside in this Addy Osmani article on hero images, you can use the type="image/webp" attribute with a link tag to specify that you want to preload a WebP image.

Next, we need to deal with screen sizes and resolutions. In addition to type, the link tag supports media and imagesrcset attributes which should look very similar to the media and srcset attributes on our source and img tags. The main difference is that you have to be more explicit with your breakpoints since the browser won’t be choosing the best image itself.

Putting that all together, you would add this code to your head tag to preload the images from the example above:

<link rel="preload" as="image" href="large-image.webp" media="(min-width: 768px)" imagesrcset="large-image.webp, large-image-2x.webp 2x" type="image/webp" />
<link rel="preload" as="image" href="small-image.webp" media="(max-width: 767px)" imagesrcset="small-image.webp, small-image-2x.webp 2x" type="image/webp" />

If you prefer an example with real images, check out this Pen on Codepen. Just click on Settings and HTML to view the preloading the code.