WP Manifestindependent plugin directory
manifest / utilities / estrategia-web-wp-plugin

Estrategia Web

Estrategia Web: WordPress plugin with switchable modules (site, forms, cookies, carousel, content types…)

★ 0stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/estrategia-web-es/estrategia-web-wp-plugin/archive/refs/heads/main.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Estrategia Web — the WordPress sites plugin, in modules

Everything that is not design on an Estrategia Web site, in a single plugin with modules you switch on and off in Estrategia Web → Modules. The design (templates, parts, patterns, theme.json) belongs to the Estrategia Web theme, which since its 1.1.0 does not register any block (a WordPress.org requirement).

David's decision of 14-10-2026: one theme + one plugin. Up to 1.0.0 (which was never published) there were three pieces: the «Estrategia Web Site» plugin (this repo), the «Estrategia Web Forms» plugin (armazon-wp-formularios, archived) and four blocks registered by the theme. The public names of 1.0.0 do not change (blocks, options, meta keys, tables, REST routes, filters): what changed is where the code lives. The general framework is in armazon/decisiones/2026-10-01-wordpress-tema-padre-compartido.md.

  • Slug, folder and text domain: estrategia-web (estrategia-web/estrategia-web.php) · WordPress 7.1+ · PHP 8.1+ · GPLv2+ · version 2.0.1
  • The repo is still called armazon-wp-sitio; the plugin folder on a site is estrategia-web/.
  • No dependencies, no node_modules, no build step.

Why a plugin and not the parent theme: what is stored and what works cannot disappear when the theme changes. What is seen belongs to the theme; what is stored and what it does, to this plugin.

Modules

Module (folder) What it does Default
site company details (Block Bindings, [estrategia_web_company]), the details of one location (Block Bindings) and sanitized SVG: sections 1 to 4 below; the content types (clients, testimonials, locations, projects, portfolio) are content-types templates since 17-10-2026 on
content-types the constructor: each site's own content types, fields and taxonomies, made in the panel or from templates (section «content-types module») on (since 17-10-2026)
matomo Matomo code, cookieless or hybrid (unreleased: previously inside site; requires site, stores in its option): section 5 on
consent the cookie notice and its API; (unreleased) cookie register with detector and table for the policy: sections 6 and 6 bis on
google-tags (unreleased) GA4, Google Ads and Tag Manager with Consent Mode v2; requires consent: section 6 ter off
webp image sizes in WebP: section 7 on
external-links links to another domain in a new tab: section 8 on
forms block-based forms, email notifications and submission log: modules/forms/README.md on
carousel estrategia-web/carousel block on
map estrategia-web/map block (two-click map) on
whatsapp estrategia-web/whatsapp block and its two options on
language-switcher estrategia-web/language-switcher block (WPML) on
social free list of networks (Estrategia Web → Social networks, option estrategia_web_social_links) and estrategia-web/social-links block; requires site. All of it in the header of modules/social/module.php on (unreleased)
local-business (unreleased) schema.org structured data: the Organization on the front page and a LocalBusiness on each location's page and on the pages tied to one (address, hours, coordinates), inside Rank Math's or Yoast's graph when they are there; requires site and content-types: section «local-business module» on
banners (unreleased) image or code banners by zone (eweb_banner, eweb_banner_zone), with dates, weight and daily views and clicks counted from the browser; estrategia-web/banner-zone block; wp estrategia-web banners migrate-adrotate. All of it in the header of modules/banners/module.php off
redirects 301/302/410 redirects editable in Estrategia Web → Redirects, CSV import/export, optional 404 log, wp estrategia-web redirects import\|list\|export: modules/redirects/module.php off

On by default: the ones Lavandería already used (also matomo, which came out of site): a site that updates does not change. Anything new that changes what the visitor sees or what the site requires (redirects, google-tags) starts off; in Lavandería the redirects are handled by Rank Math. A module that is off loads nothing (not its PHP, nor its blocks, nor its assets) and deletes nothing of what it stored.

site is core since 1.7.0: always loaded (header Core: yes), not on the Modules screen, even if a saved estrategia_web_modules lacks it.

