Build a Personal Developer Portfolio Blog in Laravel – Full Tutorial with Free Source Code
Build a Developer Portfolio Blog with Laravel – Step-by-Step Guide
🛠️ Step 1: Install Laravel Project
a. Create Laravel Project
bash
composer create-project laravel/laravel developer
b. Serve the App
bash
cd developer
php artisan serve
Visit: http://localhost:8000
🧩 Step 2: Set Up Database
a. Create Database
Use phpMyAdmin or MySQL CLI:
sql
CREATE DATABASE developer_blog;
b. Configure .env
DB_DATABASE=developer_blog
DB_USERNAME=root
DB_PASSWORD=
🧱 Step 3: Create Post Model and Migration
bash
php artisan make:model Post -m
c. Edit Model File
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $guarded = [];
}
d. Edit Migration File
database/migrations/xxxx_create_posts_table.php:
php
public function up()
{ Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->timestamps();
});
}
e. Run Migration
bash
php artisan migrate
🧠 Step 4: Create PostController and Routes
a. Create Controller
bash
php artisan make:controller PostController --resource
b. Add Route
In routes/web.php:
php
use App\Http\Controllers\PostController;
Route::get('/', [PostController::class, 'index'])->name('home');
Route::resource('posts', PostController::class);
✏️ Step 5: Controller Logic (CRUD)
In PostController.php:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Post;
use Illuminate\Support\Str;
class PostController extends Controller
{
public function index() {
$posts = Post::latest()->paginate(5);
return view('posts.index', compact('posts'));
}
public function create() {
return view('posts.create');
}
public function store(Request $request) {
$request->validate([
'title' => 'required|max:255',
'content' => 'required',
]);
Post::create([
'title' => $request->title,
'slug' => Str::slug($request->title),
'content' => $request->content,
]);
return redirect()->route('home')->with('success', 'Post Created');
}
public function show($slug) {
$post = Post::where('slug', $slug)->firstOrFail();
return view('posts.show', compact('post'));
}
public function edit($id) {
$post = Post::findOrFail($id);
return view('posts.edit', compact('post'));
}
public function update(Request $request, $id) {
$post = Post::findOrFail($id);
$post->update([
'title' => $request->title,
'slug' => Str::slug($request->title),
'content' => $request->content,
]);
return redirect()->route('home')->with('success', 'Post Updated');
}
public function destroy($id) {
Post::findOrFail($id)->delete();
return redirect()->route('home')->with('success', 'Post Deleted');
}
}
📁 Step 6: Views (Blade Templates)
a. resources/views/layout.blade.php
blade
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="description" content="@yield('meta_description', 'Developer Blog Built with Laravel')">
<meta name="keywords" content="@yield('meta_keywords', 'Laravel, Developer Blog, Portfolio, PHP')">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Developer Blog')</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4"><a href="{{ route('home') }}">Developer Blog</a></h1>
@if(session('success')) <div class="alert alert-success">{{ session('success') }}</div>
@endif @yield('content')
</div>
</body>
</html>
b. resources/views/posts/index.blade.php
@extends('layout')
@section('title', 'All Blog Posts')
@section('meta_description', 'Read the latest blog posts from a Laravel developer portfolio.')
@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">📘 Developer Blog</h1>
<a href="{{ route('posts.create') }}" class="btn btn-success">➕ Create New Post</a>
</div>
<div class="row">
@foreach ($posts as $post)
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 shadow-sm">
<div class="card-body d-flex flex-column">
<h5 class="card-title">
<a href="{{ route('posts.show', $post->slug) }}" class="text-decoration-none text-primary">
{{ $post->title }}
</a>
</h5>
<p class="card-text">{{ Str::limit($post->content, 100) }}</p>
<div class="mt-auto">
<a href="{{ route('posts.edit', $post->id) }}" class="btn btn-sm btn-outline-warning me-2">✏️ Edit</a>
<form action="{{ route('posts.destroy', $post->id) }}" method="POST" class="d-inline">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger">🗑️ Delete</button>
</form>
</div>
</div>
</div>
</div>
@endforeach
</div>
<div class="d-flex justify-content-center mt-4">
{{ $posts->links() }}
</div>
@endsection
Note: Go to your app/Providers/AppServiceProvider.php file. Then add use Illuminate\Pagination\Paginator; and find out the boot() function. Then write this line Paginator::useBootstrap(); inside the function.
c. resources/views/posts/create.blade.php
@extends('layout')
@section('title', isset($post) ? 'Edit Post' : 'Create New Post')
@section('meta_description', isset($post) ? 'Edit an existing blog post.' : 'Write a new blog post.')
@section('content')
<div class="card shadow-sm">
<div class="card-body">
<h2 class="mb-4">
{{ isset($post) ? '✏️ Edit Post' : '📝 Create New Post' }}
</h2>
<form action="{{ isset($post) ? route('posts.update', $post->id) : route('posts.store') }}" method="POST">
@csrf
@if(isset($post))
@method('PUT')
@endif
<div class="mb-3">
<label for="title" class="form-label fw-bold">Title</label>
<input type="text" name="title" id="title" class="form-control"
value="{{ old('title', $post->title ?? '') }}" required>
</div>
<div class="mb-3">
<label for="content" class="form-label fw-bold">Content</label>
<textarea name="content" id="content" rows="6" class="form-control" required>{{ old('content', $post->content ?? '') }}</textarea>
</div>
<button type="submit" class="btn btn-success me-2">
{{ isset($post) ? '💾 Update Post' : '✅ Publish Post' }}
</button>
<a href="{{ route('posts.index') }}" class="btn btn-secondary">↩️ Cancel</a>
</form>
</div>
</div>
@endsection
d. resources/views/posts/edit.blade.php
@extends('layout')
@section('title', 'Edit Post')
@section('meta_description', 'Edit your blog post content and title easily.')
@section('content')
<div class="card shadow-sm">
<div class="card-body">
<h2 class="mb-4 text-warning">✏️ Edit Post</h2>
<form action="{{ route('posts.update', $post->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label for="title" class="form-label fw-bold">Title</label>
<input type="text" name="title" id="title" class="form-control"
value="{{ old('title', $post->title) }}" required>
</div>
<div class="mb-3">
<label for="content" class="form-label fw-bold">Content</label>
<textarea name="content" id="content" rows="6" class="form-control" required>{{ old('content', $post->content) }}</textarea>
</div>
<button type="submit" class="btn btn-primary me-2">💾 Update Post</button>
<a href="{{ route('posts.index') }}" class="btn btn-secondary">↩️ Cancel</a>
</form>
</div>
</div>
@endsection
e. resources/views/posts/show.blade.php
@extends('layout')
@section('title', $post->title)
@section('meta_description', Str::limit($post->content, 150))
@section('content')
<div class="card shadow-sm mb-4">
<div class="card-body">
<h1 class="card-title text-primary">{{ $post->title }}</h1>
<p class="text-muted mb-4">Published: {{ $post->created_at->format('F d, Y') }}</p>
<p class="card-text fs-5">{{ $post->content }}</p>
</div>
</div>
<a href="{{ route('posts.edit', $post->id) }}" class="btn btn-warning me-2">✏️ Edit</a>
<form action="{{ route('posts.destroy', $post->id) }}" method="POST" class="d-inline">
@csrf @method('DELETE')
<button class="btn btn-danger">🗑️ Delete</button>
</form>
<a href="{{ route('posts.index') }}" class="btn btn-secondary ms-2">⬅️ Back to Blog</a>
@endsection