Custom Templates
Categories (for Groups), Note Types (for Notes), and Resource Categories (for Resources) support custom HTML templates that create specialized views for different content types.
Custom templates execute arbitrary HTML and JavaScript. Only use them on trusted network deployments.
Do not allow untrusted users to create or edit Categories, Resource Categories, and Note Types with custom templates, as they could inject malicious scripts.
Template Locations
Each Category (for Groups), Resource Category (for Resources), and Note Type (for Notes) can define four custom templates:
| Template | Display Location |
|---|---|
| CustomHeader | Top of the entity detail page (body area) |
| CustomSidebar | Sidebar of the entity detail page |
| CustomSummary | Entity cards in list views |
| CustomAvatar | Avatar/icon when linking to the entity |
How Custom Templates Are Rendered
Custom template content is processed in two ways:
- Shortcodes (
[meta],[property],[mrql], and plugin shortcodes) are expanded server-side. - Alpine.js directives (
x-text,x-if,:class,@click, etc.) work in CustomHeader, CustomSidebar, CustomSummary, and CustomAvatar because the outer page template wraps custom content in anx-datascope with the full entity available asentity. Alpine directives do not work incustomMRQLResulttemplates, which are rendered server-side by the shortcode engine -- use shortcodes ([meta],[property],[conditional]) instead.
Custom template content is not evaluated as a Pongo2 template. Expressions like {{ group.Name }} or {{ group|json }} will appear as literal text in the rendered HTML. Use Alpine.js directives or shortcodes instead.
Accessing Entity Data
The entity is already available as entity in the Alpine.js scope. You do not need to add an x-data wrapper -- the outer template provides it.
<!-- Your template content -- no x-data wrapper needed -->
<h2 x-text="entity.Name"></h2>
<p x-text="entity.Description"></p>
For Groups, the entity includes:
ID,Name,Description,URLCategoryId,OwnerIdMeta(JSON metadata object)CreatedAt,UpdatedAt
For Notes, the entity includes:
ID,Name,DescriptionNoteTypeId,OwnerIdMeta(JSON metadata object)StartDate,EndDateCreatedAt,UpdatedAt
For Resources, the entity includes:
ID,Name,OriginalName,DescriptionResourceCategoryId,OwnerIdMeta(JSON metadata object)Hash,ContentType,FileSize,Width,HeightCreatedAt,UpdatedAt
Basic Examples
Display Metadata Fields
If groups in a "Person" category have metadata like {"birthDate": "1990-01-15", "occupation": "Engineer"}:
<dl class="grid grid-cols-2 gap-2">
<dt class="font-medium">Birth Date</dt>
<dd x-text="entity.Meta?.birthDate || 'Unknown'"></dd>
<dt class="font-medium">Occupation</dt>
<dd x-text="entity.Meta?.occupation || 'Unknown'"></dd>
</dl>
Conditional Display
Show different content based on metadata:
<template x-if="entity.Meta?.status === 'active'">
<span class="px-2 py-1 bg-green-100 text-green-800 rounded">Active</span>
</template>
<template x-if="entity.Meta?.status === 'archived'">
<span class="px-2 py-1 bg-gray-100 text-gray-600 rounded">Archived</span>
</template>
Link to Related Content
Create links using entity data:
<template x-if="entity.Meta?.website">
<a :href="entity.Meta.website"
class="text-blue-600 hover:underline"
target="_blank">
Visit Website
</a>
</template>
<template x-if="entity.Meta?.relatedGroupId">
<a :href="'/group?id=' + entity.Meta.relatedGroupId"
class="text-blue-600 hover:underline">
View Related Group
</a>
</template>
Advanced Examples
Iterating Over Metadata Arrays
Use x-for to render lists, tables, or grids from array metadata. This pattern works for image galleries, badge lists, data tables, and any repeating content.
<template x-if="entity.Meta?.records && entity.Meta.records.length > 0">
<table class="w-full text-sm">
<thead>
<tr class="border-b">
<th class="text-left py-2">Date</th>
<th class="text-left py-2">Event</th>
<th class="text-right py-2">Value</th>
</tr>
</thead>
<tbody>
<template x-for="record in entity.Meta.records" :key="record.date">
<tr class="border-b">
<td class="py-2" x-text="record.date"></td>
<td class="py-2" x-text="record.event"></td>
<td class="py-2 text-right" x-text="record.value"></td>
</tr>
</template>
</tbody>
</table>
</template>
Dynamic Styling from Metadata
Combine :class bindings with metadata values for status badges, progress bars, or conditional formatting.
<template x-if="entity.Meta?.progress !== undefined">
<div class="mt-4">
<div class="flex justify-between text-sm mb-1">
<span>Progress</span>
<span x-text="entity.Meta.progress + '%'"></span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full"
:style="'width: ' + entity.Meta.progress + '%'"></div>
</div>
</div>
</template>
CustomSummary Example
The CustomSummary template appears in list views. Keep it compact:
<div class="text-sm text-gray-600">
<template x-if="entity.Meta?.status">
<span class="inline-block px-2 py-0.5 text-xs rounded"
:class="{
'bg-green-100 text-green-800': entity.Meta.status === 'active',
'bg-yellow-100 text-yellow-800': entity.Meta.status === 'pending',
'bg-gray-100 text-gray-600': entity.Meta.status === 'archived'
}"
x-text="entity.Meta.status"></span>
</template>
<template x-if="entity.Meta?.priority">
<span class="ml-2" x-text="'Priority: ' + entity.Meta.priority"></span>
</template>
</div>
CustomAvatar Example
The CustomAvatar template controls how the entity appears when linked:
<template x-if="entity.Meta?.avatarUrl">
<img :src="entity.Meta.avatarUrl"
class="w-8 h-8 rounded-full object-cover">
</template>
<template x-if="!entity.Meta?.avatarUrl">
<div class="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center">
<span class="text-xs font-medium text-gray-600"
x-text="entity.Name?.charAt(0) || '?'"></span>
</div>
</template>
Creating Categories with Templates
- Navigate to Categories
- Click Create
- Fill in the Name and Description
- Add your templates in the appropriate fields:
- Custom Header - HTML for the detail page header
- Custom Sidebar - HTML for the detail page sidebar
- Custom Summary - HTML for list view cards
- Custom Avatar - HTML for link avatars
- Click Submit
Creating Resource Categories with Templates
- Navigate to Resource Categories
- Click Create
- Fill in the Name and Description
- Add your templates in the appropriate fields:
- Custom Header - HTML for the resource detail page header
- Custom Sidebar - HTML for the resource detail page sidebar
- Custom Summary - HTML for list view cards
- Custom Avatar - HTML for link avatars
- Optionally define a MetaSchema (JSON Schema for metadata validation)
- Click Submit
Creating Note Types with Templates
- Navigate to Note Types
- Click Create
- Fill in the Name and Description
- Add your templates in the appropriate fields
- Click Submit
Shortcodes
Shortcodes let you embed dynamic content in custom templates without writing Alpine.js code. Three built-in shortcodes are available:
[meta]-- Schema-aware metadata display with optional inline editing[property]-- Entity field values (Name, CreatedAt, etc.)[mrql]-- Inline MRQL query results in various formats
Plugins can also register custom shortcodes via mah.shortcode().
See the Shortcodes page for full syntax, attributes, and examples.
Section Configuration
Categories, Resource Categories, and Note Types can define a sectionConfig JSON field that controls which sections appear on entity detail pages.
How It Works
When a category has a sectionConfig set, the detail page for entities in that category shows or hides sections accordingly. Any section not mentioned in the config defaults to visible. An empty config (or no config) shows all sections.
Setting via the UI
- Navigate to Categories, Resource Categories, or Note Types
- Create or edit an entry
- Use the Section Visibility form to toggle sections on/off
- Save
JSON Format
The sectionConfig is a JSON object. Each key corresponds to a section on the detail page. Boolean keys default to true (visible). Object keys support a state field with collapsible behavior.
Collapsible states:
| State | Behavior |
|---|---|
"default" | Follows the application default |
"open" | Initially expanded |
"collapsed" | Initially collapsed |
"off" | Hidden entirely |
Group Sections (via Category)
{
"tags": true,
"timestamps": true,
"metaJson": true,
"metaSchemaDisplay": true,
"description": true,
"merge": true,
"clone": true,
"treeLink": true,
"owner": true,
"breadcrumb": true,
"ownEntities": {
"state": "default",
"ownNotes": true,
"ownGroups": true,
"ownResources": true
},
"relatedEntities": {
"state": "default",
"relatedNotes": true,
"relatedGroups": true,
"relatedResources": true
},
"relations": {
"state": "default",
"forwardRelations": true,
"reverseRelations": true
}
}
Resource Sections (via Resource Category)
{
"metadataGrid": true,
"timestamps": true,
"notes": true,
"groups": true,
"tags": true,
"versions": true,
"similarResources": true,
"series": true,
"metaJson": true,
"metaSchemaDisplay": true,
"description": true,
"previewImage": true,
"imageOperations": true,
"categoryLink": true,
"fileSize": true,
"owner": true,
"breadcrumb": true,
"technicalDetails": {
"state": "default"
}
}
Note Sections (via Note Type)
{
"content": true,
"groups": true,
"resources": true,
"timestamps": true,
"tags": true,
"metaJson": true,
"metaSchemaDisplay": true,
"owner": true,
"noteTypeLink": true,
"share": true
}
To hide a section, set its key to false. For example, to hide timestamps and the raw JSON sidebar on notes:
{
"timestamps": false,
"metaJson": false
}
Custom MRQL Result Templates
Categories, Resource Categories, and Note Types can define a customMRQLResult field containing a shortcode template that controls how entities of that type render in [mrql] shortcode results. The template is processed by the shortcode engine (not Pongo2), so [meta], [property], and nested [mrql] shortcodes work, but {{ }} expressions do not.
How It Works
- Set the
customMRQLResultfield on a Category, Resource Category, or Note Type - When an
[mrql]shortcode query returns entities of that type, the custom template is used instead of the default card layout -- unless the[mrql]shortcode itself provides a block template, which takes precedence over all category-level templates - The template has access to the entity context, so shortcodes like
[meta]and[property]work inside it
Setting via the UI
- Navigate to Categories, Resource Categories, or Note Types
- Create or edit an entry
- Enter a template in the Custom MRQL Result textarea
- Save
Example
A Category with this customMRQLResult:
<div class="flex items-center gap-2 p-2 border rounded">
<strong>[property path="Name"]</strong>
<span class="text-sm text-stone-500">[meta path="status"]</span>
</div>
When an [mrql] query returns groups in this category, each result renders using this template instead of the default link card.
Format and Template Precedence
Template selection follows this priority:
- Block template -- if the
[mrql]shortcode uses block syntax with non-empty content, that block body is the per-item template.customMRQLResultandformatare both ignored. - Explicit
format--format="table",format="list", orformat="compact"override custom template rendering. customMRQLResult-- whenformatis empty (auto) or"custom", entities with acustomMRQLResultuse it; entities without one fall back to the default card layout.- Default card layout -- used when none of the above apply.
Styling Tips
Use Tailwind CSS
Tailwind CSS is included. Use utility classes for styling:
<div class="p-4 bg-gray-50 rounded-lg shadow-sm">
<h3 class="text-lg font-semibold text-gray-900">Title</h3>
<p class="mt-2 text-gray-600">Description text</p>
</div>
Responsive Design
Use Tailwind responsive prefixes:
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Content adapts to screen size -->
</div>
Nested Alpine.js Scopes
If you need additional reactive state (toggles, counters, etc.), create a nested x-data scope. The parent entity variable remains accessible:
<div x-data="{ showDetails: false }">
<button @click="showDetails = !showDetails" class="text-sm text-blue-600">
Toggle Details
</button>
<div x-show="showDetails" class="mt-2">
<p x-text="entity.Meta?.notes || 'No notes'"></p>
</div>
</div>
Debugging Templates
If a template does not render correctly:
- Check the browser console for JavaScript errors
- Verify the entity JSON is valid (view page source)
- Test with a minimal template first, then add complexity
- Use
x-textto debug values:<span x-text="JSON.stringify(entity.Meta)"></span>
MetaSchema for Validation
Categories, Resource Categories, and Note Types support a MetaSchema field -- a JSON Schema that validates metadata. This is separate from templates but works well together:
- Define a MetaSchema to ensure required fields exist
- Create templates that rely on those fields
- Users get validation errors if metadata is incomplete
Example MetaSchema:
{
"type": "object",
"required": ["status", "priority"],
"properties": {
"status": {
"type": "string",
"enum": ["active", "pending", "archived"]
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5
}
}
}
Related Pages
- Meta Schemas -- JSON Schema validation for entity metadata
- Custom Block Types -- structured content blocks within Notes
- Entity Picker -- modal for selecting entities in block and form contexts