Theming
Pinto Theme challenges how the traditional global shell of a Drupal site is put together by providing an outside-in approach.
You'll start with an existing Pinto setup and create Pinto components representing the HTML shell, just like Drupal's hook_theme:html / html.html.twig and hook_theme:page / page.html.twig. Pinto theme then provides the context of the request, including arbitrary page context (as in page.html.twig's page.content).
A key thing to know about Pinto Theme is that a true Drupal theme is never fully utilized. Pinto components are used to create the general scaffold of a page, such as Html and Page equivalents, then we use Drupal API's to take over rendering before the traditional global block and theme system is activated.
Pinto components are themselves defined in a module. A PHP attribute signals that a designated Pinto component will be the main page entry point. Because modules are used, you're able to make use of API's that themes cannot, namely: services.
Install dependencies
Install Pinto Theme and Empty Theme.
composer require drupal/pinto_theme drupal/empty_themeYou may choose to implement your own placeholder theme, similar to Empty Theme. Drupal requires a theme to attach to, but you will no longer need a real theme when using Pinto Theme.
Enable modules
Enable the Drupal module.
drush pm:install pinto_theme empty_themeConfigure the default theme.
drush theme:enable empty_theme && drush config:set system.theme default empty_theme -y && drush crExport configuration to ensure these changes are not lost.
Create components
Create two Pinto components, one for the <html> template, and another for the page.
TIP
Only one Pinto component is required, however separate Html and Page templates provides cleaner separation, and is a familiar layout to Drupal's templates.
Discovery
Add components to an enum list, and add the #[Theme] attribute to the Html component.
Add the name of the frontside/non-admin theme to #[Theme(themeName)]. If using Empty Theme, add 'empty_theme'.
#[Slots(bindPromotedProperties: TRUE)]
enum MyComponents implements ObjectListInterface {
#[\Drupal\pinto_theme\PintoTheme\Attribute\Theme('empty_theme')]
#[Definition(Html::class)]
case Html;
#[Definition(Page::class)]
case Page;
}Page component
Page template is at its core quite simple, comprising page title and content.
But you might want to also synthesise things like menus, and create a header and footer component.
class Page {
use DrupalInvokableSlotsTrait;
public function __construct(
private array $title,
private mixed $content,
) {
}
}Template
<div>
<!-- masthead and headers go here -->
<header>
<h1>{{ title }}</h1>
</header>
<main>
{{ content }}
</main>
<footer>
<!-- footer goes here -->
</footer>
</div>If you have a header or footer component, they can be added in.
class Page {
use DrupalInvokableSlotsTrait;
public function __construct(
private array $title,
private mixed $content,
private mixed $header = null,
private mixed $footer = null,
) {
}
protected function build(Slots\Build $build): Slots\Build {
return $build
->set('header', new Header($this->title))
->set('footer', new Footer());
}
}<div>
{{ header }}
<main>
{{ content }}
</main>
{{ footer }}
</div>Nested components will be automatically executed.
Html component
Html component is slightly complex, as you need to do take care of things Drupal would normally do for you.
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\AttachmentsTrait;
use Drupal\pinto_theme\PintoTheme\Html\PintoThemeHtmlContext;
use Drupal\pinto_theme\PintoTheme\Html\PintoThemeHtmlObjectInterface;
class Html implements AttachmentsInterface {
use DrupalInvokableSlotsTrait;
use AttachmentsTrait;
private function __construct(
public Page $page,
public ?array $top = NULL,
public ?array $bottom = NULL,
public string $placeholderToken = '',
public string $htmlTitle = '',
public Attribute $htmlAttributes = new Attribute(),
) {
}
public static function createHtmlObjectFrom(PintoThemeHtmlContext $context): object {
$title = $context->getTitle();
return new static(
Page::create($title, [$context->getContent()]),
top: $context->hasTop() ? $context->getTop() : NULL,
bottom: $context->hasBottom() ? $context->getBottom() : NULL,
);
}
protected function build(Slots\Build $build): Slots\Build {
$vars = DrupalHtmlTemplate::createFromContainer(\Drupal::getContainer());
$vars->applyAttachments($this);
$siteName = \Drupal::configFactory()->get('system.site')->get('name');
return $build
->set('htmlAttributes', $vars->htmlAttributes())
->set('htmlTitle', \sprintf('%s — %s', $vars->renderTitle($this->page->title), $siteName))
->set('top', $this->top)
->set('bottom', $this->bottom)
->set('placeholderToken', $vars->placeHolderToken());
}
}Template
<!DOCTYPE html>
<html{{ htmlAttributes }}>
<head>
<title>{{ htmlTitle }}</title>
<head-placeholder token="{{ placeholderToken }}">
<css-placeholder token="{{ placeholderToken }}">
<js-placeholder token="{{ placeholderToken }}">
</head>
<body>
{{ top }}
{{ page }}
{{ bottom }}
<js-bottom-placeholder token="{{ placeholderToken }}">
</body>
</html>