jeffersongoncalves/laravel-how-it-works
A Laravel package for adding clear "How it works" step-by-step sections to your app. Define and order steps, then render a ready-made explanation block to help users understand your product flow, ideal for onboarding pages, landing sections, and docs.
composer require <vendor>/<package> and publish the migration/config: php artisan vendor:publish --tag="<package>-migrations" (and --tag="<package>-config" if a config file ships). Confirm the tags exist with php artisan vendor:publish listing before assuming names.php artisan migrate. The package ships a table for steps plus a translations table (or a json translations column) backed by spatie/laravel-translatable; check which storage strategy the migration uses before you write queries by hand.use Vendor\Package\Models\Step;
Step::create([
'title' => ['en' => 'Create an account', 'fr' => 'Créer un compte'],
'description' => ['en' => 'Sign up in seconds.', 'fr' => 'Inscrivez-vous en quelques secondes.'],
'is_active' => true,
]);
@foreach (\Vendor\Package\Models\Step::active()->ordered()->get() as $step)
<div class="step">
<h3>{{ $step->title }}</h3> {{-- auto-resolved for current locale --}}
<p>{{ $step->description }}</p>
</div>
@endforeach
Where to look first: the model (fillable/translatable arrays, scopes), the migration (column names for the sort key and active flag), and the config file for fallback locale behaviour. Those three files tell you 90% of what the package actually does.
Seeding content is the primary workflow. These are editorial rows, not user-generated data. Put steps in a seeder so environments stay reproducible, and give each step an explicit sort_order rather than relying on insert order.
Ordering. Use the package's ordered() scope in every read path. If you ever sort in PHP ($steps->sortBy(...)), you will break pagination and defeat the point of the column. Reordering is a bulk update — wrap it in a transaction and, if the sort key is unique, order the writes carefully or defer the unique index.
Locale handling. Because translation resolution is delegated to spatie/laravel-translatable, read properties ($step->title) and let the package return the current locale with fallback. Do not read $step->getTranslation('title', 'en') in views — that hardcodes a locale and bypasses the fallback config. For admin edit forms, keep the raw array ($step->getTranslations('title')) so every locale round-trips on save.
API / Inertia responses. Prefer an API Resource so the front end gets a stable shape:
return StepResource::collection(Step::active()->ordered()->get());
// -> ['id', 'title', 'description', 'sort_order']
Resolve translations server-side (current locale) instead of shipping all locales to the client — it halves payload size and keeps fallback logic in one place.
Admin integration. For Nova/Filament, map each locale to its own field with a ->rules()/translatable wrapper, and expose sort_order as a reorderable column. If the package has no reorder UI, a simple position integer + nova-sortable-style handler is usually less work than building drag-and-drop from scratch.
Caching. This content changes rarely and is read constantly. Cache the resolved, ordered collection per locale (cache()->remember("steps.{$locale}", ...)) and flush on save/delete/reorder via a model observer. Include the locale in the key; include the fallback locale if fallback output differs.
Blade component. Wrap the loop in an anonymous component (<x-how-it-works />) so the query, the active scope, and the ordering live in one place instead of being copy-pasted across pages.
where('title->en', ...) for the common case — the JSON path syntax differs between MySQL, Postgres, and SQLite, and spatie/laravel-translatable provides whereTranslation()/whereTranslatable() for a reason. Raw JSON where-clauses are the #1 source of "works locally, fails on Postgres" bugs here.with('translations') matters and you will get N+1 without it. Check the migration first.null per-locale or assert coverage in a test that fails on missing keys for shipped locales.sort_order collisions. Seeding without explicit orders, or deleting a step and re-adding it, drifts the sequence. Re-normalise on delete, or accept gaps and never assume contiguity (sort_order = 1,2,3 is not guaranteed).getRouteKeyName() will resolve only the current locale's slug. Either keep a non-translated canonical slug or implement resolveRouteBinding() across all locales — otherwise switching language 404s.saved.config('app.fallback_locale') — changing APP_LOCALE mid-project can silently change rendered output in tests. Assert locale explicitly in tests (app()->setLocale('fr')) rather than depending on the ambient config.bound/configurable; if you need extra fields (icon, CTA link, duration), extend the model or add a migration rather than adding unmanaged columns — and re-check that $translatable and $fillable include anything you add, since those arrays are not auto-derived.RefreshDatabase and assert on the resolved string for a set locale, plus one assertion on the raw translation array. That combination catches both "wrong locale shown" and "translation silently not saved" regressions in a single test file.How can I help you explore Laravel packages today?