wpml-front-page was removed in 1.7.0: it papered over a WPML set up without its step 1 (existing_content_language_verified), with which WPML does not translate the front page. What to check on a WPML site: docs/web-nueva.md, «5. WPML: yes or no». A saved list that still names it is harmless: unknown slugs are ignored.

The admin (1.7.0)

One top-level item, Estrategia Web, at position 3 (right after the Dashboard), that opens on Company details:

Submenu URL (admin.php?page=…) What
Company details estrategia-web-site Company and Contact (and local-business's type of business)
Content types estrategia-web-content-types content-types module
Social networks estrategia-web-social social module
Settings estrategia-web-settings&tab=… tabs Cookies (consent), Analytics (google-tags + matomo), Media (SVG + uploads); a tab only if its module is on
Redirects estrategia-web-redirects redirects module
Modules estrategia-web always last, grouped by the modules' Category

One look for every screen (2.0.1): the same header (title + one line saying what the screen is for), every section of a form in a white box with its title, the content types builder's tabs (also in Settings and Redirects) and the save button under the boxes it saves. The pieces and how to use them: includes/admin/class-admin-ui.php; the stylesheet: assets/admin.css; the check: tests/admin-look.test.mjs.

Forms keeps its own top-level item: Forms (the list of the site's forms, where each one is, its notifications; since 2.0.0 the form posts, with «Add form» and where each one is used), Submissions, Settings.

Old URLs: estrategia-web-cookies and estrategia-web-google redirect to their tab; estrategia-web-site, estrategia-web and estrategia-web-forms did not change. A module adds a Settings tab or section through the registry in includes/admin/class-settings-page.php (its header: how, the edge case of two screens saving one option, and the trap); the menu order and the old URLs, in includes/admin/class-menu.php.

How to add a module: a modules/<slug>/ folder with its module.php, which carries a header (name, description, whether it is on by default, what it depends on, its category). It is discovered on its own: there is no central list. Everything you need to know —fields, the edge case of a new module on a site that already existed, the traps— is in the header of includes/class-modules.php. The minimal example is modules/webp/module.php; the complete one, modules/forms/module.php.

estrategia-web.php          entry point: constants and core bootstrap
includes/                   the core, always loaded:
  class-modules.php           the loader (HOW TO ADD A MODULE)
  class-plugin.php            translations and the estrategia_web_activate / _deactivate hooks
  class-blocks.php            autoRegister labels, editor translations
  admin/class-admin-ui.php      one look for every plugin screen: header, boxes, tabs, save (2.0.1)
  admin/class-menu.php          the «Estrategia Web» menu, its order and the old URLs (1.7.0)
  admin/class-settings-page.php «Estrategia Web › Settings» and its tab registry (1.7.0)
  admin/class-modules-page.php  the «Estrategia Web → Modules» screen
assets/admin.css            the shared admin stylesheet (2.0.1; drawn by class-admin-ui.php)
modules/<slug>/             module.php (header + bootstrap), includes/, blocks/, assets/, tests/, uninstall.php
languages/                  estrategia-web.pot · estrategia-web-es_ES.po · .l10n.php (one for everything)
tests/                      runner, lib.mjs, blueprint and the core tests
tools/i18n.mjs              extracts (module headers too), merges and compiles
tools/open-cookie-database.mjs  regenerates the Open Cookie Database extract (consent module)
wpml-config.xml             WPML only reads the one at the root: all modules' entries go here
uninstall.php               calls each module's uninstall.php

The contract

This is what the parent theme, the child themes and the content loading read. These names do not change in a minor version (see «Versions»).

0. Public names, with their prefix (15-10-2026)

Everything this plugin leaves in WordPress's shared space (the database, the block and post type registries, the REST API, the browser) carries a prefix, so that it does not clash with another plugin, and for WordPress.org. This table is the contract: a new name goes in here in the same commit as the code, with the prefix of its row.

What Prefix Today's names
Post types eweb_ (≤ 20 characters in total) eweb_client, eweb_testimonial, eweb_location, eweb_project, eweb_portfolio (unreleased, 1.2.0; unprefixed up to 1.1.0); eweb_content_type (private: the content-types module's definitions, one per type); eweb_form (2.0.0, not public: the forms module's form posts, REST base eweb-forms); eweb_banner (unreleased, banners module, not public, REST base eweb-banners)
Taxonomies eweb_ eweb_client_sector, eweb_project_category, eweb_portfolio_category (same); eweb_banner_zone (unreleased, banners)
Blocks estrategia-web/, estrategia-web-site/, estrategia-web-forms/ estrategia-web/carousel, /map, /whatsapp, /language-switcher, /social-links, /cookie-table; estrategia-web-site/consent; estrategia-web-forms/form, /text, /email, /tel, /textarea, /select, /checkbox, /submit, /notification, /form-ref (2.0.0); estrategia-web/banner-zone (unreleased)
Options estrategia_web_ estrategia_web_site, estrategia_web_modules, estrategia_web_modules_known, estrategia_web_forms_settings, estrategia_web_forms_db_version, estrategia_web_forms_last_purge, estrategia_web_forms_migrations_log, estrategia_web_cookies, estrategia_web_cookies_settings, estrategia_web_google_tags, estrategia_web_map_google_seen, estrategia_web_redirects_settings, estrategia_web_redirects_db_version, estrategia_web_social_links, estrategia_web_uploads, estrategia_web_whatsapp_phone, estrategia_web_whatsapp_message, estrategia_web_content_types_hash, estrategia_web_content_types_wpml_pending, estrategia_web_content_types_wpml_slugs, estrategia_web_banners_db_version
Post meta estrategia_web_ estrategia_web_banner_link, _code, _start, _end, _weight, _adrotate (banners, unreleased); those of section 3, all of them (client_url, testimonial_role and testimonial_client, from before 1.0.0, became estrategia_web_…; their read aliases went in 2.0.1, see «Names»)
Tables {prefix}estrategia_web_ estrategia_web_forms_submissions, estrategia_web_forms_definitions, estrategia_web_redirects, estrategia_web_banners_stats (unreleased)
Transients and cron estrategia_web_ estrategia_web_redirects_report_*, estrategia_web_cs_* (detector limit); events estrategia_web_forms_purge, estrategia_web_redirects_purge
Cookies estrategia_web_ estrategia_web_consent (the old armazon_consent is read and carried over)
Hooks (filters and actions) estrategia_web_ those of each section; the live list: grep -rhoE "(apply_filters\|do_action)\( *'estrategia_web[a-z_]*'" modules includes
REST routes estrategia-web/v1, estrategia-web-forms/v1 estrategia-web/v1/cookies, /cookies/seen, /content-types, /content-types/<name>, /content-types/templates/<key>, /banners/hits (unreleased); estrategia-web-forms/v1/submit, /token; the post types' routes are core's, /wp/v2/eweb_client…
Block Bindings sources estrategia-web-site/, estrategia-web/ estrategia-web-site/company, estrategia-web-site/location, estrategia-web/content-field (content-types module)
Shortcodes estrategia_web_ [estrategia_web_company], [estrategia_web_consent_settings], [estrategia_web_cookie_table]
Global functions (for themes) estrategia_web_ estrategia_web_type_name() (1.2.0)
PHP namespace EstrategiaWeb\ EstrategiaWeb\Site\…, EstrategiaWeb\Forms\……
JS and events estrategiaWeb…, estrategia-web: window.estrategiaWebConsent, estrategia-web:consent, estrategia-web:consent-ready, estrategia-web:form-sent
WP-CLI estrategia-web wp estrategia-web redirects …, migrate-location-data, migrate-forms (2.0.0), migrate-form-markers (2.0.1), banners migrate-adrotate (unreleased)
Text domain, folder estrategia-web —

Two prefixes, on purpose: estrategia_web_ / estrategia-web for everything else, and eweb_ only for post types and taxonomies, because WordPress cuts a post type name at 20 characters and estrategia_web_testimonial is 26. Why eweb_ (David, 15-10-2026): measured across the code of every plugin on WordPress.org (veloria.dev), no plugin uses eweb_; ew_ is used by 13 (Easy Watermark, Essential Widgets…) and ewp_ by 9 (ElasticPress), and they were discarded; estweb_ was not liked. The post type prefix lives in a single place (class-type-names.php) and does not change once published on a site (it would be a whole new migration).

1. Company details — option estrategia_web_site

Estrategia Web › Company details screen (capability manage_options; since 1.7.0 only the company and contact keys: allow_svg is saved from Settings › Media and matomo_* from Settings › Analytics, into this same option — each screen sends only its keys and the merge keeps the rest). A single option (array), registered with the Settings API and exposed in REST with its schema: GET/POST /wp/v2/settings → key estrategia_web_site.

Key Type Sanitizing
company_name text no HTML
legal_name text no HTML
tax_id text no HTML
registry text (0.4.0): the company registry entry exactly as the legal notice states it, without «Inscrita en» («Registro Mercantil de Mallorca, tomo 1234, folio 56, hoja PM-7890, inscripción 1.ª»). A single key on purpose: the notice writes it as a sentence, and not every company is in the Mercantile Registry no HTML
email text valid email; otherwise the previous one stays
phone text as written («971 12 34 56»)
phone_secondary text same
whatsapp text stored as digits only, with the country code («+34 600…» → 34600…); invalid → the previous one stays
whatsapp_message text no HTML
address multi-line text no HTML, keeps line breaks
schedule multi-line text same
map_url URL http(s); invalid → the previous one stays
social object: facebook, instagram, linkedin, x, youtube, tiktok http(s) URL each. Deprecated (unreleased), goes away in 2.0.0: it is no longer on the screen; the social module copies it once into its list. Networks are written in estrategia_web_social_links
enabled_types array ⊂ client, testimonial, location, project, portfolio any other value → 400 for the whole request
allow_svg boolean
rewrite_slugs object name → slug with the names client, client_sector, testimonial, location, project, project_category, portfolio, portfolio_category (0.2.0) sanitize_title («Mi Cliente!» → mi-cliente); '' = the usual one; any other name → 400 for the whole request. Merged per key. When it changes, the URL rules are rebuilt

It is saved by merging: a POST with {"estrategia_web_site": {"phone": "…"}} changes the phone and leaves the rest. That way the content loading fills it in by parts.

With WPML (1.4.3), registry, whatsapp_message, address and schedule are offered in WPML › String Translation (domain admin_texts_estrategia_web_site); untranslated, every language shows the original. The translation reaches the site through Company::value() (shortcode, Block Bindings, WhatsApp button) on its own. The «Company details» screen always shows and saves the original, whatever the admin language; /wp/v2/settings?lang=en returns the translation, so a content load reads and writes without ?lang. The rest of the details are the same in every language. Criterion and edge case, in wpml-config.xml.

# REST (as an administrator; Playground or a site with an application password)
curl -u admin:APP_PASS -X POST https://web.test/wp-json/wp/v2/settings \
  -H 'Content-Type: application/json' \
  -d '{"estrategia_web_site":{"phone":"971 12 34 56","enabled_types":["client","location"]}}'

# WP-CLI: goes through the same sanitizing (sanitize_option), so it merges too.
# (Deduced from the code, not tested: the tests go through REST.)
wp option update estrategia_web_site '{"phone":"971 12 34 56"}' --format=json

In PHP, a theme reads a value with \EstrategiaWeb\Site\Company::value( 'phone' ) (not with get_option, which skips the filter and the derived keys).

2. Block Bindings source estrategia-web-site/company

args.key = any key above (except enabled_types, allow_svg and rewrite_slugs), social.<network> with a dot (alias until 2.0.0: with the social module on, the link of the first network in its list with that icon; see class-company.php), and four derived keys:

Key Example
phone_href tel:+34971123456 (country code 34 if it has none; filter estrategia_web_site_default_country_code)
phone_secondary_href same, from phone_secondary (0.2.0)
email_href mailto:hola@empresa.com
whatsapp_href https://wa.me/34600111222?text=Hola%2C%20quer%C3%ADa…

It works for the content of paragraphs and headings (escaped text; multiple lines come out with <br>) and the url of buttons and links. In the editor the block shows the value (or [Name of the detail] if empty) and the source appears in the «Attributes» panel with the list of details; it cannot be edited from the block.

A paragraph with the phone:

<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"estrategia-web-site/company","args":{"key":"phone"}}}}} -->
<p></p>
<!-- /wp:paragraph -->

