Если вы следили за развитием front-end мира с React, Vue и всеми этими современными JavaScript фреймворками, вы можете задаваться вопросом, может ли Drupal не отставать. Хорошие новости? Определенно может. С Single Directory Components (SDC), Tailwind CSS v4, Vite и Storybook, вы можете создавать компонентные Drupal темы, которые выглядят удивительно современно.
Это руководство проведет вас через настройку полного современного рабочего процесса. Я проверил каждую команду здесь, исправил подводные камни и задокументировал то, что обычно вызывает проблемы. Давайте начнем.
Вам понадобится несколько вещей, уже установленных на вашей машине:
Если у вас чего-то не хватает, сначала настройте это. Поверьте, это избавит вас от головной боли позже.
Сначала давайте создадим подходящее место для вашей пользовательской темы. Drupal ожидает, что пользовательские темы будут находиться в определенном месте:
mkdir -p web/themes/customТеперь начинается интересная часть. У Drush есть генератор тем, который задаст вам кучу вопросов. Не волнуйтесь — я скажу вам, что отвечать:
ddev drush generate themeВот что вам будет предложено:
Blog SDC (это читаемое человеком имя)blog_sdc (только строчные буквы и подчеркивания)stable9 (это важно — не пропускайте это, иначе позже получите ошибки зависимостей)A custom theme for testing SDC componentsCustomNo (мы будем использовать Tailwind для адаптивного дизайна)No (пока оставим все просто)После создания темы давайте установим ее и сделаем темой по умолчанию для вашего сайта:
ddev drush theme:install blog_sdc -y
ddev drush config-set system.theme default blog_sdc -y
ddev drush config-get system.theme defaultПоследняя команда просто подтверждает, что ваша тема теперь активна. Вы должны увидеть blog_sdc в выводе.
Single Directory Components (SDC) — это прорыв. Вместо того, чтобы разбрасывать файлы компонентов по нескольким директориям, все живет вместе — template, CSS, JavaScript и конфигурация все в одной папке. Это компонентная архитектура, которую вы хотели в Drupal.
Давайте сгенерируем компонент Card:
ddev drush generate sdcОтветьте на вопросы:
blog_sdcCard ComponentcardA reusable card componentyn (мы добавим это позже)nnПосле генерации очистите кеш Drupal. Вы будете делать это часто, так что привыкайте к этой команде:
ddev drush crТеперь начинается интересное. Нам нужно подключить наш компонент к реальному контенту Drupal. Сначала давайте настроим отладку Twig, чтобы вы могли видеть, что происходит под капотом.
Скопируйте файл services по умолчанию:
cp web/sites/default/default.services.yml web/sites/default/services.ymlОткройте web/sites/default/services.yml и найдите секцию twig.config. Вы меняете только три значения здесь — не заменяйте всю секцию:
parameters:
twig.config:
debug: true
auto_reload: true
cache: falseЭти настройки покажут вам полезные комментарии в исходном коде HTML и будут автоматически перезагружать templates. Спасители жизни во время разработки.
Теперь давайте установим несколько инструментов для разработчиков, которые облегчат вашу жизнь:
ddev composer require drupal/devel drupal/twig_tweak 'drupal/kint-kint:^2.2'
ddev drush en devel devel_generate twig_tweak kint -yЭти модули необходимы для разработки: Devel предоставляет инструменты отладки, Twig Tweak добавляет полезные функции Twig, а Kint дает вам красивый вывод переменных.
Быстрое замечание: Да, это drupal/kint-kint с повторяющимся именем. Это странно, но это реальное имя пакета из-за конфликта с другим пакетом.
Сгенерируйте тестовую статью для работы:
ddev drush devel-generate:content 1 --bundles=articleНайдите ID вашей статьи (вам это понадобится):
ddev drush sqlq "SELECT nid, title FROM node_field_data WHERE type='article' LIMIT 1"Теперь давайте переопределим template статьи, чтобы использовать наш компонент card. Создайте директорию template и скопируйте базовый template:
mkdir -p web/themes/custom/blog_sdc/templates/node
cp web/core/themes/stable9/templates/content/node.html.twig web/themes/custom/blog_sdc/templates/node/node--article.html.twigОткройте templates/node/node--article.html.twig и замените все одним оператором include:
{% include 'blog_sdc:card' with {
title: label,
content: content.body,
image: content.field_image,
tags: content.field_tags,
url: url
} %}Вот где происходит магия. Вы говорите Drupal "используй мой компонент card и передай ему эти значения". Обратите внимание, насколько это чисто по сравнению с традиционными переопределениями template.
Но нам нужно сказать нашему компоненту, какие данные ожидать. Отредактируйте components/card/card.component.yml:
'$schema': 'https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json'
name: Card Component
status: stable
description: A reusable card component
props:
type: object
properties:
title:
type: string
title: Card Title
content:
type: string
title: Card Content
image:
type: string
title: Card Image
tags:
type: string
title: Card Tags
url:
type: string
title: Card URLСнова очистите кеш:
ddev drush crПрежде чем добавлять Tailwind, давайте убедимся, что наш компонент действительно что-то отображает. Отредактируйте components/card/card.twig:
<article {{ attributes.addClass('card') }}>
{% if title %}
<h2 class="card__title">
{% if url %}
<a href="{{ url }}">{{ title }}</a>
{% else %}
{{ title }}
{% endif %}
</h2>
{% endif %}
{% if content %}
<div class="card__content">
{{ content }}
</div>
{% endif %}
</article>И добавьте базовые стили в components/card/card.css (скоро мы заменим это на Tailwind):
[data-component-id="blog_sdc:card"] {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
background: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
}
[data-component-id="blog_sdc:card"]:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
[data-component-id="blog_sdc:card"] .card__title {
margin: 0 0 1rem 0;
font-size: 1.5rem;
color: #333;
}
[data-component-id="blog_sdc:card"] .card__title a {
color: #0073e6;
text-decoration: none;
}
[data-component-id="blog_sdc:card"] .card__title a:hover {
text-decoration: underline;
}
[data-component-id="blog_sdc:card"] .card__content {
color: #666;
line-height: 1.6;
}Очистите кеш и протестируйте:
ddev drush crПосетите вашу статью (вероятно, по адресу /node/1) и вы должны увидеть ваш компонент card в действии! Если вы выполнили тот SQL запрос ранее, вы будете точно знать, какой URL посетить.
Здесь все становится действительно интересным. Мы собираемся настроить современную систему сборки с Vite и Tailwind CSS v4. Перейдите в директорию вашей темы:
cd web/themes/custom/blog_sdc
bun init -yУстановите необходимые зависимости:
bun add -D vite @tailwindcss/vite tailwindcss
bun add -D prettier prettier-plugin-tailwindcss @ttskch/prettier-plugin-tailwindcss-anywherePrettier будет автоматически форматировать ваш код, включая сортировку классов Tailwind. Второй плагин Prettier особенный — он позволяет Prettier понимать Twig templates, что обычный Prettier не может.
Создайте файл .prettierrc:
{
"plugins": ["prettier-plugin-tailwindcss", "@ttskch/prettier-plugin-tailwindcss-anywhere"],
"overrides": [
{
"files": ["*.twig", "*.html.twig"],
"options": {
"parser": "anywhere",
"regex": "(?:class(?:Name)?|addClass)\\\\s*(?:[=:]|\\\\()\\\\s*[\"']([^\"'{}~]*)"
}
}
],
"printWidth": 100,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}Теперь настройте Vite. Создайте vite.config.js в корне вашей темы:
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
import { resolve } from 'path';
export default defineConfig({
plugins: [tailwindcss()],
build: {
outDir: 'dist',
manifest: true,
emptyOutDir: true,
rollupOptions: {
input: {
main: resolve(__dirname, 'src/main.js'),
},
output: {
entryFileNames: '[name]-[hash].js',
chunkFileNames: '[name]-[hash].js',
assetFileNames: '[name]-[hash].[ext]',
},
},
},
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
cors: true,
},
base: process.env.NODE_ENV === 'production' ? '/themes/custom/blog_sdc/dist/' : '/',
});Эта конфигурация обрабатывает сборку ваших ресурсов с хешами для кеширования и настраивает dev сервер с горячей заменой модулей.
Создайте исходную директорию для ваших основных JavaScript и CSS файлов:
mkdir -p srcСоздайте src/main.css с директивами Tailwind:
@import 'tailwindcss';
@layer base {
body {
@apply text-gray-900;
}
}
@layer components {
/* Your custom component styles */
}
@layer utilities {
/* Your custom utilities */
}Вот где все становится умным. Создайте src/main.js с автоматическим обнаружением компонентов:
/**
* @file
* Main JavaScript entry point for blog_sdc theme.
*/
// Import main CSS (includes Tailwind)
import './main.css';
// Auto-discover component JavaScript files
const componentModules = import.meta.glob('../components/**/*.js', { eager: false });
// Initialize components
async function initializeComponents() {
const componentPaths = Object.keys(componentModules);
console.log(`[blog_sdc] Found ${componentPaths.length} component JS files`);
for (const path of componentPaths) {
const componentName = path.match(/components\/([^/]+)\//)?.[1];
const componentSelector = `[data-component-id="blog_sdc:${componentName}"]`;
if (document.querySelector(componentSelector)) {
console.log(`[blog_sdc] Loading component: ${componentName}`);
try {
const module = await componentModules[path]();
if (module.default && typeof module.default === 'function') {
module.default();
}
} catch (error) {
console.error(`[blog_sdc] Error loading component ${componentName}:`, error);
}
}
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeComponents);
} else {
initializeComponents();
}
export { initializeComponents };Этот скрипт автоматически находит и загружает JavaScript для любого компонента, который есть на странице. Больше не нужно вручную импортировать каждый файл компонента!
Обновите ваш package.json полезными скриптами:
{
"name": "blog_sdc",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"format": "prettier --write \"src/**/*.{js,css}\" \"components/**/*.{js,twig}\" \"templates/**/*.twig\""
}
}Поскольку мы теперь используем Tailwind, нам больше не нужен этот CSS файл:
rm components/card/card.cssТеперь давайте пересоберем компонент card с утилитами Tailwind. Отредактируйте components/card/card.twig:
<article {{ attributes.addClass('bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300 overflow-hidden mb-6') }}>
{% if image %}
<div class="aspect-video overflow-hidden">
{{ image }}
</div>
{% endif %}
{% if title %}
<div class="p-6">
<h2 class="text-xl font-semibold text-gray-900 mb-3">
{% if url %}
<a href="{{ url }}" class="text-blue-600 hover:text-blue-800 transition-colors">
{{ title }}
</a>
{% else %}
{{ title }}
{% endif %}
</h2>
</div>
{% endif %}
{% if content %}
<div class="px-6 pb-4">
<div class="text-gray-600 leading-relaxed">
{{ content }}
</div>
</div>
{% endif %}
{% if tags %}
<div class="px-6 pb-6 border-t border-gray-100 pt-4">
<div class="flex flex-wrap gap-2">
{{ tags }}
</div>
</div>
{% endif %}
</article>Посмотрите на это — CSS файл не нужен. Все стили прямо там в template, используя утилитарные классы Tailwind.
Давайте добавим интерактивность. Создайте components/card/card.js:
/**
* @file
* Card component JavaScript.
*/
export default function initCard() {
const cards = document.querySelectorAll('[data-component-id="blog_sdc:card"]');
console.log(`[Card] Initializing ${cards.length} card(s)`);
cards.forEach((card) => {
card.addEventListener('mouseenter', () => {
card.classList.add('card--hover');
});
card.addEventListener('mouseleave', () => {
card.classList.remove('card--hover');
});
const title = card.querySelector('.card__title');
if (title) {
console.log(`[Card] Initialized: ${title.textContent.trim()}`);
}
});
}Теперь соберите все:
bun run buildОбратите внимание на вывод. Вы увидите что-то вроде:
dist/main-BuEGDbdA.css
dist/main-BicIW-eB.jsЭти значения хеша (BuEGDbdA и BicIW-eB) важны. Скопируйте их — они вам понадобятся через секунду.
Отредактируйте blog_sdc.libraries.yml и добавьте ваши собранные ресурсы с реальными значениями хеша:
# Vite-built assets with Tailwind CSS
vite-main:
css:
theme:
dist/main-BuEGDbdA.css: {} # Replace with your actual hash
js:
dist/main-BicIW-eB.js: {} # Replace with your actual hash
dependencies:
- core/drupal
# Legacy theme library (if it exists)
global:
css:
theme:
css/style.css: {}Важно: Эти значения хеша меняются каждый раз, когда вы собираете. Если вы изменяете исходные файлы и пересобираете, вам нужно будет обновить эти хеши в вашем файле библиотек. Это раздражает, но так работает кеш-busting.
Теперь скажите вашей теме загрузить эту библиотеку. Отредактируйте blog_sdc.info.yml:
name: Blog SDC
type: theme
description: 'A custom theme for testing SDC components'
package: Custom
core_version_requirement: ^10 || ^11
base theme: stable9
libraries:
- blog_sdc/vite-mainОчистите кеш еще раз:
ddev drush crХотите, чтобы ваш код выглядел красиво? Запустите форматтер:
bun run formatВот где рабочий процесс действительно сияет. Storybook позволяет разрабатывать компоненты изолированно, без необходимости навигации через административный интерфейс Drupal или создания тестового контента. Инициализируйте его:
npx storybook@latest init --builder vite --type html --no-dev --yesОчистите примеры stories, которые они включают:
rm -rf storiesУстановите accessibility addon (это очень полезно):
bun add -D @storybook/addon-a11y@10.0.7Удалите конфигурации TypeScript (мы оставляем все просто):
rm .storybook/main.ts .storybook/preview.tsСоздайте .storybook/main.js:
import { join } from 'node:path';
import { cwd } from 'node:process';
const config = {
stories: ['../components/**/*.stories.js'],
addons: [
'@storybook/addon-a11y',
'@storybook/addon-docs',
],
framework: {
name: '@storybook/html-vite',
options: {
builder: {
viteConfigPath: './.storybook/vite.config.js',
},
},
},
core: {
disableTelemetry: true,
},
};
export default config;Создайте .storybook/preview.js для настройки отображения stories:
import '../src/main.css';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#1a1a1a' },
{ name: 'gray', value: '#f3f4f6' },
],
},
layout: 'padded',
a11y: {
config: {
rules: [{ id: 'color-contrast', enabled: true }],
},
},
};
export const decorators = [
(Story) => {
const wrapper = document.createElement('div');
wrapper.style.padding = '1rem';
wrapper.appendChild(Story());
return wrapper;
},
];Создайте .storybook/vite.config.js (отдельно от вашей основной конфигурации Vite):
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [tailwindcss()],
optimizeDeps: {
include: [],
},
});Теперь создайте вашу первую story. Создайте components/card/card.stories.js:
export default {
title: 'Components/Card',
tags: ['autodocs'],
parameters: { layout: 'padded' },
argTypes: {
title: { control: 'text', description: 'The title displayed in the card header' },
content: { control: 'text', description: 'The main content of the card' },
image: { control: 'text', description: 'Optional image HTML for the card' },
tags: { control: 'text', description: 'Optional tags HTML for the card' },
url: { control: 'text', description: 'Optional URL for the title link' },
},
};
const renderCard = (args) => {
const article = document.createElement('article');
article.className = 'mb-6 overflow-hidden rounded-lg bg-white shadow-md transition-shadow duration-300 hover:shadow-lg';
article.setAttribute('data-component-id', 'blog_sdc:card');
if (args.image) {
const imageDiv = document.createElement('div');
imageDiv.className = 'aspect-video overflow-hidden';
imageDiv.innerHTML = args.image;
article.appendChild(imageDiv);
}
if (args.title) {
const titleDiv = document.createElement('div');
titleDiv.className = 'p-6';
const h2 = document.createElement('h2');
h2.className = 'mb-3 text-xl font-semibold text-gray-900';
if (args.url) {
const link = document.createElement('a');
link.href = args.url;
link.className = 'text-blue-600 transition-colors hover:text-blue-800';
link.textContent = args.title;
h2.appendChild(link);
} else {
h2.textContent = args.title;
}
titleDiv.appendChild(h2);
article.appendChild(titleDiv);
}
if (args.content) {
const contentDiv = document.createElement('div');
contentDiv.className = 'px-6 pb-4';
const innerDiv = document.createElement('div');
innerDiv.className = 'leading-relaxed text-gray-600';
innerDiv.innerHTML = args.content;
contentDiv.appendChild(innerDiv);
article.appendChild(contentDiv);
}
if (args.tags) {
const tagsDiv = document.createElement('div');
tagsDiv.className = 'border-t border-gray-100 px-6 pt-4 pb-6';
const innerDiv = document.createElement('div');
innerDiv.className = 'flex flex-wrap gap-2';
innerDiv.innerHTML = args.tags;
tagsDiv.appendChild(innerDiv);
article.appendChild(tagsDiv);
}
return article;
};
export const Basic = {
args: { title: 'Basic Card', content: 'This is a basic card with minimal styling.' },
render: renderCard,
};
export const WithImage = {
args: {
title: 'Card with Image',
content: 'This card includes a featured image at the top.',
image: '<img src="https://placehold.co/600x400" alt="Placeholder" class="w-full h-auto" />',
url: '/example',
},
render: renderCard,
};
export const Complete = {
args: {
title: 'Complete Card',
content: 'This card showcases all available features.',
image: '<img src="https://placehold.co/600x400" alt="Complete" class="w-full h-auto" />',
tags: '<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-sm">Featured</span>',
url: '/complete',
},
parameters: { backgrounds: { default: 'gray' } },
render: renderCard,
};Обновите ваш package.json еще раз, чтобы добавить скрипты Storybook:
{
"name": "blog_sdc",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"format": "prettier --write \"src/**/*.{js,css}\" \"components/**/*.{js,twig}\" \"templates/**/*.twig\"",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
}
}Запустите Storybook:
bun run storybookОткройте http://localhost:6006 в вашем браузере. Вы должны увидеть ваш компонент card с тремя различными вариантами, с которыми вы можете взаимодействовать и изменять в реальном времени.
Хотите собрать статическую версию для развертывания или обмена с вашей командой?
bun run build-storybookEcho Flow помогает канадским компаниям с Drupal-разработкой корпоративного уровня.
Теперь, когда все настроено, вот как вы будете работать изо дня в день.
Давайте пройдем через добавление нового компонента с нуля. Скажем, вы хотите добавить компонент Button:
Сгенерируйте компонент:
ddev drush generate sdc
# Theme: blog_sdc, Component: Button, machine: buttonУдалите CSS файл (мы используем Tailwind, помните?):
rm components/button/button.cssСтилизуйте его с помощью Tailwind в components/button/button.twig:
<button {{ attributes.addClass('px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors') }}>
{{ label }}
</button>Добавьте JavaScript при необходимости в components/button/button.js:
export default function initButton() {
console.log('Button component initialized');
// Your button logic here
}components/button/button.stories.js с различными вариантамиПротестируйте в Storybook:
bun run storybookСоберите ваши ресурсы:
bun run buildblog_sdc.libraries.yml новыми значениями из вывода сборкиОчистите кеш Drupal:
ddev drush crОтформатируйте ваш код:
bun run formatВот команды, которые вы будете использовать постоянно:
# Запустить Vite dev сервер (горячая перезагрузка, намного быстрее, чем пересборка)
bun run dev
# Запустить Storybook (обычно в отдельном терминале)
bun run storybook
# Сгенерировать тестовый контент, когда вам нужно больше статей для работы
ddev drush devel-generate:content 5 --bundles=article
# Найти ID узлов для посещения конкретного контента
ddev drush sqlq "SELECT nid, title FROM node_field_data WHERE type='article' LIMIT 5"
# Очистить кеш (серьезно, делайте это постоянно)
ddev drush cr
# Проверить, какие темы установлены
ddev drush pm:list --type=theme
# Удалить тему, если нужно начать заново
ddev drush theme:uninstall blog_sdcДавайте будем честны — что-то пойдет не так. Вот как исправить наиболее распространенные проблемы.
Это обычно означает, что ваш blog_sdc.info.yml имеет проблему. Проверьте, что base theme: установлен на допустимое имя темы, например stable9. Генератор Drush иногда путается и смешивает порядок входных данных во время генерации.
Это почти всегда проблема кеширования. Запустите ddev drush cr и попробуйте снова. Если это не работает:
web/themes/custom/blog_sdc/components/card/ddev drush pm:list --type=themeddev drush config-get system.theme defaultНесколько вещей для проверки:
bun run build?blog_sdc.libraries.yml?blog_sdc.info.yml на наличие секции libraries:ddev drush crОбычно это означает проблему с зависимостями:
bun install для переустановки всегоsrc/main.js и src/main.css существуютvite.config.js находится в корневой директории вашей темыnode_modules и запустите bun install снова.storybook/main.js и .storybook/preview.js существуютКлассическая проблема. Попробуйте это по порядку:
ddev drush crCmd+Shift+R на Mac или Ctrl+Shift+R на Windows/LinuxЕсли вы получаете ошибку при установке Kint, используйте точное имя пакета: drupal/kint-kint (да, "kint" появляется дважды). Это из-за конфликта имен с другим пакетом. Это запутанно, но так оно и есть.
Для тех случаев, когда вам просто нужно быстро скопировать и вставить что-то:
# Очистить кеш (используйте это постоянно)
ddev drush cr
# Сгенерировать новый SDC компонент
ddev drush generate sdc
# Собрать ресурсы для production
bun run build
# Запустить dev сервер с горячей перезагрузкой
bun run dev
# Запустить Storybook
bun run storybook
# Отформатировать ваш код
bun run format
# Сгенерировать тестовый контент
ddev drush devel-generate:content 5 --bundles=article
# Найти ID узлов
ddev drush sqlq "SELECT nid, title FROM node_field_data WHERE type='article' LIMIT 5"
# Проверить установленные темы
ddev drush pm:list --type=theme
# Получить текущую тему по умолчанию
ddev drush config-get system.theme default
# Собрать статический Storybook для развертывания
bun run build-storybookstable9, для прочной основыdrush cr (серьезно, делайте это часто)component.ymlbun run build для production развертыванийbun run dev во время разработкиbun run formattags: ['autodocs']Это не просто тема — это полная современная среда разработки:
Основываясь на этом рабочем процессе, вот что вы можете ожидать:
Это значительно быстрее, чем традиционные рабочие процессы разработки тем Drupal.
Теперь у вас есть готовый к production современный рабочий процесс разработки тем, который выводит Drupal в ту же лигу, что и современные JavaScript фреймворки. Комбинация SDC, Tailwind, Vite и Storybook дает вам изоляцию компонентов, быструю итерацию и опыт разработчика, который действительно приятен.
Настоящая сила здесь не только в инструментах — это то, как они работают вместе. Вы можете прототипировать в Storybook, стилизовать с помощью утилитарных классов и развертывать в Drupal без переключения контекста. Ваши компоненты портативны, ваши сборки быстры, и ваш рабочий процесс масштабируется по мере роста вашего проекта.
Начните с малого. Создайте несколько компонентов, освойтесь с рабочим процессом, затем расширяйте вашу библиотеку компонентов. Вскоре вы будете задаваться вопросом, как вы вообще создавали темы Drupal по-другому.
Приятной разработки тем! 🎨