Back to portfolio
WebappStudy / personal

CMS Sections

Mini-CMS built with Laravel, Inertia, and Vue for creating pages from configurable blocks, drag-and-drop ordering, image upload, and public rendering optimized through caching and lazy loading of sections.

CMS Sections

Core stack

PHP 8.2
Laravel 12
Eloquent ORM
Laravel Breeze
Inertia.js 2Vue 3TypeScriptTailwind CSS 3Vite 6vuedraggableSQLiteLaravel CacheLaravel FilesystemZiggyPHPUnit 11

The challenge

The challenge was designing an editor generic enough that pages could combine very different sections without a separate model and form for each type, while keeping the order, nested editing, image uploads, and predictable public rendering.

The solution

The solution uses a TypeScript registry as the source for the editor's schema, PageSection with JSON content for persisting each block's specific payload, and a section_type → Vue component mapping for rendering. The editor normalizes default values, manages repeaters and images, reorders sections with vuedraggable, and the backend saves the page and the section structure within a transaction. PublicPageController prepares and caches the page, and the frontend loads non-hero sections asynchronously.

Notable complexity

CMS Sections is a personal study project built as a compact page builder on top of Laravel 12, Inertia.js 2, and Vue 3. Instead of a rigid page model, content is composed of ordered sections, each with a type and a JSON payload. A TypeScript registry defines the editing schema for each type, and the same type key is used by the public frontend to select the corresponding Vue component.

The editor allows creating pages with a title and a unique slug, marking a single page as home, adding sections from a catalog, editing content in a modal, and reordering via drag-and-drop. The registry confirms 11 section types: hero, carousel, features, testimonials, team, pricing, FAQ, CTA, statistics, gallery, and contact. Fields can be text, textarea, image, or recursive repeaters, which enables structures such as slides, pricing plans, team members, FAQ questions, and galleries without a separate hardcoded form for each section.

Persistence uses two main entities: Page and PageSection. Page keeps the base metadata, and PageSection links the page to the section type, its order, and the JSON content. On save, updating the page and replacing the set of sections are executed within a transaction, so the page structure does not remain partially updated if an exception occurs. This is a simple approach suited to a study project, with the trade-off that sections are recreated on every save and there is no version history or optimistic locking.

Media is managed through an authenticated upload endpoint. Images are validated as image files of up to 2 MB, receive randomized names, and are saved to Laravel's public disk; the resulting URL is injected into the section's content. This flow is also used in the nested fields within repeaters.

The public frontend resolves the home page or a page by slug and caches the prepared payload for 10 minutes. The cache is explicitly invalidated after editing and can be manually cleared from the administration area. The hero is loaded eagerly, while the other section components use defineAsyncComponent and IntersectionObserver to mount as they approach the viewport. The gallery includes a lightbox and keyboard navigation, the carousel and testimonials have autoplay, and the public components use a responsive layout with Tailwind styles.

Authentication comes mostly from Laravel Breeze: register, login, password reset, email verification, and profile. The custom CMS functionality lives in the Page/PageSection models, the page and upload controllers, the section registry, the Vue editor, the public components, and the cache/rendering logic.

The scope of the repository remains a prototype/study. There is no content versioning, draft/published status on pages, preview separate from the live page, SEO management, editorial policies/roles, or CI/CD pipeline. The form in the Contact section only simulates a submit in the browser, and the map is a placeholder for an unconfigured Maps integration. For this reason the project is presented as a functional, extensible page builder, not as a production-ready CMS.

Key results

  • Configurable catalog with 11 reusable section types for public pages
  • Page editor with add, edit, remove, and drag-and-drop reordering of sections
  • Generic content persistence on PageSection via JSON and explicit order
  • Support for nested/repeater fields used by carousels, pricing, FAQ, team, testimonials, and galleries
  • Authenticated image upload with Laravel validation and storage on the public disk
  • Public routing for the home page and pages accessible via a unique slug
  • Page cache with a 10-minute TTL, invalidation after editing, and a manual clear-cache action
  • Lazy loading of non-hero public components with defineAsyncComponent and IntersectionObserver
  • Interactive components for carousel, testimonials, FAQ, and gallery with lightbox
  • Frontend build with TypeScript typecheck, Vite, and a configured Inertia SSR bundle

Related projects

Want something similar?

Start with the estimate wizard — answer a few questions and get a realistic price range and timeline.