* 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.
88 lines
11 KiB
PHP
88 lines
11 KiB
PHP
<x-layouts.app :title="$record->exists ? $record->title : __('Create page')">
|
|
<div class="mx-auto max-w-7xl space-y-6" x-data="pageEditor()">
|
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
<div>
|
|
<a href="{{ route('administration.pages.index') }}" class="text-sm text-zinc-500 hover:text-zinc-900 dark:hover:text-white">← Pages</a>
|
|
<div class="mt-2 flex flex-wrap items-center gap-3">
|
|
<h1 class="text-3xl font-semibold tracking-tight">{{ $record->exists ? $record->title : 'New page' }}</h1>
|
|
@if($record->exists && $record->is_home)<span class="rounded-full bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700 dark:bg-blue-950 dark:text-blue-200">Homepage</span>@endif
|
|
</div>
|
|
<p class="mt-2 text-sm text-zinc-500">Page details, search metadata, publishing and content structure.</p>
|
|
</div>
|
|
@if($record->exists)
|
|
<div class="flex flex-wrap gap-2">
|
|
@if($record->status === 'published')<flux:button :href="route('pages.public.show', $record->slug)" target="_blank" variant="ghost">Open live page</flux:button>@endif
|
|
<flux:button :href="route('content.pages.page.edit', $record)">Edit content</flux:button>
|
|
<form method="POST" action="{{ route('administration.pages.duplicate', $record) }}">@csrf<flux:button type="submit" variant="ghost">Duplicate</flux:button></form>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if(session('status'))<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">{{ session('status') }}</div>@endif
|
|
@if($errors->any())<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800 dark:border-red-900 dark:bg-red-950 dark:text-red-200"><ul class="list-disc pl-5">@foreach($errors->all() as $error)<li>{{ $error }}</li>@endforeach</ul></div>@endif
|
|
|
|
<div class="border-b border-zinc-200 dark:border-zinc-800">
|
|
<nav class="flex gap-6 overflow-x-auto" aria-label="Page editor sections">
|
|
<button type="button" @click="tab='content'" :class="tab==='content' ? 'border-zinc-900 text-zinc-900 dark:border-white dark:text-white' : 'border-transparent text-zinc-500'" class="border-b-2 px-1 py-3 text-sm font-medium">Page details</button>
|
|
<button type="button" @click="tab='seo'" :class="tab==='seo' ? 'border-zinc-900 text-zinc-900 dark:border-white dark:text-white' : 'border-transparent text-zinc-500'" class="border-b-2 px-1 py-3 text-sm font-medium">SEO & URL</button>
|
|
<button type="button" @click="tab='publishing'" :class="tab==='publishing' ? 'border-zinc-900 text-zinc-900 dark:border-white dark:text-white' : 'border-transparent text-zinc-500'" class="border-b-2 px-1 py-3 text-sm font-medium">Publishing</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<form method="POST" action="{{ $record->exists ? route('administration.pages.update', $record) : route('administration.pages.store') }}" class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_22rem]" id="page-form">
|
|
@csrf @if($record->exists) @method('PUT') @endif
|
|
<div>
|
|
<section x-show="tab==='content'" class="space-y-6">
|
|
<flux:card class="space-y-5">
|
|
<div><h2 class="text-lg font-semibold">Page identity</h2><p class="mt-1 text-sm text-zinc-500">Use a clear title editors and visitors can recognise.</p></div>
|
|
<flux:input name="title" label="Page title" value="{{ old('title', $record->title) }}" required x-on:input="suggestSlug($event.target.value)" />
|
|
<flux:textarea name="summary" label="Summary" rows="4" description="Used in page listings, search results and social previews when supported.">{{ old('summary', $record->summary) }}</flux:textarea>
|
|
</flux:card>
|
|
|
|
<flux:card class="space-y-5">
|
|
<div class="flex items-start justify-between gap-4"><div><h2 class="text-lg font-semibold">Content structure</h2><p class="mt-1 text-sm text-zinc-500">Build the page with ordered, reusable sections in the visual composer.</p></div>@if($record->exists)<span class="rounded-full bg-zinc-100 px-3 py-1 text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">{{ $record->content_placements_count ?? 0 }} sections</span>@endif</div>
|
|
@if($record->exists)
|
|
<div class="rounded-xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-950"><h3 class="font-medium">Page composer</h3><p class="mt-1 text-sm text-zinc-500">Add hero areas, text, feeds, calls to action and shared site sections, then drag them into order.</p><flux:button class="mt-4" :href="route('content.pages.page.edit', $record)" variant="primary">Open content composer</flux:button></div>
|
|
@else
|
|
<div class="rounded-xl border-2 border-dashed border-zinc-300 p-6 text-sm text-zinc-500 dark:border-zinc-700">Save the page first, then add and arrange its content sections.</div>
|
|
@endif
|
|
<details class="rounded-xl border border-zinc-200 p-4 dark:border-zinc-800"><summary class="cursor-pointer text-sm font-medium">Legacy body content</summary><p class="mt-2 text-xs text-zinc-500">Kept for compatibility with older templates. New pages should use the content composer.</p><textarea name="content" rows="10" class="mt-4 w-full rounded-xl border border-zinc-300 bg-white p-4 text-sm dark:border-zinc-700 dark:bg-zinc-950">{{ old('content', $record->content) }}</textarea></details>
|
|
</flux:card>
|
|
</section>
|
|
|
|
<section x-show="tab==='seo'" x-cloak class="space-y-6">
|
|
<flux:card class="space-y-5">
|
|
<div><h2 class="text-lg font-semibold">URL and presentation</h2><p class="mt-1 text-sm text-zinc-500">Keep URLs short, readable and stable.</p></div>
|
|
<div><label class="mb-2 block text-sm font-medium">Page URL</label><div class="flex rounded-xl border border-zinc-300 bg-white focus-within:ring-2 focus-within:ring-zinc-400 dark:border-zinc-700 dark:bg-zinc-950"><span class="border-r border-zinc-200 px-3 py-2 text-sm text-zinc-500 dark:border-zinc-800">/</span><input name="slug" x-model="slug" value="{{ old('slug', $record->slug) }}" class="min-w-0 flex-1 border-0 bg-transparent px-3 py-2 text-sm focus:ring-0" placeholder="about-us"></div></div>
|
|
<flux:select name="template" label="Page template"><option value="default" @selected(old('template', $record->template ?: 'default') === 'default')>Default</option><option value="landing" @selected(old('template', $record->template) === 'landing')>Landing page</option><option value="full-width" @selected(old('template', $record->template) === 'full-width')>Full width</option></flux:select>
|
|
</flux:card>
|
|
<flux:card><h2 class="text-lg font-semibold">Search preview</h2><div class="mt-4 rounded-xl border border-zinc-200 p-5 dark:border-zinc-800"><div class="text-xs text-emerald-700">{{ url('/') }}/<span x-text="slug || 'page-url'"></span></div><div class="mt-1 text-xl text-blue-700">{{ old('title', $record->title ?: 'Page title') }}</div><p class="mt-1 text-sm text-zinc-600 dark:text-zinc-300">{{ Str::limit(old('summary', $record->summary ?: 'Add a concise summary to improve how this page appears in search and content listings.'), 160) }}</p></div></flux:card>
|
|
</section>
|
|
|
|
<section x-show="tab==='publishing'" x-cloak class="space-y-6">
|
|
<flux:card class="space-y-5"><div><h2 class="text-lg font-semibold">Publishing schedule</h2><p class="mt-1 text-sm text-zinc-500">Publish immediately or define a controlled availability window.</p></div><flux:select name="status" label="Status"><option value="draft" @selected(old('status', $record->status ?: 'draft') === 'draft')>Draft</option><option value="published" @selected(old('status', $record->status) === 'published')>Published</option></flux:select><div class="grid gap-4 md:grid-cols-2"><flux:input type="datetime-local" name="published_at" label="Publish from" value="{{ old('published_at', $record->published_at?->format('Y-m-d\TH:i')) }}" /><flux:input type="datetime-local" name="expires_at" label="Unpublish after" value="{{ old('expires_at', $record->expires_at?->format('Y-m-d\TH:i')) }}" /></div><label class="flex items-start gap-3 rounded-xl border border-zinc-200 p-4 dark:border-zinc-800"><input type="checkbox" name="is_home" value="1" class="mt-1" @checked(old('is_home', $record->is_home))><span><strong class="block text-sm">Use as site homepage</strong><span class="mt-1 block text-xs text-zinc-500">This automatically replaces the previous homepage.</span></span></label></flux:card>
|
|
</section>
|
|
</div>
|
|
|
|
<aside class="space-y-5 xl:sticky xl:top-6 xl:self-start">
|
|
<flux:card class="space-y-4"><div class="flex items-center justify-between"><h2 class="font-semibold">Page status</h2><span class="rounded-full px-2.5 py-1 text-xs {{ $record->status === 'published' ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-200' : 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300' }}">{{ ucfirst($record->status ?: 'draft') }}</span></div>@if($record->exists)<dl class="space-y-3 text-sm"><div class="flex justify-between gap-4"><dt class="text-zinc-500">Sections</dt><dd>{{ $record->content_placements_count ?? 0 }}</dd></div><div class="flex justify-between gap-4"><dt class="text-zinc-500">Updated</dt><dd>{{ $record->updated_at?->diffForHumans() }}</dd></div><div class="flex justify-between gap-4"><dt class="text-zinc-500">Template</dt><dd>{{ ucfirst($record->template ?? 'default') }}</dd></div></dl>@endif<flux:button type="submit" variant="primary" class="w-full">{{ $record->exists ? 'Save page' : 'Create page' }}</flux:button></flux:card>
|
|
@if($record->exists && !$record->is_home)<flux:card><h2 class="font-semibold">Danger zone</h2><p class="mt-2 text-sm text-zinc-500">Deleting a page also removes its page placements, but reusable content documents remain available.</p><form method="POST" action="{{ route('administration.pages.destroy', $record) }}" class="mt-4" onsubmit="return confirm('Move this page to the bin?')">@csrf @method('DELETE')<flux:button type="submit" variant="danger" class="w-full">Delete page</flux:button></form></flux:card>@endif
|
|
</aside>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
function pageEditor() {
|
|
return {
|
|
tab: 'content',
|
|
slug: @js(old('slug', $record->slug)),
|
|
slugTouched: Boolean(@js(old('slug', $record->slug))),
|
|
suggestSlug(title) {
|
|
if (this.slugTouched || @js($record->exists)) return;
|
|
this.slug = title.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</x-layouts.app>
|