Professional searchable page library.
* Clear Live, Draft and Homepage indicators.
* Direct Edit page, Edit content and View page actions.
* Page editor divided into:
* Page details
* SEO and URL
* Publishing
* Automatic, unique URL slugs.
* Publish and expiry scheduling.
* Page templates: Default, Landing and Full Width.
* Homepage replacement handled automatically.
* Current homepage protected from accidental deletion.
* Page duplication includes its complete content structure.
* Content composer now works from the actual page record and page ID.
* Clearer section-based terminology for editors.
* Reusable content remains available as an advanced content library rather than dominating the workflow.
The package contains the eight complete changed files, preserving their project paths. It is not a patch collection.
This commit is contained in:
@@ -11,6 +11,7 @@ use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Content\Models\ContentDocument;
|
||||
use Modules\Content\Models\PageContent;
|
||||
use Modules\Pages\Models\Page;
|
||||
|
||||
final class PageCompositionController extends Controller
|
||||
{
|
||||
@@ -35,6 +36,35 @@ final class PageCompositionController extends Controller
|
||||
return view('content::admin.composition', compact('pageKey', 'placements', 'documents'));
|
||||
}
|
||||
|
||||
|
||||
public function editPage(Page $page): View
|
||||
{
|
||||
$placements = PageContent::query()
|
||||
->with('document.type')
|
||||
->where(fn ($query) => $query->where('page_id', $page->id)->orWhere('page_key', $page->slug))
|
||||
->orderByRaw("CASE region WHEN 'header' THEN 1 WHEN 'body' THEN 2 ELSE 3 END")
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
PageContent::query()
|
||||
->whereNull('page_id')
|
||||
->where('page_key', $page->slug)
|
||||
->update(['page_id' => $page->id]);
|
||||
|
||||
$documents = ContentDocument::query()
|
||||
->with('type')
|
||||
->withCount('placements')
|
||||
->orderBy('title')
|
||||
->get();
|
||||
|
||||
return view('content::admin.composition', [
|
||||
'pageKey' => $page->slug,
|
||||
'page' => $page,
|
||||
'placements' => $placements->groupBy('region'),
|
||||
'documents' => $documents,
|
||||
]);
|
||||
}
|
||||
|
||||
public function preview(string $pageKey): View
|
||||
{
|
||||
abort_unless($pageKey === 'home', 404, 'This phase currently provides the Saasland homepage preview.');
|
||||
@@ -52,8 +82,10 @@ final class PageCompositionController extends Controller
|
||||
$document = ContentDocument::query()->with('type')->findOrFail($validated['content_document_id']);
|
||||
abort_unless(in_array($validated['region'], $document->type?->allowed_regions ?? [], true), 422, 'This content type is not allowed in the selected region.');
|
||||
|
||||
$nextOrder = ((int) PageContent::query()->where('page_key', $pageKey)->where('region', $validated['region'])->max('sort_order')) + 10;
|
||||
$page = Page::query()->where('slug', $pageKey)->first();
|
||||
$nextOrder = ((int) PageContent::query()->where(fn ($query) => $query->where('page_key', $pageKey)->when($page, fn ($q) => $q->orWhere('page_id', $page->id)))->where('region', $validated['region'])->max('sort_order')) + 10;
|
||||
PageContent::query()->create([
|
||||
'page_id' => $page?->id,
|
||||
'page_key' => $pageKey,
|
||||
'content_document_id' => $document->id,
|
||||
'region' => $validated['region'],
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<x-layouts.app :title="'Page composition: '.$pageKey">
|
||||
<x-layouts.app :title="isset($page) ? 'Edit content: '.$page->title : 'Page composition: '.$pageKey">
|
||||
<div class="space-y-6" x-data="compositionEditor()">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 text-sm text-zinc-500"><a href="{{ route('content.index') }}" class="hover:text-zinc-900 dark:hover:text-white">Content documents</a><span>/</span><span>Page composition</span></div>
|
||||
<h1 class="mt-1 text-2xl font-semibold">{{ ucfirst($pageKey) }} page</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">Drag documents to reorder each region. Reusing a document keeps content synchronised across pages.</p>
|
||||
<div class="flex items-center gap-2 text-sm text-zinc-500"><a href="{{ route('administration.pages.index') }}" class="hover:text-zinc-900 dark:hover:text-white">Pages</a><span>/</span><span>Content composer</span></div>
|
||||
<h1 class="mt-1 text-3xl font-semibold tracking-tight">{{ isset($page) ? $page->title : ucfirst($pageKey).' page' }}</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">Add sections, edit their content and drag them into the order visitors should see.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<flux:button :href="route('content.documents.create')">Create document</flux:button>
|
||||
<flux:button variant="primary" :href="route('content.pages.preview', $pageKey)" target="_blank">Open preview</flux:button>
|
||||
@isset($page)<flux:button :href="route('administration.pages.edit', $page)" variant="ghost">Page settings</flux:button>@endisset
|
||||
<flux:button :href="route('content.documents.create')">Create section</flux:button>
|
||||
@if(isset($page) && $page->status === 'published')<flux:button variant="primary" :href="route('pages.public.show', $page->slug)" target="_blank">Open live page</flux:button>@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,21 +70,21 @@
|
||||
|
||||
<aside class="space-y-6 2xl:sticky 2xl:top-6 2xl:self-start">
|
||||
<flux:card class="space-y-4">
|
||||
<div><h2 class="text-lg font-semibold">Add existing document</h2><p class="text-sm text-zinc-500">Reuse published or draft content. Draft documents remain hidden from the public page.</p></div>
|
||||
<div><h2 class="text-lg font-semibold">Add content section</h2><p class="text-sm text-zinc-500">Choose an existing section from the content library. Draft sections remain hidden from visitors.</p></div>
|
||||
<form method="POST" action="{{ route('content.placements.store', $pageKey) }}" class="space-y-4">
|
||||
@csrf
|
||||
<flux:select name="region" label="Region" required>
|
||||
<option value="header">Header</option><option value="body" selected>Body</option><option value="footer">Footer</option>
|
||||
</flux:select>
|
||||
<label class="block text-sm font-medium">Document
|
||||
<label class="block text-sm font-medium">Content section
|
||||
<select name="content_document_id" required class="mt-2 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 dark:border-zinc-700 dark:bg-zinc-950">
|
||||
<option value="">Select a document</option>
|
||||
<option value="">Select a content section</option>
|
||||
@foreach($documents as $document)
|
||||
<option value="{{ $document->id }}" data-regions="{{ implode(',', $document->type?->allowed_regions ?? []) }}">{{ $document->title }} — {{ $document->type?->name }}{{ $document->status === 'draft' ? ' (draft)' : '' }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
<flux:button type="submit" variant="primary" class="w-full">Add to page</flux:button>
|
||||
<flux:button type="submit" variant="primary" class="w-full">Add section</flux:button>
|
||||
</form>
|
||||
</flux:card>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ Route::middleware(['web', 'auth', 'verified'])->prefix('admin/content')->name('c
|
||||
Route::post('/documents/{document}/duplicate', [ContentDocumentController::class, 'duplicate'])->name('documents.duplicate');
|
||||
Route::delete('/documents/{document}', [ContentDocumentController::class, 'destroy'])->name('documents.destroy');
|
||||
|
||||
Route::get('/page/{page}/compose', [PageCompositionController::class, 'editPage'])->name('pages.page.edit');
|
||||
Route::get('/pages/{pageKey}', [PageCompositionController::class, 'edit'])->name('pages.edit');
|
||||
Route::get('/pages/{pageKey}/preview', [PageCompositionController::class, 'preview'])->name('pages.preview');
|
||||
Route::post('/pages/{pageKey}/placements', [PageCompositionController::class, 'store'])->name('placements.store');
|
||||
|
||||
Reference in New Issue
Block a user