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
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ jobs:
run: |
echo "🔒 Checking for known vulnerabilities..."
npm audit --audit-level=moderate --json > audit-report.json
if jq -e '.metadata.vulnerabilities.total > 0' audit-report.json; then
if jq -e '(.metadata.vulnerabilities.moderate + .metadata.vulnerabilities.high + .metadata.vulnerabilities.critical) > 0' audit-report.json; then
echo "❌ Found security vulnerabilities. Please fix them before merging."
cat audit-report.json | jq '.metadata.vulnerabilities'
exit 1
Expand Down Expand Up @@ -158,7 +158,12 @@ jobs:
while IFS= read -r commit; do
hash=$(echo "$commit" | cut -d' ' -f1)
message=$(echo "$commit" | cut -d' ' -f2-)


# Skip git-generated merge commits
if echo "$message" | grep -qE "^Merge (branch|pull request|remote-tracking branch|[0-9a-f]{7,40}) "; then
continue
fi

if ! echo "$message" | grep -qE "$commit_regex"; then
invalid_commits="$invalid_commits\n$hash: $message"
fi
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/pre-commit-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ jobs:
invalid_commits=""

while IFS= read -r message; do
if [ -n "$message" ] && ! echo "$message" | grep -qE "$commit_regex"; then
# Skip empty lines and git-generated merge commits
if [ -z "$message" ] || echo "$message" | grep -qE "^Merge (branch|pull request|remote-tracking branch|[0-9a-f]{7,40}) "; then
continue
fi
if ! echo "$message" | grep -qE "$commit_regex"; then
invalid_commits="$invalid_commits\n$message"
fi
done <<< "$commits"
Expand Down Expand Up @@ -85,7 +89,7 @@ jobs:
console_log_found=false

for file in $changed_files; do
if [[ "$file" =~ \.(ts|tsx|js|jsx)$ ]]; then
if [[ "$file" =~ \.(ts|tsx|js|jsx)$ ]] && [ -f "$file" ]; then
if grep -q "console\.log" "$file"; then
echo "❌ Found console.log in $file"
console_log_found=true
Expand Down Expand Up @@ -115,7 +119,7 @@ jobs:
todo_found=false

for file in $changed_files; do
if [[ "$file" =~ \.(ts|tsx|js|jsx)$ ]]; then
if [[ "$file" =~ \.(ts|tsx|js|jsx)$ ]] && [ -f "$file" ]; then
if grep -q "TODO\|FIXME" "$file"; then
echo "⚠️ Found TODO/FIXME in $file"
todo_found=true
Expand Down
131 changes: 38 additions & 93 deletions app/(landing)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,117 +2,62 @@ import React from 'react';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import BlogPostDetails from '@/components/landing-page/blog/BlogPostDetails';
import { getBlogPost, getBlogPosts } from '@/lib/api/blog';
import { generateBlogPostMetadata } from '@/lib/metadata';
import { getAllBlogPosts, getBlogPostBySlug, getRelatedPosts } from '@/lib/mdx';

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

interface StaticParams {
slug: string;
}

const STATIC_GENERATION_LIMIT = 100;

export async function generateStaticParams(): Promise<StaticParams[]> {
try {
const response = await getBlogPosts({
page: 1,
limit: STATIC_GENERATION_LIMIT,
status: 'PUBLISHED',
});

const data = response.data;

if (!data || data.length === 0) {
return [];
}

return data.map(post => ({
slug: post.slug,
}));
} catch {
return [];
}
export async function generateStaticParams() {
return getAllBlogPosts().map(post => ({ slug: post.slug }));
}

export async function generateMetadata({
params,
}: BlogPostPageProps): Promise<Metadata> {
try {
const { slug } = await params;

if (!slug || typeof slug !== 'string') {
return getDefaultMetadata();
}

const post = await getBlogPost(slug);

if (!post) {
return getNotFoundMetadata();
}

return generateBlogPostMetadata(post);
} catch {
return getDefaultMetadata();
const { slug } = await params;
const post = await getBlogPostBySlug(slug);

if (!post) {
return {
title: 'Blog Post Not Found | Boundless',
description:
'The requested blog post could not be found. Please check the URL or browse our other posts.',
robots: { index: false, follow: true },
};
}

return {
title: `${post.title} | Boundless Blog`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: post.coverImage ? [{ url: post.coverImage }] : [],
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author.name],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: post.coverImage ? [post.coverImage] : [],
},
};
}

const BlogPostPage = async ({ params }: BlogPostPageProps) => {
try {
const { slug } = await params;

if (!slug || typeof slug !== 'string') {
notFound();
}

const post = await getBlogPost(slug);

if (!post) {
notFound();
}

if (!post.id || !post.title || !post.content) {
notFound();
}

return <BlogPostDetails post={post} />;
} catch (error) {
if (error instanceof Error) {
if (
error.message.includes('404') ||
error.message.includes('not found')
) {
notFound();
}
}
const { slug } = await params;
const post = await getBlogPostBySlug(slug);

if (!post) {
notFound();
}
};

function getDefaultMetadata(): Metadata {
return {
title: 'Blog Post | Boundless',
description: 'Read our latest blog posts and insights.',
robots: {
index: false,
follow: true,
},
};
}
const related = getRelatedPosts(post.slug, post.tags, post.categories);

function getNotFoundMetadata(): Metadata {
return {
title: 'Blog Post Not Found | Boundless',
description:
'The requested blog post could not be found. Please check the URL or browse our other posts.',
robots: {
index: false,
follow: true,
},
};
}
return <BlogPostDetails post={post} relatedPosts={related} />;
};

export default BlogPostPage;
70 changes: 4 additions & 66 deletions app/(landing)/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,15 @@
'use client';

import { useState, useEffect } from 'react';
import BlogHero from '@/components/landing-page/blog/BlogHero';
import StreamingBlogGrid from '@/components/landing-page/blog/StreamingBlogGrid';
import { getBlogPosts } from '@/lib/api/blog';
import { GetBlogPostsResponse } from '@/types/blog';
import { Loader2 } from 'lucide-react';

const BlogsPage = () => {
const [blogData, setBlogData] = useState<GetBlogPostsResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchInitialPosts = async () => {
try {
setIsLoading(true);
const response = await getBlogPosts({
page: 1,
limit: 12,
sortBy: 'createdAt',
sortOrder: 'desc',
status: 'PUBLISHED',
});
setBlogData(response);
} catch {
setError('Failed to load blog posts. Please try again.');
} finally {
setIsLoading(false);
}
};
import { getAllBlogPosts } from '@/lib/mdx';

fetchInitialPosts();
}, []);

const handleLoadMore = async (
page: number
): Promise<GetBlogPostsResponse> => {
const response = await getBlogPosts({
page,
limit: 12,
sortBy: 'createdAt',
sortOrder: 'desc',
status: 'PUBLISHED',
});
return response;
};
const BlogsPage = async () => {
const posts = getAllBlogPosts();

return (
<div className='bg-background-main-bg min-h-screen'>
<div className='mx-auto max-w-[1440px] px-5 py-5 md:px-[50px] lg:px-[100px]'>
<BlogHero />

{isLoading ? (
<div className='flex min-h-[400px] items-center justify-center'>
<div className='flex flex-col items-center gap-3 text-[#B5B5B5]'>
<Loader2 className='h-8 w-8 animate-spin' />
<span>Loading articles...</span>
</div>
</div>
) : error ? (
<div className='rounded-lg border border-red-500/20 bg-red-500/10 px-4 py-8 text-center text-red-400'>
<p>{error}</p>
</div>
) : blogData ? (
<StreamingBlogGrid
initialPosts={blogData.data}
hasMore={blogData.hasMore}
initialPage={blogData.currentPage}
totalPages={blogData.totalPages}
onLoadMore={handleLoadMore}
/>
) : null}
<StreamingBlogGrid initialPosts={posts} />
</div>
</div>
);
Expand Down
25 changes: 13 additions & 12 deletions app/api/blog/search/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { getBlogPosts } from '@/lib/api/blog';
import { getAllBlogPosts } from '@/lib/mdx';

export async function GET(request: NextRequest) {
try {
Expand All @@ -8,11 +8,13 @@ export async function GET(request: NextRequest) {
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '12');
const category = searchParams.get('category');
const tags = searchParams.get('tags')?.split(',').filter(Boolean);

const response = await getBlogPosts({ limit: 100 });
const allPosts = response.data;
const tags = searchParams
.get('tags')
?.split(',')
.map(t => t.trim().toLowerCase())
.filter(Boolean);

const allPosts = getAllBlogPosts();
let filteredPosts = allPosts;

if (q.trim()) {
Expand All @@ -21,21 +23,20 @@ export async function GET(request: NextRequest) {
post =>
post.title.toLowerCase().includes(query) ||
post.excerpt.toLowerCase().includes(query) ||
(post.tags &&
post.tags.some(tag => tag.tag.name.toLowerCase().includes(query)))
post.tags.some(tag => tag.toLowerCase().includes(query))
);
}

if (category) {
filteredPosts = filteredPosts.filter(
post => post.categories && post.categories.includes(category)
const normalizedCategory = category.trim().toLowerCase();
filteredPosts = filteredPosts.filter(post =>
post.categories.some(c => c.toLowerCase() === normalizedCategory)
);
}

if (tags && tags.length > 0) {
filteredPosts = filteredPosts.filter(
post =>
post.tags && tags.some(tag => post.tags.some(t => t.tag.name === tag))
filteredPosts = filteredPosts.filter(post =>
tags.some(tag => post.tags.some(t => t.toLowerCase() === tag))
);
}

Expand Down
23 changes: 23 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
@import 'tailwindcss';
@import 'tw-animate-css';
@plugin "@tailwindcss/typography";
@custom-variant dark (&:is(.dark *));

/* Prose (Tailwind Typography) dark theme overrides for blog content */
@layer base {
.prose {
--tw-prose-body: #d1d5db;
--tw-prose-headings: #ffffff;
--tw-prose-lead: #d1d5db;
--tw-prose-links: #a7f950;
--tw-prose-bold: #ffffff;
--tw-prose-counters: #b5b5b5;
--tw-prose-bullets: #b5b5b5;
--tw-prose-hr: #374151;
--tw-prose-quotes: #d1d5db;
--tw-prose-quote-borders: #a7f950;
--tw-prose-captions: #9ca3af;
--tw-prose-code: #a7f950;
--tw-prose-pre-code: #e5e7eb;
--tw-prose-pre-bg: #111827;
--tw-prose-th-borders: #374151;
--tw-prose-td-borders: #1f2937;
}
}

@theme {
--font-inter: 'Inter', sans-serif;
--color-gray-50: #f7f7f7;
Expand Down
Loading
Loading