A practical course for our team. Follow the ideas into real schemas, models and editor workflows, then check what
you understood.
16 lessons64 short slides34 questionsEnglish
Work at your own pace. Each lesson includes an exercise; allow extra time to try it in the
project. Progress stays in this browser. No login or submission of answers is needed.
Place a new requirement in the part of the application that owns it.
The concept
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. The project must define what the CMS manages. That lets us match the admin to a project's actual workflows, while making us responsible for maintaining those features.
Before the worked example, two Laravel terms need explaining. A model is the PHP code the application uses to create, find and update one kind of data. For example, a Project model works with Project records:
Illustrative structure or fragment · not a complete implementation
// This class belongs to the app/Models part of our project.
namespace App\Models;
// Import Laravel's base database model.
use Illuminate\Database\Eloquent\Model;
// Give Project model features, such as finding and saving records.
class Project extends Model
{
// These fields may be filled when a Project is created or updated.
protected $fillable = [
'title',
'summary',
'published',
];
}
The names in $fillable do not create database columns. They allow those values to be assigned together when a Project is created or updated. A migration creates or changes the actual database table and its columns:
Migration example · creates the database table
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// Create the projects table and its actual database columns.
Schema::create('projects', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('summary')->nullable();
$table->boolean('published')->default(false);
$table->timestamps();
});
}
public function down(): void
{
// Remove the table if this migration is rolled back.
Schema::dropIfExists('projects');
}
};
LaravelData, requests and application rules
FilamentForms, tables and editor actions
BladePublic HTML and presentation
Three responsibilities, one site
A worked example
A colleague is asking for something like a portfolio page. Each Project is one portfolio entry, such as a website, campaign or case study. Visitors can browse these entries on the public website, while the team uses a private admin screen to add and edit them. In WordPress terms, imagine a Project custom post type with a public archive and editing screens in wp-admin.
Laravel separates this familiar WordPress feature into three parts. A model and migration store the Projects, a Filament resource provides the editor, and a public route with a Blade view displays the portfolio to visitors.
Illustrative structure or fragment · not a complete implementation
Project model + migration → stored data
ProjectResource → editing workflow
Public route + Blade view → visitor experience
What can go wrong
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.
A shared rule, such as which projects are visible, must not exist only in a button callback. An import or another endpoint could bypass it. Put reusable rules in a model scope or service and invoke them where needed.
Try it yourself
Open Pages in the admin and the public Playground in separate tabs. Edit the Playground hero heading, save it, then reload the public tab.
The heading value is stored in the local SQLite database. The migration is the readable code that defines its storage structure:
database/migrations/2026_09_08_000001_create_demo_content.php
defines the
pages.sections
JSON column.
A schema component is PHP configuration that tells Filament which admin input to show and how it maps to saved data.
app/Filament/Resources/Pages/Schemas/PageForm.php
defines
Builder::make('sections')
and the hero block's
TextInput::make('title')
.
resources/views/section.blade.php
reads the saved title and renders it as the public hero heading. Follow these three landmarks to trace the value from storage, through the admin input, to the public page.
What you should be able to show
Trace the heading through the database structure, Filament schema component and public Blade template using the paths above.
Explain why a typography-only change belongs to the presentation/CSS layer and needs no database or Filament schema change.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 4 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Replace familiar WordPress concepts with precise responsibilities.
The concept
Custom post type → Collection. Register a Collection once and list its fields. The CMS provides shared entry storage, admin navigation, forms and validation. Each saved item is an Entry.
Underneath, Laravel still uses a model and database table, and Filament supplies editing components. For custom relational features, the boilerplate also retains php artisan cms:make-content-type Project; it generates separate source files and does not run the migration automatically.
1. Register a Projects collection
In your local CMS, add this definition to filament-demo/collections.php. The teaching project already includes it, so edit that definition rather than adding a duplicate. The boilerplate starts empty.
Open Admin → Collections → Projects and select New Project. Enter a title, slug and client, then save. The CMS already provides shared storage for every collection, so registering a collection or adding fields requires no terminal commands or database setup.
Authors manage their own entries; Contributors can save drafts but cannot publish.
3. Publish and choose the public output
Publish the entry to show it at /projects/your-slug. The list is at /projects. Drafts and private collections return 404. The default template shows the title; add resources/views/collections/projects/show.blade.php to choose which fields visitors see.
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.
Illustrative structure or fragment · not a complete implementation
use Filament\Schemas\Schema;
use Filament\Forms\Components\{TextInput, Builder};
use Filament\Forms\Components\Builder\Block;
// Inside a form-schema class: returns the editor's configuration.
public static function configure(Schema $schema): Schema
{
return $schema->components([
// 'title' names the saved field/state path; it creates no column.
TextInput::make('title')->required(),
Builder::make('sections')->blocks([
Block::make('text')->schema([
TextInput::make('title')->required(),
]),
]),
]);
}
// Separately: migrate title + sections JSON storage and cast sections to array.
This illustrative Pages fragment shows a required title and an ordered list of text blocks. The nested title belongs to a block inside sections. It is separate from the top-level page title. The migration and model must provide the storage and JSON cast; the schema only describes editing.
A worked example
Collection::register defines a collection and its fields once. The shared entry model stores custom fields in JSON, and the admin builds its form from the definition.
Public pages are enabled explicitly with public => true. Only published entries appear. The generic template displays titles; project Blade templates choose which custom fields to expose.
Collection translations, relationships, media uploads and revisions are not implemented yet. Existing Page and Post capabilities remain separate.
Illustrative structure or fragment · not a complete implementation
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.
Do not copy WordPress hooks mechanically into model events. An event is useful for reacting to persistence, but a multi-step workflow with validation and external calls may belong in an explicit service that can be tested directly.
Keep collection and field keys stable: renaming a key does not move existing data. Removing a definition hides its content without deleting entries. Adding a required field does not backfill existing records; it is enforced when an entry is next saved. The admin shows a persistent review notice for saved field keys no longer defined. Changing an entry slug automatically redirects its earlier URLs to the latest slug; renaming collection or field keys still requires an explicit migration.
Try it yourself
Register a team collection in a disposable local CMS, with name information in the built-in title and an optional job_title text field. Create a draft and check that it is absent from the public list.
Publish the entry as an Author or Editor and confirm the public page works. Switch public to false and verify that the list and detail page return 404. These changes belong in your local code, not the temporary public demo.
What you should be able to show
Explain the difference between a Collection definition and an Entry.
Find a custom field in the entry data JSON and in the generated admin form.
Explain why adding an optional field needs no new migration.
Verify draft visibility and Contributor publication restrictions.
Explain what happens to saved data when a field is removed from the definition.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Trace a public request and a save through the application.
The concept
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.
RequestLanguage + slug
Server checksVisibility + allowed data
BladeRender the response
The public request path
A worked example
For a public article, the address selects a language and slug. The query must enforce publication and active-language rules before a cached result can be shown. Blade receives the allowed record and produces HTML. A guessed draft URL must fail even if someone once cached that record.
For a save, begin at the server boundary: identify the user, authorize the operation, validate input, perform the change and return feedback. The browser is helpful for interaction, but its hidden inputs and disabled buttons are not a source of trust.
Illustrative structure or fragment · not a complete implementation
GET /en/blog/example
→ route and middleware
→ language + publication checks
→ query / allowed cached data
→ Blade
→ HTML response
What can go wrong
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.
A 404 does not always mean the template is absent. It can be the intended outcome for an unpublished or wrong-language record. Check the route and query before editing the view.
Try it yourself
Find the public article route in the project. Follow the call into PublicContentCache and locate the checks that can prevent a response.
What you should be able to show
Explain what happens for an unpublished article.
Identify which work repeats on each request and which data may be cached.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Design searchable data and preserve existing records when the schema changes.
The concept
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.
clientsid · name
projectsid · client_id → clients.id · title · language_id
languagesid · code
A client has many projects; each project belongs to one client.
A worked example
Consider Projects that belong to Clients. Store clients separately because they have their own identity and are reused. Put client_id on projects and define both sides of the relationship. A short list of decorative cards used only inside one page can reasonably remain block JSON.
If old Projects have no client, adding a required foreign key immediately may fail or force fabricated values. A safer release can add a nullable column, backfill genuine relationships, then enforce the constraint after checking the data. The exact sequence depends on the database and deployment.
Illustrative structure or fragment · not a complete implementation
// Illustrative migration fragment
$table->foreignId("client_id")->nullable()->constrained();
// Inside Project
public function client(): BelongsTo {
return $this->belongsTo(Client::class);
}
What can go wrong
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.
Choose deletion behavior deliberately. Deleting a Client should not silently erase published Projects unless that is the actual domain rule. Decide between preventing deletion, retaining the association or clearing a nullable reference.
Try it yourself
Sketch Projects, Clients and Languages. Mark primary keys, foreign keys and which relationships are one-to-many.
What you should be able to show
Explain why Clients belong in a table.
Describe a migration path for existing rows.
Decide what should happen when a referenced Client is deleted.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Build an editor workflow without confusing it with business rules.
The concept
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.
A worked example
A Projects resource needs a form for editing, a table for finding records, and resource pages for the workflow. A Publish action may be convenient, but the action still needs authorization and should use the same publication rules as any other entry point.
For relationship-heavy lists, decide which related data the table displays and load it intentionally. Showing a client name on every row can otherwise produce an extra query per row. Test with realistic record counts rather than only three demo rows.
Illustrative structure or fragment · not a complete implementation
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.
A hidden action is not a permission check. A user may invoke the server action directly. Keep authorization at the operation boundary and test it with a user who must be denied.
Try it yourself
Inspect PageResource and its schema/table files. Find the public View page action and compare it with Save changes.
What you should be able to show
Describe what the resource configures.
Identify which action opens a public URL and which action writes data.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Understand filling, reactive state, validation and dehydration.
The concept
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.
FillRecord → component state
Edit + validateUser input → checked values
Dehydrate + saveState → persistent attributes
Form state has a lifecycle. Visibility alone does not decide persistence.
A worked example
Imagine an optional card link. has_link changes which fields appear, but it does not decide automatically whether an old URL should be retained, discarded or written. Make that decision deliberately and verify it after toggling the switch off and on.
A field is filled from a record, edited in component state, validated and then dehydrated into data for persistence. Live interaction and persistence are different stages. A default helps a new form; it is not a migration for existing records.
Illustrative structure or fragment · not a complete implementation
Toggle::make("has_link")->live();
TextInput::make("link_url")
->visible(fn (Get $get) => $get("has_link"))
->required(fn (Get $get) => $get("has_link"));
// Also decide how hidden state should be dehydrated.
What can go wrong
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.
Disabled, hidden and read-only have different purposes. None is authorization. Review server validation and state handling instead of assuming that invisible values cannot be submitted or that a disabled field must always be saved.
Try it yourself
In the playground card editor, add a link, hide it and re-enable it before saving. Then save and reopen. Compare the two state transitions.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Extend a block through schema, sample and public rendering.
The concept
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.
A worked example
A block has a type and data. A Text section and a Quote section need different fields, so Builder is a good fit. A list of FAQ questions has the same question/answer structure repeated, so it belongs in a Repeater inside that block.
Our picker now renders samples from the real block views and current stylesheet. This removes manual screenshot maintenance. The samples still use example content: they do not claim to render an editor’s unsaved values.
Illustrative structure or fragment · not a complete implementation
{
"type": "quote",
"data": {"quote": "A sample", "author": "Team"}
}
Builder → varied blocks
Repeater → repeated items with one schema
What can go wrong
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.
Changing a stored block type or renaming data keys can break existing pages. Add compatible fallbacks or migrate old data. A beautiful new sample is not proof that old records still render.
Try it yourself
Add a Quote block to the playground, move it between existing sections and open its sample preview. Save and compare with View page.
What you should be able to show
Distinguish sample data from your saved quote.
Check that reordering preserves the other sections.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
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.
A worked example
An article editor describes rich content; the server renderer turns supported content into public HTML. Our shared article stylesheet also styles the editing surface, so a heading or coloured section can be checked in context while writing.
For images, save a managed storage path, derive its public URL through the shared helper and keep meaningful alternative text. A filename extension alone is not proof of the actual media type. Upload validation and safe rendering solve different problems.
Illustrative structure or fragment · not a complete implementation
Editor document
→ supported rich-content renderer
→ public HTML
Shared Tailwind article-rules.css
→ editor typography
→ public article typography
What can go wrong
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.
Raw HTML from an editor should not be treated as automatically safe. Keep the approved renderer or sanitizer in the path. Also test highlighted sections: a dark theme can make dark text or inherited colours unreadable on some backgrounds.
Try it yourself
Open the sample article editor. Compare a heading and a coloured section with the public article, then inspect the shared stylesheet.
What you should be able to show
Find the renderer used for publication.
Check the same formatting in the editor and public view.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Separate translated records from translated interface text.
The concept
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.
Estonian recordOwn title, body and publication
Translation groupConnects equivalent versions
English recordOwn title, body and publication
Linked content records keep independent text. UI strings are a separate layer.
A worked example
A page title is translated by editing a language version of that page. The Submit button is a shared interface string. These are different workflows: record translation creates an independently editable draft; String translations changes a reusable label or message.
When a visitor changes language, resolve the matching published version rather than substituting a prefix into any URL. The destination might have a different slug or might not be published. Our forms also resolve the matching language version of an embedded form.
Illustrative structure or fragment · not a complete implementation
Page in Estonian ── translation group ── Page in English
│ │
own title/body own title/body
forms.submit → shared interface string per language
What can go wrong
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.
Linked versions do not synchronize their text. Blank string overrides use file defaults, with English fallback for languages without files. Keep placeholders such as :max when editing messages that need values inserted.
Try it yourself
Open a language version of the playground and inspect Translate. Then open String translations and find forms.submit. Compare what each changes.
What you should be able to show
Explain why a button label does not need a new Page record.
Describe the behavior when the matching content version is a draft.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Follow a submission from visible fields to validated stored data.
The concept
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.
A worked example
The form record owns its field definitions. On submission, the server loads that trusted schema and validates the incoming values. A visitor cannot make a field optional by removing the required attribute from the HTML.
When validation fails, our form shows a summary containing field names, concise explanations beneath fields, and red borders. Ordinary values are restored; file inputs cannot be repopulated by the browser. Error messages use the form’s language and can be edited in String translations.
Illustrative structure or fragment · not a complete implementation
Browser values + challenge
→ published form and language check
→ trusted field schema
→ server validation
→ transaction and private file storage
→ redirect with success or error feedback
What can go wrong
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.
CSRF protection, rate limits, a honeypot and a timed challenge address different risks. None replaces validation or authorization for private attachment downloads. A successful local submission also does not imply that email delivery has been configured.
Try it yourself
Submit the demo form empty, follow an error-summary link, then enter sample values. If you send it successfully, find the stored submission in the admin.
What you should be able to show
Confirm that errors are readable without browser tooltips.
Locate the stored submission under the correct language version.
Explain why a private attachment needs a protected download route.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Distinguish identity, permissions, validation and visibility.
The concept
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.
A worked example
This demo currently admits only the configured demo admin email to its panel. That is a narrow demo policy, not a complete editor/reviewer role system. Adding roles requires a deliberate permission model and checks for relevant operations.
A public attachment URL would bypass the private download controller entirely. Store sensitive files on the private disk and check permissions before streaming them. Similarly, a public query must enforce publication rules even if admin controls already look correct.
Illustrative structure or fragment · not a complete implementation
Authentication: Who is making this request?
Authorization: May this user perform this operation?
Validation: Is this submitted data acceptable?
Visibility: May this content be served publicly?
What can go wrong
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.
CSRF tokens are not authorization tokens. A signed-in user can still be forbidden from an operation. Test direct requests, not only whether the action is visible in the browser.
Try it yourself
Inspect User::canAccessPanel and the private submission download route. List the checks each performs and the checks it does not perform.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
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.
Shared cachePublished model data
Visitor AOwn session + form feedback
Visitor BOwn session + form feedback
Shared content data; separate visitor state.
A worked example
The public cache stores model data, while each response still renders the current session and form challenge. Caching the final HTML would risk sharing another visitor’s token or feedback. Publication checks still happen before using cached data.
Model observers invalidate public content after supported saves. A direct SQL import can bypass those observers. Treat cache invalidation as part of the import workflow and verify both freshly requested and previously visited pages.
Illustrative structure or fragment · not a complete implementation
Request A ─┐
├─ shared published model data
Request B ─┘
Request A → own session, CSRF and form feedback
Request B → own session, CSRF and form feedback
What can go wrong
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.
Caching can conceal an expensive query during a demonstration. Measure cold requests too. When a list uses related models, eager loading may address repeated relationship queries before a broader cache is needed.
Try it yourself
Find PublicContentCache and its observer. Trace a Page save and a Post save. Describe what a bulk import must do differently.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Design retryable background work and distinguish available tools from configured features.
The concept
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.
A worked example
An import that calls an external service can take longer than a user should wait for a request. A queue job can move that work into a worker process, but only if the queue connection and worker are configured. A synchronous queue still runs in the request.
Retries are normal. If a job runs twice, it should not create two copies of the same imported item. Use a stable external identifier, a database constraint and an update-or-create strategy where appropriate. Do not assume a successful HTTP response proves a side effect happened only once.
Illustrative structure or fragment · not a complete implementation
External item ID → unique stored external_id
Fetch / validate → upsert by external_id
Retry → update the same record
Queue dispatch ≠ a running worker
What can go wrong
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.
Logs and failure handling are part of the feature. Record enough context to investigate a failed job without logging credentials or private payloads. After deployment, long-running workers must pick up the new code.
Try it yourself
Write a retry scenario for importing one Project twice. State the expected number of records and how you would verify it.
What you should be able to show
Choose a stable identity for imported data.
List worker, queue and failure-monitoring requirements.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
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.
A worked example
An HTTP test can prove that a draft URL returns 404 and that invalid form data creates no submission. A Livewire test can prove that changing a field type keeps its label. A browser check is still needed to see whether the type modal opens correctly and returns focus.
Use assertions on outcomes. A test that repeats the same implementation logic can pass while the user workflow is broken. Include an edge case that would have caught a plausible regression, such as editing one language after another was cached.
Illustrative structure or fragment · not a complete implementation
Unit → a pure rule
Feature → route, database, permissions
Livewire → form and action state transitions
Browser → focus, layout, interaction
One workflow may need more than one layer.
What can go wrong
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.
Passing tests do not prove every browser layout works. State what was checked and what was not. Keep fixture data independent so tests do not quietly depend on a previous test’s records.
Try it yourself
Choose a feature from this demo and write three expected outcomes: successful use, invalid input, and unauthorized or unpublished access.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Plan a release that preserves data and runs the right code.
The concept
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.
A worked example
A release needs the tested dependency versions, environment configuration, database changes and built or deployed assets. Persistent uploads and the database must survive the release directory being replaced. In this demo, Vite compiles Tailwind assets before deployment; the server serves the resulting static files.
Plan migrations and rollback together. A release that drops a column can make an older code version incompatible with the database. Prefer changes that allow a transition when uninterrupted service matters, and test backup restoration rather than only creating backups.
Illustrative structure or fragment · not a complete implementation
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.
Debug output must not expose production configuration. Check permissions and application configuration as part of the release. Review the version-specific deployment guide rather than copying commands from an unrelated project.
Try it yourself
Write a deployment checklist for this demo, marking which steps require a running service and which affect persistent data.
What you should be able to show
Identify persistent directories and database storage.
Explain how you would verify a restore.
Describe one post-release public and one admin smoke check.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
Combine the course into one reviewable implementation.
The concept
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.
A worked example
Build the smallest complete Projects feature: title, slug, summary, cover, language and publication status. Add a public list and detail view. Reuse the existing image and language patterns, and define how slug uniqueness is scoped.
Demonstrate the whole workflow to a colleague: create a draft, verify it is inaccessible publicly, publish it, edit it, and verify the public result. If you add translated versions, prove that editing one leaves the other unchanged and that missing versions do not produce misleading links.
Illustrative structure or fragment · not a complete implementation
Data contract → migration + model
Editing contract → resource + shared fields
Public contract → route + query + view
Quality contract → permissions + tests + demo
What can go wrong
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.
Do not declare the feature complete after generating the resource. Public visibility, internal links, old data, uploads and cache behavior all belong in the acceptance criteria. Keep queues or integrations out unless they are part of this specific assignment.
Try it yourself
In a separate development branch, implement Projects and prepare a five-minute walkthrough. This exercise is performed in the codebase; the course does not execute or grade submitted code.
What you should be able to show
A draft URL fails and a published URL works.
Invalid slugs and unauthorized writes are rejected.
An image and its alt text survive another edit.
Public output changes after saving.
Explain the model, schema, view and one meaningful test.
The public admin demo is temporary and cannot save changes. Real
editing exercises require an editor account at /admin. Coding exercises belong in a
separate development branch.
Check your understanding
Answer 2 questions. Feedback explains the reasoning, and you can
retry as often as you like.
You have completed the 16 knowledge checks. Finish the Projects assignment and walk a colleague through
your implementation to put the ideas into practice.
This is a self-study record stored in your browser, not a certification or an assessment of your code.