⏱️ 18 Minutes

📝 I have put a lot of work into the newest version of this site and I want to tell you about it.

Home page screenshot

In the age of social media and hosted blogs, personal websites don’t get the love they deserve. Sure, writing developer based content on a site like medium.com or dev.to will get more eyeballs on it than something you make on your own, but there is something extremely satisfying in owning every pixel on a site with your name on it. I saw this Tweet the other day and it seemed fitting to how I think about personal sites.

If we are entering the era of Web3 (which is Soylent in this Tweet, ewww) and it’s ethos is trying to revive the techno-optimisim and ownership of the Web 1.0 era, what better way to embody this than with an over-engineered personal site? Also, I just really love Greek salad (I live in Astoria after all, you walk outside on Ditmars and they force Greek salad on you).

As an aside, I find all of Web3 and blockchain stuff disgusting. I have no money in any of that stuff because I object on environmental issues and that most of it reeks of pump and dump schemes. That said, I am happy for anything that pushes the web pendulum back in the direction of user’s owning pixels attached to their name.

Guiding Principals

  1. Dynamic enough to reflect me
  2. Fetch data at compile time
  3. Do a lot and do it fast
  4. Updates should be triggered by tools/apps I use instead of manual intervention

Technology

Would be a technology blog if I didn’t nerd out about tools before describing the cool things I did?

Gatsby

I moved this site from Lektor to Gatsby last year while procrastinating on writing a talk about Redux. At my last job, I was working almost exclusively in Python + Flask, so using a Python based generator made sense at the time. But now, at my current job, I’m working a lot more in JavaScript and I wanted my site to be more dynamic. I also knew that we were planning on migrating our customer facing sites to a static site generator in the near future and Gatsby was high up on the list of possibilities for us to use. By picking it up for my personal site, I could learn how to use it beforehand and know exactly what we needed to do.

Turns out that this was a fantastic bet on the future. We are porting around 10 sites at work to Gatsby and because I had learned the basics of Gatsby months previous on a single site, I was able to dive into the more complex topics to enable us to port many sites at once.

While doing some of these more complex things in a private repo at work, I have been able to take aspects that I have learned there and ported them conceptually back to my personal site. I will make note of these learnings as I go. It has also given me a test bed to try out Gatsby upgrades before doing them at work. For example, I tried to update this site to Gatsby 4 and ran into a bunch of issues and will be holding off on pushing it at work until it’s fixed for this site.

Is Gatsby Any Good?

Oh I have complicated feelings about this! Like all frameworks, Gatsby has made a lot of trade offs to accomplish it’s goal. I would say that Gatsby’s core goal is to be the Django/Wordpress of static site generators that is capable of magical incremental builds. Let’s look at how they accomplish that goal.

  1. Your data store for all data while building the site is coming from GraphQL
  2. They have a very through lifecycle API model that allows for a massive amount of plugins to be added to your site with minimal configuration.
  3. The data in this GraphQL store is used to intelligently determine what pages are needed to be rebuilt when building a page with an established cache.

These three points are all fine on their own but, as they say, the devil is in the details. All of your data that you use must be in the GraphQL store, so you must hook into the lifecycle APIs to do this, which forces this data fetching to be divorced from your application components. This is actually very similar to the comparison of Redux vs React Hooks.

Furthermore, how these GraphQL queries are executed are very flawed. Instead of running them when generating the HTML after the JS has compiled, they are instead extracted with a Babel transformation and transpiled to a JSON file that is read when creating the HTML. This seems like a minor distinction but it means that your GraphQL queries are unable to take arguments outside of the template system AND cannot be shared between components because that makes them too hard to extract.

Compare this to Next.js, which does all of it’s data querying as plain JavaScript functions that can be shared freely. It doesn’t provide a shared data store that all data needs to go into BUT it is able to do the same incremental building that Gatsby does all the same. If I were to do this over, I would probably use Next.js, but I’m fine with Gatsby for now.

That said, there are a lot of things that Gatsby does right:

  • It’s plugin ecosystem is massive and will save you a lot of time from the jump
  • The image component is extremely good, far better than Next.js’s
  • The documentation is fantastic

Other Technologies

