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 · 8 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.

@MarnissiDev
Copy link

Thank you for your response and efforts. I apologize for any inconvenience caused.

I have created a page called default-url. Here are the results I get when testing the following URLs:

When I access:
http://localhost:8000/default-url.json
Response: The requested resource /headless.json was not found on this server.

When I access:
http://localhost:8000/default-url
Response displayed on the page: Page does not exist in “html” format, because I'm not using a Twig file.

The issue seems to be related to adding .json to the path, and I would like to resolve this problem.

Do you have any ideas or solutions for this issue? I appreciate your help in advance.

Here is my current configuration:
sulu_headless:
type: portal
resource: "@SuluHeadlessBundle/Resources/config/routing_website.yml"
defaults:
_format: json
requirements:
_format: json|html

@alexander-schranz
Copy link
Member Author

alexander-schranz commented Jan 8, 2025

Response: The requested resource /headless.json was not found on this server.

Means you did not use the HeadlessWebsiteController in your requestes pages template. Check the installation guide:

https://github.com/sulu/SuluHeadlessBundle?tab=readme-ov-file#-installation-and-usage

If you use the Headless Controller you should get into that controller and get a JSON responded:

. Make sure your page is published.

Response displayed on the page: Page does not exist in “html” format, because I'm not using a Twig file.

Your app should never access a route without .html the HTML in case of headless should be rendered by your JS application (Angular, Remix, Next, Nuxt, ...). You only require a .html for the preview for that have a look at:

#65 (comment)

Here is my current configuration:
sulu_headless:
resource: "@SuluHeadlessBundle/Resources/config/routing_website.yml"
...

This routes has nothing todo with providing Content Pages Headless. The routing_website provides just endpoints which are in "Standard" twig rendering twig extensions (navigation, snippet areas and search): See https://github.com/sulu/SuluHeadlessBundle/blob/0.10/Resources/config/routing_website.yml

The <controller> part in your template is the important part of setting up the Headless Bundle correctly.

@MarnissiDev
Copy link

I installed the SuluHeadlessBundle using this command:
composer require sulu/headless-bundle
It downloaded the files HeadlessWebsiteController.php and routing_website.yml into the vendor folder, so I think the installation was fine.

I added this line to my configuration:
Sulu\Bundle\HeadlessBundle\Controller\HeadlessWebsiteController::indexAction

It looks like enabling the bundle and adding its routes is done automatically during installation.

Then, I created a page in the admin and added an XML file for it. When I open:
http://localhost:8000/json-page
it shows HTML, so it works.

But when I try:
http://localhost:8000/json-page.json
I get this error:
Not Found
The requested resource /json-page.json was not found on this server.

I followed all the steps in the SuluHeadlessBundle docs here:
https://github.com/sulu/SuluHeadlessBundle?tab=readme-ov-file#enable-the-bundle

Any idea what I might be missing?

Thanks!

@fatimazohra19
Copy link

good job @MarnissiDev

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

5 participants