برنامه نویسی

بازسازی نمونه کارها با Next، MDX و Contentlayer

چرا استفاده از Ghost را متوقف کردم

از ایده باز کردن آی‌پدم، نوشیدن یک لاته کاراملی در یک کافه بروکلین، نوشتن یک پست فناوری جدید خوشم آمد. Ghost CMS راه من برای انجام این کار بود (به تنظیمات من مراجعه کنید). با این حال، از زمانی که هیروکو از ما جدا شد و من به Digital Ocean که 6 دلار قیمت دارد، گران بود. اما همچنین، گاهی اوقات Ghost خراب می‌شد و من نمی‌خواستم در هنگام استقرار مجدد به سرعت هر چیزی را که خراب شده بود، برای رفع اشکال طولانی مدت صرف کنم.

در نهایت، تصادفات و پول باعث زیبایی مضحک نوشتن در یک کافه نمی شد، زیرا من هرگز این کار را انجام ندادم. لاته کارامل نیز گران است.

و همچنین می‌توانم از Obsidian، یادداشت‌کننده نشانه‌گذاری خود استفاده کنم، و سپس آن را در وبلاگم کپی کنم و به همه اینها به صورت رایگان دست پیدا کنم.

فن آوری ها

  • Next JS — فریمورک فول استک مورد علاقه من

  • Tailwind CSS — زیرا من نمی دانم چگونه CSS را در غیر این صورت انجام دهم

  • MDX — برای استفاده از React در علامت گذاری من (احتمالاً از JSX زیادی استفاده نخواهد کرد، اما هی چرا حداقل آن را نداشته باشید)

  • Contentlayer — پست های mdx را به داده های json ایمن تبدیل کنید

  • Vercel — استقرار

شروع شدن

من این روزها شروع به استفاده از T3 CLI برای ساخت برنامه‌هایم کرده‌ام، زیرا معمولاً از پشته آن لذت می‌برم و انسجام با هم را دوست دارم.

npm create t3-app@latest
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

فقط Tailwind را انتخاب کنید، ما به بسته های دیگر نیاز نداریم

پس از نصب، می توانیم صفحه اصلی را پاک کنیم

import { type NextPage } from 'next';
import Head from 'next/head';
const Home: NextPage = () => {
  return (
    <>
      <Head>
        <title>Create T3 App</title>
        <meta name="description" content="Generated by create-t3-app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className="flex min-h-screen flex-col items-center bg-gradient-to-b from-[#2e026d] to-[#15162c] pt-20">
        <h1 className="text-7xl font-bold text-white">My Cool Blog</h1>
      </main>
    </>
  );
};

export default Home;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پیکربندی MDX

تا بتوانم بنویسم .mdx فایل ها، به چند پلاگین نیاز داریم

  • @next/mdx — برای استفاده با Next
  • @mdx-js/loader — بسته مورد نیاز @next/mdx
  • @mdx-js/react — بسته مورد نیاز @next/mdx
  • ماده خاکستری — برای نادیده گرفتن ماده اولیه از رندر کردن
  • rehype-autolink-headings — اجازه می دهد تا پیوندهایی به سرفصل ها با شناسه های موجود در آنجا اضافه کنید
  • rehype-slug — اجازه می دهد تا پیوندهایی را به عنوان اسنادی که قبلاً شناسه ندارند اضافه کنید
  • rehype-pretty-code – کد را با برجسته کردن نحو، شماره خطوط و غیره زیبا می کند
  • remark-frontmatter — پلاگین برای پشتیبانی از frontmatter
  • shiki — تم های کدنویسی که می توانیم برای رندر کردن قطعه کد استفاده کنیم
yarn add @next/mdx @mdx-js/loader @mdx-js/react gray-matter rehype-autolink-headings rehype-slug rehype-pretty-code remark-frontmatter shiki
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

راه اندازی Contentlayer

Contentlayer گرفتن پست های وبلاگ mdx ما را به روشی ایمن بسیار آسان می کند.

ابتدا آن و افزونه Next js مرتبط با آن را نصب کنید

yarn add contentlayer next-contentlayer
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خود را اصلاح کنید next.config.mjs

// next.config.mjs

import { withContentlayer } from 'next-contentlayer';

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Configure pageExtensions to include md and mdx
  pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
  reactStrictMode: true,
  swcMinify: true,
};

