This article was originally published on my personal blog.
Laravel is one of the most popular web frameworks that allow you to create secure and scalable websites. With Laravel, you can create almost any website, ranging from simple portfolio websites to full-fledged e-commerce solutions.
Strapi is a headless CMS framework that has been gaining a lot of attention. With Strapi, you can easily add CMS functionalities to your website, regardless of the technology being used. Strapi also removes the hassles of creating a database and models tailored to your CMS needs as it is customizable to the point that you can create your own models and entities, also called content types.
In this tutorial, you'll learn how to create a blog in Laravel using Strapi. The blog will have posts, tags, and allow users to post comments. You can find the code for this tutorial in this GitHub repository.
Prerequisites
Before you start, you need the following tools on your machine:
- Node.js. Only versions 12 and 14 are supported by Strapi, and 14 is recommended.
- PHP >= v7.3
- Composer
Please note that this tutorial will be using Strapi v4 and Laravel v8.9
Setup Strapi
Start by setting up Strapi. In your terminal, run the following command:
npx create-strapi-app@latest strapi --quickstart
Once the command is done, the server will start at localhost:1337 and a new page will open in a browser. It will be a registration form, where you need to create an admin account on Strapi.
Once you're done, you'll be redirected to the main dashboard.
Create Content-Types
Next, you'll create the content types necessary for the blog. You'll create content types for the posts and tags that the posts will use. You'll also create a comment content type which will be filled from our blog on Laravel.
Start by clicking on Create your first Content type in the dashboard. This will open a new page to create content types. Under the Content Types Builder sidebar, click on Create new collection type under Collection type. This will open a pop-up where you can enter basic and advanced settings.
You'll first create the tags content type. Enter in the Display Name field in the popup Tag
. This will automatically generate the singular and plural forms of the content type.
Once you're done, click Continue. You can now choose the fields in this content type. tags will only have a name
field other than their id. So, click on the Text field type. Then, enter in the Name field name
.
In the Advanced Settings tab, check the Required checkbox to ensure that all tags have a name.
Since that's the only field you'll add for the Tag content type, click on Finish. Then, when the pop-up closes, click on the Save button at the top right. This will restart the server. Every time you create a content type, the server is restarted.
Next, you'll create the Post content type. Again, click on Create new collection type. In the pop up that opens, enter for Display Name Post
, then click on Continue.
For posts, there will be fields for title, content, image, date posted, and tags that the post falls under.
For the title
field, choose the Text field and make it required as we did earlier. Once done, click on Add another field.
For the content
field, choose the Rich text field, and make it required.
For the image
field, choose the Media field, and choose for Type "Single media". In the Advanced Settings tab, change the allowed file types under "Select allowed types of media" to only Images. Make the field required as well.
For the date_posted
field, choose the Date field, and choose for Type "datetime". Mark this field required as well.
Finally, for the tags
field, choose the Relation field, then for the relation type choose "Post belongs to many Tags".
Once you're done, click on Finish, then click on Save at the top right. This will save the new content type and restart the server.
Finally, you need to create the Comment content type. Just like you did with the previous content types, create a new content type with the name Comment
.
The Comment
content type will have 3 fields. The first is an Email field with the name field
. Make sure to set it as required.
The second field is a Rich text field with the name content
. This is where the user's comment will go.
The third field is a Relation field between Comment and Post. The relation should be "Post has many Comments".
Note that when you create this field a new field will be added automatically in Post called comments
.
Once you're done, click on Finish, then click on Save at the top right. This will save the new content type and restart the server.
Our content types are ready!
Add Content
The next step would be to add content. Click on Content Manager in the sidebar. Start by adding a few tags by clicking on Tag in the Content Manager sidebar, then click on Add new entry at the top right.
When you create content, make sure you click Publish after saving the content.
Next, add posts the same way. You can use Lorem Ipsum Generator if you want to create mock content.
Change Permissions
The last step left is to make posts and tags public so that you can consume them in Laravel.
First, you'll create an API token to use for your requests. In the sidebar, click Settings, then API Token. Click on Add Entry at the top right.
In this form, enter the name of the token. This is just useful to remember what your API tokens are for. You can also enter a description.
In the Token type field, choose Full Access.
Once you're done, click on Save at the top right. This will create a new API token and the API token will be shown to you only once when you create it. So, copy the API token and store it somewhere as you'll use it later.
Next, you'll modify the permissions for authenticated users to be able to query content types and add new entries.
On the sidebar, click Settings, then Roles in the Settings sidebar.
You'll see two roles: Authenticated and Public. Click on the pencil icon on the Authenticated row.
Scroll down and you'll see that for each content type you can select what this role can access. Check Select All for Post, Tag, and Comment, then click Save.
Setup Laravel
Now that Strapi is ready, you'll get started with Laravel.
Run the following command to create a new Laravel project:
composer create-project laravel/laravel blog
Once this command is done, change to the directory created:
cd blog
You can then start the server with the following command:
php artisan serve
This will start the server at localhost:8000
.
Add Environment Variables
Before you can make requests to Strapi, you need to add 2 environment variables. Add the following environment variables to .env
:
STRAPI_URL=http://localhost:1337
STRAPI_API_TOKEN=
The first is the URL to Strapi. You can change it if it's not the same local URL. The second is the API token you created earlier. Paste it here after the =
sign.
Add Home Page
On the home page, you'll query all posts in Strapi and display them.
Run the following command in your terminal to create a new controller:
php artisan make:controller BlogController
Then, open app/Http/Controllers/BlogController.php
and the following method in the class:
public function home () {
//retrieve the posts from Strapi
$response = Http::withToken(env('STRAPI_API_TOKEN'))->get(env('STRAPI_URL') . '/api/posts?populate=image,tags');
$posts = [];
if ($response->failed()) {
if (isset($data['error'])) {
Log::error('Server error: ' . $data['error']['message']);
} else {
Log::error('Request Failed');
}
} else {
//get posts from response
$posts = $response->json('data');
}
return view('home', ['posts' => $posts]);
}
First, you query Strapi using Laravel's HTTP Client. You use withToken
to pass it the API token from .env
using the env
helper function. Then, you send a get
request to the endpoint localhost:1337/api/posts?populate=image,tags
.
Notice that localhost:1337
also is retrieved from .env
. As for the endpoint path, Strapi has a conventional path for all its content types. When querying a collection, the pattern for the endpoint is /api/{collection_name}
.
When you use Strapi's API, you can pass it a lot of useful parameters that allow you to filter, sort, paginate the data, and more. Here, you use the populate
parameter which allows you to retrieve a content type with its relations. You use it to retrieve the post with its image and tags.
After sending the request, you can check if the request failed using $response->failed()
. If the request failed, you log the error. If not, you set $posts
to the data
parameter in the response body. Note that you can use the json
method to retrieve the parameters from a JSON response, optionally passing it a parameter name as the first element.
Next, you need to add the home
view. Create the file resources/views/home.blade.php
with the following content:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Blog</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>
<body class="antialiased bg-light">
<div class="container mt-4 py-3 mx-auto bg-white rounded shadow-sm">
<div class="row">
@forelse ($posts as $post)
<div class="col-2 col-md-4">
<div class="card">
<img src="{{ env('STRAPI_URL') . $post['attributes']['image']['data']['attributes']['formats']['medium']['url'] }}"
class="card-img-top" alt="{{ $post['attributes']['image']['data']['attributes']['alternativeText'] }}">
<div class="card-body">
<h5 class="card-title">{{ $post['attributes']['title'] }}</h5>
<p class="card-text">{{ substr($post['attributes']['content'], 0, 50) }}...</p>
<a href="/post/{{ $post['id'] }}" class="btn btn-primary">Read More</a>
</div>
<div class="card-footer">
@if(count($post['attributes']['tags']['data']))
@foreach ($post['attributes']['tags']['data'] as $tag)
<span class="badge bg-success">{{ $tag['attributes']['name'] }}</span>
@endforeach
@endif
</div>
</div>
</div>
@empty
<div class="col">
<div class="card">
<div class="card-body">
This is some text within a card body.
</div>
</div>
</div>
@endforelse
</div>
</div>
</body>
</html>
This just displays the posts as cards using Bootstrap. Notice that the content type entries that Strapi return has the following format:
{
"data": {
"id",
"attributes": {
"title",
...
}
}
}
So, you'll find the content type's fields inside the attributes
key of data
.
Finally, change the current route in routes/web.php
to the following:
Route::get('/', [\App\Http\Controllers\BlogController::class, 'home']);
Let's test it out. Make sure that both Laravel and Strapi's servers are running. Then, open localhost:8000
. You'll see the posts you added as cards.
Add View Post Page
Next, you'll add the page to view a post. This page receives the post ID as a parameter, then queries the post's data from Strapi.
In app/Http/Controllers/BlogController.php
add a new method:
public function viewPost ($id) {
//retrieve the post from Strapi
$response = Http::withToken(env('STRAPI_API_TOKEN'))->get(env('STRAPI_URL') . '/api/posts/' . $id . '?populate=image,tags,comments');
if ($response->failed()) {
if (isset($data['error'])) {
Log::error('Server error: ' . $data['error']['message']);
} else {
Log::error('Request Failed');
}
return response()->redirectTo('/');
}
//get post from response
$post = $response->json('data');
return view('post', ['post' => $post]);
}
In this method, you use the $id
parameter, which is the post ID, to send a request to Strapi's single entry endpoint. The endpoint's pattern is /api/{collection_name}/{id}
. Similar to the previous endpoint, you can also pass it parameters like populate
.
If the request fails, you redirect the user to the home page and log the error. If the request is done successfully, you retrieve the post from the response's body and render the view post
.
Now, create resources/views/post.blade.php
with the following content:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $post['attributes']['title'] }} - Blog</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>
<body class="antialiased bg-light">
<div class="container mt-4 py-3 px-5 mx-auto bg-white rounded shadow-sm">
<h1>{{ $post['attributes']['title'] }}</h1>
<small class="text-muted d-block">{{ $post['attributes']['date_posted'] }}</small>
<img src="{{ env('STRAPI_URL') . $post['attributes']['image']['data']['attributes']['formats']['medium']['url'] }}"
class="img-fluid mx-auto d-block my-3" alt="{{ $post['attributes']['image']['data']['attributes']['alternativeText'] }}">
@if(count($post['attributes']['tags']['data']))
<div class="mb-3">
@foreach ($post['attributes']['tags']['data'] as $tag)
<span class="badge bg-success">{{ $tag['attributes']['name'] }}</span>
@endforeach
</div>
@endif
<p class="content">
{{ $post['attributes']['content'] }}
</p>
<hr />
<form action="/post/{{ $post['id'] }}" method="POST">
@csrf
<h2>Add Your Comment</h2>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="content" class="form-label">Your Comment</label>
<textarea rows="5" class="form-control" id="content" name="content" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</body>
</html>
This page just shows the post's details. Similar to how you extracted the post's data, the post's field is nested inside the attributes
field.
This page also has a comments form at the end of it. You'll implement its functionality after this.
Finally, add the new route in routes/web.php
:
Route::get('/post/{id}', [\App\Http\Controllers\BlogController::class, 'viewPost']);
Now, open the home page again and click on Read More for one of the posts. A new page will open with the post's content.
If you scroll down, you'll see a form to add your comment.
Add Comment Functionality
The last thing you'll do in this tutorial is add the commenting functionality. The form is already added, so you just need to add the POST
route to add the comment.
Add the following method in app/Http/Controllers/BlogController.php
:
public function addComment (Request $request, $id) {
$data = [
"data" => [
'email' => $request->get('email'),
'content' => $request->get('content'),
'post' => $id
]
];
$response = Http::withToken(env('STRAPI_API_TOKEN'))->post(env('STRAPI_URL') . '/api/comments', $data);
if ($response->failed()) {
if (isset($data['error'])) {
Log::error('Server error: ' . $data['error']['message']);
} else {
Log::error('Request Failed');
}
return response()->redirectTo('/');
}
//successfully added
return response()->redirectTo('/post/' . $id);
}
You first format the data as Strapi likes it. When adding a content type entry, you should nest the data inside a data
parameter. Here, you add the email
, content
, and post
fields. Notice that we are skipping validation here for tutorial simplicity.
Then, you send a POST
request to the endpoint /api/comments
. Strapi's endpoint pattern for adding a content type entry is /api/{collection_name}
. You pass the data as a second parameter to the post
method.
If the request fails, the user is redirected to the home page. If it's successful, the user is redirected back to the post's page.
Next, add before the comment form in resources/views/post.blade.php
the following:
<hr/>
@if (count($post['attributes']['comments']['data']))
<div class="comments">
<h2>Comments</h2>
@foreach ($post['attributes']['comments']['data'] as $comment)
<div class="card mb-3">
<div class="card-body">
{{ $comment['attributes']['content'] }}
</div>
<div class="card-footer">
By {{ $comment['attributes']['email'] }}
</div>
</div>
@endforeach
</div>
@endif
This will show the comments if a post has any.
Finally, add the new route in routes/web.php
:
Route::post('/post/{id}', [\App\Http\Controllers\BlogController::class, 'addComment']);
Let's test it out. Go to a post's page, then go to the comment form. Add your comment and click Submit. You'll be redirected back to the post's page, but you can see the comment below the post.
Conclusion
In this tutorial, you learned how to build a blog with Laravel and Strapi. Strapi is completely customizable, and that simplifies the process of adding content types, adding entries, and using its endpoints to query the content types or add new entries in them.
Top comments (0)