Now that we have all the pieces in our cms and front-end, we are missing one crucial thing, and that is our menu!
Once again, there are many ways to build a menu, but for this case, we will add a simple, self-nested module to do the job.
We will go a bit faster now, as we already touched on many aspects, so let's dive right in!
We can generate a self-nested module using:
php artisan twill:make:module MenuLinks --hasNesting --hasPosition --hasTranslation
To all other questions we will answer no.
Perfect.
Once again, we add the NavigationLink as provided in our app/Providers/AppServiceProvider.php
, but this time, we
change the title to make a bit more sense.
1<?php 2 3namespace App\Providers; 4 5use Illuminate\Support\ServiceProvider;
6use A17\Twill\Facades\TwillNavigation; 7use A17\Twill\View\Components\Navigation\NavigationLink; 8 9class AppServiceProvider extends ServiceProvider10{11 public function boot()12 {13 TwillNavigation::addLink(14 NavigationLink::make()->forModule('pages')15 );16 TwillNavigation::addLink(17 NavigationLink::make()->forModule('menuLinks')->title('Menu')18 );19 }20}
We do not need a description in this module, so we can open up the migration and remove that:
1<?php 2 3use Illuminate\Database\Migrations\Migration; 4use Illuminate\Database\Schema\Blueprint; 5use Illuminate\Support\Facades\Schema; 6 7return new class extends Migration 8{ 9 public function up()10 {11 Schema::create('menu_links', function (Blueprint $table) {12 createDefaultTableFields($table);13 14 $table->string('title', 200)->nullable();15 16 $table->text('description')->nullable(); 17 18 $table->integer('position')->unsigned()->nullable();19 20 $table->nestedSet();21 });22 }23 24 public function down()25 {26 Schema::dropIfExists('menu_links');27 }28};
Now you can run the migration: php artisan migrate
There are also references to description
in:
app/Models/MenuLink.php
app/Http/Controllers/Twill/MenuLinkController.php
These will need to be removed as well.
Your files should look like this:
1<?php 2 3namespace App\Models; 4 5use A17\Twill\Models\Behaviors\HasPosition; 6use A17\Twill\Models\Behaviors\HasNesting; 7use A17\Twill\Models\Behaviors\Sortable; 8use A17\Twill\Models\Model; 9 10class MenuLink extends Model implements Sortable11{12 use HasPosition, HasNesting;13 14 protected $fillable = [15 'published',16 'title',17 'position',18 ];19}
1<?php 2 3namespace App\Http\Controllers\Twill; 4 5use A17\Twill\Models\Contracts\TwillModelContract; 6use A17\Twill\Services\Forms\Form; 7use A17\Twill\Http\Controllers\Admin\NestedModuleController as BaseModuleController; 8 9class MenuLinkController extends BaseModuleController10{11 protected $moduleName = 'menuLinks';12 protected $showOnlyParentItemsInBrowsers = true;13 protected $nestedItemsDepth = 1;14 15 protected function setUpController(): void16 {17 $this->disablePermalink();18 $this->enableReorder();19 }20 21 public function getForm(TwillModelContract $model): Form22 {23 $form = parent::getForm($model);24 25 return $form;26 }27}
Now with all this in place, head back over to the CMS, click Menu in the top bar and add a first link, once it is created, you will notice, there is no way for us to refer to one of our pages! Let's fix that!
We will use a simple Twill managed browser field. A browser field is an easy way to make a connection to another model.
In this case, every menu link will have a link to a page so that we know what we should link to.
In our menu link model, we need to add the HasRelated
trait:
1<?php 2 3namespace App\Models; 4 5use A17\Twill\Models\Behaviors\HasPosition; 6use A17\Twill\Models\Behaviors\HasNesting; 7use A17\Twill\Models\Behaviors\HasRelated; 8use A17\Twill\Models\Behaviors\Sortable; 9use A17\Twill\Models\Model;10 11class MenuLink extends Model implements Sortable12{13 use HasPosition;14 use HasNesting;15 use HasRelated;16 17 protected $fillable = [18 'published',19 'title',20 'position',21 ];22}
And in our module repository we tell Twill what related browsers it has to manage:
1<?php 2 3namespace App\Repositories; 4 5use A17\Twill\Repositories\Behaviors\HandleNesting; 6use A17\Twill\Repositories\ModuleRepository; 7use App\Models\MenuLink; 8 9class MenuLinkRepository extends ModuleRepository10{11 protected $relatedBrowsers = ['page'];12 13 use HandleNesting;14 15 public function __construct(MenuLink $model)16 {17 $this->model = $model;18 }19}
In our menu link module controller we now add the form field:
1<?php 2 3namespace App\Http\Controllers\Twill; 4 5use A17\Twill\Models\Contracts\TwillModelContract; 6use A17\Twill\Services\Forms\Fields\Browser; 7use A17\Twill\Services\Forms\Form; 8use A17\Twill\Http\Controllers\Admin\NestedModuleController as BaseModuleController; 9use App\Models\Page;10 11class MenuLinkController extends BaseModuleController12{13 protected $moduleName = 'menuLinks';14 protected $showOnlyParentItemsInBrowsers = true;15 protected $nestedItemsDepth = 1;16 17 protected function setUpController(): void
18 {19 $this->disablePermalink();20 $this->enableReorder();21 }22 23 public function getForm(TwillModelContract $model): Form24 {25 $form = parent::getForm($model);26 27 $form->add(Browser::make()->name('page')->modules([Page::class]));28 29 return $form;30 }31}
As we are using the basic related table, we do not need to write a migration, that's pretty convenient. But if you want or need a real relation, make sure to check the documentation as that is possible!
Perfect, we have our full setup now! If you head back into the CMS you can add a new menu item and link it to a page.
Now let's render it!
We want to be able to put the navigation into any area on our website. For this we will make a blade component that will hold the menu.
A blade component is a blade file with an optional class file. For this case, we will use the blade + class file as the class file will hold our php logic, and the blade file will do the rendering.
Again, we can use a command to do most of the work: php artisan make:component Menu
This will generate the class app/View/Components/Menu.php
and the blade file resources/views/components/menu.blade.php
.
We will first write the code for the menu. Before we do that, go back to the CMS and add some pages and links so we have a nested structure. You can use the drag handle on the left of the content table to put things into the correct position.
Like this:
This will help us during development as we will be able to see if things actually work.
Now, let's open up the component class app/View/Components/Menu.php
, we will see an empty constructor and a render
method.
While in theory we can make it so we can have this nested to infinity, for this guide we will render just the top level and their children.
In the render method we will add the code required for us to render the tree:
1<?php 2 3namespace App\View\Components; 4 5use App\Models\MenuLink; 6use Illuminate\Contracts\View\View; 7use Illuminate\View\Component; 8 9class Menu extends Component10{11 public function render(): View12 {13 /** @var MenuLink[] $links */14 $links = MenuLink::published()->get()->toTree();15 16 return view('components.menu', ['links' => $links]);17 }18}
So what we do here is request the tree of published menu links, then we send it to our components view file as "links" .
This will expose the $links
variable to the blade file that we will now write.
Now that we have the necessary data in our blade file, we can write the markup.
We will change the contents of resources/views/components/menu.blade.php
to this:
1<nav class="mb-10 border-b border-b-primary md:sticky md:z-10 md:top-0 md:py-5 md:bg-white "> 2 <ul class="px-5 md:flex md:flex-row md:flex-nowrap md:justify-center md:px-0"> 3 @foreach($links as $link) 4 <li class="py-5 border-t border-t-secondary first:border-t-0 md:py-0 md:px-5 md:border-t-0 md:border-l md:border-l-secondary md:first:border-l-0"> 5 <a href="{{route('frontend.page', [$link->getRelated('page')->first()->slug])}}"> 6 {{$link->title}} 7 </a> 8 </li> 9 @endforeach10 </ul>11</nav>
We add just a minimal amount of styling as we will not spend too much time on that during this guide. But this will build a navigation tree that is slightly indented so that you can see the proper structure.
You cannot see it in action yet, for that we have to add the component to the main template file.
The final step for the menu, adding the component to the page view.
In reality, you might want to abstract your "layout" into a separate component as well, because if the amount of content types will grow, this would require more maintenance to put the menu in every view.
But, for this guide, we will simply open resources/views/site/page.blade.php
and add the menu:
1<!doctype html> 2<html lang="en"> 3<head> 4 <title>{{ $item->title }}</title> 5 @vite('resources/css/app.css') 6</head> 7<body> 8<x-menu/> 9<div class="max-w-2xl mx-auto">10 {!! $item->renderBlocks() !!}11</div>12</body>13</html>
Wherever you will put <x-menu/>
it will render the menu. That's useful because you could use it in a footer as well.
Now that we have pages and a menu, we have one last thing we need to do.