SuluHeadlessBundle icon indicating copy to clipboard operation
SuluHeadlessBundle copied to clipboard

Add possibility to provide a external preview renderer endpoint

Open alexander-schranz opened this issue 4 years ago • 4 comments

When you are using nextJS or something similar the preview HTML need also be rendered by the nextJS server. This could be implemented in the Headless Controller the following way.

<?php

namespace App\Controller;

use Sulu\Bundle\HeadlessBundle\Controller\HeadlessWebsiteController;
use Sulu\Component\Content\Compat\StructureInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class MyHeadlessController extends extends HeadlessWebsiteController
{
    public function indexAction(
        Request $request,
        StructureInterface $structure,
        bool $preview = false,
        bool $partial = false
    ): Response {
           if ($preview && $this->previewEndpoint) {
               $requestFormat = $request->getRequestFormat(); // do we need to handle request format here as preview currently supports only html?

               $data = $this->resolveStructure($structure);
               $json = $this->serializeData($data);
               $previewEndpointResponse = $this->httpClient->request(
                   'POST',
                   $this->previewEndpoint,
                   [
                       'json' => json_decode($json),
                       'partial' => $partial,
                   ]
               );

               $content = $previewEndpointResponse->getContent();

               // not sure if this part is needed this was provided by an exist implementation:
               $content = preg_replace('/(<body[^>]*>)/', '\1'.Preview::CONTENT_REPLACER, $content);
               $content = preg_replace('/(<\/body>)/', Preview::CONTENT_REPLACER.'\1', $content);

               return new Response(
                   $content,
                   200
               );
           }

          return parent::indexAction($request, $structure, $preview, $partial);
    }
<controller>App\Controller\MyHeadlessController::indexAction</controller>

alexander-schranz avatar Dec 07 '20 10:12 alexander-schranz

In our Slack channel, a developer came up with the following solution in his project. I have not tested this because I do not have the usecase, but maybe this will help somebody else 🙂

  <iframe
    style="position:fixed; top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;"
    id='__NUXT__'
    src="//{{ frontend_url }}{{ content.url }}?preview=true"></iframe>
  <script>
    var frame = document.getElementById('__NUXT__')
    frame.onload = function(){
      frame.contentWindow.postMessage({{ jsonData|raw }}, '*')
    }
  </script>

niklasnatter avatar Dec 11 '20 11:12 niklasnatter

On The frontend side you can add this one to get the content :)

if ((this as any).$route.query.preview) {
      window.addEventListener(
        'message',
        (event) => {
          if (event.data) {
            ;(this as any).$store.dispatch('content/preview', {
              content: event.data.content,
              view: event.data.view,
            })
          }
        },
        false
      )
    }
  },

beardcoder avatar Dec 11 '20 12:12 beardcoder

Full Example can look like the following:

Adding the following to all your templates:

    <view>pages/headless</view>

Then create this template which will include the iframe:

<!doctype html>
<html lang="{{ app.request.locale }}">
<head>
    <title>Preview</title>
    <style>
        body {
            margin: 0;
        }

        #application-frame {
            border: 0;
            position: absolute;
            width: 100%;
            height: 100%;
            top: 0;
            left: 0;
        }
    </style>
</head>
<body>
    {% if app.request.attributes.get('preview') %}
        <iframe id="application-frame" src="http://127.0.0.1:8089/test.html"></iframe>
    {% endif %}

    {% block content %}
        {% if app.request.attributes.get('preview') %}
            <script>
                var frame = document.getElementById('application-frame');

                frame.onload = function() {
                    frame.contentWindow.postMessage({{ headless|json_encode|raw }}, '*'); // TODO * should be replaced by correct origin
                }
            </script>
        {% endif %}
    {% endblock %}
</body>
</html>

The app side could look like this http://127.0.0.1:8089/test.html:

<!doctype html>
<html lang="en">
<head>
    <title>Test</title>
    <style>body { background: red; }</style>
</head>
<body>
    <pre id="content">
        Waiting for data ...
    </pre>

    <script>
        window.addEventListener(
            'message',
            function(event) {
                // TODO this should be checked to avoid unsecure data being send from other origin
                // if (event.origin !== 'https://expected-origin-address') {
                //      return;
                // }
            
                if (event.data) {
                    document.getElementById('content').innerText = JSON.stringify(event.data, null, 4);
                }
            },
            false
        )
    </script>
</body>
</html>

alexander-schranz avatar Feb 15 '21 11:02 alexander-schranz

The solution with the iframe and postMessage feels from my side good. We still would need the possibility to configure an URL (iframe) so there are for me 2 open points for this issue.

1. Where to configure the url and how to handle multi webspace support

Simple configuration:

sulu_headless:
    preview_url: '%env(PREVIEW_ENDPONT)%'

Multi Webspace endpoint:

sulu_headless:
    preview_url: 
         webspace_a: '%env(PREVIEW_WEBSPACE_A_ENDPONT)%'
         webspace_b: '%env(PREVIEW_WEBSPACE_B_ENDPONT)%'

With some symfony config magic this given config can be detected and correctly converted into the array of available webspaces. So we just need a twig extension to get the preview url e.g.:

{% set previewUrl = sulu_headless_preview_url(request.webspace) %}

2. Security for the postMessage

The postMessage has a security included that only postMessage can be receive which targetOrigin does match. As mention in the documentation about postMessage: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage this should always be used to avoid sending data to maybe an external website which should not happen.

alexander-schranz avatar Feb 16 '21 11:02 alexander-schranz