Install
openclaw skills install frontend-devGenerate production-ready Next.js projects with TypeScript, Tailwind CSS, shadcn/ui, and API integration. Use when the user asks to build, create, develop, or scaffold a Next.js application, web app, full-stack project, or frontend with backend integration. Prioritizes modern stack (Next.js 14+, TypeScript, shadcn/ui, axios, react-query) and best practices. Also triggers on requests to add features, integrate APIs, or extend existing Next.js projects.
openclaw skills install frontend-devGenerate production-ready Next.js projects from natural language, with shadcn/ui components, API integration, type safety, and modern tooling.
Fast path for simple projects:
Live preview: Projects run on PM2 (port 3002), accessible at http://localhost:3002 or via nginx proxy if configured.
Default workflow: All projects use PM2 for dev server management (prevents port conflicts, ensures single instance).
sudo apt-get install chromium-browser (Debian/Ubuntu)sudo apt-get install nginx# /etc/nginx/sites-available/<project-name>
server {
listen <external-port>; # e.g., 3001, 8081, etc.
server_name _;
location / {
proxy_pass http://localhost:3002; # PM2 dev server
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
sudo ln -s /etc/nginx/sites-available/<project-name> /etc/nginx/sites-enabled/ && sudo systemctl reload nginxhttp://localhost:3002 (PM2 port)Before starting, ask user if they want to enable optional features.
Quick reference for typical requests:
(dashboard) route group, shadcn data tables, chartsapp/page.tsx, hero section, features grid, testimonialsapp/blog/[slug]/page.tsx, markdown support(auth) group), protected routes, subscription logicAsk user: What type of project are you building? (helps determine structure and components)
Core:
API Integration (default):
Optional (based on needs):
Industry-standard Next.js 14+ App Router structure with feature-based organization:
<project-name>/
├── app/ # Next.js 14 App Router
│ ├── (auth)/ # Route group (auth pages)
│ │ ├── login/
│ │ │ └── page.tsx
│ │ ├── register/
│ │ │ └── page.tsx
│ │ └── layout.tsx # Auth-specific layout
│ ├── (dashboard)/ # Route group (protected pages)
│ │ ├── dashboard/
│ │ │ ├── page.tsx
│ │ │ └── loading.tsx
│ │ ├── profile/
│ │ │ └── page.tsx
│ │ ├── settings/
│ │ │ └── page.tsx
│ │ └── layout.tsx # Dashboard layout with sidebar
│ ├── api/ # API routes
│ │ ├── auth/
│ │ │ └── [...nextauth]/route.ts
│ │ └── users/
│ │ └── route.ts
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ ├── loading.tsx # Root loading UI
│ ├── error.tsx # Root error boundary
│ ├── not-found.tsx # 404 page
│ └── providers.tsx # Client providers (React Query, etc.)
│
├── components/
│ ├── ui/ # shadcn/ui components (auto-generated)
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── input.tsx
│ │ ├── form.tsx
│ │ └── ...
│ ├── layout/ # Layout components
│ │ ├── header.tsx
│ │ ├── footer.tsx
│ │ ├── sidebar.tsx
│ │ └── mobile-nav.tsx
│ ├── features/ # Feature-specific components
│ │ ├── auth/
│ │ │ ├── login-form.tsx
│ │ │ └── register-form.tsx
│ │ ├── dashboard/
│ │ │ ├── stats-card.tsx
│ │ │ └── recent-activity.tsx
│ │ └── profile/
│ │ ├── profile-header.tsx
│ │ └── edit-profile-form.tsx
│ └── shared/ # Shared/common components
│ ├── data-table.tsx
│ ├── search-bar.tsx
│ └── pagination.tsx
│
├── lib/ # Utility functions & configurations
│ ├── api.ts # Axios instance + interceptors
│ ├── react-query.ts # React Query client config
│ ├── utils.ts # Utility functions (cn, formatters)
│ ├── validations.ts # Zod schemas
│ ├── constants.ts # App constants
│ └── auth.ts # Auth utilities (if using next-auth)
│
├── hooks/ # Custom React hooks
│ ├── use-auth.ts # Authentication hook
│ ├── use-user.ts # User data hook (React Query)
│ ├── use-posts.ts # Posts data hook (React Query)
│ ├── use-media-query.ts # Responsive design hook
│ └── use-toast.ts # Toast notifications (shadcn)
│
├── types/ # TypeScript type definitions
│ ├── index.ts # Common types
│ ├── api.ts # API response types
│ ├── user.ts # User-related types
│ └── database.ts # Database types (Prisma generated)
│
├── actions/ # Server Actions (Next.js 14+)
│ ├── auth.ts # Auth actions
│ ├── user.ts # User actions
│ └── posts.ts # Posts actions
│
├── config/ # Configuration files
│ ├── site.ts # Site metadata (name, description, etc.)
│ └── navigation.ts # Navigation menu config
│
├── prisma/ # Prisma ORM (if using database)
│ ├── schema.prisma # Database schema
│ └── migrations/ # Database migrations
│
├── public/ # Static assets
│ ├── images/
│ ├── icons/
│ └── fonts/
│
├── styles/ # Global styles
│ └── globals.css # Tailwind imports + custom styles
│
├── .env.local # Environment variables (gitignored)
├── .env.example # Environment variables template
├── .eslintrc.json # ESLint config
├── .prettierrc # Prettier config
├── components.json # shadcn/ui config
├── next.config.js # Next.js config
├── tailwind.config.ts # Tailwind config
├── tsconfig.json # TypeScript config
├── package.json # Dependencies
└── README.md # Project documentation
app/ - Next.js 14 App Router pages and layouts. Use route groups (name) for logical grouping without affecting URLs.
components/ - All React components, organized by type:
ui/ - shadcn/ui components (copy-paste, customizable)layout/ - Shared layout components (header, footer, sidebar)features/ - Feature-specific components (scoped to one feature)shared/ - Reusable components used across featureslib/ - Utility functions, configurations, and third-party library setups.
hooks/ - Custom React hooks, especially React Query hooks for API calls.
types/ - TypeScript type definitions and interfaces.
actions/ - Server Actions for form handling and server-side operations (Next.js 14+).
config/ - App configuration (site metadata, navigation menus, constants).
prisma/ - Database schema and migrations (if using Prisma).
public/ - Static files served at root URL.
styles/ - Global CSS (Tailwind imports + custom styles).
Keep user informed at every step — this is a live build log.
⚠️ Important: All projects use PM2 for dev server management (port 3002 by default). This ensures:
Ask:
Create Next.js project:
npx create-next-app@latest <project-name> \
--typescript \
--tailwind \
--app \
--no-src-dir \
--import-alias "@/*"
→ Message user: "Next.js project initialized ✓"
Create all necessary directories following industry best practices:
cd <project-name>
# Create app route groups
mkdir -p app/\(auth\)/login app/\(auth\)/register
mkdir -p app/\(dashboard\)/dashboard app/\(dashboard\)/profile app/\(dashboard\)/settings
mkdir -p app/api/auth app/api/users
# Create component directories
mkdir -p components/ui components/layout components/features components/shared
mkdir -p components/features/auth components/features/dashboard components/features/profile
# Create utility directories
mkdir -p lib hooks types actions config
# Create static asset directories
mkdir -p public/images public/icons public/fonts
# Create styles directory
mkdir styles
# Create Prisma directory (if using database)
# mkdir -p prisma
Create essential config files:
config/site.ts - Site metadata
export const siteConfig = {
name: '<Project Name>',
description: '<Project Description>',
url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
links: {
github: 'https://github.com/...',
},
};
config/navigation.ts - Navigation menu
export const mainNav = [
{ title: 'Home', href: '/' },
{ title: 'Dashboard', href: '/dashboard' },
{ title: 'Profile', href: '/profile' },
];
export const dashboardNav = [
{ title: 'Overview', href: '/dashboard' },
{ title: 'Profile', href: '/profile' },
{ title: 'Settings', href: '/settings' },
];
.env.example - Environment variables template
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api
DATABASE_URL=postgresql://...
NEXTAUTH_SECRET=...
NEXTAUTH_URL=http://localhost:3000
→ Message user: "Directory structure created ✓"
Core dependencies:
cd <project-name>
npm install axios @tanstack/react-query
npm install -D @types/node
shadcn/ui setup (recommended):
npx shadcn-ui@latest init
This will prompt for configuration. Recommended answers:
Install essential shadcn components:
npx shadcn-ui@latest add button card input label select textarea
npx shadcn-ui@latest add dropdown-menu dialog sheet tabs
npx shadcn-ui@latest add table form avatar badge separator toast
Install form dependencies (for shadcn/ui forms):
npm install react-hook-form @hookform/resolvers zod
Optional (ask user based on needs):
npm install zustand # State management
npm install next-auth # Authentication
npm install prisma @prisma/client # Database ORM
→ Message user: "Dependencies + shadcn/ui installed ✓"
lib/api.ts (axios instance)import axios from 'axios';
export const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000/api',
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
});
// Request interceptor (add auth tokens, etc.)
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor (handle errors globally)
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized
}
return Promise.reject(error);
}
);
lib/react-query.ts (query client)import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false,
retry: 1,
},
},
});
app/providers.tsx (wrap app with providers)'use client';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from '@/lib/react-query';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Update app/layout.tsx to use Providers.
→ Message user: "Base configuration complete ✓"
Ask what features/pages to build. For each feature:
app/<feature>/page.tsx)components/features/<feature>/)hooks/use<Feature>.ts) using react-querytypes/<feature>.ts)app/api/<feature>/route.ts)Example: User Profile Feature
// types/user.ts
export interface User {
id: string;
name: string;
email: string;
}
// hooks/useUser.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import type { User } from '@/types/user';
export const useUser = (id: string) => {
return useQuery({
queryKey: ['user', id],
queryFn: async () => {
const { data } = await api.get<User>(`/users/${id}`);
return data;
},
});
};
export const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (user: Partial<User>) => {
const { data } = await api.patch<User>(`/users/${user.id}`, user);
return data;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['user', data.id] });
},
});
};
// app/profile/[id]/page.tsx
'use client';
import { useUser, useUpdateUser } from '@/hooks/useUser';
export default function ProfilePage({ params }: { params: { id: string } }) {
const { data: user, isLoading, error } = useUser(params.id);
const updateUser = useUpdateUser();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user?.name}</h1>
<p>{user?.email}</p>
</div>
);
}
→ Message user after each feature: "Profile page complete ✓"
Use shadcn/ui components (already installed) for consistent, accessible UI. Apply Design Principles (see below).
Example: Profile page with shadcn/ui
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
export default function ProfilePage({ params }: { params: { id: string } }) {
const { data: user, isLoading } = useUser(params.id);
if (isLoading) return <Card className="w-full max-w-2xl mx-auto"><CardContent>Loading...</CardContent></Card>;
return (
<Card className="w-full max-w-2xl mx-auto">
<CardHeader>
<div className="flex items-center gap-4">
<Avatar className="h-20 w-20">
<AvatarImage src={user?.avatar} />
<AvatarFallback>{user?.name[0]}</AvatarFallback>
</Avatar>
<div>
<CardTitle>{user?.name}</CardTitle>
<p className="text-sm text-muted-foreground">{user?.email}</p>
</div>
</div>
</CardHeader>
<CardContent>
<Button>Edit Profile</Button>
</CardContent>
</Card>
);
}
When to add more components:
npx shadcn-ui@latest add form input labelnpx shadcn-ui@latest add tablenpx shadcn-ui@latest add navigation-menunpx shadcn-ui@latest add toast alert→ Message user: "UI built with shadcn/ui ✓"
Important: Use PM2 to manage the dev server (ensures only 1 instance runs, prevents port conflicts).
Start dev server with PM2:
# Stop any existing instance of this project
pm2 delete <project-name> 2>/dev/null || true
# Start with PM2 (port 3002 for nginx proxy)
PORT=3002 pm2 start npm --name "<project-name>" --cwd "$(pwd)" -- run dev
# Give PM2 a moment to start
sleep 2
Wait for server to be fully ready (critical - avoid white screen screenshots):
# Wait for "Ready in" message in PM2 logs (usually 5-15 seconds)
timeout=30
elapsed=0
while [ $elapsed -lt $timeout ]; do
if pm2 logs <project-name> --nostream --lines 50 2>/dev/null | grep -q "Ready in"; then
echo "Server ready!"
sleep 3 # Extra buffer for module loading
break
fi
sleep 1
elapsed=$((elapsed + 1))
done
# Verify server is responding
if ! curl -s http://localhost:3002 > /dev/null; then
echo "Warning: Server not responding on port 3002"
pm2 logs <project-name> --nostream --lines 20
fi
Take screenshots (requires chromium):
bash scripts/screenshot.sh "http://localhost:3002" /tmp/review-desktop.png 1400 900
bash scripts/screenshot.sh "http://localhost:3002" /tmp/review-mobile.png 390 844
Review Checklist (analyze with image tool):
If issues found: Fix responsive classes, re-run screenshots.
Common fixes:
p-4 md:p-8 lg:p-12text-2xl md:text-4xlmax-w-full or px-4→ Message user: "Review complete, sending preview..."
Create .env.local:
NEXT_PUBLIC_API_BASE_URL=https://api.example.com
DATABASE_URL=postgresql://...
NEXTAUTH_SECRET=...
Create .env.example (template for user).
→ Message user: "Environment template created ✓"
Update package.json scripts:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit"
}
}
Create README.md with:
→ Message user: "Documentation complete ✓"
Stop PM2 dev server (if running):
pm2 delete <project-name> 2>/dev/null || true
pm2 save # Persist PM2 process list
Zip the project:
cd .. && zip -r /tmp/<project-name>.zip <project-name>/
Send via message tool with filePath.
Provide deployment options:
npx vercelnpm run build && netlify deploy→ Message user: "Project ready! 🚀"
1. PM2 dev server (always running after Step 7):
# Check status
pm2 list
# View logs
pm2 logs <project-name>
# Access locally
curl http://localhost:3002
2. Live preview URLs:
http://localhost:3002http://<server-ip>:<external-port>3. Screenshot review (if chromium enabled):
# Desktop (1400x900)
bash scripts/screenshot.sh "http://localhost:3002" /tmp/desktop.png 1400 900
# Mobile (390x844)
bash scripts/screenshot.sh "http://localhost:3002" /tmp/mobile.png 390 844
Full test sequence:
# 1. Check PM2 status
pm2 list | grep <project-name>
# 2. Verify dev server responding
curl -I http://localhost:3002
# 3. Take screenshots for visual verification
bash scripts/screenshot.sh "http://localhost:3002" /tmp/test-desktop.png 1400 900
bash scripts/screenshot.sh "http://localhost:3002" /tmp/test-mobile.png 390 844
# 4. Check logs for errors
pm2 logs <project-name> --lines 50 | grep -i error
# 5. Test API endpoints (if using API routes)
curl http://localhost:3002/api/health # Example health check
# 6. Production build test
npm run build && npm run start # Test production build
# 7. Type check
npm run type-check
Scenario 1: Test responsive design
# Mobile, tablet, desktop
for width in 390 768 1400; do
bash scripts/screenshot.sh "http://localhost:3002" /tmp/screen-${width}.png $width 900
done
Scenario 2: Test specific page/route
# Take screenshot of specific route
bash scripts/screenshot.sh "http://localhost:3002/dashboard" /tmp/dashboard.png 1400 900
Scenario 3: Test after making changes
# PM2 auto-reloads on file changes, verify in logs
pm2 logs <project-name> --lines 20
# Wait for "compiled successfully" then take new screenshot
bash scripts/screenshot.sh "http://localhost:3002" /tmp/updated.png 1400 900
Option 1: Screenshots
Option 2: Nginx proxy + external access
http://<server-ip>:<port>Option 3: Export & deploy
Use axios + react-query:
// hooks/usePosts.ts
import { useQuery, useMutation } from '@tanstack/react-query';
import { api } from '@/lib/api';
export const usePosts = () => {
return useQuery({
queryKey: ['posts'],
queryFn: async () => {
const { data } = await api.get('/posts');
return data;
},
});
};
export const useCreatePost = () => {
return useMutation({
mutationFn: async (post: { title: string; body: string }) => {
const { data } = await api.post('/posts', post);
return data;
},
});
};
Install:
npm install @apollo/client graphql
Setup Apollo Client, use useQuery and useMutation from Apollo.
For Next.js API routes with type safety:
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next
For form handling without API routes:
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title');
// ...
}
Always ask user which pattern they prefer for their use case.
Apply these consistently. These are quality standards.
p-4 or px-4 py-6 (never p-24 on mobile!)md:p-8 or md:px-6 md:py-8lg:p-12 xl:p-24<main className="p-4 md:p-8 lg:p-12">text-2xl → Desktop: md:text-4xltext-lg → Desktop: md:text-2xlmax-w-full on containersbreak-wordsgrid-cols-1 md:grid-cols-2 lg:grid-cols-3<Button> component with variants (default, destructive, outline, ghost)<Form> with react-hook-form integration<Card> component for content sections<Dialog> or <Sheet> components<Skeleton> component for loading UI<Alert> component for error messages<Table> component for tabular datashadcn/ui benefits: Accessible, customizable, copy-paste friendly, works with Tailwind
any (use unknown if needed)next/image)'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { useMutation } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useToast } from '@/components/ui/use-toast';
const formSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
});
export default function ContactForm() {
const { toast } = useToast();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { name: '', email: '' },
});
const mutation = useMutation({
mutationFn: async (data: z.infer<typeof formSchema>) => {
const res = await api.post('/contact', data);
return res.data;
},
onSuccess: () => {
toast({ title: 'Success', description: 'Message sent!' });
form.reset();
},
onError: (error) => {
toast({ title: 'Error', description: error.message, variant: 'destructive' });
},
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data))} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Sending...' : 'Send Message'}
</Button>
</form>
</Form>
);
}
Note: Run npx shadcn-ui@latest add form toast and install npm install react-hook-form @hookform/resolvers zod for this pattern.
const usePaginatedPosts = (page: number) => {
return useQuery({
queryKey: ['posts', page],
queryFn: async () => {
const { data } = await api.get(`/posts?page=${page}`);
return data;
},
keepPreviousData: true, // Smooth transitions
});
};
import { useInfiniteQuery } from '@tanstack/react-query';
const useInfinitePosts = () => {
return useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 1 }) => {
const { data } = await api.get(`/posts?page=${pageParam}`);
return data;
},
getNextPageParam: (lastPage, pages) => lastPage.nextPage,
});
};
any instead of proper TypeScript types@hookform/resolvers and zod before using shadcn forms<Toaster /> component when using toast notificationsProblem: Screenshots show blank white page Cause: Dev server not fully initialized before screenshot Solution:
Problem: React error "Module not found: Can't resolve @tanstack/react-query" Cause: Dev server started before all packages loaded Solution:
pkill -f "next dev" && npm run devls node_modules/@tanstack/npm install before starting dev serverProblem: Port already in use (EADDRINUSE error) Solution (PM2 method):
# Check what's running
pm2 list
# Stop the conflicting process
pm2 delete <project-name>
# Or check port directly
lsof -ti:3002
# Kill process on port (if not PM2-managed)
kill -9 $(lsof -ti:3002)
# Restart with PM2
PORT=3002 pm2 start npm --name "<project-name>" --cwd "$(pwd)" -- run dev
List all PM2 processes:
pm2 list
Check logs:
pm2 logs <project-name> --lines 50
Restart a process:
pm2 restart <project-name>
Stop a process:
pm2 stop <project-name>
Delete a process:
pm2 delete <project-name>
Ensure only one instance runs:
# Always delete before starting
pm2 delete <project-name> 2>/dev/null || true
PORT=3002 pm2 start npm --name "<project-name>" --cwd "$(pwd)" -- run dev
Common PM2 scenarios:
pm2 logs <project-name>pm2 logs <project-name> | grep compiledpm2 delete all && pm2 listpm2 monit (real-time monitoring)pm2 save (persists across reboots)When user requests changes:
npm run type-checkpm2 logs <project-name> --lines 20Always explain what changed and why.
# Start dev server
pm2 delete <project-name> 2>/dev/null || true
PORT=3002 pm2 start npm --name "<project-name>" --cwd "$(pwd)" -- run dev
# Check status
pm2 list
pm2 logs <project-name>
# Take screenshots
bash scripts/screenshot.sh "http://localhost:3002" /tmp/desktop.png 1400 900
bash scripts/screenshot.sh "http://localhost:3002" /tmp/mobile.png 390 844
# Test production build
npm run build && npm run start
# Type check
npm run type-check
components/ui/ (shadcn), components/features/ (custom)app/*/page.tsxapp/api/*/route.tsapp/globals.css, tailwind.config.tsnext.config.ts, .env.localnpx shadcn-ui@latest add button input form card table dialog toast
pm2 delete <name> then restartnpm install then restart PM2npm run type-check