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, the optional Storybook component library, and the tests scaffold. The scaffold exists only when you pick Custom (next prompt) at the installer's Theme prompt, the default. Picking 1 of the core themes instead 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
uses Drupal's default front-end theme as its base 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. 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. 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 the base theme and the custom theme, then makes the custom theme
the default. Provisioning runs deploy hooks with drush deploy:hook, which
runs each hook once per site. To make a 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 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. The
templates/layout/region--footer-bottom.html.twig template uses it 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, and an optional modifier for variant styling. Its content slot
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 Storybook
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. Drush
compiles them into <component>.stories.json with
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. The server keeps running
until you press Ctrl-C. It 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.
settings.storybook.php loads that file in the local, ci, and
development environments, where it replaces the cors.config from
services.yml. A site that already shares responses with other origins
lists 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, 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. To move the theme or
its stories, set these variables in .env:
| Variable | Default | Purpose |
|---|---|---|
VORTEX_STORYBOOK_DIR | ./web/themes/custom/<DRUPAL_THEME> | Directory with the Storybook configuration and its npm scripts |
VORTEX_STORYBOOK_STORIES_PATTERN | <VORTEX_STORYBOOK_DIR>/components/**/*.stories.twig | Twig story templates to watch |
VORTEX_STORYBOOK_DEV_SCRIPT | storybook | npm script that starts the development server |
VORTEX_STORYBOOK_BUILD_SCRIPT | storybook-build | npm script that builds the library |
VORTEX_STORYBOOK_PORT | 6006 | Development server port inside the container |
STORYBOOK_SERVER_URL | First URL in LOCALDEV_URL | Site that renders stories for the development server |
VORTEX_STORYBOOK_BUILD_SERVER_URL | None | Site that renders stories for the static library |
VORTEX_STORYBOOK_LIBRARY_PATH | /storybook | Path of the library printed after a build |
With no VORTEX_STORYBOOK_BUILD_SERVER_URL, the static library renders its
stories from the site that serves it.
The web server reads none of these variables.
.docker/config/nginx/storybook.conf hardcodes the /storybook path and
the theme's storybook-static directory. On Lagoon, so does the storybook
volume path in docker-compose.yml. To move either, edit 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, reinstall the dependencies there and keep the
stories regenerating in the container:
-
Reinstall the theme dependencies on the host:
cd web/themes/custom/your_site_themerm -rf node_modules && npm ci -
In a second terminal, regenerate the stories as they change:
ahoy storybook-stories --watch -
Start the development server against the site's local URL shown by
ahoy info:STORYBOOK_SERVER_URL=http://your-site.docker.amazee.io npm run storybookConfirm 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 in the ys_base development setup deploy step. A second deploy
step, BuildStorybookDeployStep, then generates the stories and builds the
static application into the theme's storybook-static directory. The web
server serves that directory at /storybook.
The build runs only in the local, ci, and development environments,
where the story render route is reachable. It also needs the theme
dependencies 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 development 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, outside Shield.
On Lagoon, the CLI and web containers run in separate pods, so a dedicated
persistent storybook volume shares the storybook-static directory
between them. Acquia deployment artifacts carry no theme dependencies, so
the library 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.