Tạo trang có thể tùy biến các trường dễ dàng để tạo api cho trang full (ok)

Đọc thêm ở đây: https://chatgpt.com/c/683b9024-3934-8003-8610-d5dc1cd2f628

✅ Giải pháp: Dynamic Page + JSON Field + Builder UI + API

1. Cấu trúc DB: Bảng pages

Schema::create('pages', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('slug')->unique();
    $table->json('fields')->nullable(); // chứa các trường tùy biến
    $table->timestamps();
});

2. Dữ liệu mẫu trong fields (kiểu JSON)

{
  "title": "Trang giới thiệu",
  "image_url": "https://example.com/banner.jpg",
  "content": "<p>Chúng tôi là công ty...</p>"
}

🧱 3. Giao diện tạo page: Builder UI

Bạn dùng các thành phần như:

  • Input text → map đến fields.title

  • Image URL / File chọn → map đến fields.image_url

  • WYSIWYG editor → map đến fields.content

  • Các trường bạn chọn sẽ lưu chung vào fields (json)

Giao diện dạng lặp:

@foreach($configFields as $field)
  <label>{{ $field['label'] }}</label>
  @if($field['type'] === 'text')
    <input type="text" name="fields[{{ $field['name'] }}]" value="{{ $page->fields[$field['name']] ?? '' }}">
  @elseif($field['type'] === 'image')
    <input type="text" name="fields[{{ $field['name'] }}]" value="{{ $page->fields[$field['name']] ?? '' }}">
  @elseif($field['type'] === 'editor')
    <textarea name="fields[{{ $field['name'] }}]">{{ $page->fields[$field['name']] ?? '' }}</textarea>
  @endif
@endforeach

Bạn có thể lưu $configFields trong DB hoặc config file PHP để tùy biến thêm.

4. API hiển thị nội dung

Route::get('/api/pages/{slug}', function ($slug) {
    $page = \App\Models\Page::where('slug', $slug)->firstOrFail();
    return response()->json([
        'title' => $page->title,
        'slug' => $page->slug,
        'fields' => $page->fields
    ]);
});

Kết quả trả về:

{
  "title": "Trang giới thiệu",
  "slug": "gioi-thieu",
  "fields": {
    "title": "Chúng tôi là ai",
    "image_url": "https://example.com/banner.jpg",
    "content": "<p>Chúng tôi là công ty công nghệ...</p>"
  }
}

Bạn đang xây dựng hệ thống CMS với các trường tùy biến và bây giờ muốn hỗ trợ đa ngôn ngữ trong trường fields, cụ thể là lưu theo cấu trúc:

{
  "vi": {
    "field1": "Giá trị tiếng Việt",
    "field2": "Giá trị tiếng Việt"
  },
  "en": {
    "field1": "English value",
    "field2": "English value"
  }
}

Dưới đây là hướng dẫn đầy đủ để cấu trúc fields đa ngôn ngữ và cập nhật controller, form và API.

✅ 1. Cấu trúc dữ liệu

Bạn giữ nguyên cột fields kiểu json, nhưng cấu trúc nó như sau:

[
  'vi' => [
    'title' => 'Trang chủ',
    'description' => 'Chào mừng bạn'
  ],
  'en' => [
    'title' => 'Home',
    'description' => 'Welcome'
  ]
]

✅ 2. Controller – store()

Giả sử bạn gửi từ frontend 1 trường fields_json có dạng:

{
  "vi": {
    "title": "Trang chủ",
    "description": "Chào mừng bạn"
  },
  "en": {
    "title": "Home",
    "description": "Welcome"
  }
}

Bạn không cần đổi gì nhiều, chỉ cần đảm bảo dữ liệu gửi về đúng format:

use Illuminate\Support\Str;

public function store(Request $request)
{
    $raw = json_decode($request->input('fields_json'), true);
    Page::create([
        'title' => $request->input('page_title'),
        'slug' => Str::slug($request->input('page_title')),
        'fields' => $raw,
    ]);
    return redirect()->route('pages.index')->with('success', 'Tạo trang thành công!');
}

✅ 3. API – routes/web.php

Bạn có thể thêm lang làm query string hoặc route param:

Route::get('/api/pages/{slug}', function ($slug) {
    $lang = request()->query('lang', 'vi'); // mặc định là 'vi'
    $page = Page::where('slug', $slug)->firstOrFail();

    return response()->json([
        'title' => $page->title,
        'slug' => $page->slug,
        'fields' => $page->fields[$lang] ?? [], // Trả về fields theo ngôn ngữ
    ]);
});

✅ 4. Tùy chọn: Thêm helper trong model

Để dễ dùng trong Blade hoặc nơi khác:

public function getTranslatedFields($lang = 'vi')
{
    return $this->fields[$lang] ?? [];
}

5. Gợi ý frontend (JavaScript/Vue/React)

Gửi dữ liệu JSON như sau:

const data = {
  vi: {
    title: document.querySelector('#title_vi').value,
    content: document.querySelector('#content_vi').value,
  },
  en: {
    title: document.querySelector('#title_en').value,
    content: document.querySelector('#content_en').value,
  }
};
document.querySelector('input[name="fields_json"]').value = JSON.stringify(data);

Tạo giao diện nhập liệu đa ngôn ngữ (tab "VI" / "EN")

app\Http\Controllers\Admin\PageController.php

public function create(){
    return view('admin.pages.create');
}
public function store(Request $request){
    $raw = json_decode($request->input('fields_json'), true);
    Page::create([
      'title' => $request->input('page_title'),
      'slug' => Str::slug($request->input('page_title')),
      'fields' => $raw, // ✅ Đây là format bạn muốn
    ]);
  return redirect()->route('pages.index')->with('success', 'Tạo trang thành công!');
}

routes\web.php

<?php
use App\Http\Controllers\Admin\PageController;
use App\Models\Page;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
  return view('welcome');
});
Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
Route::get('/api/pages/{slug}', function ($slug) {
  $page = Page::where('slug', $slug)->firstOrFail();
  return response()->json([
    'title' => $page->title,
    'slug' => $page->slug,
    'fields' => $page->fields
  ]);
});
Route::prefix('admin')->group(function () {
  Route::resource('pages', PageController::class);
});

resources\views\admin\pages\create.blade.php

@extends('layouts.admin')
@section('content')
<div class="container">
  <h1>Tạo trang mới</h1>
  <form action="{{ route('pages.store') }}" method="POST" onsubmit="return prepareFieldsJson();">
    @csrf
    <div class="mb-3">
      <label for="page_title" class="form-label">Tiêu đề trang</label>
      <input type="text" class="form-control" id="page_title" name="page_title" required>
    </div>
    <!-- Tabs for languages -->
    <ul class="nav nav-tabs mb-3" id="langTabs" role="tablist">
      <li class="nav-item" role="presentation">
        <button class="nav-link active" id="vi-tab" data-bs-toggle="tab" data-bs-target="#vi" type="button" role="tab">VI</button>
      </li>
      <li class="nav-item" role="presentation">
        <button class="nav-link" id="en-tab" data-bs-toggle="tab" data-bs-target="#en" type="button" role="tab">EN</button>
      </li>
    </ul>
    <div class="tab-content" id="langTabsContent">
      <!-- VI Tab -->
      <div class="tab-pane fade show active" id="vi" role="tabpanel">
        <div class="mb-3">
          <label for="title_vi" class="form-label">Tiêu đề (VI)</label>
          <input type="text" class="form-control" id="title_vi">
        </div>
        <div class="mb-3">
          <label for="content_vi" class="form-label">Nội dung (VI)</label>
          <textarea class="form-control" id="content_vi" rows="4"></textarea>
        </div>
      </div>
      <!-- EN Tab -->
      <div class="tab-pane fade" id="en" role="tabpanel">
        <div class="mb-3">
          <label for="title_en" class="form-label">Title (EN)</label>
          <input type="text" class="form-control" id="title_en">
        </div>
        <div class="mb-3">
          <label for="content_en" class="form-label">Content (EN)</label>
          <textarea class="form-control" id="content_en" rows="4"></textarea>
        </div>
      </div>
    </div>
    <!-- Hidden input to hold JSON -->
    <input type="hidden" name="fields_json" id="fields_json">
    <button type="submit" class="btn btn-primary">Tạo trang</button>
  </form>
</div>
@endsection
@section('scripts')
<script>
  function prepareFieldsJson() {
    const data = {
      vi: {
        title: document.getElementById('title_vi').value,
        content: document.getElementById('content_vi').value
      },
      en: {
        title: document.getElementById('title_en').value,
        content: document.getElementById('content_en').value
      }
    };
    document.getElementById('fields_json').value = JSON.stringify(data);
    return true;
  }
</script>
@endsection

Giao diện đơn giản làm Dynamic Builder

@extends('layouts.admin')
@section('content')
<div class="p-4">
  @if(session('success'))
  <div class="text-green-600">{{ session('success') }}</div>
  @endif
  <form method="POST" action="/admin/pages">
    @csrf
    <div class="mb-4">
      <label class="block font-bold">Tên Trang</label>
      <input name="page_title" class="form-input w-full border" required>
    </div>
    @foreach ($configFields as $field)
    <div class="mb-4">
      <label class="block font-bold">{{ $field['label'] }}</label>
      @if ($field['type'] === 'text')
      <input type="text" name="fields[{{ $field['name'] }}]" class="form-input w-full border">
      @elseif ($field['type'] === 'editor')
      <textarea name="fields[{{ $field['name'] }}]" class="form-textarea w-full border ckeditor"></textarea>
      @endif
    </div>
    @endforeach
    <button class="btn btn-success">Lưu</button>
  </form>
</div>
<script src="https://cdn.ckeditor.com/ckeditor5/41.0.0/classic/ckeditor.js"></script>
<script>
  document.querySelectorAll('.ckeditor').forEach(el => {
    ClassicEditor.create(el).catch(console.error);
  });
</script>
@endsection

routes\web.php

Route::get('/admin/pages/test', function () {
  $configFields = [
      ['name' => 'title', 'label' => 'Tiêu đề', 'type' => 'text'],
      ['name' => 'image_url', 'label' => 'Ảnh đại diện', 'type' => 'text'],
      ['name' => 'content', 'label' => 'Nội dung', 'type' => 'editor'],
  ];
  return view('admin.pages.test', compact('configFields'));
});
Route::post('/admin/pages', function (\Illuminate\Http\Request $request) {
  $page = new Page();
  $page->title = $request->input('page_title');
  $page->slug = Str::slug($request->input('page_title'));
  $page->fields = $request->input('fields');
  $page->save();
  return redirect('/admin/pages/create')->with('success', 'Tạo trang thành công');
});

Dưới đây là một giao diện form nhập dữ liệu cho bảng pages với các fields linh hoạt như: sử dụng khái báo dạng mảng fields

name="fields[button_link]"
  • image_url (chọn ảnh)

  • subtitle

  • content (textarea hoặc CKEditor)

  • button_text

  • button_link

app\Http\Controllers\Admin\PageController.php

public function store(Request $request)
  {
    $page = new Page();
    $page->title = $request->title;
    $page->slug = $request->slug;
    $page->fields = $request->input('fields', []);
    $page->save();
    return redirect()->route('admin.pages.index')->with('success', 'Trang đã được tạo.');
  }

routes\web.php

Route::get('/admin/pages/test', function () {
  $configFields = [
      ['name' => 'title', 'label' => 'Tiêu đề', 'type' => 'text'],
      ['name' => 'image_url', 'label' => 'Ảnh đại diện', 'type' => 'text'],
      ['name' => 'content', 'label' => 'Nội dung', 'type' => 'editor'],
  ];
  return view('admin.pages.test', compact('configFields'));
});
Route::prefix('admin')->name('admin.')->group(function () {
  Route::resource('pages', PageController::class);
});
// LaravelFilemanager
Route::group(['prefix' => 'laravel-filemanager', 'middleware' => ['web', 'auth']], function () {
  Lfm::routes();
});

resources\views\admin\pages\create.blade.php

@extends('layouts.admin')
@section('content')
<div class="container">
  <form action="{{ route('admin.pages.store') }}" method="POST">
    @csrf
    <div class="mb-3">
      <label class="form-label">Tiêu đề trang</label>
      <input type="text" class="form-control" name="title" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Slug</label>
      <input type="text" class="form-control" name="slug" required>
    </div>
    <hr>
    <h4 class="my-3">Fields tùy chỉnh</h4>
    <div class="mb-3">
      <label class="form-label">Ảnh (image_url)</label>
      <div class="input-group">
        <input id="image_url" class="form-control" type="text" name="fields[image_url]">
        <button type="button" class="btn btn-secondary" onclick="openLfm('image_url', 'image_preview')">Chọn ảnh</button>
      </div>
      <img id="image_preview" style="max-height: 100px; margin-top: 10px;">
    </div>
    <div class="mb-3">
      <label class="form-label">Subtitle</label>
      <input type="text" class="form-control" name="fields[subtitle]">
    </div>
    <div class="mb-3">
      <label class="form-label">Nội dung</label>
      <textarea name="fields[content]" class="form-control tinymce" rows="5"></textarea>
    </div>
    <div class="mb-3">
      <label class="form-label">Button Text</label>
      <input type="text" class="form-control" name="fields[button_text]">
    </div>
    <div class="mb-3">
      <label class="form-label">Button Link</label>
      <input type="text" class="form-control" name="fields[button_link]">
    </div>
    <button class="btn btn-primary mt-2" type="submit">Lưu trang</button>
  </form>
