Themes
Vortex ships a theme
scaffold:
a working Drupal theme, your_site_theme, already wired into the build,
lint, and test tooling. Put your site's custom styling and front-end
features in it, and the commands on this page work on it without any setup.
This page is for developers who build the front end of a Vortex project. It covers the theme's build commands and file layout, its sample Single Directory Component, its tests scaffold, and the optional Storybook component library. The scaffold exists only when you pick Custom (next prompt) at the installer's Theme prompt, the default. Picking Olivero, Claro, or Stark removes it, and most of this page then doesn't apply.
In the template, the theme is called your_site_theme. The installer
renames it to the name you enter at the Custom theme machine name
prompt, which defaults to the site's machine name. Custom modules use the
shorter module prefix instead, such as ys_ for your_site. The theme
extends the core Olivero theme.
Front-end theming is often project-specific and subject to team preferences. The scaffold shows how a custom theme can integrate with the Vortex tooling and automations, and you can adapt it or replace it with your preferred theme.
Build system
The theme builds its assets with npm scripts that call
Sass, PostCSS with
Autoprefixer, Terser, and
chokidar. The build system
covers the following:
- SCSS compilation
- JavaScript concatenation and minification
- CSS auto-prefixing for browser compatibility
- Linting for both CSS (Stylelint) and JavaScript (ESLint)
- Watch mode for development
- Copying of
images/andfonts/intobuild/
The production build compresses the CSS, and Terser minifies the
concatenated JavaScript and strips its console calls. The development
build skips minification and writes source maps instead. Autoprefixer
targets the browsers in the browserslist key of package.json, and
npm ci applies any patches in patches/ through patch-package.
The CLI container image runs npm ci and npm run build for the theme
while it builds, so the image carries compiled assets. To build the assets
on the host or during deployment instead, set
VORTEX_FRONTEND_BUILD_SKIP=1 in .env, which ships with 0.
docker-compose.yml passes it to the image as a build argument, so the
change applies the next time the image builds, for example on ahoy build.
Build commands
Inside the CLI container (open a shell there with ahoy cli), run the
theme's npm scripts from the theme directory:
cd web/themes/custom/your_site_theme
# Install dependencies
npm ci
# Build production assets
npm run build
# Build development assets (unminified)
npm run build-dev
# Run linting
npm run lint
# Auto-fix linting issues
npm run lint-fix
# Watch for changes during development
npm run watch
The Ahoy commands call the same npm scripts in the theme directory:
# Install front-end dependencies
ahoy fei
# Build production assets
ahoy fe
# Build development assets (unminified)
ahoy fed
# Watch for changes during development
ahoy few
# Lint front-end code
ahoy lint-fe
# Fix front-end lint issues
ahoy lint-fe-fix
These commands run within the container to use the Node.js environment and
tools installed there, so every development environment runs them the same
way. They also reach beyond the theme. ahoy fei installs the project
root's npm dependencies too, and the 2 lint commands also run Twig CS Fixer
and the root lint or lint-fix script, which covers the custom modules.
If you replace the scaffold with your own theme, keep the same npm script
names in its package.json: build, build-dev, watch, lint, and
lint-fix. The Ahoy commands, the CLI image build, and the CI lint step
call those names in the theme named by DRUPAL_THEME. .env sets that
variable and DRUPAL_MAINTENANCE_THEME, and the installer writes your
theme's name into both, so point both at the new theme.
Neither variable sets Drupal's default theme. The
ys_base_deploy_install_active_theme() deploy hook in ys_base.deploy.php
installs Olivero and the theme whose machine name the installer wrote into
it, then makes that theme the default. Provisioning runs deploy hooks with
drush deploy:hook, which runs each hook once per site. To make your new
theme the default, change the machine name in that hook, so fresh installs
pick it up. Then add a new deploy hook to the same file that installs your
new theme and sets it as the default on sites where the first hook already
ran.
File structure
The following tree shows the main directories and files. It leaves out build outputs, configuration and lock files, and static assets:
your_site_theme/
├── .storybook/ # Storybook configuration
│ ├── main.mjs # Story sources and framework
│ └── preview.js # Story render endpoint
├── components/ # Single Directory Components (SDC)
│ └── button/ # Sample button component
│ ├── button.component.yml # Component schema (props and slots)
│ ├── button.twig # Component template
│ ├── button.stories.twig # Component stories
│ └── button.css # Component styles
├── scss/ # Sass source files
│ ├── _variables.scss # Theme variables
│ ├── _mixins.scss # Sass mixins
│ ├── _fonts.scss # Font definitions
│ ├── _rem.scss # REM unit utilities
│ ├── styles.scss # Main stylesheet
│ └── components/ # Component-specific styles
│ └── _header.scss # Header component styles
├── js/ # JavaScript source files
│ └── your_site_theme.js # Main theme JavaScript
├── templates/ # Twig template overrides
│ └── layout/
│ └── region--footer-bottom.html.twig # Renders the button component
├── tests/ # Theme tests
│ └── src/
│ ├── Unit/ # Unit tests
│ ├── Kernel/ # Kernel tests
│ ├── Functional/ # Functional tests
│ └── FunctionalJavascript/ # Functional JavaScript tests
├── your_site_theme.info.yml # Theme definition
├── your_site_theme.libraries.yml # Asset libraries
├── your_site_theme.theme # Theme functions
├── package.json # Node.js dependencies
└── logo.svg # Theme logo
Libraries
The theme defines 1 asset library in your_site_theme.libraries.yml,
global-styling, and your_site_theme.info.yml attaches it to every page.
It loads the compiled build/css/your_site_theme.min.css and
build/js/your_site_theme.min.js, with core/jquery, core/drupal, and
core/drupalSettings as dependencies.
Single Directory Components
The theme ships a sample Single Directory
Component
(SDC) in components/button and uses it from
templates/layout/region--footer-bottom.html.twig to render a "back to top"
link whenever the footer region has content. The button takes a required
label, an optional url that turns it into a link, an optional modifier
for variant styling, and a content slot that replaces the label.
To validate the components with SDC Devel, run the following command. It exists when the SDC Devel module is selected during installation, needs a provisioned site, and fails when the validator reports critical issues, errors, or warnings:
ahoy lint-sdc
➡️ See SDC Devel for how the validator is set up and where it runs.
Storybook
Storybook documents the theme's components. To
turn it on, answer Yes at the installer's
Storybook component library? prompt. The installer asks only when you
pick the custom theme, with No as the default answer. To add it to a
project installed without it, run ahoy update-vortex --interactive and
answer Yes at that prompt. That run also updates the rest of the
project's Vortex files, as Updating Vortex
describes. Drupal renders every story through the
Storybook module, so the library
shows the same markup the site renders.
Stories are authored in Twig next to the component they document, as
<component>.stories.twig, and compiled into <component>.stories.json by
Drush (drush storybook:generate-all-stories). The compiled files are
generated artifacts, and the root .gitignore keeps them out of Git.
Working with Storybook
Like the other front-end commands, the Storybook commands run inside the CLI container and use the theme dependencies installed there. The following 2 commands start the development server and rebuild the library:
# Develop components: regenerate stories as they change and serve them live
ahoy storybook
# Refresh the component library served at /storybook
ahoy storybook-build
ahoy storybook first stops any server or watcher left running by an
earlier session. It then generates the stories and starts a development
server. When the server is ready, the command prints its address, such as
Storybook is ready at http://localhost:55197. It keeps running until you
press Ctrl+C, and regenerates the stories whenever a
*.stories.twig file changes, so new and edited stories appear without a
restart. Docker assigns the host port when the container starts, and
ahoy info shows it next to the library address.
A third command, ahoy storybook-stories, only generates the stories. Add
--watch to keep regenerating them as the Twig story templates change.
The development server runs on its own origin, so services.storybook.yml
shares story renders with localhost and 127.0.0.1 origins, on any port,
in the local, ci, and dev environments. It replaces the cors.config
from services.yml there, so a site that already shares responses with
other origins needs to list them in services.storybook.yml too.
The component library at /storybook is a static copy built during
provisioning. The build fixes the list of stories and their arguments,
while Drupal renders each story's markup when you open it. After you add or
remove a story, or change a story's name or arguments in a
*.stories.twig file, run ahoy storybook-build to bring /storybook up
to date. The development server picks those changes up on its own.
In the local environment, provisioning turns on Twig development mode. An
edit to a component's template then shows up in the development server and
in /storybook on the next render, with no cache rebuild and no library
build.
Customizing locations
The ahoy commands and provisioning run vendor/bin/vortex-storybook,
which reads its locations from environment variables. A project that moves
the theme or its stories sets them in .env instead of changing the
commands:
VORTEX_STORYBOOK_DIR- the directory with the Storybook configuration and its npm scriptsVORTEX_STORYBOOK_STORIES_PATTERN- the Twig story templates to watchVORTEX_STORYBOOK_DEV_SCRIPTandVORTEX_STORYBOOK_BUILD_SCRIPT- the npm scripts that start the development server and build the libraryVORTEX_STORYBOOK_PORT- the development server port inside the containerSTORYBOOK_SERVER_URL- the site that renders stories for the development server, which defaults to the first URL inLOCALDEV_URLVORTEX_STORYBOOK_BUILD_SERVER_URL- the site that renders stories for the static library, when it isn't the site that serves itVORTEX_STORYBOOK_LIBRARY_PATH- the path shown in the library address that the build prints
The variables reference lists their defaults.
The web server doesn't read these variables.
.docker/config/nginx/storybook.conf hardcodes the /storybook path and
the theme's storybook-static directory, and on Lagoon so does the
storybook volume path in docker-compose.yml. Moving either means
editing those files too.
Running Storybook on the host
The theme's node_modules directory is shared with the CLI container, and
holds dependencies installed for the container's architecture. To run
Storybook on the host instead, reinstall the dependencies there, keep the
stories regenerating in the container, and point the development server at
the site's local URL shown by ahoy info:
cd web/themes/custom/your_site_theme
rm -rf node_modules && npm ci
# In a second terminal: regenerate the stories as they change
ahoy storybook-stories --watch
STORYBOOK_SERVER_URL=http://your-site.docker.amazee.io npm run storybook
Confirm that the development server answers at http://localhost:6006.
Before you use the ahoy front-end commands again, run ahoy fei, so the
container gets its own dependencies back.
Published component library
Provisioning installs the Storybook module with the other development
modules, then generates the stories and builds the static application into
the theme's storybook-static directory, which the web server serves at
/storybook. The build runs in the local, ci, and dev environments
only, which are the environments where the story render route is reachable.
It's also skipped when the theme's dependencies aren't installed. To skip
the build and shorten provisioning, set DRUPAL_STORYBOOK_SKIP=1, and run
ahoy storybook-build when you need the library.
In the dev environment, Shield
protects the story render route, so a story renders only after the browser
has the Shield credentials. The web server serves the static application at
/storybook and its story index directly, so Shield doesn't protect them.
On Lagoon, the CLI and web containers run in separate pods, so a dedicated
persistent storybook volume keeps the storybook-static directory shared
between them. Acquia deployment artifacts carry no theme dependencies, so
the application isn't built there.
Tests scaffold
The tests directory holds a working example test for each test type
(unit, kernel, functional, and functional JavaScript) to start your own
from. PHPUnit picks them up through the matching test suites in
phpunit.xml.
Each test type also has a base class that pulls in the following helper
traits from the ys_base module's tests:
ArrayTraitAssertTraitMockTraitReflectionTraitBrowserHtmlDebugTrait, in the functional JavaScript base class only
Remove what you don't need.