Skip to content

Commit 468e8e5

Browse files
Merge pull request #338 from nocodb/fix-blogs
2 parents a6bff03 + 672e9f8 commit 468e8e5

30 files changed

+2092
-260
lines changed

.env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# AI Model Configuration
2+
# Choose your provider: 'openai', 'google', or 'openrouter'
3+
AI_PROVIDER=openai
4+
5+
# Specify the model ID for your chosen provider
6+
# Examples:
7+
# - OpenAI: gpt-4-turbo, gpt-4, gpt-3.5-turbo
8+
# - Google: gemini-1.5-pro, gemini-1.5-flash
9+
# - OpenRouter: openai/gpt-4-turbo, anthropic/claude-3.5-sonnet
10+
AI_MODEL=gpt-4.1-2025-04-14
11+
12+
# API Keys for each provider (only set the one you're using)
13+
OPENAI_API_KEY=
14+
GOOGLE_GENERATIVE_AI_API_KEY=
15+
OPENROUTER_API_KEY=

app/api/chat/route.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ProvideLinksToolSchema } from '@/lib/ai-tools/inkeep-qa-schema';
2+
import { SearchDocsToolSchema, searchAndFetchDocs } from '@/lib/ai-tools/search-and-fetch';
3+
import { convertToModelMessages, stepCountIs, streamText } from 'ai';
4+
import { systemPrompt } from '@/lib/searchPrompt';
5+
import { validateRateLimit } from '@/utils/rateLimit';
6+
import { createModel } from '@/lib/ai-models';
7+
8+
9+
export async function POST(req: Request) {
10+
const rateLimitError = validateRateLimit(req);
11+
if (rateLimitError) return rateLimitError;
12+
let reqJson;
13+
try {
14+
reqJson = await req.json();
15+
} catch {
16+
return new Response('Invalid request', { status: 400 });
17+
}
18+
19+
const result = streamText({
20+
model: createModel(),
21+
tools: {
22+
searchDocs: {
23+
description: 'Search the NocoDB documentation and retrieve full page content. After calling this, you MUST read the returned content and write a comprehensive answer for the user based on that content.',
24+
inputSchema: SearchDocsToolSchema,
25+
execute: async ({ query }: { query: string }) => {
26+
const { markdown, links } = await searchAndFetchDocs(query, 3);
27+
return { markdown, links };
28+
},
29+
},
30+
provideLinks: {
31+
inputSchema: ProvideLinksToolSchema,
32+
},
33+
},
34+
system: systemPrompt,
35+
messages: convertToModelMessages(reqJson.messages, {
36+
ignoreIncompleteToolCalls: true,
37+
}),
38+
stopWhen: stepCountIs(10),
39+
});
40+
41+
return result.toUIMessageStreamResponse();
42+
}

app/blog/[slug]/page.tsx

Lines changed: 50 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@ import {blogSource} from "@/lib/source";
22
import Link from "next/link";
33
import {notFound} from "next/navigation";
44
import {metadataImage} from "@/lib/metadata";
5-
import {Button} from "@/components/ui/button";
6-
import {ArrowLeft} from "lucide-react";
75
import {calculateReadingTime} from "@/lib/timeToRead";
8-
import Image from "next/image";
96
import defaultMdxComponents from "fumadocs-ui/mdx";
107
import CustomToc from "@/components/blog/CustomToc";
118
import BlogCard from "@/components/blog/BlogCard";
129
import {getCategoryColor} from "@/lib/categoryColor";
1310
import Subscribe from "@/components/blog/Subscribe";
14-
import ShareDropdown from "@/components/blog/ShareDropdown";
11+
import SignUp from "@/components/blog/SignUp";
1512