Here’s some quick hot takes on some other libraries used in this update:

  • Tailwind CSS: I think it’s a little barebones compared to other CSS frameworks. That said, it’s theming system and naming conventions made it easy to have consistent spacing, colors, fonts, etc. I do fear that updating styles in the future will be annoying because I will have forgotten it’s naming conventions, but that’s Future Eli’s problem!
  • Styled Components: I love Styled Components! It has a very intuitive API and I have become a complete CSS in JS fan boi. My personal site does not have a lot of reusable components so I didn’t get to use some of it’s more complex features, but I would be excited to in a future job!
  • twin.macro: This is the glue for Tailwind CSS to Styled Components and it’s good! It allowed me to avoid putting Tailwind classes right on my components, which I just could not stomach. It also makes the dev build fail when you provide an invalid Tailwind class.
  • mdx: What cool little language! It’s Markdown that you can import React components into! Such a simple concept executed perfectly.

Feelings API v2

Feelings widget on my homepage

I just had to show off a little bit and add that chart

Last year, I made an API to submit diary entries from the Daylio app on my phone. I am happy to announce that I am continuing to use this functionality and I think posting them on my site + Twitter has encouraged me to keep doing it (I’m at ~700 days in a row).

In the original version of this, I was pulling the data from the API at runtime using React Query. React Query is a fine library and I love using it, but this is a static site so pulling it at runtime is far slower than doing it at compile time.

As it turns out, moving this data into Gatsby was very easy using gatsby-source-custom-api. All I needed was the following in my gatsby-config.js.

{
  resolve: 'gatsby-source-custom-api',
  options: {
    url: 'https://api.eligundry.com/api/feelings',
    rootKey: 'feelings',
    schemas: {
      feelings: `
        time: String!
        mood: String!
        activities: [String]
        notes: [String!]
      `,
    },
  },
},
  

This allows me to fetch my latest diary entry like so:

import { useStaticQuery, graphql } from 'gatsby'
import parseISO from 'date-fns/parseISO'
interface Feeling
  extends Omit<GatsbyTypes.UseLatestFeelingsQuery['feelings'], 'time'> {
  time: Date
}
export default function useLatestFeelings(): Feeling {
  const entry = useStaticQuery<GatsbyTypes.UseLatestFeelingsQuery>(
    graphql`
      query UseLatestFeelings {
        feelings {
          time
          mood
          activities
          notes
        }
      }
    `
  )
  return {
    ...entry.feelings,
    // @ts-ignore
    time: parseISO(entry.feelings.time),
  }
}

Super easy! In order for this site to be built daily, I added a call to a Netlify build webhook to the API endpoint I upload the diary entries to. This build will also generate a dedicated RSS feed that I use IFTTT to post the entry to Twitter with. This, in turn, also updates all the other Gatsby sources, which allows things like my Goodreads shelves and Listening widgets to update naturally.

Speaking of those widgets…

Goodreads Shelves

Goodreads bookshelves on my home page

Similar to the feelings widget, I had a Goodreads widget of what I was reading on the previous version of my website. It was using a React component to pull the data from the Goodreads API. And this was fine until Goodreads announced it was sunsetting it’s API. Not to worry, I realized it was pretty easy to pull this data from their HTML and submitted a suggestion to this component to pull data from the HTML that was eventually adopted. But even then, I ran into more issues:

  1. Pulling this data at runtime is very slow, even slower now that I’m pulling HTML instead of (relatively) compact XML
  2. Images were at max resolution, much larger than they needed to be given the size of them on the page
  3. The component used a hosted instance of CORS Anywhere on Heroku that would get rate limited frequently

I threw up my hands and realized that I needed to make a custom Gatsby source to pull this data at compile time. At first, I had it living as a script inside my site, but then I started making custom Gatsby source packages at work and decided to extract it into my first NPM package!

npm: @eligundry/gatsby-source-goodreads

One of the added benefits of moving this to compile time is that Gatsby is able to download the cover images and plug it into it’s image plugin and resize the images to be small and load progressively as the page loads! It’s a win win win! Eventually, I might learn how to extract my progress reading these books and include them in this widget, but that is for another day.

Listening Homepage Widget

Last.fm cover on my homepage

It is very easy to pull a Last.fm collage of your top albums. A bunch of services exist for this. Why bring this up?

Well, I elevated it in 2 ways:

  1. I’m pulling the image at build time into Gatsby so that it’s image component can optimize it
  2. I’m overlaying the artist name, album and the number of scrobbles with an image map

Number 2 is the thing I want to brag about. I used gatsby-source-lastfm to pull my last 1000 scrobbles and then I whipped up a React component to overlay the info onto the image using an HTML image map (in the year 2021, it’s not often you get the chance to use an image map legitimately, so I was tickled pink to do so).

The only downside to this approach is that it’s not 100% accurate. I’ve noticed the following things:

  1. There is often lag between the data that the image provider is using and what the Last.fm API returns.
  2. If I have the same amount of scrobbles for a given album, the sorting in the image != the sorting in the API. I think the cover providers might be sorting on an internal Last.fm ID and not the album name or artist. I do not want to include the ID in this bundle so I’m fine with this being inaccurate.

This homepage widget also has my current playlist of the season. Dynamic and updates without me having to do anything special. I want to add a third widget here of the shows that I’m going to that will come from Songkick’s API. I had this somewhat working using their ical exports, but the data wasn’t clean enough for me to happy. I have applied for an API key, maybe I will make a Gatsby source for this as well.

Design

I am not a designer. I have tried to design things in the past and I’m just terrible at it. My brain just doesn’t work that way. I tried to design the previous iteration of this site and it was bad. No contrast, terrible sizing & spacing and it was just lacking pizzaz.

Going into this redesign, I was convinced that I was going to spend money on a template for this site. I really wanted to pay a premium for a good template. But, I couldn’t find a paid one that was simple enough for all my pages or really felt like “me”.

That said, I did find a free minimal blog theme from Tailwind Toolbox by Amrit Nagi that I fell in love with and adapted to have the best elements from my previous site shown off in the best possible light. Thanks Amrit, I made sure to buy you some coffees.

Fluid Background

After cribbing the template for this site, I was feeling good about the design, but it still needed something more to really show off my personality. It took me a few weeks and some endless scrolling on Twitter before I came across this:

This only works natively in Chrome + Edge, but I have been able to polyfill it so it works in Safari + Firefox, thought it’s slightly more buggy than the Chrome version.

I love that it’s super dynamic, very cute and fits the theme of my site perfectly! I converted it into a React component with a button in the bottom right part of the screen that can regenerate the pattern. It’s even responsive to my dark mode toggle, speaking of which…

import React, { useState } from 'react'
import tw, { styled, theme } from 'twin.macro'
import { FaSync } from 'react-icons/fa'
import Helmet from 'react-helmet'
import { isSSR } from '../utils/env'
// Borrowed from https://codepen.io/georgedoescode/pen/YzxrRZe
const generateSeed = () => Math.random() * 10000
const workletURL =
  'https://unpkg.com/@georgedoescode/fluid-pattern-worklet@1.0.1/worklet.bundle.js'
const FancyBackground: React.FC = () => {
  const [seed, setSeed] = useState(
    !isSSR && window?.CSS?.paintWorklet ? generateSeed() : undefined
  )
  return (
    <>
      <Helmet bodyAttributes={{ 'data-fancy-background': !!seed }} />
      {seed && (
        <>
          <Canvas seed={seed} />
          <RefreshButton
            onClick={() => setSeed(generateSeed())}
            title="Regenerate the background pattern"
            arial-label="Regenerate the background pattern"
            className="gtm-background-refresh-button"
          >
            <FaSync />
          </RefreshButton>
        </>
      )}
    </>
  )
}
export const FancyBackgroundPaintWorkletRegistration: React.FC = () => {
  return (
    <script
      dangerouslySetInnerHTML={{
        __html: `
          if (CSS && CSS.paintWorklet && CSS.paintWorklet.addModule) {
            CSS.paintWorklet.addModule('${workletURL}')
          }
        `,
      }}
    />
  )
}
const Canvas = styled.div<{ seed: number }>`
  --fluid-pattern-seed: ${(props) => props.seed};
  --fluid-pattern-bg-color: ${theme`colors.siteBackground`};
  --fluid-pattern-color-1: ${theme`colors.yellow`};
  --fluid-pattern-color-2: ${theme`colors.primary`};
  --fluid-pattern-color-3: ${theme`colors.red`};
  --fluid-pattern-color-4: ${theme`colors.green`};
  .dark & {
    --fluid-pattern-bg-color: ${theme`colors.black`};
  }
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-image: paint(fluidPattern);
  z-index: -100000;
  ${tw`print:hidden`}
`
const RefreshButton = styled.button`
  font-size: 2rem;
  z-index: 10;
  ${tw`
    fixed 
    bottom-4 
    right-4 
    rounded-full 
    bg-primary 
    hover:bg-primaryLite
    text-white 
    p-4
    sm:text-base
    sm:p-2
    print:hidden
  `}
`
export default FancyBackground

Dark Theme

You have to be sensitive to users that like dark color schemes! Luckily, TailwindCSS makes it very easy to style your site for dark mode. With that approach, I made a React provider that provides the ability to the user to toggle light to dark mode with it’s default value being the result of the media query (prefers-color-scheme: dark), which the OS sets in MacOS and probably Windows.

Github Integrations

Github is super flexible so I embedded some features into this site that depend on it.

Utterances

Years ago, I saw a talk at Brooklyn.js where someone presented echo-chamber-js. It’s a joke JavaScript library that presents a comment form that just saves the comment to the commenter’s local storage so that the comment just shows up for them. The thought behind it is that comments are mainly mean or spam, you really don’t want to read them but you have to offer them, otherwise they will email you and you really don’t want that. That talk has always stuck with me and when it came time to implement comments for my site, I decided to adapt it for React.

But, confusingly, even to me, I decided to wire up the form to my personal API so I could capture the comments and read them if I wanted. It was a pretty cool endpoint that I did a good job writing, but it really served no purpose.

Fast forward to earlier this year. I get an email from someone who read the Feelings API blog post and wanted to post kind words about it. Turns out, I had recently moved my API to a different domain but didn’t update my code to post to it. This reader went out of their way to tell me that it was broken and that they liked my article. I felt bad and decided that needed to change!

I decided to switch my commenting system to utterances. Utterances has a really cool design: It stores all it’s comments as Github comments in the issues tab of repos. It will automatically create issues on each page a user leaves a comment on and thread all replies under that. This means that a user needs a Github account to comment, which narrows my audience to just developers BUT that also acts as an anti-spam mechanism and is much nicer for me to moderate than Disqus.

Github File Embeds

In a previous blog post, I wanted to show a Gist like file embed of a file from the Github main site. Unfortunately, Github does not provide this out of the box. Luckily, a service called EmGithub provides exactly this. They don’t have a React component for this so I made one:

const GitHubFileEmbed: React.FC<Props> = ({ fileURL }) => {
  const [expanded, setExpanded] = useState(false)
  const scriptTarget = useRef<HTMLDivElement>()
  const prefersDark = usePrefersDarkMode()
  useEffect(() => {
    if (!scriptTarget.current || scriptTarget.current.innerHTML) {
      return
    }
    // emgithub uses document.write, which doesn't work well with React post
    // render. postscribe patches document.write to document.appendChild,
    // which makes it work with this effect.
    const query = new URLSearchParams({
      target: fileURL,
      style: prefersDark ? 'tomorrow-night' : 'github-gist',
      showBorder: 'on',
      showLineNumbers: 'on',
      showFileMeta: 'on',
    })
    // @ts-ignore
    import('postscribe').then(({ default: postscribe }) =>
      postscribe(
        scriptTarget.current,
        `<script async cross-origin="anonymous" src="https://emgithub.com/embed.js?${query.toString()}"></script>`
      )
    )
    /* eslint-disable-next-line consistent-return */
    return function cleanup() {
      if (scriptTarget.current) {
        scriptTarget.current.innerHTML = ''
      }
    }
  }, [!!scriptTarget.current, fileURL, prefersDark])
  return (
    <>
      <EmGitHubContainer
        ref={scriptTarget as React.MutableRefObject<HTMLDivElement>}
        expanded={expanded}
      />
      {!expanded && (
        <ExpandButtonContainer>
          <button
            onClick={() => setExpanded(true)}
            data-gtm="emgithub-file-expaded"
            data-gtm-emgithub-file-url={fileURL}
            type="button"
          >
            Expand File
          </button>
        </ExpandButtonContainer>
      )}
    </>
  )
}

Alright, time for me to 🤓 nerd out! EmGithub uses a JavaScript API called document.write, which is something that I didn’t know about until recently (for good reason). For those that don’t know, document.write is an ancient JavaScript API that allows you to programatically write HTML to a document as if the document was a file like stream. Because of how the browser’s rendering engine works, this file “closes” on DOMContentLoaded and any further writes will fail or clear the page. This is completely incompatible with how React works.

Fortunately, lots of ads work this way and some ad tech developers created a nifty library called postscribe, which patches document.write to operate using “normal” DOM operations that can be called anytime. I integrated that into this component and it works perfectly. It even responds to my dark theme toggle!

Git Based Last Modified Timestamps

Google uses the <lastmod> value if it’s consistently and verifiably (for example by comparing to the last modification of the page) accurate.

- Google’s sitemap.xml documentation

This one is just for me and Google, I swear, but I really want to show it off.

For customer facing sites where you are trying to have the best Google ranking for organic traffic, it’s best practice for your pages to have a last modified timestamp in the sitemap.xml and schema.org data. This tells Google when it was last updated and more frequently updated content rises to the top of the search results (in theory). This has been a big focus for me at work this year as we transition from developer maintained sites to sites generated from a CMS + Gatsby.

To pull a last modified timestamp from a CMS is very easy, as it’s just a database record. For a static site generated from Markdown, that timestamp is harder to pull. I could have easily added a field to my blog posts frontmatter that I manually updated when a blog post was updated. But, that is error prone and only works for blog posts. No, I needed something better if I wanted this.

Then, I cocked my head slightly to the side and looked at this a little differently and realized: Git is a database with last modified timestamps. So, I went about creating a super hacky script that hooks into Gatsby’s onCreateNode lifecycle event to directly add the latest commit timestamp for any given node. This required a lot of hard coding path rules and is in no way open sourceable, but I really just want to tell people about it!

/* eslint-disable no-console, vars-on-top, no-var, no-shadow, block-scoped-var */
import path from 'path'
import simpleGit, { LogResult } from 'simple-git'
import { CreateNodeArgs } from 'gatsby'
import dateCompareDesc from 'date-fns/compareDesc'
import util from 'util'
const git = simpleGit()
const addGitLastModifiedToNode = async (args: CreateNodeArgs) => {
  const {
    node,
    getNode,
    actions: { createNodeField },
  } = args
  const addCommitFieldsToNode = (commit: null | LogResult['latest']) => {
    createNodeField({
      node,
      name: 'latestCommit',
      value: {
        date: commit?.date ?? null,
        message: commit?.message ?? null,
        hash: commit?.hash ?? null,
      },
    })
  }
  try {
    if (node.internal.type === 'Mdx') {
      var fileNode = getNode(node.parent)
      var log = await git.log({
        // @ts-ignore
        file: (node.internal?.fileAbsolutePath ??
          fileNode.absolutePath) as string,
      })
      if (log.latest) {
        addCommitFieldsToNode(log.latest)
      }
      return
    }
    if (node.internal.type === 'SitePage') {
      if (
        node.path?.startsWith?.('/blog/') ||
        node.path?.startsWith?.('/talk/')
      ) {
        return
      }
      const paths: string[] = [node.component as string]
      switch (node.path) {
        case '/':
          paths.push(
            path.join('src', 'components', 'Home'),
            path.join('src', 'components', 'Listening')
          )
          break
        case '/feelings':
          paths.push(path.join('src', 'components', 'Daylio'))
          break
        case '/resume':
          paths.push(path.join('src', 'components', 'Resume'))
          break
        default:
          break
      }
      var logs = await Promise.all(paths.map((file) => git.log({ file })))
      const latestCommit = logs
        .map((log) => log.latest)
        .filter((commit) => !!commit)
        .sort((a, b) =>
          dateCompareDesc(new Date(a.date), new Date(b.date))
        )?.[0]
      if (latestCommit) {
        addCommitFieldsToNode(latestCommit)
      }
      return
    }
    if (node.internal.type === 'File') {
      const log = await git.log({ file: node.absolutePath as string })
      if (log && log.latest) {
        addCommitFieldsToNode(log.latest)
      }
    }
  } catch (e) {
    if (node.internal.type === 'SitePage' || node.internal.type === 'File') {
      addCommitFieldsToNode(null)
    }
    console.error(
      'could not attach modified data from git for node',
      util.inspect(
        {
          e,
          node,
          log,
          fileNode,
        },
        { showHidden: false, depth: null }
      )
    )
  }
}
export default addGitLastModifiedToNode

Performance

I would be remiss if I blogged all about these features and my static site without bringing up performance. This blog post, if nothing else, is a marketing tool for my next employer to hire me on the basis of.

The primary consumer of this site will be on desktop, on that I have a perfect 💯!

Google Page Speed Insights Desktop 100 score

Sadly, on mobile, this is another story and all I have is a 71 😧

Google Page Speed Insights Mobile 71 score

This is going to be hard to get up. The biggest issue is that I have a lot of JavaScript powering all the functionality on my site. I can try to lazy load some stuff, but because it’s statically rendered and available on first paint, it’s not a lot of help. The good news is that the content on my site shows up quickly and that extra JS loading doesn’t impact the end user as much as it would if it wasn’t statically rendered.

My pet theory is that Google Page Speed Insights mobile is hard for everyone because Google’s target device is a sub-$200 phone on an African 3G connection. This is obviously the future of the internet and the benchmark we should be targeting, but this is also an American’s personal site tailored for other Americans, so I’m not going to lose too much sleep over this.

Conclusion

I have a whole other blog post/talk to write about the resume on this site. So much content from such useless navel gazing! In any case, I hope you like this site and thank you for reading this novel!