WebP fallback on Safari

Having images that resize correctly on mobile is great, but for some reason, they weren’t loading on Safari mobile—only the alt text would appear. This means either the image isn’t loading or the browser can’t render the image. Since the images are delivered by Contentful’s CDN, and the images loaded fine on desktop, I could easily cross that off my list. Next, I considered the image format. Is it possible that in 2019, Safari (let alone Safari mobile) doesn’t support WebP? I realize now that the title is a big spoiler, but it’s true. Images weren’t loading on Safari mobile nor desktop Safari.

Initially, I looked for an auto option for Contentful’s image format parameter, but none existed. Then, after a quick Google, I came across srcset—an HTML attribute I’ve been waiting a long time to find a use for. Amongst other things, this attribute lets you specify an ideal image format that might not be supported by all browsers, so if the format isn’t support, the browser falls back to another specified format. The actual implementation might look like this:

  <source type='image/webp' srcset='myimage.webp'>
  <img src='myimage.png'>

The source tag also lets you specify different images for varying media queries, but I don’t need those—yet. For the time being, this works perfectly well for me.

Responsive images

While resizing the browser, I noticed that my images weren’t resizing as well. I immediately knew the issue, but it made me laugh to think that such a simple, mindless setting that you set once and never revisit can sometimes slip through the cracks—img { width: 100% }.

Typically, to go along with this setting, you’d have a max-width on the image to prevent the it from scaling beyond its original width, but fortunately, Contentful’s image API also lets you reference an asset’s dimensions. Instead of setting the `max-width` to 100% and having my retina-ready image scale beyond its retina size, I set it to `${ / 2}px`, which is a mouthful, but does the job. Now, my images are always true retina, and they scale with the viewport.


Continuing to make progress, little by little, I added dates to articles. In Contentful, this meant creating a new date field on the article’s content model, which is easy enough, but surprisingly, you can’t set the default date to the current date. This adds an extra step to the authoring flow, but it’s not the end of the world.

On the frontend, I added a <time> element under the article template’s title. This element takes a datetime attribute for a machine-readable format of the date, which lets you use any human-readable format for the element’s contents. For date formatting, I’m using date-fns, a lightweight date utility library that I also use in my app, Cushion.

The latest date-fns release only works with integers or Date objects, so first I needed parse Contentful’s dates. Previously, you could use the date-fns/parse method on any string, and it would handle it. Now, you need to pass the format of the date that you’re parsing. For Contentful’s dates, this looks like parse(, "yyyy-MM-dd'T'HH:mmXXX", new Date). The last argument here is a “backup date” required by date-fns to provide context to the original date, in case you’re parsing a date format with only 2-digit years. In my case, since the Contentful date format is standard, I can blindly pass the method a new Date object simply to prevent a missing argument error. Now that I have a parsed date, I can format the date into a human-readable format. For now, I’m using MMMM do, yyyy, which outputs to “November 3rd, 2019” for today’s date.

Along with the dates on the site itself, I also added the dates to my RSS feed by included a date property when calling addItem in the @nuxt/feed configuration. This method takes a Date object, so I needed to parse Contentful’s date again, but no need to format this time.

