بازسازی نمونه کارها با 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
و ما سه کار را انجام خواهیم داد
- طرح پست ما و محل زندگی محتوا را تعریف کنید
- پلاگین های 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];
}
خیلی بهتر 🙂