У меня есть сайт на next.js с mdx. Там у меня есть статьи, у которых есть теги, они указаны во фронтметтере mdx файла статьи вот так
tags: ['functions', 'javascript', 'powerful code', 'js']
. Еще у меня есть маршрутизация по тегам — по пути pages/blog/tag/[tag].js, и создаются страницы, где собраны все статьи с соответствующими тегами. Ну в общем банальная вещь для блога.
Вот так выглядит моя служебная функция, которая создает все необходимое для генерации статей по нужным маршрутам:
import fs from 'fs';
import matter from 'gray-matter';
export function getAllPosts() {
const files = fs.readdirSync('./content/posts');
const posts = files
.map((fileName) => {
const slug = fileName.replace(/\.mdx$/, '');
const { frontmatter } = getPostBySlug(slug);
return {
slug,
...frontmatter,
};
})
return posts;
}
export function getPostBySlug(slug) {
const fileName = fs.readFileSync(`content/posts/${slug}.mdx`, 'utf-8');
const { data: frontmatter, content } = matter(fileName);
return {
frontmatter,
content,
};
}
export function getAllWork() {
const data = fs.readFileSync('content/work/data.json', 'utf-8');
const jsonData = JSON.parse(data);
return jsonData.work;
}
export function getAllPostsByTag({ tag }) {
const posts = getAllPosts();
return posts.filter((post) => post.tags.includes(tag));
}
getAllPostsByTag
в самом низу занимается по сути фильтрованием фронтметтера на предмет поиска тегов. Вот как выглядит файл
pages/blog/tag/[tag].js
:
import React, { useState, useEffect } from "react"
import {getAllPosts, getAllPostsByTag} from "@/lib/getAllData";
import Head from "next/head";
import Article from "@/components/article";
export async function getStaticPaths() {
const posts = getAllPosts();
const tags = new Set(posts.flatMap((post) => post.tags));
return {
paths: [...tags].map((tag) => {
return {
params: {
tag
}
}
}),
fallback: false,
};
}
export async function getStaticProps({ params: { tag } }) {
const posts = getAllPostsByTag({tag});
return {
props: {
posts,
tag
},
};
}
export default function Tag({ posts, tag }) {
const numberPosts = 2
// State for the list
const [list, setList] = useState([...posts.slice(0, numberPosts)])
// State to trigger oad more
const [loadMore, setLoadMore] = useState(false)
// State of whether there is more to load
const [hasMore, setHasMore] = useState(posts.length > numberPosts)
// Load more button click
const handleLoadMore = () => {
setLoadMore(true)
}
// Handle loading more articles
useEffect(() => {
if (loadMore && hasMore) {
const currentLength = list.length
const isMore = currentLength < posts.length
const nextResults = isMore
? posts.slice(currentLength, currentLength + numberPosts)
: []
setList([...list, ...nextResults])
setLoadMore(false)
}
}, [loadMore, hasMore]) //eslint-disable-line
//Check if there is more
useEffect(() => {
const isMore = list.length < posts.length
setHasMore(isMore)
}, [list]) //eslint-disable-line
return (
<div>
<Head>
<title>NextJS Blog</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/public/favicon.ico" />
</Head>
<section className='px-6'>
<div className='max-w-4xl mx-auto'>
<h1 className='text-3xl font-bold mb-6 p-4'>All `{tag}` posts</h1>
{list.map((post) => (
<Article key={post.slug} className='border-b-2' post={post} />
))}
</div>
{hasMore ? (
<div><button onClick={handleLoadMore}>Еще статьи</button></div>
) : (
<div><button disabled>Больше нет статей</button></div>
)}
</section>
</div>
)
}
Дело в том, что маршруты создаются нормально, когда мой тег во фронтметтере моего .mdx файла написан на английском языке (латинскими буквами) и одним словом (например, ‘javascript’ или ‘nextjs’), но если мой тег написан на Кириллице и/или в два слова (например, 'мощный код' или 'super code'), то никакие маршруты не генерируются корректно.
В моем компоненте, который отображает статьи в списке статей везде, где этот список используется, я могу использовать свои собственные функции, такие как «транслеттер» и Lodash, которые транслитируют кириллицу и делают кебаб из тегов, которые состоят из нескольких слов. С помощью этих опций я могу менять url под каждым тегом, но самих страниц нет:
// компонент статьи
import Link from 'next/link'
import Date from '@/lib/date';
import { transliterate } from '@/lib/transletter';
const _ = require("lodash")
const getTagLink = (tag) => {
return (
<Link href={`/blog/tag/${_.kebabCase(transliterate(tag))}`} key={tag}>
{tag}
</Link>
);
};
export default function Article({ post }) {
return (
<article className={`bg-white p-4`}>
<Link href={`/blog/${post.slug}`}>
<h3 className='text-2xl mb-2 font-medium hover:text-red-400 cursor-pointer'>
{post.title}
</h3>
</Link>
<span className='text-gray-600 mb-4 block'>
<Date dateString={post.date} /> | {post.tags.map(tag => getTagLink(tag)).reduce((prev, curr) => [prev, ', ', curr])}
</span>
<p>{post.description}</p>
</article>
);
}
Этот самый
${_.kebabCase(transliterate(tag))}
все делает правильно - транслитерирует кириллические символы для url и добавляет дефис между словами, то есть вместо
blog/tag/мощный код
получается
blog/tag/moschnyi-kod
. Однако у меня нет такого маршрута, потому что [tag].js принимает имя тега буквально — как оно написано.
Как мне добавить что-то вроде _.kebabCase(transliterate(tag)) в мои функции getStaticPath или еще куда-то, чтобы все работало как надо, чтобы генерировался нужный динамический маршрут, где страница тега будет получать url с переведенным на латиницу именем тега и дефисами между словами? Возможно это вообще?