Laravel Framework

Intermediate Laravel Application: Blog Platform

By the Domain India teamPublished 9 min read
Knowledge base article
Contents (11 sections)

A blog is the classic intermediate Laravel project: it touches every core feature, from migrations and Eloquent relationships to validation, authorisation, Blade views, search and pagination. This guide builds one step by step with current Laravel conventions, then shows how to deploy it on Domain India shared hosting, where Composer isn't pre-installed.

Key takeaways

Build the blog on your own computer: create the project with a Laravel starter kit for login and registration, add Post, Category, Tag and Comment models, then controllers with validation and a policy so only authors can edit their posts. Deploy by uploading the finished project, including vendor/ and the built assets. Jailed SSH, available on request, lets you run php artisan migrate there, and Composer too if you download composer.phar.

1. What you will build

Posts
Authors write, edit and delete their own posts, each with a category and several tags.
Comments
Signed-in readers comment on posts; comments show under each post.
Search and paging
Readers search titles and text, with paginated results.

You need PHP, Composer, Node.js and a database (SQLite or MySQL) on your own computer. Laravel's documentation lists the PHP version the current release requires; choose the same PHP version for your domain in the control panel before you deploy.

2. Create the project with authentication

On your computer, install the Laravel installer and create the project:

bash
composer global require laravel/installer
laravel new blog

When the installer asks, choose a starter kit. The Livewire kit keeps everything in Blade; the React and Vue kits use Inertia. Each kit includes registration, login, password reset and a profile page, so you don't need the old laravel/ui package or php artisan ui bootstrap --auth, which are legacy.

Composer in this guide runs on your computer

Every composer create-project, composer require and composer install in this guide runs on your own machine or in CI. On Domain India shared hosting Composer isn't pre-installed; over jailed SSH you can run php composer.phar install, and if it stops with a proc_open message, run it again with --no-scripts, then php artisan package:discover. See PHP disabled functions on shared hosting.

3. Models and migrations

Generate the models with migrations, controllers and policies:

bash
php artisan make:model Category -m
php artisan make:model Tag -m
php artisan make:model Post -mcr --policy
php artisan make:model Comment -mc
php artisan make:migration create_post_tag_table

Define the tables. The key parts of each migration's up() method:

php
// categories and tags
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();

// posts
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('body');
$table->timestamps();

// post_tag (pivot table)
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->primary(['post_id', 'tag_id']);

// comments
$table->id();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->text('body');
$table->timestamps();

Run php artisan migrate locally to create them.

4. Eloquent relationships

In app/Models/Post.php:

php
class Post extends Model
{
    protected $fillable = ['title', 'slug', 'body', 'category_id'];

    public function user()     { return $this->belongsTo(User::class); }
    public function category() { return $this->belongsTo(Category::class); }
    public function tags()     { return $this->belongsToMany(Tag::class); }
    public function comments() { return $this->hasMany(Comment::class)->latest(); }
}

Add the reverse sides: posts() as hasMany(Post::class) on User, Category and (as belongsToMany) on Tag, and post() and user() as belongsTo on Comment, with protected $fillable = ['body', 'user_id'];.

$fillable lists the only columns a form may set. Never pass $request->all() straight into a model: validate first and save only the validated fields.

5. Routes

In routes/web.php:

php
use App\Http\Controllers\CommentController;
use App\Http\Controllers\PostController;

Route::middleware('auth')->group(function () {
    Route::resource('posts', PostController::class)->except(['index', 'show']);
    Route::post('posts/{post}/comments', [CommentController::class, 'store'])
        ->name('posts.comments.store');
});

Route::resource('posts', PostController::class)->only(['index', 'show']);

The signed-in routes come first so that /posts/create isn't captured by the public /posts/{post} route. Use the class syntax shown; the old string form 'PostController' no longer works in current Laravel.

6. The post controller

Validate input, attach tags and categories, and check permissions with the policy:

php
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;

public function index(Request $request)
{
    $search = trim((string) $request->query('q', ''));

    $posts = Post::with(['user', 'category'])
        ->when($search, fn ($query) => $query->where(fn ($q) => $q
            ->where('title', 'like', "%{$search}%")
            ->orWhere('body', 'like', "%{$search}%")))
        ->latest()
        ->paginate(10)
        ->withQueryString();

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

public function create()
{
    return view('posts.create', [
        'categories' => Category::orderBy('name')->get(),
        'tags' => Tag::orderBy('name')->get(),
    ]);
}

public function store(Request $request)
{
    $data = $request->validate([
        'title' => ['required', 'string', 'max:200'],
        'body' => ['required', 'string'],
        'category_id' => ['nullable', 'exists:categories,id'],
        'tags' => ['array'],
        'tags.*' => ['integer', 'exists:tags,id'],
    ]);

    $post = $request->user()->posts()->create([
        ...Arr::except($data, 'tags'),
        'slug' => Str::slug($data['title']).'-'.Str::lower(Str::random(6)),
    ]);
    $post->tags()->sync($data['tags'] ?? []);

    return redirect()->route('posts.show', $post);
}

public function show(Post $post)
{
    $post->load(['user', 'category', 'tags', 'comments.user']);

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

public function update(Request $request, Post $post)
{
    Gate::authorize('update', $post);
    // validate and save as in store(), then sync tags
}

In app/Policies/PostPolicy.php, allow only the author to change a post:

php
public function update(User $user, Post $post): bool
{
    return $user->id === $post->user_id;
}

public function delete(User $user, Post $post): bool
{
    return $user->id === $post->user_id;
}

Call Gate::authorize('update', $post) in edit and update, and Gate::authorize('delete', $post) in destroy. Current Laravel finds the policy automatically.

The grouped where in index() keeps the orWhere inside the search, so it can't escape other conditions you add later. For large blogs, replace the like search with a full-text index or Laravel Scout.

7. Comments

In CommentController:

php
public function store(Request $request, Post $post)
{
    $data = $request->validate(['body' => ['required', 'string', 'max:2000']]);

    $post->comments()->create([...$data, 'user_id' => $request->user()->id]);

    return back();
}

For threaded replies, add a nullable parent_id column to comments that references another comment, or use a maintained comments package.

8. Blade views

resources/views/posts/show.blade.php (inside your starter kit's layout):

blade
<article>
    <h1>{{ $post->title }}</h1>
    <p>By {{ $post->user->name }} in {{ $post->category?->name ?? 'Uncategorised' }}</p>
    <div>{!! nl2br(e($post->body)) !!}</div>
    @foreach ($post->tags as $tag)
        <span>#{{ $tag->name }}</span>
    @endforeach
</article>

<section>
    @foreach ($post->comments as $comment)
        <p><strong>{{ $comment->user->name }}:</strong> {{ $comment->body }}</p>
    @endforeach

    @auth
        <form method="POST" action="{{ route('posts.comments.store', $post) }}">
            @csrf
            <textarea name="body" required></textarea>
            <button type="submit">Post comment</button>
        </form>
    @endauth
</section>

In create.blade.php, list tags as checkboxes named tags[] and categories in a select named category_id. In index.blade.php, add a search form that submits q with the GET method, loop over $posts, and call {{ $posts->links() }} for page links.

{{ }} escapes output, which protects you from script injection. If you add a rich-text editor, sanitise its HTML with an HTML purifier package before displaying it with {!! !!}; never print raw user HTML.

9. Finishing touches

  • Seed test data with model factories and php artisan db:seed so you can check paging and search.
  • Cover images: validate with 'cover' => ['nullable', 'image', 'max:2048'], store with $request->file('cover')->store('covers', 'public'), and run php artisan storage:link. If the link command fails on shared hosting, ask support.
  • Build front-end assets with npm run build on your computer; the server only needs the compiled files in public/build.
  • Before going live: set APP_ENV=production and APP_DEBUG=false, then run php artisan optimize.

10. Deploying the blog on Domain India shared hosting

A blog like this, with Blade, MySQL and no queue workers, runs well on our cPanel, DirectAdmin and Webuzo shared hosting.

  1. Build locally.
    Run composer install --no-dev --optimize-autoloader and npm run build on your computer.
  2. Create the database.
    In your control panel, create a MySQL database and user.
  3. Upload the project
    with its vendor/ and public/build folders, outside public_html, and point your domain at Laravel's public folder, as shown in how to install Laravel on shared hosting.
  4. Configure .env.
    Set DB_CONNECTION=mysql, DB_HOST=localhost, and your database name, user and password.
  5. Run the migrations.
    Jailed SSH is available on every shared hosting plan; it is off by default, so ask support to enable it, then run php artisan migrate --force. See enabling and accessing jailed SSH.

Whenever you add a package later, run composer require on your computer and upload the updated vendor/ folder with composer.json and composer.lock. Queue workers, Horizon, Redis and websockets don't run on shared hosting; if the blog grows into those, move it to a VPS. Our Laravel on cPanel and DirectAdmin guide covers the scheduler, mail and what works where.

cPanel Starter
₹125/mo + GST
  • 25 GB NVMe SSD Storage
  • 50 GB Monthly Bandwidth
  • 1 Website
  • 10 Email Accounts
See plan details

The plan card shows the Domain India list price, excluding 18% GST.

Frequently asked questions

Can I run composer require on Domain India shared hosting?

Yes, over jailed SSH (on request): download composer.phar and run php composer.phar require; if it stops with a proc_open message, run it again with --no-scripts, then php artisan package:discover. Or run composer require and composer install on your computer or in CI, then upload the project with its vendor folder.

Do I need laravel/ui for login and registration?

No. laravel/ui is a legacy package. Current Laravel starter kits, chosen when you run laravel new, include registration, login, password reset and profile pages.

How do I run php artisan migrate on shared hosting?

Ask support to enable jailed SSH for your account, which is available on every shared hosting plan, then run php artisan migrate --force in your project folder. Without SSH, you can import your tables with phpMyAdmin instead.

Which database settings does Laravel need on Domain India hosting?

Create a MySQL database and user in your control panel, then set DB_CONNECTION=mysql, DB_HOST=localhost and the database name, username and password in your .env file.

Can the blog send notification emails?

Laravel's SMTP mail transport usually fails on our shared servers because the socket functions it needs are disabled. Our Laravel on cPanel and DirectAdmin guide explains what works for mail on shared hosting.

When should I move the blog to a VPS?

When you need queue workers, Redis, websockets or other long-running processes, which shared hosting stops. A standard Laravel blog with Blade and MySQL runs fine on shared hosting.

Ready to publish your blog? Follow how to install Laravel on shared hosting, read Laravel on cPanel and DirectAdmin, or open a support ticket to have jailed SSH enabled.

Host your Laravel blog

cPanel shared hosting with MySQL databases, free SSL and jailed SSH on request.

See cPanel hosting

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app