Adding navigation

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!

Generating the module

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 ServiceProvider
10{
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

Warning

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 Sortable
11{
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 BaseModuleController
10{
11 protected $moduleName = 'menuLinks';
12 protected $showOnlyParentItemsInBrowsers = true;
13 protected $nestedItemsDepth = 1;
14 
15 protected function setUpController(): void
16 {
17 $this->disablePermalink();
18 $this->enableReorder();
19 }
20 
21 public function getForm(TwillModelContract $model): Form
22 {
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!

Adding a browser field

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.

Implement HasRelated for the model

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 Sortable
12{
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 ModuleRepository
10{
11 protected $relatedBrowsers = ['page'];
12 
13 use HandleNesting;
14 
15 public function __construct(MenuLink $model)
16 {
17 $this->model = $model;
18 }
19}

Add the form field to the controller

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 BaseModuleController
12{
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): Form
24 {
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!

Rendering the menu using a component

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.

Preparing the tree

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:

Twill nested module drag and drop

This will help us during development as we will be able to see if things actually work.

Gathering the data

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 Component
10{
11 public function render(): View
12 {
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.

Tree rendering markup

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 @endforeach
10 </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.

Adding the component to our page view

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.

We need a frontpage!