When I added all the new date fields to my Contentful entries, I noticed that the order changed. Because I wasn’t specifying an order when retrieving the entries, they used the updatedAt date, so when I set the date field on all the entries, that changed the order. Now that I have my own date field, the fix was as easy as setting the order property to ',-sys.createdAt', which uses my new field, and falls back to the created timestamp.

  1. Actually, the native Date constructor can parse Contentful’s timestamps on its own without the need for date-fns’s parse method. Simply use new Date(


I was all ready to publish the previous post, but noticed that the images weren’t showing up. After inspecting the rendered HTML, I realized Contentful’s documentToHtmlString method doesn’t include a renderer for images (or assets). This method is part of the @contentful/rich-text-html-renderer package, which takes an entry’s payload—a serious amount of JSON—and maps it to rendered HTML. Looking at the documentation, I found that you can configure custom renderers to handle nodes that aren’t mapped, like assets.

For now, I set up a dead-simple renderer that assumes the asset is an image, and renders a <img> tag with the title as the alt attribute. After deploying the update, I ran Lighthouse again, and noticed that my score went down. There was a new alert indicating that I’m not using modern image formats, like webp. On a whim, I googled “contentful webp”, and discovered that Contentful has a very nice asset API, which includes specifying a format, like webp. I appended ?fm=webp to my renderer, that was that. My Lighthouse score is back up, and I’m serving webp images by default. The modern web is a beautiful thing.

Lighthouse, Now build env vars, and Nuxt manifest

In digging around Now, I discovered a Lighthouse integration, which automatically audits your site builds for performance and accessibility. I noticed that my still bare website didn’t have a perfect score out of the box, so I dug deeper and found that both my site and my PWA manifest were missing a title and description. I’m certain I set these as META_TITLE and META_DESCRIPTION environment variables when realizing that Now doesn’t carry over the process.env.npm_package_ env vars, so I’m not crazy, but it turns out my assumption that setting the top-level env vars in the now.json sets them for both build and runtime is incorrect. By removing my build.env configuration in that file also removed my env vars on build, leaving me with a blank title and description. I copy/pasted my env var configuration to appear twice as env and build.env, and it worked. Great, but now I guess I need to map my env vars twice whenever I add a new one—not so great.

I went to check the Lighthouse report for my new build, but it simply said “No report available”. This consistently happened with other builds, so I decided to run Lighthouse in the Chrome dev tools instead. I still had almost a perfect score, but my PWA manifest was still missing a title, description, and short name (used for mobile app icons). I dug into the @nuxt/pwa module, and found that they default these values to the process.env.npm_package_ env vars that Now doesn’t recognize. Simply setting these properties to my META_ env vars worked as expected, and I now had a valid PWA manifest file. I ran Lighthouse again (on desktop), and was closer than I’ll probably ever be, with 100s across the board except for a lone 99 on performance—for a 340ms “Max Potential First Input Delay”.

Lighthouse desktop audit (11/01/2019)

Researching this for a bit, I discovered that it’s par for the course with SSR websites. If I have more time to dig deeper and find a potential fix, I will, but for now, I moved on. Lighthouse also has an option to audit your website on mobile, so I ran that report, and was delighted with a flawless victory and animated fireworks on a dark theme. This felt like I truly accomplished something (rather than made sure all my env vars were set)—thanks, Lighthouse team.

Lighthouse mobile audit (11/01/2019)

Title and article links

To continue to make progress, I took care of some low-hanging fruit by linking up the article titles to individual routes and adding an h1 to the site. Now, you can link to an article and return to the root by clicking the site title. These are so small that you probably wouldn’t even factor them into the scope of a site, but it feels good to recognize them as yet another step forward.

RSS feed

I was so busy setting up the site backend that I forgot to generate an RSS feed for it. (Thanks, @rectangular, for the reminder). Surprisingly, or maybe not surprisingly, this wasn’t easy. After realizing I couldn’t create the RSS feed using a template in Nuxt, I installed @nuxt/feed, which automatically generates the feed by setting a handful of options and an async function in the nuxt.config.js file.

Since I’d need to fetch entries from Contentful in the async function, I refactored the plugin, so I could require the relative file in the config file. This worked in local dev, but crashed my site on Now. After an hour of troubleshooting and research, I realized that when Now converts the site to serverless, it doesn’t carry over any files besides the config file and those generated in the public folder—you have to specify which server files to include by adding { "serverFiles": ["plugins/**"] } to the now.json file’s builds object.

Okay. This worked, but now my RSS feed wouldn’t display any entries. At first, I thought this might be another access token issue, but the undefined environment variable I referenced in the feed clued me to revisit env vars. It turns out Now has two spots to list environment variables in the now.json file—a top-level env object and another one at build.env. All my variables were set on build.env, which only exposes these variables during the build phase, not runtime—makes sense. Listing them in the top-level env objects exposes them for both build and runtime.

Now, I have a working RSS feed, but still don’t have routes for individual articles, so those links in the feed currently 404. I’ll hook that up next.

Contentful preview API hiccup

Before getting into any serious code, I’ve already experienced several hiccups so far. This most recent one involves setting up the site to use Contentful’s preview API in my local dev environment, which allows me to see draft content. After swapping out my access tokens, I could only get 401 errors.

In retrospect, I blame this hiccup solely on myself for neglecting to thoroughly read the manual. I have a tendency to only glance at documentation before diving in. In doing so, I only updated my accessToken to use the preview API token, but missed the part about specifying the host as, too. This totally makes sense, and I’m embarrassed I even reached out to support about this. For what it’s worth, I appreciate Stripe’s use of prefixes in their tokens to specify the environment (e.g., pk_test_), so a host option isn’t even necessary.

  1. I learned after the fact that the host option is necessary here because Contentful’s delivery API serves from the CDN while the preview API doesn’t.

Hello World

I started redesigning my personal site, and thought it’d be fun to document it along the way, like I did with my freelancing app, Cushion. Rather than fall into the trap of writing serious, heavily-edited, long-form blog posts, I’ll try my best to keep these short, frequent, and to-the-point. As a rule of thumb, I should be able to write a post in less than 15 minutes.

In the spirit of posting early and often, I wanted to get this up and running from the very beginning, so I’m publishing this first post without even starting on the design—the only progress I’ve made so far is setting up the backend. I’ve been out of the loop when it comes to modern stacks, so I’m taking this opportunity to try new things. I’m experimenting with Contentful as the CMS, Nuxt.js for server-side rendering, and Now for hosting. I’m not strongly tied to any of these, so there’s a chance I might swap one out if I don’t love it—in fact, I already nixed Netlify after a cumbersome first deploy. Who knows—I might return to PHP on an FTP host if I continue to find that everything easy is hard again.

I’m also utilizing this time to try out several other tools beyond the stack that powers the site. For design, I’m giving Figma another shot after playing it safe with Sketch all these years. For prototyping, I’m using CodeSandbox—I truly want this site to jumpstart my creativity, so there might be times when I want to iterate on animations or interactions that have no real purpose besides being fun. Along with a jolt to my creativity, I’m looking to push myself technically. Rather than falling back to the same CSS or JS I’ve been writing for years, I’m eager to try new APIs using this site as my sandbox.

I already feel like I’ve written too much. Keep an eye on this space, and keep me active if I start to slack.

-Jonnie, @destroytoday

Burning Out and Finding Stability

Earlier this year, I experienced my first panic attack. It was easily the worst experience of my life. I’d like to think that I’m a mentally strong person, especially after being independent for so long, but this broke me. The perfect storm of uncertainty, pressure, and overwhelming responsibility led me to completely spiral out of control and transformed me into a fragile, easily-triggered version of myself. I found a way through it, but even months later, I’m still recovering.

My first burnout happened in 2014. I was a full-time freelancer and naively said yes to every job that came my way. Back then, I viewed tight deadlines as a challenge—a great story on top of a great portfolio piece. This time, however, I took on too much work. For two months straight, I worked all day, all night, and every weekend. I crashed each night, then woke up and did it again. After crawling across the finish line, I felt mentally and physically defeated. I needed a break, but I also needed to recognize that I actually have limits.

After taking a proper vacation, I extended my time off to build Cushion—a freelancing tool that could help me avoid burnouts by visually forecasting my schedule and income. I desperately needed an app like this, so I designed it specifically for myself. With Cushion’s help, I was able to establish a comfortable balance between freelancing and working on Cushion itself. I absolutely loved this rhythm—freelance for one month, so I could afford to spend two months on Cushion. Instead of seeing time as money, I saw money as time. How much time could a freelance gig buy me, so I could keep working on Cushion.

Eventually, Cushion became my full-time job. Working on my own product day-to-day and supporting myself with its income was the bootstrapper’s dream. I always pictured myself reaching this point, then maintaining it until I retired—riding into the sunset. As time went on, however, I felt like I was limiting myself and Cushion. Wearing every hat meant wearing only one hat at a time. When I was designing, I wasn’t coding. When I worked on the front-end, I wasn’t working on the backend. Cushion had so much potential and I didn’t want to hold it back, so I hired Larry—a much better backend dev than me. By taking a giant weight off my shoulders, we were able to make progress in parallel, and I could focus on what I’m actually good at—design and front-end. With Larry, we were able to ship twice as fast and launch features that I only dreamed about. At the same time, our costs snowballed overnight. I was completely new to paying salaries, employer taxes, healthcare contributions, and workers comp. My comfortable rhythm instantly became a free fall.

For several years, we were able to make it work, but this past year was especially hard. Instead of the one month freelancing, two months on Cushion, I freelanced three days a week to support both of us, then worked on Cushion every other waking moment. I also cut my salary to the lowest I could legally pay myself, in order to keep payroll low. It was really difficult at first, but then we found stability with a longterm client who got us through the year. Going into 2019, I finally felt like we were on the right path. We had a few ideas that could take Cushion to the next level and enough of a financial “cushion” to lead us into summer. Then January happened.

When you run your own business, tax season hits hard—especially when there are surprises. I remember the moment I learned how much we owed. I remember how the numbness that I typically have for news like this was completely gone. I remember the thick skin that I developed from years of freelancing becoming a thin paper shell. I found myself spiraling. Our 7-month cushion instantly became less than two. To make matters worse, we under-budgeted and over-promised on our latest client gig, which would only buy us two extra months for at least six months of work. I felt mentally paralyzed. For weeks, I’d wake up in a panic around 3am and just lie there—my mind spinning—until I got out of bed hours later. During the day, I felt tightness in my chest and couldn’t fully breathe. My body was in a constant state of agitation.

I tried everything to calm myself down. I switched from coffee to tea. I tried rhodiola drops and CBD. I started meditating and using breathing apps. I went to acupuncture and took sleep aids at night. I spent as much time as I could with friends to distract myself from the thoughts that would cause me to spiral. Some things helped, but most of them didn’t because I was still in the same situation—running out of time, running out of money, while people relied on me to come through. Then, on Cushion’s 5-year anniversary, I experienced my first panic attack.

I like to think that our bodies can communicate with us in their own way. This panic attack was my body screaming at me to eject from anything causing the stress and anxiety in my life. I needed stability now. I can vividly recall my body taking the wheel and instantly knowing what to do, as if I were hypnotized at the snap of my own fingers. No one could’ve convinced me otherwise because nothing was worth what I was going through. As soon as my health was at stake, there was no longer a decision.

Over the next couple weeks, I took several steps towards a healthier, more stable life—no matter how drastic the changes. I told our freelance client that we couldn’t finish what we were hired to do, regardless of how humiliating it was to admit. We gave the remaining money back and apologized for throwing a giant wrench in their plans. I told Larry how unhealthy I felt and that I needed a break from the month-to-month sprint. Larry already knew because he could visibly see it on me, and he was ready for a change, too. I told a few friends what I was going through and that I needed to find stability. At first, it felt embarrassing, as if I failed or gave up, but a close friend of mine reminded me that after exhausting myself by sprinting uphill for so many years, I deserve to run downhill for once.

Several weeks later, I found stability—by joining Stripe. It’s still shocking to think about, but I know, without a shadow of a doubt, that it was the right decision—for my own sustainability. I need to be able to work without worrying where the money is coming from. I need to be able to switch off at night and actually enjoy weekends. I need to be able to take real time off without being the sole person on-call. Coming off the most nightmarish time of my life, Stripe has been the comforting environment that I’ve so desperately needed.

With Stripe’s history of hiring founders, they know exactly what I’ve been going through and were incredibly supportive throughout the process. They understand that Cushion is a big part of me, so I’m still able to run it as the longterm side project that it started as. My work environment isn’t changing either—I’ll continue to work out of the comfort of own my studio space in Brooklyn. The most exciting part, considering my freelance work, is that I’m joining the legendary Site team to work on, which couldn’t have been a better fit for me. For years, I’ve marveled at the quality of work that comes out of Stripe, and now I get to be a part of it.

Note: If you overwork yourself and feel like you’re burning out, stop what you’re doing and find stability—whatever you’re working on isn’t worth your health. Don’t feel the need to “hustle” because some people glorify it. Doing good work while living a healthy life is much more respectable.