Review links
A link is a URL like https://example.com/?axle_review=<token>. The token is 32 random bytes in base64url. Only its HMAC (SHA-256, keyed with wp_salt( 'auth' )) is stored, so the URL is shown once, when the link is made, and never again.
Each link has:
- an expiry, from one hour to 30 days (default 7 days, from Settings > Writing > Review)
- a view limit: 0 is unlimited, 1 opens once. A view is a new session, counted atomically, so two people opening a view-once link at the same moment cannot both get in. A session lasts 12 hours or until the link expires, so the reviewer can come back to it
- an optional email lock: the reviewer asks for a one-time code (6 digits, valid 10 minutes) sent to that address, then enters it. A forwarded link still asks for the code
- an optional password, stored with
wp_hash_password() - whether the reviewer may leave notes, and whether they see notes other reviewers left
Five wrong codes or passwords lock the link, and the visitor’s IP address, out for 15 minutes. At most five codes are sent per link in 15 minutes.
A link stops working when it expires, reaches its view limit (open sessions keep working), is revoked, when its post is published, made private or trashed (axle_review_kill_link_on_status), when the post is deleted (its links and their activity are deleted too), or when the user who made it is deleted. Pause all links on Tools > Review Links turns every link off until it is resumed; on multisite a network admin can pause every site from Network Admin > Settings > Review Links.
A link that does not open shows a small page with the reason (expired, used, revoked, paused) and HTTP status 404. An unknown token gets the same page with a generic message.
The review page
The theme renders the draft like a published post. The main query asks for the post with its own status, which WP_Query lets through, so nothing pretends the post is published. Every review response sends:
Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0and core’s no-cache headersX-Robots-Tag: noindex, nofollow, and anoindex, nofollowrobots meta tagReferrer-Policy: no-referrer- the
DONOTCACHEPAGEconstant, which page caching plugins respect
Comments are closed on the review page. A post with a post password still asks for it.
The toolbar has Leave a note and View notes I left. In note mode every block of the post is outlined: click one, or Tab to it and press Enter, to open the note popover. Escape closes the popover, then leaves note mode. Text selected in the block travels with the note as a quote. The reviewer’s name is kept for the browser session.
The review render marks each block of the post with data-axr-block (its path), data-axr-hash (a fingerprint of its name and text) and data-axr-name (the block name), using WP_HTML_Tag_Processor on the block’s first tag.
Reviewer notes become core Notes
A note from the toolbar goes to POST /wp-json/axle-review/v1/guest/notes. It needs the link’s signed session cookie (axr_<link id>, HttpOnly, SameSite=Lax) and a nonce bound to that session, has a honeypot field, a minimum time on the page (3 seconds) and a limit of 30 notes and replies per link per hour. It never sets a current user.
The note is saved with wp_insert_comment() as a core Note: comment_type note, comment_approved 0 (open), user_id 0, the reviewer’s name, and their email (the link’s address when the link is email-locked). The text is plain text, stripped of tags and capped at 2,000 characters. Then its ID goes into the block’s metadata.noteId in post_content:
- the block at the path, when its fingerprint matches
- else any block with that fingerprint (the block moved)
- else the first top-level block that can take a note, and the note starts with the quoted text (the block was deleted).
axle_review_guest_note_orphanedfires
On WordPress 7.1 and later noteId is an array, as core writes it; on 6.9 and 7.0 it is a single number, so a block holds one note thread and a block that has one is skipped. The content is written with wp_update_post() under a 10-second lock, without a revision, and without kses (the request has no user, and kses would strip what editors may use, such as embeds).
An editor with the post open. Their next save would overwrite post_content and drop the new ID. So each reviewer note keeps the comment meta _axr_pending_attach. On every save of the post (wp_insert_post_data), any pending note missing from the saved content goes back on its block. The open editor also asks GET /axle-review/v1/notes/pending every 20 seconds and puts new notes on their blocks with updateBlockAttributes, so the editor shows them and its next save has them. A save that already has the note clears the flag. Flags older than a day are dropped. wp axle-review notes reattach <post> retries by hand.
Editors reply, resolve and reopen reviewer notes in the Notes sidebar like any note. The reviewer sees replies to their own threads (same session, or the same verified email on another link) on their next visit, and can reply in them. They do not see other reviewers’ threads unless the link allows it, and never editors’ own notes.
Dashboard > Notes
Needs edit_posts (axle_review_inbox_capability). Each row is checked with edit_post on its post; users without edit_others_posts see notes on their own posts.
| View | Lists |
|---|---|
| Unresolved | Open top-level notes (the default) |
| Mentions me | Notes and replies that mention you (WordPress 7.1+): a LIKE on user-<id>, then wp_get_note_mentioned_user_ids() |
| On my posts | Notes on posts you wrote |
| From reviewers | Notes left through review links |
| Resolved | Resolved top-level notes |
| All | Open and resolved |
Filters: post type, note author, date, and post (from the Notes column). Each row: the note, its author (users by name, reviewers as "Guest: name"), the post with a link that opens the editor on that note, the block type, age, replies and status. Row actions: Open in editor, Resolve, Reopen, and an inline reply. Bulk Resolve and Reopen.
Resolve and reopen write what the editor writes: the note’s status (hold or approve) and an empty child note with _wp_note_status (resolved or reopen). Counts are cached in the object cache (group axle_review) until a note changes.
Publish gate
Off by default; Settings > Writing > Review. When on, a post with open top-level notes cannot move to publish or future:
- block editor and REST:
rest_pre_insert_{post_type}returns the erroraxle_review_unresolved_notes, HTTP 409, with the note IDs indata.notes - classic editor and code:
wp_insert_post_datakeeps the old status and the user sees a notice
Options: which post types (default: all with Notes support), whether reviewer notes count, whether notes whose block was deleted count. The editor’s pre-publish panel lists the open notes with a Show button. Users with edit_others_posts (axle_review_gate_override_capability) can publish anyway after giving a reason; the reason is added as a reply to each open note and the override lasts 10 minutes. Scheduled posts are checked when they are scheduled, not when cron publishes them.
Alerts
Each user picks on their profile: Right away (default), Hourly digest, Daily digest (8:00 site time) or Off.
- Reviewer notes on my posts: the post author by default (
axle_review_notify_recipients) - Replies to notes I started: the user who started the thread
- Mentions (7.1+): in digests only, since core emails mentions as they happen
Core keeps sending its own emails (the post author’s note email, mention emails); Axle Review sends what core does not. Notes saved outside REST, such as reviewer notes, get no core email.
Slack: one incoming webhook URL per site, stored in an option and never shown in full again. A message for each new reviewer note and each publish past the gate. Failures are kept for Site Health and fire axle_review_slack_failed.
Site Health
Critical when the tables are missing. Recommended when:
- active links live longer than 30 days (a filter raised the maximum)
- the last Slack message failed
- a page cache served a review page twice in the last week. Each page render has an ID that the toolbar reports once
- the gate holds back drafts that have not changed in two weeks
Privacy
- Tools > Export Personal Data by email: a reviewer’s notes, and the links locked to that address with their activity
- Tools > Erase Personal Data: reviewer notes keep their text, with the name and email replaced by "Reviewer" (
axle_review_erase_deletes_notesdeletes them instead); links locked to the address are revoked and forget it; their activity is deleted - Settings > Privacy gets suggested text
- IP addresses are anonymised before they are stored, and only the browser family is kept, never the user agent
Limits
- Reviewers cannot mention site users, and cannot leave inline notes on a text selection; a selection travels as a quote in the note.
- On WordPress 6.9 and 7.0 a block holds one note thread. A reviewer note on a block that already has one goes on the first block without one.
- Notes need content made of blocks. A page built with Elementor or Bricks can be shared and read, but not noted.
- Blocks that show private data (a list of users, latest drafts) show it to reviewers too.
- The editor watcher polls every 20 seconds. Real-time collaboration in core is not covered yet.
- Notes on templates and the Site Editor are not supported, as in core.