// Merge MDX config with Next.js config
export default withContentlayer(nextConfig);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خود را اصلاح کنید tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "contentlayer/generated": ["./.contentlayer/generated"]
    }
  },
  "include": ["next-env.d.ts", "**/*.tsx", "**/*.ts", ".contentlayer/generated"]
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

یک فایل ایجاد کنید contentlayer.config.ts و ما سه کار را انجام خواهیم داد

  1. طرح پست ما و محل زندگی محتوا را تعریف کنید
  2. پلاگین های Remark و Rehype ما را تنظیم کنید
import { defineDocumentType, makeSource } from 'contentlayer/source-files';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrettyCode from 'rehype-pretty-code';
import rehypeSlug from 'rehype-slug';
import remarkFrontmatter from 'remark-frontmatter';

export const Post = defineDocumentType(() => ({
  name: 'Post',
  filePathPattern: `**/*.mdx`,
  contentType: 'mdx',
  fields: {
    title: {
      type: 'string',
      description: 'The title of the post',
      required: true,
    },
    excerpt: {
      type: 'string',
      description: 'The excerpt of the post',
      required: true,
    },
    date: {
      type: 'string',
      description: 'The date of the post',
      required: true,
    },
    coverImage: {
      type: 'string',
      description: 'The cover image of the post',
      required: false,
    },
    ogImage: {
      type: 'string',
      description: 'The og cover image of the post',
      required: false,
    },
  },
  computedFields: {
    url: {
      type: 'string',
      resolve: (post) => `/blog/${post._raw.flattenedPath}`,
    },
    slug: {
      type: 'string',
      resolve: (post) => post._raw.flattenedPath,
    },
  },
}));

const prettyCodeOptions = {
  theme: 'material-theme-palenight',

  onVisitLine(node: { children: string | unknown[] }) {
    if (node.children.length === 0) {
      node.children = [{ type: 'text', value: ' ' }];
    }
  },

  onVisitHighlightedLine(node: { properties: { className: string[] } }) {
    node.properties.className.push('highlighted');
  },

  onVisitHighlightedWord(node: { properties: { className: string[] } }) {
    node.properties.className = ['highlighted', 'word'];
  },
};

