Repository navigation
1.7.0
Major rewrite of the Subpage block, the content parser and the frontend rendering. Existing content needs no change: blocks saved by earlier versions stay valid, and the subpage index of every post is rebuilt in the background after the update.
Compatibility
- Tested up to WordPress 7.1.4 (iframed block editor, block themes such as Twenty Twenty-Five, classic themes such as Twenty Twenty-One) and PHP 7.4 to 8.4
- The plugin header declares
Update URI: https://github.com/PaulArgoud/sgr-nextpage-titles: WordPress.org no longer offers updates from the listing of the same slug (owned by the original author, currently 1.5.12), so a future release there can never overwrite this version. Added after the 1.7.0 tag, and included in the 1.7.0 release archive together withreadme.txt - Minimum WordPress version raised from 5.0 to 6.3 (
Requires at least: 6.3), required by theblock.jsonregistration and blockapiVersion3. PHP 7.4 is still the minimum - Subpage block rewritten for the current block editor: registered from
blocks/subpage/block.json(apiVersion3, so it works in the iframed editor), plain ES5 script using thewp.*globals with no build step, translatable placeholder. Itssave()output is byte-identical to previous versions, so every existing post stays valid (no "This block contains unexpected or invalid content" warning, no block recovery) - The block is now registered on every request (
init), whatever the editor in use, and still registers on third-party editor screens that do not bootstrap the server-side block definitions - Classic Editor plugin detection (
mpp_is_block_editor_active()) mirrors Classic Editor 1.7 settings, including the multisite network defaults and the "allow users to switch editors" option - SEO plugins: dedicated integrations for Yoast SEO, Rank Math and SEOPress replace the old Yoast string replacement (which loaded the
wordpress-seotext domain to replace "Page X of Y"). On subpages 2 and up, the subpage title fills the%%page%%(Yoast, SEOPress) or%page%(Rank Math) title variable. Each integration is only registered when that plugin is active
Fixed
- Broken page layout (user-reported on a production site): a
[nextpage]shortcode placed inside<p>,<h2>,<h3 style="…">,<div>or in the middle of a paragraph left unclosed elements, so the table of contents and the navigation ended up nested inside a heading. Every subpage is now a balanced fragment: the blocks and elements open at the cut are closed at the end of the page and reopened on the next one - Elements left empty by the cut (e.g.
<h3 style="…">[nextpage]</h3>, also with only or<br>next to the shortcode, or a paragraph/heading block holding only the shortcode) are removed instead of rendering as empty headings or paragraphs; whitespace next to the cut is trimmed (except in<pre>) - Parser rewrite: titles and pages could get out of step (wrong title on a page, missing or extra subpages). Subpages are now derived from a single parse of the live post content, in document order:
- untitled
[nextpage]shortcodes and attribute-less Subpage blocks (saved without a title) now create their own subpage (fallback title "Page N") instead of being ignored or titled "false" - posts mixing Subpage blocks,
[nextpage]shortcodes and core page breaks (<!--nextpage-->or the Page Break block) are split in document order; previously the shortcodes were ignored as soon as one block was found, and core breaks shifted every title - intro detection: an intro made only of an image, an embed or a dynamic block is recognized; whitespace,
and empty paragraphs before the first subpage no longer create an empty "Intro" page, and leading content that is not an intro (scripts, styles, separators) is kept on the first page instead of being lost - the self-closing block form and Subpage blocks nested in groups or columns are supported
[nextpage]inside an HTML comment, a tag attribute, a<script>/<style>/<textarea>element or a block delimiter is no longer treated as a page break, and the escaped[[nextpage]]is displayed literally- duplicate titles always get unique slugs (the 100-duplicate limit is gone)
- untitled
- The frontend no longer depends on the
_mpp_datapost meta: posts imported, or edited while the plugin was inactive, no longer show an outdated table of contents - Table of contents injected where it does not belong: the title, TOC and navigation are now only added to the multipage post rendered by the main query — no longer to related posts, Query Loop blocks, widgets, excerpts (
get_the_excerpt(),the_excerpt(), directwp_trim_excerpt()calls), descriptions built inwp_head, or password-protected posts before the password is entered - The TOC is no longer added twice when a theme or page builder applies
the_contentagain to the rendered output, or when a shortcode (box, tabs, accordion…) filters its inner content withthe_content - 1-based page handling:
/post/and/post/1/are the same first page everywhere. "Display comments on: first page" now shows the comments on/post/1/, and the current TOC item, rel links, navigation and document title use the same page number - Block themes: the "first page" / "last page" comments setting now also hides the comment blocks (
core/commentsand related blocks), not only the classiccomments_template(); the standard page links appended by the Post Content block are hidden when "Disable the standard pagination" is checked, and still shown when it is not - "Only on the first page" (table of contents) is now honored when the TOC is displayed below the content
- "Hide intro title" now also applies on
/post/1/, and only when the first page is the automatic intro (a first subpage with its own title keeps it) - Title escaping: one policy for every output. Block titles (HTML-encoded by the editor) keep their inline formatting (
strong,em,code,mark,sub,sup…) in the heading, the TOC and the navigation instead of showing literal tags, and any other markup is stripped. Classic shortcode titles are treated as text, soC++ <T>ora < b & cdisplay literally instead of being truncated or emptied. The document title and the SEO plugins receive plain text, without tags or entities - SEO: with Yoast SEO, subpages 2 and up declared the first page as their canonical URL (which tells search engines to drop them); they now use their own URL, and a canonical set in Yoast is kept. Yoast SEO and Rank Math no longer print their own
rel=prev/rel=nextlinks on multipage posts (computed from the raw content, they could be wrong or duplicate the plugin's) - Translations were never loaded:
load_plugin_textdomain()was called without thelanguages/path, so the bundled.mofiles were ignored and the plugin always displayed English unless a language pack was installed - TOC, navigation and rel link URLs always point to the multipage post (they were built from the global post), and use
?page=Nwhenever permalinks are plain (also when thepermalink_structureoption is missing) - The navigation no longer prints the
<!-- .nav-links -->/<!-- .mpp-post-navigation -->HTML comments, and the internal empty-page placeholder is never printed - CSS: the TOC and the navigation use
remsizes and reset the typography (font size, weight and style, line height, text transform, letter spacing), so they no longer inherit the styles of a surrounding heading - RTL: RTL sites requested
css/multipage-rtl[.min].cssandadmin/css/multipage-admin-rtl[.min].css, which did not exist (404); both stylesheets are now shipped. The "align left" / "align right" TOC positions keep their physical side - TinyMCE and Quicktags buttons: the typed title is now escaped (
&,",<,>,[,]), so quotes no longer break the shortcode and markup cannot be injected; an empty or cancelled title inserts nothing (it used to throw aReferenceErroror re-insert the previous shortcode) - Classic block: the
[nextpage]placeholder was a blank image in the Classic block of the block editor; the plugin TinyMCE stylesheet is now loaded there too - Backslashes in subpage titles are preserved when the
_mpp_dataindex is saved - The subpage index rebuild ("Build Multipage postmetas") loaded every matching post in a single request, skipped drafts and untitled shortcodes, and never removed stale entries; it now runs in batches, indexes every post that
save_postindexes and deletes stale entries - The 1.3 → 1.4 migration no longer runs a full synchronous rebuild on
admin_init, which could exceedmax_execution_timeon large sites and retry on every admin request - The updater treated 1.4–1.6 sites as fresh installs (so no upgrade routine could run); it now also reads the stored
_mpp_db_version
Changed
- Behaviour change:
/post/1/is now redirected (301) to the post permalink by WordPress core instead of being served as a duplicate of the first page - Behaviour change: an out-of-range subpage (e.g.
/post/99/) is redirected to the permalink, as WordPress does for posts split with core page breaks - Behaviour change: in contexts where the content is not split (feeds, REST API, excerpts, other
do_shortcode()callers),[nextpage]renders nothing; it used to output a<!--nextpage-->comment - Behaviour change: with the bundled translations now loaded, the intro title "Intro" is displayed as "Introduction" in French ("Einleitung" in German, "Introducción" in Spanish, "Introduzione" in Italian)
- Behaviour change: settings fields added to the Multipage settings screens by third-party code with
add_settings_field()are no longer saved automatically: register them withregister_setting( 'mpp-settings', … )(or'mpp-advanced-settings'for the Advanced screen) - Settings API: both settings screens now save through
options.php. Every value is validated against its allowed values (an invalid value keeps the previous one and shows an error naming the field), unchecked checkboxes are stored as0, and custom priorities set in the database are kept (shown as "Custom (N)"). Fields have correct labels (label_for, fieldset legends), and priority labels are translatable - "Build Multipage postmetas" is replaced by a Rebuild subpage index button on the Advanced screen: posts are processed in batches for about 15 seconds, the remaining posts continue in the background, and the screen shows the last rebuild date and the background progress
- After updating to 1.7.0, and on every activation, the subpage index is rebuilt in the background through WP-Cron (batched, resumable, protected by a lock)
- The
X-Pingbackheader is no longer sent by the plugin (WordPress core sends it since 6.1) uninstall.phpnow cleans every site of a multisite network, and also removes the new internal options and the scheduled background rebuild- Bundled translations: complete and reviewed
fr_FR,de_DE,es_ESandit_ITcatalogs (following the WordPress glossary of each locale), JavaScript translations for the block (.json) and.l10n.phpfiles (WordPress 6.5+ performant translations). Language packs from translate.wordpress.org always take precedence over the bundled files - Translatable strings are escaped (
esc_html__(),esc_attr__()), every string with a placeholder has a translators comment, and counts use_n() - The current TOC item has
aria-current="page" - The Subpage block title field: Enter at the end of the title adds a paragraph after the block, and long titles wrap
Added
- WP-CLI:
wp multipage rebuild [--batch-size=<n>] [--dry-run]rebuilds the subpage index (and completes any pending background rebuild);wp multipage status [--format=<format>]reports indexed, missing and stale entries, the background rebuild state and the database version - Rebuild subpage index button (Settings > Multipage > Advanced), see Changed
- RTL stylesheets
css/multipage-rtl[.min].css(frontend) andadmin/css/multipage-admin-rtl[.min].css(admin) - Translated fallback title "Page N" for untitled subpages
Developer
- Single source of truth:
Multipage_Parser::parse( $content )returnsis_multipage,has_introand the orderedpages(slug,title,content,kind:intro,block,shortcodeorcore).Multipage_Parser::multipage_return_array()is now derived from it (same signature and return shape). New:Multipage_Parser::build_paginated_content( $pages, $use_block = null ),Multipage_Parser::is_meaningful_content(), constantsMultipage_Parser::BLOCK_PAGE_BREAKandMultipage_Parser::EMPTY_PAGE_PLACEHOLDER - Request setup:
Multipage::setup()(idempotent) parses the main query's post frompre_handle_404,redirect_canonicalandwp, so the plugin keeps working when another plugin short-circuits one of them.mpp_post()rebuildspost_contentso that WordPress' own splitting returns exactly the parsed pages, and filterscontent_pagination. The parse result is cached in the object cache groupmultipage(key: plugin version + content hash). New public members:Multipage::$post_id,$current_page(1-based),$pages,is_multipage_view(),get_current_page(),get_title_format(),comments_allowed_on_current_page();$pagestays the rawpagequery var - New template helpers:
mpp_get_subpage_title_html( $title, $format = 'html' ),mpp_get_subpage_title_text( $title, $format = 'html' ),mpp_get_subpage_title_allowed_html();_mpp_link_page_url()takes an optional third$postargument - New options API:
mpp_get_option_definitions(),mpp_get_option_definition(),mpp_get_option_choice_labels(),mpp_validate_option_value(),mpp_sanitize_option(). All getters keep their names, signatures and filters, and now return validated values. The options cache is cleared automatically when a Multipage option is added, updated or deleted - New rebuild API (
inc/mpp-update.php):mpp_run_postmeta_rebuild(),mpp_process_postmeta_batch(),mpp_schedule_postmeta_rebuild(),mpp_get_postmeta_index_stats(),mpp_is_rebuild_locked(). WP-Cron hookmpp_rebuild_postmeta_batch; internal options_mpp-rebuild-state,_mpp-rebuild-lastand_mpp-rebuild-lock(not autoloaded); the database version is now1700 - New filters:
mpp_untitled_subpage_title,mpp_comment_block_names,mpp_subpage_title_allowed_html,mpp_parser_is_meaningful_content,mpp_get_option_definitions,mpp_get_option_choice_labels,mpp_rebuild_postmeta_batch_size,mpp_rebuild_cron_time_limit,mpp_rebuild_admin_time_limit - New files:
blocks/subpage/(block.json,index.js,index.asset.php,editor.css),inc/mpp-blocks.php(mpp_register_blocks(), bundled JS translations fallback),inc/mpp-cli.php(MPP_CLI_Command) _mpp_datais kept as an index for third-party code (written onsave_postand by the rebuild); untitled subpages are stored with title''and slugpage-N.MPP_PATTERNandMPP_GUTENBERG_PATTERNare kept for backward compatibility only- Removed:
admin/js/mpp-editor-blocks.js,admin/css/mpp-editor-blocks[.min].css(replaced byblocks/subpage/) and the globalprompt_subtitle()JavaScript function of the Quicktags button - Tooling:
composer.jsonwith dev-only dependencies (WPCS 3, PHPCompatibilityWP, PHPUnit 9.6, Yoast PHPUnit Polyfills) and thelint,phpcs,phpcbf,testandtest:wp-envscripts;phpcs.xml.dist(WordPress + PHPCompatibilityWP, PHP 7.4+, WordPress 6.3+), with 0 errors on the codebase - Tests: PHPUnit integration suite (about 380 tests) running in
wp-env(.wp-env.json: WordPress 7.1.4, PHP 8.4): parser corpus, HTML balance of every split, full requests of every subpage, rendering, titles, SEO, comments, options, rebuild, upgrade, blocks, uninstall - CI: GitHub Actions workflow (PHP 7.4–8.4 lint, PHPCS, i18n check, PHPUnit on WordPress 6.3 / PHP 7.4, 7.1.4 / PHP 7.4 and 7.1.4 / PHP 8.4, release zip)
- Release and i18n scripts:
bin/build-zip.sh(zip of the files known to git, filtered by.distignore),bin/php-lint.php,bin/check-i18n.sh(POT,.po,.mo,.jsonand.l10n.phpfiles in sync with the code);.distignoreand.gitattributeskeep development files out of the release
Deprecated
MPP_Admin::mpp_enqueue_block_editor_assets()— no longer hooked, the block is registered bympp_register_blocks()mpp_admin_settings_save()andmpp_admin_advanced_settings_save()— no-ops, the settings are saved by the Settings APImpp_admin_advanced_callback_build_mpp_postmeta_data()— replaced bympp_admin_rebuild_postmeta_section()Multipage::mpp_the_title()— no longer hooked, returns the title unchanged (see the SEO integrations)Multipage::multipage_return_array()(deprecated since 1.5.14) now triggers a_deprecated_function()notice; useMultipage_Parser::multipage_return_array()
Install: download sgr-nextpage-titles.zip below (not the "Source code" archives) and upload it via Plugins > Add New > Upload Plugin.