# wppoland.com, full markdown digest
> Curated long-form Markdown for retrieval-augmented LLM agents. Includes the canonical EN service pillars and top EN blog posts. For the short index see /llms.txt. For other locales, fetch any URL with `Accept: text/markdown` content negotiation.
Source of truth: https://github.com/wppoland/wppoland-new
Generated at build time, 61 pillars + 60 blog posts.
---
# Service pillars
## AI-built website rescue and remediation
URL: https://wppoland.com/en/ai-built-website-rescue/
Description: Audit and repair for WordPress and WooCommerce sites built or heavily modified by AI. Senior review of vibe-coded plugins, security holes, broken flows, and AI-slop content.
Published: Sat Jun 20
Updated: Fri Jul 10
Type: guide
Level: advanced
## What AI-built WordPress site rescue covers
AI can build a WordPress or WooCommerce site fast. It cannot take responsibility when that site leaks data, breaks checkout, or quietly fills Google with duplicate pages. This service is the senior cleanup after the AI: we audit what was generated, find what is unsafe or broken, and fix it, with a human accountable for every change.
This is not the same as our general [WordPress repair and technical support](/en/wordpress-repair-service-technical-support/) or a standard [WordPress security audit](/en/wordpress-security-audit/). Those assume a site built by people. Here the failure patterns are specific to generated code and content, and the remediation is different.
## How AI-built sites tend to break
The damage clusters into a few recognisable patterns. A typical case is a WooCommerce store where an AI-generated checkout customisation skipped nonce verification, so the cart could be manipulated through a forged request, and nobody noticed until chargebacks started. Another is a marketing site where an assistant generated forty near-identical service pages that compete for the same query, so none of them rank and the whole domain looks thin to Google.
Other recurring failures:
- Generated PHP that calls functions that do not exist, or that were hallucinated from a different plugin's API.
- Admin-ajax and REST endpoints registered without a capability or nonce check.
- Unsanitised form input written straight into the database or echoed back into the page.
- Plugin sprawl: ten plugins installed to solve a problem one line of code would have handled, dragging Time to First Byte over a second.
- Content with confident but wrong facts, fabricated statistics, and invented client names.
- Migrations the AI "finished" that silently dropped redirects, breaking indexed URLs.
## Symptoms of a broken AI-built site and their causes
Not every broken AI-built site needs the same response. The symptom usually points to whether a targeted rescue is enough or the foundation should be replaced. Use this table as a first-pass triage before you commit budget or downtime.
| Symptom | Likely cause | First action |
| --- | --- | --- |
| Checkout or cart fails intermittently | AI-wired WooCommerce hooks, missing nonce on cart mutations | Pause paid traffic; run a functional audit on the checkout path |
| Subscriber or anonymous user can trigger admin AJAX | Missing `current_user_can()` on a `wp_ajax` handler | Security pass on generated PHP; see [auditing AI-generated plugin code](/en/auditing-ai-generated-wordpress-plugin-code/) |
| Scanner flags critical CVEs on common plugins | Outdated versions installed during a fast AI-assisted build | Run a [WordPress security audit](/en/wordpress-security-audit/) and patch or remove exposed plugins |
| Forty near-identical service pages, none rank | AI content cannibalisation and thin duplication | Content inventory; consolidate or noindex duplicates |
| White screen or fatal error after a small change | Hallucinated function calls in generated custom code | Code inventory against developer.wordpress.org; scope rescue vs rewrite |
| Time to First Byte above one second on a simple page | Plugin sprawl from "install a plugin for that" answers | Performance strip-down; measure before adding more tooling |
| Form saves but stored data looks corrupted or executable | Unsanitised `$_POST` written straight to options or post meta | Plugin code audit; treat as active XSS or injection risk |
| REST or admin-ajax endpoint returns data it should not | Missing `permission_callback` or nonce on custom routes | Endpoint audit; block public access until fixed |
| Owner wants to keep using AI in the build | No review gate, no version control on generated output | Rescue first, then guardrails; see remediation stages below |
If most rows point to isolated, fixable gaps in otherwise sound WordPress core usage, rescue is usually the cheaper path. If hallucinated APIs, missing security primitives, and plugin sprawl show up together across custom code and content, plan for a partial or full rebuild and say so before repair work starts.
## What an audit of an AI-built website checks
The audit inventories everything the AI touched and triages it by two axes: security risk and revenue risk. We separate the code AI wrote, the plugins it chose, and the content it produced, because each needs a different fix. You get a written split of what is safe to keep, what must be rewritten, and what should be removed, with the reasoning behind each call.
## Remediation stages for an AI-built website
Remediation follows a fixed order so security and revenue risks close before cosmetic work. Stages can overlap in calendar time, but we do not skip ahead to performance tuning while checkout is still broken or a public endpoint still leaks data.
| Stage | Focus | Deliverable |
| --- | --- | --- |
| 1. Inventory and triage | Map every AI touchpoint: custom PHP, plugin choices, content, migrations | Written keep / rewrite / remove split with reasoning |
| 2. Security pass | Nonces, capabilities, sanitisation, exposed AJAX and REST routes | Patched or removed vulnerable handlers; [plugin code audit](/en/auditing-ai-generated-wordpress-plugin-code/) findings closed |
| 3. Functional repair | Checkout, forms, CRM and payment integrations | Broken user flows restored and regression-checked |
| 4. Content remediation | Duplicate pages, hallucinated facts, thin AI filler | Consolidated pages that can rank; corrected or removed false claims |
| 5. Performance recovery | Plugin bloat, render-blocking assets, cache misconfiguration | Core Web Vitals back in range on key templates |
| 6. Guardrails | Version control, staging workflow, review checklist for future AI use | Short runbook so the next prompt does not reopen the same holes |
Emergency items (active exploit, broken checkout during a campaign, legal or compliance exposure) jump the queue inside stage 2 or 3 regardless of where the rest of the site sits in the table.
## Common security gaps in AI-generated code
Generated WordPress code passes the "it runs" test while failing the "it is safe" test. We test directly for the gaps that matter: missing `wp_verify_nonce` and `current_user_can` checks, input that reaches the database without `sanitize_*` or prepared statements, output that skips `esc_*`, PHP files reachable without an `ABSPATH` guard, and endpoints exposed without authorisation. The failure patterns repeat often enough that we published a diagnostic walkthrough in [auditing AI-generated WordPress plugin code](/en/auditing-ai-generated-wordpress-plugin-code/). Where the security surface is large, this connects to a full [WordPress security audit](/en/wordpress-security-audit/). We documented the real CVE patterns these stacks carry in [outdated-plugin CVEs](/en/wordpress-security-audit-outdated-plugins-cve-2026/).
## How to clean up AI-generated content
A site built with AI usually has an AI-content problem too. We deduplicate pages that cannibalise each other, correct hallucinated facts and round-number fake statistics, and consolidate thin pages into ones that earn citations. This is the same discipline behind [GEO and LLMO optimisation](/en/geo-llmo-optimization/): content that is accurate and distinct, not generated filler.
## How to fix performance on an AI-built website
AI tends to solve problems by adding plugins. We reverse that: remove the bloat, replace plugin stacks with targeted code, and bring Core Web Vitals back into range. The dedicated [Core Web Vitals rescue for AI-built WordPress sites](/en/core-web-vitals-rescue-ai-built-wordpress/) page walks through the staged teardown, measurement checklist, and the anonymised case where median uncached TTFB dropped from 1.47 s to 0.68 s after cutting 38 plugins to 21. See [plugin sprawl after an AI build](/en/plugin-sprawl-ai-wordpress-build/) for the full diagnostic and [AI-slop content cleanup](/en/ai-slop-content-cleanup-wordpress/) when hallucinated facts need editorial triage. For a formal audit scope, the [Core Web Vitals audit](/en/services/core-web-vitals-audit/) service covers the same metrics with a written backlog.
## Should you rescue or rebuild an AI-built website
After the audit you get an honest recommendation. If most of the AI output is salvageable, a targeted rescue is the cheapest path. If the foundation is unsound, we scope a rebuild instead of patching forever. When rebuild is the right call, our [agency rescue case study](/en/case-study-astro-migration-agency-rescue-2026/) documents a full stack migration where load time went from 12 s to 0.3 s after a bad agency delivery. And if you want to keep using AI in the build, but safely, with a human gate, that is exactly what our [AI implementation for companies](/en/services/ai-implementation/) covers: agents and tooling with version control, tests, and review built in.
## What you get
A working, accountable site: insecure generated code rewritten, broken flows repaired, AI-slop content cleaned, performance restored, and a short guardrails document so the next round of AI assistance does not reopen the same holes. Every change is reviewed by a senior engineer, not applied autonomously.
## Related services
- [Auditing AI-generated WordPress plugin code](/en/auditing-ai-generated-wordpress-plugin-code/), Diagnostic patterns for vibe-coded PHP before you ship
- [WordPress security audit](/en/wordpress-security-audit/), Deep security pass for high-risk generated code
- [WordPress repair and technical support](/en/wordpress-repair-service-technical-support/), Ongoing support after the rescue
- [AI implementation for companies](/en/services/ai-implementation/), Using AI in the build safely, with human gates
- [Core Web Vitals rescue for AI-built sites](/en/core-web-vitals-rescue-ai-built-wordpress/), Staged performance recovery after AI plugin sprawl
- [Core Web Vitals audit](/en/services/core-web-vitals-audit/), Formal audit with written backlog
- [Agency rescue case study](/en/case-study-astro-migration-agency-rescue-2026/), Full rebuild when rescue alone is not enough
- [GEO and LLMO optimisation](/en/geo-llmo-optimization/), Turning cleaned content into citations
Pricing is individual and scoped after the audit. [Contact us](/en/contact/) with the site and a short note on how it was built.
---
## AMP for WordPress, speed and Core Web Vitals
URL: https://wppoland.com/en/amp-accelerated-mobile-pages-en/
Description: Make your WordPress site fast with AMP. Accelerated Mobile Pages implementation for better Core Web Vitals, mobile speed optimization, and improved user experience.
Published: Sun Jan 01
Updated: Sat Jan 31
Type: guide
Level: advanced
## What is AMP service?
AMP (Accelerated Mobile Pages) implementation service makes your WordPress site load instantly on mobile devices. With scope agreed individually, we deliver:
- **AMP Implementation** - Official AMP plugin setup with three modes (Standard, Transitional, Reader)
- **Core Web Vitals Optimization** - Achieve sub-1-second LCP, optimal INP, and zero CLS
- **Schema.org Markup** - Structured data for Top Stories carousel and rich results
- **Analytics Integration** - Track performance with amp-analytics
- **Testing & Validation** - Full validation with Google Search Console
## Who needs AMP?
### News publishers
Top Stories no longer requires AMP, so the question is whether the AMP Cache still buys you anything on top of a fast site. For a newsroom publishing dozens of articles a day it sometimes doess.
### E-commerce sites
Reduce mobile bounce rates with near-instant page loads. AMP-compatible WooCommerce integration available.
### Businesses in emerging markets
Critical for regions with poor internet infrastructure. AMP Cache serves content from Google's global CDN.
### Sites with limited dev resources
Get enterprise-level mobile performance without complex custom development.
## Where does AMP work?
AMP optimization benefits your site across multiple touchpoints:
- **Google Search** - Top Stories carousel, rich results, AMP badges
- **Mobile Devices** - Near-instant loading on all mobile browsers
- **Slow Connections** - Optimized delivery via AMP Cache
- **Social Media** - Faster sharing and preview experiences
- **Email** - AMP for Email interactive experiences
## AMP service pricing
Our AMP implementation service includes, with scope agreed individually:
### Basic package (individual quote)
- Official AMP plugin installation and configuration
- Standard mode or Transitional mode setup
- Basic theme compatibility adjustments
- Schema.org markup implementation
- Google Search Console validation
- Core Web Vitals baseline report
### Advanced package (individual quote)
- Everything in Basic, plus:
- Custom AMP template development
- WooCommerce AMP integration
- Advanced analytics setup
- AdSense/ad integration
- Page builder compatibility
- Performance monitoring dashboard
### Enterprise package (individual quote)
- Everything in Advanced, plus:
- Multi-site AMP implementation
- Custom component development
- A/B testing setup
- Monthly performance reports
- Priority support for 6 months
### Factors affecting price
- Theme complexity and customizations
- Number of custom post types
- WooCommerce product catalog size
- Third-party integrations required
- Multi-language requirements
[Get a custom quote](/en/contact/) for your WordPress site.
## What are AMP (Accelerated Mobile Pages)?
AMP (Accelerated Mobile Pages) is an open-source HTML framework developed by Google and the AMP Open Source Project to create fast-loading web pages for mobile devices. Launched in 2015, AMP was designed to address the growing need for faster mobile web experiences, especially in regions with limited internet connectivity.
The core principle behind AMP is simple: strip down web pages to their essential content and eliminate elements that slow down loading times. This is achieved through a combination of simplified HTML, restricted JavaScript usage, and the AMP Cache system that serves content from Google's servers.
## Is AMP still worth using in 2026?
A reality check before we go further: AMP is not a 2026 best practice. It is a 2015-era framework that Google demoted in 2021 when the Top Stories carousel stopped requiring it. Mobile-first indexing did not save AMP, because AMP was never the only way to be fast on mobile, and Core Web Vitals replaced it as the metric Google actually cares about.
What AMP does well, technically, has not changed. Strict CSS limits, no arbitrary JavaScript, and prefetching from Google's CDN produce sub-1-second LCP almost by default, low CLS because layout rules are enforced, and predictable INP because there is no third-party script chaos. If you are working with a small team and need a mobile variant that is hard to make slow, AMP still delivers on that narrow promise.
What AMP does not do anymore is buy you preferential search treatment. The AMP badge is gone in most markets. Top Stories ranks canonical URLs with good Core Web Vitals just as readily as AMP URLs. The crawl-efficiency argument matters only at very large scale. So if you are considering AMP for the SEO upside specifically, that upside is mostly historical at this point.
## How AMP works
### AMP HTML
AMP uses a specialized version of HTML with custom tags and restrictions:
- All CSS must be inlined and limited to 75KB
- JavaScript is restricted to AMP-approved components
- Images and videos use lazy loading by default
- Third-party scripts are sandboxed to prevent blocking
### AMP cache
Google's AMP Cache stores AMP pages and serves them directly from Google's infrastructure, providing:
- Pre-validation of AMP pages
- Compression and optimization
- Global CDN distribution
- Near-instantaneous delivery
### AMP JavaScript
The AMP JS library implements best performance practices:
- Asynchronous loading of all resources
- Pre-calculation of layout before loading
- Sandboxed iframes for third-party content
- Built-in analytics support
## Which WordPress AMP plugins are still maintained in 2026?
Before touching anything, look at what is actually still maintained.
The official **AMP** plugin (`amp` on WordPress.org, formerly "AMP for WordPress") is the only one I would still install on a new project. It is the plugin Google and Automattic backed, and it still ships updates against current WordPress and PHP versions. Three modes: Standard (the whole site is AMP), Transitional (AMP and canonical coexist), Reader (legacy templates, mostly relevant for old blogs you do not want to rebuild). Standard mode is the cleanest, but it will surface every theme incompatibility you have. Most paid themes from 2018-2020 break Standard mode in ways that take a day to fix.
**AMP for WP, Accelerated Mobile Pages** by Ahmed Kaludi and Mohammed Kaludi is the other plugin most clients arrive with. It is more permissive about ads, page builders, and WooCommerce, which is why agencies pushed it. Update cadence has slowed, GitHub issues sit open for months, and the WooCommerce add-ons broke in several WC 8.x releases. Functional, but I now treat it as legacy code I am migrating off, not a fresh choice.
**Penci AMP**, **Schema AMP** and the long tail of theme-bundled AMP plugins are mostly abandoned. If a client is on one of these, the migration question is not "if" but "when".
Whichever plugin you are on, the setup that actually matters is small: pick Standard or Transitional, run the AMP validator from Search Console (while it still exists), wire `amp-analytics` to your existing GA4 property, and audit which canonical-page features silently disappear on the AMP variant. The plugin's UI will not warn you when it strips your contact form's reCAPTCHA or your booking widget's JavaScript.
## Common AMP problems on WordPress sites
The AMP failure modes I keep seeing on real client sites, not the theoretical ones from the docs:
**Validation errors after a plugin update.** A perfectly valid AMP variant breaks the morning after someone activates a popup plugin, a chat widget, or a new analytics snippet. The non-AMP analytics tag injects inline JavaScript, and AMP rejects the whole page. The fix is removing the offending plugin's frontend output from the AMP variant via the official plugin's "Plugin Suppression" panel, but every new plugin reopens the question.
**The 75 KB inline CSS limit.** AMP requires all CSS inlined into a single `
' . esc_html__(
'Staff accounts only. Report a suspected password leak to IT.',
'your-textdomain'
) . '';
return $notice . $message;
}
add_filter( 'login_message', 'wppoland_login_message' );
```
Implementation notes:
- Escape the text (`esc_html__`), or build the markup through `wp_kses_post` if it needs a link.
- Do not replace `$message` blindly. Core puts its own notices there (after `loggedout=true`, for example). Prepend, or filter conditionally on the request.
- The `.message` class is already styled by `login.css`. Your `style-login.css` should respect it (text colour, left border), or the banner disappears into the background.
For text that depends on the action (`login`, `lostpassword`, `rp`), check `$_GET['action']` instead of showing the same note on password recovery and on the 2FA step.
## Add custom CSS to the WordPress login page
Do not dump login rules into the public `style.css`. Enqueue a dedicated file on the same hook:
```php
function wppoland_login_stylesheet() {
wp_enqueue_style(
'wppoland-custom-login',
get_stylesheet_directory_uri() . '/style-login.css',
array(),
wp_get_theme()->get( 'Version' )
);
}
add_action( 'login_enqueue_scripts', 'wppoland_login_stylesheet' );
```
Example `style-login.css`:
```css
body.login {
background-color: #0d1117;
display: flex;
align-items: center;
justify-content: center;
}
.login form {
background: #161b22;
border: 1px solid #30363d;
box-shadow: none;
border-radius: 8px;
}
.login label {
color: #c9d1d9;
}
.wp-core-ui .button-primary {
background: #238636;
border-color: rgba(27, 31, 35, 0.15);
}
.login #backtoblog a,
.login #nav a {
color: #c9d1d9;
}
```
Match contrast to WCAG where editors need it: pale grey on dark grey fails for some users. Test focus outlines on the username, password, and submit controls; removing outlines for "clean design" is a regress.
Language packs and RTL locales still use the same hooks. Avoid hard-coded English strings in CSS `content` properties.
Background images on `body.login` should be compressed and sized for a simple full-bleed photo, not a 4K marketing hero. Login traffic is frequent for editors; a heavy background taxes every session. Prefer a solid brand colour or a subtle pattern under 100 KB.
If the brand book mandates a light form on a light background, increase border contrast on `.login form` so the card still reads as a distinct surface. Many "minimal" logins fail because the form melts into the page.
## Change the WordPress login error message
A failed password for an existing user historically produced a message that confirmed the username. That helps attackers enumerate accounts before brute-forcing passwords.
```php
function wppoland_login_errors() {
return __( 'Invalid credentials.', 'your-textdomain' );
}
add_filter( 'login_errors', 'wppoland_login_errors' );
```
Keep the copy boring and identical for unknown user and wrong password paths. This is obscurity for enumeration, not a substitute for:
1. Disallowing weak admin passwords.
2. Limiting login attempts at the application or edge.
3. Turning on 2FA for administrators.
4. Not using `admin` as a username on new installs.
Lost-password flows have their own messages. Review those templates if you customise registration or membership plugins that share the login CSS.
Some membership plugins replace `wp-login.php` with a front-end form. In that case these hooks never fire. Apply the same rules on the plugin's form hooks, or style their template partials directly. Confirm which URL editors actually use before you declare the job done.
XML-RPC and the REST users endpoint are separate attack surfaces. Branding `wp-login.php` does nothing for them. Disable or [restrict XML-RPC](/en/wordpress-security-hardening-complete-guide-2026/) if you do not need it, and keep application passwords under policy if the site uses them.
## Disable the WordPress login shake animation
Failed logins trigger a shake via `wp_shake_js`. Some brand guidelines treat it as noise.
The snippet most tutorials still copy, `remove_action( 'login_head', 'wp_shake_js', 12 )`, has done nothing since WordPress 5.4. Core now attaches `wp_shake_js` to `login_footer`, and only conditionally, inside `login_header()`. You can see it in [`wp-login.php` in the wordpress-develop repository](https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-login.php); the 5.3 branch still used `login_head`. The stable method is the [`shake_error_codes`](https://developer.wordpress.org/reference/hooks/shake_error_codes/) filter, available since 3.0: an empty list of error codes means no animation.
```php
add_filter( 'shake_error_codes', '__return_empty_array' );
```
The filter is public API, so it does not depend on the priority or on which hook core happens to print the script from. Add a one-line comment above it that states the intent.
## Style 2FA and passkey fields on wp-login.php
Building full 2FA inside `functions.php` is the wrong project. Use a maintained plugin (for example the official Two Factor plugin or your host's offering) and style the extra fields so they match the branded form.
The [Two Factor](https://wordpress.org/plugins/two-factor/) plugin from the WordPress.org directory (version 0.17.0, 100,000+ active installs according to the directory API in October 2026) emits these classes on the second login step. They are taken from the 0.17.0 source:
- `.two-factor-prompt` for the instruction paragraph,
- `input.authcode` (with `id="authcode"`) for the TOTP code, the email code and the backup code; the field carries `inputmode="numeric"` and `autocomplete="one-time-code"`, so do not override those attributes with a script, or phones stop offering the code from SMS or the password manager,
- `.backup-methods-wrap` for the links to alternative methods,
- `.two-factor-email-resend` for the button that resends the email code.
```css
.login .two-factor-prompt {
color: #c9d1d9;
}
.login input.authcode {
letter-spacing: 0.2em;
text-align: center;
}
.login .backup-methods-wrap a {
color: #c9d1d9;
}
```
Older tutorials use selectors such as `.backup-methods` or `.two-factor-methods`. The current plugin code has neither, so those rules never match. Load the working rules in the same `style-login.css`. After every plugin update, re-check the second login step on staging, because class names drift. Other 2FA and passkey plugins have their own markup: read their selectors from their code, not from this guide.
…[truncated, fetch the canonical URL for the full body]…
---
## Googlebot and JSON-LD: a single unescape pass
URL: https://wppoland.com/en/googlebot-json-ld-single-unescape-pass-2026/
Description: Google changed its JSON-LD extraction and now applies only one pass of HTML unescaping. Double-escaped entities are no longer unrolled, so the block stops parsing and the structured data disappears. How to measure your own corpus and how to encode it correctly.
Published: Sun Aug 30
Updated: Wed Oct 14
Type: guide
Level: intermediate
Tags: json-ld, structured-data, googlebot, schema-org, technical-seo
Google has stopped repairing your structured data. JSON-LD extraction now applies a single pass of HTML unescaping, so a block that used to be quietly straightened out simply fails to parse and disappears. There is no error in Search Console, no warning, only a rich result that stops showing up. This piece shows how to measure your own corpus in a few minutes instead of guessing, where that encoding comes from in WordPress, and why a one-off audit is not enough. Our own measurement across 68 055 blocks is in here, along with the script.
## What changed in Google's JSON-LD extraction in 2026
The statement is short and worth quoting whole, because everything else follows from one sentence:
> To bring our parser up to JSON and other standards, we changed our JSON-LD extraction and are now only applying a single pass of HTML unescaping
Gary Illyes added a pointer to where correctness is defined: RFC 8259, the JSON specification. That is not an SEO guideline, it is a reference to the standard every JSON parser in the world already follows.
The practical consequence is stated just as briefly: double-escaped entities, such as `&` or `✔`, will no longer be unrolled. Google's parser used to make an extra pass and straighten that out. It now makes one pass and is left with text that is not valid JSON.
It is worth naming what the announcement does not contain. There is no rollout date. There is no link to updated Google documentation. The source is a LinkedIn post, reported by Search Engine Roundtable on 21 August 2026. Treat it as a state Google described, not as a specification you can cite back to a client.
## What is double escaping in JSON-LD
Take a simple case: the company name "Smith & Sons" in a JSON-LD `name` field.
| written as | what the JSON parser sees | status |
| --- | --- | --- |
| `"Smith & Sons"` | Smith & Sons | valid, JSON does not require escaping an ampersand |
| `"Smith \u0026 Sons"` | Smith & Sons | valid, universal escape |
| `"Smith & Sons"` | Smith & Sons | parses, but the value is wrong |
| `"Smith & Sons"` | Smith & Sons | this is double escaping |
An ampersand on its own will not blow up the block, because JSON does not require it to be escaped. The real trouble starts with the quotation mark. If a template writes `"` where `\"` belongs, one unescape pass leaves you with `"` rather than a quote character. The string never closes and the block stops being JSON.
Where does it come from in WordPress? Almost always from processing the same value twice. Content passes through `esc_html()` on save, then through a theme filter on output, and finally lands in a JSON-LD field that would have escaped it properly on its own. Each of those steps is correct in isolation. Composed together they produce an encoding that worked for years only because Google was straightening it out for you.
## How to check JSON-LD for double escaping
The rule that matters most: check the built HTML, not the template source. The source always looks fine, because the escaping is added on output. If you ship a static build, scan the output directory. On a classic WordPress install, pull a sample of URLs with `wget` or `curl` and scan what the server actually returned.
The check itself is a dozen lines. Pull out every `` into `\u003C` and `\u003E` ([PHP manual, JSON constants](https://www.php.net/manual/en/json.constants.php)). `wp_json_encode()` hands its flags straight to `json_encode()` with a default of 0 ([WordPress developer reference](https://developer.wordpress.org/reference/functions/wp_json_encode/)), so it stays safe unless someone adds the slash flag. `JSON.stringify()` escapes neither `<` nor `/`. If a value in your schema can come from a comment, a form or a content field, replace `<` with `\u003c` after stringifying. The data does not change, because every JSON parser decodes `\u003c` back to `<`.
## How to validate JSON-LD after every build
This is the part most often skipped. Structured data is not prose you write once. A template, a plugin or an integration generates it, and any update to those can reintroduce the escaping. An audit run today tells you about today's build and nothing more.
We turned the scan into a gate that runs after the build. It reads the output directory, counts blocks, tries to parse each one, and fails only on a real problem, meaning a block that does not parse or a double entity. With no output directory present it exits zero, so it does not produce a false red in an environment where nobody has built yet.
Three details decide whether such a gate is worth anything. First, it has to read the artefact rather than the source, because the source proves nothing about escaping. Second, it has to count blocks and print that count, so somebody notices when the number drops from sixty-eight thousand to two hundred because an integration stopped emitting them. Third, it has to separate an error from a note: single HTML entities get reported, but they do not fail the build, because that is not an outage, it is something you should know about.
## How to fix double-escaped JSON-LD in WordPress
A scan gives you a list of files, not a diagnosis. Before fixing anything, work out which layer produces the encoding, because a fix in the wrong place comes back with the next update.
Take one URL from the list and look at the raw server response, for example `curl -s URL | grep -A5 "application/ld+json"`. Check whether the damaged value comes from the post title, the SEO description or a custom field. That points at the layer faster than reading code does.
Then decide by source:
1. If the value comes from an SEO plugin, check whether two plugins are generating the same schema type. A duplicate generator is a more common cause of strange values than a bug in either one alone.
2. If the block is pasted into the theme, rewrite it to `wp_json_encode()` over the whole array. Manual string concatenation is the only real error here and it cannot be patched halfway.
3. If the entities are already in the database because a page builder put them there, do not fix it in the generator. Decode the value once before handing it to the JSON encoder, for example with `html_entity_decode()` using the quote flag and an explicit UTF-8 charset.
The classic mistake looks like this:
```php
echo '{"name":"' . esc_html( $title ) . '"}';
```
Correct looks like this:
```php
echo wp_json_encode( array( 'name' => $title ) );
```
The difference is not the character count, it is who owns the escaping. In the first version an HTML function does it in a context that is not HTML. In the second a JSON encoder does it in a JSON context.
After the fix, rerun the scan against a new build rather than the old artefact. That sounds obvious, and half of all "I fixed it and it is still broken" reports are a scan of a stale output directory.
…[truncated, fetch the canonical URL for the full body]…
---
## Mastering WordPress admin customization: A white-label guide
URL: https://wppoland.com/en/mastering-wordpress-admin-customization/
Description: Customize the WordPress dashboard to deliver a streamlined client experience. Learn menu modifications, custom widgets, and security hardening.
Published: Sat Jul 05
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: admin-customization, white-label, user-roles, settings-api
Delivering a generic, unconfigured WordPress dashboard to a client is like handing over the keys of an aircraft to someone who only needs to drive a car. The default administration area contains developer-facing options (such as Settings, Tools, and Plugins) that confuse non-technical users, increase the risk of accidental site breakages, and trigger unnecessary support tickets. A generic dashboard says you installed a theme. A customized one says you built a working tool for the roles that actually log in.
This guide is written for developers who hand sites over to editors, shop managers and client roles. It covers loading admin CSS and JS, menu cleanup, capability checks, custom roles, settings pages, color schemes, the login screen, isolation in a must-use plugin, and how to avoid breaking core screens. The official references for loading assets and building menus are [admin_enqueue_scripts](https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/) and [admin_menu](https://developer.wordpress.org/reference/hooks/admin_menu/). Learn more about our [professional WordPress development services](/en/wordpress-developer/) to build custom admin architectures.
## Why admin customization reduces support tickets
After the handover of a WooCommerce store built with a page builder, the client often sees dozens of menu items and uses a handful. The rest generates questions: "can I click Themes?", "what is ACF?", "why do I see WooCommerce > Status?".
Removing Tools, Settings and the file editors for everyone without `manage_options` is not about prettier colors. It takes dangerous paths out of sight for people who do not have full access.
The second gain is onboarding. Instead of an hour of "do not click here", you show one Support page with documentation, a contact and a short publishing checklist. That is white-label as a product, not just a logo swap.
## Admin customization in a must-use plugin instead of functions.php
Do not put admin customization in the child theme's `functions.php`. When the client switches themes, the menu rules, asset loading and capability checks disappear with it. Agency code that has to survive a theme change belongs in a must-use plugin under `wp-content/mu-plugins/`.
A typical structure:
```text
wp-content/mu-plugins/
wppoland-admin-ux.php # bootstrap (loads the folder)
wppoland-admin-ux/
menu.php
enqueue.php
capabilities.php
dashboard.php
```
The bootstrap file in the root of `mu-plugins/` loads automatically. It only requires the files inside the folder. The advantage is that the code always runs, whatever regular plugins are active, and you can version the whole folder without anyone being able to deactivate it from the Plugins screen.
```php
remove_cap( '...' )` belongs in a one-off migration routine or a CLI command, not in a request hook that runs on every admin page.
```php
/**
* Block direct URL access to Tools for non-administrators.
*/
function wppoland_block_tools_screen(): void {
if ( ! is_admin() || wp_doing_ajax() ) {
return;
}
if ( current_user_can( 'manage_options' ) ) {
return;
}
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen || 'tools' !== $screen->id ) {
return;
}
wp_die(
esc_html__( 'You do not have access to this screen.', 'wppoland-admin-ux' ),
esc_html__( 'Access denied', 'wppoland-admin-ux' ),
[ 'response' => 403 ]
);
}
add_action( 'current_screen', 'wppoland_block_tools_screen' );
```
Capability names are the contract. User IDs change, but "Shop Manager" keeps `manage_woocommerce`. Build rules around capabilities, not around user ID 12.
## Restrict WordPress admin access with a custom role
To secure the site, register a custom user role with specific primitive capabilities. Roles are stored in the database, not in code. The [add_role](https://developer.wordpress.org/reference/functions/add_role/) reference notes that the function stops and returns `null` if the role already exists, so a changed capability list has no effect until the role is removed and added again. Without a version check that would happen on every request, and since must-use plugins have no activation hook, a stored version value is the simplest guard.
```php
function wppoland_register_client_role(): void {
if ( '2' === get_option( 'wppoland_roles_version' ) ) {
return;
}
remove_role( 'client_manager' );
add_role(
'client_manager',
'Client Manager',
[
'read' => true,
'edit_posts' => true,
'edit_published_posts' => true,
'publish_posts' => true,
'delete_posts' => true,
'edit_pages' => true,
'edit_published_pages' => true,
'edit_others_pages' => true,
'publish_pages' => true,
'upload_files' => true,
'moderate_comments' => true,
]
);
update_option( 'wppoland_roles_version', '2' );
}
add_action( 'init', 'wppoland_register_client_role' );
```
Capabilities that are not in the list are simply not granted. That is safer than setting `manage_options => false` explicitly, because a forgotten key then opens nothing. When the list changes, bump the version value.
### How to block edits to key pages with map_meta_cap
Some pages should stay locked even when the role otherwise has `edit_others_pages`, typically the front page. The [map_meta_cap](https://developer.wordpress.org/reference/hooks/map_meta_cap/) filter translates meta capabilities such as `edit_post` into primitive capabilities, and there you can add `do_not_allow` for one specific page:
```php
function wppoland_protect_front_page( array $caps, string $cap, int $user_id, array $args ): array {
if ( ! in_array( $cap, [ 'edit_post', 'delete_post' ], true ) || empty( $args[0] ) ) {
return $caps;
}
$front_page = (int) get_option( 'page_on_front' );
if ( $front_page && (int) $args[0] === $front_page && ! user_can( $user_id, 'manage_options' ) ) {
$caps[] = 'do_not_allow';
}
return $caps;
}
add_filter( 'map_meta_cap', 'wppoland_protect_front_page', 10, 4 );
```
The advantage over hiding the Edit link is that the rule applies everywhere: in the pages list, in the block editor, in the REST API and in Quick Edit.
## How to hide admin menu items in WordPress
Once correct capabilities are configured, the first visible step is `remove_menu_page` on `admin_menu` with a late priority, so plugins have registered their menus before you remove them. The hook documentation is at [admin_menu](https://developer.wordpress.org/reference/hooks/admin_menu/).
```php
/**
* Clean up the admin menu for non-administrators.
*/
function wppoland_clean_admin_menu(): void {
if ( current_user_can( 'manage_options' ) ) {
return;
}
remove_menu_page( 'tools.php' );
remove_menu_page( 'options-general.php' );
remove_menu_page( 'edit-comments.php' );
remove_menu_page( 'edit.php?post_type=acf-field-group' );
remove_submenu_page( 'themes.php', 'theme-editor.php' );
remove_submenu_page( 'plugins.php', 'plugin-editor.php' );
}
add_action( 'admin_menu', 'wppoland_clean_admin_menu', 999 );
```
Warning: `remove_menu_page` only hides the link. Always combine it with a capability block where the path is dangerous (file editors, ACF field groups, plugin installation).
For WooCommerce stores it is common to let the Shop Manager keep Products, Orders and Coupons but remove the Status, Extensions and system status submenus. That cuts "can I update this plugin?" questions without taking away what daily operations need.
Priority `999` is deliberate. Many plugins register menus on `admin_menu` at the default priority 10. If you run the cleanup too early, the slug does not exist yet and `remove_menu_page` does nothing. Temporarily log `$GLOBALS['menu']` on staging if you are unsure which slug a plugin uses. ACF, Yoast and WooCommerce all have non-obvious paths.
Do not remove the Media Library for editors who actually upload images. That is classic over-cleanup: the panel looks tidy, and the next day a ticket arrives saying "Upload" is missing. Clean up technical tools first and content paths last.
## Add custom admin menu pages
Avoid heavy options frameworks if you only need one support page. The core API is fast and predictable.
```php
function wppoland_register_support_page(): void {
add_menu_page(
'Client support',
'Support',
'edit_posts',
'wppoland-support',
'wppoland_render_support',
'dashicons-sos',
90
);
}
add_action( 'admin_menu', 'wppoland_register_support_page' );
…[truncated, fetch the canonical URL for the full body]…
---
## List of ping services - does it still work in 2026?
URL: https://wppoland.com/en/ping-services-list-does-it-still-work/
Description: The "ping list" used to be the secret to fast indexing. Today Google relies on the Indexing API and XML sitemaps. Why did pinging fall out of use?
Published: Sun May 08
Updated: Wed Oct 14
Type: guide
Level: intermediate
Tags: ping, indeksowanie, sitemap, wordpress, seo
In the WordPress dashboard, under **Settings > Writing**, there is still a small field called "Update Services". For years, "magic ping lists" with hundreds of addresses (rpc.pingomatic.com, technorati and so on) circulated online, supposedly getting your post into Google within 5 minutes. In 2026 those lists are outdated and ineffective. This article explains why pinging lost its value and what to do instead to get your site indexed by search engines.
## Does pinging in WordPress still work in 2026
**Pinging in WordPress is a relic of the past**, and for most webmasters today it does more harm than good. Most services on those old lists no longer exist, and the ones still running are not followed by any serious search engine. Google does not document ping services as a discovery source and shut down its own sitemap ping endpoint after a 2023 announcement, and Microsoft promotes its own IndexNow protocol instead of traditional pinging. Googlebot is now advanced enough to discover pages on its own through crawling and sitemap analysis.
Search engine algorithms have evolved to the point where the old ways of announcing new content no longer carry much weight. Modern Google crawlers work continuously and automatically, watching for changes on websites without needing explicit notifications. So even if your WordPress site sends a ping to a ping service, the search engine will check your sitemap first and only then (if at all) pay attention to any extra signals.
In practice, adding hundreds of ping services to the "Update Services" list in WordPress will not speed up indexing in Google. It does lengthen the background job that sends the pings, because WordPress contacts every service on the list in turn and waits up to 3 seconds for each one that does not answer.
### Why pinging stopped working
Before moving on to modern indexing methods, it helps to understand why traditional pinging stopped being effective. That background makes it easier to follow how indexing has evolved and to pick the right strategy for your own site deliberately.
**Technical reasons pinging declined:**
The first and most important reason is that ping services shut down en masse over the last fifteen years. Services such as Technorati, Google Blog Search and Weblogs.com, once the backbone of the blogging ecosystem, have long since disappeared or stopped supporting the XML-RPC ping protocol. Pinging services that no longer exist brings no benefit at all and adds needless delays and server load.
The second key reason is a fundamental change in how search engines discover and index content. Google's documentation on page discovery describes links and sitemaps and does not mention ping services. The company focused on building out its own crawler infrastructure, which can find new pages on its own by analysing links and sitemaps. This strategic shift was a response to the growing amount of ping spam and fake notifications.
The third factor is the standardisation of the XML sitemap format, which became the preferred way for webmasters and search engines to communicate. SEO plugins such as Yoast SEO or Rank Math generate sitemaps automatically and update them whenever content on the site changes. This automation is far more reliable and complete than traditional pinging, because it covers every content type, not just blog posts.
The fourth reason is security. Ping services were repeatedly abused for DDoS attacks and other misuse. Current security practice favours controlled, centralised notification methods such as the Google Indexing API.
**What this means in practice for site owners:**
The consequences for site owners are clear. A ping to a service that no longer exists does not speed up indexing, and it keeps the background ping job waiting for up to 3 seconds per unreachable address. Repeated pings waste server resources and add network load. Most importantly, relying on pinging can create a false sense of security while the site owner neglects the technical SEO methods that actually matter.
## Which ping services the WordPress handbook still lists
The official [Update Services page in the WordPress Advanced Administration Handbook](https://developer.wordpress.org/advanced-administration/wordpress/update-services/), last updated on January 14, 2026, names three XML-RPC ping endpoints:
```
https://rpc.pingomatic.com/
https://rpc.twingly.com/
http://ping.blo.gs/
```
Technorati, Weblogs.com, Google Blog Search and the FeedBurner ping address from the old lists do not appear in the official documentation. The handbook also makes no promise about indexing speed for the three addresses it does list. It says only that update services "process the ping and update their proprietary indices", and neither Google nor Bing documents those indices as a source of new URLs.
A fresh install fills the field with one address. In the core file [`wp-admin/includes/schema.php`](https://github.com/WordPress/wordpress-develop/blob/7.1/src/wp-admin/includes/schema.php) the default value of the `ping_sites` option is `https://rpc.pingomatic.com/`, over HTTPS. Sites installed years ago often still carry the `http://` variant.
[Ping-O-Matic](https://pingomatic.com/) describes itself as "a service to update different search engines that your blog has updated" and says it regularly checks downstream services to make sure they are legitimate and still work. Among its common services it lists Blo.gs and FeedBurner. The same page asks users to ping specialised services only when they are relevant to the blog, so the services are not overloaded.
The handbook adds two notes that matter in practice. Some hosts, particularly free ones, disable the PHP functions WordPress uses to send pings, and on such a host the handbook recommends emptying the field so WordPress stops trying. On a Multisite network the field is disabled by default for individual sites, and the handbook shows a small must-use plugin that turns it back on through the `enable_update_services_configuration` filter. It also names Feed Shark as an alternative that pings more than 60 services.
## What WordPress does when it sends a ping
The behaviour lives in WordPress core, so it can be read in the code. When a post is published, `_publish_post_hook()` schedules a one-off WP-Cron event called `do_pings`. That event runs `generic_ping()`, which reads the `ping_sites` option line by line and calls `weblog_ping()` for every address. According to [`wp-includes/comment.php`](https://github.com/WordPress/wordpress-develop/blob/7.1/src/wp-includes/comment.php), `weblog_ping()` uses a timeout of 3 seconds per service. It first tries `weblogUpdates.extendedPing` with the site name, home URL and RSS feed URL, then falls back to `weblogUpdates.ping` with the site name and home URL. The address of the new post is not part of the request.
Two consequences follow from that code. The pings run in a cron request after publishing, outside the click on Publish, so a long list mainly lengthens that background job, by up to 3 seconds for each address that does not answer. And because only the home page and the feed travel in the ping, the receiving service has to fetch the feed itself to find out what changed.
Three settings stop pings without touching the list:
- **Settings > Reading, "Discourage search engines from indexing this site".** `privacy_ping_filter()` then returns an empty list, and Settings > Writing shows a notice that WordPress is not notifying any Update Services because of the site's visibility settings.
- **The `enable_update_services_configuration` filter.** Returning false hides the Update Services section from Settings > Writing.
- **The environment type, since WordPress 7.1.** Core removes outgoing pingbacks, trackbacks and ping service notifications, and rejects incoming trackbacks and pingbacks, whenever `wp_get_environment_type()` returns something other than `production`, so for `local`, `development` and `staging`. The `wp_should_disable_pings_for_environment` filter can reverse that decision in either direction. A staging copy with `WP_ENVIRONMENT_TYPE` set correctly no longer pings Ping-O-Matic each time someone tests a post.
## Does Google still accept a sitemap ping
No. On June 26, 2023 Google [announced on the Search Central blog](https://developers.google.com/search/blog/2023/06/sitemaps-lastmod-ping) that it was deprecating the sitemaps "ping" endpoint and that the endpoint would stop working within 6 months. The post explains that unauthenticated sitemap submissions "are not very useful" and that for Google Search "the vast majority of the submissions lead to spam". Requests to the old address now return a 404 error. Google adds that existing code or plugins calling the endpoint cause no problems for Google Search, and that calling it does nothing useful either. Sitemaps reach Google through a `Sitemap:` line in robots.txt or through Search Console.
The same post covers the sitemap fields. Google uses `lastmod` as a signal for scheduling crawls of URLs it already knows, provided the date uses a supported format and matches the real last change of the page. Google "still doesn't use the changefreq or priority elements at all", so tuning priorities in a sitemap plugin has no effect on Google.
## How to check the ping_sites option with WP-CLI
On a site with an inherited configuration, the fastest audit runs from the command line with [`wp option get`](https://developer.wordpress.org/cli/commands/option/get/), [`wp option update`](https://developer.wordpress.org/cli/commands/option/update/) and [`wp cron event list`](https://developer.wordpress.org/cli/commands/cron/event/list/):
```bash
# what the Update Services field contains
wp option get ping_sites
# keep only the WordPress default
wp option update ping_sites 'https://rpc.pingomatic.com/'
# or stop service pings entirely
wp option update ping_sites ''
# 0 means "Discourage search engines" is on, so no pings are sent
wp option get blog_public
# is a do_pings event waiting in WP-Cron
wp cron event list --fields=hook,next_run_relative | grep do_pings
# the environment type WordPress 7.1 uses to decide about pings
wp eval 'echo wp_get_environment_type();'
```
On Multisite, add `--url=` with the address of the subsite to read or change its own option.
## History of pinging in WordPress since 2003
Knowing the history of pinging explains why the technique was considered effective for so long and why it is outdated today. It is also a good window into how the web evolved and how the approach to indexing content changed.
### How pinging worked from 2003 to 2008
In the early days of blogging, pinging was a new idea that changed how search engines found new content. When you published a new post, WordPress sent an XML-RPC signal to content aggregators. Those services then visited your site, indexed the content and published a link to your article in their directory. The whole process could take just a few minutes, which was unthinkable compared with the weeks of waiting for indexing through traditional methods.
A typical ping list from those years contained dozens or even hundreds of services. The most important ones were: `http://rpc.pingomatic.com/`, the aggregator of aggregators, which distributed pings to many services at once; `http://technorati.com/ping/`, then the most popular blog service and in effect a blog search engine; `http://weblogs.com/`, one of the first ping services, running since 1999; `http://blogsearch.google.com/ping/`, Google Blog Search, Google's dedicated blog search engine; `http://ping.feedburner.com/`, FeedBurner, a service for managing RSS feeds; and `http://rpc.weblogs.com/RPC2`, Weblogs.com, a blog aggregator.
…[truncated, fetch the canonical URL for the full body]…
---
## Content governance for large teams: Scaling WordPress without chaos
URL: https://wppoland.com/en/content-governance-large-teams-wordpress-guide-2026/
Description: When dozens of editors share one WordPress site, default roles are not enough. Roles, editorial workflows, staging, plugin change control, and multilingual team patterns for 2026.
Published: Sat Oct 25
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: content-governance, editorial-workflow, user-roles, audit-logs, enterprise-wordpress
Content governance on WordPress is the operating system for who may draft, who may approve, and what may reach production. Solo sites can publish with one click. Teams with writers, SEO reviewers, legal, and locale owners cannot. This guide covers roles, editorial workflows, staging, plugin change control, and multilingual review without inventing a second CMS.
Default WordPress gives you Administrator, Editor, Author, Contributor, and Subscriber. Those roles are useful starting points. They are not an org chart. The [roles and capabilities handbook](https://developer.wordpress.org/plugins/users/roles-and-capabilities/) is explicit: capabilities are the atomic permissions, and roles are collections of capabilities. Governance starts when you stop treating "Editor" as a job title and start mapping real jobs to named capabilities.
## What each default WordPress role can do
The [WordPress user documentation](https://wordpress.org/documentation/article/roles-and-capabilities/) lists six default roles: Super Admin, Administrator, Editor, Author, Contributor, and Subscriber. The capability lists behind them decide more than the labels suggest.
| Role | Publishes | Edits others' posts | Uploads files | `unfiltered_html` |
|:---|:---|:---|:---|:---|
| Administrator (single site) | yes | yes | yes | yes |
| Editor | yes | yes | yes | yes on single site, no on Multisite |
| Author | own posts | no | yes | no |
| Contributor | no | no | no | no |
| Subscriber | no | no | no | no |
Three details matter for governance. An Author also holds `delete_published_posts`, so a writer with that role can remove their own live article without anyone signing off. A Contributor has only `read`, `edit_posts`, and `delete_posts`, which is why the role cannot attach images. On Multisite, plugin and theme installs, updates, user management, and `unfiltered_html` belong to the Super Admin alone, while on a single site the Administrator holds them.
## Who can publish unfiltered HTML in WordPress
`unfiltered_html` lets a user save markup that WordPress would otherwise sanitise, including script tags. On a single site both Administrators and Editors have it. On Multisite only the Super Admin does. For a newsroom this means every Editor on a single-site install can place raw code into a published page. If your Editors do not need that, remove the capability from the Editor role and record the decision next to your role matrix.
The same documentation notes that `unfiltered_upload` is not granted to any role by default, not even to Super Admins. It only becomes available after `ALLOW_UNFILTERED_UPLOADS` is defined in `wp-config.php`. Keep that constant out of production unless a specific workflow needs it.
## Custom WordPress user roles for editorial teams
Map people to capabilities, not to the five core labels.
An Administrator holds the nuclear set: plugins, themes, users, and destructive options. Keep that role for a short list of platform owners. Do not hand it to every "senior editor" who needs to fix a typo.
An Editor can publish and manage posts written by others. That is too broad for a junior writer and too blunt for a compliance reviewer who should approve copy without rewriting layout.
An Author can publish their own posts immediately. That skips review by design. Keep it only where instant self-publish is an accepted risk.
A Contributor can write drafts but cannot publish and, in core, cannot upload media. Many newsrooms need a "writer" role that can upload images and attach them to drafts while still blocked from `publish_posts`.
Build [custom roles](/en/wordpress-roles-and-capabilities-guide/) from capabilities such as `edit_posts`, `publish_posts`, `edit_others_posts`, `delete_posts`, `manage_categories`, and `upload_files`. Prefer capability checks in code (`current_user_can( 'publish_posts' )`) over role-name checks. A role name says nothing about what a plugin or a later migration added to it, while a capability check asks the question you actually care about.
Typical large-team roles look like this in practice:
- **Writer**: create and edit own drafts, upload media, no publish.
- **Section editor**: edit others' posts in assigned categories, move status to review, no unrestricted publish.
- **SEO metadata reviewer**: edit SEO fields and excerpts, read full content, no structural block changes if you lock patterns.
- **Compliance reviewer**: approve or reject, no theme or plugin access.
- **Publisher**: sole holders of `publish_posts` for regulated post types.
Store role definitions in version control and apply capability changes through a controlled migration, not by clicking around on production. Capability maps that live only in the database drift the moment someone "temporarily" elevates a user.
### How to add a custom role in code
The [plugin handbook](https://developer.wordpress.org/plugins/users/roles-and-capabilities/) explains that WordPress stores roles and their capabilities in the options table under the `user_roles` key. That storage detail shapes how you write the migration:
```php
function wppoland_register_writer_role() {
if ( get_role( 'newsroom_writer' ) ) {
return;
}
add_role(
'newsroom_writer',
'Newsroom writer',
array(
'read' => true,
'edit_posts' => true,
'upload_files' => true,
)
);
}
add_action( 'init', 'wppoland_register_writer_role' );
```
After the first call, the role lives in the database and later calls to `add_role()` do nothing, including when you change the capability array. To change the set in bulk, the handbook says to remove the role with `remove_role()` and add it again, and to do that only when the stored capabilities differ from what you expect, because unconditional re-registration degrades performance. Single capabilities can be added to an existing role with `get_role()` and `add_cap()`. The handbook also advises against removing the Administrator and Super Admin roles, and reminds you to update `default_role` if you remove Subscriber.
## WordPress editorial workflow with custom post statuses
Core statuses (`draft`, `pending`, `publish`, `future`, `private`) are a thin pipeline. Large teams need named gates: legal review, SEO check, locale review, scheduled lock.
[`register_post_status()`](https://developer.wordpress.org/reference/functions/register_post_status/) lets you add statuses such as `seo_review` or `legal_review`. Pair them with UI that only shows the next allowed action for the current user's capabilities. A post should not jump from draft to publish because someone found the Status dropdown.
Hook into status transitions so the system enforces the path. `transition_post_status` and related hooks fire when a post moves between statuses. Use them to:
- notify the next reviewer,
- block illegal jumps (for example, draft directly to publish),
- write an audit row with user ID, previous status, new status, and timestamp,
- clear or set locks when a reviewer takes ownership.
The [post status documentation](https://wordpress.org/documentation/article/post-status/) lists eight built-in statuses: publish, future, draft, pending, private, trash, auto-draft, and inherit. When a user has `edit_posts` but not `publish_posts`, the editor shows a "Submit for Review" button instead of "Publish", and the post lands in `pending`. That is the only review gate core gives you without code. The same page carries a notice that `register_post_status()` does not add the registered status to the administration screens (Trac ticket #12706), so a custom status needs its own UI in the editor before writers can use it.
Pending in core means "submitted for review," which is one queue. It is not five queues. If legal and SEO both need a sign-off, model two statuses or two explicit approval flags. Do not rely on Slack messages as the system of record.
Keep the publish capability scarce. Workflow plugins and custom code only work if the people in earlier stages literally cannot set `publish`. If every Editor can still publish, the Kanban board is decoration.
## How to review drafts with notes in the block editor
WordPress 6.9 introduced Notes in the block editor, and the [user documentation](https://wordpress.org/documentation/article/notes/) states that more advanced features arrived in WordPress 7.1. In the current version a reviewer can attach a note to a whole block or to a selected span of text, start separate threads on the same block, mention another user with @, reply, and resolve a thread when the point is settled. Notes appear only in the editor and never in published content.
Permissions follow edit rights. Administrators and Editors can view and add notes on all posts. Authors and Contributors can do so on their own posts. Subscribers cannot see them. By default WordPress emails the post author when someone else leaves a note and emails any user who is mentioned; the Discussion settings control that notification. Notes are available in the post and page editors, not in the Site Editor, and a custom post type needs support added by its theme or plugin.
For governance this moves review comments out of chat threads and into the post itself. A legal reviewer can mark the exact sentence that needs a disclaimer, mention the section editor, and resolve the thread after the fix. Resolved notes stay in the post's history for reference. Notes do not replace a status gate: they record the conversation, while `publish_posts` still decides who can ship.
## How to lock blocks and patterns for editors in Gutenberg
The block editor handbook groups these controls under [curating the editor experience](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/). Four of them carry most of the governance weight.
1. **A fixed palette in `theme.json`.** Turn off custom colours and gradients so editors pick only brand tokens.
```json
{
"settings": {
"color": {
"custom": false,
"customGradient": false,
"defaultPalette": false,
"palette": [
{ "slug": "brand-primary", "color": "#1a1a2e", "name": "Primary" },
{ "slug": "brand-secondary", "color": "#16213e", "name": "Secondary" }
]
}
}
}
```
2. **Locked patterns and content-only editing.** Blocks can be locked against moving or removal, and since WordPress 6.1 `"templateLock":"contentOnly"` on Group, Cover, or Columns hides design tools while text and images stay editable. The [Block Locking API page](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/block-locking/) adds a caveat that matters: by default, users with editing access can unlock blocks that a theme author locked.
3. **Who may lock and unlock.** Close that gap with the `block_editor_settings_all` filter. The handbook's example ties the right to the `edit_theme_options` capability:
```php
add_filter( 'block_editor_settings_all', function( $settings ) {
$settings['canLockBlocks'] = current_user_can( 'edit_theme_options' );
return $settings;
} );
```
4. **Fewer blocks and no code editor.** Limit the inserter with `allowed_block_types_all` or unregister blocks you do not want in the newsroom vocabulary. The same filter family can set `codeEditingEnabled` to false, which the handbook recommends where raw block markup editing could break content; add a capability check if only some users should lose it.
Test every one of these with an Editor-level account. An Administrator sees a different editor and will miss the restrictions you meant to enforce.
## When to use a WordPress staging site
Separate content workflow from infrastructure change.
Editorial drafting often happens on production with draft and review statuses. That is normal for publishers who need live media libraries and authentic preview URLs. Theme updates, plugin installs, PHP version bumps, and role-capability experiments belong on staging first.
A workable pattern:
…[truncated, fetch the canonical URL for the full body]…
---
## Docker for WordPress development in 2026: Containerization made easy
URL: https://wppoland.com/en/docker-wordpress-development-guide-2026-en/
Description: Still using Local by Flywheel or MAMP? Discover why Docker is the industry standard for WordPress development in 2026 and how to set it up.
Published: Sat Jan 03
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: docker, devops, local-development, workflow, linux
If you are a professional [WordPress developer](/en/wordpress-developer/) in 2026, **Docker** is your best friend. Gone are the days of manually installing PHP versions on your Mac or fighting with Windows-specific server bugs. Docker has become the common language of development environments, and knowing it well is part of the job for anyone working in the WordPress ecosystem today.
This guide covers Docker applied to WordPress development: from the basics and the official `wordpress` image to Compose, wp-env, debugging, CI/CD and migrating away from MAMP or LocalWP. The technical details come from the official Docker documentation, the `wordpress` image page on Docker Hub, MariaDB and the WordPress developer handbook, linked in each section.
## Why use Docker for WordPress development
The "works on my machine" excuse stops working once the environment lives in versioned files. Containerization ensures consistency between environments, and that shift has changed how WordPress teams collaborate and ship software.
### Parity between local and production environments
Your local container can use the same Linux distribution, PHP version, and MySQL configuration as your production server. That cuts down the most frustrating category of bugs: the ones that only show up in production. When you develop inside a Docker container built from the same image the server runs, code that works on your laptop behaves the same on the [staging server](/en/wordpress-staging-site-workflow-guide/) and in production.
Environment parity goes beyond software versions. It covers network configuration, environment variables and file permissions. With Docker, every team member works in an identical environment, whether the host operating system is macOS, Windows or Linux.
### Several PHP versions in isolated projects
Project A needs PHP 8.4, and Project B needs PHP 7.4? No problem. Each project lives in its own "bubble" and never conflicts with others. This level of isolation was hard to get with traditional tools like MAMP or XAMPP, where changing the PHP version affected every project at once.
Docker isolation also protects your host operating system. PHP extensions, system libraries and project dependencies stay separate. If one project needs an experimental PHP extension or an unusual server configuration, you can add it without risk to your other projects.
### A reproducible environment from files versioned in Git
Every Docker environment is defined by configuration files versioned in Git. A new team member clones the repository, runs one command and has the environment working, with no step-by-step setup guide that slowly goes out of date. The environment documentation lives in the project code itself.
## WordPress Docker Compose setup
Docker Compose is the tool that coordinates multiple containers. A typical 2026 [WordPress setup](/en/ultimate-wordpress-developer-setup/) includes several services that work together as a complete development environment.
### Which services WordPress needs in Docker Compose
1. **WordPress (PHP-FPM or Apache)**: the engine that runs the WordPress PHP code.
2. **Nginx**: the web server in front of PHP-FPM, if you choose the `fpm` variant.
3. **MariaDB/MySQL**: the database that stores all the content.
4. **Redis**: the object cache, worth adding if the project uses it in production.
5. **Mailpit**: to catch outgoing emails during testing.
```yaml
# compose.yaml snippet with the FPM variant
services:
db:
image: mariadb:11.4
volumes:
- db_data:/var/lib/mysql
wordpress:
image: wordpress:php8.4-fpm
depends_on:
- db
volumes:
db_data:
```
The `fpm` variant ships no web server, so it needs an Nginx container that passes PHP requests to it. If you do not want to configure Nginx, the Apache variant (`wordpress:php8.4-apache`) serves WordPress directly on port 80 of the container.
### Bind mounts or named volumes for WordPress
Volumes in Docker Compose matter a lot for WordPress development. You need to mount your themes and plugins directory to edit code live, while database data must persist in named volumes so nothing is lost when the container restarts.
The right strategy balances performance and function. For files you edit often (themes, your own plugins), use bind mounts. For data that only needs to persist (database, uploads), use named Docker volumes. The [official image documentation](https://hub.docker.com/_/wordpress) gives the paths: themes go in `/var/www/html/wp-content/themes/` and plugins in `/var/www/html/wp-content/plugins/`.
### Credentials in .env files with Docker Compose
Secret handling matters even locally. Use `.env` files for database credentials and API keys, and make sure those files are listed in your `.gitignore`. Docker Compose reads the `.env` file in the project directory and substitutes variables such as `${DB_PASSWORD}` inside `compose.yaml`, which keeps sensitive configuration apart from versioned code.
## Environment variables of the official WordPress image
The `wordpress` image on Docker Hub generates its own `wp-config.php` from environment variables. According to the [official image page](https://hub.docker.com/_/wordpress), these are the ones it reads:
| Variable | What it does |
| :--- | :--- |
| `WORDPRESS_DB_HOST` | Service name or host of the database |
| `WORDPRESS_DB_USER` and `WORDPRESS_DB_PASSWORD` | Database credentials |
| `WORDPRESS_DB_NAME` | Database name, which must already exist |
| `WORDPRESS_TABLE_PREFIX` | Table prefix instead of `wp_` |
| `WORDPRESS_AUTH_KEY` and the other keys and salts | If you leave them out, the image generates random values |
| `WORDPRESS_DEBUG` | Any non-empty value turns on `WP_DEBUG` |
| `WORDPRESS_CONFIG_EXTRA` | Extra PHP code evaluated inside `wp-config.php` |
The detail that wastes the most time: the WordPress container does not create the database named in `WORDPRESS_DB_NAME`. The database container creates it, through `MARIADB_DATABASE` in MariaDB or `MYSQL_DATABASE` in MySQL. If the two names differ, WordPress shows a connection error even with correct credentials.
`WORDPRESS_CONFIG_EXTRA` is for constants the image does not expose, for example `WP_ALLOW_MULTISITE` or `WP_ENVIRONMENT_TYPE`. Whatever you put there runs through `eval()`, so keep it short and versioned with the rest of the configuration.
## How to make WordPress wait for MariaDB in Docker Compose
A plain `depends_on` is not enough. The [Compose documentation on startup order](https://docs.docker.com/compose/how-tos/startup-order/) explains that Compose waits for the dependency container to be running, not for it to be ready to accept connections. MariaDB needs a few seconds to initialise on first start, and WordPress can boot earlier and fail to connect.
The fix is a `healthcheck` on the database and `condition: service_healthy` on WordPress. The official MariaDB image includes a `healthcheck.sh` script for this, and the [MariaDB documentation](https://mariadb.com/docs/server/server-management/automated-mariadb-deployment-and-administration/docker-and-mariadb/using-healthcheck-sh) publishes an example with WordPress. Adapted for a development environment it looks like this:
```yaml
services:
db:
image: mariadb:11.4
environment:
MARIADB_DATABASE: wordpress
MARIADB_USER: wordpress
MARIADB_PASSWORD: ${DB_PASSWORD}
MARIADB_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 10s
interval: 10s
timeout: 5s
retries: 3
wordpress:
image: wordpress:php8.4-apache
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
WORDPRESS_DEBUG: '1'
volumes:
- wp_data:/var/www/html
- ./my-theme:/var/www/html/wp-content/themes/my-theme
depends_on:
db:
condition: service_healthy
volumes:
db_data:
wp_data:
```
With this file, `docker compose up -d` starts the database, waits for the healthcheck to pass and only then starts WordPress on `http://localhost:8080`. Your own theme is a bind mount, and the rest of WordPress sits in the `wp_data` volume.
## WordPress Docker image variants: apache, fpm, alpine and cli
The official image is published in several variants. Tags combine the WordPress version, the PHP version and the type, for example `php8.4-apache`, `php8.4-fpm`, `php8.4-fpm-alpine` or `cli`.
- **apache**: WordPress and Apache in one container. The most direct option for local development.
- **fpm**: PHP-FPM only, meant to sit behind Nginx or another proxy.
- **fpm-alpine**: the same on Alpine Linux, with a smaller image.
- **cli**: WP-CLI without a web server, for running `wp` commands against an existing install.
The `cli` variant has a trap documented on [Docker Hub](https://hub.docker.com/_/wordpress): it is based on Alpine and its `www-data` user has UID 82, while the Debian-based variants use UID 33. Running `wordpress:cli` against a Debian install usually needs `--user 33:33`, and sometimes `-e HOME=/tmp`, so WP-CLI can write to the files.
### How to add PHP extensions to the official image
The image does not include extra PHP extensions, even when popular plugins need them. Docker Hub gives the example that the image cannot send email on its own. To add extensions you build your own image with `FROM wordpress:...` and follow the instructions of the `php` image. To change directives like `upload_max_filesize`, copying a `custom.ini` into `$PHP_INI_DIR/conf.d/` is enough:
```dockerfile
FROM wordpress:php8.4-apache
COPY custom.ini $PHP_INI_DIR/conf.d/
```
If you want themes or plugins baked into the image for several installs, the documentation says to copy them to `/usr/src/wordpress/`, which is copied to `/var/www/html/` on the container's first start.
## wp-env, the official WordPress Docker environment
If you build plugins or themes and do not need to reproduce a specific server, [wp-env](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/) saves you from writing `compose.yaml` by hand. It is a Node package maintained in the Gutenberg repository that runs Docker underneath. You install it with `npm -g i @wordpress/env` and start it with `wp-env start` from the plugin folder.
The environment runs at `http://localhost:8888` with the user `admin` and the password `password`. Configuration goes in a `.wp-env.json` file at the project root:
```json
{
"$schema": "https://schemas.wp.org/trunk/wp-env.json",
"core": null,
"phpVersion": "8.3",
"plugins": [ "." ],
"config": {
"WP_DEBUG_LOG": true
}
}
```
`core` set to `null` uses the latest stable WordPress release, `phpVersion` pins the PHP version and `plugins` lists the plugins to install and activate. There are also `themes`, `mappings` for mounting directories, `port` (8888 by default), `testsPort` (8889) and `mariadbVersion`.
Composer, PHPUnit and WP-CLI come with the `cli` container: `wp-env run cli wp plugin list` or `wp-env run cli --env-cwd=wp-content/plugins/my-plugin composer install`. Xdebug is installed but off, and you turn it on with `wp-env start --xdebug`. The documentation also describes an experimental mode, `wp-env start --runtime=playground`, which runs WordPress in WebAssembly without Docker, on SQLite and without the `run` command.
The practical difference from Compose: wp-env picks the architecture for you. For a plugin that is faster. To reproduce a production server with Nginx, Redis and an exact MariaDB version, Compose is still the right tool.
## VS Code dev containers for WordPress
Many developers no longer install PHP on their laptops. They use **VS Code Dev Containers**, which change how you work with the editor.
### How a dev container works in VS Code
…[truncated, fetch the canonical URL for the full body]…
---
## DORA Article 28 ICT third-party risk: WordPress hosting and WAF supplier audit
URL: https://wppoland.com/en/dora-article-28-ict-third-party-risk-wordpress/
Description: Article 28 of Regulation 2022/2554 makes financial entities responsible for the ICT risk of every third-party they touch. I walk through the supplier due-diligence checklist I ship with WordPress engagements for banks and insurers in 2026.
Published: Wed Apr 29
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: dora, article-28, third-party-risk, ict-supplier, wordpress, hosting, waf
# DORA Article 28 ICT third-party risk: WordPress hosting and WAF supplier audit
Article 28 of [Regulation 2022/2554](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32022R2554) is the part of DORA that decides whether a financial-entity client can sign a hosting contract or a WAF subscription at all. It applies to credit institutions, payment institutions, insurers, investment firms, crypto-asset service providers and around fifteen more categories listed in Article 2. From 17 January 2025 it applies directly without national transposition.
This is a supporting article in the [NIS2 and DORA on WordPress pillar](/en/nis2-dora-wordpress-compliance-2026/), with cross-references to the [NIS2 Annex II evidence trail](/en/nis2-annex-ii-wordpress-agencies-evidence-trail/) and the [CRA + NIS2 + DORA stack overview](/en/cra-nis2-dora-2026-compliance-stack-headless-wordpress/).
## TL;DR
- DORA Article 28 = general principles for managing ICT third-party risk.
- Article 30 = the mandatory contractual clauses.
- Article 31 = the CTPP designation regime, run by the European Supervisory Authorities.
- A WordPress engagement with a bank or insurer triggers a register entry, due diligence, mandatory clauses and an exit strategy.
- Hosting, CDN, WAF, payment gateway plugin and email-sending plugin all enter the register.
## Which financial entities DORA applies to
Article 2(1) of DORA enumerates [the financial entities](/en/nis2-vs-dora-scope-overlap-wordpress-agency-2026/). The most common in WordPress engagements:
- Credit institutions and their groups.
- Payment institutions and electronic money institutions.
- Investment firms, including those running an MTF or OTF.
- Crypto-asset service providers under MiCA.
- Insurance and reinsurance undertakings.
- Insurance and reinsurance intermediaries above the size threshold.
- Crowdfunding service providers.
- Investment funds (UCITS managers, AIFMs).
Plus the **critical ICT third-party service providers (CTPP)**, designated under Article 31 by the European Supervisory Authorities (EBA, EIOPA and ESMA) through the Joint Committee, on a recommendation from the Oversight Forum. The first list was published on [18 November 2025](https://www.eba.europa.eu/publications-and-media/press-releases/european-supervisory-authorities-designate-critical-ict-third-party-providers-under-digital) and names 19 providers: public clouds (Amazon Web Services, Google Cloud, Microsoft, Oracle), IT consultancies and outsourcers (Accenture, Capgemini, IBM, Kyndryl, NTT DATA, Tata Consultancy Services), telecom and data-centre operators (Colt, Deutsche Telekom, Equinix, InterXion, Orange), financial data vendors (Bloomberg, LSEG Data and Risk) and software vendors (FIS, SAP). No WordPress host and no CDN is on it. A WordPress agency is an ordinary ICT third-party service provider, and the obligations of Articles 28 to 30 reach it through the contract with the financial entity.
## DORA Article 28 requirements paragraph by paragraph
[Article 28](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32022R2554#art_28) has ten paragraphs. The first eight bind the financial entity; paragraphs 9 and 10 instruct the European Supervisory Authorities to draft technical standards. What each one means for a WordPress engagement:
**Article 28(1).** The financial entity manages ICT third-party risk as an integral component of ICT risk within its ICT risk-management framework. Point (a) is the sentence procurement quotes most: the entity remains fully responsible for compliance at all times, even after outsourcing the service. Point (b) adds proportionality, based on the nature, scale, complexity and importance of the ICT dependencies. The supplier provides evidence; the responsibility does not move.
**Article 28(2).** A strategy on ICT third-party risk, reviewed regularly, which includes a policy on the use of ICT services supporting critical or important functions. The management body regularly reviews the risks of those arrangements. Microenterprises and entities under the simplified regime of Article 16(1) are outside this paragraph.
**Article 28(3).** [A register of information](/en/dora-register-of-information-wordpress-supplier-fields/) on all contractual arrangements for ICT services, distinguishing those that support critical or important functions. The entity reports at least yearly to the competent authority on the number of new arrangements, the categories of providers and the services provided, makes the full register available on request, and informs the authority in a timely manner about any planned arrangement for a critical or important function. For WordPress: hosting, CDN, WAF, payment gateway, transactional email, AI provider, monitoring tool, backup destination.
**Article 28(4).** Before signing, the entity assesses whether the arrangement supports a critical or important function (point a), whether supervisory conditions for contracting are met (point b), all relevant risks, including the ICT concentration risk of Article 29 (point c), performs due diligence on the suitability of the provider (point d) and assesses conflicts of interest (point e).
**Article 28(5).** Contracts only with providers that comply with appropriate information security standards. For critical or important functions, the entity takes due account of the most up-to-date and highest quality standards. An ISO/IEC 27001 or SOC 2 report from the host counts as evidence only if its scope covers the contracted service.
**Article 28(6).** The entity pre-determines, on a risk-based approach, the frequency and the areas of audits and inspections of the provider, and verifies that the auditors have the skills to assess technically complex services.
**Article 28(7).** The contract must be terminable for a significant breach by the provider, for circumstances that could alter the performance of the functions (including material changes in the provider's situation), for evidenced weaknesses in the provider's ICT risk management, or where the competent authority can no longer effectively supervise the entity because of the arrangement.
**Article 28(8).** Exit strategies for ICT services supporting critical or important functions. The exit must not disrupt business activities, limit regulatory compliance or harm the continuity and quality of services to clients. Exit plans must cover the whole service, be documented, sufficiently tested and reviewed periodically.
**Article 28(9) and (10).** Mandates for technical standards. Paragraph 9 produced [Commission Implementing Regulation (EU) 2024/2956](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32024R2956) with the register templates. Paragraph 10 produced [Commission Delegated Regulation (EU) 2024/1773](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32024R1773) on the content of the policy in paragraph 2.
## What DORA Article 30 clauses belong in WordPress hosting contracts
Article 30(1) requires the full contract, including the service level agreements, to be documented in one written document. Article 30(2) lists the minimum elements of any ICT contract; Article 30(3) adds the elements for contracts supporting critical or important functions. The agency template I ship with hosting and WAF contracts follows the same numbering:
| Clause | Art. 30 | What goes in the WordPress contract |
|---|---|---|
| Complete description of services and subcontracting conditions | 2(a) | Hosting tier, WAF rule set, included plugins, whether subcontracting is permitted and on what conditions |
| Locations of service and data processing | 2(b) | Regions or countries of the data centre and support staff, advance notice of any location change |
| Availability, authenticity, integrity and confidentiality of data | 2(c) | Encryption at rest and in transit, access control, DPA under GDPR Article 28 |
| Access, recovery and return of data | 2(d) | SQL dump and uploads archive in an easily accessible format on insolvency, discontinuation or termination |
| Service level descriptions | 2(e) | Uptime SLA, response times, SLA updates |
| Assistance with ICT incidents | 2(f) | Help at no additional cost or at a cost determined ex ante |
| Cooperation with authorities | 2(g) | Full cooperation with competent and resolution authorities |
| Termination rights and notice periods | 2(h) | Termination grounds and minimum notice period |
| Security awareness and training | 2(i) | Participation in the entity's ICT security programmes (Article 13(6)) |
| Full service levels with measurable targets | 3(a) | RPO, RTO, quantitative and qualitative targets, corrective actions |
| Notice periods and reporting obligations | 3(b) | Notification of any development that could affect delivery of the service |
| Business contingency plans and security measures | 3(c) | Tested continuity plan, the provider's security policies |
| Threat-led penetration testing | 3(d) | Cooperation in TLPT under Articles 26 and 27 |
| Ongoing monitoring and audit | 3(e) | Unrestricted rights of access, inspection and audit for the entity and the authority |
| Exit strategy and transition period | 3(f) | Mandatory transition period while the entity migrates to another provider or in-house |
Article 30(4) adds that the parties should consider standard contractual clauses developed by public authorities. I keep this matrix as a single Markdown file in the engagement folder. Every contract is checked against it before signature.
## DORA register of information for WordPress suppliers
A WordPress engagement for a financial entity touches more ICT third-parties than the procurement team usually anticipates. The register I produce on day one of an engagement:
- **Hosting provider.** The actual data-centre operator, the management plane, the support team. Critical-or-important if the WordPress site is part of customer-facing service delivery.
- **CDN provider.** Cloudflare, Fastly, Akamai. Treats traffic, can see request bodies, terminates TLS. Often classified as critical-or-important.
- **WAF provider.** Sometimes the same entity as the CDN, sometimes separate (Sucuri, Imperva). Inspects payloads. Critical-or-important.
- **Payment gateway plugin.** Stripe, Adyen, mollie, regional gateways. The plugin author is one supplier; the gateway operator is another. Both go into the register.
- **Transactional email provider.** SES, Postmark, SendGrid. Carries password resets, KYC notifications, AML alerts. Often critical-or-important.
- **Monitoring and APM.** New Relic, Datadog, Sentry. Receives stack traces and partial request payloads.
- **Backup destination.** S3, Backblaze, Wasabi. Holds the database and the uploads. Recovery depends on it, so it usually supports a critical or important function.
- **AI provider.** If the WordPress site uses an LLM for any customer-touching function (chat, summarisation), the LLM provider is in scope.
- **Plugin marketplace.** WordPress.org, premium marketplaces. Update channels are part of the [supply chain](/en/wordpress-plugin-supply-chain-attack-audit-guide-2026/) in the sense of the "ICT service supply chain" defined in Article 1(2) of Implementing Regulation 2024/2956.
For each provider the register holds: legal name, contract reference, services description, data flows, criticality classification, location of processing, last due-diligence date, exit strategy reference.
## DORA register of information templates under Implementing Regulation 2024/2956
The register is not a free format. [Implementing Regulation (EU) 2024/2956](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32024R2956) sets the standard templates the entity uses to maintain the register of Article 28(3). Three of its rules change what a WordPress agency has to hand over:
…[truncated, fetch the canonical URL for the full body]…
---
## The future of WordPress: 2026-2030 roadmap and beyond
URL: https://wppoland.com/en/future-of-wordpress-roadmap-2026/
Description: What the WordPress roadmap has actually shipped by September 2026: Notes in 6.9, admin design tokens in 7.1, the Abilities API, and the parts still unbuilt.
Published: Mon Mar 23
Updated: Wed Oct 14
Type: guide
Level: intermediate
Tags: roadmap, data-liberation, gutenberg, collaboration
In 2015, the WordPress conversation was about the REST API.
In September 2026, with 7.1.1 as the stable release, it is about who gets to edit a post at the same time, what an AI agent is allowed to call, and whether you can get your content out of a closed builder without hand copying it.
Two numbers frame the rest of this page. [W3Techs measured WordPress](/en/is-wordpress-dying-market-share-2026/) at **40.2% of all websites** and **58.8% of sites with a known CMS** on 20 September 2026, down from 43.2% of all websites in December 2025. The roadmap is a response to that slope, not a victory lap. The launch of Cloudflare's [EmDash CMS](/en/emdash-cloudflare-open-source-cms-wordpress-successor-2026/) is one visible piece of the same pressure.
What follows is the roadmap checked against core, release by release. Where a feature is real, you get the function name. Where it is still a discussion, it says so.
## Does WordPress have real-time collaboration in 2026
The [wordpress.org roadmap](https://wordpress.org/about/roadmap/) names four Gutenberg phases: Easier Editing, Customization, Collaboration, Multilingual. Phase 3 is the active one, and it has been partially delivered.
**Notes shipped.** Block-level commenting landed in WordPress 6.9 on 2 December 2025, after an experimental run in the Gutenberg plugin. It was renamed from "block comments" to Notes specifically to keep it distinct from the `wp_comments` feature readers use. The first release covers adding, threading, resolving and deleting notes on a whole block, not on a selection inside a block. Viewing or creating one requires the `edit_post` capability, because notes only exist inside the post editor.
WordPress 7.1 extended it rather than replacing it: email notifications for at-mentions inside Notes, shareable revision links, and a fix so Notes no longer leak into comment feed queries. That last one is the sort of detail worth knowing before you file a bug: a custom comment feed built on 6.9 could see note records it never asked for.
**Real-time collaboration did not ship.** It reached core as a beta feature and was then pulled. The 11 March 2026 call for testing asked people to install WordPress 7.0 Beta 1 on a server somebody else could reach, switch on "Enable real-time collaboration" under Settings > Writing, and open the same post from two accounts. On 8 May 2026 it was removed from 7.0, and the reasons given are the useful part: surface area, race conditions, server load, memory efficiency, and bugs that kept surfacing through fuzz testing. It is not enabled in 7.1 either. The sync layer is built on Yjs, and the `core/freeform` (Classic) block is listed as incompatible. For the release-by-release view, see the [WordPress 7.1 roadmap](/en/wordpress-7-1-roadmap-2026/).
**The part that touches your code.** The test call states the rule plainly: blocks sync through their attributes, so most blocks support collaboration by default, and the ones that break are the ones holding editor state somewhere else. That makes the fix concrete rather than speculative. Declare the field in `block.json` with a type, read it from `attributes`, and write it with `setAttributes` on change, instead of parking it in a React `useState` inside `edit()` until blur. That already pays off in undo, revisions and autosave, and it is what decides whether your block survives if collaboration returns.
## When will WordPress core support multilingual sites
The roadmap describes phase 4 in one line: core implementation for multilingual sites. There is no schema, no API, no dev note and no date. The Advanced Administration Handbook still documents multilingual WordPress as something you solve with a plugin.
The sequencing is the interesting part, and it is stated openly in the phase 3 updates: the collaboration infrastructure has to be settled before multilingual can be designed, because both need the same answer to how one logical piece of content maps to several stored versions. Doing them in the other order would mean solving that twice. So phase 4 is not blocked by lack of interest, it is blocked by phase 3 not being finished, and phase 3 is a release cycle behind its own beta.
The claim that core will standardise translation tables is not on any WordPress page. Nobody has published a data model. What you can sensibly do while waiting:
* Keep translation identity in post meta or a taxonomy, not baked into template logic, so it can be remapped when a core model appears.
* Do not store a locale string inside block attributes. A block that hardcodes `de_DE` becomes invalid content the day the translation layer moves.
* Treat WPML and Polylang as long-term dependencies, not stopgaps. They will outlive this roadmap item.
## Can Data Liberation migrate a site to WordPress
The project page at [wordpress.org/data-liberation](https://wordpress.org/data-liberation/) was published on 6 December 2023 and names five phases explicitly:
| Phase | Name | Status |
|---|---|---|
| 1 | Migration Guides | Ongoing |
| 2 | Importing and Exporting Structured Data | Ongoing |
| 3 | Liberating Data From Closed Platforms | Started |
| 4 | Direct WordPress-to-WordPress Synchronization | Future |
| 5 | Content Creation Powerhouse | Future |
That table is the honest version of the story. Phases 4 and 5, which are the ones people mean when they say "one click migration", have not started.
What does exist is narrower and more useful than the slogan. The data liberation agent plugin, open sourced by Studio by WordPress.com, ships purpose-built extractors for named platforms: GoDaddy Websites and Marketing, Hostinger Website Builder, HubSpot, Shopify, Squarespace, Webflow, Weebly and Wix. That is a list of scrapers for specific closed builders, not a universal format. In parallel, the [Playground team](/en/wordpress-playground-guide-2026/) is building new PHP importers as streaming parsers, and the Playground blueprint `importWxr` step can now route through the Data Liberation importer instead of the legacy one.
The practical read for an agency: for the eight platforms on that list, there is real tooling worth testing before you quote a manual content migration. For anything else, the core export format is still WXR, and WXR still does not carry your media files, your plugin settings or your block pattern library. Budget accordingly.
## What changed in the WordPress 7.1 admin
First, a correction that circulates a lot. MP6 did not land in 2012. It was a feature plugin proposed in October 2013 and merged into **WordPress 3.8, released 12 December 2013**. And the admin has not been frozen since: 7.1 changed it.
What shipped in WordPress 7.1 on 19 August 2026 is a design system foundation, described in the [core dev note of 31 July 2026](https://make.wordpress.org/core/2026/07/31/design-system-theming-in-wordpress-7-1/). Two resources are now registered by default:
* A `wp-theme` stylesheet carrying semantic design tokens as CSS custom properties, named on the pattern `--wpds-color-background-surface-neutral-strong`, `--wpds-border-radius-lg`, `--wpds-dimension-padding-2xl`.
* A `wp-theme` script handle exporting a `ThemeProvider` React component from the `@wordpress/theme` package.
`ThemeProvider` takes five props: `color.primary` and `color.background` as seed colours (hex, rgb, rgba or a CSS named colour), `cursor.control`, `cornerRadius` with the presets `none`, `subtle`, `moderate` and `pronounced`, and `isRoot` to apply theming at the document root. The dev note's own example:
```jsx
import { ThemeProvider } from '@wordpress/theme';
import { Card } from '@wordpress/ui';
function Application() {
return (
WordPress is designed for everyone.
);
}
```
Three more 7.1 admin changes that will show up in your issue tracker before they show up in a blog post:
1. The `__next40pxDefaultSize` prop has finished its journey: as of 7.1 it is a no-op, still accepted and ignored rather than removed. Form controls render at 40px regardless, so any admin screen whose spacing was tuned against the old default shifts.
2. `wp_get_tooltip()` and `wp_get_toggletip()` give you accessible tooltips as a core function instead of a per-plugin reimplementation.
3. The post list table row header moved from the checkbox column to the title column, an accessibility fix that changes what a screen reader announces per row.
What 7.1 is not: a replacement for wp-admin, a client dashboard, or a white-label handover. Tokens and a provider component are the opening, not the finished redesign. Plan a client-facing admin on the assumption that you still own that layer.
## What to learn as a WordPress developer in 2026
Three things are concrete enough to justify study time, and one common claim is not.
**The [Abilities API](/en/wordpress-abilities-api/), shipped in 6.9.** This is the registry that lets plugins, core and external agents discover what a site can do. You hook `wp_abilities_api_init`, call `wp_register_ability()` with a namespaced name such as `my-plugin/my-ability`, and supply an execute callback plus a permission callback with typed input and output schemas. Abilities are private by default: `show_in_rest` is `false` unless you set it, and only then do they appear under the `wp-abilities/v1` REST namespace at `/abilities`, `/abilities/{name}` and `/abilities/{name}/run`. WordPress 7.0 added the client-side counterpart. Core registers three read-only abilities of its own in `wp-includes/abilities.php`, all marked `@since 6.9.0`: `core/get-site-info`, `core/get-user-info` and `core/get-environment-info`. Check the current names in trunk before you code against them, because earlier drafts of this set carried different ones. If you want a single API to learn from this roadmap, it is this one.
**The AI Client, merged into 7.0.** The merge proposal of 3 February 2026 described provider-agnostic infrastructure: a PHP prompt builder, a provider and model abstraction, credential storage shared across plugins, REST endpoints and a JavaScript API, and filters to allow or disallow prompt configurations. It was accepted on condition that the Connectors flow shipped alongside it, and 7.0 ships both. The SDK itself sits in `wp-includes/php-ai-client/`, with its vendored dependencies scoped to `WordPress\AiClientDependencies\*`; the WordPress glue on top of it (the prompt builder, the ability function resolver) sits in `wp-includes/ai-client/`. The Connectors screen has its own admin file, `wp-admin/options-connectors.php`, marked `@since 7.0.0` in trunk. The practical consequence: you no longer bundle an API key and an HTTP client in every plugin that wants a model. What core does not include is page generation from a description, content optimisation or predictive updates. That is still plugin territory.
**React, specifically the editor data layer.** JSX is the easy half. The half that decides whether your block survives collaboration and the new admin is `@wordpress/data`, attribute schemas in `block.json`, and now `@wordpress/ui` and `@wordpress/theme` for admin surfaces that follow core tokens rather than fighting them.
**And the claim to drop:** there is no "canonical API" that makes headless WordPress easy. There is the REST API, there is WPGraphQL as a plugin, and there is now the Abilities API for agent-facing calls. They are three different contracts with three different maintenance stories, and choosing between them is still an architecture decision you have to make yourself.
## How the roadmap affects WordPress plugins
The roadmap does not only touch core; it changes how plugins lean on the platform. It helps to separate what already requires action from what is still conditional.
### Do plugins need changes for real-time collaboration
…[truncated, fetch the canonical URL for the full body]…
---
## ROI of headless vs. traditional WordPress: the 2026 financial analysis
URL: https://wppoland.com/en/headless-wordpress-vs-traditional-roi-analysis-2026/
Description: Headless is mature, but is it profitable? What Next.js or Astro on top of WordPress actually costs against a PHP theme, priced from what WordPress core does and does not hand you over the API.
Published: Fri Dec 05
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: headless-wordpress, roi, nextjs, total-cost-of-ownership, business-strategy
Headless WordPress in 2026 is a settled engineering choice with an unsettled business case. The question is not whether Next.js or Astro can render WordPress content well, because both can. The question is what you are buying with the second codebase, and the honest answer is written in WordPress core: a decoupled front end pays cash for every convenience the monolith gets for nothing.
Most comparisons price this as a multiplier. Headless costs two or three times a theme build, which is a number nobody can check and everyone quotes. The useful version is a list of the specific things core stops doing for you the moment the front end is no longer a PHP theme, because those are the line items your agency will actually invoice. That list is short, it is knowable in advance, and it is the whole of year zero.
This article is the decision itself. If you want the long-form financial model with the four-year cashflow, the [headless vs monolithic WordPress enterprise TCO guide](/en/headless-vs-monolith-wordpress-2026-tco-guide/) runs the arithmetic, and the individual cost columns (hosting, full rebuild versus incremental regeneration, migration) are priced in [the economics of headless WordPress](/en/economics-of-headless-wordpress-2026/).
---
## How much does a headless WordPress site cost to build
Start with what the API actually gives you, because the gap between that and a working site is the build.
Core ships [REST, not GraphQL](/en/wordpress-rest-api-vs-graphql-2026-en/). `REST_API_VERSION` is defined as `2.0` in `wp-includes/rest-api.php`, routes have been registrable since 4.4, and the `wp/v2` namespace is what a stock WordPress 7.1.3 install answers on. GraphQL is [WPGraphQL](https://wordpress.org/plugins/wp-graphql/), which became a [canonical plugin in October 2024](https://wordpress.org/news/2024/10/wpgraphql/) with Automattic backing after Jason Bahl moved there. Canonical is a strong signal about maintenance. It is not core. You install it, you version it, and you patch it, and any agency quote that says "WordPress talks GraphQL" has skipped a dependency.
Then the specific things you will be paying a JavaScript developer to write:
**The navigation.** `wp/v2/menus` exists, `WP_REST_Menus_Controller` has been in core since 5.9, and it will not answer an anonymous request. Its `check_has_read_only_access()` returns true only for `edit_theme_options`, `edit_posts`, or an editing capability on some public post type, unless someone flips the `rest_menu_read_access` filter. So the front end either authenticates on every build, or you write a small public endpoint of your own and own its cache. This is the thing every headless post means by "you have to build the menu" without ever saying why.
**Preview.** Drafts are not public data and core is right about that. Reading unpublished content means an authenticated request, which means application passwords (core since 5.6) or a JWT plugin, plus a preview route on the front end, plus a way for an editor to click Preview in wp-admin and land on it. Three moving parts, none of them optional, none of them in any framework starter.
**Block CSS.** The bare `should_load_separate_core_block_assets` filter still defaults to `false`, but core no longer leaves it there: `_add_default_theme_supports()` flips it to true for block themes, and `wp_load_classic_theme_block_styles_on_demand()` (since 6.9) does the same for classic themes through the template output buffer. So a stock 7.x site prints the styles of the blocks actually used on the page, from `wp_head()` and `wp_footer()`. Your front end calls neither. You either import that stylesheet and accept it, or you restyle every core block your editors can insert. Columns, gallery, quote, table, buttons, separator, cover. That is not a hard job. It is a job somebody has to scope, and it never appears in the estimate.
**Forms, and everything else with behaviour.** Covered in section four, because it is the cost that keeps arriving after launch rather than before it.
None of this makes headless a bad decision. It makes year zero an easy call: if you are not already funding a front end team, the monolith wins on build cost every time, and the reason is that WordPress core is doing unpaid work for you that you would otherwise commission.
---
## Headless WordPress maintenance and security costs
This is where the usual argument goes wrong, so it is worth correcting the security claim directly rather than repeating it.
### Security
The common version says a decoupled architecture separates the attack surface, so a vulnerable form plugin can no longer reach the database. That is false as stated. The plugin still runs inside the same WordPress install, against the same database, with the same capabilities. Decoupling moved the rendering. It did not move the vulnerability.
What decoupling genuinely buys is an option the monolith does not have: because the public site is no longer WordPress, you can put the WordPress origin behind an IP allowlist, a VPN, or a private network, and expose only the front end and whatever endpoints it needs. A monolith cannot do that, because the thing you would be hiding is the thing visitors are looking at. That is a real reduction in exposure, and it costs network engineering rather than architecture. If your headless WordPress is sitting on a public hostname with `wp-admin` reachable from anywhere, and most are, you paid for the architecture and skipped the benefit.
The second half is less flattering. You now patch two runtimes on two cadences. WordPress ships minor releases with automatic background updates, and the 7.1 branch took three of them in three weeks, [7.1.1 on 17 September, 7.1.2 on 22 September and 7.1.3 on 6 October 2026](https://wordpress.org/news/2026/10/wordpress-7-1-3-maintenance-and-security-release/), without anyone on your team doing anything. Nothing in your JavaScript dependency tree behaves like that. Next.js 16.3.5 published on 11 September 2026 and Astro 7.3.3 on 16 September 2026, and Node's LTS lines expire on their own schedule (Node 18 Hydrogen took its final release on 27 March 2025). A monolith has one upgrade treadmill. Headless has two, and the faster one is the one nobody budgeted for.
### Redesigns
The agility argument is the strongest real point in favour of headless, and it is usually overstated by one word. "The front end is pure React or Vue" is not accurate for Astro, which ships no client JavaScript by default and is a common choice precisely because of that. The accurate claim is narrower and better: when the content model holds still, a decoupled front end can be rebuilt without touching WordPress at all, and that is genuinely cheaper than fighting a page builder's markup or a theme that grew ten years of conditional templates.
The caveat is the same sentence in reverse. When the redesign changes what content exists, not just how it looks, you are now editing two repositories, coordinating two deploys, and writing a migration that has to land in both. Half of the redesigns that get sold as "front end only" turn out to need one new field, and that is the day the second repository stops being free.
For public-sector readers in the United States there is a date attached to this. The DOJ final rule under ADA Title II, published in the Federal Register on 24 April 2024, sets WCAG 2.1 Level AA as the standard for state and local government web content, with compliance dates in 2027 and 2028 depending on population. If a rebuild is happening anyway, that deadline is the cheapest moment to absorb the accessibility work, in either architecture.
---
## Is headless WordPress faster and does it convert better
The performance case for headless was strong in 2020. It has been narrowed from both ends since, and both changes are datable.
The metric changed first. INP replaced FID as a Core Web Vital on [12 March 2024](https://web.dev/blog/inp-cwv-march-12). FID measured how long the browser took to acknowledge the first interaction, which was close to free for any server-rendered page. [INP](/en/interaction-to-next-paint-inp-optimization-guide-2026/) measures the latency of interactions across the whole visit, which is a direct measurement of main-thread work. A hydration-heavy React front end is, by construction, the architecture that ships the most main-thread work. Server-side rendering gets a headless build level with a PHP theme on the paint metrics. It does not put it ahead, and on INP it can put it behind. A build that treats JavaScript as opt-in, which is what Astro islands or React Server Components are for, is the version of headless that actually wins this argument.
Core moved at the other end. Speculative loading landed in WordPress 6.8, documented in the [dev note of 6 March 2025](https://make.wordpress.org/core/2025/03/06/speculative-loading-in-6-8/) and living in `wp-includes/speculative-loading.php`. On a site with pretty permalinks, for logged out visitors, core now emits [Speculation Rules](/en/speculation-rules-api-wordpress-woocommerce-performance-2026-en/) by default, and 7.1 added `WP_SPECULATIVE_LOADING_DEFAULT_MODE` and `WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS` so a host or a site can set the policy. The defaults are mode `prefetch` with `conservative` eagerness, so core fetches the next document when the pointer goes down on a link. That is a prefetch, not a prerender, so it removes document latency rather than rendering the page ahead of time. It still takes away part of what a JavaScript router used to be the visible reason for, on a stock theme with no router at all.
That leaves the business case, which is arithmetic you can do on your own numbers rather than on anyone's benchmark. Take your current conversion rate, add one percentage point, multiply by a year of orders and average order value, and compare the result against [a headless build](/en/nextjs-astro-migration/) plus a standing front end engineer. On a high-volume store the lift clears the cost inside the first year and the decision is obvious. On a catalogue site doing a modest order volume, it never clears it, and the honest recommendation is the one most agencies skip: spend the same money on caching, images and the checkout, and stay on the theme.
---
## Why WordPress plugins break on a headless front end
This is the cost that arrives after launch, it is the one that ends the marketing team's goodwill, and it has a precise mechanism worth knowing before you sign anything.
Look at what the REST API actually returns. In `WP_REST_Posts_Controller::prepare_item_for_response()`, the content field is built like this:
```php
$data['content']['rendered'] = post_password_required( $post )
? ''
: apply_filters( 'the_content', $post->post_content );
```
So `the_content` runs. Shortcodes expand, block render callbacks execute, and the markup a plugin generates does come back over the API. What does not come back is everything that plugin registered on `wp_enqueue_scripts`, printed in `wp_head()`, or hooked to `wp_footer()`, because your front end never calls any of them.
The result is specific and it looks like a bug rather than a design decision. A slider shortcode returns a `div` with the right classes, no stylesheet, and no initialiser, so it renders as a stack of unstyled images. A pricing table returns a table with no CSS. A form returns its fields and no submit handler. The editor sees it working in wp-admin, because wp-admin is still a monolith, and sees it broken in production, and files a ticket against the front end team.
So the tax is not "find a React library for a wheel of fortune". It is that every plugin your marketing team installs arrives half-delivered, the half that arrives is the half that looks fine in the CMS, and the missing half is a developer ticket every single time. Two plugins a year is noise. Two a month is a retainer.
…[truncated, fetch the canonical URL for the full body]…
---
## How to display custom fields in WordPress: A developer's blueprint
URL: https://wppoland.com/en/how-to-display-custom-fields-wordpress/
Description: Learn how to query, display, and scale WordPress custom fields. Covers get_post_meta, Gutenberg Block Bindings, custom DB tables, and REST API.
Published: Wed Jun 25
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: custom-fields, postmeta, database-optimization, gutenberg
In WordPress, custom fields are the foundation of structured content. By associating custom metadata with posts, pages, or custom post types, you can extend the platform from a blogging engine into a content management system that handles directory listings, real estate catalogs, and e-commerce inventories. Managing custom fields at scale requires an understanding of the database schema, sanitization on save, escaping on output, and query performance. This guide covers reading and writing post meta, the deprecated `the_meta()`, ACF, block themes and Block Bindings, query scaling, custom data tables, and REST endpoints.
Learn more about our [professional WordPress development services](/en/wordpress-developer/).
## How wp_postmeta stores and serializes custom fields
WordPress uses the **Entity-Attribute-Value (EAV)** model to store post metadata. In this schema, all metadata is saved in a single table: `wp_postmeta`.
### wp_postmeta table columns
The table contains four fields:
- `meta_id`: The primary key (auto-increment).
- `post_id`: The foreign key referencing the post table.
- `meta_key`: The string identifier for the custom field.
- `meta_value`: The text field storing the data.
### How PHP serialization works in wp_postmeta
When you store complex datasets (like arrays or objects) inside a custom field, WordPress serializes the data using PHP's `maybe_serialize()` function:
```
Array in PHP ---> serialize() ---> String stored in wp_postmeta (meta_value)
|
[ Query execution ]
|
Read string ---> maybe_unserialize() ---> Reconstruct PHP Array
```
A typical serialized PHP array looks like this:
`a:2:{i:0;s:5:"Apple";i:1;s:6:"Banana";}`
In this structure:
- **`a:2`**: Defines an **Array** containing exactly 2 elements.
- **`i:0`**: Represents an **Integer** index key of `0`.
- **`s:5:"Apple"`**: Represents a **String** of length `5` containing `"Apple"`.
- **`i:1`**: Represents an **Integer** index key of `1`.
- **`s:6:"Banana"`**: Represents a **String** of length `6` containing `"Banana"`.
Other common serialization type markers include:
- **`b:1`** or **`b:0`**: Represents a **Boolean** value (`true` or `false`).
- **`d:45.99`**: Represents a **Double** (float) decimal value.
- **`O:8:"stdClass":1:{...}`**: Represents an instantiated **Object** of class length `8` (`"stdClass"`) containing `1` property.
- **`N;`**: Represents a **Null** value.
In this format, type and string-length markers are explicitly defined (e.g., `s:6` indicates a string of exactly 6 characters: `"Banana"`). Because serialized data is stored as raw text, you cannot query individual array elements inside `wp_postmeta` efficiently using SQL. Queries that search inside serialized data need slow `LIKE` string matching, which scans the whole table.
Executing direct database search-and-replace SQL commands (for example, renaming a domain or URL path strings) will corrupt serialized arrays. If you replace `http://olddomain.com` (20 characters) with `https://newdomain.com` (21 characters) inside a serialized string without updating the string-length indicator (`s:20` to `s:21`), `unserialize()` fails and returns `false`, breaking the field. Always use [serialization-aware migration tools](/en/how-to-update-urls-in-the-wordpress-database-when-the-site-is-moved-to-a-new-domain/) (such as WP-CLI's `wp search-replace`) to modify metadata safely.
## How to use get_post_meta() in WordPress
The core function for fetching metadata is `get_post_meta()`:
```php
// Single value
$value = get_post_meta( $post_id, 'meta_key', true );
// Array of every value stored under the same key
$values = get_post_meta( $post_id, 'meta_key', false );
// All metadata of the post
$all_meta = get_post_meta( $post_id );
```
### get_post_meta() parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `$post_id` | int | Post ID |
| `$key` | string | Meta key name; empty returns all keys |
| `$single` | bool | true = one value, false = array (default) |
The third argument is a classic source of bugs. With `$single = true`, missing meta returns an empty string. With `false`, you always get an array, even with one element. Code that does `if ( $value )` behaves differently on a string and on an array.
Prefix keys with the project name (`wppoland_event_city`, `wppoland_sku`). Avoid generic names like `price`, `date` or `city` without a namespace: sooner or later WooCommerce, an SEO plugin or an events plugin claims the same name.
### What get_post_meta() returns for numbers, booleans and missing keys
Everything stored in `wp_postmeta` comes back as text unless it was serialized. The docblock of [`get_post_meta()` in WordPress core](https://github.com/WordPress/wordpress-develop/blob/7.1/src/wp-includes/post.php) lists the conversions: `false` is returned as an empty string, `true` as `'1'`, and integers and floats as strings. Arrays and objects keep their type because WordPress serializes them on save and unserializes them on read.
Edge cases from the same docblock: an invalid post ID (non-numeric, zero or negative) returns `false`. A valid post ID without the key returns `''` with `$single = true` and an empty array with `$single = false`. A `default` registered with `register_meta()` (available since WordPress 5.5) is never stored, so it comes back in the registered type.
```php
update_post_meta( $post_id, 'wppoland_stock', 0 );
$stock = get_post_meta( $post_id, 'wppoland_stock', true );
var_dump( $stock ); // string(1) "0"
var_dump( 0 === $stock ); // bool(false)
$stock = (int) $stock; // cast before arithmetic or strict comparison
```
A strict comparison against an integer never matches a stored number. Cast with `(int)`, `(float)` or `absint()` before arithmetic, and compare with `'0' === $value` when zero is a meaningful value.
### How to check if a custom field exists in WordPress
An empty string from `get_post_meta()` does not tell you whether the key was saved as empty or never saved. `metadata_exists()` (since WordPress 3.3) answers that question and reads the same meta cache, so it adds no query when the post meta is already loaded:
```php
if ( metadata_exists( 'post', $post_id, 'wppoland_sku' ) ) {
$sku = get_post_meta( $post_id, 'wppoland_sku', true );
// The key exists, even if its value is an empty string.
}
```
The function runs the `get_post_metadata` filter first, as [its source in meta.php](https://github.com/WordPress/wordpress-develop/blob/7.1/src/wp-includes/meta.php) shows, so a plugin that short-circuits meta reads can change the answer. Use it when "empty" and "not set" need different handling, for example when an editor clears a field on purpose and the template should not fall back to a default.
## get_post_meta() versus the_meta()
`the_meta()` is deprecated since WordPress 6.0.2. Calling it triggers `_deprecated_function()` with `get_post_meta()` as the replacement, which shows a notice when `WP_DEBUG` is on. It prints a `
` list of every custom field of the current post in the loop. It escapes keys and values with `esc_html()` and skips protected keys (those starting with an underscore), but it takes no key argument and gives you no control over the markup or over which keys appear.
```php
// Deprecated: prints every non-protected key of the current post
the_meta();
// Preferred: read one key, decide the HTML, escape for the context
$venue = get_post_meta( get_the_ID(), 'wppoland_venue', true );
if ( $venue ) {
echo '
' . esc_html( $venue ) . '
';
}
```
Keep `the_meta()` only in old themes that still call it, or in a debug view on staging. In production, archive templates, single CPT templates and card partials should read explicit keys. That way internal meta (ERP IDs, integration tokens, editorial notes) saved without an underscore prefix never ends up on the public page. The source is in [post-template.php](https://github.com/WordPress/wordpress-develop/blob/7.1/src/wp-includes/post-template.php).
## How to display a custom field in a theme
### How to show a custom field inside the loop
```php
```
### How to get a custom field outside the loop
```php
$post_id = 42;
$author_name = get_post_meta( $post_id, 'custom_author', true );
echo esc_html( $author_name );
```
On CPT archives, do not rely on the global `$post` outside the loop: take the ID from `get_queried_object_id()` or from an argument passed to the partial. It is a frequent cause of "the meta of the previous post shows up" in listings with a shared sidebar.
For reusable partials (an event card, a property sheet), pass the ID as a function argument. Classic templates and block theme patterns often live side by side, and an explicit ID avoids surprises when the same partial runs inside a Query Loop.
### How to escape custom field output
Always escape values upon rendering to prevent Cross-Site Scripting (XSS) injection:
```php
declare(strict_types=1);
namespace WPPoland\Meta;
/**
* Renders custom event details safely.
*/
function render_event_metadata(): void {
$post_id = get_the_ID();
$price = get_post_meta( $post_id, 'event_price', true );
$location = get_post_meta( $post_id, 'event_location', true );
$link = get_post_meta( $post_id, 'event_ticket_url', true );
if ( ! empty( $price ) ) {
printf(
'
',
esc_url( $link )
);
}
}
```
The context picks the function. Visible text: `esc_html`. Attribute: `esc_attr`. URL: `esc_url`. Trusted editorial HTML: `wp_kses_post`. Meta filled in by a front-end form (a quote request, a job application, a CV upload) is always untrusted, even on an internal site.
On the save side, use `sanitize_text_field`, `absint`, `sanitize_email`, or your own callback in `register_post_meta`. Do not assume the meta box "only exists in the admin": the REST API and bulk edit write meta too.
## How to save custom fields in WordPress
### update_post_meta(), add_post_meta() and delete_post_meta()
```php
update_post_meta( $post_id, 'meta_key', 'meta_value' );
add_post_meta( $post_id, 'meta_key', 'meta_value' );
delete_post_meta( $post_id, 'meta_key' );
```
`update_post_meta` creates the row if it does not exist. `add_post_meta` with the default `$unique = false` allows several rows under the same key, which should be a deliberate decision and not a side effect of a CSV import.
### How to add a meta box with add_meta_box()
```php
function wppoland_add_sku_metabox() {
add_meta_box(
'product_sku_box',
'Catalogue code',
'wppoland_render_sku_metabox',
'post',
'side'
);
}
add_action( 'add_meta_boxes', 'wppoland_add_sku_metabox' );
function wppoland_render_sku_metabox( $post ) {
wp_nonce_field( 'save_sku', 'sku_nonce' );
$sku = get_post_meta( $post->ID, 'wppoland_sku', true );
?>
Network Setup**.
4. On the Network Setup screen choose sub-domains or sub-directories and install.
5. Add the lines WordPress generates to `wp-config.php` and `.htaccess`, then log in again.
The [network administration handbook](https://developer.wordpress.org/advanced-administration/multisite/administration/) adds a point that saves time on modern stacks: WordPress can generate Apache and IIS rewrite rules, "but it cannot generate Nginx configuration". On Nginx you write the Multisite rules yourself from the documented examples.
On the command line, WP-CLI does the same conversion with `wp core multisite-convert`, which creates the Multisite tables and adds the constants to `wp-config.php`. Add `--subdomains` for a domain-based network.
### Subdomain or subdirectory multisite
The choice decides how site URLs look and what the server must support. The [prepare-network guide](https://developer.wordpress.org/advanced-administration/multisite/prepare-network/) lists the rules:
| | Sub-domain install | Sub-directory install |
|:---|:---|:---|
| **URL pattern** | `site1.example.com` | `example.com/site1` |
| **Server needs** | DNS and server mapping for each name (wildcard, virtual hosts or panel-added subdomains) | Working pretty permalinks |
| **Sites created on demand** | Needs wildcard subdomains | Works without extra DNS |
| **Not available when** | WordPress URL contains a path, or is `localhost` or an IP address | The existing site has been set up for more than a month |
| **Main site side effect** | None | Posts on the main site get a `/blog/` prefix |
A network cannot be created at all when the WordPress address uses a port other than 80 or 443. Switching type later is possible but heavy: changing `SUBDOMAIN_INSTALL` and the rewrite rules "does not migrate existing site addresses or configure DNS, virtual hosts, TLS certificates, redirects, and cookies". For a 100-site plan, decide once and test on staging.
## WordPress multi-network and database sharding
For organizations that have outgrown a single network, **multi-network** setups group sites into several networks inside one installation. Core supports the data model, but the management UI comes from plugins such as [WP Multi Network](https://wordpress.org/plugins/wp-multi-network/).
### Regional grouping
You can have a network for Europe, a network for the Americas and a network for APAC, all within one installation. Each network has its own settings and its own list of network-activated plugins.
| Network | Example sites |
|:---|:---|
| **Europe** | site-spain.es, site-germany.de, site-france.fr, site-italy.it |
| **Americas** | site-usa.com, site-mexico.mx, site-brazil.com.br, site-canada.ca |
| **APAC** | site-japan.jp, site-australia.com.au, site-india.in |
In WP-CLI every network has a numeric ID. `wp site create --network_id=2` adds a site to the second network, and `wp site list --network=2` lists only its sites.
### Sharding with a db.php drop-in
To prevent the database from becoming a bottleneck, a `db.php` drop-in such as [HyperDB](https://wordpress.org/plugins/hyperdb/) distributes site tables across multiple database servers and sends reads to replicas.
**Sharding strategies:**
- **By region**: each regional network has its own database server
- **By volume**: high-traffic sites get dedicated servers
- **Hybrid**: a mix of both, based on measured load
### What is shared and what is per site
| Data | Where it lives |
|:---|:---|
| Users | Common tables for the whole network; a user needs a role on a site to access it |
| Network settings | Network options, stored by default in `wp_sitemeta` and read with `get_site_option()` |
| Posts, pages, site options | Separate tables for each site |
| Uploads | Main site in `/wp-content/uploads/`, other sites in `/wp-content/uploads/sites//` |
| Plugin and theme code | One copy on disk for all sites |
The [administration handbook](https://developer.wordpress.org/advanced-administration/multisite/administration/) also adds that network activating a plugin "does not change where the plugin stores its data". Whether settings are network-wide or per site depends on how each plugin is written, so check that before you approve a plugin for the network.
## SSO and user roles in WordPress Multisite
With 100 sites, you likely have hundreds of users. You cannot manage them manually.
### SSO integration
Integrate your Multisite with an identity provider such as Okta or Microsoft Entra ID (formerly Azure AD). When an employee leaves the company, their access to all 100 websites is revoked in one place. That closes the gap of orphaned accounts, a common way into old sites.
### Super Admin controls
[Define which sites can activate which plugins](/en/advanced-wordpress-multisite-governance-2026/). A regional marketing team shouldn't be allowed to install a risky plugin that could break the whole network. WordPress enforces part of this by default: in a network, site admins "cannot install new themes or plugins and cannot edit the profiles of users on their site". Only the Super Admin can.
**Recommended role hierarchy:**
| Role | Scope | Permissions |
|:---|:---|:---|
| **Super Admin** | Whole installation | Full control: plugins, themes, sites, users |
| **Administrator** | One site | Site settings, adding users if the network allows it |
| **Editor** | One site | Publishing and editing all content |
| **Author** | One site | Publishing own posts |
| **Contributor** | One site | Writing drafts for review |
Custom roles, such as an SEO manager with access to meta fields only, are possible with a role plugin or code, and they are defined per site.
### Plugin access for site admins
By default site admins do not see the Plugins screen at all. The [Network Settings](https://developer.wordpress.org/advanced-administration/multisite/admin/settings/) screen has a Menu Settings option that enables it. Even then, site admins can only activate or deactivate plugins the Super Admin installed; network-activated plugins show as "Network Active" with no option to deactivate.
A plugin can also declare `Network: true` in its header, which makes it network-only.
### Themes per site
Network enabling a theme does not switch any site to it. It only makes the theme available. A theme can also be enabled for a single site from the Edit Site screen, Themes tab. New sites get the newest default theme unless you set `WP_DEFAULT_THEME` in `wp-config.php` to the folder name of your corporate theme, which saves a manual step on every new site.
## How to syndicate content across a WordPress multisite network
Sites in a network do not share content by default. The handbook compares a network to "a mini version of WordPress.com" and points to plugins for sharing. Plan syndication as a feature you add, not one you get.
### Publishing one announcement on every site
Broadcast plugins let you post a global corporate announcement once and copy it to all 100 sites. Typical candidates are press releases, product updates, security and compliance notices and seasonal campaigns.
### Global search
Use Elasticsearch (via [ElasticPress](https://wordpress.org/plugins/elasticpress/)) to provide a search bar that can find content across every site in the entire network. Native WordPress search queries the tables of one site.
### Reading content from other sites
Every site exposes its own REST API, so a hub site can pull posts from regional sites over HTTP. Inside PHP, `switch_to_blog()` and `restore_current_blog()` let code read another site's tables directly, which is how most syndication plugins work.
## WordPress Multisite performance checklist for 100+ sites
| Component | Recommendation | Why it matters |
| :--- | :--- | :--- |
| **Object cache** | Redis (cluster at scale) | Prevents database overload from shared queries |
| **CDN** | Edge caching | Speeds up TTFB for global sites |
| **Storage** | S3-compatible | Offloads media files to prevent server bloat |
| **Search** | Elasticsearch | Native WP search does not search across sites |
| **Database** | MySQL 8+ with read replicas | Spreads read load across servers |
| **Monitoring** | APM with per-site data | Shows which site causes load |
### Redis for Multisite
- **Keys per site**: the object cache keys data by site ID, so sites do not overwrite each other's cache
- **Global groups**: users and network options are cached once for the network
- **Memory alerts**: one large site can fill the shared cache
### Upload limits per site
…[truncated, fetch the canonical URL for the full body]…
---
## Programmatic SEO & topical authority: How to dominate a niche in 2026
URL: https://wppoland.com/en/programmatic-seo-topical-authority-guide/
Description: Stop chasing high-competition keywords. Learn how to use Programmatic SEO to generate thousands of high-value pages and build Topical Authority.
Published: Sat Mar 30
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: programmatic-seo, topical-authority, keyword-research, content-strategy
In 2013 many sites still tried to rank by repeating the same keyword in the footer. In 2026 the dominant signal is different: topical authority. Google does not need one isolated article about a term; it needs to see whether the domain covers a subject end to end, with pages that answer real intents and link to each other.
Learn more about our [SEO and GEO optimization services](/en/geo-llmo-optimization/) at WPPoland.
If all you have is a generic text about a WordPress security audit, you will struggle against a site that documents malware, HTTP headers, SSL, 2FA, backups and incident response in the same cluster. Programmatic SEO fits here as a coverage tool, not as a shortcut to spam.
## What is programmatic SEO and why it matters
Programmatic SEO is the practice of generating targeted, database-driven landing pages at scale to capture search traffic for structured keyword permutations. Instead of manually writing individual posts, you develop a structured dataset, map it to a programmatic page template, and configure your content management system (CMS) to render hundreds or thousands of high-quality pages.
### Programmatic SEO examples from Zillow and TripAdvisor
- **Zillow**: Automatically generates custom search landing pages for every zip code, neighborhood, and city in the US by pulling public property transaction registries and local demographic data.
- **TripAdvisor**: Creates dedicated directory pages for every hotel, restaurant, and municipal attraction globally, utilizing user-generated reviews and geo-coordinates.
- **Nomad List**: Integrates cost of living indices, internet speeds, safety ratings, and temperature metrics to produce thousands of comparative pages (e.g., "Best Cities for Digital Nomads in Europe with Warm Weather").
These platforms do not rely on copywriters drafting individual pages. Instead, they design data ingestion engines and dynamic front-end templates that display unique, structured information. This approach is highly search-engine friendly because it addresses highly specific search intents that traditional, generic articles miss entirely.
## Programmatic SEO vs thin content and spam
Google's algorithms, powered by spam filters like SpamBrain, are trained to detect and penalize low-value mass-generated pages. To ensure your programmatic pages are indexed and rank consistently, you must understand the distinction between valuable pSEO and penalizable thin content:
| Attribute | Programmatic SEO (Value-First) | Thin Content / Spam (Auto-Generated) |
| :--- | :--- | :--- |
| **Data Quality** | Employs verified databases, API integrations, and original analysis. | Replaces synonym keywords in boilerplate sentences. |
| **User Experience** | Includes charts, filtering tools, tables, and maps. | Consists of unformatted wall-of-text blocks. |
| **Search Intent** | Directly resolves long-tail queries (e.g., pricing, features). | Manipulates rankings without answering the search query. |
| **Indexation Health** | Highly crawlable, low index-to-crawl ratio, low error rate. | High rate of "Discovered - currently not indexed" in GSC. |
### What unique value each programmatic page needs
To maintain a high quality threshold, each generated page must offer unique value. If you are generating comparison pages for tools, do not simply output "Tool A is good for dentists." Instead, compute and show an aggregated score, generate interactive comparison matrixes, display custom SVG charts of performance metrics, or embed localized pricing plans.
## What Google's scaled content abuse policy says
On 5 March 2024 Google announced a new policy called scaled content abuse on the [Search Central blog](https://developers.google.com/search/blog/2024/03/core-update-spam-policies). The announcement says the policy "builds on our previous spam policy about automatically-generated content". For pSEO the change is in the test: it no longer matters whether a machine or a person wrote the pages, it matters why they exist.
The current definition in [Google Search's spam policies](https://developers.google.com/search/docs/essentials/spam-policies) reads: "Scaled content abuse is when many pages are generated for the primary purpose of manipulating search rankings and not helping users." The policy adds that it typically targets unoriginal content "that provides little to no value to users, no matter how it's created."
Google's examples read like a list of typical failures in a programmatic project:
| Example from the policy | How it shows up in a pSEO project |
| :--- | :--- |
| Using generative AI or similar tools to generate many pages without adding value | One prompt per CSV row, published straight away |
| Scraping feeds, search results or other content, including through synonymizing, translating or other obfuscation | Copying listings from someone else's directory and running them through a paraphraser |
| Stitching or combining content from different web pages without adding value | A template that pastes descriptions from three different sources |
| Creating multiple sites to hide the scaled nature of the content | Spreading the same landings across satellite domains |
| Creating many pages that make little or no sense to a reader but contain search keywords | Paragraphs generated only to fit the long-tail keyword |
The same page defines doorway abuse: sites or pages created to rank for specific, similar queries that lead users to intermediate pages less useful than the final destination. Its examples include "Having multiple domain names or pages targeted at specific regions or cities that funnel users to one page" and "Creating substantially similar pages that are closer to search results than a clearly defined, browseable hierarchy." The second example describes an unstructured programmatic listing precisely, and it is why the hub and spoke topology below is part of the requirement rather than decoration.
## Can you use generative AI to create programmatic pages
Yes, under conditions Google puts in writing. Its [guidance on using generative AI content on your website](https://developers.google.com/search/docs/fundamentals/using-gen-ai-content) repeats that using generative AI tools to produce many pages without adding value for users may violate the scaled content abuse policy. The guidance does not ban the tool; it flags the empty result.
Two points from that guidance change the workflow of a programmatic project:
- Fact checking before publishing. The guidance says "It is critical to manually factcheck and review all AI-generated content for accuracy and trustworthiness before publishing," and gives the reason: generative models "don't retrieve facts, but predict a likely sequence of words based on their training data." In pSEO this means every generated sentence must come from the dataset columns, not from what the model believes it knows about a city or a product.
- Metadata is in scope too. The guidance covers `` elements, meta descriptions and structured data, because they can appear in search results. JSON-LD carrying a price the model invented is an error visible in the SERP, not an internal detail.
The working rule that follows is simple: AI may draft from verified fields (for example, turning `tool_price_monthly` and `tool_integrations` into a readable sentence) and a person reviews a sample of every batch. If the model has to invent something to complete the page, that record is not ready to publish.
## How to structure topical authority with hub and spoke
[Topical Authority](/en/topical-authority-wordpress-content-moat-2026-en/) is achieved when a search engine recognizes your domain as a comprehensive authority on a specific subject cluster. Under the **Hub and Spoke** (or Topic Cluster) model, you structure your programmatic pages around central pillar resources:
```
+-----------------------------+
| Hub Page (Pillar Post) |
| "Enterprise CRM Guide" |
+--------------+--------------+
|
+-----------------------+-----------------------+
| | |
+--------v---------+ +--------v---------+ +--------v---------+
| Spoke Page | | Spoke Page | | Spoke Page |
| "CRM for | | "CRM for | | "CRM for |
| Marketing" | | Dentists" | | Online Stores" |
+------------------+ +------------------+ +------------------+
```
### Internal linking rules for hub and spoke pages
1. **Bidirectional Linking**: The Hub page must link to all Spoke pages, and every Spoke page must contain a prominent link back to the parent Hub page.
2. **Contextual Cross-Linking**: Spoke pages within the same category must link to each other (e.g., "Compare HubSpot with other tools in this category") to distribute PageRank and assist crawlers in mapping semantic relationships.
3. **Anchor Text Optimization**: Avoid using generic phrases like "click here" or "read more." Use descriptive, keyword-rich anchor texts such as "read our full analysis of CRM solutions for dental clinics."
## Programmatic SEO in WordPress with CPTs and ACF
WordPress is an excellent engine for programmatic SEO when configured correctly. Avoid relying on standard posts or pages; instead, register a dedicated Custom Post Type (CPT) and structure data fields using Advanced Custom Fields (ACF).
### How to register a custom post type for pSEO
Add the following code to your theme's `functions.php` file or a custom system plugin to register a `tool` post type:
```php
declare(strict_types=1);
namespace WPPoland\Programmatic;
function register_tool_cpt(): void {
$labels = [
'name' => _x('Tools', 'post type general name', 'wppoland'),
'singular_name' => _x('Tool', 'post type singular name', 'wppoland'),
'menu_name' => _x('pSEO Tools', 'admin menu', 'wppoland'),
'add_new_item' => __('Add New Tool', 'wppoland'),
'edit_item' => __('Edit Tool', 'wppoland'),
'all_items' => __('All Tools', 'wppoland'),
'view_item' => __('View Tool', 'wppoland'),
'search_items' => __('Search Tools', 'wppoland'),
'not_found' => __('No tools found', 'wppoland'),
'not_found_in_trash' => __('No tools found in Trash', 'wppoland'),
];
$args = [
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => ['slug' => 'tools', 'with_front' => false],
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 25,
'supports' => ['title', 'editor', 'thumbnail', 'excerpt'],
'show_in_rest' => true, // Enabled for block editor and REST API access
];
register_post_type('tool', $args);
}
add_action('init', __NAMESPACE__ . '\\register_tool_cpt');
```
### ACF fields for programmatic pages
Register the following metadata keys for the CPT to hold structured variables:
- `tool_price_monthly` (Number): Base pricing tier.
- `tool_target_industry` (Text): Targeted business vertical.
- `tool_score_overall` (Number): Computed rating (e.g., `4.5` out of `5`).
- `tool_features_list` (Checkbox/Select): List of core system features.
- `tool_integrations` (Text/Array): Third-party connection options.
…[truncated, fetch the canonical URL for the full body]…
---
## The ROI of migrating to WordPress for business in 2026: A cfo's guide
URL: https://wppoland.com/en/roi-of-migrating-to-wordpress-2026-en/
Description: Is migrating to WordPress worth the investment? We analyze the long-term Return on Investment (ROI) for businesses moving to WordPress in 2026.
Published: Wed Oct 22
Updated: Wed Oct 14
Type: guide
Level: intermediate
Tags: roi, migration, business, wordpress, strategy
A migration to WordPress is usually approved on one question: does it pay back, and when. The answer sits in documents you already have: the CMS licence renewal, the hosting contract, the hours your agency billed for small changes last year, and how many days a new landing page waits before it goes live. This guide walks through each of those lines and shows how to turn them into a payback estimate you can defend.
## 1. How much a WordPress migration saves on licences
The most immediate impact of a WordPress migration is on the balance sheet.
* **License Fee Elimination**: Enterprise CMS licenses are an annual recurring line that scales with seats and modules, and it renews whether or not you shipped anything that year. Moving to WordPress removes that line entirely for the software itself.
* **Hosting Optimization**: High-performance managed WordPress hosting (like WP Engine or WordPress VIP) is often more cost-effective than managing a custom, over-provisioned cloud stack for a proprietary CMS.
## 2. WordPress productivity gains for marketing teams
In 2026, time is the most expensive commodity.
* **Non-Technical Content Creation**: In WordPress, a marketing manager can build a landing page in 30 minutes using the Block Editor. On a legacy system, this often requires a developer ticket and a 1-week turnaround.
* **Reduced Training Costs**: Almost everyone knows how to use WordPress. New hires are productive from day one, rather than spending weeks learning a niche proprietary interface.
## 3. Does migrating to WordPress improve SEO
Google ranks relevance first, so a migration mostly decides whether you keep what you have and how much control you gain over the technical side.
* **Control over the markup**: WordPress does not rank better for being WordPress. With a well-built block theme you decide headings, markup and structured data yourself instead of accepting what a proprietary template generates.
* **Core Web Vitals**: modern WordPress themes and architectures (especially [headless and decoupled setups](/en/headless-wordpress-vs-traditional-roi-analysis-2026/)) can pass the field thresholds described in section 11. A Lighthouse score of 90 to 100 is a lab test, not the measurement Google uses.
* **Structured data**: Product and Article markup can be generated from the content model instead of being maintained by hand. FAQ and HowTo markup are no argument for a migration: since 2023 Google shows [FAQ rich results only for well-known, authoritative government and health websites, and no longer shows HowTo rich results](https://developers.google.com/search/blog/2023/08/howto-faq-changes).
## 4. How to avoid vendor lock-in with WordPress
The ability to pivot is priceless.
* **Plugin Ecosystem**: Need to add a community hub? A member's portal? An AI-driven search? In WordPress, these features can be integrated in days. In a proprietary CMS, they are "custom projects" that take months.
* **No Vendor Lock-in**: You own your code. You are never at the mercy of a single provider's price hikes or lack of innovation.
---
## Proprietary CMS vs WordPress ROI over 3 years
| Category | Legacy Proprietary CMS | WordPress (Enterprise) |
| :--- | :--- | :--- |
| **Software Cost** | High / Recurring | No license line |
| **Lead Generation Speed**| Slow (Dev-dependent) | Fast (Market-driven) |
| **Content Ops Cost** | Very High | Low |
| **Innovation Speed** | Low | Very High |
| **Payback horizon** | Never, the license renews | One-off migration, then a permanently lighter run rate |
## 5. How to calculate WordPress migration payback
You do not need our numbers for this. Put your own in.
* **Your baseline**: this year's CMS license plus the specialist maintenance you buy because nobody else can touch the system.
* **Year 1**: a one-off migration cost. The license line goes to nothing. The maintenance line drops, because ordinary WordPress engineers can do most of the work that used to need a certified specialist.
* **Year 2 onwards**: the license and the specialist premium are simply gone, every year.
* **The test**: if the one-off migration is smaller than roughly one year of license plus the specialist premium you stop paying, the project has paid for itself before the next renewal. That comparison, not a vendor's ROI slide, is what your CFO should be asked to sign.
## 6. What to measure before a CMS migration
An ROI figure is only as good as the baseline it is compared against, and the baseline disappears the day the old system is shut down. Capture it first.
* **The contract**: renewal date, notice period, auto-renew clause, and what the vendor charges per seat, per module and per environment. Check the exit clause too: whether you get a full content export, in which format, and whether the export itself is billed.
* **Every invoice line the CMS drags along**: hosting, CDN, support retainer, the certified partner who handles upgrades, and any add-on for search, forms or personalisation. Vendors such as Adobe Experience Manager or Sitecore sell these as separate lines, so they are easy to list and easy to miss.
* **Lead time**: the median number of days from "marketing asks for a page" to "the page is live" over the last quarter. Your ticket system already holds this. It is the number that turns the productivity argument from an opinion into a measurement.
* **Search baseline**: export clicks and impressions per URL from [Google Search Console](/en/mastering-google-search-console-indexing-api/). It keeps 16 months of data, so an export taken now lets you compare the same season next year instead of comparing December with June.
* **Conversion baseline**: form submissions, demo requests or orders per month from your analytics, split by landing page.
## 7. One-off costs of a WordPress migration
The one-off side of the calculation is not only the build. List these separately, because each one is priced individually and each one can be the line that delays payback.
* **Content inventory and cleanup**: deciding what moves, what merges and what is retired. Moving pages nobody reads costs the same as moving pages that sell.
* **The redirect map**: every old URL mapped to its new home with a 301. Skipping it is the fastest way to turn a positive ROI negative, because lost rankings show up as lost revenue for months.
* **Integrations**: CRM, marketing automation, single sign-on, ERP or product feeds. Each one is a small project with its own testing.
* **Parallel running**: if the cutover lands after the renewal date, you pay one more licence period while the new site is being finished. Planning the launch backwards from the notice deadline is often the single cheapest decision in the project.
* **Training and a support window after launch**: editors need a few sessions on the Block Editor and a named person to ask during the first weeks. Cutting this is how teams end up filing developer tickets for work they could now do themselves.
## 8. Capitalising website development costs under GAAP and IFRS
The same spend can look very different on paper depending on how it is classified. A licence or SaaS subscription is an operating expense every year. Website development work can often be capitalised: US GAAP covers website development costs in ASC 350-50, and IFRS has SIC-32 on web site costs. Broadly, planning-stage and ongoing operating costs are expensed, while application and infrastructure development can be capitalised and amortised over the site's useful life. The IFRS Interpretations Committee also published an agenda decision in 2021 on configuration and customisation costs in cloud computing arrangements, which usually pushes that work in a SaaS CMS towards expense.
Which of your costs qualify is your auditor's call, not ours. The practical point is to agree the treatment before the project starts, so the business case presented to the board uses the same classification the accounts will use afterwards.
## 9. Measuring CMS migration ROI after 12 months
Take the same five measurements from section 6 again and put them next to the baseline. Three rules keep the comparison honest:
* **Recovered developer hours are capacity, not cash**, unless a contract or a headcount line actually changed. Report them as work moved back to the product backlog.
* **Keep search gains in their own row.** They depend on redirects, content quality and competition as much as on the platform, so mixing them into "savings" makes the whole case easier to dispute.
* **Compare like with like.** Same months, same pages, same definition of a conversion. If the site structure changed, compare at the level of page groups rather than single URLs.
A migration that clears the licence line but has not shortened the lead time or held the search baseline has paid back on cost alone. That is still a result, and a clear baseline is what lets you say so with evidence.
## 10. What happens to Google rankings after a CMS migration
Google covers this case in its guide to [site moves with URL changes](https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes), and three points from it belong in the business case. The redirects: Google asks for permanent HTTP redirects such as 301 or 308 and advises keeping them "for as long as possible, generally at least 1 year". The dip: the same guide says to expect temporary fluctuation in rankings during the move, and that for a small to medium-sized site it can take a few weeks for most pages to move. The order: for small and medium sites Google recommends moving all URLs at the same time rather than one section at a time.
For the payback calculation this means search gains have no place in the first months. Book them, if at all, from the second half of the first year, and keep the redirect map running long after the old CMS contract has ended. Launching outside the season that brings most of your leads costs nothing and keeps the fluctuation Google describes away from your best months.
## 11. Which Core Web Vitals thresholds to check after migrating to WordPress
The "90 to 100" figure often quoted for WordPress themes is a Lighthouse lab score. The [Core Web Vitals](https://web.dev/articles/vitals) are measured on real visits, and a page passes when the 75th percentile of page loads, split into mobile and desktop, meets three thresholds:
| Metric | What it measures | Good |
| :--- | :--- | :--- |
| LCP | Loading of the main content | 2.5 seconds or less |
| INP | Response to interaction | 200 milliseconds or less |
| CLS | Visual stability | 0.1 or less |
INP became a stable Core Web Vital in 2024 and replaced First Input Delay, so a comparison with an older audit of the previous site has to use the same set of metrics. Google's [page experience documentation](https://developers.google.com/search/docs/appearance/page-experience) states that Core Web Vitals are used by its ranking systems. It also states that good results in its reports do not guarantee top rankings, and that Google Search always tries to show the most relevant content even when its page experience is weaker. In the ROI sheet, treat passing Core Web Vitals as protection against losing ground, not as a revenue line.
## 12. What still costs money in WordPress after the licence disappears
WordPress itself is released under the [GPLv2 (or later)](https://wordpress.org/about/license/), so there is no fee for the software. The running costs do not drop to zero, and the business case is more credible when it names them:
…[truncated, fetch the canonical URL for the full body]…
---
## A working guide to WooCommerce performance (2026)
URL: https://wppoland.com/en/the-definitive-guide-to-woocommerce-performance-2026-edition/
Description: Default WooCommerce stalls under real load. Practical notes on HPOS, Redis vs transients, cart fragments, autoload bloat, and Action Scheduler from production audits.
Published: Thu Jan 30
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: hpos, redis, database, scaling
Default WooCommerce stalls before the catalogue gets interesting. A clean install with 200 SKUs feels fast in staging, then the same template under 30k products and 50k orders hits TTFB in the 1.5 to 3 second range, cart fragments serialise on the shop archive, and the wp_options autoload row count quietly crosses 100k.
The platform itself is fine. [WooCommerce](/en/woocommerce-developer/) powers stores doing eight figures a year. The configuration is what kills it.
This is a field guide for hardening WooCommerce against real load. It assumes you already have shell access, WP-CLI, and a willingness to read slow query logs. For mobile checkout lag without a rebuild, start with the [performance recovery case study](/en/case-studies/confidential-woocommerce-performance-recovery/) and the [checkout latency diagnostic CSV](https://gist.github.com/wppoland/f3c62b7ff2846ee9c1de24b82bf735de); the layered playbook lives in [WooCommerce performance optimization 2026](/en/woocommerce-performance-optimization-2026/).
## WooCommerce HPOS migration and what it changes
For a decade WooCommerce stored orders as `shop_order` posts in `wp_posts`, with every line item, address, and metadata field exploded into rows in `wp_postmeta`. A "show me orders from May" query meant scanning a table mixed with blog revisions, autosaves, and draft pages, then JOINing postmeta three or four times to reconstruct each order.
High Performance Order Storage moves orders into `wc_orders`, `wc_order_addresses`, `wc_order_operational_data`, and `wc_orders_meta`. Order list queries now hit indexed columns directly rather than reconstructing each row from postmeta.
HPOS has been the default for new installations since WooCommerce 8.2, released in October 2023, according to the [HPOS documentation](https://developer.woocommerce.com/docs/features/high-performance-order-storage/). Stores that launched before that release often still run on posts storage, which is why the migration below is still the first item on most audits.
What HPOS does not fix on its own:
- Custom plugins that still write to postmeta keys instead of the new HPOS API will silently desync once compatibility mode is off
- Third party plugins that read order data with `get_post_meta($order_id, ...)` instead of `$order->get_meta()` will return nothing once the legacy table stops being authoritative
- Reporting plugins built on `WP_Query` against `shop_order` need a rewrite
Workflow:
1. WooCommerce -> Settings -> Advanced -> Features -> turn on High Performance Order Storage with compatibility mode on
2. Let the sync run during low-traffic hours; on a store with 200k orders this can run for several hours
3. Verify with `wp wc hpos verify_data`
4. Run your full plugin suite against the new tables for a week with compatibility mode still writing to postmeta
5. Disable compatibility mode only after you have confirmed every order-touching plugin reads from HPOS
```bash
wp wc hpos status
wp wc hpos sync --batch-size=500
wp wc hpos verify_data
```
### HPOS WP-CLI commands after WooCommerce 8.9
Older tutorials use the `wp wc cot` namespace. WooCommerce 8.9.0 deprecated it: the old commands still run, but each one prints a warning that names its `wp wc hpos` replacement, and `verify_cot_data` became `verify_data` ([CLIRunner.php](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php)). The [HPOS CLI documentation](https://developer.woocommerce.com/docs/features/high-performance-order-storage/cli-tools/) and the command docblocks list the current set:
- `wp wc hpos status` shows the HPOS settings and the sync state
- `wp wc hpos count_unmigrated` counts orders still waiting for sync
- `wp wc hpos sync` migrates orders in batches, and `--batch-size` defaults to 500
- `wp wc hpos verify_data` compares both datastores; its `--re-migrate` flag re-syncs orders that differ and can overwrite an order, so try it on staging first
- `wp wc hpos diff 123` shows the differences for a single order
- `wp wc hpos compatibility-mode enable` and `disable` switch synchronisation without opening the admin screen
- `wp wc hpos cleanup` removes legacy post data for migrated orders. The documentation calls it destructive, and without arguments it does nothing; `wp wc hpos cleanup all` processes every order
Run `cleanup` only after compatibility mode has been off long enough that no plugin reads the legacy copy, and only after a database backup.
### How to make a custom plugin HPOS compatible
WooCommerce warns the store owner when they try to enable HPOS while a plugin that declared itself incompatible is active, and shows the same warning on the Plugins screen. For plugins that are not WooCommerce extensions, the warning appears only when the plugin header carries a `WC tested up to` line. A custom plugin declares its status in its main file, as shown in the [HPOS recipe book](https://developer.woocommerce.com/docs/features/high-performance-order-storage/recipe-book/):
```php
add_action( 'before_woocommerce_init', function() {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
} );
```
The declaration is a promise, not a fix. The code behind it has to load orders with `wc_get_order()` instead of `get_post()`, and write metadata through the order object (`update_meta_data()`, `add_meta_data()`, `delete_meta_data()`, then `save()`) instead of `update_post_meta()`. WooCommerce then decides which tables receive the write. The recipe book notes that `save()` is relatively expensive, so a loop that saves after every single meta change is a performance bug of its own. Code that must branch on the storage engine can call `OrderUtil::custom_orders_table_usage_is_enabled()`.
## How to disable cart fragments in WooCommerce
Run an older WooCommerce store, or one with a cart widget in the header, through GTmetrix or WebPageTest and you will see `?wc-ajax=get_refreshed_fragments` showing up on every page, often eating 800 to 1500 ms. This is WooCommerce asking PHP "did the cart icon in the header change?" on the homepage, on the blog, on category archives, on the about page. Even when the cart is empty. Even when the user has never added anything.
A real failure mode: a Stripe-checkout fashion store hit Black Friday with a paid traffic spike around 200 concurrent sessions. The checkout flow itself was fine, but cart fragments serialised through `admin-ajax.php` on the shop archive page where most of the paid traffic landed. PHP-FPM workers queued, TTFB on the archive crossed 8 seconds, and conversions collapsed for ninety minutes until the engineering team scoped fragments to commerce pages with a hard dequeue.
The simplest scoping rule:
```php
add_action( 'wp_enqueue_scripts', function() {
if ( is_cart() || is_checkout() || is_product() ) {
return;
}
wp_dequeue_script( 'wc-cart-fragments' );
}, 11 );
```
Plugins like Perfmatters and Asset CleanUp expose this as a UI toggle. Either path works; the goal is the same. Cart fragments should fire only where a cart icon needs live updating.
If you genuinely need a header cart count on every page, store it in `localStorage` and update from JavaScript on add-to-cart events rather than polling the server.
### When WooCommerce still loads cart fragments after version 7.8
WooCommerce 7.8 changed the default. According to the WooCommerce developer blog post [Best practices for the use of the "cart fragments" API](https://developer.woocommerce.com/2023/06/16/best-practices-for-the-use-of-the-cart-fragments-api/) (16 June 2023), the script is no longer enqueued on every route of the store. It still loads in three cases: on pages where the Cart widget is rendered, when a third-party script registers the cart fragments script as a dependency, and when code enqueues it explicitly. The same post notes that some themes, Storefront among them, hard-code the widget into their templates.
On a current store the request in the waterfall is therefore a symptom with a findable cause. Search the page source for the `wc-cart-fragments` handle, then trace which theme template or plugin pulls it in. Dequeueing it without that check can break a header cart that the theme still refreshes through fragments, and a plugin that lists the script as a dependency will enqueue it again anyway.
## Object cache versus database transients
WooCommerce uses transients heavily for variation price ranges, attribute term lookups, and shipping rate calculations. Without an object cache backend, transients live in `wp_options`. Whether they load on every request depends on the expiry: [`set_transient()`](https://developer.wordpress.org/reference/functions/set_transient/) stores a transient without an expiry as autoloaded, and a transient with an expiry as not autoloaded, plus a separate `_transient_timeout_` row. WooCommerce sets its variation price and variation children transients with a 30-day expiry ([variable product data store](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php)), so they mostly inflate the row count of `wp_options`. Autoload weight comes from options that plugins write without an expiry.
Two failure modes worth recognising:
- A bookstore with 12k variable products had `wc_var_prices_*` transients accumulating in `wp_options` for two years. The table crossed 80 MB. Every uncached page load did a SELECT on a `wp_options` table with 130k rows just to bootstrap WordPress. TTFB sat at 2.4 seconds before any business logic ran
- A B2B catalogue store ran 30+ plugins, baseline TTFB 1.8 s. After identifying four plugins that wrote autoloaded options on every product save and disabling them, TTFB dropped to around 600 ms with no other change
Pick one cache backend and commit. Either Redis or Memcached as a real object cache, or stay on database transients. Mixing them causes invalidation bugs that are hard to diagnose.
```bash
# Audit autoload size in wp_options
wp db query "SELECT SUM(LENGTH(option_value)) AS bytes, COUNT(*) AS rows FROM wp_options WHERE autoload IN ('yes', 'on', 'auto-on', 'auto');"
# Find the worst offenders
wp db query "SELECT option_name, LENGTH(option_value) AS bytes FROM wp_options WHERE autoload IN ('yes', 'on', 'auto-on', 'auto') ORDER BY bytes DESC LIMIT 20;"
```
With Redis configured at the host level and `object-cache.php` dropped in, `set_transient()` writes to the object cache instead of `wp_options`, transients move to RAM, autoload pressure drops, and product page render time on a cold cache typically goes from the 500 to 800 ms range to under 100 ms.
### Autoload values in WordPress 6.6 and the 800 KB warning
WordPress 6.6 changed how autoload is stored. The core developer note [Options API: disabling autoload for large options](https://make.wordpress.org/core/2024/06/18/options-api-disabling-autoload-for-large-options/) introduces the database values `on`, `off`, `auto`, `auto-on` and `auto-off`; `on` and `off` are explicit choices, while `auto-on` and `auto-off` are set by WordPress itself. The function [`wp_autoload_values_to_autoload()`](https://developer.wordpress.org/reference/functions/wp_autoload_values_to_autoload/) returns `yes`, `on`, `auto-on` and `auto`, which is why the audit queries above filter on that list. A query that still checks only `autoload = 'yes'` misses every option saved after the upgrade and reports a store as cleaner than it is.
…[truncated, fetch the canonical URL for the full body]…
---
## Why large corporations choose WordPress in 2026: An enterprise analysis
URL: https://wppoland.com/en/why-large-corporations-choose-wordpress-2026-analysis/
Description: Why large organisations such as Siemens, SAP, Capgemini, TIME and the White House run on WordPress: market data, documented migrations, security and compliance, and the limits.
Published: Sat Sep 06
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: enterprise-wordpress, corporate-decision-making, cms-migration, open-source-enterprise, digital-transformation
The boardroom question about WordPress has changed over the last decade. Few large organisations still ask whether an open-source CMS can carry a corporate website. They ask which parts of their portfolio belong on it, who will host it and how the editorial teams will work once it is live.
For years, large corporations stayed on monolithic, proprietary CMS platforms partly because a high licence fee felt like a guarantee. Today the arguments that decide a CMS choice are editorial speed, performance, the size of the hiring pool and how easily the platform can be replaced or extended. Those are the arguments where WordPress is strongest.
This analysis walks through those reasons with named organisations and public sources: the market data, the companies that documented their moves, the security and compliance record, and the places where WordPress needs more engineering than its reputation suggests.
---
## How many websites use WordPress in 2026
W3Techs, which updates its reports daily, reports that WordPress is used by **58.5% of all websites whose content management system is known**, which is **40.0% of all websites** ([W3Techs, 14 October 2026](https://w3techs.com/technologies/details/cm-wordpress)). The same report shows how quickly the installed base moves to new major releases: on that date **65.7%** of WordPress sites ran version 7, 27.4% version 6 and 4.8% version 5.
WordPress.org's own security page uses a different figure, more than 43% of the web ([WordPress.org, Security](https://wordpress.org/about/security/)). The difference comes from the moment and the method of measurement, so when a procurement document quotes a market share, it should name the source and the date next to the number.
For an enterprise buyer, the share matters for two practical reasons. A platform used by that many sites has a large supply of developers, agencies, hosting companies and security vendors. And a large installed base means vulnerabilities are looked for, found and fixed in public, which is the opposite of the security-through-obscurity model of a niche CMS.
---
## Which large companies use WordPress
The list below contains only organisations whose WordPress use is documented by a public source: the WordPress.org Showcase, the WordPress.org enterprise case studies, or WordPress VIP's published case studies.
| Organisation | What runs on WordPress | Source |
|---|---|---|
| The White House | WhiteHouse.gov, on WordPress since 2017, rebuilt in 2021 | [WordPress.org Showcase](https://wordpress.org/showcase/the-white-house/) |
| NASA | nasa.gov and NASA Science, both enterprise-level WordPress builds | [WordPress.org Showcase](https://wordpress.org/showcase/nasa/) |
| TIME | TIME.com, on WordPress and WordPress VIP since 2015 | [WordPress VIP case study](https://wpvip.com/case-studies/time/) |
| Al Jazeera | Headless WordPress with a GraphQL layer and a React front end | [WordPress VIP case study](https://wpvip.com/case-studies/al-jazeera/) |
| Siemens | Ingenuity content platform, migrated from Adobe Experience Manager | [Siemens case study (PDF)](https://wordpress.org/files/2023/10/Siemens-Case-Study.pdf) |
| SAP | SAP News Center, a Multisite network with 44 languages | [SAP News case study (PDF)](https://wordpress.org/files/2023/10/SAP-News-Case-Study.pdf) |
| Capgemini | A network of 38 sites, migrated from Drupal 7 | [Capgemini case study (PDF)](https://wordpress.org/files/2023/10/Capgemini-Case-Study.pdf) |
| Ubisoft | A global intranet for teams in roughly 40 to 45 countries | [WordPress VIP case study](https://wpvip.com/case-studies/ubisoft/) |
The case study PDFs are linked from the [WordPress.org enterprise page](https://wordpress.org/enterprise/) and were written by the agencies that delivered the projects (Human Made for Siemens and Capgemini, Inpsyde for SAP). They describe each project from the vendor's side, so read them as documented examples, not as independent audits.
---
## WordPress vs Sitecore for marketing teams
In a fast-paced market, a corporation's greatest enemy is **latency in communication**.
- **The proprietary problem**: in systems like Sitecore or Adobe AEM, even minor layout changes often require a ticket to a specialised IT team and a deployment cycle.
- **The WordPress solution**: with the maturity of block-based editing (Gutenberg), marketing teams can build, test and launch complex landing pages from patterns and reusable blocks without waiting for a release.
This marketing agility allows brands to respond to market trends or global events without technical bottlenecks.
### What Siemens gained by leaving Adobe Experience Manager
Siemens is the clearest documented example of this argument. Its Ingenuity platform, where Siemens employees around the world publish under their own names, was originally built on Adobe Experience Manager. According to the [Siemens case study](https://wordpress.org/files/2023/10/Siemens-Case-Study.pdf), the move to WordPress made custom development much quicker, and the money saved went into building the features the team needed. The same platform later became a virtual events platform, launched in three months.
The case study also describes the multilingual problem that pushed the change. The original site was English only. Audience interest, legal requirements in Germany and the global nature of the business required more languages. Siemens now uses AWS to translate English and German content automatically, editors correct the translations, and every language version gets its own URL.
---
## Moving from Drupal to WordPress at enterprise scale
Capgemini, a technology consultancy present in over 40 countries, launched a series of websites on Drupal 7 in 2012. The [Capgemini case study](https://wordpress.org/files/2023/10/Capgemini-Case-Study.pdf) says the Marketing and Communications team hit blockers at almost every stage of publishing, and the lack of backwards compatibility between Drupal versions made the sites increasingly unstable.
The detail that matters most for a CIO is the staffing one. Capgemini has 200,000 people, and the case study notes that there were **fewer than five qualified Drupal webmasters** in the organisation. The recruitment team, which publishes job roles for tens of thousands of applicants a year, could not keep up. Capgemini moved its network of **38 sites** to WordPress, with the goal of letting local teams publish while staying within the corporate brand.
That is a talent-pool argument, not a feature argument. A CMS that only a handful of people inside the company can operate is a key-person risk, however good its feature list looks.
---
## Headless WordPress and Core Web Vitals at enterprise scale
Google's ranking systems reward good page experience, and Core Web Vitals (LCP, INP and CLS) are part of that assessment.
- **Lightweight by design**: proprietary enterprise systems are often heavy. WordPress, when optimised correctly, can be fast.
- **Headless capabilities**: large programmes keep [WordPress as the authoring plane](/en/headless-wordpress-architecture-2026-enterprise-guide/) while Astro or similar edge-friendly front ends stream HTML with aggressive caching. LCP depends on images, fonts and third-party scripts far more than on the CMS, so these have to be budgeted deliberately.
### How Al Jazeera runs headless WordPress
Al Jazeera's newsroom has more than 70 bureaus around the world and more than 10 channels and divisions. Before consolidating, its global teams worked across more than six different CMSes, including more than three versions of Drupal ([WordPress VIP case study](https://wpvip.com/case-studies/al-jazeera/)).
The new stack has three layers: WordPress as the editorial back end, a GraphQL query layer in the middle and a React-based application on top. GraphQL gives every front end a standard way to request content. The first application built on it was the AJ Alpha mobile app, and the architecture has since been extended across Al Jazeera's channels. WordPress support for bidirectional languages made Arabic and Chinese editions possible on the same platform.
The pattern is useful beyond news. One editorial back end feeds several front ends (web, apps, partner feeds) through an API, and each front end can be rebuilt without migrating the content.
---
## How much traffic can WordPress handle
The usual objection from IT is scale. Two documented cases answer it.
TIME.com moved to WordPress and WordPress VIP in 2015, replacing a legacy publishing system that its team described as dated and inflexible. When Taylor Swift was named Person of the Year, the site handled **more than 100,000 requests per second** at peak, according to the [WordPress VIP case study](https://wpvip.com/case-studies/time/). More than 100 TIME.com staff publish with the block editor, custom blocks and reusable patterns.
WhiteHouse.gov has run on WordPress since 2017. The [WordPress.org Showcase entry](https://wordpress.org/showcase/the-white-house/) describes the 2021 rebuild led by 10up with Wide Eye Creative, launched on Inauguration Day, with layered caching, a global CDN and auto-scaling to absorb traffic spikes. The same entry notes that WordPress has been used across three administrations.
In both cases the scale comes from the hosting architecture (page caching, object caching, CDN, horizontal scaling), not from WordPress alone. A self-hosted WordPress on a single server will not behave like either of them.
---
## Enterprise CMS cost compared with WordPress
For a large company, an annual CMS licence may look small next to its total IT budget, but the hidden costs are larger than the licence.
- **Implementation cost**: proprietary systems need specialised integrators, and the initial build is often expensive.
- **Support cost**: you are locked into a small pool of high-priced specialised agencies.
- **Incremental innovation**: in WordPress, thousands of contributors improve the software in public, at no licence cost. In proprietary systems, you wait for the vendor's next paid upgrade.
WordPress itself has no licence fee. The [WordPress.org enterprise page](https://wordpress.org/enterprise/) states that the source code "is made freely available and may be distributed and modified subject to the GPL license". The costs move to hosting, development, support and editorial training, which are the line items that should go into a five-year comparison.
The White House move is one of the few public cost statements. The Showcase entry cites an administration official telling the Washington Examiner that the 2017 redesign would save taxpayers nearly $3 million per year. That figure is the administration's own estimate, reported by the press, so it supports the direction of the argument rather than a precise saving for any other organisation.
---
## Is WordPress secure enough for enterprises
The idea that open source is less secure does not survive a look at how WordPress handles vulnerabilities. Its transparency is part of its security model.
- **Active auditing**: the source code is public, the WordPress Security Team develops fixes for responsibly disclosed vulnerabilities and releases them in bugfix releases.
- **High-profile validation**: WhiteHouse.gov and NASA run on WordPress, and with the right [environment hardening](/en/advanced-wordpress-security-hardening-2026/), WordPress meets the requirements of government and enterprise security reviews.
### What the WordPress Security Team does
According to [WordPress.org](https://wordpress.org/about/security/), the core development lifecycle includes code review throughout, with contributions reviewed by trusted committers. The Security Team includes **more than 50 trusted experts**, among them lead developers, security researchers and key contributors to every component of WordPress, plus sponsored members. It hardens the software against threats such as the OWASP Top Ten.
…[truncated, fetch the canonical URL for the full body]…
---
## WordPress history, from 4.9 to the Gutenberg era
URL: https://wppoland.com/en/wordpress-evolution-4-9-gutenberg-era/
Description: WordPress 4.9 was "the calm before the storm." We analyze this pivotal moment in WP history that prepared the ground for the block editor. What changed since? How did the ecosystem evolve?
Published: Thu Sep 14
Updated: Wed Oct 14
Type: guide
Level: intermediate
Tags: wordpress, gutenberg, block editor, wordpress history, customizer, web development
## WordPress 4.9 Tipton release in 2017
Learn more about [WordPress development services](/en/wordpress-developer/) at WPPoland.
WordPress 4.9 "Tipton", released in November 2017, occupies a unique place in WordPress history as the final major release before the step-change WordPress 5.0, which introduced the Gutenberg block editor. This version represented a moment of maturity for WordPress as a developer platform, introducing conveniences that we now consider standard but that were important at the time. A feature-by-feature walkthrough of the release itself is in [what was new in WordPress 4.9](/en/wordpress-4-9-whats-new/); this article follows what came after it.
Looking from our 2026 perspective, version 4.9 stands as a fascinating historical artifact - a snapshot of WordPress at the precipice of its most significant transformation since the platform's inception. The developers and designers who built sites in 4.9 witnessed an era where PHP template files still ruled supreme, where widget areas were managed through drag-and-drop interfaces, and where the concept of "blocks" was associated primarily with media blocks in content rather than the fundamental building element of entire pages.
The development cycle for WordPress 4.9 introduced several key improvements that enhanced the platform's appeal to developers and site administrators alike. The focus on code quality, performance optimization, and user experience refinements demonstrated WordPress' commitment to continuous improvement even as the team prepared for the massive undertaking that would become Gutenberg.
### What was new in WordPress 4.9
WordPress 4.9 brought several transformative features that shaped the development landscape for years to come. The most significant improvements focused on the Customizer, code editing capabilities, and widget management, each addressing pain points that developers and site administrators had long expressed frustration with.
The Customizer improvements in 4.9 represented a major step forward in the platform's journey toward a more dynamic and user-friendly site editing experience. These enhancements laid important groundwork for the [Full Site Editing](/en/gutenberg-blocks-patterns-full-site-editing-guide/) vision that would emerge in subsequent releases, while the WordPress team kept developing the Customizer as the central place for site customization.
```php
// Example of Customizer enhancements in WordPress 4.9
// Adding a new control with real-time preview
$wp_customize->add_setting('theme_options[accent_color]', array(
'default' => '#0073aa',
'transport' => 'postMessage',
'sanitize_callback' => 'sanitize_hex_color',
));
$wp_customize->add_control(new WP_Customize_Color_Control(
$wp_customize,
'theme_options[accent_color]',
array(
'label' => __('Accent Color', 'my-theme'),
'section' => 'colors',
'priority' => 5,
)
));
```
The integration of CodeMirror into the WordPress core for theme and plugin editing was another landmark improvement. Before 4.9, editing theme files or adding custom CSS meant working with a basic textarea that offered no syntax highlighting, line numbers, or error detection. Developers who accidentally introduced syntax errors would often break their sites, leading to the infamous "white screen of death" and requiring FTP access to fix the problem.
## CodeMirror code editor in WordPress 4.9
The introduction of CodeMirror-based editors in WordPress 4.9 fundamentally changed how developers interacted with the platform's code editing capabilities. Syntax highlighting, line numbering, bracket matching, and error detection became standard features, dramatically reducing the likelihood of site-breaking mistakes and improving overall developer productivity.
This enhancement was particularly significant for site administrators who frequently needed to add custom CSS or modify theme files. The visual feedback provided by syntax highlighting made it much easier to identify typos, missing semicolons, and other common coding errors before they could cause problems on the live site. Line numbers also facilitated collaboration when working with development teams or when seeking help from the WordPress community.
```css
/* Example CSS with syntax highlighting - WordPress 4.9 CodeMirror support */
.site-header {
background-color: #ffffff;
padding: 20px 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.site-header.scrolled {
padding: 10px 0;
background-color: #f8f9fa;
}
/* Responsive design with media queries */
@media screen and (max-width: 768px) {
.site-header {
padding: 15px 0;
}
.main-navigation {
display: none;
}
}
```
The CodeMirror integration also improved the "Additional CSS" section in the Customizer, making it a more viable option for storing custom CSS rules. The [4.9 release post](https://wordpress.org/news/2017/11/tipton/) lists two safeguards that came with it: WordPress warns before saving code that contains an error, and it warns that editing theme and plugin files directly from the admin is a dangerous practice. The bundled linters were CSSLint, JSHint, HTMLHint and JSONLint, according to the [code editing dev note](https://make.wordpress.org/core/2017/10/22/code-editing-improvements-in-wordpress-4-9/); PHP was highlighted but not linted.
## WordPress 4.9 gallery widget and widget changes
WordPress 4.9 introduced native gallery support for widgets, addressing a common need among site administrators who wanted to display photo galleries in sidebars and other widget areas without requiring custom code or third-party solutions. This feature built on the image, video and audio media widgets that WordPress 4.8 had added a release earlier.
The Text widget changed too. According to the [WordPress 4.9 field guide](https://make.wordpress.org/core/2017/11/07/wordpress-4-9-field-guide/), it gained support for shortcodes, images, galleries, video, audio and embeds, and the release post adds that menu and widget placement became more persistent when switching themes. These changes were incremental next to what the block editor would bring, but they moved widgets closer to post content.
## Why WordPress 4.9 widgets were called proto-blocks
The gallery widget looks like a minor sidebar feature, but the dev note that announced it ties it directly to Gutenberg. In [Introducing the Gallery widget](https://make.wordpress.org/core/2017/09/25/introducing-the-gallery-widget/) on make.wordpress.org (25 September 2017), Weston Ruter wrote:
> "The media widgets are being developed with Gutenberg in mind, as widgets are essentially proto-blocks."
The same note says that as Gutenberg matures, "widgets are planned to eventually transition over to use blocks", and that the image, video, audio and gallery widgets would be migrated at that point. It also records the movement in the other direction: Gutenberg had already ported the Categories and Recent Posts widgets to dynamic blocks, so content that used to be restricted to sidebars could be placed inside posts.
That plan landed almost four years later. WordPress 5.8 "Tatum" ([release post, 20 July 2021](https://wordpress.org/news/2021/07/tatum/)) brought blocks to "both the Block Widgets Editor and the Customizer". Sites that were not ready can return to the old screens with [Classic Widgets](https://wordpress.org/plugins/classic-widgets/), an official plugin maintained by the WordPress team, which the plugin directory lists at more than 2 million active installations. For anyone maintaining a theme from the 4.9 period, widget areas built from text and media widgets are the part of the site that changes first when the block widget editor switches on.
## What changed in WordPress 4.9.6 and 4.9.8 before Gutenberg
Version 4.9 stayed the current branch for more than a year, and two of its maintenance releases changed more than bugs.
WordPress 4.9.6 ([release post, 17 May 2018](https://wordpress.org/news/2018/05/wordpress-4-9-6-privacy-and-maintenance-release/)) was a privacy release. It added:
- a setting to designate a privacy policy page, which is then shown on the login and registration pages;
- an export tool that produces a ZIP file with a user's personal data, including data collected by participating plugins;
- an erasure tool for the same data;
- an email-based method to confirm export and erasure requests;
- a checkbox in the comment form that lets logged-out commenters decide whether their name, email and website are saved in the browser.
WordPress 4.9.8 ([release post, 2 August 2018](https://wordpress.org/news/2018/08/wordpress-4-9-8-maintenance-release/)) added the "Try Gutenberg" callout to the dashboard, described as "an opportunity for users to use the Gutenberg block editor before it is released in WordPress 5.0". Who saw it depended on the site:
- with Gutenberg neither installed nor activated: single-site administrators and multisite super admins;
- with Gutenberg installed and activated: users with the Contributor role and above;
- with the Classic Editor plugin active: nobody.
The same release carried 18 privacy fixes for the tools added in 4.9.6. The order matters for anyone reading old changelogs: the block editor was never bundled in 4.9. It was a separate plugin, and 4.9.8 was the first core release that pointed users to it.
## WordPress 5.0 Gutenberg vs the Classic Editor
WordPress 5.0 "Bebo", released on 6 December 2018, introduced the Gutenberg block editor and fundamentally changed how users create and edit content in WordPress. The community's reaction to this change was, to put it mildly, polarized. The transition from the classic TinyMCE-based editor to a block-based approach represented the most significant change to the WordPress editing experience in the platform's history.
The community split into two distinct camps following Gutenberg's introduction. Traditionalists, including many experienced developers and site administrators, expressed concern about the learning curve, performance implications, and the loss of familiar workflows. This group adopted the "Classic Editor" plugin, which still has millions of active installations and continues to receive updates for users who prefer the traditional editing experience.
```php
// Classic Editor plugin - maintaining the traditional WordPress experience
// The plugin has been installed by millions who prefer the classic approach
// Key features preserved:
// - TinyMCE-based content editing
// - No blocks required
// - Familiar keyboard shortcuts
// - Traditional metabox workflow
```
Modern creators, on the other hand, embraced the block-based approach, recognizing its potential for content flexibility and design consistency. The block editor introduced a new paradigm where content is constructed from reusable, configurable components rather than a single rich text field. This approach aligned with broader industry trends toward component-based architecture and made it easier to create consistent, responsive layouts without custom code.
## What shipped in WordPress 5.0 Bebo
The [5.0 release post](https://wordpress.org/news/2018/12/bebo/) names three parts of the release. The block editor, which it calls "the first step toward an exciting new future with a streamlined editing experience across your site". Twenty Nineteen, a new default theme whose styles match the default blocks, so what an editor sees in the editor matches the live site. And a supported way back: sites could install the Classic Editor plugin from the plugins screen to restore the previous editor. The post records 423 contributors with props in the release, led by Matt Mullenweg with eleven co-leads.
## How long the Classic Editor plugin is supported
…[truncated, fetch the canonical URL for the full body]…
---
## WordPress incident response under NIS2: 24-hour early warning playbook
URL: https://wppoland.com/en/wordpress-incident-response-nis2-24h-playbook/
Description: Article 23 of NIS2 gives 24 hours from awareness to file an early warning with the CSIRT. This playbook lists the WordPress-specific signals that trigger the clock and the template I file when the clock starts.
Published: Wed Apr 29
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: nis2, article-23, incident-response, csirt, early-warning, wordpress
# WordPress incident response under NIS2: 24-hour early warning playbook
[Article 23 of Directive 2022/2555](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32022L2555) compresses the early phase of incident response into three deadlines: 24 hours, 72 hours, one month. The 24-hour clock is the one that catches WordPress operators off guard, because it starts at awareness, not at remediation, and awareness on a typical WordPress site can come from a Slack message rather than a SOC dashboard.
This is a supporting article in the [NIS2 and DORA on WordPress pillar](/en/nis2-dora-wordpress-compliance-2026/), with cross-references to the [Annex II evidence trail](/en/nis2-annex-ii-wordpress-agencies-evidence-trail/) and the [DORA Article 28 supplier audit](/en/dora-article-28-ict-third-party-risk-wordpress/).
## TL;DR
- 24 hours from awareness: early warning to the CSIRT.
- 72 hours from awareness: incident notification with initial assessment.
- 1 month after the 72-hour notification: final report with root cause and remediation.
- "Awareness" is the trigger, not a SIEM alert.
- Six WordPress signals that I treat as "the clock starts now".
- File templates per language are in this playbook.
## What NIS2 Article 23 requires
Article 23(1) sets the obligation: notify the CSIRT or the competent authority of any incident having a significant impact on the provision of their services. Article 23(3) defines significant: capable of causing severe operational disruption of the services or financial loss for the entity, or capable of affecting other natural or legal persons by causing considerable material or non-material damage.
Article 23(4) splits the timeline:
- **(a) Without undue delay and in any event within 24 hours of becoming aware of the significant incident**: an early warning indicating whether it is suspected to be caused by unlawful or malicious acts, whether it could have a cross-border impact.
- **(b) Without undue delay and in any event within 72 hours**: an incident notification updating the early warning information and providing an initial assessment, including severity, impact, indicators of compromise.
- **(c) On request from the CSIRT or competent authority**: an intermediate report on the relevant status updates.
- **(d) Not later than one month after the incident notification under (b)**: a final report including detailed description, threat type or root cause, mitigation measures applied, cross-border impact if any.
The clock starts at **awareness of the significant incident**. The Directive does not define that moment, and recital 102 only repeats the deadlines. This playbook records awareness when a competent person in the entity concludes with reasonable confidence that a significant incident occurred, and writes down who concluded it and on what evidence. This matters for WordPress because awareness usually comes from a customer email, an external monitoring service, or a vulnerability scanner, not from an internal SOC.
## When an incident is significant under Implementing Regulation 2024/2690
Article 23(3) of the Directive gives the general test. For some entity types the Commission made it concrete. [Implementing Regulation (EU) 2024/2690](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32024R2690) covers DNS service providers, TLD name registries, cloud computing service providers, data centre service providers, content delivery network providers, managed service providers, managed security service providers, providers of online marketplaces, online search engines and social networking services platforms, and trust service providers (Article 1). A WordPress agency that runs hosting, maintenance and monitoring for clients may fall into the managed service provider category. Whether it does is a classification under the Directive's annexes and national law, not something this playbook decides.
For those entities, Article 3(1) of the Regulation treats an incident as significant when one or more of these criteria are met:
- direct financial loss above EUR 500 000 or 5% of the entity's total annual turnover in the preceding financial year, whichever is lower;
- exfiltration of trade secrets within the meaning of Directive (EU) 2016/943;
- the death of a natural person, or considerable damage to a person's health;
- a successful, suspectedly malicious and unauthorised access to network and information systems capable of causing severe operational disruption;
- the entity-specific thresholds in Articles 5 to 14.
For managed service providers and managed security service providers, Article 10 sets those thresholds: the service is completely unavailable for more than 30 minutes; availability is limited for more than 5% of its users in the Union, or more than 1 million users, whichever is smaller, for more than one hour; or the integrity, confidentiality or authenticity of data related to the service is compromised as a result of a suspectedly malicious action, or with an impact on more than 5% of users or 1 million users, whichever is smaller.
Two rules change triage on a WordPress estate. Article 3(2) excludes scheduled interruptions and the planned consequences of scheduled maintenance, so an announced maintenance window is not a significant incident under this test. Article 4 adds recurring incidents: incidents that are not significant on their own count together as one significant incident when they occurred at least twice within six months, have the same apparent root cause and together meet the financial loss criterion. Point 3.4.2(b) of the Annex asks for a quarterly check for such recurring incidents. On WordPress, repeated reinfection through the same unpatched plugin is the pattern that check should catch.
Entities outside the Article 1 list still apply the Article 23(3) test as transposed into national law.
## Signs a WordPress site has been compromised
These findings require an incident record and immediate significance triage. They do not automatically start a NIS2 deadline. Article 23 applies when an in-scope entity becomes aware that an incident is significant under the applicable rules.
**1. Defacement on a customer-facing page.** Search-engine indexed defacement, replaced homepage, replaced product pages. The visibility makes severity assessment easy: customer trust impact is automatic.
**2. Compromised admin user with confirmed login.** A new admin account, an existing admin account showing logins from an unusual IP range or country, an admin account changing other accounts. Logged in audit-log plugins (WP Activity Log, Stream, Wordfence audit log).
**3. Malicious plugin install or theme upload.** A plugin file appearing in `wp-content/plugins/` outside the regular update channel, or with PHP that calls `eval`, `base64_decode`, `gzinflate`, `assert` on user-supplied input. Identified by file integrity monitoring or Wordfence scan.
**4. Database injection with confirmed write.** SQL injection that managed to insert rows or modify rows. Confirmed by audit-log diff or by `wp_users` / `wp_options` tampering. The classic signal: `siteurl` option suddenly points to a domain you do not own.
**5. Unauthorised personal data export.** WordPress export, REST API call returning user lists, large `wp-json/wp/v2/users` response from an unexpected IP. GDPR Article 33 may apply alongside NIS2 Article 23.
**6. Ransomware or filesystem encryption on the production server or backups.** Files appearing with new extensions, `.html` ransom notes in upload directories, encrypted backup archives. This is a high-severity signal, but scope and significance still require a recorded decision.
For any of these, the agency runbook says: timestamp the awareness moment in the IR ticket, name the responsible person, start the 24-hour clock, notify the entity's CISO or equivalent inside the first hour. The agency does not file with the CSIRT; the entity does. The agency provides the technical content.
## NIS2 24-hour early warning template
The early warning is intentionally short. The template I file with the entity's compliance team:
```
Entity: [legal name]
Sector: [Annex I or Annex II classification]
National identifier: [tax ID or registry number]
Incident summary
Date and time of awareness: [ISO-8601 with timezone]
Detection source: [audit log / customer report / external scanner / hosting provider notification]
Affected service: [public-facing WordPress site at https://example.com, customer portal, etc.]
Initial classification
Suspected cause: [malicious / accidental / under investigation]
Cross-border impact: [yes / no / under investigation]
Indicators of cross-border impact: [if yes, what suggests it]
Initial impact estimate
Service availability: [up / degraded / down]
Confirmed data access: [none / suspected / confirmed]
Data subjects potentially affected: [count if known, otherwise "under investigation"]
Initial response
Containment actions taken: [credentials rotated, plugin removed, IP blocked, etc.]
Forensics in progress: [yes / no, with vendor name if external]
Next update: [time of 72-hour notification or earlier intermediate]
Contact
Reporter name and role: [CISO, Head of IT, etc.]
Contact email and phone: [direct line]
```
This is the early warning, not the incident notification. The 72-hour notification expands it with severity scoring, indicators of compromise, and a clearer cross-border assessment.
## What goes in the NIS2 72-hour incident notification
Within 72 hours from awareness, the notification adds:
- **Severity assessment.** Number of affected users, geographic spread, financial loss estimate, recovery time estimate.
- **Indicators of compromise.** IP addresses, file hashes, URLs, malicious user-agent strings, attack signatures. Article 23(4)(b) asks for them where available.
- **Cross-border impact assessment.** Confirmed or ruled out, with reasoning.
- **Containment status.** What is contained, what is still open.
- **Cooperation needs.** Whether the entity needs help from the CSIRT or other authorities.
For WordPress, the indicators block usually contains: attacker IPs from access logs, malicious file paths in `wp-content/`, hashes of malicious files, the user-agent strings observed during exploitation, and any HTTP request bodies preserved by the WAF.
## What the NIS2 final incident report must contain
Article 23(4)(d): final report not later than one month after the incident notification. Contents:
- Detailed description of the incident.
- Threat type or root cause.
- Applied and ongoing mitigation measures.
- Cross-border impact, where applicable.
For WordPress incidents, the root cause section typically lands on one of: [outdated plugin with known CVE](/en/wordpress-security-audit-outdated-plugins-cve-2026/), weak admin credentials without MFA, exposed `wp-config.php` from a backup file, RCE via theme function, [supply-chain compromise of an update channel](/en/wordpress-plugin-supply-chain-2026-four-backdoors-one-month/), server-level misconfiguration. The mitigation section maps the root cause to the [Article 21(2)](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32022L2555) measure that should have prevented it and updates the entity's risk register.
## What Article 23 requires beyond the three NIS2 reports
The 24-hour, 72-hour and one-month deadlines are the part everyone remembers. The same article carries more duties and one protection:
…[truncated, fetch the canonical URL for the full body]…
---
## WordPress REST API vs. GraphQL in 2026: The architectural showdown
URL: https://wppoland.com/en/wordpress-rest-api-vs-graphql-2026-en/
Description: Choosing between REST and GraphQL for your headless WordPress project in 2026? We compare performance, developer experience, and scaling for modern apps.
Published: Sat Dec 27
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: headless, graphql, rest-api, javascript, architecture
The "Headless WordPress" revolution has matured. In 2026, the question is no longer *if* you should [decouple your frontend](/en/headless-wordpress-architecture-2026-enterprise-guide/), but *how* you should fetch your data. The choice sits between the **native REST API** in WordPress core and **GraphQL**, which on WordPress almost always means the WPGraphQL plugin.
The decision now rests on engineering requirements you can measure: how many requests a page needs, how much JSON the browser has to parse, how responses get cached at the edge, how the frontend gets its types, and who maintains the extra code. Both options are mature. This guide compares them on those points, with every API name, setting and default checked against the [REST API Handbook](https://developer.wordpress.org/rest-api/) and the [WPGraphQL documentation](https://www.wpgraphql.com/docs/).
---
## 1. Differences between REST API and GraphQL in WordPress
```
REST (endpoint-driven)
GET /wp-json/wp/v2/posts/101
GET /wp-json/wp/v2/users/4
GET /wp-json/wp/v2/categories?post=101
= several HTTP requests, each with a fixed response shape
GraphQL (document-driven)
POST /graphql
query SinglePost {
post(id: "101", idType: DATABASE_ID) {
title
date
author { node { name } }
categories { nodes { name slug } }
}
}
= one HTTP request, the client names every field it wants
```
The REST API is built on fixed resources. A request to `/wp-json/wp/v2/posts` returns the full post representation: rendered content, excerpt, GUID, comment status, links and more. WPGraphQL inverts this. The client sends a query document, and the server returns only the fields that document names.
### Over-fetching and under-fetching in the REST API
The WPGraphQL team describes the REST problem as simultaneous over-fetching and under-fetching: the posts endpoint returns category IDs, tag IDs, content and excerpt that a list view does not need, yet still lacks the author name and avatar, which takes more requests ([WPGraphQL vs WP REST API](https://www.wpgraphql.com/docs/wpgraphql-vs-wp-rest-api)).
Core has two answers to this, and they are often overlooked. The `_fields` global parameter returns only a subset of fields, for example `?_fields=id,title,link`, and since WordPress 5.3 it supports nested properties such as a single meta key. The `_embed` parameter includes linked resources (author, featured media, terms) in the same response, and since WordPress 5.4 you can limit it to specific link relations ([Global parameters](https://developer.wordpress.org/rest-api/using-the-rest-api/global-parameters/)). With both, a REST list view can come close to a GraphQL payload. What REST still lacks is a typed schema the client can query against.
---
## 2. WordPress REST API pros and cons
The WordPress REST API has been part of core for a decade, and it is more robust than ever.
* **The strength**: It works out of the box. No extra plugins are required for basic functionality. It is highly cacheable at the HTTP level using standard CDNs, because every resource has its own URL.
* **The weakness**: **Over-fetching** by default. If you only need a post title and slug and forget `_fields`, REST sends the whole post object, including content and metadata. This wastes bandwidth, especially on mobile.
The REST API uses JSON for requests and responses, and any language that can make HTTP requests and read JSON can use it ([REST API Handbook](https://developer.wordpress.org/rest-api/)). That is why almost every integration platform, CRM connector and webhook consumer speaks REST first.
---
## 3. WPGraphQL pros and cons
GraphQL (usually implemented via WPGraphQL) allows the frontend developer to define the shape of the response.
* **The strength**: One request, exactly the data you want. You can fetch a post, its categories, the author's latest 3 posts and the site settings in a **single network round-trip**. The schema is strongly typed, and every field declares its type.
* **The weakness**: It requires an additional plugin to maintain. Query complexity can sometimes lead to slow database lookups if it is not optimised with an object cache (Redis).
The plugin facts, read from the [wordpress.org plugin directory](https://wordpress.org/plugins/wp-graphql/) on 14 October 2026: WPGraphQL 2.24.0, requires WordPress 6.0 and PHP 7.4, tested up to WordPress 7.0.7, more than 30,000 active installations. The plugin page states that WPGraphQL is becoming a Canonical Plugin on WordPress.org. It follows Semantic Versioning, so a major version bump signals breaking changes and is worth reading in the release notes before you update.
---
## 4. How WPGraphQL solves the N+1 problem
A naive resolver for "the last 20 posts and each author's name" runs one query for the posts and then one query per author. That is the N+1 problem. WPGraphQL avoids it with the DataLoader pattern: during resolution it collects the IDs it needs (all `post_author` values, for example) and loads them in one batch. In the WPGraphQL documentation's own example, the SQL queries for five posts' authors drop from five to one ([WPGraphQL vs WP REST API](https://www.wpgraphql.com/docs/wpgraphql-vs-wp-rest-api)).
```sql
SELECT * FROM wp_users WHERE ID IN (2, 5, 8, 14);
```
On top of that, WPGraphQL has a Model Layer: a set of PHP classes that wrap WordPress objects and check sensitive fields, such as a user's email address, before they are returned ([WPGraphQL concepts](https://www.wpgraphql.com/docs/wpgraphql-concepts)).
### How to extend the WPGraphQL schema in PHP
```php
__('Measured Core Web Vitals for a post', 'wppoland'),
'fields' => [
'lcpSeconds' => ['type' => 'Float'],
'inpMilliseconds' => ['type' => 'Int'],
'clsScore' => ['type' => 'Float'],
],
]);
register_graphql_field('Post', 'cwvMetrics', [
'type' => 'PerformanceMetrics',
'resolve' => function ($post): ?array {
$raw = get_post_meta($post->databaseId, '_wppoland_cwv_data', true);
if (!is_array($raw)) {
return null;
}
return [
'lcpSeconds' => isset($raw['lcp']) ? (float) $raw['lcp'] : null,
'inpMilliseconds' => isset($raw['inp']) ? (int) $raw['inp'] : null,
'clsScore' => isset($raw['cls']) ? (float) $raw['cls'] : null,
];
},
]);
});
```
---
## 5. How to create a custom REST endpoint in WordPress
The core pattern is `register_rest_route()` on the `rest_api_init` hook, or a class that extends `WP_REST_Controller`. The handbook rules that matter in production ([Adding custom endpoints](https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/)):
- Since WordPress 5.5, a route registered without `permission_callback` triggers a `_doing_it_wrong` notice. For a public endpoint, pass `__return_true` explicitly.
- Namespaces should follow the `vendor/v1` pattern, and the version goes up when you break compatibility.
- Errors are a `WP_Error` with the HTTP code set in the `status` key of its data, for example `400` for bad input.
- When you wrap an existing callback, run its return value through `rest_ensure_response()`.
```php
namespace, '/' . $this->rest_base . '/(?P\d+)', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_item'],
'permission_callback' => '__return_true',
'args' => [
'id' => ['type' => 'integer', 'required' => true],
],
],
'schema' => [$this, 'get_public_item_schema'],
]);
}
public function get_item($request)
{
$post = get_post((int) $request['id']);
if (!$post || $post->post_status !== 'publish') {
return new WP_Error('rest_post_invalid_id', __('Not found.', 'wppoland'), ['status' => 404]);
}
return rest_ensure_response([
'id' => $post->ID,
'title' => get_the_title($post),
]);
}
public function get_item_schema(): array
{
return [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'wppoland_metrics',
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'title' => ['type' => 'string'],
],
];
}
}
add_action('rest_api_init', function (): void {
(new Wppoland_Metrics_Controller())->register_routes();
});
```
The properties are declared without a type on purpose: `WP_REST_Controller` declares `$namespace` and `$rest_base` untyped, and PHP refuses a typed redeclaration in a child class. For the full controller walkthrough see our guide to [custom REST API endpoints](/en/wp-rest-api-custom-endpoints/).
---
## 6. Is GraphQL faster than REST in WordPress
In 2026, performance isn't just about speed; it's about efficiency.
* **Network latency**: GraphQL wins on complex pages. By bundling multiple data points into one request, it removes the sequential round-trips that a REST page with post, author, terms and related posts needs.
* **Caching strategy**: REST wins on ease of use. Since every endpoint is a unique URL, you can cache it globally on Cloudflare with little effort. GraphQL needs GET requests or persisted queries to achieve similar edge-caching results.
* **Server CPU**: REST is usually lighter for one simple resource. GraphQL has to parse and validate the query document before it resolves anything, which is why an object cache matters more on a GraphQL site.
The WPGraphQL team published one measurement: fetching 100 posts took 335 kb and 7.91 s from the REST endpoint against 6.4 kb and 67 ms from WPGraphQL ([WPGraphQL vs WP REST API](https://www.wpgraphql.com/docs/wpgraphql-vs-wp-rest-api)). Treat that as the vendor's own test with default REST output. A REST request with `_fields` closes much of the payload gap, so measure your own pages before you choose on this argument alone.
---
## 7. How to cache GraphQL at the CDN with persisted queries
GraphQL queries are usually sent as HTTP POST with the query in the body, and a CDN does not cache POST by URL. WPGraphQL's own guidance is that POST is the typical method and GET has limited support ([Interacting with WPGraphQL](https://www.wpgraphql.com/docs/interacting-with-wpgraphql)). Edge caching is handled by the companion plugin **WPGraphQL Smart Cache** (version 2.3.3, more than 6,000 active installations, now part of the WPGraphQL monorepo, per its [plugin page](https://wordpress.org/plugins/wpgraphql-smart-cache/)).
How it works, according to the Smart Cache documentation:
…[truncated, fetch the canonical URL for the full body]…
---
## Video SEO in 2026: Integrating Rich Media with WordPress
URL: https://wppoland.com/en/wordpress-video-seo-guide-2026-en/
Description: In 2026, text is just the beginning. Learn how to optimize video content for YouTube, TikTok, and direct Google search integration using WordPress.
Published: Sun Jun 29
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: video-seo, rich-media, youtube, schema, performance
Text alone is no longer enough to win the SEO war. **Video SEO** is the engine that drives high-intent traffic to your WordPress site. If a search engine can't "watch" your content, it might ignore it entirely.
In 2026, video sits high in search results for how-to, review and comparison queries, and Google's AI features favour pages that answer in both text and usable media. Google publishes no share figure for this, so the percentages circulating in SEO posts are marketing, not measurement. This guide shows you how to integrate and optimize video content on your WordPress site for stronger visibility and useful engagement.
## How to optimize video for SEO in 2026
If you are looking for a single, actionable guide to video SEO in 2026, this is it. The landscape has shifted: video units now sit above the fold for many informational queries, and a page that answers in text and video holds attention longer than text alone. Neither effect has a published figure behind it, which is why none appears here.
This guide covers everything in a practical sequence:
1. **Integration strategy** -- how to connect YouTube and WordPress for maximum search visibility
2. **Technical implementation** -- VideoObject schema, Key Moments, and structured data
3. **Performance** -- lazy loading video players without destroying [Core Web Vitals](/en/services/core-web-vitals-audit/)
4. **Conversion** -- using video as E-E-A-T trust signals
5. **Platform tactics** -- YouTube, TikTok, and direct Google Video optimization
6. **Measurement** -- tracking video SEO ROI with real metrics
Each section includes copy-paste code examples and WordPress-specific implementation details.
## 1. How to embed YouTube videos in WordPress for SEO
YouTube remains the second largest search engine after Google itself. In 2026, successful content creators use a "Dual-Publishing" strategy that maximizes reach across both platforms.
### How to publish a video on YouTube and WordPress
1. **Upload to YouTube First**: Optimize your video with keyword-rich titles, detailed descriptions, and relevant tags
2. **Embed on WordPress**: Use the optimized embed code with custom parameters
3. **Add Unique Commentary**: Surround the video with original text content that adds value
4. **Include Transcripts**: Provide full text transcripts for accessibility and SEO
### Where an embedded YouTube video gets traffic
This approach captures traffic from multiple sources simultaneously:
- YouTube's internal search and recommendation algorithm
- Google's "Video" tab search results
- Google's main search results with video thumbnails
- Social media sharing across platforms
### How to optimize video on YouTube and on WordPress
**YouTube Optimization:**
- Titles: Front-load primary keywords, keep under 60 characters
- Descriptions: First 100 characters appear in search; include timestamps and links
- Tags: Use 5-8 specific tags rather than 15+ generic ones
- Thumbnails: Custom 1280x720 images with readable text at small sizes
**WordPress Optimization:**
- Surround videos with 300+ words of unique content
- Use descriptive headings that include target keywords
- Implement proper schema markup (detailed below)
- Add internal links to related content
## 2. How to add VideoObject schema in WordPress
Without the correct schema markup, your video is just a black box to search engines. The VideoObject schema tells Google exactly what your video contains, enabling rich snippets and special search features.
### Which properties VideoObject schema needs
```json
{
"@context": "https://schema.org",
"@type": "VideoObject",
"name": "How to Optimize WordPress for Core Web Vitals",
"description": "Complete guide to improving WordPress site speed and passing Core Web Vitals assessment in 2026.",
"thumbnailUrl": [
"https://example.com/thumbnail-480x360.jpg",
"https://example.com/thumbnail-1280x720.jpg"
],
"uploadDate": "2026-01-15T10:00:00+00:00",
"duration": "PT12M30S",
"contentUrl": "https://example.com/video.mp4",
"embedUrl": "https://www.youtube.com/embed/VIDEO_ID",
"interactionStatistic": {
"@type": "InteractionCounter",
"interactionType": { "@type": "WatchAction" },
"userInteractionCount": 15234
}
}
```
### How to add video chapters for key moments
Google's "Key Moments" feature allows users to jump directly to specific sections of your video from search results. Implement this using the `hasPart` property:
```json
{
"@context": "https://schema.org",
"@type": "VideoObject",
"name": "WordPress Security Best Practices",
"hasPart": [
{
"@type": "Clip",
"name": "Introduction to WordPress Security",
"startOffset": 0,
"endOffset": 120,
"url": "https://example.com/video/?t=0"
},
{
"@type": "Clip",
"name": "Essential Security Plugins",
"startOffset": 120,
"endOffset": 480,
"url": "https://example.com/video/?t=120"
},
{
"@type": "Clip",
"name": "Two-Factor Authentication Setup",
"startOffset": 480,
"endOffset": 720,
"url": "https://example.com/video/?t=480"
}
]
}
```
### Adding video schema with a plugin or theme code
Use a plugin such as Schema Pro or the separately licensed [Yoast Video SEO](https://yoast.com/wordpress/plugins/video-seo/) add-on, which adds VideoObject and video sitemaps for the videos it detects, or print the markup yourself from a must-use plugin or child theme:
```php
// Prints VideoObject JSON-LD from a post meta array (keys: name, description,
// thumbnailUrl, uploadDate, duration, embedUrl or contentUrl).
add_action('wp_head', function () {
if (!is_singular()) {
return;
}
$video = get_post_meta(get_the_ID(), 'video_schema', true);
if (empty($video['name']) || empty($video['thumbnailUrl']) || empty($video['uploadDate'])) {
return;
}
$schema = array_merge(['@context' => 'https://schema.org', '@type' => 'VideoObject'], $video);
echo '';
});
```
## 3. How to lazy load YouTube videos in WordPress
Video embeds are traditionally heavy and can ruin your LCP (Largest Contentful Paint) scores. A single YouTube embed can add 2MB+ to your page weight and significantly delay interactivity.
### How the YouTube facade pattern works
This applies to article and product pages where the video supports the text. A dedicated watch page is the exception, explained in the facade section further down.
Never load the video player on initial page load. Instead, use a **Thumbnail Placeholder** that only loads the actual player when the user interacts with it.
**How It Works:**
1. Display a high-resolution thumbnail image with a play button overlay
2. Load the heavy JavaScript only when the user clicks or scrolls the video into view
3. Replace the thumbnail with the actual video player on demand
**Implementation Example:**
```html
```
```javascript
// Lazy load video on interaction
document.querySelectorAll('.video-facade').forEach(facade => {
facade.addEventListener('click', () => {
const videoId = facade.dataset.videoId;
facade.innerHTML = `
`;
});
});
```
### How lazy loading video affects Core Web Vitals
| Metric | Direct Embed | Lazy-Loaded |
|--------|--------------|-------------|
| LCP | 3.2s | 1.1s |
| TTI | 4.8s | 1.9s |
| Page Weight | +2.1MB | +45KB |
| Mobile Score | 42/100 | 94/100 |
## 4. How video supports E-E-A-T and conversions
In 2026, video isn't just for entertainment - it's a critical component of [E-E-A-T](/en/wordpress-seo-2026-eeat-ai-search-guide/) (Experience, Expertise, Authoritativeness, Trustworthiness) that Google uses to evaluate content quality.
### How video builds trust
**Expert Interviews**: Featuring recognized experts in your videos builds credibility that text alone cannot achieve. Seeing a real person speak creates a connection that establishes trust.
**Product Demonstrations**: On a [WooCommerce site](/en/woocommerce-developer/), a demo video shows the product working. Marketing surveys quote high conversion lifts for that format; the original studies are vendor surveys, not Google measurements, so we do not repeat a percentage. Show the product in action anyway.
**Behind-the-Scenes Content**: Authentic glimpses into your process humanize your brand and differentiate you from AI-generated content.
### Which video types convert best
1. **Tutorial Videos**: How-to content that solves specific problems
2. **Testimonials**: Customer success stories in video format
3. **Product Comparisons**: Side-by-side feature demonstrations
4. **FAQ Videos**: Address common objections and questions
5. **Webinar Recordings**: Long-form educational content
### How to show E-E-A-T in a video
- Show the speaker/expert clearly in the video
- Include credentials and experience in the video description
- Link to author profiles and social media
- Use professional production quality (good lighting, clear audio)
- Include publication dates to show content freshness
## 5. YouTube, TikTok and LinkedIn video SEO
### YouTube SEO in 2026
YouTube's algorithm has evolved to prioritize watch time and engagement over simple view counts:
**Title Optimization:**
- Front-load primary keywords
- Use numbers and brackets for CTR boost: "[2026 Guide]"
- Keep under 60 characters for full display
- Example: "WordPress SEO: 10 Tips for 2026 [Tutorial]"
**Description Best Practices:**
- First 100 characters are crucial (appear in search)
- Include timestamps for longer videos
- Add relevant links with UTM parameters
- Use 3-5 relevant hashtags
**Thumbnail Strategy:**
- 1280x720 resolution minimum
- Readable text at small sizes (under 20 characters)
- High contrast colors
- Consistent branding across your channel
- Include human faces when possible (increases CTR)
### Does TikTok video help SEO
Short-form video (under 60 seconds) has exploded in 2026. While TikTok doesn't directly impact Google rankings, it drives significant brand awareness and traffic:
- Repurpose long-form content into 30-60 second clips
- Use trending sounds and hashtags
- Post consistently (1-3 times daily)
- Include clear CTAs linking to your WordPress site
- Embed TikTok videos on your site for cross-promotion
### How to use LinkedIn video for B2B
For WordPress agencies and B2B service providers:
- Native LinkedIn video usually holds attention better than a bare link; measure yours, do not copy an infographic multiple
- Keep videos under 2 minutes for maximum reach
- Use professional captions (many watch without sound)
- Share case studies and client success stories
## 6. Video vs text content for SEO
| Metric | Text-only article | Article + video | Video-first page |
|--------|-------------------|-----------------|------------------|
| Time on page | Baseline | Wistia measured about 1.4x vs pages without video | Measure on your own property |
| Bounce and conversion | Do not copy a template rate | Do not copy a template rate | Read Search Console and shop analytics |
| Search visibility | Web results | Web + video features | Web + video + YouTube |
| What a shopper can see | Specs and photos | Specs plus a working demo | Demo first, text as support |
## 7. Best WordPress video plugins for SEO
### Presto Player, Videopack and WP Rocket for video
…[truncated, fetch the canonical URL for the full body]…
---
## How to downgrade WordPress? (Plugin, FTP, wp-CLI)
URL: https://wppoland.com/en/how-to-downgrade-wordpress-rollback-guide/
Description: Update crashed your site? Don't panic. See 3 proven ways to rollback WordPress core, plugins, or themes to a previous version.
Published: Tue Jan 03
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: update, backup, wp-cli, debugging
You click "Update", wait a moment, and suddenly see the "White Screen of Death" or a critical PHP error. Your site is down, and the client is calling.
Don't panic. In WordPress, you can almost always revert changes. In this guide, I'll show you three methods for a **downgrade**, ranging from easiest to most advanced. Before that, one step saves you from fixing the wrong thing: figure out what actually broke, so you roll back the specific component at fault instead of guessing.
---
## How to find which update broke your WordPress site
A rollback is only useful if you revert the right thing. If a theme update broke the layout and you downgrade WordPress core, you waste time and add risk. Spend five minutes on diagnosis first.
### How to enable WP_DEBUG and read debug.log
WordPress hides fatal errors behind the white screen by default. Switch on debugging so the real message is written to a file rather than shown to visitors. Edit `wp-config.php`, above the line that says `/* That's all, stop editing! */`:
```php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
```
With `WP_DEBUG_LOG` on, errors land in `wp-content/debug.log`. Open that file and read the last entries. A typical line names the exact file and function, for example a fatal error in `wp-content/plugins/some-plugin/includes/class-loader.php`. That path tells you which plugin is at fault before you touch anything.
If `debug.log` is empty or missing, check your host's own logs. Most panels expose a PHP error log and a server (Apache or Nginx) error log under a "Logs" or "Metrics" section. The server log catches memory-limit and timeout failures that never reach the WordPress log.
### How to find the faulty plugin by deactivating plugins
If the log points at a plugin, or if nothing is conclusive, deactivate plugins to narrow it down. With wp-admin still reachable, deactivate them one at a time and reload the broken page after each. When the error clears, the last plugin you switched off is the cause.
If you are locked out of the dashboard, do the same over FTP or SSH. Renaming the whole `wp-content/plugins` folder to `plugins_off` deactivates every plugin at once. If the site returns, rename it back and disable plugins individually until the offender shows itself. Over WP-CLI the same check is two commands: `wp plugin deactivate --all`, then reactivate one by one.
### Should you downgrade core, a plugin, a theme or PHP
Match the symptom to the layer, because each one rolls back differently:
- **A plugin** is the most common cause. Downgrade that single plugin (Method 1 or 2 below) and leave everything else alone.
- **A theme** update usually breaks layout, styling, or the block editor rather than throwing a fatal error. Roll the theme back, or switch to a default theme like Twenty Twenty-Four to confirm.
- **WordPress core** is the culprit when the site broke immediately after a core update and plugins are ruled out. Core needs Method 2 or 3.
- **A PHP version bump** breaks the site when your host upgraded PHP (often automatically), not WordPress. The tell is a fatal error mentioning a deprecated or removed function even though you did not update anything in WordPress. That is a hosting-panel fix, covered further down.
Only once you know which layer failed does picking a rollback method make sense.
---
## What to check before you downgrade WordPress
A rollback destroys evidence. Before you replace a single file, record what the site is running right now, so you can move forward again once the bug is patched and so you can compare states if the rollback does not help.
With WP-CLI it takes under a minute:
```bash
wp core version --extra
wp plugin list --fields=name,status,version,update_version --format=csv > plugins-before.csv
wp theme list --fields=name,status,version --format=csv > themes-before.csv
wp db export backup-before-rollback.sql
```
`wp core version --extra` prints the core version together with the database revision that the running code expects. Compare it with `wp option get db_version`, the revision actually stored in the database: if the stored number is higher than the one your target release expects, the database has already migrated forward and a file-only rollback will not match it. The two CSV files give you the exact versions to return to afterwards, instead of guessing from memory at midnight.
Two plugin headers decide whether a core downgrade is even possible. A plugin can declare `Requires at least` and `Requires PHP` in its main file header, and where those headers are present WordPress blocks activation when the running core or PHP version does not meet them. So if a plugin declares WordPress 6.6 and you drop core to 6.4, you cannot reactivate it after the downgrade, and its code may call core functions that the older release does not ship. Read the headers before you pick a target version:
```bash
grep -RE "Requires (at least|PHP)" wp-content/plugins/*/*.php
```
Finally, put the site behind maintenance mode so visitors and crawlers get a 503 instead of a half-replaced install: `wp maintenance-mode activate`, and `wp maintenance-mode deactivate` when the work is done. A 503 with a short retry window is the response Google expects during maintenance, so it does not treat the outage as a permanent change.
---
## Method 1: revert a plugin update with WP Rollback
If you still have access to the Dashboard (wp-admin), you are saved. The best tool for reverting plugins and themes is the free **WP Rollback** plugin.
1. Install and activate **WP Rollback**.
2. Go to **Plugins > Installed Plugins**.
3. A new link **"Rollback"** will appear next to each plugin.
4. Click it and choose the version you want to return to (e.g., from 5.4 to 5.3).
5. Confirm. Done!
**Note:** The free version of WP Rollback works with plugins and themes from WordPress.org, according to its [plugin page](https://wordpress.org/plugins/wp-rollback/); premium plugins need the paid Pro version. It does not revert WordPress core. For core, use a separate plugin, **Core Rollback** by Andy Fragen, which sits under **Tools**.
---
## Method 2: downgrade the WordPress version with WP-CLI
If you have SSH access (see our [SSH guide](/en/ssh-for-wordpress-developers-guide/)), this is the fastest and safest method.
To downgrade WordPress Core:
```bash
wp core update --version=7.0.7 --force
```
The `--force` parameter, in the words of the [WP-CLI docs](https://developer.wordpress.org/cli/commands/core/update/), updates "even when installed WP version is greater than the requested version".
To downgrade a plugin (e.g., WooCommerce):
```bash
wp plugin install woocommerce --version=8.5.0 --force
```
Here `--force` belongs to `wp plugin install`, where it overwrites any installed version without prompting ([docs](https://developer.wordpress.org/cli/commands/plugin/install/)). `wp plugin update` has no `--force` flag, although it also accepts `--version`. Themes follow the same pattern: `wp theme install twentytwentyfour --version=1.0 --force`.
### Which WordPress version to roll back to
Not every older release is a safe target. WordPress.org publishes the security status of every core release through the [stable-check API](https://api.wordpress.org/core/stable-check/1.0/), and the Core Rollback plugin uses the same list: its [description](https://wordpress.org/plugins/core-rollback/) says it only offers the latest release and outdated, secure ones. On 14 October 2026 the API marks 7.1.3 as `latest`, the final point release of each older branch (7.0.7, 6.9.10, 6.8.11 and so on) as `outdated`, and every earlier point release of those branches as `insecure`. The 6.4.3 this guide used as its example until this update is on the insecure list.
So the rule is: roll back to the last point release of the branch that worked, not to the exact number you ran before. If 7.1.x broke the site, the target is 7.0.7, not 7.0.2. Check the status before you run the command:
```bash
curl -s https://api.wordpress.org/core/stable-check/1.0/ | grep '"7.0.7"'
```
The second check is PHP. The core team's [PHP compatibility table](https://make.wordpress.org/core/handbook/references/php-compatibility-and-wordpress-versions/) lists which PHP versions each release is tested with, and a rollback can move you off it:
| Server PHP | Oldest core release the table lists for it |
| --- | --- |
| 8.5 | 6.9 |
| 8.4 | 6.7 |
| 8.3 | 6.4 |
| 8.2 | 6.1 |
If the host has already moved the server to PHP 8.5, rolling back to 6.8 lands on a combination the core team never listed as supported. The limit runs the other way too: WordPress 7.0 dropped support for PHP 7.2 and 7.3, so on such a server 6.9 is the newest release you can run. Pick the target that passes both checks: secure according to stable-check and listed for the PHP version your server runs. `wp eval 'echo PHP_VERSION;'` shows the version WordPress actually sees.
---
## Method 3: downgrade WordPress manually via FTP
If you have no access to the panel or SSH, only "open heart surgery" via FTP remains.
**This is risky. Backup your database before starting!**
1. Go to [WordPress.org Releases](https://wordpress.org/download/releases/) and download the older WP version (zip). If you have WP-CLI on your own computer, `wp core download --version=7.0.7 --skip-content --path=wp-7.0.7` fetches the same release without the default themes and plugins ([docs](https://developer.wordpress.org/cli/commands/core/download/)).
2. Unzip it on your computer.
3. **DELETE** the `wp-content` folder and `wp-config-sample.php` file from the unzipped folder.
* *Why?* So you don't accidentally overwrite your photos, themes, and configuration!
4. Connect via FTP (FileZilla).
5. Upload the remaining files (`wp-admin`, `wp-includes`, root files) to the server, choosing **"Overwrite"**.
After uploading, try accessing `wp-admin`. If WordPress shows **Database Update Required**, confirm with **Update WordPress Database**.
---
## How to restore WordPress from a hosting backup
Before you touch a single file, check whether your host can undo the update for you. On managed WordPress hosting this is often the fastest route, and it needs no command line at all.
Most managed hosts keep automatic daily restore points and expose a one-click restore in the control panel, usually under a "Backups" or "Restore" tab. You pick a snapshot from before the update and confirm. The host rebuilds the site (files and database together) to that moment. Because both layers move back in sync, this sidesteps the schema-mismatch trap described in the next section.
Two things to check first. Some hosts restore files and database separately, so read the labels and choose the full snapshot, not files-only. And a restore overwrites everything since the snapshot, so any orders, comments, or posts created after that point are lost. On a busy store, weigh that against a targeted plugin rollback. For a brochure site that changed nothing overnight, the one-click restore is usually the cleanest option.
---
## How to tell whether the rollback actually worked
"The homepage loads" is not verification. A rollback can leave a half-copied file tree, a stale bytecode cache, or a broken admin path while the front page still renders from cache.
Start by checking the version that is actually in place, not the one you intended to install:
```bash
wp core version
wp plugin get woocommerce --field=version
wp core verify-checksums
wp plugin verify-checksums --all
```
…[truncated, fetch the canonical URL for the full body]…
---
## Core Web Vitals 2026: The Complete INP Optimization Guide for WordPress
URL: https://wppoland.com/en/core-web-vitals-2026-inp-optimization-guide/
Description: How to optimize Interaction to Next Paint (INP) on WordPress sites. Practical fixes for the newest Core Web Vital metric that directly impacts Google rankings.
Published: Sat Mar 21
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: core-web-vitals, inp, interaction-to-next-paint, page-speed, wordpress-performance, lcp, cls
Google's Core Web Vitals are no longer optional for SEO. In 2026, these metrics directly influence search rankings, and **INP (Interaction to Next Paint)** has become the most challenging one for WordPress sites. While LCP and CLS can be fixed with image optimization and layout reservations, INP requires fundamentally rethinking how JavaScript runs on your pages.
INP replaced FID (First Input Delay) as a Core Web Vital in March 2024. The difference is significant: FID only measured the first interaction, while INP measures **every interaction** throughout the entire page visit. A site could score well on FID but terribly on INP - and many WordPress sites do.
## Core Web Vitals thresholds for LCP, INP and CLS
### Good and poor values for LCP, INP and CLS
| Metric | What It Measures | Good | Needs Work | Poor |
|---|---|---|---|---|
| **LCP** (Largest Contentful Paint) | Loading speed | < 2.5s | 2.5-4s | > 4s |
| **INP** (Interaction to Next Paint) | Responsiveness | < 200ms | 200-500ms | > 500ms |
| **CLS** (Cumulative Layout Shift) | Visual stability | < 0.1 | 0.1-0.25 | > 0.25 |
### Why is INP harder to fix than LCP and CLS
LCP is primarily about server speed and [image optimization](/en/wordpress-image-optimisation-critical-css-performance-guide/) - well-understood problems with clear solutions. CLS is about reserving space for dynamic content - a CSS and HTML discipline. INP is about **JavaScript execution efficiency** - a fundamentally harder problem because it requires understanding the browser's main thread, task scheduling, and event handling.
WordPress sites are particularly vulnerable to poor INP because:
1. **Plugin bloat** - every plugin can add JavaScript that blocks the main thread
2. **Page builders** - Elementor, Divi, WPBakery add heavy frontend frameworks
3. **Third-party scripts** - analytics, ads, chat widgets, social embeds all compete for main thread time
4. **jQuery dependency** - many WordPress themes and plugins still rely on jQuery, adding 85KB+ of blocking JavaScript
5. **No code splitting** - traditional WordPress themes load all JavaScript on every page
## How INP works
### What are the three phases of an interaction
When a user clicks a button, taps a link, or presses a key, the browser processes it in three phases:
1. **Input delay** - time between the physical interaction and the browser starting to process the event handler (caused by main thread being busy with other tasks)
2. **Processing time** - time to execute all event handlers for this interaction
3. **Presentation delay** - time to render the visual update after event handlers complete
INP = Input delay + Processing time + Presentation delay
The metric reports the **slowest interaction** of the visit. On busy pages the rule is slightly softer: according to [web.dev](https://web.dev/articles/inp), one highest interaction is ignored for every 50 interactions. Most pages see fewer than 50 interactions per visit, so in practice the worst one is what gets reported. Google then rates the page at the 75th percentile of all visits recorded in the field. This means a single slow interaction can tank your INP score, even if every other interaction is fast. On a typical WordPress site with forms, menus, filters and carousels there are dozens of possible interactions, and one bad one is enough.
INP became a Core Web Vital and replaced FID on 12 March 2024, as announced on [web.dev](https://web.dev/blog/inp-cwv-march-12). FID was deprecated in the same transition and removed from the Search Console report.
### What counts as an interaction
Not every user action registers as an interaction for INP purposes. The distinction matters because optimizing for the wrong events wastes effort.
**These actions DO trigger INP:**
- **Clicks** on buttons, links, toggles, menus, checkboxes, radio buttons
- **Taps** on mobile (touch events including `pointerdown`, `pointerup`, `click`)
- **Key presses** in input fields, search boxes, text areas, and contenteditable elements
- **Keyboard shortcuts** - pressing Enter to submit a form, Space to toggle a checkbox
- **Custom interactive elements** - any element with a `click`, `pointerdown`, or `keydown` handler attached
**These actions do NOT trigger INP:**
- **Scrolling** - scroll is measured separately and does not count as a discrete interaction
- **Hovering** - mouse hover alone doesn't generate an INP entry, even if a tooltip appears
- **Pinch-to-zoom** - this is a continuous gesture, not a discrete interaction
- **CSS-only transitions** - animations triggered purely by CSS (`:hover`, `:focus`) without JavaScript event handlers
- **Page load** - the initial rendering of the page is not an interaction; that's what LCP measures
One nuance: a single physical action like a tap can generate multiple events (`pointerdown`, then `mousedown`, `pointerup`, `mouseup` and finally `click`). The browser groups these into a **single interaction** and measures the total duration from the first event to the final paint. This grouping means you don't get penalized five times for one tap, but it also means slow handlers on any of those grouped events will increase the overall interaction duration.
## Measuring INP on WordPress sites
### Google Search Console (field data)
The most important data source. Navigate to Core Web Vitals, then Mobile or Desktop. Search Console shows real-user INP data aggregated over 28 days. Pages are grouped by URL pattern and rated as Good, Needs Improvement, or Poor.
Always start here. Field data reflects what real users experience, including slow devices, weak connections and interactions that lab tests never simulate. If Search Console reports poor INP, Google is seeing that problem, and it can affect rankings.
### PageSpeed Insights (lab + field)
Enter any URL and get both:
- **Field data** - real user measurements from the Chrome UX Report (CrUX). This is the data Google uses for its assessment
- **Lab data** - simulated measurements from Lighthouse, run in a controlled environment
For INP, field data matters more because lab tests may not trigger the same interactions real users perform. A site can show a clean lab report and still have poor field INP, because real users open complex menus, search filters and forms that Lighthouse never touches.
### Chrome DevTools (debugging)
To diagnose a specific INP problem, Chrome DevTools is the tool. The process:
1. Open DevTools, go to the **Performance** panel and click **Record**
2. Interact with the page: click buttons, open menus, fill in forms
3. Stop the recording and analyse the result
Look for:
- **Long tasks** - tasks over 50ms, marked with a red triangle in the corner. Any task above this threshold blocks the main thread
- **Event handlers** - how long each click or tap takes to process. If one click triggers several handlers, the time adds up
- **Layout thrashing** - forced synchronous layouts during interactions. This happens when JavaScript alternately reads and writes layout properties, forcing the browser to recalculate again and again
### How to read a waterfall chart in DevTools
The Performance panel waterfall can be overwhelming, but a systematic approach makes it manageable. After recording an interaction, focus on these areas:
1. **Find the interaction marker** - Chrome marks discrete interactions with a dashed vertical line in the timeline. Click it to highlight the associated events.
2. **Check the main thread flame chart** - look directly below the interaction marker for the stack of function calls. The widest bars represent the longest-running functions - these are your optimization targets.
3. **Identify the three INP phases** - input delay appears as idle time before the first event handler fires, processing time spans the event handler execution, and presentation delay covers style recalculation, layout, and paint after handlers complete.
4. **Look for forced reflows** - purple "Layout" bars inside event handlers indicate synchronous layout calculations. These appear when JavaScript reads a layout property (like `offsetHeight`) after modifying the DOM, forcing the browser to recalculate layout immediately rather than batching it.
5. **Check the "Interactions" track** - Chrome DevTools has a dedicated Interactions lane that shows each interaction's total duration and its rating (green/yellow/red). This gives you a quick pass/fail before diving into the flame chart.
A common pattern on WordPress sites: clicking a menu toggle triggers a 200ms long task because the event handler forces a reflow by reading `element.getBoundingClientRect()` immediately after adding a CSS class. Moving the DOM read before the DOM write eliminates the forced reflow and can cut interaction time by 60-80%.
## How to improve INP in WordPress
### 1. Defer non-critical JavaScript
The single most impactful fix. Most WordPress plugins load JavaScript in the ``, blocking the main thread before the page even renders:
```html
```
The `defer` attribute tells the browser to download the file in parallel but execute it only after the HTML has been fully parsed. That frees the main thread during the initial load and cuts input delay for early interactions.
Plugins that benefit most from deferring:
- **Analytics** (GA4, GTM) - add `defer` or load via `requestIdleCallback`
- **Chat widgets** (Intercom, Tawk.to) - load after user scroll or after 5 seconds
- **Social embeds** (Facebook, Twitter) - load only when visible (Intersection Observer)
- **Sliders and carousels** - defer initialization until visible
### 2. Remove unused plugin JavaScript
Many plugins load their scripts on every page of the site, whether they are needed there or not. Audit which plugins actually need frontend JavaScript:
```bash
# List the scripts enqueued on the front end
wp eval 'do_action("wp_enqueue_scripts"); global $wp_scripts; foreach ($wp_scripts->queue as $s) echo $s . "\n";'
```
`wp eval` runs outside a page request, so the `wp_enqueue_scripts` hook has to be fired by hand, otherwise the queue is empty. Scripts that a plugin enqueues only for a specific template or query will not show up this way. For a per-page view, the [Query Monitor](https://wordpress.org/plugins/query-monitor/) plugin lists the enqueued scripts of the page you are looking at. From that list, mark the ones that page does not need.
Common offenders:
- **Contact Form 7** loads its CSS and JavaScript on every page, but the forms usually live on one or two pages. Fix: load it conditionally, only where a form exists
- **WooCommerce cart fragments** - the cart fragments AJAX runs on every page, even outside the shop. Fix: disable it on pages that are not part of the store
- **Gutenberg block styles** - WordPress loads CSS and JavaScript for all registered blocks, even ones not used on the page. Fix: strip the styles of unused blocks
Conditional loading can be done in the theme's `functions.php` or with an asset manager plugin such as Asset CleanUp or Perfmatters.
### 3. Break long tasks
Any JavaScript task over 50ms blocks the main thread. While it runs, the browser cannot respond to user input, which directly raises the input delay part of INP. The fix is to split long tasks into smaller pieces and hand control back to the browser between them.
`scheduler.yield()` is the modern API. It is not available in every browser yet (the [web.dev long tasks guide](https://web.dev/articles/optimize-long-tasks) lists Chrome and Edge 129 and Firefox 142, with Safari still missing), so wrap it with a `setTimeout` fallback and yield on a deadline:
…[truncated, fetch the canonical URL for the full body]…
---
## Corporate web development in Spain with Astro and headless WordPress in 2026
URL: https://wppoland.com/en/corporate-web-development-astro-headless-wordpress-spain/
Description: A detailed look at rolling out Astro and headless WordPress for corporate websites in Spain. Mobile performance, Core Web Vitals and SEO in 2026.
Published: Fri May 08
Updated: Wed Oct 14
Tags: astro, headless, guide, 2026
# Corporate web development in Spain with Astro and headless WordPress in 2026
The Spanish corporate market in 2026 demands an unprecedented level of digital rigour. Mid-sized and large companies in Spain understand that their online presence is not a static business card but a direct commercial tool that generates business. With mobile traffic dominating browsing and Google penalising slow sites hard, the classic corporate web architecture built on monolithic CMSs has speed and security limits that are hard to justify to a board of directors.
In response, the **Astro and headless WordPress** architecture has become the technical reference standard in Spain for leading brands in B2B, retail and the institutional sector. The model combines the familiarity and editorial power of WordPress with the speed and security of Astro on the frontend. This technical guide walks through the components of the architecture, integration best practices and their direct impact on SEO and GEO (Generative Engine Optimization) in the Spanish market.
---
## 1. Why use headless WordPress for a corporate website
For years, IT and marketing departments in Spanish companies have been in constant conflict over the organisation's content management system (CMS):
- **Marketing** insists on WordPress because its writing teams know the interface, are fluent in the Gutenberg editor, can schedule posts quickly and rely on the plugin ecosystem for content optimisation.
- **IT / development** rejects WordPress because of its history of security problems (recurring plugin vulnerabilities), the complexity of maintaining Apache/Nginx servers with FPM and, above all, poor load performance caused by repeated queries to the MySQL database and the code overhead injected by classic themes.
A [headless architecture](/en/nextjs-astro-migration/) settles this dilemma at the root:
```mermaid
graph LR
subgraph Backend Privado
A[WordPress Core + Gutenberg] --> B[Base de Datos MySQL]
A --> C[REST API / GraphQL]
end
subgraph Frontend Público
C --> D[Compilación de Build en Astro]
D --> E[Cloudflare Pages & Workers]
E --> F[HTML estático + CSS ligero]
end
```
Once the CMS is decoupled, the marketing team keeps using the usual WordPress dashboard on a restricted subdomain (`wp.empresa.es`) to manage news, landing pages and success stories. The public visitor of the corporate website, however, interacts with an ultra-fast static application compiled by Astro and distributed over Cloudflare's global network (`empresa.es`).
---
## 2. What Astro is and why it loads so fast
Astro is a development framework built specifically for fast, content-driven corporate websites. Its approach differs radically from traditional [single-page application (SPA) frameworks](/en/headless-wordpress-nextjs-vs-astro-2026/) such as React or Angular, which ship large amounts of JavaScript to the visitor's browser and slow down the mid-range mobile devices common in the Spanish market.
### Astro islands architecture explained
Astro uses an approach called the "islands architecture". By default, every page generated in Astro compiles to plain HTML and lightweight CSS, with all unnecessary JavaScript removed. If a section of the page needs interactivity (a dynamic search box, an interactive carousel or a product filter form), Astro creates an "island" of interactivity and injects the required framework code (React, Vue, Svelte or vanilla JS) only for that specific component.
The result is flawless mobile performance:
- **LCP (Largest Contentful Paint)**: the main visual element of the page (usually the hero image or the main title) appears in under 0.8 seconds on most mobile networks.
- **INP (Interaction to Next Paint)**: user interactions with the page get an instant response, removing the lag typical of heavy websites.
### View Transitions and CSS animations in Astro
In the 2026 corporate market, being fast is not enough; the site also has to delight the user visually. Astro has native support for the **View Transitions** API, which enables smooth navigation between pages that mimics a native mobile or desktop app.
- Shared elements (the navigation menu, the logo or the header) stay in place while the page content fades smoothly.
- It supports scroll-driven animations and optimised parallax effects that run directly on the device GPU without blocking the CPU main thread. The result is dynamic interfaces that respond naturally to touch and keep users on the site longer.
### How to serve WordPress images as AVIF in Astro
Image weight is the main cause of slow loading on Spanish corporate websites. Astro ships native optimisation tools that process the files editors upload to the WordPress media library. According to the [Astro images guide](https://docs.astro.build/en/guides/images/), the transformation happens at build time for prerendered pages, and on the fly when a page rendered on demand is viewed.
- **AVIF has to be requested**: it is not the default format. The [`astro:assets` reference](https://docs.astro.build/en/reference/modules/astro-assets/) states that `` produces a `.webp` file by default and that `` uses `['webp']` as its list of formats. To serve AVIF with a WebP fallback, set `formats={['avif', 'webp']}` on ``, or `format="avif"` on ``.
- **Automatic sizing**: it generates several image sizes for different devices (responsive images), so a phone never downloads a high-resolution image meant for desktop screens.
- **Authorised domains**: WordPress images arrive as remote URLs (`wp.empresa.es/wp-content/uploads/...`). Astro only optimises them if the domain is listed in `image.domains` or `image.remotePatterns` in `astro.config.mjs`. Images from unauthorised domains are displayed with no processing.
- **Remote image dimensions**: Astro cannot know the size of a remote file in advance. Pass `width` and `height`, which the [REST API media endpoint](https://developer.wordpress.org/rest-api/reference/media/) already returns in `media_details`, or enable `inferSize`, which defaults to `false`. With the dimensions set, `` prevents layout shift (CLS).
---
## 3. Integrating the WordPress API with Astro
Astro and WordPress communicate asynchronously through the CMS's native APIs. Depending on project complexity, development agencies in Spain choose one of two integration paths:
### A. WordPress REST API
The REST API ships by default in WordPress core. It lets you query posts, pages, categories, tags and custom post types (CPT) through standard HTTP requests. It is the most mature, stable and simplest option for mid-sized projects.
```typescript
// Ejemplo de consulta de posts de WordPress en Astro
export async function getLatestPosts() {
const res = await fetch('https://wp.empresa.es/wp-json/wp/v2/posts?_embed&per_page=10');
const posts = await res.json();
return posts.map(post => ({
title: post.title.rendered,
slug: post.slug,
content: post.content.rendered,
featuredImage: post._embedded?.['wp:featuredmedia']?.[0]?.source_url
}));
}
```
### B. WPGraphQL
For large enterprise projects, WPGraphQL is the engineers' preferred option. It replaces the REST API with a [GraphQL query interface](/en/wordpress-rest-api-vs-graphql-2026-en/), fetching exactly the data required in a single call and avoiding the network overhead caused by redundant REST API queries (overfetching).
### C. How to map Gutenberg blocks to Astro components
To preserve the design fidelity editors set up in the WordPress Gutenberg block editor, the Astro developer should not simply inject raw HTML with the `set:html` directive. Best practice is to parse the block content (with parsers such as `@wordpress/block-serialization-default-parser`) and map each Gutenberg block to an equivalent Astro component:
- A `core/paragraph` block maps to an Astro `` component.
- A `core/image` block maps to an Astro `` component that optimises the asset automatically.
- Column blocks or dynamic grids are rendered with vanilla CSS Flexbox or Grid using optimised brand styles. This guarantees full style consistency and keeps redundant CSS out of the browser.
### D. How to load WordPress into Astro content collections
The `getLatestPosts()` function above works, but it bypasses the tools Astro provides for content. The [content collections guide](https://docs.astro.build/en/guides/content-collections/) describes two models that fit a headless WordPress:
- **Build-time collections**: defined with `defineCollection()` in `src/content.config.ts`. The built-in `glob()` and `file()` loaders read local files, so the WordPress API needs a custom loader that fetches the data and writes it to the Astro data store. Pages then query it with `getCollection()` and `getEntry()`. Astro suggests this model whenever possible, and it suits news, case studies and service pages that change a few times a day at most.
- **Live collections**: defined with `defineLiveCollection()` in `src/live.config.ts`, they fetch data on every request through `getLiveCollection()` and `getLiveEntry()`. They need an adapter for on-demand rendering, such as the Cloudflare one, and Astro ships no built-in live loaders. The docs propose them for data that must be current on every visit, for example previewing CMS drafts.
Live collections have limits worth knowing before you pick them: no MDX support, no image optimisation and, without caching, a call to the WordPress API on every visit. For a corporate website, a sensible split is a build-time collection for all published content and one live route only for editor previews.
---
## 4. How to deploy Astro on Cloudflare from Spain
The choice of hosting infrastructure in Spain directly affects the speed of a corporate website and compliance with privacy regulation (AEPD). Deploying Astro on the **Cloudflare** edge network has become the standard solution for three operational reasons:
### 1. Cloudflare data centres and latency in Spain
Cloudflare runs several data centres directly on the Iberian Peninsula (Madrid, Barcelona, Lisbon). Hosting the static frontend on **Cloudflare Pages** and routing dynamic functions through **Cloudflare Workers** means requests are answered with extremely low latency (often below 20ms time to first byte, TTFB). Visitors in Spain get the page from their own city, not from distant data centres in Central Europe or the United States.
### 2. Cloudflare GDPR compliance and EU data residency
Unlike other platforms based on US servers, Cloudflare lets you configure data residency within the European Union and sign a Data Processing Agreement (DPA) that strictly complies with the EU General Data Protection Regulation (GDPR) and the requirements of the AEPD in Spain. This avoids legal trouble in privacy audits by corporate clients or in public tenders.
### 3. Serverless compute with Cloudflare Workers, KV and D1
When the corporate site needs complex dynamic features (private client portals, authentication, or secure processing of GDPR-compliant registration forms), development agencies in Spain use Cloudflare's edge ecosystem:
- **Cloudflare Workers**: to run JavaScript code on the node closest to the user.
- **Cloudflare KV**: an ultra-fast key-value store for user sessions, authorisation tokens or the WordPress API cache.
- **Cloudflare D1**: a relational SQLite database distributed natively at the edge, which runs complex SQL queries in milliseconds without network latency.
---
## 5. SEO and GEO with Astro and headless WordPress
Building with Astro and headless WordPress has a direct, positive impact on the two main traffic acquisition channels of the future in Spain: traditional search engines and assistants built on large language models (LLMs).
…[truncated, fetch the canonical URL for the full body]…
---
## Google Maps integration in WordPress: A developer's guide
URL: https://wppoland.com/en/google-maps-integration-wordpress-guide/
Description: Learn how to integrate Google Maps in WordPress programmatically. Optimize performance, secure API keys, and satisfy GDPR compliance requirements.
Published: Wed Jun 18
Updated: Wed Oct 14
Type: guide
Level: advanced
Tags: google-maps, page-speed, gdpr, javascript-api
# Google Maps Integration in WordPress: A Developer's Guide
Integrating Google Maps into WordPress websites is a common requirement for businesses, directory sites, and real estate portals. However, standard iframe embedding techniques introduce serious performance bottlenecks, privacy liabilities, and security risks. To build production-grade web applications in 2026, developers must prioritize asset loading optimization, enforce strict **General Data Protection Regulation (GDPR)** cookie controls, secure application API keys, and build robust fallback mapping architectures. This guide provides a detailed blueprint for optimizing Google Maps integrations in WordPress.
Learn more about our [WordPress development services](/en/wordpress-developer/) to construct high-performance integrations.
---
## 1. Why Google Maps iframe embeds hurt speed and GDPR compliance
Copying and pasting the standard HTML `