</div>
@endsection
@section('scripts')
<script src="https://cdn.ckeditor.com/ckeditor5/41.0.0/classic/ckeditor.js"></script>
  <script>
  function openLfm(inputId, previewId) {
    window.open('/laravel-filemanager?type=image', 'fm', 'width=800,height=600');
    window.SetUrl = function(url) {
      // Nếu url là mảng, lấy phần tử đầu tiên và trường url
      if (Array.isArray(url)) {
        url = url[0]?.url || '';
      }
      if (!url) {
        alert('Không lấy được url ảnh');
        return;
      }
      // Gán url vào input hoặc ảnh hiển thị
       document.getElementById(inputId).value = url;
      document.getElementById(previewId).src = url;
    };
  }
  ClassicEditor.create(document.querySelector('.tinymce')).catch(error => console.error(error));
</script>
@endsection

— Cập nhật hoàn chỉnh index, edit

Sau chỉnh sửa

Thử cập nhật bằng cách thêm một trường test12

Khi quay lại https://lva4.com/admin/pages/5/edit nó báo lỗi

có thể vì nó không phải dụng jsong như này

Chỉnh sửa giao diện create mới

Cập nhật lại code

resources\views\admin\pages\create.blade.php

@extends('layouts.admin')
@section('content')
<div class="container mt-4">
  <form action="{{ route('admin.pages.store') }}" method="POST">
    @csrf
    <div class="mb-3">
      <label class="form-label">Tiêu đề</label>
      <input type="text" name="title" class="form-control" value="{{ old('title') }}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Slug</label>
      <input type="text" name="slug" class="form-control" value="{{ old('slug') }}" required>
    </div>
    <hr>
    <h5>Các trường tùy biến</h5>
    <div id="custom-fields">
      @if(old('fields'))
      @foreach (old('fields') as $key => $field)
      <div class="field-group border p-3 mb-2">
        <div class="row">
          <div class="col-md-5">
            <input type="text" name="fields[{{ $key }}][key]" class="form-control" value="{{ $field['key'] }}" placeholder="Tên trường">
          </div>
          <div class="col-md-5">
            <input type="text" name="fields[{{ $key }}][value]" class="form-control" value="{{ $field['value'] }}" placeholder="Giá trị">
          </div>
          <div class="col-md-2">
            <button type="button" class="btn btn-danger w-100" onclick="this.closest('.field-group').remove()">Xoá</button>
          </div>
        </div>
      </div>
      @endforeach
      @endif
    </div>
    <button type="button" class="btn btn-secondary mb-3" onclick="addField()">+ Thêm trường</button>
    <div>
      <button type="submit" class="btn btn-primary">Tạo trang</button>
      <a href="{{ route('admin.pages.index') }}" class="btn btn-light">Quay lại</a>
    </div>
  </form>
</div>
@endsection
@section('scripts')
<script>
  let fieldCount = {{old('fields') ? count(old('fields')) : 0}};
  function addField() {
    const container = document.getElementById('custom-fields');
    const html = `
          <div class="field-group border p-3 mb-2">
            <div class="row">
              <div class="col-md-5">
                <input type="text" name="fields[${fieldCount}][key]" class="form-control" placeholder="Tên trường">
              </div>
              <div class="col-md-5">
                <input type="text" name="fields[${fieldCount}][value]" class="form-control" placeholder="Giá trị">
              </div>
              <div class="col-md-2">
                <button type="button" class="btn btn-danger w-100" onclick="this.closest('.field-group').remove()">Xoá</button>
              </div>
            </div>
          </div>
        `;
    container.insertAdjacentHTML('beforeend', html);
    fieldCount++;
  }
</script>
@endsection

resources\views\admin\pages\edit.blade.php

@extends('layouts.admin')
@section('content')
<div class="container">
  <h2 class="font-semibold text-xl text-gray-800 leading-tight">
    Sửa trang: {{ $page->title }}
  </h2>
  <div class="container mt-4">
    <form action="{{ route('admin.pages.update', $page) }}" method="POST">
      @csrf
      @method('PUT')
      <div class="mb-3">
        <label class="form-label">Tiêu đề</label>
        <input type="text" name="title" class="form-control" value="{{ old('title', $page->title) }}" required>
      </div>
      <div class="mb-3">
        <label class="form-label">Slug</label>
        <input type="text" name="slug" class="form-control" value="{{ old('slug', $page->slug) }}" required>
      </div>
      <hr>
      <h5>Các trường tùy biến</h5>
      <div id="custom-fields">
        @php $fields = old('fields', $page->fields ?? []); @endphp
        @foreach ($fields as $key => $value)
        <div class="field-group border p-3 mb-2">
          <div class="row">
            <div class="col-md-5">
              <input type="text" name="fields[{{ $key }}][key]" class="form-control" value="{{ $key ?? '' }}" placeholder="Tên trường">
            </div>
            <div class="col-md-5">
              <input type="text" name="fields[{{ $key }}][value]" class="form-control" value="{{ $value ?? '' }}" placeholder="Giá trị">
            </div>
            <div class="col-md-2">
              <button type="button" class="btn btn-danger w-100" onclick="this.closest('.field-group').remove()">Xoá</button>
            </div>
          </div>
        </div>
        @endforeach
      </div>
      <button type="button" class="btn btn-secondary mb-3" onclick="addField()">+ Thêm trường</button>
      <div>
        <button type="submit" class="btn btn-primary">Cập nhật</button>
        <a href="{{ route('admin.pages.index') }}" class="btn btn-light">Quay lại</a>
      </div>
    </form>
  </div>
</div>
@endsection
@section('scripts')
<script>
  let fieldCount = {{count($fields)}};
  function addField() {
    const container = document.getElementById('custom-fields');
    const html = `
          <div class="field-group border p-3 mb-2">
            <div class="row">
              <div class="col-md-5">
                <input type="text" name="fields[${fieldCount}][key]" class="form-control" placeholder="Tên trường">
              </div>
              <div class="col-md-5">
                <input type="text" name="fields[${fieldCount}][value]" class="form-control" placeholder="Giá trị">
              </div>
              <div class="col-md-2">
                <button type="button" class="btn btn-danger w-100" onclick="this.closest('.field-group').remove()">Xoá</button>
              </div>
            </div>
          </div>
        `;
    container.insertAdjacentHTML('beforeend', html);
    fieldCount++;
  }
</script>
@endsection

resources\views\admin\pages\index.blade.php

@extends('layouts.admin')
@section('content')
<div class="container">
  <h2 class="font-semibold text-xl text-gray-800 leading-tight">Danh sách trang</h2>
  <div class="container mt-4">
    <a href="{{ route('admin.pages.create') }}" class="btn btn-success mb-3">Tạo trang mới</a>
    <table class="table table-bordered">
      <thead>
        <tr>
          <th>ID</th>
          <th>Tiêu đề</th>
          <th>Slug</th>
          <th>Thao tác</th>
        </tr>
      </thead>
      <tbody>
        @foreach($pages as $page)
        <tr>
          <td>{{ $page->id }}</td>
          <td>{{ $page->title }}</td>
          <td>{{ $page->slug }}</td>
          <td>
            <a href="{{ route('admin.pages.edit', $page) }}" class="btn btn-sm btn-primary">Sửa</a>
            <form action="{{ route('admin.pages.destroy', $page) }}" method="POST" class="d-inline" onsubmit="return confirm('Xoá trang này?')">
              @csrf
              @method('DELETE')
              <button class="btn btn-sm btn-danger">Xoá</button>
            </form>
          </td>
        </tr>
        @endforeach
      </tbody>
    </table>
  </div>
