You’re reading the ZotLit v2 beta 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.

Two migration paths:

  • Keep Eta: rename files, update the data vocabulary, enable the JavaScript Templates gate. The rest of this page covers this path.
  • Convert to Liquid: rewrite each template in v2's default Liquid engine. No gate needed. See 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 templates use a zotlit- prefix (replacing zt-) and the template formerly called annots is now content.

v1 filenamev2 filename
zt-note.eta.mdzotlit-note.eta.md
zt-annots.eta.mdzotlit-content.eta.md
zt-annot.eta.mdzotlit-annotation.eta.md
zt-cite.eta.mdzotlit-cite.eta.md
zt-cite2.eta.mdzotlit-cite2.eta.md

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 itzt 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 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 writes each child note to a separate file and provides link-only entries:

v1v2
it.notes — array of {title, content}zt.notes — array of {key, title, noteLink()}
Note content inlinedContent in a separate file; noteLink() links to it

The default content template renders child notes as a bulleted list of links:

<% 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 only annotations.

Update citation templates

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).

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 (primary creator type) 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 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 recreate the fields in Settings > ZotLit > Templates > Frontmatter:

For each field, configure:

  • Key: the YAML property name.
  • Expression: a JavaScript or Liquid expression evaluated against zt.
  • Language: select JavaScript to use JS expressions (requires the JavaScript Templates gate), or Liquid for Liquid expressions.
  • Merge strategy: Replace, Append arrays, or Keep existing.
v1 template linev2 frontmatter field
title: "<%= it.title %>"Key: title, Expr: zt.title, Merge: Replace
authors: ...Key: authors, Expr: zt.authors.map(c => c.fullName), Merge: Replace
year: "<%= it.date %>"Key: year, Expr: zt.date?.year, Merge: Replace
tags: ...Key: tags, Expr: zt.tags.map(t => t.name), Merge: Append arrays

YAML escaping is handled automatically.

ZotLit manages reserved frontmatter keys that you cannot target from a user field. For literature notes: zotero-key and citekey. For imported notes: zotero-note-key and zotero-lastmod. See Frontmatter reference for details.

Language selector

JavaScript expressions (like zt.authors.map(...)) require selecting JavaScript in the Language dropdown for that field. The JavaScript Templates gate must be on.

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, inserting the managed-region markers.

After overwriting, the note responds to ZotLit: Update literature note for incremental updates going forward. 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.

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

Liquid template files use the .liquid.md extension (e.g. zotlit-note.liquid.md). The data variable is still zt and the property names are identical to the Eta equivalents documented above.

See also

On this page