cwsdigital/twill-metadata
A Twill CMS package that adds configurable SEO metadata fields to your modules, letting editors manage page titles, descriptions, and Open Graph tags from the admin UI, plus Blade helpers to render the resulting meta tags in your front end.
cwsdigital/twill-metadata adds a metadata layer (SEO tags, Open Graph, social cards, etc.) to Twill CMS modules without you hand-rolling JSON columns and form fieldsets on every model.
Minimal setup:
composer require cwsdigital/twill-metadataphp artisan vendor:publish --tag=twill-metadata-migrations && php artisan migrateApp\Models\Page).resources/views/admin/pages/form.blade.php).Where to look first: the package's README, the published migration (it tells you the table shape and whether metadata is morph-related per model), and the service provider (look for a repository binding — see the v1.4.1 note on depending on abstractions). Those three files answer 90% of "how do I wire this up" questions.
First use case: a Page module that needs per-page <title>, description, OG image, and canonical URL, editable in the Twill admin and rendered in layouts/app.blade.php. Start there before rolling it across every module — the trait/migration pattern is uniform, so validating it on one model de-risks the rest.
Trait on the model, fieldset in the form. The consistent pattern is: trait on the Eloquent model, Twill form partial in the admin form, accessors in the view. Keep the admin include in a shared partial (admin/partials/metadata) and @include it from each module's form so field naming and translations stay in sync.
Repository-backed persistence. v1.4.1 changed the repository behaviour to depend on abstractions rather than concrete classes. If you're extending or overriding how metadata is saved (custom validation, computed defaults, audit logging), bind against the package's interface in a service provider rather than subclassing a concrete repository — the concrete class is explicitly the unstable surface. Check your AppServiceProvider for any instance()/bind() calls against concrete metadata repository classes and switch them to the interface.
Fill metadata from the model, not the form, for derived values. For things like og:image pulled from a Twill media relation or a title falling back to the model's title, compute in the model/accessor layer and let the admin field be an override. This keeps CLI/imported content from shipping empty tags.
Decorate the output, don't inline it. Wrap rendering in a Blade component or a small presenter (<x-metadata :model="$page" />) so you can swap defaults, add noindex for non-published states, and unit-test the tag output without booting Twill.
Multi-module rollout. Add the trait per model, but consider one shared metadata table (morph) vs per-model columns depending on what the migration ships. If it's morph-based, watch index size and add a whereMorphRelation scope early for admin listings.
Seeding/importing. Set metadata in seeders and imports through the same repository/interface your app uses, not raw DB inserts — the repository layer is where defaults and normalization live.
v1.4.1 forces metadata columns to be nullable — for a reason. This was required for Laravel 11. If you published the migrations on an earlier version, re-publish and diff them; existing installs upgrading to Laravel 11 can hit insert failures on models that don't supply every metadata field. Running php artisan vendor:publish --tag=twill-metadata-migrations --force on an old install will not alter an existing table — write your own ->change() migration to drop the NOT NULL constraints.
Abstractions over concretes is a forward-compatibility signal. Treat any concrete repository class as internal. Overrides should target the interface and be resolved via the container, or the next minor bump can break your app in a way that only shows up at runtime.
Migrations are the upgrade risk, not the PHP code. Both changes in v1.4.1 touch schema and persistence. After upgrading, run the full flow on a fresh DB and on a copy of production data; the nullable change behaves differently in each case.
Twill compatibility matrix. Verify the package against your Twill major version before upgrading either. Metadata fieldsets rely on Twill's form/block APIs, and Twill majors have historically moved those around.
Debugging missing tags. Order of checks: is the trait on the model → does the admin form actually include the fieldset (and is it inside the right locale/translatable block) → is the record's metadata relation/casts populated ($model->metadata in tinker) → is the Blade component reading the right locale. Most "metadata doesn't save" reports are a form include placed outside the translatable wrapper, not a package bug.
Null vs empty string. Post-nullable-change, distinguish "no metadata row" from "row with null fields" in your accessors, and give the presenter explicit fallbacks (?? $model->title) so you never render <meta name="description" content="">.
Caching. If you cache rendered pages, metadata changes made in Twill won't invalidate your cache automatically — hook model saved events into your cache-busting, including on the metadata record itself if it's a separate model.
Extension points that are safe: trait methods on your own model, the Blade component, published migrations, and interface bindings. Extension points that are risky: concrete repository subclasses, published view overrides (they drift across releases — re-diff on upgrade), and relying on the exact column names in your own queries rather than through the model.
How can I help you explore Laravel packages today?