You’re reading the ZotLit v2 docs. Still on v1? Read the v1 docs

ZotLit

Migrate v1 Eta templates

Rename, update variables, and rewrite custom v1 Eta templates to the v2 format, or convert them to Liquid.

Custom v1 Eta templates need renamed files and updated variables to run in v2. If you never customized your templates (using only the defaults), no action is needed. V2 ships updated defaults.

Coming from ZotLit 2.1?

This page covers v1 Eta templates. If you updated from 2.1, where a note's look was spread across files such as zotlit-note.liquid.md, see Migrate from 2.1 templates instead.

Two paths exist. Keep Eta: rename the files, update the data vocabulary, and enable the JavaScript templates gate, which the rest of this page covers. Or convert to Liquid: rewrite each template in v2's default Liquid engine, which needs no gate, with Eta to Liquid translations at the end.

JavaScript Templates gate required for Eta

Eta templates require the JavaScript templates gate. See Enable JavaScript templates for the opt-in procedure.

Rename template files

v2 reads templates from a zotlit- prefix, replacing zt-, and the template formerly called annots is now content. Rename your files to these names first. ZotLit loads and renders them under these names, and the one-pass conversion below folds them into v2's template documents.

v1 filenameRename toWhere its content ends up
zt-note.eta.mdzotlit-note.eta.mdThe note body of the default profile document
zt-annots.eta.mdzotlit-content.eta.mdThe managed block inside that same note body
zt-annot.eta.mdzotlit-annotation.eta.mdThe annotation section of that same profile document

Your two v1 citation files, zt-cite*.eta.md, take the same zt- to zotlit- rename. Both of them fold into one citation text document, zotlit-citation.md, as an if/else on zt.variant.

Delete these files if present (they have no v2 equivalent):

  • zt-field.eta.md: replaced by the settings-based frontmatter system.
  • zt-colored.eta.md: color styling is now handled automatically during note import.

Replace it with zt

The template data variable changed from it to zt. A search-and-replace of it. to zt. within template tags covers most cases:

it.title       →  zt.title
it.creators    →  zt.creators
it.backlink    →  zt.backlink
it.pageLabel   →  zt.pageLabel

The exceptions below (comment, tags, collections, date, notes) have changed semantics. Handle them in the following steps rather than with a blanket replace.

Update renamed and reshaped fields

These fields changed name or type:

v1 propertyv2 propertyNotes
it.citekeyzt.citationKeyzt.citekey also works as an alias
it.abstractNotezt.abstractzt.abstractNote also works
it.publicationTitlezt.containerTitlezt.publicationTitle also works
it.colorzt.colorHexHex string, e.g. #ffd400
—zt.colorNameNew: palette name ("yellow", "red", etc.)
it.imgExcerptembed(zt.imgLink)zt.imgLink is a link helper or null; embed() adds the ! prefix
it.imgPath(removed)Use zt.imgLink() instead
it.imgUrl(removed)Use zt.imgLink() instead
it.textExcerptzt.textRenamed
it.commentMdzt.commentMarkdown comment is now the default
it.commentzt.commentHtmlRaw HTML (only <i>/<b>/<sub>/<sup>)

Direct Zotero fields that are plain strings (title, DOI, url, volume, issue, pages, publisher, etc.) carry over unchanged after the it → zt rename.

Update tag access

v1 exposed tags as formatted strings. v2 exposes them as objects with name and type properties. Tags coerce to their name in string contexts, so join still works:

<%/* Tags coerce to their name */%>
<%= zt.tags.join(", ") %>

<%/* Explicit property access */%>
<%= zt.tags.map(t => t.name).join(", ") %>

<%/* Filter to manual tags only */%>
<%= zt.tags.filter(t => t.type === "manual").join(", ") %>

Update collection access

v2 reshapes collection objects. Key differences:

Aspectv1v2
path orderleaf → rootroot → leaf
path renderingauto-rendered "A > B > C"plain string[]; use c.path.join(" > ")
Singular aliasit.collectionremoved; use the zt.collections array
<%/* Collections coerce to their name in string contexts */%>
<%= zt.collections.join(", ") %>

<%/* Render each collection's full hierarchy */%>
<%= zt.collections.map(c => c.path.join(" > ")).join("; ") %>

Update date fields

v1 propertyv2 propertyType change
it.datezt.dateWas a year-only string; now a parsed ItemDate object. <%= zt.date %> renders ISO; zt.date?.year gets the numeric year.
it.dateAdded / it.dateModifiedzt.dateAdded / zt.dateModifiedWere raw SQL strings ("YYYY-MM-DD HH:MM:SS"); now Temporal.Instant. Render as local date in <%= %> tags (e.g. 2026-06-21).

If your v1 template used it.date as a year string, replace with zt.date?.year:

<%/* v1 */%>
year: "<%= it.date %>"

<%/* v2 */%>
year: "<%= zt.date?.year %>"

Update child note access

v1 inlined child-note content as Markdown on it.notes. v2 provides link-only entries. A child note becomes a separate file only when you import it:

v1v2
it.notes: array of {title, content}zt.notes: array of {key, title, noteLink()}
Note content inlinednoteLink() links to the imported file, or else opens the note in Zotero

The default template no longer lists child notes. To list them as links, add a loop like this to your template:

<% if (zt.notes.length) { %>
## Notes

<% for (const note of zt.notes) { -%>
- <%~ note.noteLink() %>
<% } %>
<% } %>

Rewrite the annotation template with bq()

v1 relied on external blockquote handling. v2 uses the bq() helper inside the annotation template:

<% bq(() => { %>
[!note] Page <%= zt.pageLabel %>

<%= embed(zt.imgLink) %><%= zt.text %>
<% if (zt.comment) { %>

<%= zt.comment %>
<% } %>
<% }) %>

bq() wraps all content in a Markdown blockquote (> per line), trims the result, and collapses consecutive empty blockquote lines. It must appear in a <% %> execute tag.

Update the content template

The template is renamed from annots to content. It now receives the full context object instead of a raw annotation array:

<%/* v1: received a raw array as `it` */%>
<% for (const annotation of it) { %>

<%/* v2: receives full context, access annotations via zt.annotations */%>
<% for (const annotation of zt.annotations) { %>

Update the include call in your note template:

<%/* v1 */%>
<%~ include("annots", it.annotations) %>

<%/* v2 */%>
<%~ include("content", zt) %>

The content template now has access to all zt.* properties (attachments, tags, notes, etc.), not annotations alone.

Update the citation source

v1 passed it as a raw array. v2 wraps items in an object:

<%/* v1 */%>
[<%= it.filter(lit => !!lit.citekey).map(lit => `@${lit.citekey}`).join("; ") %>]

<%/* v2 */%>
[<%= zt.citations.filter(c => c.item.citationKey).map(c =>
  `${c.suppressAuthor ? "-" : ""}@${c.item.citationKey}`
).join("; ") %>]

Key changes:

  • it (array) becomes zt.citations (array inside an object). zt.items exposes the same items without citation metadata.
  • lit.citekey becomes c.item.citationKey (or c.item.citekey).
  • Both of your v1 citation files render from one v2 document. The conversion below writes them as the two branches of an if/else on zt.variant, which is "main" or "alt". See Citation templates.

Update attachment access

v1 selected one attachment and exposed it as it.fileLink (a string). v2 provides all attachments as an array, and fileLink is a link helper you call:

<%= zt.attachments.map(a => a.fileLink()).filter(Boolean).join(" ") %>

Each attachment object has: key, filename, contentType, linkMode, filePath, fileLink.

Update creator access

v2 uses a flat array with explicit properties. Creators coerce to their full name in string contexts:

<%/* All authors (coerces to fullName) */%>
<%= zt.authors.join(", ") %>

<%/* Explicit property access */%>
<%= zt.authors.map(c => c.fullName).join(", ") %>

<%/* Filter by role */%>
<%= zt.creators.filter(c => c.role === "editor").join(", ") %>

Creator properties: family, given, literal (institutional names), role, fullName.

Convenience properties on the item context: zt.authors (the first populated role: primary creator, editor, director, then contributor) and zt.authorsShort (formatted short string, e.g. "Smith et al.").

Update the filename template

The filename template is a template file (zotlit-filename.eta.md), not a setting string. The one-pass conversion later folds it into the filename key of your profile document's manifest. The v2 default adds suffix() for collision avoidance:

<%= zt.citationKey ?? zt.DOI ?? zt.title ?? zt.key %><%= suffix() %>
  • it.citekey becomes zt.citationKey (canonical name; zt.citekey also works).
  • The .md extension is no longer included. ZotLit appends it automatically.
  • suffix() appends a random string only when the name collides with an existing note.

Migrate frontmatter fields

If you customized zt-field.eta.md, delete it and rebuild its fields as managed frontmatter entries in your profile document. Open the document from the Templates and properties row at Settings > ZotLit with Edit profile, then select the Properties tab.

For each field, select Add a property and fill in:

  • Property name: the YAML property name.
  • Value format: Zotero field · Liquid for a Zotero field, Fixed text for fixed text, or Rule · JSON-e for a JSON-e rule.
  • Value: the value. A Zotero field uses a zt. path, such as zt.title.
  • When the note is updated: Replace the existing value, Add to the existing list, or Keep the existing value.
v1 template lineProperty nameValueWhen the note is updated
title: "<%= it.title %>"titlezt.titleReplace the existing value
authors: ...authorszt.authors | map: "fullName" | join: ", "Replace the existing value
year: "<%= it.date %>"yearzt.date | date: "%Y"Replace the existing value
tags: ...tagszt.tags | map: "name" | join: ", "Add to the existing list

YAML escaping is handled automatically.

The Properties tab writes each property as one entry in the frontmatter list of the profile document manifest. An entry carries a key and one value member, and the member name declares the language: expr holds a Liquid value, which is what Zotero field · Liquid writes; value holds fixed text or a JSON-e rule, which is what Fixed text and Rule · JSON-e write; and js holds a JavaScript value, which you write in Source.

ZotLit manages reserved frontmatter keys that you cannot target from a user field, such as zotero-key, zotlit-profile, zotero-note-key, and zotero-lastmod. The default citekey entry is an ordinary managed frontmatter field. See Frontmatter reference for details.

JavaScript values

A js entry runs only while JavaScript templates is on. See Enable JavaScript templates.

Overwrite v1 notes to add the managed region

v1 notes lack the %%zt-managed%% / %%/zt-managed%% markers that v2 uses to isolate generated content. Incremental update cannot target their body. Running ZotLit: Update literature note on a v1 note refreshes frontmatter only.

To add the managed region, run the overwrite command on each note you want refreshed:

ZotLit: Overwrite literature note

A confirmation modal appears. Click Overwrite to proceed. ZotLit regenerates the entire note body from the current templates. Your renamed zotlit-content template becomes the managed region, so ZotLit inserts the managed-region markers.

After overwriting, the note responds to ZotLit: Update literature note for incremental updates. You do not need to overwrite every note at once. Leave notes untouched until you are ready.

See Refresh v1 notes for the full procedure.

Whitespace change from v1

v1 defaulted autoTrim to [false, "nl"] (trim one newline after tags). v2 defaults to [false, false] (preserve all whitespace). Migrated templates may produce extra blank lines where %> previously consumed a trailing newline. Add -%> to the closing side of tags that need the old trim behavior. See Whitespace control.

Expected result

After completing these steps, your custom Eta templates render with v2 field names and helpers. Notes that have been overwritten contain the %%zt-managed%% markers and respond to incremental updates. Notes you have not overwritten continue to work for frontmatter-only refreshes.

Convert to template documents

Renamed files load and render as they are, so you can stop here. ZotLit also offers to fold them into v2's template documents in one pass, from the setup banner above the Library View or from a reminder row at the top of settings, and it verifies the output before it writes anything.

Migrate from 2.1 templates covers that conversion: what each file becomes, what the pass checks, and what it refuses.

Eta to Liquid translations

v2's default engine is Liquid. Converting your templates to Liquid removes the need for the JavaScript templates gate entirely. For each construct, the table below shows the Liquid equivalent and links to the relevant syntax reference section.

Eta constructLiquid equivalentReference
<%= zt.title %>{{ zt.title }}Outputs and tags
<% if (zt.abstract) { %> ... <% } %>{% if zt.abstract %} ... {% endif %}Control flow
<% for (const a of zt.annotations) { %> ... <% } %>{% for a in zt.annotations %} ... {% endfor %}for / endfor
<%~ include("content", zt) %>{% render "content" with zt as zt %}Includes with render
<% bq(() => { %> ... <% }) %>{% bq %} ... {% endbq %}bq: blockquote block
embed(zt.imgLink){{ zt.imgLink | embed }}embed filter
<%= suffix() %>{% suffix %}suffix tag
zt.attachments[0].fileLink(){{ zt.attachments[0] | file_link }}Link filters
zt.collections.map(c => c.path.join(" > ")){{ zt.collections | collection_paths: " > " }}collection_paths
zt.tags.map(t => t.name).join(", "){{ zt.tags | map: "name" | join: ", " }}Array filters
zt.dateAdded (Temporal.Instant formatting){{ zt.dateAdded | date: "%Y-%m-%d" }}date filter
-%> (trim one newline after tag)-%} / -}}Whitespace control

A renamed 2.1 Liquid file uses the .liquid.md extension (e.g. zotlit-note.liquid.md); a converted template document carries no language in its name. The data variable is still zt and the property names are identical to the Eta equivalents documented above.

See also

Last updated on

On this page