A WhatsApp button (the button text is fixed; the url, bound):

<!-- wp:buttons -->
<div class="wp-block-buttons"><!-- wp:button {"metadata":{"bindings":{"url":{"source":"estrategia-web-site/company","args":{"key":"whatsapp_href"}}}}} -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">Escríbenos por WhatsApp</a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons -->

A «Call» button with text and url bound at the same time (the button's text has no test of its own; the url does):

<!-- wp:button {"metadata":{"bindings":{"text":{"source":"estrategia-web-site/company","args":{"key":"phone"}},"url":{"source":"estrategia-web-site/company","args":{"key":"phone_href"}}}}} -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button"></a></div>
<!-- /wp:button -->

2 bis. A detail inside a sentence: [estrategia_web_company key="…"] (0.4.0)

A binding replaces the whole paragraph (WP 7.1 has no inline bindings), so for the legal notice (LSSI, art. 10) there is a shortcode. Same keys as the source (Company::value(): also social.<network> and the derived ones), escaped text and on one line: address and opening hours come out joined with «, ». A key that does not exist or is empty prints nothing.

<!-- wp:paragraph -->
<p>Titular: [estrategia_web_company key="legal_name"], con NIF [estrategia_web_company key="tax_id"] y domicilio en [estrategia_web_company key="address"]. Teléfono: [estrategia_web_company key="phone"]. Correo: [estrategia_web_company key="email"]. Inscrita en el [estrategia_web_company key="registry"].</p>
<!-- /wp:paragraph -->
  • In the editor you see the shortcode as is; the value shows on the site. A detail alone in its paragraph is better as a binding (which is visible in the editor).
  • It works in the content and in block templates; not in a block attribute nor in an href (for links, the button binding).
  • [[estrategia_web_company …]] with double brackets is WordPress's escape: it comes out literally. Why a shortcode and not a block, in the header of modules/site/includes/class-shortcode.php.

2 ter. Block Bindings source estrategia-web-site/location (0.2.0)

The details of one location (post type location, which must be enabled). Two ways to say which, through the args:

args Which location
{"slug": "maioris", "key": "…"} that one, from any page. The identifier is the location's slug (its «permalink»), not the title
{"key": "…"} the one in context: inside a Query Loop of locations, the one of each row (postId). Outside a location, empty

Keys:

Key What
title the location's name
estrategia_web_location_address derived (2.0.0): the address text, from street (1-2 lines), postal code, locality and region, in the format of the location's language. Multi-line (<br>)
estrategia_web_location_schedule derived (2.0.0): the hours text, from the hours field («Abierto todos los días» / «De 8:00 a 23:00»). Multi-line
estrategia_web_location_map_url, estrategia_web_location_map_embed derived (2.0.0): «Open in Google Maps» and the embed URL, from «What to search on Google Maps» (estrategia_web_location_map_query)
estrategia_web_location_static_map derived (2.0.0): the URL of the media library image estrategia_web_location_static_map_id
estrategia_web_location_phone, estrategia_web_location_email, every other field of the definition as stored

The derived keys only exist in THIS source (2.0.0). They were stored meta up to 1.7.0, and a theme that bound them with core/post-meta must switch those bindings to estrategia-web-site/location (same key, no slug inside a Query Loop) BEFORE running the data migration — after it the meta is gone and core/post-meta shows nothing. Until the migration runs, the old stored text is what these keys show, so updating the plugin changes nothing.

The wording of the derived texts is a per-language format (Location_Data::FORMATS in class-location-data.php: es, ca, en; any other language uses en). A site changes it with a filter, in its theme or a mu-plugin:

add_filter( 'estrategia_web_location_formats', function ( array $format, string $lang ) {
    if ( 'es' === $lang ) {
        $format['open']      = 'Abrimos {days}';          // first line when every open day has the same hours
        $format['interval']  = 'de {open} a {close} h';   // each range
        $format['address']   = "{street}\n{postal_code} {locality} ({region})";
    }
    return $format;
}, 10, 2 );

Keys: address (placeholders {street} {postal_code} {locality} {region} {country}; an empty part takes its separator with it), open, line ({days}: {times}, one per set of hours when there are several), every_day, day_range ({from}/{to}, three days or more), list_and, days (seven names, Monday first), interval ({open}/{close}), times_and, time (a PHP date format: G:i, g:ia). The company's hours (Company details) use the same format. | estrategia_web_location_phone_href | tel:+34971112233, from estrategia_web_location_phone (same country code as the company) | | estrategia_web_location_email_href | mailto:…, from estrategia_web_location_email |

Same treatment as the company source: escaped text with <br>, and in url only http, https, tel and mailto. A location that does not exist, is a draft or is private comes out empty (in the editor, [Name of the detail]). In the editor it shows the value of the locations published when it was opened; a location created later shows in brackets until reloading.

Button with the phone of the «maioris» location (text and url):

<!-- wp:button {"metadata":{"bindings":{"text":{"source":"estrategia-web-site/location","args":{"slug":"maioris","key":"estrategia_web_location_phone"}},"url":{"source":"estrategia-web-site/location","args":{"slug":"maioris","key":"estrategia_web_location_phone_href"}}}}} -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button"></a></div>
<!-- /wp:button -->

The address of each location inside a Query Loop ("postType":"location"):

<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"estrategia-web-site/location","args":{"key":"estrategia_web_location_address"}}}}} -->
<p></p>
<!-- /wp:paragraph -->

