惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
N
Netflix TechBlog - Medium
宝玉的分享
宝玉的分享
Google DeepMind News
Google DeepMind News
腾讯CDC
G
Google Developers Blog
Martin Fowler
Martin Fowler
Microsoft Security Blog
Microsoft Security Blog
Recent Announcements
Recent Announcements
爱范儿
爱范儿
Engineering at Meta
Engineering at Meta
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
有赞技术团队
有赞技术团队
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
罗磊的独立博客
博客园 - 三生石上(FineUI控件)
美团技术团队
WordPress大学
WordPress大学
阮一峰的网络日志
阮一峰的网络日志

少数派

派早报:Google 发布 Fitbit Air 等 - 少数派 「新人报到」確認需求,再開始 - 少数派 从 SOLO 独立开发者社区,我看到了越来越多开发者开始做自己的产品 - 少数派 我怎么管理那些"不常做,但总会忘"的生活事项 - 少数派 人形机器人量产元年,数据才是具身智能的“生死线” - 少数派 BuhoLaunchpad 高度还原 Mac 启动台:开发历程与思考 - 少数派 五年陪伴依然不舍,DIY 换壳后让罗技 MX Master 3 继续服役 - 少数派 新玩意 240|少数派的编辑们最近买了啥? - 少数派 一日一技|为什么你应该关闭 iOS 的键盘声音 - 少数派 我做了个插件和 Skills,一键提取任何网站的设计规范 Design.md - 少数派 住在三四线城市的你,该开始录播客了 - 少数派 甘南秘境,大白高国 - 少数派 AI的审美:谁让把我变成川内倫子 - 少数派 返工怎能不烦恼,打工人片单总有一部是你的「嘴替」 - 少数派 为了让「上厕所」更健康,我做了一个小工具 - 少数派 AI + Skill,能够让生成的文章去除 AI 味吗? - 少数派 新玩意|韶音OpenDots ONE 耳夹式耳机 - 少数派 《美满》| 在每一个春天的晚上相爱(362) - 少数派 新玩意|优篮子 PS01 MagSnap 磁吸支架 - 少数派 自我整合手记 | 我开始早睡了:用稳定规则,为自由托底 - 少数派 用龙虾(OpenClaw)两个多月,我最深的12个体会 - 少数派 听歌时间到,12 张你可能错过的 2025 华语乐坛好专辑 - 少数派 承诺能追吗 - 少数派 macOS 26启动台没了? 我做了个不一样的App启动器 - Keboard - 少数派 《四海为家的人》| INTJ对话INTJ(361) - 少数派 你发过的那些黑历史,是时候一次清干净了 - 少数派 新玩意:安安静静玩,越玩越专注:计客密码机 - 少数派 iPad 用户首次体验 Android 平板:vivo Pad6 Pro - 少数派 数据逻辑强 - 少数派 极北行+ | 一路向北,探访日本至北之地 | 001 - 少数派
Next.js项目App目录如何简单集成markdown博客 - 少数派
2025-03-07 · via 少数派

本教程将带你一步步在Next.js App目录中集成Markdown博客,适合刚入门Next.js的开发者。无需复杂配置,快速实现博客功能

Next.js官方关于markdown的文档有说明过如何渲染markdown,也是针对App目录的,但我尝试过并不太行,可能是版本的问题,不管怎么样,最后我并没有解决这个问题,而是用了别的方案去实现。

此教程适用于app目录的next项目,下面的例子刚好是多语言结构的项目。

实现思路

结合文件结构解说一下大致逻辑:

Next.js项目App目录markdown结构

Markdown文件放在/app/_articles/[lang]文件夹下管理,如果你是多语言目录,那么每个语种都是单独一个文件夹,如果不是,那么可以直接放在/app/_articles文件夹下。

另外markdown文件里从第一行开始可以放入一些Frontmatter,一般放在文件开头,用—符号分割开,提供一些额外信息,如发布时间、更新时间,是否已经发布,对应的描述,这类的信息可以自定义的,方便你做很多个性化的操作,一般我用来做meta信息的填充。

这里可以给一些Frontmatter的例子:

---
title: "这是博客标题"
createdAt: "2024-11-12"
updatedAt: "2024-11-12"
isPublished: true
description: "这是博客描述"
---

随着你文件的增多,你需要一些代码来管理、显示你的markdown信息,比如:

  1. 在你的blog页面展示所有的markdown博客。
  2. 根据markdown文件名称跳转对应的博客详情,比如访问https://i18ncode.com/blog/how-nextjs-app-simply-make-i18n能正常显示how-nextjs-app-simply-make-i18n.mdx文件内的文本。
  3. 渲染markdown文本,当然要包括对应页面的meta信息。

具体代码

大致要做的事情如上所述,下面贴对应的代码。

先封装好一些通用方法在/lib/mdx.ts文件中,方便后续调用:

// mdx.ts

import fs from "fs";

import path from "path";

import matter from "gray-matter";

import readingTime from "reading-time";

const articlesDirectory = path.join(process.cwd(), "app/_articles");

const webContentDirectory = path.join(process.cwd(), "app/_contents");

// 获取 MDX/MD 原始数据

export function getMdxRawData(fileName: string, lang: string, hasSuffix: boolean) {

   let fullPath = path.join(articlesDirectory, lang, `${fileName}`);

   let suffix = hasSuffix // 判断是否有后缀,没有的话就加上后缀

       ? ""

       : fs.existsSync(`${fullPath}.mdx`)

           ? ".mdx"

           : ".md";

   const fileContents = fs.readFileSync(`${fullPath}${suffix}`, "utf8");

   return fileContents;

}

// 处理 MDX/MD 原始数据中的 frontmatter

export function getMdxFrontmatter(mdxRawData: string) {

   const { content, data } = matter(mdxRawData);

   return {

       content,

       frontmatter: data,

       readingTime: readingTime(content).text, // 计算阅读时间

   };

}

// 获取文章的所有信息

export function getArticlesData(fileName: string, lang: string, hasSuffix = false) {

   return {

       ...getMdxFrontmatter(getMdxRawData(fileName, lang, hasSuffix)),

       fileName: fileName.split(".").slice(0, -1).join("."), // 去除后缀

   };

}

// 获取 _articles 目录下的所有文章

export function getAllArticlesData(lang: string) {

   const fileNames = fs.readdirSync(articlesDirectory + "/" + lang);

   const allArticlesData = fileNames.map((fileName) => {

       return getArticlesData(fileName, lang,true);

   });

   return allArticlesData;

}

你可以根据你项目的具体情况来调整上面的代码。

在你的blog页面展示所有的markdown博客

调用上面封装好的getAllArticlesData方法,该方法支持一个叫lang的参数,这是多语言项目里有的参数,如果你传入的值为en,那么它就会去/app/_articles/en下获取所有的markdown文件。

然后不要忘记按时间排序:

export default async function BlogPage({params: {lang}}: { params: { lang: Locale } }) {

   const allArticlesData = getAllArticlesData(lang);

   const dictionary = await getDictionary(lang);

   const sortedArticles = allArticlesData.sort((a, b) => {

       // 将日期字符串转换为日期对象

       const dateA = new Date(a.frontmatter.createdAt).getTime();

       const dateB = new Date(b.frontmatter.createdAt).getTime();

       // 比较日期,返回值决定排序

       return dateB - dateA; // 倒序排序

   });

   return (

       <div>

           <div className="mb-16">

               <h1 className={title()}>{dictionary.blog.title}</h1>

               <div className="mt-8">

                   {sortedArticles.map(article => (

                       <Blog blog={article} key={article.fileName} lang={lang} />

                   ))}

               </div>

           </div>

           <CallToAction dictionary={dictionary} />

       </div>

   );

}

