如何使用 SolidStart 和 MDX 创建我的博客

博客地址:https://andi.dev/blog/how-solid-start-blog

SolidStart 之于 SolidJs 就像 NextJs 之于 React。

它是一个通用的“全栈”框架,允许您使用 SSR、CSR、SSG 和所有其他缩写词。

它附带 SSG 文档和 `with-mdx` 入门模板,可让您快速开始使用由 markdown 支持的静态网站。

**那么我为什么要写这个呢?**

因为技术博客有一些额外的要求,默认情况下不支持,所以我必须自己弄清楚。希望这能为其他人节省一些时间。

我认为您已经熟悉 SolidStart 的工作原理,尤其是基于文件的路由器。

如果您不是的话,官方文档会比我更好地解释这一点。

通过编程列出所有帖子

`FileRoutes` 路由器很棒。但它不会公开其路由上的任何信息。

大多数(如果不是全部)博客在主页上都有某种最新帖子列表,或者在某个地方有存档。理想情况下,我们会根据帖子目录中的文件自动生成这些列表。

现在,我这里总共只有 5 个帖子。我完全可以只在某处用代码手动保存帖子列表,并在添加新帖子文件时更新它。

但我宁愿花一天时间弄清楚如何实现自动化,而不是花 5 分钟手动完成。

我发现最轻松的设置方法是使用自定义 vite 插件。

vite 插件

vite 插件只是一个符合 vite 插件 api 的对象。

import type { Plugin } from "vite";

export const blogPostsPlugin = (): Plugin => {
  return {
    name: "blog-posts-gen",
    async buildStart() {
      processFiles();
    },
    configureServer(server) {
      server.watcher.on("change", (filePath) => {
        if (filePath.includes("/src/routes/blog")) {
          processFiles();
        }
      });
    },
  };
};

这是整个插件。

