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
25 changes: 24 additions & 1 deletion apps/ui/content-collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,29 @@ const changelog = defineCollection({
}),
});

const blog = defineCollection({
name: "blog",
directory: "src/content/blog",
include: "**/*.md",
schema: z.object({
id: z.string(),
slug: z.string(),
date: z.string(),
title: z.string(),
summary: z.string(),
draft: z.boolean().optional(),
categories: z.array(z.string()).default([]),
image: z
.object({
src: z.string(),
alt: z.string(),
width: z.number(),
height: z.number(),
})
.optional(),
}),
});

export default defineConfig({
collections: [changelog],
collections: [changelog, blog],
});
24 changes: 24 additions & 0 deletions apps/ui/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,30 @@ const nextConfig: NextConfig = {
// devtoolSegmentExplorer: true,
// globalNotFound: true,
// },
async redirects() {
return [
{
source: "/docs",
destination: "https://docs.llmgateway.com",
permanent: true,
},
{
source: "/discord",
destination: "https://discord.gg/3u7jpXf36B",
permanent: true,
},
{
source: "/github",
destination: "https://github.com/theopenco/llmgateway",
permanent: true,
},
{
source: "/twitter",
destination: "https://twitter.com/llmgateway",
permanent: true,
},
];
},
};

// withContentCollections must be the outermost plugin
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
123 changes: 123 additions & 0 deletions apps/ui/src/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { allBlogs, type Blog } from "content-collections";
import { ArrowLeftIcon } from "lucide-react";
import Markdown from "markdown-to-jsx";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";

import Footer from "@/components/landing/footer";
import { HeroRSC } from "@/components/landing/hero-rsc";
import { getMarkdownOptions } from "@/lib/utils/markdown";

interface BlogEntryPageProps {
params: Promise<{ slug: string }>;
}