Blog组件中使用简单的跳转:

<Link href={`/${lang}/blog/${blog.fileName}`} />

将文件名传递过去,详情页面会根据文件名找到对应的文件进行渲染。

渲染markdown文本

在/app/[lang]/blog/[id]/page.tsx页面下则是对具体的markdown进行解析和渲染,将对应的内容填入页面,渲染meta信息:

import { getArticlesData } from "@/lib/mdx";

import { Remarkable } from 'remarkable';

import hljs from 'highlight.js';

import {getDictionary} from "@/get-dictionaries";

import CallToAction from "@/components/cta";

import React from "react";

export const generateMetadata = async ({ params }: any) => {

   const { content, frontmatter, readingTime } = getArticlesData(params.id, params.lang);

   const lang = await getDictionary(params.lang);

   return {

       title: frontmatter.title + " | " + lang.blog.meta.title,

       description: frontmatter.description,

       openGraph: {

           title: frontmatter.title + " | " + lang.blog.meta.title,

           type: "website",

           url: ``,

           images: [

               {

                   // 此处还可以有width和height属性,see:https://medium.com/@moh.mir36/open-graph-with-next-js-v13-app-directory-22c0049e2087

                   url: "/logo.png",

                   alt: ""

               }

           ],

           siteName: "",

           description: frontmatter.description,

           locale: ""

       },

       twitter: {

           images: [

               {

                   url: "/logo.png",

                   alt: ""

               }

           ],

           title: frontmatter.title + " | " + lang.blog.meta.title,

           description: frontmatter.description,

           card: "summary_large_image"

       },

   }

}

// !important:博客的排版需要在tailwind.config.js中添加插件:require("@tailwindcss/typography"),自行查看对应代码

const Page = async ({ params }: any) => {

   const { content, frontmatter, readingTime } = getArticlesData(params.id, params.lang);

   const md = new Remarkable({

       html: true,

       breaks: true,

       linkify: true,

       typographer: true,

       highlight: function (str: string, lang: string) {

           if (lang && hljs.getLanguage(lang)) {

               try {

                   return hljs.highlight(lang, str).value;

               } catch (err) {}

           }

           try {

               re

这里用了Remarkable方案代替了Next的MDXRemote组件

到这里基本上完成了一半,但是样式方面可能会用欠缺,需要在tailwind.config.js中添加插件:require(“@tailwindcss/typography”),代码如下:

import {nextui} from '@nextui-org/theme'

/** @type {import('tailwindcss').Config} */

module.exports = {

   //...

   plugins: [

       // ....

       require("@tailwindcss/typography"), // markdown typography

   ],

}

OK,到这里基本大功告成,就可以正常显示了,当然,过程中需要安装一些依赖,根据你项目里缺的依赖来安装就可以了。

关于多语言Markdown文件的管理和翻译

你可以看到,使用这种方式,如果是多语言的站点,那么你不可避免地要翻译和管理好对应的markdown文件。

用gpt翻译的话长度会受限制,第一个语种还好,第二个语种之后就会开始忘记原文,然后就开始胡言乱语了;要么你就每次对话都带上原文让gpt翻译,这样对话没几轮就得开启一个新的对话了。

我刚开始做这类工作的时候完成一篇博客需要一整个下午的时间,这实在是太耗时了。

机器翻译更无法接受,它无法识别markdown的符号,会格式错乱,另外机翻效果略显生硬。

基于这块的考虑我做了个专门针对这种情况的翻译器,有需要的朋友可以体验一下markdown翻译器

markdown翻译器考虑了长度问题,做了文本切割并分段请求,你可以把一整个markdown文本塞进去翻译,直接获取最后的整体结果,经过反复尝试我这是没什么问题的;另外也做了markdown格式的识别和保留,不用害怕丢失格式;最后也考虑了本土化的情况,同样的文本也尽量要求AI用更本土化的方式表达出来,应该是比较适合做国际化的朋友了。