Miguel Piedrafita

Miguel Piedrafita

Building a Laravel Blog App with Fly

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

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

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:

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]

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:

$table->string('domain')->nullable();

Finally, we can run our migration:

php artisan migrate

There's a bug in Laravel 5 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:

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

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

Post Model and Migration

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

php artisan make:model Post -m

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

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

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:

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

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

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:

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

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

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

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:

Route::get('/', 'PostsController@index')->name('index');

Auth::routes();

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

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 = '/';

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

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

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);
}

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>
		<div class="container">
			<div class="row">
				<div class="col-md-8">
					<h1 class="my-4">Posts</h1>

					@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>

				<div class="col-md-4">
					@auth
						<div class="card my-4">
							<h5 class="card-header">New Post</h5>
							<div class="card-body">
								<form method="POST" action="{{ route('create') }}">
									@csrf
									<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>
		</div>
		<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>
		</footer>
		<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>

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>
    	<div class="container">
			<div class="row">
				<div class="col-lg-12">
					<h1 class="mt-4">{{ $post->title }}</h1>
					<p class="lead">
						by {{ $post->user->name }}
					</p>
					<hr>
					<p>Posted {{ $post->created_at->diffForHumans() }}</p>
					<hr>
					<p>{{ $post->body }}</p>
				</div>
			</div>
			<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>
			</footer>
		</div>
    	<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>

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-up then 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

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

'fly' => [
	'token'     => env('FLY_TOKEN'),
	'site'	  => env('FLY_SITE')
],

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

FLY_TOKEN=[YOUR_FLY_TOKEN]
FLY_SITE=[YOUR_FLY_SITE_SLUG]

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:

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

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

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

// the rest of the routes

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:

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

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
								<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

DomainController

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

php artisan make:controller DomainController

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

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.");
}

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!

And checkout the PHP Fly API module on GitHub!