Skip to content

1400 add the generate alt text buttons to the product editor in the images sections - #23568

Closed
vraja-pro wants to merge 10 commits into
feature/ai-alt-text-image-productfrom
1400-add-the-generate-alt-text-buttons-to-the-product-editor-in-the-images-sections
Closed

1400 add the generate alt text buttons to the product editor in the images sections#23568
vraja-pro wants to merge 10 commits into
feature/ai-alt-text-image-productfrom
1400-add-the-generate-alt-text-buttons-to-the-product-editor-in-the-images-sections

Conversation

@vraja-pro

@vraja-pro vraja-pro commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Context

Summary

This PR can be summarized in the following changelog entry:

  • Adds the image alt notices to the product page.

Relevant technical choices:

  • DOM-based image tracking: The three hooks (useProductImage, useProductGallery, useVariationImages) read image data directly from the classic editor DOM via querySelectorAll / MutationObserver rather than tapping into
    WooCommerce's internal JS store because it is the classic editor, keeping the integration compatible with the classic editor without relying on WooCommerce internals.
  • Thickbox close event for featured image: useProductImage observes #TB_overlay / #TB_window removal from
    document.body rather than watching #set-post-thumbnail directly. WordPress updates the thumbnail DOM before closing the thickbox, so the overlay removal is the most reliable signal that the image has been set or changed.
  • Mount point at #postimagediv instead of .inside: The featured image notice is appended to #postimagediv (the
    outer metabox wrapper) rather than .inside. WordPress's AJAX thumbnail replacement replaces the inner HTML of
    .inside, which would unmount the React tree if it were mounted there.
  • PHP pre-seeds variation images: The PHP integration passes variation image data via wpseoProductImageAlt.variationImages on page load so the variations notice renders immediately, before WooCommerce loads variation rows into the DOM via its woocommerce_load_variations AJAX call.
  • REST API fetch for authoritative alt text: After reading image data from the DOM, the hooks call the WP REST API
    (/wp/v2/media) to get the saved alt attribute from the database. Newly set images may have an empty img.alt in the DOM until the modal closes, so the REST fetch is the source of truth.

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps:

Prerequisites

  • WordPress with WooCommerce active.
  • No Woo SEO installed.
  • At least one image in the media library with no alt text set, and one with alt text set.

Test instructions for the acceptance test before the PR gets merged

Simple product — featured image:

  • Go to WooCommerce > Products > Add New (or edit an existing simple product).
  • In the Product image metabox, set a product image that has no alt text. A red "Needs improvement" notice
    should appear below the thumbnail reading "1 Product image is missing alt text."
  • Open the media library for the same image, add alt text, and save. The notice should disappear.
  • Click Remove product image. The notice should disappear.
Screenshot 2026-08-25 at 17 15 03

Simple product — gallery:

  • In the Product gallery metabox, add one or more images without alt text. A notice should appear at the bottom
    of the gallery box showing the count of images missing alt text.
  • Add alt text to all gallery images via the media modal. The gallery notice should disappear.
  • Remove all gallery images. The gallery notice should disappear.
Screenshot 2026-08-25 at 17 15 10

Variable product:

  • Edit a variable product that has variations with images set.
  • In the Variations tab, verify a notice appears at the top of the variation rows if any variation image is
    missing alt text. The notice should render immediately on page load (seeded from PHP — no waiting for AJAX).
  • Add or remove variation images and verify the notice count updates.
Screenshot 2026-08-21 at 13 33 50

Relevant test scenarios

  • If there are images with missing alt in several places, all the notices should have the same number of images with missing alt.
  • When having a notice save and refresh, check the notice is still there.

Relevant test scenarios

  • Changes should be tested with the browser console open
  • Changes should be tested on different posts/pages/taxonomies/custom post types/custom taxonomies
  • Changes should be tested on different editors (Default Block/Gutenberg/Classic/Elementor/other)
  • Changes should be tested on different browsers
  • Changes should be tested on multisite

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above.