3. Content types

Since 17-10-2026 they are templates of the content-types module (see «content-types module: the constructor»): same type, taxonomy and meta names as in the table below, stored as definitions in each site's database and edited in Estrategia Web → Content types. A site that had them gets them stored once, with the URLs it serves today; enabled_types and rewrite_slugs still switch them on and set the site language's slug (modules/content-types/includes/class-site-types.php). The editor panel and the WPML config come from the definition. What follows is the contract as it was, and still is, for those names.

Each one is registered only if it is in enabled_types. Disabling it deletes nothing: its posts stay in the database and come back as they were when it is re-enabled. All of them public, show_in_rest (REST route = registered name: /wp/v2/eweb_client…), with archive, icon and labels in Spanish on es_ES.

Key and registered name (unreleased, 1.2.0). In the table, the type's key (client): it is what is ticked in enabled_types, what rewrite_slugs carries and what the estrategia_web_site_rewrite_slug filter receives. WordPress registers it with the prefix in front: eweb_client, eweb_client_sector… (the prefix lives only in modules/site/includes/class-type-names.php, Type_Names::PREFIX; decided by David on 15-10-2026, the reason in section 0). That name is the one in wp_posts.post_type, a Query Loop's "postType", the REST route and WPML. A theme asks for it with estrategia_web_type_name( 'client' ). The public URLs (/clientes/) do not change. Whatever was stored before with the unprefixed names was carried over by the prefix migrator, removed in 2.0.1 (section «Names»).

