Skip to content

Commit b9a5ee2

Browse files
committed
feat(core): support tags & index page in blog plugin
1 parent 60fd1c3 commit b9a5ee2

16 files changed

Lines changed: 670 additions & 255 deletions

File tree

apps/docs/content/blog/index.mdx

Lines changed: 0 additions & 5 deletions
This file was deleted.

apps/docs/content/docs/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"pages": ["blog", "---Documentation---", "..."]
2+
"pages": ["[Rss][Blog](/blog)", "---Documentation---", "..."]
33
}

apps/docs/press.config.tsx

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import { blog, docs } from "./.source/server";
66
import { lucideIconsPlugin } from "fumadocs-core/source/plugins/lucide-icons";
77
import { fumadocsMdx } from "fumapress/adapters/mdx";
88
import { flexsearchPlugin } from "fumapress/plugins/flexsearch";
9-
import { createDocsLayout } from "fumapress/layouts/docs";
10-
import { createLayoutSwitchAuto } from "fumapress/layouts/switch";
11-
import { createBlogLayout } from "fumapress/layouts/blog";
9+
import { createDocsLayoutPage } from "fumapress/layouts/docs";
10+
import { blogPlugin } from "fumapress/plugins/blog";
1211

1312
export default defineConfig({
1413
mode: "static",
@@ -56,7 +55,7 @@ export default defineConfig({
5655
},
5756
})
5857
.useAdapters(fumadocsMdx())
59-
.usePlugins(flexsearchPlugin(), llmsPlugin(), takumiPlugin())
58+
.usePlugins(flexsearchPlugin(), llmsPlugin(), takumiPlugin(), blogPlugin())
6059
.useLayouts({
6160
defaultProps() {
6261
return {
@@ -81,33 +80,11 @@ export default defineConfig({
8180
},
8281
};
8382
},
84-
page: createLayoutSwitchAuto({
85-
docs: createDocsLayout({
86-
render: () => ({
87-
pageProps: {
88-
tableOfContent: { style: "clerk" },
89-
},
90-
}),
91-
}),
92-
blog: createBlogLayout({
93-
indexPath: "/blog",
94-
render: () => ({
95-
layoutProps: {
96-
links: [
97-
{
98-
type: "main",
99-
url: "/",
100-
text: "Documentation",
101-
},
102-
{
103-
type: "main",
104-
url: "/blog",
105-
text: "Blog",
106-
active: "nested-url",
107-
},
108-
],
109-
},
110-
}),
83+
page: createDocsLayoutPage({
84+
render: () => ({
85+
pageProps: {
86+
tableOfContent: { style: "clerk" },
87+
},
11188
}),
11289
}),
11390
});

packages/core/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323
"./adapters/mdx/schema": "./dist/adapters/mdx/schema.js",
2424
"./layouts/blog": "./dist/layouts/blog.js",
2525
"./layouts/blog.index": "./dist/layouts/blog.index.js",
26+
"./layouts/blog.tags": "./dist/layouts/blog.tags.js",
2627
"./layouts/docs": "./dist/layouts/docs.js",
2728
"./layouts/home": "./dist/layouts/home.js",
2829
"./layouts/notebook": "./dist/layouts/notebook.js",
2930
"./layouts/root": "./dist/layouts/root.js",
3031
"./layouts/switch": "./dist/layouts/switch.js",
32+
"./plugins/blog": "./dist/plugins/blog.js",
3133
"./plugins/flexsearch": "./dist/plugins/flexsearch.js",
3234
"./plugins/llms.txt": "./dist/plugins/llms.txt.js",
3335
"./plugins/orama-search": "./dist/plugins/orama-search.js",
@@ -42,7 +44,7 @@
4244
"access": "public"
4345
},
4446
"scripts": {
45-
"dev": "tsdown --watch",
47+
"dev": "tsdown --watch --clean false",
4648
"build": "tsdown",
4749
"types:check": "tsc --noEmit"
4850
},
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { ConfigContext } from "@/config";
2+
import { cn } from "@/lib/cn";
3+
import { joinPathname } from "@/lib/join-pathname";
4+
import { AppContext, getCreationDate } from "@/lib/shared";
5+
import { BlogContext } from "@/plugins/blog";
6+
import { buttonVariants } from "fumadocs-ui/components/ui/button";
7+
import { CornerLeftUpIcon } from "lucide-react";
8+
import { Link } from "waku";
9+
10+
export function BlogItem<C extends ConfigContext>({
11+
page,
12+
date,
13+
}: {
14+
page: C["loaderConfig"]["page"];
15+
date: Date;
16+
}) {
17+
return (
18+
<Link
19+
to={page.url}
20+
className="flex flex-col bg-fd-card rounded-2xl border shadow-sm p-4 transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
21+
>
22+
<p className="font-medium">{page.data.title}</p>
23+
<p className="text-sm text-fd-muted-foreground">{page.data.description}</p>
24+
25+
<p className="mt-auto pt-4 text-xs text-fd-primary">{date.toDateString()}</p>
26+
</Link>
27+
);
28+
}
29+
30+
export async function OrderedBlogGrid<C extends ConfigContext>({
31+
posts,
32+
ctx,
33+
}: {
34+
posts: C["loaderConfig"]["page"][];
35+
ctx: AppContext<C>;
36+
}) {
37+
const currentDate = new Date(Date.now());
38+
const orderedPosts: { page: C["loaderConfig"]["page"]; date: Date }[] = [];
39+
40+
for (const page of posts) {
41+
const date = await getCreationDate(ctx, page);
42+
orderedPosts.push({ page, date: date ?? currentDate });
43+
}
44+
45+
orderedPosts.sort((a, b) => b.date.getTime() - a.date.getTime());
46+
47+
return (
48+
<div className="grid grid-cols-1 gap-2 mt-4 md:grid-cols-3 xl:grid-cols-4">
49+
{orderedPosts.map(({ page, date }) => (
50+
<BlogItem key={page.url} page={page} date={date} />
51+
))}
52+
</div>
53+
);
54+
}
55+
56+
export function LinkToHome<C extends ConfigContext>({
57+
lang,
58+
blog,
59+
}: {
60+
blog: BlogContext<C>;
61+
lang?: string;
62+
}) {
63+
if (!blog.indexPath) return;
64+
65+
return (
66+
<Link
67+
to={lang ? joinPathname(lang, blog.indexPath) : blog.indexPath}
68+
className={cn(
69+
buttonVariants({
70+
variant: "ghost",
71+
className: "text-fd-muted-foreground gap-2",
72+
}),
73+
)}
74+
>
75+
<CornerLeftUpIcon className="size-3.5" />
76+
Back to Home
77+
</Link>
78+
);
79+
}

packages/core/src/config.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,20 @@ export interface Config<C extends ConfigContext = ConfigContext> {
3030
/** adapter for content sources, use `fumadocs-mdx` if not specified */
3131
adapters?: Adapter[];
3232
i18n?: I18nConfig<C["lang"]>;
33-
meta?: {
34-
/** render meta tags for any pages */
35-
root?: (this: AppContext<C>) => ReactNode;
36-
37-
/** render meta tags for page */
38-
page?: (this: AppContext<C>, page: C["loaderConfig"]["page"]) => ReactNode;
39-
};
33+
meta?: MetaConfig<C>;
4034
}
4135

4236
export interface Layouts<C extends ConfigContext = ConfigContext> {
43-
root: ComponentType<AppContext<C> & { lang?: string; children: ReactNode }>;
44-
page: ComponentType<AppContext<C> & { lang?: string; slugs: string[] }>;
45-
notFound: ComponentType<AppContext<C> & { lang?: string }>;
37+
root: ComponentType<{ lang?: string; ctx: AppContext<C>; children: ReactNode }>;
38+
page: ComponentType<{ lang?: string; ctx: AppContext<C>; slugs: string[] }>;
39+
notFound: ComponentType<{ lang?: string; ctx: AppContext<C> }>;
4640

4741
/**
4842
* Define default props for page layouts, will be merged with current props.
4943
*/
5044
defaultProps?: (
5145
this: AppContext<C>,
52-
page: C["loaderConfig"]["page"],
46+
env: { lang: string | undefined },
5347
) => Awaitable<Omit<BaseLayoutProps, "children">>;
5448
}
5549

@@ -66,6 +60,14 @@ export interface I18nConfig<Lang extends string = string> extends Pick<
6660
};
6761
}
6862

63+
export interface MetaConfig<C extends ConfigContext = ConfigContext> {
64+
/** render meta tags for any pages */
65+
root?: (this: AppContext<C>) => ReactNode;
66+
67+
/** render meta tags for page */
68+
page?: (this: AppContext<C>, page: C["loaderConfig"]["page"]) => ReactNode;
69+
}
70+
6971
export interface SiteConfig {
7072
/** full URL of app, used for metadata generation*/
7173
baseUrl?: string;

packages/core/src/layouts/blog.index.tsx

Lines changed: 29 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,52 @@
1+
import { OrderedBlogGrid } from "@/components/blog";
12
import type { ConfigContext } from "@/config";
2-
import { getCreationDate, type AppContext } from "@/lib/shared";
3-
import path from "node:path";
4-
import type { ComponentType, ReactNode } from "react";
3+
import { cn } from "@/lib/cn";
4+
import { joinPathname } from "@/lib/join-pathname";
5+
import type { BlogIndexPage } from "@/plugins/blog";
6+
import { buttonVariants } from "fumadocs-ui/components/ui/button";
7+
import { ListIcon } from "lucide-react";
8+
import type { ReactNode } from "react";
59
import { Link } from "waku";
610

711
export interface BlogIndexPageOptions {
812
heading?: ReactNode;
913
description?: ReactNode;
1014
}
1115

12-
export type BlogIndexPage<C extends ConfigContext = ConfigContext> = ComponentType<
13-
AppContext<C> & {
14-
lang?: string;
15-
indexPage: C["loaderConfig"]["page"];
16-
blogDir: string;
17-
}
18-
>;
19-
2016
export function createBlogIndexPage<C extends ConfigContext = ConfigContext>({
2117
heading,
2218
description,
2319
}: BlogIndexPageOptions = {}): BlogIndexPage<C> {
24-
return async function BlogIndexPage(props) {
25-
const { lang, getLoader, indexPage, blogDir } = props;
26-
const source = await getLoader();
27-
28-
const currentDate = new Date(Date.now());
29-
const postPromises: Promise<{
30-
page: C["loaderConfig"]["page"];
31-
date: Date;
32-
}>[] = [];
33-
for (const page of source.getPages(lang)) {
34-
if (path.relative(blogDir, page.path).startsWith("..") || page === indexPage) continue;
35-
const datePromise = Promise.resolve(getCreationDate(props, page));
36-
postPromises.push(datePromise.then((date) => ({ page, date: date ?? currentDate })));
37-
}
38-
const posts = await Promise.all(postPromises);
39-
40-
posts.sort((a, b) => b.date.getTime() - a.date.getTime());
20+
return async function BlogIndexPage({ lang, blog, ctx }) {
21+
const source = await ctx.getLoader();
4122

4223
return (
4324
<>
44-
<div className="border-2 border-dashed border-fd-primary bg-fd-primary/10 p-4 z-2 md:p-8">
45-
<h1 className="text-3xl font-semibold">{heading ?? indexPage.data.title ?? "Blog"}</h1>
46-
<p className="mt-4 text-fd-primary overline decoration-fd-primary empty:hidden">
47-
{description ?? indexPage.data.description}
25+
<div className="flex flex-col gap-4 items-start border-2 border-dashed border-fd-primary rounded-xl bg-fd-primary/10 p-4 z-2 md:p-8">
26+
<h1 className="text-3xl font-semibold">{heading ?? "Blog"}</h1>
27+
<p className="text-fd-primary overline decoration-fd-primary empty:hidden">
28+
{description}
4829
</p>
49-
</div>
50-
51-
<div className="grid grid-cols-1 gap-2 mt-4 md:grid-cols-3 xl:grid-cols-4">
52-
{posts.map(({ page: post, date }) => (
30+
{blog.tagsPath !== false && (
5331
<Link
54-
key={post.url}
55-
to={post.url}
56-
className="flex flex-col bg-fd-card rounded-2xl border shadow-sm p-4 transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
32+
to={lang ? joinPathname(lang, blog.tagsPath) : blog.tagsPath}
33+
className={cn(
34+
buttonVariants({
35+
variant: "primary",
36+
}),
37+
"gap-2",
38+
)}
5739
>
58-
<p className="font-medium">{post.data.title}</p>
59-
<p className="text-sm text-fd-muted-foreground">{post.data.description}</p>
60-
61-
<p className="mt-auto pt-4 text-xs text-fd-primary">{date.toDateString()}</p>
40+
<ListIcon className="size-4" />
41+
All Tags
6242
</Link>
63-
))}
43+
)}
6444
</div>
45+
46+
<OrderedBlogGrid
47+
posts={source.getPages(lang).filter((page) => blog.isBlog.call(ctx, page))}
48+
ctx={ctx}
49+
/>
6550
</>
6651
);
6752
};

0 commit comments

Comments
 (0)