Building A Blog In Laravel Vapor: Part 3

Image of Wrong Way sign
Headshot image of author

Sep 17th, 2022 By Taylor Perkins Full Stack Developer

Hello and welcome! Once again, I have some distractions to discuss before we dive into where we left off last week. Again, from the perspective of moving fast and iterating, I went down a bit of a rabbit hole last post with not being able to render blade syntax to actually show the code to you guys on how I rendered dynamic blade components. This certainly bothered me, but I published the post and figured I would have it solved sooner or later. That night I spent working on trying to pass an HTML string as a prop to Vue with Inertia, thinking this could possibly solve my problem, not rendering blade syntax in a blade template. There’s two flaws there.

  • While I did want to implement this project in the Inertia Vue stack, I would have to implement SSR (Server Side Rendering) to be SEO friendly, and I don’t want that overhead right now.
  • At the end of the day, whichever way you look at it, you have to render a blade component. The HTML string being passed to Vue as a prop had to be rendered through Blade then through Torchlight's BladeManager. See below.
1$content = json_encode(
2 BladeManager::renderContent(Blade::render(htmlspecialchars_decode($post->content))),
3 JSON_UNESCAPED_SLASHES
4 );

The important piece there that I had trouble with was the json_encode option I had to pass to the second parameter, JSON_UNESCAPED_SLASHES. This kept from breaking some of my html by not adding an extra slash. The BladeManager you can see in is the same stuff you’ll see in torchlights Middleware provided out of the box. Some extra information here, checkout the bottom section, Un-rendered Blocks.

Now here’s the rather important part, as once I had this solution pieced together, I came to a quick and dissatisfying realization. I am still rendering a blade component, and therefore blade syntax still attempts to be rendered. It’s interesting the way the mind works, I have been aware and read over the documentation several times. It had just never clicked for me though. Particularly considering their use case was documented as a solution to rendering javascript syntax in blade. I can literally just escape my blade syntax…

1@section('post')
2 
3 @php
4 $content = Blade::render(htmlspecialchars_decode($post->content));
5 @endphp
6 {!! $content !!}
7 
8@endsection

Needless to say, I was a bit disappointed in myself. However, these are the kind of mistakes that you make, and once solved, you will never forget the solution. Trial by fire can sometimes be a great tool. But enough of my mistakes, I do enjoy sharing them, but let's get back on track to where we left off last post.

Paginating Our Post model using Livewire WithPagination

I wanted to start with wrapping up Posts, let’s start with the Posts index. There’s really nothing special about this, since my admin dashboard is built in Livewire (another huge shoutout to Filament), I went ahead and made a simple paginated Livewire component. Fortunately, Livewire provides all that you need out of the box for pagination, which is great. I could have done a simple blade template, but I figure as I iterate I will have more reason to have a more complex, and interactive Post index. It really is pretty much as simple as running the make:livewire command.

1php artisan make:livewire PostsIndex

This will create your class app/Http/Livewire/PostsIndex.php and your blade template resources/views/livewire/posts-index.blade.php. You will want to use the WithPagination trait that Livewire comes with. Here’s what my Component class looks like.

1class PostsIndex extends Component
2{
3 use WithPagination;
4 
5 public function render()
6 {
7 return view('livewire.posts-index', [
8 'posts' => Post::where('status', 'published')->paginate(10)
9 ]);
10 }
11}

Then my blade template.

1<div class="my-20">
2 <div class="grid grid-cols-1 gap-4">
3 @if($posts)
4 @foreach($posts as $post)
5 (post-card component goes here)
6 @endforeach
7 @endif
8 </div>
9 <div class="py-8">
10 @if($posts)
11 {{ $posts->links() }}
12 @endif
13 </div>
14</div>

We simply use the pagination object to show the pagination links and iterate through each post. I did create a post-card blade component, which to be honest may have been unnecessary and a little too specific, but I tend to lean towards breaking things up into little bits and pieces like that when I see an opportunity. At my day job we follow the Atomic design pattern, which if I were to have followed that, I would have created a card not specifically catered to my Post model. However, I also believe in small and quick iterations, so I just created a simple component for my post-card, and would reiterate later. You can feel free to create a card atom, no components at all, or follow my bad example :). Implementation is totally up to you, but I think you get the idea so I won’t dive into my post-card component. Let’s check out the last piece for managing the Post model with Filament.

Pro Tip: I really do like the atomic design pattern, it enables you to create reusable components. Whether it’s Vue, Livewire or blade, doesn’t matter. They all give you abilities to do this. Blade components have slots just as Vue components do.

Handling Creating And Updating Eloquent Model Events

So the final piece we will get to go over one of my favorite things in Laravel, Eloquent Model Events. I have found so many productive use cases for these, they are super handy. In my case, I wanted to have a published_at timestamp column, that once a post was created or updated, and the status was “published” would set the published_at field with the current date. This is pretty trivial given the Laravel framework. I like to use an Observer anytime I have at least more than one event I want to implement.

1php artisan make:observer PostObserver --model=Post

I prefer to pass it a model parameter as well, it will give you some doc blocks and everything that you merely modify to fit your needs. Less typing :). Since the Observer stubs only come with the past tense event verbs, and we only want to handle “creating” and “updating” for now, we’re going to delete everything but the “created” and “updated” methods. Then just change those to the present participle verbs, “creating” and “updating”. What these do is simply allow you to do things to the model BEFORE they get saved to the database. 

Pro Tip: it's important to note here, that the present participle verbs do require you to return a bool. If you don’t return true, the model won’t actually get saved to the database, AND it will do it quietly with no failures. This can lead to hours of debugging if you’re not totally aware of this consequence.

Both my creating and updating events are fairly straightforward. I don’t want to fill the published_at column, unless of course, the status is published. Hence we handle both creating and updating with similar logic to make sure we handle both cases. Here’s the creating event.

1public function creating(Post $post): bool
2{
3 if ($post->status == 'published') {
4 $post->published_at = now();
5 }
6 
7 return true;
8}

This one is a bit simpler than updating, we’re just checking if the status of the post being created is already set to published. If so, let's fill the timestamp using the Laravel Carbon now() helper and return true. Next we have the slightly more complicated updating event.

1public function updating(Post $post)
2{
3 if (!$post->hasBeenPublished() && $post->status == 'published') {
4 $post->published_at = now();
5 }
6 
7 return true;
8}

The only difference here is we want to first check if the Post has already been published. We don’t want to “re-publish” or change the published_at date if we need to go in and make a minor modification. So basically don’t update the published_at field if the Post hasBeenPublished already. Here’s hasBeenPublished on the Post model.

1public function hasBeenPublished(): bool
2{
3 return !empty($this->published_at);
4}

Last but not least, if you’re not familiar with Laravel events this is pretty important. Since we’re using an Observer we need to register the Observer in the EventServiceProvider boot method.

1public function boot()
2{
3 Post::observe(PostObserver::class);
4}

Conclusion

That’s it for the events and our Post model implementation. There will likely be more use cases as we iterate on this application more. But alas, that seems to be all the time we have this week. When we pick back up next week, we will be moving on from Posts. If I have written these blog posts well enough, and you have followed along, then you should have a fully functional blog Post managed by the Filament admin dashboard with dynamically rendered code highlighted content. 

Next week, we’ll start with taking a look at managing Users in Filament, and my favorite Roles and Permissions package, Bouncer. We’re getting pretty close to having a fully functional and SEO friendly blog application in Laravel Vapor! Until next time.

Please sign in or create an account to join the conversation.