Changelog iga-core

Change history of the iga-core plugin.

IGA (Infinity Gems)Updated 21 Oct 2026

Changelog iga-core

Note on terms: "mệnh" is the Vietnamese five-element (feng shui) type of a person. The five elements are Kim (Metal), Mộc (Wood), Thủy (Water), Hỏa (Fire) and Thổ (Earth).

0.5.33 - 2026-10-17

  • /chon-menh/: when every stone type has been chosen, the page opens showing all 62 types (the "Tất cả" (All) filter) so staff can review and edit them; a type not yet chosen still shows only that type.

0.5.32 - 2026-10-16

  • data/menh.json + data/stones.json: Quartz (type unknown), green quartz and rutilated quartz (color unknown) are assigned all 5 elements as chosen by the advisors (when the color is unknown, all are selected).

0.5.31 - 2026-10-16

  • /chon-menh/: by default shows only the stone types not yet chosen (the "Chưa chọn" (Not chosen yet) filter button comes first); as soon as a type is chosen it hides itself; when none are left it reports that everything has been chosen.

0.5.30 - 2026-10-16

  • data/menh.json + data/stones.json: applied the advisors' choices (a screenshot of the /chon-menh/ page at 23:12) for 23 stone types that differ from the color table: Topaz (Metal, Water); golden rutilated quartz, yellow quartz (Metal, Water, Earth); tiger-eye quartz, storm stone (Wood, Water); dragon-eye stone, fluorite (Wood, Fire, Earth); labradorite (Wood, Fire); pyrite (Metal, Water); all 5 elements: opal, zircon, beryl, ametrine, tri-color rutilated quartz, five-color quartz, Super 23, rainbow quartz, Super Seven, chalcedony, water-algae ice jade, amazonite, mica, Mix.

0.5.29 - 2026-10-16

  • /chon-menh/: choosing an element saves immediately (the note saves itself after 1.2 seconds), with no Xác nhận (Confirm) step that is easy to forget; a missing name is reported clearly and saved as soon as the name is entered; choices that could not be saved are kept in the browser (localStorage) and sent automatically when the page is reopened.

0.5.28 - 2026-10-16

  • data/menh.json + data/stones.json: "ưu linh" quartz (phantom quartz) and Super Seven quartz are assigned all 5 elements (no element to avoid, per the shop).

0.5.27 - 2026-10-16

  • data/menh.json + data/stones.json: ruby, sapphire, diamond, jadeite, amber, tourmaline and "ưu linh" quartz are assigned all 5 elements (field menh + moi_menh: true) per the shop: gemstones have no element to avoid. This override wins even over the color in the product name (for example a yellow sapphire).

0.5.26 - 2026-10-16

  • New internal page /chon-menh/ (class Tools\Menh_Vote, assets/js/chon-menh.js, assets/css/chon-menh.css): advisors choose the suitable elements for each stone type in data/menh.json, with a note box and a field for the chooser's name. Saved through GET/POST /wp-json/iga/v1/chon-menh, one option iga_chon_menh__<slug> per stone type (autoload no, keeping 10 old versions). Only enabled when WP_ENVIRONMENT_TYPE is staging or IGA_CHON_MENH = true; the page is noindex and not cached.

0.5.25 - 2026-10-16

  • Data\Menh::for_product: applies the stone type's menh override when assigning to each product (before, only stone_menh read this field, so 0.5.24 changed no product); the override wins even over the color written in the product name.

0.5.24 - 2026-10-16

  • data/menh.json: a stone type can override its element with the menh field (Data\Menh::stone_menh); jadeite is assigned Wood only (Fire removed) per the advisors' opinion.

