DEV Community

Cover image for Building a Laravel Blog App with Fly
Miguel Piedrafita
Miguel Piedrafita

Posted on • Originally published at miguelpiedrafita.com on

Building a Laravel Blog App with Fly

Originally published on my blog

Laravel makes it easy to build modern applications with custom domains by providing a powerful routing system which allows developers to build logic around multiple domains in your application.

Fly, on the other hand, is an easy and reliable application delivery network, that provides useful services like routing different sources to one domain, securing your application or providing the logic for accepting custom domains in a secure, easy and fast way.

In this post, I will be showing you how to build a laravel blog application with Fly. I will be using a custom client to interact with the Fly API, but you can also use Guzzle, cURL or any other HTTP client of your preference.

The code of the completed demo is available on GitHub and you can explore the live demo here. If you'd like to play with the PHP Fly API client, you can find it on GitHub.

Part I: Blogging app

Setting Up Laravel

We’ll start by creating a new Laravel project. While there are different ways of creating a new Laravel project, I prefer using the Laravel installer. Open your terminal and run the code below:

laravel new laravel-blog
Enter fullscreen mode Exit fullscreen mode

This will create a laravel-blog project within the directory where you ran the command above.

Authenticating Users

Our app will require users to be logged in before they can make a post or add a domain. So, we need an authentication system, which with Laravel is as simple as running an artisan command in the terminal:

php artisan make:auth
Enter fullscreen mode Exit fullscreen mode

This will create the necessary routes, views and controllers needed for an authentication system.

Before we go on to create users, we need to run the users migration that comes with a fresh installation of Laravel. But to do this, we first need to setup our database. Open the .env file and enter your database details:

// .env

DB_CONNECTION=mysql
DB_HOST=[YOUR_DATABASE_HOST]
DB_PORT=3306
DB_DATABASE=[YOUR_DATABASE_NAME]
DB_USERNAME=[YOUR_DATABASE_USER]
DB_PASSWORD=[YOUR_USER_PASSWORD]
Enter fullscreen mode Exit fullscreen mode

The last thing to do before we run our migration is to make a change to allow an user to have custom domains. To do so, open the users migration in the database/migrations directory and add the following code before the timestamps:

// database/migrations/2014_10_12_000000_create_users_table.php

$table->string('domain')->nullable();
Enter fullscreen mode Exit fullscreen mode

Finally, we can run our migration:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

There’s a bug in Laravel 5 that if you’re running a version of MySQL older than 5.7.7 or MariaDB older than 10.2.2. More info here. This can be fixed by replacing the boot()method of the AppServiceProvider with:

// app/Providers/AppServiceProvider.php

// add this under the namespace line
Illuminate\Support\Facades\Schema;

/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
  Schema::defaultStringLength(191);
}
Enter fullscreen mode Exit fullscreen mode

Post Model and Migration

Create a Post model along with the migration file by running the command:

php artisan make:model Post -m
Enter fullscreen mode Exit fullscreen mode

Open the Post model and add the code below to it:

// app/Post.php

/**
 * Fields that are mass assignable
 *
 * @var array
 */
protected $guarded = [];
Enter fullscreen mode Exit fullscreen mode

Within the databases/migrations directory, open the posts table migration that was created when we ran the command above and update the up() method with:

// database/migrations/xxxx_xx_xx_xxxxxx_create_posts_table.php

Schema::create('posts', function (Blueprint $table) {
  $table->increments('id');
  $table->integer('user_id')->unsigned();
  $table->string('title')->default('Untitled');
  $table->text('body');
  $table->timestamps();
});
Enter fullscreen mode Exit fullscreen mode

The post will have six columns: an auto-incrementing id, user_id, title, body, created_at and updated_at.

The user_id column will hold the ID of the user that sent a message, the titlecolumn will hold the title of the post and the body column will hold the content of the post.

Run the migration:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

User To Post Relationship

We need to setup the relationship between a user and a post. A user can have many posts while a particular posts was created by a user. So, the relationship between the user and message is a one to many relationship.

To define this relationship, add the code below to User model:

// app/User.php

/**
 * A user can have many posts
 *
 * @return \Illuminate\Database\Eloquent\Relations\HasMany
 */
public function posts()
{
  return $this->hasMany(Post::class);
}
Enter fullscreen mode Exit fullscreen mode

Next, we need to define the inverse relationship by adding the code below to Post model:

// app/Post.php

/**
 * A post belongs to a user
 *
 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 */
public function user()
{
  return $this->belongsTo(User::class);
}
Enter fullscreen mode Exit fullscreen mode

Defining App Routes

Let’s create the routes our app will need. Open routes/web.php and replace the routes with the code below to define three simple routes:

// routes/web.php
Route::get('/', 'PostsController@index')->name('index');

Auth::routes();

Route::get('posts/{post}', 'PostsController@show')->name('post');
Route::post('posts', 'PostsController@create')->name('create');
Enter fullscreen mode Exit fullscreen mode

The homepage will display the user's posts and an input field to add a new post. A GET post route will show a specific post and a POST posts route will be used for creating new posts.

