WP Manifestindependent plugin directory
manifest / content / wp-personalization

WP Personalization

B2B content personalization for WordPress. Resolves a visitor's market segment via IP address lookup, then renders the matching personalization layout from a configured collection. Designed for block themes (Full Site Editing).

by blake-c · github.com/blake-c/wp-personalization

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/blake-c/wp-personalization/archive/refs/heads/main.zip

This plugin is still under development. It is not finished and is not ready to run on a production site.

B2B content personalization for WordPress. Resolves a visitor's market segment via IP address lookup, then renders the matching personalization layout from a configured collection. Designed for block themes (Full Site Editing).


How It Works

  1. A Collection block is placed on a page. It holds a set of Personalizations — one per market segment.
  2. On page load, a skeleton loader is shown immediately (cacheable HTML).
  3. The browser posts to the REST API. If the visitor has a cached segment slug in localStorage from a previous visit (or just picked one in the fallback modal), that slug is sent on the request and the server skips IP lookup entirely. Otherwise the server resolves the visitor's IP to an organization name, matches it against the segment keyword list, and returns the matching personalization's HTML.
  4. The skeleton is replaced with the personalized content. If the timeout fires first, the default segment is shown instead.
  5. The resolved segment slug is stored in localStorage (TTL-controlled) so subsequent visits short-circuit IP resolution. Stale cache entries (e.g. when an editor changes which personalizations belong to a collection) are detected via the response's is_default flag and dropped automatically.

Requirements

  • WordPress 6.4+
  • PHP 8.1+
  • A block theme (FSE)
  • An ipinfo.io or ip-api.com API key (free tiers available)

Installation

  1. Upload the wp-personalization folder to wp-content/plugins/.
  2. Activate the plugin in Plugins > Installed Plugins.
  3. Go to WP Personalization > Settings and configure your IP provider and API key.
  4. Build the JavaScript assets (see Build).

Build

Dependencies and build commands run from the plugin root.

pnpm install
pnpm run build
Command Description
pnpm run build Production build — compiles block JS/SCSS to build/
pnpm run start Watch mode for development
pnpm run lint:js ESLint
pnpm run lint:style Stylelint
pnpm run lint:php PHPCS (requires Composer install at wp-content/)
pnpm run lint:php:fix PHPCBF auto-fix

Configuration

All settings live under WP Personalization > Settings.

Setting Description
IP Provider ipinfo.io, ip-api.com, or none (disables resolution)
API Key Provider API key (encrypted at rest — see API Key Storage)
Resolve Timeout Milliseconds before the default segment is shown (default: 3000)
Cache TTL Seconds to cache an IP-to-org lookup transient (default: 86400)
Consent Provider builtin, onetrust, or none
Proxy Header Which header carries the real client IP behind a proxy

API Key Storage

API keys are encrypted with AES-256-GCM before being written to wp_options. The encryption key is derived via HKDF-SHA256 from one of:

  1. The WP_PERSONALIZATION_KEY constant if defined in wp-config.php (recommended for production — key management is decoupled from WordPress salts).
  2. WordPress AUTH_KEY / SECURE_AUTH_KEY / AUTH_SALT as a fallback.
// wp-config.php — recommended for production
define( 'WP_PERSONALIZATION_KEY', 'a long random string, 32+ bytes' );

Key rotation: rotating WordPress salts (or WP_PERSONALIZATION_KEY) invalidates any stored ciphertexts — you'll need to re-enter API keys in WP Personalization > Settings afterwards.

Threat model: encryption protects stored secrets against DB-only compromise (SQL injection, leaked backups). An attacker with filesystem access to wp-config.php can still read the key material. Never commit real keys to version control.

The settings UI never echoes stored keys back to the browser — only a masked placeholder is shown. To overwrite a key, type a new one; to remove it, tick Clear stored key and save.


Segment Rules

Custom keyword-to-segment mappings are managed under WP Personalization > Segment Rules. Rules are evaluated before the built-in keyword list. A lower priority number runs first.


Data Model

Post Type Slug Description
Personalization wpp_personalization A single segment-specific layout (block content)
Collection wpp_collection A group of personalizations with a designated default
Taxonomy Slug Description
Segment wpp_segment Market vertical assigned to a personalization

REST API

Both endpoints are public (no nonce or user context required) and protected by the same per-IP rate limiter (10 requests per 60 seconds, keyed by the admin-configured client-IP source).

Method Endpoint Description
POST /wp-json/wp-personalization/v1/resolve Resolve segment and return personalization HTML
GET /wp-json/wp-personalization/v1/segments List available segments for a collection

POST /resolve request body:

Field Type Required Description
collection_id integer yes Published wpp_collection post ID
segment string no Client-supplied segment slug. When provided, the server skips IP resolution and renders the personalization that matches the slug. Used by the fallback modal selection and the localStorage cache. If the slug doesn't match any personalization in the collection, the response falls back to the default and is_default: true.

POST /resolve response:

{ "segment": "banking", "html": "...", "is_default": false }

When IP resolution fails or no segment matches, the response is { "segment": "", "html": "...default...", "is_default": true }.


IP Providers

Provider Free Tier Notes
ipinfo.io 50,000 req/month HTTPS; returns ASN + org name
ip-api.com 45 req/min (non-commercial) HTTP on free tier — blocked in production (wp_get_environment_type() === 'production'); paid key enables HTTPS via pro.ip-api.com

ipinfo.io is the recommended provider because it serves HTTPS on the free tier. The ip-api.com free tier is retained for local/dev/staging use only — in production the plugin refuses to call it and logs a WP_Error so that on-path attackers cannot forge the org value and steer visitors to attacker-chosen segments.

An admin notice appears when ipinfo.io usage reaches 80% of the free-tier limit, and when ip-api.com is selected without a paid key.


IP processing and localStorage writes only happen after consent is granted.

  • OneTrust: listens for OnetrustActiveGroups including the configured group ID (default C0002).
  • Built-in: renders a fixed bottom banner; sets a wpp_consent=accepted cookie on accept.
  • None: skips consent gating entirely.

Data retention notes

  • The visitor's IP is processed server-side to resolve an organisation name; it is never returned to the client and never written to post data.
  • The IP lookup result (org name) is cached as a WordPress transient. The cache key is an HMAC-SHA256 derived from the IP and wp_salt('auth') — without access to the WordPress salts, an attacker who reads the options table cannot enumerate the IPv4 keyspace to recover which IPs were seen.
  • The cached value itself (org name, e.g. "Wells Fargo") is still identifying data about the network a visitor came from. Treat this cache entry like other GDPR-relevant data in your retention policy, and purge it when required. Rotating WordPress auth salts invalidates all existing cache keys and effectively clears the mapping.
  • Only the non-PII segment slug (e.g. banking) is ever written to localStorage on the client.

Changelog

See CHANGELOG.md.