0.5.23 - 2026-10-16

  • New command wp iga menh (class CLI\Menh_Command, pure logic in Data\Menh, table in data/menh.json): reassigns the "Mệnh hợp" (Suitable element) attribute (pa_menh) by the stone type's DOMINANT COLOR. A stone of element E suits element E and the element that E generates (red ruby: Fire, Earth; golden rutilated quartz: Metal, Earth; blue/black sapphire: Wood, Water). A multi-color stone type reads the color written right after the stone name in the product name; if there is none, it takes the element stated explicitly in the shop's description; if there is none either, it stays empty (no guessing).
    • apply --dry-run [--report=<file>] gives statistics and lists products whose shop description states a different element from the new table; apply --backup-dir=<dir> backs up rows.jsonl + rows.sha256 (read back to verify), then writes term_relationships in one transaction, recounts the term counts, reindexes the filter for exactly the changed products, changes the filter cache version and reads back to compare; rollback --dir=<dir> [--force]; table prints the table. Running it again changes nothing more.
  • data/stones.json: the menh column of the 62 stone types changed to follow menh.json (32 types changed; menh_source is now only rule or none; multi-color types are left empty instead of listing all 5 elements), with a matching test (tests/phpunit/Data/MenhTest.php).
  • Schema\Product_Schema: removed hasMerchantReturnPolicy (a 3-day return MerchantReturnPolicy) and shippingDetails (an OfferShippingDetails with free shipping at 0 VND) from every Offer of Product/ProductGroup. The shop does not commit to returns, and production only gives free shipping when the customer pays by bank transfer in advance, so the schema must not make promises on the shop's behalf. An Offer with no price is still dropped as before.

