Spectrum is now read-only. Learn more about the decision in our official announcement.


Rapidly create a custom admin console that content publishers will love. Twill is an open source CMS toolkit for Laravel, crafted by AREA 17.


Step by step II- Creating a Twill app.

April 1, 2019 at 5:29pm

Step by step II- Creating a Twill app.

April 1, 2019 at 5:29pm (Edited 3 years ago)
The first part of this turorial is available here

Step by step - Creating a Twill app.

All the code written for this tutorial is available on github

Associations and Browsers

For this practice, I'll assume that an Article can have an Invitee writer. As we don't want to type Invitee's name every time, we need to create a Lookup Table for the Invitees.
The Invitee will have a Title and a Bio which can be translates(-T), may have his own page for which we need a slug(-S), and it will have a profile picture(-M)
php artisan twill:module invitees -TSM
We adjust the migration.
class CreateInviteesTables extends Migration
public function up()
Schema::create('invitees', function (Blueprint $table) {
Schema::create('invitee_translations', function (Blueprint $table) {
createDefaultTranslationsTableFields($table, 'invitee');
Schema::create('invitee_slugs', function (Blueprint $table) {
createDefaultSlugsTableFields($table, 'invitee');
public function down()
Let's run the migration.
php artisan migrate
And then adjust the Model.
namespace App\Models;
use A17\Twill\Models\Behaviors\HasTranslation;
use A17\Twill\Models\Behaviors\HasSlug;
use A17\Twill\Models\Behaviors\HasMedias;
use A17\Twill\Models\Model;
class Invitee extends Model
use HasTranslation, HasSlug, HasMedias;
protected $fillable = [
public $translatedAttributes = [
public $slugAttributes = [
public $checkboxes = [
public $mediasParams = [
'profile' => [
'default' => [
'name' => 'landscape',
'ratio' => 1 / 1,
in this case, we need to pay special attention to the InviteeTranslation model.
class InviteeTranslation extends Model
protected $fillable = [
With the DB structure in place we now want to access via our CMS, hence we add it to the CMS menu by adding the routes.
And enabling the menu option in the navigation config\twill-navigation.php
'invitees' => [
'title' => 'Invitees',
'module' => true
Last but not least, the form
@formField('input', [
'name' => 'bio',
'label' => 'BIO',
'type' => 'textarea',
'translated' => true,
'maxlength' => 100
'name' => 'profile',
'label' => 'Profile picture',
Now we can enter as many Invitees as we want
Screen Shot 2019-03-31 at 18.04.02.pngScreen Shot 2019-03-31 at 18.08.05.png
The next step is to be able to select the invitees to the Articles, for that let's add a browser for the invitees to the article's form Up to 2 invitees in this case.
@formField('browser', [
'moduleName' => 'invitees',
'name' => 'invitees',
'label' => 'Invitees',
'max' => 2
With that in-place we can add attach invitees to an article.
Screen Shot 2019-03-31 at 18.57.50.png
But if we save it, we will not receive any error and nothing will be stored. This means it it time to create the association table with php artisan make:migration CreateArticleInviteeTable The command will create an empty migration and we will add what we need.
public function up()
Schema::create('article_invitee', function (Blueprint $table) {
createDefaultRelationshipTableFields($table, 'article', 'invitee');
Migration time...
php artisan migrate
With the DB to support we need to store the selected Invitees after saving the Article. Lets make sure that Articles knows about their invitees by adding to the Article Model, this:
public function invitees()
return $this->belongsToMany(\App\Models\Invitee::class);
On the ArticleRepository we add the afterSave function.
public function afterSave($object, $fields)
$this->updateBrowser($object, $fields, 'invitees');
parent::afterSave($object, $fields);
Also, for displaying the selected records we need to update the browser Fields that are displayed on the Form.
public function getFormFields($object)
$fields = parent::getFormFields($object);
$fields['browsers']['invitees'] = $this->getFormFieldsForBrowser($object, 'invitees');
return $fields;
We are all set for adding and saving the associated data.
Screen Shot 2019-03-31 at 19.55.07.png

Visual Block Editor

Now we have everything we need to build an Article we may want to enter the flexible content in the Block Editor directly.
Screen Shot 2019-03-31 at 20.16.51.png
If we click on any of the buttons we will receive an error
Screen Shot 2019-03-31 at 20.25.42.png
That cause of the error is we don't have the Blocks built for the FE. Time to follow the instructions again.
mkdir resources/views/layouts
touch resources/views/layouts/block.blade.php
touch resources/views/layouts/app.blade.php
The app.blade.php file will contain the basic HTML and the yield for the content.
<!DOCTYPE html>
<html dir="ltr" lang="en-US">
<meta charset="utf-8" />
We will extend the block's layout with the app layout.
Now the error is gone, but if we want to see the actual blocks, let's add some views.
Let's create the quote.blade.php inside the resources/views/site/blocks/
<div class="quote">
<p>{{ $block->translatedinput('quote') }}</p>
For the paragraph.blade.php
<div class="paragraph">
{!! $block->translatedinput('paragraph') !!}
There is one thing to consider when working with images For the image_with_text.blade.php
<div class="image_with_text">
{{ $block->input('text') }}
{{ $block->input('text') }}
And the gallery.blade.php
@php( $images = $block->images('gallery', 'default') )
@if( $images && sizeof($images) )
<div class="gallery">
<ul class="slides">
@foreach( $images as $item )
<li class="glide__slide">
<img src="{{ $item }}" alt="">
Everything ready for using the Block Editor.
Screen Shot 2019-03-31 at 22.33.47.pngScreen Shot 2019-03-31 at 22.34.35.png


Now we have the basic FE for the Block Editor. We can create the preview for the Articles.
First, let's click on the Preview link to see the error.
Screen Shot 2019-03-31 at 23.26.24.png
I'm not gonna create the resources/views/site/article.blade.php file. I'll add a new folder resources/views/site/articles and in there I'll create my view. So:
mkdir resources/views/site/articles
touch resources/views/site/articles/show.blade.php
In the show view, I'll add a simple code for this displaying the Article.
<div class='hero'>
@if( $item->hasImage('hero_image'))
<img src="{{ $item->image('hero_image', 'default') }}">
<h1>{{ $item->title }}</h1>
<p>{{ $item->description }}</p>
We are in good shape to render the Blocks in the show view.
<div class='hero'>
@if( $item->hasImage('hero_image'))
<img src="{{ $item->image('hero_image', 'default') }}">
<h1>{{ $item->title }}</h1>
<p>{{ $item->description }}</p>
<div class='content'>
{!! $item->renderBlocks(false) !!}
And will let Twill know which view we'll use for the preview
class ArticleController extends ModuleController
protected $moduleName = 'articles';
protected $previewView = '';


  • FE with CSS
  • Repeater inside Blocks and inside Main Structure
  • Browsers inside Blocks
  • Generic Pages
Show previous messages

July 13, 2020 at 1:10pm
I think the current available Youtube tutorial will get you started to pull the data. Then you can manage the front-end just how you like it.

July 27, 2020 at 2:29pm
If you meet an error like
Route [admin.lectors.edit] not defined.
your browser object most likely is 'in primary_navigation' or/and module name differs from relation name In this case you should use getFormFieldsForBrowser functions like:
getFormFieldsForBrowser($object, 'relationName','prefix','title', 'moduleName');

August 8, 2020 at 9:48pm
I just tried to follow this step on Twill 2.1.1. I had a problem on the cross table: the browser worked only swapping model names ( instead of: createDefaultRelationshipTableFields($table, 'table1', 'table2') I had to use createDefaultRelationshipTableFields($table, 'table2', 'table1') ): it is a Laravel convention of many to many table that should be created with models in alphabetical order.