.label` / `.value` in the row. * All three adapters fill the same shape, so one spec-table component is identical on a commerce site and * an enquiry site. * * `specs` was added on 2026-08-22 and its origin is worth recording. Before that it was marked "not * implemented", so the only route for NexCart's attributes to reach a layout was the shadow meta * `_aumnexcart_attr_` — and the shadow is **only generated for attributes registered as "filterable" * in NexCart's settings**, while a kit could not write plugin settings at all (the importer's options section * knew only a fixed handful). * The consequence was the spec table completely blank after a buyer imported the kit, with no error, while * our own target site had been configured by hand once and showed nothing wrong — the same class of accident * as "nobody but us has that import button". * An attribute is the product's own data and should not depend on a filtering feature's switch. * * post.product.gallery additional images, a list of `{url, alt}` rows, also ready for a loop * * `gallery` works exactly like `specs` (`source:'field'`) and is likewise filled to one shape by all three. * The main image is not in it — that is WordPress's featured image (`post.thumbnail.url`) and is the * same across all three engines. `gallery` means "the ones besides the main image"; merging them would make a * detail page render the main image twice. * * # Two kinds of button, both needed * * 1. `action_url` + `action_label` — the button is drawn in the editor, its styling entirely the * designer's, and the component is 100% portable. The cost: Woo's add-to-cart is an ordinary link * navigation with no AJAX. * 2. The `[aumframe_product_action]` shortcode — the adapter emits its own native HTML (Woo's AJAX add-to-cart * button, NexCart's contact button group). Full functionality, with the appearance decided by the adapter. * * Commerce sites use 2 and everything else uses 1. A Shortcode node is all it takes; no new editor capability * is required. * * @package Aumframe */ /** * The contract an adapter implements. * * An abstract class rather than an interface, deliberately: most fields do not apply to any given engine, and * an interface would force every adapter to write a pile of `return null`. */ abstract class Adapter { /** Which content types this adapter covers. The registry indexes by it, and the editor lists fields by it. */ abstract public function post_types(): array; /** * Read one neutral field's value. * * Unresolvable must return null, never an empty string — this is Aumframe's "unresolvable is not * empty" contract: null makes the renderer keep whatever the author wrote at bake time, while an empty * string blanks that block. */ abstract public function field(string $key, int $post_id); /** The native action's HTML (for [aumframe_product_action]). Return an empty string when unsupported. */ public function native_action_html(int $post_id): string { return ''; } /** The adapter's name, shown in the admin and in logs. */ abstract public function label(): string; /** * Is this the fallback adapter? * * The content-model one is the fallback: it serves "ordinary CPTs with no dedicated engine". * A dedicated adapter (Woo, NexCart) always wins — otherwise, when two claim the same type, the winner * depends on filename order and renaming a file breaks it. */ public function is_fallback(): bool { return false; } }