Grav Theme Assets — CSS/JS Pipeline, Merging and Minification
In this tutorial, you'll learn Grav theme asset management — the CSS/JS pipeline, asset merging and minification, loading strategies (async, defer, inline), and performance optimization for production delivery.
What You'll Learn
- Grav's asset manager: how it works and why it matters
- Registering CSS and JS assets in templates and plugins
- Asset merging and minification for production
- Async and deferred script loading
- Inline CSS and JS for critical resources
- Pipeline priority and ordering strategies
Why It Matters
In WordPress, enqueuing scripts and styles uses PHP functions in functions.php. In Grav, the asset manager is a Twig-based pipeline. You register assets in templates, plugins, or theme files, and Grav handles merging, minification, and delivery. A well-configured asset pipeline can reduce page weight by 60-80%, improve load times, and increase Lighthouse scores. This is especially important for sites that use multiple plugins, each adding their own CSS and JS.
Real-World Use
A marketing site uses 12 plugins, each adding its own CSS and JS files. Without asset management, the page loads 24 separate files — 12 CSS and 12 JS — causing 24 HTTP requests. Using Grav's asset pipeline, all CSS is merged into one file and minified. All JS is merged into another and minified. The result: 2 HTTP requests instead of 24, and a page weight reduction from 400KB to 180KB.
Learning Path
flowchart LR
A["Theme Configuration"] --> B["Theme Assets
← You are here"]:::current
B --> C["Theme Inheritance"]
C --> D["Theme Languages"]
D --> E["Plugin Architecture"]
E --> F["Plugin Events"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Asset Manager Overview
Grav's asset manager is a Twig-based system that collects, processes, and outputs CSS and JS assets. It supports:
- Registration: Add assets from templates, plugins, and themes
- Ordering: Priority-based loading order
- Merging: Combine multiple files into one
- Minification: Remove whitespace and comments
- Pipeline: Buffer and inline assets for performance
Registering Assets in Templates
Basic CSS Registration
{% do assets.addCss('theme://css/styles.css') %}
{% do assets.addCss('theme://css/custom.css', 10) %} {# Priority 10 #}
Basic JS Registration
{% do assets.addJs('theme://js/main.js') %}
{% do assets.addJs('theme://js/vendor.js', { priority: 10 }) %}
Registration with Attributes
{% do assets.addCss('theme://css/print.css', {
priority: 5,
media: 'print',
group: 'critical'
}) %}
{% do assets.addJs('theme://js/analytics.js', {
priority: 1,
loading: 'async',
group: 'footer'
}) %}
Outputting Assets
<!DOCTYPE html>
<html>
<head>
{% block stylesheets %}
{% do assets.addCss('theme://css/styles.css') %}
{{ assets.css() }}
{% endblock %}
</head>
<body>
{{ page.content|raw }}
{% block javascripts %}
{% do assets.addJs('theme://js/main.js') %}
{% do assets.addJs('theme://js/vendor.js') %}
{{ assets.js() }}
{% endblock %}
</body>
</html>
Pipeline Configuration
Enable the asset pipeline in user/config/system.yaml:
system:
assets:
css_pipeline: true
css_minify: true
js_pipeline: true
js_minify: true
What the Pipeline Does
When enabled, the asset manager:
- Collects all registered CSS/JS files
- Reads their contents
- Minifies them (removes whitespace, comments, shortens variable names)
- Merges them into a single file
- Caches the merged file
- Outputs a single
<link>or<script>tag
Result:
<!-- Before pipeline: 12 separate files -->
<link rel="stylesheet" href="file1.css" />
<link rel="stylesheet" href="file2.css" />
<!-- ... 10 more ... -->
<!-- After pipeline: 1 merged file -->
<link rel="stylesheet" href="/assets/merged-abc123.css" />
Asset Groups
Group assets to output them in different locations:
{% do assets.addCss('theme://css/critical.css', { group: 'critical' }) %}
{% do assets.addCss('theme://css/styles.css', { group: 'head' }) %}
{% do assets.addJs('theme://js/analytics.js', { group: 'head' }) %}
{% do assets.addJs('theme://js/main.js', { group: 'footer' }) %}
{# Output in head #}
{{ assets.css('critical') }}
{{ assets.css('head') }}
{{ assets.js('head') }}
{# Output at end of body #}
{{ assets.js('footer') }}
Async and Defer Loading
{# Async - load in background, execute when ready #}
{% do assets.addJs('theme://js/analytics.js', {
loading: 'async',
group: 'footer'
}) %}
{# Defer - load in background, execute after HTML parsed #}
{% do assets.addJs('theme://js/main.js', {
loading: 'defer',
group: 'footer'
}) %}
{# Render output #}
{{ assets.js('footer') }}
Output:
<script src="/assets/analytics.js" async></script>
<script src="/assets/main.js" defer></script>
Inline Assets
Inline CSS
{% do assets.addInlineCss("
body { background: #f5f5f5; }
h1 { color: var(--primary); }
") %}
Inline JS
{% do assets.addInlineJs("
document.addEventListener('DOMContentLoaded', function() {
console.log('Page loaded');
});
") %}
Critical CSS Pattern
Inline critical CSS in the head for fast first paint:
<head>
<style>
{% do assets.addInlineCss(critical_css) %}
{{ assets.css('critical') }}
</style>
<link rel="preload" href="{{ url('theme://css/styles.css') }}" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="{{ url('theme://css/styles.css') }}"></noscript>
</head>
Asset Priority
Control load order with priority values (lower loads first):
{% do assets.addCss('theme://css/reset.css', { priority: 1 }) %} {# Load first #}
{% do assets.addCss('theme://css/styles.css', { priority: 5 }) %} {# Load second #}
{% do assets.addCss('theme://css/custom.css', { priority: 10 }) %} {# Load third #}
Priority range: 1 (earliest) to 10 (latest). Default is 5.
Registering Assets from Plugins
Plugins can register assets on events:
<?php
namespace Grav\Plugin;
use Grav\Common\Plugin;
class MyPlugin extends Plugin
{
public static function getSubscribedEvents()
{
return [
'onAssetsInitialized' => ['onAssetsInitialized', 0],
];
}
public function onAssetsInitialized()
{
$this->grav['assets']->addCss('plugin://myplugin/css/style.css');
$this->grav['assets']->addJs('plugin://myplugin/js/script.js');
}
}
Remote Assets
{% do assets.addCss('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap') %}
{% do assets.addJs('https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js', {
loading: 'defer'
}) %}
Cache Busting
Grav automatically adds a version query string to pipeline assets:
<link rel="stylesheet" href="/assets/merged/styles-abc123.css?v=2.3.1" />
The version changes when you modify any source file, forcing browsers to download the new version.
Learning Path
flowchart LR
A["Theme Configuration"] --> B["Theme Assets
← You are here"]:::current
B --> C["Theme Inheritance"]
C --> D["Theme Languages"]
D --> E["Plugin Architecture"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Common Mistakes
Enabling pipeline without testing: Merging assets can break the site if CSS or JS files are not compatible. Test with pipeline enabled on a staging environment first.
Not using
theme://stream wrapper: Always usetheme://css/style.cssinstead of relative paths. The stream wrapper ensures the correct theme directory is used, even with child themes.Forgetting to output assets: Registering assets with
{% do assets.addCss() %}does not output them. You must call{{ assets.css() }}in the template for the assets to appear in the HTML.Mixing pipelined and non-pipelined assets: Once the pipeline is enabled, all assets are merged. If you need some assets excluded from the pipeline, use the
pipeline: falseattribute:{% do assets.addJs('theme://js/lazy.js', { pipeline: false }) %}.Not setting loading strategy for non-critical JS: All JavaScript blocks HTML Parsing by default. Use
loading: 'defer'for all non-critical scripts andloading: 'async'for independent scripts.
Practice Questions
What does
css_pipeline: truedo in Grav? Answer: It merges all CSS files into a single file and minifies it. This reduces HTTP requests from N files to 1, and reduces file size by 30-60%.How do you load a JavaScript file with the
deferattribute? Answer: Use{% do assets.addJs('theme://js/main.js', { loading: 'defer' }) %}. This tells the browser to download the script in parallel and execute it after parsing the HTML.What is the difference between
assets.addInlineCss()andassets.addCss()? Answer:addInlineCss()adds CSS content directly into a<style>tag in the HTML, without a separate file.addCss()adds a<link>tag pointing to an external CSS file.How do you control the order in which CSS files load? Answer: Use the
priorityparameter. Lower priority values load first. Default is 5. Range is 1 (earliest) to 10 (latest).Challenge: Build a complete asset loading strategy for a production Grav site. Configure pipeline, minification, and cache busting. Separate assets into critical (inline in head), render-blocking (pipelined in head), and non-critical (deferred in footer). Create a Twig template that loads Google Fonts with the correct preload strategy, loads legacy CSS without pipeline, loads analytics JS asynchronously, and loads main JS deferred. Measure page load with and without the pipeline to demonstrate the improvement.
FAQ
Mini Project
Goal: Build and optimize a complete asset loading system for a Grav theme.
- Create a CSS directory with 6 files: reset.css, variables.css, typography.css, layout.css, components.css, utilities.css
- Create a JS directory with 3 files: vendor.js (third-party libraries), app.js (main application), analytics.js (tracking)
- Register all assets with appropriate priority and groups
- Configure pipeline and minification in system.yaml
- Create a critical CSS file with above-the-fold styles and inline it
- Load non-critical styles with preload and loadCSS fallback
- Defer all JavaScript except critical inline scripts
- Create a production vs development asset configuration
- Test page load with and without pipeline (use browser DevTools network tab)
- Measure and document the performance improvement
What's Next
Now you can optimize assets for performance. Next, learn theme inheritance:
Continue to Lesson 22: Theme Inheritance — Child themes, overriding parent templates, and theme customization.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro