Skip to main content

Bundled WordPress Plugins

Scope​

ReactWP builds nine normal WordPress plugins from src/plugins/. They are independent packages in the generated installation: their behavior applies only while the corresponding generated plugin is active.

PluginSource versionResponsibility
reactwp-frontend1.0.0remove public WordPress output that conflicts with the controlled React shell
reactwp-backend1.0.0reshape the classic administration experience
reactwp-images1.0.0disable WordPress image derivatives and preserve editor quality
reactwp-accept-svg1.2.0capability-gated, sanitized SVG uploads
reactwp-acf-local-json1.1.0store ACF JSON with the active theme
reactwp-seo1.0.1ACF SEO fields, robots policy, and direct/client-navigation head output
universal-legal-pages1.5.1manage portable legal documents, editable consent categories, per-service choices, and consent-aware integrations
universal-smtp1.0.0route WordPress email through a configured SMTP server with protected credentials and delivery testing
wp-registry0.3.0use an independent signed plugin/theme catalogue and update service, including their translations

The six ReactWP-prefixed plugins keep empty JavaScript, SCSS, and media starter entries so projects can add bundle assets deliberately. The three Universal plugins are template-only build items: their PHP, authored administration assets, translations, and documentation are copied without creating Webpack entry bundles. The build removes stale generated entry files, manifests, and chunks for those items while preserving their authored runtime files.

reactwp-frontend​

The plugin applies these public-side choices:

  • hides the WordPress admin bar
  • disables WordPress's automatic sizes="auto" image addition
  • returns null from the speculation-rules configuration
  • dequeues global-styles, wp-block-library, and classic-theme-styles
  • removes global-style enqueue callbacks from the head/footer
  • removes RSD, generator, index relation, Windows Live Writer, start/adjacent post relation, REST discovery, oEmbed discovery, feed, resource-hint, shortlink, and emoji output
  • removes the shortlink HTTP header callback

These choices reduce duplicate or framework-owned output; they also mean a project that intentionally uses block-library CSS, feeds, oEmbed discovery, resource hints, speculation rules, or WordPress auto-size behavior must change or disable this plugin. It does not unregister WordPress REST routes, feeds, oEmbed endpoints, or blocks themselves; it removes the listed frontend discovery/enqueue output.

reactwp-backend​

Editor and Dashboard​

The plugin disables the block editor for every post type through use_block_editor_for_post_type. It additionally removes classic editor support from the built-in post and page types.

At dashboard setup priority 99 it removes the welcome panel and these dashboard boxes when present: incoming links, plugins, secondary news, recent comments, At a Glance, activity, Site Health, WordPress events/news, Quick Draft, and recent drafts.

Side Menu and Customizer​

The normal side menu hides Tools, Media, Appearance, Plugins, Comments, Users, and ACF field groups. Under Settings it hides Privacy, Media, Writing, and Discussion. The Customizer removes the themes and navigation panels plus Site Identity, Homepage Settings, Additional CSS, and Colors sections.

This is interface cleanup, not an authorization boundary. The plugin does not remove the underlying WordPress capabilities, routes, screens, post types, or APIs. A user who still has permission can reach several hidden screens through the ReactWP admin bar or a direct admin URL.

Admin Bar​

The plugin removes the WordPress logo, default site-name, comments, and new-content nodes. It removes Updates unless the user has all three update capabilities for core, plugins, and themes.

It can then add:

  • the site name linking to / in a new tab
  • Menus when the user can edit theme options and at least one ReactWP theme location exists
  • Images & files when the user can upload files
  • Users plus Profile when the user can list users, or only Profile otherwise
  • a ReactWP group when the user can edit theme options
  • Site settings and Theme settings when ACF options-page APIs exist
  • Themes and its Editor child according to switch_themes / edit_themes
  • Plugins, Cache, plugin Editor, ACF, Import, and Export according to the corresponding capabilities and feature availability

The Cache node is added after Plugins and requires manage_options, but it is nested inside the branch that also requires activate_plugins. Production DISALLOW_FILE_EDIT still controls whether WordPress permits a linked theme/plugin editor screen; the admin-bar link does not override WordPress configuration.

Finally, the plugin hides the top-level Site settings and Theme settings menu nodes and theme action buttons with administration CSS. Their intended entry points are the ReactWP admin-bar group.

reactwp-images​

The plugin returns an empty array from intermediate_image_sizes_advanced, so WordPress does not generate registered intermediate sizes for new image processing operations. It also returns quality 100 from both jpeg_quality and wp_editor_set_quality.

It does not resize originals, create responsive derivatives, transcode formats, optimize previously uploaded files, or control the separate ReactWP theme-build optimization performed by Sharp/SVGO. Projects that use WordPress thumbnails or srcset derivatives should change or deactivate this policy before relying on those files.

reactwp-acf-local-json​

The runtime directory is:

<active stylesheet theme>/datas/acf/

In this repository, author the equivalent deployable source at:

