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()
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
pages
với các fields
linh hoạt như: sử dụng khái báo dạng mảng fieldsname="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?