Building CoinS for Zotero Integration

11 November 2025

Authors

zotero

Based on our development experience at the CDH, CoinS (ContextObjects in Spans) turned out to be the most effective method for integrating Zotero with websites so far. Despite being a well-established but aging standard, it remains simple, lightweight, and natively supported by Zotero. Since up-to-date resources are scarce, this guide walks through the implementation process, using Django and Wagtail code examples.

Understanding how CoinS works

CoinS (ContextObjects in Spans) is a method for embedding OpenURL metadata in your HTML using a simple <span> tag. Zotero scans the page, finds these spans, and automatically extracts the citation details. The information inside the title attribute is URL-encoded — meaning it’s machine-friendly, not human-readable — and contains a series of key-value pairs describing the item (title, author, date, type, etc.).

Building the OpenURL string (Unencoded)

At its core, a CoinS span holds an OpenURL string that looks like this before encoding:

ctx_ver=Z39.88-2004
&rft_val_fmt=info:ofi/fmt:kev:mtx:journal
&rft.genre=blogPost
&rft.atitle=Integrating Zotero with the CDH Blog
&rft.jtitle=CDH Blog
&rft.date=2025-08-15
&rft.au=Jane Doe
&rft_id=https://cdh.princeton.edu/blog/integrating-zotero

This pre-encoded payload lists the fields that Zotero needs to import the item (here: a journal-article-like “article”). Later, you will need to URL-encode this string and place it in the span’s title attribute.

For maintainability, it’s wise to make this pre-encoded logic contained and reusable, so you don’t have to repeat the concatenation logic across templates. This is especially important for bulk export pages like our PPA search results case here, where you need to loop over each item and output a <span> for every result. In our solution, we wrote a Django template tag. But this could also be done with a model method. You could think and choose the solution that best suits your needs.

URL-encoding the string

Since HTML can’t include spaces and special characters in attribute values, the OpenURL string must be URL-encoded before it’s inserted into the title attribute. In a Django project, we can use Django’s built-in urlencode filter to handle encoding directly in the template.

We suggest applying the encoding filter only once at the final print, rather than encoding individual fields first and then put them together. This avoids the potential risk of double-encoding and keeps the code cleaner:

<span class="Z3988" title="{{ coins_string|urlencode }}"></span>

Adding it to your template

For a single-item page, you can assemble the OpenURL string from the page’s fields and then output a single <span> with the encoded value. In Wagtail, that might look like:

{% with "ctx_ver=Z39.88-2004&rft_val_fmt=info:ofi/fmt:kev:mtx:journal&rft.genre=article&rft.atitle="|add:page.title|add:"&rft.jtitle=CDH Blog&rft.date="|add:page.first_published_at|add:"&rft.au="|add:page.author_name|add:"&rft_id="|add:page.full_url as coins_string %}
    <span class="Z3988" title="{{ coins_string|urlencode }}"></span>
{% endwith %}

For bulk export pages, loop over each item in the search results and output one span per item so Zotero’s “Save All” option works:

{% for result in search_results %}
    {% with "ctx_ver=Z39.88-2004&rft_val_fmt=info:ofi/fmt:kev:mtx:journal&rft.genre=article&rft.atitle="|add:result.title|add:"&rft.jtitle=Princeton Prosody Archive&rft.date="|add:result.date|add:"&rft.au="|add:result.author_name|add:"&rft_id="|add:result.full_url as coins_string %}
        <span class="Z3988" title="{{ coins_string|urlencode }}"></span>
    {% endwith %}

Testing and edge cases

After adding the spans, open the page in a browser with the Zotero Connector installed. The browser icon should reflect the correct item type (e.g., blog post, article, book).

Two things to be aware of when implementing CoinS based on our experience:

  • Posts without visible authors — If you leave out the rft.au field entirely (for example, when a post has no displayed author), Zotero will still detect the item but will store the raw OpenURL string in its “Extra” field. This seems to be an expected Zotero behavior when no author is provided.

  • Special characters in titles — Make sure you only URL-encode the CoinS string once, at the final output step. Encoding individual fields and then encoding the whole string again can cause character issues (for example, garbled apostrophes).

In short, CoinS gives you reliable one-click saves with very little code: build a single OpenURL string, encode it exactly once, and emit one span per item (or per search result). The Django/Wagtail snippets here are drop-in, but the same pattern works in any stack that can render HTML. If you run into odd types, multiple creators, or non-Latin text, tweak rft_val_fmt/rft.genre and repeat rft.au as needed, then verify with the Zotero icon and a quick “Save All” test. If you adapt this approach or find a cleaner path, let us know—we’re happy to update the guide so others can benefit.