ساخت وبلاگ استاتیک با Next.js

در مقاله ساخت وب‌سایت استاتیک با Next.js به چگونگی ساخت و بارگزاری یک سایت استاتیک در نکست پرداختیم.

در این پروژه قصد داریم با استفاده از نکست یک وبلاگ استاتیک به سایت قبلی اضافه کرده و نوشته‌های آن را با استفاده از مارک‌داون یا Markdown ایجاد و مدیریت کنیم.

یک پوشه جدید به نام posts در دایرکتوری اصلی پروژه‌ ایجاد کنید. در داخل این پوشه، یک فایل مارک‌داون جدید برای هر پست وبلاگی که می‌خواهید ایجاد کنید. سپس برای هر فایل مارک‌داون، با استفاده از فرنتمتر، عنوان و تاریخ پست را مشخص کنید.

---
title: My First Blog Post
date: '2023-03-05'
---

This is my first blog post. Stay tuned for more!

در این پروژه، پکیج gray-matter برای پردازش کردن فرونتمتر فایل‌های Markdown استفاده خواهد شد، پکیج ‍remark برای رندر کردن محتوای Markdown و پکیج ‍remark-gfm نیز برای افزودن پشتیبانی از نسخه‌ی گیت‌هاب Markdown استفاده خواهد شد.

برای نصب وابستگی‌های مورد نیاز، از دستورات زیر استفاده کنید.

pnpm add --save-dev gray-matter remark remark-gfm remark-html

در ادامه، یک پوشه جدید به نام utils در دایرکتوری اصلی پروژه‌ی خود ایجاد کنید. در داخل پوشه ‍utils، یک فایل جدید با نام markdown-to-html.ts و با محتوای زیر را ایجاد کنید.

import { remark } from 'remark'
import gfm from 'remark-gfm'
import html from 'remark-html'

export async function markdownToHtml(markdown: string): Promise<string> {
  const result = await remark().use(gfm).use(html).process(markdown)
  return result.toString()
}

در اینجا یک تابع به نامmarkdownToHtml برای تبدیل متن Markdown به HTML تعریف می‌کنیم.

این تابع با گرفتن یک رشته متن Markdown به عنوان ورودی، آن را به HTML تبدیل کرده و به صورت رشته باز می‌گرداند. این تابع از remark-gfm برای اضافه کردن پشتیبانی از Markdown نسخه GitHub استفاده می‌کند.

در داخل پوشه ‍utils، یک فایل جدید با نام posts.ts و با محتوای زیر را ایجاد کنید.

import fs from 'fs'
import matter from 'gray-matter'
import path from 'path'

const postsDirectory = path.join(process.cwd(), 'posts')

export type Post = {
  slug: string
  title: string
  date: string
  content: string
}

export function getSortedPostsData(): Post[] {
  const fileNames = fs.readdirSync(postsDirectory)
  const allPostsData = fileNames.map((fileName) => {
    const slug = fileName.replace(/\.md$/, '')
    const fullPath = path.join(postsDirectory, fileName)
    const fileContents = fs.readFileSync(fullPath, 'utf8')
    const { data, content } = matter(fileContents)
    return {
      slug,
      ...data,
      content,
    }
  })
  // @ts-ignore
  return allPostsData.sort((a, b) => {
    // @ts-ignore
    if (a.date < b.date) {
      return 1
    } else {
      return -1
    }
  })
}

تابع getSortedPostsData مسئول دریافت متادیتای (فرونتمتر) تمامی پست‌های وبلاگ است و آن‌ها را بر اساس تاریخ به صورت نزولی مرتب می‌کند. این تابع یک آرایه از آبجکت‌ها بر می‌گرداند که هر آبجکت، یک پست وبلاگ با متادیتای مربوط به آن (عنوان و تاریخ) را نمایش می‌دهد.

سپس، یک پوشه جدید به نام ‍‍components در دایرکتوری اصلی پروژه‌ی خود ایجاد کنید. در داخل این پوشه، یک فایل جدید با نام layout.tsx و با محتوای زیر را ایجاد کنید.

