Facet Developer's Guide — SSR Framework Reference

Complete reference for Facet's features and capabilities

Facet is a data-driven web framework that transforms JSON documents into server-rendered HTML through path-based templates. It is a RESTHeart plugin that provides a hybrid API/UI from the same endpoint — JSON for API clients, HTML for browsers.

Core principle: Convention over Configuration. Your template structure mirrors your API paths. A request to /mydb/products automatically uses templates/mydb/products/list.html or falls back to index.html when the browser requests HTML.

Technology stack:

  • RESTHeart — MongoDB REST API server (plugin architecture, HTTP layer, auth)
  • Pebble — Template engine with Jinja2-like syntax
  • GraalVM — JDK with JavaScript interop for plugins

Table of Contents

  1. Quick Start with Docker
  2. Understanding the SSR Framework
  3. Template Structure & Conventions
  4. Authentication & Security
  5. Tutorial: Creating Your First Application
  6. Tutorial: Building Custom SSR Applications
  7. Tutorial: Selective SSR
  8. Template Context Variables
  9. Working with HTMX
  10. CRUD Operations with HTMX Fragments
  11. Handling Mutations
  12. Advanced Patterns
  13. JavaScript Plugins
  14. Configuration Reference
  15. Best Practices
  16. Troubleshooting

Quick Start with Docker

git clone https://github.com/SoftInstigate/facet.git
cd facet
docker compose up

Open http://localhost:8080/, log in with admin / secret, then visit /mydb/products to see seeded data rendered by the default template.


Understanding the SSR Framework

How It Works

The SSR framework automatically transforms REST API JSON responses into HTML using path-based template resolution:

Request: GET /mydb/products
Accept: text/html

1. MongoService returns MongoDB data
2. HtmlResponseInterceptor intercepts
3. PathBasedTemplateResolver finds template
4. Pebble renders HTML
5. Browser receives HTML page

Template Resolution Algorithm

Collection requests (e.g., GET /mydb/products):

Search order:
1. templates/mydb/products/list.html   <- recommended (explicit collection view)
2. templates/mydb/products/index.html  (optional fallback)
3. templates/mydb/list.html            (parent fallback)
4. templates/mydb/index.html           (parent fallback)
5. templates/list.html                 (global collection template)
6. templates/index.html                (global fallback)
7. No template -> return JSON unchanged

Document requests (e.g., GET /mydb/products/123):

Search order:
1. templates/mydb/products/123/view.html  (document-specific override)
2. templates/mydb/products/view.html      <- recommended (explicit document view)
3. templates/mydb/products/index.html     (optional fallback)
4. templates/mydb/view.html               (parent fallback)
5. templates/mydb/index.html              (parent fallback)
6. templates/view.html                    (global document template)
7. templates/index.html                   (global fallback)
8. No template -> return JSON unchanged

HTMX fragment requests (requests with HX-Target header):

GET /mydb/products
HX-Request: true
HX-Target: #product-list

1. templates/mydb/products/_fragments/product-list.html  (resource-specific)
2. templates/_fragments/product-list.html                 (root fallback)
3. No template -> 500 error (strict mode surfaces issues early)

Key behaviors:

  • Full-page: SSR is opt-in. No template = JSON returned unchanged
  • Fragment: strict mode. Missing fragment = 500 error
  • Full page templates walk up the directory tree; fragment templates do not

Template Naming Convention

Name Purpose
list.html Collection view — explicit, preferred
view.html Document view — explicit, preferred
index.html Shared fallback when list/view logic is identical
_fragments/ HTMX entry points (underscore = not URL-accessible)
layout.html Base layout for Pebble inheritance

Template Structure & Conventions

Recommended Directory Layout

templates/
├── layout.html              # Main layout (Pebble extends)
├── list.html                # Root list (databases)
├── error.html               # Global error page
├── _fragments/              # Shared HTMX fragments
│   └── my-fragment.html
└── mydb/
    ├── list.html            # Collections list for 'mydb'
    └── products/
        ├── list.html        # Products collection
        ├── view.html        # Single product
        └── _fragments/
            └── product-list.html  # HTMX partial for products

favicon.ico — Prevent Auth Popup

