Adding a blog to a Laravel Application

JhumanJ • February 7, 2021

laravel blog moovino

About two months ago I started to work on Moovino, a platform to help Londoners find their dream home with ease. Moovino is a simple alert system, allowing you to be notified only for relevant listings and almost in real time. If you're interested about the project, you can find more information here.

In this article I'll explain how I created this blog for Moovino.

Laravel Wink

To keep things simple, I wanted Moovino's blog to be part of the main Laravel application. By doing so, it's easy for us to keep everything under the same domain, to generate a single sitemap etc. I knew about Laravel Wink beacause I already used it in past projects. It's an open-source tiny blog CMS by @themsaid, one of the core team members of Laravel.

Wink does not provide a front-end for your blog, so you get to build your own custom experience. However, all the models, the Admin UI, and all the administration logic is there out of the box. And it's all very polished. To avoid being an intrusive package, it even uses another database, so that the blog remains isolated from the main application.

Installing Wink is very easy. Here are the outline steps: - composer require themsaid/wink - Create a new database, and create a wink config entry for it in your config/database.php. If you need some inspiration, here is mine:

// config/database.php
// ...
'wink' => [
          'driver' => 'pgsql',
          'url' => env('WINK_DATABASE_URL'),
          'host' => env('WINK_DB_HOST', '127.0.0.1'),
          'port' => env('WINK_DB_PORT', '5432'),
          'database' => env('WINK_DB_DATABASE', 'moovinoblog'),
          'username' => env('WINK_DB_USERNAME', env('DB_USERNAME', 'forge')),
          'password' => env('WINK_DB_PASSWORD', env('DB_PASSWORD', '')),
          'charset' => 'utf8',
          'prefix' => '',
          'prefix_indexes' => true,
          'schema' => 'public',
          'sslmode' => 'prefer',
      ],

Building the blog front-end

As mentioned before, wink does not come with a front-end, but it provides you with few models that can be used very easily to build the face of your blog. Let's start by creating a new controller:

php artisan make:controller BlogController

Here is what mine looks like:

<?php

namespace App\Http\Controllers;

use Wink\WinkPost;

class BlogController extends Controller
{
    const PAGINATION_COUNT = 18;

    public function index()
    {
        $posts = WinkPost::paginate(self::PAGINATION_COUNT);

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

    public function show($slug)
    {
        $post = WinkPost::whereSlug($slug)->first();
        if (!$post) {
            flash()->error('Post not found.');
            return redirect()->route('blog.index');
        }

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

Then I added the two associated routes to my web.php route file:

// Blog routes
Route::group( [ 'prefix' => 'blog', 'as' => 'blog.'], function () {
    Route::get('/', [\App\Http\Controllers\BlogController::class, 'index'])->name('index');
    Route::get('/{slug}', [\App\Http\Controllers\BlogController::class, 'show'])->name('show');
});

And finally I created the two views rendered by the index and show controller methods. To save some time, I used some tailwind blog components from the Kitwind website. Note that Wink as some built-in supports for some SEO meta tags. I used them in the templates, but I didn't need to use them all. Here are the two base views:

<!-- For the show page -->
@section('meta_title', $post->title . ' | Moovino Blog')
@if($post->featured_image)
@section('meta_image', $post->featured_image)
@endif
@section('meta_description',$post->meta['meta_description']??$post->excerpt)

<x-layout.main>

    <div class="flex-grow w-full" id="blog-show">
        <div class="border-b sticky top-0 bg-white">
            <x-navbar class="max-w-5xl mx-auto"/>
            <scroll-progress-bar bar-container-class="w-full"></scroll-progress-bar>
        </div>
        <div class="max-w-4xl mx-auto">
            <div class="w-full px-10 pt-10 lg:pt-20">
                @if($post->tags()->count())
                    <p>@foreach($post->tags as $tag)
                            <span
                                class="rounded-full p-1 bg-purple-200 text-purple-800 text-xs mr-1 font-semibold px-3">{{$tag->name}}</span>
                        @endforeach</p>
                @endif
                <h1 class="mt-5 text-gray-900 font-semibold text-4xl lg:text-5xl">{{$post->title}}</h1>
                <p class="text-gray-700 mt-5">{{$post->excerpt}}</p>
                <p class="mt-5 text-gray-700">{{$post->created_at->diffForHumans()}} • {{$timeToRead}} min read</p>
            </div>
        </div>
        <div class="max-w-5xl mx-auto pt-10 lg:pt-20">
            <img class="w-full" src="{{$post->featured_image}}"/>
        </div>
        <div class="max-w-4xl mx-auto mb-20">
            <div class="w-full px-10">
                <div class="blog-content my-10">
                    {!!  $post->body !!}
                </div>
                @if($post->tags()->count())
                    <p>@foreach($post->tags as $tag)
                            <span
                                class="rounded-full p-1 bg-purple-200 text-purple-800 text-xs mr-1 font-semibold px-3">{{$tag->name}}</span>
                        @endforeach</p>
                @endif
            </div>
        </div>
    </div>
    <x-footer/>
</x-layout.main>

And for the index page:

<x-layout.main>

    <div class="flex-grow w-full" id="blog-index">
        <div class="max-w-5xl mx-auto">
            <x-navbar/>
            <div class="w-full px-10">
                <h1 class="mt-10 text-gray-900 font-semibold text-3xl lg:text-4xl text-center">Moovino Blog</h1>
            </div>

            <div class="w-full px-0 py-0 pt-10 sm:px-10 lg:px-0 sm:py-10 md:py-20">

                <x-flash/>

                <div class="grid gap-8 lg:grid-cols-2 sm:max-w-sm sm:mx-auto lg:max-w-full auto-rows-auto">
                    @foreach($posts as $post)
                        <div
                            class="overflow-hidden transition-shadow duration-300 bg-white rounded shadow-sm flex flex-col">
                            <img src="{{$post->featured_image}}" class="object-cover w-full h-64" alt=""/>
                            <div class="p-5 border border-t-0 h-full flex-grow flex flex-col">
                                <p class="mb-3 text-xs font-semibold tracking-wide">
                                    @if($post->tags()->count())
                                        @foreach($post->tags as $tag)
                                            <span
                                                class="rounded-full p-1 bg-purple-200 text-purple-800 text-xs mr-1 font-semibold px-3">{{$tag->name}}</span>
                                        @endforeach
                                        —
                                    @endif
                                    <span class="text-gray-600 uppercase">{{$post->created_at->format('j M Y')}}</span>
                                </p>
                                <a href="{{route('blog.show',[$post->slug])}}" aria-label="Category"
                                   title="{{$post->title}}"
                                   class="inline-block mb-3 text-2xl font-bold leading-5 transition-colors duration-200 hover:text-deep-purple-accent-700">{{$post->title}}</a>
                                <p class="mb-2 text-gray-700 flex-grow">
                                    {{$post->excerpt}}
                                </p>
                                <a href="{{route('blog.show',[$post->slug])}}" aria-label=""
                                   class="inline-flex items-center font-semibold transition-colors duration-200 text-purple-800 hover:text-purple-900">Learn
                                    more</a>
                            </div>
                        </div>
                    @endforeach
                </div>

            </div>
        </div>
    </div>
    <x-footer/>
</x-layout.main>

Polishing

I wanted to go a bit further and give the blog a closer look to Medium's website.

Progress Bar

I like blogs where you can visualize your progress, so I added a progress bar. I built it with Vue.js, and you can see the result directly on this page (I couldn't resist to also add it to this blog). The code for it is very simple. We just have two divs, one container, and one progress bar that grows as user scrolls the page.

<template>
    <div :class="barContainerClass?barContainerClass:'absolute top-0 w-full inset-x-0'">
        <div
            :class="barClass ? barClass : 'bg-blue-600 transition-all'"
            :style="{
              height: '1px',
            width: `${width}%`,
          }"
        />
    </div>
</template>

<script>
import throttle from 'lodash/throttle'

export default {
    components: {},
    props: {
        barClass: {type:String},
        barContainerClass: {type:String},
    },
    mounted () {
        window.addEventListener('scroll', throttle(this.onScroll, 5))
        window.dispatchEvent(new Event('scroll'))
    },
    data () {
        return {
            width: 0
        }
    },
    destroyed () {
        window.removeEventListener('scroll', this.handleScroll)
    },
    methods: {
        onScroll () {
            const height = document.documentElement.scrollHeight - document.documentElement.clientHeight
            this.width = (window.scrollY / height) * 100
            const eventWidth = Math.ceil(this.width)
            if (eventWidth === 0) {
                this.$emit('begin')
            }
            if (eventWidth === 100) {
                this.$emit('complete')
            }
        }
    },
    computed: {}
}
</script>

Read time

To give users an estimate of the length of the article, I added a read time estimate. Again, the function is very simple:

function getMinutesToRead($content, $wpm = 200) {
    $wordCount = str_word_count(strip_tags($content));
    return max(1, (int) floor($wordCount / $wpm));
}

Share Buttons

Finally, I added the social sharing buttons that you can find at the bottom of the page. They all are simple links, you can inspect the code to see how it looks.

Conclusion

Wink is super easy to use. I was able to create a blog style itm and deploy it in around 4 hours. I've used it before, and I will be using it again for sure ! Thank you Mohamed Said (for Wink, and the rest)!


If you have an issue setting up your own blog, or if you're just interested in my work, follow me on twitter: @JhumanJ.