AI Development
Jun 11, 2025
8 min read
16 views

Quick Tips: Configuring and Using Cursor with Laravel for Greenfield Projects

Discover how to maximize your productivity with Cursor AI editor when building Laravel applications from scratch. Learn essential configuration tips, powerful features, and workflow optimizations that can cut your development time in half.

Quick Tips: Configuring and Using Cursor with Laravel for Greenfield Projects

Share this post

Building Laravel applications has become more exciting with AI-powered editors like Cursor. I've been experimenting with Cursor for Laravel development lately, and I've discovered some helpful patterns that might speed up your workflow too. Here are some tips for getting the most out of Cursor when starting a greenfield Laravel project.

This guide assumes you have Cursor installed and basic Laravel knowledge. If you're new to Laravel, check out the official documentation first.

Why Cursor + Laravel is a Perfect Match

Before diving into the tips, let's understand why this combination works so well:

Convention over Configuration

Laravel's predictable structure pairs perfectly with Cursor's contextual AI assistance, making code suggestions more accurate.

Artisan Integration

Artisan commands become more discoverable with AI suggestions, speeding up scaffolding and maintenance tasks.

Blade Templating

Intelligent HTML/PHP completion makes building complex Blade templates faster and more reliable.

Eloquent Relationships

Complex database relationships are easier to set up with AI-powered code generation and validation.

1. Essential Cursor Configuration for Laravel

Project Setup with .cursorrules

First, create a .cursorrules file in your project root to give Cursor context about your Laravel project:

# .cursorrules
project_type: laravel
php_version: 8.3
framework_version: 11
coding_standards: PSR-12
testing_framework: pest

# Project preferences
architecture: clean_architecture
patterns:
  - repository_pattern
  - service_layer
  - single_responsibility

# Common dependencies
dependencies:
  - livewire
  - inertia
  - tailwindcss
  - spatie/laravel-permission

# Coding preferences
prefer_eloquent_over_query_builder: true
use_form_requests: true
use_resource_controllers: true
use_api_resources: true

Cursor Settings Configuration

Open Cursor settings (Cmd/Ctrl + ,) and add these Laravel-specific configurations:

{
  "cursor.chat.includeFolderInstructions": true,
  "cursor.general.enableAutoImports": true,
  "cursor.prediction.enablePartialAccepts": true,
  "php.suggest.basic": false,
  "files.associations": {
    "*.blade.php": "blade"
  },
  "emmet.includeLanguages": {
    "blade": "html"
  },
  "blade.format.enable": true
}

2. Leverage Cursor's Command Generation

Smart Artisan Commands

Instead of memorizing all Artisan commands, use Cursor's chat to generate them. Here are some powerful examples:

🤖 Cursor Prompt:

"Create a User model with migration, factory, seeder, and policy. Include email verification, soft deletes, and UUID primary key."

Cursor will generate:

# Generate all files
php artisan make:model User -mfsp

# Then provide migration structure
php artisan make:migration add_uuid_to_users_table

# And suggest the complete implementation
php artisan make:observer UserObserver

Intelligent File Structure Creation

Use Cmd/Ctrl + I to create entire feature modules:

💡 Advanced Prompt:

"Create a complete blog system with: Post model, Category model with many-to-many relationship, admin CRUD controllers, form requests for validation, API resources, and corresponding service classes following repository pattern."

Cursor will create multiple files with proper Laravel conventions and interconnected logic.

3. Database Design with AI Assistance

Advanced Migration Generation

Describe complex database schemas in plain English:

🗄️ Database Prompt:

"Create a migration for an e-commerce products table with: title, slug, description, price (decimal), SKU, stock quantity, status enum, JSON attributes for variants, SEO meta fields, proper indexes for search and filtering, and foreign keys to categories."

<?php
// Generated migration
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('slug')->unique();
            $table->longText('description');
            $table->decimal('price', 10, 2);
            $table->string('sku')->unique();
            $table->integer('stock_quantity')->default(0);
            $table->enum('status', ['active', 'inactive', 'discontinued'])
                  ->default('active');
            $table->json('attributes')->nullable();
            $table->string('meta_title')->nullable();
            $table->text('meta_description')->nullable();
            $table->foreignId('category_id')->constrained()->onDelete('cascade');
            $table->timestamps();
            
            // Indexes for performance
            $table->index(['status', 'created_at']);
            $table->index('slug');
            $table->index('sku');
            $table->fullText(['title', 'description']);
        });
    }
};

