Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Twill Api Laravel Package

area17/twill-api

Twill API adds a RESTful JSON API layer to Twill, Area 17's Laravel CMS. It exposes configurable endpoints for Twill models with authentication, filtering, pagination, and relationships, letting content managed in Twill be consumed by SPAs, mobile apps, or other services.

View on GitHub
Deep Wiki
Context7

Getting Started

area17/twill-api is a thin, read-only JSON:API layer over Twill models. It does not replace Twill's admin — it gives your front end (Next/Nuxt/Vue) a clean, spec-compliant read surface over the content editors already manage.

Install and publish

composer require area17/twill-api
php artisan vendor:publish --provider="Area17\TwillApi\TwillApiServiceProvider"

This drops a config file (typically config/twill-api.php) containing the route prefix, middleware stack, locale handling, and the model → resource map. Confirm what actually got registered:

php artisan route:list | grep api
php artisan vendor:publish --provider="Area17\TwillApi\TwillApiServiceProvider" --tag=config

Expose your first model

  1. Register the model/resource pair in the config (or in the package's resource registry, depending on version).
  2. Set prefix to api/v1 and middleware to ['api'] (add auth:sanctum later if the content isn't public).
  3. Scaffold the JSON:API classes with the package's Twill generator — run php artisan list twill to see the exact twill:make:*api* signature in your installed version. It generates a schema/resource plus a repository under app/JsonApi/<Model>/.
  4. Hit GET /api/v1/pages and confirm the envelope: data[].type, data[].id, data[].attributes, data[].relationships, plus top-level links and meta.

Where to look first

  • config/twill-api.php — prefix, middleware, locale, resource map.
  • The generated app/JsonApi/<Model>/ directory — this is where 90% of your work happens.
  • vendor/area17/twill-api/src — the base repository and base resource/schema classes you will subclass; read them before overriding anything, since the package is small and the base classes are the real documentation.
  • php artisan route:list --path=api — the fastest way to see what endpoints and parameter names the installed version actually supports.

First real use case: a marketing site's page-by-page fetch — GET /api/v1/pages?filter[slug]=about&include=blocks,cover&fields[pages]=title,seo returning one JSON:API document the front end can render without a second round trip.

Implementation Patterns

Pair a schema with a repository. The schema declares the JSON:API contract (attributes, relationships, filters, sortables, pagination, sparse fieldsets); the repository owns the query. Keep query concerns in the repository — eager loads, published scoping, ordering — and keep the schema declarative. When a front end asks for a new filter, the change should be a repository method plus one line of schema registration, not a new controller.

// app/JsonApi/Pages/PageRepository.php
protected function baseQuery()
{
    return parent::baseQuery()
        ->published()                    // Twill's published scope
        ->with(['blocks', 'cover'])      // kill N+1 for common includes
        ->orderBy('publish_start_date', 'desc');
}

Declare only what you support. Add filters and sortables explicitly (filter[slug], filter[tag], sort=-publish_start_date). Undeclared parameters should 400 rather than silently no-op — that's a feature, not a bug, and it keeps your API contract honest as the front end grows.

Translations need a decision, not a default. Twill content is translatable; the API resolves attributes per locale, usually via Accept-Language or a ?locale= query parameter. Pick one (header is more cache-friendly with a Vary: Accept-Language), document the fallback chain, and make sure your CDN cache key includes the resolved locale. Never mix locales in one response document.

Relationships and includes. Model Twill's browsers, media, and block relations as JSON:API relationships and let clients opt in with ?include=blocks.images. Support nested includes (blocks.media) but cap depth — a client asking for include=a.b.c.d on a polymorphic block tree can generate an enormous query. Prefer allowing two levels and documenting the canonical front-end query.

Media and images. Don't leak storage paths. Return role-based URLs from Twill's image crops (cover, thumbnail) in an attribute or a dedicated media resource; if the disk is private, generate signed/temporary URLs at serialization time rather than storing them.

Custom endpoints. For anything outside plain resource reads (search, aggregated navigation, sitemap), add a route in routes/api.php pointing at a controller that extends the package's base controller. You keep the JSON:API error/serialization behavior and only write the query.

Caching is the whole point. A read-only API over CMS content is the easiest caching win in the stack: cache the rendered response keyed on URL + locale + Accept, and bust on save via Twill model events (saved, deleted) using cache tags or a versioned key prefix. Because there are no writes through the API, invalidation is entirely under your control.

Testing. Use Laravel's getJson() against the real routes — assert data.0.type, attribute keys, include resolution, and 4xx behavior for bad filters. These tests double as your API contract for the front end.

Gotchas and Tips

  • Maturity risk. 14 stars and a pre-1.0 posture mean breaking changes and thin docs. Pin the exact version in composer.json, and read the base classes in vendor/ rather than trusting blog posts or the README alone.
  • JSON:API strictness. Clients must send Accept: application/vnd.api+json; a plain Accept: application/json can yield a 406 from the underlying JSON:API server. Test with curl before blaming the front end.
  • published() may not mean "live". Twill supports scheduled publishing (publish_start_date / publish_end_date). Confirm whether your installed version's scope respects dates, or add the window to baseQuery() yourself — otherwise embargoed pages leak.
  • Filter/sort whitelists. Undeclared filters and sorts are rejected by design. When a front end reports a 400, check the schema first, not the URL.
  • Pagination abuse. Set a max page size in the schema/config; ?page[size]=10000 will otherwise happily materialize your entire table with all includes.
  • N+1 through includes. The base repository only loads what you tell it. Any relationship a client can include should either be eager-loaded in baseQuery() or explicitly handled in the schema's relationship resolution.
  • Locale in the cache key. The single most common production bug with this package is serving French attributes to English clients from a shared cache. Include the resolved locale and Vary header explicitly.
  • Media URL rot. Image URLs are built from APP_URL and the storage disk config; changing either silently breaks every cached response. Generate absolute URLs at serialization time and re-warm caches after config changes.
  • CORS. The api middleware group doesn't grant CORS. Configure config/cors.php for the front-end origin, including Accept and Accept-Language in allowed_headers, and credentials only for authenticated endpoints.
  • Writes are out of scope. Adding POST/PATCH means implementing JSON:API write semantics, validation documents, and authorization yourself — at which point you're fighting the package's read-only design. Keep writes in Twill or a purpose-built controller.
  • Debugging includes. Error responses from the JSON:API layer usually name the offending parameter in source.parameter or detail; log the full error document in dev. If a relationship include returns empty, the inverse isn't declared — check both sides of the schema, not just the query.
  • Extension points. Base repository/resource classes are intended for subclassing, but they change between versions. Override narrowly (a method, not a fork) and keep a note of every override so upgrades are a diff, not an archaeology project.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource