Integrated into GitLab Pipelines
Hyva Themes brings Tailwind CSS v4 to Magento as a CSS first build. Anyone integrating this build into a GitLab pipeline needs to coordinate Node.js versions, npm caching, artifact handoff and the ordering against the PHP build cleanly. This article shows how that works in practice.
Table of Contents
- 1. Why the frontend build belongs in the pipeline
- 2. Pinning and reproducing the Node.js version
- 3. Configuring npm caching correctly in GitLab CI
- 4. Compiling Tailwind CSS v4 in the pipeline job
- 5. Handing off the build artifact cleanly
- 6. Static content deploy after the frontend build
- 7. Common errors with frontend builds in CI
- 8. Frontend build strategies compared
- 9. Optimizing build time
- 10. Summary
- 11. FAQ
1. Why the frontend build belongs in the pipeline
The most common mistake in Magento projects using Hyva Themes is a frontend build that runs on a developer's laptop and whose results get committed to the repository. That sounds pragmatic, but in practice it is a constant source of inconsistency: different Node.js versions, different npm versions, and different Tailwind configurations across developer machines produce CSS files that vary slightly depending on the environment. Committing the result means versioning artifacts instead of source code.
The alternative is a reproducible build inside the GitLab pipeline. There, the Node.js version is fixed, npm ci guarantees that exactly the versions from package-lock.json get installed, and the CSS build output becomes a deterministic artifact of the pipeline run. The result then no longer lives in the repository; instead it is passed between jobs as a pipeline artifact and deployed to the server at the end. That approach is cleaner, more reproducible and protects the repository from generated code.
2. Pinning and reproducing the Node.js version
The Node.js version is the most common source of configuration drift between a developer's environment and CI. In Hyva projects using Tailwind CSS v4, Node.js 20 (LTS) should be used. The version is pinned in an .nvmrc file in the repository, a single line file containing 20. GitLab CI then uses the node:20-alpine Docker image for the frontend build job. That way the version is identical both locally (via nvm) and in the pipeline.
Another common pitfall is npm itself. npm install and npm ci behave differently: npm install updates package-lock.json when needed, while npm ci fails if package.json and package-lock.json do not match. In the CI pipeline, only npm ci belongs, it is faster, deterministic, and immediately flags when a developer forgot to commit package-lock.json. This is not a convenience feature, it is a reproducibility guarantee.
# .gitlab-ci.yml: Frontend build job for Hyva / Tailwind CSS v4
build:frontend:
stage: build
image: node:20-alpine
variables:
# Cache npm modules across pipeline runs
NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.cache/npm"
THEME_PATH: "app/design/frontend/Mironsoft/default"
cache:
key:
files:
# Cache key changes when lock file changes
- "${THEME_PATH}/web/tailwind/package-lock.json"
paths:
- "${THEME_PATH}/web/tailwind/node_modules/"
- .cache/npm/
policy: pull-push
script:
# Install exact versions from lock file (no updates)
- npm ci
--prefix "${THEME_PATH}/web/tailwind"
--prefer-offline
# Build Tailwind CSS v4 (CSS-first approach)
- npm run build
--prefix "${THEME_PATH}/web/tailwind"
artifacts:
name: "frontend-${CI_COMMIT_SHORT_SHA}"
paths:
# Only the compiled CSS, not node_modules
- "${THEME_PATH}/web/css/"
expire_in: 1 day
when: on_success
3. Configuring npm caching correctly in GitLab CI
The biggest time sink in the frontend build job is downloading npm packages. With a properly configured GitLab cache for node_modules, this step disappears entirely when dependencies have not changed. The cache key must point to package-lock.json: when the lock file changes, a new cache is created. When it stays the same, the existing cache is reused and npm ci runs with --prefer-offline, which prioritizes the local cache directory.
It is important to distinguish between a GitLab cache and a GitLab artifact. node_modules belongs in the cache: it is large, gets reused across pipeline runs and does not need to be versioned. The compiled CSS belongs in the artifact: it is small, gets passed between jobs within a single pipeline run and must be exactly reproducible. Defining node_modules as an artifact means uploading and downloading hundreds of megabytes unnecessarily between jobs. Keeping the compiled CSS only in the cache risks missing artifacts in the next job.
4. Compiling Tailwind CSS v4 in the pipeline job
Tailwind CSS v4 uses a CSS first approach: instead of a tailwind.config.js, a theme.css file drives the configuration. In Hyva projects the build command is typically npm run build, which internally calls the Tailwind CLI with the configured input file. In the GitLab pipeline it must be ensured that the input file is referenced correctly and that the output path matches the expected artifact path.
A common problem: by default, Tailwind v4 scans all template files in the project for used CSS classes. In a CI environment the scan path matters: it needs to cover the .phtml files of the Hyva theme, but must not search the vendor/ folder, which can contain hundreds of megabytes of files. Restricting the Tailwind configuration in theme.css with @source directives to the relevant template directories cuts build time in half in many projects.
5. Handing off the build artifact cleanly
In a multi stage GitLab pipeline, the frontend build artifact needs to be passed from the build job to the deploy job. This happens through GitLab artifacts with artifacts.paths. The build job defines which paths get stored as an artifact, and the deploy job can access those artifacts via needs. Important: artifacts are automatically passed between jobs within the same pipeline, even when they run in different stages.
The critical decision is which files should be part of the artifact. For the frontend build these are: the compiled CSS file under web/css/styles.css and optional source maps for debugging. Not in the artifact: node_modules/, the build tool itself, temporary files. A lean artifact considerably speeds up upload and download between jobs and reduces GitLab artifact storage. A typical compiled Tailwind CSS file is 50 to 150 KB, the artifact should never grow beyond a few megabytes.
6. Static content deploy after the frontend build
In Magento with Hyva, Static Content Deploy (SCD) is a separate step that runs after the frontend build. SCD compiles Magento module specific assets, processes require-js bundles (minimal in Hyva) and copies theme assets into the pub/static/ folder. In the GitLab pipeline, SCD either runs as part of the build job (which requires Magento as a dependency) or on the target server as part of the deploy job.
The cleaner solution for Hyva projects: run SCD in the build job using the PHP image, after the frontend build has produced the CSS. The result, the complete pub/static/ folder, is passed on as an artifact and deployed to the server, without a single SCD command ever running on the server. That is faster, more reproducible and avoids the problem of the server being unable to serve static assets while SCD is running.
# Combined PHP + Frontend build job
build:magento:
stage: build
image: php:8.4-cli
variables:
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.cache/composer"
THEME_PATH: "app/design/frontend/Mironsoft/default"
cache:
key:
files:
- composer.lock
paths:
- .cache/composer/
policy: pull-push
before_script:
# Install Node.js 20 into PHP image
- curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
- apt-get install -y nodejs
# Install PHP extensions required by Magento
- docker-php-ext-install pdo_mysql bcmath intl
script:
# PHP dependencies
- composer install
--no-dev --prefer-dist --no-interaction --optimize-autoloader
# Frontend build (npm ci + Tailwind)
- npm ci --prefix "${THEME_PATH}/web/tailwind" --prefer-offline
- npm run build --prefix "${THEME_PATH}/web/tailwind"
# Magento DI compile
- php bin/magento setup:di:compile
# Static content deploy for all locales
- php bin/magento setup:static-content:deploy
de_DE en_US
-t Mironsoft/default --force
artifacts:
name: "magento-build-${CI_COMMIT_SHORT_SHA}"
paths:
- vendor/
- generated/
- pub/static/
expire_in: 2 hours
7. Common errors with frontend builds in CI
The most common error is a missing or outdated package-lock.json. When npm ci fails in the pipeline, it is almost always because the lock file no longer matches the current state of package.json. The fix: run npm install locally, commit the updated lock file, and restart the pipeline. npm ci must never silently update the lock file, that is its design principle and it protects against unintended dependency updates in CI.
A second typical error is missing CSS classes in the compiled output. When Tailwind cannot find the template files, it only generates the base styles. The fix: set explicit @source paths in theme.css pointing to the template directories. In Hyva projects those are typically the .phtml files in the theme directory and the Alpine.js components. A third error: running the build outside the theme directory, so relative paths in the Tailwind configuration cannot be resolved.
8. Frontend build strategies compared
There are several ways to handle the frontend build in Magento projects. The choice has a considerable impact on reproducibility, pipeline speed and team convenience.
| Strategy | Reproducible | Pipeline speed | Recommendation |
|---|---|---|---|
| CSS built locally, committed to the repo | No | Fast (no build) | Not recommended |
| Build on the server during deploy | Conditional | Slow (no cache) | Not recommended |
| Build in GitLab CI, artifact | Yes | Fast with cache | Recommended |
| Build in a separate job, then combined | Yes | Parallelizable | Recommended for large projects |
| Docker image with pre built assets | Yes | Very fast | For container deployments |
9. Optimizing build time
If the frontend build job takes longer than two minutes, several optimization approaches are available. The most effective one is correct npm caching: when node_modules is loaded from the cache, downloading disappears entirely. The second approach is limiting the Tailwind scan scope: with explicit @source paths pointing to the relevant template directories, Tailwind only scans a few hundred files instead of the entire project.
A third lever is running the PHP and frontend build in parallel. When both run as separate jobs in the same stage, they can execute simultaneously on different runners. A combined build job that first installs PHP dependencies and then runs the frontend build is simpler to configure, but slower than two parallel jobs. For projects with long build times, splitting them up pays off: the deploy job then waits for both via needs: ["build:php", "build:frontend"].
10. Summary
The frontend build with npm and Tailwind CSS v4 belongs in the GitLab pipeline, not on a developer's laptop or the production server. The combination of a pinned Node.js version, npm ci for deterministic dependency installs, a GitLab cache for node_modules and a lean artifact for the compiled CSS is the reproducible standard for Hyva Magento projects. This structure guarantees that every deployment on every server ends up with the same CSS state, regardless of who triggered the build.
Integrating Static Content Deploy into the same build job brings the complete pub/static/ folder into the pipeline as an artifact, so the target server no longer needs to run a single build step. That makes deployments faster, reduces server load during deployment and eliminates an entire class of errors that only show up in production because a different Node.js version is installed there than on the developer's laptop.
Frontend Builds in GitLab CI: The Essentials at a Glance
Node.js version
.nvmrc plus the node:20-alpine Docker image in the pipeline, identical to the local development environment.
npm ci instead of npm install
Deterministic install from package-lock.json, fails immediately on mismatches, mandatory in CI.
Cache vs. artifact
node_modules goes into the cache, compiled CSS goes into the artifact, never the other way around.
Limit the Tailwind scan
Restrict @source paths in theme.css to the template directories, cuts build time in half on large projects.