export default makeSource({
  contentDirPath: 'content',
  documentTypes: [Post],
  mdx: {
    remarkPlugins: [remarkFrontmatter],
    rehypePlugins: [
      rehypeSlug,
      [rehypeAutolinkHeadings, { behavior: 'wrap' }],
      [rehypePrettyCode, prettyCodeOptions],
    ],
  },
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر از git استفاده می کنید، فراموش نکنید که محتوای تولید شده را به خود اضافه کنید gitignore

# contentlayer
.contentlayer
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اضافه کردن محتوای پست

یک پوشه به نام ایجاد کنید content

ایجاد یک فایل در content تماس گرفت first-post.mdx

---
title: "First Post"
excerpt: My first ever post on my blog
date: '2022-02-16'
---
# Hello World

My name is Roze and I built this blog to do cool things

- Like talking about pets
- And other cool stuff

## Random Code

```mdx {1,15} showLineNumbers title="Page.mdx"
import { MyComponent } from '../components/...';

# My MDX page

This is an unordered list

- Item One
- Item Two
- Item Three

<section>And here is _markdown_ in **JSX**</section>

Checkout my React component

<MyComponent />
```
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هنگامی که یک پست جدید ایجاد کردید، مطمئن شوید که برنامه خود را اجرا کنید تا لایه محتوا را ایجاد کند

yarn dev
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

شما باید یک پوشه جدید به نام ببینید .contentlayer که یک خواهد داشت generated پوشه ای که طرحواره ها و انواع شما را تعریف می کند.

نمایش تمام پست های وبلاگ

ما میتوانیم استفاده کنیم getStaticProps برای استخراج داده ها از ما content پوشه چون contentlayer در اختیار ما قرار می دهد allPosts

import { allPosts } from "../../.contentlayer/generated";
import { type GetStaticProps } from "next";
...
export const getStaticProps: GetStaticProps = () => {
  const posts = allPosts.sort(
    (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
  );

  return {
    props: {
      posts,
    },
  };
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

سپس کامپوننت را برای نمایش این پست ها به روز کنید

interface Props {
  posts: Post[];
}

const Home: NextPage<Props> = ({ posts }) => {
  return (
    <>
      ...
      <ul className="pt-20">
        {posts.map((post, index) => (
          <li key={index} className="space-y-2 py-2 text-white">
            <h1 className="text-4xl font-semibold hover:text-yellow-200">
              <Link href={post.url}>{post.title} ↗️</Link>
            </h1>
            <h2>{post.excerpt}</h2>
          </li>
        ))}
      </ul>
      ...
    </>
  );
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

یک پست واحد ارائه دهید

اکنون وقتی کاربر روی یکی از پست ها کلیک می کند، باید آنها را به صفحه جدیدی ارسال کنیم که پست کامل را نشان می دهد.

یک پوشه جدید در pages تماس گرفت blog و فایل درست کنید [slug].tsx

برای تعریف میدیم getStaticPaths برای تولید مسیرهای پویا و getStaticProps برای بازیابی و بازگرداندن یک پست واحد

export const getStaticPaths: GetStaticPaths = () => {
  const paths = allPosts.map((post) => post.url);

  return {
    paths,
    fallback: false,
  };
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

interface IContextParams extends ParsedUrlQuery {
  slug: string;
}

export const getStaticProps: GetStaticProps = (context) => {
  const { slug } = context.params as IContextParams;
  const post = allPosts.find((post) => post.slug === slug);

  if (!post) {
    return {
      notFound: true,
    };
  }

  return {
    props: {
      post,
    },
  };
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کامپوننت ما را راه اندازی کنیم

interface Props {
  post: Post;
}

const BlogPost: NextPage<Props> = ({ post }) => {
  return <></>;
};

export default BlogPost;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قبل از رندر BlogPost، می‌توانیم برخی از آن را با استفاده از Tailwind Typography استایل‌بندی کنیم

yarn add -D @tailwindcss/typography
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آن را به خود اضافه کنید tailwind.config.cjs

module.exports = {
  theme: {
    // ...
  },
  plugins: [
    require('@tailwindcss/typography'),
    // ...
  ],
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حال، چگونه می‌توانیم پست وبلاگ را رندر کنیم؟ Contentlayer یک قلاب خاص NextJS به ما می دهد useMDX که به ما امکان می دهد MDX را رندر کنیم

import { useMDXComponent } from "next-contentlayer/hooks";
...
  const Component = useMDXComponent(post.body.code);
  return (
    <main className="flex min-h-screen flex-col items-center bg-gradient-to-b from-[#2e026d] to-[#15162c] pt-20">
      <header>
        <h1 className="pb-10 text-7xl text-white">{post.title}</h1>
      </header>
      <article className="prose">
        <Component />
      </article>
    </main>
  );
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در کد بالا ما useMDX به ما اجازه می دهد mdx و the خود را رندر کنیم className="prose" سبک‌های Tailwind Typography را روی محتوا اعمال می‌کند.

اما پست ما خشن به نظر می رسد.

تصویر اولین پست

ما می توانیم برخی از سبک ها را در آن تغییر دهیم globals.css

ابتدا اجازه می دهد تایپوگرافی را اصلاح کنیم

.prose :is(h1, h2, h3, h4, h5, h6) > a {
  @apply no-underline text-white;
}
.prose {
  @apply text-white;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و اجازه می‌دهیم به پلاگین‌های کدمان سبک دهیم

code[data-line-numbers] {
  padding-left: 0 !important;
  padding-right: 0 !important;
}

code[data-line-numbers] > .line::before {
  counter-increment: line;
  content: counter(line);
  display: inline-block;
  width: 1rem;
  margin-right: 1.25rem;
  margin-left: 0.75rem;
  text-align: right;
  color: #676e95;
}

div[data-rehype-pretty-code-title] + pre {
  @apply !mt-0 !rounded-tl-none;
}

div[data-rehype-pretty-code-title] {
  @apply !mt-6 !max-w-max !rounded-t !border-b !border-b-slate-400 !bg-[#2b303b] !px-4 !py-0.5 !text-gray-300 dark:!bg-[#282c34];
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خیلی بهتر 🙂

اولین پست تصویر با سبک

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا