The SuluHeadlessBundle provides controllers and services for using the Sulu content management system in a headless way.
To achieve this, the bundle includes a controller that allows to retrieve the content of a Sulu page as plain JSON content. Furthermore, the bundle provides APIs for accessing features that are available via Twig extensions in traditional templates such as navigation contexts and snippet areas. Finally, the bundle includes an optional single page application setup that is built upon React and MobX and utilizes the functionality of the bundle.
The SuluHeadlessBundle is compatible with Sulu starting from version 2.0. Have a look at the require
section in
the composer.json to find an
up-to-date list of the requirements of the bundle.
Please be aware that this bundle is still under development and might not cover every use-case yet.
Depending on the feedback of the community, future versions of the bundle might contain breaking changes.
Execute the following composer command to add the bundle to the dependencies of your project:
composer require sulu/headless-bundle
Enable the bundle by adding it to the list of registered bundles in the config/bundles.php
file of your project:
return [
/* ... */
Sulu\Bundle\HeadlessBundle\SuluHeadlessBundle::class => ['all' => true],
];
Include the routes of the bundle in a new config/routes/sulu_headles_website.yml
file in your project:
sulu_headless:
type: portal
resource: "@SuluHeadlessBundle/Resources/config/routing_website.yml"
To provide an API for retrieving the content of a page in the JSON format, the controller of the page template
must be set to the HeadlessWebsiteController
included in this bundle:
<?xml version="1.0" ?>
<template xmlns="..." xmlns:xsi="..." xsi:schemaLocation="...">
<!-- ... -->
<controller>Sulu\Bundle\HeadlessBundle\Controller\HeadlessWebsiteController::indexAction</controller>
<!-- ... -->
</template>
This controller will provide the content of the page as JSON object if the page is requested in the JSON format
via {pageUrl}.json
.
The main use-case of the SuluHeadlessBundle is delivering the content of a page as a JSON object. This can be
enabled individually per template by setting the controller of the template of the page
to Sulu\Bundle\WebsiteBundle\Controller\DefaultController::indexAction
. When using the HeadlessWebsiteController
as controller for a template, the content of the page is available as JSON object via {pageUrl}.json
.
Additionally to the content of the page, the JSON object returned by the HeadlessWebsiteController
contains meta
information such as the page template and the data of the page excerpt:
{
"id": "a5181a5a-b030-4933-b3b0-e9faf7ec756c",
"type": "page",
"template": "headless-template",
"content": {
"title": "Headless Example Page",
"url": "/headless-example",
"contacts": [
{
"id": 416,
"firstName": "Homer",
"lastName": "Simpson",
"fullName": "Homer Simpson",
"title": "Dr. ",
"position": "Nuclear safety Inspector at the Springfield Nuclear Power Plan"
}
]
},
"view": {
"title": [],
"url": [],
"contacts": []
},
"extension": {
"seo": {
"title": "",
"description": "",
"keywords": "",
"canonicalUrl": "",
"noIndex": false,
"noFollow": false,
"hideInSitemap": false
},
"excerpt": {
"title": "",
"more": "",
"description": "",
"categories": [],
"tags": [],
"icon": [],
"images": []
}
},
"author": "2",
"authored": "2019-12-03T11:01:38+0100",
"changer": 2,
"changed": "2020-01-30T07:47:46+0100",
"creator": 2,
"created": "2019-12-03T11:01:38+0100"
}
If the content of a page that uses the HeadlessWebsiteController
is requested without the .json
suffix, the
controller will render Twig template that is set as view
of the template of the page.
In this case, the data that would have been returned in case of a .json
request is available in the twig
template via a headless
variable.
This behaviour is compatible with the default Sulu WebsiteController
and allows to start a javascript application
that utilizes the functionality of the SuluHeadlessBundle after the initial request of the user.
Internally, Sulu uses ContentType
services that are responsible for persisting page content when a page is modified
and resolving the data that is passed to the Twig template when a page is rendered. Unfortunately, some ContentType
services pass non-scalar values such as media entities to the Twig template. As a JSON object must contain only scalar
values, the SuluHeadlessBundle cannot use the existing ContentType
services for resolving the content of a page.
To solve this problem, the SuluHeadlessBundle introduces ContentTypeResolver
services to resolve the content of
pages to scalar values. The bundle already includes ContentTypeResolver
services for various content types.
If your project includes custom content types or if you are not satisfied with an existing ContentTypeResolver
,
you can register your own ContentTypeResolver
by implementing the ContentTypeResolverInterface
and
adding a sulu_headless.content_type_resolver
tag to the service.
The Sulu content management system comes with various services and Twig extensions to simplify the development and the rendering complex websites. This functionality is not available when serving the content of the website in a headless way, therefore the SuluHeadlessBundle includes controllers to provide JSON APIs for accessing these features.
The APIs are registered as portal URLs and therefore their path is prefixed with the URL of the webspace. If you have configured a language-specific URL for your webspace, the API URL will look something like this:
https://example.org/en/api/...
/api/navigations/{contextKey}
Parameter | Type | Default Value | Description |
---|---|---|---|
depth | integer | 1 |
Maximum depth of the navigation tree that is loaded. |
flat | boolean | false |
Return navigation as flat list instead of tree. |
excerpt | boolean | false |
Include excerpt data in the returned navigation. |
Example: /api/navigations/main?depth=2&flat=false&excerpt=true
/api/search?q={searchTerm}
Parameter | Type | Default Value | Description |
---|---|---|---|
q | string | The text you want to search for. |
Example: /api/search?q=CMS
/api/snippet-areas/{area}
Parameter | Type | Default Value | Description |
---|---|---|---|
includeExtension | boolean | false |
Include extension data (e.g. excerpt) in the returned result. |
Example: /api/snippet-areas/settings?includeExtension=true
The SuluHeadlessBundle is completely frontend independent and does not require the use of a specific technology or
framework. Still, the bundle contains an independent and optional single page application setup in the
Resources/js-website
directory that allows you to quick-start your project and serves as a reference implementation
for utilizing the bundle functionality.
The provided reference implementation builds upon React as rendering library and utilizes MobX for state
management. It is built around a central viewRegistry
singleton that allows you to register React components
as view for specific types of resources (eg. pages of a specific template). The application contains a router that will
intercept the navigation of the browser, load the JSON data for the requested resource and render the respective view
with the loaded data.
To use the provided single page application setup, you need to include the following lines in your Twig template to initialize and start the application:
{% block content %}
{# ... #}
{# define container element for rendering single page application #}
<div id="sulu-headless-container"></div>
{# initialize application with json data of current page to prevent second request on first load #}
<script>window.SULU_HEADLESS_VIEW_DATA = {{ headless|json_encode|raw }};</script>
<script>window.SULU_HEADLESS_API_ENDPOINT = '{{ sulu_content_path('/api') }}';</script>
{# start single page application by including built javascript code #}
<script src="/build/headless/js/index.js"></script>
{% endblock %}
Additionally, you need to add the following files to your project to setup the single page application:
assets/headless/package.json
{
"name": "my-frontend-application",
"main": "src/index.js",
"private": true,
"scripts": {
"build": "webpack src/index.js -o ../../public/build/headless/js/index.js --module-bind js=babel-loader -p --display-modules --sort-modules-by size",
"watch": "webpack src/index.js -w -o ../../public/build/headless/js/index.js --module-bind js=babel-loader --mode=development --devtool source-map"
},
"dependencies": {
"sulu-headless-bundle": "file:../../vendor/sulu/headless-bundle/Resources/js-website",
"core-js": "^3.0.0",
"loglevel": "^1.0.0",
"mobx": "^4.0.0",
"mobx-react": "^5.0.0",
"prop-types": "^15.7.0",
"react": "^16.8.0",
"react-dom": "^16.8.0",
"whatwg-fetch": "^3.0.0",
"history": "^4.10.1"
},
"devDependencies": {
"@babel/core": "^7.6.0",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@babel/preset-react": "^7.0.0",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.8"
}
}
assets/headless/webpack.config.js
const path = require('path');
const nodeModulesPath = path.resolve(__dirname, 'node_modules');
/* eslint-disable-next-line no-unused-vars */
module.exports = (env, argv) => {
return {
resolve: {
modules: [nodeModulesPath, 'node_modules'],
},
resolveLoader: {
modules: [nodeModulesPath, 'node_modules'],
},
};
};
assets/headless/babel.config.js
module.exports = {
presets: ['@babel/env', '@babel/react'],
plugins: [
['@babel/plugin-proposal-decorators', {'legacy': true}],
['@babel/plugin-proposal-class-properties', {'loose': true}]
]
};
assets/headless/src/index.js
import { startApp } from 'sulu-headless-bundle';
import viewRegistry from 'sulu-headless-bundle/src/registries/viewRegistry';
import HeadlessTemplatePage from './views/HeadlessTemplatePage';
// register views for rendering page templates
viewRegistry.add('page', 'headless-template', HeadlessTemplatePage);
// register views for rendering article templates
// viewRegistry.add('article', 'headless-template', HeadlessTemplateArticle);
// start react application in specific DOM element
startApp(document.getElementById('sulu-headless-container'));
assets/headless/src/views/HeadlessTemplatePage.js
import React from 'react';
import { observer } from 'mobx-react';
@observer
class HeadlessTemplatePage extends React.Component {
render() {
const serializedData = JSON.stringify(this.props.data, null, 2);
return (<pre>{ serializedData }</pre>);
}
}
export default HeadlessTemplatePage;
Finally, you can build your frontend application by executing npm install
and npm run build
in the assets/headless
directory.
The Sulu content management system is a community-driven open source project backed by various partner companies. We are committed to a fully transparent development process and highly appreciate any contributions.
In case you have questions, we are happy to welcome you in our official Slack channel. If you found a bug or miss a specific feature, feel free to file a new issue with a respective title and description on the the sulu/SuluHeadlessBundle repository.
The Sulu content management system is released under the under terms of the MIT License.