import Head from 'next/head'
import React, { ReactNode } from 'react'

type Props = {
  title: string
  children?: ReactNode
}

const Layout: React.FC<Props> = ({ title, children }) => {
  return (
    <>
      <Head>
        <title>{title}</title>
        <meta name="description" content="My static blog" />
      </Head>
      <header>
        <h1>My Static Blog</h1>
      </header>
      <main>{children}</main>
      <footer>
        <p>© {new Date().getFullYear()} My Static Blog</p>
      </footer>
    </>
  )
}

export default Layout

این کامپوننت به عنوان لایوت اصلی وبلاگ شما استفاده خواهد شد.

در داخل پوشه pages در ریشه پروژه، یک پوشه جدید به نام blog ایجاد کنید. یک فایل جدید به نام index.tsx با محتوای زیر ایجاد کنید.

import Link from 'next/link'
import Layout from '../../components/layout'
import { getSortedPostsData, Post } from '../../utils/posts'

type Props = {
  posts: Post[]
}

export default function Blog({ posts }: Props) {
  return (
    <Layout title="My Static Blog">
      {posts.map((post: Post) => (
        <article key={post.slug}>
          <Link href={`/blog/${post.slug}`}>
            <h2>{post.title}</h2>
            <time dateTime={post.date}>{post.date}</time>
          </Link>
        </article>
      ))}
    </Layout>
  )
}

export async function getStaticProps() {
  const posts = getSortedPostsData()
  return {
    props: {
      posts,
    },
  }
}

در این مثال، ما آرایه posts را به عنوان props به کامپوننت Blog منتقل می‌کنیم. در داخل تابع getStaticProps، ما getSortedPostsData را صدا می‌زنیم تا اطلاعات مربوط به تمامی پست‌های بلاگ را دریافت کرده و آن‌ها را براساس تاریخ مرتب کنیم. سپس نتیجه را به عنوان props به کامپوننت خود ارسال می‌کنیم.

در مرحله بعد می‌بایست صفحات تک پست وبلاگ را ایجاد کنیم. برای این کار در پوشه blog فایل جدیدی با نام [slug].tsx و با محتوای زیر ایجاد کنید.

import Head from 'next/head'
import Layout from '../../components/layout'
import { markdownToHtml } from '../../utils/markdown-to-html'
import { getSortedPostsData, Post } from '../utils/posts'

type Props = {
  post: Post
}

export default function BlogPost({ post }: Props) {
  return (
    <Layout title={post.title}>
      <Head>
        <title>{post.title}</title>
      </Head>
      <article>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
    </Layout>
  )
}

export async function getStaticPaths() {
  const posts = getSortedPostsData()
  const paths = posts.map((post) => ({
    params: {
      slug: post.slug,
    },
  }))

  return {
    paths,
    fallback: false,
  }
}

// @ts-ignore
export async function getStaticProps({ params }) {
  const slug = params?.slug
  const post = getSortedPostsData().find((p) => p.slug === slug)
  // @ts-ignore
  const { title, date, content } = post

  return {
    props: {
      post: {
        slug,
        title,
        date,
        content: await markdownToHtml(content),
      },
    },
  }
}

در تابع getStaticPaths، ابتدا با استفاده از تابع getAllPostSlugs که از فایل posts.ts فراخوانی کرده‌ایم، تمام اسلاگ‌های پست‌های وبلاگ را دریافت کنیم. سپس با استفاده از تابع map، هر اسلاگ را به یک آبجکت با پارامتر slug تبدیل می‌کنیم.

در نهایت، آرایه ایجاد شده از اسلگ‌ها را به عنوان پارامتر paths در آبجکتی که از تابع برمی‌گردانیم، قرار می‌دهیم. همچنین، مقدار fallback را false می‌کنیم تا در صورتی که اسلاگ وارد شده توسط کاربر معتبر نباشد، پیام خطای 404 را نشان دهیم.