Remix:以编程方式生成所有路由的链接



我正在用Remix创建一个博客。

Remix支持MDX作为路由,这对我来说是完美的,因为我可以把我的博客文章写成.mdx文件,它们自然会成为路由。

但是,如果您访问索引路由-我希望显示所有文章的链接列表,(理想情况下按创建日期或.mdx文件中的某种元数据排序)

我在Remix文档中找不到一个自然的方法来做到这一点。

我得到的最佳解决方案是,我会运行一个脚本作为我的构建过程的一部分,检查routes/文件夹并生成一个TableOfContents.tsx文件,索引路由可以使用。

是否有开箱即用的解决方案?

remix文档确实暗示了这一点,但似乎没有提供一个可靠的建议。

显然,这不是一个可扩展的解决方案,为数千篇文章的博客。现实地说,写作是很难的,所以如果你的博客开始受到太多内容的困扰,这是一个可怕的问题。如果你的帖子达到100篇(恭喜你!),我们建议你重新考虑你的策略,把你的帖子变成存储在数据库中的数据,这样你就不必每次修复一个错别字都要重新构建和重新部署你的博客。

(就我个人而言,我不认为每次修复一个错别字都重新部署是一个太大的问题,更重要的是我不想每次添加新帖子时都要手动添加链接)。

为了子孙后代,我将公布我目前的解决方案。

我在构建时运行这个脚本,它检查所有博客文章并创建一个<ListOfArticles>组件。

import { readdir, writeFile, appendFile } from 'node:fs/promises';
const PATH_TO_ARTICLES_COMPONENT = "app/generated/ListOfArticles.tsx";
const PATH_TO_BLOG_POSTS = "app/routes/posts"
async function generateListOfArticles() {
try {
const files = await readdir(PATH_TO_BLOG_POSTS);

await writeFile(PATH_TO_ARTICLES_COMPONENT, `
export const ListOfArticles = () => {

return <ul>`);
for (const file of files) {
if (!file.endsWith(".mdx")) {
console.warn(`Found a non-mdx file in ${PATH_TO_BLOG_POSTS}: ${file}`)
}
else {
const fileName = file.split('.mdx')[0];
await appendFile(PATH_TO_ARTICLES_COMPONENT, `
<li>
<a
href="/posts/${fileName}"
>
${fileName}        </a>
</li>
`)
}

}
appendFile(PATH_TO_ARTICLES_COMPONENT, `
</ul>
}
`)

} catch (err) {
throw err;
}
}
generateListOfArticles().then(() => {
console.info(`Successfully generated ${PATH_TO_ARTICLES_COMPONENT}`)
})

是相当初级的—可以通过检查MDX文件的元数据部分来扩展以获得适当的标题,执行排序顺序等。

这不是你想要的,但我用你的解决方案作为灵感来找出我自己的问题的解决方案:为路由生成类型,以便类型检查器可以确保在应用程序中没有断开的链接。

这是我的脚本,generate-app-path-type.ts:
import { appendFile, readdir, writeFile } from "node:fs/promises";
const PATH_TO_APP_PATHS = "app/utils/app-path.generated.ts";
const PATHS_PATH = "app/routes";
async function collectFiles(currentPaths: string[], currentPath = "") {
const glob = await readdir(PATHS_PATH + currentPath, { withFileTypes: true });
for (const entry of glob) {
if (entry.isFile()) {
currentPaths.push(currentPath + "/" + entry.name);
} else if (entry.isDirectory()) {
await collectFiles(currentPaths, currentPath + "/" + entry.name);
}
}
}
// NOTE: the `withoutNamespacing` regex only removes top level namespacing like routes/__auth but
// wouldn't catch namespacing like routes/my/__new-namespace
function filenameToRoute(filename: string): string {
const withoutExtension = filename.split(/.tsx?$/)[0];
const withoutIndex = withoutExtension.split(//index$/)[0];
const withoutNamespacing = withoutIndex.replace(/^/__w+/, "");
// Return root path in the case of "index.tsx"
return withoutNamespacing || "/";
}
async function generateAppPathType() {
const filenames: string[] = [];
await collectFiles(filenames);
await writeFile(PATH_TO_APP_PATHS, `export type AppPath =n`);
const remixRoutes = filenames.slice(0, filenames.length - 1).map(filenameToRoute);
// only unique routes, and alphabetized please
const routes = [...new Set(remixRoutes)].sort();
for (const route of routes) {
await appendFile(PATH_TO_APP_PATHS, `  | "${route}"n`);
}
await appendFile(
PATH_TO_APP_PATHS,
`  | "${filenameToRoute(filenames[filenames.length - 1])}";n`,
);
}
generateAppPathType().then(() => {
// eslint-disable-next-line no-console
console.info(`Successfully generated ${PATH_TO_APP_PATHS}`);
});

脚本生成如下类型:

export type AppPath =
| "/"
| "/login";

我在一个实用程序文件中使用这种类型:

import type { AppPath } from "./app-path.generated";
import { SERVER_URL } from "./env";
export function p(path: AppPath): AppPath {
return path;
}
export function url(path: AppPath): string {
return new URL(path, SERVER_URL).toString();
}

最后,当我需要一个路径时(比如当我重定向用户时),我可以这样做,这比字符串字面值多一点输入,但根据类型检查器是一个确认的URL:

p("/login") // or url("/login")

最新更新