src/themes/<theme>/template/datas/acf/

On admin_init and before an ACF JSON save, the plugin creates the directory when possible and adds missing index.php, .htaccess, and web.config deny files. It returns that directory as ACF's one save path. For loading, it removes ACF's first default path and appends the theme directory only when it exists.

The directory therefore needs write permission when WordPress is expected to save JSON from the admin. On read-only production deployments, commit JSON from a development environment and ship it with the theme instead. Nginx must apply the equivalent HTTP deny rule because .htaccess and web.config are server-specific.

reactwp-accept-svg​

SVG MIME registration defaults to users with manage_options; rwp_svg_upload_capability can return another capability. The maximum file size defaults to 2 MiB through rwp_svg_max_bytes, with a hard minimum of 1 KiB after filtering.

Every normal upload, sideload, and SVG filetype/extension verification goes through the same path sanitizer. It:

  1. requires a readable regular non-symlink file
  2. rejects the byte limit, DOCTYPE, and entity declarations
  3. parses XML with network access disabled
  4. requires an SVG document element in the empty or standard SVG namespace
  5. sanitizes with bundled enshrined/svg-sanitize 1.0.0
  6. removes remote references and minifies the result
  7. validates the sanitized XML again
  8. writes a temporary sibling with an exclusive lock, preserves file permissions when possible, and atomically renames it over the upload

The ReactWP sanitizer extension keeps only empty or same-document #fragment href/xlink:href values. JavaScript URLs, remote images, event attributes, scripts, external entities, malformed XML, and a missing sanitizer fail closed. Circular element references or other sanitizer failures return the existing invalid-SVG upload error without exposing exception details or changing the original file.

The plugin ships its Composer vendor/ directory so deployed installations do not need Composer. The manifest allows compatible ^1.0 releases, while composer.lock pins the shipped version and source reference. When updating the dependency, regenerate and commit both the lockfile and vendor/; changing the manifest and lock alone leaves the deployed runtime on the old library.

From configs/, refresh the shipped files and verify them with:

composer --working-dir=../src/plugins/reactwp-accept-svg/template install --no-dev --no-scripts --no-plugins --no-interaction
composer --working-dir=../src/plugins/reactwp-accept-svg/template validate --strict --no-check-publish
composer --working-dir=../src/plugins/reactwp-accept-svg/template audit --locked --no-plugins --no-scripts
npm run test:svg-sanitizer
npm run build:plugins

The regression suite checks the bundled version and source reference against the lockfile, the fragment-only link policy, remote CSS references, malformed XML, upload permissions, and the upload byte limit. Remote CSS removal is a best-effort defense rather than a full CSS parser; isolate untrusted SVG when embedding it.

The plugin sanitizes SVG markup; it does not turn an SVG into a raster image, inspect the visual meaning of safe vector paths, or make arbitrary inline SVG strings in project React components trustworthy.

reactwp-seo​

The SEO plugin registers its content and global ACF groups only in request contexts that need them, contributes the rwp_wp_head tag set, updates WordPress robots directives, and localizes global defaults as window.RWP_SEO when the main theme handle is enqueued.

Its exact fields, output tags, fallback order, robots rules, browser head allowlist, and PHP resolver methods are documented in Head and SEO.

This plugin is deliberately independent from ReactWP, ACF, and the active WordPress theme. It registers the public legal_page post type under the translated Legal pages administration menu and uses WordPress's native title, editor, revisions, publication state, and page capability mapping. English is the source and fallback language, so a WordPress locale without a plugin catalogue displays English; complete fr_CA and fr_FR catalogues translate the administration, validation messages, standalone document fallback, and default consent copy. The browser manager uses the same English defensive fallbacks. The plugin loads the catalogue from its own languages/ directory according to the active WordPress locale.

Published documents use /legal/<document-slug>/ by default. On every legal_page insert or update, the wp_insert_post_data boundary canonicalizes the title to plain text and constrains rich content with wp_kses_post() before storage. That boundary covers the block editor, REST requests, imports, and other WordPress insertion paths, so a separate headless consumer does not receive executable markup from the stored field. On singular requests, the plugin replaces the active theme template with its own complete PHP document, removes queued theme/plugin styles and scripts except the logged-in admin-bar assets, and enqueues a neutral responsive print-friendly stylesheet. This specifically keeps the ReactWP browser runtime from mounting against its absent #app shell, and prevents active-theme JavaScript from taking ownership of the standalone page. The template still runs wp_head(), wp_body_open() when available, and wp_footer() so normal WordPress metadata and ecosystem hooks remain available. It renders exactly one page heading, falls back to the translatable Legal document label when the stored title is empty, and displays the document's localized WordPress last-modified date beneath that heading through an escaped semantic <time datetime> element. As defense in depth, the standalone template passes stored content through the_content and applies wp_kses_post() again at the final HTML output boundary. Password-protected documents render WordPress's native password form through a separate narrow wp_kses() allowlist for its form, alert, label, input, accessibility, and validation markup instead of passing their stored content into the public filter/sink. When the active theme does not support WordPress's title-tag feature, the template also emits the escaped wp_get_document_title() value itself.

The plugin can therefore be copied from the generated dist/wp-content/plugins/universal-legal-pages/ directory into an unrelated WordPress installation. It requires WordPress 5.8 or newer and PHP 7.4 or newer; it does not require the ReactWP mu-plugin or theme.

Each settings-section header describes the controls in its paired body: activation, renewal, GPC and revisit behavior; localized interface text; editable categories; detected-resource limits; ordered legal links and explicit acceptance; or built-in and custom integrations. The legal-page selector stacks its full-width displayed-pages list above a visually separated full-width list of the remaining published pages. In the public dialog, every explicit-acceptance checkbox aligns with one natural text line shared by its confirmation label and legal link; the link does not create a separate tall inline box. Every legal-document link in the banner, preferences dialog, and explicit-acceptance controls opens in a separate tab with noopener noreferrer so the consent page remains in place without exposing an opener relationship.

The translated Legal pages > Consent screen stores one allowlisted WordPress option and requires manage_options. Both its authenticated, dedicated-nonce AJAX path and native Settings API fallback submit the exact version-2 settings contract. The complete transport is bounded to 4 MiB and uses a mode-aware top-level allowlist: standalone WordPress and ReactWP multilingual forms have their own complete required field sets, while executable custom-integration fields are accepted only for an administrator who also has unfiltered_html. Unknown, missing, outdated, oversized, malformed, or invalid fields reject the complete submission and preserve the previous option atomically. The AJAX path reads the canonical option back after the write; a mismatch returns settings_persistence_failed and is never announced as a successful save. A fixed, dismissible live notice reports pending, successful, validation-error, and persistence-error states without reloading the screen. New custom-integration identifiers returned by the canonical sanitizer are synchronized back into their hidden controls so repeated asynchronous saves keep the same integration identity. The native Settings API submission and complete WordPress notice stream remain as the no-JavaScript fallback. The banner remains disabled until an administrator enables it. On unrelated WordPress installations, its complete editable copy contract remains one required plain-text bundle and has no ReactWP dependency. When the global ReactWP runtime and rwp_admin_langs() capability are both present, a valid list of up to 32 unique Site settings languages activates one complete copy bundle per language. Invalid, duplicate, empty, or oversized ReactWP language data falls back to the portable monolingual contract instead of partly enabling language management.

The complete public interface copy is editable: banner title/message, legal-navigation name, six action labels (including the permanent cookie button), preferences-dialog title/description, service and blocked-content copy, terms confirmation copy, GPC notice, and errors. Copy, category names and descriptions, and public service labels are non-empty plain text with field-specific Unicode limits. Submitted HTML is rejected rather than treated as formatting, and accepted values are escaped again or assigned through text-only DOM sinks when rendered. One labelled native selector switches between the six main configuration sections without a crowded row of top-level tabs. WordPress's early JavaScript state exposes that selector and keeps every section after the first out of the initial paint, so the complete form never flashes before being classified; without JavaScript, the selector stays hidden and all sections remain available in document order. Switching sections only changes the visible panel: every field stays in the same form, unsaved values remain intact, hidden invalid fields reveal their owning section, and the one asynchronous Save settings action remains global below the panels. The Consent interface editor uses the same two-column section composition as detected services, consent links, and integrations: title and description on the left, controls on the right. Every settings section also uses the Integrations section's vertical inset, both in the wide two-column layout and after the sections stack on narrow screens. ReactWP languages appear directly as accessible tabs, without redundant language-source notices, above five concise native accordion groups. Every group starts closed, its summary describes the editing scope, and the shared name keeps only one group open at a time. Within each group, every labelled copy unit occupies the full editor width and the units always stack vertically. Category names and descriptions now live in a dedicated Consent categories section. Each category card contains only visitor-facing copy in one readable column, with compact per-language tabs when ReactWP is connected. In Consent module links, the language tabs begin directly with the ordered displayed pages. A dedicated Explicit acceptance fieldset follows them and keeps the global toggle above an ordered list of confirmation cards. Each card can be added, removed, or reordered and contains one legal-document selector per ReactWP language behind a compact accessible tab rail. Standalone WordPress stores one exact consent_strings bundle. With ReactWP, the Site settings languages activate one complete consent_string_translations[language] bundle per configured language. Missing, additional, oversized, structured, or scriptable copy leaves reject the complete submitted bundle atomically. Legacy banner-only values are hydrated without writing and remain synchronized compatibility projections after a valid save.

