diff --git "a/18.1/k8s1_\346\246\202\345\277\265\345\222\214\350\277\220\350\241\214\346\234\272\345\210\266\347\256\200\344\273\213.md" "b/18.1/k8s1_\346\246\202\345\277\265\345\222\214\350\277\220\350\241\214\346\234\272\345\210\266\347\256\200\344\273\213.md" index 97341474..bdb249a9 100644 --- "a/18.1/k8s1_\346\246\202\345\277\265\345\222\214\350\277\220\350\241\214\346\234\272\345\210\266\347\256\200\344\273\213.md" +++ "b/18.1/k8s1_\346\246\202\345\277\265\345\222\214\350\277\220\350\241\214\346\234\272\345\210\266\347\256\200\344\273\213.md" @@ -1,6 +1,3 @@ ---- -tags: k8s,docker,kubernetes ---- # 初识kubernetes kubernetes是管理docker容器的工具,他可以解决这样的一些问题,动态的扩展docker,同一服务类型多个docker的负载均衡,分布式的容器管理,实时保持设定好的服务数目。 ## 1 分布式容器调度工具 diff --git a/24.06/github_markdown_test.md b/24.06/github_markdown_test.md index 387f8c4c..06c5dab0 100644 --- a/24.06/github_markdown_test.md +++ b/24.06/github_markdown_test.md @@ -84,9 +84,9 @@ var test = function this_is(){ } ``` -```bogus_language -var test = function this_is(){ - console.log("language declared as bogus_language"); +```diff ++ var test = function this_is(){ +- console.log("language declared as bogus_language"); } ``` diff --git "a/24.06/\346\210\221\347\232\204\345\215\232\345\256\242\347\275\221\347\253\231\346\230\257\345\246\202\344\275\225\346\220\255\345\273\272\347\232\204.md" "b/24.06/\346\210\221\347\232\204\345\215\232\345\256\242\347\275\221\347\253\231\346\230\257\345\246\202\344\275\225\346\220\255\345\273\272\347\232\204.md" new file mode 100644 index 00000000..36b97106 --- /dev/null +++ "b/24.06/\346\210\221\347\232\204\345\215\232\345\256\242\347\275\221\347\253\231\346\230\257\345\246\202\344\275\225\346\220\255\345\273\272\347\232\204.md" @@ -0,0 +1,400 @@ +--- +title: 我的博客(笔记)网站是如何搭建的 +description: 本文主要介绍了我的博客网站是如何搭建的,主要是使用next.js和mdx来实现。 +created: '2024-06-08 16:48:00' +tags: mdx,next.js,mdx-bundle,rehype,remark +--- +# 1 内容载体选择 +基础内容载体的选择,一般而言可以有以下几种选择: +- 1 直接使用`csdn`、`掘金`等平台托管博客内容。 +- 2 使用功能全面的cms平台,例如`wordpress`。 +- 3 纯静态的渲染方式,例如`github pages`。 +- 4 在`github pages`之前对文档加工,例如`hexo`,`gatsby`等。 +- 5 使用服务端渲染,`nextjs`托管`vercel`等。 + +以下对比时基础的设置下的对比: + +|代表|编写格式|内容存储|个人开销|自由度|评论系统| +|---|---|---|---|---|---| +|csdn|markdown/富文本|csdn平台|-|-|完善| +|wordpress|markdown/富文本|自己服务器|服务器|低,第三方扩展|完善| +|github pages|markdown|github|-|-|-| +|hexo|markdown|github|-|高,可自行修改构建程序|-| +|nextjs|mdx|github|-|高,可引入react到md|-| + +我之前用过好多种方式,18年左右选择使用,自行集成静态文件,然后托管到`github pages`,也就是类似`hugo`的方式,只不过是自己处理的`md`转`html`的过程。 + +可以看我19年录制的视频,为什么用这种方式的原因介绍的比较清楚。[视频地址](https://www.bilibili.com/video/BV1BE411o7jE/?spm_id_from=333.999.0.0&vd_source=f4a1de78011612c7aa6504c1845b5b83)。 简单讲,就是我需要一个目录页能快速查找之前记录的笔记。 + +现在6年过去了,尤其在看国外一些大佬的`blog`的时候,发现有很多不错的功能,比如说直接把`codesandbox`这种在线代码工具嵌入进来;直接把自己写一个前端组件嵌入进来;代码高亮直接指定行号高亮等等。这些功能都对于表达清楚文章的内容非常有帮助。 + +而要实现这么高的自由度,就只能通过服务端渲染的方式来实现了,借助`next`和`mdx`,将`md`与`react`整合,打开新世界大门。 + +当然ssr也有很多其他选择,但是`next`显然是最成熟的方案,所以最后我选择了`nextjs` + `vercel`来部署我的博客。 + +# 2 基础配置 +我的目的就是为了支持`mdx`渲染,哦对,简单说一下`mdx`,就是在`md`中能插入`jsx`,而`jsx`是`html`标签的超集,这样换取了超高的自由度。算是在`完全自己写一个网页` 和 `不写网页纯写md` 之间做了很好的权衡,即保留前者的自由度,又不会太复杂,导致写文档成为麻烦的事。 + +![mdx](https://i.imgur.com/w2BnarF.png) + +先配置`nextjs`项目,这里不展开讲基础的创建了,我有个仓库详细展示了如何一步步搭建基于`nextjs`的项目,可以参考:[nextjs-blog](https://github.com/sunwu51/nextjs-mdx-blog-tutorial)。这个文档一共分为四步,对应了四次代码提交。 + +- 1 创建项目 +- 2 添加mdx支持 +- 3 添加样式 +- 4 如何写自定义markdown插件 + +![image](https://i.imgur.com/gethdgH.png) + +# 3 Remote MDX +`nextjs`官方文档也有这一节[remote-mdx](https://nextjs.org/docs/app/building-your-application/configuring/mdx#remote-mdx),是因为步骤2中配置的`mdx`支持,其实是支持`page.mdx`的渲染,他需要我们的目录结构是这样: + + + +这样的结构需要创建很多的目录,与正常人写文章的习惯是不符的,比如我当前的目录结构想要前移过来就要大改,我当前的结构比较简单就是一个月份目录下面,每个`md`文件是一篇文章。 + + + +为了避免这样的改造,就需要`remote mdx`远程mdx的工具,而不是使用原生的mdx支持,所以第二步中配置的mdx相关的支持可以都删除了。 + +这种工具很多,上面官网介绍了一个第三方的lib: `next-mdx-remote`,但因为版本兼容性问题,一开始按照官方的demo并没有成功启动,所以我选择了`mdx-bundler` + +![img](https://i.imgur.com/iqQnc6I.png) + +`mdx-bundler`除了能将`mdx`源码编译为`jsx`代码,还支持`import`引入外部文。 + +![img](https://i.imgur.com/2A4yPNG.png) + +```bash +$ npm install --save mdx-bundler esbuild +``` + +安装依赖后,接下来要做的是利用`nextjs`的slug功能,将文章的`md`文件名作为`slug`,然后在参数中获取`slug`即文件名,根据文件名到对应的目录下读取文件的内容,然后用`mdx-bundler`解析即可。 + +创建`app/blog/[month]/[slug]/page.js`文件 + +```js :page.js +import fs from 'fs'; +import path from 'path'; +import { bundleMDX } from 'mdx-bundler' +import { getMDXComponent } from 'mdx-bundler/client' + +export default async function Post({ params }) { + // 从param中能拿到路径中的month和slug + let { month, slug } = params; + // slug对应我们的文件名,因为有中文所以需要反转 + slug = querystring.unescape(slug); + + // 按照md或者mdx后缀去寻找对应的文件系统中的文件,读取文本内容 + var mdxPath = path.join(process.cwd(), month, `${slug}.mdx`); + var mdPath = path.join(process.cwd(), month, `${slug}.md`); + const mdxSource = fs.existsSync(mdxPath) ? + fs.readFileSync(mdxPath, 'utf8') : + fs.readFileSync(mdPath, 'utf8'); + + // 用mdx-bundler解析mdx,解析成jsx代码 + const result = await bundleMDX({ + source: mdxSource, + }); + + // code是解析出的jsx代码,frontmatter是解析的前言部分 + const { code, frontmatter } = result + + // 通过getMDXComponent转换解析的code为react组件 + const Component = getMDXComponent(code) + return ( + <> +
+ +
+ + ) +} +``` +上面代码就可以实现远程`mdx`的功能,当我们访问`http://localhost:3000/blog/24.06/我的博客是如何搭建的`这样的网址的时候,路径中的`24.06`和`我的博客是如何搭建的`会作为`params`中的`month`和`slug`参数传递进来,然后找到`24.06`下`我的博客是如何搭建的.md`文件;将其内容读取出来后,用`mdx-bundler`把`md`文件内容转换为react组件了。 + +因为在步骤2基础配置中,我们已经引入了`github-markdown.css`(如果没有引入的话,回看下之前步骤的教程来引入这个css即可),所以页面可以直接渲染成比较好看的样式。 + +# 4 插件 +正如步骤2中我们引入了很多`remark`和`rehype`一样,对于remote形式我们也可以引入这些插件,只需要修改`bundleMDX`这行代码,通过`mdxOptions`来增加各种插件,同时增加了`cwd`和`esbuildOptions`的部分配置,是为了解决一些react组件引入时候的问题。 +```diff {8-11} :page.js ++ import remarkGfm from 'remark-gfm'; ++ import rehypeSlug from 'rehype-slug'; ++ import rehypePrismPlus from 'rehype-prism-plus'; ++ import toc from "@jsdevtools/rehype-toc"; ++ import remarkCodeTitles from "remark-flexible-code-titles"; ++ import rehypeAutolinkHeadings from 'rehype-autolink-headings'; + ++ //注意@/rehypePlugins目录下是自定义的一部分组件后续会说他们的作用 ++ import rehypeCodeCopyButton from '@/rehypePlugins/rehype-code-copy-button.mjs' ++ import rehypeImageSrcModifier from '@/rehypePlugins/rehype-image-src-modifier.mjs' ++ import rehypeTocExt from '@/rehypePlugins/rehype-toc-ext.mjs' + +- const result = await bundleMDX({ +- source: mdxSource, +- }); ++ const result = await bundleMDX({ ++ source: mdxSource, ++ cwd: path.join(process.cwd(), 'app', 'components'), ++ mdxOptions: (options, frontmatter) => { ++ options.remarkPlugins = [ ++ ...(options.remarkPlugins ?? []), ++ ...[remarkGfm, remarkCodeTitles] ++ ] ++ options.rehypePlugins = [ ++ ...(options.rehypePlugins ?? []), ++ ...[ ++ rehypeSlug, ++ [rehypeAutolinkHeadings, { behavior: 'wrap' }], ++ [rehypePrismPlus, { ignoreMissing: true, showLineNumbers: true }], ++ rehypeCodeCopyButton, ++ rehypeImageSrcModifier, ++ toc, ++ rehypeTocExt, ++ ] ++ ] ++ return options; ++ }, ++ esbuildOptions: options => { ++ options.outdir = path.join(process.cwd(), 'public') ++ options.write = true ++ return options ++ } ++ }) +``` +上面代码中引入的`@/rehypePlugins`目录下是我自定义的几个组件,以`rehype-toc-ext.mjs`为例,我们来看下如何写一个插件,插件本身就是一个函数,函数返回值是`(tree)=>{}`的函数,其中`tree`就是当前的`mdx`的ast树,我们可以对这个ast树进行各种操作,一般会用到`visit`函数,如下代码: + +代码实现了把原来的内容放到新的div中,同时删除原来的nav标签,并添加一个新的nav的div标签,一起放到一个总的div中按照左右排列,实现toc放到右边的效果(原来是上面) +```js :rehype-toc-ext.mjs +import { visit } from 'unist-util-visit'; + +export default function rehypeTocExt() { + return (tree) => { + let tocNav = null; + + // 遍历ast树所有dom节点找到 nav标签,并且class=toc将其删除 + // nav.toc是另一个toc插件生成的dom + visit(tree, 'element', (node, index, parent) => { + if (node.tagName === 'nav' && node.properties && node.properties.className && node.properties.className.includes('toc')) { + tocNav = node; + parent.children.splice(index, 1); + } + }); + + // 创建3个div,一个是container,一个是content,一个是toc + // 第1个 包含 后面2个 + const { children } = tree; + const container = { + type: 'element', + tagName: 'div', + properties: { className: ['container-wrapper', 'container'] }, + children: [], + }; + const contentDiv = { + type: 'element', + tagName: 'div', + properties: { className: ['content-wrapper'] }, + children: [], + }; + + // 把tocNav塞到toc中 + const tocDiv = { + type: 'element', + tagName: 'div', + properties: { className: ['toc-wrapper'] }, + children: [tocNav], + }; + + // 把原来tree下面所有节点,塞到content中 + while (children.length > 0) { + contentDiv.children.push(children.shift()); + } + + container.children.push(contentDiv); + container.children.push(tocDiv); + children.push(container); + }; +} +``` +除了插件之外我还增加了一些小的功能,比如代码阅读时间和利用`frontmatter`展示一些信息,代码如下: +```js :page.js +import readingTime from'reading-time' + +...... +...... + // 这个库根据字数,推算大概需要的阅读时间 + const { text: readingTimeText } = readingTime(mdxSource); +...... + const { code, frontmatter } = result +...... + + return + <> +
+ {/* 从frontmatter中获取title展示到最上面作为标题 */} +

