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

April 4, 2019 at 3:42am
what does without translations enabled entails to you? If your translatable configuration only defines a single locale code the UI doesn't show anything relating to translations. If you're absolutely sure you will never want to translate your content, you would not use the translations traits and table. But if you have even a slight doubt you might need to in the future, I would recommend to use the feature with a single language to avoid having to migrate columns in the future.

April 4, 2019 at 5:15pm
Thats a perfectly reasonable argument . I just had to change more of the default behavior to remove the 'en' from the slug. Unless I setup something wrong. I made sure I only had one language set in preferences. My use case is medium to large size local businesses and churches with no need to have more than one language presented.

April 6, 2019 at 8:34pm
Hey guys, while following this tutorial I received the error (Your submission could not be validated, please fix and retry) in the log file it said (Column not found: 1054 Unknown column 'position' in 'field list' (SQL: insert into article_invitee). I added the column and then it worked. I was curious why in my case a position column was needed in the pivot table article_invitee and not for other people following this tutorial. I think it's because I rearranged the order of the invitees once in the user interface. Is this reasoning correct or is it something else?

April 8, 2019 at 1:39am
Hey , you are right that field is needed in the migration if you want to re-order the Invitees. If you take a look at the source code, it is there. I've updated the tutorial to include that in the migration.

April 8, 2019 at 9:57pm
I understand. And Twill seems great but I'm now looking for other solutions since chatting and researching cost me to much time. I think a straight forward doc would be nice. For everyone to get started.
  • Create CRUD Module
  • Add Tags
  • BelongsTo x
  • HasMany x
  • Remove Published
  • Change default title field to e.g name

April 11, 2019 at 3:54pm
Ok thanks for confirming. Just wanted to point it out for other people following this tutorial :). Looking forward for the front end stuff next time. When do you think the next tutorial will be released? I get that there are other priorities so don't feel rushed I'm just curious

April 13, 2019 at 3:17pm
I'm polishing a bit the article and will come out hopefuly on monday.

April 15, 2019 at 1:17pm
This is exactly what I needed. Thanks for this work . Looking forward to the next one.

April 30, 2019 at 3:50pm
looking forward for the next part! Thanks!

May 2, 2019 at 2:39pm
When will the new part be released?

May 3, 2019 at 8:27pm
I want to say thankyou for taking your time to create these very useful guides, hoping to see more of them.

May 14, 2019 at 12:56pm
Can't wait for the next tutorial, it's been very helpfull.

May 23, 2019 at 12:03pm
Any plan / Schedule on tutorial for the font end

July 16, 2019 at 8:51pm
next tutorial for front end and user registration using twill auth

July 22, 2019 at 1:53am
Thank you please keep these coming,.... info on fully customizing the index view too please!

August 23, 2019 at 7:49pm
I get a fatal error having tried to copy this project and run it locally. A few tweaks to the config to get the database and migrations and an admin user setup worked fine but when trying to run the login page I get...
ErrorException (E_ERROR) The Mix manifest does not exist. (View: twilldemo\vendor\area17\twill\views\partials\head.blade.php) (View: twilldemo\vendor\area17\twill\views\partials\head.blade.php) (View: twilldemo\vendor\area17\twill\views\partials\head.blade.php)

August 28, 2019 at 7:54pm

September 3, 2019 at 2:47pm
Implementing the middle part of code to create a browser field outside of a block structure to link two data models together isn't working for me. I've tried following the code in two separate projects now and in both cases the interface appears, updating the record appears to work with the yellow success msg, yet no record is ever written to the association table that was created so when you reload the edit page in twill the linked record is not there.
I've created a post on this here, if anyone can suggest ways to troubleshoot ?

September 19, 2019 at 6:36am
About title . As we can see on this example a title can be some person's name. And I want to have a table with title that is not translateable and description is. And I can't achieve that because I must have the title translateable - this title is the field when you add a record. I don't want to repeat the name of a person in other languages because I suppose it doesn't change and I don't use Slugs in this example.

September 19, 2019 at 1:12pm
Hi , there are multiple ways to go about disabling the title field translation on a translated model but I think the simplest approach is to create a create.blade.php file in the views folder for that Twill module, next to the form file, with the following code: ('twill::partials.create', ['translateTitle' => false]).

September 21, 2019 at 2:23pm

September 25, 2019 at 12:22pm
Hey, It works fine. But there is the Permalink I see I can switch it off and I added it manually with 'translated' => false but permalink gets value [object Object], when I fill this field manually the value gets saved in slug in table (not with translations of course). But this permalink doesn't appear under title (because not HasSlugs?) and when I edit gain title it get value [object Object]. Permalink Prefix is working well. I just wanted to have a translatable module but with Title/Slug that is not translatable. Is that possible?

April 27, 2020 at 5:50am
Where can I find next step? this year is 2020.

April 28, 2020 at 9:30pm
Please visit our YouTube Channel for more examples and tutorials

April 29, 2020 at 5:26am
Yes I am doing with Youtube now. But there's no code samples or repository. When I encountered problems, It's hard to find where and what I missed or mistaken :(
Show more messages