Back to blog

How I Originally Built This Site

7 min read

Cover image for How I Originally Built This Site
Image crafted by robots

At the beginning of 2022, I decided I wanted to start blogging about coding. So in order to blog I needed a platform. I’ve also wanted to build a new portfolio under jimmy.codes. This all led to building this blogfolio. The goal for this was simplicity, performance and content powered by markdown. As a disclaimer, this is more of an overview rather than a how to guide.

Table of Contents

Technology Overview

Here are the main technologies:

Here are the services:

Next.js vs Astro

I first started with astro because for this use case it made perfect sense, so what is astro?

Astro is a new kind of static site builder for the modern web. Powerful developer experience meets lightweight output.

With features such as:

This meant that it was perfect to build a simple performant blogfolio while still using React. But it’s still in beta, so it’s lacking features such as easy image optimization, bug-free developer experience tooling, build customization, and others. Still, I’m very optimistic about Astro’s future and who knows, I might refactor this site to use Astro in the future.

Due to Astro lacking the features I mentioned, I went with the popular and proven framework, Next.js.

Markdown Content

Since Next.js does not come with out-the-box Markdown support like Astro we have to roll our own. Next.js gives us a great example powered by remark(like Astro) of how to do this but I wanted to leverage Next.js’s image component which gives features such as:

To accomplish this I went with react-markdown due to it’s components feature, i.e

import Image from "next/image";
import ReactMarkdown from "react-markdown";
const renderers = {
img: (
image: DetailedHTMLProps<
ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
>,
) => {
if (!image.src) return null;
return (
<Image
className="rounded-lg"
blurDataURL={image.src}
src={image.src}
alt={image.alt}
layout="responsive"
width={945}
height={645}
placeholder="blur"
quality={65}
/>
);
},
};
interface MarkdownContentProps {
content: string;
}
const MarkdownContent = ({ content }: MarkdownContentProps) => {
return <ReactMarkdown components={renderers}>{content}</ReactMarkdown>;
};

This allows replacing an image displayed with markdown, such as:

![Image](sample.jpg)

With <Image /> from next/image for optimization.

In addition, since React Markdown still leverages unified extensive plugin system. I took advantage by adding features such as:

Which easily is accomplished by using <ReactMarkdown />’s remarkPlugins and rehypePlugins props, i.e

import ReactMarkdown from "react-markdown";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeExternalLinks from "rehype-external-links";
import rehypeHighlight from "rehype-highlight";
import rehypeSlug from "rehype-slug";
import remarkUnwrapImages from "remark-unwrap-images";
import "highlight.js/styles/base16/material.css";
interface MarkdownContentProps {
content: string;
}
const MarkdownContent = ({ content }: MarkdownContentProps) => {
return (
<ReactMarkdown
components={renderers}
remarkPlugins={[remarkUnwrapImages]}
rehypePlugins={[
rehypeExternalLinks,
rehypeHighlight,
rehypeSlug,
rehypeAutolinkHeadings,
]}
>
{content}
</ReactMarkdown>
);
};

Image Optimization

I already mentioned Next.js’s image component but alongside Cloudinary features such as:

And react-cool-dimensions to dynamically change sizes, i.e

import Image, { ImageProps } from "next/image";
import useDimensions from "react-cool-dimensions";
const ResponsiveImage = ({
src,
...rest
}: Omit<ImageProps, "layout" | "sizes">) => {
const { observe, width } = useDimensions<HTMLDivElement | null>();
return (
<div ref={observe}>
<Image {...rest} layout="responsive" sizes={`${Math.round(width)}px`} />
</div>
);
};

We get performant and high quality images at every size!

CI/CD Overview

CI CD Flow Diagram image

The entire CI/CD pipeline is powered by GitHub Actions with Vercel driving the deployment aspect.

Pull Requests

When a pull request happens Vercel immediately starts deploying the site under a preview url which allows for immediate visibility, automated tests and quick collaboration.

While that is happening GitHub Actions is doing a couple things:

These code quality checks fall under this projects’ status checks which prevents a pull requests from merging if they are not passing.

Deployments

Like I mentioned previously, deployments happen during pull requests under a preview url, but they also happen with any push to master which deploys to production. master is protected so any change needs to go through a pull request. In order words, a successful pull request must be made to release to production.

Automation Testing

Any time there’s a successful deployment, Cypress tests are executed with the base url from Vercel. Once those automated tests finish, they are uploaded to the Cypress Dashboard. This is accomplished by using the cypress-io/github-action, i.e:

on: [deployment_status]
jobs:
e2e:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: pnpm/action-setup@15569a497d6aff479ba1c47c859888e22a431052
- run: pnpm install --frozen-lockfile
- uses: cypress-io/github-action@v4
with:
install: false
record: true
env:
CYPRESS_BASE_URL: ${{ github.event.deployment_status.target_url }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

On top of the successful code quality checks needed for a pull request to be merged, these Cypress tests are also needed. This gives us an extra layer of confidence before deploying to production. 👍 🚀

Performance Testing

Since one of the goals was performance, I’ve also added Lighthouse as part of CI/CD pipeline to validate wether or not there’s been any performance degradation. And as a bonus we also get SEO, Accessibility and Best Practices validations.

In order to accomplish this, I used the Lighthouse CI Action which followed the same approach as the Cypress Action so it was a simple setup, i.e

lighthouse:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Lighthouse
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
${{ github.event.deployment_status.target_url }}
${{ github.event.deployment_status.target_url }}/blog
${{ github.event.deployment_status.target_url }}/about
${{ github.event.deployment_status.target_url }}/blog/posts/how-i-built-simple-blogfolio
uploadArtifacts: true
temporaryPublicStorage: true
configPath: .github/.lighthouserc.yml

Since this GitHub Action is powered by the Lighthouse CI, it offers extensive configuration.

Analytics

Using Fathom with Next.js is extremely simple, given you setup a custom domain first, all you need to do is use <Script /> component, i.e

{
process.env.NEXT_PUBLIC_FATHOM_KEY && (
<Script
src="https://wild-wind-innovate.jimmy.codes/script.js"
data-site={process.env.NEXT_PUBLIC_FATHOM_KEY}
strategy="afterInteractive"
/>
);
}

This will allow Next.js to take care of any optimizations and will give you all of Fathom’s features such as:

For transparency’s sake, I made this site’s dashboard public.

Conclusion

There’s nothing game changing mentioned in this post and this is mostly a collection of cool things others have done. But it felt appropriate as my first post on my new personal site to be about how it came to be. With most of my personal projects, I will most likely refactor this with a new cool technology or service that comes along. For now Next.js, TypeScript, the Unified System, Fathom, Cloudinary and Vercel is a perfect combination to build a highly performant blogfolio site.

Let's Discuss

Questions or feedback? Send me an email.

Last updated on

Back to blog