From WordPress to Laravel.
A practical handbook for our team: the mental model, content types, Filament editing, security, deployment and maintenance. Read by chapter and use the knowledge check at the end.
01 / The mental model: three layers
WordPress provides content storage, administration, themes and an extension system as one product. This project separates those responsibilities into three layers. Laravel is the application framework: requests, data, security and processing. Filament is the toolkit for building the admin interface. Blade renders the public website. Our project code turns those tools into a CMS.
Installing Filament does not automatically create pages, articles, a media library or translations. Our Page, Post, Language and ContentForm models define what this system manages. That lets us match the admin to a project's actual workflows, while making us responsible for maintaining those features.
Filament uses Livewire for interactivity. We describe much of the admin in PHP, but JavaScript still runs in the browser. This demo has no separate React application or production Node server. Other Laravel projects can use different frontend architectures.
02 / A WordPress vocabulary for this stack
Custom post type → model, table and resource. A model represents data and relationships; a migration defines database structure; a Filament resource provides the administration screens. This is a useful analogy, not a one-to-one technical replacement.
ACF fields → a Filament schema. TextInput, Select, FileUpload and other components describe the form. A field might map to a database column, part of a JSON document or a relationship. Choose according to how the data needs to be queried and maintained.
ACF Flexible Content → Builder. Our page sections are ordered records containing a block type and its data. A Repeater works well for repeated items with the same structure, such as cards and FAQ entries.
Theme template → Blade view. Views define public HTML; WordPress's template hierarchy does not apply automatically. WP_Query → an Eloquent query. Hooks → events, observers, middleware and services. Which mechanism fits depends on where the behavior belongs in the request or data lifecycle.
Plugin → often a Composer package. A package might provide an integration or Filament component. Installation is only part of the decision: check version compatibility, maintenance, licensing and security.
03 / How a request moves through the application
The browser requests an address. A route maps that address to a handler, middleware performs relevant checks, and a controller or route function retrieves the data. Blade produces HTML and Laravel returns the response. Form submissions use a separate POST route, validation and persistence.
Our public pages and articles use the PublicContentCache service. Publication and language activity are checked before cached data is read. Blade receives the model, renders its sections and creates a fresh response.
In the Filament editor, form state travels to the server through Livewire requests. Visible fields are not the same as trusted server data: someone can submit altered values manually. Rules must run on the server, rather than relying on showing or hiding inputs.
Business logic used by both the admin and another entry point belongs in a shared service. For example, translation-copy rules live in TranslateContent rather than only inside the modal button's callback. This gives imports and future APIs a way to apply the same rules.
04 / Models, migrations and relationships
Start with the data. Which objects have an independent identity? Which values will we search, sort and relate? An article title and publication status fit ordinary columns; a page's variable block structure may fit JSON. When a repeated item needs its own lifecycle or complex queries, consider a separate table.
A migration is a versioned database change. New columns, indexes and relationships travel with the code between environments. Plan defaults and backfills for existing records. Production data should not be reset to a development seed state, and destructive seeders are not a deployment procedure.
Eloquent models define casts and relationships. A language has many pages; a form has many submissions. Foreign keys and unique indexes enforce constraints even when requests arrive concurrently. Here, each translation group can contain only one record per language.
Mass assignment requires deliberate control over accepted attributes. This demo's open guarded configuration assumes services and forms provide an explicit, validated set of fields. Never pass an entire public request directly into model creation.
Avoid N+1 queries: load the relationships needed by a list together, rather than querying again for every row. Paginate large lists and add indexes that match real queries. SQLite suits this demo; switching databases requires checking actual queries and expected workload.
05 / Filament resources and panels
A resource connects a model to its admin interface. Forms describe creation and editing; tables describe lists. Resource pages organize List, Create, Edit and View workflows. A panel brings together navigation, authentication, resources, widgets and visual configuration.
Our PageResource, PostResource and LanguageResource use separate schema and table classes to keep them readable. The resource should not become the home of every business rule. Move complex processing into services and repeated field groups into shared components.
Choose table columns, filters, search and sorting that help the editor make decisions. Bulk actions operate on several records and must account for each record's permissions and possible failures. Relation managers are useful for managing related records alongside their parent.
An action combines a button, an optional modal form and a server-side operation. Translate and Add new form are examples. Prefer native actions to retain validation, keyboard behavior and error handling. Custom JavaScript should address a specific gap rather than replace the whole interaction.
06 / Form schemas and the state lifecycle
A schema defines inputs, layout, visibility, defaults and validation. A record fills the form, the editor changes its state, and saving prepares the data for persistence. Reactive dependencies need carefully chosen state paths, especially inside nested Builders and Repeaters.
Hidden, disabled and non-dehydrated fields can behave differently. Decide whether turning off a condition keeps a previous value or removes it. Our card-link toggle preserves its settings while preventing the public renderer from displaying the link.
When the destination type changes from page to article, clear the previous record ID. If a choice depends on publication, verify that condition when saving and rendering as well. Limiting the options in a dropdown alone is insufficient.
Validation errors must appear at the correct nested field in the active form. For our translation modal, that means the mounted action schema's state path. Before building a custom component, check what the native components already support.
07 / Page blocks and reusable design
The seven page blocks are hero, text, quote, FAQ, cards, related articles and form. Each has an input schema and a public Blade renderer. The block chooser and the floating sample in the block header render layout examples automatically using current templates and styles. They are not previews of unsaved content.
When adding a block, first define the editor's inputs, reasonable limits and empty-state behavior. Then add the Builder schema, public view, styles, preview sample and a meaningful behavior test. Check older records where new keys may not exist.
Use the shared ImageFields and LinkFields groups so the same task behaves consistently. Safely resolving a stored image or link into a public URL also belongs in shared logic.
Reordering sections must not discard uploaded images, conditional values or relationships. Test insertion between blocks as well as at the end. The public site's design is separate from the Filament admin theme; changing one does not automatically redesign the other.
08 / Articles, HTML and images
Rich text is not automatically a safe string. Articles use Filament's RichContentRenderer with supported plugins. Page text blocks are sanitized before output. Ordinary values use Blade escaping; raw HTML output is reserved for deliberately controlled content.
Background colour turns selected content into a block while retaining formatting and paragraphs. Small text changes the text size. The editor and public article share content styles. A new mark or node needs support in both the editor and the server renderer.
Hero, card and cover images have an upload, preview and alternative-text group. New JPEG, PNG, WebP and GIF images are limited to 2 MB. Older trusted SVG samples may remain, but new SVG uploads are not accepted.
A public content image and a form attachment need different access rules. A form attachment does not belong behind a public storage URL. Its download runs through an authorized admin endpoint. The server checks allowed type, size and filename constraints rather than trusting an extension alone.
This demo does not include a dedicated media library, automatic image cropping, responsive image generation or orphan-file cleanup. Add those as explicit workflows if a project needs them.
09 / Languages, translations and URLs
Language is our own content model. The searchable catalogue and flags help editors select a real locale. Pages, articles and forms reference a language and can belong to a translation group. This CMS behavior is our implementation alongside Laravel's localization facilities.
Translate copies saved content into a new draft. Unsaved fields are not included. It does not perform machine translation. Linked versions share a group identifier, not live content: each language has independently editable content and publication.
Page and article slugs must be unique within a language; they can repeat across languages. Copies receive a numeric suffix when needed. Form slugs are internal identifiers and remain globally unique. Public language navigation includes only published equivalents in active languages, while the admin can also link to drafts.
Default-language URLs have no locale prefix; other languages include one. Canonical and hreflang links use actual published equivalents. Changing the default language affects URLs and therefore needs a redirect plan on a deployed site.
Copied internal content links retain the same destination record; they do not automatically find another language's equivalent. Embedded forms do resolve a published linked version matching the page language. Separate SEO-title fields, a sitemap and a general redirect manager are not implemented here.
10 / Public forms, validation and submissions
ContentForm is a reusable form record. A page's Form block stores its ID. Several pages can share a form, but form publication is independent of page publication. Add new form creates a draft from the page editor and preserves the page's unsaved state.
The 17 field types cover text, email, phone, URL, number, date, time, choices, consent, files, headings and help text. Each input has a stable unique key. Editing a label should not silently change the meaning of stored data.
The server uses the saved schema. The browser does not decide which fields are accepted or required. Choice values must belong to the allowed options, required consent must be accepted, and attachments must meet limits. Each file field accepts one PDF, JPEG, PNG or WebP file up to 2 MB.
Submissions are stored locally with field labels and types. The admin detail escapes values. This demo sends neither emails nor webhooks. Agree retention and deletion procedures before real use; automatic retention cleanup is not implemented.
Forms support linked language versions. Translate copies field labels, choices, help and success text to a draft; submissions stay with their original form. Keys and option values are preserved. Publish the translated form to use it on matching-language pages; missing versions are hidden rather than falling back to another language.
CSRF, rate limiting, a honeypot and a timed single-use challenge provide basic protection. They are not a guarantee against all spam. A published form can accept a valid request even if one page that embedded it has been hidden. Unpublish the form itself to close submissions.
11 / Permissions and security boundaries
Authentication asks who the user is. Authorization asks what they may do to a particular record. Panel access, viewing, editing, deleting and custom actions need suitable checks. Hiding a button does not replace server-side authorization.
Laravel provides policies and gates. This demo does not include a detailed role-and-permission management interface. For a project with several roles, write down allowed operations first and test forbidden actions through direct requests.
CSRF ties a web form to a session, validation constrains input, and escaping helps prevent HTML injection. These solve different problems. Private files additionally need the right storage configuration and authorized downloads.
Secrets belong in environment configuration, not the repository, public HTML or logs. Disable debug in production, use HTTPS and give people separate accounts. Multi-tenancy, audit trails and additional authentication need explicit design; adding an admin panel does not automatically implement them.
12 / Caching and performance
This project caches public model data in a dedicated file-backed store, with a default lifetime of ten minutes. It does not cache the complete HTML response, session, CSRF fields or form feedback. Shared content can be reused without sharing users' state.
Model saves and deletes rotate the cache generation. Related-article displays must refresh too. Publication checks run before cache access, so hidden or deleted records do not remain visible until expiration.
A direct SQL update may bypass a model observer. Explicitly clear public content cache after imports or deployments. Check language and route separation: an incorrect cache key can expose the wrong language's content.
Measure queries, page weight and loading time before optimizing. A cache does not fix a missing index or oversized image. Redis, a CDN and other tools are options for specific needs, not automatic features of this demo.
13 / Background jobs and integrations
Laravel provides queues for longer work, a scheduler for recurring tasks, and mail and notifications for messages. They can support imports, periodic synchronization or publication notifications.
This demo does not run those business workflows. An asynchronous queue needs a suitable connection and an operating worker. Scheduled tasks require scheduler execution on the server. Email needs a configured delivery service. A success message in the interface does not prove a background task has completed.
Define timeouts, failure handling, retry limits and idempotency for integrations. Repeating a job must not create duplicate orders or other unwanted results. Keep useful diagnostic logs without exposing personal data or secrets. Critical failures need monitoring that someone can act on.
14 / Testing the behavior that matters
Unit tests suit isolated business rules. Feature tests check HTTP routes, the database and application behavior together. Filament and Livewire tests can fill forms, invoke actions and inspect saved records. Browser tests cover focus, keyboard interaction, modals and visible behavior.
Important scenarios here include preserving blocks during reordering, enforcing upload limits, keeping translations independent, hiding drafts, invalidating caches and separating form sessions. Also test manipulated input, missing permissions and deleted references.
Use a test database and isolated storage. RefreshDatabase must never point at real content. A browser-edited sample does not replace a repeatable automated test, and an automated suite does not prove the mobile layout works well.
Check user outcomes rather than only whether your own method was called. If a custom interaction depends on JavaScript, test that layer. Include accessibility: labels, associated errors, focus restoration and keyboard use.
15 / Development, deployment and maintenance
Composer and the lockfile define PHP dependencies. Use the versions locked for this project and read documentation for the matching major version. This demo uses Laravel 12 and Filament 5. Upgrading means reviewing the upgrade guide, testing and checking custom integrations.
Keep code in version control, environment settings separate and test data apart from live content. Avoid editing vendor files. When overriding a native view, document why: it may require manual work during an upgrade.
The production web root must be the public directory. Database and storage must survive releases. Take a restorable backup before migrations and apply changes in an appropriate order. If workers are used, restart them correctly so they load the new code.
Keep APP_KEY stable: changing it can make encrypted data unreadable. Turn off debug. Check write permissions, logs, HTTPS, cache and server health. A rollback plan must account for database changes as well as code.
Maintenance includes dependency updates, backup-restoration drills, error monitoring, permission reviews and data retention. The framework provides mechanisms; the team provides the operating process.
16 / Your first practical assignment
1. Create a Projects content type. Define a title, slug, summary, cover image and publication status. Add the migration, model and Filament resource. Build a public list and detail view. Reuse the shared image field group.
2. Add the rules. Enforce slug uniqueness, image types and sizes, and public draft exclusion. Decide who can publish or delete a project and add real authorization checks.
3. Add a relationship. Let a project reference articles. Store record IDs, resolve links from current records and hide unavailable targets. Check the query count.
4. Demonstrate the result. Test creation, editing, invalid input, publication and permissions. Check mobile layout and keyboard behavior in a browser. Document new settings and deployment steps.
This exercise combines the fundamentals. One multiple-choice test cannot establish complete competence. Build a small real feature, review the code with a colleague and explain the decisions you made.
Check your understanding
24 questions about development, content models, security and maintenance. Check your answers to see an explanation for each one. Results are not sent to the server or stored.