Browsers auto-request /favicon.ico. If auth is required it causes an unwanted popup. Fix:

/static-resources:
  - what: /static/favicon.ico
    where: /favicon.ico
    embedded: true

Authentication & Security

Default Credentials

RESTHeart includes a default admin user for development:

  • Username: adminPassword: secret

⚠️ Change this password before deploying to production.

File-Based Authentication (Recommended for Examples)

users.yml — credentials and role assignments:

users:
  - userid: admin
    password: secret
    roles: [admin]
  - userid: viewer
    password: viewer
    roles: [viewer]

restheart.yml — role-based permissions:

/fileAclAuthorizer:
  enabled: true
  permissions:
    - role: admin
      predicate: path-prefix[path=/]
      mongo:
        readFilter: null
        writeFilter: null       # Full read/write
    - role: viewer
      predicate: path-prefix[path=/]
      mongo:
        readFilter: null
        writeFilter: '{"_id": {"$exists": false}}'  # Read-only

Requires a restart to apply changes. For dynamic user management, see RESTHeart MongoDB-based auth.

Template Context: Auth Variables

{% if isAuthenticated %}
    <p>Logged in as: {{ username }}</p>
    <p>Roles: {{ roles | join(", ") }}</p>
    <a href="{{ loginUrl }}?logout">Logout</a>
{% else %}
    <a href="{{ loginUrl }}">Login</a>
{% endif %}

{% if roles and 'admin' in roles %}
    <button>+ Add Product</button>
{% endif %}

Tutorial: Creating Your First Application

Step 1: Create the Layout

<!-- templates/layout.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{% block title %}My Application{% endblock %}</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
    <script src="https://unpkg.com/htmx.org@2.0.8"></script>
</head>
<body>
    <nav>
        <a href="/">My App</a>
        {% if username %}<span>{{ username }}</span>{% endif %}
    </nav>
    <main class="container">
        {% block main %}{% endblock %}
    </main>
</body>
</html>

Step 2: Create the Collection List Template

<!-- templates/mydb/products/list.html -->
{% extends "layout" %}

{% block title %}Products{% endblock %}

{% block main %}
<h1>Products ({{ totalDocuments }} total)</h1>

{% if documents %}
<table>
    <thead>
        <tr><th>Name</th><th>Price</th><th>Actions</th></tr>
    </thead>
    <tbody>
        {% for doc in documents %}
        <tr>
            <td>{{ doc.name }}</td>
            <td>${{ doc.price }}</td>
            <td><a href="{{ path }}/{{ doc._id }}">View</a></td>
        </tr>
        {% endfor %}
    </tbody>
</table>

{% if totalPages > 1 %}
<nav>
    {% if page > 1 %}
        <a href="?page={{ page - 1 }}&pagesize={{ pagesize }}">Previous</a>
    {% endif %}
    <span>Page {{ page }} of {{ totalPages }}</span>
    {% if page < totalPages %}
        <a href="?page={{ page + 1 }}&pagesize={{ pagesize }}">Next</a>
    {% endif %}
</nav>
{% endif %}

{% else %}
<p>No products found.</p>
{% endif %}
{% endblock %}

Step 3: Create the Document View Template

<!-- templates/mydb/products/view.html -->
{% extends "layout" %}

{% block title %}{{ documents[0].name }}{% endblock %}

{% block main %}
{% if documents %}
    {% set product = documents[0] %}
    <article>
        <h1>{{ product.name }}</h1>
        <p class="price">${{ product.price }}</p>
        <p>{{ product.description }}</p>
        <a href="{{ path | parentPath }}">Back to Products</a>
    </article>
{% endif %}
{% endblock %}

Step 4: Create an Error Page

<!-- templates/error.html -->
{% extends "layout" %}

{% block title %}Error {{ statusCode }}{% endblock %}

{% block main %}
<div class="alert">
    <h1>{{ statusCode }} — {{ statusMessage }}</h1>
    <p>Path: <code>{{ path }}</code></p>
    <a href="/">Go Home</a>
</div>
{% endblock %}

Tutorial: Building Custom SSR Applications

Scoped Layouts per Area

Each section of your app can use its own layout:

templates/
├── layout.html              # Global layout
├── admin/
│   ├── _layouts/
│   │   └── layout.html      # Admin layout with sidebar
│   └── users/
│       └── list.html        # Uses admin layout
└── ping/
    └── index.html           # Uses minimal layout

Admin layout:

<!-- templates/admin/_layouts/layout.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Admin{% endblock %}</title>
</head>
<body>
    <nav class="sidebar">
        <a href="/admin/users">Users</a>
        <a href="/admin/settings">Settings</a>
    </nav>
    <main>{% block main %}{% endblock %}</main>
</body>
</html>

Admin users template:

<!-- templates/admin/users/list.html -->
{% extends "admin/_layouts/layout" %}

{% block title %}User Management{% endblock %}

{% block main %}
<h1>Users</h1>
<table>
    {% for doc in documents %}
    <tr>
        <td>{{ doc.username }}</td>
        <td>{{ doc.email }}</td>
        <td>
            {% for role in doc.roles %}
                <span class="badge">{{ role }}</span>
            {% endfor %}
        </td>
    </tr>
    {% endfor %}
</table>
{% endblock %}

Tutorial: Selective SSR

SSR is opt-in per resource. If no template exists, the API returns JSON unchanged.

templates/
└── shop/
    └── products/
        └── list.html   <- only products get HTML

GET /shop/products   -> HTML (template exists)
GET /shop/orders     -> JSON (no template)
GET /api/*           -> JSON (no templates at all)

Content Negotiation

The same endpoint serves both formats:

# HTML (browser default)
curl -H "Accept: text/html" http://localhost:8080/shop/products

# JSON (API client)
curl -H "Accept: application/json" http://localhost:8080/shop/products

Template Context Variables

Facet injects rich context into every template. See the Template Context Reference for a complete list.

Quick Reference

Variable Type Description
documents List Array of MongoDB documents
database String Database name
collection String Collection name
path String Full request path
page Integer Current page number
pagesize Integer Items per page
totalPages Integer Total number of pages
totalDocuments Long Total document count
isAuthenticated Boolean Whether user is logged in
username String Authenticated user name
roles Set<String> User's roles
requestMethod String HTTP method (GET, POST, PATCH, …)
filter String MongoDB filter query
sort String MongoDB sort spec
keys String Field projection

Custom Pebble Filters

Facet adds two path-manipulation filters:

{# Strip trailing slash #}
{{ "/shop/products/" | stripTrailingSlash }}  {# -> "/shop/products" #}

{# Append segment safely #}
{{ "/shop" | buildPath("products") }}         {# -> "/shop/products" #}

Use these to build navigation links that work with any MongoDB mount prefix.


Working with HTMX

Declarative HTMX (in Templates)

Control HTMX behavior directly in templates — no Java changes needed:

<!-- Partial update -->
<a href="/shop/products?sort=price"
   hx-get="/shop/products?sort=price"
   hx-target="#product-list"
   hx-swap="innerHTML"
   hx-push-url="true">
    Sort by price
</a>

<!-- Inline form submission -->
<form hx-post="/shop/products"
      hx-target="#product-list"
      hx-swap="beforeend">
    <input name="name" required>
    <button type="submit">Add</button>
</form>

HTMX Fragment Templates

When HTMX sends HX-Request: true and HX-Target: #product-list, Facet looks for a fragment template matching the target name:

HX-Target: #product-list
-> templates/shop/products/_fragments/product-list.html  (resource-specific)
-> templates/_fragments/product-list.html                 (root fallback)

Fragment templates contain only the partial HTML (no <html>, <head>, <body>):

<!-- templates/_fragments/product-list.html -->
<div id="product-list">
    {% for doc in documents %}
    <article>
        <h3>{{ doc.name }}</h3>
        <p>${{ doc.price }}</p>
    </article>
    {% endfor %}
</div>

Server-Side HTMX Control

For custom Java services that need to control client behavior from the server:

// Trigger a client-side event after DOM swap
HtmxResponseHelper.triggerEventAfterSwap(response, "productSaved", details);

// Redirect client
HtmxResponseHelper.redirect(response, "/shop/products");

// Retarget the swap element
HtmxResponseHelper.retarget(response, "#error-notifications");

// Force full-page refresh
HtmxResponseHelper.refresh(response);

// Push URL to browser history
HtmxResponseHelper.pushUrl(response, "/shop/products");

For template-only customizations, use declarative HTMX attributes. HtmxResponseHelper is for custom Java services.

Handling Server-Triggered Events (Client-Side)

Listen to events triggered by HtmxResponseHelper.triggerEventAfterSwap():

document.addEventListener('productSaved', (event) => {
    const { id, name } = event.detail;
    showToast(`Saved: ${name}`);
});

Event timing:

  • triggerEvent() — fires immediately (before DOM swap)
  • triggerEventAfterSwap() — fires after content is in DOM (most common)
  • triggerEventAfterSettle() — fires after CSS animations complete

CRUD Operations with HTMX Fragments

The pattern: URLs represent REST resources. Forms load on demand as HTMX fragments. HTTP methods are the actions.

View Product (GET)

<!-- templates/shop/products/view.html -->
{% extends "layout" %}

{% block main %}
{% if documents %}
    {% set product = documents[0] %}
    <article>
        <h1>{{ product.name }}</h1>
        <p>${{ product.price }}</p>
        <p>{{ product.description }}</p>

        <button hx-get="{{ path }}"
                hx-target="#product-form"
                hx-swap="innerHTML">
            Edit
        </button>
    </article>
    <div id="product-form"></div>
{% endif %}
{% endblock %}

Edit Product Fragment (PATCH)

Loaded dynamically via HTMX when "Edit" is clicked:

<!-- templates/_fragments/product-form.html -->
<article>
    <h2>Edit Product</h2>
    <form id="productEditForm">
        <label>Name
            <input type="text" name="name" value="{{ documents[0].name }}" required>
        </label>
        <label>Price
            <input type="number" name="price" value="{{ documents[0].price }}" step="0.01" required>
        </label>
        <button type="submit">Save</button>
    </form>
</article>

<script>
document.getElementById('productEditForm').addEventListener('submit', async function(e) {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.target));
    data.price = parseFloat(data.price);

    const response = await fetch('{{ path }}', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify(data)
    });

    if (response.ok) window.location.reload();
});
</script>

Create Product (POST)

From the list page, "+ Add Product" loads a creation fragment:

<!-- In list.html -->
<button hx-get="{{ path }}"
        hx-target="#product-new"
        hx-swap="innerHTML">
    + Add Product
</button>
<div id="product-new"></div>
<!-- templates/_fragments/product-new.html -->
<form id="productNewForm">
    <label>Name <input name="name" required></label>
    <label>Price <input name="price" type="number" step="0.01" required></label>
    <button type="submit">Create</button>
</form>
<script>
document.getElementById('productNewForm').addEventListener('submit', async function(e) {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.target));
    data.price = parseFloat(data.price);

    const response = await fetch('{{ path }}', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify(data)
    });

    if (response.ok) window.location.reload();
});
</script>

Delete Product (DELETE)

<button onclick="deleteProduct()">Delete</button>

<script>
async function deleteProduct() {
    if (!confirm('Delete this product?')) return;
    const response = await fetch('{{ path }}', {
        method: 'DELETE',
        credentials: 'include'
    });
    if (response.ok) window.location.href = '{{ path | parentPath }}';
}
</script>

Pattern Benefits

  • Clean URLs: /products/123 not /products/123/edit
  • No routing conflicts: document _id: "edit" is unambiguous
  • Progressive enhancement: works with/without JavaScript
  • Reusable fragments: same product-form.html from list and detail pages

Handling Mutations

Three patterns for responding to write operations:

Pattern 1: HTMX Fragment Swap (Recommended for SPAs)

After a mutation, return only the updated HTML fragment. The requestMethod context variable lets the same fragment render differently for GET vs POST:

<!-- templates/shop/products/_fragments/product-row.html -->
{% if requestMethod == "POST" %}
<tr>
    <td>{{ documents[0].name }}</td>
    <td>{{ documents[0].price }}</td>
    <td><span class="badge">Created</span></td>
</tr>
{% else %}
<tr>
    <td>{{ documents[0].name }}</td>
    <td>{{ documents[0].price }}</td>
</tr>
{% endif %}

Form that triggers the fragment swap:

<form hx-post="/shop/products"
      hx-target="#product-list tbody"
      hx-swap="afterbegin">
    <input name="name" required>
    <input name="price" type="number" step="0.01" required>
    <button type="submit">Add Product</button>
</form>

Pattern 2: Post-Redirect-Get (PRG)

For non-HTMX forms or when you need the browser address bar to update. RESTHeart returns 201 Created with a Location header; the browser follows it automatically to a clean GET.

This avoids the "Confirm Form Resubmission" dialog on page refresh.

Pattern 3: Inline Response with requestMethod

Show a success message on the same page after a write:

<!-- templates/shop/products/view.html -->
{% if requestMethod == "POST" %}
    <div class="alert success">Product created!</div>
{% elseif requestMethod == "PATCH" %}
    <div class="alert info">Product updated.</div>
{% endif %}

<article>
    <h1>{{ documents[0].name }}</h1>
    <p>${{ documents[0].price }}</p>
</article>

Available requestMethod values: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, OTHER

Choosing the Right Pattern

Scenario Recommended Pattern
SPA / HTMX-powered page HTMX Fragment Swap
Multi-page app, SEO URLs Post-Redirect-Get
Simple admin forms Inline requestMethod check
File uploads Post-Redirect-Get

Advanced Patterns

Template Composition with include

Break large templates into smaller reusable parts:

<!-- templates/shop/products/list.html -->
{% extends "layout" %}

{% block main %}
    {% include "shop/products/filters" %}

    <div class="products">
        {% for doc in documents %}
            {% include "shop/products/product-card" with {'product': doc} %}
        {% endfor %}
    </div>
{% endblock %}
<!-- templates/shop/products/product-card.html -->
<div class="product-card">
    <h3>{{ product.name }}</h3>
    <span class="price">${{ product.price }}</span>
</div>

Client-Side Enhancement

Hydrate server-rendered HTML with client-side behavior using Alpine.js, React, Vue, or plain JS:

<div x-data="{
    products: {{ documents | json_encode }},
    filter: '',
    get filtered() {
        return this.products.filter(p => p.name.toLowerCase().includes(this.filter.toLowerCase()));
    }
}">
    <input type="text" x-model="filter" placeholder="Filter...">

    <template x-for="p in filtered" :key="p._id">
        <div>
            <h3 x-text="p.name"></h3>
            <span x-text="'$' + p.price"></span>
        </div>
    </template>
</div>

Conditional Template Logic

{% if totalDocuments == 0 %}
    <div class="empty-state">No products found.</div>
{% elseif totalDocuments > 1000 %}
    <p>Large dataset. Use filters to narrow results.</p>
    {% include "shop/products/list-compact" %}
{% else %}
    {% include "shop/products/list-full" %}
{% endif %}

HTMX Polling

<!-- Poll every 5 seconds -->
<div hx-get="/api/status"
     hx-trigger="every 5s"
     hx-swap="innerHTML">
    Status: Loading...
</div>

JavaScript Plugins

RESTHeart runs on GraalVM, enabling server-side plugins in JavaScript with no compilation step. JavaScript plugins are hot-reloaded exactly like Pebble templates — save and the next request picks up the change.

Why JavaScript Plugins?

Java Plugin JavaScript Plugin
Compilation mvn package + restart None
Hot reload No Yes
MongoDB access @Inject("mclient") mclient global
Performance Highest Excellent (GraalVM JIT)

Writing a Service Plugin

A minimal service needs two exports in a .mjs file:

export const options = {
    name: "productStatsService",
    description: "Aggregated product statistics",
    uri: "/shop/stats",
    secured: true,
    matchPolicy: "EXACT"
};

export function handle(request, response) {
    const BsonDocument = Java.type("org.bson.BsonDocument");
    const db   = mclient.getDatabase("shop");
    const coll = db.getCollection("products", BsonDocument.class);

    let total = 0, inStock = 0, totalValue = 0;

    coll.find().forEach(doc => {
        total++;
        const price = doc.containsKey("price") ? doc.getNumber("price").doubleValue() : 0;
        const stock = doc.containsKey("stock") ? doc.getNumber("stock").intValue() : 0;
        if (stock > 0) inStock++;
        totalValue += price * stock;
    });

    response.setContent(JSON.stringify({ total, inStock, totalValue }));
    response.setContentTypeAsJson();
}

BSON accessor methods:

Type Method Returns
String doc.getString("key").getValue() String
Number doc.getNumber("key").doubleValue() double
Integer doc.getNumber("key").intValue() int
Boolean doc.getBoolean("key").getValue() boolean
Key check doc.containsKey("key") boolean

Use LOGGER.info("value: {}", someValue) for structured logging.

Plugin Manifest (package.json)

{
  "name": "my-plugin",
  "version": "1.0.0",
  "rh:services": ["my-service.mjs"],
  "rh:interceptors": []
}

Files not listed are ignored by RESTHeart.

Deploying via Docker Compose

volumes:
  # Mount each plugin folder individually
  # (preserves existing JAR plugins in /opt/restheart/plugins/)
  - ./plugins/my-plugin:/opt/restheart/plugins/my-plugin:ro

Rendering Plugin Output as HTML

Facet's HtmlResponseInterceptor intercepts responses from any service, not just MongoDB. For JavaScript services, every top-level JSON field becomes a Pebble template variable:

The plugin returns:

{ "total": 10, "avgPrice": 309.49, "categories": [...] }

The template receives total, avgPrice, categories directly as variables:

<p>{{ total }} products</p>
<p>Average: ${{ avgPrice | numberformat("#,##0.00") }}</p>
{% for cat in categories %}
    <li>{{ cat.name }}: {{ cat.count }}</li>
{% endfor %}

Template resolution for non-MongoDB services uses index.html only:

GET /shop/stats
-> templates/shop/stats/index.html   <- place your template here
-> templates/shop/index.html         (parent fallback)
-> templates/index.html              (root fallback)

Working Example

The product catalog ships a complete example:

File Purpose
plugins/product-stats/product-stats.mjs Service — queries MongoDB, returns stats JSON
plugins/product-stats/package.json Plugin manifest
templates/shop/stats/index.html Template — renders stats dashboard
# JSON (API client)
curl -u admin:secret http://localhost:8080/shop/stats

# HTML (browser / Facet template)
curl -u admin:secret -H "Accept: text/html" http://localhost:8080/shop/stats

For complete reference including TypeScript, interceptor plugins, and npm module support: RESTHeart JavaScript Plugins.


Configuration Reference

Pebble Template Processor

/pebble-template-processor:
  enabled: true
  use-file-loader: true           # Load from filesystem (enables hot-reload)
  templates-path: /opt/templates/ # Custom templates location
  cache-active: false             # Disable caching for development
  locale: en-US

HTML Response Interceptor

/html-response-interceptor:
  enabled: true
  response-caching: true   # ETag-based caching (use false in dev)
  max-age: 300             # Cache-Control max-age in seconds (5 min)

Development settings:

/html-response-interceptor:
  response-caching: false
  max-age: 5

Best Practices

  1. Start simple — begin with list.html + view.html per resource, add complexity as needed
  2. Use explicit templates — prefer list.html/view.html over index.html (cleaner, no conditional logic)
  3. Reuse with include — extract repeated UI into shared partials
  4. Scope layouts — each major section should have its own layout
  5. Test both formats — verify JSON API still works with Accept: application/json
  6. Progressive enhancement — add HTML gradually; keep JSON as the fallback
  7. Hot-reload in dev — set use-file-loader: true and cache-active: false for fast iteration
  8. Fragment strict mode — missing fragments intentionally return 500 to surface issues early

Troubleshooting

Template Not Found (Returns JSON Instead of HTML)

Check that the template exists at the correct path:

Request: GET /mydb/products
Expected: templates/mydb/products/list.html
      or: templates/mydb/products/index.html
      or: templates/list.html

Verify Accept: text/html is sent (browsers do this by default).

Wrong Layout Applied

Check the extends directive in your template — it determines which layout file is loaded.

Template Changes Not Appearing

Set in restheart.yml:

/pebble-template-processor:
  cache-active: false
  use-file-loader: true

Variables Undefined in Template

Debug by printing the context. In Pebble you can iterate _context to see all variables available in the current request.

Authentication Popup on favicon.ico

See the favicon.ico section above.


Next Steps