虽然我们认为数据和显示的强分离非常重要,但我们也理解将两者混合在一起的格式,例如MDX(包含嵌入式 JSX 组件的 Markdown)已成为开发人员流行且强大的创作格式。
Remix 通过两种方式内置支持在构建时使用 MDX
.mdx
文件用作您的路由模块之一.mdx
文件导入
到您的路由模块之一(在 app/routes
中)在 Remix 中开始使用 MDX 的最简单方法是创建一个路由模块。就像 app/routes
目录中的 .tsx
、.js
和 .jsx
文件一样,.mdx
(和 .md
)文件将参与基于自动文件系统的路由。
MDX 路由允许您定义元数据和标头,就像它们是基于代码的路由一样
---
meta:
- title: My First Post
- name: description
content: Isn't this awesome?
headers:
Cache-Control: no-cache
---
# Hello Content!
上面文档中 ---
之间的行称为“前置 matter”。您可以将它们视为文档的元数据,格式为YAML。
您可以在 MDX 中通过全局 attributes
变量引用您的前置 matter 字段
---
componentData:
label: Hello, World!
---
import SomeComponent from "~/components/some-component";
# Hello MDX!
<SomeComponent {...attributes.componentData} />
通过创建一个 app/routes/posts.first-post.mdx
,我们可以开始撰写一篇博文
---
meta:
- title: My First Post
- name: description
content: Isn't this just awesome?
---
# Example Markdown Post
You can reference your frontmatter data through "attributes". The title of this post is {attributes.meta.title}!
您甚至可以在您的 mdx 文件中导出此模块中的所有其他内容,就像在常规路由模块中一样,例如 loader
、action
和 handle
---
meta:
- title: My First Post
- name: description
content: Isn't this awesome?
headers:
Cache-Control: no-cache
handle:
someData: abc
---
import styles from "./first-post.css";
export const links = () => [
{ rel: "stylesheet", href: styles },
];
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export const loader = async () => {
return json({ mamboNumber: 5 });
};
export function ComponentUsingData() {
const { mamboNumber } = useLoaderData<typeof loader>();
return <div id="loader">Mambo Number: {mamboNumber}</div>;
}
# This is some markdown!
<ComponentUsingData />
除了路由级别的 MDX 之外,您还可以像常规 JavaScript 模块一样在任何地方自己导入这些文件。
当您导入
.mdx
文件时,模块的导出为
import Component, {
attributes,
filename,
} from "./first-post.mdx";
以下示例演示了如何使用 MDX 构建简单的博客,包括帖子本身的各个页面和显示所有帖子的索引页面。
import { json } from "@remix-run/node"; // or cloudflare/deno
import { Link, useLoaderData } from "@remix-run/react";
// Import all your posts from the app/routes/posts directory. Since these are
// regular route modules, they will all be available for individual viewing
// at /posts/a, for example.
import * as postA from "./posts/a.mdx";
import * as postB from "./posts/b.md";
import * as postC from "./posts/c.md";
function postFromModule(mod) {
return {
slug: mod.filename.replace(/\.mdx?$/, ""),
...mod.attributes.meta,
};
}
export async function loader() {
// Return metadata about each of the posts for display on the index page.
// Referencing the posts here instead of in the Index component down below
// lets us avoid bundling the actual posts themselves in the bundle for the
// index page.
return json([
postFromModule(postA),
postFromModule(postB),
postFromModule(postC),
]);
}
export default function Index() {
const posts = useLoaderData<typeof loader>();
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link to={post.slug}>{post.title}</Link>
{post.description ? (
<p>{post.description}</p>
) : null}
</li>
))}
</ul>
);
}
显然,对于包含数千个帖子的博客来说,这不是一个可扩展的解决方案。现实地说,写作很难,所以如果您的博客开始遭受过多的内容困扰,这是一个很棒的问题。如果您获得了 100 篇帖子(恭喜!),我们建议您重新考虑您的策略,并将您的帖子转换为存储在数据库中的数据,以便您不必每次修复错别字时都重新构建和重新部署您的博客。您甚至可以继续使用MDX Bundler 使用 MDX。
如果您希望配置自己的 remark 插件,可以通过 remix.config.js
的 mdx
导出进行配置
const {
remarkMdxFrontmatter,
} = require("remark-mdx-frontmatter");
// can be an sync / async function or an object
exports.mdx = async (filename) => {
const [rehypeHighlight, remarkToc] = await Promise.all([
import("rehype-highlight").then((mod) => mod.default),
import("remark-toc").then((mod) => mod.default),
]);
return {
remarkPlugins: [remarkToc],
rehypePlugins: [rehypeHighlight],
};
};