menu
announcement

Spectrum will become read-only on August 10, 2021. Learn more about the decision in our official announcement.

Twill

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

Channels
# All channels
view-forward
# General
view-forward
# Announcements
view-forward
# Feature requests
view-forward
# Help
view-forward
# Jobs
view-forward
# Product vision
view-forward
# Resources
view-forward
# Show and tell
view-forward
# Tips and tricks
view-forward
Team

Step by step - Creating a Twill app.

March 18, 2019 at 12:54am

Step by step - Creating a Twill app.

March 18, 2019 at 12:54am (Edited 2 years ago)
In this post, I'll create an app from the ground up. I'll assume you have PHP, Laravel and Composer. Of course a mysql DB for storing "everything."
All the code it's here for reference.

Installation

Let's create a new app.
laravel new my-blog
Add homestead for easy development ;-)
cd my-blog
composer require laravel/homestead --dev
php vendor/bin/homestead make
and our main actor
composer require area17/twill:"1.2.*"
Now we are almost ready to start building our app. Let's verify that the configuration on the Homestead.yml is ok. I've changed the default sites for this:
sites:
- map: my-blog.dev.a17.io
to: "/home/vagrant/code/public"
- map: admin.my-blog.dev.a17.io
to: "/home/vagrant/code/public"
[NOTE]: An alternative way of creating the app is with these two commands
composer global require yanhaoli/create-twill-app
create-twill-app new blog
More info here

Running

Provision your vagrant machine with vagrant up
Add the IP and domain defined at Homestead.yaml to your /etc/hosts file.
192.168.10.44 my-blog.dev.a17.io
192.168.10.44 admin.my-blog.dev.a17.io
Now you should be able to visit your URL and receive the Laravel Homepage
Screen Shot 2019-03-16 at 22.13.24.png
This means that it is time for the magic touch. Jump into the VM and run the installation script
vagrant ssh
cd code
php artisan twill:install
It prompts for the superadmin email, password and password confirmation.
Now we can visit the admin URL and see everything working.
Screen Shot 2019-03-16 at 23.53.58.png

Building the structure

I need an Article entity which can have a Hero image, a title, a description and a flexible structure(Block Editor). Every article can be translated and have a specific URL(slug).
php artisan twill:module articles -TSMBR
Adapt the Table to our needs.
class CreateArticlesTables extends Migration
{
public function up()
{
Schema::create('articles', function (Blueprint $table) {
// this will create an id, a "published" column, and soft delete and timestamps columns
createDefaultTableFields($table);
});
Schema::create('article_translations', function (Blueprint $table) {
createDefaultTranslationsTableFields($table, 'article');
$table->string('title', 200)->nullable();
$table->text('description')->nullable();
});
Schema::create('article_slugs', function (Blueprint $table) {
createDefaultSlugsTableFields($table, 'article');
});
Schema::create('article_revisions', function (Blueprint $table) {
createDefaultRevisionsTableFields($table, 'article');
});
}
public function down()
{
Schema::dropIfExists('article_revisions');
Schema::dropIfExists('article_translations');
Schema::dropIfExists('article_slugs');
Schema::dropIfExists('articles');
}
}
Create the DB table
php artisan migrate
Rearrange the model counterpart app/Models/Article.php
class Article extends Model
{
use HasBlocks, HasTranslation, HasSlug, HasMedias, HasRevisions;
protected $fillable = [
'published',
];
public $translatedAttributes = [
'title',
'description',
];
public $slugAttributes = [
'title',
];
public $checkboxes = [
'published'
]
}
Now we have the DB structure we can add the routes to routes/admin.php
Route::module('articles');
and the option to the Twill menu config/twill-navigation.php
'articles' => [
'title' => 'Articles',
'module' => true
]
With that we have something like this:
Screen Shot 2019-03-17 at 13.19.18.png
Make sure you have all the languages you want to handle on the config/translatable.php file.
return [
'locales' => [
'en',
'es',
],
Our basic CMS for creating articles with translations is ready.
Screen Shot 2019-03-17 at 13.37.27.png

Hero Image

For adding images, we need to decide if we are going to use local storage or S3. I'll use local storage. Later in the process, we will switch to S3 and Imgix. Hence, let's add what the manual says:
  • for .env
MEDIA_LIBRARY_ENDPOINT_TYPE=local
MEDIA_LIBRARY_LOCAL_PATH=uploads/
MEDIA_LIBRARY_IMAGE_SERVICE=A17\Twill\Services\MediaLibrary\Local
Now let's make sure we have the mediasParams in the Article model.
public $mediasParams = [
'hero_image' => [
'default' => [
[
'name' => 'landscape',
'ratio' => 16 / 9,
]
]
]
];
also, the form field to upload the images articles/form.blade.php
@formField('medias',[
'name' => 'hero_image',
'label' => 'Hero image',
])
Great! Now we can upload the Hero Image.
Screen Shot 2019-03-17 at 16.54.53.pngScreen Shot 2019-03-17 at 16.55.14.png

Block Editor (flexible structure)

For this tutorial, I want the ability to add a gallery, an Image with text, a quote, and a paragraph. Let's go. Inside resources/views/admin/ create a folder called blocks. This folder contains all the Blocks for the CMS. Add the blocks (files containing the form fields we want to use for building an Article).
  • Gallery resources/views/admin/blocks/gallery.blade.php
@formField('medias', [
'name' => 'gallery',
'label' => 'Gallery',
'max' => 5,
'note' => 'Minimum image width: 1500px'
])
  • Image with text resources/views/admin/blocks/image_with_text.blade.php
@formField('medias', [
'name' => 'cover',
'label' => 'Image',
'note' => 'Minimum image width 1300px'
])
@formField('input', [
'translated' => true,
'name' => 'image_subtitle',
'label' => 'Image Subtitle (translated)',
'maxlength' => 250,
'required' => true,
'placeholder' => 'Description.',
'type' => 'textarea'
])
  • Quote resources/views/admin/blocks/quote.blade.php
@formField('input', [
'translated' => true,
'name' => 'quote',
'label' => 'Quote (translated)',
'maxlength' => 250,
'required' => true,
'type' => 'textarea',
'rows' => 3
])
  • Paragraph resources/views/admin/blocks/paragraph.blade.php
@formField('wysiwyg', [
'translated' => true,
'name' => 'paragraph',
'label' => 'Paragraph',
'maxlength' => 200,
'editSource' => true,
'note' => 'You can edit the source.',
])
Now with the blocks created Twill needs to know that these blocks in the config/twill.php like:
<?php
return [
'block_editor' => [
'block_single_layout' => 'layouts.block',
'blocks' => [
'gallery' => [
'title' => 'Gallery',
'icon' => 'image',
'component' => 'a17-block-gallery',
],
'image_with_text' => [
'title' => 'Image with text',
'icon' => 'image-text',
'component' => 'a17-block-image_with_text',
],
'quote' => [
'title' => 'Quote',
'icon' => 'quote',
'component' => 'a17-block-quote',
],
'paragraph' => [
'title' => 'Paragraph',
'icon' => 'text',
'component' => 'a17-block-paragraph',
],
],
'crops' => [
'cover' => [
'default' => [
[
'name' => 'default',
'ratio' => 1 / 1,
'minValues' => [
'width' => 100,
'height' => 100,
],
],
],
],
'gallery' => [
'default' => [
[
'name' => 'default',
'ratio' => 16 / 9,
'minValues' => [
'width' => 1024,
'height' => 768,
],
],
],
],
],
],
];
Please note that I've added a crops section. That is needed for the blocks that have images, without those the images are not stored.
If we have RTFM correctly then we need to add the scripts to the project's package.json:
"scripts": {
"twill-build": "npm run twill-copy-blocks && cd vendor/area17/twill && npm ci && npm run prod && cp -R public/* ${INIT_CWD}/public",
"twill-copy-blocks": "npm run twill-clean-blocks && mkdir -p resources/assets/js/blocks/ && mkdir -p vendor/area17/twill/frontend/js/components/blocks/customs/ && cp -R resources/assets/js/blocks/ vendor/area17/twill/frontend/js/components/blocks/customs",
"twill-clean-blocks": "rm -rf vendor/area17/twill/frontend/js/components/blocks/customs/*"
}
On the articles/form.blade.php we need to add the Block Editor selector.
@formField('block_editor')
Or
@formField('block_editor', [
'blocks' => ['gallery', 'image_with_text', 'quote', 'paragraph']
])
With that in place:
Let's create a folder for storing the blocks with
mkdir -p resources/assets/js/blocks/
[Note]: this step will be deprecated in future versions of Twill.
And let's build the blocks.
php artisan twill:blocks
npm run twill-build
Now the blocks are ready
Screen Shot 2019-03-17 at 17.41.09.png

Coming up next

  • Association and Browsers
  • Basic FE creation
The second part of the tutorial is available here:

Step by step II- Creating a Twill app.

thumbsup
18
message-simple
43

March 18, 2019 at 4:23pm
The final commands are swapped, they have to be run in this order:
npm run twill-build
php artisan twill:blocks
That tripped me up at first because I was receiving file_put_contents() errors.
  • reply
  • like
Hi this order wouldn't work to update the build with your custom blocks actually. Maybe you ran php artisan twill:blocks before creating some?
  • reply
  • like
ahhh okay, then my bad. You're right, I ran this command yesterday already. I was under the impression that those commands can be run as needed, not just one-time.
  • reply
  • like
They can be run as needed, definitely. I think it's just that right now the twill:blocks command fails when you didn't create any blocks under resources/views/admin/blocks yet. But the idea is that as soon as you create or edit blocks' Blade files, you need to generate their corresponding Vue components with php artisan twill:blocks and then to get those blocks into your build you need to run npm run twill-build. You can also run npm run twill-dev to run a watcher that is going to rebuild as soon as the Vue components changes, which means as soon as you run php artisan twill:blocks again.
Edited
  • reply
  • like
  • reply
  • like
okay that makes sense, thanks for explaining that. I'm having some issues also, maybe I just have a bad day haha. I followed the tutorial, except for the little snafu with the file_put_contents error everything went super smooth.
But now the blocks have no content editor in them, everything is just empty. Any idea why that could happen?
  • reply
  • like
yeah, it is usually an issue with the build. Can you take a look under vendor/area17/twill/frontend/js/components/blocks/customs and see if you have your generated Vue files in there or a blocks folder? If you see a blocks folder instead of the files directly, it is most likely an issue with the npm script in your project that does the copy operation before building. Make sure to update your scripts with the latest provided at https://twill.io/docs/#npm or try using the new experimental Artisan build command: php artisan twill:build.
Edited
  • reply
  • like
okay, you know what, I think I just messed up the test build because I remember I ran some of those commands yesterday, without having any blocks. Running php artisan twill:build fixed it, thanks you :)
like-fill
1
  • reply
  • like