0.5.22 - 2026-10-16

  • Content\Desc_Fix (the lead paragraph of wp iga desc-fix) dropped two self-inferences: (1) "natural" is only written when the product NAME or a SEPARATE line of the short description says it is natural (the shop's general commitment line, which starts with a tick mark or contains "cam kết" (commitment) and appears in most short descriptions, no longer counts; new function excerpt_product_text()); (2) origin is only written as "originates from X" exactly as in the data (preferring the most specific entry such as "Phan Thiết (Bình Thuận)"), no longer "mined at", because production only has the label "Xuất xứ" (Origin). Tests updated + added test_sapo_natural_only_from_product_own_text.
  • CLI\Lp5_Hero (the overview page /bo-suu-tap/): the intro sentences of the 3 groups take the production wording verbatim or just list the model names ("Tantric Buddhism proposes 8 Bản Tôn deities who govern the 12 zodiac animals"; removed "the most worshipped and worn" and "bringing luck, wealth and peace" because production does not have them).

0.5.21 - 2026-10-16

  • wp iga desc-fix v2 (a better-structured description, run on the ORIGINAL content): the LEAD paragraph is built from the product's real data (the name without a bold "#code", product type, motif, stone type, origin, stone group + an easy-to-understand introductory sentence taken from the short description); a NEW H2 names the old opening paragraphs ("Meaning of {motif}", "Meaning of the Buddha of your birth sign ...", "Meaning of ... stone", "About {name}"); the part A heading (motif) becomes an H3 under the opening H2, the heading that opens part B (stone type) is an H2 and the later sections are H3/H4; with no part B the old heading is an H3 (an H2 is kept when there are 3 or more major topics of the same level). A description that already opens with a lead paragraph is skipped (idempotent).
  • Option --original-from=<backup folder> (only together with --dry-run): preview on the original content taken from the rows.jsonl of an earlier run without having to roll back the DB.
  • Content\Headings::retag()/step() were split out to assign heading levels by their own rule; the test tests/phpunit/Content/DescFixTest.php was rewritten for v2.

0.5.20 - 2026-10-16

  • New command wp iga desc-fix (class CLI\Desc_Fix_Command, pure logic in Content\Desc_Fix): normalizes product descriptions (post_content) directly in the DB.
    • Removes a title line at the start that repeats the product name (any plain-text h1; h2/h3 only when they repeat the name; section headings such as "Ý nghĩa ..." (Meaning of ...) or "... là gì?" (What is ...?) are kept).
    • Builds the lead paragraph: if the next block is a <blockquote> it is converted into <p> elements (the first p gets class="sapo"); if it is a <p>/bare text of 60 characters or more it gets class="sapo"; otherwise it takes the first introductory line of the short description (not a "Label: value" line, not a commitment line with a tick mark, 40 characters or more) as <p class="sapo">.
    • Headings: h1 becomes h2, the highest level is h2, with no skipped levels (an opening h3 becomes h2, an h4 after an h2 becomes h3).
  • New command wp iga headings-fix [--type=page,post] (class CLI\Headings_Fix_Command, pure logic in Content\Headings): the same heading rules for published pages and posts. For pages built with blocks: changes level in the wp:heading comment and the HTML tag together, checked with parse_blocks before/after (same block count, level matches the tag). Skips WooCommerce pages; an empty heading or one that holds only an image is not changed (only noted).
  • Both commands share CLI\Content_Fix_Base: --dry-run (statistics + before/after samples), a backup of rows.jsonl outside the webroot (read-back check + hash) before writing, writing with $wpdb->update in one transaction and then reading back to compare hashes, clean_post_cache for each ID, --rollback=<folder> (hash check per row), and running it again changes nothing more. Every row goes through a before/after text check; a row that fails is left untouched.
  • Tests: tests/phpunit/Content/HeadingsTest.php, DescFixTest.php.

0.5.19 - 2026-10-15 (batch G1)

  • New command wp iga image-swap (class CLI\Image_Swap_Command, pure logic in Media\Image_Swap): replaces images taken from the internet with AI images in the post_content and post_excerpt of posts/products.
    • import --dir=<folder>: imports new images into the Library with wp media import (title, Vietnamese alt text, caption "Ảnh AI tạo (G1)" (AI-generated image (G1))), and sets the meta _iga_g1_job; running it again does not import duplicates.
    • swap [--dry-run|--apply --backup-dir=<outside the webroot>]: replaces the original-size URL and every -WxH size (src, srcset, percent-encoded form), the wp-image-<id> class and the "id":<id> of the wp:image block. Banners use products in the manifest to change only in the right product group. Backs up rows.jsonl (read-back check + hash), writes in one transaction, and reads back to compare hashes before the COMMIT; it does not delete the old images and does not change post_modified.
    • rollback --dir=<backup folder> [--force]: restores and checks the hash of each row.
    • A job with supersedes (a redo) also replaces the AI image of the earlier version; a job with featured_posts also changes the featured image (_thumbnail_id) of those posts (backs up thumbs.json, and the rollback checks the values).
    • The job list: data/image-swap/g1.json (cumulative across rounds). Tests: tests/phpunit/Media/ImageSwapTest.php.

0.5.18 - 2026-10-15

  • Fixed the "Xem thêm" (See more) button not being clickable in a group that has a search box (the button took the "Không có lựa chọn phù hợp" (No matching option) line as the list).

0.5.17 - 2026-10-15

  • Price ranges: the label "nghìn" (thousand) was changed to "k" (data/price-buckets.json); the "Giá liên hệ" (Contact for price) option is not shown (but is still kept when an old URL has it selected, and is still counted in the price coverage rate).

0.5.16 - 2026-10-15

  • A filter group with more than 10 options has a search box (typing without diacritics still matches, and while typing the entries after "Xem thêm" (See more) are shown too; the typed text is kept when the filter group is redrawn).
  • sort: "alpha" (Vietnamese A-Z through Collator vi_VN) for Stone type and Motif.
  • Removed the Origin (Xuất xứ) filter group from every type_facets set (the definition is kept so old URLs with xuat-xu still filter).

0.5.15 - 2026-10-15

  • The Stone type, Craft (Chế tác) and Motif (Hình tượng) filters show a flat list (ui list) instead of a group tree; Query_Engine::term_options() drops the pure group terms g-* from the flat list (unless one is selected through the URL). Origin stays a tree.

0.5.14 - 2026-10-14 (batch LP5)

  • wp iga page-fixes step 9 (Lp5_Hero::enable_load_more): turns on loadMore for the iga/product-grid blocks, keeping columns and rows; running it again changes nothing.

0.5.13 - 2026-10-14 (batch LH1)

  • wp iga page-fixes step 10 (Lh1_Map): on the lien-he (contact) page, replaces the HTML block containing a Google Maps iframe (plus the 15px spacer block right after it) with the theme's iga/store-map block (coordinates, name, address and place_id of the "Infinity Gems An Thịnh" Google profile). Only writes when there is exactly one Google Maps frame and no map block yet; running it again changes nothing. Backs up outside the webroot, checks hashes, and rolls back with --rollback.

0.5.12 - 2026-10-14 (batch F2)

  • Removed the ton-kho (Availability) filter group: data/facets.json, Query_Engine (kind stock), Index_Builder (no longer writes a ton-kho row). Archive::redirect_legacy_params() (template_redirect) 301-redirects a URL containing ton-kho to the URL without that parameter; Request ignores the parameter.
  • Index_Builder::PRIORITY_AVAILABLE/PRIORITY_SOLD: stock_priority is now only 1 (on sale, including onbackorder) and 3 (outofstock); Indexer::reconcile() fixes mismatches under the new rule.
  • Schema::DB_VERSION 2: the upgrade step deletes the ton-kho index rows, changes stock_priority 2 and 4 to 1, and raises the filter cache version.
  • New class Filter\Stock_Order: the query variable iga_sold_last; posts_clauses adds a LEFT JOIN on the iga_product_sort table (primary key product_id; if there is no index yet it uses the meta _stock_status) and inserts the sold-out expression at the start of ORDER BY (so pagination is correct). Measured: a JOIN on postmeta is 10-40 times slower (postmeta has no index on post_id + meta_key), while a JOIN on the sort table is not; it turns on automatically for the main product queries (search, tag, attribute, category, shop); posts_pre_query reorders the search results of the tandoanh-code engine.
  • The old product grid shortcode (Compat\FlatsomeShortcodes::sc_products) turns on iga_sold_last.

0.5.11 - 2026-10-14 (batch LP5)

  • wp iga page-fixes step 8 (Lp5_Hero::strip_dup_heading): removes the first heading in the content that duplicates the hero's H1, and the spacer block right after it; running it again changes nothing.

0.5.9 - 2026-10-14 (batch LP5)

  • wp iga page-fixes: step 6 adds a vertical image focal point keyed by the image key (AI_FY) and swaps the image when the image or the focal point changes; step 7 updates the imageId of the 6 stone cards on the home page to the newest AI image.

0.5.8 - 2026-10-14 (batch LP5)

  • wp iga page-fixes step 7 (Lp5_Hero): places an iga/collection-grid block of type stone "Bộ sưu tập theo loại đá" (Collection by stone type) in the spot of the old "Chọn theo loại tác phẩm" (Choose by type of piece) block on the home page and removes the old block (if the stone block is already elsewhere it is moved back); if the old block is gone it is inserted before the "Top sản phẩm nổi bật" (Top featured products) grid; skipped if it is already in the right place or the AI images are missing. Backup and rollback work like the earlier steps.

0.5.4 - 2026-10-14 (batch P3D)

  • New Portfolio\Portfolio: re-registers the post type featured_item, the category featured_item_category, the tag featured_item_tag and the Flatsome Portfolio URL rules (/bo-suu-tap/<category slug>/, /bo-suu-tap/<comma-joined category slugs>/<item slug>/, /featured_item/). It only runs when this post type does not exist yet; the URL prefix can be changed with the filter iga_core_portfolio_base. It writes nothing to the DB. After copying the plugin onto a site you must run wp rewrite flush (as the www-data user) once.
  • Forms\Ask_Price: the price-inquiry key bst-<ID> for 3D jewelry models (published featured_item posts); subject() and message() take an extra noun (default "sản phẩm" (product), for 3D models "mẫu trang sức 3D" (3D jewelry model)). WooCommerce products are unchanged.
  • Test: tests/phpunit/Portfolio/PortfolioTest.php.

0.5.3 - 2026-10-14 (batch Q2)

  • Editor\Search keeps type=post, shows the date/category and only warns when a post has no featured image (the 1200 px warning was removed).
  • wp iga page-fixes: task 1c adjusts the iga/news-feed block of /kien-thuc/ (showBreadcrumb, showHeading, showFeatured, showChips = false; excludeCategories = the category with slug khuyen-mai) and the excludeCategories of the banner. Backup/rollback like the earlier tasks.

0.5.2 - 2026-10-14 (batch Q2)

  • Editor\Search: a new kind type=post for the post picker of the iga/news-banner block (diacritic-insensitive filter on the titles of published posts, cached for 5 minutes; returns the date, category, a small image, and a warning when the post has no featured image or the image is under 1200 px).
  • wp iga page-fixes: added task 1b: if the kien-thuc page already has an iga/news-feed block but no banner, it inserts an iga/news-banner block (the newest posts with a large image, 5 posts, 7 seconds) at the very start of the content, leaving the rest as it is. It has a backup outside the webroot, a hash check and a rollback like the earlier tasks; running it again changes nothing.

0.5.1 - 2026-10-13 (batch Q1)

  • assets/js/iga-filter.js (+ .min.js): the state class on <html> when the drawer opens was renamed to is-filter-open / is-sort-open (the old name matched the open button's class, so the button CSS was applied to <html> and broke the page at 1100px and below).
  • New WP-CLI command wp iga page-fixes [--dry-run|--apply --backup-dir=<dir>|--rollback=<dir>]: (1) the kien-thuc page is rebuilt with a single iga/news-feed block (only written while it still shows signs of the old version: a linear-gradient banner or iga/news-teaser, and no iga/news-feed yet); (2) an iga/product-grid block whose heading repeats the page title text has its heading removed (the H1 and H2 are no longer the same). Backs up JSON + ROLLBACK.sql outside the webroot, checks the hash of each file, checks that slug/title/status/meta are unchanged after writing, and rolls back with --rollback. Reusable at go-live.

Chưa phát hành (Unreleased)

Fixed (batch L8, security and SEO)

  • Ip::client: behind Cloudflare it only trusts CF-Connecting-IP (before, it also read X-Forwarded-For/X-Real-IP that a visitor could send through Cloudflare, so changing IP could dodge the rate limit); behind an internal proxy it takes the right-most untrusted hop of X-Forwarded-For. Added Ip::bucket(): IPv6 is grouped by /64 in Rate_Limiter (also applied to Forms\Throttle).
  • Filter\Seo::print_canonical: filter/sort URLs are both noindex,follow and carry a canonical to the clean URL (Rank Math drops the canonical tag on noindex pages, so the old canonical filter had no effect).
  • Editor\Search: the route iga/v1/editor-search does not return unpublished products to a person who has only edit_posts (checks read_post).
  • Tests: IpTest (CF ignores XFF/X-Real-IP, right-most hop, IPv6 /64), RateLimiterTest (the same /64 shares one counter).

0.5.0 - 2026-10-13

Batch LP3: landing pages that can be edited the hungphat way (the right column shows the right settings panel for each section).

Added

  • wp iga blocks-upgrade --dry-run | --apply --backup-dir=<dir> | --rollback=<dir>: upgrades raw block pages (core/group, core/cover, core/heading, core/spacer produced by wp iga ux2blocks) into iga/* blocks. It only recognizes known patterns and leaves unknown ones alone; the home page (page_on_front) is never touched.
    • A. A pair of banner covers (one iga-hide-sm for desktop, one iga-show-sm for phones) becomes a one-slide iga/hero-slider (desktop image, phone image, image focal point, "Thấp" (Low) style). If the phone text equals the desktop text they are merged; if it differs it is kept as mobileTitle. A desktop banner standing alone is left as it is (converting it would make it also appear on phones).
    • B. An h2 heading (with its spacing) placed before an iga/product-grid that has no heading yet becomes the grid's heading attribute ("Gọn" (Compact) style); a heading with nested tags, h3 or deeper, or an unknown class is not merged.
    • C. A plain iga-sec/iga-col group that, after B, holds only iga/* blocks loses its wrapper.
    • It only writes a page that passes every check: text matches 100% (using the text extractor of Ux\Text), the set of links, image IDs and URLs, product grid settings, the H1 count unchanged, block names registered, and the block sequence re-reads stably. After writing it checks that slug, title, status and meta (including Rank Math and template) are unchanged; on an error it restores that page by itself.
    • Backup OUTSIDE the webroot before writing: pages/<ID>.json (content and all meta, with a hash), ROLLBACK.sql, posts-rows.sql.gz (a mysqldump REPLACE INTO of exactly the post rows to be written, read back to verify), manifest.json, report.json. The DB password for mysqldump goes through a temporary option file with chmod 600, deleted immediately. The old version is still in the revisions.
    • IGA\Core\Upgrade\Transformer (pure PHP on the block array), IGA\Core\Upgrade\Checker, IGA\Core\CLI\Blocks_Upgrade_Command. PHPUnit tests/phpunit/Upgrade.
  • Ux\Text: the attribute key mobileTitle counts as display text.

Changed

  • Ux\Runner::meta_hash does not count rank_math_og_content_image (Rank Math's OG image cache recomputes itself when the page is viewed, which skewed the meta hash on rollback).

0.4.1 - 2026-10-13

Batch L6: public forms (contact, price inquiry, checkout). Writes nothing to the DB, and no data command is needed when going to production.

Added

  • IGA\Core\Forms\Phone: correct Vietnamese phone number validation (mobile 03x 05x 07x 08x 09x according to the prefix ranges currently issued, landlines 02x of 11 digits, the forms +84, 84, 0084, +84 (0) ..., accepting spaces, dots, hyphens and parentheses). It replaces the broken regex /.../g of the old Flatsome child theme (the g flag does not exist in PCRE, so the function always returned false). Attached to wpcf7_validate_tel and wpcf7_validate_tel* with a clear error message that gives an example.
  • IGA\Core\Forms\Turnstile: Cloudflare Turnstile for CF7 through CF7 6.x's built-in integration. The keys come from the CONSTANTS IGA_TURNSTILE_SITEKEY and IGA_TURNSTILE_SECRET in wp-config.php (filters wpcf7_turnstile_sitekey, wpcf7_turnstile_secret); the real keys are not stored in the DB. When the keys exist, the form drops the "10 x 5 = ?" quiz block and adds [turnstile theme:dark size:flexible] before the submit button (the form template is rewritten at runtime, without editing the form in the DB); when there are no keys the quiz stays as before.
  • IGA\Core\Forms\Throttle: rate limiting by IP and by group (contact 5 times/10 minutes, orders 12 times/10 minutes, logins 10 times/10 minutes; filterable through iga_core_throttle_limits), reusing Filter\Rate_Limiter and Filter\Ip (it only trusts the Cloudflare headers when the connection comes from a Cloudflare/internal range). The counter: persistent object cache if available (Redis), otherwise an expiring transient. The contact form counts after the field checks pass and before the Cloudflare call; checkout only counts a valid order attempt (it does not block a customer who mistypes).
  • IGA\Core\Forms\Ask_Price: /lien-he/?hoi-gia=<product code> pre-fills the subject Hỏi giá sản phẩm <name> (<code>) (Price inquiry for product <name> (<code>)) and the message ... <link> for the CF7 form (default field names your-subject, your-message; changeable through iga_core_ask_fields) together with a line "Bạn đang hỏi giá" (You are asking for the price of). The GET parameter is only a lookup key (letters, digits, . _ -, at most 64 characters); the text filled in comes from the DB, so there is no XSS path. A page with the parameter does not go into the page cache.
  • IGA\Core\Forms\Comment_Guard: comments posted straight to wp-comments-post.php are limited to 3 times/10 minutes/IP (returns 429).
  • IGA\Core\Forms\Woo_Guard: rate limiting for Woo orders and logins. No captcha is used at checkout (every captcha failure loses an order).
  • CF7 only loads its JS (and the Turnstile api.js) on pages that have a form (wpcf7_load_js, the filter iga_core_cf7_needed for other places).
  • The phone field of the form raises maxlength from 11 to 20 (numbers with spaces or +84), keeping minlength.
  • PHPUnit tests tests/phpunit/Forms (47 phone number cases, rate limiting, form rewriting, price inquiry).

0.4.0 - 2026-10-13

Batch LP v2: live search for the block editor.

Added

  • IGA\Core\Editor\Search: REST GET /iga/v1/editor-search?type=product|product_cat|product_tag|category|term[&tax=pa_*]&q=..|ids=..|slugs=..&limit<=20, with the edit_posts capability. Products use tandoanh-code's diacritic-insensitive Vietnamese search engine (index, cache) when available, otherwise a name and SKU query that ignores diacritics; it returns a small image, name, code, price or "Giá liên hệ" (Contact for price), and stock. Terms are filtered in place without diacritics (cached for 5 minutes). Returned text does not use long dashes. Verified with PHPUnit tests/phpunit/Editor.

Chưa phát hành (Unreleased)

Fixed (batch L8, security and SEO)

  • Ip::client: behind Cloudflare it only trusts CF-Connecting-IP (before, it also read X-Forwarded-For/X-Real-IP that a visitor could send through Cloudflare, so changing IP could dodge the rate limit); behind an internal proxy it takes the right-most untrusted hop of X-Forwarded-For. Added Ip::bucket(): IPv6 is grouped by /64 in Rate_Limiter.
  • Filter\Seo::print_canonical: filter/sort URLs are both noindex,follow and carry a canonical to the clean URL (Rank Math drops the canonical tag on noindex pages, so the old canonical filter had no effect).
  • Editor\Search: the route iga/v1/editor-search does not return unpublished products to a person who has only edit_posts (checks read_post).
  • Tests: IpTest (CF ignores XFF/X-Real-IP, right-most hop, IPv6 /64), RateLimiterTest (the same /64 shares one counter).

Changed

  • Filter JS: added assets/js/iga-filter.min.js (esbuild; 22.2 KB down to about 11 KB), with no behavior change. Assets::enqueue loads the minified build when the file exists and SCRIPT_DEBUG is off; the file name in uploads carries the mtime so cache-busting is automatic.

Added

  • IGA\Core\Schema\Product_Schema (batch L3): adds Product schema for Rank Math, replacing the hard-coded snippet of the old Flatsome child theme. MerchantReturnPolicy (3-day return, applicableCountry VN) and OfferShippingDetails are attached to every Offer; a product with a price of 0 or empty outputs no Offer; adds a VideoObject for products that have a YouTube video, with Product.video pointing to it. Filters iga/schema/return_policy, iga/schema/shipping.

0.2.1 - 2026-10-13

Batch L2 (category page UI) needs a few small things in the filter module.

Added

  • Filter iga/filter/mobile_breakpoint (default 959): the point where it switches to a drawer; the renderer writes data-iga-bp into the filter column and the JS reads it instead of a fixed number.
  • Filter iga/filter/grid_html: the theme changes the empty state and prioritizes loading the first card image (LCP) on both the page-load path and the AJAX path.
  • Filter iga/filter/card_version: a card markup version stamp that goes into the card cache key, so when the theme/card changes the old cards are no longer used.
  • JS: supports multiple chip areas [data-iga-chips] (the filter column and above the grid) and an error frame supplied by the theme ([data-iga-error], [data-iga-error-msg], [data-iga-retry]), shown right in the results area even when the drawer is closed.
  • A selected chip has a group name prefix ("Loại đá: Sapphire" (Stone type: Sapphire)).

Fixed

  • The lightweight endpoint does not load Rank Math, so product and category URLs in the AJAX response were WooCommerce's original URLs (/sản phẩm/slug/, /danh-muc-san-pham/cha/con/), different from the URLs on the page and needing a 301. The endpoint now rebuilds the same /slug/ URLs according to Rank Math's settings (it only reads the option rank-math-options-general); the JS changes the base part of the response URLs (pushState, pagination links) to the page's data-iga-base.
  • The counts in the filter column and the number of ticked options no longer have parentheses (the theme styles them itself).

0.3.0 - 2026-10-13

Batch LP: converting UX Builder to blocks and a Flatsome compatibility layer.

Added

  • wp iga ux2blocks --dry-run|--apply [--ids=..] [--backup-dir=..] [--out-dir=..] [--no-home] [--home-file=..] and --rollback=<folder> [--force]. It parses Flatsome shortcodes (nested several levels, double-quoted/single-quoted/unquoted attributes, ] inside values, NBSP between attributes, unclosed shortcodes, stray closing tags) and converts them to core blocks (heading, paragraph, list, quote, image, gallery, columns/column, group, cover, buttons, details, spacer, separator, YouTube embed, html, shortcode) and iga/* blocks (ux_products becomes iga/product-grid with the same source, ux_slider becomes a banner-style iga/hero-slider, blog_posts becomes iga/news-teaser).
  • Text is kept word for word: the text is compared before/after on every page, and only written when it matches 100% (ignoring whitespace) and the blocks are valid. [block id=N] becomes a reusable wp_block block (meta _iga_ux_block) so editing in one place changes every page. The home page (page_on_front) is replaced by themes/iga/data/pages/home.blocks.html.
  • Safety: backs up post_content + all meta of each page to a folder OUTSIDE the webroot and checks hashes before writing, writes post_content/post_modified directly (with a revision of the old version; it does not change the slug, title, template or Rank Math meta), checks again after writing and restores a failed page by itself, ROLLBACK.sql + --rollback with a hash check after restoring.
  • IGA\Core\Compat\FlatsomeShortcodes: minimal rendering of section/row/col/gap/title/button/ux_image/ux_banner/ux_slider/ux_products/ux_gallery/block/blog_posts... only when the shortcode does not exist (Flatsome is not running), so a raw shortcode is never shown; the filter iga_core_flatsome_compat_tags.
  • The action iga_core_ux2blocks_page_written after each page is written (to clear the cache).

0.2.0 - 2026-10-13

Batch F1: writing filter data and the AJAX filter module.

Added

  • wp iga extract --apply [--batch=200] [--only=<group>] --backup-dir=<folder>: writes the taxonomies pa_loai-san-pham, pa_loai-da, pa_nhom-da, pa_che-tac, pa_hinh-tuong, pa_menh, pa_xuat-xu, pa_chat-lieu (registering the WooCommerce attribute if it does not exist) and the measurement meta _iga_*. Idempotent, in batches inside a transaction, does not change post_modified, does not touch descriptions, only writes high and medium confidence, skips attributes already used as variations, and keeps existing unknown terms.
  • A backup before writing (compressed file, a manifest with hashes and row counts, table checksums), a ROLLBACK.sql built before writing; wp iga extract --rollback=<folder> runs in one transaction and then compares the checksums.
  • The element-by-stone-color table is editable in the admin (option iga_core_menh_overrides, D-12) and is read by the extractor.
  • The IGA\Core\Filter filter module (forked from hungphat product-filter v6.12.1): the tables {prefix}iga_filter_index and {prefix}iga_product_sort, an indexer that rebuilds in batches without TRUNCATE, reindexes on save, stock change or price change (including direct meta writes), an hourly reconcile, and a rebuilding flag together with DONOTCACHEPAGE.
  • A shared Query_Engine for page loads through URLs (replacing the main query) and AJAX; AND between groups, OR within a group, and counts calculated excluding the current group so choosing one value does not hide other values in the same group.
  • Filter groups: the pa_* above, price in the 7 ranges of D-09 and a "Giá liên hệ" (Contact for price) group (D-02, always placed last when sorting by price), g-* measurement ranges (height, weight, carat, bead size), stock, Has video; configured per product type in data/facets.json (D-10).
  • A lightweight endpoint runtime/iga-filter.php (copied to the webroot) + the mu-plugin runtime/iga-fast-filter.php: a limit of 60 requests per minute per IP (429), Redis response cache with a site prefix and version, and no nonce needed for visitors.
  • assets/js/iga-filter.js (no jQuery): pushState, popstate, AJAX pagination that updates the URL, a drawer with a focus trap, a "Xem N kết quả" (View N results) button; sorting is a button.
  • Canonical to the clean URL and noindex,follow for filter URLs (Rank Math and wp_robots).
  • Admin page iga-core > Bộ lọc (Filters): index status, a rebuild button, data coverage, unmapped values, a list needing manual fixes (D-14), the element table, broken YouTube links, settings.
  • IGA\Core\Video\ProductVideo::get() and iga_product_video(): YouTube video from the Flatsome meta wc_productdata_options, independent of the theme; the filter iga/product_video.
  • Template tags for the theme: iga_filter_toolbar(), iga_filter_sidebar(), iga_filter_results(), iga_filter_pagination(); the filter iga/filter/card_html lets the theme replace the product card.
  • The command wp iga filter rebuild|reindex|reconcile|status|query.

Notes

  • Measurement meta names: _iga_cao_cm, _iga_trong_luong_g, _iga_carat, _iga_size_hat_ly...
  • The index tables are created when the plugin is activated and only deleted when the plugin is uninstalled (uninstall.php); deactivating does not delete data.

0.1.0 - 2026-10-13

  • Plugin skeleton, the attribute extractor (wp iga extract --dry-run), mapping tables for stone type, element, motif, origin and material.