1613
export async function generateMetadata(props: {
1714
params: Promise<{ slug?: string }>;
@@ -50,61 +47,52 @@ export default async function page(props: {
5047
}).slice(0, 2);
5148

5249
return (
53-
<>
54-
<div className="container pt-[40px] lg:px-10 pb-10">
55-
<div className="flex justify-between items-center">
56-
<Link className="text-sm font-normal" href="/blog">
57-
<Button className="cursor-pointer hover:underline underline-red" variant="none">
58-
<div className="flex text-nc-content-grey-subtle items-center gap-2">
59-
<ArrowLeft/>
60-
Back
61-
</div>
62-
</Button>
63-
</Link>
64-
<ShareDropdown
65-
url={`https://nocodb.com/blog/${params.slug}`}
66-
title={page.data.title}
67-
/>
68-
</div>
69-
<div className="my-8 flex flex-col gap-3">
70-
<div
71-
className="text-nc-content-grey-emphasis text-2xl lg:text-[40px] font-bold leading-9 lg:leading-[64px]">
72-
{page.data?.title}
50+
<div className="relative w-full bg-gradient-to-b from-blue-50/50 via-purple-50/30 via-30% to-white to-60%">
51+
{/* Decorative background elements */}
52+
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-blue-400/10 rounded-full blur-3xl -z-10" />
53+
<div className="absolute top-[40%] left-0 w-[400px] h-[400px] bg-purple-400/10 rounded-full blur-3xl -z-10" />
54+
55+
{/* Hero Section */}
56+
<div className="container py-4 lg:py-12">
57+
{/* Category Badge */}
58+
<Link className="inline-block mb-6" href={`/blog?category=${page.data?.category}`}>
59+
<div
60+
style={{backgroundColor: getCategoryColor(page?.data?.category)}}
61+
className="rounded-lg px-3 py-1.5 text-sm font-medium text-nc-content-grey-default hover:opacity-80 transition-opacity"
62+
>
63+
{page.data?.category}
7364
</div>
74-
<Link className="w-[fit-content]" href={`/blog?category=${page.data?.category}`}>
75-
<div style={{backgroundColor: getCategoryColor(page?.data?.category)}}
76-
className="rounded-[6px] px-2 text-nc-content-grey-default">
77-
{page.data?.category}
65+
</Link>
66+
67+
{/* Title */}
68+
<h1 className="text-nc-content-grey-emphasis text-3xl lg:text-5xl font-bold leading-tight lg:leading-tight mb-6 max-w-4xl">
69+
{page.data?.title}
70+
</h1>
71+
72+
{/* Metadata */}
73+
<div className="flex flex-wrap items-center gap-4 text-nc-content-grey-subtle-2">
74+
<div className="flex items-center gap-2">
75+
<div className="w-10 h-10 rounded-full bg-nc-background-grey-light flex items-center justify-center text-nc-content-grey-emphasis font-semibold">
76+
{page?.data?.author?.charAt(0).toUpperCase()}
7877
</div>
79-
</Link>
80-
</div>
81-
<div className="flex justify-center items-center leading-6 text-nc-content-grey-subtle-2">
82-
<div className="flex-1">
83-
<span className="mr-3">
84-
{page?.data?.author}
85-
</span>
86-
|
87-
<span className="mx-3">
88-
{new Date(page.data.date).toLocaleDateString("en-US", {})}
89-
</span>
90-
</div>
91-
92-
<div>
93-
{calculateReadingTime(page?.data?.structuredData)}
78+
<span className="font-medium">{page?.data?.author}</span>
9479
</div>
80+
<span className="text-nc-content-grey-muted"></span>
81+
<span>{new Date(page.data.date).toLocaleDateString("en-US", {
82+
month: "long",
83+
day: "numeric",
84+
year: "numeric"
85+
})}</span>
86+
<span className="text-nc-content-grey-muted"></span>
87+
<span>{calculateReadingTime(page?.data?.structuredData)}</span>
9588
</div>
9689
</div>
9790

98-
<div className="container mx-auto">
99-
<div className="relative w-full aspect-video">
100-
<Image className="w-full object-cover" src={page.data.image} alt={page.data.title} fill/>
101-
</div>
102-
</div>
103-
104-
<article className="container py-10 mx-auto">
91+
<article className="container py-10 lg:py-16 mx-auto">
10592
<div className="flex nc-blog-layout relative gap-8">
106-
<div className="sticky hidden lg:block h-48 top-8">
93+
<div className="sticky hidden lg:block top-8 self-start space-y-8">
10794
<CustomToc toc={page.data.toc}/>
95+
<SignUp />
10896
</div>
10997
<div className="prose min-w-0 flex-1">
11098
<page.data.body components={defaultMdxComponents}/>
@@ -115,19 +103,20 @@ export default async function page(props: {
115103

116104
{
117105
related?.length === 2 ? (
118-
<div className="container mx-auto">
119-
<h1 className="text-nc-content-grey-emphasis leading-[62px] font-bold text-[40px]">
120-
Related
121-
</h1>
122-
<div className="pt-15 gap-8 lg:gap-10 grid grid-cols-1 lg:grid-cols-2 pb-20">
123-
{related.map((post) => (
124-
<BlogCard post={post} key={post.url}/>
125-
))}
106+
<div className="bg-nc-background-grey-extra-light/30 py-16">
107+
<div className="container mx-auto">
108+
<h2 className="text-nc-content-grey-emphasis text-3xl lg:text-4xl font-bold mb-8">
109+
Related Articles
110+
</h2>
111+
<div className="flex flex-col gap-0">
112+
{related.map((post) => (
113+
<BlogCard post={post} key={post.url}/>
114+
))}
115+
</div>
126116
</div>
127117
</div>
128118
) : (<div className="py-10"></div>)
129119
}
130-
131-
</>
120+
</div>
132121
);
133122
}

app/blog/layout.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {Footer} from "@/components/blog/home/Footer";
55
import {Navbar} from "@/components/blog/home/Navbar";
66
import { ReCaptchaProvider } from "next-recaptcha-v3";
77
import { NuqsAdapter } from 'nuqs/adapters/next/app';
8+
import {blogSource} from "@/lib/source";
9+
import TopBarNavigationClient from "@/components/blog/TopBarNavigationClient";
10+
import {Suspense} from "react";
811

912
export const metadata = {
1013
icons: {
@@ -13,10 +16,19 @@ export const metadata = {
1316
}
1417

1518
export default function Layout({children,}: { children: ReactNode; }): React.ReactElement {
19+
const posts = blogSource.getPages();
20+
const categories = new Set<string>();
21+
posts.forEach((post) => {
22+
categories.add(post.data.category ?? "Uncategorized");
23+
});
24+
1625
return (
17-
<HomeLayout {...baseOptions} nav={{
26+
<HomeLayout className='!pt-0' {...baseOptions} nav={{
1827
component: Navbar()
1928
}}>
29+
<Suspense fallback={<div className="h-12 border-b border-nc-border-grey-medium" />}>
30+
<TopBarNavigationClient categories={Array.from(categories)} />
31+
</Suspense>
2032
<NuqsAdapter>
2133
<ReCaptchaProvider reCaptchaKey="6LcdnI0oAAAAAHYW3hwztfZw9qAjX4TiviE4fWip">
2234
{children}

app/blog/page.tsx

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {blogSource} from "@/lib/source";
22
import BlogCard from "@/components/blog/BlogCard";
33
import {Separator} from "@/components/ui/separator";
44
import Link from "next/link";
5-
import {CategoryTabs} from "@/components/blog/Category";
65
import Subscribe from "@/components/blog/Subscribe";
76
import {SearchInput} from "@/components/blog/SearchInput";
87
import Image from "next/image";
@@ -49,29 +48,24 @@ export default async function BlogPage({searchParams}: {
4948
const hasMorePosts = endIndex < searchFilteredPosts.length;
5049
const nextPage = currentPage + 1;
5150
return (
52-
<main className="py-8 w-full md:py-12">
53-
<div className="container py-10 lg:pt-16 lg:pb-8">
51+
<main className="w-full bg-gradient-to-b from-blue-50/50 via-purple-50/30 to-white">
52+
<div className="container py-10 relative">
53+
{/* Decorative background elements */}
54+
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-blue-400/10 rounded-full blur-3xl -z-10" />
55+
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-purple-400/10 rounded-full blur-3xl -z-10" />
56+
5457
<h1 className="text-nc-content-grey-emphasis text-[40px] font-semibold leading-15.5">
5558
Blog
5659
</h1>
5760
<h5 className="text-nc-content-grey-subtle mt-6 lg:mt-2 text-base leading-6 font-medium">
5861
Insights, tutorials, and updates from the team building the future of no-code databases.
5962
</h5>
60-
<div className="mt-6">
61-
<SearchInput />
62-
</div>
6363
</div>
64+
<Separator className="mb-12" />
6465

65-
66-
<div className="container mt-5">
67-
68-
<CategoryTabs categories={Array.from(categories)} selectedCategory={selectedCategory} searchQuery={searchQuery}/>
69-
<Separator className="border-nc-border-grey-medium"/>
70-
</div>
71-
72-
<div className="container py-8 lg:pt-15 lg:pb-20 gap-8 lg:gap-16 grid grid-cols-1 lg:grid-cols-2">
66+
<div className="container">
7367
{displayedPosts.length === 0 ? (
74-
<div className="col-span-full flex flex-col items-center justify-center py-16">
68+
<div className="flex flex-col items-center justify-center py-16">
7569
<Image
7670
src="/img/no-search-result-found.png"
7771
alt="No results found"
@@ -91,9 +85,11 @@ export default async function BlogPage({searchParams}: {
9185
</p>
9286
</div>
9387
) : (
94-
displayedPosts.map((post) => (
95-
<BlogCard post={post} key={post.url}/>
96-
))
88+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-0">
89+
{displayedPosts.map((post) => (
90+
<BlogCard post={post} key={post.url}/>
91+
))}
92+
</div>
9793
)}
9894
</div>
9995

@@ -115,9 +111,8 @@ export default async function BlogPage({searchParams}: {
115111
</Link>
116112
</div>
117113
)}
118-
119114
<Subscribe/>
120-
<Separator/>
115+
<Separator className="mb-12"/>
121116
</main>
122117
);
123118
}

app/docs/layout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {ReactNode} from "react";
22
import Navbar from "@/components/layout/Navbar";
33
import TopBarNaigation from "@/components/layout/TopBarNaigation";
44
import { DocsNavigationProvider } from "./DocsNavigationProvider";
5+
import { AISearchTrigger } from "@/components/search";
56

67
/**
78
* Layout component for rendering the main structure of the documentation page.
@@ -22,6 +23,8 @@ export default function Layout({children}: { children: ReactNode }) {
2223
</div>
2324
{children}
2425
</DocsNavigationProvider>
26+
<AISearchTrigger />
27+
2528
</main>
2629
)
2730
}

app/global.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@
312312
--color-nc-content-grey-subtle: var(--color-grey-700);
313313
--color-nc-content-grey-subtle-2: var(--color-grey-600);
314314
--color-nc-content-grey-muted: var(--color-grey-500);
315+
--color-nc-content-grey-muted-2: var(--color-grey-400);
315316

316317
--color-nc-content-brand-default: var(--color-brand-500);
317318
--color-nc-content-brand-hover: var(--color-grey-300);

app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ClientAnalytics from "@/components/Analytics";
55
import NcSearchDialog from "@/components/layout/Search";
66
import {CustomThemeProvider} from "@/app/ThemeProvider";
77
import {RootProvider} from "fumadocs-ui/provider";
8+
import { Toaster } from "sonner";
89

910
const inter = Inter({
1011
subsets: ['latin'],
@@ -20,6 +21,7 @@ export default function Layout({children}: { children: ReactNode }) {
2021
</CustomThemeProvider>
2122
</RootProvider>
2223
<ClientAnalytics/>
24+
<Toaster />
2325
</body>
2426
</html>
2527
);

cli.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"aliases": {
3+
"uiDir": "./components/ui",
4+
"componentsDir": "./components",
5+
"blockDir": "./components",
6+
"cssDir": "./styles",
7+
"libDir": "./lib"
8+
},
9+
"baseDir": "",
10+
"commands": {}
11+
}

0 commit comments

Comments
 (0)