Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add possibility to provide a external preview renderer endpoint #65

Open
alexander-schranz opened this issue Dec 7, 2020 · 4 comments
Labels
Feature New functionality not yet included in the Bundle

Comments

@alexander-schranz
Copy link
Member

alexander-schranz commented Dec 7, 2020

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 alexander-schranz added the Feature New functionality not yet included in the Bundle label Dec 7, 2020
@alexander-schranz alexander-schranz changed the title Add possibility to provide a preview renderer endpoint Add possibility to provide a external preview renderer endpoint Dec 7, 2020
@niklasnatter
Copy link
Contributor

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>

@beardcoder
Copy link

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
      )
    }
  },

@alexander-schranz
Copy link
Member Author

alexander-schranz commented Feb 15, 2021

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
Copy link
Member Author

alexander-schranz commented Feb 16, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature New functionality not yet included in the Bundle
Projects
None yet
Development

No branches or pull requests

3 participants