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:
- NextJS + SSG + MDX
- TailwindUI (Spotlight)
- Vercel
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.
-
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>
-
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$/, ''), } }
-
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.