feat(blog): implement blog structure with post listing, tagging, and layout enhancements (#1962)

* feat(blog): implement blog structure with post listing and tagging functionality

* feat(blog): enhance blog layout and post metadata display with new components

* fix(blog): address PR #1962 review feedback and fix lint issues (#14)

* fix: format

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
JeffJiang
2026-04-10 20:24:52 +08:00
committed by GitHub
parent 809b341350
commit 7dc0c7d01f
16 changed files with 868 additions and 11 deletions
+7 -7
View File
@@ -1,13 +1,12 @@
import type { PageMapItem } from "nextra";
import { getPageMap } from "nextra/page-map";
import { Footer, Layout } from "nextra-theme-docs";
import { Layout } from "nextra-theme-docs";
import { Footer } from "@/components/landing/footer";
import { Header } from "@/components/landing/header";
import { getLocaleByLang } from "@/core/i18n/locale";
import "nextra-theme-docs/style.css";
const footer = <Footer>MIT {new Date().getFullYear()} © Nextra.</Footer>;
const i18n = [
{ locale: "en", name: "English" },
{ locale: "zh", name: "中文" },
@@ -15,7 +14,7 @@ const i18n = [
function formatPageRoute(base: string, items: PageMapItem[]): PageMapItem[] {
return items.map((item) => {
if ("route" in item) {
if ("route" in item && !item.route.startsWith(base)) {
item.route = `${base}${item.route}`;
}
if ("children" in item && item.children) {
@@ -29,6 +28,7 @@ export default async function DocLayout({ children, params }) {
const { lang } = await params;
const locale = getLocaleByLang(lang);
const pages = await getPageMap(`/${lang}`);
const pageMap = formatPageRoute(`/${lang}/docs`, pages);
return (
<Layout
@@ -39,9 +39,9 @@ export default async function DocLayout({ children, params }) {
locale={locale}
/>
}
pageMap={formatPageRoute(`/${lang}/docs`, pages)}
docsRepositoryBase="https://github.com/bytedance/deerflow/tree/main/frontend/src/app/content"
footer={footer}
pageMap={pageMap}
docsRepositoryBase="https://github.com/bytedance/deerflow/tree/main/frontend/src/content"
footer={<Footer />}
i18n={i18n}
// ... Your additional layout options
>
@@ -0,0 +1,178 @@
import { notFound } from "next/navigation";
import { importPage } from "nextra/pages";
import { cache } from "react";
import { PostList, PostMeta } from "@/components/landing/post-list";
import {
BLOG_LANGS,
type BlogLang,
formatTagName,
getAllPosts,
getBlogIndexData,
getPreferredBlogLang,
} from "@/core/blog";
import { getI18n } from "@/core/i18n/server";
import { useMDXComponents as getMDXComponents } from "../../../mdx-components";
// eslint-disable-next-line @typescript-eslint/unbound-method
const Wrapper = getMDXComponents().wrapper;
function isBlogLang(value: string): value is BlogLang {
return BLOG_LANGS.includes(value as BlogLang);
}
const loadBlogPage = cache(async function loadBlogPage(
mdxPath: string[] | undefined,
preferredLang?: (typeof BLOG_LANGS)[number],
) {
const slug = mdxPath ?? [];
const matches = await Promise.all(
BLOG_LANGS.map(async (lang) => {
try {
// Try every localized source for the same public /blog slug,
// then pick the best match for the current locale.
const page = await importPage([...slug], lang);
return { lang, page };
} catch {
return null;
}
}),
);
const availableMatches = matches.filter(
(match): match is NonNullable<(typeof matches)[number]> => match !== null,
);
if (availableMatches.length === 0) {
return null;
}
const selected =
(preferredLang
? availableMatches.find(({ lang }) => lang === preferredLang)
: undefined) ?? availableMatches[0];
if (!selected) {
return null;
}
return {
...selected.page,
lang: selected.lang,
metadata: {
...selected.page.metadata,
languages: availableMatches.map(({ lang }) => lang),
},
slug,
};
});
export async function generateMetadata(props) {
const params = await props.params;
const mdxPath = params.mdxPath ?? [];
const { locale } = await getI18n();
const preferredLang = getPreferredBlogLang(locale);
if (mdxPath.length === 0) {
return {
title: "Blog",
};
}
if (mdxPath[0] === "tags" && mdxPath[1]) {
return {
title: formatTagName(mdxPath[1]),
};
}
const page = await loadBlogPage(mdxPath, preferredLang);
if (!page) {
return {};
}
return page.metadata;
}
export default async function Page(props) {
const params = await props.params;
const searchParams = await props.searchParams;
const mdxPath = params.mdxPath ?? [];
const { locale } = await getI18n();
const localePreferredLang = getPreferredBlogLang(locale);
const queryLang = searchParams?.lang;
const preferredLang =
typeof queryLang === "string" && isBlogLang(queryLang)
? queryLang
: localePreferredLang;
if (mdxPath.length === 0) {
const posts = await getAllPosts(preferredLang);
return (
<Wrapper
toc={[]}
metadata={{ title: "All Posts", filePath: "blog/index.mdx" }}
sourceCode=""
>
<PostList title="All Posts" posts={posts} />
</Wrapper>
);
}
if (mdxPath[0] === "tags" && mdxPath[1]) {
let tag: string;
try {
tag = decodeURIComponent(mdxPath[1]);
} catch {
notFound();
}
const title = formatTagName(tag);
const { posts } = await getBlogIndexData(preferredLang, { tag });
if (posts.length === 0) {
notFound();
}
return (
<Wrapper
toc={[]}
metadata={{ title, filePath: "blog/index.mdx" }}
sourceCode=""
>
<PostList
title={title}
description={`${posts.length} posts with the tag “${title}`}
posts={posts}
/>
</Wrapper>
);
}
const page = await loadBlogPage(mdxPath, preferredLang);
if (!page) {
notFound();
}
const { default: MDXContent, toc, metadata, sourceCode, lang, slug } = page;
const postMetaData = metadata as {
date?: string;
languages?: string[];
tags?: unknown;
};
return (
<Wrapper toc={toc} metadata={metadata} sourceCode={sourceCode}>
<PostMeta
currentLang={lang}
date={
typeof postMetaData.date === "string" ? postMetaData.date : undefined
}
languages={postMetaData.languages}
pathname={slug.length === 0 ? "/blog" : `/blog/${slug.join("/")}`}
/>
<MDXContent {...props} params={{ ...params, lang, mdxPath: slug }} />
</Wrapper>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { Layout } from "nextra-theme-docs";
import { Footer } from "@/components/landing/footer";
import { Header } from "@/components/landing/header";
import { getBlogIndexData } from "@/core/blog";
import "nextra-theme-docs/style.css";
export default async function BlogLayout({ children }) {
const { pageMap } = await getBlogIndexData();
return (
<Layout
navbar={<Header className="relative max-w-full px-10" homeURL="/" />}
pageMap={pageMap}
sidebar={{ defaultOpen: true }}
docsRepositoryBase="https://github.com/bytedance/deerflow/tree/main/frontend/src/content"
footer={<Footer />}
>
{children}
</Layout>
);
}
+24
View File
@@ -0,0 +1,24 @@
import { PostList } from "@/components/landing/post-list";
import { getAllPosts, getPreferredBlogLang } from "@/core/blog";
import { getI18n } from "@/core/i18n/server";
import { useMDXComponents as getMDXComponents } from "../../../mdx-components";
// eslint-disable-next-line @typescript-eslint/unbound-method
const Wrapper = getMDXComponents().wrapper;
export const metadata = {
title: "All Posts",
filePath: "blog/index.mdx",
};
export default async function PostsPage() {
const { locale } = await getI18n();
const posts = await getAllPosts(getPreferredBlogLang(locale));
return (
<Wrapper toc={[]} metadata={metadata} sourceCode="">
<PostList title={metadata.title} posts={posts} />
</Wrapper>
);
}
+51
View File
@@ -0,0 +1,51 @@
import { notFound } from "next/navigation";
import { PostList } from "@/components/landing/post-list";
import {
formatTagName,
getBlogIndexData,
getPreferredBlogLang,
} from "@/core/blog";
import { getI18n } from "@/core/i18n/server";
import { useMDXComponents as getMDXComponents } from "../../../../mdx-components";
// eslint-disable-next-line @typescript-eslint/unbound-method
const Wrapper = getMDXComponents().wrapper;
export async function generateMetadata(props) {
const params = await props.params;
return {
title: formatTagName(params.tag),
filePath: "blog/index.mdx",
};
}
export default async function TagPage(props) {
const params = await props.params;
const tag = params.tag;
const { locale } = await getI18n();
const { posts } = await getBlogIndexData(getPreferredBlogLang(locale), {
tag,
});
if (posts.length === 0) {
notFound();
}
const title = formatTagName(tag);
return (
<Wrapper
toc={[]}
metadata={{ title, filePath: "blog/index.mdx" }}
sourceCode=""
>
<PostList
title={title}
description={`${posts.length} posts with the tag “${title}`}
posts={posts}
/>
</Wrapper>
);
}