Key (registered: eweb_ + key) Menu Slug (es_ES) Uses Fields (register_post_meta, single, show_in_rest) Taxonomy
client Clients /clientes/ title = name, featured image = logo/photo, excerpt, manual order estrategia_web_client_url (URL; client_url up to 1.4.3), estrategia_web_client_image_fit (cover by default or contain; any other value → cover; 0.2.0), estrategia_web_client_business_type (text: «agencia de alquiler vacacional», what used to go in brackets in the title; translated with WPML; unreleased) client_sector (hierarchical, slug /sector/)
testimonial Testimonials /opiniones/ title (internal), content = the testimonial, image = author's photo, order estrategia_web_testimonial_author, estrategia_web_testimonial_role (text), estrategia_web_testimonial_rating (integer 0-5; 0 = no rating), estrategia_web_testimonial_client (id of a client, or 0) (the role and the client were testimonial_role / testimonial_client up to 1.4.3) —
location Locations /sedes/ title = location, content, image = static map, excerpt, order 2.0.0, each datum once («A location's data stored once», at the end): estrategia_web_location_phone, _email, _street (1-2 lines), _postal_code, _locality, _region, _country, _hours, _map_query, _static_map_id (an image attachment; otherwise 0), _latitude, _longitude, _business_type. The address and hours texts, the Maps link, the embed and the static map URL are derived (section 2 ter). Up to 1.7.0 also _address, _schedule, _map_url, _map_embed, _static_map: removed by the migration —
project Projects /proyectos/ title, content, image, excerpt, order, revisions — project_category (/categoria-proyecto/)
portfolio Portfolio /portfolio/ same — portfolio_category (/categoria-portfolio/)

Where they are edited (unreleased): on the location and testimonial screens, the «Location details» / «Testimonial details» panel in the editor sidebar; on the client one (classic screen: it has no editor), the «Client details» box. With WPML, phone, email, links, map, image fit, author and rating are copied from the original to the translation; address, opening hours, business type and role are translated. The criterion, in the header of modules/site/includes/class-meta-panel.php.

With WPML, the types and taxonomies (1.4.3) are declared by the plugin's wpml-config.xml, all as display-as-translated (translatable, showing the original while there is no translation): a new language never empties a list. WPML locks that setting on its screen; a site that needs another mode for a type changes it with the wpml_config_array filter. The criterion, in wpml-config.xml.

All the types have custom-fields, so the fields are rendered with the core/post-meta source inside a Query Loop (the business type on a client's page: a paragraph with {"source":"core/post-meta","args":{"key":"estrategia_web_client_business_type"}}). A Query Loop of the clients of one sector (id 3) with logo and name:

<!-- wp:query {"queryId":1,"query":{"perPage":12,"pages":0,"offset":0,"postType":"eweb_client","order":"asc","orderBy":"title","inherit":false,"taxQuery":{"eweb_client_sector":[3]}}} -->
<div class="wp-block-query"><!-- wp:post-template -->
<!-- wp:post-featured-image /-->
<!-- wp:post-title /-->
<!-- /wp:post-template --></div>
<!-- /wp:query -->

(Manual order —page-attributes, "orderBy":"menu_order"— is in the types but has no test in a Query Loop yet: see TODO.md.)

A field inside the Query Loop (the author of a testimonial):

<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"estrategia_web_testimonial_author"}}}}} -->
<p></p>
<!-- /wp:paragraph -->

