Table of Contents
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.
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 because I already used it in past projects. It’s an open-source tiny blog CMS by [@themsaid](https://twitter.com/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',
],
php artisan wink:install
. Then you can edit theconfig/wink.php
file, specifying the database connection and the filesystem you are going to use.
php artisan storage:link
if you are using the local filesystem. Do not forget to also run this command in production.
php artisan wink:migrate
that will create tables for the database you just created. The output should also contain a login email and a password for you to connect to the admin space of wink.
- Go to
/wink
, connect and change email and password.
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 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)!