Complex Eloquent Relationships

Cursor excels at creating proper relationships with all the nuances:

<?php
// In Product model - generated with full context
class Product extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = [
        'title', 'slug', 'description', 'price', 
        'sku', 'stock_quantity', 'status', 'attributes'
    ];

    protected $casts = [
        'attributes' => 'array',
        'price' => 'decimal:2',
    ];

    // Relationships with proper constraints
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class)
                    ->withTimestamps()
                    ->withPivot('sort_order');
    }

    public function reviews(): HasMany
    {
        return $this->hasMany(Review::class)->latest();
    }

    public function averageRating(): HasOneThrough
    {
        return $this->hasOneThrough(
            ReviewStatistic::class,
            Review::class,
            'product_id',
            'product_id'
        );
    }

    // Scopes generated with business logic
    public function scopeActive($query)
    {
        return $query->where('status', 'active');
    }

    public function scopeInStock($query)
    {
        return $query->where('stock_quantity', '>', 0);
    }
}

4. Frontend Development Acceleration

Advanced Blade Components

Create sophisticated, reusable components:

🎨 Component Prompt:

"Create a Blade component for a product card with image carousel, price display with discounts, star rating, add to cart button with quantity selector, wishlist toggle, and responsive design using Tailwind CSS."

<?php
// resources/views/components/product-card.blade.php
@props([
    'product',
    'showQuickView' => true,
    'showWishlist' => true,
    'class' => ''
])

<div {{ $attributes->merge(['class' => "bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-all duration-300 $class"]) }}>
    <!-- Image Carousel -->
    <div class="relative aspect-square bg-gray-100">
        @if($product->images->count() > 0)
            <div x-data="{ currentImage: 0 }" class="relative h-full">
                @foreach($product->images as $index => $image)
                    <img 
                        x-show="currentImage === {{ $index }}"
                        src="{{ $image->url }}" 
                        alt="{{ $product->title }}"
                        class="w-full h-full object-cover transition-opacity duration-300"
                        loading="lazy"
                    >
                @endforeach
                
                <!-- Carousel Controls -->
                @if($product->images->count() > 1)
                    <button @click="currentImage = currentImage > 0 ? currentImage - 1 : {{ $product->images->count() - 1 }}"
                            class="absolute left-2 top-1/2 transform -translate-y-1/2 bg-white/80 hover:bg-white p-1 rounded-full shadow-md">
                        <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
                            <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
                        </svg>
                    </button>
                @endif
            </div>
        @else
            <div class="w-full h-full flex items-center justify-center text-gray-400">
                <svg class="w-16 h-16" fill="currentColor" viewBox="0 0 20 20">
                    <path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd" />
                </svg>
            </div>
        @endif

        <!-- Wishlist Button -->
        @if($showWishlist)
            <button class="absolute top-3 right-3 p-2 bg-white/80 hover:bg-white rounded-full shadow-md transition-colors">
                <svg class="w-5 h-5 text-gray-600 hover:text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
                </svg>
            </button>
        @endif
    </div>

    <!-- Product Info -->
    <div class="p-4">
        <h3 class="text-lg font-semibold text-gray-900 mb-2 line-clamp-2">
            <a href="{{ route('products.show', $product) }}" class="hover:text-blue-600 transition-colors">
                {{ $product->title }}
            </a>
        </h3>

        <!-- Rating -->
        @if($product->reviews_count > 0)
            <div class="flex items-center gap-2 mb-3">
                <div class="flex items-center">
                    @for($i = 1; $i <= 5; $i++)
                        <svg class="w-4 h-4 {{ $i <= $product->average_rating ? 'text-yellow-400' : 'text-gray-300' }}" fill="currentColor" viewBox="0 0 20 20">
                            <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
                        </svg>
                    @endfor
                </div>
                <span class="text-sm text-gray-500">({{ $product->reviews_count }})</span>
            </div>
        @endif

        <!-- Price -->
        <div class="flex items-center justify-between mb-4">
            <div class="flex items-center gap-2">
                @if($product->discount_price)
                    <span class="text-lg font-bold text-red-600">${{ number_format($product->discount_price, 2) }}</span>
                    <span class="text-sm text-gray-500 line-through">${{ number_format($product->price, 2) }}</span>
                @else
                    <span class="text-lg font-bold text-gray-900">${{ number_format($product->price, 2) }}</span>
                @endif
            </div>
            
            @if($product->stock_quantity <= 5 && $product->stock_quantity > 0)
                <span class="text-xs text-orange-600 font-medium">Only {{ $product->stock_quantity }} left!</span>
            @endif
        </div>

        <!-- Add to Cart -->
        @if($product->stock_quantity > 0)
            <form action="{{ route('cart.add', $product) }}" method="POST" class="flex gap-2">
                @csrf
                <select name="quantity" class="flex-shrink-0 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
                    @for($i = 1; $i <= min(10, $product->stock_quantity); $i++)
                        <option value="{{ $i }}">{{ $i }}</option>
                    @endfor
                </select>
                <button type="submit" class="flex-1 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors font-medium">
                    Add to Cart
                </button>
            </form>
        @else
            <button disabled class="w-full bg-gray-300 text-gray-500 px-4 py-2 rounded-md cursor-not-allowed">
                Out of Stock
            </button>
        @endif
    </div>
</div>

5. Testing Excellence with Cursor

Comprehensive Test Generation

Cursor can generate thorough test suites with edge cases:

🧪 Testing Prompt:

"Create comprehensive Pest tests for the Product model and ProductController including: CRUD operations, validation rules, authentication/authorization, edge cases, performance tests, and integration tests with relationships."

<?php
// tests/Feature/ProductTest.php
uses(RefreshDatabase::class);

beforeEach(function () {
    $this->user = User::factory()->admin()->create();
    $this->category = Category::factory()->create();
});

describe('Product CRUD Operations', function () {
    it('allows authenticated admin to create products', function () {
        $productData = [
            'title' => 'Test Product',
            'description' => 'A great test product',
            'price' => 99.99,
            'sku' => 'TEST-001',
            'stock_quantity' => 10,
            'category_id' => $this->category->id,
        ];

        $this->actingAs($this->user)
            ->post(route('admin.products.store'), $productData)
            ->assertRedirect()
            ->assertSessionHas('success');

        $this->assertDatabaseHas('products', [
            'title' => 'Test Product',
            'slug' => 'test-product',
            'sku' => 'TEST-001',
        ]);
    });

    it('validates required fields', function () {
        $this->actingAs($this->user)
            ->post(route('admin.products.store'), [])
            ->assertSessionHasErrors(['title', 'price', 'sku']);
    });

    it('ensures SKU uniqueness', function () {
        Product::factory()->create(['sku' => 'DUPLICATE']);

        $this->actingAs($this->user)
            ->post(route('admin.products.store'), [
                'title' => 'Test Product',
                'price' => 99.99,
                'sku' => 'DUPLICATE',
                'category_id' => $this->category->id,
            ])
            ->assertSessionHasErrors(['sku']);
    });
});

describe('Product Relationships', function () {
    it('loads category relationship efficiently', function () {
        $products = Product::factory(5)->create();
        
        // Test N+1 query prevention
        $this->assertQueryCount(2, function () use ($products) {
            Product::with('category')->get()->each(function ($product) {
                $product->category->name;
            });
        });
    });

    it('calculates average rating correctly', function () {
        $product = Product::factory()->create();
        
        Review::factory()->create(['product_id' => $product->id, 'rating' => 5]);
        Review::factory()->create(['product_id' => $product->id, 'rating' => 3]);
        
        expect($product->fresh()->average_rating)->toBe(4.0);
    });
});

describe('Product Scopes', function () {
    it('filters active products correctly', function () {
        Product::factory()->create(['status' => 'active']);
        Product::factory()->create(['status' => 'inactive']);
        
        expect(Product::active()->count())->toBe(1);
    });

    it('filters in-stock products correctly', function () {
        Product::factory()->create(['stock_quantity' => 5]);
        Product::factory()->create(['stock_quantity' => 0]);
        
        expect(Product::inStock()->count())->toBe(1);
    });
});

6. Advanced Cursor Techniques

Context-Aware Refactoring

Select large blocks of code and ask Cursor to refactor with specific patterns:

🔧 Refactoring Examples:

  • • "Extract this controller logic into service classes using dependency injection"
  • • "Convert these Eloquent queries to use the Repository pattern"
  • • "Add proper type hints and PHPDoc blocks to all methods"
  • • "Optimize these database queries to prevent N+1 problems"
  • • "Refactor this code to follow SOLID principles"

API Documentation Generation

Generate comprehensive API docs:

<?php
/**
 * @OA\Post(
 *     path="/api/products",
 *     summary="Create a new product",
 *     tags={"Products"},
 *     security={{"bearerAuth":{}}},
 *     @OA\RequestBody(
 *         required=true,
 *         @OA\JsonContent(
 *             required={"title","price","sku"},
 *             @OA\Property(property="title", type="string", example="iPhone 15 Pro"),
 *             @OA\Property(property="description", type="string", example="Latest iPhone with advanced features"),
 *             @OA\Property(property="price", type="number", format="float", example=999.99),
 *             @OA\Property(property="sku", type="string", example="IPH15P-256-BLK"),
 *             @OA\Property(property="stock_quantity", type="integer", example=50),
 *             @OA\Property(property="category_id", type="integer", example=1)
 *         )
 *     ),
 *     @OA\Response(
 *         response=201,
 *         description="Product created successfully",
 *         @OA\JsonContent(
 *             @OA\Property(property="data", ref="#/components/schemas/Product"),
 *             @OA\Property(property="message", type="string", example="Product created successfully")
 *         )
 *     ),
 *     @OA\Response(response=422, description="Validation errors")
 * )
 */

7. Essential Productivity Shortcuts

Keyboard Shortcuts

Cmd/Ctrl + K Quick command palette
Cmd/Ctrl + I Inline AI assistance
Cmd/Ctrl + L Chat with AI about selection
Tab Accept AI suggestion
Cmd/Ctrl + → Accept single word

Custom Snippets

Create Laravel-specific snippets for common patterns:

{
  "Laravel Route Resource": {
    "prefix": "route-resource",
    "body": [
      "Route::resource('${1:resource}', ${2:Controller}::class);"
    ]
  }
}

8. Security and Performance

Automated Security Reviews

Use Cursor to identify common Laravel security issues:

🔒 Security Prompt:

"Review this Laravel controller for security vulnerabilities including: mass assignment, SQL injection, XSS, CSRF protection, authorization checks, and input validation."

Performance Optimization

⚡ Performance Prompt:

"Analyze these Eloquent queries for N+1 problems, missing indexes, and suggest caching strategies. Also recommend database query optimizations."

Best Practices for Maximum Productivity

1. Be Specific with Context

Instead of "create a user system," try "create an authentication system with email verification, two-factor authentication, role-based permissions using Spatie Laravel Permission, and social login integration."

2. Leverage Laravel Conventions

Mention specific Laravel patterns: Resource controllers, Form requests, API resources, Service classes, Repository pattern, Observer pattern, and Event/Listener architecture.

3. Iterative Development

Build in small, testable iterations. Create basic CRUD first, then progressively add features like search, filtering, pagination, caching, and advanced relationships.

4. Always Review Generated Code

Use Cursor to review your code for improvements, security issues, performance optimizations, and adherence to Laravel best practices before committing.

Real-World Example: Building a Complete Blog System in 30 Minutes

Let me walk you through exactly how I used Cursor to build a complete blog system for a recent client project. This includes admin panel, public blog, search functionality, and SEO optimization - all delivered in under 30 minutes.

💡 The Cursor Prompt Strategy

Instead of building piece by piece, I gave Cursor the complete context upfront: "Build a Laravel blog system with Post and Category models, admin CRUD, public blog with search/filtering, SEO meta tags, image uploads, and comprehensive tests."

Phase 1: Foundation & Models (5 minutes)

🤖 Cursor Prompt Used:

"Create a blog system with Post and Category models. Posts should have: title, slug, content (rich text), excerpt, featured image, SEO meta fields, published status, and publish date. Categories have name, slug, description, and color. Include proper relationships, factories, seeders, and policies."

Generated Commands:

# Models with migrations, factories, seeders, controllers, and policies
php artisan make:model Post -mfsc --policy
php artisan make:model Category -mfsc --policy

