Score:2

Programmatically get a full page html rendering of a route/page

cn flag

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.

john Smith avatar
gr flag
just as a probably "not-so-performant" and non-drupal way, use a file_get_contents on the page url ...
john Smith avatar
gr flag
PS: i just stumbled across `\Drupal::service('renderer')->renderRoot(` in some of my code, maybe it helps instead using `renderPlain`
4uk4 avatar
cn flag
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.
cn flag
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!!
Score:1
cn flag

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.

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.