Frontend Assets Since 3.5.0
T3 doesn’t own your CSS or JavaScript build pipeline. You choose the tools, write the source files, and output the compiled assets to a public directory. T3’s asset functions (cms.builder.css(), cms.builder.js(), etc.) handle loading them into your templates with automatic cache busting.
This guide covers the recommended setup using Vite, with notes on alternatives at the end.
Quick Start
Section titled “Quick Start”# From your project root (same level as tcms-data/)npm create vite@latest frontend -- --template vanillacd frontendnpm installDirectory Structure
Section titled “Directory Structure”A typical T3 builder project looks like this:
project/├── tcms-data/│ └── builder/│ ├── layouts/│ ├── pages/│ └── partials/├── public/ ← docroot (web server serves this)│ ├── assets/ ← compiled output (Vite writes here)│ │ ├── style.css│ │ ├── app.js│ │ └── manifest.json│ └── index.php ← T3 entry point└── frontend/ ← source files (not public) ├── src/ │ ├── css/ │ │ └── style.css │ └── js/ │ └── app.js ├── vite.config.js └── package.jsonSource files live outside the docroot in frontend/. Vite compiles them to public/assets/ where the web server serves them directly.
Vite Configuration
Section titled “Vite Configuration”import { defineConfig } from 'vite'import { resolve } from 'path'
export default defineConfig({ build: { // Output to the public assets directory outDir: resolve(__dirname, '../public/assets'), emptyOutDir: true,
// Generate manifest.json for hashed filenames manifest: true,
rollupOptions: { input: { style: resolve(__dirname, 'src/css/style.css'), app: resolve(__dirname, 'src/js/app.js'), }, }, },})What This Does
Section titled “What This Does”outDir— writes compiled files topublic/assets/manifest: true— generatesmanifest.jsonso T3 can resolve hashed filenamesrollupOptions.input— defines your entry points (add as many as needed)
Package Scripts
Section titled “Package Scripts”{ "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }}Development Workflow
Section titled “Development Workflow”Without Vite Dev Server
Section titled “Without Vite Dev Server”The simplest approach — run npm run build after making changes:
cd frontendnpm run buildVite compiles your source files to public/assets/. T3’s asset functions pick them up automatically via mtime cache busting.
For faster iteration, use Vite’s watch mode:
npm run build -- --watchWith Vite Dev Server
Section titled “With Vite Dev Server”For hot module replacement (HMR) during development, run the Vite dev server and reference its URL directly in your layout during development:
{# layouts/default.twig #}{% if cms.env == 'development' %} <script type="module" src="http://localhost:5173/src/js/app.js"></script>{% else %} {{ cms.builder.css('style.css') }} {{ cms.builder.js('app.js', {module: true}) }}{% endif %}Start the dev server:
cd frontendnpm run devThe Vite dev server handles CSS injection via JavaScript in dev mode, so you only need the JS script tag. In production, cms.builder.css() and cms.builder.js() load the compiled, hashed files from the manifest.
Production Build
Section titled “Production Build”cd frontendnpm run buildThis generates hashed filenames and a manifest.json:
public/assets/├── style.a1b2c3d4.css├── app.e5f6a7b8.js└── manifest.jsonT3’s asset functions read the manifest and output the correct filenames:
{{ cms.builder.css('src/css/style.css') }}{# Output: <link rel="stylesheet" href="/assets/style.a1b2c3d4.css"> #}
{{ cms.builder.js('src/js/app.js', {module: true}) }}{# Output: <script type="module" src="/assets/app.e5f6a7b8.js"></script> #}The path you pass to css() and js() is the manifest key (the input path from your Vite config), not the output filename.
Using Tailwind CSS
Section titled “Using Tailwind CSS”Tailwind works naturally with this setup:
cd frontendnpm install -D tailwindcss @tailwindcss/viteimport { defineConfig } from 'vite'import { resolve } from 'path'import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ plugins: [tailwindcss()], build: { outDir: resolve(__dirname, '../public/assets'), emptyOutDir: true, manifest: true, rollupOptions: { input: { style: resolve(__dirname, 'src/css/style.css'), app: resolve(__dirname, 'src/js/app.js'), }, }, },})@import "tailwindcss";Point Tailwind at your templates so it can scan for classes:
@import "tailwindcss";@source "../../tcms-data/builder/**/*.twig";Using Sass/SCSS
Section titled “Using Sass/SCSS”cd frontendnpm install -D sassVite handles .scss files automatically — just change your entry point:
rollupOptions: { input: { style: resolve(__dirname, 'src/css/style.scss'), app: resolve(__dirname, 'src/js/app.js'), },},Layout Template Example
Section titled “Layout Template Example”A complete layout using all asset functions:
<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{% block title %}{{ page.title }}{% endblock %}</title> <meta name="description" content="{{ page.description }}">
{{ cms.builder.preload('fonts/inter.woff2', 'font') }} {{ cms.builder.css('src/css/style.css') }}</head><body> {% include 'partials/nav.twig' %}
<main>{% block content %}{% endblock %}</main>
{% include 'partials/footer.twig' %}
{{ cms.builder.js('src/js/app.js', {module: true}) }}</body></html>Without a Build Tool
Section titled “Without a Build Tool”If you don’t need a build pipeline, just place plain CSS and JS files directly in public/assets/:
public/assets/├── style.css└── app.js{{ cms.builder.css('style.css') }}{{ cms.builder.js('app.js') }}T3 adds ?v={mtime} query strings for cache busting. No manifest, no build step — just files.
Alternatives to Vite
Section titled “Alternatives to Vite”esbuild
Section titled “esbuild”const esbuild = require('esbuild')
esbuild.build({ entryPoints: ['src/css/style.css', 'src/js/app.js'], outdir: '../public/assets', bundle: true, minify: true,})esbuild doesn’t generate a Vite-compatible manifest by default. You can use the esbuild-plugin-manifest package or rely on mtime cache busting.
Webpack
Section titled “Webpack”Webpack works but is heavier than Vite for this use case. Use webpack-manifest-plugin to generate a compatible manifest.json.
See Also
Section titled “See Also”- Builder Twig Reference —
css(),js(),asset(),preload()documentation - Site Builder Overview