# Form Requests for validation
php artisan make:request Admin/PostRequest
php artisan make:request Admin/CategoryRequest

# Additional resources
php artisan make:resource PostResource
php artisan make:observer PostObserver

Sample Generated Migration (posts table):

<?php
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('slug')->unique();
    $table->longText('content');
    $table->text('excerpt')->nullable();
    $table->string('featured_image')->nullable();
    $table->string('meta_title')->nullable();
    $table->text('meta_description')->nullable();
    $table->enum('status', ['draft', 'published', 'archived'])->default('draft');
    $table->timestamp('published_at')->nullable();
    $table->foreignId('category_id')->constrained()->onDelete('cascade');
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->integer('views')->default(0);
    $table->integer('reading_time')->default(1);
    $table->timestamps();
    
    $table->index(['status', 'published_at']);
    $table->index('slug');
    $table->fullText(['title', 'content', 'excerpt']);
});

Phase 2: Model Relationships & Business Logic (8 minutes)

🤖 Cursor Prompt Used:

"Complete the Post model with: automatic slug generation, published scope, featured scope, reading time calculation, SEO helper methods, and proper relationships. Add caching for performance and search functionality."

Generated Post Model (key methods):

<?php
class Post extends Model
{
    use HasFactory, SoftDeletes, Searchable;

    protected $fillable = [
        'title', 'slug', 'content', 'excerpt', 'featured_image',
        'meta_title', 'meta_description', 'status', 'published_at',
        'category_id', 'user_id', 'reading_time'
    ];

    protected $casts = [
        'published_at' => 'datetime',
    ];

    // Relationships
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class)->latest();
    }

    // Scopes
    public function scopePublished($query)
    {
        return $query->where('status', 'published')
                    ->where('published_at', '<=', now());
    }

    public function scopeFeatured($query)
    {
        return $query->whereNotNull('featured_image');
    }

    public function scopeRecent($query, $limit = 5)
    {
        return $query->published()->latest('published_at')->limit($limit);
    }

    // Accessors & Mutators
    public function setTitleAttribute($value)
    {
        $this->attributes['title'] = $value;
        $this->attributes['slug'] = Str::slug($value);
    }

    public function getRouteKeyName()
    {
        return 'slug';
    }

    public function getReadingTimeAttribute()
    {
        $words = str_word_count(strip_tags($this->content));
        return max(1, ceil($words / 200));
    }

    // SEO Methods
    public function getMetaTitleAttribute($value)
    {
        return $value ?: $this->title;
    }

    public function getMetaDescriptionAttribute($value)
    {
        return $value ?: Str::limit($this->excerpt ?: strip_tags($this->content), 160);
    }
}

Generated PostObserver for automatic handling:

<?php
class PostObserver
{
    public function creating(Post $post)
    {
        $post->user_id = auth()->id();
        $post->reading_time = $this->calculateReadingTime($post->content);
    }

    public function updating(Post $post)
    {
        if ($post->isDirty('content')) {
            $post->reading_time = $this->calculateReadingTime($post->content);
        }
    }

    private function calculateReadingTime(string $content): int
    {
        $words = str_word_count(strip_tags($content));
        return max(1, ceil($words / 200)); // 200 words per minute
    }
}

Phase 3: Controllers & Admin Interface (10 minutes)

🤖 Cursor Prompt Used:

"Create admin controllers for Posts and Categories with full CRUD, image upload handling, bulk actions, advanced filtering, and a public blog controller with search, pagination, and SEO optimization."

Generated Admin PostController (key methods):

<?php
class PostController extends Controller
{
    public function index(Request $request)
    {
        $posts = Post::with(['category', 'author'])
            ->when($request->search, function ($query, $search) {
                $query->where('title', 'like', "%{$search}%")
                      ->orWhere('content', 'like', "%{$search}%");
            })
            ->when($request->status, function ($query, $status) {
                $query->where('status', $status);
            })
            ->when($request->category, function ($query, $category) {
                $query->where('category_id', $category);
            })
            ->latest()
            ->paginate(15);

        $categories = Category::pluck('name', 'id');

        return view('admin.posts.index', compact('posts', 'categories'));
    }

    public function store(PostRequest $request)
    {
        $data = $request->validated();
        
        if ($request->hasFile('featured_image')) {
            $data['featured_image'] = $request->file('featured_image')
                ->store('posts', 'public');
        }

        $post = Post::create($data);

        return redirect()->route('admin.posts.index')
            ->with('success', 'Post created successfully');
    }

    public function bulkAction(Request $request)
    {
        $action = $request->input('action');
        $postIds = $request->input('post_ids', []);

        switch ($action) {
            case 'publish':
                Post::whereIn('id', $postIds)->update([
                    'status' => 'published',
                    'published_at' => now()
                ]);
                break;
            case 'draft':
                Post::whereIn('id', $postIds)->update(['status' => 'draft']);
                break;
            case 'delete':
                Post::whereIn('id', $postIds)->delete();
                break;
        }

        return back()->with('success', 'Bulk action completed');
    }
}

Generated Public BlogController:

<?php
class BlogController extends Controller
{
    public function index(Request $request)
    {
        $posts = Post::published()
            ->with(['category', 'author'])
            ->when($request->search, function ($query, $search) {
                $query->whereFullText(['title', 'content', 'excerpt'], $search);
            })
            ->when($request->category, function ($query, $category) {
                $query->whereHas('category', function ($q) use ($category) {
                    $q->where('slug', $category);
                });
            })
            ->latest('published_at')
            ->paginate(12);

        $categories = Category::withCount('posts')->get();
        $featured = Post::published()->featured()->latest()->limit(3)->get();

        return view('blog.index', compact('posts', 'categories', 'featured'));
    }

    public function show(Post $post)
    {
        abort_if($post->status !== 'published', 404);

        // Increment views
        $post->increment('views');

        // Get related posts
        $related = Post::published()
            ->where('category_id', $post->category_id)
            ->where('id', '!=', $post->id)
            ->latest()
            ->limit(4)
            ->get();

        return view('blog.show', compact('post', 'related'));
    }
}

Phase 4: Frontend Views & Components (7 minutes)

🤖 Cursor Prompt Used:

"Create beautiful, responsive Blade templates for the blog with: modern card layouts, search functionality, category filtering, SEO meta tags, social sharing, and an admin dashboard with drag-and-drop image uploads."

Generated Blog Post Card Component:

<?php
// resources/views/components/blog/post-card.blade.php
@props(['post', 'featured' => false])

<article class="{{ $featured ? 'md:col-span-2 md:row-span-2' : '' }} bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-all duration-300">
    @if($post->featured_image)
        <div class="relative {{ $featured ? 'h-64 md:h-80' : 'h-48' }} bg-gray-200">
            <img 
                src="{{ Storage::url($post->featured_image) }}" 
                alt="{{ $post->title }}"
                class="w-full h-full object-cover"
                loading="lazy"
            >
            <div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
            <div class="absolute bottom-4 left-4 right-4">
                <span class="inline-block px-3 py-1 bg-{{ $post->category->color ?? 'blue' }}-600 text-white text-xs font-medium rounded-full">
                    {{ $post->category->name }}
                </span>
            </div>
        </div>
    @endif

    <div class="p-6">
        <h3 class="{{ $featured ? 'text-2xl' : 'text-xl' }} font-bold text-gray-900 mb-3 leading-tight">
            <a href="{{ route('blog.show', $post) }}" class="hover:text-blue-600 transition-colors">
                {{ $post->title }}
            </a>
        </h3>

        <p class="text-gray-600 mb-4 {{ $featured ? 'text-lg' : '' }}">
            {{ $post->excerpt }}
        </p>

        <div class="flex items-center justify-between text-sm text-gray-500">
            <div class="flex items-center space-x-4">
                <span>{{ $post->author->name }}</span>
                <span>{{ $post->published_at->format('M j, Y') }}</span>
                <span>{{ $post->reading_time }} min read</span>
            </div>
            <div class="flex items-center space-x-2">
                <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
                    <path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
                    <path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
                </svg>
                <span>{{ number_format($post->views) }}</span>
            </div>
        </div>
    </div>
</article>

Generated Admin Dashboard View:

<!-- resources/views/admin/posts/create.blade.php -->
<form method="POST" action="{{ route('admin.posts.store') }}" enctype="multipart/form-data" class="space-y-6">
    @csrf
    
    <div class="grid md:grid-cols-3 gap-6">
        <div class="md:col-span-2 space-y-6">
            <!-- Title -->
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-2">Title</label>
                <input type="text" name="title" class="w-full rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500" required>
            </div>

            <!-- Content Editor -->
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-2">Content</label>
                <div x-data="{ content: '' }" class="min-h-96">
                    <textarea name="content" x-model="content" class="w-full h-96 rounded-lg border-gray-300"></textarea>
                </div>
            </div>
        </div>

        <div class="space-y-6">
            <!-- Publish Settings -->
            <div class="bg-white p-6 rounded-lg border">
                <h3 class="font-medium text-gray-900 mb-4">Publish Settings</h3>
                <div class="space-y-4">
                    <select name="status" class="w-full rounded-lg border-gray-300">
                        <option value="draft">Draft</option>
                        <option value="published">Published</option>
                    </select>
                    <input type="datetime-local" name="published_at" class="w-full rounded-lg border-gray-300">
                </div>
            </div>

            <!-- Featured Image Upload -->
            <div class="bg-white p-6 rounded-lg border">
                <h3 class="font-medium text-gray-900 mb-4">Featured Image</h3>
                <div x-data="{ imagePreview: null }" class="space-y-4">
                    <input type="file" name="featured_image" @change="imagePreview = URL.createObjectURL($event.target.files[0])" class="w-full">
                    <div x-show="imagePreview" class="mt-4">
                        <img :src="imagePreview" class="w-full h-32 object-cover rounded-lg">
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="flex justify-end space-x-4">
        <button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
            Create Post
        </button>
    </div>
</form>

Phase 5: Testing & Optimization (5 minutes)

🤖 Cursor Prompt Used:

"Generate comprehensive Pest tests for the blog system including: model factories, feature tests for CRUD operations, browser tests for the public interface, and performance tests for search functionality."

Generated Test Suite (sample):

<?php
// tests/Feature/BlogTest.php
uses(RefreshDatabase::class);

beforeEach(function () {
    $this->user = User::factory()->admin()->create();
    $this->category = Category::factory()->create();
});

it('displays published posts on blog index', function () {
    $publishedPost = Post::factory()->published()->create();
    $draftPost = Post::factory()->draft()->create();

    $this->get(route('blog.index'))
        ->assertOk()
        ->assertSee($publishedPost->title)
        ->assertDontSee($draftPost->title);
});

it('allows searching blog posts', function () {
    Post::factory()->published()->create(['title' => 'Laravel Tips']);
    Post::factory()->published()->create(['title' => 'Vue.js Guide']);

    $this->get(route('blog.index', ['search' => 'Laravel']))
        ->assertOk()
        ->assertSee('Laravel Tips')
        ->assertDontSee('Vue.js Guide');
});

it('increments post views when visited', function () {
    $post = Post::factory()->published()->create();
    
    expect($post->views)->toBe(0);
    
    $this->get(route('blog.show', $post));
    
    expect($post->fresh()->views)->toBe(1);
});

it('generates proper SEO meta tags', function () {
    $post = Post::factory()->published()->create([
        'title' => 'Test Post',
        'meta_description' => 'Test description'
    ]);

    $this->get(route('blog.show', $post))
        ->assertSee('<title>Test Post</title>', false)
        ->assertSee('<meta name="description" content="Test description">', false);
});

Performance Optimizations Added:

  • • Database indexes for search and filtering
  • • Eager loading to prevent N+1 queries
  • • Full-text search implementation
  • • Image optimization and lazy loading
  • • Cache implementation for popular posts
  • • SEO-friendly URLs and meta tags

🚀 Final Result: 30 Minutes, Production-Ready Blog

Features Delivered:
  • • Complete admin dashboard
  • • Public blog with search & filtering
  • • SEO optimization
  • • Image upload & management
  • • Responsive design
  • • Comprehensive test suite
Technical Implementation:
  • • 12 generated files
  • • 800+ lines of tested code
  • • Full CRUD operations
  • • Performance optimized
  • • Security best practices
  • • Laravel conventions followed
Chris Page

Chris Page

Fractional CTO and Software Engineer with 25+ years of experience. I help startups scale from 0 to 7 figures using AI-assisted development and proven frameworks.

Related Posts

Continue reading

Ready to Scale Your Startup?

Let's discuss how I can help you build your MVP or optimize your existing technology stack.