Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions apps/docs/app/[lang]/blog/[[...slug]]/page.tsx
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;
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lang parameter 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.

Copilot uses AI. Check for mistakes.

// 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
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generateStaticParams function is missing the lang parameter that should be included in the returned objects since the route includes [lang] as a dynamic segment. This could cause build errors or missing routes for different languages.

Copilot uses AI. Check for mistakes.
}

export async function generateMetadata({
params,
}: {
params: Promise<{ slug?: string[] }>;
Comment on lines +175 to +178
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generateMetadata function's params type is missing the lang parameter that exists in the route. The type should be params: Promise<{ lang: string; slug?: string[] }> to match the route structure and the main component's params type.

Copilot uses AI. Check for mistakes.
}) {
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,
};
}
10 changes: 5 additions & 5 deletions apps/docs/app/[lang]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ export default async function HomePage({
{t.hero.title.line1} <br/> {t.hero.title.line2}
</h1>

<p className="mx-auto max-w-2xl text-lg text-fd-muted-foreground sm:text-xl leading-relaxed">
<p className="mx-auto max-w-2xl text-lg text-fd-foreground/80 sm:text-xl leading-relaxed">
{t.hero.subtitle.line1}
<br className="hidden sm:inline" />
<span className="text-fd-foreground font-medium">{t.hero.subtitle.line2}</span>
<span className="text-fd-foreground font-semibold">{t.hero.subtitle.line2}</span>
</p>

<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4">
Expand Down Expand Up @@ -124,7 +124,7 @@ export default async function HomePage({

{/* Personas Section */}
<div className="mt-32 mb-16 w-full max-w-5xl px-4">
<h2 className="text-3xl font-bold tracking-tight mb-12 bg-gradient-to-r from-fd-foreground to-fd-muted-foreground bg-clip-text text-transparent">
<h2 className="text-3xl font-bold tracking-tight mb-12 bg-gradient-to-r from-fd-foreground to-fd-foreground/70 bg-clip-text text-transparent">
{t.personas.heading}
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
Expand Down Expand Up @@ -166,7 +166,7 @@ function FeatureCard({ icon, title, description, href }: { icon: React.ReactNode
<h3 className="mb-2 text-lg font-semibold text-fd-card-foreground group-hover:text-fd-primary transition-colors">
{title}
</h3>
<p className="text-sm text-fd-muted-foreground leading-relaxed">
<p className="text-sm text-fd-foreground/70 leading-relaxed">
{description}
</p>
</div>
Expand All @@ -188,7 +188,7 @@ function PersonaCard({ icon, title, description, href, action }: { icon: React.R
<Link href={href} className="flex flex-col items-start p-8 rounded-2xl bg-fd-secondary/30 border border-fd-border/50 hover:bg-fd-secondary/60 hover:border-fd-primary/30 transition-all group text-left">
{icon}
<h3 className="text-xl font-bold mb-3">{title}</h3>
<p className="text-fd-muted-foreground mb-6 text-sm leading-relaxed flex-grow text-left">
<p className="text-fd-foreground/70 mb-6 text-sm leading-relaxed flex-grow text-left">
{description}
</p>
<div className="flex items-center text-sm font-semibold text-fd-primary mt-auto group-hover:translate-x-1 transition-transform">
Expand Down
5 changes: 5 additions & 0 deletions apps/docs/app/layout.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export const baseOptions: BaseLayoutProps = {
url: '/docs/',
active: 'nested-url',
},
{
text: 'Blog',
url: '/blog',
active: 'nested-url',
},
// {
// text: 'Concepts',
// url: '/docs/concepts/manifesto',
Expand Down
9 changes: 7 additions & 2 deletions apps/docs/app/source.ts
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(),
});
18 changes: 16 additions & 2 deletions apps/docs/source.config.ts
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(),
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date schema uses z.coerce.string() which is redundant. Since the frontmatter dates are already in string format (e.g., "2024-01-20"), you should use z.string() directly. The .coerce is unnecessary and can cause unexpected behavior when the date is already a string.

Suggested change
date: z.coerce.string().optional(),
date: z.string().optional(),

Copilot uses AI. Check for mistakes.
tags: z.array(z.string()).optional(),
});

export const blog = defineDocs({
dir: '../../content/blog',
docs: {
schema: blogSchema,
},
});

export default defineConfig();
Loading
Loading