QA can test this PR by following these steps:

Impact check

This PR affects the following parts of the plugin, which may require extra testing:

Other environments

  • This PR also affects Shopify. I have added a changelog entry starting with [shopify-seo], added test instructions for Shopify and attached the Shopify label to this PR.
  • This PR also affects Yoast SEO for Google Docs. I have added a changelog entry starting with [yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached the Google Docs Add-on label to this PR.

Documentation

  • I have written documentation for this change. For example, comments in the Relevant technical choices, comments in the code, documentation on Confluence / shared Google Drive / Yoast developer portal, or other.

Quality assurance

  • I have tested this code to the best of my abilities.
  • During testing, I had activated all plugins that Yoast SEO provides integrations for.
  • I have added unit tests to verify the code works as intended.
  • If any part of the code is behind a feature flag, my test instructions also cover cases where the feature flag is switched off.
  • I have written this PR in accordance with my team's definition of done.
  • I have checked that the base branch is correctly set.
  • I have run grunt build:images and committed the results, if my PR introduces or edits images or SVGs.

Innovation

  • No innovation project is applicable for this PR.
  • This PR falls under an innovation project. I have attached the innovation label.
  • I have added my hours to the WBSO document.

Fixes Add the "Generate alt text" buttons to the product editor in the images sections.

vraja-pro and others added 8 commits August 20, 2026 12:53
…ctions

Registers a product-image-alt script and integration that injects an
ImageAltNotice into both the featured image metabox (via the
admin_post_thumbnail_html filter) and the WooCommerce product gallery
metabox (via JS DOM injection into #woocommerce-product-images .inside),
showing the count of images missing alt text when non-zero.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lassic product editor

Rewrites useProductImages to read featured and gallery images directly
from the WooCommerce classic editor DOM and keep them in sync via
MutationObservers and jQuery/AJAX event listeners. Adds useVariationImages
as a dedicated hook that handles variation image discovery and alt text
fetching. Extracts countImagesMissingAlt into a shared helper and adds
JSDoc typedefs for ProductImage, VariationImage, ProductImagesState and
ProductImages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…se event

Replaces the stale-element MutationObserver on #set-post-thumbnail with a
body-level observer that detects when the WooCommerce thickbox overlay is
removed — WordPress updates the thumbnail DOM before closing the modal, so
the image is already readable when this fires. Moves the container mount
point to #postimagediv (outside .inside) so WordPress AJAX replacements
don't unmount React. Removes the PHP admin_post_thumbnail_html filter in
favour of JS injection. Renames shouldRenderNotice to shouldHideNotice and
updates hook return keys for isLoadingAlts to be more descriptive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
function_exists is a PHP internal that Patchwork cannot stub; Brain\Monkey
automatically defines wc_get_product when an expect() is set up, so
function_exists('wc_get_product') returns true without any mock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vraja-pro vraja-pro added the changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog label Aug 25, 2026
@coveralls-official

coveralls-official Bot commented Aug 25, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 8

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.09%) to 56.732%

Details

  • Coverage increased (+0.09%) from the base build.
  • Patch coverage: 42 uncovered changes across 3 files (200 of 242 lines covered, 82.64%).
  • 6 coverage regressions across 1 file.

Uncovered Changes

File Changed Covered %
packages/js/src/product-image-alt/initialize.js 24 0 0.0%
packages/js/src/product-image-alt/components/app.js 16 0 0.0%
packages/js/src/product-image-alt/components/image-alt-notice.js 2 0 0.0%
Total (10 files) 242 200 82.64%

Coverage Regressions

6 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
src/bulk-editor/user-interface/posts-route.php 6 93.97%

Coverage Stats

Coverage Status
Relevant Lines: 73101
Covered Lines: 41398
Line Coverage: 56.63%
Relevant Branches: 19350
Covered Branches: 11051
Branch Coverage: 57.11%
Branches in Coverage %: Yes
Coverage Strength: 41173.56 hits per line

💛 - Coveralls

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new WooCommerce admin integration and accompanying JS module to display “image alt text missing” notices (with a “Generate image alt text” CTA) in the classic WooCommerce product editor’s featured image, gallery, and variations sections.

Changes:

  • Introduces a new PHP integration that conditionally enqueues a product-image-alt admin asset and passes initial variation-image data to JS.
  • Adds a new JS entrypoint that mounts a React notice app into the WooCommerce product editor DOM and keeps image/alt state in sync via DOM observers + REST fetches.
  • Adds unit test coverage for the PHP integration plus Jest tests for the new hooks/helpers.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/Unit/Integrations/Third_Party/WooCommerce_Product_Image_Alt_Integration_Test.php Unit tests for conditional hook registration and asset enqueue/localization behavior.
src/integrations/third-party/woocommerce-product-image-alt-integration.php New WooCommerce integration that enqueues assets and provides initial variation image data to JS.
packages/js/tests/product-image-alt/hooks/use-variations-images.test.js Tests for variation image DOM parsing, refresh triggers, REST alt fetch, and cleanup.
packages/js/tests/product-image-alt/hooks/use-product-image.test.js Tests for featured image DOM parsing, thickbox detection, REST alt fetch, and cleanup.
packages/js/tests/product-image-alt/hooks/use-product-gallery.test.js Tests for gallery DOM parsing, refresh triggers, REST alt fetch, and cleanup.
packages/js/tests/product-image-alt/helpers/should-hide-notice.test.js Tests for notice visibility logic across featured/gallery/variations locations.
packages/js/tests/product-image-alt/helpers/fetch-attachment-alts.test.js Tests for REST fetching alt text and failure handling.
packages/js/tests/product-image-alt/helpers/count-images-missing-alt.test.js Tests for counting missing-alt images across featured/gallery/variations.
packages/js/src/product-image-alt/initialize.js Mounts the notices into the WooCommerce product editor DOM on domReady.
packages/js/src/product-image-alt/hooks/use-variations-images.js Hook to track variation images/alts from DOM + WooCommerce events + REST.
packages/js/src/product-image-alt/hooks/use-product-image.js Hook to track featured image/alts via DOM observers + AJAX/media events + REST.
packages/js/src/product-image-alt/hooks/use-product-gallery.js Hook to track gallery images/alts via DOM observers + gallery events + REST.
packages/js/src/product-image-alt/hooks/index.js Barrel exports for the new hooks.
packages/js/src/product-image-alt/helpers/should-hide-notice.js Helper to decide when the notice should be hidden per location.
packages/js/src/product-image-alt/helpers/index.js Barrel exports for the new helpers.
packages/js/src/product-image-alt/helpers/fetch-attachment-alts.js Helper to fetch attachment alt_text via REST and return a Map keyed by ID.
packages/js/src/product-image-alt/helpers/count-images-missing-alt.js Helper to count images missing alt text across the three image sources.
packages/js/src/product-image-alt/components/image-alt-notice.js Presentational notice component including the “Generate image alt text” button.
packages/js/src/product-image-alt/components/app.js App wiring: gathers image state, decides visibility, and renders ImageAltNotice.
config/webpack/paths.js Registers product-image-alt as a webpack entry.
admin/class-admin-asset-manager.php Registers the product-image-alt stylesheet in the admin asset manager.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/js/src/product-image-alt/components/app.js Outdated
Comment thread packages/js/src/product-image-alt/components/app.js
Comment thread packages/js/src/product-image-alt/hooks/use-variations-images.js Outdated
vraja-pro and others added 2 commits August 26, 2026 09:45
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

@JorPV JorPV left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments 🚧 :

}, [] );
const initialVariationImages = get( window, "wpseoProductImageAlt.variationImages", [] );