它做两件事:

  • 在构建过程开始时调用 processFiles(这是当您为产品构建网站时)
  • 它挂接到 vite 的开发服务器监听器,并在 /src/routes/blog 目录中的任何文件发生改变时调用 processFiles。
  • 确保将其添加到你的应用程序配置中

    export default defineConfig({
      ...
        vite: {
            plugins: [
                ...other plugins
                blogPostsPlugin(), // Add it here
            ],
        },
      ...
    });

    处理文件

    你的“processFiles”函数可能看起来与我的非常不同,但我的功能如下:

    import { resolve, join } from "node:path";
    import { readdirSync, statSync, writeFileSync } from "node:fs";
    
    const processFiles = () => {
        const outputFile = resolve("src/data/posts.json");
        const blogDir = resolve("src/routes/blog");
        const files = readdirSync(blogDir);
    
        const blogPosts = files
            .filter((file) => 
            statSync(join(blogDir, file)).isFile() && file.endsWith(".mdx")
          )
            .map((file) => ({slug: file.replace(".mdx", "")}));
    
        writeFileSync(outputFile, JSON.stringify(blogPosts, null, 2), "utf-8");
    };

    它获取博客目录中所有以 .mdx 结尾的文件,并将它们映射到类型为 `{slug: string}[]` 的 json 列表。

    然后将该列表写入“src/data/posts.json”。

    如果文件夹结构如下

    Folder structure

    那么 json 文件将如下所示

    [
      { "slug": "post-1" },
      { "slug": "post-2" },
      { "slug": "post-3" }
    ]

    我正在使用 typescript,因此我有一个额外的文件,用于导入 JSON 并向其中添加类型:

    import JSONPosts from "./posts.json";
    type Post = { slug: string };
    export const posts: Post[] = JSONPosts;

    使用帖子列表

    现在它可以作为模块导出,您可以导入它并在实体组件的任何位置使用它:

    
      {post => {post.slug}}
    

    对我来说,最好的部分是,只要“blog”目录发生变化,json 文件就会更新。

    这将触发 vite 的 HMR 并在您本地开发时自动刷新依赖于它的任何模块。

    将帖子的元数据与内容保存在同一文件中

    帖子不仅包含内容,通常还包含一些与之相关的元数据。

    我想支持的有:

  • 标题
  • 发布日期
  • 标签列表
  • 我对将事物放在同一个域下很挑剔。

    在这种情况下,我想将帖子的元数据保存在与内容所在的同一个 mdx 文件中。

    向 markdown 文件添加元数据的常用方式是使用 frontmatter。

    我还想从我的动态帖子列表中使用这些元数据。

    你可能已经注意到上面的组件正在使用 slug 来呈现帖子链接:

    {post.slug}

    但实际上应该使用帖子的标题

    {post.title}

    解析前言

    我最终修改了 `processFiles` 函数来解析文件 frontmatter,并结合使用 to-vfile 和 vfile-matter。

    const processFiles = () => {
        const outputFile = resolve("src/data/posts.json");
        const blogDir = resolve("src/routes/blog");
        const files = readdirSync(blogDir);
    
        const blogPosts = files
            .filter((file) => 
            statSync(join(blogDir, file)).isFile() && file.endsWith(".mdx")
          )
            .map((file) => {
    
          // Turn each of the post files into vfiles
                const vfile = readSync(resolve("src/routes/blog", file));
          // Parse their frontmatter
                matter(vfile);
    
          return { 
            // Add the frontmatter properties to each post's metadata
                    ...(f.data.matter as object),
            slug: file.replace(".mdx", "")
          }
        });
    
        writeFileSync(outputFile, JSON.stringify(blogPosts, null, 2), "utf-8");
    };

    我使用 yaml 编写前言,因此当我将以下内容添加到帖子顶部时:

    ---
    title: This is my first post!
    date: 2024-09-04
    tags:
      - solidjs
      - solid-start
    ---

    那么元数据 json 将变成:

    {
      "slug": "post-1",
      "title": "This is my first post!",
      "date": "2024-09-04",
      "tags": ["solidjs", "solid-start"]
    }

    然后我可以随时使用该元数据,特别是在呈现帖子列表时:

    {post.title}

    **重要信息:**

    您还应该安装“remark-frontmatter”并将其添加到 app.config 中的 remarkPlugins

    export default defineConfig({
      ...
        vite: {
            plugins: [
                mdx.withImports({})({
                    remarkPlugins: [remarkFrontmatter], // Add it here
                }),
            ],
        },
      ...
    });

    这样做的原因是,您希望在 solidjs mdx 管道将 mdx 文件转换为静态 html 时排除 frontmatter。

    如果不这样做,当您导航到帖子页面时,您会看到前置内容呈现为 html。

    编译时代码高亮

    HackerNews 上脾气暴躁的工程师们向我抱怨。我希望即使有人禁用了 javascript,也能支持代码高亮显示。

    这意味着将突出显示过程从在客户端(浏览器,使用 js)上运行移到在生成静态 HTML 时运行。

    我使用 refractor 来实现这一点。它是 `prismjs` 的包装器,可让您在虚拟文件上进行突出显示。

    为了将其纳入 solid-mdx 构建过程,我必须创建自己的自定义 rehype 插件:

    import { visit } from "unist-util-visit";
    import { toString as nodeToString } from "hast-util-to-string";
    import { refractor } from "refractor";
    import tsx from "refractor/lang/tsx.js";
    
    refractor.register(tsx);
    
    export const mdxPrism = () => {
        return (tree: any) => {
            visit(tree, "element" as any, visitor);
        };
    
        function visitor(node: any, index: number | undefined, parent: any) {
            if (parent.type !== "mdxJsxFlowElement") {
                return;
            }
    
            const attrs = parent.attributes.reduce((a: any, c: any) => {
                if (c.type === "mdxJsxAttribute") {
                    a[c.name] = c.value;
                }
                return a;
            }, {});
    
            const lang = attrs.lang;
            if (!lang) {
                return;
            }
    
            const result = refractor.highlight(nodeToString(node), lang);
            node.children = result.children;
        }
    };

    我不会详细介绍它的具体功能。它的要点是从解析的 markdown 中找到代码块并对其使用 refractor。

    它也需要添加到应用程序配置中,在 rehypePlugins 下

    export default defineConfig({
      ...
        vite: {
            plugins: [
                mdx.withImports({})({
                    rehypePlugins: [mdxPrism], // Add it here
                }),
            ],
        },
      ...
    });

    Refractor 生成与 prism 相同的类名,因此只要您加载了 prism 主题 css 文件,它就会显示一些漂亮的突出显示。

    完整代码示例

    我将该网站的代码保存在公共 github repo 中。

    我试图使这篇文章保持简短,所以如果我遗漏了什么,请随意查看其中的完整工作实现。