The map of each location with estrategia-web/map, inside a Query Loop of location — since 2.0.0 through THIS source (the four values are derived; core/post-meta cannot show them):

<!-- wp:estrategia-web/map {"metadata":{"bindings":{"address":{"source":"estrategia-web-site/location","args":{"key":"estrategia_web_location_address"}},"embedUrl":{"source":"estrategia-web-site/location","args":{"key":"estrategia_web_location_map_embed"}},"mapsUrl":{"source":"estrategia-web-site/location","args":{"key":"estrategia_web_location_map_url"}},"staticMapUrl":{"source":"estrategia-web-site/location","args":{"key":"estrategia_web_location_static_map"}}}}} /-->

staticMapUrl gets the URL of the media library image (estrategia_web_location_static_map_id); the block's own staticMapId (a number) cannot be bound. The map block checks the URL again when rendering (estrategia_web_map_static_url).

Public slugs per site: rewrite_slugs in the option (0.2.0). The routes belong to the plugin, not the theme: a site with a page /clientes/ (which the type's archive would overwrite) moves the archive like this, via REST or in its content loading:

curl -u admin:APP_PASS -X POST https://web.test/wp-json/wp/v2/settings \
  -H 'Content-Type: application/json' \
  -d '{"estrategia_web_site":{"rewrite_slugs":{"client":"cliente"}}}'

Order: the translated slug ← the one in rewrite_slugs if not empty ← the filter estrategia_web_site_rewrite_slug( $slug, $type_or_taxonomy ), which still rules (it already receives the option's one). A change through the filter does not rebuild the rules on its own; one through the option does.

Classes the plugin puts on a post (post_class filter: they come out on a Query Loop's <li> and on the single page). They describe the data; the plugin adds no CSS, the look belongs to the theme:

Class When What for (in the theme)
has-image-fit-contain a client with estrategia_web_client_image_fit = contain (0.2.0) the image is a logo: it must not be cropped, e.g. .has-image-fit-contain .wp-block-post-featured-image img { object-fit: contain; }

Only on the site: the Query Loop preview in the editor is rendered by JS and does not go through post_class (deduced from core, not tested).

4. SVG

With allow_svg enabled, only whoever has unfiltered_html or manage_options can upload .svg (not .svgz). It is always sanitized on the server with an allowlist of elements and attributes (modules/site/includes/class-svg-sanitizer.php): out go `

This README is longer than the copy stored here. Read the rest on GitHub →