const { featuredImage, isLoadingFeaturedImageAlt } = useProductImage();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three hooks (image, gallery, variations) run here even though this mount only needs one location's data — that triples REST calls and DOM observers across the three notice mounts.

const { galleryImages, isLoadingProductGalleryAlts } = useProductGallery();
const { variationImages, isLoadingVariationImagesAlts } = useVariationImages( initialVariationImages );

const isLoading = isLoadingFeaturedImageAlt || isLoadingProductGalleryAlts || isLoadingVariationImagesAlts;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of the above, this waits on all three sections' fetches before showing anything — so a section whose own data is ready still waits on the other two.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats ok, because the count in the notice is for all the sections.

const isLoading = isLoadingFeaturedImageAlt || isLoadingProductGalleryAlts || isLoadingVariationImagesAlts;

const imagesWithoutAlt = useMemo( () =>
countImagesMissingAlt( { featuredImage, galleryImages, variationImages } )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This counts missing alt text across all sections, but it's shown identically in every notice — each section's notice ends up reporting the combined total, not just its own count.

const ids = images.map( ( img ) => img.id ).filter( ( id ) => id > 0 );
const alts = await fetchAttachmentAlts( ids );

setGalleryImages( images.map( ( img ) => ( { ...img, alt: alts.get( img.id ) ?? img.alt } ) ) );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If two refresh() calls overlap and resolve out of order, this can overwrite newer state with a stale image list — no check for which call is the latest.

// Re-run when images are added or removed from the gallery <ul>.
const galleryContainer = document.querySelector( ".product_images" );
if ( galleryContainer ) {
const galleryObserver = new MutationObserver( () => refresh() );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This MutationObserver setup/teardown is hand-rolled here and in the other two hooks — there's already a useMutationObserver hook in packages/js/src/hooks for this. Also worth debouncing the callback (like editor-watcher.js does), since refresh() re-scans the DOM and refetches on every single mutation.

ids.map( ( id ) =>
apiFetch( { path: `/wp/v2/media/${ id }?_fields=id,alt_text` } )
.then( ( media ) => [ media.id, media.alt_text ?? "" ] )
.catch( () => [ id, "" ] )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed fetches map to "", same as a real empty alt — so the ?? img.alt fallback in the callers never actually triggers on failure, and a network hiccup wrongly flags an image as missing alt text.

/**
* Counts the number of product images missing alt text across featured, gallery, and variation images.
*
* @param {object|null} featuredImage The product's featured image, or null.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are documented as three separate params, but the function actually takes one destructured object — doc doesn't match the function.

return true;
}

const countGalleryImagesWithoutAlt = galleryImages.filter( ( img ) => ! img.alt ).length;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two counts are computed even when location is 'product-image' (already returned above).

// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, used only to pass data to JS.
$post_id = isset( $_GET['post'] ) ? (int) $_GET['post'] : 0;

// Use add_inline_script with wp_json_encode so that number values are

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says add_inline_script + wp_json_encode is used to keep numbers as numbers, but the code below actually calls localize_script (which wraps wp_localize_script) — comment doesn't match the implementation.

$this->asset_manager->localize_script(
'product-image-alt',
'wpseoProductImageAlt',
[ 'variationImages' => $this->get_variation_images( $post_id ) ],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No current_user_can check before this data goes to JS. Not exploitable today since WP core already gates this admin page, but worth a defensive check if this method ever gets reused elsewhere.

@vraja-pro

Copy link
Copy Markdown
Contributor Author

Moved that logic to the wpseo-woocommerce repo: https://github.com/Yoast/wpseo-woocommerce/pull/1197/changes.

@vraja-pro vraja-pro closed this Aug 26, 2026
@vraja-pro
vraja-pro deleted the 1400-add-the-generate-alt-text-buttons-to-the-product-editor-in-the-images-sections branch August 26, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants