Skip to content

Custom Template Engine

Django IncludeContents provides a custom template engine that extends Django's standard template functionality with additional features while maintaining full compatibility.

Engine Features

The custom template engine (includecontents.django.DjangoTemplates) provides:

  1. HTML Component Syntax: <include:component> tags
  2. Multi-line Template Tags: Break long tags across lines
  3. Auto-loaded Template Tags: No need for {% load includecontents %}
  4. All Standard Django Features: 100% compatibility with existing templates

Installation

Replace Django's default template backend in your settings.py:

TEMPLATES = [
    {
        'BACKEND': 'includecontents.django.DjangoTemplates',
        'DIRS': [
            BASE_DIR / 'templates',
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Architecture

Template Processing Pipeline

  1. Template Loading: Standard Django template loading
  2. Preprocessing: Convert HTML component syntax to Django tags
  3. Multi-line Processing: Handle multi-line template tags
  4. Standard Compilation: Use Django's standard template compiler
  5. Rendering: Standard Django template rendering

Component Discovery

The engine automatically discovers components from:

templates/
└── components/
    ├── button.html          → <include:button>
    ├── forms/
    │   └── field.html       → <include:forms:field>
    └── ui/
        ├── card.html        → <include:ui:card>
        └── icons/
            └── star.html    → <include:ui:icons:star>

Template Tag Auto-loading

These template tags are automatically available without {% load %}:

  • includecontents / endincludecontents
  • contents / endcontents
  • wrapif / wrapelif / wrapelse / endwrapif
  • attrs
  • not filter

HTML Component Syntax Processing

Syntax Transformation

The engine transforms HTML component syntax to standard Django tags:

<!-- Input: HTML component syntax -->
<include:card title="Hello" class="my-card">
    <p>Content</p>
    <content:footer>Footer content</content:footer>
</include:card>

<!-- Transformed to: Django template tags -->
{% includecontents "components/card.html" title="Hello" class="my-card" %}
    <p>Content</p>
    {% contents footer %}Footer content{% endcontents %}
{% endincludecontents %}

Attribute Processing

Complex attribute processing handles:

  • String literals: title="Hello"
  • Template variables: title="{{ title }}"
  • Template expressions: href="{% url 'home' %}"
  • Shorthand syntax: {title}title="{{ title }}"
  • Conditional classes: class:active="{{ is_active }}"
  • Boolean attributes: disableddisabled="True"

Self-closing Components

<!-- Self-closing syntax -->
<include:icon name="star" size="24" />

<!-- Transformed to -->
{% includecontents "components/icon.html" name="star" size="24" %}{% endincludecontents %}

Multi-line Tag Processing

Line Continuation

The engine handles multi-line template tags by:

  1. Detecting unclosed tags: Tags that span multiple lines
  2. Concatenating lines: Join lines until tag is complete
  3. Preserving whitespace: Maintain proper spacing in output
  4. Error reporting: Accurate line numbers in error messages

Example Processing

<!-- Input -->
{% if user.is_authenticated 
    and user.is_staff 
    and user.has_perm('admin')
%}
    Content
{% endif %}

<!-- Processed as -->
{% if user.is_authenticated and user.is_staff and user.has_perm('admin') %}
    Content
{% endif %}

Advanced Features

Context Processors

The engine supports all standard Django context processors and custom ones:

# settings.py
TEMPLATES = [
    {
        'BACKEND': 'includecontents.django.DjangoTemplates',
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'myapp.context_processors.custom_processor',
            ],
        },
    },
]

Template Loaders

Works with all Django template loaders:

# Custom loader configuration
TEMPLATES = [
    {
        'BACKEND': 'includecontents.django.DjangoTemplates',
        'OPTIONS': {
            'loaders': [
                'django.template.loaders.filesystem.Loader',
                'django.template.loaders.app_directories.Loader',
                ('django.template.loaders.cached.Loader', [
                    'django.template.loaders.filesystem.Loader',
                    'django.template.loaders.app_directories.Loader',
                ]),
            ],
        },
    },
]

Custom Template Tags Integration

Custom template tags work seamlessly:

# myapp/templatetags/custom_tags.py
from django import template

register = template.Library()

@register.simple_tag
def custom_component(**kwargs):
    return render_to_string('components/custom.html', kwargs)
<!-- In templates -->
{% load custom_tags %}
<include:card>
    {% custom_component data=my_data %}
</include:card>

Performance Considerations

Template Caching

  • Preprocessing is cached: HTML component syntax transformation is cached
  • Standard Django caching: All Django template caching mechanisms work
  • Development vs Production: Use cached loader in production

Memory Usage

  • Minimal overhead: Engine adds minimal memory overhead
  • Template parsing: Slightly more parsing for HTML components
  • Context isolation: Each component gets isolated context (small overhead)

Optimization Tips

# Production settings
TEMPLATES = [
    {
        'BACKEND': 'includecontents.django.DjangoTemplates',
        'OPTIONS': {
            'loaders': [
                ('django.template.loaders.cached.Loader', [
                    'django.template.loaders.filesystem.Loader',
                    'django.template.loaders.app_directories.Loader',
                ]),
            ],
        },
    },
]

Debugging

Debug Information

Enable template debugging for detailed error information:

# settings.py
DEBUG = True
TEMPLATES = [
    {
        'BACKEND': 'includecontents.django.DjangoTemplates',
        'OPTIONS': {
            'debug': True,
        },
    },
]

Error Messages

The engine provides enhanced error messages:

TemplateSyntaxError: Invalid component syntax in template 'home.html' at line 15:
<include:card title="Hello>
              ^
Expected closing quote for attribute 'title'

Template Source Maps

Line numbers in errors map to original template source:

TemplateSyntaxError at /
Missing required prop 'title' for component 'card'
Template: home.html
Line: 23 (original: 23)
Component: components/card.html

Compatibility

Django Versions

  • ✅ Django 3.2 LTS
  • ✅ Django 4.0
  • ✅ Django 4.1
  • ✅ Django 4.2 LTS
  • ✅ Django 5.0

Template Engine Compatibility

Django Template Engine Specific

This custom template engine is designed specifically for Django templates. For Jinja2 users, similar functionality is provided through the IncludeContentsExtension which offers:

  • HTML component syntax via preprocessing
  • Multi-line tag support (native in Jinja2)
  • Context isolation and props system
  • Template tag functionality

Both engines provide equivalent features with different implementation approaches.

Third-party Packages

The engine works with popular Django packages:

  • Django REST Framework: API serialization with component templates
  • Django Crispy Forms: Form rendering in components
  • Django Compressor: Asset compression with component assets
  • Django Debug Toolbar: Full debugging support
  • Django Extensions: Template debugging tools

Migration from Standard Engine

Migrating is seamless - all existing templates work unchanged:

# Before
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        # ... rest of config
    },
]

# After
TEMPLATES = [
    {
        'BACKEND': 'includecontents.django.DjangoTemplates',
        # ... same config - everything else unchanged
    },
]

Customization

Engine Subclassing

Extend the engine for custom behavior:

# myapp/engine.py
from includecontents.django import DjangoTemplates

class CustomEngine(DjangoTemplates):
    def __init__(self, params):
        super().__init__(params)
        # Custom initialization

    def get_template(self, template_name):
        # Custom template loading logic
        return super().get_template(template_name)
# settings.py
TEMPLATES = [
    {
        'BACKEND': 'myapp.engine.CustomEngine',
        # ... config
    },
]

Custom Component Discovery

Override component path resolution:

class CustomEngine(DjangoTemplates):
    def resolve_component_template(self, component_name):
        # Custom component resolution logic
        if component_name.startswith('admin:'):
            return f'admin/components/{component_name[6:]}.html'
        return super().resolve_component_template(component_name)

Testing

Unit Testing Templates

Test templates using Django's test client:

from django.test import TestCase
from django.template import Template, Context

class TemplateTest(TestCase):
    def test_component_rendering(self):
        template = Template('<include:card title="Test">Content</include:card>')
        html = template.render(Context({}))
        self.assertIn('Test', html)
        self.assertIn('Content', html)

Integration Testing

Test full template rendering:

from django.test import TestCase, Client

class ComponentIntegrationTest(TestCase):
    def setUp(self):
        self.client = Client()

    def test_page_with_components(self):
        response = self.client.get('/page-with-components/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'expected-component-output')

Best Practices

1. Gradual Migration

Start using HTML components in new templates:

<!-- New templates: Use HTML component syntax -->
<include:card title="New Feature">
    Modern component syntax
</include:card>

<!-- Existing templates: Keep working as-is -->
{% load includecontents %}
{% includecontents "components/card.html" title="Existing" %}
    Legacy syntax still works
{% endincludecontents %}

2. Component Organization

Organize components logically:

templates/components/
├── ui/              # UI components
│   ├── button.html
│   ├── card.html
│   └── modal.html
├── forms/           # Form components
│   ├── field.html
│   └── fieldset.html
├── layout/          # Layout components
│   ├── header.html
│   ├── footer.html
│   └── sidebar.html
└── content/         # Content components
    ├── article.html
    └── summary.html

3. Development Workflow

# Development settings
TEMPLATES = [
    {
        'BACKEND': 'includecontents.django.DjangoTemplates',
        'OPTIONS': {
            'debug': True,
            'string_if_invalid': 'INVALID_VARIABLE_%s',
        },
    },
]

4. Production Configuration

# Production settings
TEMPLATES = [
    {
        'BACKEND': 'includecontents.django.DjangoTemplates',
        'OPTIONS': {
            'debug': False,
            'loaders': [
                ('django.template.loaders.cached.Loader', [
                    'django.template.loaders.filesystem.Loader',
                    'django.template.loaders.app_directories.Loader',
                ]),
            ],
        },
    },
]

Troubleshooting

Common Issues

Issue: Components not found

TemplateSyntaxError: Template 'components/button.html' does not exist
Solution: Ensure component files are in templates/components/ directory.

Issue: Multi-line tags not parsing

TemplateSyntaxError: Unclosed tag 'if'
Solution: Check for missing closing tags or unbalanced parentheses.

Issue: HTML syntax not working

TemplateSyntaxError: Invalid block tag 'include:card'
Solution: Verify you're using the custom engine, not standard Django engine.

Performance Issues

Monitor template rendering performance:

# Add to middleware for debugging
import time
from django.template.response import TemplateResponse

class TemplateTimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        start_time = time.time()
        response = self.get_response(request)

        if isinstance(response, TemplateResponse):
            render_time = time.time() - start_time
            print(f"Template render time: {render_time:.3f}s")

        return response

Next Steps