awesome!
  • reply
  • like
I think if I can figure out how to really customize fields, and use all of the components you guys have in there, this is the perfect CMS for me. Def. going to be active in this community! Thanks for making this open source guys.
like-fill
3
  • reply
  • like
Hey I wonder now how it works with the Blocks. As far as I can read and find the Block Editor is like a advanced WYSIWYG Editor for the content for a Entry withing a CRUD Mod, right? But how does it store and where no need for extra field?
  • reply
  • like
Exactly, you can see it in action in our demo at https://demo.twill.io. Any of the works there is using a block editor. On this project we only have 4 blocks because that's the need of the design system on pentagram.com but some of our projects require much more, where we have all sort of blocks, even for related content for example. It stores content in a JSON column inside of the polymorphic blocks table that is created by Twill.
  • reply
  • like
Generating Twill Blocks gets me an error:
λ php artisan twill:blocks
Starting to scan block views directory...
ErrorException : file_put_contents(C:\laragon\www\twill-content\resources\assets/js/blocks/BlockParagraph.vue): failed to open stream: No such file or directory
at C:\laragon\www\twill-content\vendor\laravel\framework\src\Illuminate\Filesystem\Filesystem.php:122
118| * @return int|bool
119| */
120| public function put($path, $contents, $lock = false)
121| {
> 122| return file_put_contents($path, $contents, $lock ? LOCK_EX : 0);
123| }
124|
125| /**
126| * Write the contents of a file, replacing it atomically if it already exists.
Exception trace:
1 file_put_contents("C:\laragon\www\twill-content\resources\assets/js/blocks/BlockParagraph.vue", "<template>
<!-- eslint-disable -->
<div class="block__body">
<a17-locale type="a17-wysiwyg" :attributes="{ label: 'Paragraph', name: fieldName('paragraph'), note: 'You can edit the source.', maxlength: 200, editSource: true, inStore: 'value' }" ></a17-locale>
</div>
</template>
<script>
import BlockMixin from '@/mixins/block'
export default {
mixins: [BlockMixin]
}
</script>
")
C:\laragon\www\twill-content\vendor\laravel\framework\src\Illuminate\Filesystem\Filesystem.php:122
2 Illuminate\Filesystem\Filesystem::put("C:\laragon\www\twill-content\resources\assets/js/blocks/BlockParagraph.vue", "<template>
<!-- eslint-disable -->
<div class="block__body">
<a17-locale type="a17-wysiwyg" :attributes="{ label: 'Paragraph', name: fieldName('paragraph'), note: 'You can edit the source.', maxlength: 200, editSource: true, inStore: 'value' }" ></a17-locale>
</div>
</template>
<script>
import BlockMixin from '@/mixins/block'
export default {
mixins: [BlockMixin]
}
</script>
")
C:\laragon\www\twill-content\vendor\laravel\framework\src\Illuminate\Support\Facades\Facade.php:237
Please use the argument -v to see more details.
C:\laragon\www\twill-content
λ clear
C:\laragon\www\twill-content
λ php artisan twill:blocks
Starting to scan block views directory...
ErrorException : file_put_contents(C:\laragon\www\twill-content\resources\assets/js/blocks/BlockGallery.vue): failed to open stream: No such file or directory
at C:\laragon\www\twill-content\vendor\laravel\framework\src\Illuminate\Filesystem\Filesystem.php:122
118| * @return int|bool
119| */
120| public function put($path, $contents, $lock = false)
121| {
> 122| return file_put_contents($path, $contents, $lock ? LOCK_EX : 0);
123| }
124|
125| /**
126| * Write the contents of a file, replacing it atomically if it already exists.
Exception trace:
1 file_put_contents("C:\laragon\www\twill-content\resources\assets/js/blocks/BlockGallery.vue", "<template>
<!-- eslint-disable -->
<div class="block__body">
<a17-inputframe label="Gallery" name="medias.gallery" > <a17-slideshow :name="fieldName('gallery')" :max="5" crop-context="gallery" >Minimum image width: 1500px</a17-slideshow> </a17-inputframe>
</div>
</template>
<script>
import BlockMixin from '@/mixins/block'
export default {
mixins: [BlockMixin]
}
</script>
")
C:\laragon\www\twill-content\vendor\laravel\framework\src\Illuminate\Filesystem\Filesystem.php:122
2 Illuminate\Filesystem\Filesystem::put("C:\laragon\www\twill-content\resources\assets/js/blocks/BlockGallery.vue", "<template>
<!-- eslint-disable -->
<div class="block__body">
<a17-inputframe label="Gallery" name="medias.gallery" > <a17-slideshow :name="fieldName('gallery')" :max="5" crop-context="gallery" >Minimum image width: 1500px</a17-slideshow> </a17-inputframe>
</div>
</template>
<script>
import BlockMixin from '@/mixins/block'
export default {
mixins: [BlockMixin]
}
</script>
")
C:\laragon\www\twill-content\vendor\laravel\framework\src\Illuminate\Support\Facades\Facade.php:237
Please use the argument -v to see more details.
  • reply
  • like
Exactly, you can see it in action in our demo at https://demo.twill.io. Any of the works there is using a block editor. On this project we only have 4 blocks because that's the need of the design system on pentagram.com but some of our projects require much more, where we have all sort of blocks, even for related content for example. It stores content in a JSON column inside of the polymorphic blocks table that is created by Twill.
Cool seems like a great concept. Only need to get it to work, Im only trying Gallery and Paragraph. But cant get it to work yet.
  • reply
  • like
it seems like it is running into issues creating the folder where blocks get generated. can you try to manually create the resources/assets/js/blocks folder?
Edited
like-fill
1
  • reply
  • like
to clarify, that's because you never had to build the assets before. I'll chat with to update the tutorial above!
  • reply
  • like
it seems like it is running into issues creating the folder where blocks get generated. can you try to manually create the resources/assets/js/blocks folder?
This also worked for me while I did run php artisan twill:blocks before creating blocks. Seems a great Idea to fix this.
  • reply
  • like
This also worked for me while I did run php artisan twill:blocks before creating blocks. Seems a great Idea to fix this.
definitely, I'm on it. this is actually a small regression on Twill 1.2.1 because we do not require developers to build when starting their project as we now ship with the default compiled assets. Before you always had to build first and so that folder was automatically created.
  • reply
  • like
definitely, I'm on it. this is actually a small regression on Twill 1.2.1 because we do not require developers to build when starting their project as we now ship with the default compiled assets. Before you always had to build first and so that folder was automatically created.
Alright Cool, looking forward to see this CMS grow. It seems like the perfect balance for CMS and still creating your own site. I'm defently gonna make a youtube series on it to contribute. Also got some ideas open.
  • reply
  • like

March 19, 2019 at 12:02pm
I gotta say, playing around with it today some more, and I can see this will be a game changer. It's pretty much possible to build the whole custom backend to a site in like a day and then focus on design / templating etc.
Just need better documentation/recipes. Now I'll go back to figuring out how to use the endpoint for select fields. PS: I probably should open an issue for this, but the tags form field has the name hardcoded, wouldn't it be better to have that dynamic so one can create multiple tag types for taxonomy purposes?
Or is there a form field I haven't found yet that allows true taxonomization?
  • reply
  • like
Ah okay scratch that, I see it in the docs, basically one can create a Crud module "Attributes", make that available for other modules via editInModal so that for example Categories could be selected & created directly on the Articles publishing form? is that correct?
  • reply
  • like

March 20, 2019 at 12:52pm
When are you hoping to be able to put together the Basic FE creation guide?
  • reply
  • like
Hey , hopefully this week, it is WIP.
like-fill
1
  • reply
  • like
Busy week so far :)
  • reply
  • like
Hey , in the meantime, I just wanted to say that Twill doesn't make any assumptions about the FE you want to build. If you've been working with Laravel before, there's really no difference in how you would retrieve content from Eloquent models. Twill definitely provides some helpers to work with attached images and blocks that I'm sure is going to introduce in his next tutorial part, but feel free to ask any question you have in the meantime.
  • reply
  • like
Show more messages