Skip to main content
Opal’s template system allows you to wrap your markdown content in HTML templates to create complete static websites.

Template Engines

Opal supports four popular template engines:

EJS

Embedded JavaScript templates with full JS syntax

Mustache

Logic-less templates for simple substitution

Nunjucks

Rich templating inspired by Jinja2

Liquid

Safe templates with simple syntax

Template Basics

Templates are files in your workspace with specific extensions:
  • EJS: .ejs or .eta files
  • Mustache: .mustache files
  • Nunjucks: .njk or .nunjucks files
  • Liquid: .liquid files
  • HTML: .html files (no processing)
Templates can include markdown content and have access to workspace data

Creating Templates

1

Create template file

Right-click in file tree and create a new file with template extension
2

Write template markup

Use template syntax to define structure and include content
3

Preview

Open template file to see rendered preview
4

Use in build

Templates are automatically applied during static site build

EJS Templates

EJS templates use JavaScript syntax for logic:
<!DOCTYPE html>
<html>
<head>
  <title><%= title %></title>
</head>
<body>
  <h1><%= heading %></h1>
  
  <% if (showContent) { %>
    <div class="content">
      <%- content %>
    </div>
  <% } %>
  
  <ul>
    <% items.forEach(item => { %>
      <li><%= item %></li>
    <% }) %>
  </ul>
</body>
</html>
  • <%= value %>: Output escaped value
  • <%- value %>: Output raw HTML (unescaped)
  • <% code %>: Execute JavaScript code
  • <%# comment %>: Template comments
  • <%- include('partial') %>: Include another template

Markdown in EJS

EJS templates can import markdown content:
<article>
  <%- await include('content/post.md', { process: 'markdown' }) %>
</article>
Use await include() with markdown files to process them before inclusion

Mustache Templates

Mustache provides logic-less templates:
<!DOCTYPE html>
<html>
<head>
  <title>{{title}}</title>
</head>
<body>
  <h1>{{heading}}</h1>
  
  {{#showContent}}
    <div class="content">
      {{{content}}}
    </div>
  {{/showContent}}
  
  <ul>
    {{#items}}
      <li>{{.}}</li>
    {{/items}}
  </ul>
</body>
</html>
  • {{value}}: Output escaped value
  • {{{value}}}: Output raw HTML
  • {{#section}}...{{/section}}: Conditional section
  • {{^section}}...{{/section}}: Inverted section
  • {{!comment}}: Template comments
  • {{>partial}}: Include partial template

Nunjucks Templates

Nunjucks offers rich templating features:
<!DOCTYPE html>
<html>
<head>
  <title>{{ title }}</title>
</head>
<body>
  <h1>{{ heading }}</h1>
  
  {% if showContent %}
    <div class="content">
      {{ content | safe }}
    </div>
  {% endif %}
  
  <ul>
    {% for item in items %}
      <li>{{ item }}</li>
    {% endfor %}
  </ul>
</body>
</html>
  • {{ value }}: Output escaped value
  • {{ value | safe }}: Output raw HTML
  • {% if condition %}...{% endif %}: Conditional
  • {% for item in list %}...{% endfor %}: Loop
  • {# comment #}: Template comments
  • {% include "partial.njk" %}: Include template
  • {{ value | filter }}: Apply filters

Nunjucks Filters

Apply transformations with filters:
{{ title | upper }}
{{ date | date("YYYY-MM-DD") }}
{{ content | markdown | safe }}

Liquid Templates

Liquid provides safe templating:
<!DOCTYPE html>
<html>
<head>
  <title>{{ title }}</title>
</head>
<body>
  <h1>{{ heading }}</h1>
  
  {% if showContent %}
    <div class="content">
      {{ content }}
    </div>
  {% endif %}
  
  <ul>
    {% for item in items %}
      <li>{{ item }}</li>
    {% endfor %}
  </ul>
</body>
</html>
  • {{ value }}: Output value
  • {% if condition %}...{% endif %}: Conditional
  • {% for item in array %}...{% endfor %}: Loop
  • {% comment %}...{% endcomment %}: Comments
  • {% include "partial" %}: Include template
  • {{ value | filter }}: Apply filters

Template Data

Templates have access to various data:

Available Variables

{
  // Custom data you provide
  title: "Page Title",
  content: "<p>Rendered markdown</p>",
  
  // Workspace context
  workspace: {
    name: "my-site",
    files: [...]
  },
  
  // File context
  currentFile: {
    path: "/index.md",
    name: "index.md"
  }
}

Passing Custom Data

Provide data when rendering:
const templateManager = new TemplateManager(workspace);

await templateManager.renderTemplate(
  absPath("/templates/layout.ejs"),
  {
    title: "My Page",
    author: "Jane Doe",
    date: new Date()
  }
);

Template Organization

Organize templates for maintainability:
Create base layouts:
/templates/
  base.ejs          # Main layout
  minimal.ejs       # Minimal layout
  sidebar.ejs       # Layout with sidebar

Including Partials

Reuse template fragments:
<%- include('partials/header.ejs', { title: 'Home' }) %>
<main>
  <%- content %>
</main>
<%- include('partials/footer.ejs') %>

Error Handling

When template errors occur:
  • Syntax errors: Highlighted in editor with line numbers
  • Runtime errors: Displayed with stack trace
  • Missing includes: Clear error message with path
  • Variable errors: Shows undefined variable name
Template errors prevent build completion. Fix errors before deploying.

Best Practices

  • Minimize logic in templates
  • Use helpers for complex operations
  • Separate data processing from presentation
  • Prefer partials over duplication
Choose the right template engine:
  • EJS: When you need JavaScript logic
  • Mustache: For simple variable substitution
  • Nunjucks: For feature-rich templates
  • Liquid: For safe, sandboxed templates
Optimize performance:
  • Pre-compute expensive operations
  • Reuse data across templates
  • Minimize file system access
  • Use workspace data efficiently
Track template changes:
  • Commit templates to git
  • Document template variables
  • Test templates before deploying
  • Maintain changelog for major changes

Advanced Features

Custom Filters

Extend template engines with custom filters:
// Add custom filter for date formatting
const formatted = value.toLocaleDateString('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
});

Template Inheritance

Create template hierarchies:
{# base.njk #}
<!DOCTYPE html>
<html>
<head>
  {% block head %}
    <title>{% block title %}{% endblock %}</title>
  {% endblock %}
</head>
<body>
  {% block content %}{% endblock %}
</body>
</html>

{# page.njk #}
{% extends "base.njk" %}

{% block title %}My Page{% endblock %}

{% block content %}
  <h1>Welcome</h1>
{% endblock %}
Template inheritance is supported in Nunjucks and Liquid

Debugging Templates

Troubleshoot template issues:
1

Check syntax

Verify template syntax is correct for the engine
2

Inspect variables

Log available data to console
3

Test partials

Verify included templates exist and work
4

Preview output

Use live preview to see rendered result
Enable detailed error messages in development mode for better debugging

Build docs developers (and LLMs) love