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

Headless Bundle Laravel Package

sulu/headless-bundle

SuluHeadlessBundle provides controllers and services for using the Sulu CMS in a headless way. It exposes page content as plain JSON and offers APIs for navigation contexts and snippet areas, replacing Twig template features. Compatible with Sulu 2.0 and later.

View on GitHub
Deep Wiki
Context7

Getting Started

Before you composer require anything, understand what this is: sulu/SuluHeadlessBundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/sulu-headless.php, no php artisan vendor:publish, and Laravel's package auto-discovery will not find it. Treat it as two separate concerns:

  1. Sulu side (Symfony app, separate process/repo): the bundle is enabled in config/bundles.php alongside SuluBundle, and it exposes a JSON API over Sulu's content structures (pages, articles, snippets, media, navigation, search, security).
  2. Laravel side: you consume that JSON API over HTTP.

Minimal steps for the Laravel developer:

  • Install Sulu + SuluHeadlessBundle in a standalone Symfony application (this is not something you bolt onto an artisan serve app).
  • Point your browser at the API root (/api by default — confirm against the bundle's routing config) and browse the endpoints interactively. This is the fastest way to learn the response contract.
  • First use case: fetch a single page by path and render it.
// routes/web.php
Route::get('/{path}', [PageController::class, 'show'])->where('path', '.*');

// app/Http/Controllers/PageController.php
public function show(string $path)
{
    $page = Http::baseUrl(config('services.sulu.url'))
        ->withToken(config('services.sulu.token'))
        ->get('/api/pages', ['path' => '/' . $path, 'locale' => app()->getLocale()])
        ->throw()
        ->json();

    return view('page', ['page' => $page]);
}

Where to look first: the bundle's response-shape documentation for Page, Media, Snippet, and Navigation, then your own Sulu template XML files — the API output mirrors the <property> names you define there. That XML is your schema contract.

Implementation Patterns

Wrap the API in a single client class. Don't scatter Http:: calls. One SuluClient with baseUrl, auth headers, retry policy, and typed return values keeps the coupling isolated and makes it swappable in tests.

final class SuluClient
{
    public function __construct(private PendingRequest $http) {}

    public function page(string $path, string $locale): array
    {
        return $this->http->get('/api/pages', ['path' => $path, 'locale' => $locale])
            ->throw()
            ->json();
    }
}

Bind it in a service provider with Http::baseUrl(...)->acceptJson()->retry(3, 200)->timeout(5).

Map responses into DTOs, not arrays. Sulu's payloads are nested and verbose (blocks, _embedded, media formats). Use a small fromArray() constructor per content type, then expose them through Laravel API Resources if you're also serving an API. This is where you absorb schema drift instead of letting it leak into Blade/Inertia components.

Cache aggressively, invalidate deliberately. Headless content endpoints are read-heavy. Use Cache::remember() keyed on path + locale + portal, with a reasonable TTL. If you control the Sulu side, wire a webhook or a scheduled cache-flush on publish so editors don't wait out the TTL.

Preview/draft mode. Sulu exposes a preview mechanism with separate credentials/headers. Keep it behind your own auth middleware — never expose the preview token to the browser.

Localization and portals. Responses are scoped by webspace/portal and locale. Always pass both explicitly; don't infer locale from the path alone, and don't assume a page exists at the same path across locales.

Testing with Http::fake(). Record real Sulu payloads into tests/Fixtures/sulu/*.json and fake against them. This is the single highest-value habit with this integration — it makes schema changes visible in CI rather than in production.

Gotchas and Tips

Composer conflicts are the first wall. The bundle constrains Symfony components; a Laravel application pins symfony/http-foundation, symfony/console, symfony/process, etc. to Laravel's supported ranges. Installing this into an existing Laravel app is likely to fail resolution or force downgrades. Keep Sulu in its own application. If you must share a monorepo, isolate with separate composer.json files.

3.0.2 adds Symfony 8 support — this does nothing for your Laravel app. Laravel's own Symfony constraints still govern what you can install. Don't upgrade on the assumption that "Symfony 8 support" means anything on the Laravel side; it only matters if your Sulu app (or a shared CI matrix) is testing against Symfony 8 components. Verify Laravel compatibility separately if you ever proxy through shared code.

Media response shape grew in 3.0.2. The release pulls in media properties introduced upstream in Sulu 3.0.9. Fields were added, not renamed — but that's enough to break strict snapshot tests and assertSame() on decoded arrays. Two rules: (1) assert on a subset (assertJsonStructure / explicit key checks on the fields you consume), never on whole-payload equality; (2) pin the Sulu version in your fixture-generation docs so you know which shape your fixtures encode. Silently ignoring extra fields is correct here.

Error shapes are Symfony, not Laravel. Failures come back as Symfony HTTP exceptions serialized as application/problem+json, not Laravel's {message, errors} validation envelope. ->throw() gives you RequestException; parse problem+json yourself if you want meaningful messages in logs.

Path resolution is webspace-relative. /about may resolve differently per webspace and portal, and trailing-slash handling is not forgiving. Normalize paths in one place before they hit the client.

Nothing is auto-discovered. No facade, no config publishing, no artisan commands, no migrations. Any "install the package" instructions you find for this bundle are written for Symfony developers — translate them into HTTP calls.

Extension points live on the Symfony side. If the default response shape doesn't fit, the answer is a Sulu serializer/normalizer override or a custom controller in the Symfony app, not a Laravel-side hack. Accept that and keep the Laravel layer thin.

Version the contract. Record the exact Sulu + bundle versions your fixtures came from in a README or fixture header. When 3.0.x moves again, you'll want to know instantly whether a failing test is your bug or upstream's added field.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums