Home
8 min read
·

165 views

How I built my blog

Introduction

I have always wanted to write posts on software development through my experiences. It is always great to share my knowledge and also to write new and interesting things I discover over time. There are many ways to do this but since I am a front-end developer, I might as well build my own simple blog from scratch and share this journey with my readers. If you are interested in building one yourself too, hop right in!

The amount of tools and development kits today has made software development so much easier than it was when I first started almost more than 10 years ago, as of this post, as a teenager. Today, we have frameworks which reduced the amount of boilerplate and configuration needed to start a project.

I am very privileged to be able to use the following main tech stacks for my blog:

Landing page
Landing page

Managing content with Contentlayer

Contentlayer is a tool designed to make it easier for developers to work with content in modern web development environments. It acts as a content transformation and validation layer that converts content from various sources like Markdown and/or MDX (Markdown for JSX) into a format that can be easily used within a web application.

Setting up Next.js was straightforward, thanks to the comprehensive documentation and examples provided by the Next.js team. I leveraged dynamic routing to create a seamless navigation experience for my blog readers, allowing them to move effortlessly between posts. However, integrating a custom CMS for dynamic content posed a challenge. After some experimentation, I opted for a headless CMS that I connected to Next.js via API, a decision that greatly simplified content management.

Below is a piece of code to create a Post schema using ContentLayer:

// contentlayer.config.js
import { defineDocumentType, makeSource } from "contentlayer/source-files";

export const Post = defineDocumentType(() => {
  return {
    name: "Post",
    filePathPattern: "posts/**/*.mdx",
    contentType: "mdx",
    fields: {
      title: { type: "string", required: true },
      publishedAt: { type: "date", required: true },
      description: { type: "string", required: true },
    },
    computedFields: {
      slug: {
        type: "string",
        resolve: (post) => post._raw.flattenedPath,
      },
    },
  };
});

export default makeSource({
  contentDirPath: "content",
  documentTypes: [Post],
});

Metadata a.k.a Frontmatter

Contentlayer allows us to extract the metadata from a post's Frontmatter. This metadata, or Frontmatter, provides a summary of a post's content such as the title, published date, and excerpt. This helped with creating the Search Engine Optimisation (SEO) very easily using Next.jst generateMetadata function which I will talk about later.

Example of Frontmatter in MDX:

---
title: Building a Portfolio Blog with Next.js and Deploy with Vercel
publishedAt: "2024-02-24"
description: I built my portfolio blog with Next.js, Contentlayer, Tailwind, and deployed it with Vercel
---

(Blog contents)

Creating the front-end UI with Next.js

Next.js was the chosen React framework for several reasons. It has server-side rendering which allowed for static contents like a blog post which does not need to be updated often to render and load instantly. The framework's file-based routing system helped made creating dynamic pages based on the slug of each post very easily. It is also packed with features to help with SEO like generating metadata, sitemap.xml and robots.txt.

Post page
Post page

Example of creating a post using the dynamic routing:

// app/posts/[slug]/page.js
export default function PostPage({ params }) {
  // Get the matching post based on the slug in the URL
  const post = allPosts.find((post) => post.slug === params.slug);

  if (!post) {
    // Return 404 if the slug in the URL does not match any of the posts generated from Contentlayer
    return notFound();
  }

  return (
    <div>
      <h1>{post.title}</h1>
    </div>
  );
}

Fun idea - View count feature for each posts

Tracking the view count for each post is a fun idea and something optional. It has been a long time since I last used Laravel (PHP framework) which includes databases, I thought it would be interesting to play around with databases again.

To fetch and store the view count each time the page is visited, I need a database. I chose Vercel Postgres together with Prisma ORM to work with this feature. For those who are unsure what Prisma does, it is a type-safe query builder with migrations to help working with databases easier.

*However, due to some unforeseen circumstances, I had moved my database out of Vercel Postgres to another provider. I will share more on this in another blog post.

Setting up Prisma ORM is easy by following the documentation. I did not really have to customise a lot of the configuration in any way.

With the database and Prisma successfully setup, I created an API and the ViewCounter component to handle reading and updating the view count of each post.

Example code of the API /api/views/:slug:

// app/api/views/[slug]/route.js

// POST request to update the view count when landing on the page
export async function POST(req, { params }) {
  unstable_noStore(); // Prevents caching
  const slug = params.slug;

  if (!slug) {
    // Return an error for no matching slug
  }
  // Update the view count
}

// GET request to fetch the view count based on a matching slug
export async function GET(req, { params }) {
  unstable_noStore(); // Prevents caching
  const slug = params.slug;

  if (!slug) {
    // Return an error for no matching slug
  }

  // Update the view count
}

The beauty of Prisma is that I could easily fetch and update the database really quickly without the raw SQL syntax.

Example of getting the view count based on the slug provided:

async function getViewCountBySlug(slug) {
  const data = await prisma.views.findUnique({
    where: { slug },
  });

  return data?.count || 0;
}

For the function that updates the view count, I will add 1 to the existing view count if it already exist. Otherwise, I will create a new slug and set the matching view count to 1.

Example of updating the view count based on the slug provided:

async function updateViewCount(slug, count) {
  prisma.views.upsert({
    where: { slug },
    create: { slug, count: 1 },
    update: { count: count + 1 },
  });
}

Example code for the ViewCounter Component:

// components/ViewCounter.js
const fetcher = (...args) => fetch(...args).then((res) => res.json());

export default function ViewCounter({ slug }) {
  const { data } = useSWR(`/api/views/${slug}`, fetcher);

  useEffect(() => {
    fetch(`/api/views/${slug}`, { method: "POST" });
  }, [slug]);

  const viewCount = data?.count || 0;

  return <div>{viewCount} views</div>;
}

Dynamic SEO enhancement with Next.js

With Next.js, I was able to leverage on its generateMetadata to create dynamic metadata that depends on the values of current route parameters, external data, or metadata in parent segments.

Example of creating the required tags for SEO purposes using the metadata:

// app/posts/[slug]/page.js
export async function generateMetadata({ params }) {
  // Get the matching post based on the slug in the URL
  const post = await allPosts.find((post) => post.slug === params.slug);

  const title = post.title;
  const description = post.description;
  const slug = post.slug;

  return {
    title,
    description,
    opengraph: {
      title,
      description,
    },
    twitter: {
      title,
      description,
    },
    alternates: {
      canonical: `${YOUR_DOMAIN}/${slug}`,
    },
  };
}

Sitemap and Robots

Generating a sitemap and creating the robots.txt for search engines to crawl is as easy as the following code:

// sitemap.xml
export default function sitemap() {
  return [
    {
      url: `${YOUR_DOMAIN}`, // Landing page
      lastModified: formatLastModified(),
    },
    ...allPosts.map(({ publishedAt, slug }) => ({
      url: `${YOUR_DOMAIN}/${slug}`,
      lastModified: formatLastModified(publishedAt),
    })),
  ];
}

function formatLastModified(datetime = new Date()) {
  return new Date(datetime).toISOString().split("T")[0];
}

Do note that at time of this post, there is a problem with Google Search Console not being able to read the Next.js's /sitemap.xml properly. This seemed to be a widespread issue: Next 13 - Sitemap can't fetch on Google Search Console #51649 on GitHub.

// robots.txt
export default function robots() {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
    },
    sitemap: `${YOUR_DOMAIN}/sitemap.xml`,
  };
}

Deploying with Vercel

Vercel, was my choice for deployment because it works right out of the box with Next.js.

Fun fact: Next.js is developed by the team at Vercel

Vercel's integration with Next.js is seamless, and there is zero configuration to get it to work for almost all use cases. The entire process from feature development to deploying to production is as simple as a push of my code to GitHub repository.

Conclusion

Building my blog with Next.js and deploying it on Vercel has been an incredibly fun and rewarding journey. Initially, I was exploring a Content Management System (CMS) like Sanity with the idea of automatically posting to social media (e.g. Twitter) on each published post using webhooks, but eventually went with using Markdown or MDX as the content in the same project. And by integrating Contentlayer with the Markdown contents, it made me even easier to maintain and update my blog. I hope this finds inspiration for you on creating your next (no pun intended) portfolio blog too!

You may visit the source code to this blog on GitHub at https://github.com/ruchernchong/portfolio.