Everything the plugin stores.
Every name is prefixed clara_ve_ — the plugin's original internal prefix,
kept after the rename to Visual Edit because changing it would orphan every
existing install from its data and invalidate every exported content package.
Per-page version history. Schema version 3, installed idempotently on init.
| Column | Type | |
|---|---|---|
id |
BIGINT UNSIGNED | |
page_key |
VARCHAR(191) | Which page. Indexed |
content |
LONGBLOB | The markup, gzip-compressed |
content_hash |
CHAR(64) | SHA-256. Used for no-op detection and HEAD marking |
pseudo |
LONGTEXT | Decorative styling map as JSON |
responsive |
LONGTEXT | Responsive block rules as JSON |
message |
VARCHAR(255) | Custom label, or NULL for automatic |
kind |
VARCHAR(20) | save or restore |
restored_from_id |
BIGINT UNSIGNED | |
author |
BIGINT UNSIGNED | |
created_at |
DATETIME | Indexed |
Retention: 300 entries per page_key, oldest pruned. Per key rather than
globally, so an actively-edited page cannot evict a quiet page's history.
Append-only. A restore writes the old content back as live but records no row; HEAD is derived by comparing hashes against the live source, not by row order.
Mailing-list signups and their consent record. Schema version 2 (Clara_VE_Optin::DB_VERSION); version 1 had no theme column.
| Column | Type | |
|---|---|---|
id |
bigint(20) unsigned | |
email |
varchar(191) | Indexed |
theme |
varchar(191) | Which theme was active at signup. Empty on rows written before schema 2 |
list_id |
varchar(64) | |
form_id |
varchar(64) | |
status |
varchar(20) | pending or confirmed |
token_hash |
char(64) | SHA-256 of the confirmation token. The token itself is never stored |
consent_text |
text | The exact wording the person agreed to |
ip |
varchar(100) | |
created_at |
datetime | |
confirmed_at |
datetime |
Retention: pending rows swept after 14 days. Confirmed rows never — they are the consent evidence.
Two families, one row each per edited page per theme:
clara_ve_source__{theme}__{key}— the page's markupclara_ve_pseudo__{theme}__{key}— decorative styling
The theme segment arrived in 1.16.0, when two converted themes became able to
share one install. The unscoped clara_ve_source__{key} form is still READ as
a fallback for installs that predate it, gated on the ownership guard.
The front page keeps the older names clara_ve_front_source and
clara_ve_pseudo_css, byte-for-byte, from before multi-page support.
Both are written with autoload = false. Neither can be enumerated from a
fixed list, which is why uninstall sweeps by prefix.
Theme lifecycle — clara_ve_themes (one record per theme this plugin has
content for: its name, keys, navigation zones, the redirects it established,
whether it is currently parked, and the site options as they were before its
import — the only thing that survives the theme's directory being deleted),
clara_ve_theme_registry_backfill, clara_ve_legacy_media_stamp_version
State — clara_ve_redirects, clara_ve_history_db_version,
clara_ve_optin_db_version, clara_ve_form_secrets_repaired,
clara_ve_seo_synced_host, clara_ve_seo_reindex_due
Design — clara_ve_google_fonts
Forms — clara_ve_form_to, _from_name, _from_email, _min_seconds,
_akismet, _consent, _consent_text, clara_ve_remove_all_data
Mail — clara_ve_mailer, clara_ve_smtp_host, _port, _encryption,
_auth, _user, _pass, clara_ve_api_brevo, _sendgrid, _postmark,
_mailgun, clara_ve_mailgun_domain, _region
Lists and opt-in — clara_ve_list_provider, _doi_template,
_doi_redirect, clara_ve_optin_mode, _confirm_subject, _confirm_body,
_deliver_subject, _deliver_body, _deliver_file
SEO and structured data — clara_ve_seo_entity_type, _entity_name,
_entity_logo, _entity_extra, clara_ve_seo_same_as,
_default_og_image, _title_separator, clara_ve_geo_ai_crawlers
Registered on every block type, so it travels with the block through copies,
patterns, revisions and template parts. Rendered by Clara_VE_Block_Extras,
which puts a content-addressed cve-r-<hash> class on the block and prints
its rules once per page. Values are validated on every render; anything
outside the grammar is dropped.
| Key | Shape |
|---|---|
responsive |
{ tablet | mobile: { 'typography.fontSize': '22px', … } } |
ornaments |
{ before | after: { content, color, font-size, font-family, font-weight, line-height, hidden } } |
form |
{ label, field, focus, placeholder, button, buttonHover } — see below |
form styles every form inside the block through what forms are made of —
label/legend, text inputs/select/textarea, submit buttons — never a
theme's or plugin's class names, so it applies to a theme shortcode, Contact
Form 7, WPForms or a hand-written form alike. Properties per target:
| Target | Properties |
|---|---|
label |
color, font-family, font-size, font-weight, letter-spacing, text-transform |
field |
color, background-color, font-family, font-size, border (underline | box | none), border-color, border-width, border-radius |
focus |
border-color |
placeholder |
color |
button |
color, background-color, border-color, border-radius, font-size, letter-spacing, text-transform |
buttonHover |
color, background-color |
Colours, font families and sizes may be theme presets (var:preset|color|slug).
A block whose output has no element of its own at render time — a shortcode
block (still [shortcode] when blocks render), an HTML block with several
roots, output starting with <style> — is wrapped in <div class="cve-r-…">.
The editor preview and the public site share one generator
(ClaraVEModel.formCss / Clara_VE_Block_Extras::form_css, kept identical by
tests/form-css-congruence.mjs).
Registered from PHP (Clara_VE_Form_Blocks); the editor script is
assets/form-blocks.js. Every block saves plain form markup; only
clara-ve/form has a render callback, which connects the saved <form> via
Clara_VE_Tokens::connect_form() (action /clara-ve/v1/form-submit, origin
token, honeypot, time-trap, consent note) and removes a data-demo marker.
formType, listId and recipient are the owner's delivery choice, and they
travel in the markup where anyone can retype them — so the renderer signs the
three of them into cve_delivery (wp_hash) and the endpoint honours only what
this site rendered. A form left at the defaults sends an enquiry to the Form
Settings address, which is also what an unsigned or edited submission falls back
to.
| Block | Attributes | Saves |
|---|---|---|
clara-ve/form |
formId (groups submissions), formClass, wrapperClass, redirect (path or same-site URL), message, formType (contact | list), listId, recipient |
<div class="wrapperClass"><form class="formClass">…</form></div>, or the form alone |
clara-ve/form-group |
groupClass |
<div class="groupClass">…</div> |
clara-ve/field |
type (text, email, tel, url, number, date) + the field attributes |
<div class="wrapperClass"><label for>label <span class="hintClass">hint</span></label><input></div> |
clara-ve/textarea |
rows + field attributes |
as above, <textarea> |
clara-ve/select |
options (string[]) + field attributes |
as above, <select> |
clara-ve/checkbox |
field attributes | input before label |
clara-ve/submit |
text, buttonClass |
<button type="submit"> |
Field attributes: name (submission key; derived from the label when empty),
inputId, label, labelClass, hint, hintClass, placeholder, required,
wrapperClass (default field), inline (label and control sit directly in
the parent row — saved with display:contents). ClaraVEFormBlocks.fromHtml()
converts existing form markup into these blocks (tests/form-convert.cjs).
Form submissions. public => false, show_ui => true, nested under the
Visual Edit menu, supports title and custom-fields.
Title: {form_id} — {mysql datetime}.
| Meta | |
|---|---|
| one per submitted field | Key is sanitize_key(field name), value sanitize_textarea_field'd. Flat arrays (name[]) joined with ", "; nested arrays skipped |
_clara_ve_form_ip |
Resolved client IP |
_clara_ve_spam |
'1' or '' |
_clara_ve_mail_failed |
'1' when the notification failed |
_clara_ve_list_error |
Provider error message |
The leading-underscore convention is load-bearing: the exporter treats underscore-prefixed keys as bookkeeping and everything else as a visitor-typed field.
| Key | On | |
|---|---|---|
_clara_ve_key |
pages | The visual-edit key. Registered with show_in_rest => false — internal |
_clara_ve_theme |
pages, posts, attachments, submissions | Which theme's tenure created it. Also a TERM meta, on categories, tags and nav menus. Absent means nobody's — the owner's own, or content predating the stamp — and that is what keeps it alive when a theme leaves |
_clara_ve_status |
pages, posts | The status held before the theme was parked, so restoring puts back what was there rather than assuming published |
_clara_ve_slug |
pages, posts | The address held before parking. Present only while parked |
_clara_ve_parked |
attachments | Names the parked theme. Attachments keep inherit, which WordPress relies on, so they are hidden by query filters rather than by status |
_clara_ve_seo |
pages, posts | The whole SEO record as one array: title, description, canonical, noindex, og, twitter, jsonld |
_clara_ve_noindex |
pages, posts | A denormalised mirror of the noindex flag, existing only so it can be queried — no meta query can look inside a serialised array, and "every page the owner hid" is exactly what the sitemap and llms.txt need to ask |
_clara_ve_responsive |
pages, posts | Sanitised JSON map of block anchor → breakpoint → property → value. Exposed to authenticated REST edits so Gutenberg saves it with the page; access still requires edit_post for that object |
The SEO record is deliberately one array written and read as a unit, so it cannot half-update.
When Yoast or Rank Math is installed, values are also mirrored into their meta keys — 11 each, mapped as data rather than a branch per field.
All prefixed clara_ve_, which is what lets uninstall sweep them in one
query.
| Key | TTL | |
|---|---|---|
clara_ve_article_css |
1 week | Compiled specimen CSS. One row carrying its own hash, not hash-keyed |
clara_ve_google_font_catalog |
1 week | ~2.7 MB fetched once, trimmed to three fields |
clara_ve_lists_brevo |
5 min | Mailing lists |
clara_ve_geo_audit |
15 min | Readiness report |
clara_ve_geo_faq_{hash} |
1 week | Extracted Q&A, keyed by page with the content hash inside the value |
clara_ve_import_plan_{id} |
30 min | A reviewed import plan |
clara_ve_import_scrap_{id} |
30 min | Scratch directory |
clara_ve_import_report_{user} |
1 min | Result summary |
clara_ve_form_rl_{md5 ip} |
60 s | Rate limit |
The FAQ cache is keyed by page with the hash inside the value, rather than by hash. Keying by hash left roughly forty orphan rows in
wp_optionsafter a single editing session, since every edit produced a new key and nothing removed the old one.
Five options hold credentials:
clara_ve_smtp_pass · clara_ve_api_brevo · _sendgrid · _postmark ·
_mailgun
Encryption:
key = hash('sha256', wp_salt('auth'), true)
iv = openssl_random_pseudo_bytes(16)
cipher = openssl_encrypt($plain, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv)
stored = base64_encode($iv . $cipher)
What this protects, precisely: a database-only exposure — a leaked backup, read-only database access, another plugin dumping the options table.
What it does not: an attacker with both filesystem and database access.
The key derives from wp-config.php salts, so anyone holding both has
everything.
No key rotation, no versioning, and CBC without a MAC. Stated plainly because a security claim that overreaches is worse than a modest one.
Two implementation details worth knowing if you touch this:
- The sanitiser detects its own ciphertext by trying to decrypt it, because
WordPress runs a sanitiser twice on a first-ever save (
update_optionthenadd_option), which double-encrypted every secret before the guard existed - A one-shot repair migrates values written before that fix
The same five names are listed independently in three places — the export
allowlist, the settings class, and uninstall.php (which deliberately does
not load plugin classes). Redundant on purpose.
- Bundle format — what travels in an export
- Data and privacy — retention, deletion
- Security