Homepage singleton module

Homepage singleton module

·

5 min read

In this article, we will see the creation and customization of a Twill singleton module to provide a Homepage for our application.

Singleton module creation

In Twill terminology, a Singleton Module is globally like a classic module except that it manages only one Model.

The main differences:

  • No need to handle a listing page

  • No need to provide create, delete, and probably publish features

  • Creation of a seeder to create the Model in the database

Generation

A Singleton Module can be initiated with the CLI generator:

# PageHome module with Blocks, Translation, Revisions
php artisan twill:make:singleton PageHome -BTR

The output we can see in our terminal:

Migration created successfully! Add some fields!
Models created successfully! Fill your fillables!
Repository created successfully! Control all the things!
Controller created successfully! Define your index/browser/form endpoints options!
Form request created successfully! Add some validation rules!

 Do you also want to generate the preview file? [no]:
  [0] no
  [1] yes
 > 

Seed created successfully!
The following snippet has been added to routes/twill.php:
-----
TwillRoutes::singleton('pageHome');
-----
To add a navigation entry add the following to your AppServiceProvider BOOT method.
-----
use A17\Twill\Facades\TwillNavigation;
use A17\Twill\View\Components\Navigation\NavigationLink;
TwillNavigation::addLink(
    NavigationLink::make()->forSingleton('pageHome')
);
-----
Migrate your database & seed your singleton module:

    php artisan migrate

    php artisan db:seed PageHomeSeeder

Enjoy.

Files generated

What next

  • Edit the Routes and create a Navigation entry

  • Customize the fields of the Model

  • Customize the admin Controller setup, form

  • Morph map

  • Seed the Model

  • Initiate the front-end: Controller, Route, Page

Twill module customization

Routes and Navigation

Routes configuration

/routes/twill.php

<?php

use A17\Twill\Facades\TwillRoutes;

TwillRoutes::singleton('pageHome');

Navigation entry

We add a NavigationLink as a sibling of our previous PageContent module.

/app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use A17\Twill\Facades\TwillNavigation;
use A17\Twill\View\Components\Navigation\NavigationLink;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        TwillNavigation::addLink(
            NavigationLink::make()
                ...
                ->setChildren([
                    NavigationLink::make()
                        ->title(Str::ucfirst(__('home')))
                        ->forSingleton('pageHome'),
                    ...
                ]),
        );
    }
}

Model

In our case, we want:

  • to remove the default published attribute (our homepage will always be published)

  • a title that can be translated

  • meta title and description that can be translated

  • a block editor for all the content (we will focus on this part in a later article)

/database/migrations/..._create_page_homes_tables.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePageHomesTables extends Migration
{
    public function up()
    {
        Schema::create('page_homes', function (Blueprint $table) {
            createDefaultTableFields($table, true, false);
        });

        Schema::create('page_home_translations', function (Blueprint $table) {
            createDefaultTranslationsTableFields($table, 'page_home');
        });

        Schema::table('page_home_translations', function (Blueprint $table) {
            $table->after('page_home_id', function ($table) {
                $table->string('title', 200)->nullable();
                $table->string('meta_title', 100)->nullable();
                $table->text('meta_description', 200)->nullable();
            });
        });

        Schema::create('page_home_revisions', function (Blueprint $table) {
            createDefaultRevisionsTableFields($table, 'page_home');
        });
    }

    public function down()
    {
        Schema::dropIfExists('page_home_revisions');
        Schema::dropIfExists('page_home_translations');
        Schema::dropIfExists('page_homes');
    }
}

To remove the published attribute, you have to set the third parameter of createDefaultTableFields() method to false.

We can now run our migration to create the tables for our Module:

php artisan migrate

/app/Models/PageHome.php

<?php

namespace App\Models;

use A17\Twill\Models\Behaviors\HasBlocks;
use A17\Twill\Models\Behaviors\HasTranslation;
use A17\Twill\Models\Behaviors\HasRevisions;
use A17\Twill\Models\Model;

class PageHome extends Model
{
    use HasBlocks, HasTranslation, HasRevisions;

