Skip to content

Release v2.30.0 - #1246

Merged
rizaardiyanto1412 merged 86 commits into
masterfrom
release-2.30.0
Oct 26, 2026
Merged

rizaardiyanto1412 merged 86 commits into
masterfrom
release-2.30.0

Conversation

@repile

@repile repile Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Release v2.30.0

What this PR changes

  • publishpress-checklists.php: plugin header Version and PPCH_VERSION set to 2.30.0
  • readme.txt: Stable tag: 2.30.0 and Tested up to: 7.1
  • CHANGELOG.md: 2.30.0 entry (date set to 20 October, 2026; check it on release day)

Not listed in the changelog (internal only): Dependabot bumps #1224 #1225 #1227 and the build-zip workflow #1237. Issues #1169, #1212 and #1215 were closed in the milestone without merged code, so they are not listed either.

Checks run locally on this branch

  • composer check:php (PHP 5.6 to 8.5): passed
  • composer lint:php: passed, 140 files
  • composer lint:phpcs (both passes, .phpcs.xml and .phpcs-plugin-review.xml): passed, no findings
  • composer build: built publishpress-checklists-2.30.0.zip, and the packaged plugin carries 2.30.0
  • npx webpack: the rebuilt JS bundles match the committed ones byte for byte

Still to do before release (per #1199)

Companion Pro PR: publishpress/publishpress-checklists-pro#180

🤖 Generated with Claude Code

rizaardiyanto1412 and others added 30 commits August 13, 2026 00:20
…-2.29.0

Merge master into development after v2.29.0
Adds the teaser entry for the new Pro "Required Words" requirement,
alongside the existing Prohibited Words entry, so free users see it in
the Pro feature list and the requirement is labelled correctly.

Pairs with the publishpress-checklists-pro branch
feature/required-words-checklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…the admin host

Resolves #439. On sites where the front-end runs on a different host
than wp-admin (e.g. website.com vs wp.website.com), the internal-links
editor check counted zero internal links, no matter the content: JS
extract_internal_links()/extract_external_links() defaulted their host
to window.location.host, which in wp-admin is the admin host, never
the host content actually links to.

- localize the site's real hosts (home_url + site_url) as
  ppChecklists.internal_link_hosts and match against all of them, in
  both the editor JS and the PHP-side validation, via a new
  publishpress_checklists_internal_link_hosts filter for setups where
  the front-end is neither home_url() nor site_url().
- fix Internal_links::extract_internal_links(): `strpos(...) == false`
  is true when a link starts with the host (strpos returns 0), so such
  links were wrongly excluded. Changed to `!== false`.

Passing an explicit $website/website still restricts matching to that
one host, so existing callers of these methods are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves #1097 and #467. Counter tasks (excerpt/title/word count, tag
and category count, taxonomy counts, internal/external link count)
only showed pass/fail, giving no sense of how close the content was to
the configured range. This adds a small live badge, e.g. "(42 / 60)",
next to the task label, updating on the same tic/content-change cycle
each counter already uses.

Two shared helpers in meta-box.js:
- format_live_count(count, min, max): "count / max" when a max is
  configured, "count (min N)" for a minimum-only rule, else just the
  count.
- update_live_count(id, text): updates the badge in the metabox DOM
  and appends it to ppChecklists.requirements[id].label (caching the
  original label first, so repeated updates replace rather than stack
  the badge).

Wired into all 9 counter-type tasks across both the classic editor and
Gutenberg code paths (11 call sites): tags, categories, hierarchical
and non-hierarchical taxonomies, excerpt and title character counts,
word count, and internal/external link counts.

image_alt_count is intentionally NOT covered: it validates each
image's alt text length separately (Array.every()), so there is no
single number to show.

No changes needed in gutenberg-panel.jsx: the sidebar already renders
requirement labels via dangerouslySetInnerHTML and re-syncs whenever
'pp-checklists.requirements-updated' fires, which every tic handler
already triggers after a status check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves #453. Custom task titles were a single-line <input>, so
longer instructions had to be squeezed onto one line.

- Custom_item::get_setting_title_html() now renders a <textarea>,
  matching what Openai_item already does (same
  pp-checklists-custom-item-title class, which already has textarea
  sizing in admin-pages.css).
- global-checklists.js built an <input> for newly-added custom tasks
  and a <textarea> only for OpenAI tasks (branched on item type). That
  meant a freshly-added custom task and a saved one would render
  differently. Both now get a <textarea>; every consumer in that file
  (validation, value reads) already works off the shared class
  selector and .val(), not the tag, so this needed no other changes.
- sanitize_checklists_options() ran sanitize_text_field() over every
  option value, which collapses newlines, so the textarea alone would
  have silently discarded them on save. Added is_multiline_option() to
  route keys ending in "_title" (custom/OpenAI task titles - the only
  option keys with that suffix in this codebase) through
  sanitize_textarea_field() instead.
- white-space: pre-line on custom task labels so saved line breaks
  actually render in the editor list item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two issues caught in review before opening the PR:

- format_live_count()'s min-only branch returned an already-
  parenthesized string ("42 (min 50)"), which update_live_count()
  then wrapped in another pair of parens for both the label and the
  DOM badge, producing "(42 (min 50))" for every min-only counter task
  (a common configuration - filled_excerpt, title_count, and most of
  the count-type tasks in this plugin are typically configured with
  only a minimum). Changed to a bare "42, min 50" so the single
  outer wrap in update_live_count produces clean text.

- The live badge is appended into req.label, which
  gutenberg-panel.jsx's normalizeLabelForSort() also uses to build the
  alphabetical-sort key when "Checklist task sort order" is set to
  Alphabetical. Since the badge's digits change as the user types, two
  counter tasks that happen to share a label (e.g. after using the
  per-task label rename feature) could visibly swap positions in the
  sidebar on every keystroke. normalizeLabelForSort() now strips the
  .pp-checklists-live-count element before extracting the sort text.

Rebuilt gutenberg-panel.min.js via webpack (NODE_ENV=development, to
match how the checked-in bundle is already built - production mode
produces a fully minified single line that would otherwise replace the
whole file). gutenberg-warning.min.js is untouched, matching its
unmodified .jsx source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d custom ones

Caught in review before opening the PR: the white-space: pre-line rule
was scoped to .pp-checklists-custom-item, which for Custom_item is
NOT "this is a custom task" - filter_requirements_list() overwrites
that array key with isUserRolePermitted(), i.e. whether the *viewing
user's role* is allowed to complete the task. A custom task restricted
to Administrators would render correctly for an Administrator but
collapse back to one line for an Author viewing the identical post,
since the class (and the CSS rule with it) is simply absent from the
markup for that viewer.

There's no reliable "is this architecturally a custom/openai task"
signal to key off instead - $req['type'] is always the string 'base'
(Base_requirement's default, never overridden by any subclass) - so
the simplest correct fix is to apply white-space: pre-line to the
.status-label of every task, not just custom ones. Built-in task
labels are single-line translated strings with no embedded newlines,
so this is a no-op for them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Caught in review before opening the PR (low severity, but the PHP
side already guarded against it). get_internal_link_hosts() localizes
the site's hosts and is filterable via
publishpress_checklists_internal_link_hosts for headless/multi-domain
setups. If a filter callback ever introduced an empty string, JS's
extract_internal_links()/extract_external_links() would treat it as
matching every link - link.indexOf('') is always 0 - misclassifying
every link (including genuinely external ones) as internal in the
live editor. Internal_links.php and External_links.php already guard
with $host !== '' before matching; checklists.php's
get_internal_link_hosts() and the JS get_internal_link_hosts() now
filter out empty entries too, so PHP and JS agree even under an
unusual filter callback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.5 to 3.1.7.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](fastify/fast-uri@v3.1.5...v3.1.7)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.1 to 4.28.8.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](browserslist/browserslist@4.28.1...4.28.8)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Clicking "No" on the "Are you sure you want to publish anyway?" modal in
the Classic Editor left the whole editor unclickable until a page reload.

Remodal only leaves its "opening"/"closing" state once the CSS animation
fires animationstart and animationend. Both checklist modals share a
single overlay element, so opening one while the other is still animating
hides and re-shows that shared node mid-animation and the events are lost.
The instance then stays in the "opening" state forever, and close() bails
out at its first guard, so "No" does nothing: the overlay stays on top of
the editor and absorbs every click, including the ones on Publish. This is
also what made the modal appear to flash in twice.

Rather than patching Remodal's state machine, opt these two modals out of
the animation. getAnimationDuration() then reports 0 and Remodal takes its
synchronous path, where the state is always settled, so the whole class of
stuck-overlay bugs goes away.

On top of that:

- Only open a checklist modal when neither of them is open, so the two
  instances can no longer fight over the shared overlay.
- On dismissal, reset the publishing state and re-enable the submit
  buttons and spinners. The handler in wp-admin/js/post.js disables them
  as soon as the form is submitted and never restores them when another
  handler blocks the submission.
- Introduce the recommendation list with a "These recommendations are
  missing:" heading and move the question below it, so the modal says what
  the items under it actually are.
- Drop a dead branch that printed ppChecklists.msg_missed_required. That
  key is never localized, so it would have rendered the literal string
  "undefined". The branch was unreachable, as it only runs when the list
  of required items is empty.

