mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-21 07:26:50 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user