November 29, 2024

Building a Next.js Blog with MDX and TypeScript: A Complete Guide

Learn how to build a lightweight and customizable blog with Next.js and MDX. A step-by-step guide covering everything from dynamic fetching to custom components.

Building a Next.js Blog with MDX and TypeScript: A Complete Guide

If you're short on time, you can find the full code on GitHub.

Introduction

Building an MDX blog with Next.js is a popular choice for many websites, whether it's for a portfolio or a SaaS platform. While tools like Sanity, TinaCMS, or Contentful are great, they often offer more complexity than necessary for a simple blog.

Next.js combined with MDX allows you to create a lightweight, flexible blog without the extra overhead.

In this guide, we'll cover:

  • Dynamically fetching blog posts stored locally.
  • Rendering MDX content in Next.js pages.
  • Adding custom React components to your blog posts.
  • Customizing the styles of your MDX content.

By the end, you'll have a fully functional MDX blog in Next.js, ready to showcase your content with ease.

What is MDX and Why Use It for Your Blog?

MDX is a powerful superset of Markdown that allows you to write JSX directly inside your markdown files. This means you can seamlessly import and use React components within your content, making it incredibly flexible and interactive.

For creating blog posts, MDX is an excellent choice because it combines the simplicity of Markdown with the power of React. You can write standard markdown content, but if you need advanced features, MDX gives you the tools to implement them effortlessly.

Why Use MDX for Your Blog?

Here are some key benefits of using MDX for blogging:

  • Ease of Use: Writing a blog post with MDX is as straightforward as using Markdown, making it accessible even to non-technical users.
  • Flexibility: Add any React component to your blog post, such as charts, diagrams, or call-to-action elements, to make your content more engaging.
  • Content Ownership: Store your MDX files locally without needing a database or external CMS. You retain full control over your content.
  • Version Control: If your project is hosted on GitHub, you can track changes to your content and easily revert to previous versions if needed.

With these features, MDX is a fantastic option for building a blog that's simple, scalable, and highly customizable.

Step 1: Install and Configure Your Project

Let's start by setting up a new Next.js project and configuring it to work with MDX.

Create a New Next.js Project

Run the following command to create a new Next.js application:

npx create-next-app@latest

You'll be prompted to name your project. Enter a name, then press Enter to select the default options for each prompt. Once the setup is complete, you should see the new project structure in your directory.

Next.js installation terminal output

Install MDX Dependencies

Next, we'll install the required dependencies for MDX support. Run the following command using pnpm (or your preferred package manager):

pnpm add @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

Configure MDX in next.config.mjs

Now, update your next.config.mjs file to enable MDX support. Replace the contents of the file with the following configuration:

import createMDX from "@next/mdx";/** @type {import('next').NextConfig} */const nextConfig = {  // Configure `pageExtensions` to include markdown and MDX files  pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],  // Optionally, add any other Next.js config below};const withMDX = createMDX({  // Add markdown plugins here, as desired});// Merge MDX config with Next.js configexport default withMDX(nextConfig);

This setup ensures that Next.js recognizes .md and .mdx files as valid pages.

Define Custom MDX Components

At the root of your project (alongside the app directory), create a new file named mdx-components.tsx. This file will define custom React components that can be used in your MDX content. For now, we'll use the default configuration:

import type { MDXComponents } from "mdx/types";export function useMDXComponents(components: MDXComponents): MDXComponents {  return {    ...components,  };}

This basic setup allows you to extend or customize MDX components later.

Ready to Render MDX

With the installation and configuration done, you're now ready to start rendering MDX content in your Next.js project. Let's move on to creating and displaying your first blog post.

Step 2: Render Your First Blog Post

Now that MDX is configured, let's render your first blog post. There are two ways to render MDX files in Next.js:

Option 1: Directly Render .mdx Files as Pages

You can create .mdx files directly in your app directory, and they will be treated as regular Next.js pages. For example, you could use the following structure:

next-mdx-blog├── app│   └── blog│       └── my-blog-post│           └── page.mdx├── mdx-components.tsx└── package.json

In this setup, page.mdx will be rendered as a normal Next.js page, accessible at http://localhost:3000/blog/my-blog-post. This method works well for simple use cases.

Option 2: Dynamic Blog Posts with Slugs

For a blog, we often want dynamic pages with unique URLs based on the post's filename. To achieve this, we'll use the following structure:

next-mdx-blog├── app│   └── blog│       └── [slug]│           └── page.tsx├── private│   └── posts│       └── hello-world.mdx├── mdx-components.tsx└── package.json

This approach allows us to dynamically load and render blog posts based on their filenames.

Create Your First Blog Post

  1. Add a Blog Post

    In the root of your project, create a private/posts folder. Inside, create a new file named hello-world.mdx with the following content:

    # Hello WorldThis is my first post!

    This will be your first blog post.

  2. Create a Dynamic Blog Page

    In the app/blog/[slug] directory, create a page.tsx file with the following content:

    import Post from "@/private/posts/hello-world.mdx";export default function BlogPostPage() {  return <Post />;}

    Here, the page.tsx file imports the MDX file and renders it as a React component.

  3. Run Your Development Server

    Start your development server:

    pnpm dev
  4. View Your Blog Post

    Visit http://localhost:3000/blog/hello-world in your browser. You should see your blog post rendered on the page. Pretty cool, right?

Step 3: Render Dynamic Blog Posts

So far, we've rendered a single blog post. But manually creating a page for each post isn't practical for a real blog. Let's make the process dynamic by adding multiple posts and metadata.

Populating the Blog Posts

  1. Add More Blog Posts

    To populate your blog with content:

    • Create a few .mdx files in the /private/posts folder.
    • You can write your own content, ask ChatGPT to generate placeholder posts, or use pre-existing examples from the GitHub repository.

    For example, you might create the following files:

    • /private/posts/first-post.mdx
    • /private/posts/second-post.mdx
    • /private/posts/third-post.mdx
  2. Add Metadata

    At the top of each .mdx file, include a metadata object in the following structure:

    export const metadata = {  title: "Some title",  featuredImage: "path-to-image",  publishDate: "2024-11-20",  lastModified: "2024-11-20",  author: {    name: "Author Name",    bio: "A short bio about the author",    avatar: "path-to-avatar-image",  },  excerpt: "Short description of the post, to be displayed on post cards",  tags: ["Array", "of", "tags"],  seo: {    metaTitle: "A Title for SEO, can be the same as your post title",    metaDescription: "Description for SEO, can be the same as excerpt",  },};;

    This metadata object provides essential information about each blog post, such as its title, author details, tags, and SEO fields.

  3. Example Blog Post

    Here's what a complete .mdx file might look like:

    export const metadata = {  title: "Build Stunning Landing Pages in Minutes with Ready.js",  featuredImage: "/uploads/posts/landing-page.jpg",  publishDate: "2024-11-20",  lastModified: "2024-11-20",  author: {    name: "John Doe",    bio: "Frontend Developer and Creator of Ready.js Boilerplate",    avatar: "/uploads/authors/john-doe.jpg",  },  excerpt:    "Ready.js offers 16 prebuilt landing page sections to kickstart your web projects.",  tags: ["landing-pages", "nextjs", "ready.js", "developer-tools"],  seo: {    metaTitle: "Build Stunning Landing Pages in Minutes with Ready.js",    metaDescription:      "Ready.js includes prebuilt landing page sections to help developers launch quickly and efficiently.",  },};# Build Stunning Landing Pages in Minutes with Ready.jsReady.js comes with 16 prebuilt landing page sections designed to help you launch faster. From hero sections to testimonials and CTAs, these components are fully customizable and easy to use. Perfect for developers looking to get their projects off the ground quickly.## Why Use Ready.js for Landing Pages?Building landing pages from scratch can be time-consuming and tedious. With Ready.js, you can skip the initial setup and focus on customizing the content to fit your brand. Whether you're a solo developer or part of a team, Ready.js makes it easy to create stunning landing pages that convert.## How to Get StartedTo start using Ready.js for your landing pages, simply install the package via npm or yarn. Then, import the components you need and start building. With Ready.js, you can create beautiful landing pages in minutes, not hours.
  4. Organize Images

    • Add author avatars to the /public/uploads/authors/ folder. For example: /public/uploads/authors/avatar.jpg
    • Add featured images for your blog posts to /public/uploads/posts/. For example: /public/uploads/posts/landing-page.jpg

    Ensure the paths in your metadata (e.g., featuredImage and avatar) point to the correct locations.

  5. Verify Folder Structure

    After adding your posts and images, your folder structure should look something like this:

Project structure with blog posts and images

Once this is set up, you're ready to move on.

Create a Fetch Function

Now, let's enhance our blog post page to fetch posts dynamically based on their slug.

First, we need a function to load a blog post by its slug. Create a new file at app/blog/[slug]/_queries/get-post.tsx and add the following code:

import { promises as fs } from "fs";import { MDXContent } from "mdx/types";import path from "path";type Author = {  name: string;  bio: string;  avatar: string;};type SEO = {  metaTitle: string;  metaDescription: string;};export type PostMetadata = {  title: string;  featuredImage?: string;  publishDate: string;  lastModified?: string;  author: Author;  excerpt: string;  tags: string[];  seo: SEO;};export type Post = PostMetadata & {  slug: string;  content: MDXContent;};export async function getPost(slug: string): Promise<Post | null> {  try {    const post = await import(`@/private/posts/${slug}.mdx`);    return {      slug,      ...post.metadata,      content: post.default,    } as Post;  } catch (error) {    console.error("Error loading blog post:", error);    return null;  }}

What's Happening Here?

  • We define several types to structure our blog data:
    • Author for author details.
    • SEO for SEO metadata.
    • PostMetadata for post-specific metadata.
    • Post, which includes metadata, the slug, and the MDX content.
  • The getPost function takes a slug as input and imports the corresponding .mdx file from the /private/posts folder.
  • The function returns the post's metadata, slug, and content as an object.

Update the Blog Post Page

Now, update the page at app/blog/[slug]/page.tsx to dynamically fetch and display blog content:

import { notFound } from "next/navigation";import { getPost } from "./_queries/get-post";export default async function BlogPostPage({  params,}: {  params: { slug: string };}) {  const { slug } = await params;  const post = await getPost(slug);  if (!post) {    notFound();  }  const Content = post.content;  return <Content />;}

What's Happening Here?

  • The params object is used to extract the slug from the URL.
  • The getPost function is called to fetch the corresponding blog post. If the post is not found, the notFound() function renders a 404 page.
  • The MDX content is dynamically rendered as a React component.

Test Your Setup

  1. Start the development server: pnpm dev.
  2. Visit http://localhost:3000/blog/your-blog-post, replacing your-blog-post with the name of one of your MDX files (e.g., hello-world).
  3. You should see the content of your blog post rendered on the page.
Project structure with blog posts and images

So far, it's not styled, but the dynamic fetching works as expected. Let's improve the appearance in the next steps!

Add Styling to Your Blog Post Page

Let's add some basic styling to make your MDX content visually appealing using Tailwind CSS.

Tailwind provides a typography plugin that simplifies styling for markdown content, which is useful for rendering MDX content.

  1. Install the plugin:

    pnpm add -D @tailwindcss/typography
  2. Update your tailwind.config.ts file located at the root of your project. Add the plugin as shown below:

    import typographyPlugin from "@tailwindcss/typography"; import type { Config } from "tailwindcss";export default {  content: [    "./pages/**/*.{js,ts,jsx,tsx,mdx}",    "./components/**/*.{js,ts,jsx,tsx,mdx}",    "./app/**/*.{js,ts,jsx,tsx,mdx}",  ],  theme: {    extend: {      colors: {        background: "var(--background)",        foreground: "var(--foreground)",      },    },  },  plugins: [typographyPlugin], } satisfies Config;
    • The @tailwindcss/typography plugin provides ready-to-use classes like prose for styling HTML content such as headings, lists, and paragraphs.

Now, apply the prose class to style the MDX content:

  1. Open your app/blog/[slug]/page.tsx file.

  2. Wrap the MDX content in a section element with Tailwind's prose class:

    import { notFound } from "next/navigation";import { getPost } from "./_queries/get-post";export default async function BlogPostPage({  params,}: {  params: { slug: string };}) {  const { slug } = await params;  const post = await getPost(slug);  if (!post) {    notFound();  }  const Content = post.content;  return (     <section className="prose prose-lg prose-li:mb-2 prose-ul:mt-4 hover:prose-a:text-primary hover:prose-a:decoration-primary prose-a:underline prose-p:mt-2 prose-p:mb-4 max-w-none dark:prose-invert prose-headings:font-bold prose-headings:mt-6 prose-headings:mb-2">      <Content />    </section>   ); }

What's Happening Here?

  • The prose class is applied to style all HTML elements within the section.
  • Customizations like prose-headings:font-bold or prose-li:mb-2 allow you to target specific HTML elements (e.g., h1, li) and apply Tailwind styles.

For example:

  • Target headings: Use prose-headings:[your-class] to style all heading tags (h1, h2, h3, etc.).
  • Target paragraphs: Use prose-p:[your-class] for p tags.
  • Target links: Use hover:prose-a:[your-class] to customize link styles.

Before we move on, you can verify that your styling works:

  1. Start the development server: pnpm dev.
  2. Visit a blog post (e.g., http://localhost:3000/blog/your-blog-post).
  3. The MDX content should now be styled with a clean and readable design.

You're now equipped with styled markdown content, making your blog posts visually appealing!

Enhance Your Blog Post Page

Now, let's finish the blog post page by adding metadata like the featured image, publication date, and author information to create a polished layout.

Replace the existing code in your app/blog/[slug]/page.tsx file with the following:

import Image from "next/image";import { notFound } from "next/navigation";import { getPost } from "./_queries/get-post";export default async function BlogPostPage({  params,}: {  params: { slug: string };}) {  const { slug } = await params;  const post = await getPost(slug);  if (!post) {    notFound();  }  const Content = post.content;  return (    <article className="mx-auto py-20 max-w-screen-md">      <header className="text-center">        <p className="mb-8 text-sm text-muted-foreground">          {new Date(post.publishDate).toLocaleDateString([], {            day: "numeric",            month: "long",            year: "numeric",          })}        </p>        <h1 className="text-4xl">{post.title}</h1>        <p className="mb-4 text-lg text-muted-foreground">          {post.excerpt}        </p>        {post.featuredImage && (          <div className="relative mb-8 h-64 w-full md:h-[500px]">            <Image              src={post.featuredImage}              alt={post.title}              fill              className="rounded-lg object-cover"              priority            />          </div>        )}      </header>      <section className="prose prose-lg prose-li:mb-2 prose-ul:mt-4 hover:prose-a:text-primary hover:prose-a:decoration-primary prose-a:underline prose-p:mt-2 prose-p:mb-4 max-w-none dark:prose-invert prose-headings:font-bold prose-headings:mt-6 prose-headings:mb-2">        <Content />      </section>      <footer className="mt-16">        <div className="flex items-center gap-4 rounded-lg border p-6">          {post.author.avatar && (            <div className="relative h-16 w-16 flex-shrink-0">              <Image                src={post.author.avatar}                alt={post.author.name}                fill                className="rounded-full object-cover"              />            </div>          )}          <div>            <h3 className="font-semibold">{post.author.name}</h3>            {post.author.bio && (              <p className="text-sm text-muted-foreground">{post.author.bio}</p>            )}          </div>        </div>      </footer>    </article>  );}

What's Happening Here?

  • Header:
    • Displays the publication date, title, excerpt, and the featured image of the blog post.
    • The date is formatted using toLocaleDateString for better readability.
    • If the featuredImage exists, it is displayed using next/image with responsive and optimized settings.
  • Content:
    • The MDX content is rendered as before, wrapped in a section with Tailwind's prose classes for styling.
  • Footer:
    • Adds a card-like layout to show the author's avatar, name, and bio, using the metadata retrieved from the getPost function.

Verify the Layout

  1. Start your development server: pnpm dev.
  2. Visit a blog post (e.g., http://localhost:3000/blog/your-blog-post).
  3. You should see something like this:
Styled blog post page in MDX

This step adds significant polish to your blog post page. The layout is now visually appealing and ready for further customization if needed.

Step 4: Creating the Post List Page

Now that your blog posts are rendering dynamically, let's build a page to display all your posts in a visually appealing list.

Fetching All Blog Posts

The first step is to create a function that retrieves all your blog posts along with their metadata. This will allow us to display a preview of each post on the list page.

  1. In the app/blog directory, create a _queries folder.
  2. Inside _queries, create a file named get-all-posts.tsx with the following code:
import { readdir } from "fs/promises";import path from "path";import { Post } from "../[slug]/_queries/get-post";export async function getAllPosts() {  const postsPath = path.join(process.cwd(), "private/posts");  // Get all MDX files  const files = (await readdir(postsPath, { withFileTypes: true }))    .filter((file) => file.isFile() && file.name.endsWith(".mdx"))    .map((file) => file.name);  // Retrieve metadata from MDX files  const posts = await Promise.all(    files.map(async (filename) => {      const slug = filename.replace(/\.mdx$/, ""); // Remove .mdx extension to get slug      const { metadata } = await import(`@/private/posts/${filename}`);      return { slug, ...metadata };    }),  );  // Sort posts from newest to oldest  posts.sort((a, b) => +new Date(b.publishDate) - +new Date(a.publishDate));  return posts as Post[];}

What This Does:

  • Retrieves All MDX Files: The function scans the private/posts folder and filters for .mdx files.
  • Extracts Metadata: For each .mdx file, it imports the metadata and associates it with a slug (based on the filename).
  • Sorts Posts: Posts are sorted by their publishDate, ensuring the newest posts appear first.
  • Returns an Array: You'll get an array of posts, each containing metadata like title, tags, excerpt, and slug.

Creating the Blog List Page

Next, create a page to display your posts.

  1. In the app/blog directory, create a page.tsx file.
  2. Add the following code:
import Image from "next/image";import Link from "next/link";import { getAllPosts } from "./_queries/get-all-posts";export default async function BlogPage() {  const posts = await getAllPosts();  return (    <main className="container mx-auto py-20">      <div className="mb-16 text-center">        <h1 className="text-4xl font-bold">Blog</h1>      </div>      <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">        {posts.map(          ({            slug,            title,            publishDate,            tags,            author,            excerpt,            featuredImage,          }) => (            <Link href={`/blog/${slug}`} key={slug} className="group">              <div className="h-full overflow-hidden rounded-md border border-gray-500 transition-shadow duration-300 hover:shadow-lg">                {featuredImage && (                  <div className="relative h-48 w-full">                    <Image                      src={featuredImage}                      alt={title}                      layout="fill"                      objectFit="cover"                      className="transition-transform duration-300 ease-in-out group-hover:scale-105"                    />                  </div>                )}                <div className="p-4">                  <h2 className="mb-2 text-xl font-semibold text-gray-900 group-hover:underline dark:text-white">                    {title}                  </h2>                  <p className="mb-4 line-clamp-3 text-sm text-gray-500 dark:text-gray-400">                    {excerpt}                  </p>                  <div className="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">                    <time dateTime={publishDate}>                      {new Date(publishDate).toLocaleDateString()}                    </time>                    <span>|</span>                    <span>by {author.name}</span>                  </div>                </div>                <div className="p-4 pt-0">                  <div className="flex flex-wrap gap-2">                    {tags.map((tag) => (                      <span                        key={tag}                        className="rounded-full bg-blue-50 px-1.5 py-0.5 text-xs text-blue-900"                      >                        {tag}                      </span>                    ))}                  </div>                </div>              </div>            </Link>          ),        )}      </div>    </main>  );}

How It Works:

  • Fetch Posts: The getAllPosts function retrieves the metadata and slugs for all posts.
  • Display Cards: Each post is displayed as a card in a responsive grid.
  • Interactive Links: Clicking on a card redirects users to the corresponding blog post.

Test Your Blog Page

  1. Run your development server: pnpm dev.
  2. Navigate to http://localhost:3000/blog.
  3. You should see a grid of blog post cards, looking like this:
Styled blog post list page in MDX

Clicking on a card should take you to the detailed blog post page.

And that's it! You now have a functional and styled blog list page showcasing your posts.

Step 5: Add Custom Components in your MDX

Now that your blog is functional, let's add some interactivity. Blog posts often benefit from rich features like diagrams, comparison charts, or tools. With MDX, you can easily include any React component directly in your post.

In this section, we'll create a simple Call to Action (CTA) component to encourage readers to take action—such as buying a product. A visible and engaging CTA stands out better than a plain link in the text.

Create the CTA Component

  1. In your app/blog directory, create a _components folder.
  2. Inside _components, create a file named cta-card.tsx with the following content:
import Link from "next/link";interface CTACardProps {  title: string;  description: string;  buttonText: string;  buttonLink: string;}export const CTACard = ({  title,  description,  buttonText,  buttonLink,}: CTACardProps) => {  return (    <div className="not-prose my-20 w-full border border-gray-500 rounded-md p-6 flex flex-col items-center">      <div className="text-center text-blue-500 text-3xl font-bold mb-2">        {title}      </div>      <div className="text-center text-muted-foreground mb-8">        {description}      </div>      <Link        href={buttonLink}        className="bg-blue-500 text-white rounded-md px-4 py-2"      >        {buttonText}      </Link>    </div>  );};

What's Happening Here:

  • The CTACard component takes title, description, buttonText, and buttonLink as props. This makes it versatile for use in different contexts.
  • The card is styled using Tailwind classes to make it visually appealing and ensure it stands out in the blog post.

Add the Component to MDX

To use this component in your MDX files, you'll need to add it to your MDX component registry.

  1. Open your mdx-components.tsx file (located at the root of your project).
  2. Update it as follows:
import type { MDXComponents } from "mdx/types";import { CTACard } from "./app/blog/_components/cta-card"; export function useMDXComponents(components: MDXComponents): MDXComponents {  return {    ...components,    CTACard,   };}

Now, CTACard is available for use in any MDX file.

Use the Component in an MDX File

Let's see how to use this CTA component in a blog post:

export const metadata = { ... }# Build Stunning Landing Pages in Minutes with Ready.jsReady.js comes with 16 prebuilt landing page sections designed to help you launch faster. From hero sections to testimonials and CTAs, these components are fully customizable and easy to use. Perfect for developers looking to get their projects off the ground quickly.## Why Use Ready.js for Landing Pages?Building landing pages from scratch can be time-consuming and tedious. With Ready.js, you can skip the initial setup and focus on customizing the content to fit your brand. Whether you're a solo developer or part of a team, Ready.js makes it easy to create stunning landing pages that convert.<CTACardtitle="Ready to Get Started?"description="Download Ready.js and start building beautiful landing pages today."buttonText="Buy Now"buttonLink="/buy"/>## How to Get StartedTo start using Ready.js for your landing pages, simply install the package via npm or yarn. Then, import the components you need and start building. With Ready.js, you can create beautiful landing pages in minutes, not hours.

What's Happening Here:

  • The <CTACard> component is instantiated like any other React component.
  • We pass title, description, buttonText, and buttonLink as props to define the CTA's content.

See It in Action

Run your development server and visit the blog post where you added the CTA. You should see a beautifully styled card inviting readers to take action.

CTA component rendered in Next.js with MDX

This example shows how easily you can enhance your blog posts with interactive elements using MDX. While we created a simple CTA here, you can extend this idea to include charts, calculators, or any other React components that make your content engaging and unique.

Conclusion: Going Further with Your Blog

Congratulations! 🎉 You've built your own blog system using Next.js and MDX. While this is a basic version, it includes all the essential features to get started: dynamic blog post fetching, MDX rendering, custom components, and styling. This foundation is flexible, allowing you to scale and customize your blog as your needs grow.

With this setup, you own your content, can version control it, and have the ability to add interactive elements directly within your posts. It's a powerful, simple solution for developers who want complete control over their blog.

Now, if you want to take your blog a step further, here are a few ideas to make it even better:

  • Related Posts Carousel: Add a carousel of related blog posts at the end of each post. Fetch posts with similar tags or categories to keep readers engaged.
  • Dynamic Table of Contents: Create a table of contents for long posts that updates dynamically. You'll need to create a custom component for headings that adds an id to each h2 and allows for smooth scrolling.
  • Code Highlighting: If you're writing a technical blog, add custom code blocks with proper syntax highlighting using libraries like Prism or Shiki.
  • Custom Image Component: Gain more control over your images by creating a custom MDX component for them. You can manage positioning, sizing, quality, and more for a polished look.
  • SEO Improvements: Optimize your blog for search engines by adding JSON-LD schema, custom metadata, and other SEO-friendly features to help your posts rank higher.

Happy coding!

Want to Create Your Blog Faster?

Ready.js is a Next.js boilerplate that saves you 40+ hours of coding. It includes all the features mentioned above to create your blog, plus everything you need to build a SaaS application, like Stripe integration, authentication, and more.
MC

Manuel Coffin

Author

Outdoor sports enthusiast, frontend geek, and Creator of Ready.js Boilerplate