{frontmatter.title || slug}

+ + {/* 从frontmatter中获取description展示到第2行 */} + {frontmatter.description &&

{frontmatter.description}

} + +
+ {/* 阅读时间和tags展示 */} + {readingTimeText + (frontmatter.created ? (" created at" + frontmatter.created): "")} +
+ {frontmatter.tags && frontmatter.tags + .split(',').filter(item => item.trim().length) + .map((tag, i) => + ( + {tag.trim()} + ))} +
+
+
+
+
+ +
+ +``` +效果是这样的: + +![image](https://i.imgur.com/Niv2DFV.png) + +# 5 配置提前生成的静态内容 +还是在`blog/[month]/[slug]/page.js`文件最后追加如下,这个函数会在`next build`的时候被调用,返回值是一个数组,数组中的每一项就是一个路由,会提前把这些路径的静态内容生成到`public`目录下,这样就可以在`next export`的时候直接使用这些静态文件,而不需要再次生成,提高了生成速度。 +```js :page.js +...... +export async function generateStaticParams() { + const path = require('path'); + const POSTS_PATH = process.cwd(); + var filenames = fs.readdirSync(POSTS_PATH); + // 搜索当前路径下所有的24.06这种格式的路径,这是我存放blog文件的地方 + const monthes = filenames.filter(item => item.match(/\d\d\.\d\d?/)); + + // 然后在这个月份路径下面,搜索所有的md和mdx格式的文件 + return monthes.flatMap((month) => { + const filenames = fs.readdirSync(path.join(POSTS_PATH, month)); + const posts = filenames.filter(item => item.match(/\.mdx?/)) + .map(item => { + const slug = item.replace(/\.mdx?/, ''); + return { slug, month }; + }); + return posts; + }) +} +``` +# 6 添加评论区 +利用`github discussion`作为评论区,方式很简单,我这里直接用了[https://giscus.app/zh-CN](https://giscus.app/zh-CN)进行配置。 + +![image](https://i.imgur.com/clj7dDB.png) + +![image](https://i.imgur.com/PX2lbzh.png) + +因为我们是react风格的,直接引入script标签有点怪,所以也可以用`giscus`提供的react组件,使用也很简单,如下: +```bash +$ npm i @giscus/react +``` +```jsx :Discussion.jsx +'use client' +import Giscus from '@giscus/react'; + +export default function () { + return +} +``` +然后在`page.js`引入并放到页面最后即可 +```js :page.js +import Discussion from '@/app/components/Discussion'; + +...... +
+ +
+
+ +
+``` + +![image](https://i.imgur.com/FO8ZYRY.png) + +评论区的内容是存储到了`github discussions`中,title就是我们页面的url(因为有中文所以有些转义) + +![image](https://i.imgur.com/PtSNvHh.png) + +# 7 添加全文搜索功能 +这里借助了`algolia docsearch`提供的免费功能,具体接入流程可以参考[如何引入搜索功能到博客](./%E5%A6%82%E4%BD%95%E5%BC%95%E5%85%A5%E6%90%9C%E7%B4%A2%E5%8A%9F%E8%83%BD%E5%88%B0%E5%8D%9A%E5%AE%A2.md) + +# 8 添加自定义组件 +在`blog/[month]/[slug]/page.js`文件中,我们可以直接使用`mdx`的自定义组件,例如在写文章中经常会用到的文件结构目录,如果用markdown直接写的话,样式不够美观。就可以从`antd`中引入。在上面第三步中已经能看到文件目录的结构。 + +代码就是直接在`md`文件中写,但是要想这个标签生效,需要引入`antd`,并将这个组件注册进来。 +```jsx + +``` + +安装 +```bash +$ npm i antd +``` + +注意这里不能再`page.js`直接从`antd`中引入,因为`page.js`是服务端渲染的,如果直接引入antd组件,组件中如果使用了`React.useState`等客户端渲染才有的功能,就会报错。所以先创建了一个`Antd.jsx`文件: +```jsx :Antd.jsx {1} +'use client' +import { Tree } from 'antd'; // 这里还可以引入其他你需要的组件 +const { DirectoryTree } = Tree; + +export { DirectoryTree } +``` +还是修改`page.js`如下,这样就可以在md文件中直接使用`DirectoryTree`标签了,就像上面的代码。 +```js :page.js +import { DirectoryTree } from '@/app/components/Antd'; +...... +
+ +
+``` \ No newline at end of file diff --git "a/24.06/enjoy_\347\254\224\350\256\260\347\275\221\347\253\231\346\215\242\346\226\260\347\232\256\350\202\244\345\225\246.mdx" "b/24.06/\347\254\224\350\256\260\347\275\221\347\253\231\346\215\242\346\226\260\347\232\256\350\202\244\345\225\246.mdx" similarity index 100% rename from "24.06/enjoy_\347\254\224\350\256\260\347\275\221\347\253\231\346\215\242\346\226\260\347\232\256\350\202\244\345\225\246.mdx" rename to "24.06/\347\254\224\350\256\260\347\275\221\347\253\231\346\215\242\346\226\260\347\232\256\350\202\244\345\225\246.mdx" diff --git a/app/blog/[month]/[slug]/page.js b/app/blog/[month]/[slug]/page.js index 4e5832a4..9389a0cf 100644 --- a/app/blog/[month]/[slug]/page.js +++ b/app/blog/[month]/[slug]/page.js @@ -1,7 +1,5 @@ import fs from 'fs'; import path from 'path'; -import matter from 'gray-matter'; -import { bundleMDX } from 'mdx-bundler' import remarkGfm from 'remark-gfm'; import rehypeSlug from 'rehype-slug'; import rehypePrismPlus from 'rehype-prism-plus'; @@ -11,22 +9,31 @@ import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeCodeCopyButton from '@/rehypePlugins/rehype-code-copy-button.mjs' import rehypeImageSrcModifier from '@/rehypePlugins/rehype-image-src-modifier.mjs' import rehypeTocExt from '@/rehypePlugins/rehype-toc-ext.mjs' +import { bundleMDX } from 'mdx-bundler' import { getMDXComponent } from 'mdx-bundler/client' -import '@/app/globals.css' -import '@/app/prism-dracula.css' +import readingTime from 'reading-time'; import querystring from 'querystring'; -import Discussion from '@/app/components/discussion'; -const readingTime = require('reading-time'); +import Discussion from '@/app/components/Discussion'; import { Button, Card, Tooltip, DirectoryTree } from '@/app/components/Antd'; import { Tabs, Item } from '@/app/components/Tabs'; + export default async function Post({ params }) { let { month, slug } = params; slug = querystring.unescape(slug); + if (slug.endsWith(".md") || slug.endsWith(".mdx") || slug.endsWith(".html")) { + slug = slug.replace(/\.(md|mdx|html)$/, ''); + } + var mdxPath = path.join(process.cwd(), month, `${slug}.mdx`); var mdPath = path.join(process.cwd(), month, `${slug}.md`); - const mdxSource = fs.existsSync(mdxPath) ? fs.readFileSync(mdxPath, 'utf8') : fs.readFileSync(mdPath, 'utf8'); + let mdxSource; + try { + mdxSource = fs.existsSync(mdxPath) ? fs.readFileSync(mdxPath, 'utf8') : fs.readFileSync(mdPath, 'utf8'); + } catch (e) { + return <>
404 PAGE NOT FOUND
; + } const { text: readingTimeText } = readingTime(mdxSource); const result = await bundleMDX({ source: mdxSource + '\n\n# ' + month, @@ -58,7 +65,7 @@ export default async function Post({ params }) {

{frontmatter.title || slug}

{frontmatter.description &&

{frontmatter.description}

}
- {readingTimeText + (frontmatter.created ? (" created at" + frontmatter.created): "")} + {readingTimeText + (frontmatter.created ? (" created at " + frontmatter.created): "")}
{frontmatter.tags && frontmatter.tags.split(',').filter(item => item.trim().length) .map((tag, i) => {tag.trim()})} diff --git a/app/components/navbar-menu.jsx b/app/components/NavbarMenu.jsx similarity index 100% rename from app/components/navbar-menu.jsx rename to app/components/NavbarMenu.jsx diff --git a/app/components/Tabs.jsx b/app/components/Tabs.jsx index 5cdd87e3..939d2645 100644 --- a/app/components/Tabs.jsx +++ b/app/components/Tabs.jsx @@ -23,14 +23,14 @@ function Tabs({ children, ...props }) {
{children.map((item, index) => ( ))}
{children.map((item, index) => ( -
+
{item}
))} diff --git a/app/layout.js b/app/layout.js index 6a1bbe18..f2b42884 100644 --- a/app/layout.js +++ b/app/layout.js @@ -1,5 +1,5 @@ import { Inter } from "next/font/google"; -import NavBar from "@/app/components/navbar-menu"; +import NavBar from "@/app/components/NavbarMenu"; import DocSearch from "@/app/components/DocSearch" import "./globals.css"; import "./prism-dracula.css"; diff --git a/package-lock.json b/package-lock.json index 6be3fcdc..1b85a2a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "framer-motion": "^11.2.10", "mdx-bundler": "^10.0.2", "next": "14.2.3", + "node-fetch": "^3.3.2", "react": "^18.1.0", "react-aria": "^3.33.1", "react-dom": "^18.1.0", @@ -34,6 +35,7 @@ "rehype-autolink-headings": "^7.1.0", "rehype-prism-plus": "^2.0.0", "rehype-slug": "^6.0.0", + "remark-embed-images": "^4.0.0", "remark-flexible-code-titles": "^1.2.0", "remark-gfm": "^4.0.0", "remark-reading-time": "^2.0.1", @@ -5786,6 +5788,14 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -6908,6 +6918,28 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -7021,6 +7053,17 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/framer-motion": { "version": "11.2.10", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.10.tgz", @@ -7789,6 +7832,17 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -8138,6 +8192,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-relative-url": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-relative-url/-/is-relative-url-4.0.0.tgz", + "integrity": "sha512-PkzoL1qKAYXNFct5IKdKRH/iBQou/oCC85QhXj6WKtUQBliZ4Yfd3Zk27RHu9KQG8r6zgvAA2AQKC9p+rqTszg==", + "dependencies": { + "is-absolute-url": "^4.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-set": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", @@ -9704,6 +9772,17 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -10038,6 +10117,41 @@ "node": ">= 10.13" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -11600,6 +11714,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-embed-images": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-embed-images/-/remark-embed-images-4.0.0.tgz", + "integrity": "sha512-c8qQMdhNTSO9TMMfbvf5CWI+HhIX5UZPTBVNaNSpxaEuRt0KCLFk68s5CQ7szbSERLnpos+c9K6xZoILWRh+eQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "is-relative-url": "^4.0.0", + "mime": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-flexible-code-titles": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/remark-flexible-code-titles/-/remark-flexible-code-titles-1.2.0.tgz", @@ -13255,6 +13385,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webpack": { "version": "5.91.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", diff --git a/package.json b/package.json index d0c1ba40..1d524a85 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "framer-motion": "^11.2.10", "mdx-bundler": "^10.0.2", "next": "14.2.3", + "node-fetch": "^3.3.2", "react": "^18.1.0", "react-aria": "^3.33.1", "react-dom": "^18.1.0", @@ -35,6 +36,7 @@ "rehype-autolink-headings": "^7.1.0", "rehype-prism-plus": "^2.0.0", "rehype-slug": "^6.0.0", + "remark-embed-images": "^4.0.0", "remark-flexible-code-titles": "^1.2.0", "remark-gfm": "^4.0.0", "remark-reading-time": "^2.0.1", diff --git a/rehypePlugins/rehype-image-src-modifier.mjs b/rehypePlugins/rehype-image-src-modifier.mjs index f0f438e4..aa5f095a 100644 --- a/rehypePlugins/rehype-image-src-modifier.mjs +++ b/rehypePlugins/rehype-image-src-modifier.mjs @@ -1,4 +1,7 @@ import { visit } from 'unist-util-visit'; +import fetch from 'node-fetch'; +import fs from 'fs' +import path from 'path'; function rehypeImageSrcModifier() { return (tree) => { @@ -29,9 +32,26 @@ function rehypeImageSrcModifier() { if (src.startsWith('./img')) { node.properties.src = src.replace('./img', '/oriimg/' + month); } - if (src.startsWith('img')) { + else if (src.startsWith('img')) { node.properties.src = src.replace('img', '/oriimg/' + month); } + else if (src.startsWith('https://i.imgur.com/')) { + let picName = src.replace('https://i.imgur.com/', ''); + fetch(src).then(response => { + if (!response.ok) { + console.error(`Failed to fetch ${url}: ${response.statusText}`); + } + return response.buffer(); + }) + .then(buffer => { + if (!fs.existsSync(path.join(process.cwd(), "public", "imgur"))) { + fs.mkdirSync(path.join(process.cwd(), "public", "imgur")); + } + // 将图片数据写入本地文件 + fs.writeFileSync(path.join(process.cwd(), "public", "imgur", picName), buffer); + }) + node.properties.src = "/imgur/" + picName; + } } }); };