This guide covers common issues specific to Grav CMS sites. Issues often relate to caching, Twig template rendering, and plugin interactions.
Common Issues Overview
Grav is a fast, simple, and flexible file-based CMS. Common troubleshooting scenarios involve cache management, Twig template debugging, plugin conflicts, and ensuring analytics work correctly with Grav's caching system.
Common Issue Categories
Performance Issues
- Cache clearing and analytics
- Plugin performance impact
- Large site performance
- Image processing overhead
Tracking Issues
Installation Problems
Grav Installation Issues
Server Requirements Not Met
Symptoms:
- White screen on installation
- PHP errors
- Missing extensions warnings
Check requirements:
# PHP version (needs 7.3.6+, recommend 8.1+)
php -v
# Required extensions
php -m | grep -E "(curl|ctype|dom|gd|json|mbstring|openssl|session|simplexml|xml|zip)"
Install missing extensions:
# Ubuntu/Debian
sudo apt-get install php-curl php-gd php-mbstring php-xml php-zip
# CentOS/RHEL
sudo yum install php-curl php-gd php-mbstring php-xml php-zip
Permissions Issues
Error: "Unable to write to cache folder"
Fix permissions:
# From Grav root directory
cd /path/to/grav
# Set correct permissions
find . -type f | xargs chmod 644
find ./bin -type f | xargs chmod 755
find . -type d | xargs chmod 755
find . -type d | xargs chmod +s
# Ensure cache and logs are writable
chmod -R 775 cache/ logs/ images/ assets/ tmp/
Plugin Installation
Plugin Not Activating
Install via admin:
- Admin → Plugins
- Click "+ Add"
- Search for plugin
- Click "Install"
Install via CLI:
# From Grav root
bin/gpm install plugin-name
# List available plugins
bin/gpm index
# Update plugins
bin/gpm update
Manual installation:
# Download plugin to user/plugins/
cd user/plugins
git clone https://github.com/getgrav/grav-plugin-name.git plugin-name
Clear cache after install:
bin/grav cache --clear
Analytics Plugin Setup
Google Analytics Plugin
Install:
bin/gpm install google-analytics
Configure:
# user/config/plugins/google-analytics.yaml
enabled: true
tracking_id: 'G-XXXXXXXXXX'
position: 'head' # or 'body'
objectType: 'gtag' # or 'analytics'
forceSsl: true
anonymizeIp: true
Alternative - Manual Implementation:
{# user/themes/mytheme/templates/partials/analytics.html.twig #}
{% if not grav.user.username %}
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
{% endif %}
Include in base template:
{# user/themes/mytheme/templates/partials/base.html.twig #}
<head>
{% block head %}
{% include 'partials/analytics.html.twig' %}
{% endblock %}
</head>
Configuration Issues
| Issue | Symptoms | Common Causes | Solutions |
|---|---|---|---|
| Cache Not Clearing | Changes not appearing | Cache persists | Run bin/grav cache --clear or disable cache in dev |
| Admin Tracking | Own visits in analytics | No admin exclusion | Add {\% if not grav.user.username \%} check |
| Twig Variables Missing | Template errors | Variable not defined | Use {\{ variable|default('') \}\} |
| Plugin Conflicts | Site breaks after plugin install | Plugin incompatibility | Disable plugins one by one to isolate |
| Markdown Not Rendering | Raw markdown showing | Markdown plugin disabled | Enable Markdown plugin |
| Images Not Loading | Broken image links | Wrong path or cache | Check paths and clear image cache |
| Form Not Submitting | Form errors | Form plugin misconfigured | Check form plugin config and CSRF |
| Multi-language Issues | Wrong language showing | Language config incorrect | Verify language plugin settings |
| Sitemap Not Updating | Old sitemap | Cache not cleared | Clear cache and rebuild sitemap |
| Cached Pages Stale | Old content showing | Cache timeout too long | Reduce cache lifetime or clear cache |
Debugging with Developer Tools
Grav Debug Mode
Enable Debug Bar
Activate debugger:
# user/config/system.yaml
debugger:
enabled: true
provider: clockwork # or debugbar
censored: false
Debug bar shows:
- PHP errors and warnings
- Database queries (if using DB)
- Twig rendering time
- Memory usage
- Loaded plugins
- Cache statistics
Twig Debug Mode
Enable Twig debugging:
# user/config/system.yaml
twig:
cache: false # Disable cache during development
debug: true # Enable debug mode
auto_reload: true # Auto-reload templates
autoescape: false
Debug in templates:
{# Dump all available variables #}
{{ dump() }}
{# Dump specific variable #}
{{ dump(page) }}
{{ dump(page.header) }}
{{ dump(config) }}
{# Check if variable exists #}
{% if page.header.custom_field is defined %}
{{ page.header.custom_field }}
{% endif %}
{# Output variable type #}
Type: {{ attribute(page, 'header')|type }}
CLI Debugging Tools
Cache Management
Clear all cache:
# Clear everything
bin/grav cache --clear
# Clear specific cache types
bin/grav cache --clear-images
bin/grav cache --clear-cache
bin/grav cache --clear-compiled
# Purge cache (more aggressive)
bin/grav cache --purge
Check cache info:
# View cache statistics
bin/grav cache --stats
Check Configuration
List all settings:
# Show all system config
bin/plugin dev dump-system
Check plugin status:
# List installed plugins
bin/gpm list
# Check for updates
bin/gpm update
Log File Analysis
Check error logs:
# View recent errors
tail -f logs/grav.log
# Search for specific errors
grep "ERROR" logs/grav.log
# Check PHP errors
tail -f /var/log/php_errors.log
Enable detailed logging:
# user/config/system.yaml
errors:
display: 1
log: true
Platform-Specific Challenges
Caching System
Analytics Not Updating with Cache
Problem: Cached pages serve old analytics code
Solution 1 - Exclude analytics from cache:
# user/config/system.yaml
cache:
enabled: true
check:
method: file
driver: auto
prefix: 'g'
lifetime: 604800 # 7 days
gzip: false
# Don't cache pages with dynamic analytics
pages:
never_cache_twig: true # For pages with dynamic content
Solution 2 - Use Twig's nocache:
{# Don't cache this block #}
{% cache 0 %}
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
{% endcache %}
Solution 3 - Disable cache for specific pages:
# In page frontmatter (user/pages/01.home/default.md)
---
title: Home
cache_enable: false
---
Page content here
Admin Session Exclusion
Don't track logged-in users:
{% if not grav.user.username %}
{# Analytics code only for non-admin users #}
{% include 'partials/analytics.html.twig' %}
{% endif %}
More robust check:
{% set user = grav.user %}
{% if not user.authenticated %}
{# Analytics for guests only #}
{% endif %}
Twig Template Rendering
Tracking Page Views
Get page metadata for analytics:
<script>
// Track page with metadata
gtag('event', 'page_view', {
'page_title': '{{ page.title|e('js') }}',
'page_location': '{{ page.url(true)|e('js') }}',
'page_path': '{{ page.route|e('js') }}',
'content_group': '{{ page.taxonomy.category[0]|default('')|e('js') }}'
});
</script>
Track custom events:
{# Track downloads #}
{% for file in page.media.files %}
<a href="{{ file.url }}" 'file_download', {
'file_name': '{{ file.filename|e('js') }}',
'file_extension': '{{ file.extension|e('js') }}'
});">
Download {{ file.filename }}
</a>
{% endfor %}
Dynamic Content Tracking
Track taxonomy-based content:
{% set category = page.taxonomy.category[0]|default('uncategorized') %}
{% set tags = page.taxonomy.tag|join(', ')|default('none') %}
<script>
gtag('config', 'G-XXXXXXXXXX', {
'custom_map': {
'dimension1': 'content_category',
'dimension2': 'content_tags'
}
});
gtag('event', 'page_view', {
'content_category': '{{ category|e('js') }}',
'content_tags': '{{ tags|e('js') }}'
});
</script>
Plugin Event System
Tracking with Plugin Events
Create custom plugin:
# Create plugin structure
mkdir -p user/plugins/analytics-tracker
Plugin file:
<?php
// user/plugins/analytics-tracker/analytics-tracker.php
namespace Grav\Plugin;
use Grav\Common\Plugin;
class AnalyticsTrackerPlugin extends Plugin
{
public static function getSubscribedEvents()
{
return [
'onPluginsInitialized' => ['onPluginsInitialized', 0]
];
}
public function onPluginsInitialized()
{
if ($this->isAdmin()) {
return;
}
$this->enable([
'onPageContentRaw' => ['onPageContentRaw', 0],
'onOutputGenerated' => ['onOutputGenerated', 0]
]);
}
public function onOutputGenerated()
{
// Don't track admin users
if ($this->grav['user']->authenticated) {
return;
}
// Inject analytics only for non-cached output
$trackingCode = $this->getTrackingCode();
$output = $this->grav->output;
// Insert before </head>
$output = str_replace('</head>', $trackingCode . '</head>', $output);
$this->grav->output = $output;
}
private function getTrackingCode()
{
$config = $this->config->get('plugins.analytics-tracker');
$trackingId = $config['tracking_id'] ?? '';
if (empty($trackingId)) {
return '';
}
return <<<HTML
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={$trackingId}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{$trackingId}');
</script>
HTML;
}
}
Plugin config:
# user/plugins/analytics-tracker/analytics-tracker.yaml
enabled: true
tracking_id: 'G-XXXXXXXXXX'
Multi-Language Sites
Tracking Different Languages
Language-specific analytics:
{% set language = grav.language.getActive() %}
<script>
gtag('config', 'G-XXXXXXXXXX', {
'language': '{{ language }}',
'content_group': 'Lang: {{ language }}'
});
</script>
Different tracking IDs per language:
# user/config/site.yaml
google_analytics:
en: 'G-AAAAAAAAAA'
es: 'G-BBBBBBBBBB'
fr: 'G-CCCCCCCCCC'
{% set language = grav.language.getActive() %}
{% set tracking_id = config.site.google_analytics[language]|default('') %}
{% if tracking_id %}
<script async src="https://www.googletagmanager.com/gtag/js?id={{ tracking_id }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ tracking_id }}');
</script>
{% endif %}
Error Messages and Solutions
Common Grav Errors
"Unable to locate template"
Error:
Twig\Error\LoaderError: Unable to locate template file: partials/base.html.twig
Check template paths:
# Template search order:
# 1. user/themes/mytheme/templates/
# 2. user/themes/mytheme/templates/partials/
# 3. system/templates/
# Verify file exists
ls -la user/themes/*/templates/partials/base.html.twig
"Undefined variable"
Error in Twig:
Twig\Error\RuntimeError: Variable "custom_field" does not exist
Safe access:
{# Wrong - will error if undefined #}
{{ page.header.custom_field }}
{# Correct - with default #}
{{ page.header.custom_field|default('') }}
{# Or check existence #}
{% if page.header.custom_field is defined %}
{{ page.header.custom_field }}
{% endif %}
"Cache directory not writable"
Error:
RuntimeException: Cache directory /path/to/grav/cache is not writable
Fix:
# Set proper permissions
chmod -R 775 cache/
chown -R www-data:www-data cache/
# Or for development
chmod -R 777 cache/
Plugin Conflicts
Identify conflicting plugins:
# Disable all plugins
bin/gpm disable-all-plugins
# Enable one by one
cd user/plugins
for plugin in */; do
echo "Testing: $plugin"
# Enable plugin and test
done
Performance Problems
Slow Page Load
Enable performance optimizations:
# user/config/system.yaml
cache:
enabled: true
check:
method: file
driver: auto
prefix: 'g'
lifetime: 604800
gzip: true # Enable gzip
assets:
css_pipeline: true
css_minify: true
js_pipeline: true
js_minify: true
pages:
markdown:
extra: true
process:
markdown: true
twig: false # Disable Twig processing if not needed
Optimize images:
# Install image optimization plugin
bin/gpm install image-optimize
# Or manually optimize
find user/pages -name "*.jpg" -exec jpegoptim --max=85 {} \;
find user/pages -name "*.png" -exec optipng -o5 {} \;
Large Site Performance
Pagination for collections:
# In page header
content:
items: '@self.children'
order:
by: date
dir: desc
limit: 10 # Items per page
pagination: true
Lazy load images:
{% for image in page.media.images %}
<img src="{{ image.url }}"
loading="lazy"
alt="{{ image.meta.alt|default('') }}">
{% endfor %}
When to Contact Support
Grav Community Support
Contact when:
- Bug in Grav core
- Plugin issues
- Theme development questions
- General configuration help
Support channels:
- Forum: https://getgrav.org/forum
- Discord: https://chat.getgrav.org
- GitHub Issues: https://github.com/getgrav/grav/issues
- Documentation: https://learn.getgrav.org
When to Hire a Developer
Complex scenarios:
- Custom plugin development
- Complex theme customization
- Performance optimization for large sites
- Custom analytics integration
- Multi-language site architecture
- E-commerce integration
- Custom API development
- Migration from other platforms
Advanced Troubleshooting
Complete Debug Setup
Development environment:
# user/config/system.yaml
debugger:
enabled: true
provider: debugbar
censored: false
errors:
display: 1
log: true
twig:
cache: false
debug: true
auto_reload: true
cache:
enabled: false # Disable during development
Performance Profiling
Enable profiling:
# Install Xdebug
sudo apt-get install php-xdebug
# Configure
# php.ini:
xdebug.mode=profile
xdebug.output_dir=/tmp/xdebug
Analyze with:
- Webgrind
- KCacheGrind
- Blackfire.io
Related Global Guides
For platform-agnostic troubleshooting, see: