For the last three years, my blog was built using a javascript rich text editor that generated all the HTML code for my posts. I've never been 100% happy with it as the editor generated a lot of unnecessary HTML and the styling of code blocks was not pretty good. In addition, I share my posts here in Dev.to and even though sometimes I can copy/paste my HTML articles, some parts were not styled properly.
I've always enjoyed taking notes in Markdown so I decided to give it a try for my blog. I created a quick demo with a Markdown editor and then parsed the articles to HTML using GitHub's public API. I gotta say the writing experience is way better than what I had before. Cross posting to Dev.to is a lot easier now and I can use GitHub's code highlighting and styling. See below how I did it.
Bootstraping a Laravel blog (model, controller and routes)
First thing we need is a bootstrapped Laravel app. You can check this previous article in which I create a Laravel + Tailwind CSS project.
Once we have our bootstrapped project, lets focus in the blog. First we'd need to create a migration to create the articles table . You can generate one with the running php artisan make:migration create_articles_table
.
This will create a new file in the database/migrations folder. Edit it to include all the fields we're going to need:
// database/migrations/2020_xx_xx_xxxxxx_create_articles_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateArticlesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->index(); //to generate seo friendly urls
$table->text('body_md'); // the article body in mardkdown
$table->text('summary_md'); // the summary in markdown
$table->text('body_html')->nullable(); //the article body in html
$table->text('summary_html')->nullable(); //the summary in html
$table->char('online', 1); // we'll use this to have articles published or in draft mode
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('articles');
}
}
Make sure your database connection details are correctly configured in your .env file and then run php artisan:migrate
to create the table in the database.
Next step is to create the Model and Controller with php artisan make:model Article --resource
. This will generate a model in /app/Article.php folder and the controller in /app/Http/Controllers/ArticleController.php .
Now we need to create the routes that we'll need to:
- Return the page the contains the form to create a new article
- Endpoint to store the article
- Return the page to display the list of articles
- Return the page to display a single article
Add them in the /routes/web.php file as follows:
<?php
use Illuminate\Support\Facades\Route;
Route::get('/articles/create', 'ArticleController@create')->name('articles.create');
Route::post('/articles', 'ArticleController@store')->name('articles.store');
Route::get('/articles', 'ArticleController@index')->name('articles.index');
Route::get('/articles/{param}', 'ArticleController@show')->name('articles.show');
The blog index page
As we've detailed in the routes file (/routes/web.php), to retrieve the articles index page we'll call the index method in the ArticleController. The controller would have to query the database and retrieve all the published articles (articles with the online field to true) and pass them to the view:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Article;
public function index()
{
//query the database
$articles = Article::orderByDesc('updated_at')->where('online', true)->get();
//return the view passing the list of articles
return view ('articles.index', ['articles' => $articles]);
}
In the blog index page, we'll display the title, summary_html and the date of all the articles. Let's create the view file in /resources/views/articles/index.blade.php with the following content:
// resources/views/articles/index.blade.php
@extends('layouts.app')
@section('content')
<main class="text-gray-800 px-8 md:w-2/3 lg:w-3/5 xl:w-1/2 sm:w-full mx-auto">
<div class="text-center">
<h1 class="text-center text-3xl my-8">Articles</h1>
<a class="my-3 hover:underline" href="{{route('articles.create')}}">New article</a>
</div>
@foreach ($articles as $article)
<section class="border-b">
<h2 class="text-3xl font-bold text-center mt-4 hover:underline"><a href="{{Route('articles.show', $article->slug)}}">{{$article->title}}</a></h2>
<p class="text-sm text-center leading-5 text-gray-700 mt-3">Posted {{\Carbon\Carbon::parse($article->updated_at)->format('d/m/Y')}}</p>
<article class="markdown-body">
{!! $article->summary_html!!}
<div class="text-right mt-8">
<a href="{{Route('articles.show', $article->slug)}}" role="button" class="px-4 py-2 border border-gray-300 rounded bg-white text-sm font-medium text-gray-700 hover:border-gray-500 focus:z-10 focus:outline-none focus:border-gray-300 focus:shadow-outline-gray active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
Read more
</a>
</div>
</article>
</section>
@endforeach
@endsection
Note: I'm using a lot of css classes from TailwindCSS. You can ignore them if you're using another CSS library (like Bootstrap or Bulma).
The important part in this view is the foreach loop that will will print the section inside it for every article we pass to the view.
The blog article page
The blog article page will be very similar to the index one, although in this case controller would have to filter for a specific article using the parameter we'll pass in the URL (note the route is /articles/{param}). This parameter could be the article id or the slug so we can access the articles with /articles/1 or /articles/this-is-the-title 😉
public function show($param)
{
//seach by id or the slug
$articleFound = Article::where('id', $param)
->orWhere('slug', $param)
->firstOrFail();
if ($articleFound->online){
//if article is published, go to article page
return view('articles.show', ['article' => $articleFound]);
}
//if article not published, redirect to articles.index page
return redirect()->route('articles.index');
}
In the view (show.blade.php file), we'll display the article's title, the date (using the updated_at) and the body_html fields:
// resources/views/articles/show.blade.php
@extends('layouts.app')
@section('content')
<main class="text-gray-800 md:w-2/3 lg:w-3/5 xl:w-1/2 sm:w-full mx-auto">
<div class="text-left pb-4 mt-4">
<a class="text-sm leading-5 text-gray-700 hover:underline" href="{{ url()->previous() }}">Back to blog</a>
</div>
<h1 class="text-5xl text-center ">{{$article->title}}</h1>
<p class="text-sm text-center leading-5 text-gray-700 mt-3">Posted {{\Carbon\Carbon::parse($article->updated_at)->format('d/m/Y')}}</p>
<article class="markdown-body">
{!! $article->body_html !!}
</article>
</main>
@endsection
With this, we have 2 out of 4 routes done, the next one will be the one that returns the page that contains the form to create a new article
The new article page with markdown editor
At the controller level, this is pretty straight forward as we don't need to query the database. We just have to return a page that will contain the form to create the new article:
public function create()
{
return view('articles.create');
}
The view we'll include a form which action would be the articles.store route. The form will have inputs for all the fields we want: title, summary, body and the online indicator.
For the summary and body fields we'll use textarea inputs and we'll include a mardown editor in them. I've chosen SimpleMDE as (as the name indicates) it's super simple to integrate, has a toolbar with shortcuts to most of the things I'd need and it can be customized to your needs (see documentation in GitHub).
@extends('layouts.app')
@section('content')
<section class="w-full">
<h3 class="text-center text-3xl font-semibold">New Article</h3>
<form class="w-full px-6" action="{{route('articles.store')}}" method="post" enctype="multipart/form-data">
@csrf
<div class="flex flex-wrap -mx-3 mb-6">
<div class="w-full md:w-4/5 px-3 mb-6 md:mb-0">
<label class="label" for="title">
Title
</label>
<input class="input" id="title" name="title" type="text" >
<p class="text-red-500 text-xs italic">Please fill out this field.</p>
</div>
<div class="w-full md:w-1/5 px-3 mb-6 md:mb-0">
<label class="label" for="online">
Online?
</label>
<div class="relative">
<select class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-3 px-4 pr-8 rounded leading-tight " id="online" name="online">
<option value="0">No</option>
<option value="1">Yes</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/></svg>
</div>
</div>
</div>
</div>
<div class="flex flex-wrap -mx-3 mb-6">
<div class="w-full px-3">
<label class="label" for="grid-password">
Article body
</label>
<p class="text-gray-600 text-xs italic mb-2">This is actual article body</p>
<textarea class="input " name="body" id="body">{{ old('body') }}</textarea>
</div>
</div>
<div class="flex flex-wrap -mx-3 mb-6">
<div class="w-full px-3">
<label class="label" for="grid-password">
Summary
</label>
<p class="text-gray-600 text-xs italic mb-2">This is the text that will appear in the blog index page</p>
<textarea class="input " name="summary" id="summary">{{ old('summary') }}</textarea>
</div>
</div>
<div class="w-full text-right pa-3 mb-6">
<input class="btn btn-green my-4" type="submit" value="Save article">
</div>
</form>
</section>
{{-- Import CSS and JS for SimpleMDE editor --}}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
<script>
// Initialise editors
var bodyEditor = new SimpleMDE({ element: document.getElementById("body") });
var summaryEditor = new SimpleMDE({ element: document.getElementById("summary") });
</script>
@endsection
Note: At the end of the file you can see I import the CSS and JS files required for the editor and create new instances of them targeting the textarea elements using their id's
Now, if you open this page (localhost:8000/articles/create) you should see the form with the mardown editors in the body and summary fields.
Storing articles and parsing Markdown to HTML
In the editors of the form we just created we'll write Mardown but, in the index and show views we're going to display the html fids (remember we're displaying the sumary_html and body_html fields from the database). So how can we parse the markdown to HTML 🤔? and when do we parse it? For both questions I found different options.
How to parse it?
When to parse it?
- Parse the markdown to HTML every time we display the index or show pages
- Parse the markdown to HTML before storing it in the database
At the end I opted to use GitHub's API and parse it before storing the articles in the database. There is package called GitDown that will simplify it a lot, so we'd have to install it via compose with composer require calebporzio/gitdown
. Once installed, we can go ahead and create the store method of the ArticleController, the action of the form we just created:
// app/Http/Controllers/ArticleController.php
// import GitDown dependency
use GitDown;
use App\Article;
public function store(Request $request)
{
//I'm just validating the presence of the title, but you can validate more fields
$request->validate([
'title'=> 'required',
]);
//create new Article instance and assign values
$article = new Article;
$article->title = $request->title;
$article->body_md = $request->body;
$article->summary_md = $request->summary;
$article->online = $request->online;
//generate article slug for nice URLs
$article->slug = str_slug($article->title, '-') . '-' . $article->id;
//parse body and summary to HTML via GitHub API
$article->body_html = GitDown::parseAndCache($request->body) ;
$article->summary_html = GitDown::parseAndCache($request->summary);
//save article
$article->save();
return redirect()->route('articles.index');
}
Note: we're using the parseAndCache method exposed by the GitDown class and pass it the body and summary fields of the request we received.
In order to use the str_slug()
function, we'd need to install the laravel/herlpers dependency via composer: composer require laravel/helpers
.
What about code highlighting? Luckily for us, GitDown ships with the helper @gitdown that we can include in the header of our views to add all the CSS classes we need (see docs).
Have you noticed that the divs that contain the body and summary or the articles have a class markdown-body? This is the class targeted by the GitDown CSS.
Of couse, you can use a different class and style everything as you want, or override just some of the classes as you wish, that's up to you 😉.
Conclusion
There are a few things I havent covered in this article, like authentication so only logged users can create new articles or edit/delete articles, but there are plenty of articles out there that explain those concepts. I wanted to focus in the Markdown/HTML part. You can find the whole code of this article in this repo in GitHub. Feel free to clone/download it and use it a base for your projects. Hope you found this useful.
Happy coding!
This article was originally posted in my website. For dev tips and interesting articles follow me on Twitter 😎.
Top comments (0)