What the proper method of outputting a page from your module without any additional theming beyond your template, for example to make a page that is good for printing?

I've tried a few different methods based on Drupal 7 versions. The only one to work being to render the build array and the kill further PHP loading, however this doesn't feel right. In Drupal 7, I could set up a 'delivery callback' in the hook_menu() - but I can't seem to figure out how to replicate that behaviour now.


  path: '/my_module/timesheets/{weekOf}/{uid}'
    _controller: '\Drupal\my_module\Controller\MyModuleAdminController::timesheets'
    _title: 'My Module Timesheets'
    _permission: 'admin'


  $theme['timesheets'] = [
    'render element' => 'build',
    'template' => 'timesheets',


public function timesheets($weekOf, $uid = null) {
    $build['#theme'] = 'timesheets';

    ...code building values...

    $build['#staff'] = $staff;

    $build['#title'] = 'Timesheets - ' . $weekOf . ' to ' . $this->dateService->getWeekStartDate($weekOf);
    $build['#attached']['library'][] = 'my_module/timesheets';
    //print render($build);
    return $build;


      lib/timesheets.css: {}

Edit: To over-ride something like page.html.twig in your module, you make a template called page--my_module--html.twig and then declare it in your modules my_module_theme() function.

 * Implements hook_theme().
function my_module_theme() {
  $theme['page__my_module__timesheets'] = [
    'template' => 'page--my_module--timesheets',

   return $theme;

3 Answers 3


The controller does not have to return a render array, it can also provide a symphony response, for example:

use Symfony\Component\HttpFoundation\Response;

$response = new Response();
$response->setContent('<h1>Hello world</h1>');
//$response->headers->set('Content-Type', 'text/xml');
return $response;

This will bypass the Drupal rendering and theming.


You can use two approaches to replace your Drupal 7 code:

1) Override page.html.twig and html.html.twig, remove anything out of this templates and put only that in what you want.

2) Render the HTML like you are doing it now in Drupal 7 and use a Symfony response.

Rendering in Drupal 8:

use Symfony\Component\HttpFoundation\Response;

$output = \Drupal::service('renderer')->renderRoot($build);

$response = new Response();

return $response;
  • Using the symphony response would mean I can't leverage the render array features, such as templating - no? I'm trying to migrate a module, and it the Drupal 7 version the template that is being loaded is an entire html page using a delivery callback and printing with drupal_render(). So I'm trying to replicate that similar functionality.
    – EmGee
    Commented Nov 25, 2015 at 16:58
  • Please see the edit in my answer.
    – 4uk4
    Commented Nov 25, 2015 at 17:16
  • Thanks for all the help, 4k4. The second method seems to be almost there, but for some reason the RendererInterface doesn't seem to include my css library. I've edited my code slightly above to show my library adding code.
    – EmGee
    Commented Nov 25, 2015 at 17:30
  • With method 2) you must do all things yourself, like add stylesheets to the html head. I think method 1) is the way to go for drupal 8. You can for example put a nearly empty page--my_module--timesheets.html.twig in your theme. There you can remove anything like menus and footer, so that only the main content remains.
    – 4uk4
    Commented Nov 25, 2015 at 17:48
  • You say in my theme, but can it be done from the module as well? When I put page--my_module--timesheets.html.twig in my-module/templates/ it doesn't seem to override.
    – EmGee
    Commented Nov 25, 2015 at 17:54

If you utilize the html_response.attachments_processor and renderer services along with BareHtmlPageRenderer you can still get libraries to load in the head tag of an otherwise un-templated page.

use Drupal\Core\Render\BareHtmlPageRenderer;

$attachments = \Drupal::service('html_response.attachments_processor');
$renderer = \Drupal::service('renderer');

$bareHtmlPageRenderer = new BareHtmlPageRenderer($renderer, $attachments);

$response = $bareHtmlPageRenderer->renderBarePage($build, 'Page Title', 'markup');
return $response;
  • This is exactly what I was looking for. I was unable to load attached libs before. But I can now using your advice! Thanks! Commented Jun 24, 2018 at 16:05

I do not have enough reputation to add a comment on a reply, but the anwser by @Arosboro is almost complete, except the "build variable is in the wrong spot. It must be the last variable, like so:

return $bareHtmlPageRenderer->renderBarePage([], 'Address', 'checkout_address', $build);

This way the twig variables I placed in $build were rendered correctly.

My full code:

$build = [
  '#theme' => 'checkout_address',
  '#content' => [],
  '#edit' => FALSE,


$attachments = \Drupal::service('html_response.attachments_processor');
$bareHtmlPageRenderer = new BareHtmlPageRenderer($this->renderer, $attachments);

return $bareHtmlPageRenderer->renderBarePage([], 'Address', 'checkout_address', $build);

And the template:

{{ content }}
{% if edit == true %}
    <button class="btn-edit-address">Edit Address</button>
{% endif %}

This is how I got it to work.

  • 1
    It's worth noting here that any attachments that happen as a result of hook_page_attachments() functions across modules will not get attached here. The HTMLRenderer is what handles those attachments in core. Many contrib modules make use of that for attaching things, so if you need those attachments you will have to do something like this (pulled from the HtmlRenderer): $attachments = []; foreach (\Drupal::moduleHandler()->getImplementations('page_attachments') as $module) { $function = $module . '_page_attachments'; $function($attachments); } Commented Jul 9, 2020 at 14:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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