Fixes #1209

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bumps [composer/composer](https://github.com/composer/composer) from 2.10.2 to 2.10.3.
- [Release notes](https://github.com/composer/composer/releases)
- [Changelog](https://github.com/composer/composer/blob/main/CHANGELOG.md)
- [Commits](composer/composer@2.10.2...2.10.3)

---
updated-dependencies:
- dependency-name: composer/composer
  dependency-version: 2.10.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Serialize the complete checklist settings payload as JSON before submission so PHP can save every post type without hitting max_input_vars. Keep the decoded payload available to existing validation callbacks.

Fixes #1229
WordPress 7.1 disables the Save draft button and Ctrl+S while post saving
is locked. Checklists used lockPostSaving() to disable the Publish button,
so drafts could not be saved while a blocking requirement was incomplete.

Disable only the Publish/Update button with a body class instead. Publishing
is still blocked by the savePost() filter in gutenberg-panel.jsx.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9vmMdC7igi2xofoqVtEgE
Fix Save draft blocked by incomplete requirements on WordPress 7.1
Run it from Actions or add the build-zip label to a pull request. The
zip is attached to the run as an artifact named after the ref and commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX51hBJ7p6KQ31drzHVXTx
The build steps now live in publishpress/github-workflows, so this file
only declares the triggers and the plugin's inputs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX51hBJ7p6KQ31drzHVXTx
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX51hBJ7p6KQ31drzHVXTx
Add a workflow that builds the plugin zip from any branch or PR
…lder

The Yoast SEO module and the Util plugin-detection helpers call
is_plugin_active() during init. WordPress before 6.8 only loads
wp-admin/includes/plugin.php for wp-admin after init, so every admin
screen failed with "Call to undefined function is_plugin_active()" when
Yoast SEO was not active. Before 2.29.0, Plugin::deactivateLegacyPlugin()
loaded that file on init as a side effect; removing it exposed the bug.

Add Util::isPluginActive(), which loads wp-admin/includes/plugin.php when
needed, and use it in all plugin-detection helpers. The Yoast module now
uses Util::isYoastSeoActivated() instead of its own copy.

Fixes #1238

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st-uri-3.1.7

Bump fast-uri from 3.1.5 to 3.1.7
…owserslist-4.28.8

Bump browserslist from 4.28.1 to 4.28.8
…er/composer-2.10.3

Bump composer/composer from 2.10.2 to 2.10.3
Riza and others added 19 commits October 19, 2026 20:04
Resolve the meta-box.js conflict with #1217 by keeping both
format_live_count()/update_live_count() and get_internal_link_hosts()/
normalize_link_hosts().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…slatable

- Gutenberg sidebar: keep copies of the requirement objects in state and
  re-read ppChecklists.requirements on requirements-updated, so in-place
  label/status changes from meta-box.js produce a new hash and re-render.
  The title badge no longer stays stale while typing.
- Start the word/internal/external link count caches at null so a count of
  0 renders a badge on new posts.
- format_live_count(): a max lower than the min is ignored, mirroring
  check_valid_quantity(), so min 50 / max 20 shows "42, min 50".
- Badge formats are localized (live_count_max, live_count_min) and escaped
  before going into the label HTML.
- Show the badge for the Yoast focus keyword and meta description counters.
- Rebuild gutenberg-panel.min.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on one line

The pre-publish panel registered its update-failed-requirements handler
under the 'publishpress/checklists' namespace, which the sidebar panel
removes when it unmounts, so the panel never rendered. Give it its own
namespace and register it on mount. Rebuild gutenberg-warning.min.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nter

Feat: show a live count next to counter checklist tasks
Brings in #1218 (live count badges). Its updateRequirements() change
stores { ...req } copies, so a custom task toggled in the block editor
Checklists panel now re-renders immediately instead of waiting for the
next unrelated change.

Conflicts: kept both the a11y strings and the live count strings in
checklists.php; rebuilt gutenberg-panel.min.js and
gutenberg-warning.min.js with webpack --mode development.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Post editor accessibility fixes for VPAT / WCAG 2.2 AA
…#1208)

Gutenberg turns off visual revisions whenever any classic meta box is
registered in the block editor (and turns off real-time collaboration for
meta boxes without __rtc_compatible_meta_box). The block editor no longer
gets the pp_checklist_meta box; the Classic Editor keeps it unchanged.

- meta-box.js still evaluates every requirement in the block editor, and
  add-ons (Pro, Yoast SEO, Permalinks) still extend it: its list is printed
  in a hidden container in the admin footer instead of a meta box.
- Tasks ticked by hand (custom tasks, OpenAI tasks, Approved by, Pro's
  Approved by user) are saved as REST-registered post meta with the same
  pp_checklist_custom_item_* keys, so existing data keeps working and RTC
  shares the ticks between editors (no more last-save-wins). The meta is
  registered on every request (REST saves are not in wp-admin), and
  custom-fields support is added where a post type lacks it.
- Ticked-by-hand tasks without REST meta (e.g. added by another plugin)
  are saved through admin-ajax after the post is saved.
- The checklist is also shown in a "Checklist" panel in the Post sidebar,
  where the meta box used to be.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without Pro, approved_by_user is a Pro placeholder requirement that
can't be ticked. Registering its post meta made meta-box.js copy the
empty meta value into the list, so a saved Required rule showed as
failing and blocked publishing, while development shows it as passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The block editor saves ticked-by-hand tasks as REST post meta, whose
auth only checks edit_post. An author who isn't in the "Approved by"
roles (or the Pro "Approved by user" users, or a custom task's
allowed roles) could approve their own post with one editPost() call
or a REST request.

CustomItemsMeta::userCanComplete() applies the same rules the
requirements use to make a task clickable. In REST requests the write
of a task the user may not tick is skipped, so the rest of the post
still saves (the block editor sends every registered meta field). The
Classic Editor form and the admin-ajax fallback use the same check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel keeps failedRequirements up to date from a wp.data
subscription, which reads this.state.requirements. Ticking a task now
edits the post meta, so the subscription runs before the new list is in
the state and keeps the old result. On WordPress 6.7, unticking a
required task on a published post and pressing Update right away saved
the post.

Validate again in the setState callback of updateRequirements(), once
the new list is in the state. This also covers changes that don't touch
the editor store at all (on development, unticking a required custom
task on a published post never blocked Update).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a save was blocked, the panel set the edited status back to the
status read when the editor opened. For a post published in the same
session that is 'auto-draft', so the next Update failed with
"Invalid parameter(s): status". This was already the case on
development; it shows up more now that unticking a required task on a
published post reliably blocks Update. Use the saved post status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1243 (GPT-6-Astra), two findings, both reproduced on SandyWP
with Gutenberg 24.2 real-time collaboration:

1. Restricted approvals could be injected through RTC. Collaboration
   shares registered post meta with everyone who can edit the post, so
   an author's editPost() of pp_checklist_custom_item_approved_by showed
   as ticked in an editor's session, and the editor's save of an
   unrelated change stored it. The write guard only knew the saver.

   Restricted tasks ("Approved by", Pro's "Approved by user", custom
   tasks limited to some roles) are no longer REST post meta, so they
   are never in the shared state or in a post save. The user who clicks
   one saves it at once through pp_checklists_save_custom_items, which
   checks the nonce, edit_post, the post type and that this user may
   tick the task. Other editors get the saved state through the
   Heartbeat API (about 5-10 s). Tasks anyone may tick stay REST meta
   and keep syncing live.

2. With the custom-fields opt-out, ticks were lost. The meta was
   registered but not exposed, so the browser used the admin-ajax
   fallback, and the handler skipped the task because it was registered
   and still answered success. Tasks are now registered as REST meta
   only where the post type exposes it, the browser and the server use
   the same rule (CustomItemsMeta::isRestItem()), and every other task
   is saved per click.

The admin-ajax save sends only the task the user clicked (the old
fallback re-sent every task, which could overwrite another user's
approval), accepts only yes/no, saves all or nothing, and returns an
error instead of success when it refuses. On an error the task goes
back to its previous state and an error notice is shown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…no-metabox

Block editor: no Checklist meta box, so visual revisions and RTC work (#1208)
The site imports the line break as <br>, splitting the sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne-line

Join CHANGELOG.md introduction onto one line
- Add .phpcs.xml (based on publishpress-future) and .phpcs-plugin-review.xml
  (needed by the second, WordPress.org plugin-review pass of lint:phpcs).
- Exclude the three naming sniffs until a major version: Pro and third-party
  code use those class and method names.
- Exclude the 31 phpcbf-fixable formatting codes for now, with a TODO for a
  phpcbf pass after 2.30.
- Add explicit public visibility to class constants.
- Review every nonce-verification finding: annotate read-only uses, and in
  save_global_checklist() verify the nonce before reading the form.
- Use wp_parse_url(), wp_json_encode(), get_stylesheet_directory() and
  get_template_directory(); annotate the remaining intentional findings.
- Run the code-standards workflow on pull requests and put
  needs: [code-check] back on the WordPress.org deploy job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI creates .cache/wp_test inside the repository, and both lint:phpcs
passes scanned its mu-plugin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add PHPCS rulesets and restore the code-standards deploy gate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@repile repile Bot added this to the 2.30 milestone Oct 20, 2026
root and others added 7 commits October 22, 2026 10:17
…task" buttons

WordPress core sets ".button .dashicons" to line-height 1.9 (38px for a
20px icon), so the icon was drawn below the label. Lay the buttons out
with inline-flex and give the icon a normal line-height.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tattributes

Replace inconsistent `defined('ABSPATH') or die()` guards with standardized `\defined('ABSPATH') || exit;` syntax across all core files. Add .phpcs.xml, README.md, dist, and node_modules to .gitattributes export-ignore list.
@rizaardiyanto1412
rizaardiyanto1412 changed the base branch from development to master October 26, 2026 18:39
@rizaardiyanto1412
rizaardiyanto1412 merged commit d3eae72 into master Oct 26, 2026
1 check passed
@rizaardiyanto1412
rizaardiyanto1412 deleted the release-2.30.0 branch October 26, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants