Sarah Ting

Site revamp!

I had thrown sarahjting.com together after I quit my job in 2022 as a landing page for prospective employers, and up until this month it has just been a domain that I had pointing towards a public Notion page.

Now that I'm actually employed, I've finally got the time to build out a proper site! 🥳

The final stack I ended up with:

I went on a journey to find a set-up that worked for me, and wanted to review the options I tried.

Ghost CMS

This was a big one for me. My top priority was finding an editor that I loved to write in, and Ghost has far and away the prettiest and smoothest admin UI that I tried. I installed and uninstalled it a good half dozen times just because of how much I liked the admin panel before remembering reasons why it wouldn't fit my use-case 😆

I ended up being turned off by it due to the lack of customisation. It really is an all-in solution for blogs, but I wanted a mixed blog-portfolio site that I could use to keep track of my projects and services.

If writing was a primary income stream for me, I would have found this solution much more attractive. Ghost has a lot of very cool out-of-the-box features specifically useful to a blog platform. And again, it is very, very pretty.

Strapi

After Ghost, I moved onto setting up Strapi with a GQL backend API. The admin panel isn't as pretty as Ghost or Notion, but it's possible to install CKEditor which works alright.

This was very fun to work with and I actually developed a Strapi integration to completion before ditching it because the deploy flow was too inconvenient for me. It didn't really make sense to me to maintain a dedicated backend API server when I'm the only person pushing updates, but I also didn't like the idea of using a localhost admin panel to push updates (what if I have to make changes to the site while I'm away from my PC?).

Other options

  • Wordpress, Craft CMS, Laravel: I steered away from PHP; I've used it too much and wanted to try something new ✨
  • sanity.io: Disqualified due to having no self-hosted solution.
  • directus: This was disappointing for me; I heard that it was a better alternative to Strapi, but I set it up and found that it felt clunky in comparison.
  • Notion: I love the writing experience in Notion so much that I was considered hooking my front-end up to the Notion API and using it as a headless CMS. But the rate limits are restrictive, and performance is poor.

Vercel, NextJS & SSG

I ended up feeling rather silly back at markdown and SSG -- an option which had been suggested to me multiple times and that I had turned down because I was stubborn about wanting a nice WYSIWYG 😂

I used NextJS mostly because it already came out of the box with the Tailwind CSS template I was using, and I used Vercel because it's made for NextJS, is free, and comes with a pleasant CICD flow.

Deployments were fantastically easy to set up -- I just plugged my GitHub repo into Vercel, and the production site will automatically re-deploy on push to main. In theory, I can even write updates from my phone just by using GitHub's inline editor.

I ended up not getting my pretty editor, but I'll live!


NextJS + SSG + MDX integration

The markdown file integration was pretty straightforward! I used TailwindCSS's Spotlight template as a guide.

  1. Set up blog posts in a data folder, eg: /data/blog-posts/blog-post-slug.mdx

    ---
    title: "My cool blog title"
    blurb: "My cool blog blurb"
    tags: ["javascript"]
    publishedAt: "2023-02-06"
    ---
    # Blog title
    
    Blog content goes here! **Markdown friendly!** <u>And I can use HTML too!</u>
    
  2. Set up the blog post fetching library, eg. /src/lib/blog-posts.js

      export async function getAll() {
          let blogFileNames = await glob(['*.mdx'], {cwd: dataDir})
          let blogs = await Promise.all(blogFileNames.map(fileName => importBlog(fileName)))
          return blogs.sort((a, z) => z.meta.publishedAt > a.meta.publishedAt ? 1 : -1)
      }
    
      async function importBlog(blogFileName) {
          const source = fs.readFileSync(path.join(dataDir, blogFileName), 'utf-8')
          const {content, data} = matter(source)
          return {
              mdxSource: await serialize(content),
              meta: data,
              slug: blogFileName.replace(/\.mdx$/, ''),
          }
      }
    
  3. Load the blog posts via getStaticProps(), eg.

    export default function BlogsPage({blogPosts}) {
      // ...
    }
    
    export async function getStaticProps() {
      return {
        props: {
          blogPosts: await BlogPosts.getAll(),
        }
      }
    }
    

I'm sure I'll want to re-code the whole thing eventually, but for now I'm just looking forward to adding more content and making styling tweaks.