近期公司內部服務都朝向 Laravel framework 來維護,其中常見需求是製作具有 CMS 管理的機制,提供不同部門的同事編輯資料及發佈出去。就來試試看 Twill 這個 CMS Toolkit 套件。從他的文件得知,引入他可以快速擁有後台登入機制,以及省去自己規劃資料庫的資料表,而相較 wordpress 則是有更大的彈性做事。
目前在 Macbook M1 和 MacPorts 環境下,操作一下,先弄個 Laravel project 出來:
```
% sudo port install php83 php83-iconv php83-intl php83-mbstring php83-openssl php83-curl php83-sqlite php83-zip php83-gd php83-exif
% alias php=php83
% wget https://getcomposer.org/download/latest-stable/composer.phar -O /tmp/composer.phar
% php /tmp/composer.phar self-update
% alias composer="php /tmp/composer.phar"
% composer create-project --prefer-dist laravel/laravel /tmp/laravel-workspace
Creating a "laravel/laravel" project at "/tmp/laravel-workspace"
Installing laravel/laravel (v11.1.4)
- Installing laravel/laravel (v11.1.4): Extracting archive
Created project in /tmp/laravel-workspace
> @php -r "file_exists('.env') || copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies
...
```
這邊偷懶用 alias composer="php /tmp/composer.phar" ,後面會碰到類似的的錯誤訊息時,其實就是找不到 composer 指令:
```
Symfony\Component\Process\Exception\ProcessStartFailedException
The command "'composer' 'dump-autoload'" failed.
Working directory: /private/tmp/laravel-workspace
Error: proc_open(): posix_spawn() failed: No such file or directory
```
因此可以把 /tmp/composer.phar 擺到 PATH 內會尋找指令的地方,或是人工再補個 composer dump-autoload 等等
```
% cp /tmp/composer.phar ~/.bin/composer
% chmod 755 ~/.bin/composer
```
```
% cd /tmp/laravel-workspace
laravel-workspace % composer require area17/twill:"^3.0"
laravel-workspace % php artisan twill:install
...
Let's create a superadmin account!
Enter an email:
> user@example.com
Enter a password:
>
Confirm the password:
>
Your account has been created
All good!
```
接著安裝後台模組 Pages:
```
laravel-workspace % php artisan twill:make:module pages
Do you need to use the block editor on this module? [yes]:
[0] no
[1] yes
>
Do you need to translate content on this module? [yes]:
[0] no
[1] yes
>
Do you need to generate slugs on this module? [yes]:
[0] no
[1] yes
>
Do you need to attach images on this module? [yes]:
[0] no
[1] yes
>
Do you need to attach files on this module? [yes]:
[0] no
[1] yes
>
Do you need to manage the position of records on this module? [yes]:
[0] no
[1] yes
>
Do you need to enable revisions on this module? [yes]:
[0] no
[1] yes
>
Do you need to enable nesting on this module? [no]:
[0] no
[1] yes
>
Do you also want to generate a model factory? [yes]:
[0] no
[1] yes
>
Do you also want to generate a model seeder? [yes]:
[0] no
[1] yes
>
Migration created successfully! Add some fields!
INFO Factory [database/factories/PageFactory.php] created successfully.
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? [yes]:
[0] no
[1] yes
>
INFO Seeder [database/seeders/PageSeeder.php] created successfully.
The following snippet has been added to routes/twill.php:
-----
TwillRoutes::module('pages');
-----
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;
public function boot()
{
...
TwillNavigation::addLink(
NavigationLink::make()->forModule('pages')
);
}
-----
Do not forget to migrate your database after modifying the migrations.
Enjoy.
```
上述的意思是 routes/twill.php 已經增加好後台管理介面的 routing 規則:
```
% cat routes/twill.php
<?php
use A17\Twill\Facades\TwillRoutes;
// Register Twill routes here eg.
// TwillRoutes::module('posts');
TwillRoutes::module('pages');
```
但是 app/Providers/AppServiceProvider.php 必須自己處理,調整成下方:
```
laravel-workspace % cat app/Providers/AppServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use A17\Twill\Facades\TwillNavigation;
use A17\Twill\View\Components\Navigation\NavigationLink;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
TwillNavigation::addLink(
NavigationLink::make()->forModule('pages')
);
}
}
```
這些更動是讓後台 /admin 時,上方導覽可以多一項 Pages 的功能,後續透過下方來使用:
laravel-workspace % php artisan migrate
laravel-workspace % php artisan serve --host 0.0.0.0 --port 8000
如此,就可以用 http://localhost:8000/admin 登入後台,點擊 Pages 切換到 http://localhost:8000/admin/pages 可以新增 Pages ,可點擊 Add new 按鈕一則,這時可以看到前台網址規則是 localhost/en/pages/hello-world ,但實際上在 PHP Laravel routing 規則中,前台網址規則都還沒實作處理,所以是看不到資料的。
上述僅做了簡易的後台搭建,後續要處理的有:
- 讓後來編輯界面更加豐富,例如有更多的元件(image, text)等,主要是擴增編輯使用的表單元素
- 處理後台編輯時,preview 缺少的 css 資源 (運行 npm install && npm run build)
- 建立新元素的樣板資料
- 建立前台網頁的網址規則跟處理的 Controller (PageDisplayController)
如此,在後台把頁面發布出去後,就可以在前台被瀏覽到。但光上述四點的操作項目是不少的。
由於預設的前台網頁網址規則是跟語言相關的,例如建立個 Hello World Page 後,可以看到他的前台網址是 localhost/en/pages/hello-world ,若要去掉語言,就是調整 app/Http/Controllers/Twill/PageController.php :
```
18 protected function setUpController(): void
19 {
20 $this->setPermalinkBase(''); // 去掉 /pages/ 那層網址
21 $this->withoutLanguageInPermalink(); // 去掉 /en/ 那層網址
22 }
```
接著來調整撰文時的表單功能,例如目前每一個 Page 都可以填寫 title 跟 description 可增加 SEO 的效果,而增加文章分享後的美觀,則是要增加圖片,這時直接修改 app/Http/Controllers/Twill/PageController.php ,添加發文時可以上傳圖片功能:
```
6 use A17\Twill\Services\Forms\Fields\Medias;
...
29 public function getForm(TwillModelContract $model): Form
30 {
31 $form = parent::getForm($model);
32
33 $form->add(
34 Input::make()->name('description')->label('Description')->translatable()
35 );
36
37 $form->add(
38 Medias::make()->name('cover')->label('Cover image')
39 );
40
41 return $form;
42 }
...
```
主要是新增引入 `use A17\Twill\Services\Forms\Fields\Medias;` 跟 `$form->add( Medias::make()->name('cover')->label('Cover image') );`
這時我們的 laravel 是透過 artisan 跑在 8000 port,拖拉上傳圖片時,其實會顯示不出來
因為他的圖片網址規則並沒有帶 port ,且只要把網址複製出來加上 port number 就可以正常顯示,代表只需處理 Laravel framework 的 .env 中 APP_URL 規則:
```
% cat .env | grep APP_URL
APP_URL=http://localhost
```
這時建議測試時可以有兩套環境設置,運行時指定設定檔案:
```
% cp .env .env.local
% cat .env.local | grep APP_URL
APP_URL=http://localhost:8000
% php artisan serve --host 0.0.0.0 --port 8000 --env local
INFO Server running on [http://0.0.0.0:8000].
Press Ctrl+C to stop the server
```
如此也解掉後台圖片顯示失敗的問題。
下一刻則是擴充文章編輯環境,引入 Block Editor 架構,也就是在編輯文章時,可以拖拉區塊到文章內,還可以自行開發,把常用的項目元件化。從 Twill 官網的範例資訊,預設有兩個 Block 了,一個是 image block ,另一個是 wysiwyg block,官網範例會試著新增一個小的區塊,例如名為 Text 的區塊。
首先,先啟用 Block Editor,啟用方式是在 app/Http/Controllers/Twill/PageController.php 裡引入 `use A17\Twill\Services\Forms\Fields\BlockEditor;` 和增加 `$form->add( BlockEditor::make() );`
接著則是試著建立一個 text block:
```
laravel-workspace % php artisan twill:make:block text
Should we also generate a view file for rendering the block? (yes/no) [no]:
> yes
Creating block...
File: /private/tmp/laravel-workspace/resources/views/twill/blocks/text.blade.php
Block text was created.
Block text blank render view was created.
Block is ready to use with the name 'text'
laravel-workspace % cat resources/views/twill/blocks/text.blade.php
@twillBlockTitle('Text')
@twillBlockIcon('text')
@twillBlockGroup('app')
<x-twill::input
name="title"
label="Title"
:translated="true"
/>
<x-twill::wysiwyg
name="text"
label="Text"
placeholder="Text"
:toolbar-options="[
'bold',
'italic',
['list' => 'bullet'],
['list' => 'ordered'],
[ 'script' => 'super' ],
[ 'script' => 'sub' ],
'link',
'clean'
]"
:translated="true"
/>
```
可以看到其樣板也長出來了
但是在後台 preview 時,仍會有待處理的訊息:
This is a basic preview. You can use dd($block) to view the data you have access to. <br />This preview file is located at: /private/tmp/laravel-workspace/resources/views/site/blocks/text.blade.php
將他修改一下:
```
% cat resources/views/site/blocks/text.blade.php
<div class="prose">
<h2>{{$block->translatedInput('title')}}</h2>
{!! $block->translatedInput('text') !!}
</div>
```
接著再產生另一個 image block 並更新他的 block view 跟 preview:
```
laravel-workspace % php artisan twill:make:block image
Should we also generate a view file for rendering the block? (yes/no) [no]:
> yes
Creating block...
File: /private/tmp/laravel-workspace/resources/views/twill/blocks/image.blade.php
Block image was created.
Block image blank render view was created.
Block is ready to use with the name 'image'
laravel-workspace % cat resources/views/twill/blocks/image.blade.php
@twillBlockTitle('Image')
@twillBlockIcon('text')
@twillBlockGroup('app')
<x-twill::medias
name="highlight"
label="Highlight"
/>
laravel-workspace % cat resources/views/site/blocks/image.blade.php
<div class="py-8 mx-auto max-w-2xl flex items-center">
<img src="{{$block->image('highlight', 'desktop')}}"/>
</div>
```
此外,image block 要生效還需要調整 config/twill.php
```
% cat config/twill.php
<?php
return [
'block_editor' => [
'crops' => [
'highlight' => [
'desktop' => [
[
'name' => 'desktop',
'ratio' => 16 / 9,
],
],
'mobile' => [
[
'name' => 'mobile',
'ratio' => 1,
],
],
],
],
],
];
```
接著,還要幫前後台產的文章添加 CSS 效果 `@vite('resources/css/app.css')` ,添加方式是修改 `resources/views/site/layouts/block.blade.php` 跟 `resources/views/site/page.blade.php`
```
laravel-workspace % cat resources/views/site/layouts/block.blade.php
<!doctype html>
<html lang="en">
<head>
<title>#madewithtwill website</title>
@vite('resources/css/app.css')
</head>
<body>
<div>
@yield('content')
</div>
</body>
</html>
laravel-workspace % cat resources/views/site/page.blade.php
<!doctype html>
<html lang="en">
<head>
<title>{{ $item->title }}</title>
@vite('resources/css/app.css')
</head>
<body>
<div class="mx-auto max-w-2xl">
{!! $item->renderBlocks() !!}
</div>
</body>
</html>
```
此外,還要用 npm 工具編譯出 resources/css/app.css:
```
% nvm use v20
Now using node v20.9.0 (npm v10.3.0)
% npm install
added 23 packages, and audited 24 packages in 577ms
5 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
laravel-workspace % tree resources/css/
resources/css/
└── app.css
1 directory, 1 file
```
如此,在 Twill 後台編輯文章時,就可以在 Block editor 操作下增加文字區塊、圖片等等的功能。
不過,直到現在前台機制還沒打通,尚未提供前台 routing rule 等顯示前台網頁,先建立個 PageDisplayController 來處理:
```
% php artisan make:controller PageDisplayController
INFO Controller [app/Http/Controllers/PageDisplayController.php] created successfully.
```
把 PageDisplayController 更新為可以接一個參數,並且立刻把它印出 debug 訊息:
```
% cat app/Http/Controllers/PageDisplayController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Contracts\View\View;
class PageDisplayController extends Controller
{
public function show(string $slug): View
{
dd($slug);
}
}
```
接著,再把 routing 設置好:
```
% cat routes/web.php
<?php
use Illuminate\Support\Facades\Route;
//Route::get('/', function () {
// return view('welcome');
//});
Route::get('{slug}', [\App\Http\Controllers\PageDisplayController::class, 'show'])->name('frontend.page');
```
如此在前台瀏覽網頁就會看到:
最後,再把 PageDisplayController 調整成顯示正確的資料:
```
laravel-workspace % cat app/Http/Controllers/PageDisplayController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Contracts\View\View;
use App\Repositories\PageRepository;
class PageDisplayController extends Controller
{
//public function show(string $slug): View
//{
// dd($slug);
//}
public function show(string $slug, PageRepository $pageRepository): View
{
$page = $pageRepository->forSlug($slug);
if (!$page) {
abort(404);
}
return view('site.page', ['item' => $page]);
}
}
```
如此瀏覽前台時,例如 http://localhost:8000/hello-world 就可以顯示網頁內容了。
最後,如果碰到前台圖片沒有顯示出來的部分,則是留意拖拉建立圖片時,需要依照不同裝置版型做設定,例如 PC 瀏覽時看不到圖片,那應當是少設定的 desktop 的設置:
```
laravel-workspace % cat resources/views/site/blocks/image.blade.php
<div class="py-8 mx-auto max-w-2xl flex items-center">
<img src="{{$block->image('highlight', 'desktop')}}"/>
</div>
```
需留意在 block editor 時,其 Image 拖拉進去時,有沒有 `desktop crop` 的描述