Consent categories are an ordered, atomic registry rather than fixed copy fields. The registry accepts up to 16 records. A fresh registry contains only necessary; it is always first and cannot be removed. Preferences, analytics, marketing, external-content, or any other optional group exists only after an administrator creates it. Every optional built-in-compatible or category-{16 lowercase hex} custom identifier can be renamed, reordered, or deleted. A category is a visitor-facing group with a stable ID, name, and description, and its assignment is the complete administrative loading rule: Necessary is always active, while every other category requires a visitor choice. There is no technical-purpose or Privacy behavior field beside that choice. Standalone installations submit exactly the ID, one non-empty plain-text name (80 Unicode characters maximum), and one description (300 maximum) per record. ReactWP instead requires the ID and both translated leaves for every configured Site settings language, then exposes a bounded categoryTranslations map for route changes. Missing, additional, duplicated, sparse, scriptable, oversized, or otherwise malformed category transport emits invalid_consent_categories and preserves the complete previous registry. Deleting a category still referenced by a detected service, predefined integration, or custom integration emits category_in_use and preserves the prior categories until those dependants are reassigned or removed in the same save. Existing stored categories, older fixed category copy, and 1.4 technical-purpose assignments are migrated in memory without forcing a write or silently deleting prior configuration.

Consent-module links follow the same host capability. Standalone WordPress stores one ordered consent_page_ids list and one ordered terms_page_ids confirmation list. With ReactWP, the Consent module links section exposes language tabs and stores one exact consent_link_translations[language] bundle containing both lists for every configured Site settings language. Administrators can select all available display links and arrange each language's list by drag and drop or accessible move buttons. Every display list accepts unique positive published legal_page IDs with a 1,000-entry technical safety ceiling rather than the former 20-page product limit. The confirmation list accepts up to 50 unique published legal-page IDs, uses contiguous positions, and requires the same number of entries in every language so each confirmation card remains aligned during route changes. Missing, additional, sparse, malformed, duplicate, foreign/unpublished, mismatched, or oversized multilingual values emit invalid_consent_link_translations and preserve the complete previous language map atomically. Explicit acceptance requires at least one published document in every configured language. Existing global selections and older fixed privacy/cookies/terms selections are duplicated across the configured languages as a no-write compatibility migration until a valid multilingual save occurs; the old scalar terms_page_id becomes the first-item compatibility projection. Public link titles and HTTP(S) URLs are always resolved from WordPress rather than accepted from the request. Detected-service names and classifications, policy version, 30–365 day duration, Global Privacy Control and revisit-button flags, and publishable GA4, GTM, Google Ads, or Meta Pixel identifiers remain global technical settings. Each predefined integration stores either one existing category assignment—including Necessary—or the explicit Not selected state. The identifier remains editable in that state but is omitted from the active public adapter configuration. The optional google_ads_id is empty when disabled, otherwise accepts only canonical AW- plus 5–20 ASCII digits; malformed input emits invalid_google_ads_id and preserves the previous valid value. Invalid types, malformed versions, out-of-range durations, unknown fields, non-empty category references, and other malformed integration IDs are rejected server-side.

The global Integrations section keeps the GA4, GTM, Google Ads, and Meta Pixel presets, whose category selectors start at Not selected, and also accepts up to 12 persisted custom integrations. Each custom record supplies a public service name, one category assignment, one unique absolute HTTPS script URL, and optional initialization JavaScript; the redundant Privacy behavior control is absent. A custom record may stay at Not selected: it remains stored in administration but is excluded from the public service registry and executable configuration. Assignment to Necessary makes it always active without an optional visitor choice. Assignment to any other existing category makes it its own managed visitor choice; accepting it does not grant sibling services in the same category. Adding, editing, or deleting this executable configuration requires both manage_options and unfiltered_html. A user without unfiltered_html can save other settings while the existing custom list is preserved. The Settings API transport is one atomic replacement containing only id, label, category, script_url, and init_code: missing, malformed, sparse, unknown-shape, duplicate, colliding, oversized, insecure, unauthorized, or non-empty category-orphaned values preserve the complete previous list. Legacy stored records with a valid purpose remain readable but are normalized to the new shape. A deliberate empty list removes all custom integrations.

Custom labels are non-empty plain text of at most 80 Unicode characters. A script URL is at most 2,048 bytes, contains one canonical host, and permits no credentials, whitespace, backslash, or fragment; ports and query strings are permitted. Within consent settings, init_code is the only intentional exception to the plain-text/no-executable-markup rule: it is trusted public application JavaScript, not formatted copy, and changing it requires both manage_options and unfiltered_html. Initialization code is valid UTF-8, NUL-free, and at most 8,192 bytes. The complete active custom public payload is bounded below 128 KiB. Once a category activates the definition, URL and code become public browser configuration and must contain only publishable identifiers, never passwords, private keys, bearer tokens, private download URLs, or server credentials. The browser revalidates the matching custom-… service, category—including Necessary—exact unique hostname, URL, code, and size bounds before registering it.

The public consent script is enqueued in the footer with WordPress's native defer strategy so its download does not block HTML parsing or theme preloaders. The inline registration queue and public configuration still run before the deferred manager, preserving early integration registration and initialization order. WordPress continues to resolve script dependencies; this change does not add a delay to consent initialization or alter which integrations require consent.

