Nature Zotero Publications
Display a searchable and filterable Zotero bibliography with publication metadata, links, and pagination.
Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/lwangdu/nature-zotero-publications/archive/refs/heads/main.zipNature Zotero Publications is a dynamic WordPress block that displays a complete Zotero user or group library as a searchable, filterable bibliography.
Current version: 1.0.7
Requirements
- WordPress 7.0 or newer
- PHP 8.3 or newer
- A numeric Zotero user ID or group ID
- A Zotero API key for private libraries; public libraries do not require one
- Working WordPress Cron so large libraries can synchronize in background batches
Features
- User libraries, group libraries, and individual Zotero collections
- Complete-library synchronization beyond Zotero's 100-item response limit
- Server-rendered initial results for SEO and no-JavaScript access
- WordPress Interactivity API search, filters, counts, and pagination
- Year and item-type filters
- Accessible, on-demand author autocomplete
- Linked titles, authors, publication details, item-type badges, dates, DOI, source and Zotero links, citation keys, and expandable abstracts
- Configurable sorting, 100-item default pagination, statistics, filters, search, and abstracts
- Local querying after synchronization, without requesting Zotero again for every visitor interaction
- Query-aligned local database indexes for faster large-library display, facets, and author filtering
- Server-only handling of private Zotero API keys
- Plugin-owned data cleanup on uninstall
Frequently Asked Questions
Should I select Group Library or User Library?
Choose Group Library for a shared Zotero group, or User Library for an individual account’s library. The type must match the numeric ID. Group Library is the default for new configurations; existing saved choices are preserved. Blocks inherit the plugin settings unless you set an override.
Where do I find my library or group ID?
For a group library, copy the numeric group ID from its Zotero group URL. For a user library, find your numeric user ID at Zotero API Keys. Enter only the digits in Default Library / Group ID, not a URL, username, or API key.
When do I need a Zotero API key?
Publicly readable libraries can be displayed without a key. Private libraries require a valid key with read permission for the selected user or group library. Enter only the raw key, without quotes, a Bearer prefix, or a header name. Never share your key in screenshots or support messages.
Why is the API key field blank after saving?
The field stays blank intentionally so the saved key is not displayed. Check Synchronization diagnostics for “API key: configured”; this confirms a key is available, but does not verify its permissions. Leaving the field blank keeps the saved key. Select Remove the saved API key to delete it. If your host defines ZOTERO_DISPLAY_API_KEY, that value overrides the saved key.
Why does the page say “Synchronizing publications”?
The plugin is waiting for its local publication index to populate. Open the published page to initialize its source; editor previews alone do not start synchronization. Background batches need working WordPress Cron. If progress stops, open Settings → Nature Zotero Publications → Synchronization diagnostics, check the error and retry deadline, and use Run one sync batch when eligible. Refresh settings to see the result; manual runs still respect retry delays.
What do HTTP 400 and 403 errors mean?
HTTP 400 indicates a rejected request; HTTP 403 indicates access was refused. Check the Response explanation in Synchronization diagnostics rather than assuming a database problem. Verify the library type and ID first. “Invalid key” means the supplied key was rejected; check the saved key and any overriding constant. For “Forbidden,” check library visibility and key permissions. An HTML error may also come from a hosting intermediary. Do not repeatedly clear the cache: it removes the local index and does not fix credentials or bypass retry delays.
Will updating the plugin preserve my settings and publications?
Replacing the installed plugin through WordPress’s upload/update flow preserves saved settings and the local publication index. Back up the site before updating, and do not delete the plugin first: deletion runs uninstall cleanup and removes plugin-owned data. Changing connection settings or selecting Clear Zotero Cache also clears the local index and requires synchronization again. The Group Library default does not override an existing saved User Library choice.
For more help, see Troubleshooting.
How synchronization works
Zotero limits an API response to 100 items. The plugin uses a resumable background synchronization system in includes/class-sync.php:
- The block schedules synchronization for its configured library or collection.
- WordPress Cron downloads top-level Zotero items in 100-item batches.
- Normalized items and creators are stored in plugin-owned local index tables. Creator names in the author-filter index are limited to 191 Unicode characters; hashes use the original names, and full names remain in publication data and search text.
- A newly completed generation replaces the previous generation only after every batch succeeds.
- The completed index powers search, filters, entry counts, and pagination.
The initial synchronization screen reloads as soon as its first indexed publications are available, then displays the partial index while background synchronization continues. Frontend page rendering schedules synchronization without waiting for Zotero. New sources show a lightweight loading state until the background worker supplies the first publications; existing indexed results render immediately. Scheduled workers process up to four pages per run. Each background Zotero request has a 20-second HTTP timeout. Connection failures automatically retry the same batch up to three times, after 60, 120, and 240 seconds, retaining indexed publications and progress. After the final failure, the normal error state and five-minute retry eligibility apply. Automatic retries depend on WordPress Cron running. Editor previews and REST requests only read the local index: typing a source ID never starts or schedules a synchronization. View the frontend page to initialize a new source. Library IDs must contain only digits; collection keys must be empty (the whole library) or exactly eight uppercase letters or digits.
Visitors continue to see the last completed generation while a refresh is running. During the first synchronization, the page displays already-indexed publications together with an accessible progress indicator and automatically reloads when the complete index is ready. Search, filters, pagination, and author suggestions work with the available partial index. Visitor page and REST requests never download a synchronous 2,000-item fallback.
Version 1.0.2 stores each Zotero page with short multi-row writes and processes one page per background run. This substantially reduces write-lock time on SQLite-backed WordPress Studio sites while remaining compatible with MySQL.
Version 1.0.4 processes up to four Zotero pages per background run, stops a run after 12 seconds, and polls first-sync progress every five seconds. The bounded worker improves throughput without returning to long row-by-row database locks. Off-screen publication cards use browser rendering containment to reduce initial layout and paint work on larger result pages.
Recent updates cache completed server-rendered block fragments, default block and REST pagination to 100 items per page, and add composite database indexes for the main publication list, year/type facets, and author lookup queries.
The cache-duration setting controls when a completed source becomes eligible for another background refresh. Changing connection settings or selecting Clear Zotero Cache removes both transient data and the local index so the source can synchronize again.
Installation and configuration
- Upload the plugin to
/wp-content/plugins/nature-zotero-publications, or install the release ZIP in WordPress. - Activate Nature Zotero Publications.
- Open Settings → Nature Zotero Publications.
- Select the default library type: User Library or Group Library. Group Library is the default for new configurations; existing saved choices are preserved.
- Enter the numeric Zotero user ID or group ID.
- Enter an API key only when the library is private.
- Choose the cache duration in minutes and save the settings.
- Add the Nature Zotero Publications block to a post or page.
The numeric group ID appears in a Zotero group URL. A Zotero user ID and API key can be found or created at zotero.org/settings/keys.
Block settings
Each block can use the plugin defaults or override them with its own:
- Library type
- Library or group ID
- Collection key
- Sort field and direction
- Items per page
- Entry-count display
- Search field
- Filters
- Abstract display
Changing a block's library, collection, or sorting configuration creates a distinct source configuration and cache key.
The block defaults to 100 items per page.
Frontend performance and accessibility
The first result page, year options, and item-type options are rendered by PHP. The WordPress Interactivity API progressively enhances the controls without replacing the initial server-rendered content.
The complete author index is not embedded in page HTML. After a visitor enters at least two characters, the browser requests a small set of author suggestions from the local creator index. The autocomplete supports mouse use and keyboard navigation with Arrow Down, Enter, Space, and Escape.
Search, filter, and pagination requests omit the large facet collections they do not need. Small initial facets are cached briefly, public REST requests are limited to server-signed rendered block sources, and the private Zotero key is never placed in block context or browser requests.
Completed initial block markup is cached in transients and invalidated when a synchronization generation completes. The local index schema includes composite indexes for the common source/token/sort, source/token/type, source/token/year, and source/token/creator query patterns.
REST endpoints
The plugin provides two public, read-only WordPress REST routes:
GET /wp-json/zotero-display/v1/itemsreturns normalized, paginated publications from the completed local index.GET /wp-json/zotero-display/v1/authorsreturns a limited set of author suggestions after at least two search characters.
These routes identify the library or collection with a server-issued source signature from rendered block markup, but never return or accept the saved private API key. Zotero requests are performed only on the server.
Publication data
The plugin displays Zotero's native citationKey value when present. For older Better BibTeX records, it also recognizes a Citation Key: value in the Extra field.
Child attachments and annotations are excluded. Top-level standalone attachments remain visible so the total aligns more closely with Zotero's items in this view count. Untitled top-level records receive an Untitled display label.
General search covers titles, authors, dates, DOI values, citation keys, abstracts, publications, and tags. Tags remain searchable but are not displayed on publication cards. Publication titles link to their best available external URL and fall back to the Zotero item page.
Data storage and uninstall
The plugin stores Zotero publication data in plugin-owned WordPress database tables:
{$wpdb->prefix}zotero_display_items{$wpdb->prefix}zotero_display_creators
The private Zotero API key, default library settings, schema version, synchronization state, and transient cache records are stored through WordPress options/transients. The API key is used only for server-side Zotero requests and is not included in browser markup, REST responses, or scheduled-event arguments.
Deleting the plugin through WordPress runs uninstall.php, which removes the plugin-owned tables, settings, schema and sync options, transients, object-cache group data when supported, and pending synchronization events. Deactivation keeps the local index in place and only clears temporary caches and scheduled work.
Development
Install dependencies and create a production build:
npm install
npm run build
Other useful commands:
npm run start
npm run lint:js
npm run lint:css
npm run plugin-zip
The build uses wp-scripts build --experimental-modules so viewScriptModule can load the Interactivity API frontend module. npm run plugin-zip creates the WordPress-ready distribution archive.
Project structure
nature-zotero-publications.php Plugin bootstrap, version, activation hooks
includes/
class-zotero-api.php Zotero API client and transient fallback
class-sync.php Background synchronization and local index
class-settings.php Settings page and cache controls
class-rest-controller.php Publication and author REST routes
class-block.php Dynamic block registration and rendering
src/
block.json Block metadata and Interactivity support
index.js Block editor registration
edit.js Inspector controls and editor preview
frontend.js Interactivity API store and actions
style.scss Shared frontend and editor styles
editor.scss Editor-only styles
build/ Production assets generated by npm run build
uninstall.php Plugin-owned data cleanup on deletion
Troubleshooting
The full library has not appeared yet
Large libraries require multiple background requests. Visit the page once to ensure the source is scheduled, confirm WordPress Cron can run, and allow the synchronization to finish. The page reports first-sync progress and reloads automatically when ready; existing completed data remains visible during later refreshes.
The Zotero API returns 403
Confirm that the selected library type matches the ID. For a group URL, use Group Library and the numeric group ID. Private groups also require a Zotero key with permission to read that group.
Search or filters show old data
Open Settings → Nature Zotero Publications and select Clear Zotero Cache. This removes cached responses and the completed local index; the next page request starts a new synchronization.
The displayed count differs from Zotero
Compare against Zotero's top-level items in this view count. Child attachments and annotations are intentionally excluded, while standalone attachments are included.
Changelog
1.0.7
-
Preserve case-sensitive Zotero sorting values such as
dateAdded. -
Load editor previews when using the plugin-wide default library.
-
Support arrow navigation and Escape dismissal in author suggestions, restoring input focus on dismissal or keyboard selection.
-
Show first indexed publications before the full initial synchronization completes; limit browser-triggered priming to one 100-item page.
-
Require PHP 8.3 or newer.
-
Require WordPress 7.0 or newer.
-
Keep the five-second polling delay after failed HTTP responses.
-
Honor the five-minute retry interval after synchronization failures.
1.0.6
- Require server-signed rendered block sources for public REST publication and author requests.
- Add a PHP string-length fallback for hosts without mbstring.
- Preserve canonical DOI slashes in DOI links.
- Improve uninstall cleanup variable prefixing and refresh REST security documentation.
1.0.5
- Added
uninstall.phpto remove plugin-owned tables, options, transients, object-cache data, and scheduled sync events when the plugin is deleted. - Added composite local-index database indexes for faster publication listing, facets, and author lookup.
- Defaulted block and REST fallback pagination to 100 items per page.
- Cached completed server-rendered block fragments.
- Kept Zotero tags searchable while removing tag display from publication cards.
1.0.4
- Processed up to four Zotero pages in each time-bounded background run.
- Polled first-sync progress every five seconds.
- Deferred rendering work for off-screen publication cards.
- Added compact numbered pagination with responsive wrapping.
1.0.3
- Displayed already-indexed publications during the first full-library synchronization.
- Kept accessible progress and automatic reload while partial search, filters, pagination, and author suggestions remain available.
1.0.2
- Removed the synchronous 2,000-item visitor fallback.
- Added accessible first-sync progress with automatic completion reload.
- Replaced row-by-row index writes with short multi-row writes.
- Limited each background event to one 100-item Zotero page to reduce SQLite write contention.
1.0.1
- Added resumable background synchronization for complete Zotero libraries.
- Added a generation-based local publication and creator index.
- Migrated frontend interactions to the WordPress Interactivity API.
- Added accessible, on-demand author autocomplete.
- Reduced initial page and REST payload sizes by removing the inline author facet.
- Raised the minimum WordPress version to 6.5.
1.0.0
- Initial release.
Index and request hardening
Changing only cache duration preserves the publication index; changing the API key or default library clears it after the settings are saved. The API key field is intentionally blank: leave it blank to retain the key, or select Remove the saved API key. You may instead define ZOTERO_DISPLAY_API_KEY in wp-config.php; it overrides the database setting. Saving settings then removes the database copy. After rotating a configured constant, use Clear Cache to discard data from the previous credentials.
Scientific titles and abstracts are stored as supplied and displayed as escaped text. Existing sources receive a full normalization refresh the next time they become stale, before conditional version checks are used. Long creator names are truncated only in the autocomplete index; full publication metadata and original-name hashes are retained.
Synchronization uses an expiring, owner-checked database mutex. Zotero Backoff and Retry-After delays postpone work; unchanged libraries use conditional requests. Schema upgrades run in the admin area (activation still installs tables). Visit the administration area after updating before sending traffic to the site.
Queries and author suggestions use bounded cache slots (32 per source and query kind) with a 15-second lifetime. Facet keys use query parameters rather than prepared SQL. Completed REST results have private browser caching; pending responses are not cached. Hidden tabs pause polling, and request failures back off to 60 seconds. Fragment caches expire against the last completed sync time and do not prevent refresh scheduling.
Publication privacy: publishing a bibliography exposes its indexed metadata through signed public REST queries, including abstracts and tags even when display controls hide them. Those controls are presentation settings, not access controls. Only publish sources whose metadata may be public; use a separate public Zotero collection for confidential libraries.
Full-text search, a single index supporting local sorts, incremental Zotero synchronization, and major build-tool upgrades remain separate architectural or maintenance work. Query caches reduce repeated work but do not replace host-level request rate limits.
Administrator synchronization diagnostics
Settings > Nature Zotero Publications includes administrator-only synchronization diagnostics. It checks whether the publication tables are readable, shows saved source progress, sanitized errors, retry deadlines, and pending cron events. It reports rejected scheduling attempts made after this update. API keys, raw SQL, and raw transport error details are never displayed. Table readability does not establish write permission or full schema compatibility.
Run one sync batch uses a capability and nonce-protected POST and processes at most one 100-item page for a known source. It preserves completed publications and respects locks, API cooldowns, and retry delays. It can take up to 20 seconds. The panel includes the valid default source and up to 100 recent stored source options; open a configured block on the frontend first if its override is missing. No WP Crontrol installation is needed. Refresh the settings page after a run to inspect progress. Missing or unreadable tables disable manual runs; inspect activation and database permissions before proceeding.
The plugin records a bounded, redacted explanation for new HTTP failures. Explanations appear only in the administrator diagnostics panel, not public REST responses or frontend errors. For HTML responses, the panel shows redacted title, heading, first paragraph, and selected identifying headers (content-type, server, via, zotero-api-version). If those HTML elements are absent, a bounded redacted plain-text body is shown instead. Raw HTML, cookies, authorization headers, and redirect locations are not displayed. Old failures need a new manual batch after the retry deadline to capture an explanation.
Outbound Zotero requests use the Zotero-API-Key header. Enter only the API key in settings, without a Bearer prefix or header name; surrounding whitespace is removed before sending.
Regression tests
Run npm run test:php with PHP 8.3 or newer and the mbstring extension. The standalone CLI fixtures in tests/ cover response redaction, diagnostic access controls, table-health reporting, and scheduling-failure reporting. They use dummy credentials and in-memory WordPress/database stubs, with no network calls or site-data changes. Each fixture runs in a separate PHP process; do not load them into WordPress. Exceptions produce a nonzero exit status and stop the command.
These tests do not verify actual WordPress nonce validation, MariaDB behavior, cron execution, or live Zotero authentication. Runtime checks remain necessary. The current npm run plugin-zip packaging rules exclude tests/.
Operational notes: Clearing publication caches preserves Zotero cooldowns. Backoff and Retry-After deadlines are honored in full, including waits longer than one hour. Plain and pretty permalinks are supported. After rotating WordPress salts or deploying frontend changes, purge page/CDN caches so embedded source signatures and markup are refreshed.
Public API requests are limited to 120 per minute per server-reported client IP, shared across items and authors; editors are exempt. Shared-IP visitors share this allowance. The limiter stores a count under a salted hash of the client IP and minute for up to 60 seconds; it does not store the raw IP. Configure the host to provide the actual client IP behind trusted proxies. The database-only counter is best-effort under concurrency; use host/WAF limits for strong abuse protection. Public requests cannot request the complete author facet; use bounded author autocomplete. Pagination is capped at 10,000 pages. Pending synchronization polling slows progressively to 30 seconds.
Source retention: removing a block does not remove its indexed source because another page or cached block may still use it. Clear Zotero Cache removes all source indexes and they rebuild when rendered; uninstall permanently removes plugin data. Do not use automatic age-based deletion as a substitute for checking source usage.
API credentials: prefer the server-side ZOTERO_DISPLAY_API_KEY constant where hosting permits it. Saved settings are not autoloaded; existing installations are updated when an administrator visits settings.
Readable JavaScript is included in src/ in the ZIP. Developer tooling and tests are available from https://github.com/lwangdu/nature-zotero-publications; clone the repository to run the documented npm tests and build commands.
Audit regression coverage: npm run test:js verifies plain/pretty REST URL construction and legacy cached markup. npm run test:php also checks cooldown preservation, cache slot deletion, malformed metadata, DOI encoding, byte-bounded writes, and the public request allowance. These use isolated stubs rather than live Redis/MariaDB. Existing indexed items adopt the updated normalization on the next stale refresh; the completed index remains readable while it refreshes.