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 ofcreateDefaultTableFields()
method tofalse
.
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:
disable permalink, create, delete, publish and editor features (https://twillcms.com/docs/modules/controllers.html#content-disable-defaults)
configure the form:
a block editor for all the content
meta title and description that can be translated in the sidebar
/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