The public manager is framework-independent vanilla JavaScript rendered inside a Shadow DOM so active-theme selectors do not restyle its controls. It exposes a non-modal first-choice banner and an accessible modal preferences dialog built from the current ordered category registry. Only explicitly configured integrations and detected managed services that an administrator has approved with a real category enter the visitor interface. Optional approved services remain individually selectable inside those categories; a category control is a master shortcut and displays a mixed state when only some of its services are accepted. The service-choice heading and description are omitted when no approved individual service is available. When Necessary is the only category, the dialog also omits Reject all because no optional choice can be refused; its global save action remains available. In ReactWP mode, the server resolves the initial complete interface-copy, category-copy, and link bundles from the current Polylang language, ReactWP CL, WordPress locale, or first configured language in that order when available. The bounded public translation maps then let the ReactWP route transition synchronize later client-side language changes through setLanguage(code) without reloading or replacing executable markup. This updates the banner, popup, permanent cookie button, actions, categories, services, terms, notices, errors, accessible labels, ordered legal-link navigation, and every explicit-acceptance document in place while preserving each corresponding checkbox state and an open dialog. Standalone output does not expose those maps. The selected links do not create or alter a WordPress/theme menu.

Because that Shadow DOM intentionally blocks ordinary theme selectors, consent styling has an explicit theme override. Place a complete stylesheet at universal-legal-pages/consent-manager.css in the active theme; ReactWP projects author it at src/themes/reactwp/template/universal-legal-pages/consent-manager.css. The file replaces the plugin stylesheet rather than loading beside it. WordPress theme-file resolution gives a child theme priority over its parent, while an absent or unreadable override falls back to the plugin stylesheet. Local plugin and theme files receive a modification-time query value so edits invalidate browser caches. Trusted project PHP can replace the final HTTP(S) URL through universal_legal_pages_consent_stylesheet_url; invalid, credential-bearing, non-HTTP(S), control-character, and oversized filter results fall back to the resolved local file. A remote filtered URL owns its own cache-versioning and Content Security Policy configuration.

Choices use the site-partitioned cookie base name ulp_consent_{blog_id}. The exact schema-v4 cookie contract contains schema, policy, and service-registry versions; one exact boolean map keyed by every current category ID; compatibility booleans derived from accepted service behaviors; one exact boolean for every activatable service; one aggregate terms boolean confirming that every active explicit-confirmation box was checked; the GPC boolean; and an ISO timestamp. Necessary services are implicitly active and unclassified services are never activatable or stored as granted. Category IDs, internal service purposes, and the ordered confirmation-document registry while explicit acceptance is enabled participate in serviceRegistryVersion, so structural category, adapter-signal, or active confirmation-list changes invalidate an older cookie while translation-only edits do not. A label-only rename of an ordinary built-in or detected service does not invalidate consent; every persisted custom definition value—label, category, full URL, and initialization code—is additionally hashed so an edit requires a new choice before changed optional executable code may load. On HTTPS, the effective name is __Host-ulp_consent_{blog_id} with Secure; Path=/ and no Domain, so a sibling subdomain cannot plant it. Plain HTTP remains a local/legacy compatibility mode and should not carry optional production integrations. Changing the policy version also invalidates the old choice. Necessary storage and anything deliberately assigned to Necessary are always active, while an enabled browser GPC signal forces optional marketing-behavior services and integrations off without disabling unrelated services grouped in the same category.

Built-in adapters expose Google Analytics 4 as an individual analytics service, and Meta Pixel and standalone Google Ads standard remarketing as individual marketing services. Google consent defaults are queued locally before any optional Google request: accepted preference-behavior services map to functionality_storage and personalization_storage; accepted analytics-behavior services map to analytics_storage; accepted marketing-behavior services map to ad_storage, ad_user_data, and ad_personalization; and necessary security storage remains granted. This preserves an accepted direct Google service when its category is mixed without granting a refused sibling service. GTM remains a multi-purpose container rather than one service toggle: it is requested only after its explicitly assigned category is accepted, receives the current service-derived Consent Mode update before startup, and still requires the correct consent checks on every tag. Direct GA4 and Google Ads destinations share one gtag.js loader and each destination is configured once. The Google Ads adapter needs only an AW-… identifier; it does not generate conversion labels or project-specific dynamic-remarketing events. The built-in implementation uses basic consent mode: no adapter-owned vendor script is requested before its gate is granted. Revocation updates vendors when their APIs are available, removes the revoked known vendor's first-party cookies on a best-effort basis, and reloads so a previously loaded adapter does not remain active on the current document. Enabling GPC after an earlier marketing grant also forces every marketing-behavior service off and performs the same best-effort Google Ads/Meta cleanup.

When consent is enabled, published content and oEmbed HTML are inspected server-side at the end of their normal WordPress filter pipelines before the browser sees an external iframe src. Valid YouTube /embed/{id} URLs are replaced by an inert service placeholder and normalized to https://www.youtube-nocookie.com; valid Vimeo player embeds and unknown external iframes are handled the same fail-closed way. YouTube and Vimeo have reviewed dormant adapters, but no provider is proposed to visitors merely because the plugin knows how to control it. When a user with manage_options visits affected content, the service is added to the non-autoloaded detected registry as unclassified. It remains blocked without a visitor allow action and stays absent from the public registry, dialog, and consent cookie until an administrator explicitly names it and assigns an existing category. After that approval, the manager offers the individual service and creates its iframe only after consent. The quote-aware scan is bounded to 1 MiB and 64 URL candidates; ambiguous tags, duplicate sources, srcdoc, malformed closures, or payloads it cannot safely process fail closed without re-emitting executable iframe markup.

External scripts registered through WordPress are inventoried only while an administrator visits the site. The record is limited to its handle, canonical host, kind, and managed state; paths, queries, and secrets are not stored. Reporting-only entries remain admin-only and are excluded from the public registry, consent hash, cookie, and visitor controls even if an administrator classifies them. Inventory is not retroactive blocking: a reported script remains uncontrolled until trusted project code removes its eager loader, publishes a reviewed managed: true service definition, and supplies its consent-aware adapter. Raw client-created iframes, direct script tags, template output outside the filtered content pipelines, Shadow DOM contents, and arbitrary GTM custom tags likewise cannot be intercepted reliably after the browser has begun their requests. The supported proactive client contract is an inert [data-ulc-service][data-ulc-src] placeholder whose service and reviewed loader are already registered.

The manager cannot control a vendor snippet independently emitted by a theme, another plugin, or an incorrectly configured Tag Manager container. Remove duplicate snippets, do not configure the same GA4 or Google Ads destination directly and in GTM, and declare the correct consent requirements on every container tag; unrestricted custom tags can otherwise bypass the visitor's choice. A persisted custom script in an optional category creates no node or request before individual consent, waits for the external script's load event before appending initialization through textContent, and remains retryable after a network failure. A custom script assigned to Necessary intentionally loads during initialization. Revocation disables optional code and reloads the page, but arbitrary vendor cookies, local storage, network state, or remote account data cannot be cleaned automatically; add reviewed vendor-specific cleanup when needed. Purge full-page/CDN caches after editing a custom definition so its registry version and public code change together.

Sites with a Content Security Policy must account for https://www.googletagmanager.com for the built-in Google script loaders, https://connect.facebook.net for Meta Pixel, the selected YouTube/Vimeo origins in frame-src, and the documented product collection domains in the applicable final proxy/CDN/web-server directives. The plugin does not broaden CSP automatically. When WordPress rewrites plugins_url() to an asset CDN, that reviewed origin must also be allowed by style-src for the Shadow DOM stylesheet. The WordPress bootstrap and public configuration are inline scripts, while dialog scroll locking changes the document's inline overflow style. Custom external and initialization script nodes inherit a nonce already approved on the consent-manager script. Under CSP3, that nonce can authorize the configured external script without a matching host source and therefore explicitly delegates CSP trust to that exact administrator-configured URL; without such a nonce, the custom host must be allowed by script-src. Inline initialization also needs the nonce or another compatible site policy. CSP or Trusted Types can still reject vendor code; do not enable unrestricted unsafe-inline. Vendor redirects and later changes to an allowed remote file remain inside that supplier's trust boundary, and the custom editor does not provide Subresource Integrity.

The optional explicit-acceptance boxes are deliberately separate from cookie categories. Each configured document receives its own required checkbox, and optional choices cannot be saved until every box is checked. The public configuration exposes their complete ordered termsLinks array and keeps termsLink as a first-item compatibility projection. The cookie retains only one aggregate browser-local acknowledgement tied to the active ordered document registry and policy version; it is not an authenticated signature or immutable audit record. A transaction that requires contractual proof must capture acceptance again in its authenticated server-side workflow.

Trusted project JavaScript can extend the vendor catalog:

window.UniversalLegalConsent.registerIntegration({
id: 'example',
serviceId: 'example-service',
categories: ['analytics'],
purpose: 'analytics',
mode: 'all',
load(consent){
// Load one reviewed integration after consent.
},
update(consent){
// Apply later grants or revocations.
}
});

Supported category values are all current category IDs in the public registry, including necessary; deleting a category also removes that identifier from the browser API. A gate made entirely of Necessary loads during initialization without waiting for a consent cookie. mode is all or any. Optional serviceId instead links the loader to one managed service already present in the validated public registry, makes that individual choice authoritative, and derives the internal adapter purpose from the service. If categories or purpose is also supplied with serviceId, it must match that service or registration fails closed. Without serviceId, a non-empty exact category list plus an explicit internal purpose (preferences, analytics, marketing, or external) and the optional mode form the gate. Unknown or duplicate categories, invalid purposes, and unknown modes reject the complete adapter rather than weakening its requirements. Reporting-only discoveries cannot be promoted through JavaScript alone: trusted PHP must publish a reviewed managed: true definition and the project must register its matching adapter. load runs only after its gate is granted and is considered loaded after a successful call; a thrown error or rejected promise permits a retry after a later consent change. The API also provides getConsent(), hasConsent(categoryId), hasServiceConsent(serviceId), openPreferences(), and setLanguage(code). setLanguage(code) accepts a code present in the validated ReactWP translation maps or a regional code whose primary language is present, updates all visible and accessible consent copy plus the category, legal-link, and complete confirmation-document bundles through text nodes and safe attributes, returns true on success, and returns false in standalone mode or for an unknown code. Calls made by a ReactWP route before the manager loads are retained and applied after initialization.

Trusted PHP can extend the bounded registry for a reviewed project adapter. IDs are lowercase [a-z0-9][a-z0-9._-]{0,63}, labels are at most 80 Unicode characters, and each service has at most eight canonical domains. The filter returns no more than 32 exact records; malformed output is rejected as a whole:

add_filter('universal_legal_pages_services', function($services){
$services[] = [
'id' => 'example-service',
'label' => 'Example service',
'category' => 'analytics',
'purpose' => 'analytics',
'domains' => ['analytics.example.com'],
'kind' => 'integration',
'managed' => true,
];
return $services;
});

A theme can disable the plugin's floating revisit button and open the same preferences dialog from its own footer control without inline JavaScript:

<button type="button" data-ulc-open aria-haspopup="dialog">
Manage my privacy choices
</button>

The manager delegates this trigger from the document, so it continues to work when client-side navigation renders or replaces the control after initialization. Its outer Shadow DOM host stays fixed to the viewport and lets pointer input pass through outside the visible consent surfaces. This prevents smooth-scroll themes from treating dialog focus as a request to scroll to an off-screen element; opening and closing preferences keeps the background page in place. It preserves the trigger for focus restoration when the dialog closes and leaves modified link clicks to the browser. The document dispatches universal-legal-pages:ready and universal-legal-pages:change events with copied consent state. Custom adapters and persisted initialization snippets are trusted application code and remain responsible for fixed reviewed URLs, accurate category assignment and privacy behavior, vendor cleanup, CSP, supply-chain changes, and secret hygiene.

This is a technical consent mechanism, not a jurisdiction-specific compliance determination. Site owners remain responsible for truthful copy, vendor/purpose disclosure, retention decisions, and legal review.

The public base can be changed before registration:

add_filter('universal_legal_pages_rewrite_slug', function(){
return 'documents-legaux';
});

The filter result is normalized with sanitize_title() and falls back to legal for an empty or non-string value. After changing it on an existing site, save Settings > Permalinks once. Deactivation unregisters the post type before flushing rewrite rules so its permastruct is removed; deactivation or removal preserves every stored document.

universal-smtp​

Universal SMTP is a portable WordPress plugin that routes the existing wp_mail() flow through a configured SMTP server using WordPress's bundled PHPMailer instance. It does not require the ReactWP mu-plugin, theme, or ACF, and can be copied from the generated dist/wp-content/plugins/universal-smtp/ directory into another WordPress installation. It requires WordPress 5.9 or newer and PHP 7.4 or newer.

The plugin adds Settings > SMTP. One section selector switches between connection, authentication, sender identity, and test delivery without discarding unsaved values. A global Save settings action remains available across sections and submits the complete SMTP configuration. The configuration screen and test-email action require manage_options. With JavaScript available, settings save through an authenticated WordPress AJAX request; the normal WordPress form submission remains available as the no-JavaScript fallback.

Every submitted setting is checked against an exact writable-field allowlist, scalar type, length and field-specific grammar before the complete configuration is stored. HTML is rejected from plain-text fields, and stored values are escaped again for their exact administration-screen output context. Passwords preserve their exact permitted characters because they are encrypted and never rendered.

Installation and configuration​

  1. Build ReactWP and activate Universal SMTP from the WordPress Plugins screen, or copy the generated universal-smtp directory into another site's wp-content/plugins/ directory and activate it there.
  2. Open Settings > SMTP.
  3. Enter the SMTP connection, authentication, and sender values supplied by the mail provider or server administrator.
  4. Enable SMTP delivery and save the settings.
  5. Send a test message to an address you control, then confirm the result at both the receiving mailbox and the provider when provider activity is available.
SettingMeaning
Enable SMTPRoutes WordPress messages that use wp_mail() through the configured SMTP connection.
SMTP hostThe provider or server hostname. Authorized internal SMTP hosts are supported.
PortThe connection port required by the provider.
Encryptiontls uses STARTTLS, ssl uses implicit TLS, and none uses an unencrypted SMTP transport. The plugin does not offer a switch to disable TLS certificate verification.
AuthenticationEnables SMTP authentication. When disabled, the username and password are not used.
Authentication typeauto lets PHPMailer negotiate the method. login, plain, and cram-md5 are available when a provider requires one explicitly.
UsernameThe SMTP account or login name.
PasswordA write-only secret stored encrypted at rest, unless UNIVERSAL_SMTP_PASSWORD supplies it at runtime.
From emailThe default sender address. The independent force control overrides a value supplied by another theme or plugin.
From nameThe default sender name. Its independent force control likewise overrides another supplied value.
Return-PathA boolean control that uses the configured From email address exactly as PHPMailer’s envelope sender. It does not accept a separate Return-Path address. The value must agree with the provider and domain policy.
TimeoutThe SMTP connection timeout, bounded from 5 to 60 seconds.

STARTTLS is commonly offered on port 587 and implicit TLS on port 465, but these are conventions rather than plugin defaults or guarantees. Always use the exact combination documented by the SMTP provider. Choose none only for a deliberately trusted server and network configuration.

Some providers reject or rewrite a From address that their account is not authorized to use. Sender values in WordPress do not replace the provider's sender-verification requirements.

Password and access boundary​

The password field is write-only: the administration does not send the saved secret back to the browser. When a password is saved in WordPress, the plugin encrypts it before database storage. This reduces exposure from a raw database read but does not protect the secret if the WordPress, PHP, or server runtime is compromised. The encryption key is derived from the WordPress authentication salts. If those salts change, the stored password becomes unreadable and Universal SMTP stops applying the SMTP configuration until an administrator saves the password again, unless a valid runtime constant supplies it.

For deployments that manage secrets outside the database, define UNIVERSAL_SMTP_PASSWORD in wp-config.php before WordPress loads:

define('UNIVERSAL_SMTP_PASSWORD', getenv('UNIVERSAL_SMTP_PASSWORD'));

Supply the environment variable through the hosting platform or server configuration. Do not commit the password or a private environment file to the repository. The constant takes precedence over the stored password when it is defined.

Only trusted administrators should receive manage_options, because that capability grants access to SMTP configuration and test delivery. Internal SMTP hosts remain available for legitimate private relays; the plugin does not decide whether an administrator is authorized to use a specific mail server.

Test delivery and status​

The test tool sends through the same wp_mail() and PHPMailer path used by WordPress. It is restricted to administrators and limited, per administrator, to five attempts in ten minutes. The quota lock is stored in the WordPress database, so every web instance must share that database for the limit to be enforced strictly across the deployment. This bound reduces accidental or abusive repeated sends; it is not a provider quota.

The plugin retains only the latest delivery outcome, time, generic result code, and recipient count. It does not keep message addresses, subjects, bodies, or credentials as a delivery log. The panel is therefore an operational signal rather than an audit trail. A successful test means the configured path accepted that attempt; it does not guarantee final delivery or inbox placement.

Provider and domain responsibilities​

Universal SMTP configures WordPress to use a mail server. It is not an email delivery service and does not create mailboxes, provider credentials, sending permission, or DNS records. The site operator remains responsible for:

  • valid SMTP credentials and an authorized sender;
  • the provider's connection requirements, sending limits, and account status;
  • SPF, DKIM, and DMARC records appropriate to the sending domain and provider;
  • provider logs, bounce processing, reputation, and deliverability investigation;
  • testing transactional messages used by the installed themes and plugins.

Settings and the last-delivery status are retained on deactivation and uninstall so a temporary plugin removal does not silently destroy an operational mail configuration. Remove stored values deliberately before uninstalling when a project requires their erasure.

Universal SMTP affects messages sent through WordPress's wp_mail() path. A theme or plugin that sends directly through an external API or its own mail transport remains outside this configuration.

Build and Activation Boundary​

configs/webpack.plugins.config.js is the authoritative list of generated normal plugins. The production/development build copies each plugin's template/ directory and builds its optional JavaScript, SCSS, and media entry. Building a plugin does not activate it in WordPress; activation remains WordPress state in the generated installation.

npm run build:legal-pages-translations and npm run build:smtp-translations validate the Universal plugins' gettext calls against their corresponding French JSON catalogues, then regenerate each portable POT plus the fr_CA and fr_FR PO/MO catalogues. Both build:plugins and prod:plugins run these validations before copying the plugins, so adding or removing a translatable source string without synchronizing its French catalogue fails the build.

The shared ReactWP mu-plugin is not one of these eight normal plugins. It is loaded separately from wp-content/mu-plugins and owns the route, payload, rendering, cache, settings, and REST contracts described in PHP Runtime.

WP Registry​

The wp-registry 0.3.0 plugin connects ordinary WordPress installations to an independently hosted signed plugin/theme catalogue and update service. Its PHP/MySQL companion imports public catalogues, stores verified archives and lets authorized publishers publish their releases. WordPress retains native core updates. The client template remains inactive as a provider until configured. The complete cPanel kit includes the portal at wpregistry.studiochampgauche.com and the API/editor service at area.wpregistry.studiochampgauche.com. See WP Registry for the client contract, installation and migration from Universal Registry.