Developed with love by KnpLabs Hire us for your project!
30

SuluHeadlessBundle

by sulu

Bundle that provides controllers and services for using Sulu as headless content management system

SuluHeadlessBundle

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 the managed
navigation contexts and the search functionality of Sulu via AJAX requests. 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.

🚀  Installation and Usage

Install the bundle

Execute the following composer command to add the bundle to the dependencies of your
project:

composer require sulu/headless-bundle

Enable the 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

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"

This will enable a JSON API to access the search functionality of Sulu via {host}/api/search and a JSON API for
retrieving the navigation contexts of the project via {host}/api/navigations/{contextName}.

Set the controller of you template

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.

💡  Key Concepts

Deliver content of pages with the HeadlessWebsiteController

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. This is similar to the
default behavior of Sulu and allows to start a javascript application that utilizes the functionality of the
SuluHeadlessBundle after the initial request of the user.

Be aware that the data that is passed to the Twig template
by the HeadlessWebsiteController contains only scalar values and therefore might differ from the data that would
be passed by the default Sulu WebsiteController.

Resolve content data to scalar values via ContentTypeResolver

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.

Provide popular Sulu functionality via JSON APIs

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.

  • Sulu navigation contexts of the application can be retrieved as JSON object via {host}/api/navigations/{contextName}.
    Similar to the Twig extension, the API respects the following query parameters depth, flat and excerpt.

  • The search functionality of SULU is accessible as JSON API via via {host}/api/search?q={searchTerm}.

Reference single page application implementation

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.

Reference Frontend Implementation

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 = {{ jsonData|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.

❤️  Support and Contributions

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.

📘  License

The Sulu content management system is released under the under terms of the MIT License.

MIT License

Copyright (c) 2020 Sulu GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
  • Add symfony 5 compatibility (#35)
    By web-flow, 1 month ago
  • Update dependencies for symfony 5 (#33)
    By web-flow, 1 month ago
  • Do not throw exception when resolving unexpected data in SingleMediaSelectionResolver (#31)
    By web-flow, 2 months ago
  • Merge pull request #28 from nnatter/enhancement/composer-json-2
    By web-flow, 3 months ago
  • Remove full stop in description in composer.json
    By nnatter, 3 months ago
  • Minor adjustments in README.md (#27)
    By web-flow, 3 months ago
  • Add Sulu bundle seal to README.md (#25)
    By web-flow, 3 months ago
  • Add interface for Serializer classes (#24)
    By web-flow, 3 months ago
  • Merge pull request #23 from alexander-schranz/enhancement/branch-alias
    By web-flow, 4 months ago
  • Add branch alias
    By alexander-schranz, 4 months ago
  • Merge pull request #22 from martinlagler/feature/anchor-hash
    By web-flow, 4 months ago
  • Added hash to router
    By Martin Lagler, 4 months ago
  • Merge pull request #20 from reyostallenberg/indexed-and-tagged-service-collections
    By web-flow, 4 months ago
  • Satisfy PHPStan iterator_to_array argument expectation
    By reyostallenberg, 4 months ago
  • Remove leftover aliases for RewindableGenerator
    By reyostallenberg, 4 months ago
  • Remove usage of internal RewindableGenerator
    By reyostallenberg, 4 months ago
  • Remove compiler passes and use default-index-method in tags for ContentTypeResolver and DataProviderResolver
    By reyostallenberg, 4 months ago
  • Adjust twig template in README.md (#17)
    By web-flow, 5 months ago
  • Restructure README.md file (#12)
    By web-flow, 5 months ago
  • Merge pull request #11 from wachterjohannes/structure-resolver-document-type
    By web-flow, 5 months ago
  • add comments
    By wachterjohannes, 5 months ago
  • use document type to determine the type of the structure
    By wachterjohannes, 5 months ago
  • Merge pull request #7 from alexander-schranz/enhancement/readme-usage
    By web-flow, 5 months ago
  • Update readme
    By web-flow, 5 months ago
  • Add usage to readme
    By alexander-schranz, 5 months ago
  • Allow to set error handler on requester singleton (#5)
    By web-flow, 5 months ago
  • Use handcraftedinthealps/code-coverage-checker package for checking code coverage (#4)
    By web-flow, 5 months ago
  • Prepare publication (#1)
    By alexander-schranz, 6 months ago
  • Merge branch 'feature/preview-template' into 'master'
    By alexander-schranz, 6 months ago
  • Use correct location format for rendering preview template
    By alexander-schranz, 6 months ago