Every WooCommerce fix we catalog
SEOLZ catalogs 182 fixes for WooCommerce across 6 areas — SEO, answer-engine readiness, accessibility, security and site-health. Each lists the exact steps for WooCommerce, with a link to the official docs.
SEO · 52 fixes
Catalog coverageModerate effort
Audit your XML sitemap and internal linking so that every product and category page is discoverable and crawlable by Google — not hidden behind JavaScript, pagination, or missing links.
On WooCommerce
- Install Yoast SEO or Rank Math (free): both auto-generate an XML sitemap at yourstore.com/sitemap_index.xml that includes WooCommerce Products and Product Categories as separate sitemap files.
- Yoast: Settings › Yoast SEO › General › Features — ensure XML Sitemaps is toggled ON. Then go to Yoast SEO › Search Appearance › Content Types and confirm Products is set to 'Show in search results'.
- Rank Math: Rank Math › Sitemap Settings — enable the Sitemap module, then check the Products and Product Categories boxes under 'Include in Sitemap'.
- WordPress Admin › Appearance › Menus: verify every Product Category is reachable from at least one navigation menu or homepage link.
- Check for accidental noindex: in Yoast, open any product edit screen and look for the Yoast meta box at the bottom. Confirm 'Allow search engines to show this Post in search results' is set to Yes.
- Submit yourstore.com/sitemap_index.xml in Google Search Console › Indexing › Sitemaps.
<?php
/**
* Ensure WooCommerce product and product category pages are indexable.
* Add to child theme's functions.php.
*/
add_action( 'wp_head', 'ensure_products_indexable', 1 );
function ensure_products_indexable() {
if ( is_product() || is_product_category() || is_shop() ) {
// Remove any noindex added by other plugins/code
remove_action( 'wp_head', 'noindex', 1 );
}
}Add to your child theme's functions.php to force product and product category pages to be indexable (safety net; Yoast/Rank Math handles this in their UI but this confirms it at the WordPress level).
Cwv clsModerate effort
Eliminate unexpected layout shifts by reserving space for every image, video, embed, and dynamically injected element so nothing jumps when the page loads.
On WooCommerce
- Images: Install the 'Specify Image Dimensions' plugin (free, WordPress.org) to automatically add missing width/height attributes to all images output by WordPress and WooCommerce. Alternatively, use the 'Perfmatters' plugin → Image tab → 'Add Missing Image Dimensions'.
- Theme: In your child theme's functions.php, ensure add_theme_support('html5', ['img']) is present so WordPress outputs modern image markup. For block themes, check theme.json for any layout that may cause reflow.
- Fonts: In Appearance → Customize → Additional CSS (or your child theme's style.css), add font-display: swap (with override descriptors if needed) to all @font-face rules. If using Google Fonts via a plugin, switch to 'Local Google Fonts' plugin which gives you control over font-display.
- Cookie / GDPR banners: In your cookie consent plugin settings (e.g. Complianz, CookieYes), set the banner display style to 'bottom bar' or 'modal overlay' rather than a top push-down banner.
- Caching / optimisation plugin: In WP Rocket, LiteSpeed Cache, or W3 Total Cache → Media settings, enable 'Add missing image dimensions' and 'Lazy load images'. These tools also defer non-critical JS that may inject shifting content.
- Verify: Run PageSpeed Insights or use the Chrome Web Vitals extension on your site; target CLS < 0.1.
<?php
/**
* Automatically add width and height attributes to images
* that are missing them, to prevent Cumulative Layout Shift.
* Add to: wp-content/themes/your-child-theme/functions.php
*/
add_filter( 'the_content', 'mytheme_add_image_dimensions', 99 );
add_filter( 'post_thumbnail_html', 'mytheme_add_image_dimensions', 99 );
function mytheme_add_image_dimensions( $html ) {
// Match img tags that are missing width or height attributes
$pattern = '/<img(?![^>]*(?:width|height))[^>]*>/i';
preg_match_all( $pattern, $html, $matches );
foreach ( $matches[0] as $img_tag ) {
// Extract src
preg_match( '/src=["\']([^"\']+)["\']/i', $img_tag, $src_match );
if ( empty( $src_match[1] ) ) continue;
$src = $src_match[1];
$attach_id = attachment_url_to_postid( $src );
if ( ! $attach_id ) continue;
$meta = wp_get_attachment_metadata( $attach_id );
if ( empty( $meta['width'] ) || empty( $meta['height'] ) ) continue;
$new_tag = str_replace(
'<img ',
'<img width="' . esc_attr( $meta['width'] ) . '" height="' . esc_attr( $meta['height'] ) . '" ',
$img_tag
);
$html = str_replace( $img_tag, $new_tag, $html );
}
return $html;
}Add to your child theme's functions.php to automatically inject missing width/height attributes on all WordPress images, eliminating the most common CLS source.
Cwv inpLarger project
Reduce Interaction to Next Paint (INP) to under 200 ms by breaking up long JavaScript tasks, deferring non-critical third-party scripts, and lightening event handlers so the page feels fast and responsive after every tap or click.
On WooCommerce
- Install a performance plugin such as WP Rocket, Perfmatters, or Asset CleanUp Pro. In its settings, enable 'Delay JavaScript execution' or 'Load JS deferred' for third-party scripts (chat, CRM pixels, review widgets).
- Go to WP Admin → Plugins and deactivate any plugin whose frontend script you don't actively need. Each deactivated plugin typically removes one or more scripts from the page.
- In WP Rocket (or equivalent), use the 'Delay JavaScript' feature to defer specific scripts (e.g., Facebook Pixel, Intercom) until after first user interaction.
- If using a page builder (Elementor, Divi, etc.), go to its performance settings and enable 'Improved Asset Loading' or 'Script Loading Optimizations' — these defer builder-injected JS.
- For custom code, open your child theme's functions.php or a custom plugin and change `wp_enqueue_script` calls to use `'in_footer' => true` and add the `defer` strategy via the `$args` array (WP 6.3+): `wp_enqueue_script('my-script', $src, $deps, $ver, ['strategy' => 'defer'])`.
- Verify with PageSpeed Insights and the Chrome DevTools Performance panel; monitor Google Search Console → Core Web Vitals report.
Cwv lcpModerate effort
Reduce Largest Contentful Paint (LCP) to under 2.5 seconds by serving the hero image in a next-gen format (WebP/AVIF), preloading it, and eliminating render-blocking resources.
On WooCommerce
- IDENTIFY LCP IMAGE: Run PageSpeed Insights on your homepage and key product/category pages. The LCP element is usually the hero image set in your theme's Customizer or a page-builder block.
- CONVERT TO WEBP: Install the free plugin 'WebP Express' or 'Imagify' (freemium). Activate and run a bulk conversion. Imagify: Plugins › Add New › search 'Imagify' › Install & Activate › Settings › choose WebP format › Bulk Optimize.
- PRELOAD: Install 'WP Rocket' (paid) or 'LiteSpeed Cache' (free). In WP Rocket: Settings › WP Rocket › Media tab › enable 'Preload LCP Image'. In LiteSpeed Cache: Settings › LiteSpeed Cache › Page Optimization › enable 'Preload Hero Image'.
- DEFER JS & REMOVE RENDER-BLOCKING: In WP Rocket: Settings › WP Rocket › File Optimization › enable 'Defer JavaScript Execution' and 'Delay JavaScript Execution'. In LiteSpeed Cache: Page Optimization › JS Deferred/Delayed.
- NO LAZY-LOAD ON HERO: In your theme's Customizer or code, ensure the hero/banner img tag has loading='eager' and fetchpriority='high'. Most caching plugins have an option to exclude the LCP image from lazy-loading — use it.
- VERIFY: Re-run PageSpeed Insights; check Google Search Console › Core Web Vitals report.
<!-- WooCommerce / WordPress hero block — paste in Custom HTML block or theme template -->
<img
src="/wp-content/uploads/hero-banner.webp"
srcset="/wp-content/uploads/hero-banner-768.webp 768w,
/wp-content/uploads/hero-banner-1200.webp 1200w,
/wp-content/uploads/hero-banner.webp 1920w"
sizes="100vw"
alt="[Describe your hero image here]"
width="1920"
height="700"
loading="eager"
fetchpriority="high"
>If using a page builder for the hero, switch to the HTML/Custom HTML block and use this markup. Otherwise apply via functions.php (see WordPress snippet above).
Duplicate meta descriptionModerate effort
Write a unique, page-specific meta description for every page on your store so Google can display a compelling, accurate snippet for each one in search results.
On WooCommerce
- Install the Yoast SEO or Rank Math plugin if not already present (Plugins → Add New, search for 'Yoast SEO' or 'Rank Math').
- For a single product: go to Products → All Products, open the product, scroll to the Yoast SEO / Rank Math meta box below the editor, click the 'Edit snippet' / 'Edit' button, and enter a unique meta description.
- For a category page: go to Products → Categories, click 'Edit' on the category, scroll to the Yoast SEO / Rank Math section, and fill in the meta description.
- For bulk updates with Yoast: use the Yoast SEO → Tools → Bulk Editor, which lists all posts/pages with their current descriptions in one editable table.
- For pages/posts: go to Pages or Posts, open each item, and use the same SEO plugin meta box at the bottom of the editor.
- Save changes and submit affected URLs for re-indexing via Google Search Console.
<!-- Rendered by Yoast SEO plugin in <head> -->
<meta name="description" content="Shop our full range of basketball sneakers for men and women. Premium grip, cushioning, and style — free returns on every order.">If using Yoast SEO, this is the HTML it renders in <head>. You enter the text in the Yoast SEO meta box on each product/page edit screen — no manual code needed.
Duplicate titleModerate effort
Give every page a unique, descriptive title tag that accurately reflects that page's specific content.
On WooCommerce
- Install the Yoast SEO or Rank Math plugin if not already active.
- For individual products: Products → All Products → Edit a product → scroll to the 'Yoast SEO' or 'Rank Math SEO' meta box → edit the 'SEO title' field.
- For template-level control (all products, all categories): Yoast SEO → Search Appearance → Content Types (or Taxonomies) → set the Title template using variables like %%title%% – %%sitename%% to auto-generate unique titles from the product name.
- Use Yoast's 'SEO → Tools → Bulk editor' to review and update titles for many posts/products at once without opening each individually.
add_filter( 'pre_get_document_title', 'mytheme_unique_title', 10 );
function mytheme_unique_title( $title ) {
if ( is_singular( 'product' ) ) {
$product = wc_get_product( get_the_ID() );
if ( $product ) {
return get_the_title() . ' | ' . get_bloginfo( 'name' );
}
}
return $title;
}In your child theme's functions.php — only needed if you are NOT using Yoast/Rank Math. Yoast/Rank Math handle this automatically via their title templates.
Faceted url indexableModerate effort
Point the rel=canonical on every faceted/filter URL to its clean parent category URL (or add noindex), so Google treats filtered pages as a single authoritative page instead of thousands of duplicates.
On WooCommerce
- WooCommerce shop/category pages use query strings like ?filter_color=navy or ?orderby=price for filtering (via the WooCommerce Layered Nav widget or filter plugins).
- Install and activate the Yoast SEO or Rank Math plugin if not already present — both handle canonical logic for WooCommerce.
- In Yoast SEO: go to Yoast SEO → Search Appearance → Taxonomies, and ensure product category pages have a canonical set. Then go to Yoast SEO → Advanced (or use their WooCommerce SEO add-on) and look for settings to canonicalize filtered URLs to the base shop/category URL.
- In Rank Math: go to Rank Math → Titles & Meta → WooCommerce → scroll to 'Noindex Empty Category Pages' and review canonical options for filtered pages.
- For fine-grained control, add a code snippet to your child theme's functions.php or a custom plugin: hook into 'wpseo_canonical' (Yoast) or 'rank_math/frontend/canonical' (Rank Math) filters to strip query parameters and return the clean term/shop URL whenever filter params are present in the URL.
- Alternatively, for URL-based filters (e.g., /shop/category/color/navy/), configure Yoast or Rank Math to noindex those pages: in Yoast go to Search Appearance → Taxonomies → set the filter taxonomy to 'noindex'.
- Verify with a filtered URL in Google Search Console's URL Inspection tool.
<?php
/**
* Force canonical URL on WooCommerce filtered/faceted pages to the clean term URL.
* Hooks into Yoast SEO's wpseo_canonical filter.
*/
add_filter( 'wpseo_canonical', 'fix_woo_facet_canonical' );
function fix_woo_facet_canonical( $canonical ) {
if ( ! is_product_category() && ! is_shop() ) {
return $canonical;
}
// Strip known WooCommerce Layered Nav / filter query params
$filter_params = [ 'filter_color', 'filter_size', 'filter_cat', 'orderby', 'min_price', 'max_price', 'rating_filter' ];
$parsed = parse_url( $canonical );
if ( isset( $parsed['query'] ) ) {
parse_str( $parsed['query'], $query_args );
foreach ( $filter_params as $param ) {
unset( $query_args[ $param ] );
}
$clean_query = http_build_query( $query_args );
$canonical = $parsed['scheme'] . '://' . $parsed['host'] . $parsed['path'];
if ( ! empty( $clean_query ) ) {
$canonical .= '?' . $clean_query;
}
}
return $canonical;
}Add to your child theme's functions.php. Hooks into Yoast SEO's canonical filter to strip WooCommerce filter query params from category page canonicals.
Images missing altModerate effort
Add descriptive alt text to every product and content image so search engines and screen readers can understand what the image shows.
On WooCommerce
- Product images: In WordPress Admin → Products → [select product] → click the main product image in the 'Product image' box → the Media Library opens → click the image → fill in the 'Alt Text' field in the right sidebar → click 'Update'.
- Product gallery images: Same product edit screen → scroll to 'Product gallery' → click each thumbnail → add Alt Text in the Media Library panel → Update.
- Media Library bulk edit: Admin → Media → Library → click any image → fill the Alt Text field. For bulk work, install the free 'Bulk Auto Image Alt Text' or 'SEO Friendly Images' plugin.
- Theme/page builder images: In Elementor, Gutenberg, or Divi, click the image block → look for the 'Alt Text' or 'Alternative Text' field in the block/widget settings panel → fill in and Save/Update.
<?php
/**
* Auto-populate empty WooCommerce product image alt text
* with the product title as a fallback.
* Add to child-theme functions.php
*/
add_filter( 'woocommerce_product_get_image', 'prefix_fallback_product_image_alt', 10, 5 );
function prefix_fallback_product_image_alt( $image, $product, $size, $attr, $placeholder ) {
// Only act when no alt was explicitly passed
if ( empty( $attr['alt'] ) ) {
$attachment_id = $product->get_image_id();
$saved_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
if ( empty( $saved_alt ) ) {
// Update the attachment meta so it persists
update_post_meta( $attachment_id, '_wp_attachment_image_alt', $product->get_name() );
}
}
return $image;
}In a child theme's functions.php, use this filter to auto-populate empty alt text with the product title as a fallback (does not overwrite existing alt text).
Images missing alt bulkModerate effort
Add descriptive alt text to every product and content image at the template level so search engines and screen readers can understand all images on the page.
On WooCommerce
- Navigate to Appearance → Theme Editor (or edit files via FTP/SSH). Locate `woocommerce/single-product/product-image.php` inside your child theme's WooCommerce overrides folder.
- If the override does not exist, copy it from `wp-content/plugins/woocommerce/templates/single-product/product-image.php` into `wp-content/themes/YOUR-CHILD-THEME/woocommerce/single-product/product-image.php`.
- Ensure `the_post_thumbnail` and `woocommerce_single_product_image_thumbnail_html` calls pass the alt text via the `$attr` array: `array('alt' => get_the_title())`.
- For existing product images, go to Media Library, click each image → add alt text in the 'Alternative Text' field. For bulk editing, use a plugin such as 'SEO Bulk Image Alt Text' or 'Auto Image Alt Text' to generate alt text from product titles automatically.
- For the shop/category grid, override `woocommerce/loop/loop-start.php` or check `content-product.php` to ensure `woocommerce_template_loop_product_thumbnail()` includes alt attributes.
- Save and visit a product page; use browser DevTools to verify alt attributes on all images.
<?php
/**
* Single Product Image — child theme override
* Path: wp-content/themes/YOUR-CHILD-THEME/woocommerce/single-product/product-image.php
*/
defined( 'ABSPATH' ) || exit;
global $product;
$post_thumbnail_id = $product->get_image_id();
$alt_text = get_post_meta( $post_thumbnail_id, '_wp_attachment_image_alt', true );
if ( empty( $alt_text ) ) {
$alt_text = get_the_title(); // fallback to product title
}
echo wp_get_attachment_image(
$post_thumbnail_id,
'woocommerce_single',
false,
array(
'alt' => esc_attr( $alt_text ),
'class' => 'wp-post-image',
)
);Place in your child theme at woocommerce/single-product/product-image.php. Ensures alt text falls back to product title.
Low performance scoreLarger project
Improve your Lighthouse/PageSpeed performance score by reducing page weight, eliminating render-blocking resources, optimising images, and addressing Core Web Vitals so Google ranks your store higher in search results.
On WooCommerce
- Install a caching plugin: WordPress Admin → Plugins → Add New → install WP Rocket (paid, best all-in-one) or W3 Total Cache (free). Enable page caching, GZIP, and browser caching.
- Install an image optimisation plugin: Plugins → Add New → Imagify or ShortPixel → run bulk optimisation on your Media Library; enable WebP conversion and lazy loading.
- Add a CDN: WP Rocket integrates with Cloudflare and BunnyCDN out of the box; alternatively configure Cloudflare free tier via your hosting DNS to proxy all traffic.
- Minify & defer JS/CSS: in WP Rocket → File Optimisation → enable Minify JS, Combine JS, Defer JavaScript execution, Minify CSS, Combine CSS, and Load CSS asynchronously.
- Reduce plugin bloat: deactivate and delete any plugin that is not in active use — each adds HTTP requests and JS. Plugins → Installed Plugins → bulk deactivate unused ones.
- Switch to a lightweight theme: Themes → Add New → install Kadence, Astra, or GeneratePress — all score well on Core Web Vitals with WooCommerce.
- Verify via PageSpeed Insights after enabling each feature, then check WordPress Admin → WooCommerce → (Google integration) or a Search Console plugin for field data CWV.
<?php
/**
* Add explicit width/height and lazy loading to WooCommerce product images
* to prevent Cumulative Layout Shift (CLS) and improve LCP.
* Add to: wp-content/themes/your-child-theme/functions.php
*/
add_filter( 'woocommerce_product_get_image', 'perf_add_image_attrs', 10, 5 );
function perf_add_image_attrs( $image, $product, $size, $attr, $placeholder ) {
// Ensure lazy loading is set on non-featured images
if ( strpos( $image, 'loading=' ) === false ) {
$image = str_replace( '<img ', '<img loading="lazy" decoding="async" ', $image );
}
return $image;
}
/**
* Preload the hero/featured image on single product pages for faster LCP.
*/
add_action( 'wp_head', 'perf_preload_product_hero_image' );
function perf_preload_product_hero_image() {
if ( ! is_product() ) return;
global $product;
if ( ! $product ) return;
$image_id = $product->get_image_id();
if ( ! $image_id ) return;
$image_url = wp_get_attachment_image_url( $image_id, 'woocommerce_single' );
if ( $image_url ) {
echo '<link rel="preload" as="image" href="' . esc_url( $image_url ) . '" fetchpriority="high">' . "\n";
}
}Add to your child theme's functions.php to force explicit image dimensions and lazy loading on WooCommerce product images.
Meta description too longQuick win
Shorten your meta description to 150–160 characters so Google displays it in full instead of cutting it off with "…" in search results.
On WooCommerce
- Install and activate the Yoast SEO (or Rank Math) plugin if not already installed.
- For a product page: go to Products → All Products → click the product → scroll to the Yoast SEO / Rank Math meta box below the editor.
- Click the 'Edit snippet' button, then edit the 'Meta description' field. The plugin shows a colour-coded character/pixel-width bar — keep it in the green zone.
- For other page types (pages, categories): go to Pages or Products → Categories, open the item, and use the same SEO meta box.
- Click Update / Save.
add_filter( 'wpseo_metadesc', function( $desc ) {
if ( mb_strlen( $desc ) > 155 ) {
$desc = mb_substr( $desc, 0, 152 ) . '...';
}
return $desc;
} );If you are building a custom theme without Yoast/Rank Math, add this to your theme's functions.php. With Yoast or Rank Math installed, edit descriptions in the plugin UI instead.
Meta description too shortModerate effort
Expand your meta description to 120–160 characters so Google displays a compelling, complete snippet in search results that drives more clicks to your store.
On WooCommerce
- Install the free Yoast SEO or Rank Math plugin if not already active (Plugins → Add New → search for plugin → Install & Activate).
- For a product: Admin → Products → select the product → scroll to the Yoast SEO / Rank Math meta box below the editor → click the 'Edit snippet' button → type your new description in the 'Meta description' field → Update the product.
- For a category: Admin → Products → Categories → select a category → scroll to the Yoast SEO / Rank Math section at the bottom of the edit screen → update the meta description → Save.
- For the homepage: go to Yoast SEO → Search Appearance → Content Types (or Rank Math → Titles & Meta) to set a default, or edit the homepage post/page directly.
<meta name="description" content="Shop the official Nike national team jerseys — advanced Dri-FIT heat tech meets bold heritage design. Free shipping on orders over $75. Explore the full collection now.">Place this inside the <head> element of your HTML page. Replace the content value with your 120–160 character description.
Missing canonicalModerate effort
Add a self-referencing canonical tag to every page so Google knows which URL is the definitive version of that content.
On WooCommerce
- Install the free Yoast SEO plugin (or Rank Math SEO) from Plugins → Add New in your WordPress Admin — both output canonical tags automatically for all page types.
- Once Yoast SEO is active, go to Yoast SEO → Settings → Advanced and confirm 'Canonical URL' output is enabled (it is on by default).
- For individual pages where you need to override the canonical, open the product/post editor, scroll to the Yoast SEO meta box at the bottom, click 'Advanced', and enter the preferred URL in the 'Canonical URL' field.
- If you prefer Rank Math: go to Rank Math → Titles & Meta → and verify canonical tags are enabled; per-page overrides are in the Rank Math panel inside each post/product editor under 'Advanced'.
- Save and verify by viewing page source and searching for 'canonical'.
<?php
add_action( 'wp_head', 'my_store_canonical_tag', 1 );
function my_store_canonical_tag() {
if ( is_singular() || is_front_page() || is_post_type_archive() || is_tax() || is_category() || is_tag() ) {
$canonical = get_permalink();
if ( $canonical ) {
echo '<link rel="canonical" href="' . esc_url( $canonical ) . '" />' . PHP_EOL;
}
}
}
?>Add to your child theme's functions.php only if NOT using Yoast SEO or Rank Math (those plugins handle this automatically). This outputs a self-referencing canonical on every front-end page.
Missing h1Quick win
Add a single, descriptive H1 tag to every page that currently lacks one, clearly stating the page's main topic.
On WooCommerce
- For product pages: WordPress Admin → Products → open the product → the 'Product name' field at the top is output as the H1 by most WooCommerce themes (via the woocommerce_template_single_title hook). Ensure the product name is filled in.
- For shop/category pages: WordPress Admin → Products → Categories → edit the category → the 'Name' field becomes the H1.
- For standard pages/posts: WordPress Admin → Pages (or Posts) → open the page → in the Gutenberg editor, the first block should be a 'Heading' block set to H1, OR the page title field at the very top (above the editor) renders as H1.
- If the theme does not output an H1, open your child theme's single-product.php or archive-product.php and add: <h1 class="product_title"><?php the_title(); ?></h1>.
- Use the free 'Rank Math' or 'Yoast SEO' plugin — both include on-page analysis that flags missing H1s per page/product.
- Verify with View Page Source and search for '<h1>'.
<?php
/**
* Single Product title — child theme override
* Place at: yourchildtheme/woocommerce/single-product/title.php
*/
defined( 'ABSPATH' ) || exit;
?>
<h1 class="product_title entry-title"><?php the_title(); ?></h1>In your child theme's woocommerce/single-product/title.php override, output the product name as H1.
Missing meta descriptionModerate effort
Write and add a unique meta description (120–160 characters) to every page that is missing one, so Google can display an accurate, compelling snippet in search results.
On WooCommerce
- Install the free 'Yoast SEO' plugin (or 'Rank Math SEO') from Plugins → Add New if not already installed.
- For a product: Admin → Products → click the product → scroll to the 'Yoast SEO' (or 'Rank Math') meta box below the editor → click the 'Search Appearance' tab → type your description in the 'Meta description' field; the snippet preview updates in real time.
- For a category page (Shop, category archive): Admin → Products → Categories → click the category → fill in the 'Yoast SEO' meta description field at the bottom.
- For the homepage (if set as a static page): Admin → Pages → click the homepage → same Yoast/Rank Math meta box.
- For site-wide fallback templates (product type, category archives): Yoast SEO → Search Appearance → Content Types → set a template using variables like %%sitename%% and %%excerpt%% as a safety net for pages with no manual description.
<meta name="description" content="Handmade leather wallets crafted in full-grain cowhide. Free UK shipping. Personalise with initials. Shop slim bifold, trifold & card holders now.">Yoast SEO renders this automatically in your <head>. This is the raw output you will see in page source after Yoast is configured.
Missing og descriptionModerate effort
Add an og:description meta tag to every page so that social media platforms display a compelling, accurate preview when your pages are shared.
On WooCommerce
- Install the free Yoast SEO plugin (Plugins → Add New → search 'Yoast SEO') or RankMath SEO if not already installed.
- For Yoast: go to each product's edit screen, scroll to the Yoast SEO meta box, click the 'Social' tab (Facebook icon), and enter your og:description in the 'Facebook description' field. For a site-wide template, go to Yoast SEO → Search Appearance → Content Types → Products and set a dynamic template using variables like %%excerpt%%.
- For RankMath: edit a product, find the RankMath panel → Edit Snippet → Social tab, and fill in the description. Set bulk defaults under RankMath → Titles & Meta → Products.
- Without a plugin, open your child theme's header.php (Appearance → Theme File Editor) and add the tag manually inside <head>, using <?php echo esc_attr(get_the_excerpt()); ?> as the content source.
<?php
/**
* Add Open Graph description tag to <head>.
* Place in child theme's functions.php.
*/
add_action( 'wp_head', 'custom_og_description', 5 );
function custom_og_description() {
if ( is_singular() ) {
$description = '';
if ( has_excerpt() ) {
$description = get_the_excerpt();
} else {
$description = wp_trim_words( get_the_content(), 25, '' );
}
$description = esc_attr( wp_strip_all_tags( $description ) );
} elseif ( is_shop() || is_product_category() ) {
$description = esc_attr( woocommerce_get_loop_display_mode() );
$description = esc_attr( get_bloginfo( 'description' ) );
} else {
$description = esc_attr( get_bloginfo( 'description' ) );
}
if ( ! empty( $description ) ) {
echo '<meta property="og:description" content="' . $description . '" />' . "\n";
}
}Add to your child theme's functions.php only if you are NOT using Yoast SEO or RankMath (those plugins handle this automatically).
Missing og imageModerate effort
Add an og:image meta tag to every page so that social media platforms and messaging apps display a rich, branded image preview whenever someone shares your store's link.
On WooCommerce
- Install the free 'Yoast SEO' plugin (Plugins → Add New → search 'Yoast SEO' → Install → Activate) — it is the most reliable way to manage og:image across all WooCommerce page types.
- Go to Yoast SEO → Social → Facebook tab → enable 'Add Open Graph meta data' → save.
- Upload a site-wide default og:image in Yoast SEO → Social → Facebook → Default image for sharing.
- For individual product pages, open the product in wp-admin → Products → edit a product → scroll to the 'Yoast SEO' meta box → click the 'Social' tab → upload a Facebook image specific to that product.
- For category (taxonomy) pages, go to Products → Categories → edit a category → scroll to the Yoast SEO block → set a social image there.
- Validate using Facebook's Sharing Debugger after saving.
Missing og titleQuick win
Add an og:title meta tag to every page so your store's title appears correctly when shared on social media platforms like Facebook, LinkedIn, and Pinterest.
On WooCommerce
- The most reliable method is to use an SEO plugin. Install and activate either Yoast SEO or Rank Math (free versions cover this fully) from Plugins → Add New.
- With Yoast SEO active: go to any product, page, or post editor → scroll to the 'Yoast SEO' meta box below the editor → click the 'Social' tab → enter your desired og:title in the 'Facebook title' field, or leave it blank to have Yoast inherit the SEO title automatically.
- With Rank Math active: in the post/product editor, open the Rank Math panel → go to 'Social' → fill in the 'Title' field for Facebook/OpenGraph.
- For sitewide defaults with Yoast: go to SEO → Social → Facebook and ensure 'Add Open Graph meta data' is enabled.
- If you prefer to code it manually, edit your child theme's header.php and add the og:title meta tag inside the <head> section using PHP to echo the_title().
- Verify using Facebook Sharing Debugger after saving.
<?php
// Add og:title to <head> — only use if no SEO plugin is active
add_action( 'wp_head', 'my_store_og_tags', 5 );
function my_store_og_tags() {
if ( is_singular() ) {
$og_title = get_the_title();
} elseif ( is_shop() ) {
$og_title = get_option( 'blogname' ) . ' | Shop';
} elseif ( is_product_category() ) {
$og_title = single_cat_title( '', false );
} else {
$og_title = get_option( 'blogname' );
}
echo '<meta property="og:title" content="' . esc_attr( $og_title ) . '" />' . "\n";
}Add to your child theme's functions.php ONLY if you are not using Yoast SEO or Rank Math (those plugins handle this automatically and should not be doubled up).
Missing titleQuick win
Add a unique, descriptive title tag (30–60 characters) to every page that is missing one.
On WooCommerce
- Install and activate the free Yoast SEO or Rank Math plugin if not already present — these are the standard tools for managing title tags in WordPress/WooCommerce.
- For a product: go to Products → All Products → select the product → scroll to the Yoast SEO (or Rank Math) meta box below the editor → click the 'SEO title' field and type your title.
- For a shop/category page: go to Products → Categories → select the category → scroll to the Yoast/Rank Math section at the bottom of the edit screen → fill in the SEO title.
- For the homepage or a static page: go to Pages → All Pages → select the page → use the Yoast/Rank Math meta box.
- For global title templates (e.g. auto-format for all products): go to Yoast SEO → Settings → Content types → Products, or Rank Math → Titles & Meta → Products, and set a template like '%title% – %sitename%'.
- Click Update/Publish and verify in the page source that <title> contains your intended text.
<?php
/**
* Custom title tag for WooCommerce pages (use only without an SEO plugin).
* Add to: wp-content/themes/your-child-theme/functions.php
*/
add_filter( 'pre_get_document_title', 'custom_wc_page_title' );
function custom_wc_page_title( $title ) {
if ( is_product() ) {
global $post;
$title = get_the_title( $post->ID ) . ' | ' . get_bloginfo( 'name' );
} elseif ( is_product_category() ) {
$term = get_queried_object();
$title = $term->name . ' | ' . get_bloginfo( 'name' );
} elseif ( is_shop() ) {
$title = 'Shop | ' . get_bloginfo( 'name' );
}
return $title;
}
?>Add this filter to your child theme's functions.php ONLY if you are not using Yoast SEO or Rank Math. If you use those plugins, set titles in the plugin UI instead.
Multiple h1Moderate effort
Remove all but one H1 tag from each page so every page has exactly one clear, descriptive primary heading.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SSH).
- Open the relevant template: single-product.php for product pages, archive.php or taxonomy-product_cat.php for category pages, front-page.php or page.php for other pages.
- Search for '<h1' — WooCommerce's default woocommerce_template_single_title hook outputs the product name as an H1; ensure no other element in the same template also uses H1.
- If a page builder (Elementor, Divi, Gutenberg) is used, open the page in the builder and change any Heading blocks set to H1 (other than the main title) to H2 or lower.
- For theme-generated product cards on shop/category pages, check loop/content-product.php — product titles in loops should be H2 or H3, not H1.
- Install a free plugin like Detailed SEO Plugin or use browser DevTools to spot-check the rendered page for stray H1 tags.
<?php
/**
* Child theme override: woocommerce/single-product/title.php
* WooCommerce default already uses <h1> for the product name.
* This file is shown for illustration — the KEY fix is ensuring
* no other template part on this page also uses <h1>.
*/
?>
<h1 class="product_title entry-title"><?php the_title(); ?></h1>
<?php
/**
* In woocommerce/content-product.php (the shop/category grid card),
* the product title must be h2 (WooCommerce default), NOT h1.
* Verify your theme has not overridden this to h1:
*/
?>
<!-- Correct in product cards: -->
<h2 class="woocommerce-loop-product__title"><?php the_title(); ?></h2>Place in your child theme's single-product.php or use a WooCommerce template override at woocommerce/single-product/title.php — WooCommerce outputs the product title as H1 by default via woocommerce_template_single_title; ensure nothing else on the page also uses H1.
Noindex detectedQuick win
Remove or replace the noindex directive on any page you want to appear in Google search results.
On WooCommerce
- WordPress global toggle: Go to WordPress Admin → Settings → Reading. Make sure 'Discourage search engines from indexing this site' is UNCHECKED. This is the single most common cause of a site-wide noindex.
- Yoast SEO (most common plugin): Go to Yoast SEO → Settings → Content types. For each post type (Products, Posts, Pages) confirm 'Show in search results' is set to 'Yes'. Then check the individual page/product in the editor — in the Yoast sidebar find 'Advanced' and confirm 'Allow search engines to show this post in search results?' is set to 'Yes, show in search results'.
- Rank Math: Go to Rank Math → Titles & Meta → choose the post type → confirm 'Robots Meta' does NOT include noindex. Also check the per-post Rank Math panel under 'Advanced' in the editor.
- Theme/code check: In your child theme's header.php or functions.php search for 'noindex' and remove any hardcoded meta tags.
- After fixing, use the Yoast / Rank Math sitemap to request re-crawling, and confirm in Google Search Console.
<meta name="robots" content="index,follow">Place inside <head>. This is the CORRECT tag that allows indexing and link-following.
Non self canonicalQuick win
Audit every page whose canonical tag points to a different URL, then either correct it to self-reference or confirm the cross-reference is intentional and correct.
On WooCommerce
- WooCommerce relies on WordPress's SEO plugin for canonical tags — the most common culprits are Yoast SEO, Rank Math, or All in One SEO.
- For Yoast SEO: navigate to the affected page/product in wp-admin, scroll to the Yoast SEO meta box, click the 'Advanced' tab (gear icon), and check the 'Canonical URL' field — clear it if it contains a wrong URL so Yoast defaults to the page's own URL.
- For Rank Math: in the post/product editor, open the Rank Math sidebar, go to the 'Advanced' tab, and clear any value in the 'Canonical URL' field.
- For All in One SEO: in the post editor, open the AIOSEO meta box, go to 'Advanced', and clear the 'Canonical URL' field.
- If no plugin override exists, check your theme's header.php or a custom plugin for a hardcoded <link rel="canonical"> tag and remove or correct it.
- After saving, view-source the page on the frontend and confirm the canonical href matches the page's own URL.
<link rel="canonical" href="https://www.example.com/your-page-url/">Place this tag inside the <head> element of every page. The href must exactly match the page's own preferred URL.
Og url mismatchModerate effort
Set a unique, page-specific og:url meta tag on every page that exactly matches that page's canonical URL.
On WooCommerce
- Install or open the Yoast SEO or Rank Math plugin (both handle og:url automatically on a per-page basis — this is the recommended fix path for WooCommerce).
- In Yoast SEO: go to WordPress Admin → SEO → Social → Facebook tab and ensure 'Add Open Graph meta data' is enabled. Yoast will then output the correct og:url automatically on every page.
- In Rank Math: go to WordPress Admin → Rank Math → General Settings → Social Meta and ensure 'Open Graph Meta' is ON.
- If you are NOT using an SEO plugin and og:url is hardcoded in your theme's header.php or functions.php, locate the echo or printf statement outputting og:url and replace the static value with the dynamic <?php echo esc_url(get_permalink()); ?> (see code snippets).
- After saving, use 'View Source' on a product, a category, and a post to confirm each shows its own URL.
- Clear any caching plugin (WP Super Cache, W3 Total Cache, WP Rocket) after making changes.
<?php
// Add this to functions.php in your child theme.
// Only needed if no SEO plugin is managing og:url.
add_action( 'wp_head', 'custom_og_url_tag', 5 );
function custom_og_url_tag() {
if ( is_singular() ) {
$og_url = get_permalink();
} elseif ( is_home() || is_front_page() ) {
$og_url = home_url( '/' );
} elseif ( is_tax() || is_category() || is_tag() ) {
$og_url = get_term_link( get_queried_object() );
} else {
$og_url = home_url( add_query_arg( null, null ) );
}
echo '<meta property="og:url" content="' . esc_url( $og_url ) . '" />' . "\n";
}Add to your child theme's header.php inside <head>, or to functions.php via wp_head hook. Only use this if you are NOT using Yoast SEO or Rank Math (those plugins handle it automatically).
Page not foundModerate effort
Fix broken internal links pointing to pages that return a 404 error by either removing those links or setting up a 301 redirect from the missing URL to the correct replacement page.
On WooCommerce
- Install the free 'Redirection' plugin (by John Godley) from WordPress.org if not already active: Plugins → Add New → search 'Redirection' → Install & Activate.
- Go to Tools → Redirection → Add New. Enter the old broken URL in 'Source URL' and the correct destination in 'Target URL'. Make sure the redirect type is '301 - Moved Permanently'. Click 'Add Redirect'.
- To fix the internal links in page/post content, go to Pages or Posts, open the editor for each affected page, and update or remove the broken link in the block editor or Classic Editor.
- For broken links in menus, go to Appearance → Menus, find and update the affected menu item URL, and save.
- For broken links in widgets or theme files, edit via Appearance → Widgets or directly in the theme PHP/template files (requires FTP or file manager access).
- Verify with the Redirection plugin's log (Tools → Redirection → Log) — it will show the 301 being served successfully.
<?php
// Add to child theme's functions.php
// Redirect a single broken URL with a 301
function mytheme_301_redirect_broken_url() {
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
$request = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
if ( $request === '/old-products/discontinued-item' ) {
wp_redirect( '/products/replacement-item', 301 );
exit;
}
}
}
add_action( 'template_redirect', 'mytheme_301_redirect_broken_url' );Alternatively, add a programmatic 301 redirect to your child theme's functions.php — but the Redirection plugin (Tools → Redirection) is the recommended no-code approach.
Page speed warningModerate effort
Reduce your page load time to under 1.5 seconds by optimizing images, eliminating render-blocking resources, and enabling caching and compression.
On WooCommerce
- Caching plugin: In your WordPress admin go to Plugins > Add New and install WP Rocket (paid) or W3 Total Cache (free). Enable page caching, browser caching, Gzip compression, and CSS/JS minification from the plugin's settings dashboard.
- Image optimization: Install the Imagify or ShortPixel plugin (Plugins > Add New) to bulk-convert all existing images to WebP and automatically optimize future uploads; enable lazy loading for below-the-fold images.
- CDN: In your caching plugin's settings (or via Cloudflare — add your site at cloudflare.com for free), enable a CDN so static assets (images, CSS, JS) are served from edge servers closer to each visitor.
- Hosting: If you are on shared hosting, consider upgrading to a managed WordPress host (e.g. Kinsta, WP Engine, Cloudways) — server response time (TTFB) is the single biggest WooCommerce bottleneck and is fixed at the hosting layer.
- Plugins: Go to Plugins > Installed Plugins and deactivate then delete any plugin you do not use. Each active plugin may load JS/CSS on every page.
- Database: Install WP-Optimize (Plugins > Add New) and run the database cleanup to remove expired transients, post revisions, and spam comments, which speeds up database queries on large product catalogues.
Redirect chainModerate effort
Update every internal link that passes through a redirect chain to point directly at the final destination URL, eliminating all intermediate hops.
On WooCommerce
- Install and run Screaming Frog or Sitebulb against your WordPress site; export the 'Redirect Chains' report to identify every internal link that goes through multiple hops.
- For links inside posts, pages, or product descriptions: go to wp-admin → Posts / Pages / Products, open each affected entry in the Block Editor or Classic Editor, locate the linked text, click the link, and replace the href with the final 200-OK URL.
- For navigation menu links: go to Appearance → Menus, expand the menu item whose URL is chained, replace the URL field with the final destination, and save the menu.
- For links inside theme template files (header.php, footer.php, functions.php, or page templates): use Appearance → Theme File Editor (or edit via FTP/SFTP) to find and update the hard-coded href values.
- Review any redirect plugin (Redirection, Safe Redirect Manager, Yoast SEO redirects) to ensure no redirect entry points to another redirect; update the target to the true final URL.
- Flush any caching plugin (WP Super Cache, W3 Total Cache, WP Rocket) after saving changes, then re-crawl to verify.
<?php
// BEFORE: URL passes through a redirect chain
?>
<a href="<?php echo esc_url( 'https://example.com/old-shop' ); ?>">Shop</a>
<?php
// AFTER: URL points directly to the final destination
?>
<a href="<?php echo esc_url( 'https://example.com/shop' ); ?>">Shop</a>If the link is hard-coded in a WordPress theme template file (e.g. header.php or footer.php in your child theme), update the href directly.
Search results indexableModerate effort
Add a noindex robots meta tag to all internal search result pages and block the search URL path in robots.txt to prevent Google from crawling and indexing thin, duplicate search results.
On WooCommerce
- Install the free 'Yoast SEO' or 'Rank Math SEO' plugin if not already installed (Plugins → Add New).
- In Yoast SEO: go to SEO → Search Appearance → Archives tab → find 'Search Pages' and toggle 'Show search pages in search results for search engines?' to OFF. Yoast will automatically inject the noindex tag on all /?s= pages.
- In Rank Math: go to Rank Math → Titles & Meta → Local SEO / Search → set 'Robots Meta' to 'noindex'.
- For robots.txt: go to SEO → Tools → File Editor (Yoast) or Rank Math → General Settings → Edit robots.txt, and add: Disallow: /?s=
- Alternatively, if you prefer manual control, edit your active child theme's header.php and add: <?php if ( is_search() ) { echo '<meta name="robots" content="noindex,follow">'; } ?> inside the <head> tag.
- Verify with Google Search Console URL Inspection on a sample /?s= URL.
<?php
// Add noindex to WordPress search result pages
add_action( 'wp_head', function() {
if ( is_search() ) {
echo '<meta name="robots" content="noindex,follow">' . "\n";
}
}, 1 );Add this to your child theme's functions.php OR directly into header.php inside the <head> tag.
Seo category description missingModerate effort
Write a 100–200 word description for every category page explaining what products it contains and who they are for, so Google can rank it for category-level keywords.
On WooCommerce
- In your WordPress admin, go to Products → Categories.
- Click Edit next to the category you want to update.
- In the 'Description' field (below the Slug field), type or paste your 100–200 word category description. This field supports basic HTML — you can add <strong> tags or <ul> lists.
- If you want a full visual editor here, install the 'WooCommerce Category Description WYSIWYG' plugin (free on WordPress.org) or use a page builder plugin that hooks into category descriptions.
- Click Update to save.
- WooCommerce passes this description to your theme via the woocommerce_archive_description hook. Most WooCommerce-compatible themes display it automatically above the product grid on the /product-category/[slug]/ page.
- If it doesn't appear, check your theme's archive template (woocommerce/archive-product.php) and confirm the hook is called, or use the Yoast SEO or Rank Math plugin to add supplemental category content fields.
<?php
/**
* Display WooCommerce category description above product loop.
* Add to: wp-content/themes/your-child-theme/functions.php
*/
add_action( 'woocommerce_before_shop_loop', function() {
if ( is_product_category() ) {
$term = get_queried_object();
$description = term_description( $term->term_id, 'product_cat' );
if ( $description ) {
echo '<div class="category-description woocommerce-product-details__short-description">';
echo wp_kses_post( $description );
echo '</div>';
}
}
}, 5 );Add to your child theme's functions.php to ensure the category description appears above the product loop on shop archive pages.
Seo category description thinModerate effort
Expand your category page description to at least 100 words of genuinely helpful, keyword-rich content that explains what shoppers will find in that category.
On WooCommerce
- In your WordPress admin, go to Products > Categories.
- Click 'Edit' next to the category you want to update.
- In the 'Description' field (plain text by default), write your 100+ word description. To enable a full visual editor here, install the 'WooCommerce Category Description Editor' plugin or add 'remove_filter("term_description","wpautop");' plus a custom function in your child theme's functions.php.
- If you want a rich-text editor, install the free plugin 'Rich Text Tags Editor for WooCommerce' or 'WP Category Description' — these add a full TinyMCE/Gutenberg editor to the category screen.
- Click 'Update'. By default WooCommerce themes display the description above the product grid via the woocommerce_archive_description hook.
- To move the description below the grid, add a snippet in your child theme's functions.php: remove_action('woocommerce_archive_description', 'woocommerce_taxonomy_archive_description', 10); and re-add it with a higher priority after the loop.
<?php
// Move WooCommerce category description to below the product loop
add_action( 'init', 'mytheme_move_category_description' );
function mytheme_move_category_description() {
remove_action( 'woocommerce_archive_description', 'woocommerce_taxonomy_archive_description', 10 );
add_action( 'woocommerce_after_shop_loop', 'woocommerce_taxonomy_archive_description', 11 );
}
?>Add to your child theme's functions.php to move the category description below the product grid instead of above.
Seo category emptyQuick win
Either populate the empty category with products, or add a noindex tag and remove it from your navigation so Google does not waste crawl budget on a page with no content.
On WooCommerce
- In your WordPress admin, go to Products → Categories.
- Identify the empty category (product count shown in the Count column).
- Option A — Add products: Edit existing products and assign them to this category via Products → All Products → Edit → Product categories.
- Option B — Noindex with Yoast SEO (most common): Go to Products → Categories, hover over the empty category, click 'Edit'. Scroll to the Yoast SEO meta box at the bottom, click the 'Advanced' tab, and set 'Allow search engines to show this category in search results?' to 'No'. Save.
- Option B — Noindex with Rank Math: Same path, use the Rank Math meta box → Advanced → Robots Meta → check 'No Index'.
- Option C — Delete & redirect: Click 'Delete' on the category. Then install the 'Redirection' plugin, go to Tools → Redirection, add a 301 redirect from the old category URL to the parent or nearest relevant category URL.
- Remove the category from any custom navigation menus under Appearance → Menus.
add_action( 'wp_head', 'noindex_empty_woo_categories', 1 );
function noindex_empty_woo_categories() {
if ( is_product_category() ) {
$term = get_queried_object();
if ( $term && isset( $term->count ) && (int) $term->count === 0 ) {
echo '<meta name="robots" content="noindex, follow">' . "\n";
}
}
}Add to your child theme's functions.php. Works without a plugin — outputs noindex on any product category that has zero products.
Seo category sparseModerate effort
Add more products to sparse category pages, or consolidate very thin categories into broader, related ones so every indexed category page delivers genuine value to shoppers and search engines.
On WooCommerce
- Go to Products → Categories in your WordPress admin.
- Click 'Edit' on a sparse category. Add a keyword-rich description in the Description field; this text appears on the category archive page and is crawlable.
- To add products: go to each product's Edit screen, scroll to 'Product Categories' in the right sidebar, and tick the target category.
- To merge categories: reassign all products from the sparse category to the surviving category (bulk-edit: select products → Edit → change Category), then delete the old category and set up a 301 redirect using a plugin like Redirection or Yoast SEO's redirect manager.
- To noindex a sparse category: install Yoast SEO or Rank Math → open the category edit page → find the SEO meta box at the bottom → set 'Allow search engines to show this category in search results' to No (Yoast) or toggle 'No Index' on (Rank Math).
add_action( 'wp_head', 'noindex_sparse_product_categories', 1 );
function noindex_sparse_product_categories() {
if ( is_product_category() ) {
$term = get_queried_object();
$args = array(
'post_type' => 'product',
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => 'product_cat',
'field' => 'term_id',
'terms' => $term->term_id,
),
),
'fields' => 'ids',
);
$products = get_posts( $args );
if ( count( $products ) < 6 ) {
echo '<meta name="robots" content="noindex, follow">' . PHP_EOL;
}
}
}Add to your child theme's functions.php to automatically noindex WooCommerce product categories with fewer than 6 products.
Seo facet url not canonicalizedModerate effort
Add a canonical tag to every filtered, sorted, and paginated collection/category URL pointing back to the clean (unfiltered) category URL, so Google consolidates duplicate pages and focuses crawl budget on your real content.
On WooCommerce
- Install or ensure Yoast SEO, Rank Math, or SEOPress is active — these plugins handle canonical output for WooCommerce pages.
- In Yoast SEO: Go to SEO → Search Appearance → Taxonomies → Product Categories. Ensure canonical tags are enabled (they are by default). Yoast automatically outputs a self-referencing canonical, but for filter URLs you need additional configuration.
- To suppress or canonicalize facet/filter URLs, go to SEO → Tools → Bulk Editor, or use Yoast's 'Advanced' tab on individual pages. For query-string-based filters, configure Yoast to strip known parameters: SEO → Search Appearance → Advanced → 'URL Parameters to ignore'.
- If you use WooCommerce Product Filters (Barn2), FacetWP, or AJAX Filters, check those plugins' SEO/canonical settings — FacetWP has an explicit 'Canonical URL' option under Settings → FacetWP → Settings → SEO.
- For Rank Math: Dashboard → Titles & Meta → Products → ensure 'Canonical URL' is set and use the 'Remove Query Parameters from Canonical' option if available.
- Test with a filtered URL in Google Search Console URL Inspection or by viewing page source and searching for 'canonical'.
<head>
<!-- All other head tags -->
<link rel="canonical" href="https://www.example.com/collections/hoops">
</head>Place this inside the <head> of every filtered/sorted/paginated category page. The href must be the full, absolute clean URL (no query parameters).
Seo product content client renderedLarger project
Server-render your product descriptions in the initial HTML so that all crawlers — including Google, AI search bots, and social media scrapers — can read your content without executing JavaScript.
On WooCommerce
- WooCommerce renders product pages via PHP (server-side) by default. If your description is missing from raw HTML, a page builder (Elementor, Divi, WPBakery) with AJAX-loading or a headless/React front-end is causing the issue.
- For standard themes: verify the short description and full description are output inside single.php or woocommerce/templates/single-product/description.php — not loaded via AJAX.
- If using Elementor or another builder, ensure the Product Description widget is placed statically, not inside a lazy-loaded section (disable 'Load More' AJAX on that container).
- For Product JSON-LD: WooCommerce outputs its own basic JSON-LD. Enhance it with the free Rank Math or Yoast SEO plugin — both inject server-rendered Product schema including description, price, and availability into the <head> via PHP, not JavaScript.
- In Rank Math: Settings → Titles & Meta → WooCommerce → enable 'Product Schema'. In Yoast: Yoast SEO → Search Appearance → WooCommerce (requires Yoast WooCommerce SEO add-on).
- Verify with: curl -s https://yourstore.com/product/example | grep -i 'description' — your product description text must appear in the output.
Seo product description minimalModerate effort
Expand your product descriptions to at least 300 words by adding specifications, use cases, and benefits so search engines have enough content to rank and shoppers have enough information to buy.
On WooCommerce
- Go to WordPress Admin → Products → All Products.
- Click 'Edit' on the product.
- Scroll to the 'Product Description' block (the large editor below the product data tabs) — this is the long description indexed by Google. Do NOT confuse it with the 'Short Description' sidebar field.
- Use the WordPress block editor (Gutenberg) to add Paragraph blocks, Heading blocks (H2/H3), and List blocks for specs and benefits.
- Aim for 300+ words of original content with natural keyword usage.
- Click 'Update' to save. Optionally, install the Yoast SEO or Rank Math plugin to see a real-time content/readability analysis in the same editor.
<?php
/**
* Ensure WooCommerce long product description is output in full
* and not inadvertently stripped by theme filters.
* Add to: wp-content/themes/your-child-theme/functions.php
*/
add_filter( 'woocommerce_short_description', 'wpautop' );
add_filter( 'the_content', 'wpautop' );
/**
* Optional: add a word-count check and admin notice for thin descriptions.
* Fires on the product list screen.
*/
function my_thin_description_admin_notice() {
$screen = get_current_screen();
if ( ! $screen || $screen->id !== 'edit-product' ) {
return;
}
echo '<div class="notice notice-warning"><p>'
. '<strong>SEO tip:</strong> Ensure all product descriptions are at least 300 words for better search visibility.'
. '</p></div>';
}
add_action( 'admin_notices', 'my_thin_description_admin_notice' );Add to your child theme's functions.php if you need to programmatically ensure the long description renders and is not stripped. This is a safety filter — normally just use the WP editor.
Seo product description missingModerate effort
Write and publish a 200–400 word product description covering what the product is, its key specs, who it's for, and how to use it.
On WooCommerce
- In your WordPress admin, go to Products → All Products.
- Click 'Edit' on the product missing a description.
- The large WordPress block editor or classic editor area directly below the product title is the 'Long Description' — this is the main product description shown on the product page. Add your 200–400 word text here.
- Do NOT confuse this with the 'Short Description' meta box (lower on the page) — the long description is what search engines read as the primary content.
- Scroll to the Yoast SEO or Rank Math meta box to review/set a custom meta description snippet (155–160 characters).
- Click 'Update' and view the live product page to confirm.
<?php
/**
* Output the WooCommerce product long description in a custom location.
* Add to your child theme's single-product template where needed.
*/
if ( have_posts() ) {
the_post();
}
global $product;
if ( $product && $product->get_description() ) {
echo '<div class="product-description">';
echo wp_kses_post( $product->get_description() );
echo '</div>';
} else {
echo '<p class="no-description">Full description coming soon.</p>';
}In your child theme's single-product template (single-product/description.php) or via functions.php — WooCommerce outputs the long description automatically via woocommerce_product_tabs. Only add this if you need a custom rendering location.
Seo product description thinModerate effort
Expand every product description to at least 300 words of unique, keyword-rich content that answers real shopper questions and gives Google enough text to understand and rank the page.
On WooCommerce
- Go to WP Admin → Products → [select the product].
- In the product edit screen, use the large 'Product description' editor (below the product name) — this is the full WordPress block editor (Gutenberg) or Classic Editor depending on your setup.
- Write or paste your expanded 300+ word description; use Heading, Paragraph, and List blocks to add structure.
- The shorter 'Product short description' field (further down the page) feeds the summary shown near the Add-to-Cart button — keep it punchy (2–3 sentences) and use the long description for full detail.
- Click 'Update' to save, then use Yoast SEO or Rank Math's 'Request Indexing' feature, or submit the URL in Google Search Console.
<?php
/**
* Flag products with descriptions under 300 words in the WP admin products list.
* Add this to your child theme's functions.php.
*/
add_filter( 'manage_product_posts_columns', 'seo_add_description_word_count_column' );
function seo_add_description_word_count_column( $columns ) {
$columns['desc_word_count'] = 'Desc. Words';
return $columns;
}
add_action( 'manage_product_posts_custom_column', 'seo_render_description_word_count_column', 10, 2 );
function seo_render_description_word_count_column( $column, $post_id ) {
if ( 'desc_word_count' !== $column ) {
return;
}
$product = wc_get_product( $post_id );
$description = $product ? $product->get_description() : '';
$word_count = str_word_count( wp_strip_all_tags( $description ) );
$colour = $word_count < 300 ? '#c00' : '#060';
printf(
'<strong style="color:%s">%d</strong>',
esc_attr( $colour ),
(int) $word_count
);
}In your child theme's functions.php — only needed if you want to programmatically flag thin descriptions for internal review; normal editing is done in the WP admin editor.
Seo product image alt filenameModerate effort
Replace auto-generated filename-based alt text on product images with short, descriptive phrases that accurately describe what the image shows.
On WooCommerce
- Go to WordPress Admin → Products → open the product.
- Click the main product image (featured image) in the 'Product image' meta box on the right, then click the image in the Media Library popup.
- Fill in the 'Alternative Text' field in the Attachment Details panel on the right side of the Media Library.
- Click Update/Save to apply.
- For gallery images, click 'Product gallery', select each image, and fill in the same 'Alternative Text' field.
- For bulk updates across many products, install the 'SEO Optimized Images' plugin or use the 'WP Sheet Editor – Media Library' plugin to edit alt text in a spreadsheet-style view.
<?php
/**
* Ensure WooCommerce product images never fall back to a filename.
* Falls back to the product title when the alt field is empty.
*/
add_filter( 'woocommerce_product_get_image', 'prefix_fix_product_image_alt', 10, 5 );
function prefix_fix_product_image_alt( $html, $product, $size, $attr, $placeholder ) {
// If an explicit alt was passed in $attr, honour it.
if ( ! empty( $attr['alt'] ) ) {
return $html;
}
$image_id = $product->get_image_id();
$media_alt = trim( get_post_meta( $image_id, '_wp_attachment_image_alt', true ) );
if ( '' === $media_alt ) {
// Replace whatever alt is currently in the rendered HTML with the product title.
$safe_alt = esc_attr( $product->get_name() );
$html = preg_replace( '/alt="[^"]*"/', 'alt="' . $safe_alt . '"', $html );
}
return $html;
}In your child theme's functions.php — this filter ensures the product image alt falls back to the product name if the Media Library alt field is empty, rather than using the filename.
Seo product missing brandModerate effort
Add a `brand` property to your Product schema markup so search engines can match your listings to brand-specific queries.
On WooCommerce
- Install a structured data plugin such as 'Rank Math SEO', 'Yoast SEO + WooCommerce SEO add-on', or 'Schema Pro'.
- For Rank Math: go to WordPress Admin → Rank Math → Titles & Meta → Products, find the Schema tab, select 'Product' schema type, and enable the Brand field — map it to your product brand attribute or custom field.
- For Yoast + WooCommerce SEO: go to Yoast SEO → Search Appearance → WooCommerce; the plugin pulls brand from a WooCommerce product attribute named 'brand' — create that attribute under Products → Attributes and assign values to your products.
- Alternatively, add the brand via `functions.php` using the `wpseo_schema_product` or `rank_math/json_ld` filter hooks to inject `"brand": {"@type": "Brand", "name": get_term_name}` programmatically.
- Test any product URL with Google's Rich Results Test after saving.
<?php
// Inject brand into Rank Math Product schema via filter
add_filter( 'rank_math/json_ld', function( $data, $jsonld ) {
if ( ! isset( $data['Product'] ) ) {
return $data;
}
global $product;
if ( ! $product instanceof WC_Product ) {
$product = wc_get_product( get_the_ID() );
}
if ( $product ) {
$brand_terms = wc_get_product_terms( $product->get_id(), 'pa_brand', [ 'fields' => 'names' ] );
if ( ! empty( $brand_terms ) ) {
$data['Product']['brand'] = [
'@type' => 'Brand',
'name' => $brand_terms[0],
];
}
}
return $data;
}, 10, 2 );
?>Add this snippet to your child theme's functions.php to inject brand into Rank Math's Product schema using the 'Brand' product attribute.
Seo product missing gtinModerate effort
Add a GTIN (and/or MPN) field to your Product structured data so Google can match your listings to its product knowledge graph for rich results and Google Shopping.
On WooCommerce
- Install the free plugin 'WooCommerce Product GTIN (EAN, UPC, ISBN)' (by WP Lab) or the 'Perfect WooCommerce Brands' plugin, which both add a dedicated GTIN field to the product edit screen.
- Go to WordPress Admin → Products → [select a product] → scroll to the new GTIN/EAN/UPC field added by the plugin and enter the barcode digits.
- If you use the 'Rank Math SEO' or 'Yoast SEO' plugins with WooCommerce schema output, find the GTIN field in the SEO meta box on the product edit page (Rank Math: Schema tab → Product → GTIN field).
- For manual injection without a plugin: edit your child theme's 'functions.php' and hook into 'woocommerce_structured_data_product' to append 'gtin' and 'mpn' to the schema array, reading values from a custom product meta field.
- Validate with Google's Rich Results Test after saving.
Seo product missing stock statusModerate effort
Add a visible stock availability label and an `availability` property to your Product structured data so Google can display rich results and shoppers know immediately whether a product is in stock.
On WooCommerce
- Install the free *Rank Math SEO* or *Yoast SEO* plugin (both support WooCommerce Product schema with `availability`).
- With Rank Math: go to Rank Math → Titles & Meta → WooCommerce and confirm 'Use WooCommerce Stock Status' is toggled on — Rank Math reads WooCommerce stock and automatically outputs `InStock`/`OutOfStock`/`PreOrder` in the JSON-LD.
- With Yoast SEO + Yoast WooCommerce SEO add-on: the plugin auto-maps WooCommerce stock to the schema; confirm stock management is enabled on each product under WooCommerce → Products → Edit Product → Inventory tab → 'Stock status'.
- For the visible label, WooCommerce outputs stock status text natively if 'Manage stock' or 'Stock status' is set per product — verify in WooCommerce → Products → Edit → Inventory. Enable 'Display stock availability' in WooCommerce → Settings → Products.
- Validate a product URL in the Rich Results Test.
<?php
/**
* Inject Product JSON-LD with availability on WooCommerce single product pages.
* Add to: wp-content/themes/your-child-theme/functions.php
*/
add_action( 'wp_head', 'custom_wc_product_jsonld' );
function custom_wc_product_jsonld() {
if ( ! is_singular( 'product' ) ) return;
global $post;
$product = wc_get_product( $post->ID );
if ( ! $product ) return;
$availability_url = $product->is_in_stock()
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock';
$price = $product->get_price();
$currency = get_woocommerce_currency();
$sku = $product->get_sku();
$name = wp_strip_all_tags( $product->get_name() );
$description = wp_strip_all_tags( $product->get_short_description() );
$image_id = $product->get_image_id();
$image_url = $image_id ? wp_get_attachment_url( $image_id ) : '';
$brand = ''; // Add your brand name or pull from a custom field
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => $name,
'description' => $description,
'sku' => $sku,
'image' => $image_url,
'brand' => [ '@type' => 'Brand', 'name' => $brand ],
'offers' => [
'@type' => 'Offer',
'url' => get_permalink(),
'priceCurrency' => $currency,
'price' => $price,
'availability' => $availability_url,
'itemCondition' => 'https://schema.org/NewCondition',
],
];
echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . '</script>' . PHP_EOL;
}
/**
* Optional: Display a visible stock label on the product page.
* Hooked after the Add-to-Cart button area.
*/
add_action( 'woocommerce_single_product_summary', 'custom_wc_stock_label', 31 );
function custom_wc_stock_label() {
global $product;
if ( $product->is_in_stock() ) {
echo '<p class="custom-stock-label" style="color:#2a7a2a;font-weight:bold;">✓ In Stock</p>';
} else {
echo '<p class="custom-stock-label" style="color:#b00020;font-weight:bold;">✕ Out of Stock</p>';
}
}Add to your child theme's functions.php. This injects a complete Product JSON-LD block on single product pages, pulling availability from WooCommerce stock.
Seo product no imagesModerate effort
Add at least one high-quality image to every product page so shoppers can see what they're buying and Google can surface your products in Image Search.
On WooCommerce
- Log in to your WordPress admin and go to Products → All Products.
- Click 'Edit' on the product missing images.
- In the right-hand sidebar, find the 'Product image' meta box and click 'Set product image' to upload or select your main image from the Media Library.
- Scroll down to the 'Product gallery' meta box below it and click 'Add product gallery images' to add additional photos.
- Click each image in the Media Library, fill in the 'Alt Text' field on the right, and click 'Set product image' / 'Add to gallery'.
- Click 'Update' to save, then visit the product page on the front end to confirm images display correctly.
<?php
/**
* Output product gallery images with descriptive alt text.
* Hooked to woocommerce_before_single_product_summary.
*/
add_action( 'woocommerce_before_single_product_summary', 'custom_product_gallery_images', 25 );
function custom_product_gallery_images() {
global $product;
$image_id = $product->get_image_id();
$gallery_ids = $product->get_gallery_image_ids();
$all_ids = $image_id ? array_merge( [ $image_id ], $gallery_ids ) : $gallery_ids;
if ( empty( $all_ids ) ) {
echo '<p class="no-product-image">No product images available.</p>';
return;
}
echo '<div class="custom-product-gallery">';
foreach ( $all_ids as $index => $img_id ) {
$alt = get_post_meta( $img_id, '_wp_attachment_image_alt', true );
$alt = $alt ? esc_attr( $alt ) : esc_attr( get_the_title( $img_id ) );
$loading = ( $index === 0 ) ? 'eager' : 'lazy';
echo wp_get_attachment_image(
$img_id,
'woocommerce_single',
false,
[
'alt' => $alt,
'loading' => $loading,
]
);
}
echo '</div>';
}Add to your child theme's functions.php to programmatically output the product gallery images with correct alt text on the single product page.
Seo product no spec tableModerate effort
Add a structured specifications table or list to every product page so shoppers, search engines, and AI tools can instantly find and use key product details.
On WooCommerce
- In WordPress admin go to Products → (select product) → scroll to the 'Product data' meta box → click the 'Attributes' tab.
- Add custom attributes (e.g. 'Weight', 'Material', 'Dimensions') with their values, and tick 'Visible on the product page' for each one — WooCommerce renders these automatically as a 'Additional information' tab.
- For a richer, fully styled spec table, install a plugin such as 'WooCommerce Product Table', 'Yith WooCommerce Tab Manager', or 'Product Specifications for WooCommerce', then configure it under the plugin's settings page.
- To customise the default tab's appearance, copy 'woocommerce/templates/single-product/tabs/additional-information.php' into your child theme at 'your-child-theme/woocommerce/single-product/tabs/additional-information.php' and edit the markup.
- Save the product and verify the Specifications tab appears on the front-end product page.
Seo product price not in htmlModerate effort
Render your product price in the server-generated HTML so Google and other crawlers can read it directly — without executing JavaScript.
On WooCommerce
- WooCommerce renders prices server-side by default via PHP templates. If prices appear blank in source, a plugin (e.g. a price-personalisation or currency-switcher plugin) may be replacing them with JS.
- Go to WP Admin → Appearance → Theme File Editor (or use a child theme) and open woocommerce/single-product/price.php. Confirm it uses wc_price( $product->get_price() ) — this is a PHP (server-side) call.
- If a plugin is the culprit, check its settings for a 'server-side rendering' or 'initial price in HTML' option, or replace it with one that preserves static HTML prices.
- For structured data, install a plugin such as Rank Math SEO or Yoast SEO (with WooCommerce SEO add-on) — these automatically inject Product JSON-LD with the 'offers' price block into server-rendered HTML.
- Verify by viewing page source and searching for the numeric price value.
Seo product single imageModerate effort
Add at least 4 product images (multiple angles, detail shots, and lifestyle/in-use photos) to every product page to improve click-through rates, conversions, and Google Images visibility.
On WooCommerce
- Go to WordPress Admin → Products → [select your product] → Edit.
- In the right sidebar, find 'Product image' (main/featured image) and 'Product gallery' (additional images).
- Click 'Add product gallery images' to open the Media Library and upload or select additional images.
- To add alt text: go to Media Library → select each image → fill in the 'Alt Text' field on the right, then click 'Update'.
- Drag images within the Product Gallery box to reorder them, then click 'Update' to save the product.
<?php
/**
* Override in: yourchildtheme/woocommerce/single-product/product-image.php
* This snippet ensures gallery images always have descriptive alt text.
*/
global $product;
$gallery_image_ids = $product->get_gallery_image_ids();
$main_image_id = $product->get_image_id();
$all_image_ids = array_merge( [ $main_image_id ], $gallery_image_ids );
echo '<div class="product-gallery">';
foreach ( $all_image_ids as $index => $image_id ) {
$alt_text = trim( get_post_meta( $image_id, '_wp_attachment_image_alt', true ) );
if ( empty( $alt_text ) ) {
$alt_text = get_the_title( $image_id );
}
echo wp_get_attachment_image(
$image_id,
'woocommerce_single',
false,
[
'alt' => esc_attr( $alt_text ),
'loading' => $index === 0 ? 'eager' : 'lazy',
'fetchpriority' => $index === 0 ? 'high' : 'auto',
]
);
}
echo '</div>';Add to your child theme's functions.php to ensure the product gallery images all output with explicit alt text. The gallery is rendered by woocommerce/templates/single-product/product-image.php — override it in your child theme.
Seo product title genericModerate effort
Rewrite every product page title tag to include the specific product name plus at least one differentiating attribute (variant, material, brand, or model) so Google and shoppers can instantly tell what the page is about.
On WooCommerce
- Install and activate the Yoast SEO or Rank Math plugin if not already present.
- Go to Products → All Products and click Edit on the product.
- Scroll to the Yoast SEO (or Rank Math SEO) meta box below the product description.
- Click into the 'SEO title' field and type your descriptive title. The snippet preview shows how it will look in Google.
- For bulk updates, use Yoast's bulk editor (SEO → Tools → Bulk editor) or Rank Math's bulk editing table.
- To set a default template for all products, go to Yoast SEO → Search Appearance → WooCommerce (or Rank Math → Titles & Meta → WooCommerce) and set the Product Title template, e.g. '%%title%% – %%ct_product_cat%% – %%sitename%%'.
<?php
/**
* Append product type and brand to product page title tags.
* Requires: WooCommerce active. Add to child theme functions.php.
*/
add_filter( 'woocommerce_page_title', 'custom_product_seo_title' );
function custom_product_seo_title( $page_title ) {
if ( is_product() ) {
global $post;
$product = wc_get_product( $post->ID );
$categories = wp_get_post_terms( $post->ID, 'product_cat', [ 'fields' => 'names' ] );
$category = ! empty( $categories ) ? $categories[0] : '';
$brand = get_bloginfo( 'name' );
if ( $product && $category ) {
return esc_html( $product->get_name() ) . ' – ' . esc_html( $category ) . ' – ' . esc_html( $brand );
}
}
return $page_title;
}
?>Add to your child theme's functions.php ONLY if you are not using Yoast SEO or Rank Math (those plugins handle this via their UI). This filter overrides the WooCommerce product page title tag.
Seo variant urls in sitemapModerate effort
Remove product variant URLs (e.g. ?variant= or ?sku= query strings) from your XML sitemap and keep only the canonical base product URL for each product.
On WooCommerce
- If using Yoast SEO: go to WP Admin → SEO → Search Appearance → Products → ensure 'Product Variations' is set to 'noindex' and that variations are excluded from the sitemap under SEO → General → Features → XML Sitemaps (click the '?' then 'See the XML sitemap') — variations should not appear.
- If using Rank Math: WP Admin → Rank Math → Sitemap Settings → Products → toggle off 'Include Product Variations in Sitemap'.
- If using All in One SEO (AIOSEO): WP Admin → All in One SEO → Sitemaps → Additional Pages — review for any manually added variant URLs, and under 'Post Types' ensure product variations are excluded.
- If using a custom or plugin-generated sitemap, add a filter in your child theme's functions.php to strip URLs matching the pattern '?attribute_' or any query-string parameter before they are added to the sitemap.
- Re-submit the updated sitemap URL in Google Search Console.
<?php
/**
* Exclude product variation URLs (any URL with a query string)
* from the Yoast SEO XML sitemap.
* Add this to your child theme's functions.php
*/
add_filter( 'wpseo_sitemap_entry', 'exclude_variant_urls_from_sitemap', 10, 3 );
function exclude_variant_urls_from_sitemap( $url, $type, $object ) {
// If the URL contains a query string, exclude it by returning false
if ( isset( $url['loc'] ) && strpos( $url['loc'], '?' ) !== false ) {
return false;
}
return $url;
}
/**
* Also exclude WooCommerce product variations post type from the sitemap entirely.
* Yoast: this prevents variation CPT entries appearing as sitemap URLs.
*/
add_filter( 'wpseo_sitemap_exclude_post_type', 'exclude_product_variation_post_type', 10, 2 );
function exclude_product_variation_post_type( $excluded, $post_type ) {
if ( 'product_variation' === $post_type ) {
return true; // true = exclude
}
return $excluded;
}Add to your child theme's functions.php to exclude product variation URLs from Yoast SEO's sitemap.
Seo variant urls not canonicalizedModerate effort
Add a canonical tag to every product variant URL pointing back to the base product page, so Google consolidates ranking signals on one URL instead of splitting them across hundreds of duplicates.
On WooCommerce
- Install the free Yoast SEO or Rank Math plugin if not already installed (Plugins → Add New).
- Yoast SEO: Go to Yoast SEO → Search Appearance → Products. Yoast automatically outputs a canonical pointing to the base product permalink for variable product pages. Confirm by viewing source on a product URL with query parameters (e.g. ?attribute_pa_color=red) — the canonical should show the base product URL.
- Rank Math: Go to Rank Math → Titles & Meta → Products. Rank Math similarly outputs the base product canonical automatically.
- If you have a custom or page-builder theme that outputs its own canonical, search your theme's header.php or functions.php for any hardcoded canonical output and remove duplicates.
- If query parameters are added by plugins (e.g. filters), go to Yoast → SEO → Search Appearance → Advanced and confirm query parameters are not being indexed as separate URLs. Also use Google Search Console → URL Inspection on a variant URL to confirm the canonical Google sees.
<?php
/**
* Force canonical URL on WooCommerce variable product pages to base product URL.
* Add to child theme's functions.php
*/
add_action( 'wp_head', 'wc_custom_variant_canonical', 1 );
function wc_custom_variant_canonical() {
if ( ! is_product() ) {
return;
}
global $post;
// Remove any canonical output by other plugins first
remove_action( 'wp_head', 'rel_canonical' );
$canonical = get_permalink( $post->ID ); // Always the base product permalink
echo '<link rel="canonical" href="' . esc_url( $canonical ) . '">' . PHP_EOL;
}Add to your child theme's functions.php ONLY if Yoast/Rank Math is not handling canonicals. This forces the canonical on variable product pages to the base product permalink.
Server errorModerate effort
Identify and fix every page returning an HTTP 5xx server error so search engines can crawl and index those URLs again.
On WooCommerce
- Enable WordPress debug logging: in wp-config.php set define('WP_DEBUG', true); and define('WP_DEBUG_LOG', true); — errors are written to /wp-content/debug.log.
- In your hosting control panel (cPanel, Kinsta, etc.) open the Error Logs section to read PHP fatal errors and the exact stack trace.
- Deactivate plugins one by one via Plugins > Installed Plugins (start with recently updated ones) until the 500 disappears, then reactivate them to identify the culprit.
- Switch temporarily to a default WordPress theme (Twenty Twenty-Four) via Appearance > Themes to rule out a theme-level error.
- Fix the identified cause (update the plugin, contact its support, increase PHP memory in php.ini or wp-config.php with define('WP_MEMORY_LIMIT', '256M');), then re-enable debug mode off in production.
- Submit the repaired URL in Google Search Console > URL Inspection > Request Indexing.
// Enable WordPress/WooCommerce debug logging (add to wp-config.php)
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
define( 'WP_MEMORY_LIMIT', '256M' );Same wp-config.php entries as WordPress.org above — WooCommerce runs on WordPress. After identifying and fixing the error, revert WP_DEBUG to false.
Slow pageModerate effort
Reduce your page load time to under 3 seconds by compressing images, eliminating render-blocking scripts and styles, and improving server response speed.
On WooCommerce
- Install the 'WP Rocket' plugin (paid, most comprehensive) or 'LiteSpeed Cache' / 'W3 Total Cache' (free) via WordPress admin → Plugins → Add New.
- Within the caching plugin's settings, enable: Page Caching, Browser Caching, GZIP Compression, CSS & JS Minification, and Defer JS.
- Images: Install 'Imagify' or 'ShortPixel Image Optimizer' (Plugins → Add New) to bulk-compress existing images and auto-compress new uploads. Enable WebP conversion in the plugin settings.
- In WordPress admin → Settings → Media, set maximum upload dimensions matching your largest display size to prevent oversized originals being stored.
- Audit plugins: WordPress admin → Plugins → Installed Plugins — deactivate and delete any plugin not in active use, as each can add page weight.
- On your hosting control panel (cPanel, Kinsta, WP Engine dashboard, etc.), enable HTTP/2, PHP 8.x, and server-side caching (OPcache) if not already active. Switch to a host with a built-in CDN if your current host lacks one.
<?php
/**
* Defer non-critical scripts sitewide.
* Add to: wp-content/themes/your-child-theme/functions.php
*/
add_filter( 'script_loader_tag', 'wc_defer_non_critical_scripts', 10, 3 );
function wc_defer_non_critical_scripts( $tag, $handle, $src ) {
// Scripts to exclude from deferring (keep these synchronous)
$no_defer = array( 'jquery', 'wc-checkout', 'woocommerce' );
if ( in_array( $handle, $no_defer, true ) ) {
return $tag;
}
// Add defer to everything else
if ( strpos( $tag, ' defer' ) === false ) {
$tag = str_replace( ' src=', ' defer src=', $tag );
}
return $tag;
}Add to your child theme's functions.php to defer all non-essential scripts registered by WordPress/WooCommerce.
Thin contentModerate effort
Add meaningful, original written content to pages that contain fewer than 300 words so Google has enough substance to rank and display them confidently.
On WooCommerce
- Log in to WordPress Admin → Products → All Products → click 'Edit' on the thin product.
- In the main content editor (the large block editor area at the top of the edit screen, NOT the short description), write your full expanded product description using paragraphs, headings, bullet lists, and FAQ blocks.
- The 'Product short description' box (below the main editor) is used for the brief intro shown near the Add-to-Cart button — keep this punchy but also make it at least 2–3 sentences.
- For category pages: go to Products → Categories → click 'Edit' on the category → add descriptive text in the 'Description' field. Note: your theme must be configured to display this text; check your theme's category template if it doesn't appear.
- For standard pages: Pages → All Pages → Edit → add content in the block editor.
- Install the free 'Yoast SEO' or 'Rank Math' plugin to get an inline content analysis that flags thin pages and guides you toward sufficient length directly inside the editor.
<?php
/**
* Add a Specifications tab to WooCommerce product pages.
* Place in your child theme's functions.php
*/
add_filter( 'woocommerce_product_tabs', 'mytheme_add_specifications_tab' );
function mytheme_add_specifications_tab( $tabs ) {
global $product;
$specs = get_post_meta( $product->get_id(), 'product_specifications', true );
if ( ! empty( $specs ) ) {
$tabs['specifications'] = array(
'title' => __( 'Specifications', 'woocommerce' ),
'priority' => 25,
'callback' => 'mytheme_specifications_tab_content',
);
}
return $tabs;
}
function mytheme_specifications_tab_content() {
global $product;
$specs = get_post_meta( $product->get_id(), 'product_specifications', true );
echo '<h2>' . esc_html__( 'Specifications', 'woocommerce' ) . '</h2>';
echo '<div class="product-specifications">' . wp_kses_post( $specs ) . '</div>';
}Add to your child theme's functions.php to inject a custom 'Specifications' tab on every product page — fill the content via a custom field or ACF field named 'product_specifications'.
Title too longQuick win
Shorten your page title tag to 60 characters or fewer so it displays fully — without truncation — in Google search results.
On WooCommerce
- Install and activate the Yoast SEO or Rank Math plugin if not already present.
- For a product/page: open the edit screen, scroll to the Yoast SEO / Rank Math meta box at the bottom, click on the 'SEO title' field, and type your new title (the plugin's traffic-light indicator turns green at the right length).
- For site-wide title templates: in Yoast go to SEO → Search Appearance → Content Types and edit the title template for Products, Pages, etc. In Rank Math go to Rank Math → Titles & Meta → Products.
- Keep the template under 60 characters including any appended brand tokens (%sep% %sitename%).
- Click Update/Publish to save.
<?php
// Override WooCommerce product page title tag (child theme functions.php)
add_filter( 'wpseo_title', 'custom_shorten_seo_title', 10, 1 );
function custom_shorten_seo_title( $title ) {
if ( is_product() ) {
// Trim to 60 chars as a safety net
$title = mb_substr( $title, 0, 60 );
}
return $title;
}
?>Add to your child theme's functions.php ONLY if you need a programmatic override. Normally use the Yoast/Rank Math meta box in the editor instead.
Title too shortQuick win
Expand your page title to between 30 and 60 characters so Google displays it fully in search results and shoppers can immediately understand what the page is about.
On WooCommerce
- Install the free Yoast SEO or Rank Math plugin if you haven't already — both add an SEO panel directly to every edit screen.
- For a product: go to Products → All Products → select the product → scroll to the Yoast SEO / Rank Math panel → click the 'Edit snippet' button → update the 'SEO Title' field.
- For a category page: go to Products → Categories → click 'Edit' on the category → scroll to the Yoast SEO / Rank Math panel → update the 'SEO Title'.
- For the homepage: go to SEO (Yoast) → Search Appearance → Content Types tab for pages, or navigate to Pages → Home → edit with the SEO panel.
- Use the Yoast / Rank Math character counter in the snippet preview to confirm the title is 30–60 characters before saving.
<?php
/**
* Customise the page title for WooCommerce product pages.
* Add to child theme's functions.php.
* Prefer Yoast SEO / Rank Math for a UI-based solution instead.
*/
add_filter( 'woocommerce_page_title', 'custom_product_page_title' );
function custom_product_page_title( $page_title ) {
// Example: append store name if missing
$site_name = get_bloginfo( 'name' );
if ( is_product() && strpos( $page_title, $site_name ) === false ) {
$page_title = $page_title . ' – ' . $site_name;
}
return $page_title;
}
?>Only use this snippet if you are NOT using Yoast SEO or Rank Math. Add to your child theme's functions.php to filter the document title.
Answer Engine Optimization · 17 fixes
Aeo schema client renderedModerate effort
Move your JSON-LD structured data from client-side JavaScript injection to server-rendered HTML so that AI crawlers and search engines can read it in the raw page source.
On WooCommerce
- In your WordPress admin go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- If you use an SEO plugin like Rank Math or Yoast SEO, check Settings → Structured Data / Schema — both plugins output schema server-side by default; ensure no conflicting JavaScript schema plugin is also active.
- If you are adding schema manually, open your theme's single-product template (woocommerce/single-product.php or the block template equivalent) and add the JSON-LD <script> block using PHP to echo dynamic values (get_the_title(), get_post_meta(), $product->get_price(), etc.).
- Alternatively, use the wp_head action hook in your child theme's functions.php to output the JSON-LD block server-side for every product page.
- Deactivate any plugin or GTM tag that injects schema via JavaScript, as these will duplicate or override the server-rendered version and may confuse crawlers.
- View the page source on a live product page and confirm the JSON-LD block is present before the closing </head> or in the <body>, with no JavaScript required to render it.
<?php
add_action( 'wp_head', 'my_woo_product_json_ld' );
function my_woo_product_json_ld() {
if ( ! is_product() ) return;
global $product;
if ( ! $product instanceof WC_Product ) {
$product = wc_get_product( get_the_ID() );
}
if ( ! $product ) return;
$availability = $product->is_in_stock()
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock';
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => $product->get_name(),
'description' => wp_strip_all_tags( $product->get_short_description() ?: $product->get_description() ),
'sku' => $product->get_sku(),
'brand' => [
'@type' => 'Brand',
'name' => get_bloginfo( 'name' ),
],
'image' => [ wp_get_attachment_url( $product->get_image_id() ) ],
'offers' => [
'@type' => 'Offer',
'url' => get_permalink( $product->get_id() ),
'priceCurrency' => get_woocommerce_currency(),
'price' => $product->get_price(),
'availability' => $availability,
'itemCondition' => 'https://schema.org/NewCondition',
],
];
echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . '</script>' . "\n";
}Add this to your child theme's functions.php. It hooks into wp_head to output Product schema server-side on single product pages.
Article missing authorQuick win
Add a structured-data author field (schema.org Person) to every article or blog post so AI engines and Google can identify and trust who wrote the content.
On WooCommerce
- Install the Yoast SEO or Rank Math plugin if not already active — both support structured data for posts.
- In Yoast SEO: go to each post's edit screen → Yoast SEO panel → Schema tab → set 'Article type' and ensure the author is pulled from the WordPress post author field.
- To enrich the author entity, go to Users → Edit the author's profile → fill in 'Biographical Info' and 'Website'. Yoast will use these to build the Person schema automatically.
- In Rank Math: post edit screen → Rank Math panel → Schema → Article → the author is auto-populated from the post author. Add social profile URLs under Rank Math → Titles & Meta → Local SEO → Social Profiles.
- Alternatively, paste the JSON-LD snippet manually into your child theme's single.php or via a plugin like 'Insert Headers and Footers' scoped to single posts.
- Validate with the Rich Results Test.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "How to Choose the Best Running Shoes",
"datePublished": "2024-03-10T09:00:00+00:00",
"dateModified": "2024-06-20T14:00:00+00:00",
"author": {
"@type": "Person",
"name": "Alex Johnson",
"url": "https://www.yourstore.com/about/alex-johnson",
"sameAs": [
"https://www.linkedin.com/in/alexjohnson",
"https://twitter.com/alexjohnson"
]
},
"publisher": {
"@type": "Organization",
"name": "Your Store Name",
"logo": {
"@type": "ImageObject",
"url": "https://www.yourstore.com/assets/logo.png",
"width": 300,
"height": 60
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://www.yourstore.com/blog/how-to-choose-running-shoes"
}
}
</script>For any static site or custom CMS: paste inside the <head> of each article page and fill in real values.
Faq schema not visibleModerate effort
Render all FAQ answers in the server-delivered HTML so that both Google and your FAQ structured data can see every question and answer — not just the one currently open in a JavaScript accordion.
On WooCommerce
- Install a structured-data plugin such as Rank Math SEO or Yoast SEO (with the Schema add-on) — these can inject FAQPage JSON-LD automatically.
- Edit the FAQ page in WordPress (Pages → your FAQ page → Edit with Gutenberg).
- Add the Rank Math / Yoast FAQ block, which outputs both visible HTML and matching JSON-LD in one step — all answers are in the HTML regardless of accordion state.
- If you have an existing JavaScript accordion plugin (e.g. a page-builder widget), remove it and use the native FAQ block instead, or ensure the plugin outputs all answers in the server HTML (check its settings for 'render all panels' or 'disable lazy-load').
- Validate with Google's Rich Results Test after publishing.
<section>
<details>
<summary>What is your return policy?</summary>
<p>We accept returns within 30 days of purchase. Items must be unused and in original packaging. Contact support@example.com to start a return.</p>
</details>
<details>
<summary>How long does shipping take?</summary>
<p>Standard shipping takes 3–5 business days. Express shipping (1–2 business days) is available at checkout.</p>
</details>
<details>
<summary>Do you ship internationally?</summary>
<p>Yes, we ship to over 40 countries. International orders typically arrive within 7–14 business days.</p>
</details>
<details>
<summary>Can I change or cancel my order?</summary>
<p>Orders can be changed or cancelled within 1 hour of placement. After that, please wait for delivery and use our return process.</p>
</details>
</section>Replace JS-only accordion with native <details>/<summary> elements — all answers stay in the HTML at all times, visible to crawlers regardless of open/closed state.
Invalid rating valueModerate effort
Fix the AggregateRating.ratingValue in your structured data so it is a valid number between 1 and 5 (or your declared scale's minimum and maximum).
On WooCommerce
- If you use an SEO plugin (Yoast SEO, Rank Math, or All in One SEO), go to that plugin's settings and locate the 'Schema' or 'Structured Data' section — ensure the WooCommerce product schema is enabled and set to pull the real WooCommerce average rating.
- In Yoast SEO: SEO → Search Appearance → Content Types → Products → Schema tab → confirm 'Product' and 'AggregateRating' are enabled. Yoast reads WooCommerce's native rating automatically when reviews are present.
- In Rank Math: Rank Math → Titles & Meta → Products → Schema → ensure Schema Type is 'Product'; Rank Math auto-populates ratingValue from WooCommerce reviews.
- If you have custom or theme-level structured data, open your theme's functions.php or the responsible template file (single-product.php, woocommerce/single-product.php) and find where AggregateRating is built. Replace any hardcoded ratingValue with wc_get_product()->get_average_rating() and wrap the entire block in a check: if ( $product->get_review_count() > 0 ).
- Ensure WooCommerce product reviews are enabled: WooCommerce → Settings → Products → Reviews → check 'Enable product reviews'.
- Validate with the Google Rich Results Test after saving.
add_action( 'wp_head', 'mytheme_product_aggregate_rating_schema' );
function mytheme_product_aggregate_rating_schema() {
if ( ! is_product() ) {
return;
}
global $product;
if ( ! $product instanceof WC_Product ) {
$product = wc_get_product( get_the_ID() );
}
$review_count = (int) $product->get_review_count();
$rating_value = (float) $product->get_average_rating();
if ( $review_count < 1 || $rating_value < 1 ) {
return; // No valid rating — suppress the block entirely.
}
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => get_the_title(),
'aggregateRating' => [
'@type' => 'AggregateRating',
'ratingValue' => (string) round( $rating_value, 1 ),
'bestRating' => '5',
'worstRating' => '1',
'reviewCount' => (string) $review_count,
],
];
echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) . '</script>' . PHP_EOL;
}Add to your child theme's functions.php. This hooks into wp_head on single product pages and outputs valid AggregateRating JSON-LD only when real reviews exist.
Low answer first scoreModerate effort
Restructure page content to lead with a direct, concise answer, use question-format headings, and add FAQ structured data so search engines and AI assistants can surface your answers prominently.
On WooCommerce
- For product pages: WooCommerce → Products → Edit Product. In the main description editor (not the short description), write the 160-character answer summary as the very first paragraph.
- Rewrite any H2/H3 headings inside the description using the block editor's Heading block, phrasing them as questions.
- For static pages (About, FAQ, Landing): Pages → Edit. Add a Paragraph block at the top with the answer summary; add Heading blocks phrased as questions.
- Install the free 'Rank Math SEO' or 'Yoast SEO' plugin. Both include a dedicated FAQ Block (Gutenberg block) that renders a visible Q&A section AND automatically outputs FAQPage JSON-LD schema — no manual coding required.
- After publishing, paste the page URL into Google's Rich Results Test (search 'Rich Results Test') to confirm the FAQ schema is detected.
<!-- Answer-first page structure template -->
<!-- 1. Direct-answer summary (≤160 characters) -->
<p class="answer-summary">
[Product Name] does [core benefit] by [key mechanism] — ideal for [target customer].
</p>
<!-- 2. Question-format H2 subheading -->
<h2>What does [Product Name] do?</h2>
<p>Direct 1–2 sentence answer here. Then expand with supporting detail below.</p>
<!-- 3. Another question-format heading -->
<h2>Who is [Product Name] designed for?</h2>
<p>Direct answer. Then elaborate.</p>
<!-- 4. FAQ section (visible on page — required for FAQPage schema) -->
<section id="faq">
<h2>Frequently Asked Questions</h2>
<h3>How long does shipping take?</h3>
<p>Standard shipping takes 3–5 business days. Express options are available at checkout.</p>
<h3>What is your return policy?</h3>
<p>We accept returns within 30 days of delivery on unused items in original packaging.</p>
<h3>Is [Product Name] compatible with [common concern]?</h3>
<p>Yes. [One-sentence direct answer with any important caveats.]</p>
</section>
<!-- 5. FAQPage JSON-LD (copy the JSON-LD snippet above and paste here or in <head>) -->Page content structure template — place this as the opening content block on any product, category, or landing page.
Low review countModerate effort
Collect at least 5 published reviews so Google will display star ratings in search results alongside your product listings.
On WooCommerce
- Go to WooCommerce → Settings → Products → Reviews and ensure 'Enable product reviews' and 'Show verified owner label' are checked.
- Enable automated review request emails via a plugin: install 'YITH WooCommerce Review Reminder' or 'Reviewer – WooCommerce Review Reminder' from WordPress.org plugins.
- For structured data output, install 'Rank Math SEO' or 'Yoast SEO' (with WooCommerce SEO add-on) — both automatically output Product + AggregateRating JSON-LD once reviews exist.
- To import reviews: use the 'WP All Import' plugin with the WooCommerce add-on, or the built-in WordPress importer under Tools → Import.
- Test structured data by pasting a product URL into Google's Rich Results Test after reaching 5 reviews.
<?php
/**
* Output Product AggregateRating JSON-LD only when review count >= 5.
* Add to: child-theme/functions.php
*/
add_action( 'wp_head', 'my_product_aggregate_rating_jsonld' );
function my_product_aggregate_rating_jsonld() {
if ( ! is_singular( 'product' ) ) {
return;
}
global $post;
$product = wc_get_product( $post->ID );
if ( ! $product ) {
return;
}
$review_count = $product->get_review_count();
if ( $review_count < 5 ) {
// Do not output markup until the Google minimum is met.
return;
}
$average_rating = $product->get_average_rating();
$name = esc_js( $product->get_name() );
$sku = esc_js( $product->get_sku() );
$price = $product->get_price();
$currency = get_woocommerce_currency();
$url = esc_url( get_permalink( $post->ID ) );
$image = esc_url( wp_get_attachment_url( $product->get_image_id() ) );
$in_stock = $product->is_in_stock() ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock';
echo "<script type='application/ld+json'>\n";
echo json_encode( [
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => $name,
'image' => $image,
'sku' => $sku,
'aggregateRating' => [
'@type' => 'AggregateRating',
'ratingValue' => number_format( (float) $average_rating, 1 ),
'reviewCount' => (string) $review_count,
'bestRating' => '5',
'worstRating' => '1',
],
'offers' => [
'@type' => 'Offer',
'url' => $url,
'priceCurrency' => $currency,
'price' => $price,
'availability' => $in_stock,
'itemCondition' => 'https://schema.org/NewCondition',
],
], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT );
echo "\n</script>\n";
}
?>Add this to your child theme's functions.php file. It outputs AggregateRating JSON-LD on single product pages only when at least 5 reviews exist, so no partial/invalid markup is ever output.
Missing schema articleModerate effort
Add Article schema markup (JSON-LD) to every blog post and editorial page so Google can identify and rich-result-enhance your content in search.
On WooCommerce
- Install the free 'Yoast SEO' plugin (or 'Rank Math SEO') from Plugins → Add New.
- For Yoast: go to SEO → Search Appearance → Content Types → Posts, and confirm 'Show Posts in search results' is enabled — Yoast automatically outputs Article/BlogPosting JSON-LD for every post.
- For Rank Math: go to Rank Math → Titles & Meta → Posts → Schema Type and select 'Article' or 'BlogPosting'.
- To add or override schema manually, edit your child theme's 'single.php' and insert the JSON-LD <script> block in the loop, using PHP variables (get_the_title(), get_the_author(), get_the_date('Y-m-d'), get_the_modified_date('Y-m-d')) to populate fields.
- Validate any post URL with the Rich Results Test.
<?php
add_action( 'wp_head', 'mytheme_article_schema' );
function mytheme_article_schema() {
if ( ! is_single() ) {
return;
}
global $post;
$author_name = get_the_author_meta( 'display_name', $post->post_author );
$date_pub = get_the_date( 'Y-m-d', $post );
$date_mod = get_the_modified_date( 'Y-m-d', $post );
$description = wp_strip_all_tags( get_the_excerpt( $post ) );
$image_url = get_the_post_thumbnail_url( $post, 'large' );
$post_url = get_permalink( $post );
$blog_name = get_bloginfo( 'name' );
$logo_url = '';
$custom_logo_id = get_theme_mod( 'custom_logo' );
if ( $custom_logo_id ) {
$logo_url = wp_get_attachment_image_url( $custom_logo_id, 'full' );
}
$schema = [
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
'headline' => get_the_title( $post ),
'description' => $description,
'author' => [
'@type' => 'Person',
'name' => $author_name,
],
'publisher' => [
'@type' => 'Organization',
'name' => $blog_name,
'logo' => [
'@type' => 'ImageObject',
'url' => $logo_url,
],
],
'datePublished' => $date_pub,
'dateModified' => $date_mod,
'mainEntityOfPage' => [
'@type' => 'WebPage',
'@id' => $post_url,
],
'url' => $post_url,
];
if ( $image_url ) {
$schema['image'] = [
'@type' => 'ImageObject',
'url' => $image_url,
];
}
echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . '</script>' . "\n";
}
?>Add to your child theme's functions.php. Outputs Article JSON-LD in the <head> of all single posts. Remove this if you already use Yoast SEO or Rank Math (they handle it automatically).
Missing schema breadcrumblistModerate effort
Add BreadcrumbList structured data (JSON-LD) to every page so Google can display your site's navigation path directly in search results.
On WooCommerce
- Install and activate the free Yoast SEO or Rank Math SEO plugin from the WordPress plugin directory (Plugins › Add New).
- Yoast SEO: go to SEO › Search Appearance › Breadcrumbs and ensure breadcrumbs are enabled; Yoast automatically outputs BreadcrumbList JSON-LD on every page — no additional configuration needed.
- Rank Math: go to Rank Math › Titles & Meta › each post type; breadcrumb schema is output automatically when the Schema module is active.
- If you prefer a code-only approach, add the dynamic JSON-LD block to your child theme's functions.php using wp_head action and WooCommerce conditional tags (is_product(), is_product_category()) to build the correct hierarchy.
- Validate using Google's Rich Results Test on a product page and a category page.
<?php
add_action( 'wp_head', 'my_wc_breadcrumb_jsonld' );
function my_wc_breadcrumb_jsonld() {
if ( ! is_product() ) {
return;
}
global $product;
$shop_url = get_permalink( wc_get_page_id( 'shop' ) );
$shop_name = get_the_title( wc_get_page_id( 'shop' ) );
$terms = get_the_terms( $product->get_id(), 'product_cat' );
$cat_name = '';
$cat_url = '';
if ( $terms && ! is_wp_error( $terms ) ) {
$term = array_shift( $terms );
$cat_name = $term->name;
$cat_url = get_term_link( $term );
}
$schema = [
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => [
[
'@type' => 'ListItem',
'position' => 1,
'name' => 'Home',
'item' => home_url( '/' ),
],
[
'@type' => 'ListItem',
'position' => 2,
'name' => $shop_name,
'item' => $shop_url,
],
[
'@type' => 'ListItem',
'position' => 3,
'name' => $cat_name,
'item' => $cat_url,
],
[
'@type' => 'ListItem',
'position' => 4,
'name' => get_the_title(),
'item' => get_permalink(),
],
],
];
echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . '</script>' . "\n";
}Add to your child theme's functions.php. Outputs BreadcrumbList JSON-LD on single product pages automatically.
Missing schema faqpageQuick win
Add FAQPage schema markup to any page that contains a Frequently Asked Questions section so Google can display your answers as rich results directly in search.
On WooCommerce
- Install and activate the free 'Rank Math SEO' or 'Yoast SEO' plugin from the WordPress Plugin Directory — both include built-in FAQ schema blocks.
- For Rank Math: open any page in the WordPress block editor (Gutenberg), add a 'Rank Math FAQ Block' from the block inserter, type your questions and answers directly into the block — Rank Math outputs the FAQPage JSON-LD automatically.
- For Yoast SEO: open the page, add a 'Yoast FAQ' block via the block editor, fill in Q&A pairs — Yoast injects the schema on save.
- If you prefer manual JSON-LD: install 'Insert Headers and Footers' plugin, go to Settings → Insert Headers and Footers, and paste your <script> block in the footer section (or use a per-page plugin like 'WP Custom Code Shortcode').
- Check output with Google's Rich Results Test.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Frequently Asked Questions | Your Store</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is your return policy?",
"acceptedAnswer": {
"@type": "Answer",
"text": "We accept returns within 30 days of purchase. Items must be unused and in their original packaging."
}
},
{
"@type": "Question",
"name": "How long does shipping take?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Standard shipping takes 5–7 business days. Expedited options are available at checkout."
}
}
]
}
</script>
</head>
<body>
<h1>Frequently Asked Questions</h1>
<h2>What is your return policy?</h2>
<p>We accept returns within 30 days of purchase. Items must be unused and in their original packaging.</p>
<h2>How long does shipping take?</h2>
<p>Standard shipping takes 5–7 business days. Expedited options are available at checkout.</p>
</body>
</html>Full HTML page example showing exactly where to place the FAQPage script block inside the <head>.
Missing schema howtoModerate effort
Add HowTo structured data (JSON-LD) to any page that walks customers through a multi-step process, so Google can display your instructions as rich results in search.
On WooCommerce
- Install a structured data plugin such as 'Rank Math SEO' or 'Schema Pro' from the WordPress plugin directory (Plugins > Add New).
- In Rank Math: edit the specific post or page, open the Rank Math sidebar panel > Schema tab, click 'Add New Schema', choose 'HowTo', and fill in the step-by-step form fields — no code required.
- In Schema Pro (Settings > Schema Pro > Add New): assign the HowTo schema type to the specific page or post type, then map fields to your content.
- If you prefer manual code: open your child theme's functions.php and use wp_head action to inject the JSON-LD only on the relevant page (use is_page() or is_single() conditional checks).
- Validate with Google's Rich Results Test after saving.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>How to Complete Your Task | Your Store</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to Complete Your Task",
"description": "Follow these steps to complete your task easily.",
"totalTime": "PT10M",
"step": [
{
"@type": "HowToStep",
"position": 1,
"name": "Prepare your materials",
"text": "Gather everything you need before you start: list materials here."
},
{
"@type": "HowToStep",
"position": 2,
"name": "Complete the main action",
"text": "Describe exactly what the user should do in this core step."
},
{
"@type": "HowToStep",
"position": 3,
"name": "Verify the result",
"text": "Explain how the user confirms the task was completed successfully."
}
]
}
</script>
</head>
<body>
<!-- Your how-to page content -->
</body>
</html>Place this block inside the <head> element of any static HTML page that contains step-by-step instructions.
Missing schema localbusinessQuick win
Add LocalBusiness JSON-LD structured data to your store so search engines and AI answer engines can surface your business name, address, phone number, and hours in rich results and direct answers.
On WooCommerce
- Install and activate the free plugin 'Yoast SEO' or 'Rank Math SEO' from the WordPress plugin directory (Plugins → Add New).
- For Yoast SEO: go to Yoast SEO → Settings → Site representation. Fill in your organization name, logo, and social profiles — Yoast auto-generates the LocalBusiness/Organization JSON-LD.
- For Rank Math: go to Rank Math → Titles & Meta → Local SEO and enable 'Local Business Schema'. Fill in your address, phone, and hours.
- Alternatively, paste a custom JSON-LD block via Appearance → Theme Editor → header.php just before </head>, or use a plugin like 'Insert Headers and Footers' (Settings → Insert Headers and Footers → Scripts in Header).
- Save and validate with Google's Rich Results Test.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Store</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Store",
"name": "Your Business Name",
"url": "https://www.yourdomain.com",
"logo": "https://www.yourdomain.com/logo.png",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main Street",
"addressLocality": "Springfield",
"addressRegion": "IL",
"postalCode": "62701",
"addressCountry": "US"
},
"telephone": "+1-217-555-0100",
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "09:00",
"closes": "18:00"
}
]
}
</script>
</head>
<body>
<!-- page content -->
</body>
</html>Paste this block anywhere inside the <head> element of your HTML page. Replace all placeholder values before deploying.
Missing schema organizationQuick win
Add an Organization schema markup block to your store's homepage (and/or sitewide template) so search engines and AI assistants can definitively identify your brand.
On WooCommerce
- Install the free plugin 'Rank Math SEO' or 'Yoast SEO' — both have built-in Organization schema settings that auto-generate the markup.
- For Rank Math: go to Rank Math → Titles & Meta → Local SEO, set Knowledge Graph type to 'Organization', fill in your name, logo, and social profiles, then Save.
- For Yoast: go to Yoast SEO → Settings → Site representation, choose 'Organization', fill in name, logo, and social profiles, then Save.
- If you prefer manual control, go to Appearance → Theme File Editor → open your active theme's functions.php and add a wp_head hook that outputs the JSON-LD script (see code snippet below).
- Verify output using the Rich Results Test on your homepage URL.
<?php
function acme_organization_schema() {
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'Organization',
'name' => 'Acme Store',
'url' => 'https://www.acmestore.com',
'logo' => array(
'@type' => 'ImageObject',
'url' => 'https://www.acmestore.com/images/logo.png',
'width' => 250,
'height' => 250,
),
'sameAs' => array(
'https://www.facebook.com/acmestore',
'https://www.instagram.com/acmestore',
'https://twitter.com/acmestore',
'https://www.linkedin.com/company/acmestore',
),
'contactPoint' => array(
'@type' => 'ContactPoint',
'contactType' => 'customer service',
'email' => 'hello@acmestore.com',
),
);
echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . '</script>' . PHP_EOL;
}
add_action( 'wp_head', 'acme_organization_schema' );Add to your active child theme's functions.php. Rank Math / Yoast generate this automatically; use this snippet only if you prefer manual control.
Missing schema productModerate effort
Add Product schema markup (JSON-LD) to every product page so search engines can display rich results — including price, availability, and ratings — directly in search.
On WooCommerce
- Install the free 'Rank Math SEO' plugin or 'Schema & Structured Data for WP & AMP' plugin from the WordPress plugin repository (Plugins → Add New, search the plugin name).
- For Rank Math: go to Rank Math → Titles & Meta → Products, ensure 'Schema Type' is set to 'Product'. Rank Math auto-populates all required fields from your WooCommerce product data.
- For manual control: add the JSON-LD snippet to your child theme's 'woocommerce/single-product.php' template inside the wp_head() hook, using PHP variables like get_the_title(), $product->get_price(), $product->get_sku().
- Check the output on a live product page using Google's Rich Results Test.
Missing schema webpageModerate effort
Add WebPage schema markup to every page so search engines and AI answer engines can precisely understand and describe your content.
On WooCommerce
- Install the free 'Yoast SEO' plugin (or 'Rank Math SEO') — both automatically generate WebPage/WebSite schema across all page types with no code required.
- For Yoast: go to SEO → Search Appearance, and ensure Schema settings are enabled (they are by default). Yoast outputs context-aware schema (WebPage, ItemPage, ProductPage, etc.) on every page automatically.
- For Rank Math: go to Rank Math → Titles & Meta → set the Schema Type per post type (Page, Post, Product). Rank Math outputs WebPage schema globally.
- To add custom or supplemental JSON-LD without a plugin, open your child theme's header.php and paste the JSON-LD block inside the <head> section, using PHP to echo dynamic values: <?php echo get_the_title(); ?> and <?php echo get_permalink(); ?>.
- Validate with the Rich Results Test after saving.
<?php
function mytheme_webpage_schema() {
if ( is_product() || is_shop() ) {
return; // Product/shop pages use Product or CollectionPage schema instead
}
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'WebPage',
'name' => get_the_title(),
'description' => get_the_excerpt() ? get_the_excerpt() : get_bloginfo( 'description' ),
'url' => get_permalink(),
'inLanguage' => get_bloginfo( 'language' ),
'isPartOf' => array(
'@type' => 'WebSite',
'name' => get_bloginfo( 'name' ),
'url' => home_url( '/' ),
),
);
echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT ) . '</script>' . "\n";
}
add_action( 'wp_head', 'mytheme_webpage_schema' );Add this to your child theme's functions.php to inject WebPage schema in the <head> on all non-product, non-shop pages (Yoast/Rank Math handle those). Remove if you use an SEO plugin that already outputs WebPage schema.
Non iso dateQuick win
Change every datePublished (and dateModified) value in your structured data from a human-readable string to a strict ISO-8601 date — formatted as YYYY-MM-DD or YYYY-MM-DDThh:mm:ss+hh:mm — so search engines can reliably parse your content's freshness.
On WooCommerce
- Install or open the Yoast SEO or Rank Math plugin (both auto-generate schema with correct ISO-8601 dates if configured properly).
- In Yoast SEO: go to SEO → Search Appearance → Content Types and ensure schema is enabled; Yoast outputs dates in ISO-8601 automatically from WordPress post meta.
- If you have custom JSON-LD in a child theme, open Appearance → Theme File Editor → find functions.php or the file where you call wp_head() with schema output.
- Replace any PHP date output like get_the_date() (which returns a human-readable string) with get_the_date('c') — the 'c' format code returns a full ISO-8601 string, e.g. 2025-03-12T09:00:00+00:00.
- For dateModified, use get_the_modified_date('c').
- Clear any caching plugin cache, then verify with Google's Rich Results Test.
<?php
add_action( 'wp_head', 'my_iso_date_schema' );
function my_iso_date_schema() {
if ( ! is_singular() ) return;
$published = get_the_date( 'c' ); // ISO-8601: 2025-03-12T09:00:00+00:00
$modified = get_the_modified_date( 'c' );
$title = esc_js( get_the_title() );
$url = esc_url( get_permalink() );
echo '<script type="application/ld+json">' . PHP_EOL;
echo '{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "' . $title . '",
"url": "' . $url . '",
"datePublished": "' . $published . '",
"dateModified": "' . $modified . '"
}';
echo PHP_EOL . '</script>';
}Add to your child theme's functions.php or a custom plugin to inject correct schema into single post/page heads.
Org missing sameasQuick win
Add a `sameAs` array to your Organization schema, linking to your verified social profiles, Wikipedia page, LinkedIn, and other authoritative sources so search engines can confidently identify your brand as a known entity.
On WooCommerce
- Install the free 'Rank Math SEO' or 'Yoast SEO' plugin if not already installed (both handle Organization schema natively).
- For Rank Math: go to WordPress Admin → Rank Math → Titles & Meta → Local SEO tab. Fill in all social profile URLs; Rank Math automatically outputs them as sameAs in the Organization schema.
- For Yoast SEO: go to WordPress Admin → Yoast SEO → Social. Enter the URLs for Facebook, Twitter/X, Instagram, LinkedIn, YouTube, Pinterest, and Wikipedia. Yoast automatically injects these as sameAs in its Organization schema output.
- If you prefer manual control, add a custom JSON-LD snippet via your child theme's functions.php using wp_head action (see code snippet below) or use the 'Insert Headers and Footers' plugin to paste the JSON-LD into the site-wide header.
- Validate with Google's Rich Results Test.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Your Brand Name",
"url": "https://www.yourdomain.com",
"logo": "https://www.yourdomain.com/images/logo.png",
"sameAs": [
"https://www.linkedin.com/company/your-company",
"https://www.facebook.com/yourpage",
"https://twitter.com/yourhandle",
"https://www.instagram.com/yourhandle",
"https://www.youtube.com/@yourchannel",
"https://en.wikipedia.org/wiki/Your_Brand",
"https://www.wikidata.org/wiki/QXXXXXXX"
]
}
</script>Paste this directly before the closing </head> tag in your HTML template. Remove any sameAs entries for platforms your brand does not have a presence on.
Page unreachableModerate effort
Ensure every page that should appear in AI-powered answers and rich results is actually reachable by crawlers, then add structured data so search engines and AI systems can read and surface its content.
On WooCommerce
- In WordPress admin, go to Pages / Products and confirm the post status is 'Published' and visibility is 'Public' (not 'Password protected' or 'Private').
- Check your Yoast SEO or Rank Math plugin settings: SEO → Search Appearance → confirm the post type is not set to 'noindex'. Also check individual page meta under the SEO tab on each page.
- Review Settings → Reading — ensure 'Discourage search engines from indexing this site' is NOT checked (this sets a sitewide noindex/robots block).
- For redirect management, use the Redirection plugin (Tools → Redirection) to create 301 redirects from broken URLs to correct live pages.
- To add or fix structured data, install Yoast SEO (includes Product and breadcrumb schema automatically for WooCommerce), or use a dedicated plugin like 'Schema Pro' or 'Rank Math' for FAQPage and additional schema types.
- After fixing, submit in Google Search Console and validate with the Rich Results Test.
Accessibility (WCAG) · 66 fixes
Aria allowed attrModerate effort
Audit every element that uses ARIA attributes and ensure each attribute is permitted for that element's role — removing or correcting any mismatched aria-* attributes.
On WooCommerce
- Log in to WordPress admin → Appearance → Theme File Editor (or use a code editor via SFTP/SSH for a child theme).
- Search your active child theme's template files (header.php, woocommerce/ overrides, and any custom page-builder shortcode output) for the flagged aria-* attribute.
- If the violation comes from a plugin (e.g. a slider, mega-menu, or review plugin), check that plugin's settings for an 'accessibility' or 'ARIA' option first; otherwise contact the plugin author.
- Edit the template or plugin file to remove or correct the mismatched attribute, then save.
- Install the free 'Accessibility Checker' plugin (Equalize Digital) to run a site-wide scan and confirm the violation is resolved.
- Test with the axe browser extension on the live page to verify.
<?php
/**
* Product card add-to-cart button with valid ARIA.
* Override: yourchildtheme/woocommerce/content-product.php
* Corrected: aria-label on <a> (allowed); no disallowed aria-* for this role.
*/
?>
<a
href="<?php echo esc_url( $product->add_to_cart_url() ); ?>"
class="button add_to_cart_button"
aria-label="<?php echo esc_attr( sprintf( __( 'Add %s to cart', 'woocommerce' ), $product->get_name() ) ); ?>"
rel="nofollow"
>
<?php echo esc_html( $product->add_to_cart_text() ); ?>
</a>Place this in your child theme's woocommerce/content-product.php template override to ensure product card buttons use valid ARIA.
Aria allowed roleQuick win
Remove or replace any ARIA `role` attribute that is not permitted on the HTML element it is applied to — in particular, do not add `role="textbox"` (or other interactive roles) to elements that already have a native semantic role which conflicts with it.
On WooCommerce
- The markup likely lives in your active theme or a plugin. Use your browser's DevTools Inspector to identify which template file renders the element.
- For theme files: in WordPress Admin go to Appearance → Theme File Editor (or edit files via FTP/SFTP). Find the relevant template (e.g. `woocommerce/templates/`, or your child theme's override of it).
- For plugin-rendered markup: either use a child theme override of the plugin template, or use the `wp_nav_menu`, `the_content`, or appropriate filter/action hook to modify the output.
- Apply the fix: remove or replace the invalid `role` attribute, or replace the element with the correct native HTML tag.
- Save and verify with axe DevTools or the WAVE browser extension.
<?php
// Before (invalid): <input type="text" role="textbox" ... />
// The role="textbox" attribute is redundant AND can be
// flagged as invalid depending on context. Remove it.
// After (correct): use the native <input> without an overriding role
?>
<p class="form-row form-row-first">
<label for="billing_first_name">
<?php esc_html_e( 'First name', 'woocommerce' ); ?>
<abbr class="required" title="required">*</abbr>
</label>
<input type="text"
class="input-text"
name="billing_first_name"
id="billing_first_name"
placeholder=""
value="<?php echo esc_attr( $field['value'] ?? '' ); ?>">
</p>Example PHP template override in your child theme (e.g. woocommerce/form-billing.php) — remove the invalid role.
Aria command nameModerate effort
Give every button, link, and menuitem a clear, accessible name so screen readers can announce what it does.
On WooCommerce
- In WordPress admin, go to Appearance → Theme File Editor (or use a child theme and edit files via FTP/SFTP).
- Identify the template file rendering the flagged element: common files are `header.php`, `functions.php` partial includes, or block template HTML files in block-based themes.
- Find the `<button>`, `<a>`, or ARIA-role element without text.
- Add `aria-label="Descriptive action name"` to the tag, OR wrap the inner text/icon in `<span class="screen-reader-text">Label text</span>` — WooCommerce and most WP themes already include a `.screen-reader-text` CSS class that hides text visually but keeps it accessible.
- Alternatively, install the 'WP Accessibility' plugin (by Joe Dolson) which automatically adds accessible labels to common WooCommerce controls like cart and search buttons.
- Save, clear any caching plugins, and re-run axe to verify.
<?php
// Icon-only cart link with accessible name — WooCommerce pattern.
// Place in header.php or a custom header template partial.
?>
<a
class="cart-contents"
href="<?php echo esc_url( wc_get_cart_url() ); ?>"
title="<?php esc_attr_e( 'View your shopping cart', 'your-theme-textdomain' ); ?>"
aria-label="<?php
/* translators: %d: number of items in cart */
printf( esc_attr__( 'Shopping cart: %d items', 'your-theme-textdomain' ), WC()->cart->get_cart_contents_count() );
?>"
>
<span class="cart-icon" aria-hidden="true"></span>
<span class="screen-reader-text">
<?php
printf(
/* translators: %d: number of items in cart */
esc_html__( 'Shopping cart (%d items)', 'your-theme-textdomain' ),
WC()->cart->get_cart_contents_count()
);
?>
</span>
</a>Add to your child theme's header.php or a custom partial. Uses the WP/WooCommerce screen-reader-text class.
Aria dialog nameModerate effort
Add a visible or programmatic accessible name to every dialog element so screen readers can announce what the dialog is about when it opens.
On WooCommerce
- Identify which plugin or theme generates the dialog: common sources are WooCommerce core (mini-cart drawer), popup plugins (e.g., Elementor Popup, Ninja Popups), or your theme (e.g., Storefront, Flatsome).
- For theme-generated modals, go to Appearance → Theme File Editor (or use a child theme) and locate the template file rendering the dialog (e.g., header.php, woocommerce/cart/mini-cart.php).
- Add a unique id to the dialog's heading and add aria-labelledby="that-id" to the element with role="dialog"; or add aria-label="Mini cart" if no heading exists.
- For plugin-generated popups, check the plugin's settings for an 'Accessibility' or 'ARIA label' field; if none exists, add the attribute via a small JavaScript snippet in Appearance → Customize → Additional CSS/JS or in your child theme's functions.php using wp_add_inline_script.
- Clear any caching plugins (e.g., WP Rocket, W3 Total Cache) and verify with a screen reader or axe browser extension.
<!-- Preferred: point to the dialog's own heading -->
<div role="dialog" aria-modal="true" aria-labelledby="cart-dialog-title">
<h2 id="cart-dialog-title">Shopping Cart</h2>
<p>You have 3 items in your cart.</p>
<button type="button">Close</button>
</div>
<!-- Alternative: use aria-label when there is no visible heading -->
<div role="dialog" aria-modal="true" aria-label="Shopping Cart">
<p>You have 3 items in your cart.</p>
<button type="button">Close</button>
</div>Preferred method: aria-labelledby points to the dialog's own heading. Use this whenever a visible title exists inside the dialog.
Aria hidden focusModerate effort
Remove `aria-hidden="true"` from any element that contains focusable children (links, buttons, inputs), or remove those focusable children from the keyboard tab order so they cannot receive focus while hidden from assistive technology.
On WooCommerce
- The aria-hidden issue usually originates in your WordPress theme or a plugin (slider, mega-menu, cookie banner, or page builder). Start by identifying the source: run axe DevTools in Chrome and note the HTML element flagged.
- If the issue is in your theme: go to Appearance → Theme File Editor (or use a child theme) and locate the template file rendering the offending element (commonly header.php, functions.php, or a template-parts/ file).
- If the issue is in a plugin (e.g., a slider or off-canvas menu plugin), check the plugin's settings panel for an 'Accessibility' or 'ARIA' toggle first. If none exists, use a child theme's functions.php to enqueue a small JavaScript fix (see code snippet below).
- Apply the fix in JavaScript: when a panel/drawer is hidden, set aria-hidden='true' on the container AND loop through its focusable children to set tabindex='-1'; reverse on open. Add this script to your child theme's js/ folder and enqueue it in functions.php with wp_enqueue_script().
- Install the 'WP Accessibility' plugin (by Joe Dolson) as a complement — it patches several common ARIA mistakes in popular themes automatically.
- Re-run Lighthouse or axe in Chrome DevTools on your storefront to confirm the violation is gone.
Aria input field nameModerate effort
Add a descriptive accessible name (aria-label, aria-labelledby, or a visible <label>) to every ARIA input field so assistive technologies can identify it.
On WooCommerce
- Install and activate the free WP accessibility plugin (e.g. 'WP Accessibility' by Joe Dolson) to catch common label issues automatically.
- For theme-level fixes, navigate in WordPress Admin to Appearance → Theme File Editor (or use a child theme) and locate the relevant template: woocommerce/templates/cart/cart.php (quantity inputs), woocommerce/templates/global/form-login.php, or your theme's search form (searchform.php).
- To override WooCommerce templates safely, copy the file from /wp-content/plugins/woocommerce/templates/ into /wp-content/themes/your-child-theme/woocommerce/ preserving the folder structure, then edit that copy.
- In the quantity input template (form-quantity-input.php or inside cart.php), find <input type="number" ...> and add: aria-label="<?php printf( esc_attr__( 'Quantity for %s', 'woocommerce' ), $product_name ); ?>"
- For the site search bar, open searchform.php (or your theme's search widget template) and ensure the <input type="search"> has either a wrapping <label> or aria-label="Search".
- Update and verify with the WAVE browser extension or axe DevTools.
<?php
$product_name = $cart_item['data']->get_name();
?>
<label for="quantity_<?php echo esc_attr( $cart_item_key ); ?>" class="screen-reader-text">
<?php printf( esc_html__( 'Quantity for %s', 'woocommerce' ), $product_name ); ?>
</label>
<input
type="number"
id="quantity_<?php echo esc_attr( $cart_item_key ); ?>"
class="input-text qty text"
name="cart[<?php echo esc_attr( $cart_item_key ); ?>][qty]"
value="<?php echo esc_attr( $cart_item['quantity'] ); ?>"
min="0"
step="1"
aria-label="<?php printf( esc_attr__( 'Quantity for %s', 'woocommerce' ), esc_attr( $product_name ) ); ?>"
>Place in your child theme at woocommerce/templates/cart/cart.php (override). Find the quantity input and replace with this.
Aria meter nameQuick win
Add a descriptive accessible name (via aria-label, aria-labelledby, or a visible <label>) to every element with role="meter" so screen readers can announce what the meter represents.
On WooCommerce
- In WordPress admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SSH).
- Find the template file rendering the meter widget — often woocommerce/templates/single-product/rating.php or a custom block in your page builder.
- If the meter is output by a plugin (e.g., a reviews plugin), check the plugin's settings for an accessibility label option first before editing templates.
- Add aria-label="Average customer rating" (or the appropriate description) to the role="meter" element. For the native <meter> tag, also wrap it: <label for="meter-id" class="screen-reader-text">Average customer rating</label><meter id="meter-id" ...>.
- Add or verify aria-valuenow, aria-valuemin, and aria-valuemax attributes match the displayed values.
- Use the axe DevTools browser extension on the frontend to verify the fix.
<?php
// Child theme: woocommerce/single-product/rating.php
// (Copy original from woocommerce/templates/single-product/rating.php first)
global $product;
$rating_count = $product->get_rating_count();
$average = $product->get_average_rating();
if ( $rating_count > 0 ) : ?>
<div class="woocommerce-product-rating">
<meter
role="meter"
aria-label="<?php esc_attr_e( 'Average customer rating', 'woocommerce' ); ?>"
aria-valuenow="<?php echo esc_attr( $average ); ?>"
aria-valuemin="0"
aria-valuemax="5"
value="<?php echo esc_attr( $average ); ?>"
min="0"
max="5"
class="star-rating">
<?php printf( esc_html__( '%s out of 5', 'woocommerce' ), $average ); ?>
</meter>
</div>
<?php endif; ?>Use in a child theme override of woocommerce/templates/single-product/rating.php or in a custom function.
Aria progressbar nameQuick win
Add an accessible name (aria-label or aria-labelledby) to every progress bar element so screen readers can announce its purpose.
On WooCommerce
- Progress bars in WooCommerce most commonly appear in: the checkout multi-step plugin templates, review/rating bars (theme or plugin), password-strength meters (wp-login.php and My Account page), and shipping progress bars.
- For theme-generated bars: in wp-admin go to Appearance → Theme File Editor (or use a child theme). Locate the template file rendering the bar — often woocommerce/templates/single-product/review.php, or a theme partial like template-parts/progress.php.
- Add aria-label='…' directly to the role='progressbar' or <progress> element in that template.
- For the built-in WooCommerce password-strength meter, the element is rendered by the WooCommerce JS. Override it by enqueuing a small JavaScript snippet in your child theme's functions.php that adds the aria-label attribute after DOM ready: document.querySelectorAll('.woocommerce-password-strength').forEach(el => el.setAttribute('aria-label', 'Password strength')).
- For plugin-generated bars (e.g. WooCommerce Checkout Steps, YITH plugins), check the plugin's settings for label options first; if none exist, add the attribute via a JS snippet in Appearance → Customize → Additional CSS/JS or in functions.php.
- Verify with the axe DevTools browser extension.
<?php
// Add to child-theme functions.php
add_action( 'wp_footer', 'mytheme_label_progressbars' );
function mytheme_label_progressbars() {
?>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Label WooCommerce password-strength meter
var pwStrength = document.querySelector('.woocommerce-password-strength');
if (pwStrength && !pwStrength.getAttribute('aria-label')) {
pwStrength.setAttribute('role', 'progressbar');
pwStrength.setAttribute('aria-label', 'Password strength');
pwStrength.setAttribute('aria-valuenow', '0');
pwStrength.setAttribute('aria-valuemin', '0');
pwStrength.setAttribute('aria-valuemax', '4');
}
// Label any other unlabelled progress bars on the page
document.querySelectorAll('[role="progressbar"]').forEach(function(el) {
if (!el.getAttribute('aria-label') && !el.getAttribute('aria-labelledby')) {
el.setAttribute('aria-label', 'Progress');
}
});
});
</script>
<?php
}Add this JavaScript via wp_footer hook in your child theme's functions.php to label WooCommerce's password-strength meter and any other unlabelled progress bars.
Aria prohibited attrModerate effort
Remove or replace any ARIA attributes that are prohibited for the role assigned to each HTML element, so assistive technologies receive accurate, unambiguous information.
On WooCommerce
- Access your WordPress Admin → Appearance → Theme File Editor (or use a plugin like Advanced Code Editor for syntax highlighting).
- Locate the theme file containing the flagged element — typically header.php, content-product.php, or a block template in /templates/.
- Search for the prohibited ARIA attribute and correct it as described in the generic steps.
- If the attribute is injected by a plugin (e.g. a slider or mega-menu plugin), check that plugin's settings panel or custom CSS/JS area for the offending markup, or contact the plugin author.
- Save, then validate with the axe DevTools browser extension on the frontend.
<?php
/**
* content-product.php override in child theme
* Corrects a prohibited aria-checked attribute on the product title heading.
*/
defined( 'ABSPATH' ) || exit;
?>
<li <?php wc_product_class( '', $product ); ?>>
<?php do_action( 'woocommerce_before_shop_loop_item' ); ?>
<a href="<?php the_permalink(); ?>">
<?php
/**
* Before fix (prohibited attribute):
* <h2 class="woocommerce-loop-product__title" aria-checked="false">
*
* After fix: remove aria-checked entirely — it is prohibited on headings.
*/
?>
<h2 class="woocommerce-loop-product__title">
<?php the_title(); ?>
</h2>
<?php woocommerce_template_loop_price(); ?>
</a>
<?php do_action( 'woocommerce_after_shop_loop_item' ); ?>
</li>Example override template at wp-content/themes/your-child-theme/woocommerce/content-product.php — correct a prohibited ARIA attribute on a product card heading.
Aria required attrModerate effort
Add all required ARIA attributes (such as aria-expanded and aria-controls) to every interactive element that uses an ARIA role, so assistive technologies can communicate widget state to users.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- Locate the template file for the affected component — e.g. header.php, functions.php-registered navigation menus, or a block template in /templates/.
- Add aria-expanded='false' and aria-controls='[target-id]' to the relevant HTML element in the template.
- If the toggle behaviour comes from a plugin (e.g. a mega-menu or accordion plugin), check the plugin's settings panel for built-in ARIA options first; if none exist, enqueue a small custom JS file via functions.php that patches the attribute on page load and toggle.
- Alternatively, install the 'WP Accessibility' plugin (by Joe Dolson), which patches common ARIA issues in nav menus automatically.
- Test with the axe DevTools browser extension.
<?php
/**
* Enqueue ARIA toggle fix for nav menus.
* Add this to your child theme's functions.php
*/
function mytheme_enqueue_aria_fix() {
wp_enqueue_script(
'mytheme-aria-fix',
get_stylesheet_directory_uri() . '/js/aria-fix.js',
array(),
'1.0.0',
true // load in footer
);
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_aria_fix' );Add to your child theme's functions.php to enqueue a small JS fix for nav menu toggles.
Aria required childrenModerate effort
Add the required child ARIA roles (e.g. `role="option"` inside `role="listbox"`, or `role="group"`/`role="option"` inside `role="combobox"`) so every composite ARIA widget contains the child roles the ARIA specification demands.
On WooCommerce
- In WordPress Admin go to Appearance → Theme File Editor (or use a local editor / SFTP).
- Open the child theme file that renders the broken widget — typically woocommerce/content-single-product.php, woocommerce/global/form-row.php, or a plugin's template override in /wp-content/plugins/<plugin>/templates/.
- Locate the container element with the incorrect role and add the required child roles to its children (e.g. add role="option" to <li> items inside a role="listbox" wrapper).
- Always edit from a child theme so WooCommerce updates do not overwrite your changes.
- Alternatively, install an accessibility plugin such as 'WP Accessibility' to audit issues, but note that automated fixes from plugins rarely fix custom widget ARIA trees — manual template edits are usually required.
- Test with the axe browser extension and a screen reader after saving.
Aria required parentModerate effort
Wrap every ARIA role that requires a parent container (e.g. `menuitem`, `option`, `listitem`) inside the correct parent element carrying the matching ARIA role (e.g. `menu`/`menubar`, `listbox`, `list`).
On WooCommerce
- In WordPress Admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- Identify the template file rendering the broken component — typically 'header.php', 'navigation.php', or a plugin's template partial. Check WooCommerce plugin templates under 'woocommerce/templates/'.
- Wrap the orphaned child-role elements in the correct parent element with the required ARIA role. Use a child theme override: copy the template to 'your-child-theme/woocommerce/' so updates don't overwrite your fix.
- If a plugin (e.g. a mega-menu plugin) generates the broken markup, check the plugin's settings for ARIA options, or contact the plugin author; as a last resort, use a JavaScript patch enqueued in 'functions.php' to inject the parent wrapper after DOM load.
- Test with the axe DevTools browser extension and verify with a screen reader.
<?php
/**
* Custom nav walker for correct ARIA role hierarchy.
* Add this class to your child theme's functions.php or a separate file included there.
*/
class ARIA_Nav_Walker extends Walker_Nav_Menu {
public function start_lvl( &$output, $depth = 0, $args = null ) {
$role = ( $depth === 0 ) ? 'menubar' : 'menu';
$output .= "\n<ul role=\"$role\">\n";
}
public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
$has_children = in_array( 'menu-item-has-children', $item->classes );
$output .= '<li role="none">';
$output .= '<a href="' . esc_url( $item->url ) . '" role="menuitem"';
if ( $has_children ) {
$output .= ' aria-haspopup="true" aria-expanded="false"';
}
$output .= '>' . esc_html( $item->title ) . '</a>';
}
public function end_el( &$output, $item, $depth = 0, $args = null ) {
$output .= "</li>\n";
}
public function end_lvl( &$output, $depth = 0, $args = null ) {
$output .= "</ul>\n";
}
}
// In your header.php, call the menu with the walker:
// wp_nav_menu( [ 'theme_location' => 'primary', 'walker' => new ARIA_Nav_Walker() ] );
?>Place in your child theme's header.php or a custom nav walker. Register the walker in functions.php.
Aria rolesQuick win
Audit every element that has a `role` attribute and replace any invalid or misspelled role value with a valid WAI-ARIA role from the official specification.
On WooCommerce
- In WordPress Admin, go to Appearance → Theme File Editor (or use a child theme and edit via FTP/SFTP).
- Search your active theme's `.php` template files and `functions.php` for `role=` — common locations are `header.php`, `footer.php`, page templates, and WooCommerce template overrides in `woocommerce/` within your theme folder.
- Edit the file containing the invalid role, correct the value (or remove the attribute), and click Update File.
- If the role is injected by a plugin, search for the offending plugin in wp-content/plugins/ and either configure it, update it, or contact its developer.
- Clear any caching plugins (e.g. WP Rocket, W3 Total Cache) after saving.
- Re-run axe or WAVE on the affected page to confirm the fix.
<?php
/**
* Child theme WooCommerce template override:
* woocommerce/content-product.php
*
* BEFORE (invalid role):
* <li <?php wc_product_class( '', $product ); ?> role="prodcard">
*
* AFTER: use a valid ARIA role or remove the role attribute entirely.
* The native <li> element already has implicit list-item semantics.
*/
?>
<li <?php wc_product_class( '', $product ); ?> aria-label="<?php echo esc_attr( $product->get_name() ); ?>">
<?php do_action( 'woocommerce_before_shop_loop_item' ); ?>
<?php do_action( 'woocommerce_before_shop_loop_item_title' ); ?>
<?php do_action( 'woocommerce_shop_loop_item_title' ); ?>
<?php do_action( 'woocommerce_after_shop_loop_item_title' ); ?>
<?php do_action( 'woocommerce_after_shop_loop_item' ); ?>
</li>Example correction inside a WooCommerce template override (e.g. woocommerce/content-product.php in your child theme):
Aria toggle field nameQuick win
Add a visible or programmatic accessible name to every toggle control (checkbox, switch, or button with role="switch") so screen readers can announce what the control does.
On WooCommerce
- Access your WordPress Admin dashboard and go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- Search your theme's template files (especially header.php, footer.php, woocommerce/ overrides, and any page-builder shortcodes) for checkbox inputs or custom toggle markup.
- For each unlabelled checkbox, add a matching <label for="INPUT_ID">Descriptive text</label> or wrap both elements in a <label>.
- For icon-only switches (e.g. in mega menus or cookie plugins), add aria-label="Description" to the <button> or toggle element.
- If a plugin (e.g. a cookie consent or wishlist plugin) generates the unlabelled toggle, check the plugin's settings for an accessible label option, or contact the plugin developer. As a workaround, use a small Custom HTML block or a site-wide CSS+JS snippet via Appearance → Customize → Additional CSS / Custom JS plugin to inject the aria-label.
- Test using the axe DevTools browser extension or the WordPress Accessibility Checker plugin.
<?php
/**
* Accessible checkbox in a WooCommerce template override.
* Place this in your child theme, e.g.:
* /wp-content/themes/your-child-theme/woocommerce/myaccount/form-login.php
* (copy the original file from woocommerce/templates/ first, then edit)
*/
?>
<p class="woocommerce-form__label woocommerce-form__label-for-checkbox inline">
<input
class="woocommerce-form__input woocommerce-form__input-checkbox"
name="rememberme"
type="checkbox"
id="rememberme"
value="forever"
/>
<label for="rememberme"><?php esc_html_e( 'Remember me', 'woocommerce' ); ?></label>
</p>PHP template override — add to your child theme at woocommerce/myaccount/ or wherever the unlabelled checkbox lives.
Aria tooltip nameModerate effort
Give every tooltip element an accessible name so screen readers can announce its content to users who cannot see the screen.
On WooCommerce
- In WordPress admin, go to Appearance → Theme File Editor (or use a child theme and edit via FTP/SFTP).
- Search all theme PHP/HTML template files for role="tooltip" — common locations: woocommerce/templates/, your theme's inc/ folder, and any custom shortcode output.
- Edit the template to wrap tooltip content in descriptive text, or add aria-label="[description]" to the tooltip element.
- If the tooltip is from a plugin (e.g., WooCommerce Product Add-Ons, WPForms), check the plugin's settings for an accessibility label field, or use a Custom CSS/JS plugin to inject aria-label via JavaScript as a temporary fix.
- Install the free axe DevTools browser extension, reload the affected page, and run a scan to confirm the violation is resolved.
<?php
/**
* Render an accessible tooltip.
*
* @param string $trigger_label Screen-reader label for the trigger button.
* @param string $tooltip_text Visible + accessible tooltip content.
* @param string $tooltip_id Unique HTML id for this tooltip instance.
*/
function my_accessible_tooltip( $trigger_label, $tooltip_text, $tooltip_id ) {
$trigger_label = esc_attr( $trigger_label );
$tooltip_text = esc_html( $tooltip_text );
$tooltip_id = esc_attr( $tooltip_id );
echo '<span class="tooltip-wrapper">';
echo '<button type="button" class="tooltip-trigger" aria-describedby="' . $tooltip_id . '" aria-label="' . $trigger_label . '">';
echo '<span aria-hidden="true">ⓘ</span>'; // info symbol
echo '</button>';
echo '<span id="' . $tooltip_id . '" role="tooltip" class="tooltip-content">' . $tooltip_text . '</span>';
echo '</span>';
}
// Example usage inside a template:
// my_accessible_tooltip( 'Shipping information', 'Standard shipping: 2–3 business days.', 'ship-tooltip-1' );Add this function to your child theme's functions.php to output an accessible tooltip anywhere via do_action or a shortcode.
Aria valid attrQuick win
Find and replace every misspelled or non-existent ARIA attribute name (e.g. `aria-labeledby`) with the correct, spec-defined name (e.g. `aria-labelledby`) across your entire site's HTML and templates.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a code editor like VS Code connected via FTP/SFTP).
- Search your active theme's files (especially `header.php`, `footer.php`, `functions.php`, and any custom page templates) for the misspelled attribute using a global find (Ctrl+Shift+F in VS Code).
- Also search any active plugins' output — if the invalid attribute comes from a plugin, update the plugin first; if it persists, contact the plugin developer.
- Correct every instance of the misspelled attribute and save the file.
- If you use a page builder (Elementor, Divi, WPBakery), open the affected page in the builder and look for any custom HTML/code blocks containing the invalid attribute, then correct it there.
- Flush any caching plugin (e.g. WP Rocket, W3 Total Cache) after saving, then re-scan with axe DevTools.
<?php
/**
* WooCommerce add-to-cart loop button template (child theme override)
* Located at: wp-content/themes/your-child-theme/woocommerce/loop/add-to-cart.php
*
* WRONG: aria-labeledby is not valid
* <button aria-labeledby="product-title-<?php echo esc_attr( $product->get_id() ); ?>">
*
* CORRECT: aria-labelledby (double 'l')
*/
?>
<h2 class="woocommerce-loop-product__title" id="product-title-<?php echo esc_attr( $product->get_id() ); ?>">
<?php echo wp_kses_post( $product->get_name() ); ?>
</h2>
<a
href="<?php echo esc_url( $product->add_to_cart_url() ); ?>"
class="button add_to_cart_button"
aria-labelledby="product-title-<?php echo esc_attr( $product->get_id() ); ?>"
data-product_id="<?php echo esc_attr( $product->get_id() ); ?>"
>
<?php echo esc_html( $product->add_to_cart_text() ); ?>
</a>Example of correcting a misspelled ARIA attribute inside a WooCommerce PHP template override (e.g. woocommerce/templates/loop/add-to-cart.php copied to your child theme).
Aria valid attr valueModerate effort
Audit every ARIA attribute on your pages and correct any that have invalid or mismatched values so assistive technologies can interpret them correctly.
On WooCommerce
- In WordPress admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- Identify which template file renders the flagged element — common locations: `header.php`, `functions.php`, a block template in `/templates/`, or a page builder shortcode output.
- Search for the invalid ARIA attribute and correct its value in the template file.
- If the issue comes from a plugin (e.g. a slider, mega-menu, or product tabs plugin), go to the plugin's settings page or contact the plugin author — some plugins allow custom HTML/attribute overrides via hooks in `functions.php`.
- Use the WP Accessibility plugin (free, wordpress.org) to catch additional ARIA issues site-wide.
- Verify the fix with Lighthouse (Chrome DevTools → Lighthouse tab → Accessibility) or the axe browser extension.
<?php
/**
* Example: override ARIA attribute values on WooCommerce output
* using output buffering on a specific hook.
*
* Place this in your child theme's functions.php.
* Adjust the hook name and the attribute correction to match your specific issue.
*/
add_action( 'wp_footer', function() {
?>
<script>
// Correct any aria-expanded values that were set to integers instead of strings.
// Run after the DOM is ready.
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[aria-expanded]').forEach(function (el) {
var val = el.getAttribute('aria-expanded');
// Normalize numeric or non-boolean values to valid strings
if (val !== 'true' && val !== 'false') {
el.setAttribute('aria-expanded', val ? 'true' : 'false');
}
});
// Correct aria-describedby references pointing to non-existent IDs.
document.querySelectorAll('[aria-describedby]').forEach(function (el) {
var targetId = el.getAttribute('aria-describedby');
if (targetId && !document.getElementById(targetId)) {
// Remove the broken reference to avoid confusing screen readers.
el.removeAttribute('aria-describedby');
console.warn('Removed broken aria-describedby reference: ' + targetId);
}
});
});
</script>
<?php
}, 99 );Add to your child theme's functions.php to patch ARIA attribute values output by WooCommerce core or a plugin via a filter or action hook.
Autocomplete validModerate effort
Add valid `autocomplete` attribute values to every form field that collects personal information, using the exact tokens specified by the HTML standard.
On WooCommerce
- WooCommerce's core checkout and account forms include autocomplete attributes by default in recent versions. First update WooCommerce to the latest version to get these improvements.
- If you have a custom or third-party checkout plugin overriding the form fields, locate the plugin's template override in your child theme at `woocommerce/checkout/form-billing.php`, `form-shipping.php`, or `form-login.php`.
- Edit each `<input>` element to add the correct `autocomplete` attribute value.
- Alternatively, use the `woocommerce_form_field_args` PHP filter in your child theme's `functions.php` to programmatically inject `autocomplete` values (see code snippet).
- Clear any caching plugin's cache (e.g. WP Super Cache, W3 Total Cache, LiteSpeed Cache) after making changes.
- Validate with an axe browser extension on the checkout and My Account pages.
<?php
/**
* Add correct autocomplete attributes to WooCommerce checkout fields.
* Add this to your child theme's functions.php
*/
add_filter( 'woocommerce_checkout_fields', 'mytheme_add_autocomplete_to_checkout_fields' );
function mytheme_add_autocomplete_to_checkout_fields( $fields ) {
// Billing fields
$billing_map = array(
'billing_first_name' => 'billing given-name',
'billing_last_name' => 'billing family-name',
'billing_company' => 'billing organization',
'billing_address_1' => 'billing address-line1',
'billing_address_2' => 'billing address-line2',
'billing_city' => 'billing address-level2',
'billing_state' => 'billing address-level1',
'billing_postcode' => 'billing postal-code',
'billing_country' => 'billing country',
'billing_phone' => 'billing tel',
'billing_email' => 'email',
);
foreach ( $billing_map as $field_key => $autocomplete_value ) {
if ( isset( $fields['billing'][ $field_key ] ) ) {
$fields['billing'][ $field_key ]['autocomplete'] = $autocomplete_value;
}
}
// Shipping fields
$shipping_map = array(
'shipping_first_name' => 'shipping given-name',
'shipping_last_name' => 'shipping family-name',
'shipping_company' => 'shipping organization',
'shipping_address_1' => 'shipping address-line1',
'shipping_address_2' => 'shipping address-line2',
'shipping_city' => 'shipping address-level2',
'shipping_state' => 'shipping address-level1',
'shipping_postcode' => 'shipping postal-code',
'shipping_country' => 'shipping country',
);
foreach ( $shipping_map as $field_key => $autocomplete_value ) {
if ( isset( $fields['shipping'][ $field_key ] ) ) {
$fields['shipping'][ $field_key ]['autocomplete'] = $autocomplete_value;
}
}
// Account fields
if ( isset( $fields['account']['account_password'] ) ) {
$fields['account']['account_password']['autocomplete'] = 'new-password';
}
return $fields;
}Add to your child theme's functions.php to programmatically inject autocomplete values on WooCommerce checkout fields.
Avoid inline spacingModerate effort
Remove hard-coded text-spacing CSS values from inline `style` attributes so users can override them with their own stylesheets or browser settings.
On WooCommerce
- In WordPress admin, go to Appearance → Theme File Editor (or use a code editor via FTP/SFTP).
- Open the template file containing the inline styles — commonly `woocommerce/single-product.php` or a block template — and search for `style=`.
- Add your CSS class definitions to your child theme's `style.css` (never edit the parent theme directly).
- Replace each inline `style` attribute with the matching class name in the template.
- If the inline style is coming from the Block Editor (Gutenberg), select the block, go to Block settings → Advanced → Additional CSS class, and style it via `style.css`; remove any spacing values set in the block's Typography panel that output as inline styles.
- Use the axe DevTools browser extension to verify the issue is resolved after saving.
/* Add to child-theme/style.css — overrides any inline spacing on WooCommerce product descriptions */
.woocommerce div.product .woocommerce-product-details__short-description,
.woocommerce div.product #tab-description p {
line-height: 1.6;
letter-spacing: 0.04em;
word-spacing: 0.1em;
}Add to your child theme's style.css. Targets WooCommerce product description output.
Button nameModerate effort
Add a descriptive, screen-reader-accessible name to every button on your site so assistive technology users know what each button does.
On WooCommerce
- In WordPress Admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP to avoid losing changes on theme updates).
- Locate the template responsible for the button: single-product.php or woocommerce/templates/single-product/add-to-cart/ for product buttons; header.php or your theme's header partial for nav icon buttons.
- Add aria-label attributes to icon-only <button> elements. For WooCommerce core buttons (e.g. the cart icon widget), use a filter hook in your child theme's functions.php to modify output rather than editing plugin files directly.
- To modify WooCommerce's own button markup, copy the relevant template from wp-content/plugins/woocommerce/templates/ into wp-content/themes/your-child-theme/woocommerce/ (matching the folder structure) and edit the copy.
- For third-party slider or gallery plugins (Swiper, Slick) that render unlabelled prev/next arrows, configure their accessibility options in the plugin settings, or add aria-label via the plugin's button customization fields.
- Install the free 'WP Accessibility' plugin (by Joe Dolson) — it automatically adds aria-labels to some common WordPress patterns and provides a framework for further fixes.
<?php
/**
* Add aria-labels to WooCommerce quantity input buttons.
* Add this code to your child theme's functions.php
*/
add_filter( 'woocommerce_quantity_field_html', 'my_theme_accessible_quantity_html', 10, 5 );
function my_theme_accessible_quantity_html( $html, $input_name, $input_value, $args, $max_value ) {
// Replace generic minus/plus buttons with labelled versions
$product_title = isset( $args['product_name'] ) ? esc_attr( $args['product_name'] ) : 'product';
$html = preg_replace(
'/<button([^>]+class="[^"]*minus[^"]*")([^>]*)>/',
'<button$1$2 aria-label="Decrease quantity for ' . $product_title . '">',
$html
);
$html = preg_replace(
'/<button([^>]+class="[^"]*plus[^"]*")([^>]*)>/',
'<button$1$2 aria-label="Increase quantity for ' . $product_title . '">',
$html
);
return $html;
}
?>Add this to your child theme's functions.php to inject accessible labels on WooCommerce's quantity buttons via a filter, without editing plugin core files.
Color contrastQuick win
Increase the contrast ratio between your text color and its background color to meet the WCAG 4.5:1 minimum, so all users — including those with low vision — can read your content clearly.
On WooCommerce
- In WordPress admin go to Appearance → Customize → Additional CSS.
- Add a targeted override rule (see code snippet below) that sets the failing element's color to your accessible value. This avoids editing theme files directly.
- Alternatively, if your theme has a Colors panel inside the Customizer, update the body text or secondary text color picker there.
- For block-based (FSE) themes, go to Appearance → Editor → Styles (palette icon) → Typography or Colors and change the value.
- Click Publish and re-test contrast.
/* WooCommerce: fix low-contrast text in product descriptions and meta */
.woocommerce-product-details__short-description p,
.woocommerce-review__meta,
.product_meta .sku,
.woocommerce p {
color: #595959; /* was #777777 — ratio 7.0:1 on white */
}Paste into Appearance → Customize → Additional CSS in WordPress.
Definition listModerate effort
Ensure every `<dl>` (definition list) element contains only properly nested `<dt>` and `<dd>` children, with no stray text, divs, or other elements placed directly inside it.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme and edit files via FTP/SSH).
- Search your active (child) theme's template files for `<dl` — key files: woocommerce/single-product/tabs/additional-information.php (product attributes), woocommerce/order/order-details.php (order details).
- Edit the PHP/HTML in those templates to ensure the `<dl>` contains only `<dt>` and `<dd>` (or `<div>` wrappers) as direct children.
- If WooCommerce core templates are the source, copy the template file into your child theme under woocommerce/ and edit the copy (never edit plugin files directly).
- Install and run the free WP Accessibility plugin or axe DevTools browser extension to verify the fix after saving.
<?php
/**
* Correctly structured definition list for WooCommerce product additional information tab.
* Child theme override: woocommerce/single-product/tabs/additional-information.php
*/
defined( 'ABSPATH' ) || exit;
global $product;
$attributes = $product->get_attributes();
if ( ! $attributes ) {
return;
}
?>
<section class="woocommerce-product-attributes">
<h2><?php esc_html_e( 'Additional information', 'woocommerce' ); ?></h2>
<dl class="woocommerce-product-attributes-list">
<?php foreach ( $attributes as $attribute ) : ?>
<div class="woocommerce-product-attributes-item">
<dt class="woocommerce-product-attributes-item__label">
<?php echo esc_html( wc_attribute_label( $attribute->get_name() ) ); ?>
</dt>
<dd class="woocommerce-product-attributes-item__value">
<?php echo wp_kses_post( wc_get_product_attribute_value_string( $attribute, $product ) ); ?>
</dd>
</div>
<?php endforeach; ?>
</dl>
</section>Copy to your child theme at woocommerce/single-product/tabs/additional-information.php to override the core template.
DlitemModerate effort
Wrap every `<dt>` (term) and `<dd>` (description) element inside a parent `<dl>` element so description list items have their required container.
On WooCommerce
- In WordPress admin, go to Appearance → Theme File Editor (or use a child theme and edit files via FTP/SFTP).
- Search your active theme's PHP and template files for `<dt` or `<dd` — common locations are single-product.php, woocommerce/templates/single-product/tabs/, and any custom shortcode output.
- Also search any active plugins that render product attributes or FAQs, as they may output `<dt>`/`<dd>` tags.
- Wrap orphaned `<dt>`/`<dd>` elements with `<dl>` … `</dl>` in the appropriate template file.
- If you cannot edit plugin templates directly, use a child theme template override: copy the plugin template into your child theme under the same directory path and edit that copy.
- Clear any caching plugins (WP Rocket, W3 Total Cache, etc.) after saving, then re-validate.
<?php
/**
* Child theme override:
* woocommerce/templates/single-product/tabs/additional-information.php
* Ensures product attributes render as a correct <dl> structure.
*/
defined( 'ABSPATH' ) || exit;
global $product;
$heading = apply_filters( 'woocommerce_product_additional_information_heading', __( 'Additional information', 'woocommerce' ) );
if ( $heading ) {
echo '<h2>' . esc_html( $heading ) . '</h2>';
}
?>
<dl class="woocommerce-product-attributes">
<?php foreach ( $product->get_attributes() as $attribute ) : ?>
<?php if ( ! $attribute->get_visible() ) continue; ?>
<div class="woocommerce-product-attributes-item">
<dt class="woocommerce-product-attributes-item__label">
<?php echo wp_kses_post( wc_attribute_label( $attribute->get_name() ) ); ?>
</dt>
<dd class="woocommerce-product-attributes-item__value">
<?php
$values = array();
if ( $attribute->is_taxonomy() ) {
$attribute_taxonomy = $attribute->get_taxonomy_object();
$attribute_values = wc_get_product_terms( $product->get_id(), $attribute->get_name(), array( 'fields' => 'all' ) );
foreach ( $attribute_values as $attribute_value ) {
$values[] = esc_html( $attribute_value->name );
}
} else {
$values = $attribute->get_options();
foreach ( $values as &$value ) {
$value = esc_html( $value );
}
}
echo wp_kses_post( apply_filters( 'woocommerce_attribute', wpautop( wptexturize( implode( ', ', $values ) ) ), $attribute, $values ) );
?>
</dd>
</div>
<?php endforeach; ?>
</dl>Place in your child theme at woocommerce/templates/single-product/tabs/additional-information.php to override WooCommerce's product attributes template.
Document titleQuick win
Add a descriptive, non-empty `<title>` element to every page so browsers, screen readers, and search engines can identify it.
On WooCommerce
- Install and activate the Yoast SEO or Rank Math plugin if not already present.
- For Yoast SEO: go to SEO → Search Appearance → Content Types and set the title template for Products, Categories, and Pages using variables like `%%title%% – %%sitename%%`.
- For individual products: open the product edit screen → scroll to 'Yoast SEO' (or 'Rank Math SEO') meta box → 'SEO title' field → enter/edit the title.
- For pages/posts: same meta box on each edit screen.
- If managing titles directly in the theme: edit `header.php` in your child theme (Appearance → Theme File Editor) and ensure `<title><?php wp_title('|', true, 'right'); ?></title>` or `wp_head()` is present inside `<head>`.
- Never edit the parent theme directly — always use a child theme or the plugin's template override.
<title><?php
if ( is_front_page() ) {
echo get_bloginfo( 'name' ) . ' – ' . get_bloginfo( 'description' );
} elseif ( is_singular() || is_archive() ) {
echo wp_title( '–', false, 'right' ) . get_bloginfo( 'name' );
} else {
echo get_bloginfo( 'name' );
}
?></title>In your child theme's header.php, inside <head>. With Yoast SEO or Rank Math active, wp_head() outputs the title automatically — this snippet is for themes without a plugin.
Empty headingQuick win
Find every empty heading tag on your store and add visible, descriptive text (or remove the tag entirely) so screen readers and search engines can interpret your page structure.
On WooCommerce
- In WordPress Admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- Open the relevant template file (e.g. `archive-product.php`, `single-product.php`, `page.php`) and search for empty `<h1>`–`<h6>` tags.
- Add descriptive text directly, or wrap the heading in a PHP conditional: `<?php if ( $heading ) : ?><h2><?php echo esc_html( $heading ); ?></h2><?php endif; ?>`.
- For pages built with Elementor or other page builders: open the page editor, click the offending Heading widget, and type text into the 'Title' field — or delete the widget if it serves no purpose.
- For Gutenberg block themes (Full Site Editor): go to Appearance → Editor, open the template or template part, locate the Heading block, and add text or delete the block.
- Install the 'WP Accessibility' plugin (Toolbar → Tools) for ongoing detection of empty headings site-wide.
- Re-check with the axe DevTools browser extension.
<?php
/**
* Only output the heading when $heading contains text.
* Replace $heading with however your template exposes the value.
*/
if ( ! empty( $heading ) ) : ?>
<h2 class="woocommerce-loop-category__title">
<?php echo esc_html( $heading ); ?>
</h2>
<?php endif; ?>Drop into a child-theme template file (e.g. archive-product.php) where a heading variable might be empty.
Empty table headerQuick win
Add visible or screen-reader-accessible text to every empty `<th>` table header cell so assistive technology users can understand what each column or row represents.
On WooCommerce
- In your WordPress Admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- Search your active (child) theme's template files — especially woocommerce/templates/single-product/tabs/ and any comparison plugin templates — for empty `<th>` tags.
- If the empty header comes from a plugin (e.g., WooCommerce Product Table, YITH WooCommerce Compare), check that plugin's settings for a 'column header' or 'label' field before editing code.
- Add visible text or an `aria-label` to each empty `<th>` found, then save.
- Alternatively install the 'WP Accessibility' plugin (by Joe Dolson) which flags and sometimes auto-fixes empty header cells.
- Verify with axe DevTools in your browser.
<?php
/**
* Override WooCommerce additional information (attributes) table
* to ensure all <th> cells have accessible text.
*
* Copy woocommerce/templates/single-product/tabs/additional-information.php
* into your child theme at:
* yourchildtheme/woocommerce/single-product/tabs/additional-information.php
* then use the markup below.
*/
defined( 'ABSPATH' ) || exit;
global $product;
$heading = apply_filters( 'woocommerce_product_additional_information_heading', __( 'Additional information', 'woocommerce' ) );
?>
<?php if ( $heading ) : ?>
<h2><?php echo esc_html( $heading ); ?></h2>
<?php endif; ?>
<table class="woocommerce-product-attributes shop_attributes">
<thead>
<tr>
<!-- Both header cells now have visible, accessible text -->
<th class="woocommerce-product-attributes-item__label" scope="col"><?php esc_html_e( 'Attribute', 'woocommerce' ); ?></th>
<th class="woocommerce-product-attributes-item__value" scope="col"><?php esc_html_e( 'Details', 'woocommerce' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $product->get_attributes() as $attribute ) : ?>
<tr class="woocommerce-product-attributes-item woocommerce-product-attributes-item--attribute_<?php echo esc_attr( $attribute->get_name() ); ?>">
<td class="woocommerce-product-attributes-item__label"><?php echo wp_kses_post( wc_attribute_label( $attribute->get_name() ) ); ?></td>
<td class="woocommerce-product-attributes-item__value"><?php echo wp_kses_post( wc_get_product_attribute_table_cell_content( $product, $attribute ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>Add to your child theme's functions.php or a custom plugin to override WooCommerce's product attributes table template output.
Error not associatedModerate effort
Add `aria-invalid="true"` and a linked `aria-describedby` error message to every form field that fails validation, so screen reader users are told exactly what went wrong.
On WooCommerce
- Go to your WordPress admin → Appearance → Theme File Editor (or use a child theme and edit files via FTP/SSH).
- WooCommerce checkout fields are rendered by woocommerce/templates/checkout/form-checkout.php and individual field templates in woocommerce/templates/form-fields/.
- The recommended approach is to use WooCommerce's woocommerce_form_field filter in your child theme's functions.php to modify field output and inject aria-describedby and a sibling error span into each field's HTML.
- Alternatively, install the 'Accessible WooCommerce' plugin or a dedicated WooCommerce accessibility plugin that patches ARIA attributes automatically.
- For custom or page-builder checkout pages, add a small JavaScript snippet (via Appearance → Customize → Additional CSS/JS or a plugin like 'Code Snippets') that hooks WooCommerce's checkout_error event to apply aria-invalid and populate the linked error spans.
- Test on the /checkout and /my-account pages with a screen reader.
<?php
/**
* Inject aria-describedby and an error span into WooCommerce form fields.
* Hooked into woocommerce_form_field_args.
*/
add_filter( 'woocommerce_form_field_args', 'wcaa_add_aria_to_fields', 10, 3 );
function wcaa_add_aria_to_fields( $args, $key, $value ) {
// Build a predictable error-span ID from the field key.
$error_id = esc_attr( $key ) . '-error';
// Add aria-describedby to the input's custom_attributes array.
if ( ! isset( $args['custom_attributes'] ) ) {
$args['custom_attributes'] = array();
}
$args['custom_attributes']['aria-describedby'] = $error_id;
return $args;
}
/**
* After each WooCommerce field is output, append a hidden error span.
* The span is populated and aria-invalid is toggled via JavaScript.
*/
add_action( 'woocommerce_form_field', 'wcaa_append_error_span', 10, 4 );
function wcaa_append_error_span( $field, $key, $args, $value ) {
$error_id = esc_attr( $key ) . '-error';
echo '<span id="' . $error_id . '" class="woocommerce-error-msg" role="alert" hidden></span>';
}
/**
* Enqueue a small JS snippet that sets aria-invalid on WooCommerce
* checkout validation failure. Add this inline or as an enqueued asset.
*/
add_action( 'wp_footer', 'wcaa_inline_aria_validation_js' );
function wcaa_inline_aria_validation_js() {
if ( ! is_checkout() && ! is_account_page() ) return;
?>
<script>
(function () {
document.addEventListener('invalid', function (e) {
var input = e.target;
input.setAttribute('aria-invalid', 'true');
var errorId = input.getAttribute('aria-describedby');
if (errorId) {
var span = document.getElementById(errorId);
if (span) {
span.textContent = input.validationMessage;
span.removeAttribute('hidden');
}
}
}, true);
document.addEventListener('input', function (e) {
var input = e.target;
if (input.validity && input.validity.valid) {
input.setAttribute('aria-invalid', 'false');
var errorId = input.getAttribute('aria-describedby');
if (errorId) {
var span = document.getElementById(errorId);
if (span) {
span.textContent = '';
span.setAttribute('hidden', '');
}
}
}
}, true);
})();
</script>
<?php
}Add to your child theme's functions.php to filter WooCommerce field output and inject ARIA attributes.
Focus not visibleQuick win
Add a clearly visible focus indicator to every interactive element so keyboard users can always see which element is currently selected.
On WooCommerce
- In WordPress Admin, go to Appearance → Themes → click your active theme → Edit (or use Appearance → Theme File Editor).
- Open style.css (or the child theme's style.css). Search for 'outline: none' or 'outline: 0'.
- Remove or override those rules, then add the universal focus rule from the code snippet below.
- Alternatively, install the Simple Custom CSS plugin and paste the rule there so it survives theme updates.
- Save changes and test by tabbing through the shop, product, cart, and checkout pages.
/* --- Visible focus indicator fix (WCAG 2.4.7) ---
Add to child theme style.css OR
Appearance → Customize → Additional CSS */
*:focus-visible {
outline: 3px solid #005FCC !important;
outline-offset: 2px !important;
border-radius: 2px;
box-shadow: none; /* remove any competing box-shadow outline */
}
*:focus {
outline: 3px solid #005FCC !important;
outline-offset: 2px !important;
}
*:focus:not(:focus-visible) {
outline: none !important; /* hides ring on mouse click in supporting browsers */
}Add to Appearance → Customize → Additional CSS, or to your child theme's style.css. The !important overrides WooCommerce / theme defaults.
Frame titleQuick win
Add a descriptive `title` attribute to every `<iframe>` and `<frame>` element on your store so screen-reader users know what each embedded frame contains before entering it.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme and edit files via FTP/SSH).
- Search for '<iframe' in your active theme's template files (header.php, footer.php, page templates, single-product.php, etc.).
- Add title="…" to each iframe tag you find and save.
- For iframes inserted by WordPress plugins (maps, video embeds, contact forms, etc.), check the plugin's settings page for an 'iframe title' or 'accessibility label' option. If absent, use the plugin's provided filter/hook or add a JavaScript snippet to your child theme's functions.php or via a code-snippet plugin (e.g. WPCode) to set the attribute after the DOM loads.
- Install the free axe DevTools browser extension and scan your product and checkout pages to confirm the fix.
<?php
// Example: a shortcode that outputs an iframe with a required title.
// Usage: [accessible_map title="Our Birmingham showroom on Google Maps" src="https://www.google.com/maps/embed?pb=..."]
function wc_accessible_iframe_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'title' => 'Embedded content',
'src' => '',
'width' => '100%',
'height' => '400',
),
$atts,
'accessible_map'
);
if ( empty( $atts['src'] ) ) {
return '';
}
return sprintf(
'<iframe title="%1$s" src="%2$s" width="%3$s" height="%4$s" loading="lazy" style="border:0;"></iframe>',
esc_attr( $atts['title'] ),
esc_url( $atts['src'] ),
esc_attr( $atts['width'] ),
esc_attr( $atts['height'] )
);
}
add_shortcode( 'accessible_map', 'wc_accessible_iframe_shortcode' );Add to your child theme's functions.php to output a titled iframe via a shortcode, or edit the relevant template file (e.g. single-product.php) directly.
Heading orderModerate effort
Restructure your page headings so they follow a logical, sequential order (H1 → H2 → H3) with no levels skipped.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme) and locate the template files responsible for your headings: typically woocommerce/archive-product.php (category pages), single-product.php (product pages), and page.php or front-page.php (homepage).
- Identify hard-coded heading tags (e.g., <h3 class='woocommerce-loop-product__title'>) and change them to the correct semantic level (e.g., <h2>) — always work in a child theme to prevent changes being overwritten by updates.
- For page content edited with the Block Editor (Gutenberg), open each page, click on any Heading block, and change the heading level in the block toolbar (the H1/H2/H3 dropdown on the left of the toolbar).
- If you use Elementor or another page builder, click each Heading widget, open its settings, and change the 'HTML Tag' option (H1–H6) to the correct level.
- Install the free Accessibility Checker plugin (by Equalize Digital) to run a site-wide audit and confirm all heading-order issues are resolved.
<?php
/**
* Child theme override for WooCommerce product loop title.
* File path: /wp-content/themes/your-child-theme/woocommerce/loop/loop-start.php
*
* The correct approach: override woocommerce_template_loop_product_title
* to use the right heading level for the page hierarchy.
*
* Add the following to your child theme's functions.php
*/
// Remove the default WooCommerce loop product title (which outputs an <h2>)
remove_action( 'woocommerce_shop_loop_item_title', 'woocommerce_template_loop_product_title', 10 );
// Add a custom version using the correct heading level
add_action( 'woocommerce_shop_loop_item_title', 'custom_loop_product_title', 10 );
function custom_loop_product_title() {
// On a shop/category archive, the page <h1> is the category name.
// Product card titles should therefore be <h2> (or <h3> if they sit
// inside an already-labelled <h2> section). Adjust the level to
// match your page's actual heading hierarchy.
echo '<h2 class="woocommerce-loop-product__title">' . get_the_title() . '</h2>';
}Child-theme override: woocommerce/archive-product.php — change the product loop title from h2 to the correct level.
Html has langQuick win
Add a valid `lang` attribute to the `<html>` element so browsers, screen readers, and search engines know what language your page is written in.
On WooCommerce
- The `lang` attribute in WordPress is typically output by the `language_attributes()` template tag in your theme's header.
- In your WordPress admin, go to Appearance → Theme File Editor (or use an FTP/SFTP client).
- Open your active theme's header.php file.
- Find the `<html` tag. It should call `<html <?php language_attributes(); ?>>` — this is the correct WordPress way and automatically outputs the correct `lang` attribute based on Settings → General → Site Language.
- If the tag instead has a hard-coded `xml:lang` or is missing the language altogether, replace it with `<html <?php language_attributes(); ?>>`.
- Go to Settings → General and confirm the 'Site Language' is set to your primary language.
- Save and verify via View Source. If using a page-builder theme (Elementor, Divi, etc.) that overrides header.php, check that theme's specific header template or use a plugin like 'Head, Footer and Post Injections' as a last resort.
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>Place this in your active theme's header.php. The language_attributes() function outputs the correct lang attribute automatically based on WordPress settings.
Html lang validQuick win
Add a valid BCP 47 language code to the `lang` attribute on your site's `<html>` element so browsers and assistive technologies know what language your content is written in.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use an FTP client / file manager).
- Open your active theme's 'header.php' file (or the file that outputs the `<html>` tag — in block themes this may be 'templates/index.html' or 'parts/header.html').
- Find the `<html` tag. In most WordPress themes it is generated by the `language_attributes()` template tag: `<html <?php language_attributes(); ?>>`.
- If the tag is hard-coded without `language_attributes()`, replace it so it reads `<html <?php language_attributes(); ?>>`. WordPress will then output the correct `lang` attribute based on the language set under Settings → General → Site Language.
- Verify the correct language is selected under Settings → General → Site Language, then save and view page source to confirm.
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>In your active theme's header.php, ensure the <html> tag uses WordPress's language_attributes() function.
Image altModerate effort
Add descriptive alt text to every meaningful image, and mark purely decorative images with an empty alt attribute or role="presentation".
On WooCommerce
- Product images: Admin → Products → [Product name] → click the featured image or any gallery image → in the Media Library modal, fill in the 'Alt Text' field on the right side → click 'Update'.
- For all images in the Media Library: Admin → Media → Library → click any image → fill in the 'Alternative Text' field in the Attachment Details panel.
- Theme/banner images: If using a page builder (Elementor, Divi, etc.), click the image widget → find the 'Alt Text' or 'Image Alt' field in the widget settings.
- For bulk alt-text updates consider the 'SEO Optimized Images' or 'Bulk Auto Alt Text' plugins available from the WordPress plugin directory.
- Use the Yoast SEO or Rank Math plugin — they flag images missing alt text during their content analysis and link directly to the edit screen.
<?php
/**
* Fallback: use the product title as alt text when the
* Media Library alt text field is empty.
* Add to child theme functions.php
*/
add_filter( 'woocommerce_product_get_image', 'wcfix_ensure_product_image_alt', 10, 5 );
function wcfix_ensure_product_image_alt( $html, $product, $size, $attr, $placeholder ) {
// If alt is already set and non-empty, do nothing
if ( ! empty( $attr['alt'] ) ) {
return $html;
}
// Inject the product title as a fallback alt attribute
$fallback = esc_attr( $product->get_name() );
$html = str_replace( 'alt=""', 'alt="' . $fallback . '"', $html );
return $html;
}
?>Place in your child theme's functions.php to filter alt text for product images that are missing it.
Image redundant altQuick win
Remove or clear the alt text on images whose description is already provided by visible text immediately surrounding them, so screen readers don't read the same information twice.
On WooCommerce
- In WordPress admin, go to Appearance → Theme File Editor (or use a child theme and edit files via FTP/SFTP).
- Open the relevant template: for product cards it is typically woocommerce/content-product.php or templates/loop/loop-start.php inside your theme or child theme.
- Locate the woocommerce_template_loop_product_thumbnail() call or the direct <img> tag output. WooCommerce renders product thumbnails via the woocommerce_get_product_thumbnail() function, which passes the alt from the media library.
- Go to Media → Library, find the product image, click Edit, and clear the 'Alternative Text' field so it saves as empty — WooCommerce will then render alt="".
- Alternatively, use the filter hook: add_filter('woocommerce_product_get_image', ...) in your child theme's functions.php to strip redundant alt text programmatically.
- Flush any caching plugin cache, then re-test with axe DevTools.
<?php
/**
* When a WooCommerce product thumbnail is rendered inside a loop card
* that already displays the product title as visible text, the alt
* text is redundant. This filter clears it so screen readers skip
* the image and read the title only once.
*
* Add to: wp-content/themes/YOUR-CHILD-THEME/functions.php
*/
add_filter( 'woocommerce_product_get_image', 'wcfix_clear_redundant_loop_thumbnail_alt', 10, 5 );
function wcfix_clear_redundant_loop_thumbnail_alt( $image, $product, $size, $attr, $placeholder ) {
// Only modify output in the main shop/archive loop, not on single product pages.
if ( is_shop() || is_product_category() || is_product_tag() ) {
// Replace whatever alt value is present with an empty string.
$image = preg_replace( '/\salt=(["\']).*?\1/', ' alt=""', $image );
}
return $image;
}Add to your child theme's functions.php to programmatically set redundant product thumbnail alt text to empty.
Input button nameQuick win
Add a visible label or accessible name to every input button so assistive technologies can announce what the button does.
On WooCommerce
- Empty-value input buttons in WooCommerce most often originate in theme template files, not WooCommerce core.
- In your WordPress admin go to Appearance → Theme File Editor (or use a child-theme folder via FTP/SSH).
- Search template files for `<input type="submit"` with an empty or missing value — common locations are `woocommerce/templates/cart/cart.php`, `checkout/form-checkout.php`, and widget templates.
- Edit the relevant template in your child theme (copy from the plugin folder first if not already overridden): add `value="Place Order"` or the appropriate label.
- Alternatively, install the free WP Accessibility plugin and use its button-label helpers, or add a small code snippet to `functions.php` using `woocommerce_order_button_text` filter to ensure submit buttons carry a label.
- Test with the axe DevTools browser extension after saving.
<?php
/**
* Ensure the WooCommerce checkout Place Order button
* always has a non-empty, translatable label.
* Add to: child-theme/functions.php
*/
add_filter( 'woocommerce_order_button_text', function( $button_text ) {
return ! empty( $button_text ) ? $button_text : __( 'Place Order', 'woocommerce' );
} );
?>Add to your child theme's functions.php to ensure the WooCommerce order submit button always carries a label.
Input image altQuick win
Add a descriptive `alt` attribute to every `<input type="image">` element so screen readers can announce its purpose to visually impaired shoppers.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme to avoid overwriting on updates).
- Search your active theme's template files (commonly `woocommerce/templates/cart/`, `woocommerce/templates/checkout/`, or `header.php`) for `type="image"`.
- Add the `alt` attribute to any matching `<input>` tag and save the file.
- Alternatively, install the 'Code Snippets' plugin and add a WordPress filter to programmatically inject the alt attribute without editing core files.
- If the image button is injected by a plugin (e.g. PayPal Checkout), check the plugin's settings panel for an alt/label field, or file a support request with the plugin author.
- Flush any caching plugin (e.g. WP Rocket, W3 Total Cache) after saving, then verify with axe DevTools.
<?php
/**
* Ensure image submit buttons have alt text.
* Place in child theme's functions.php.
*/
add_filter( 'wp_kses_allowed_html', function( $allowed, $context ) {
if ( 'post' === $context ) {
$allowed['input']['alt'] = true;
}
return $allowed;
}, 10, 2 );
/**
* Example: if you render a custom image button in a template,
* use this pattern directly in the .php / .html template file:
*
* <input
* type="image"
* src="<?php echo esc_url( get_template_directory_uri() . '/images/buy-now.png' ); ?>"
* alt="<?php esc_attr_e( 'Buy now', 'your-text-domain' ); ?>"
* >
*/Add to your child theme's functions.php to patch a missing alt on any programmatically rendered input[type=image] buttons.
LabelQuick win
Add a visible or programmatic label to every form input so assistive technologies can identify it for users.
On WooCommerce
- Install and activate the free plugin 'WP Accessibility' (by Joe Dolson) — it automatically fixes several common labelling issues including search forms.
- For custom or theme form fields, go to Appearance → Theme File Editor (or use a child theme) and open the relevant template: `woocommerce/templates/checkout/form-checkout.php` or `woocommerce/templates/myaccount/form-login.php`.
- Add `<label for='input-id'>Descriptive text</label>` before each unlabelled `<input>`, or add `aria-label='...'` directly on the input tag.
- If using a page-builder form (Elementor, WPForms, Gravity Forms), open the widget/field settings and fill in the 'Label' field — these plugins output proper `<label>` markup when a label is provided.
- Save, clear caches (W3 Total Cache / WP Rocket), and re-test.
<?php
/**
* Example: ensure a custom field added to WooCommerce checkout has a label.
* Add to your child theme's functions.php.
*/
add_action( 'woocommerce_after_order_notes', 'my_theme_add_labelled_custom_field' );
function my_theme_add_labelled_custom_field( $checkout ) {
echo '<div id="my-custom-field">';
// woocommerce_form_field outputs a proper <label> + <input> pair automatically.
woocommerce_form_field(
'my_custom_field',
array(
'type' => 'text',
'class' => array( 'my-field-class form-row-wide' ),
'label' => __( 'Company VAT number', 'my-theme' ), // This becomes the <label> text.
'placeholder' => __( 'e.g. GB123456789', 'my-theme' ),
'required' => false,
),
$checkout->get_value( 'my_custom_field' )
);
echo '</div>';
}Override in your child theme at woocommerce/templates/checkout/form-checkout.php or use a filter. This shows a typical billing field fix.
Label title onlyModerate effort
Add a visible text label to every form field so it is not labeled only by a hidden tooltip (title attribute) or supplementary description (aria-describedby).
On WooCommerce
- Identify which template file contains the offending input: WooCommerce templates live in your child theme under woocommerce/ (e.g. woocommerce/global/quantity-input.php for quantity fields, woocommerce/checkout/form-checkout.php for checkout).
- Copy the file from wp-content/plugins/woocommerce/templates/ into your child theme's woocommerce/ folder to override it safely.
- Open the copied file and locate the <input> element. Add a matching <label for='input-id'>Quantity</label> above it (or use a visually-hidden class from your theme).
- For the site-wide WordPress search widget (<input type='search'>), edit your child theme's searchform.php and ensure a <label> wraps or precedes the input.
- Alternatively, install the free 'WP Accessibility' plugin (by Joe Dolson) — it automatically adds visible/screen-reader labels to the default WordPress search form and comment fields.
- Flush any page/object cache (Plugins → WP Super Cache / LiteSpeed / etc. → Purge All) and verify with the axe extension.
<?php
/**
* Accessible quantity input — child theme override.
* Path: woocommerce/global/quantity-input.php
*/
defined( 'ABSPATH' ) || exit;
?>
<div class="quantity">
<label
for="quantity_<?php echo esc_attr( $product_id ); ?>"
class="screen-reader-text"
>
<?php
printf(
/* translators: %s: product name */
esc_html__( 'Quantity for “%s”', 'woocommerce' ),
esc_html( $product_name )
);
?>
</label>
<input
type="number"
id="quantity_<?php echo esc_attr( $product_id ); ?>"
class="qty"
name="<?php echo esc_attr( $input_name ); ?>"
value="<?php echo esc_attr( $input_value ); ?>"
min="<?php echo esc_attr( $min_value ); ?>"
max="<?php echo esc_attr( $max_value ); ?>"
step="<?php echo esc_attr( $step ); ?>"
aria-label="<?php esc_attr_e( 'Product quantity', 'woocommerce' ); ?>"
/>
</div>Place as woocommerce/global/quantity-input.php in your child theme to override the default quantity input template.
Landmark banner is top levelModerate effort
Move your site's `<header>` element (or `role="banner"` landmark) so it is a direct child of `<body>`, not nested inside another landmark region.
On WooCommerce
- In your WordPress dashboard, go to Appearance → Theme File Editor (or use a local editor / SFTP).
- Open header.php in your active theme (or child theme).
- Check that the `<header>` tag (or `<div role="banner">`) is the very first semantic block after the `<body>` tag — it must NOT be inside a `<main>`, `<div role="main">`, or any other landmark wrapper.
- If it is nested, cut the `<header>...</header>` block and paste it directly inside `<body>` in the correct position (before `<main>`).
- Save, flush any caching plugin (e.g. WP Rocket: Dashboard → WP Rocket → Clear Cache), and retest.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Store</title>
</head>
<body>
<!-- CORRECT: <header> is a direct child of <body> -->
<header>
<a href="/"><img src="/logo.svg" alt="My Store home"></a>
<nav aria-label="Primary">
<ul>
<li><a href="/shop">Shop</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main id="main-content">
<!-- page content here -->
</main>
<footer>
<!-- footer content here -->
</footer>
</body>
</html>Correct top-level structure: <header> is a direct child of <body>, not nested in any landmark.
Landmark complementary is top levelModerate effort
Move your <aside> element (or role="complementary") to the top level of the page so it is not nested inside another landmark region.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- Open the relevant template file where the sidebar appears — typically `sidebar.php`, `page.php`, `single.php`, or `archive.php` in your active (child) theme folder.
- Locate the `get_sidebar()` call or the `<aside id="secondary"…>` block. If it sits between the opening and closing `<main>` tags, move it outside: place the `get_sidebar()` call after the closing `</main>` tag and before `</div>` of any wrapper.
- In the corresponding template (e.g., `page.php`), ensure the structure reads: `<div class="content-area"> <main id="main">…</main> <?php get_sidebar(); ?> </div>` — both `<main>` and the sidebar `<aside>` are siblings inside the wrapper.
- Apply Flexbox or Grid in your child theme's `style.css` to `.content-area` so the columns render correctly without changing the DOM nesting.
- Add `aria-label="Sidebar"` (or a more specific label) to the `<aside>` in `sidebar.php`, update the files, and run axe DevTools or WAVE to confirm the fix.
<?php get_header(); ?>
<div class="page-layout">
<main id="main" class="site-main" role="main">
<?php
while ( have_posts() ) :
the_post();
the_content();
endwhile;
?>
</main><!-- #main -->
<?php get_sidebar(); ?>
<!-- sidebar.php wraps content in:
<aside id="secondary" class="widget-area" aria-label="Sidebar"> -->
</div><!-- .page-layout -->
<?php get_footer(); ?>Place in your child theme's page.php (or single.php) — <main> and sidebar <aside> are siblings.
Landmark contentinfo is top levelQuick win
Move your site's footer element (or role="contentinfo") so it is a direct child of the page body, not nested inside another landmark region.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a local/staging environment with a code editor for safety).
- Open your active theme's 'footer.php' file.
- Check whether the <footer> tag inside footer.php is wrapped by a parent element that carries a landmark role (e.g. <main>, <div role="main">, <article>). Also check 'page.php' or 'index.php' to see how footer.php is included relative to </main>.
- If </main> closes inside footer.php before the <footer> tag, you are fine. If <footer> appears before </main> closes, move the </main> closing tag to appear before the opening <footer> tag.
- If the theme uses a wrapper div with role="main" or similar around the footer, remove the footer from inside that wrapper.
- Save, then use the axe DevTools browser extension on the frontend to verify the landmark structure is correct.
<?php
/**
* The template for displaying the footer.
* Correct structure: </main> closes BEFORE <footer> opens.
*/
?>
</main><!-- #main / .site-main -->
</div><!-- #content / .site-content (non-landmark wrapper, OK) -->
<footer id="colophon" class="site-footer">
<div class="site-info">
<a href="<?php echo esc_url( __( 'https://wordpress.org/', 'textdomain' ) ); ?>">
<?php
/* translators: %s: CMS name, i.e. WordPress. */
printf( esc_html__( 'Proudly powered by %s', 'textdomain' ), 'WordPress' );
?>
</a>
</div><!-- .site-info -->
</footer><!-- #colophon / .site-footer -->
</div><!-- #page / .site (outermost non-landmark wrapper) -->
<?php wp_footer(); ?>
</body>
</html>In your theme's footer.php, the <footer> tag must appear after the closing </main> (or </div> that closes the main landmark). Ensure get_template_part or wp_footer() calls are ordered correctly.
Landmark main is top levelQuick win
Move the `<main>` element (or `role="main"`) so it is a direct child of `<body>`, not nested inside any other landmark element such as `<header>`, `<nav>`, `<aside>`, or `<footer>`.
On WooCommerce
- In your WordPress Admin, go to Appearance → Theme File Editor (or use a local editor/FTP).
- Open your active theme's 'header.php'. Look for a `<main>`, `<div id='main'>`, or `<div role='main'>` that opens inside the `<header>` tag — this is the most common location of the bug.
- If `<main>` opens inside `header.php` but inside a landmark wrapper, move its opening tag to just after the closing `</header>` tag (or to the very top of 'index.php' / page templates before other content).
- Check that the matching closing `</main>` tag is in 'footer.php' before the `<footer>` landmark, not inside it.
- If you use a page builder (Elementor, Divi, etc.), those tools often inject their own wrappers — install the 'Editorskit' or 'Accessibility Checker' plugin and follow its landmark audit to locate the nesting.
- Save, flush any caching plugin, and verify with WAVE or axe in your browser.
<?php
/**
* header.php — correct landmark order
* <main> opens AFTER </header>, not inside it.
*/
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<header id="masthead" role="banner">
<nav id="site-navigation" role="navigation" aria-label="<?php esc_attr_e( 'Primary Navigation', 'your-textdomain' ); ?>">
<?php wp_nav_menu( array( 'theme_location' => 'primary' ) ); ?>
</nav>
</header><!-- #masthead -->
<!-- CORRECT: <main> opens here, as a direct child of <body> -->
<main id="primary" class="site-main">Typical WordPress header.php — close <header> BEFORE opening <main>, then open <main> as a top-level element.
Landmark no duplicate bannerModerate effort
Ensure your page has only one banner landmark (one `<header>` or `role="banner"` element that is not nested inside another landmark), so screen reader users can navigate your site reliably.
On WooCommerce
- The header markup lives in your active WordPress theme. In wp-admin go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- Open 'header.php' — confirm it contains exactly one top-level `<header>` or `role="banner"` element.
- Search all theme template files (header.php, page.php, single.php, functions.php, and any template-parts/) for additional `<header` or `role="banner"` strings.
- If a plugin (e.g. Elementor, WPBakery, a sticky-header plugin) is injecting a second banner, check its settings for a 'header tag' or 'HTML tag' option and change it to `<div>`.
- Use a child theme to override only the affected template file so your changes survive theme updates.
- Clear caching plugins (e.g. WP Rocket, W3 Total Cache), then re-scan.
<?php
/**
* header.php — Child theme override
* ONE <header role="banner"> per page.
*/
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<?php if ( function_exists('woocommerce_store_notice') ) :
woocommerce_store_notice(); // outputs a <div class="woocommerce-store-notice">, not a <header>
endif; ?>
<header id="masthead" class="site-header" role="banner">
<div class="site-branding">
<?php the_custom_logo(); ?>
<?php if ( is_front_page() && is_home() ) : ?>
<h1 class="site-title">
<a href="<?php echo esc_url( home_url('/') ); ?>">
<?php bloginfo('name'); ?>
</a>
</h1>
<?php else : ?>
<p class="site-title">
<a href="<?php echo esc_url( home_url('/') ); ?>">
<?php bloginfo('name'); ?>
</a>
</p>
<?php endif; ?>
</div>
<nav id="site-navigation" class="main-navigation" aria-label="<?php esc_attr_e('Main navigation', 'textdomain'); ?>">
<?php
wp_nav_menu([
'theme_location' => 'menu-1',
'menu_id' => 'primary-menu',
]);
?>
</nav>
</header>
<div id="page" class="site">
<main id="primary" class="site-main" role="main">In your child theme's header.php — one <header role="banner">; any inner section headers use <div>.
Landmark no duplicate contentinfoModerate effort
Ensure your page has only one footer/contentinfo landmark by removing or re-marking any duplicate `<footer>` elements or `role="contentinfo"` attributes.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- Open footer.php in your active (child) theme. This file typically contains the single <footer> landmark — confirm it is wrapped in one <footer id="colophon"> or similar element.
- Search all theme PHP files for additional occurrences of <footer or role="contentinfo" using a code editor search across the theme directory.
- If a page builder (Elementor, Divi, Beaver Builder) is in use, open the page in the builder, select any footer widget/section, switch to its Advanced tab, find the HTML Tag setting, and change it from 'footer' to 'div'.
- If a plugin (e.g. a sticky footer bar or chat widget) is injecting a duplicate, check the plugin settings for HTML/accessibility options, or add a small JavaScript snippet in Appearance → Customize → Additional CSS/JS to change its element role: document.querySelector('.plugin-footer-class').removeAttribute('role').
- Update/save and verify with axe DevTools or WAVE browser extension.
<?php
/**
* Child theme: footer.php
* Ensure exactly one <footer> landmark exists.
*/
?>
<footer id="colophon" class="site-footer">
<div class="site-info">
<?php
/* translators: %s: CMS name, i.e. WordPress. */
printf( esc_html__( 'Proudly powered by %s', 'your-theme' ), 'WordPress' );
?>
<span class="sep"> | </span>
<?php
/* translators: 1: Theme name, 2: Theme author. */
printf( esc_html__( 'Theme: %1$s by %2$s.', 'your-theme' ), 'Your Theme', '<a href="https://example.com">Author</a>' );
?>
</div>
</footer>
<?php wp_footer(); ?>
</body>
</html>
<!-- JavaScript workaround for a third-party plugin injecting a duplicate footer.
Add via Appearance > Customize > Additional CSS/JS or functions.php -->
<script>
(function() {
document.addEventListener('DOMContentLoaded', function() {
// Replace any plugin-injected <footer> elements that are NOT the main footer
var allFooters = document.querySelectorAll('footer');
if (allFooters.length > 1) {
for (var i = 1; i < allFooters.length; i++) {
var dup = allFooters[i];
var div = document.createElement('div');
div.setAttribute('aria-label', 'Additional site information');
div.innerHTML = dup.innerHTML;
Array.from(dup.attributes).forEach(function(attr) {
if (attr.name !== 'role') div.setAttribute(attr.name, attr.value);
});
dup.parentNode.replaceChild(div, dup);
}
}
});
})();
</script>In your child theme's footer.php — one <footer> element only. Use a JavaScript snippet to fix a third-party widget if needed.
Landmark no duplicate mainQuick win
Find and remove every duplicate `<main>` element or `role="main"` attribute so that exactly one main landmark exists on each page.
On WooCommerce
- In WordPress admin go to Appearance → Theme File Editor (or use a file manager / SFTP).
- Open your active theme's header.php, footer.php, sidebar.php, and page.php / single.php / archive.php templates. Search each for '<main' and 'role="main"'.
- The canonical pattern for most themes is a single '<main id="main" class="site-main">' in page.php or index.php that wraps the Loop. Remove 'role="main"' from any other element that also carries it, or change extra '<main>' tags to '<div>'.
- If you use a page builder (Elementor, Divi, Beaver Builder), switch to the builder's page view and inspect the rendered HTML — builders sometimes wrap sections in their own '<main>' containers. Fix via the builder's HTML/custom code widget or by editing the builder's core template.
- Install the free axe DevTools browser extension and run it on the frontend to confirm only one main landmark remains.
<?php
/* header.php — does NOT contain <main> */
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<header id="masthead" class="site-header">
<nav id="site-navigation" aria-label="<?php esc_attr_e('Primary', 'textdomain'); ?>">
<?php wp_nav_menu(['theme_location' => 'primary']); ?>
</nav>
</header>
<?php
/* page.php — opens exactly ONE <main> */
get_header();
?>
<main id="primary" class="site-main">
<?php
while (have_posts()) :
the_post();
the_content();
endwhile;
?>
</main>
<?php
get_footer();
/* footer.php — does NOT contain <main> */
?>Canonical header.php / page.php pairing for a custom child theme. Only page.php (or index.php) opens <main>; header.php must NOT contain a <main> tag.
Landmark one mainQuick win
Add a single `<main>` landmark element (or `role="main"`) to every page so assistive technologies can identify and navigate directly to the primary content.
On WooCommerce
- The correct file to edit depends on your active theme. In WordPress Admin, go to Appearance → Theme File Editor (or edit files via FTP/SSH).
- Open your active theme's `header.php`. Look for the closing `</header>` tag — the `<main>` element typically opens immediately after it.
- If the content area is a `<div id="primary">` or similar, change it to `<main id="primary" class="site-main">` and update the matching closing tag in `footer.php` or `index.php`.
- For block themes (WordPress 6+): go to Appearance → Editor → Templates → select a template (e.g. Single Product). In the block list panel, locate the 'Group' or 'Content' block that holds page content, click it, and in the sidebar under 'Advanced → HTML element' choose `<main>`.
- Install and run the free WP Accessibility plugin — it can audit and patch missing landmarks automatically.
- Re-test with axe DevTools.
<?php
/**
* header.php (child theme)
* Replace any <div id="primary"> or <div id="main"> wrapper with:
*/
?>
<main id="primary" class="site-main" role="main">
<!-- WordPress / WooCommerce loop content is injected here -->In your child theme's header.php, replace the opening <div id="primary"> with <main> and close it in footer.php or index.php.
Landmark uniqueModerate effort
Add a unique aria-label (or aria-labelledby) to every repeated landmark role on the page so screen-reader users can tell them apart.
On WooCommerce
- Landmark HTML is output by your active WordPress theme (e.g. Storefront, Astra, GeneratePress), NOT by WooCommerce itself.
- If you are using a block theme (Full Site Editing), go to Appearance → Editor → Templates, open the template that contains the duplicate landmark (e.g. 'Single Product', 'Page'), click the block you want to label, open the Block Settings sidebar → Advanced, and add the aria-label in the 'HTML anchor' / 'Additional CSS class' area — or use a Custom HTML block to wrap it.
- For classic themes, go to Appearance → Theme File Editor (or use a file manager/FTP) and open the relevant template file (header.php, footer.php, sidebar.php, page.php). Locate the landmark tag and add aria-label='...' directly.
- If you cannot edit theme files safely, install a plugin such as 'Add Attributes to Blocks' or use a child theme's functions.php to hook into the output and inject the aria-label.
- After saving, run axe DevTools in your browser on the storefront to confirm each landmark is uniquely labelled.
<!-- BEFORE: two <nav> elements with no label — screen readers can't tell them apart -->
<nav>
<ul><!-- main menu items --></ul>
</nav>
<nav>
<ul><!-- footer links --></ul>
</nav>
<!-- AFTER: each <nav> has a unique aria-label -->
<nav aria-label="Main navigation">
<ul><!-- main menu items --></ul>
</nav>
<nav aria-label="Footer navigation">
<ul><!-- footer links --></ul>
</nav>
<!-- Alternative: use aria-labelledby when a visible heading already exists -->
<section aria-labelledby="filters-heading">
<h2 id="filters-heading">Product Filters</h2>
<!-- filter controls -->
</section>
<section aria-labelledby="recommendations-heading">
<h2 id="recommendations-heading">You May Also Like</h2>
<!-- recommended products -->
</section>Apply unique aria-label attributes to every repeated landmark element. Use aria-labelledby when a visible heading already labels the region.
Link in text blockQuick win
Increase the color contrast between inline links and their surrounding body text to at least 3:1, or add a non-color visual distinction (such as underline) so links are identifiable without relying on color alone.
On WooCommerce
- In WordPress admin, go to Appearance → Customize → Additional CSS.
- Add a scoped CSS rule targeting links inside post/page content, e.g.: .entry-content a, .woocommerce-product-details__short-description a { color: #0044cc; text-decoration: underline; }
- Alternatively, edit your child theme's style.css (Appearance → Theme File Editor) to add the rule permanently.
- For block-based themes (Full Site Editing), go to Appearance → Editor → Styles → Custom CSS and add the rule there.
- Save and use a color-contrast checker to verify the new link color vs. body text color.
/* WooCommerce: target links in product descriptions and post content */
.entry-content a,
.woocommerce-product-details__short-description a,
.woocommerce-Tabs-panel a {
color: #0057d8; /* update to your chosen compliant color */
text-decoration: underline;
}Paste into Appearance → Customize → Additional CSS, or into your child theme's style.css.
Link nameQuick win
Add a descriptive, screen-reader-accessible text label to every link on your store so assistive technology can announce its purpose.
On WooCommerce
- Go to WordPress Admin → Appearance → Theme File Editor (or use a child theme via FTP/SFTP to avoid overwriting on updates).
- Common problem files: header.php or header-*.php (navigation and cart icon links), footer.php (social icons), and woocommerce/content-product.php or loop/loop-start.php (product card image links).
- For the mini-cart icon link, locate the cart widget output and add aria-label='Shopping cart, [item count] items' or use a visually-hidden span.
- Install the 'WP Accessibility' plugin (author: Joe Dolson) — it automatically adds skip links and fixes some common unnamed link issues in standard WordPress output without code edits.
- For social icon links added by plugins (e.g. Simple Social Icons), configure the plugin's settings to include link labels, or override the template with a child theme snippet that adds aria-label attributes.
- After saving, validate with the axe browser extension on your storefront.
ListModerate effort
Fix all HTML lists so that only valid child elements appear directly inside `<ul>` and `<ol>` tags, and only `<ul>` or `<ol>` elements wrap `<li>` items.
On WooCommerce
- In WordPress Admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP to avoid overwriting on updates).
- Open header.php and any navigation-related template parts (e.g. inc/navigation.php, template-parts/header/site-branding.php) — confirm all `<ul>` nav menus contain only `<li>` children.
- Check footer.php for footer widget list markup.
- For WooCommerce-specific pages, locate woocommerce/templates/ (or your theme's overrides in yourtheme/woocommerce/) — inspect product loops, breadcrumbs (breadcrumb.php), and widget templates.
- If using a page builder (Elementor, Divi), open the affected page in the builder, find the List or Nav widget, and switch to its HTML/Custom Code view to correct the markup.
- Save, clear your caching plugin cache, and re-test with the axe browser extension.
<?php
wp_nav_menu(
array(
'theme_location' => 'primary',
'container' => 'nav',
'container_attr' => array( 'aria-label' => 'Main navigation' ),
'menu_class' => 'nav-list',
'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
// WordPress outputs <li> children automatically — do NOT add extra
// wrappers inside items_wrap that would sit between <ul> and <li>.
)
);
?>Place in your child theme's header.php or a nav template part — correct wp_nav_menu list structure.
ListitemModerate effort
Ensure every `<li>` element is a direct child of a `<ul>` or `<ol>` element (or an element explicitly given `role="list"`), so that screen readers can correctly announce list structure to users.
On WooCommerce
- Open your WordPress admin → Appearance → Theme File Editor (or use a local/FTP editor for a safer edit).
- Identify which template file renders the broken list — common files are `header.php`, `footer.php`, `functions.php`, page templates, or a plugin shortcode output. Use your browser's View Source / DevTools to trace the HTML to its template.
- Find the `<li>` element and confirm its direct parent is not `<ul>` or `<ol>`. Wrap the `<li>` elements in the appropriate container: `<ul>` or `<ol>`.
- If the issue is inside a plugin (e.g. a WooCommerce widget, product tabs plugin, or mega-menu plugin), first try the plugin's own settings for 'list markup' or 'HTML output'. If unavailable, use a child-theme template override: copy the plugin's template file into your child theme's directory and edit it there.
- Clear any caching plugin cache (e.g. WP Super Cache, W3 Total Cache, LiteSpeed Cache) after saving.
- Re-check with the axe DevTools browser extension to confirm the violation is resolved.
<?php
/**
* Example: product feature list output in a WooCommerce template partial.
* Place in: child-theme/woocommerce/single-product/your-feature-list.php
*
* Every <li> is a direct child of <ul>.
*/
?>
<section class="product-features">
<h2><?php esc_html_e( 'Product Highlights', 'your-textdomain' ); ?></h2>
<ul class="product-features__list">
<?php
$features = get_post_meta( get_the_ID(), '_product_features', true );
if ( is_array( $features ) ) {
foreach ( $features as $feature ) {
echo '<li class="product-features__item">' . esc_html( $feature ) . '</li>';
}
}
?>
</ul>
</section>
<?php
/**
* Example: navigation widget — wrap <li> items in <ul> inside <nav>.
*/
?>
<nav class="site-navigation" aria-label="<?php esc_attr_e( 'Main navigation', 'your-textdomain' ); ?>">
<ul id="primary-menu" class="nav-menu">
<?php
wp_nav_menu([
'theme_location' => 'primary',
'container' => false, // <nav> is already the container
'items_wrap' => '%3$s', // output only <li> items — they go inside the <ul> above
]);
?>
</ul>
</nav>In a child-theme template override (e.g. woocommerce/templates/loop/loop-start.php or a custom template partial).
Meta refreshQuick win
Remove or disable the `<meta http-equiv="refresh">` tag that forces an automatic page reload, or replace any redirect use-case with a server-side redirect instead.
On WooCommerce
- In WordPress admin go to Appearance → Theme File Editor (or use a code editor via FTP/SSH).
- Open your active theme's `header.php` (or `functions.php` if the tag is injected programmatically). Search for `http-equiv` or `meta.*refresh` and delete the line.
- If the tag comes from a plugin, go to Plugins → Installed Plugins, deactivate plugins one at a time while re-checking the page source, until the responsible plugin is identified — then check that plugin's settings to disable the redirect/refresh feature.
- For URL redirects, install the free 'Redirection' plugin (johngodley.com) and create a 301 redirect there instead of using a meta-refresh.
- Save changes, clear any caching plugin (e.g. WP Rocket, W3 Total Cache), and re-scan.
<?php
// REMOVE this kind of code from your theme's functions.php:
// add_action( 'wp_head', function() {
// echo '<meta http-equiv="refresh" content="30">';
// } );
// For redirects, use the Redirection plugin, or add a proper PHP redirect
// in a one-off template before any output is sent:
// wp_redirect( 'https://example.com/new-page', 301 );
// exit;If the meta-refresh is added via functions.php, find and remove the wp_head hook that outputs it.
Meta viewportQuick win
Remove `user-scalable=no` (and any `maximum-scale` below 5) from your site's `<meta name="viewport">` tag so visitors can pinch-to-zoom on mobile devices.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a code editor via SFTP).
- Open your active (child) theme's header.php file.
- Search for 'viewport' and edit the <meta name="viewport"> tag to remove `user-scalable=no` and fix `maximum-scale`.
- If you cannot find the tag in header.php, check your theme's functions.php — some themes inject it via wp_head hooks — or search all theme files for 'user-scalable'.
- Save the file. If using SFTP, upload the edited file to wp-content/themes/your-child-theme/header.php.
- Verify on mobile that pinch-zoom now works.
<meta name="viewport" content="width=device-width, initial-scale=1">In your child theme's header.php, replace the existing viewport meta tag with this line.
Meta viewport largeQuick win
Remove or raise the `maximum-scale` value in your meta viewport tag so users can pinch-zoom on mobile devices.
On WooCommerce
- Log in to your WordPress Admin dashboard.
- Go to Appearance → Theme File Editor (or use a child theme's files via FTP/SFTP).
- Open your active theme's 'header.php' file.
- Search for 'viewport' and edit the <meta name="viewport"> tag to remove maximum-scale restrictions.
- If your theme uses a page-builder (Elementor, Divi, etc.) that injects the viewport tag via its own settings, also check that builder's global site settings for a 'Disable Zoom' or 'Mobile Zoom' toggle and turn it off.
- Click 'Update File' to save.
<meta name="viewport" content="width=device-width, initial-scale=1">In your child theme's header.php, replace the existing viewport meta tag with this line inside the <head> section.
Nested interactiveModerate effort
Remove or restructure nested interactive elements so that no focusable control (button, link, input, etc.) is placed inside another interactive control, and never rely on tabindex="-1" or aria-hidden="true" to hide an inner interactive element from assistive technologies.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme Editor (or use a code editor / FTP client) and locate the theme template responsible for the affected component — commonly woocommerce/content-product.php, woocommerce/loop/add-to-cart.php, or a custom block/widget.
- If your theme uses a block editor (FSE), go to Appearance → Editor, navigate to the template containing the issue, and inspect the block markup in Code Editor view (three-dot menu → Code editor).
- Search for `<a` or `<button` tags that wrap another `<a>` or `<button>`. Refactor so only one interactive element wraps the action.
- If the nesting comes from a WooCommerce core template override, copy the template to your child theme under /woocommerce/ and edit it there so updates don't overwrite your fix.
- Install the axe DevTools browser extension and re-scan the fixed page to confirm the violation is gone.
<?php
/**
* Child theme override: woocommerce/loop/add-to-cart.php
* ✅ CORRECT — single <a> used as the add-to-cart control; no nested button.
*/
?>
<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
<?php do_action( 'woocommerce_before_add_to_cart_button' ); ?>
<a
href="<?php echo esc_url( $product->add_to_cart_url() ); ?>"
data-quantity="1"
class="button add_to_cart_button ajax_add_to_cart <?php echo esc_attr( $product->is_purchasable() && $product->is_in_stock() ? 'add_to_cart_button' : '' ); ?>"
data-product_id="<?php echo absint( $product->get_id() ); ?>"
aria-label="<?php echo esc_attr( $product->add_to_cart_description() ); ?>"
rel="nofollow">
<?php echo esc_html( $product->add_to_cart_text() ); ?>
</a>
<?php do_action( 'woocommerce_after_add_to_cart_button' ); ?>In your child theme at woocommerce/loop/add-to-cart.php: ensure the add-to-cart control is a single element.
Object altQuick win
Add a meaningful text alternative (via `title` attribute or `aria-label`) to every `<object>` element so screen readers can describe its content to users who cannot see it.
On WooCommerce
- Log in to WordPress Admin and go to Appearance → Theme File Editor (or use a code editor via FTP/SSH).
- If the <object> is in a page or post, edit that content in the Block Editor: find the Custom HTML block or Classic block containing the <object> tag and add the `title` attribute directly.
- If it is in a theme template file (e.g., single-product.php, page.php), open that file and add `title='Descriptive label'` to the <object> tag.
- If the <object> is injected by a plugin (e.g., a PDF viewer plugin), check that plugin's settings for a 'title' or 'label' field, or use a Code Snippets plugin to filter the output.
- Save changes, clear any caching plugin cache (e.g., WP Rocket, W3 Total Cache), and verify with axe DevTools.
<?php
// In a template file — use esc_attr() to safely output the title.
$object_title = esc_attr__( 'Winter 2024 Product Catalog (PDF)', 'your-textdomain' );
$object_url = esc_url( get_attached_file( get_post_thumbnail_id() ) ); // adjust URL source as needed
?>
<object
data="<?php echo $object_url; ?>"
type="application/pdf"
width="100%"
height="500"
title="<?php echo $object_title; ?>">
<p>
<a href="<?php echo $object_url; ?>">Download the catalog</a>
</p>
</object>Place in the relevant template file (e.g., single-product.php) inside your child theme, or inside a Custom HTML block in the WordPress block editor.
Page has heading oneQuick win
Add a single, descriptive `<h1>` heading to every page so assistive technologies and search engines can identify the page's main topic.
On WooCommerce
- For product pages: most WooCommerce themes output the product name as <h1 class='product_title entry-title'>. Verify this in your child theme's template file woocommerce/single-product.php (or the parent theme's copy). If it's wrong, copy the file to your child theme and correct the element.
- For standard WordPress pages (About, Contact, etc.): in the WordPress Block Editor (Gutenberg), add a Heading block at the top of the page content and set its level to H1 in the block toolbar on the right.
- For the homepage: if using a page builder (Elementor, Divi, etc.), find the hero text widget, click it, and change its HTML tag from 'div' or 'p' to 'h1' in the widget's advanced/style settings.
- If your theme suppresses the page title visually, check Appearance → Customize → (theme-specific) Title settings, or re-enable it via the theme's page options.
- Install the free 'Accessibility Checker' plugin (by Equalize Digital) to audit all pages from the WordPress dashboard.
<h1 class="product_title entry-title"><?php the_title(); ?></h1>In your child theme's woocommerce/single-product.php (or the WooCommerce template override), the product title should be output as shown. This mirrors WooCommerce's default markup.
Presentation role conflictModerate effort
Remove conflicting ARIA attributes (global ARIA properties and tabindex) from any element that has been marked as purely presentational with role="presentation" or role="none".
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme and edit files via FTP/SFTP).
- Search your active theme's template files (header.php, footer.php, page.php, single.php, woocommerce/ templates) for role="presentation" or role="none".
- For each match, check the element for conflicting ARIA attributes or tabindex and remove them as described in the generic steps.
- If the conflict originates inside a plugin's output, install the 'Code Snippets' plugin and use a PHP filter (e.g. ob_start / ob_get_clean on the relevant hook) to post-process the HTML, or contact the plugin developer.
- Save your changes, clear any caching plugin (e.g. WP Rocket, W3 Total Cache) under Plugins → Settings → Clear Cache, then re-scan with axe DevTools in Chrome.
<?php
/**
* Sale flash / badge — decorative only.
* Child theme override: woocommerce/single-product/sale-flash.php
*/
if ( ! defined( 'ABSPATH' ) ) exit;
?>
<?php if ( $post->post_type === 'product' ) : ?>
<span class="onsale">
<?php
// Use translatable text inside the element for screen readers
// Do NOT add role="presentation" here — the <span> carries visible text meaning.
esc_html_e( 'Sale!', 'woocommerce' );
?>
</span>
<?php endif; ?>Example of a corrected decorative image in a WooCommerce child theme template (e.g. woocommerce/single-product/sale-flash.php). Remove role and aria-label from purely decorative badges.
RegionModerate effort
Wrap all visible page content inside HTML landmark regions (e.g. `<header>`, `<main>`, `<nav>`, `<footer>`) so screen-reader users can navigate the page by section.
On WooCommerce
- Landmark structure is controlled by your active WordPress theme. In your WordPress admin go to Appearance → Theme File Editor (or use a local FTP/SSH connection for child themes).
- Open 'header.php' — wrap the site header and nav output in `<header role='banner'>` and `<nav aria-label='Primary'>` respectively if not already present.
- Open 'footer.php' — ensure all footer content is inside `<footer role='contentinfo'>`.
- Open 'page.php', 'single.php', 'archive.php', and 'index.php' — confirm the main loop is wrapped in `<main id='main' role='main'>`.
- If using a block theme (WordPress 6+), edit templates via Appearance → Editor (Site Editor). Select the template part (Header, Footer, etc.) and use the Group block with its HTML element set to the correct semantic tag (header, main, footer) in the block settings sidebar.
- Install the free 'WP Accessibility' plugin (by Joe Dolson) for quick fixes like adding a skip-link and ensuring the main content is correctly identified; install 'Equalize Digital Accessibility Checker' to scan for remaining issues.
Role img altModerate effort
Add a meaningful aria-label (or aria-labelledby) to every element with role="img" so screen readers can announce what the image represents.
On WooCommerce
- In WordPress Admin, go to Appearance → Theme File Editor (or use a local IDE with your child theme files).
- Search your active child theme files for role="img" — common locations: header.php, functions.php, template-parts/, and any custom plugin shortcodes.
- Add aria-label="[description]" to each flagged element in the template file.
- For SVG icons added via a plugin (e.g. WooCommerce product badges), check the plugin's settings for an 'Alt text' or 'Label' field; if none exists, use a custom CSS/JS snippet via Appearance → Customize → Additional CSS / Additional JS, or a plugin like 'Code Snippets' to patch the output.
- If icons come from an icon font plugin (e.g. Font Awesome), wrap each meaningful icon in a <span role="img" aria-label="[description]"> and add aria-hidden="true" to the inner icon element.
- Run the axe DevTools browser extension on your storefront to confirm the issue is resolved.
<?php
/**
* Fix: add aria-label to WooCommerce star rating spans that use role="img".
* Add to: wp-content/themes/your-child-theme/functions.php
*/
add_filter( 'woocommerce_product_get_rating_html', 'custom_accessible_rating_html', 10, 3 );
function custom_accessible_rating_html( $html, $rating, $count ) {
if ( 0 === $rating || '' === $rating ) {
return $html;
}
$label = sprintf(
/* translators: 1: rating value 2: max rating */
esc_attr__( 'Rated %1$s out of %2$s', 'woocommerce' ),
number_format( $rating, 1 ),
'5'
);
// Replace the existing role="img" span — add aria-label if missing
$html = preg_replace(
'/<span role="img"((?!aria-label)[^>])*/i',
'<span role="img" aria-label="' . $label . '"',
$html
);
return $html;
}
?>Add to your child theme's functions.php, or to a custom template-parts file. The filter below patches WooCommerce's star-rating output.
Scrollable region focusableQuick win
Make every scrollable region on your site reachable by keyboard by adding tabindex="0" (or placing focusable content inside it) so keyboard and assistive-technology users can scroll it.
On WooCommerce
- In your WordPress Admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SSH).
- Find the template part that renders the scrollable region — common locations: woocommerce/single-product/description.php, or your theme's page templates.
- Add tabindex="0" and aria-label="[Region name]" to the scrollable wrapper element in that template file.
- Add a CSS :focus rule in Appearance → Customize → Additional CSS (or your child theme's style.css): .scrollable-region:focus { outline: 2px solid #005fcc; }
- Alternatively, install a plugin like 'Accessibility Toolkit' or 'WP Accessibility' which can inject tabindex on configurable containers.
- Test with the Tab key and arrow keys in a browser to confirm scrolling works without a mouse.
<?php
/**
* Single Product Description — child theme override
* Adds keyboard accessibility to the scrollable description wrapper.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<div
id="tab-description"
class="woocommerce-Tabs-panel woocommerce-Tabs-panel--description panel entry-content wc-tab"
tabindex="0"
aria-label="<?php esc_attr_e( 'Product description', 'woocommerce' ); ?>"
role="region"
style="max-height: 400px; overflow-y: auto;"
>
<?php the_content(); ?>
</div>Override woocommerce/single-product/description.php in your child theme at woocommerce/single-product/description.php.
Select nameQuick win
Add a visible or programmatic label to every `<select>` dropdown so assistive technologies can announce its purpose to users.
On WooCommerce
- Unlabeled selects in WooCommerce most often appear in: product variation dropdowns, the country/state fields in the checkout, and the shop sort dropdown.
- Create a child theme (if you haven't already) to avoid losing changes on theme updates. In your child theme, copy the relevant template: `woocommerce/templates/single-product/add-to-cart/variable.php` for variation selects, or `woocommerce/templates/checkout/form-billing.php` for checkout selects.
- In `variable.php`, find the `<select>` for each attribute. Ensure the `<label>` above it has a `for` attribute matching the select's `id` attribute. Example: `<label for="pa_size">Size</label>` and `<select id="pa_size" name="attribute_pa_size">`.
- For the shop sort dropdown, copy `woocommerce/templates/loop/orderby.php` to your child theme and add `<label for="orderby" class="screen-reader-text">Sort by</label>` before the `<select id="orderby">` element.
- Alternatively, use the `woocommerce_form_field` filter or a small plugin/snippet in your child theme's `functions.php` to inject `aria-label` attributes on problematic selects without editing templates.
- Flush any page/object caches, then re-test with the axe DevTools browser extension.
<?php
// In: yourchildtheme/woocommerce/single-product/add-to-cart/variable.php
// Locate the loop that outputs attribute selects and ensure each has a linked label.
// The key change: add a unique id to the <select> and a matching for= on the <label>.
foreach ( $attributes as $attribute_name => $options ) :
$select_id = sanitize_title( $attribute_name ) . '-select';
?>
<div class="variations_form__row">
<label for="<?php echo esc_attr( $select_id ); ?>">
<?php echo wc_attribute_label( $attribute_name ); ?>
</label>
<select
id="<?php echo esc_attr( $select_id ); ?>"
name="attribute_<?php echo esc_attr( sanitize_title( $attribute_name ) ); ?>"
data-attribute_name="attribute_<?php echo esc_attr( sanitize_title( $attribute_name ) ); ?>"
data-show_option_none="yes"
>
<option value=""><?php esc_html_e( 'Choose an option', 'woocommerce' ); ?></option>
<?php wc_dropdown_variation_attribute_options(
array(
'options' => $options,
'attribute' => $attribute_name,
'product' => $product,
)
); ?>
</select>
</div>
<?php endforeach; ?>Copy woocommerce/templates/single-product/add-to-cart/variable.php to your child theme at the same path and replace the select markup with this pattern.
Skip linkQuick win
Add a valid, focusable target element (with a matching `id`) for every "Skip to main content" link so keyboard and assistive-technology users can bypass repeated navigation.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- Open `header.php` in your active (child) theme. Immediately after the opening `<body>` tag, add: `<a class="skip-link screen-reader-text" href="#primary">Skip to main content</a>`.
- Open `index.php` (or `page.php`, `single.php` as appropriate) and find the main content wrapper. Ensure it reads: `<main id="primary" class="site-main" tabindex="-1">`.
- Many themes (Storefront, Astra, Kadence, GeneratePress) already include a skip link — search for `skip` in `header.php`. If found, just ensure the `href` fragment matches the `id` on the `<main>` element.
- Add the CSS for `.skip-link` to your child theme's `style.css` (see code snippet below).
- Alternatively, install the 'WP Accessibility' plugin (by Joe Dolson) — it adds a working skip link automatically without editing code.
Svg img altModerate effort
Add a meaningful text alternative to every SVG image so screen readers can describe it to visually impaired shoppers.
On WooCommerce
- SVGs in WooCommerce stores live in theme template files, plugin output, or the WordPress Media Library. First, check your active theme's folder (wp-content/themes/your-theme/) using a file manager or FTP client, or use the WordPress Customizer > Additional CSS / Theme Editor (Appearance > Theme File Editor — note: editing theme files directly risks being overwritten on update, so use a child theme).
- For theme SVGs: create or open your child theme, copy the relevant template file (e.g. header.php, footer.php, or a WooCommerce template), locate the `<svg>` tag, and add `role="img"` plus `aria-label="Descriptive text"`.
- For SVGs inserted via the Block Editor (Gutenberg): click the SVG/Image block, and in the Block settings sidebar under 'Alt text', enter a description. For SVG files uploaded to the Media Library, edit the Attachment page and fill in the Alt Text field.
- For plugin-generated SVGs (e.g. WooCommerce icon fonts or SVG badges), install the 'Safe SVG' plugin which sanitises uploads and allows you to edit SVG alt attributes. Then use a code snippet plugin (e.g. WPCode) to add a filter that injects `aria-label` attributes.
- For decorative SVGs in theme files, add `aria-hidden="true"` to the opening `<svg>` tag in your child theme template.
<?php
/**
* Add role="img" and aria-label to meaningful inline SVGs
* that WooCommerce or the theme outputs without text alternatives.
* Use a code-snippet plugin (e.g. WPCode) or child theme functions.php.
*/
add_filter( 'the_content', 'fix_svg_accessibility', 20 );
add_filter( 'widget_text', 'fix_svg_accessibility', 20 );
function fix_svg_accessibility( $content ) {
// Example: add aria-label to the WooCommerce cart icon SVG if missing.
// Adjust the selector string and label to match your theme's actual SVG.
$content = str_replace(
'<svg class="icon icon-cart"',
'<svg class="icon icon-cart" role="img" aria-label="Shopping cart"',
$content
);
return $content;
}
?>Place in your child theme's functions.php or a code-snippet plugin to patch SVGs output by WooCommerce core or plugins at render time.
TabindexModerate effort
Remove or replace all positive tabindex values (tabindex="1" or higher) on your page elements, using tabindex="0" for focusable elements and tabindex="-1" only when programmatic focus is needed.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme and edit files via FTP/SFTP).
- Search for 'tabindex' across your theme and child theme files — focus on header.php, footer.php, page.php, and any custom template parts.
- Also search active plugins: go to Plugins → Plugin File Editor and check any plugin that adds custom UI components.
- Change any tabindex value greater than 0 to '0', or remove the attribute from native interactive elements.
- Alternatively, install the 'Accessibility Checker' plugin (by Equalize Digital) to scan for this issue automatically and pinpoint the exact files and line numbers.
- Save changes and clear any caching plugin (e.g., WP Rocket, W3 Total Cache) under Settings → [your cache plugin] → Clear Cache.
<?php
/**
* ❌ WRONG — positive tabindex on a checkout button:
* <button tabindex="5" type="submit">Place Order</button>
*
* ✅ CORRECT — no tabindex needed on a native button:
*/
?>
<button type="submit" class="button alt wp-element-button" id="place_order">
<?php esc_html_e( 'Place order', 'woocommerce' ); ?>
</button>
<?php
/**
* ✅ CORRECT — non-native element that needs to be keyboard-focusable:
*/
?>
<div
tabindex="0"
role="button"
aria-label="<?php esc_attr_e( 'Open product gallery', 'your-theme' ); ?>"
class="gallery-trigger">
View Gallery
</div>Place corrected markup in your child theme's template file, e.g., wp-content/themes/your-child-theme/woocommerce/checkout/form-checkout.php or any custom template part.
Valid langQuick win
Add or correct the `lang` attribute on every HTML element that contains content in a different language, using a valid BCP 47 language code.
On WooCommerce
- WordPress automatically outputs lang="{locale}" on the <html> tag via the language_attributes() template tag — verify your active theme calls <?php language_attributes(); ?> inside the <html> tag in header.php (Appearance → Theme File Editor → header.php).
- To change the site language go to Settings → General → Site Language and select the correct language from the dropdown; WordPress will translate the locale code to a valid BCP 47 value.
- For inline foreign-language text in product descriptions, switch the block editor to HTML view (⋮ menu → Edit as HTML) and wrap the passage: <span lang="fr">Texte en français</span>.
- Install the 'WPML' or 'Polylang' plugin if you run a multilingual store — both plugins hook into language_attributes() and output the correct lang code for each translated page automatically.
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>In your active theme's header.php — language_attributes() outputs the correct lang attribute automatically from WordPress Settings → General → Site Language.
Security (OWASP) · 26 fixes
Dmarc policy noneModerate effort
Strengthen your DMARC policy from p=none (monitor-only) to p=quarantine, then p=reject, so that emails fraudulently sent from your domain are blocked rather than silently allowed through.
On WooCommerce
- WooCommerce sends transactional email through WordPress (via PHP mail or an SMTP plugin). Confirm you are using an SMTP plugin such as WP Mail SMTP, FluentSMTP, or Postman SMTP and that DKIM signing is enabled in that plugin or your SMTP provider (e.g. SendGrid, Mailgun, Brevo).
- In WordPress Admin → Plugins, check your SMTP plugin settings and follow its guide to publish the DKIM DNS key your provider gives you.
- Add your SMTP provider's sending IPs/includes to your domain's SPF record at your DNS host.
- At your DNS provider, edit the TXT record for _dmarc.yourdomain.com — change p=none to p=quarantine, monitor rua reports, then advance to p=reject.
v=DMARC1; p=quarantine; sp=quarantine; pct=100; rua=mailto:dmarc-reports@yourdomain.com; ruf=mailto:dmarc-failures@yourdomain.com; ri=86400; aspf=r; adkim=r; fo=1;This is the DNS TXT record value for _dmarc.yourdomain.com — paste this as the record content at your DNS provider. Replace the rua/ruf addresses with your own reporting inbox or DMARC reporting service address.
Hsts disabledQuick win
Set a valid HTTP Strict-Transport-Security (HSTS) header with a max-age of at least 31536000 (one year) and includeSubDomains to force browsers to always use HTTPS for your store.
On WooCommerce
- WooCommerce runs on WordPress, so HSTS must be set at the web server or hosting level, not inside the WordPress admin.
- For Apache hosting: open or create a .htaccess file in your site root (via File Manager in cPanel or via FTP) and add inside the <IfModule mod_headers.c> block: Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
- For Nginx hosting: open your server block config (e.g., /etc/nginx/sites-available/yoursite.conf) and add inside the server {} block: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; — then run nginx -t and sudo systemctl reload nginx.
- Alternatively, install the free 'Headers and Footers by WPBeginner' plugin or 'WP Headers and Footers' — however, note that PHP-level headers must be set before any output; using a caching plugin or server-level config is more reliable.
- If you use a managed WordPress host (WP Engine, Kinsta, Flywheel), open a support ticket or use their dashboard's 'Security Headers' section — many set HSTS for you automatically.
- Verify with DevTools or curl: curl -I https://yourdomain.com | grep -i strict
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPressAdd inside the existing <IfModule mod_headers.c> block in the .htaccess file in your WordPress root (above the # BEGIN WordPress section). Edit via cPanel File Manager or FTP.
Hsts max age too shortQuick win
Increase your HSTS max-age directive to at least 31536000 seconds (1 year) so browsers enforce HTTPS connections for a meaningful period.
On WooCommerce
- WooCommerce runs on WordPress; HSTS is set at the web server or plugin level, not inside WordPress itself.
- Recommended plugin approach: Install the 'Headers Security Advanced & HSTS WP' plugin (or 'HTTP Headers' by Davide Casali) from WordPress.org. In the plugin settings, set Strict-Transport-Security max-age to 31536000 and enable includeSubDomains if appropriate.
- Alternatively, if you manage your own Nginx server, add inside your HTTPS server block: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; Then reload Nginx: sudo nginx -s reload
- For Apache, add to your VirtualHost block or .htaccess (inside <IfModule mod_headers.c>): Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" Then restart Apache.
- If your host uses cPanel, go to cPanel → Security → ModSecurity or use the 'Headers' section in your hosting control panel to add the header.
- Verify by visiting your store and checking the response headers in browser DevTools → Network tab.
Strict-Transport-Security: max-age=31536000; includeSubDomainsThis is an HTTP response header — it must be sent by your server, NOT as an HTML meta tag. The meta tag is not supported for HSTS.
Https not availableQuick win
Enable HTTPS by installing a valid SSL/TLS certificate and redirecting all HTTP traffic to the secure HTTPS version of your store.
On WooCommerce
- Log in to your hosting control panel (cPanel, Plesk, or your host's dashboard).
- Locate the SSL/TLS section and install a free Let's Encrypt certificate for your domain — most hosts (SiteGround, Kinsta, WP Engine, Bluehost) offer a one-click Let's Encrypt install.
- Once the certificate is active, in your WordPress Admin go to Settings → General and change both 'WordPress Address (URL)' and 'Site Address (URL)' from http:// to https://.
- Install the free 'Really Simple SSL' plugin (Plugins → Add New → search 'Really Simple SSL') and activate it — it handles the 301 redirect, mixed-content fixes, and can enable HSTS.
- Alternatively, add redirect rules manually: in your .htaccess file (root of your WordPress install), add: RewriteEngine On / RewriteCond %{HTTPS} off / RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
- Flush any caching plugin's cache (e.g. WP Super Cache, W3 Total Cache, WP Rocket) after making changes.
Info disclosure serverQuick win
Remove or suppress the HTTP `Server` response header so your web server no longer advertises its software name and version number to the public internet.
On WooCommerce
- WooCommerce runs on WordPress, so the fix lives at the web-server level of your hosting account — not inside the WooCommerce or WordPress admin.
- For *nginx hosting*: SSH into your server, open `/etc/nginx/nginx.conf` (or the server block file for your site, often in `/etc/nginx/sites-available/`). Inside the `http {}` block add `server_tokens off;`. Save and run `sudo nginx -t && sudo systemctl reload nginx`.
- For *Apache hosting*: Open or create a `.htaccess` file in your WordPress root (`public_html/`) and add `ServerTokens Prod` and `ServerSignature Off` — note these directives require `AllowOverride All` or must go in `httpd.conf`; on shared hosting you may need to ask your host to set them.
- For *cPanel/shared hosting*: Contact your hosting provider and request they set `ServerTokens Prod` at the Apache level, or install the *'Headers and Options'* or *'LiteSpeed Cache'* plugin (LiteSpeed Cache > Toolbox > Config tab has a 'Hide Server Signature' option if on LiteSpeed servers).
- Alternatively, install the WordPress plugin *'HTTP Headers'* (by Fer Urbina) or *'Security Headers'* (by Patrick Labbé) — these allow you to add a custom `Server` header value or remove it via PHP's `header_remove('Server')` hook, which works for PHP-generated responses but may not affect static assets served directly by the web server.
- Verify with `curl -I https://yourdomain.com` — the `Server` line should be absent or show only `nginx` or `Apache` with no version.
<!-- The Server header cannot be controlled from HTML.
Apply the fix at your web server, CDN, or hosting control panel
using one of the platform-specific configurations shown below. -->There is no HTML tag to set or suppress the Server header — it is purely a server/CDN-level configuration. Use the server-specific snippets below.
Info disclosure x powered byQuick win
Remove or suppress the X-Powered-By HTTP response header so your server stops advertising which technology stack, hosting platform, or software version powers your store.
On WooCommerce
- WooCommerce runs on WordPress + PHP on your own or managed hosting, so the header can come from PHP, Apache/Nginx, or the host itself.
- Install the free plugin 'HTTP Headers' (by Caspar Hübinger) or 'Security Headers' from WordPress.org: Dashboard → Plugins → Add New → search 'HTTP Headers' → Install → Activate.
- In the plugin settings, add a rule to remove X-Powered-By (most plugins provide a toggle or a 'Remove' action for this specific header).
- Alternatively, if you have server access, add the Apache/Nginx snippet shown in the code examples below to your .htaccess (Apache) or server block (Nginx).
- To suppress it at the PHP level, add header_remove('X-Powered-By'); to your theme's functions.php or a must-use plugin (wp-content/mu-plugins/remove-headers.php).
- Verify via DevTools or an online header checker.
<!-- X-Powered-By is an HTTP response header set by the server, not in HTML markup.
Use the server-side code snippets below for your specific platform. -->There is no HTML tag for this — it is a server-side HTTP response header. Use the server/framework snippets below.
Insecure cookieModerate effort
Set the HttpOnly, Secure, and SameSite flags on all session and authentication cookies to prevent them from being stolen or misused by attackers.
On WooCommerce
- Install the 'Headers & Cookies Security' plugin (e.g. 'WP Headers and Cookies' or 'Sucuri Security') to apply secure cookie flags globally without editing code.
- Alternatively, add the following to your theme's functions.php or a custom plugin (see code snippet): use session_set_cookie_params() before session_start() with httponly=true, secure=true, samesite=Strict.
- For the WordPress session cookie (wordpress_logged_in_*, wordpress_sec_*), add a filter in functions.php using add_filter('auth_cookie_expiration', ...) — but more directly, add ini_set and session_set_cookie_params as shown in the code snippet.
- In your wp-config.php ensure FORCE_SSL_ADMIN is set to true: define('FORCE_SSL_ADMIN', true);
- Go to WP Admin → Settings → General and ensure both WordPress Address and Site Address use https://.
- Verify flags in browser DevTools → Application → Cookies.
<?php
// In functions.php or a custom plugin file:
add_action('init', 'harden_session_cookies', 1);
function harden_session_cookies() {
if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
}
}
// Also add to wp-config.php:
// define('FORCE_SSL_ADMIN', true);
// define('COOKIE_SECURE', true);Add to functions.php or a custom plugin. Applies to PHP/WP session cookies. WooCommerce uses WordPress cookies, so this covers both.
Missing content security policyModerate effort
Add a Content-Security-Policy (CSP) HTTP response header to every page to block unauthorized scripts, styles, and resources from loading in your visitors' browsers.
On WooCommerce
- Install the free 'HTTP Headers' plugin by BTC (available on WordPress.org plugin repository) or the 'Headers Security Advanced & HSTS WP' plugin.
- In WordPress admin, go to Settings → HTTP Headers (or the plugin's menu item), find the Content-Security-Policy field, and enter your policy string.
- Alternatively, add the header via your theme's functions.php or a custom plugin using the send_headers action hook: add_action('send_headers', function(){ header("Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';"); });
- If your host gives you access to Apache, add the header in .htaccess: Header set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none';"
- If your host uses Nginx, add add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none';" always; inside the relevant server {} block in your Nginx config.
- Verify in browser DevTools → Network → select a page → Response Headers.
Missing dmarcQuick win
Add a DMARC DNS TXT record at _dmarc.yourdomain.com to tell receiving mail servers how to handle emails that fail authentication checks for your domain.
On WooCommerce
- WooCommerce runs on WordPress and does not manage DNS. Log in to your hosting provider's control panel (cPanel, Plesk, or a standalone DNS dashboard like Cloudflare).
- In cPanel: navigate to Domains → Zone Editor → click 'Manage' next to your domain → Add Record → Type: TXT, Name: _dmarc, TTL: 3600, Value: v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com → Save.
- Ensure your outgoing WordPress email (via WP Mail SMTP or similar plugin) is authenticated with SPF and DKIM at your mail provider (e.g. SendGrid, Mailgun, Google Workspace) before enforcing a strict DMARC policy.
- Install a plugin like 'WP Mail SMTP' (Settings → WP Mail SMTP → Setup Wizard) to route mail through an authenticated SMTP provider that supports DKIM, then publish DMARC.
_dmarc.yourdomain.com. 3600 IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; ruf=mailto:dmarc-forensic@yourdomain.com; sp=none; adkim=r; aspf=r"Add this as a DNS TXT record at your domain registrar or DNS provider. Replace yourdomain.com and the rua address with your actual domain.
Missing permissions policyQuick win
Add a Permissions-Policy HTTP response header to your store to explicitly restrict which browser features (camera, microphone, geolocation, etc.) pages and embedded third-party scripts are allowed to access.
On WooCommerce
- You are running on WordPress + Apache or Nginx, so you can set headers at the server config or .htaccess level.
- Apache (.htaccess in your WordPress root): Open the .htaccess file via FTP/SFTP or cPanel File Manager. Add the directive shown in the code snippets section inside a <IfModule mod_headers.c> block.
- Nginx (nginx.conf or site config): Add the add_header directive shown in the code snippets section inside the relevant server {} or location / {} block, then run 'nginx -t' and reload nginx.
- WordPress plugin alternative: Install the 'HTTP Headers' plugin (by David Gwyer) or 'Security Headers' plugin from the WordPress plugin directory. Go to Settings → HTTP Headers (or the plugin's settings page) → add a new header named 'Permissions-Policy' with the value 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()'.
- Verify in browser DevTools → Network → HTML document → Response Headers.
<!-- NOT recommended — use a real HTTP header instead. Shown for reference only. -->
<meta http-equiv="Permissions-Policy" content="camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()">Meta tag delivery is not recommended — use HTTP headers instead. Included for reference only; browser support is partial.
Missing referrer policyQuick win
Add a Referrer-Policy HTTP response header set to strict-origin-when-cross-origin to control what URL information browsers send when users navigate away from your store.
On WooCommerce
- Log in to your WordPress admin.
- Install and activate the free plugin 'Headers Security Advanced & HSTS WP' (or 'HTTP Headers' by John Blackbourn) from Plugins → Add New.
- Navigate to the plugin settings and enable / add Referrer-Policy with value strict-origin-when-cross-origin.
- Alternatively, edit your theme's functions.php (or better, a site-specific plugin) and add: add_action('send_headers', function(){ header('Referrer-Policy: strict-origin-when-cross-origin'); });
- For Apache hosts, add 'Header always set Referrer-Policy "strict-origin-when-cross-origin"' inside your .htaccess file or VirtualHost block.
- For Nginx hosts, add 'add_header Referrer-Policy "strict-origin-when-cross-origin" always;' inside your server{} or location{} block in nginx.conf, then reload Nginx.
- Verify with DevTools or securityheaders.com.
<?php
/**
* Add Referrer-Policy security header.
* Add this to your child theme's functions.php or a custom plugin.
*/
add_action( 'send_headers', function() {
header( 'Referrer-Policy: strict-origin-when-cross-origin' );
} );Add to your child theme's functions.php or a site-specific plugin. Sends the header on every WordPress page load.
Missing security txtQuick win
Create and publish a /.well-known/security.txt file with at least a Contact: line so security researchers know how to report vulnerabilities responsibly.
On WooCommerce
- In your WordPress file manager (cPanel File Manager, SFTP, or a plugin like WP File Manager), navigate to the root of your site (the folder containing wp-config.php).
- Create a folder named .well-known if it does not already exist.
- Inside .well-known, upload or create a plain-text file named security.txt containing your contact details (see code snippets).
- Confirm the file is served by visiting https://yourdomain.com/.well-known/security.txt — WordPress should pass through static files without needing any rewrite rules.
- If you get a 404, add a rewrite rule to your .htaccess (Apache) or nginx config to ensure /.well-known/ is served as a static directory.
# Allow .well-known directory
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^.well-known/ - [L]
</IfModule>If you receive a 404 for /.well-known/security.txt on Apache/WordPress, add these lines to your .htaccess BEFORE the # BEGIN WordPress block.
Missing spfQuick win
Add a DNS TXT record containing a valid SPF policy to your domain so that email receivers can verify which servers are authorised to send mail on your behalf.
On WooCommerce
- WooCommerce sends email through your WordPress hosting server's PHP mail function by default — this is the most common cause of SPF failures for WooCommerce stores.
- Install a transactional email plugin such as WP Mail SMTP (free) or FluentSMTP: WordPress Admin → Plugins → Add New → search for 'WP Mail SMTP' → Install → Activate.
- Configure the plugin to route all WordPress/WooCommerce email through an authenticated SMTP service (SendGrid, Mailgun, Gmail/Google Workspace, etc.) — follow the plugin's setup wizard.
- Log in to your domain registrar's DNS panel (where your domain is registered or where DNS is hosted).
- Add a TXT record: Name = '@', Value = v=spf1 include:[your-smtp-provider-spf-hostname] -all. Your SMTP provider's documentation lists the exact include: string (e.g. 'include:sendgrid.net' for SendGrid, 'include:mailgun.org' for Mailgun).
- Verify with MXToolbox SPF Lookup and send a WooCommerce test order email to confirm delivery.
v=spf1 include:_spf.google.com include:sendgrid.net include:spf.mailgun.org -allAdd this as a DNS TXT record at your domain apex ('@'). Replace the include: values with the exact hostnames from each of your email providers' SPF documentation. Only one SPF TXT record is allowed per domain — combine all providers into one record.
Missing strict transport securityQuick win
Add the Strict-Transport-Security (HSTS) response header to every HTTPS page so browsers always use an encrypted connection to your store.
On WooCommerce
- Log in to your WordPress admin. The HSTS header is a server-level response header, not a WordPress setting, so you must set it in one of three places.
- Option A — via .htaccess (Apache servers): Open your root .htaccess file via FTP/SFTP or cPanel File Manager. Add the HSTS block from the Apache snippet below ABOVE the WordPress permalink block.
- Option B — via nginx.conf (Nginx servers): SSH into your server or use your host's control panel to edit your server block config. Add the add_header line from the Nginx snippet below inside your HTTPS server {} block, then reload nginx.
- Option C — via a WordPress plugin: Install the 'HTTP Headers' plugin (by David Gwyer) or 'Security Headers' plugin. Go to Settings → HTTP Headers (or Security Headers), find Strict-Transport-Security, enable it, set max-age to 31536000, and tick includeSubDomains.
- Verify with browser DevTools (Network tab → click any request → Response Headers) that the header appears on all pages including the homepage, shop, and checkout.
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>
# BEGIN WordPress
# (existing WordPress permalink rules go here — do not remove them)
# END WordPressAdd this block to your WordPress root .htaccess file, ABOVE the # BEGIN WordPress comment block.
Missing x content type optionsQuick win
Add the `X-Content-Type-Options: nosniff` HTTP response header to every page and asset your store serves.
On WooCommerce
- Install the free 'Headers Security Advanced & HSTS WP' plugin (or similar: 'HTTP Headers', 'Security Headers') from the WordPress plugin directory: WP Admin → Plugins → Add New → search 'Security Headers'.
- Activate the plugin, go to its settings page, find the `X-Content-Type-Options` option, and enable/set it to `nosniff`. Save.
- Alternatively, edit your theme's `functions.php` (Appearance → Theme File Editor → functions.php) and add: `add_action('send_headers', function(){ header('X-Content-Type-Options: nosniff'); });`
- Or add the header in your `.htaccess` file (Apache host) via Hosting → File Manager → `.htaccess`: add `Header always set X-Content-Type-Options "nosniff"` inside the `<IfModule mod_headers.c>` block.
- For Nginx hosting, add `add_header X-Content-Type-Options 'nosniff' always;` inside the `server {}` block of your site config and reload Nginx.
- Verify with DevTools or securityheaders.com.
<?php
add_action( 'send_headers', function () {
header( 'X-Content-Type-Options: nosniff' );
} );Add to your child theme's functions.php via Appearance → Theme File Editor → functions.php (or use a code-snippets plugin).
Missing x frame optionsQuick win
Add the X-Frame-Options HTTP response header set to DENY or SAMEORIGIN to prevent your store's pages from being embedded in iframes on other sites.
On WooCommerce
- Install the free WordPress plugin 'HTTP Headers' (by David Gwyer) or 'Security Headers' from the WordPress plugin directory: WP Admin → Plugins → Add New → search 'Security Headers' or 'HTTP Headers' → Install & Activate.
- Alternatively, use the popular security plugin Wordfence or iThemes Security, which include security header settings.
- With the 'HTTP Headers' plugin: go to Settings → HTTP Headers → Add New Header → Name: X-Frame-Options, Value: SAMEORIGIN → Save.
- Manual method (advanced): edit your theme's functions.php or a site-specific plugin and add: add_action('send_headers', function(){ header('X-Frame-Options: SAMEORIGIN'); });
- Or add to your server's .htaccess file (Apache): Header always set X-Frame-Options 'SAMEORIGIN' — place this inside the <IfModule mod_headers.c> block.
- Verify by visiting your site and checking Network tab in browser DevTools for the X-Frame-Options response header.
X-Frame-Options: SAMEORIGINThis is an HTTP response header — it must be sent by the server, not a meta tag. It should appear in the HTTP response headers for every page.
Passive scan onlyModerate effort
Set up regular active security testing (DAST) on a staging copy of your store to catch vulnerabilities that passive header/cookie/TLS scans cannot find.
On WooCommerce
- Create a staging environment using your host's built-in staging tool (e.g., WP Engine, Kinsta, SiteGround all offer one-click staging) or use the free 'WP Staging' plugin: Plugins → Add New → search 'WP Staging' → Install & Activate → WP Staging → 'Create new staging site'.
- Ensure the staging site is not indexed (Settings → Reading → 'Discourage search engines') and is either password-protected at the server level or accessible only from your IP.
- Install OWASP ZAP on your local machine (download from zaproxy.org). Launch ZAP → Automated Scan → enter your staging URL.
- Add a 'Context' in ZAP with your WooCommerce test-account login so the scanner can reach the cart, checkout, and My Account pages.
- Run the active scan, note High/Critical findings, then fix them in staging (theme files via Appearance → Theme File Editor, plugin settings, or .htaccess via Plugins → File Manager).
- After fixes pass re-scan, sync or deploy staging to production using your host's push-to-live feature or WP Staging Pro's push function.
<meta name="robots" content="noindex, nofollow">Add this meta tag to your staging site's <head> to prevent search engines from indexing it while DAST scanning is in progress.
Ssl cert expiring soonQuick win
Renew your SSL/TLS certificate before it expires to keep your store secure, trusted, and visible in search results.
On WooCommerce
- WooCommerce itself does not manage SSL — the certificate is controlled by your web host or server.
- Log in to your hosting control panel (cPanel, Plesk, Kinsta, WP Engine dashboard, etc.) and navigate to the SSL/TLS section.
- If your host uses Let's Encrypt (free, auto-renewing 90-day certs): confirm the auto-renewal cron job is active. In cPanel go to Security → SSL/TLS → Manage SSL Sites and check the expiry; in Plesk go to Domains → your domain → SSL/TLS Certificates.
- If your host provides a paid certificate, follow their renewal wizard, complete domain validation, and re-install the certificate through the SSL/TLS section.
- After renewal, visit your wp-admin → Settings → General and confirm 'WordPress Address' and 'Site Address' both start with https://.
- Install the free 'Really Simple SSL' plugin to catch any mixed-content issues that might linger after renewal.
<!-- Place this in your maintenance/holding page only if HTTPS is temporarily unavailable -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">Meta-refresh redirect for maintenance: not applicable to SSL renewal. Instead, use this HTML to display a temporary notice if your cert has lapsed and you need to redirect HTTP traffic while fixing HTTPS.
Ssl cert invalidModerate effort
Install a valid SSL/TLS certificate that exactly matches your store's domain name, so browsers trust your site and customer data is encrypted in transit.
On WooCommerce
- WooCommerce runs on WordPress, so SSL is managed at your web host, not inside WooCommerce itself.
- Log in to your hosting control panel (cPanel, Plesk, or host-specific dashboard). Look for 'SSL/TLS', 'Let's Encrypt', or 'SSL Certificates' section.
- In cPanel: Security → SSL/TLS → Manage SSL Sites. Check which domain the installed certificate covers. If it is the wrong domain, use 'Let's Encrypt SSL' (AutoSSL) or install a new certificate for the correct domain.
- For hosts that offer AutoSSL (e.g. cPanel AutoSSL with Let's Encrypt): Security → SSL/TLS Status → run AutoSSL for your domain.
- After installing the correct certificate, in WordPress Admin go to Settings → General and confirm both WordPress Address and Site Address begin with https:// and use the exact domain the certificate covers.
- Install the 'Really Simple SSL' plugin (WordPress Admin → Plugins → Add New → search 'Really Simple SSL') to automatically redirect HTTP to HTTPS and fix mixed-content issues site-wide.
- Verify with your browser padlock or SSL Labs.
Ssl errorModerate effort
Replace or reissue your SSL/TLS certificate so it is valid for the exact domain name your store uses, eliminating the hostname mismatch error.
On WooCommerce
- WooCommerce runs on WordPress (self-hosted), so SSL is managed at the hosting layer, not inside WordPress itself.
- Log in to your hosting control panel (cPanel, Plesk, Kinsta, WP Engine dashboard, etc.).
- Navigate to the SSL/TLS section (cPanel: Security → SSL/TLS or 'Let's Encrypt SSL').
- Issue or re-issue a certificate that covers BOTH yourstore.com AND www.yourstore.com as SANs — most hosts offer a free Let's Encrypt certificate covering both with one click.
- After installing the new certificate, go to WordPress Admin → Settings → General and ensure both 'WordPress Address' and 'Site Address' start with https://.
- Install the 'Really Simple SSL' plugin if you need help forcing HTTPS site-wide and verifying the certificate is being served correctly.
Ssl not accessibleModerate effort
Enable HTTPS on your store by opening port 443 and installing a valid SSL/TLS certificate so every page is served over a secure connection.
On WooCommerce
- WooCommerce runs on WordPress, which runs on your own hosting — you (or your host) control port 443.
- Log in to your hosting control panel (cPanel, Plesk, or your host's dashboard) and navigate to the SSL/TLS section.
- Install a free Let's Encrypt certificate: in cPanel go to Security → SSL/TLS → Let's Encrypt™ SSL (or use the 'SSL/TLS Status' tool); click 'Issue' next to your domain.
- Ensure your hosting firewall / server security group has port 443 open. In cPanel, check Security → ModSecurity or contact your host to confirm port 443 is not blocked at the network level.
- In WordPress Admin → Settings → General, change both 'WordPress Address (URL)' and 'Site Address (URL)' to https://.
- Install the 'Really Simple SSL' plugin (free, WordPress.org repository) to handle HTTP→HTTPS redirects automatically and fix mixed-content issues.
Vulnerable component axiosModerate effort
Update the Axios JavaScript library to its latest stable release to eliminate known security vulnerabilities in your storefront code.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a code editor / SFTP) and search your active theme and child theme for 'axios' in any .js or .php files (look for wp_enqueue_script calls).
- If a plugin is loading the vulnerable Axios, go to Plugins → check for available updates and apply them; if no update exists, contact the plugin author.
- If your custom theme or child theme loads Axios, open your project locally, run `npm install axios@latest`, rebuild your JS bundle, and upload the compiled file via SFTP to wp-content/themes/your-theme/assets/ (or wherever your JS is served from).
- If Axios is enqueued via a CDN URL in functions.php, update the URL string to the latest version and save.
- Run `npm audit` locally to confirm no remaining Axios vulnerabilities, then test cart/checkout AJAX flows on a staging site before pushing to production.
<!-- Load the latest Axios from jsDelivr CDN (always points to newest stable release) -->
<script src="https://cdn.jsdelivr.net/npm/axios@latest/dist/axios.min.js" integrity="" crossorigin="anonymous"></script>
<!-- OR pin to a specific safe version (recommended for production stability) -->
<script src="https://cdn.jsdelivr.net/npm/axios@1.7.9/dist/axios.min.js" crossorigin="anonymous"></script>Replace any hardcoded Axios CDN script tag in your HTML templates with this — always uses the latest stable version.
Vulnerable component jqueryModerate effort
Update jQuery to the latest stable version (3.7+) to eliminate known cross-site scripting (XSS) and prototype-pollution vulnerabilities that attackers actively exploit.
On WooCommerce
- WordPress core and WooCommerce ship their own bundled copy of jQuery — update WordPress (Dashboard → Updates → Update Now) to the latest version, which upgrades the bundled jQuery automatically.
- In Dashboard → Plugins → Installed Plugins, update WooCommerce and every other plugin to their latest versions, as plugins often enqueue their own jQuery copies.
- Go to Dashboard → Appearance → Themes and update your active theme; also check the theme's child theme for any manually enqueued jQuery.
- If a plugin still loads an old jQuery, install 'Query Monitor' (free plugin) to identify which plugin/theme enqueues it, then contact that developer or deactivate the plugin.
- To force a specific jQuery version site-wide, add a snippet to your child theme's functions.php: deregister the default 'jquery' handle and re-register it pointing to a CDN URL for 3.7.1 (see code snippet below) — but test thoroughly as this can break poorly-coded plugins.
- Use a browser DevTools Network filter for 'jquery' after clearing cache to verify the correct version is loaded.
<?php
/**
* Upgrade jQuery loaded on the front end to the latest CDN version.
* Place in child theme functions.php. Test on a staging site first.
*/
function woo_upgrade_jquery() {
if ( ! is_admin() ) {
wp_deregister_script( 'jquery' );
wp_deregister_script( 'jquery-core' );
wp_register_script(
'jquery',
'https://code.jquery.com/jquery-3.7.1.min.js',
array(),
'3.7.1',
false
);
wp_enqueue_script( 'jquery' );
}
}
add_action( 'wp_enqueue_scripts', 'woo_upgrade_jquery', 100 );Add to your child theme's functions.php. This deregisters the WP-bundled jQuery and loads from CDN. Test on staging before deploying.
Weak spfQuick win
Add a hard-fail (-all) or soft-fail (~all) terminator to your SPF DNS record so that mail servers reject or flag email sent from servers not explicitly authorized by your domain.
On WooCommerce
- WooCommerce is a WordPress plugin; your DNS is managed by your domain registrar or hosting provider (e.g., cPanel, Cloudflare, GoDaddy).
- Log in to your hosting control panel (cPanel → Zone Editor, or Plesk → DNS Settings) or your domain registrar's DNS management page.
- Find the TXT record for your root domain (`@`) starting with `v=spf1`. Click Edit.
- Ensure the record ends with `-all` or `~all`. Save and allow DNS to propagate.
- If you use Cloudflare for DNS: log in to Cloudflare → your domain → DNS tab → find the SPF TXT record → Edit → add `-all` at the end of the value.
v=spf1 include:_spf.google.com include:sendgrid.net -allDNS TXT record value for your root domain (@). Replace the include/redirect mechanisms with the ones your email service provider specifies, and always end with -all or ~all.
X content type options weakQuick win
Set the X-Content-Type-Options response header to exactly `nosniff` (once, not duplicated) on every page and asset your store serves.
On WooCommerce
- Install the free 'HTTP Headers' plugin (author: John Henckel) or 'Headers Security Advanced & CORS' from the WordPress plugin directory.
- WordPress Admin → Settings → HTTP Headers (or the plugin's settings page) — find the `X-Content-Type-Options` field and set it to `nosniff`. Save.
- If you are also setting the header via your server (see below), disable it in one location to prevent duplication.
- Alternatively, edit your `.htaccess` file (Apache) in the WordPress root directory and add: `<IfModule mod_headers.c>` / `Header set X-Content-Type-Options "nosniff"` / `</IfModule>` — but only if you are NOT setting it via a plugin.
- For Nginx hosting, ask your host to add `add_header X-Content-Type-Options "nosniff" always;` to the server block in the nginx config, and remove any plugin that also sets it.
- Verify: use a browser's DevTools Network tab or securityheaders.com to confirm exactly one `nosniff` value.
X frame options weakQuick win
Replace the weak X-Frame-Options header value with DENY or SAMEORIGIN to prevent clickjacking attacks on your store.
On WooCommerce
- Log in to your WordPress admin. Go to Plugins → Add New and search for 'Headers Security Advanced & HSTS WP' or 'HTTP Headers' (by BestWebSoft) — both allow setting custom HTTP headers without server access.
- Alternatively, if you have access to your web server config: for Apache, open your .htaccess file (in the WordPress root) and add: Header always set X-Frame-Options "DENY" and Header always set Content-Security-Policy "frame-ancestors 'none';"
- For Nginx servers, open your nginx.conf or the site's server block and add inside the server{} block: add_header X-Frame-Options "DENY" always; and add_header Content-Security-Policy "frame-ancestors 'none';" always;
- If using a managed WordPress host (WP Engine, Kinsta, SiteGround), check their dashboard for a 'Security Headers' or 'HTTP Headers' panel — most managed hosts provide a UI for this.
- Install a caching plugin like WP Rocket or W3 Total Cache only after setting headers to ensure cached responses also include the updated headers.
- Verify with DevTools → Network → Response Headers on your site's homepage and checkout page.
<?php
/**
* Set X-Frame-Options and CSP frame-ancestors headers.
* Add to functions.php or a custom plugin file.
*/
add_action( 'send_headers', 'my_store_set_framing_headers' );
function my_store_set_framing_headers() {
header( 'X-Frame-Options: DENY' );
header( "Content-Security-Policy: frame-ancestors 'none';" );
}Add to your theme's functions.php or a site-specific plugin. Sets the header on every WordPress/WooCommerce page response.
Site Lifecycle · 12 fixes
Cms versionModerate effort
Update your CMS and ecommerce platform to the latest stable version immediately to close known security vulnerabilities and maintain vendor support.
On WooCommerce
- WooCommerce is a WordPress plugin, so WordPress core must be updated first (see WordPress.org steps above).
- In wp-admin go to Dashboard > Updates. WooCommerce updates appear in the Plugins section.
- Before updating WooCommerce, note the current version, then click 'Update' next to WooCommerce.
- After updating WooCommerce, navigate to WooCommerce > Status > System Status and verify there are no red warnings about database updates; if prompted to run a database update, do so.
- Test add-to-cart, checkout, and payment flows thoroughly after each WooCommerce major version upgrade.
- Keep all WooCommerce extensions (payments, shipping, etc.) updated via Dashboard > Updates > Plugins to avoid compatibility gaps.
<!-- REMOVE or suppress this tag from your <head> — it advertises your CMS version to attackers -->
<!-- <meta name="generator" content="WordPress 5.8.1" /> -->
<!-- In WordPress, add this to your theme's functions.php to remove it: -->
<!-- remove_action('wp_head', 'wp_generator'); -->If your site injects a generator meta tag exposing your CMS version publicly, remove it to reduce information exposure to attackers. This does not replace upgrading — it is a secondary hardening step.
Domain expiryQuick win
Log in to your domain registrar, confirm your domain's expiry date, and enable auto-renew so your store never goes offline due to an expired domain.
On WooCommerce
- WooCommerce itself does not manage domains — your domain was registered separately (e.g. GoDaddy, Namecheap, your web host).
- Log in to your registrar or hosting control panel (cPanel/Plesk), find the domain, and enable auto-renew.
- If your domain was included free with a hosting plan (e.g. Bluehost, SiteGround), log in to the hosting account dashboard → Domains section → find the domain → toggle auto-renew ON and verify a valid payment method is saved.
<!-- No HTML change required. Domain renewal is managed at your domain registrar's dashboard (e.g. GoDaddy, Namecheap, Cloudflare Registrar). Log in, find the domain, enable auto-renew, and confirm a valid payment method is on file. -->No code change is needed on your website — this fix is completed entirely in your registrar's dashboard. There is no markup or configuration file involved.
Gtm auditModerate effort
Install Google Tag Manager on your store and configure it to fire GA4 with full ecommerce event tracking (view_item, add_to_cart, purchase, and more).
On WooCommerce
- Install the free 'GTM4WP' plugin (by Thomas Geiger) from the WordPress plugin directory: Plugins › Add New › search 'GTM4WP'.
- Go to Settings › Google Tag Manager, enter your GTM Container ID (GTM-XXXXXXX), enable 'Track eCommerce', and save.
- GTM4WP automatically populates a full WooCommerce data layer with view_item, add_to_cart, begin_checkout, and purchase events.
- In GTM, create GA4 Event tags listening for those data layer events and publish.
- Verify using GTM Preview mode and GA4 DebugView.
<?php
// Add GTM <script> to <head>
function my_gtm_head() {
?>
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
<!-- End Google Tag Manager -->
<?php
}
add_action( 'wp_head', 'my_gtm_head', 1 );
// Add GTM <noscript> after <body>
function my_gtm_body() {
?>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<?php
}
add_action( 'wp_body_open', 'my_gtm_body', 1 );
?>Only needed if you are NOT using GTM4WP plugin — add to your child theme's functions.php.
Html langQuick win
Add a lang attribute to your site's <html> tag that matches the primary language of your store's content.
On WooCommerce
- In WordPress admin, go to Appearance → Theme File Editor (or use a local FTP/SFTP client).
- Open your active (child) theme's header.php file.
- Find the <html line — WordPress outputs this via the language_attributes() template tag, which automatically includes the correct lang attribute.
- Confirm the line reads: <html <?php language_attributes(); ?>>
- If it is missing, add it exactly as above. WordPress reads the site language from Settings → General → Site Language and injects it automatically via this tag.
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>In your (child) theme's header.php — language_attributes() outputs lang="en" (or whatever language is set in WordPress Settings → General).
Lifecycle oos schema not updatedModerate effort
Update the `offers.availability` value in your Product structured data from `InStock` to `OutOfStock` (or `PreOrder`) whenever a product sells out, so Google's records always match your actual inventory.
On WooCommerce
- WooCommerce outputs Product schema via its built-in structured data class (`WC_Structured_Data`), which reads live stock status automatically — so the fix is usually ensuring no plugin or snippet is overriding it.
- In your WordPress admin, go to WooCommerce → Settings → Products → Inventory and confirm 'Manage stock' is enabled so WooCommerce knows the true stock status.
- Check for an SEO plugin (Yoast SEO, Rank Math, or Schema Pro) that may be outputting a second Product schema block with a hardcoded availability. Go to the plugin's settings and ensure it reads stock from WooCommerce rather than a static value.
- If you have a custom `woocommerce_structured_data_product` filter in your theme's `functions.php` or a child theme, review and correct it to use `$product->is_in_stock()` for the availability value.
- Use the Yoast SEO Schema tab (if installed) on an out-of-stock product's edit screen to preview the schema output and confirm it says `OutOfStock`.
- Validate with Google's Rich Results Test.
<?php
/**
* Dynamically correct the availability field in WooCommerce Product schema.
* Add to: wp-content/themes/your-child-theme/functions.php
*/
add_filter( 'woocommerce_structured_data_product', 'fix_wc_schema_availability', 10, 2 );
function fix_wc_schema_availability( $markup, $product ) {
if ( ! isset( $markup['offers'] ) ) {
return $markup;
}
$availability = $product->is_in_stock()
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock';
if ( is_array( $markup['offers'] ) && isset( $markup['offers'][0] ) ) {
// Multiple offers (variable product)
foreach ( $markup['offers'] as $key => $offer ) {
$markup['offers'][ $key ]['availability'] = $availability;
}
} else {
// Single offer (simple product)
$markup['offers']['availability'] = $availability;
}
return $markup;
}Add this filter to your child theme's functions.php to override the availability value in WooCommerce's built-in structured data output.
Lifecycle orphaned productsModerate effort
Add internal links pointing to every orphaned product page from relevant category pages, collection pages, or other on-site content so search crawlers and shoppers can discover them.
On WooCommerce
- In WordPress Admin, go to Products → All Products and open each orphaned product.
- In the 'Product categories' panel on the right, check every relevant category. Save/Update the product.
- To create a new category, go to Products → Categories, add the category, then assign the orphaned products to it.
- For 'Related products,' WooCommerce automatically shows related items based on shared categories and tags — so assigning the correct categories (step 2) immediately improves related product visibility.
- For manual cross-links, install the free 'YITH WooCommerce Frequently Bought Together' or 'WooCommerce Product Recommendations' plugin and explicitly link orphaned products to bestsellers.
- Add the category to your navigation via Appearance → Menus if it was missing from the nav.
<?php
/**
* Display orphaned/rescued products on single product pages.
* Tag the orphaned products with 'featured-rescue' in WooCommerce.
*/
add_action( 'woocommerce_after_single_product_summary', 'show_rescued_products', 15 );
function show_rescued_products() {
$args = array(
'post_type' => 'product',
'posts_per_page' => 4,
'tax_query' => array(
array(
'taxonomy' => 'product_tag',
'field' => 'slug',
'terms' => 'featured-rescue',
),
),
);
$products = new WP_Query( $args );
if ( $products->have_posts() ) {
echo '<section class="rescued-products"><h2>You May Also Like</h2><ul>';
while ( $products->have_posts() ) {
$products->the_post();
$product = wc_get_product( get_the_ID() );
echo '<li><a href="' . esc_url( get_permalink() ) . '">' . esc_html( get_the_title() ) . '</a></li>';
}
echo '</ul></section>';
wp_reset_postdata();
}
}Add to your child theme's functions.php to display a 'You may also like' row of previously orphaned products (tagged 'featured-rescue') below every single product page.
Lifecycle products missing from sitemapQuick win
Add all canonical product URLs to your XML sitemap so search engines discover and index every product page quickly.
On WooCommerce
- If you use Yoast SEO (most common): go to WordPress Admin → Yoast SEO → Settings → Site features → XML Sitemaps → confirm it is toggled ON.
- Visit yourdomain.com/product-sitemap.xml (Yoast) or yourdomain.com/wp-sitemap.xml (WordPress core) to confirm product URLs are listed.
- In WordPress Admin → Products, check that missing products have Status = 'Published' (not Draft, Private, or Pending).
- In Yoast SEO → Settings → Content types → Products, ensure 'Show in search results' is set to 'Yes' — if it's 'No', products are noindexed and excluded from the sitemap.
- If using Rank Math instead: go to Admin → Rank Math → Sitemap Settings → Products → toggle ON 'Include in Sitemap'.
- Regenerate the sitemap by saving Yoast/Rank Math settings, then submit the product sitemap URL in Google Search Console → Indexing → Sitemaps.
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- Example product page entry -->
<url>
<loc>https://www.example.com/products/blue-running-shoes</loc>
<lastmod>2024-06-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<!-- Repeat one <url> block per canonical product page -->
<url>
<loc>https://www.example.com/products/red-yoga-mat</loc>
<lastmod>2024-05-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>Standard XML sitemap structure. Place this at yourdomain.com/sitemap.xml (or reference it from a sitemap index file).
Lifecycle products too deepModerate effort
Move deeply buried product pages closer to the homepage by flattening your site's navigation structure so every product is reachable within 4 clicks (ideally 3) from the homepage.
On WooCommerce
- In your WordPress admin go to Products → Categories.
- Identify categories that are children of children (grandchild categories) — each nesting level adds a click.
- Edit grandchild categories and change their 'Parent Category' to a top-level category, or set Parent to 'None' to make them top-level.
- Go to Appearance → Menus, open your primary menu, and add the previously nested categories directly as top-level menu items or as direct children (one level deep) of a top-level item.
- On the Homepage (Pages → Home, or via the block/Elementor/etc. editor), add a 'Products' block or widget and filter it by the affected category — this creates a direct 1-click path.
- Install Rank Math or Yoast SEO and use their internal linking suggestions to add links from popular posts/pages directly to buried products.
<?php
/**
* Surface previously deep products on the homepage via a shortcode.
* Use [featured_deep_products ids="101,202,303"] in any page/widget.
*/
function fsp_featured_products_shortcode( $atts ) {
$atts = shortcode_atts(
array( 'ids' => '' ),
$atts,
'featured_deep_products'
);
if ( empty( $atts['ids'] ) ) {
return '';
}
$product_ids = array_map( 'absint', explode( ',', $atts['ids'] ) );
$query_args = array(
'post_type' => 'product',
'post__in' => $product_ids,
'posts_per_page' => count( $product_ids ),
'orderby' => 'post__in',
);
$query = new WP_Query( $query_args );
if ( ! $query->have_posts() ) {
return '';
}
ob_start();
echo '<ul class="fsp-products woocommerce columns-4">';
while ( $query->have_posts() ) {
$query->the_post();
global $product;
echo '<li class="product">';
echo '<a href="' . esc_url( get_permalink() ) . '">';
echo get_the_post_thumbnail( get_the_ID(), 'woocommerce_thumbnail' );
echo '<span class="woocommerce-loop-product__title">' . esc_html( get_the_title() ) . '</span>';
echo '<span class="price">' . wp_kses_post( $product->get_price_html() ) . '</span>';
echo '</a>';
echo '</li>';
}
echo '</ul>';
wp_reset_postdata();
return ob_get_clean();
}
add_shortcode( 'featured_deep_products', 'fsp_featured_products_shortcode' );Add this to your child theme's functions.php or a site-specific plugin to output a featured products shortcode block on the homepage programmatically.
Mixed contentModerate effort
Ensure every resource (images, scripts, stylesheets, fonts, iframes) on your store is loaded over HTTPS, never HTTP, so browsers never block or warn about mixed content.
On WooCommerce
- In WordPress Admin, go to Settings → General and confirm both 'WordPress Address (URL)' and 'Site Address (URL)' begin with 'https://'.
- Install the free plugin 'Better Search Replace' (Plugins → Add New → search 'Better Search Replace'). Run a search for 'http://yourdomain.com' and replace with 'https://yourdomain.com' across all database tables — tick 'Run as dry run' first to preview.
- Also search for any absolute 'http://' third-party resource URLs in your theme files: go to Appearance → Theme File Editor (or via FTP/cPanel) and search template files for 'http://'.
- If you use a page builder (Elementor, Divi, etc.), open affected pages and update any manually entered HTTP image/video URLs.
- Flush caches (caching plugin + server cache) and re-test with DevTools Console.
<?php
add_filter( 'script_loader_src', 'force_https_asset_url', 20 );
add_filter( 'style_loader_src', 'force_https_asset_url', 20 );
add_filter( 'woocommerce_get_image_url', 'force_https_asset_url', 20 );
function force_https_asset_url( $url ) {
if ( is_string( $url ) && strpos( $url, 'http://' ) === 0 ) {
$url = str_replace( 'http://', 'https://', $url );
}
return $url;
}
?>Add to child theme's functions.php — ensures WooCommerce asset URLs are also upgraded.
Mobile viewportQuick win
Add a responsive viewport meta tag to every page so mobile browsers render your site at the correct width and scale.
On WooCommerce
- In your WordPress admin, go to Appearance → Theme File Editor (or use a child theme via FTP/SFTP).
- Open header.php in your active (child) theme.
- Search for `<meta name="viewport">`. Most modern themes include it; if yours does not, add `<meta name="viewport" content="width=device-width, initial-scale=1">` directly after `<head>` or inside the `wp_head()` hook.
- Alternatively, add it via a plugin: install 'Insert Headers and Footers' (WPCode), go to Code Snippets → Header, and paste the tag there — no theme file editing required.
- Save. Verify by viewing the page source on your phone or using Chrome DevTools device toolbar.
<?php
// Add to your child theme's functions.php
// Only needed if your theme's header.php is missing the viewport tag.
add_action( 'wp_head', 'my_theme_viewport_meta', 1 );
function my_theme_viewport_meta() {
echo '<meta name="viewport" content="width=device-width, initial-scale=1">' . "\n";
}
?>Paste into child theme's functions.php, or use WPCode plugin → Add Snippet → Header location.
Server versionQuick win
Remove or suppress HTTP response headers that advertise your server software name and version to the public internet.
On WooCommerce
- In your WordPress root (or in a must-use plugin), add this to suppress PHP/WordPress disclosure: add_filter('wp_headers', function($headers){ unset($headers['X-Powered-By']); return $headers; });
- In your server config (Apache): add 'Header unset X-Powered-By' and 'ServerTokens Prod' and 'ServerSignature Off' inside your VirtualHost or .htaccess file.
- In your server config (Nginx): add 'server_tokens off;' in the http{} or server{} block, and 'more_clear_headers X-Powered-By;' (requires ngx_headers_more module) or handle via FastCGI params.
- If using a managed host (WP Engine, Kinsta, Cloudways), check their dashboard Security or Server Settings panel — most expose a toggle or do it automatically. Contact support to confirm.
- Install the 'HTTP Headers' WordPress plugin (free, by 8Degree Themes) for a no-code UI to remove specific response headers without editing server files.
- Verify with DevTools or a header-checker that 'X-Powered-By: PHP/x.x' and 'Server: Apache/x.x' are gone.
<!-- HTTP response headers cannot be set via HTML meta tags.
Use the server-side or CDN configuration snippets below. -->Meta tags cannot suppress HTTP response headers — this must be done at the server/CDN layer. The snippets below cover the most common server/framework approaches.
Ssl expiryQuick win
Renew your TLS/SSL certificate before it expires and enable automatic renewal so your store never goes offline or shows security warnings.
On WooCommerce
- WooCommerce itself does not manage SSL — the certificate is controlled by your web hosting provider.
- Log in to your hosting control panel (cPanel, Plesk, or your host's dashboard) and navigate to the SSL/TLS section.
- If your host offers a free Let's Encrypt certificate, look for an 'AutoSSL' toggle (cPanel) or 'Free SSL' option and ensure it is enabled.
- If managing manually: SSH into your server and run `sudo certbot renew --dry-run` to test; then confirm the cron job at `/etc/cron.d/certbot` or `sudo systemctl status certbot.timer` is active.
- Some managed WordPress hosts (WP Engine, Kinsta, SiteGround) have a one-click SSL renewal button in their own dashboards — check your host's specific control panel.
<?php
/**
* Force HTTPS for all WordPress/WooCommerce pages.
* Add to functions.php or a site-specific plugin.
* NOTE: A server-level 301 redirect is preferred; use this only if you cannot edit server config.
*/
add_action( 'template_redirect', function() {
if ( ! is_ssl() ) {
wp_redirect( 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], 301 );
exit();
}
} );Add to your theme's functions.php or a site-specific plugin to force HTTPS on all WordPress/WooCommerce pages.
Generative Engine Optimization · 9 fixes
Ai crawler blockedQuick win
Your robots.txt explicitly blocks one or more AI assistants from crawling your store, so those engines can't discover, index, or cite your products when shoppers ask for recommendations.
On WooCommerce
- If a physical robots.txt exists at your site root, edit it via SFTP/your host file manager.
- Otherwise edit it in Yoast SEO (Tools > File editor) or Rank Math (General Settings > Edit robots.txt).
- Remove the 'Disallow: /' line under the AI bot's 'User-agent' block, or add an explicit 'User-agent: <bot>' followed by 'Allow: /'.
- Save and load https://yoursite.com/robots.txt to verify.
Ai crawler not specifiedQuick win
Your robots.txt doesn't explicitly allow several AI crawlers. They aren't blocked, but a number of AI engines act on explicit permission, so naming them removes any ambiguity.
On WooCommerce
- If a physical robots.txt exists at your site root, edit it via SFTP/your host file manager.
- Otherwise edit it in Yoast SEO (Tools > File editor) or Rank Math (General Settings > Edit robots.txt).
- Remove the 'Disallow: /' line under the AI bot's 'User-agent' block, or add an explicit 'User-agent: <bot>' followed by 'Allow: /'.
- Save and load https://yoursite.com/robots.txt to verify.
Geo brand name inconsistentModerate effort
Your brand name appears in multiple different formats across your site (schema, og:site_name, footer, logo). AI engines may treat the variants as separate entities.
On WooCommerce
- WooCommerce + Yoast/Rank Math output Product and Organization schema automatically; make sure the SEO plugin's schema/social settings are filled in (org name, logo, social profiles for sameAs).
- For missing Product fields, ensure the product has price, image, SKU, and brand set in the product data tabs.
- Validate with Google's Rich Results Test.
Geo eeat trust signals weakModerate effort
Your store is missing trust signals (E-E-A-T) that AI engines and shoppers look for before recommending a store: clear policies, real contact details, and a credible business identity.
On WooCommerce
- WooCommerce + Yoast/Rank Math output Product and Organization schema automatically; make sure the SEO plugin's schema/social settings are filled in (org name, logo, social profiles for sameAs).
- For missing Product fields, ensure the product has price, image, SKU, and brand set in the product data tabs.
- Validate with Google's Rich Results Test.
Geo faqpage missing productsModerate effort
Your product pages don't use FAQPage schema, so AI assistants can't pull direct answers to common buyer questions (sizing, materials, shipping) about your products.
On WooCommerce
- WooCommerce + Yoast/Rank Math output Product and Organization schema automatically; make sure the SEO plugin's schema/social settings are filled in (org name, logo, social profiles for sameAs).
- For missing Product fields, ensure the product has price, image, SKU, and brand set in the product data tabs.
- Validate with Google's Rich Results Test.
Geo price not crawlableModerate effort
Your product prices appear to be rendered by JavaScript and may not be present in the static HTML, so AI crawlers (which often don't run JS) can't read or cite them.
On WooCommerce
- WooCommerce + Yoast/Rank Math output Product and Organization schema automatically; make sure the SEO plugin's schema/social settings are filled in (org name, logo, social profiles for sameAs).
- For missing Product fields, ensure the product has price, image, SKU, and brand set in the product data tabs.
- Validate with Google's Rich Results Test.
Geo schema org missing sameasModerate effort
Your Organization schema has no sameAs links to your social profiles, Wikipedia, or other authoritative sources - so AI engines can't confidently connect the dots about your brand.
On WooCommerce
- WooCommerce + Yoast/Rank Math output Product and Organization schema automatically; make sure the SEO plugin's schema/social settings are filled in (org name, logo, social profiles for sameAs).
- For missing Product fields, ensure the product has price, image, SKU, and brand set in the product data tabs.
- Validate with Google's Rich Results Test.
Geo schema product incompleteModerate effort
Your Product structured data is missing fields AI engines rely on to cite your products accurately (price, availability, currency, brand, ratings).
On WooCommerce
- WooCommerce + Yoast/Rank Math output Product and Organization schema automatically; make sure the SEO plugin's schema/social settings are filled in (org name, logo, social profiles for sameAs).
- For missing Product fields, ensure the product has price, image, SKU, and brand set in the product data tabs.
- Validate with Google's Rich Results Test.
Llms txt missingQuick win
Your store has no llms.txt - the emerging standard (like robots.txt, but for AI) that tells language models which content to read and cite. SEOLZ has generated a ready-to-upload file from your own pages.
On WooCommerce
- Create a plain text file named llms.txt and paste the generated content.
- Upload it to your WordPress site root (same folder as robots.txt / wp-config.php) via SFTP or your host file manager.
- Verify at https://yoursite.com/llms.txt.