    protected $fillable = [
        'title',
        'meta_title',
        'meta_description',
    ];

    public $translatedAttributes = [
        'title',
        'meta_title',
        'meta_description',
        'active',
    ];

    public array $publicAttributes = [
        'title',
        'meta_title',
        'meta_description',
    ];
}

Controller

In our case, we want to:

/app/Http/Controllers/Twill/PageHomeController.php

<?php

namespace App\Http\Controllers\Twill;

use A17\Twill\Models\Contracts\TwillModelContract;
use A17\Twill\Services\Forms\Fields\Input;
use A17\Twill\Services\Forms\Form;
use A17\Twill\Http\Controllers\Admin\SingletonModuleController as BaseModuleController;
use A17\Twill\Services\Forms\Fields\BlockEditor;
use A17\Twill\Services\Forms\Fieldset;

class PageHomeController extends BaseModuleController
{
    protected $moduleName = 'pageHomes';

    protected function setUpController(): void
    {
        $this->disablePermalink();
        $this->disableCreate();
        $this->disableDelete();
        $this->disablePublish();
        $this->disableEditor();
    }

    protected function formData($request)
    {
        return [
            'customPermalink' => route('home'),
        ];
    }

    public function getForm(TwillModelContract $model): Form
    {
        $form = parent::getForm($model);

        $form->add(
            BlockEditor::make()
                ->withoutSeparator()
        );

        return $form;
    }

    public function getSideFieldsets(TwillModelContract $model): Form
    {
        $form = parent::getSideFieldsets($model);

        $form->addFieldset(
            Fieldset::make()
                ->title('SEO')
                ->id('seo')
                ->fields([
                    Input::make()
                        ->name('meta_title')
                        ->label('Title')
                        ->translatable()
                        ->maxLength(100),

                    Input::make()
                        ->name('meta_description')
                        ->label('Description')
                        ->translatable()
                        ->maxLength(200),
                ])
        );

        return $form;
    }
}

Morph map

/app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Database\Eloquent\Relations\Relation;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Relation::enforceMorphMap([
            ...
            'pageHome' => 'App\Models\PageHome',
        ]);
    }
}

Seeding

By default, Twill will automatically run the seeder when you visit the admin edition page (managed by the configuration value twill.auto_seed_singletons).

But you can manually run the Laravel db:seed command:

php artisan db:seed PageHomeSeeder

Front-end with Inertia

This module aims to display a homepage on our front-end. To do so, we need:

  • a Laravel Route

  • a Laravel Controller

  • an Inertia Page

Route

/routes/web.php

<?php

use App\Http\Controllers\App\PageHomeController;
use Illuminate\Support\Facades\Route;

Route::get('/', PageHomeController::class)->name('home');

Controller

/app/Http/Controllers/App/PageHomeController.php

<?php

namespace App\Http\Controllers\App;

use App\Models\PageHome;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;

class PageHomeController extends Controller
{
    public function __invoke(): InertiaResponse
    {
        $locale = app()->getLocale();

        $item = Cache::rememberForever(
            'page.home.' . $locale,
            function () {
                $item = PageHome::first();

                if ($item !== null) {
                    $item->load('translations', 'blocks');
                }

                return $item;
            }
        );

        abort_if($item === null, Response::HTTP_NOT_FOUND);

        return Inertia::render('Page/Home', [
            'item' => $item->only($item->publicAttributes),
            'locale' => $locale,
        ]);
    }
}

Page

/resources/views/Pages/Page/Home.vue

<script setup lang="ts">
import Head from '@Theme/Head.vue'

defineOptions({
  name: 'PageHome',
})

interface Props {
  item: Model.Page
}

defineProps<Props>()
</script>

<template>
  <Head :item="item"></Head>
  <h1 class="text-center text-4xl font-semibold text-gray-900">
    {{ item.title }}
  </h1>
</template>

Final result

You can now click on the permalink to open the front-end page in a new tab:

and you should see:


We now have a singleton module to handle the homepage, we will see in later articles how to improve logic, performance and handle more complex content structure with blocks


We'll do our best to provide source code of the serie on GitHub