Abdulkader Safi

The audit found four blockers and one 200 that should have been a 404

I read every task file against the actual code instead of against memory. Four things marked as done were not, and each was quietly blocking a later feature. Then a nested URL turned out to return 200 with the wrong page rendered, which is worse than the 404 I was expecting.

5 min read

Share
The Atelier task breakdown with status markers, next to a terminal showing a nested URL returning the wrong page.

v0.1.2 is out. It exists because I stopped building and read the task files against the code instead of against memory.

The task breakdown had ten feature files with checkboxes. Most of them were written before the code existed and never revisited. So I went through every one, opened the files it described, and marked what was actually true.

Four items were marked or assumed done and were not. Each was blocking something later, which is why they mattered more than their size suggested.

The bug I found on the way

While checking the routing claims I tried a nested slug, /services/web-design, expecting a 404 because nested pages were never built.

It returned 200 with the services page rendered.

The route is /{locale}/{slug?}. Laravel bound locale to services and slug to web-design. The controller saw that services is not a configured locale, reassigned the slug to that first segment, and threw the rest of the path away.

A 404 tells you a page does not exist. A 200 with the wrong content tells a crawler that two URLs are the same page and tells a client their new page is broken in a way they cannot describe. The fix is that a slug is now the whole path after the locale, so services/web-design is one row in the slugs table like any other and needs no parent relationship.

I would not have found it by building features. I found it by trying to confirm a sentence in a document.

Design tokens, because the preview was lying by luck

The whole argument for this project is that the editor preview renders the real page. That holds only if both sides read the same source for colour and spacing.

They did not. They both read hardcoded Tailwind classes, which is not the same thing as sharing a source, it is two copies that happen to match. The first time somebody restyled a client site, the preview would drift.

Tokens are now emitted as CSS custom properties into the head of the layout that both the preview and the public page use. Defaults live in PHP, config overrides them key by key, so changing one colour does not mean restating the rest and an existing install picks them up without republishing anything.

A block stores a reference rather than a value:

"background": { "token": "color.primary" }

The renderer turns that into var(--atelier-color-primary) before the view runs. Which means changing a palette is a config edit rather than a data migration.

Shared controls, and why they emit inline styles

Blocks declared a supports() method from the start. Nothing read it. So every block that wanted a background colour would have grown its own field, which is exactly how a page builder turns into Elementor.

Now a block opts in and gets those controls built once. The decision worth writing down is what they emit: an inline style built from tokens, never a utility class.

A class written in PHP is a class Tailwind never scans. It would compile in my example app, where the class also appears in a Blade file, and vanish on a client site where it does not. That failure is invisible in development and total in production.

Revisions, and a delete confirmation that was telling the truth

Publishing overwrote the published copy with no snapshot. The editor's delete confirmation said the action was not reversible, and it was correct.

Every publish now snapshots the tree that went live, with who published it, pruned to a configurable count. Restoring copies a revision back into the draft, deliberately not into the live page, because restoring is an undo you then look at and publish rather than a silent change to a public site.

There is no UI for browsing them yet. The data is kept, which is the half that cannot be added retroactively.

What the audit changed about the docs

Every task file now carries a dated banner with what is genuinely built, and the index has a "what blocks what" section. Three of the four gaps above were holding up features scheduled after them, and none of that was visible from the checkboxes.

The other thing it produced: a list of work that had no home. CI, a security policy, a changelog and the documentation surface are not features, so no task file covered them, and they went untracked until an external audit named most of them. That is now a section in the foundation file rather than nowhere.

Where it stands

v0.1.2. Design tokens, shared section controls, page revisions, and a nested slug that resolves to the page you asked for.

The release adds a table, so it needs a vendor:publish and a migrate. New tables ship as new migration files rather than edits to one that already ran, which is the only version of this that is safe on somebody else's database.

Last updated 18 Aug 2026 · filed under Laravel, blade, tailwindcss, filamentphp, plugin, tools, web application

Every entry on this project

28 build notes, in order

Including the ones where nothing worked. You are on part 6.

Read the series →

FAQ

Frequently asked questions

Why is a 200 with the wrong page worse than a 404?

A 404 is unambiguous. It tells a visitor the page does not exist, it tells a crawler not to index anything, and it tells the person who built the site exactly what to fix. A 200 serving different content than the URL asked for creates duplicate content across two URLs, gets both indexed, and gives the client a symptom they cannot describe beyond the page looking wrong. It also hides itself from tests that only assert a successful response. The rule that follows is to assert on content rather than on status codes when checking routing, because a status code alone cannot tell you the router matched the segment you meant.

Why store design tokens as references rather than colour values?

Because a stored value is a copy, and copies drift. If a block stores the literal colour, then changing a palette means finding and rewriting every block on every page, which is a data migration with a chance of missing one. Storing a reference like a token key means the block records an intention, and the renderer resolves it to a CSS custom property at display time. Changing the palette becomes a config edit that every page picks up at once. It also makes the editor preview honest, since both the preview and the public page read the same custom properties from the same layout rather than two sets of hardcoded values that happen to match today.

Why can a page builder not emit Tailwind utility classes from PHP?

Tailwind generates CSS by scanning source files for class names that appear literally in the text. A class assembled in PHP at runtime never appears in any file Tailwind reads, so the utility is never generated and the style silently does nothing. It is worse than a plain error because it usually works in the package author's own test app, where the same class also happens to appear in a Blade template, and fails only once installed somewhere it does not. The reliable alternative is emitting an inline style built from CSS custom properties, which needs no build step and no scanning.

How should a package add a database table in an update?

As a new migration file, never as an edit to one that has already run on somebody else's database. Editing an existing migration does nothing for anyone who has already migrated, since the framework records that file as run and skips it, so the change lands only for fresh installs and creates a difference between two databases that claim the same version. A separate file is picked up by publishing migrations again and running migrate, which is safe to repeat and skips what is already there. The upgrade instructions then stay the same three commands for every release, and the changelog only has to say which releases need them.

Written by

Abdulkader Safi

Software Engineer

Lead engineer at DSRPT, from Lebanon and based in Kuwait. I write about the tools and bugs from real client work, with the numbers I measured.

About me → GitHub LinkedIn

Need this kind of work done on your project?

Start a project →

Keep reading

More from Filament Atelier

All 28 entries →
  • A version number changing from 0.5.0 to 1.0.0 beside a list of deferred items.
    Filament Atelier

    · part 28 of 28

    Tagging 1.0.0 with four features missing, on purpose

    The gate list had thirteen items. Six went in, seven did not, and the tag went out anyway. What the number promises is that the API stops moving, not that the feature list is finished, and conflating those two is how packages sit at 0.x for three years.

  • The same block of JavaScript appearing in three different Blade layout files.
    Filament Atelier

    · part 26 of 28

    A script in three layouts is a contract nobody signed

    The editor's preview needed a few lines of JavaScript in the page it renders. I put them in the shipped layout, then copied them into two more. Anyone writing their own layout had to copy them too, and missing them broke half the editor with no error at all.