2

We provide our content via APIs. At times we use the Views Restful approach, and at times the JSONAPI.

We need to add an API field on nodes that is a full HTML rendering of that node's page according to the theme (technically I just need ... but I'll take the whole doc if I can get it).

I have tried a number of approaches:

I tried using the renderer service. It renders full html from the html.html.twig template but items such as blocks on the page are missing. I guess it doesn't have all the context it needs.

$view_builder = \Drupal::entityTypeManager()->getViewBuilder('node');
$content = $view_builder->view($node);
$build = [
  '#type' => 'html',
  'page' => [
    '#type' => 'page',
    '#theme' => 'page',
    '#title' => $node->get("title")->value,
    'content' => $content,
  ],
];
$page = \Drupal::service('renderer')->renderPlain($build);

Very similarly I tried using twig_render_template. It similarly renders full html but items such as blocks on the page are missing.

$markup = twig_render_template(drupal_get_path('theme', 'neato') . '/templates/base/html.html.twig', array(
  'page' => [
    '#type' => 'page',
    '#theme' => 'page',
    '#title' => $node->get("title")->value,
    'content' => $content,
  ],
  // Needed to prevent notices when Twig debugging is enabled.
  'theme_hook_original' => 'not-applicable',
));
$body = (string) $markup;

As a separate approach, I tried to make a 'subrequest'. With this approach, I get the rendered HTML but it causes fatal early rendering errors such as "A stray renderRoot() invocation is causing bubbling of attached assets to break."

$kernel = \Drupal::service('http_kernel.basic');
$sub_request = \Symfony\Component\HttpFoundation\Request::create("/node/".$value->_entity->id(), 'GET');
$subResponse = $kernel->handle($sub_request, \Symfony\Component\HttpKernel\HttpKernelInterface::SUB_REQUEST);
$html = $subResponse->getContent();

I even tried to mimic the full drupal 'bootstrap'

$autoloader = require '/app/web/autoload.php';
$sub_request = Request::create("/node/".$node->id(), 'GET');
$site_path = DrupalKernel::findSitePath($sub_request);
$kernel = DrupalKernel::createFromRequest($sub_request, $autoloader, 'prod');
$sub_response = $kernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST);
$html = $sub_response->getContent();

I am open to any and all pointers and suggestions. I really appreciate it.

4
  • just as a probably "not-so-performant" and non-drupal way, use a file_get_contents on the page url ...
    – john Smith
    Jan 17 at 10:03
  • PS: i just stumbled across \Drupal::service('renderer')->renderRoot( in some of my code, maybe it helps instead using renderPlain
    – john Smith
    Jan 17 at 10:19
  • Normally you don't need to render variables at all. But if you need to, then use \Drupal::service('renderer')->render(). The two methods above are only for specific use cases, not for normal page renderings.
    – 4k4
    Jan 17 at 19:29
  • Agreed. This is not how pages are normally rendered. It's an edge case for my spec. I tried \Drupal::service('renderer')->renderRoot and render() many ways. It never had all the context. I got the HTML skeleton of the page but blocks and such were still missing. I found you would have to mimic the full Drupal request, which is complex, and started to feel like a worse direction. We went a similar path to @johnSmith's rec of using file_get_contents(). When nodes are saved, we use httpClient to make a 2nd request to the site. That response is saved in a field and included in the API. Thx!!
    – maestrojed
    Jan 18 at 19:50
1

We went a similar path to @johnSmith's rec of using file_get_contents(). Thanks!

TL;DR; When nodes are saved, we use httpClient to make a 2nd request to the site. That response is saved in a field and included in the API. Thx!!

Details

  • A new field was added to the associated content types. This field will be used to store the rendered page HTML.

  • A custom module was written. When nodes of the associated content types are created or updated, a subsequent request is made using httpClient. This request is made to the node page. That HTML response is saved in the new field.

    //We have saved the configured domain name in which to make this render request to in Drupal's config.

    $config = \Drupal::service('config.factory')->getEditable('myApi.static_server_settings');

    $domain = $config->get('myApi.theme_domain');

    //Request this node's rendered page

    $nid = $node->id();

    $client = \Drupal::httpClient();

    $request = $client->get($domain.'/node/'.$nid);

    $render = (string) $request->getBody();

  • Our API was extended to include this field. As a fallback, when the API runs, if the new field is empty that same subsequent httpClient request is made on the fly.

  • We built a batch process that updates the 'rendered page' field for all the content. This can be run if the theme is updated. We may eventually trigger it from our theme-building scripts/gulp/CI stuff.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.