WordPress plugin for academic citation management. Provides 20 citation formats, 6 export formats, bibliography and inline citation shortcodes, JSON-LD/Scholar/OG structured data, GDPR-compliant analytics, and a Gutenberg dynamic block.
| Minimum | Recommended | |
|---|---|---|
| WordPress | 5.0 | 6.4+ |
| PHP | 7.2 | 8.0+ |
| MySQL | 5.6 | 8.0+ |
No Composer dependencies. No npm build step. Drop-in compatible.
# From WordPress admin
Plugins → Add New → Upload → citepress-2.9.13.zip
# Manual
unzip citepress-2.9.13.zip -d /var/www/html/wp-content/plugins/
wp plugin activate citepressOn activation, register_activation_hook calls citepress_create_analytics_table() which runs dbDelta() to create the {prefix}citepress_analytics table. The table is only created if analytics are enabled and the table does not already exist.
citepress/
│
├── citepress.php Bootstrap. Defines constants, loads defaults, sets globals,
│ registers activation/deactivation hooks, requires includes.
│
├── uninstall.php Runs on plugin deletion. Drops analytics table, removes
│ all citepress_* options, clears the daily purge cron event.
│
├── includes/
│ ├── analytics.php Database setup and upgrade (dbDelta). GDPR personal data
│ │ exporter and eraser. Daily purge cron. Manual purge handler.
│ │ AJAX tracking endpoint (logged-in and non-logged-in).
│ │ Consent detection from cookie values.
│ │
│ ├── settings.php Registers the citepress_setting option group. Sanitization
│ │ callback. Admin menu pages (Settings and Analytics).
│ │ Settings page HTML with tabbed UI (General, Formats, Display,
│ │ Metadata & SEO, Preview). Analytics page HTML with stats.
│ │
│ ├── shortcodes.php All citation logic. Author resolution (WordPress user,
│ │ guest-author meta, Co-Authors Plus). Citation style
│ │ definitions (20 formats). Inline citation templates.
│ │ Bibliography mode detection. [cite] and [cite_bibliography]
│ │ shortcode handlers and their namespaced aliases.
│ │ Wikipedia citation builder.
│ │
│ ├── metadata.php Auto-display hook on the_content. SEO plugin conflict guard.
│ │ Google Scholar <meta> tag output. JSON-LD ScholarlyArticle
│ │ structured data. Open Graph academic article tags.
│ │
│ └── assets.php Frontend CSS/JS enqueue. Per-page citepressSharedData and
│ citepressCitationData inline JSON output (wp_footer).
│ Admin CSS/JS enqueue (Settings and Analytics pages only).
│ Block editor asset enqueue. Gutenberg block registration
│ and server-side render callback.
│
├── assets/
│ ├── css/
│ │ ├── frontend.css Citation box, inline citation, and bibliography styles.
│ │ │ Supports light/dark mode via data-theme attribute and
│ │ │ prefers-color-scheme media query.
│ │ ├── admin.css Settings page, Analytics page, and Preview tab styles.
│ │ └── block-editor.css Block editor sidebar and citation box preview styles.
│ │
│ └── js/
│ ├── frontend.js Citation rendering, format switching, copy-to-clipboard,
│ │ export file generation, analytics AJAX dispatch,
│ │ and consent detection logic.
│ ├── admin.js Settings tab switching (with active-tab persistence),
│ │ radio card selection, live preview rendering,
│ │ and analytics purge confirm dialog.
│ └── block.js Gutenberg block edit component. Reads citepressBlockData
│ global. Renders citation preview in block editor sidebar.
│
├── languages/
│ └── cite.pot POT file for translation. Text domain: citepress.
│
├── CHANGELOG.md Full technical changelog (developer audience).
├── UPGRADING.md Post-release issue log, root cause analysis, upgrade notes.
└── readme.txt WordPress.org readme (general/layman audience).
Include file boundary rule: Each file in
includes/owns a clearly defined responsibility. A single function defined in two include files causes a PHP fatal on every page load. Before releasing, run:grep -rh "^function citepress_" includes/ | sort | uniq -dAny output means a duplicate — fix it before packaging.
Defined in citepress.php:
| Constant | Value | Purpose |
|---|---|---|
CITEPRESS_VERSION |
'3.0.0' |
Cache-busting for enqueued assets |
CITEPRESS_PLUGIN_DIR |
plugin_dir_path(__FILE__) |
Absolute filesystem path |
CITEPRESS_PLUGIN_URL |
plugin_dir_url(__FILE__) |
Full URL to plugin root |
CITEPRESS_ASSETS_URL |
CITEPRESS_PLUGIN_URL . 'assets/' |
Full URL to assets directory |
CITEPRESS_DB_VERSION |
'1.1' |
Analytics table schema version |
DB version: Bump CITEPRESS_DB_VERSION in citepress.php whenever the analytics table schema changes. citepress_maybe_upgrade_db() runs on admin_init and calls dbDelta() automatically when the stored citepress_db_version option differs from the constant.
Set in citepress.php at plugin load time:
| Global | Type | Purpose |
|---|---|---|
$citepress_setting |
array |
Merged user settings + defaults. Available everywhere via global $citepress_setting. |
$citepress_citation_data |
array |
Accumulates per-instance citation data during shortcode rendering. Serialised to citepressCitationData in wp_footer. |
$citepress_cited_posts |
array |
Tracks which post IDs have been cited (for bibliography deduplication). |
$citepress_ref_counter |
int |
Auto-incrementing footnote reference number for bibliography mode. |
$citepress_bibliography_mode |
bool |
Set to true when [cite_bibliography] is present on the page. Makes [cite] render as superscript markers instead of full boxes. |
$citepress_instance_counter |
int |
Unique ID for each citation box instance on the page. |
Stored as a single serialised array in wp_options under the key citepress_setting. Option group: citepress_setting. All values pass through citepress_sanitize_settings().
Default values (filterable via citepress_default_setting):
| Key | Default | Description |
|---|---|---|
auto_display |
'no' |
Append citation box to post content automatically |
display_position |
'bottom' |
'top' or 'bottom' for auto-display |
show_toggle |
'yes' |
Show collapse/expand toggle on citation box |
enable_analytics |
'no' |
Enable analytics tracking |
require_consent_for_analytics |
'yes' |
Gate analytics on cookie consent |
analytics_cooldown_mode |
'session' |
'session', 'ip_hash', or 'none' |
analytics_retention_days |
'90' |
Days before analytics rows are auto-purged |
post_types |
['post'] |
Post types to auto-display on |
excluded_posts |
'' |
Comma-separated post IDs to skip |
show_google_scholar |
'yes' |
Output Scholar <meta> tags |
show_jsonld |
'yes' |
Output JSON-LD structured data |
show_og_academic |
'yes' |
Output Open Graph academic tags |
et_al_threshold |
'3' |
Author count before "et al." kicks in |
format_display_mode |
'dropdown' |
'dropdown', 'tabs', or 'buttons' |
color_scheme |
'auto' |
'auto', 'light', or 'dark' |
enabled_export_formats |
all 6 | Which export buttons to show |
enabled_formats |
all 20 | Which citation styles are available |
| Hook | Callback | Priority | File |
|---|---|---|---|
admin_init |
citepress_register_setting |
10 | settings.php |
admin_init |
citepress_maybe_upgrade_db |
10 | analytics.php |
admin_init |
citepress_handle_analytics_purge |
10 | analytics.php |
admin_init |
citepress_add_privacy_policy_content |
10 | analytics.php |
admin_menu |
citepress_setting_menu |
10 | settings.php |
admin_enqueue_scripts |
citepress_enqueue_admin_assets |
10 | assets.php |
wp |
citepress_schedule_purge |
10 | analytics.php |
wp_enqueue_scripts |
citepress_enqueue_frontend_styles |
10 | assets.php |
wp_enqueue_scripts |
citepress_enqueue_frontend_scripts |
10 | assets.php |
wp_footer |
citepress_output_citation_data |
10 | assets.php |
wp_head |
citepress_add_google_scholar_tags |
5 | metadata.php |
wp_head |
citepress_add_jsonld_structured_data |
10 | metadata.php |
wp_head |
citepress_add_og_academic_tags |
5 | metadata.php |
wp_ajax_citepress_track_analytics |
citepress_track_analytics |
10 | analytics.php |
wp_ajax_nopriv_citepress_track_analytics |
citepress_track_analytics |
10 | analytics.php |
init |
citepress_register_block_type |
10 | assets.php |
enqueue_block_editor_assets |
citepress_enqueue_block_editor_assets |
10 | assets.php |
citepress_daily_purge_event |
citepress_do_daily_purge |
10 | analytics.php |
register_activation_hook |
citepress_create_analytics_table |
— | citepress.php |
register_deactivation_hook |
citepress_deactivate |
— | citepress.php |
These are the apply_filters() call sites — where you can hook in to modify behaviour.
Citation output:
| Filter | Args | Description |
|---|---|---|
citepress_citation_styles |
$styles (array of 20 format definitions) |
Add, remove, or modify citation style templates. Memoised — fires once per request. |
citepress_inline_templates |
$templates (array, one per format) |
Modify the parenthetical inline citation templates. |
citepress_inline_template |
$template, $style |
Modify a single inline template for a specific format at render time. |
citepress_inline_citation |
$html, $atts, $post |
Filter the complete rendered inline citation HTML before output. |
citepress_bibliography_heading |
$heading (string) |
Modify the bibliography section heading text. |
citepress_bibliography_entry |
$entry_html, $ref_number, $entry |
Modify a single bibliography list item before it is appended. |
citepress_bibliography_output |
$output, $unique_entries |
Filter the complete bibliography HTML before output. |
Structured data:
| Filter | Args | Description |
|---|---|---|
citepress_scholar_meta |
$meta_data (array) |
Modify the Scholar <meta> tag data array before output. |
citepress_jsonld_data |
$jsonld, $post |
Modify the JSON-LD structured data array before json_encode. |
citepress_og_academic_data |
$og_data (array) |
Modify the Open Graph academic tag data array before output. |
citepress_output_scholar_meta |
false |
Return true to force Scholar meta output even when a supported SEO plugin is active. |
citepress_output_jsonld |
false |
Return true to force JSON-LD output even when a supported SEO plugin is active. |
citepress_output_og_tags |
false |
Return true to force OG tag output even when a supported SEO plugin is active. |
Analytics and GDPR:
| Filter | Args | Description |
|---|---|---|
citepress_analytics_optout_active |
false |
Return true to suppress analytics tracking for the current request (e.g. for admins or logged-in users). |
citepress_analytics_consent_granted |
null |
Return true to grant consent programmatically, false to deny it. Return null to fall through to the cookie-detection logic. |
citepress_analytics_identity_resolver |
null, $email_address |
Map an email address to an array ['post_ids' => [...]] for GDPR data export and erase. Return null if the email cannot be mapped (default — plugin uses session-based tracking with no PII). |
Settings:
| Filter | Args | Description |
|---|---|---|
citepress_default_setting |
$defaults (array) |
Modify the default settings array before it is merged with stored options. Use this to change defaults for new installs or to force-override specific settings. |
Renders a citation box or inline citation.
| Attribute | Type | Default | Description |
|---|---|---|---|
style |
string | 'apa' |
Default citation format to show on load |
mode |
string | 'box' |
'box', 'inline', or 'bibliography' |
formats |
string | — | Comma-separated list of format IDs to include (overrides enabled formats) |
exclude_formats |
string | — | Comma-separated format IDs to hide for this instance |
show_copy |
string | 'true' |
'true' or 'false' |
show_export |
string | 'true' |
'true' or 'false' |
show_toggle |
string | 'true' |
'true' or 'false' |
custom_author |
string | — | Override the post author name |
page |
int | — | Page number appended in inline mode |
link |
string | 'true' |
'true' or 'false' — include a link in inline mode |
Renders a numbered reference list. When this shortcode is detected anywhere on the page, all [cite] shortcodes on that page switch into bibliography mode and render as superscript footnote markers [1], [2], etc. instead of full citation boxes.
| Attribute | Type | Default | Description |
|---|---|---|---|
heading |
string | 'References' |
Section heading text. Filterable via citepress_bibliography_heading. |
style |
string | 'apa' |
Citation format for the bibliography list entries |
Action: citepress_track_analytics
Methods: POST (both wp_ajax_* and wp_ajax_nopriv_* — no nonce required by design; nonces break full-page caching)
POST fields:
| Field | Type | Description |
|---|---|---|
post_id |
int | Post being cited |
citation_style |
string | Format ID (e.g. 'apa') |
action_type |
string | 'view', 'copy', or 'export' |
Server-side consent logic (citepress_track_analytics()):
- Check
citepress_analytics_optout_activefilter — iftrue, bail. - Check
citepress_analytics_consent_grantedfilter — iffalse, bail; iftrue, proceed; ifnull, fall through. - Call
citepress_detect_consent_from_cookies()— inspects cookies set by supported consent plugins (Borlabs Cookie, Cookiebot, GDPR Cookie Consent, Cookie Notice). If a known plugin is present and has explicitly denied consent, bail. If no known plugin is detected, allow (nothing to check against). - Apply cooldown: session-based (transient keyed to
PHPSESSID), IP-hash-based, or none. - Insert row into
{prefix}citepress_analyticsand invalidate the stats cache.
JavaScript consent logic (hasAnalyticsConsent() in frontend.js):
Performs the same cookie checks client-side as a pre-flight before dispatching the AJAX call. The PHP side makes the authoritative decision — it can read HttpOnly cookies that JS cannot.
Created by citepress_create_analytics_table() on plugin activation and kept current by citepress_maybe_upgrade_db() on admin_init.
| Column | Type | Description |
|---|---|---|
id |
bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT |
Primary key |
post_id |
bigint(20) UNSIGNED NOT NULL |
WP post ID |
citation_style |
varchar(50) NOT NULL |
Format ID string |
action_type |
varchar(20) NOT NULL |
'view', 'copy', or 'export' |
timestamp |
datetime DEFAULT CURRENT_TIMESTAMP |
UTC insert time |
Indexes: PRIMARY KEY (id), KEY post_id (post_id), KEY action_type (action_type), KEY timestamp (timestamp).
Schema version: Controlled by CITEPRESS_DB_VERSION constant. To add a column: update the CREATE TABLE statement in citepress_create_analytics_table(), bump the constant, and citepress_maybe_upgrade_db() will run dbDelta() on the next admin_init.
Output in wp_footer by citepress_output_citation_data() in assets.php. Only present on pages that contain at least one citation box.
Output once per page. Contains data shared across all citation boxes.
{
styles: { apa: { name, template, ... }, mla: { ... }, ... }, // all 20 formats
ajaxUrl: "https://example.com/wp-admin/admin-ajax.php",
enableAnalytics: true,
requireConsent: true
}Array of per-instance objects, one per [cite] shortcode on the page.
[
{
instanceId: "citepress-1",
postId: 42,
author: "Smith, J.",
title: "Article Title",
year: "2024",
url: "https://...",
accessDate: "11 March 2026",
formats: ["apa", "mla", "chicago"], // enabled for this instance
defaultStyle: "apa",
showCopy: true,
showExport: true,
showToggle: true,
colorScheme: "auto",
exportFormats: ["bibtex", "ris", ...]
},
...
]Output by citepress_enqueue_block_editor_assets(). Available in the block editor only.
{
styles: { ... }, // same as citepressSharedData.styles
defaultStyle: "apa",
postData: { author, title, year, url }
}| Handle | File | Hook | Condition |
|---|---|---|---|
citepress-frontend |
assets/css/frontend.css |
wp_enqueue_scripts |
All front-end pages |
citepress-frontend |
assets/js/frontend.js |
wp_enqueue_scripts |
All front-end pages |
citepress-admin |
assets/css/admin.css |
admin_enqueue_scripts |
Settings and Analytics pages only |
citepress-admin |
assets/js/admin.js |
admin_enqueue_scripts |
Settings and Analytics pages only |
citepress-block |
assets/js/block.js |
enqueue_block_editor_assets |
Block editor only |
citepress-block-editor |
assets/css/block-editor.css |
enqueue_block_editor_assets |
Block editor only |
Admin assets are guarded by hook suffix check:
if ( 'toplevel_page_citepress' !== $hook && 'citepress_page_citepress-analytics' !== $hook ) {
return;
}Block name: citepress/block
Registration: register_block_type() in citepress_register_block_type() on init
Render type: Dynamic — save() returns null; output is generated server-side by citepress_render_citation_block( $attributes )
Sidebar controls: Default citation style, format display mode (dropdown/tabs/buttons), which formats to show, show/hide copy and export buttons
The block calls citepress_display_citation() server-side with the block attributes merged over the page-level shortcode defaults.
citepress_seo_plugin_active() in metadata.php returns true if any of these plugins is active:
- Yoast SEO (
wpseo-generaloption) - RankMath (
rank_math_modulesoption) - All in One SEO (
aioseo_optionsoption) - The SEO Framework (
autodescription-site-settingsoption)
When active, Scholar meta, JSON-LD, and OG tags are suppressed by default to avoid duplicate structured data. Each can be re-enabled individually via its corresponding filter:
add_filter( 'citepress_output_jsonld', '__return_true' );
add_filter( 'citepress_output_scholar_meta', '__return_true' );
add_filter( 'citepress_output_og_tags', '__return_true' );| Field key | Description |
|---|---|
guest-author |
Overrides the WordPress post author name in citations. Takes precedence over Co-Authors Plus. |
orcid |
Author ORCID identifier. Included in citation output where the format supports it and in Scholar meta tags. |
CitePress registers a personal data exporter and eraser with WordPress privacy tools (wp_privacy_personal_data_exporters / wp_privacy_personal_data_erasers).
By default these tools do nothing because session-based tracking stores no PII. To wire them up for a custom identity resolver (e.g. if you extend the plugin to associate analytics with user accounts), implement the citepress_analytics_identity_resolver filter:
add_filter( 'citepress_analytics_identity_resolver', function( $identifier, $email ) {
$user = get_user_by( 'email', $email );
if ( ! $user ) return null;
$post_ids = get_posts([
'author' => $user->ID,
'post_status' => 'any',
'fields' => 'ids',
'numberposts' => -1,
]);
return $post_ids ? [ 'post_ids' => $post_ids ] : null;
}, 10, 2 );add_filter( 'citepress_citation_styles', function( $styles ) {
$styles['my_format'] = [
'name' => 'My Custom Format',
'template' => '{author} ({year}). <em>{title}</em>. {url}.',
];
return $styles;
} );Format IDs must be lowercase alphanumeric with underscores. The template tokens available are: {author}, {year}, {title}, {url}, {access_date}, {publisher}, {journal}, {volume}, {issue}, {pages}, {doi}, {orcid}.
Object caching: Analytics stats queries use wp_cache_get / wp_cache_set with the citepress cache group. On object-cache-enabled hosts (Redis, Memcached), the cache is invalidated on every successful analytics insert and on manual purge. No action needed — WP's cache API handles this transparently.
Full-page caching: The analytics AJAX endpoint (admin-ajax.php) must not be cached. Most caching plugins exclude admin-ajax.php by default. The analytics endpoint intentionally has no nonce — nonces contain a timestamp and change every 12 hours, which would break cached pages that embed a nonce in the markup.
Cron: citepress_schedule_purge() registers a daily WP-Cron event (citepress_daily_purge_event) that deletes analytics rows older than the configured retention period. On hosts with WP-Cron disabled (DISABLE_WP_CRON = true), set up a real cron job:
*/30 * * * * php /var/www/html/wp-cron.php > /dev/null 2>&1Database: The plugin creates one table. No custom indexes beyond what is listed in the schema section above. Queries use $wpdb->prepare() for all user-supplied values. Integer IDs from absint() are inlined directly.
Multisite: Not tested in multisite. The analytics table is created in the active site's database. Settings are stored per-site. No network admin UI.
| Threat | Mitigation |
|---|---|
| XSS | All output goes through esc_html(), esc_attr(), esc_url(), or wp_kses() |
| SQL injection | $wpdb->prepare() for all parameterised queries; integer IDs inlined after absint() |
| CSRF | Settings form uses settings_fields() (WP nonce). Manual purge uses a dedicated nonce. Analytics AJAX has no nonce (intentional — see caching note above) |
| Privilege escalation | Settings and analytics pages require manage_options. Purge action verifies nonce and capability before acting |
| Rate limiting | Analytics tracking uses transients (session or IP-hash keyed) to limit one event per cooldown window per visitor |
Security reports: security@menj.net — please do not open public issues for vulnerabilities.
See readme.txt for the user-facing privacy policy. Technical detail:
- Default cooldown mode is
session— a transient is keyed to the PHP session ID, no IP stored. ip_hashmode storeshash('sha256', $ip . $salt)where$saltis a site-specificAUTH_KEY. This is a one-way hash. Whether it constitutes personal data under GDPR Article 4(1) depends on jurisdiction — consult your DPA.- No data leaves the server. No external connections.
git clone https://github.com/menj/cite
cd cite
# No build step required — plain PHP/JS/CSSRun Plugin Check before submitting a PR:
wp plugin check citepressGPL v2 or later — https://www.gnu.org/licenses/gpl-2.0.html