如何使用 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(); } }); }, }; };
这是整个插件。
它做两件事:
确保将其添加到你的应用程序配置中
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”。
如果文件夹结构如下

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