-
Notifications
You must be signed in to change notification settings - Fork 1
Fix homepage font color and add blog section with in-depth analytical articles using fumadocs #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
93865a1
650c6f2
8e71906
dee0344
e1063f6
929b5b3
5967a13
7f30e49
9bb056c
526ccb0
c70f4ca
0115e54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| import { notFound } from 'next/navigation'; | ||
| import { blog } from '@/app/source'; | ||
| import defaultMdxComponents from 'fumadocs-ui/mdx'; | ||
| import { HomeLayout } from 'fumadocs-ui/layouts/home'; | ||
| import { baseOptions } from '@/app/layout.config'; | ||
| import Link from 'next/link'; | ||
| import { ArrowLeft } from 'lucide-react'; | ||
|
|
||
| // Extended type for blog post data | ||
| interface BlogPostData { | ||
| title: string; | ||
| description?: string; | ||
| author?: string; | ||
| date?: string; | ||
| tags?: string[]; | ||
| body: React.ComponentType; | ||
| } | ||
|
|
||
| export default async function BlogPage({ | ||
| params, | ||
| }: { | ||
| params: Promise<{ lang: string; slug?: string[] }>; | ||
| }) { | ||
| const { slug } = await params; | ||
|
|
||
| // If no slug, show blog index | ||
| if (!slug || slug.length === 0) { | ||
| const posts = blog.getPages(); | ||
|
|
||
| return ( | ||
| <HomeLayout {...baseOptions}> | ||
| <main className="container max-w-5xl mx-auto px-4 py-16"> | ||
| <div className="mb-12"> | ||
| <h1 className="text-4xl font-bold mb-4">Blog</h1> | ||
| <p className="text-lg text-fd-foreground/80"> | ||
| Insights, updates, and best practices from the ObjectStack team. | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="grid gap-8 md:grid-cols-2"> | ||
| {posts.map((post) => { | ||
| const postData = post.data as unknown as BlogPostData; | ||
| return ( | ||
| <Link | ||
| key={post.url} | ||
| href={post.url} | ||
| className="group block rounded-lg border border-fd-border bg-fd-card p-6 transition-all hover:border-fd-primary/30 hover:shadow-md" | ||
| > | ||
| <div className="mb-3"> | ||
| <h2 className="text-2xl font-semibold mb-2 group-hover:text-fd-primary transition-colors"> | ||
| {postData.title} | ||
| </h2> | ||
| {postData.description && ( | ||
| <p className="text-fd-foreground/70"> | ||
| {postData.description} | ||
| </p> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="flex items-center gap-4 text-sm text-fd-foreground/70"> | ||
| {postData.date && ( | ||
| <time dateTime={postData.date}> | ||
| {new Date(postData.date).toLocaleDateString('en-US', { | ||
| year: 'numeric', | ||
| month: 'long', | ||
| day: 'numeric', | ||
| })} | ||
| </time> | ||
| )} | ||
| {postData.author && ( | ||
| <span>By {postData.author}</span> | ||
| )} | ||
| </div> | ||
|
|
||
| {postData.tags && postData.tags.length > 0 && ( | ||
| <div className="mt-4 flex flex-wrap gap-2"> | ||
| {postData.tags.map((tag: string) => ( | ||
| <span | ||
| key={tag} | ||
| className="inline-flex items-center rounded-full bg-fd-primary/10 px-2.5 py-0.5 text-xs font-medium text-fd-primary" | ||
| > | ||
| {tag} | ||
| </span> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </Link> | ||
| ); | ||
| })} | ||
| </div> | ||
|
|
||
| {posts.length === 0 && ( | ||
| <div className="text-center py-12"> | ||
| <p className="text-fd-foreground/70">No blog posts yet. Check back soon!</p> | ||
| </div> | ||
| )} | ||
| </main> | ||
| </HomeLayout> | ||
| ); | ||
| } | ||
|
|
||
| // Show individual blog post | ||
| const page = blog.getPage(slug); | ||
|
|
||
| if (!page) { | ||
| notFound(); | ||
| } | ||
|
|
||
| const pageData = page.data as unknown as BlogPostData; | ||
| const MDX = page.data.body; | ||
|
|
||
| return ( | ||
| <HomeLayout {...baseOptions}> | ||
| <main className="container max-w-4xl mx-auto px-4 py-16"> | ||
| <Link | ||
| href="/blog" | ||
| className="inline-flex items-center gap-2 text-sm text-fd-foreground/70 hover:text-fd-foreground mb-8 transition-colors" | ||
| > | ||
| <ArrowLeft className="w-4 h-4" /> | ||
| Back to Blog | ||
| </Link> | ||
|
|
||
| <article className="prose prose-neutral dark:prose-invert max-w-none"> | ||
| <header className="mb-8 pb-8 border-b border-fd-border"> | ||
| <h1 className="text-4xl font-bold mb-4">{pageData.title}</h1> | ||
|
|
||
| {pageData.description && ( | ||
| <p className="text-xl text-fd-foreground/80 mb-6"> | ||
| {pageData.description} | ||
| </p> | ||
| )} | ||
|
|
||
| <div className="flex items-center gap-4 text-sm text-fd-foreground/70"> | ||
| {pageData.date && ( | ||
| <time dateTime={pageData.date}> | ||
| {new Date(pageData.date).toLocaleDateString('en-US', { | ||
| year: 'numeric', | ||
| month: 'long', | ||
| day: 'numeric', | ||
| })} | ||
| </time> | ||
| )} | ||
| {pageData.author && ( | ||
| <span>By {pageData.author}</span> | ||
| )} | ||
| </div> | ||
|
|
||
| {pageData.tags && pageData.tags.length > 0 && ( | ||
| <div className="mt-4 flex flex-wrap gap-2"> | ||
| {pageData.tags.map((tag: string) => ( | ||
| <span | ||
| key={tag} | ||
| className="inline-flex items-center rounded-full bg-fd-primary/10 px-3 py-1 text-sm font-medium text-fd-primary" | ||
| > | ||
| {tag} | ||
| </span> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </header> | ||
|
|
||
| <MDX components={defaultMdxComponents} /> | ||
| </article> | ||
| </main> | ||
| </HomeLayout> | ||
| ); | ||
| } | ||
|
|
||
| export async function generateStaticParams() { | ||
| return blog.getPages().map((page) => ({ | ||
| slug: page.slugs, | ||
| })); | ||
|
Comment on lines
+169
to
+172
|
||
| } | ||
|
|
||
| export async function generateMetadata({ | ||
| params, | ||
| }: { | ||
| params: Promise<{ slug?: string[] }>; | ||
|
Comment on lines
+175
to
+178
|
||
| }) { | ||
| const { slug } = await params; | ||
|
|
||
| // If no slug, return default metadata for blog index | ||
| if (!slug || slug.length === 0) { | ||
| return { | ||
| title: 'Blog', | ||
| description: 'Insights, updates, and best practices from the ObjectStack team.', | ||
| }; | ||
| } | ||
|
|
||
| const page = blog.getPage(slug); | ||
|
|
||
| if (!page) { | ||
| notFound(); | ||
| } | ||
|
|
||
| return { | ||
| title: page.data.title, | ||
| description: page.data.description, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,14 @@ | ||
| import { docs } from 'fumadocs-mdx:collections/server'; | ||
| import { docs, blog as blogCollection } from 'fumadocs-mdx:collections/server'; | ||
| import { loader } from 'fumadocs-core/source'; | ||
| import { i18n } from '@/lib/i18n'; | ||
|
|
||
| export const source = loader({ | ||
| baseUrl: '/docs', | ||
| i18n, | ||
| source: (docs as any).toFumadocsSource(), | ||
| source: docs.toFumadocsSource(), | ||
| }); | ||
|
|
||
| export const blog = loader({ | ||
| baseUrl: '/blog', | ||
| source: blogCollection.toFumadocsSource(), | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,7 +1,21 @@ | ||||||
| import { defineDocs, defineConfig } from 'fumadocs-mdx/config'; | ||||||
| import { defineDocs, defineConfig, frontmatterSchema } from 'fumadocs-mdx/config'; | ||||||
| import { z } from 'zod'; | ||||||
|
|
||||||
| export const docs = defineDocs({ | ||||||
| dir: '../../content/docs', | ||||||
| }) as any; | ||||||
| }); | ||||||
|
|
||||||
| const blogSchema = frontmatterSchema.extend({ | ||||||
| author: z.string().optional(), | ||||||
| date: z.coerce.string().optional(), | ||||||
|
||||||
| date: z.coerce.string().optional(), | |
| date: z.string().optional(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
langparameter is extracted from params but never used in the BlogPage component. The component should either use this parameter for internationalization or remove it from the destructuring if it's not needed. Consider whether blog posts should support multiple languages.