Building a Multilingual Blog with Next.js and Contentlayer

5 min read
·

In this post, I'll share how I built my multilingual blog using Next.js and Contentlayer. As Contentlayer is becoming increasingly popular in the Next.js ecosystem, it's a great choice for content management. We'll cover everything from setting up the project to implementing automatic date management and dynamic sitemap generation in Next.js.

Why Contentlayer?

When building a blog with Next.js, there are several approaches to handle MDX content. While Next.js offers various options for content management, Contentlayer stands out as a superior solution for Next.js projects:

  • Using @next/mdx directly (basic Next.js approach)
  • Using a headless CMS with Next.js
  • Using Contentlayer with Next.js (recommended)

I chose Contentlayer because it offers excellent integration with Next.js and provides:

  • Type-safe content for Next.js projects
  • Excellent MDX support with Contentlayer's processing
  • Fast build times in Next.js environments
  • Simple integration with Next.js routing and API

Project Setup

First, we need to install the necessary dependencies for our Next.js and Contentlayer integration:

pnpm add contentlayer next-contentlayer

Then, update next.config.js to enable Contentlayer in your Next.js application:

import { withContentlayer } from 'next-contentlayer';

/** @type {import('next').NextConfig} */
const nextConfig = {};

export default withContentlayer(nextConfig);

Configuring Contentlayer

The core of our Next.js blog system is the Contentlayer configuration. Contentlayer works seamlessly with Next.js to provide a robust content management solution. Here's how we set it up:

import { defineDocumentType, makeSource } from 'contentlayer/source-files';
import { statSync } from 'fs';
import { join } from 'path';

const computedFields = {
  language: {
    type: 'string',
    resolve: (doc) => doc._raw.flattenedPath.split('/')[1],
  },
  createdAt: {
    type: 'date',
    resolve: (doc) => {
      const stats = statSync(join('./content', doc._raw.sourceFilePath));
      return stats.birthtime;
    },
  },
  updatedAt: {
    type: 'date',
    resolve: (doc) => {
      const stats = statSync(join('./content', doc._raw.sourceFilePath));
      return stats.mtime;
    },
  },
};

export const Blog = defineDocumentType(() => ({
  name: 'Blog',
  filePathPattern: 'blog/**/*.mdx',
  contentType: 'mdx',
  fields: {
    title: { type: 'string', required: true },
    description: { type: 'string' },
    slug: { type: 'string', required: true },
  },
  computedFields,
}));

export default makeSource({
  contentDirPath: './content',
  documentTypes: [Blog],
});

Key features of this configuration:

  1. Multilingual Support: Blog posts are organized by language in content/blog/{language}/
  2. Automatic Date Management: Uses filesystem timestamps for creation and modification dates
  3. Type-Safe Fields: All fields are properly typed for better development experience

TypeScript Configuration

For better type safety and development experience in your Next.js and Contentlayer project, we need to configure TypeScript. Update your tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"],
      "contentlayer/generated": ["./.contentlayer/generated"]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".contentlayer/generated"
  ]
}

This configuration ensures TypeScript recognizes the generated Contentlayer types and enables path aliases.

Blog Post Structure

Blog posts in our Next.js and Contentlayer setup are stored as MDX files with a simple frontmatter:

---
title: Your Post Title
description: A brief description
slug: your-post-slug
type: Blog
createdAt: '2024-11-19 15:47:21'
updatedAt: '2024-11-21 10:39:50'
---

Notice we don't need to manually manage dates - they're handled automatically by the filesystem!

Implementing MDX Components

To render MDX content with custom components, we create a client-side MDX component:

'use client';

import Image from 'next/image';
import { useMDXComponent } from 'next-contentlayer/hooks';

// Define custom components to be used in MDX files
const components = {
  Image, // Use Next.js Image component for optimized images
  // Add more custom components here
};

interface MdxProps {
  code: string;
}

export function Mdx({ code }: MdxProps) {
  const Component = useMDXComponent(code);
  return <Component components={components} />;
}

This implementation allows us to:

  1. Use custom components in MDX files
  2. Leverage Next.js optimized components like Image
  3. Add client-side interactivity when needed

To use custom components in your MDX files, simply use them as JSX elements:

<Image src="/path/to/image.jpg" alt="My Image" width={800} height={400} />

Rendering Blog Posts

Here's how we render blog posts in our Next.js pages:

import { allBlogs } from 'contentlayer/generated';
import { Mdx } from '@/components/mdx-components';

export default function BlogPost({ params }) {
  const post = allBlogs.find(
    (post) => post.language === params.language && post.slug === params.slug
  );

  if (!post) notFound();

  return (
    <article className="prose dark:prose-invert max-w-none">
      <h1>{post.title}</h1>
      <Mdx code={post.body.code} />
    </article>
  );
}

Dynamic Sitemap Generation

We also implemented dynamic sitemap generation that uses our blog posts' timestamps:

import { allBlogs } from 'contentlayer/generated';
import { MetadataRoute } from 'next';

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = 'https://yuanzhixiang.com';

  const blogUrls = allBlogs.map((blog) => ({
    url: `${baseUrl}/${blog.language}/blog/${blog.slug}`,
    lastModified: blog.updatedAt,
    changeFrequency: 'weekly',
    priority: 0.7,
  }));

  const staticUrls = ['en', 'zh'].map((lang) => ({
    url: `${baseUrl}/${lang}`,
    lastModified: new Date(),
    changeFrequency: 'daily',
    priority: 1,
  }));

  return [...staticUrls, ...blogUrls];
}

Benefits of This Approach

  1. Type Safety: Contentlayer generates TypeScript types for our content
  2. Automatic Date Management: No manual date management needed
  3. Easy to Maintain: Simple file structure and configuration
  4. SEO Friendly: Automatic sitemap generation with proper timestamps
  5. Developer Experience: Great IDE support with type hints and auto-completion

Conclusion

Using Contentlayer with Next.js has made building a multilingual blog system a pleasant experience. The automatic date management and type safety features have significantly reduced the maintenance burden, while the integration with Next.js's app router makes the system fast and SEO-friendly.