Action Links
SkillMonitoring & opsGuides implementation of structured action links on log events. Use when adding get_action_links() to a logger or migrating from get_log_row_details_output().
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Action Links skill
What this skill tells your AI
The instructions your AI receives, as published by bonny/wordpress-simple-history in .claude/skills/action-links/SKILL.md and read by ahel’s review.
Structured navigational links rendered below log events. Added in 5.24.0.
UX Principle
Icons represent action type, not destination. Use a small, consistent icon vocabulary so users learn the pattern once. The label text describes where the link goes — the icon just reinforces what kind of action it is.
Label Wording
The verb is supplied by the icon + link styling. The label encodes the destination, not the action.
Rule: if the verb describes "navigate to" — drop it. If the verb describes a state change or a non-obvious operation — keep it.
| Drop the verb (destination labels) | Keep the verb (state-change / non-obvious) |
|---|---|
| About this version (not "View About …") | Restore from Trash |
| Site Health (not "View Site Health") | Approve comment |
| Plugin info (not "View plugin info") | Trash comment |
| All updates (not "View all updates") | Stick post |
| Changelog (not "View changelog") | Revert revision |
Edit · View · Preview · Revisions on the same noun is the one exception — those verbs differ from each other and the verb is the whole differentiator, so keep them (e.g. on a post: Edit post, View post, Preview post, Revisions). The icons already match the verbs; the labels still need the verbs to disambiguate intent across the row.
Describe the content, not the page name. Sometimes WP's own wording for a destination is generic (e.g. the admin bar's "About WordPress" item points to a version-specific page). A label that describes what the user will find there — like "About this version" instead of "About WordPress" — beats a literal mirror of WP's vocabulary when the literal mirror is less informative.
Self-describing in REST. Action links also appear in REST JSON. The bare-noun label + the structured action field ("view", "edit") is enough — consumers that want a verbose string concatenate them. Don't smuggle the verb into the label to compensate.
Overview / Destination Links
In addition to per-item links (Edit/View/Preview the specific thing this event is about), consider adding a second link that points to the admin list for that item type. Examples:
| Item type | Overview link | URL |
|---|---|---|
| Users | All users | users.php |
| Plugins | All plugins | plugins.php |
| Posts (per-CPT) | All posts / All pages / All <cpt> | edit.php?post_type=<type> |
| Media | All media | upload.php |
| Comments | All comments | edit-comments.php |
Why include them:
- Admin shortcut. From an event card the user often wants to "go act on the related thing" — the overview link saves the trip through the left nav.
- REST / CLI / agent consumers. Action links also appear in the REST response. Consumers outside wp-admin (CLI, dashboards, AI agents) can't fall back on the admin menu at all; the overview link is the only navigable hand-off into wp-admin they get.
- Delete events. When the item no longer exists, the per-item link is dead. Always include the overview link on delete events — it's the only useful destination. (Post and Media loggers do this: even when the per-post/per-attachment block is suppressed for
post_deleted/attachment_deleted, the overview link still renders.)
Label pattern: All <plural> — bare noun per the Label Wording rule. For post types use the registered plural label (e.g. strtolower( $post_type_obj->labels->name )) so custom post types render correctly. Cap-check against the destination's required capability (list_users, activate_plugins, $post_type_obj->cap->edit_posts, upload_files, etc.), not the logger's general capability.
Drift-aware gating. Some destinations only make sense for the current state of the site. The clearest example: wp-admin/about.php always shows the current WP version's content, so linking to it from a 2-year-old "Updated to 6.4" event would land the user on info about 6.9 — misleading. Solution in class-core-updates-logger.php: compare the event's new_version against the current install; only render the local about-page link when they match. Apply the same pattern any time a link's destination drifts out of sync with the historical state the event captured.
Action Types
Use only these five types. Do not invent new ones unless truly necessary.
| Action | Icon | When to use |
|---|---|---|
view | Eye (visibility) | Navigate to see/inspect something |
edit | Pencil | Navigate to modify something |
preview | Preview | View a draft or unpublished item |
revisions | History clock | Compare versions or view change history |
details | Info | Open the event details modal (auto-appended via Logger::event_has_more_details()) |
External vs internal links
The action type (view/edit/etc.) describes intent. The icon you actually see depends on where the link goes:
- Internal URL (same host as the admin) → renders the action's icon (
view→ eye, etc.). - External URL (different host) → renders the external-link icon (box with arrow exiting) and is opened in a new tab with
rel="noopener noreferrer". This swap is automatic inEventActionLinks.jsxviaisExternalUrl()— loggers don't set a flag.
Keep action: 'view' on external links anyway. The action describes the user's intent (read more); the external indicator is a UI concern handled at render time.
Most links are view. When in doubt, use view. Don't return details from get_action_links() directly — opt in via event_has_more_details() instead so the modal-link wiring stays consistent.
event_has_more_details() returns string|false: the label to render (e.g. __( 'Show error message', ... )), or false to skip. Pick a label that names the actual payload (Show error message, Show all 47 roles) rather than a generic Show details — the icon already signals "more info", the label should sell what's behind it.
Verify the payload exists before returning a label. A logger opted into this should still inspect $row->context and return false when the relevant keys are missing — otherwise older events without the payload would lead users to a modal with nothing extra. Match on both the message key and the presence of the data.
PHP: Adding Action Links to a Logger
Override get_action_links() in your logger class. Always check capabilities.
public function get_action_links( $row ) {
if ( ! current_user_can( 'required_capability' ) ) {
return [];
}
return [
[
'url' => admin_url( 'about.php' ),
'label' => __( 'About this version', 'simple-history' ),
'action' => 'view',
],
];
}
The single-link example uses a bare-noun label per the Label Wording rule above.
Required Keys
Each link must have all three keys:
url— Full URL (useadmin_url(),get_edit_post_link(), etc.)label— Translated, human-readable textaction— One of:view,edit,preview,revisions
Multiple Links
Return multiple links when relevant. Order: edit first, then view, then others.
The example below keeps the verbs (Edit post, View post) because they're the same-noun disambiguation exception from the Label Wording rule — when several actions target the same noun, the verb is the differentiator and must stay.
$action_links = [];
if ( current_user_can( 'edit_post', $post_id ) ) {
$action_links[] = [
'url' => get_edit_post_link( $post_id, 'raw' ),
'label' => __( 'Edit post', 'simple-history' ),
'action' => 'edit',
];
}
if ( get_post_status( $post_id ) === 'publish' ) {
$action_links[] = [
'url' => get_permalink( $post_id ),
'label' => __( 'View post', 'simple-history' ),
'action' => 'view',
];
}
return $action_links;
No Links in the Message Body When Action Links Cover the Destination
Action links are the canonical "what can I do?" affordance. The message body's job is "what happened?" — a declarative sentence. Putting an <a> inside the message (e.g. wrapping {post_title} in the template) re-introduces the visual hierarchy collapse action links were designed to eliminate: the linked title reads as a CTA mid-sentence and competes with the action row below.
Rule: Inline links inside message templates are permitted only when they point somewhere the action row cannot reach. If get_action_links() already returns a link to that destination (Edit, View, Revisions, the overview page, …), the message text must be plain.
Examples:
| Message template | OK? |
|---|---|
Updated page "<a href='…'>{post_title}</a>" with Edit/View in action row | ❌ duplicates the Edit link — drop the <a>, keep just "{post_title}" |
Updated page "{post_title}" | ✅ plain title, action row handles navigation |
Mentioned {external_url} where no action link points there | ✅ legitimate — the action row can't represent arbitrary external references |
Deleted-item events stay plain too. When the per-item link would be dead (post trashed, plugin uninstalled), a plain title beats a broken link. The overview action link (All pages, All plugins) is the right hand-off.
A11y: screen reader users navigating by link list hear fewer, clearer labels — "Edit page" beats an ambiguous quoted title followed by "Edit page".
This is not an urgent migration. Apply opportunistically when touching a logger for other reasons. See the logger-messages skill for the corresponding guidance on the message side.
Migrating from Inline Links
When moving a link from get_log_row_details_output() to action links:
- Add
get_action_links()with the link - Remove the inline
<a>HTML fromget_log_row_details_output() - Migrate the remaining details HTML to the Event Details API (see logger-messages skill)
- Keep capability checks in the new method
Common inline links to migrate: "View/Edit" comment links, "View plugin info" thickbox links, post edit links embedded in detail tables. These are all navigational — they belong here, not in Event Details.
When migrating, apply the Label Wording rule — don't carry "View X" labels over verbatim. A legacy "View comment" inline link becomes Comment (or Approve comment if that's the action you're actually surfacing). The icon supplies the verb.
Constraints
get_action_links()runs on every event in the REST response — it is not gated behind the experimental-features flag (reactions are; action links are not). Keep the body cheap: short-circuit when no link could possibly apply, prefer cap checks before DB-touching helpers likeget_post().- The same action type may appear more than once per event when it points to genuinely different destinations (e.g. two
viewlinks — one to a local admin page, one to an external reference page). React keys are scoped byurl, not by action.
Architecture
REST Controller (prepare_item_for_response)
→ Simple_History::get_action_links($row)
→ Logger::get_action_links($row)
→ filter: simple_history/get_action_links
→ REST response (action_links field)
→ EventActionLinks.jsx renders with icons
Key Files
| File | Role |
|---|---|
loggers/class-logger.php | Base class, default empty get_action_links() |
inc/class-simple-history.php | Routes to logger, applies filter |
inc/class-wp-rest-events-controller.php | REST schema and response |
src/components/EventActionLinks.jsx | Frontend rendering with icons |
css/styles.css | Icon mask-image rules for action links |
Adding a New Action Type (Rare)
Only if the four standard types truly don't fit:
- Add SVG to
css/icons/(Material Symbols, 48px, FILL0, wght400) - Add CSS mask rule in
css/styles.cssunder the action links section - Add mapping in
ACTION_ICONSinsrc/components/EventActionLinks.jsx - Update this skill document
Examples in Codebase
- Drift-aware + external link:
loggers/class-core-updates-logger.php— localAbout this versionlink gated bycurrent install X.Y == event new_version X.Y, always-shown externalWordPress {version} release noteslink with auto external-link icon. - Overview + per-item:
loggers/class-user-logger.php,loggers/class-plugin-logger.php,loggers/class-media-logger.php— overview link (All users/All plugins/All media) plus per-item action when applicable. - Per-post-type overview + same-noun disambiguation:
loggers/class-post-logger.php—Edit %s/View %s/Preview %s/Revisionson the existing post plusAll <plural>overview gated by$post_type_obj->cap->edit_posts. Overview link still rendered onpost_deletedevents. Canonical example of the same-noun exception keeping verbs across the row. - Single overview link:
loggers/class-site-health-logger.php(Site Health),loggers/class-available-updates-logger.php(All updates/Changelog).
Signals
- GitHub stars
- 317
- Forks
- 74
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
action-links- Source
- github.com/bonny/wordpress-simple-history