</div>
@endsection
@section('scripts')
@endsection

database\migrations\2025_06_02_025221_create_pages_table.php

Schema::create('pages', function (Blueprint $table) {
      $table->id();
      $table->string('title');
      $table->string('slug')->unique();
      $table->string('featured_image')->nullable();
      $table->longText('content')->nullable();
      $table->json('meta')->nullable(); // SEO: meta_title, meta_description, etc.
      $table->boolean('is_active')->default(true);
      $table->json('fields')->nullable();
      $table->timestamps();
    });

routes\web.php

Route::get('/admin/pages/test', function () {
  $configFields = [
      ['name' => 'title', 'label' => 'Tiêu đề', 'type' => 'text'],
      ['name' => 'image_url', 'label' => 'Ảnh đại diện', 'type' => 'text'],
      ['name' => 'content', 'label' => 'Nội dung', 'type' => 'editor'],
  ];
  return view('admin.pages.test', compact('configFields'));
});
Route::prefix('admin')->name('admin.')->middleware(['auth'])->group(function () {
  Route::resource('pages', PageController::class);
});
// LaravelFilemanager
Route::group(['prefix' => 'laravel-filemanager', 'middleware' => ['web', 'auth']], function () {
  Lfm::routes();
});

app\Http\Controllers\Admin\PageController.php

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Page;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class PageController extends Controller
{
  /**
   * Display a listing of the resource.
   */
  public function index()
  {
    $pages = Page::latest()->get();
    return view('admin.pages.index', compact('pages'));
  }
  /**
   * Show the form for creating a new resource.
   */
  public function create()
  {
    return view('admin.pages.create');
  }
  /**
   * Store a newly created resource in storage.
   */
  public function store(Request $request)
  {
    $request->validate([
      'title' => 'required|string|max:255',
      'slug' => 'required|string|max:255|unique:pages,slug',
    ]);
    Page::create([
      'title' => $request->title,
      'slug' => $request->slug,
      'fields' => $request->fields ?? [],
    ]);
    return redirect()->route('admin.pages.index')->with('success', 'Trang đã được tạo.');
  }
  /**
   * Display the specified resource.
   */
  public function show(Page $page)
  {
    return view('admin.pages.show', compact('page'));
  }
  /**
   * Show the form for editing the specified resource.
   */
  public function edit(Page $page)
  {
    return view('admin.pages.edit', compact('page'));
  }
  /**
   * Update the specified resource in storage.
   */
  public function update(Request $request, Page $page)
  {
    $request->validate([
      'title' => 'required|string|max:255',
      'slug' => 'required|string|max:255|unique:pages,slug,' . $page->id,
    ]);
    $page->update([
      'title' => $request->title,
      'slug' => $request->slug,
      'fields' => $request->fields ?? [],
    ]);
    return redirect()->route('admin.pages.index')->with('success', 'Cập nhật trang thành công.');
  }
  /**
   * Remove the specified resource from storage.
   */
  public function destroy(Page $page)
  {
    $page->delete();
    return redirect()->route('admin.pages.index')->with('success', 'Đã xoá trang.');
  }
}

app\Models\Page.php

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
  protected $casts = [
    'fields' => 'array',
  ];
  protected $fillable = [
    'title',
    'slug',
    'fields'
  ];
}

Xử lý thêm một bức ảnh để học tạo file riêng js để sử dụng

app\Http\Controllers\Admin\PageController.php

public function store(Request $request)
  {
    $request->validate([
      'title' => 'required|string|max:255',
      'slug' => 'required|string|max:255|unique:pages,slug',
    ]);
    Page::create([
      'title' => $request->title,
      'slug' => $request->slug,
      'image' =>  $request->image,
      'fields' => $request->fields ?? [],
    ]);
    return redirect()->route('admin.pages.index')->with('success', 'Trang đã được tạo.');
  }

app\Models\Page.php

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
  protected $casts = [
    'fields' => 'array',
  ];
  protected $fillable = [
    'title',
    'slug',
    'image',
    'fields'
  ];
}

public\js\lfm.js

function openLfm() {
  window.open('/laravel-filemanager?type=image', 'fm', 'width=800,height=600');
  window.SetUrl = function (url) {
    // Nếu url là mảng, lấy phần tử đầu tiên và trường url
    if (Array.isArray(url)) {
      url = url[0]?.url || '';
    }
    if (!url) {
      alert('Không lấy được url ảnh');
      return;
    }
    // Gán url vào input hoặc ảnh hiển thị
    document.getElementById('image').value = url;
    document.getElementById('image-preview').src = url;
  }
}

Thay vì sử dụng như này openLfm()

<div class="mb-3">
  <label for="image" class="form-label">Ảnh</label>
  <div class="input-group">
    <input id="image" class="form-control" type="text" name="image" value="{{ old('image', $page->image ?? '') }}">
    <button type="button" class="btn btn-secondary" onclick="openLfm()">Chọn ảnh</button>
  </div>
  <img id="image-preview" src="{{ old('image', $page->image ?? '') }}" style="max-height: 150px; margin-top: 10px">
</div>

Tao sẽ sử dụng kiểu động openLfm('image_url', 'image_preview')

<div class="mb-3">
  <label class="form-label">Ảnh (image_url)</label>
  <div class="input-group">
    <input id="image_url" class="form-control" type="text" name="fields[image_url]">
    <button type="button" class="btn btn-secondary" onclick="openLfm('image_url', 'image_preview')">Chọn ảnh</button>
  </div>
  <img id="image_preview" style="max-height: 100px; margin-top: 10px;">
</div>

public\js\lfm.js

function openLfm(inputId, previewId) {
  window.open('/laravel-filemanager?type=image', 'fm', 'width=800,height=600');
  window.SetUrl = function(url) {
    // Nếu url là mảng, lấy phần tử đầu tiên và trường url
    if (Array.isArray(url)) {
      url = url[0]?.url || '';
    }
    if (!url) {
      alert('Không lấy được url ảnh');
      return;
    }
    // Gán url vào input hoặc ảnh hiển thị
     document.getElementById(inputId).value = url;
    document.getElementById(previewId).src = url;
  };
}

resources\views\admin\pages\create.blade.php

@extends('layouts.admin')
@section('content')
<div class="container mt-4">
  <form action="{{ route('admin.pages.store') }}" method="POST">
    @csrf
    <div class="mb-3">
      <label class="form-label">Tiêu đề</label>
      <input type="text" name="title" class="form-control" value="{{ old('title') }}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Slug</label>
      <input type="text" name="slug" class="form-control" value="{{ old('slug') }}" required>
    </div>
    <hr>
    <div class="mb-3">
      <label for="image" class="form-label">Ảnh</label>
      <div class="input-group">
        <input id="image_url" class="form-control" type="text" name="image" value="{{ old('image', $page->image ?? '') }}">
        <button type="button" class="btn btn-secondary" onclick="openLfm('image_url', 'image_preview')">Chọn ảnh</button>
      </div>
      <img id="image-preview" src="{{ old('image', $page->image ?? '') }}" style="max-height: 150px; margin-top: 10px">
    </div>
    <div>
      <button type="submit" class="btn btn-primary">Tạo trang</button>
      <a href="{{ route('admin.pages.index') }}" class="btn btn-light">Quay lại</a>
    </div>
  </form>
</div>
@endsection
@section('scripts')
<script src="{{ asset('js/lfm.js') }}"></script>
@endsection

app\Http\Controllers\Admin\PageController.php

 public function store(Request $request)
  {
    $request->validate([
      'title' => 'required|string|max:255',
      'slug' => 'required|string|max:255|unique:pages,slug',
    ]);
    Page::create([
      'title' => $request->title,
      'slug' => $request->slug,
      'image' =>  $request->image,
      'fields' => $request->fields ?? [],
    ]);
    return redirect()->route('admin.pages.index')->with('success', 'Trang đã được tạo.');
  }

app\Models\Page.php

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
  protected $casts = [
    'fields' => 'array',
  ];
  protected $fillable = [
    'title',
    'slug',
    'image',
    'fields'
  ];
}

routes\web.php

Route::prefix('admin')->name('admin.')->middleware(['auth'])->group(function () {
  Route::resource('pages', PageController::class);
});
// LaravelFilemanager
Route::group(['prefix' => 'laravel-filemanager', 'middleware' => ['web', 'auth']], function () {
  Lfm::routes();
});

Thêm trường nội dung tiếng việt , tiếng anh có thể upload ảnh

app\Http\Controllers\Admin\PageController.php

public function store(Request $request)
  {
    $request->validate([
      'title' => 'required|string|max:255',
      'slug' => 'required|string|max:255|unique:pages,slug',
    ]);
    Page::create([
      'title' => $request->title,
      'slug' => $request->slug,
      'image' =>  $request->image,
      'content' => json_encode([
        'vi' => $request->content_vi,
        'en' => $request->content_en,
      ], true),
      'fields' => $request->fields ?? [],
    ]);
    return redirect()->route('admin.pages.index')->with('success', 'Trang đã được tạo.');
  }

app\Models\Page.php

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
  protected $casts = [
    'fields' => 'array',
  ];
  protected $fillable = [
    'title',
    'slug',
    'image',
    'content',
    'fields'
  ];
}

resources\views\admin\pages\create.blade.php

@extends('layouts.admin')
@section('content')
<div class="container mt-4">
  <form action="{{ route('admin.pages.store') }}" method="POST">
    @csrf
    <div class="mb-3">
      <label class="form-label">Tiêu đề</label>
      <input type="text" name="title" class="form-control" value="{{ old('title') }}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Slug</label>
      <input type="text" name="slug" class="form-control" value="{{ old('slug') }}" required>
    </div>
    <hr>
    <div class="mb-3">
      <label for="image" class="form-label">Ảnh</label>
      <div class="input-group">
        <input id="image_url" class="form-control" type="text" name="image" value="{{ old('image', $page->image ?? '') }}">
        <button type="button" class="btn btn-secondary" onclick="openLfm('image_url', 'image_preview')">Chọn ảnh</button>
      </div>
      <img id="image-preview" src="{{ old('image', $page->image ?? '') }}" style="max-height: 150px; margin-top: 10px">
    </div>
    <div class="mb-3">
      <label for="content_vi" class="form-label">Nội dung (Tiếng Việt)</label>
      <textarea name="content_vi" class="form-control editor" rows="10">{{ old('content_vi', $page->content_vi ?? '') }}</textarea>
    </div>
    <div class="mb-3">
      <label for="content_en" class="form-label">Nội dung (English)</label>
      <textarea name="content_en" class="form-control editor" rows="10">{{ old('content_en', $page->content_en ?? '') }}</textarea>
    </div>
    <div>
      <button type="submit" class="btn btn-primary">Tạo trang</button>
      <a href="{{ route('admin.pages.index') }}" class="btn btn-light">Quay lại</a>
    </div>
  </form>
</div>
@endsection
@section('scripts')
<script src="https://cdn.ckeditor.com/ckeditor5/39.0.1/classic/ckeditor.js"></script>
<script>
  document.querySelectorAll('.editor').forEach(editorEl => {
    ClassicEditor.create(editorEl, {
      ckfinder: {
        uploadUrl: '/laravel-filemanager/upload?type=Images&_token={{ csrf_token() }}'
      }
    }).catch(error => {
      console.error(error);
    });
  });
</script>
<script src="{{ asset('js/lfm.js') }}"></script>
@endsection

Để bạn chèn ảnh từ thư viện (Laravel File Manager) trực tiếp vào nội dung trong CKEditor

Chèn ảnh vào thư viện ở trình soạn thảo

resources\views\admin\pages\create.blade.php

@extends('layouts.admin')
@section('content')
<div class="container mt-4">
  <form action="{{ route('admin.pages.store') }}" method="POST">
    @csrf
    <div class="mb-3">
      <label class="form-label">Tiêu đề</label>
      <input type="text" name="title" class="form-control" value="{{ old('title') }}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Slug</label>
      <input type="text" name="slug" class="form-control" value="{{ old('slug') }}" required>
    </div>
    <hr>
    <div class="mb-3">
      <label for="image" class="form-label">Ảnh</label>
      <div class="input-group">
        <input id="image_url" class="form-control" type="text" name="image" value="{{ old('image', $page->image ?? '') }}">
        <button type="button" class="btn btn-secondary" onclick="openLfm('image_url', 'image_preview')">Chọn ảnh</button>
      </div>
      <img id="image-preview" src="{{ old('image', $page->image ?? '') }}" style="max-height: 150px; margin-top: 10px">
    </div>
    <div class="mb-3">
      <label for="content_vi" class="form-label">Nội dung (Tiếng Việt)</label>
      <textarea name="content_vi" class="form-control editor" rows="10">{{ old('content_vi', $page->content_vi ?? '') }}</textarea>
    </div>
    <div class="mb-3">
      <label for="content_en" class="form-label">Nội dung (English)</label>
      <textarea name="content_en" class="form-control editor" rows="10">{{ old('content_en', $page->content_en ?? '') }}</textarea>
    </div>
    <div>
      <button type="submit" class="btn btn-primary">Tạo trang</button>
      <a href="{{ route('admin.pages.index') }}" class="btn btn-light">Quay lại</a>
    </div>
  </form>
</div>
@endsection
@section('scripts')
<script src="https://cdn.ckeditor.com/ckeditor5/39.0.1/classic/ckeditor.js"></script>
<script>
  const editors = {};
  function insertImageToEditor(editorName, imageUrl) {
    const editor = editors[editorName];
    if (editor) {
      editor.model.change(writer => {
        const imageElement = writer.createElement('imageBlock', {
          src: imageUrl
        });
        editor.model.insertContent(imageElement, editor.model.document.selection);
      });
    }
  }
  function openLfmForEditor(editorName) {
    window.open('/laravel-filemanager?type=Images', 'fm', 'width=800,height=600');
    window.SetUrl = function(items) {
      const url = Array.isArray(items) ? items[0].url : items.url;
      insertImageToEditor(editorName, url);
    };
  }
  document.querySelectorAll('.editor').forEach((textarea, index) => {
    const name = textarea.getAttribute('name') || 'editor_' + index;
    ClassicEditor.create(textarea, {
      toolbar: {
        items: [
          'heading', '|'
          , 'bold', 'italic', 'link', 'bulletedList', 'numberedList', '|'
          , 'undo', 'redo'
        ]
      }
    }).then(editor => {
      editors[name] = editor;
      // Tạo nút chèn ảnh
      const insertBtn = document.createElement('button');
      insertBtn.type = 'button';
      insertBtn.className = 'btn btn-sm btn-secondary mt-2';
      insertBtn.innerText = 'Chèn ảnh từ thư viện';
      insertBtn.addEventListener('click', () => {
        openLfmForEditor(name);
      });
      // Gắn nút sau editor
      textarea.parentNode.appendChild(insertBtn);
    }).catch(error => {
      console.error(error);
    });
    // Thêm nút chèn ảnh ngay sau textarea
    const button = document.createElement('button');
    button.type = 'button';
    button.innerText = 'Chèn ảnh từ thư viện';
    button.className = 'btn btn-sm btn-secondary mt-2';
    button.onclick = () => openLfmForEditor(name);
    textarea.parentNode.insertBefore(button, textarea.nextSibling);
  });
</script>
<script src="{{ asset('js/lfm.js') }}"></script>
@endsection

public\js\lfm.js

function openLfm(inputId, previewId) {
  window.open('/laravel-filemanager?type=image', 'fm', 'width=800,height=600');
  window.SetUrl = function(url) {
    // Nếu url là mảng, lấy phần tử đầu tiên và trường url
    if (Array.isArray(url)) {
      url = url[0]?.url || '';
    }
    if (!url) {
      alert('Không lấy được url ảnh');
      return;
    }
    // Gán url vào input hoặc ảnh hiển thị
     document.getElementById(inputId).value = url;
    document.getElementById(previewId).src = url;
  };
}

app\Models\Page.php

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
  protected $casts = [
    'fields' => 'array',
  ];
  protected $fillable = [
    'title',
    'slug',
    'image',
    'content',
    'fields'
  ];
}

app\Http\Controllers\Admin\PageController.php

public function store(Request $request)
  {
    $request->validate([
      'title' => 'required|string|max:255',
      'slug' => 'required|string|max:255|unique:pages,slug',
    ]);
    Page::create([
      'title' => $request->title,
      'slug' => $request->slug,
      'image' =>  $request->image,
      'content' => json_encode([
        'vi' => $request->content_vi,
        'en' => $request->content_en,
      ], true),
      'fields' => $request->fields ?? [],
    ]);
    return redirect()->route('admin.pages.index')->with('success', 'Trang đã được tạo.');
  }

routes\web.php

<?php
use App\Http\Controllers\Admin\PageController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use UniSharp\LaravelFilemanager\Lfm;
Auth::routes();
Route::prefix('admin')->name('admin.')->middleware(['auth'])->group(function () {
  Route::resource('pages', PageController::class);
});
// LaravelFilemanager
Route::group(['prefix' => 'laravel-filemanager', 'middleware' => ['web', 'auth']], function () {
  Lfm::routes();
});

Sử dụng cho màn chỉnh sửa thêm ảnh, chèn ảnh

resources\views\admin\pages\edit.blade.php

@extends('layouts.admin')
@section('content')
<div class="container">
  <h2 class="font-semibold text-xl text-gray-800 leading-tight">
    Sửa trang: {{ $page->title }}
  </h2>
  <div class="container mt-4">
    <form action="{{ route('admin.pages.update', $page) }}" method="POST">
      @csrf
      @method('PUT')
      <div class="mb-3">
        <label class="form-label">Tiêu đề</label>
        <input type="text" name="title" class="form-control" value="{{ old('title', $page->title) }}" required>
      </div>
      <div class="mb-3">
        <label class="form-label">Slug</label>
        <input type="text" name="slug" class="form-control" value="{{ old('slug', $page->slug) }}" required>
      </div>
      <hr>
      <div class="mb-3">
        <label for="image" class="form-label">Ảnh</label>
        <div class="input-group">
          <input id="image_url" class="form-control" type="text" name="image" value="{{ old('image', $page->image ?? '') }}">
          <button type="button" class="btn btn-secondary" onclick="openLfm('image_url', 'image_preview')">Chọn ảnh</button>
        </div>
        <img id="image-preview" src="{{ old('image', $page->image ?? '') }}" style="max-height: 150px; margin-top: 10px">
      </div>
      <div class="mb-3">
        <label for="content_vi" class="form-label">Nội dung (Tiếng Việt)</label>
        <textarea name="content_vi" class="form-control editor" rows="10">{{ old('content_vi', $page->getTranslation('content', 'vi')) }}</textarea>
      </div>
      <div class="mb-3">
        <label for="content_en" class="form-label">Nội dung (English)</label>
        <textarea name="content_en" class="form-control editor" rows="10">{{ old('content_en', $page->getTranslation('content', 'en')) }}</textarea>
      </div>
      <button type="button" class="btn btn-secondary mb-3" onclick="addField()">+ Thêm trường</button>
      <div>
        <button type="submit" class="btn btn-primary">Cập nhật</button>
        <a href="{{ route('admin.pages.index') }}" class="btn btn-light">Quay lại</a>
      </div>
    </form>
  </div>
</div>
@endsection
@section('scripts')
<script src="https://cdn.ckeditor.com/ckeditor5/39.0.1/classic/ckeditor.js"></script>
<script>
  const editors = {};
  function insertImageToEditor(editorName, imageUrl) {
    const editor = editors[editorName];
    if (editor) {
      editor.model.change(writer => {
        const imageElement = writer.createElement('imageBlock', {
          src: imageUrl
        });
        editor.model.insertContent(imageElement, editor.model.document.selection);
      });
    }
  }
  function openLfmForEditor(editorName) {
    window.open('/laravel-filemanager?type=Images', 'fm', 'width=800,height=600');
    window.SetUrl = function(items) {
      const url = Array.isArray(items) ? items[0].url : items.url;
      insertImageToEditor(editorName, url);
    };
  }
  document.querySelectorAll('.editor').forEach((textarea, index) => {
    const name = textarea.getAttribute('name') || 'editor_' + index;
    ClassicEditor.create(textarea, {
      toolbar: {
        items: [
          'heading', '|'
          , 'bold', 'italic', 'link', 'bulletedList', 'numberedList', '|'
          , 'undo', 'redo'
        ]
      }
    }).then(editor => {
      editors[name] = editor;
      // Tạo nút chèn ảnh
      const insertBtn = document.createElement('button');
      insertBtn.type = 'button';
      insertBtn.className = 'btn btn-sm btn-secondary mt-2';
      insertBtn.innerText = 'Chèn ảnh từ thư viện';
      insertBtn.addEventListener('click', () => {
        openLfmForEditor(name);
      });
      // Gắn nút sau editor
      textarea.parentNode.appendChild(insertBtn);
    }).catch(error => {
      console.error(error);
    });
    // Thêm nút chèn ảnh ngay sau textarea
    const button = document.createElement('button');
    button.type = 'button';
    button.innerText = 'Chèn ảnh từ thư viện';
    button.className = 'btn btn-sm btn-secondary mt-2';
    button.onclick = () => openLfmForEditor(name);
    textarea.parentNode.insertBefore(button, textarea.nextSibling);
  });
</script>
<script src="{{ asset('js/lfm.js') }}"></script>
@endsection

Cập nhật lại bản create và edit để nó lưu vào databse đúng định dạng json en & vi

resources\views\admin\pages\create.blade.php

@extends('layouts.admin')
@section('content')
<div class="container mt-4">
  <form action="{{ route('admin.pages.store') }}" method="POST">
    @csrf
    <div class="mb-3">
      <label class="form-label">Tiêu đề</label>
      <input type="text" name="title" class="form-control" value="{{ old('title') }}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Slug</label>
      <input type="text" name="slug" class="form-control" value="{{ old('slug') }}" required>
    </div>
    <hr>
    <div class="mb-3">
      <label for="image" class="form-label">Ảnh</label>
      <div class="input-group">
        <input id="image_url" class="form-control" type="text" name="image" value="{{ old('image', $page->image ?? '') }}">
        <button type="button" class="btn btn-secondary" onclick="openLfm('image_url', 'image_preview')">Chọn ảnh</button>
      </div>
      <img id="image-preview" src="{{ old('image', $page->image ?? '') }}" style="max-height: 150px; margin-top: 10px">
    </div>
    <div class="mb-3">
      <label for="content_vi" class="form-label">Nội dung (Tiếng Việt)</label>
      <textarea name="content_vi" class="form-control editor" rows="10">{{ old('content_vi', $page->content_vi ?? '') }}</textarea>
    </div>
    <div class="mb-3">
      <label for="content_en" class="form-label">Nội dung (English)</label>
      <textarea name="content_en" class="form-control editor" rows="10">{{ old('content_en', $page->content_en ?? '') }}</textarea>
    </div>
    <div>
      <button type="submit" class="btn btn-primary">Tạo trang</button>
      <a href="{{ route('admin.pages.index') }}" class="btn btn-light">Quay lại</a>
    </div>
  </form>
</div>
@endsection
@section('scripts')
<script src="https://cdn.ckeditor.com/ckeditor5/39.0.1/classic/ckeditor.js"></script>
<script>
  const editors = {};
  function insertImageToEditor(editorName, imageUrl) {
    const editor = editors[editorName];
    if (editor) {
      editor.model.change(writer => {
        const imageElement = writer.createElement('imageBlock', {
          src: imageUrl
        });
        editor.model.insertContent(imageElement, editor.model.document.selection);
      });
    }
  }
  function openLfmForEditor(editorName) {
    window.open('/laravel-filemanager?type=Images', 'fm', 'width=800,height=600');
    window.SetUrl = function(items) {
      const url = Array.isArray(items) ? items[0].url : items.url;
      insertImageToEditor(editorName, url);
    };
  }
  document.querySelectorAll('.editor').forEach((textarea, index) => {
    const name = textarea.getAttribute('name') || 'editor_' + index;
    ClassicEditor.create(textarea, {
      toolbar: {
        items: [
          'heading', '|'
          , 'bold', 'italic', 'link', 'bulletedList', 'numberedList', '|'
          , 'undo', 'redo'
        ]
      }
    }).then(editor => {
      editors[name] = editor;
      // Tạo nút chèn ảnh
      const insertBtn = document.createElement('button');
      insertBtn.type = 'button';
      insertBtn.className = 'btn btn-sm btn-secondary mt-2';
      insertBtn.innerText = 'Chèn ảnh từ thư viện';
      insertBtn.addEventListener('click', () => {
        openLfmForEditor(name);
      });
      // Gắn nút sau editor
      textarea.parentNode.appendChild(insertBtn);
    }).catch(error => {
      console.error(error);
    });
    // Thêm nút chèn ảnh ngay sau textarea
    const button = document.createElement('button');
    button.type = 'button';
    button.innerText = 'Chèn ảnh từ thư viện';
    button.className = 'btn btn-sm btn-secondary mt-2';
    button.onclick = () => openLfmForEditor(name);
    textarea.parentNode.insertBefore(button, textarea.nextSibling);
  });
</script>
<script src="{{ asset('js/lfm.js') }}"></script>
@endsection

resources\views\admin\pages\edit.blade.php

@extends('layouts.admin')
@section('content')
<div class="container">
  <h2 class="font-semibold text-xl text-gray-800 leading-tight">
    Sửa trang: {{ $page->title }}
  </h2>
  <div class="container mt-4">
    <form action="{{ route('admin.pages.update', $page) }}" method="POST">
      @csrf
      @method('PUT')
      <div class="mb-3">
        <label class="form-label">Tiêu đề</label>
        <input type="text" name="title" class="form-control" value="{{ old('title', $page->title) }}" required>
      </div>
      <div class="mb-3">
        <label class="form-label">Slug</label>
        <input type="text" name="slug" class="form-control" value="{{ old('slug', $page->slug) }}" required>
      </div>
      <hr>
      <div class="mb-3">
        <label for="image" class="form-label">Ảnh</label>
        <div class="input-group">
          <input id="image_url" class="form-control" type="text" name="image" value="{{ old('image', $page->image ?? '') }}">
          <button type="button" class="btn btn-secondary" onclick="openLfm('image_url', 'image_preview')">Chọn ảnh</button>
        </div>
        <img id="image-preview" src="{{ old('image', $page->image ?? '') }}" style="max-height: 150px; margin-top: 10px">
      </div>
      <div class="mb-3">
        <label for="content_vi" class="form-label">Nội dung (Tiếng Việt)</label>
        <textarea name="content_vi" class="form-control editor" rows="10">{{ old('content_vi', $page->getTranslation('content', 'vi')) }}</textarea>
      </div>
      <div class="mb-3">
        <label for="content_en" class="form-label">Nội dung (English)</label>
        <textarea name="content_en" class="form-control editor" rows="10">{{ old('content_en', $page->getTranslation('content', 'en')) }}</textarea>
      </div>
      <button type="button" class="btn btn-secondary mb-3" onclick="addField()">+ Thêm trường</button>
      <div>
        <button type="submit" class="btn btn-primary">Cập nhật</button>
        <a href="{{ route('admin.pages.index') }}" class="btn btn-light">Quay lại</a>
      </div>
    </form>
  </div>
</div>
@endsection
@section('scripts')
<script src="https://cdn.ckeditor.com/ckeditor5/39.0.1/classic/ckeditor.js"></script>
<script>
  const editors = {};
  function insertImageToEditor(editorName, imageUrl) {
    const editor = editors[editorName];
    if (editor) {
      editor.model.change(writer => {
        const imageElement = writer.createElement('imageBlock', {
          src: imageUrl
        });
        editor.model.insertContent(imageElement, editor.model.document.selection);
      });
    }
  }
  function openLfmForEditor(editorName) {
    window.open('/laravel-filemanager?type=Images', 'fm', 'width=800,height=600');
    window.SetUrl = function(items) {
      const url = Array.isArray(items) ? items[0].url : items.url;
      insertImageToEditor(editorName, url);
    };
  }
  document.querySelectorAll('.editor').forEach((textarea, index) => {
    const name = textarea.getAttribute('name') || 'editor_' + index;
    ClassicEditor.create(textarea, {
      toolbar: {
        items: [
          'heading', '|'
          , 'bold', 'italic', 'link', 'bulletedList', 'numberedList', '|'
          , 'undo', 'redo'
        ]
      }
    }).then(editor => {
      editors[name] = editor;
      // Tạo nút chèn ảnh
      const insertBtn = document.createElement('button');
      insertBtn.type = 'button';
      insertBtn.className = 'btn btn-sm btn-secondary mt-2';
      insertBtn.innerText = 'Chèn ảnh từ thư viện';
      insertBtn.addEventListener('click', () => {
        openLfmForEditor(name);
      });
      // Gắn nút sau editor
      textarea.parentNode.appendChild(insertBtn);
    }).catch(error => {
      console.error(error);
    });
    // Thêm nút chèn ảnh ngay sau textarea
    const button = document.createElement('button');
    button.type = 'button';
    button.innerText = 'Chèn ảnh từ thư viện';
    button.className = 'btn btn-sm btn-secondary mt-2';
    button.onclick = () => openLfmForEditor(name);
    textarea.parentNode.insertBefore(button, textarea.nextSibling);
  });
</script>
<script src="{{ asset('js/lfm.js') }}"></script>
@endsection

routes\web.php

<?php
use App\Http\Controllers\Admin\PageController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use UniSharp\LaravelFilemanager\Lfm;
Auth::routes();
Route::prefix('admin')->name('admin.')->middleware(['auth'])->group(function () {
  Route::resource('pages', PageController::class);
});
// LaravelFilemanager
Route::group(['prefix' => 'laravel-filemanager', 'middleware' => ['web', 'auth']], function () {
  Lfm::routes();
});

public\js\lfm.js

function openLfm(inputId, previewId) {
  window.open('/laravel-filemanager?type=image', 'fm', 'width=800,height=600');
  window.SetUrl = function(url) {
    // Nếu url là mảng, lấy phần tử đầu tiên và trường url
    if (Array.isArray(url)) {
      url = url[0]?.url || '';
    }
    if (!url) {
      alert('Không lấy được url ảnh');
      return;
    }
    // Gán url vào input hoặc ảnh hiển thị
     document.getElementById(inputId).value = url;
    document.getElementById(previewId).src = url;
  };
}

app\Models\Page.php

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Translatable\HasTranslations;
class Page extends Model
{
  use HasTranslations;
  public $translatable = ['title', 'content'];
  protected $casts = [
    'fields' => 'array',
  ];
  protected $fillable = [
    'title',
    'slug',
    'image',
    'content',
    'fields'
  ];
}

app\Http\Controllers\Admin\PageController.php

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Page;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class PageController extends Controller
{
  /**
   * Display a listing of the resource.
   */
  public function index()
  {
    $pages = Page::latest()->get();
    return view('admin.pages.index', compact('pages'));
  }
  /**
   * Show the form for creating a new resource.
   */
  public function create()
  {
    return view('admin.pages.create');
  }
  /**
   * Store a newly created resource in storage.
   */
  public function store(Request $request)
  {
    $request->validate([
      'title' => 'required|string|max:255',
      'slug' => 'required|string|max:255|unique:pages,slug',
    ]);
    Page::create([
      'title' => $request->title,
      'slug' => $request->slug,
      'image' => $request->image,
      'content' => [
        'vi' => $request->content_vi,
        'en' => $request->content_en,
      ],
      'fields' => is_array($request->fields) ? json_encode($request->fields) : $request->fields,
    ]);
    return redirect()->route('admin.pages.index')->with('success', 'Trang đã được tạo.');
  }
  /**
   * Display the specified resource.
   */
  public function show(Page $page)
  {
    return view('admin.pages.show', compact('page'));
  }
  /**
   * Show the form for editing the specified resource.
   */
  public function edit(Page $page)
  {
    return view('admin.pages.edit', compact('page'));
  }
  /**
   * Update the specified resource in storage.
   */
  public function update(Request $request, Page $page)
  {
    $request->validate([
      'title' => 'required|string|max:255',
      'slug' => 'required|string|max:255|unique:pages,slug,' . $page->id,
    ]);
    $page->update([
      'title' => $request->title,
      'slug' => $request->slug,
      'image' =>  $request->image,
      'content' => [
        'vi' => $request->content_vi,
        'en' => $request->content_en,
      ],
      'fields' => $request->fields ?? [],
    ]);
    return redirect()->route('admin.pages.index')->with('success', 'Cập nhật trang thành công.');
  }
  /**
   * Remove the specified resource from storage.
   */
  public function destroy(Page $page)
  {
    $page->delete();
    return redirect()->route('admin.pages.index')->with('success', 'Đã xoá trang.');
  }
}

database\migrations\2025_06_02_025221_create_pages_table.php

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
  /**
   * Run the migrations.
   */
  public function up(): void
  {
    Schema::create('pages', function (Blueprint $table) {
      $table->id();
      $table->string('title');
      $table->string('slug')->unique();
      $table->string('image')->nullable();
      $table->string('featured_image')->nullable();
      $table->longText('content')->nullable();
      $table->json('meta')->nullable(); // SEO: meta_title, meta_description, etc.
      $table->boolean('is_active')->default(true);
      $table->json('fields')->nullable();
      $table->timestamps();
    });
  }
  /**
   * Reverse the migrations.
   */
  public function down(): void
  {
    Schema::dropIfExists('pages');
  }
};

Last updated

Was this helpful?