Publishing to Packagist found the paths I never walked
The package could not be installed by anyone, because composer.json sat in a subdirectory. Then v0.1.0 shipped with creating a page from the panel completely broken. Both were paths a new user hits immediately and I never took once, because a seeder did it for me.
5 min read
Atelier worked. Twenty-six tests passing, a full-screen builder, bilingual public pages, images uploading. Then I tried to install it somewhere else and found two things that had been broken the whole time, both on the very first thing a new user does.
Nobody could install it, including me
The repository was laid out as a small monorepo:
Docs/
packages/filament-atelier/ ← composer.json lives here
example/ ← a Laravel app that installs it
The example app installed it through a composer path repository pointing at the package directory, so everything worked locally and had done for two days.
Composer and Packagist read composer.json from the repository root. There was none. So composer require safi/filament-atelier could never have worked, and neither could installing straight from the GitHub URL. The one setup that worked was the one I had been using, which was the only one that pointed at the subdirectory explicitly.
The fix is that the package becomes the repository. Docs/ and example/ stay as siblings and get export-ignored:
/art export-ignore
/example export-ignore
/Docs export-ignore
/tests export-ignore
so composer require pulls 170 KB of package and none of the test app. Verified by checking what a consumer actually downloads:
git archive HEAD | tar -t
Then verified properly, by resolving the package from a clean directory somewhere else on disk, which is the check I should have been running from the start.
The tracked node_modules
Same fix uncovered a smaller thing. .gitignore had:
/vendor
/node_modules
Both anchored to the root with that leading slash, which is precisely what the leading slash means. packages/filament-atelier/node_modules matched neither, so about 500 files had been committed without me noticing.
Dropping the slashes fixes it going forward. The files stay in history, which for a repository about to be public is worth knowing even if it is not worth a rewrite.
v0.1.0 shipped with "New page" broken
The package went up on Packagist. I installed it on a different project. Clicked New page. Got this:
SQLSTATE[HY000]: General error: 1
table atelier_pages has no column named slugs
insert into "atelier_pages" ("title", "slugs", "seo", ...)
Slugs live in their own table, because a JSON map cannot carry a unique index and two pages sharing a slug in one locale is a real bug rather than a hypothetical one. But the settings form edits them as slugs.{locale}, so the form data arrives with a top-level slugs key that is not a column.
The edit screen already handled this. It stripped the key before saving and wrote the slugs afterwards:
protected function mutateFormDataBeforeSave(array $data): array
{
$this->slugsToSave = $data['slugs'] ?? [];
unset($data['slugs']);
return $data;
}
The create action did not. So the key went straight into the insert.
Why a passing test suite did not catch it
This is the part worth keeping.
Every page in the example app was created by the demo seeder, which builds models directly. Every test that needed a page did the same. Not one test, and not one manual click in two days of development, ever created a page through the resource's create action.
The path a new user hits within thirty seconds of installing was the only path nobody had taken. It was invisible precisely because the tooling I built to make development convenient, a seeder that gives you three realistic pages instantly, removed my reason to ever use the real one.
The fix is small. Both screens now share one trait:
trait HandlesPageSlugs
{
protected function pullSlugs(array $data): array
{
$this->slugsToSave = $data['slugs'] ?? [];
unset($data['slugs']);
return $data;
}
protected function applySlugs(Page $page): void
{
// Always call it. With no slugs typed, setSlugs() generates
// them from the title, and a page with no slug is unreachable.
$page->setSlugs($this->slugsToSave ?: array_fill_keys(
array_keys(config('atelier.locales', [])), null,
));
}
}
One place, so a third code path cannot forget. Plus three tests: create with slugs, create without, and edit slugs without duplicating rows. The first of those reproduces the exact error before it fixes it.
Released as v0.1.1 within the hour, with a release note that says plainly that anyone on v0.1.0 should update because creating a page was impossible.
The documentation had the same shape of bug
I had written a wiki page specifically so an AI agent could install the package on a fresh project and verify it. Before assuming it worked, I gave the page to a model with no other context and asked what it would do.
It produced a correct install sequence, the right verification commands, and correctly identified all three silent failure modes. It also answered the first question exactly the way a real agent would have:
First command:
composer require safi/filament-atelierWould it succeed if the package is not on Packagist? No. Does the page address that? No.
At that point the package was not on Packagist. The installation page mentioned the workaround; the agent quickstart did not, and anyone following only the quickstart would have stopped dead on line one.
Same failure as the other two. I knew the workaround, so I never needed the instruction, so I never noticed it was missing.
What I actually take from this
Three bugs, one shape. The thing that makes development comfortable is the thing that stops you walking the path a new user starts on.
A seeder means never using the create form. Knowing the repository layout means never doing a clean install. Knowing a workaround means never reading your own first instruction.
The cheap defences, in the order they would have helped:
- Install from a clean directory somewhere else on disk, before publishing rather than after.
- Have at least one test per resource that creates a record the way the UI does, not the way the seeder does.
- Hand your setup document to someone, or something, that knows nothing, and watch where they stop.
None of those are clever. All three would have caught a bug that shipped.
Last updated 17 Aug 2026 · filed under Laravel, filamentphp, plugin, tools, web application