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
2 changes: 1 addition & 1 deletion website/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function generateStaticParams() {

async function getPostContent(slug: string): Promise<string | null> {
try {
const filePath = path.join(process.cwd(), "content", "blog", `${slug}.md`);
const filePath = path.join(process.cwd(), "app", "blog", "posts", `${slug}.md`);
const content = await fs.readFile(filePath, "utf-8");
return content;
} catch {
Expand Down
248 changes: 124 additions & 124 deletions website/app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,124 +1,124 @@
"use client";
import React from "react";
import Link from "next/link";
import postsData from "./posts.json";
// Convert posts object to array and sort by date (newest first)
const BLOG_POSTS = Object.values(postsData).sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return dateB.getTime() - dateA.getTime();
});
export default function BlogPage() {
return (
<main className="min-h-screen pt-32 pb-24 px-6 lg:px-8">
<div className="relative z-10 mx-auto max-w-6xl">
{/* Header */}
<div className="text-center mb-20 animate-slide-up">
<h1 className="text-4xl lg:text-6xl font-bold text-white tracking-tight mb-6">
AutoClaw <span className="text-red-500">Blog</span>
</h1>
<p className="text-xl text-slate-400 max-w-2xl mx-auto leading-relaxed">
Insights on AI agent deployment, OpenClaw, and building the future of
personal AI assistants.
</p>
</div>
{/* Featured Post (First one) */}
<div className="mb-16 animate-slide-up" style={{ animationDelay: "0.1s" }}>
<Link href={`/blog/${BLOG_POSTS[0].slug}`} className="group block relative bg-slate-900/40 backdrop-blur-md border border-slate-800 rounded-3xl overflow-hidden hover:border-red-500/50 transition-all duration-300">
<div className="grid md:grid-cols-2 gap-0">
<div className="h-64 md:h-auto bg-gradient-to-br from-red-900/40 to-slate-900 relative overflow-hidden">
{/* Fallback pattern since we don't have real images yet */}
<div className="absolute inset-0 opacity-30 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-red-500 via-slate-900 to-black"></div>
<div className="absolute inset-0 flex items-center justify-center text-red-700/20">
<svg className="w-32 h-32" fill="currentColor" viewBox="0 0 24 24"><path d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
</div>
</div>
<div className="p-8 md:p-12 flex flex-col justify-center">
<div className="flex items-center gap-3 text-sm mb-4">
<span className="px-3 py-1 rounded-full bg-red-500/10 text-red-400 font-medium border border-red-500/20">
{BLOG_POSTS[0].category}
</span>
<span className="text-slate-500">{BLOG_POSTS[0].date}</span>
<span className="text-slate-600">•</span>
<span className="text-slate-500">{BLOG_POSTS[0].readTime}</span>
</div>
<h2 className="text-3xl font-bold text-white mb-4 group-hover:text-red-400 transition-colors">
{BLOG_POSTS[0].title}
</h2>
<p className="text-slate-400 text-lg leading-relaxed mb-6">
{BLOG_POSTS[0].excerpt}
</p>
<div className="text-red-400 font-medium flex items-center group-hover:translate-x-2 transition-transform">
Read Article
<svg className="w-4 h-4 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</div>
</div>
</div>
</Link>
</div>
{/* Recent Posts Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 animate-slide-up" style={{ animationDelay: "0.2s" }}>
{BLOG_POSTS.slice(1).map((post) => (
<Link key={post.slug} href={`/blog/${post.slug}`} className="group flex flex-col bg-slate-900/30 backdrop-blur-sm border border-slate-800 rounded-2xl overflow-hidden hover:border-slate-600 hover:bg-slate-800/50 transition-all duration-300">
<div className="h-48 bg-gradient-to-br from-slate-800 to-slate-900 relative overflow-hidden">
<div className="absolute inset-0 opacity-20 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-slate-700 via-slate-900 to-black"></div>
{/* Visual placeholder */}
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-6xl opacity-5 select-none text-white font-serif italic">
{post.title.charAt(0)}
</span>
</div>
</div>
<div className="p-6 flex flex-col flex-1">
<div className="flex items-center justify-between text-xs mb-3">
<span className="text-red-400 font-medium">{post.category}</span>
<span className="text-slate-500">{post.readTime}</span>
</div>
<h3 className="text-xl font-bold text-white mb-3 group-hover:text-red-400 transition-colors line-clamp-2">
{post.title}
</h3>
<p className="text-slate-400 text-sm leading-relaxed mb-4 line-clamp-3 flex-1">
{post.excerpt}
</p>
<div className="pt-4 border-t border-slate-800 mt-auto flex items-center justify-between">
<span className="text-xs text-slate-500">{post.date}</span>
<span className="text-xs font-semibold text-red-400/80 group-hover:text-red-400">Read more →</span>
</div>
</div>
</Link>
))}
</div>
{/* Newsletter Signup */}
<div className="mt-24 p-8 md:p-12 rounded-3xl bg-red-900/10 border border-red-500/20 text-center animate-slide-up" style={{ animationDelay: "0.3s" }}>
<h3 className="text-2xl font-bold text-white mb-4">Subscribe to our newsletter</h3>
<p className="text-slate-400 mb-8 max-w-xl mx-auto">
Get the latest updates on AutoClaw features, OpenClaw tips, and articles about AI agent deployment.
</p>
<form className="flex flex-col sm:flex-row gap-3 justify-center max-w-md mx-auto" onSubmit={(e) => e.preventDefault()}>
<input
type="email"
placeholder="you@example.com"
className="px-4 py-3 rounded-xl bg-slate-950/50 border border-slate-700 text-white placeholder-slate-500 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500 w-full"
/>
<button className="px-6 py-3 bg-red-600 hover:bg-red-500 text-white font-semibold rounded-xl transition-colors whitespace-nowrap shadow-lg shadow-red-900/20 cursor-pointer">
Subscribe
</button>
</form>
</div>
</div>
</main>
);
}
"use client";
import React from "react";
import Link from "next/link";
import postsData from "./posts.json";

// Convert posts object to array and sort by date (newest first)
const BLOG_POSTS = Object.values(postsData).sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return dateB.getTime() - dateA.getTime();
});

export default function BlogPage() {
return (
<main className="min-h-screen pt-32 pb-24 px-6 lg:px-8">
<div className="relative z-10 mx-auto max-w-6xl">

{/* Header */}
<div className="text-center mb-20 animate-slide-up">
<h1 className="text-4xl lg:text-6xl font-bold text-white tracking-tight mb-6">
AutoClaw <span className="text-red-500">Blog</span>
</h1>
<p className="text-xl text-slate-400 max-w-2xl mx-auto leading-relaxed">
Insights on AI agent deployment, OpenClaw, and building the future of
personal AI assistants.
</p>
</div>

{/* Featured Post (First one) */}
<div className="mb-16 animate-slide-up" style={{ animationDelay: "0.1s" }}>
<Link href={`/blog/${BLOG_POSTS[0].slug}`} className="group block relative bg-slate-900/40 backdrop-blur-md border border-slate-800 rounded-3xl overflow-hidden hover:border-red-500/50 transition-all duration-300">
<div className="grid md:grid-cols-2 gap-0">
<div className="h-64 md:h-auto bg-gradient-to-br from-red-900/40 to-slate-900 relative overflow-hidden">
{/* Fallback pattern since we don't have real images yet */}
<div className="absolute inset-0 opacity-30 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-red-500 via-slate-900 to-black"></div>
<div className="absolute inset-0 flex items-center justify-center text-red-700/20">
<svg className="w-32 h-32" fill="currentColor" viewBox="0 0 24 24"><path d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
</div>
</div>
<div className="p-8 md:p-12 flex flex-col justify-center">
<div className="flex items-center gap-3 text-sm mb-4">
<span className="px-3 py-1 rounded-full bg-red-500/10 text-red-400 font-medium border border-red-500/20">
{BLOG_POSTS[0].category}
</span>
<span className="text-slate-500">{BLOG_POSTS[0].date}</span>
<span className="text-slate-600">•</span>
<span className="text-slate-500">{BLOG_POSTS[0].readTime}</span>
</div>
<h2 className="text-3xl font-bold text-white mb-4 group-hover:text-red-400 transition-colors">
{BLOG_POSTS[0].title}
</h2>
<p className="text-slate-400 text-lg leading-relaxed mb-6">
{BLOG_POSTS[0].excerpt}
</p>
<div className="text-red-400 font-medium flex items-center group-hover:translate-x-2 transition-transform">
Read Article
<svg className="w-4 h-4 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</div>
</div>
</div>
</Link>
</div>

{/* Recent Posts Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 animate-slide-up" style={{ animationDelay: "0.2s" }}>
{BLOG_POSTS.slice(1).map((post) => (
<Link key={post.slug} href={`/blog/${post.slug}`} className="group flex flex-col bg-slate-900/30 backdrop-blur-sm border border-slate-800 rounded-2xl overflow-hidden hover:border-slate-600 hover:bg-slate-800/50 transition-all duration-300">
<div className="h-48 bg-gradient-to-br from-slate-800 to-slate-900 relative overflow-hidden">
<div className="absolute inset-0 opacity-20 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-slate-700 via-slate-900 to-black"></div>
{/* Visual placeholder */}
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-6xl opacity-5 select-none text-white font-serif italic">
{post.title.charAt(0)}
</span>
</div>
</div>

<div className="p-6 flex flex-col flex-1">
<div className="flex items-center justify-between text-xs mb-3">
<span className="text-red-400 font-medium">{post.category}</span>
<span className="text-slate-500">{post.readTime}</span>
</div>

<h3 className="text-xl font-bold text-white mb-3 group-hover:text-red-400 transition-colors line-clamp-2">
{post.title}
</h3>

<p className="text-slate-400 text-sm leading-relaxed mb-4 line-clamp-3 flex-1">
{post.excerpt}
</p>

<div className="pt-4 border-t border-slate-800 mt-auto flex items-center justify-between">
<span className="text-xs text-slate-500">{post.date}</span>
<span className="text-xs font-semibold text-red-400/80 group-hover:text-red-400">Read more →</span>
</div>
</div>
</Link>
))}
</div>

{/* Newsletter Signup */}
<div className="mt-24 p-8 md:p-12 rounded-3xl bg-red-900/10 border border-red-500/20 text-center animate-slide-up" style={{ animationDelay: "0.3s" }}>
<h3 className="text-2xl font-bold text-white mb-4">Subscribe to our newsletter</h3>
<p className="text-slate-400 mb-8 max-w-xl mx-auto">
Get the latest updates on AutoClaw features, OpenClaw tips, and articles about AI agent deployment.
</p>
<form className="flex flex-col sm:flex-row gap-3 justify-center max-w-md mx-auto" onSubmit={(e) => e.preventDefault()}>
<input
type="email"
placeholder="you@example.com"
className="px-4 py-3 rounded-xl bg-slate-950/50 border border-slate-700 text-white placeholder-slate-500 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500 w-full"
/>
<button className="px-6 py-3 bg-red-600 hover:bg-red-500 text-white font-semibold rounded-xl transition-colors whitespace-nowrap shadow-lg shadow-red-900/20 cursor-pointer">
Subscribe
</button>
</form>
</div>

</div>
</main>
);
}
32 changes: 16 additions & 16 deletions website/app/components/ConditionalNavbar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"use client";
import { usePathname } from "next/navigation";
import Sidebar from "./Sidebar";
export default function ConditionalNavbar() {
const pathname = usePathname();
const isCallPage = pathname?.startsWith("/call");
if (isCallPage) {
return null;
}
return <Sidebar />;
}
"use client";

import { usePathname } from "next/navigation";
import Sidebar from "./Sidebar";

export default function ConditionalNavbar() {
const pathname = usePathname();
const isCallPage = pathname?.startsWith("/call");

if (isCallPage) {
return null;
}

return <Sidebar />;
}

20 changes: 10 additions & 10 deletions website/app/components/PageWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client";
export default function PageWrapper({ children }: { children: React.ReactNode }) {
return (
<div className="pt-24 lg:pt-0 lg:pl-24 transition-[padding] duration-300">
{children}
</div>
);
}
"use client";

export default function PageWrapper({ children }: { children: React.ReactNode }) {
return (
<div className="pt-24 lg:pt-0 lg:pl-24 transition-[padding] duration-300">
{children}
</div>
);
}

29 changes: 29 additions & 0 deletions website/app/components/WebVitals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import { useEffect } from "react";
import { onCLS, onINP, onLCP, onFCP, onTTFB, type Metric } from "web-vitals";

function sendToAnalytics(metric: Metric) {
// Log in development
if (process.env.NODE_ENV === "development") {
console.log(`[Web Vitals] ${metric.name}:`, metric.value.toFixed(2), metric.rating);
}

// TODO: Send to your analytics backend
// Examples:
// - Google Analytics: gtag('event', metric.name, { value: metric.value, ... })
// - Custom endpoint: fetch('/api/vitals', { method: 'POST', body: JSON.stringify(metric) })
// - Plausible: plausible(metric.name, { props: { value: metric.value } })
}

export function WebVitals() {
useEffect(() => {
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
}, []);

return null;
}
2 changes: 2 additions & 0 deletions website/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ClerkProvider } from "@clerk/nextjs";
import ConditionalNavbar from "./components/ConditionalNavbar";
import PageWrapper from "./components/PageWrapper";
import { ThemeProvider } from "./contexts/ThemeContext";
import { WebVitals } from "./components/WebVitals";

export const metadata: Metadata = {
title: "AutoClaw — Deploy OpenClaw in Minutes",
Expand Down Expand Up @@ -83,6 +84,7 @@ export default function RootLayout({
>
<html lang="en">
<body>
<WebVitals />
<ThemeProvider>
<ConditionalNavbar />
<PageWrapper>{children}</PageWrapper>
Expand Down
3 changes: 2 additions & 1 deletion website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"next": "^16.1.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-markdown": "^10.1.0"
"react-markdown": "^10.1.0",
"web-vitals": "^5.1.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
Expand Down
8 changes: 8 additions & 0 deletions website/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.