Integrate article and page management into the Laravel admin dashboard
to serve as a headless CMS for the Next.js frontend (usher-site).
Backend:
- 7 migrations: article_categories, article_tags, articles, pivots, attachments, pages
- 5 models with relationships: Article, ArticleCategory, ArticleTag, ArticleAttachment, Page
- 4 admin controllers: articles (with publish/archive/pin), categories, tags, pages
- Admin views with EasyMDE markdown editor, multi-select categories/tags
- Navigation section "官網管理" in admin sidebar
API (v1):
- GET /api/v1/articles (filtered by type, category, tag, search; paginated)
- GET /api/v1/articles/{slug} (with related articles)
- GET /api/v1/categories
- GET /api/v1/pages/{slug} (with children)
- GET /api/v1/homepage (aggregated homepage data)
- Attachment download endpoint
- CORS configured for usher.org.tw, vercel.app, localhost:3000
Content migration:
- ImportHugoContent command: imports Hugo markdown files as articles/pages
- Successfully imported 27 articles, 17 categories, 11 tags, 9 pages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
8.3 KiB
PHP
132 lines
8.3 KiB
PHP
<x-app-layout>
|
||
<x-slot name="header">
|
||
<div class="flex items-center justify-between">
|
||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||
建立頁面
|
||
</h2>
|
||
<a href="{{ route('admin.pages.index') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||
← 返回列表
|
||
</a>
|
||
</div>
|
||
</x-slot>
|
||
|
||
<div class="py-12">
|
||
<div class="mx-auto max-w-4xl sm:px-6 lg:px-8">
|
||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||
<form method="POST" action="{{ route('admin.pages.store') }}" class="space-y-6 p-6">
|
||
@csrf
|
||
|
||
<!-- Title -->
|
||
<div>
|
||
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">標題 <span class="text-red-500">*</span></label>
|
||
<input type="text" name="title" id="title" value="{{ old('title') }}" required
|
||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||
placeholder="輸入頁面標題">
|
||
@error('title')
|
||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||
@enderror
|
||
</div>
|
||
|
||
<!-- Slug -->
|
||
<div>
|
||
<label for="slug" class="block text-sm font-medium text-gray-700 dark:text-gray-300">網址代碼(選填,自動產生)</label>
|
||
<input type="text" name="slug" id="slug" value="{{ old('slug') }}"
|
||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||
placeholder="about-us">
|
||
@error('slug')
|
||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||
@enderror
|
||
</div>
|
||
|
||
<!-- Parent Page -->
|
||
<div>
|
||
<label for="parent_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">上層頁面(選填)</label>
|
||
<select name="parent_id" id="parent_id"
|
||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||
<option value="">無(頂層頁面)</option>
|
||
@foreach($parentPages as $parentPage)
|
||
<option value="{{ $parentPage->id }}" {{ old('parent_id') == $parentPage->id ? 'selected' : '' }}>{{ $parentPage->title }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Content (Markdown Editor) -->
|
||
<div>
|
||
<label for="content" class="block text-sm font-medium text-gray-700 dark:text-gray-300">內容 <span class="text-red-500">*</span></label>
|
||
<textarea name="content" id="content" rows="15" required>{{ old('content') }}</textarea>
|
||
@error('content')
|
||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||
@enderror
|
||
</div>
|
||
|
||
<!-- Template -->
|
||
<div>
|
||
<label for="template" class="block text-sm font-medium text-gray-700 dark:text-gray-300">模板(選填)</label>
|
||
<input type="text" name="template" id="template" value="{{ old('template') }}"
|
||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||
placeholder="例如:homepage, about, contact">
|
||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">用於前端顯示不同版面配置</p>
|
||
</div>
|
||
|
||
<!-- Sort Order -->
|
||
<div>
|
||
<label for="sort_order" class="block text-sm font-medium text-gray-700 dark:text-gray-300">排序</label>
|
||
<input type="number" name="sort_order" id="sort_order" value="{{ old('sort_order', 0) }}" min="0"
|
||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||
</div>
|
||
|
||
<!-- Meta Description -->
|
||
<div>
|
||
<label for="meta_description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">SEO 描述(選填)</label>
|
||
<textarea name="meta_description" id="meta_description" rows="2"
|
||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||
placeholder="搜尋引擎描述">{{ old('meta_description') }}</textarea>
|
||
</div>
|
||
|
||
<!-- Action Buttons -->
|
||
<div class="flex items-center justify-end space-x-3 border-t border-gray-200 dark:border-gray-700 pt-6">
|
||
<a href="{{ route('admin.pages.index') }}" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||
取消
|
||
</a>
|
||
<button type="submit" name="status" value="draft" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||
儲存為草稿
|
||
</button>
|
||
<button type="submit" name="status" value="published" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
|
||
發布頁面
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@push('styles')
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
|
||
@endpush
|
||
|
||
@push('scripts')
|
||
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
new EasyMDE({
|
||
element: document.getElementById('content'),
|
||
spellChecker: false,
|
||
autosave: {
|
||
enabled: true,
|
||
uniqueId: 'page-create',
|
||
delay: 10000,
|
||
},
|
||
placeholder: '使用 Markdown 撰寫頁面內容...',
|
||
toolbar: [
|
||
'bold', 'italic', 'heading', '|',
|
||
'quote', 'unordered-list', 'ordered-list', '|',
|
||
'link', 'image', 'table', '|',
|
||
'preview', 'side-by-side', 'fullscreen', '|',
|
||
'guide'
|
||
],
|
||
});
|
||
});
|
||
</script>
|
||
@endpush
|
||
</x-app-layout>
|