export default async function BlogEntryPage({ params }: BlogEntryPageProps) {
const { slug } = await params;

Comment thread
smakosh marked this conversation as resolved.
const entry = allBlogs.find((entry: Blog) => entry.slug === slug);

if (!entry) {
notFound();
}

Comment thread
smakosh marked this conversation as resolved.
return (
<>
<HeroRSC navbarOnly />
<div className="min-h-screen bg-white text-black dark:bg-black dark:text-white pt-30">
<main className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<Link
href="/blog"
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground"
>
<ArrowLeftIcon className="mr-2 h-4 w-4" />
Back to blog
</Link>
</div>

<article className="prose prose-lg dark:prose-invert max-w-none">
<header className="mb-8">
<h1 className="text-4xl font-bold mb-4">{entry.title}</h1>
<div className="text-muted-foreground">
{entry.summary && (
<p className="text-lg mb-2">{entry.summary}</p>
)}
<time dateTime={entry.date} className="text-sm italic">
{new Date(entry.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
</div>
</header>

{entry.image && (
<div className="mb-8">
<Image
src={entry.image.src}
alt={entry.image.alt || entry.title}
width={entry.image.width}
height={entry.image.height}
className="w-full rounded-lg object-cover"
/>
</div>
)}

<div className="prose prose-lg dark:prose-invert max-w-none">
<Markdown options={getMarkdownOptions()}>
{entry.content}
</Markdown>
</div>
</article>
</div>
</main>
<Footer />
</div>
</>
);
}

export async function generateStaticParams() {
return allBlogs.map((entry: Blog) => ({
slug: entry.slug,
}));
}
Comment thread
smakosh marked this conversation as resolved.

export async function generateMetadata({ params }: BlogEntryPageProps) {
const { slug } = await params;

const entry = allBlogs.find((entry: Blog) => entry.slug === slug);

if (!entry) {
return {};
}

Comment thread
smakosh marked this conversation as resolved.
return {
title: `${entry.title} - Blog - LLM Gateway`,
description: entry.summary || "LLM Gateway blog post",
openGraph: {
title: `${entry.title} - Blog - LLM Gateway`,
description: entry.summary || "LLM Gateway blog post",
type: "article",
images: entry.image
? [
{
url: entry.image.src,
width: entry.image.width || 800,
height: entry.image.height || 400,
alt: entry.image.alt || entry.title,
},
]
: ["/opengraph.png"],
},
twitter: {
card: "summary_large_image",
title: `${entry.title} - Blog - LLM Gateway`,
description: entry.summary || "LLM Gateway blog post",
},
};
}
68 changes: 68 additions & 0 deletions apps/ui/src/app/blog/category/[category]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { BlogList } from "@/components/blog/list";
import { HeroRSC } from "@/components/landing/hero-rsc";

interface BlogItem {
id: string;
slug: string;
date: string;
title: string;
summary: string;
categories?: string[];
}

function slugify(label: string) {
return label
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}

interface CategoryPageProps {
params: Promise<{ category: string }>;
}

export default async function BlogCategoryPage({ params }: CategoryPageProps) {
const { category } = await params;
const slug = decodeURIComponent(category);
const { allBlogs } = (await import("content-collections")) as any;

Comment thread
smakosh marked this conversation as resolved.
const filtered = (allBlogs as any[])
.filter((entry: any) => !entry?.draft)
.filter((entry: any) =>
(entry.categories || []).some((c: string) => slugify(c) === slug),
)
.sort(
(a: any, b: any) =>
new Date(b.date).getTime() - new Date(a.date).getTime(),
) as BlogItem[];

Comment on lines +27 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Eliminate any casts; rely on module types.

Avoid “as any” per guidelines. Use a typed dynamic import (or static import).

- const { allBlogs } = (await import("content-collections")) as any;
-
- const filtered = (allBlogs as any[])
-   .filter((entry: any) => !entry?.draft)
-   .filter((entry: any) =>
-     (entry.categories || []).some((c: string) => slugify(c) === slug),
-   )
-   .sort(
-     (a: any, b: any) =>
-       new Date(b.date).getTime() - new Date(a.date).getTime(),
-   ) as BlogItem[];
+ const { allBlogs } = await import("content-collections");
+ const filtered = allBlogs
+   .filter((entry) => !entry?.draft)
+   .filter((entry) =>
+     (entry.categories ?? []).some((c) => slugify(c) === slug),
+   )
+   .sort(
+     (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
+   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { allBlogs } = (await import("content-collections")) as any;
const filtered = (allBlogs as any[])
.filter((entry: any) => !entry?.draft)
.filter((entry: any) =>
(entry.categories || []).some((c: string) => slugify(c) === slug),
)
.sort(
(a: any, b: any) =>
new Date(b.date).getTime() - new Date(a.date).getTime(),
) as BlogItem[];
const { allBlogs } = await import("content-collections");
const filtered = allBlogs
.filter((entry) => !entry?.draft)
.filter((entry) =>
(entry.categories ?? []).some((c) => slugify(c) === slug),
)
.sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
);

return (
<>
<HeroRSC navbarOnly />
<BlogList
entries={filtered}
selectedCategory={slug}
heading="Blog"
subheading="Latest news and updates from LLM Gateway"
/>
</>
);
}

export async function generateStaticParams() {
const { allBlogs } = (await import("content-collections")) as any;
const slugs = new Set<string>();
for (const post of allBlogs as any[]) {
(post.categories || []).forEach((c: string) => slugs.add(slugify(c)));
}
return Array.from(slugs).map((category) => ({ category }));
}

export async function generateMetadata({ params }: CategoryPageProps) {
const { category } = await params;
const decoded = decodeURIComponent(category);
return {
title: `Blog: ${decoded} - LLM Gateway`,
description: `Articles in the ${decoded} category at LLM Gateway`,
};
}
Comment on lines +61 to +68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix generateMetadata params typing; remove Promise/await.

Aligns with App Router conventions.

-export async function generateMetadata({ params }: CategoryPageProps) {
-  const { category } = await params;
+export async function generateMetadata({ params }: CategoryPageProps) {
+  const { category } = params;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function generateMetadata({ params }: CategoryPageProps) {
const { category } = await params;
const decoded = decodeURIComponent(category);
return {
title: `Blog: ${decoded} - LLM Gateway`,
description: `Articles in the ${decoded} category at LLM Gateway`,
};
}
export async function generateMetadata({ params }: CategoryPageProps) {
const { category } = params;
const decoded = decodeURIComponent(category);
return {
title: `Blog: ${decoded} - LLM Gateway`,
description: `Articles in the ${decoded} category at LLM Gateway`,
};
}
🤖 Prompt for AI Agents
In apps/ui/src/app/blog/category/[category]/page.tsx around lines 61 to 68, the
generateMetadata implementation is incorrectly awaiting params and treating
params as a Promise; update the params typing to a plain object (e.g. { params:
{ category: string } } or the correct CategoryPageProps shape where
params.category is a string), remove the unnecessary await before params, and
read/ decode the category via decodeURIComponent(params.category) so the
function uses the correct synchronous params type and no Promise/await.

3 changes: 3 additions & 0 deletions apps/ui/src/app/blog/category/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return null;
}
50 changes: 50 additions & 0 deletions apps/ui/src/app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { BlogList } from "@/components/blog/list";
import { HeroRSC } from "@/components/landing/hero-rsc";

interface BlogItem {
id: string;
slug: string;
date: string;
title: string;
summary: string;
}

export default async function BlogPage() {
const { allBlogs } = (await import("content-collections")) as any;

const sortedEntries = (allBlogs as any[])
.sort(
(a: any, b: any) =>
new Date(b.date).getTime() - new Date(a.date).getTime(),
)
.filter((entry: any) => !entry?.draft)
.map(({ ...entry }: any) => entry as BlogItem);

Comment thread
smakosh marked this conversation as resolved.
return (
<div>
<HeroRSC navbarOnly />
<BlogList
entries={sortedEntries}
heading="Blog"
subheading="Latest news and updates from LLM Gateway"
/>
</div>
);
}

export async function generateMetadata() {
return {
title: "Blog - LLM Gateway",
description: "News, tutorials, and deep-dives from the LLM Gateway team.",
openGraph: {
title: "Blog - LLM Gateway",
description: "News, tutorials, and deep-dives from the LLM Gateway team.",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Blog - LLM Gateway",
description: "News, tutorials, and deep-dives from the LLM Gateway team.",
},
};
}
Loading