How I built my website using next js and contentlayer

In today's digital age, having a dynamic and engaging website is crucial for establishing an online presence. In this article, I’ll walk you through the process how I built this SEO-friendly website using Next.js and Contentlayer, two powerful tools that streamlined the development process and empowered me to bring my vision to life. I’ll cover everything from setting up the project to optimizing performance and SEO. Let’s dive in!

Why Next.js and Contentlayer?

Before we get started, let’s briefly discuss why I chose Next.js and Contentlayer for this project:

  • Next.js: Next.js is a powerful React framework that offers server-side rendering (SSR) capabilities out of the box. SSR ensures that search engines receive fully-rendered HTML content, which is crucial for SEO. Additionally, Next.js provides built-in optimizations like automatic code splitting and prefetching.
  • Contentlayer: Contentlayer is a lightweight content management system (CMS) that allows you to manage your content using Markdown files. It seamlessly integrates with Next.js, making it an excellent choice for creating dynamic pages.

Setting Up the Project

To kick off the project, I started by setting up Next.js and then moved on to configure Contentlayer.

1. Initialized a Next.js Project:

npx create-next-app essamayari
cd essamayari

2. Installed contentlayer:

npm install contentlayer next-contentlayer

3. Set Up Next.js Plugin

To enable live-reload and simplify the build setup, I've set up the ContentLayer's Next.js plugin in next.config.mjs file

import { withContentlayer } from "next-contentlayer";

export default withContentlayer({
  // Your Next.js config...
});

Creating Dynamic Pages

With the project structure in place, I began creating dynamic pages for my website. Leveraging Next.js's dynamic routing capabilities, I defined page templates and connected them to Contentlayer's data sources. This seamless integration enabled me to generate dynamic pages based on the content stored in my Markdown files, providing a flexible and scalable solution for managing website content.

Contentlayer's intuitive configuration allowed me to define my content schema and organize my Markdown files efficiently. By creating contentlayer.config.ts file in the root folder of the project, this TypeScript file will define the schema for the blog posts, ensuring that the content structure is consistent and that I can leverage TypeScript’s type safety.

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

const Post = defineDocumentType(() => ({
  name: "Post",
  filePathPattern: `**/*.mdx`,
  contentType: "mdx",
  fields: {
    title: { type: "string", required: true },
    publishedAt: { type: "string", required: true },
    updatedAt: { type: "string", required: false },
    description: { type: "string", required: true },
    published: { type: "boolean", required: false, default: false },
    image: { type: "string", required: false },
  },
  computedFields: {
    slug: {
      type: "string",
      resolve: (doc) => doc._raw.flattenedPath,
    },
    url: {
      type: "string",
      resolve: (doc) => `/blog/${doc._raw.flattenedPath}`,
    }
  },
}));

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

With the configuration set as described, simply placing my MDX files within the data/posts, directory is all that’s required. Contentlayer will then automatically generate the content within the .contentlayer folder. By incorporating the path "contentlayer/generated": ["./.contentlayer/generated"] in the paths configuration in tsconfig.json I'm able to import them in my TypeScript and TSX files:

import { allPosts, Post } from "contentlayer/generated";

While I’d love to delve into more details of how I used each package, I want to keep this post concise and focused on the broader picture. However, it’s worth mentioning a few key packages that significantly enhanced the functionality of my website:

reading-time: This handy package estimates the time a reader might need to go through a post, adding a thoughtful touch for the audience. github-slugger: It generates user-friendly and SEO-compliant slugs for my posts, ensuring they are easily accessible and rank well. rehype-autolink-headings: This plugin allows me to create a navigable table of contents by automatically linking headings, improving the user experience. rehype-pretty-code: It beautifies the code snippets within my content, making them not only functional but also visually appealing.

Styling and Theming

Once the core functionality was implemented, I focused on styling and theming to enhance the visual appeal of my website. To reach that I choose Tailwind CSS as it provides utility classes that make it easy to create responsive and visually appealing designs. Using Tailwind CSS also it was easy to implement the dark mode. using tailwind.config.ts I've set the darkMode to selector to support both the user’s system preference or a manually selected mode and that's what I did in the DarkModeSwitcher component

//...
  useEffect(() => {
    if (
      localStorage.theme === "dark" ||
      (!("theme" in localStorage) &&
        window.matchMedia("(prefers-color-scheme: dark)").matches)
    ) {
      document.documentElement.classList.add("dark");
    } else {
      document.documentElement.classList.remove("dark");
    }
  });
  const toggleDarkMode = () => {
    if (document.documentElement.classList.contains("dark")) {
      localStorage.theme = "light";
      document.documentElement.classList.remove("dark");
    } else {
      localStorage.theme = "dark";
      document.documentElement.classList.add("dark");
    }
  };
//...

Then to define the custom styles for the dark mode it was just simple to add the dark: prefix in HTML.

<div class="bg-white dark:bg-dark">
  <!-- Content here -->
</div>

Optimizing Performance

Performance optimization is a crucial aspect of web development, and Next.js offers several features out of the box to improve website performance. I utilized features like automatic code splitting, image optimization, and static site generation to minimize load times and deliver a smooth user experience.

In the other hand Contentlayer's ability to generate static content further enhanced performance by serving pre-rendered pages to visitors, reducing server load and latency.

the code bellow is an example of how I used generateStaticParams to statically generate routes at build time instead of on-demand at request time for the tags pages.

export const generateStaticParams = async () =>
  tags.map((tag) => ({ tag: tag.slug }));

Optimizing SEO

To optimize SEO, I focused on the following:

  • Meta Tags and Title Optimization: Use dynamic meta tags and titles for each page.
  • Structured Data Markup: Include structured data to help search engines understand your content better.
  • URL Structure and Canonical URLs: Generate clean URLs based on your page structure.
  • Sitemap Generation: Use tools like next-sitemap to create a sitemap for your website.

Deployment and Beyond

For the deployment of my website, I opted for Vercel due to its straightforward and effortless deployment process. Vercel offers automated pipelines upon code push, streamlining the deployment workflow and providing a hassle-free experience right out of the box. This choice ensures that my website is always up-to-date with the latest changes, making it an ideal solution for continuous integration and delivery.

Conclusion

In conclusion, building an SEO-friendly website with Next.js and Contentlayer has been a journey of combining the right tools and technologies to create a fast, efficient, and user-friendly website.

Thank you for following along on this development adventure. I hope this article inspires you to build your own SEO-friendly websites using Next.js and Contentlayer, and I wish you success in your web development endeavors. Happy coding! 🚀