NOTE : Since we have removed the /home route, you might want to update the redirectTo property of both app/Http/Controllers/Auth/LoginController.php and app/Http/Controllers/Auth/RegisterController.php to:

protected $redirectTo = '/';
Enter fullscreen mode Exit fullscreen mode

PostsController

Now let’s create the controller which will handle the logic of our chat app. Create a PostsController with the command below:

php artisan make:controller PostsController
Enter fullscreen mode Exit fullscreen mode

Open the controller we've just created and add the following code to it:

// app/Http/Controllers/PostsController.php

use App\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

public function __construct()
{
  $this->middleware('auth')->only('create');
}

/**
 * Show posts.
 *
 * @return \Illuminate\Http\Response
 */
public function index()
{
  return view('posts', ['posts' => Post::all()]);
}

/**
 * Show a specific post
 *
 * @return \Illuminate\Http\Response
 */
public function show(Post $post)
{
  return view('post')->withPost($post);
}

/**
 * Persist post to database
 *
 * @param Request $request
 * @return \Illuminate\Http\Response
 */
public function create(Request $request)
{

  $post = Auth::user()->posts()->create($request->validate([
    'title' => 'required|string',
    'body' => 'required|string',
  ]));

  return redirect()->route('post', $post);
}
Enter fullscreen mode Exit fullscreen mode

Using the auth middleware in ChatsController's __construct() indicates that all the methods with the controller will only be accessible to authorized users.

The index() method will simply return a view file which we will create shortly.

The show() method returns a view file with a post attached to it.

Lastly, the create() method will persist the post to the database and return a redirect to the post page.

Creating the Views

To keep everything simple, we'll be using a modified version of the StartBootstrap blog templates.

Create a new resources/views/posts.blade.php file and paste into it:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <title>Posts {{ config('app.name') }}</title>

    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
  </head>

  <body>
    <!-- Page Content -->
    <div class="container">
      <div class="row">
        <!-- Blog Entries Column -->
        <div class="col-md-8">

          <h1 class="my-4">Posts</h1>

          <!-- Blog Post -->
          @foreach($posts as $post)
          <div class="card mb-4">
            <div class="card-body">
              <h2 class="card-title">{{ $post->title }}</h2>
              <p class="card-text">{{ str_limit($post->body, 200) }}</p>
              <a href="{{ route('post', $post) }}" class="btn btn-primary">Read More &rarr;</a>
            </div>
            <div class="card-footer text-muted">
              Posted {{ $post->created_at->diffForHumans() }}
            </div>
          </div>
          @endforeach
        </div>

        <!-- Sidebar Widgets Column -->
        <div class="col-md-4">
          @auth
          <!-- New Post Widget -->
          <div class="card my-4">
            <h5 class="card-header">New Post</h5>
            <div class="card-body">
              <form method="POST" action="{{ route('create') }}">
              {{ csrf_field() }}
                <div class="form-group">
                  <label for="title">Title:</label>
                  <input type="text" class="form-control" id="title" name="title">
                </div>
                <div class="form-group">
                  <label for="body">Content:</label>
                    <textarea class="form-control" rows="5" id="body" name="body"></textarea>
                </div>
                <button class="btn btn-primary" type="submit">Post</button>
              </form>
            </div>
          </div>
          @else
          <div class="card my-4">
            <p class="text-center"><a href="{{ route('login') }}">Login</a> to make a post</p>
          </div>
          @endauth
        </div>

      </div>
      <!-- /.row -->

    </div>
    <!-- /.container -->

    <!-- Footer -->
    <footer class="py-5 bg-dark">
      <div class="container">
        <p class="m-0 text-center text-white">Copyright &copy; {{ config('app.name') }} {{ date('Y') }}</p>
      </div>
      <!-- /.container -->
    </footer>

    <!-- Bootstrap core JavaScript -->
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

  </body>

</html>
Enter fullscreen mode Exit fullscreen mode

We also need a view for displaying a single post, so let's create a new resources/views/post.blade.php file and paste the following into it:

<!doctype html>
<html lang="en">

  <head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="author" content="{{ $post->user->name }}">

    <title>{{ $post->title }} - {{ config('app.name') }}</title>

    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
  </head>

  <body>

    <!-- Page Content -->
    <div class="container">

      <div class="row">

        <!-- Post Content Column -->
        <div class="col-lg-12">

          <!-- Title -->
          <h1 class="mt-4">{{ $post->title }}</h1>

          <!-- Author -->
          <p class="lead">
            by {{ $post->user->name }}
          </p>

          <hr>

          <!-- Date/Time -->
          <p>Posted {{ $post->created_at->diffForHumans() }}</p>

          <hr>

          <!-- Post Content -->
          <p>{{ $post->body }}</p>
      </div>
      <!-- /.row -->

    </div>
    <!-- /.container -->

    <!-- Footer -->
    <footer class="py-5 bg-dark">
      <div class="container">
        <p class="m-0 text-center text-white">Copyright &copy; {{ config('app.name') }} {{ date('Y') }}</p>
      </div>
      <!-- /.container -->
    </footer>

    <!-- Bootstrap core JavaScript -->
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

  </body>

</html>
Enter fullscreen mode Exit fullscreen mode

Now, we have a simple blogging platform. Let's add custom domains.

Part II: Custom Domains

Setting Up Fly.io

If you don’t have one already, create a free Fly account at https://fly.io/app/sign-upthen login to your dashboard and create a site.

First, let’s install a package that will help us interact with the Fly API. To do so, simply open your terminal and run the following code:

composer require m1guelpf/fly-api
Enter fullscreen mode Exit fullscreen mode

Now, let’s fill in our Fly app credentials. Open the config/services.php file and add the following before the closing square bracket:

// config/services.php

'fly' => [
  'token' => env('FLY_TOKEN'),
  'site' => env('FLY_SITE')
],
Enter fullscreen mode Exit fullscreen mode

You probably noticed that we’re pulling data from the .env file, so let’s update the .env file to contain it:

// .env

FLY_TOKEN=[YOUR_FLY_TOKEN]
FLY_SITE=[YOUR_FLY_SITE_SLUG]
Enter fullscreen mode Exit fullscreen mode

If you don’t know where to get your Token, go to your Fly dashboard, click the account button on the top navigation bar, open the settings menu, click the personal access tokens item on the navigation bar and create a new one.

Setting up routing

We need to setup two new routes, one for the page where users can add custom domains and the other for the page where users will see when accessing a custom domain. To do so, we'll first add our settings route like so:

// routes/web.php

// the routes we defined before
Route::view('domain', 'domain-setup')->middleware('auth')->name('domain-setup');
Route::post('domain', 'DomainController@create')->middleware('auth');
Enter fullscreen mode Exit fullscreen mode

Finally, we'll add a route group at the very begining of the file :

// routes/web.php

Route::group(['domain' => '{domain}'], function() {
  Route::get('/', 'DomainController@index');
});

// the rest of the routes
Enter fullscreen mode Exit fullscreen mode

Also, to make the index page load when we're not using a custom domain, we'll need to move the index route to a route group before the one we've just defined :

// routes/web.php

Route::group(['domain' => '[YOUR_APP_DOMAIN_HERE]'], function() {
  Route::get('/', 'PostsController@index')->name('index');
});

// the route group we defined before

// the rest of the routes, minus the index one
Enter fullscreen mode Exit fullscreen mode

Creating the views

We'll need a page where users can add a custom domain, so we are going to create a standard Bootstrap page with a form. Create a resources/views/domain-setup.blade.php file and paste the following into it:

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Custom Domain</div>

                <div class="panel-body">
                    @if (session('status'))
                        <div class="alert alert-success">
                            {!! session('status') !!}
                        </div>
                    @endif
                    @if (count($errors->all()) > 0)
                          <div class="alert alert-danger">
                              {{ $errors->first() }}
                          </div>
                    @endif

                    @if(is_null(Auth::user()->domain))
                        <form class="text-center" method="POST">
                          {{ csrf_field() }}
                          <input class="form-control" name="domain" type="text" placeholder="yourdomain.com" value="{{ old('domain') }}" required>
                          <br>
                          <button type="submit" class="btn btn-primary">Setup Custom Domain</button>
                        </form>
                    @else
                  <p>You have setup <b>{{ Auth::user()->domain }}</b> as your custom domain.</p>
                    @endif
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
Enter fullscreen mode Exit fullscreen mode

DomainController

Now let’s create the controller which will handle custom domains. Create a DomainController with the command below:

php artisan make:controller DomainController
Enter fullscreen mode Exit fullscreen mode

Open the controller we've just created and add the following code to it:

// app/Http/Controllers/PostsController.php

use App\User;
use Facades\M1guelpf\FlyAPI\Fly;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

/**
 * Render the index page for custom domains.
 *
 * @return \Illuminate\Http\Response
 */
public function index($domain)
{
  $user = User::where('domain', $domain)->findOrFail();

  return view('posts', ['posts' => $user->posts]);
}

/**
 * Persist a custom domain to database
 *
 * @param Request $request
 * @return \Illuminate\Http\Response
 */
public function create(Request $request)
{
  Auth::user()->update($request->validate([
    'domain' => 'required|string|unique:users',
  ]));

  $domain = Fly::connect(config('services.fly.token'))->createHostname(config('services.fly.site'), $request->input('domain'));

  return redirect()->back()->withStatus("Success! To finish the setup, you need to point your domain to <b>{$domain['data']['attributes']['preview_hostname']}</b>. After that, everything's good to go.");
}
Enter fullscreen mode Exit fullscreen mode

Part III: How to improve it?

In this post, we've created a blog application that lets users connect custom domains. Well, I have created the app, you're just reading about it! So, to fix this, here is a list of things that you can improve:

  • Letting users remove custom domains
  • Supporting more than one custom domain
  • Improving the interface
  • Allowing private posts
  • Supporting Markdown for posts
  • That thing I missed but you realized and want to implement

Keep playing with Fly, use it in some side projects and maybe in your next awesome project!

Liked this article? Consider supporting me on Patreon. Learn more.

Top comments (0)