Library-skills selmer
Django-inspired HTML templating system for Clojure with filters, tags, and template inheritance
git clone https://github.com/hugoduncan/library-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/hugoduncan/library-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/clojure-libraries/skills/selmer" ~/.claude/skills/hugoduncan-library-skills-selmer && rm -rf "$T"
plugins/clojure-libraries/skills/selmer/SKILL.mdSelmer
Django-inspired HTML templating system for Clojure providing a fast, productive templating experience with filters, tags, template inheritance, and extensive customization options.
Overview
Selmer is a pure Clojure template engine inspired by Django's template syntax. It compiles templates at runtime, supports template inheritance and includes, provides extensive built-in filters and tags, and allows custom extensions. Designed for server-side rendering in web applications, email generation, reports, and any text-based output.
Key Features:
- Django-compatible template syntax
- Variable interpolation with nested data access
- 50+ built-in filters for data transformation
- Control flow tags (if, for, with)
- Template inheritance (extends, block, include)
- Custom filter and tag creation
- Template caching for performance
- Auto-escaping with override control
- Validation and error reporting
- Middleware support
Installation:
Leiningen:
[selmer "1.12.65"]
deps.edn:
{selmer/selmer {:mvn/version "1.12.65"}}
Quick Start:
(require '[selmer.parser :refer [render render-file]]) ;; Render string (render "Hello {{name}}!" {:name "World"}) ;=> "Hello World!" ;; Render file (render-file "templates/home.html" {:user "Alice"})
Core Concepts
Variables
Variables use
{{variable}} syntax and are replaced with values from the context map.
(render "{{greeting}} {{name}}" {:greeting "Hello" :name "Bob"}) ;=> "Hello Bob"
Nested Access:
(render "{{person.name}}" {:person {:name "Alice"}}) ;=> "Alice" (render "{{items.0.title}}" {:items [{:title "First"}]}) ;=> "First"
Missing Values:
(render "{{missing}}" {}) ;=> "" (empty string by default)
Filters
Filters transform variable values using the pipe
| operator.
(render "{{name|upper}}" {:name "alice"}) ;=> "ALICE" ;; Chain filters (render "{{text|upper|take:5}}" {:text "hello world"}) ;=> "HELLO"
Tags
Tags use
{% tag %} syntax for control flow and template structure.
{% if user %} Welcome {{user}}! {% else %} Please log in. {% endif %}
Template Inheritance
Parent template (
base.html):
<html> <head>{% block head %}Default Title{% endblock %}</head> <body>{% block content %}{% endblock %}</body> </html>
Child template:
{% extends "base.html" %} {% block head %}Custom Title{% endblock %} {% block content %}<h1>Hello!</h1>{% endblock %}
API Reference
Rendering Functions
render
renderRender a template string with context.
(render template-string context-map) (render template-string context-map options) ;; Examples (render "{{x}}" {:x 42}) ;=> "42" (render "[% x %]" {:x 42} {:tag-open "[%" :tag-close "%]"}) ;=> "42"
Parameters:
- Template as stringtemplate-string
- Data map for templatecontext-map
- Optional map withoptions
,:tag-open
,:tag-close
,:filter-open:filter-close
render-file
render-fileRender a template file from classpath or resource path.
(render-file filename context-map) (render-file filename context-map options) ;; Examples (render-file "templates/email.html" {:name "Alice"}) (render-file "custom.tpl" {:x 1} {:tag-open "<%%" :tag-close "%%>"})
File Resolution:
- Checks configured resource path
- Falls back to classpath
- Caches compiled template
Caching
cache-on!
cache-on!Enable template caching (default).
(require '[selmer.parser :refer [cache-on!]]) (cache-on!)
Templates are compiled once and cached. Use in production.
cache-off!
cache-off!Disable template caching for development.
(require '[selmer.parser :refer [cache-off!]]) (cache-off!)
Templates recompile on each render. Use during development.
Configuration
set-resource-path!
set-resource-path!Configure base path for template files.
(require '[selmer.parser :refer [set-resource-path!]]) (set-resource-path! "/var/html/templates/") (set-resource-path! nil) ; Reset to classpath
set-missing-value-formatter!
set-missing-value-formatter!Configure how missing values are rendered.
(require '[selmer.parser :refer [set-missing-value-formatter!]]) (set-missing-value-formatter! (fn [tag context-map] (str "MISSING: " tag))) (render "{{missing}}" {}) ;=> "MISSING: missing"
Introspection
known-variables
known-variablesExtract all variables from a template.
(require '[selmer.parser :refer [known-variables]]) (known-variables "{{x}} {{y.z}}") ;=> #{:x :y.z}
Useful for validation and documentation.
Validation
validate-on!
/ validate-off!
validate-on!validate-off!Control template validation.
(require '[selmer.validator :refer [validate-on! validate-off!]]) (validate-on!) ; Default - validates templates (validate-off!) ; Skip validation for performance
Validation catches undefined filters, malformed tags, and syntax errors.
Custom Filters
add-filter!
add-filter!Register a custom filter.
(require '[selmer.filters :refer [add-filter!]]) (add-filter! :shout (fn [s] (str (clojure.string/upper-case s) "!!!"))) (render "{{msg|shout}}" {:msg "hello"}) ;=> "HELLO!!!" ;; With arguments (add-filter! :repeat (fn [s n] (apply str (repeat (Integer/parseInt n) s)))) (render "{{x|repeat:3}}" {:x "ha"}) ;=> "hahaha"
remove-filter!
remove-filter!Remove a filter.
(require '[selmer.filters :refer [remove-filter!]]) (remove-filter! :shout)
Custom Tags
add-tag!
add-tag!Register a custom tag.
(require '[selmer.parser :refer [add-tag!]]) (add-tag! :uppercase (fn [args context-map] (clojure.string/upper-case (first args)))) ;; In template: {% uppercase "hello" %}
Block Tags:
(add-tag! :bold (fn [args context-map content] (str "<b>" (get-in content [:bold :content]) "</b>")) :bold :endbold) ;; In template: ;; {% bold %}text here{% endbold %}
remove-tag!
remove-tag!Remove a tag.
(require '[selmer.parser :refer [remove-tag!]]) (remove-tag! :uppercase)
Error Handling
wrap-error-page
wrap-error-pageMiddleware to display template errors with context.
(require '[selmer.middleware :refer [wrap-error-page]]) (def app (wrap-error-page handler))
Shows error message, line number, and template snippet.
Escaping Control
without-escaping
without-escapingRender template without HTML escaping.
(require '[selmer.util :refer [without-escaping]]) (render "{{html}}" {:html "<b>Bold</b>"}) ;=> "<b>Bold</b>" (without-escaping (render "{{html}}" {:html "<b>Bold</b>"})) ;=> "<b>Bold</b>"
Built-in Filters
String Filters
upper - Convert to uppercase
{{name|upper}} ; "alice" → "ALICE"
lower - Convert to lowercase
{{NAME|lower}} ; "ALICE" → "alice"
capitalize - Capitalize first letter
{{word|capitalize}} ; "hello" → "Hello"
title - Title case
{{phrase|title}} ; "hello world" → "Hello World"
addslashes - Escape quotes
{{text|addslashes}} ; "I'm" → "I\'m"
remove-tags - Strip HTML tags
{{html|remove-tags}} ; "<b>text</b>" → "text"
safe - Mark as safe (no escaping)
{{html|safe}} ; Renders HTML without escaping
replace - Replace substring
{{text|replace:"old":"new"}}
subs - Substring
{{text|subs:0:5}} ; First 5 characters
abbreviate - Truncate with ellipsis
{{text|abbreviate:10}} ; "Long text..." (max 10 chars)
Formatting Filters
date - Format date
{{timestamp|date:"yyyy-MM-dd"}} {{timestamp|date:"MMM dd, yyyy"}}
currency-format - Format currency
{{amount|currency-format}} ; 1234.5 → "$1,234.50"
double-format - Format decimal
{{number|double-format:"%.2f"}} ; 3.14159 → "3.14"
pluralize - Pluralize noun
{{count}} item{{count|pluralize}} ; 1 item, 2 items {{count}} box{{count|pluralize:"es"}} ; 1 box, 2 boxes
Collection Filters
count - Get collection size
{{items|count}} ; [1 2 3] → "3"
first - First element
{{items|first}} ; [1 2 3] → "1"
last - Last element
{{items|last}} ; [1 2 3] → "3"
join - Join with separator
{{items|join:", "}} ; [1 2 3] → "1, 2, 3"
sort - Sort collection
{{items|sort}} ; [3 1 2] → [1 2 3]
sort-by - Sort by key
{{people|sort-by:"age"}}
reverse - Reverse collection
{{items|reverse}} ; [1 2 3] → [3 2 1]
take - Take first N
{{items|take:2}} ; [1 2 3] → [1 2]
drop - Drop first N
{{items|drop:1}} ; [1 2 3] → [2 3]
Utility Filters
default - Default if falsy
{{value|default:"N/A"}}
default-if-empty - Default if empty
{{text|default-if-empty:"None"}}
hash - Compute hash
{{text|hash:"md5"}} {{text|hash:"sha256"}}
json - Convert to JSON
{{data|json}} ; {:x 1} → "{\"x\":1}"
length - String/collection length
{{text|length}} ; "hello" → "5"
Built-in Tags
Control Flow
if
/ else
/ endif
ifelseendifConditional rendering.
{% if user %} Hello {{user}}! {% else %} Please log in. {% endif %}
With operators:
{% if count > 10 %} Many items {% elif count > 0 %} Few items {% else %} No items {% endif %}
Operators:
=, !=, <, >, <=, >=, and, or, not
ifequal
/ ifunequal
ifequalifunequalCompare two values.
{% ifequal user.role "admin" %} Admin panel {% endifequal %} {% ifunequal status "active" %} Inactive {% endifunequal %}
firstof
firstofRender first truthy value.
{% firstof user.nickname user.name "Guest" %}
Loops
for
forIterate over collections.
{% for item in items %} {{forloop.counter}}. {{item}} {% endfor %}
Loop Variables:
- 1-indexed positionforloop.counter
- 0-indexed positionforloop.counter0
- True on first iterationforloop.first
- True on last iterationforloop.last
- Total itemsforloop.length
With empty:
{% for item in items %} {{item}} {% empty %} No items found {% endfor %}
Destructuring:
{% for [k v] in pairs %} {{k}}: {{v}} {% endfor %}
cycle
cycleCycle through values in a loop.
{% for item in items %} <tr class="{% cycle 'odd' 'even' %}">{{item}}</tr> {% endfor %}
Template Structure
extends
extendsInherit from parent template.
{% extends "base.html" %}
Must be first tag in template.
block
blockDefine overridable section.
Parent template:
{% block content %}Default content{% endblock %}
Child template:
{% block content %}Custom content{% endblock %}
Block super:
{% block content %} {{block.super}} Additional content {% endblock %}
include
includeInsert another template.
{% include "header.html" %}
With context:
{% include "item.html" with item=product %}
Other Tags
comment
commentTemplate comments (not rendered).
{% comment %} This won't appear in output {% endcomment %}
now
nowRender current timestamp.
{% now "yyyy-MM-dd HH:mm" %}
with
withCreate local variables.
{% with total=items|count %} Total: {{total}} {% endwith %}
verbatim
verbatimRender content without processing.
{% verbatim %} {{this}} won't be processed {% endverbatim %}
Useful for client-side templates.
script
/ style
scriptstyleInclude script/style blocks without escaping.
{% script %} var x = {{data|json}}; {% endscript %} {% style %} .class { color: {{color}}; } {% endstyle %}
debug
debugOutput context map for debugging.
{% debug %}
Common Patterns
Email Templates
(defn send-welcome-email [user] (let [html (render-file "emails/welcome.html" {:name (:name user) :activation-link (generate-link user)})] (send-email {:to (:email user) :subject "Welcome!" :body html})))
Web Page Rendering
(defn home-handler [request] {:status 200 :headers {"Content-Type" "text/html"} :body (render-file "pages/home.html" {:user (:user request) :posts (fetch-recent-posts)})})
Template Fragments
;; Reusable components (render-file "components/button.html" {:text "Click me" :action "/submit"})
Dynamic Form Generation
(render-file "forms/user-form.html" {:fields [{:name "username" :type "text"} {:name "email" :type "email"} {:name "password" :type "password"}] :action "/register"})
Report Generation
(defn generate-report [data] (render-file "reports/monthly.html" {:period (format-period) :totals (calculate-totals data) :items data :generated-at (java.time.LocalDateTime/now)}))
Template Composition
;; Base layout {% extends "layouts/main.html" %} ;; Page-specific {% block title %}Dashboard{% endblock %} {% block content %} {% include "components/stats.html" %} {% include "components/chart.html" %} {% endblock %}
Custom Marker Syntax
;; Compatible with client-side frameworks (render-file "spa.html" data {:tag-open "[%" :tag-close "%]" :filter-open "[[" :filter-close "]]"})
Validation and Error Handling
(require '[selmer.parser :refer [render-file known-variables]]) (require '[selmer.validator :refer [validate-on!]]) (validate-on!) (defn safe-render [template-name data] (try (let [required (known-variables (slurp (io/resource template-name)))] (when-not (every? #(contains? data %) required) (throw (ex-info "Missing template variables" {:required required :provided (keys data)}))) (render-file template-name data)) (catch Exception e (log/error e "Template rendering failed") "Error rendering template")))
Error Handling
Common Errors
Missing Template:
(render-file "nonexistent.html" {}) ;=> Exception: resource nonexistent.html not found
Solution: Verify file exists in classpath or resource path.
Undefined Filter:
(render "{{x|badfilter}}" {:x 1}) ;=> Exception: filter badfilter not found
Solution: Check filter name or define custom filter.
Malformed Tag:
(render "{% if %}" {}) ;=> Exception: malformed if tag
Solution: Ensure tag syntax is correct.
Error Middleware
(require '[selmer.middleware :refer [wrap-error-page]]) (def app (-> handler wrap-error-page wrap-other-middleware))
Displays detailed error page with:
- Error message
- Line number
- Template excerpt
- Context data
Validation
(require '[selmer.validator :refer [validate-on!]]) (validate-on!) (render "{% unknown-tag %}" {}) ;=> Validation error with details
Catches errors at compile time rather than runtime.
Performance Considerations
Template Caching
Enable in production:
(cache-on!)
Templates compile once, cache compiled version. Significant performance improvement.
Disable in development:
(cache-off!)
Recompiles on each render. See changes immediately.
Resource Path Configuration
(set-resource-path! "/var/templates/")
Reduces classpath scanning overhead.
Filter Performance
Expensive operations:
;; Avoid in loops {% for item in items %} {{item.data|json|hash:"sha256"}} {% endfor %} ;; Better: preprocess in Clojure (render-file "template.html" {:items (map #(assoc % :hash (compute-hash %)) items)})
Validation Overhead
;; Development (validate-on!) ;; Production (after testing) (validate-off!)
Validation adds minimal overhead but can be disabled if templates are thoroughly tested.
Template Inheritance
Shallow inheritance trees perform better than deep nesting.
Good:
base.html → page.html
Slower:
base.html → layout.html → section.html → page.html
Best Practices
- Use template caching in production
- Keep templates in dedicated directory (
)resources/templates/ - Validate templates in development
- Preprocess complex data in Clojure rather than in templates
- Use includes for reusable components
- Leverage template inheritance for consistent layouts
- Escape user content (default behavior) unless explicitly safe
- Name templates descriptively (
, notuser-profile.html
)page1.html - Document custom filters and tags
- Test templates with various data to catch edge cases
Platform Notes
Clojure: Full support, production-ready.
ClojureScript: Not supported. Selmer is JVM-only due to template compilation requiring Java classes.
Babashka: Not supported. Selmer requires classes and compilation not available in Babashka.
Alternatives for ClojureScript:
- Reagent (Hiccup-style)
- Rum
- UIx
Alternatives for Babashka:
- Hiccup
- String templates with
format