Every WordPress.org fix we catalog
SEOLZ catalogs 66 fixes for WordPress.org across 5 areas — SEO, answer-engine readiness, accessibility, security and site-health. Each lists the exact steps for WordPress.org, with a link to the official docs.
SEO · 20 fixes
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 WordPress.org
- Install WP Rocket or Perfmatters (Plugins → Add New → search) for an all-in-one performance plugin that handles preloading, CSS/JS deferral, and lazy-load exclusions.
- Install Imagify or ShortPixel to auto-convert existing and newly uploaded images to WebP with automatic <picture> tag fallback — no manual image re-export needed.
- In WP Rocket → Media: find 'Exclude from LazyLoad' and add the CSS selector or filename of your hero image (e.g., .hero-image, hero-banner.jpg) so it loads eagerly.
- In WP Rocket → Preload: enable 'Preload Links' and use the 'Prefetch DNS Requests' field to add your CDN domain.
- Manually add a preload tag for maximum control: Appearance → Theme File Editor → header.php → inside <head> add: <link rel='preload' as='image' href='/wp-content/uploads/hero.webp' fetchpriority='high'>.
- Verify with PageSpeed Insights mobile score and WP Rocket's Cache Lifespan settings to ensure the optimized page is served from cache to all visitors.
<?php
/**
* Preload the LCP hero image.
* Add to child theme's functions.php
*/
add_action( 'wp_head', 'my_preload_lcp_image', 1 );
function my_preload_lcp_image() {
// Replace with your actual WebP hero image URL
$hero_url = get_template_directory_uri() . '/assets/images/hero-banner.webp';
echo '<link rel="preload" as="image" href="' . esc_url( $hero_url ) . '" fetchpriority="high">' . "\n";
}
/**
* Remove lazy-loading from the first (LCP) image.
* Works in WordPress 5.9+
*/
add_filter( 'wp_lazy_loading_enabled', function( $default, $tag_name, $context ) {
// Disable lazy-load for images in the hero/banner context if you control the context name
return $default;
}, 10, 3 );
// Alternatively, add fetchpriority="high" and loading="eager" directly in your template's hero <img> tag.Add to your child theme's functions.php to preload the LCP image. Replace the image URL with your actual hero image path.
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 WordPress.org
- Install Yoast SEO or Rank Math if not already active (Plugins → Add New → search → Install → Activate).
- For any post or page: open the editor → scroll to the Yoast SEO / Rank Math panel → click 'Edit snippet' (Yoast) or expand 'General' (Rank Math) → type your unique description in the Meta Description field → Update/Publish.
- For bulk editing: Yoast SEO → Tools → Bulk editor → Descriptions tab — edit descriptions for all content in a single table.
- For archive/author pages: Yoast → Settings → Content types → Authors/Archives → Meta description template.
- For paginated archives (/page/2 etc.): Yoast and Rank Math automatically add `rel=canonical` to page 1 by default, preventing duplicate description penalties for pagination.
<head>
<meta charset="UTF-8">
<meta name="description" content="Shop women's running shoes at Acme Store — lightweight, cushioned designs for road and trail. Free shipping on orders over $50.">
<title>Women's Running Shoes | Acme Store</title>
</head>Place inside the <head> element of every page. Replace the content value with a unique 120–160 character description for that specific page.
Duplicate titleModerate effort
Give every page a unique, descriptive title tag that accurately reflects that page's specific content.
On WordPress.org
- Install Yoast SEO or Rank Math if not already active.
- For individual posts/pages: open the editor, scroll to the Yoast SEO / Rank Math panel, click 'Edit snippet', and type a unique SEO title.
- For sitewide templates: Yoast SEO → Search Appearance → Content Types / Taxonomies; set patterns with %%title%% %%sep%% %%sitename%% so every page outputs a distinct title.
- Use Yoast's Bulk Editor (Yoast SEO → Tools → Bulk Editor → Titles) to review and edit all page titles in one place without opening each post.
- If using a theme that outputs its own <title> tag and conflicts with the SEO plugin, ensure the theme declares add_theme_support('title-tag') in functions.php and removes any hardcoded <title> in header.php.
<head>
<title>Merino Wool Running Socks – Free UK Shipping | BrandName</title>
</head>Place inside <head>. Make the title unique per page — include the specific item name and brand.
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 WordPress.org
- Media Library: Admin → Media → Library → click any image → fill in the 'Alt Text' field in the right sidebar → Update.
- Gutenberg block editor: Click an Image block in a post or page → in the Block settings sidebar (right panel) → find 'Alt text (alternative text)' → fill it in. Mark as decorative via the toggle if purely ornamental.
- Classic editor: Click an image in the editor → click the pencil (Edit) icon → fill the 'Alternative Text' field in the Image Details dialog → Update.
- For bulk alt text: Install the free 'SEO Optimized Images' or 'Bulk Auto Image Alt Text' plugin → configure auto-fill rules using product/post title or custom templates → run bulk update.
<!-- BEFORE (missing alt — bad) -->
<img src="/images/red-leather-bag.jpg">
<!-- AFTER (descriptive alt — correct) -->
<img src="/images/red-leather-bag.jpg" alt="Red leather crossbody bag with gold buckle clasp">
<!-- Decorative image (intentionally empty alt — correct) -->
<img src="/images/divider-line.png" alt="">Place this wherever an <img> tag appears in your HTML. Replace the placeholder text with your real 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 WordPress.org
- Install Yoast SEO or Rank Math (Plugins → Add New → search for the plugin → Install → Activate).
- Both plugins output self-referencing canonical tags automatically for every post, page, and custom post type.
- To override the canonical on a specific page: open the post/page editor → find the Yoast SEO or Rank Math meta box → click 'Advanced' → enter the preferred URL in the 'Canonical URL' field.
- If you are using a custom theme without an SEO plugin, add `<link rel="canonical" href="<?php echo esc_url( get_permalink() ); ?>">` inside the `<head>` section of your `header.php` file.
<link rel="canonical" href="https://www.yourstore.com/your-page/" />Paste this inside the <head> element of every page. Replace the href value with the full, absolute preferred URL of that specific 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 WordPress.org
- For pages and posts: the page/post Title field in the Block Editor (Gutenberg) or Classic Editor automatically renders as the H1 via your theme's single.php or page.php template. Ensure the title is filled in.
- If your theme does not output the title as an H1, install a child theme and edit the relevant template file to wrap the_title() in <h1> tags.
- For pages where the title is intentionally hidden but you still need an H1, add a Heading block (set to H1) at the top of the editor content area.
- Plugins like Rank Math SEO or Yoast SEO will flag pages with missing H1s under their on-page analysis — use these to audit all pages at once.
<h1>Women's Running Shoes</h1>Place exactly one of these per page, as the first heading in the main content area.
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 WordPress.org
- Install Yoast SEO or Rank Math (Admin → Plugins → Add New).
- For any post or page: open the editor → find the Yoast SEO / Rank Math block at the bottom → click 'Edit snippet' → type in the Meta description field → Update/Publish.
- For the homepage (static page): edit that page the same way; if using 'Latest Posts' as homepage, go to SEO (Yoast) → Search Appearance → Content Types → Posts → set a homepage description template.
<head>
<meta charset="UTF-8">
<meta name="description" content="Shop our full range of handmade leather wallets. Free shipping on orders over $50. Slim, durable & personalised — order yours today.">
<title>Handmade Leather Wallets | YourStoreName</title>
</head>Place this tag inside the <head> element of every HTML page. Keep the content between 120–160 characters.
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 WordPress.org
- Install 'Yoast SEO' or 'Rank Math SEO' (Plugins → Add New) — both provide complete Open Graph support including per-page og:image fields.
- In Yoast SEO: go to Yoast SEO → Social → Facebook → enable Open Graph → set a default og:image. For individual pages/posts, use the Yoast meta box → Social tab → upload a Facebook image.
- In Rank Math: go to Rank Math → General Settings → Open Graph → enable. Per page, use the Rank Math sidebar in the block editor → Social tab → Facebook Image.
- For pages without a plugin, add the tag manually in your theme's header.php inside the <head> tag.
- Ensure your theme or plugin outputs the tag as: <meta property="og:image" content="FULL-URL-TO-IMAGE">.
- 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 WordPress.org
- Install the Yoast SEO or Rank Math plugin from Plugins → Add New if not already present.
- With Yoast SEO: ensure Open Graph output is enabled at SEO → Social → Facebook → toggle 'Add Open Graph meta data' ON.
- Edit any post/page/product → in the Yoast SEO meta box → Social tab → set the Facebook/OG title, or leave it blank to auto-inherit the SEO title.
- For theme-level manual addition (without a plugin): edit your child theme's header.php and add inside <head>: <meta property="og:title" content="<?php wp_title('|', true, 'right'); echo get_bloginfo('name'); ?>" />
- Verify with the Facebook Sharing Debugger.
<?php
// Add og:title — skip if Yoast or Rank Math is active
add_action( 'wp_head', 'theme_og_title_tag', 1 );
function theme_og_title_tag() {
if ( function_exists( 'wpseo_init' ) || function_exists( 'rank_math' ) ) {
return; // Let the SEO plugin handle it
}
if ( is_singular() ) {
$title = get_the_title() . ' | ' . get_bloginfo( 'name' );
} else {
$title = get_bloginfo( 'name' ) . ' | ' . get_bloginfo( 'description' );
}
echo '<meta property="og:title" content="' . esc_attr( $title ) . '" />' . "\n";
}Add to your child theme's functions.php only when not using an SEO plugin like Yoast or Rank Math.
Missing titleQuick win
Add a unique, descriptive title tag (30–60 characters) to every page that is missing one.
On WordPress.org
- Install Yoast SEO (free) or Rank Math (free) from Plugins → Add New.
- Edit any post or page → scroll to the Yoast SEO or Rank Math meta box → click into the 'SEO title' field and type your title.
- For site-wide title templates: Yoast SEO → Settings → Content types; Rank Math → Titles & Meta → select content type and set the title format.
- Click Update/Publish.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Slim Bifold Leather Wallet | BrandName</title>
</head>
<body>
<!-- page content -->
</body>
</html>Place this inside the <head> element of every HTML page. Keep the text between 30–60 characters.
Multiple h1Moderate effort
Remove all but one H1 tag from each page so every page has exactly one clear, descriptive primary heading.
On WordPress.org
- In WordPress admin, navigate to Appearance → Theme File Editor. Always work in a child theme to avoid losing changes on updates.
- Open the template for the affected page type (single.php, page.php, index.php, archive.php) and search for '<h1'.
- Ensure only the primary content title — <?php the_title('<h1>', '</h1>'); ?> — uses the H1 tag. Convert all others to <h2> or <h3>.
- If using Gutenberg (block editor): open the page/post, click any Heading block that is set to H1 but is not the main title, and use the block toolbar to change it to H2 or H3.
- Install a heading-audit plugin or use the 'Headings Map' browser extension to scan all flagged pages after saving.
- Use an SEO plugin (Rank Math or Yoast SEO) to get a per-post H1 check under the 'SEO Analysis' tab in the post editor.
<!-- CORRECT: One H1 per page -->
<h1>Blue Running Shoes – Men's Lightweight Trainers</h1>
<!-- Major sections use H2 -->
<h2>Product Details</h2>
<p>Breathable mesh upper, responsive foam midsole...</p>
<h2>Customer Reviews</h2>
<!-- Sub-sections within a major section use H3 -->
<h3>Top Review: 5 Stars</h3>
<p>Best shoes I've ever bought!</p>
<!-- WRONG: Multiple H1 tags — do NOT do this -->
<!--
<h1>Blue Running Shoes</h1>
<h1>Product Details</h1>
<h1>Customer Reviews</h1>
-->Correct semantic heading structure for an ecommerce page — one H1, everything else H2 or lower.
Noindex detectedQuick win
Remove or replace the noindex directive on any page you want to appear in Google search results.
On WordPress.org
- First, check Settings → Reading in WordPress Admin — uncheck 'Discourage search engines from indexing this site' if it is checked. This affects the whole site.
- If using Yoast SEO: Check SEO → Settings → Content types for global noindex, and the per-post Yoast panel → Advanced tab → 'Allow search engines to show this post in search results?' → set to 'Yes'.
- If using Rank Math: Check Rank Math → Titles & Meta → post type settings, and per-post Rank Math panel → Advanced → Robot Meta → remove noindex.
- If using All in One SEO (AIOSEO): Check AIOSEO → Search Appearance → Content Types; and per-page AIOSEO meta box → Advanced tab → Robot Settings → uncheck 'No Index'.
- Check theme files (header.php, functions.php) for any hardcoded noindex meta tags and remove them.
- Submit to Google Search Console → URL Inspection → Request Indexing after saving.
<?php
add_action( 'wp_head', 'force_index_follow_robots', 1 );
function force_index_follow_robots() {
if ( is_singular() || is_archive() || is_home() ) {
echo '<meta name="robots" content="index,follow">' . PHP_EOL;
}
}Add to your child theme's functions.php ONLY if you need to programmatically force index,follow on all public pages. Remove any conflicting hardcoded noindex output first.
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 WordPress.org
- Canonical tags in WordPress are controlled by your active SEO plugin. Identify which one you use: Yoast SEO, Rank Math, All in One SEO, or SEOPress.
- For Yoast SEO: Edit the affected post/page, scroll to the Yoast SEO meta box, click the 'Advanced' tab (gear icon), and clear the 'Canonical URL' field to restore the default self-canonical.
- For Rank Math: In the post editor, open the Rank Math sidebar panel, click 'Advanced', and clear the 'Canonical URL' field.
- For All in One SEO: In the post editor's AIOSEO meta box, click 'Advanced' and clear the 'Canonical URL' field.
- Also check your theme's header.php or any custom plugin for a hardcoded <link rel="canonical"> — remove or correct any that override the SEO plugin output.
- Update/save the post, then view-source the frontend page and confirm the canonical matches the page URL.
add_action( 'wp_head', function() {
if ( is_singular() ) {
echo '<link rel="canonical" href="' . esc_url( get_permalink() ) . '">' . "\n";
}
} );Only use this if you are NOT using a SEO plugin and need to add a canonical programmatically in your theme's functions.php.
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 WordPress.org
- Install Yoast SEO or Rank Math if not already present (both output a correct per-page og:url automatically).
- In Yoast SEO: WordPress Admin → SEO → Social → Facebook → enable 'Add Open Graph meta data' and save. No further action needed — Yoast handles og:url dynamically.
- In Rank Math: WordPress Admin → Rank Math → General Settings → Social Meta → enable 'Open Graph Meta' and save.
- If you are managing og:url manually in your theme's header.php or functions.php, find the line echoing og:url and replace it with: <meta property='og:url' content='<?php echo esc_url(get_permalink()); ?>' />
- If two sources are outputting og:url (e.g. your theme AND an SEO plugin), you will have duplicate tags — disable the theme's output to let the plugin take control (theme documentation will describe a filter hook to disable its social meta).
- Clear caching plugins and verify with 'View Source' on several page types.
<?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";
}Same as WooCommerce — add to child theme functions.php only if not using Yoast SEO or Rank Math.
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 WordPress.org
- Install the free Redirection plugin (johngodley/redirection) or use Screaming Frog to audit all internal links and identify those that pass through redirect chains.
- For links inside posts or pages: go to wp-admin → Posts or Pages, open the affected entry, select the linked text in the Block or Classic Editor, and update the href to the final destination URL. Update/publish the post.
- For navigation menus: Appearance → Menus → expand the chained menu item → update the Custom Link URL field to the final URL → Save Menu.
- For theme template links: Appearance → Theme File Editor → locate the template file (e.g. header.php, footer.php) → find the href and update it to the final URL. Alternatively, use a child theme to override.
- In the Redirection plugin (Tools → Redirection): open the redirect list, find any entry whose 'Target URL' is itself a redirect, and update it to point directly to the final destination URL.
- Purge all caches (caching plugin + server-level) and re-crawl to verify.
<!-- BEFORE: link points to an intermediate URL that itself redirects -->
<a href="https://example.com/old-page">Our Story</a>
<!-- AFTER: link points directly to the final destination URL -->
<a href="https://example.com/about-us">Our Story</a>Replace every intermediate redirect URL in your anchor href with the final destination URL — the one that returns HTTP 200 with no further redirects.
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 WordPress.org
- Install Yoast SEO or Rank Math if not already active.
- In Yoast SEO: SEO → Search Appearance → Archives → Search Pages → toggle noindex ON.
- In Rank Math: Rank Math → Titles & Meta → Search Results → enable 'No Index'.
- Edit your robots.txt (accessible via Yoast SEO → Tools → File Editor or directly on the server at the WordPress root) and add: Disallow: /?s=
- Confirm by visiting /?s=anyterm and checking View Source for <meta name="robots" content="noindex">
<meta name="robots" content="noindex,follow">Place this tag inside the <head> element of every search results page template.
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 WordPress.org
- Enable debug logging in wp-config.php: set define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); and read /wp-content/debug.log for the exact error.
- In your hosting control panel, check the PHP error log for fatal errors and the exact file/line.
- Deactivate all plugins via Plugins > Installed Plugins > Bulk Action > Deactivate, then reactivate one by one to find the culprit.
- Switch to a default WordPress theme (Appearance > Themes) to rule out theme-level errors.
- Increase PHP memory: add define('WP_MEMORY_LIMIT', '256M'); to wp-config.php or set memory_limit = 256M in php.ini.
- After fixing, turn off debug mode and submit the URL in Google Search Console > URL Inspection > Request Indexing.
// Enable WordPress debug logging (add to wp-config.php)
define( 'WP_DEBUG', true ); // Enable debug mode
define( 'WP_DEBUG_LOG', true ); // Write errors to /wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false ); // Hide errors from site visitors
define( 'WP_MEMORY_LIMIT', '256M' ); // Increase PHP memory limitAdd these lines to wp-config.php (above the '/* That's all, stop editing! */' line) to enable debug logging. Remove or set to false before going live.
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 WordPress.org
- Install a performance plugin: WP Rocket (paid), LiteSpeed Cache, or W3 Total Cache via Plugins → Add New.
- Enable page caching, browser caching, GZIP, and asset minification/combination within the plugin's settings panel.
- Images: Install ShortPixel or Imagify (Plugins → Add New) → run a bulk optimisation of your Media Library → enable auto-optimise on upload and WebP conversion.
- Defer JS: In your performance plugin, enable 'Delay JavaScript execution' or 'Defer JS' to prevent render-blocking scripts.
- Remove unused plugins (Plugins → Installed Plugins) and switch to a lightweight, performance-focused theme.
- At the hosting level, ensure PHP 8.x and OPcache are enabled (via your host's control panel) and consider a managed WordPress host with a built-in CDN (Kinsta, WP Engine, Cloudways).
<img
src="product-image.webp"
alt="Descriptive product alt text"
width="800"
height="600"
loading="lazy"
decoding="async"
>Add these attributes to every image below the fold to enable native lazy loading and prevent layout shift.
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 WordPress.org
- Install and activate Yoast SEO or Rank Math if not already present.
- Open any post or page for editing, scroll to the SEO meta box below the editor, and click the 'SEO title' field.
- Rewrite the title to ≤60 characters; the character/pixel meter in the plugin will confirm it's within range.
- For global templates: in Yoast go to SEO → Search Appearance → Content Types and set a short template. In Rank Math go to Rank Math → Titles & Meta.
- Click Update/Publish.
<head>
<meta charset="UTF-8">
<title>Running Shoes for Men | YourBrand</title>
</head>Place inside the <head> element of every HTML page. Keep the total character count at or under 60.
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 WordPress.org
- Install the Yoast SEO or Rank Math plugin if not already active.
- Edit the page or post in the WordPress block editor (Gutenberg) or classic editor.
- Scroll down to the Yoast SEO / Rank Math panel below the content area.
- Click on the 'SEO title' field and update it with your new 50–60 character title. The snippet preview will update in real time and a colour-coded meter shows title length.
- Click 'Update' or 'Publish' to save.
<head>
<meta charset="UTF-8">
<title>Men's Merino Wool Beanies – BrandName</title>
</head>Place inside the <head> element of every page. Replace the example with your actual descriptive title (30–60 chars).
Answer Engine Optimization · 12 fixes
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 WordPress.org
- Install Yoast SEO (free) or Rank Math (free) if not already active.
- In Yoast: Users → Edit Author Profile → fill in First/Last Name, bio, and website URL. Yoast automatically includes this in Person schema on article pages.
- In Rank Math: go to Rank Math → Titles & Meta → Authors → enable author schema. Also fill in social profile URLs under Rank Math → Titles & Meta → Local SEO → Social Profiles.
- To add sameAs manually, use the 'Additional JSON-LD Schema' option in either plugin, or add the snippet to your child theme's functions.php using wp_head action.
- Validate with the Rich Results Test.
<?php
add_action( 'wp_head', 'add_article_author_jsonld' );
function add_article_author_jsonld() {
if ( ! is_single() ) {
return;
}
global $post;
$author_id = $post->post_author;
$author_name = get_the_author_meta( 'display_name', $author_id );
$author_url = get_author_posts_url( $author_id );
$author_twitter = get_the_author_meta( 'twitter', $author_id ); // requires custom field or plugin
$author_linkedin = get_the_author_meta( 'linkedin', $author_id );
$same_as = array_filter( [ $author_twitter, $author_linkedin ] );
$schema = [
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
'headline' => get_the_title(),
'datePublished' => get_the_date( 'c' ),
'dateModified' => get_the_modified_date( 'c' ),
'author' => [
'@type' => 'Person',
'name' => $author_name,
'url' => $author_url,
'sameAs' => array_values( $same_as ),
],
'publisher' => [
'@type' => 'Organization',
'name' => get_bloginfo( 'name' ),
'logo' => [
'@type' => 'ImageObject',
'url' => get_site_icon_url(),
],
],
'mainEntityOfPage' => [
'@type' => 'WebPage',
'@id' => get_permalink(),
],
];
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 outputs Article + Person JSON-LD automatically on all single post pages using native WordPress author data.
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 WordPress.org
- Install Rank Math SEO (free) or Yoast SEO with Schema add-on.
- Edit the FAQ page in the Gutenberg editor (Pages → your FAQ page → Edit).
- Add an FAQ block (Rank Math FAQ Block or Yoast FAQ Block) — these output all questions and answers as visible HTML AND generate matching FAQPage JSON-LD automatically.
- If you are using a page builder (Elementor, Divi, etc.), look for an 'FAQ' or 'Accordion' widget that has a 'FAQ Schema' toggle — enable it. Confirm all answers appear in page source HTML.
- Validate with the Rich Results Test after publishing.
<?php
add_action( 'wp_head', 'mytheme_faq_schema' );
function mytheme_faq_schema() {
if ( ! is_page( 'faq' ) ) {
return;
}
$faqs = [
[
'question' => 'What is your return policy?',
'answer' => 'We accept returns within 30 days of purchase. Items must be unused and in original packaging.',
],
[
'question' => 'How long does shipping take?',
'answer' => 'Standard shipping takes 3–5 business days. Express shipping (1–2 business days) is available at checkout.',
],
[
'question' => 'Do you ship internationally?',
'answer' => 'Yes, we ship to over 40 countries. International orders typically arrive within 7–14 business days.',
],
[
'question' => 'Can I change or cancel my order?',
'answer' => 'Orders can be changed or cancelled within 1 hour of placement. After that, please use our return process.',
],
];
$schema = [
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => array_map( function( $faq ) {
return [
'@type' => 'Question',
'name' => $faq['question'],
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => $faq['answer'],
],
];
}, $faqs ),
];
echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) . '</script>' . "\n";
}
?>Add to your child theme's functions.php to inject FAQPage JSON-LD on any page with the slug 'faq'. Edit the $faqs array to match your actual content.
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 WordPress.org
- Edit the page or post in the block editor (Gutenberg).
- Add a Paragraph block as the very first block in the content area; write the 160-character direct-answer summary there.
- Use Heading blocks (H2/H3) phrased as questions throughout the rest of the content.
- Install Rank Math (free) or Yoast SEO (free/premium): both provide a native FAQ Block in Gutenberg that outputs FAQPage JSON-LD automatically — drag it into the page after your answer summary.
- If you prefer manual control, add a Custom HTML block or use the theme's header/footer scripts setting (Appearance → Theme File Editor → header.php or footer.php) to inject the JSON-LD <script>.
- Verify the schema with Google's Rich Results Test.
<?php
function mytheme_inject_faq_schema() {
if ( is_singular( array( 'page', 'post', 'product' ) ) ) {
$faq_schema = get_post_meta( get_the_ID(), 'faq_schema', true );
if ( ! empty( $faq_schema ) ) {
echo '<script type="application/ld+json">' . wp_kses_post( $faq_schema ) . '</script>' . "\n";
}
}
}
add_action( 'wp_head', 'mytheme_inject_faq_schema' );
?>Add to your child theme's functions.php to inject FAQPage JSON-LD into the <head> of pages/posts that have an 'faq_schema' custom field populated.
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 WordPress.org
- Install Yoast SEO or Rank Math from Plugins → Add New.
- Yoast SEO: navigate to SEO → Search Appearance → Content Types → Posts → Schema tab; set 'Page type' to 'Article' and 'Article type' to 'BlogPosting' (or your preferred sub-type). Yoast will automatically output the schema on all posts.
- Rank Math: go to Rank Math → Titles & Meta → Posts → Schema Type → select 'Article'. You can also set this per-post in the Rank Math sidebar within the post editor.
- Validate any post URL with Google's Rich Results Test to confirm Article schema appears without errors.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>How to Choose the Right Running Shoes</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "How to Choose the Right Running Shoes",
"description": "A complete guide to selecting running shoes based on your gait, terrain, and training goals.",
"image": {
"@type": "ImageObject",
"url": "https://www.example.com/images/running-shoes-guide.jpg",
"width": 1200,
"height": 630
},
"author": {
"@type": "Person",
"name": "Jane Smith",
"url": "https://www.example.com/authors/jane-smith"
},
"publisher": {
"@type": "Organization",
"name": "Example Store",
"logo": {
"@type": "ImageObject",
"url": "https://www.example.com/logo.png",
"width": 250,
"height": 60
}
},
"datePublished": "2024-03-15",
"dateModified": "2024-06-01",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://www.example.com/blog/how-to-choose-running-shoes"
},
"url": "https://www.example.com/blog/how-to-choose-running-shoes"
}
</script>
</head>
<body>
<!-- page content -->
</body>
</html>Place this entire block inside the <head> element of any article or blog-post page. Fill in all values before publishing.
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 WordPress.org
- Install 'Rank Math SEO' (free) from the WordPress Plugin Directory — it adds a dedicated FAQ block to the Gutenberg editor.
- Open any page with a FAQ section in the block editor, click the '+' to add a block, search for 'Rank Math FAQ', and enter your Q&A pairs — schema is generated automatically.
- If using a classic/page-builder theme (Elementor, Divi), use the plugin 'Schema & Structured Data for WP & AMP' which adds a metabox to every page where you can select FAQPage type and input Q&A pairs.
- For manual injection without a plugin: go to Appearance → Theme File Editor → open 'header.php' and paste the script just before </head> (wrapping it in a conditional to target only FAQ pages using is_page()).
- Validate with Google's Rich Results Test.
<?php
// Inject FAQPage schema on the FAQ page only
function my_faqpage_schema() {
if ( is_page( 'faq' ) ) {
$schema = [
'@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.'
]
],
[
'@type' => 'Question',
'name' => 'Do you ship internationally?',
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => 'Yes, we ship to over 40 countries. Delivery takes 10–14 business days. Import duties may apply.'
]
]
]
];
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', 'my_faqpage_schema' );Add this to your child theme's functions.php to inject FAQPage schema only on the page with the slug 'faq'. Replace Q&A content as needed.
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 WordPress.org
- Install 'Rank Math SEO' (free) or 'Yoast SEO' (with Schema add-on) from Plugins > Add New.
- In Rank Math: edit the target post/page, click the Rank Math icon in the editor toolbar, go to the Schema tab, click 'Add New Schema', select 'HowTo', and complete the step form fields interactively.
- For manual implementation: in your child theme's functions.php, add a function hooked to wp_head that outputs the JSON-LD block inside an is_page('your-slug') conditional, then call wp_enqueue_scripts or add_action('wp_head', 'your_function_name').
- Validate with Google's Rich Results Test after updating.
<?php
function my_howto_schema() {
if ( is_page( 'your-page-slug' ) ) {
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'HowTo',
'name' => 'How to Complete Your Task',
'description' => 'A step-by-step guide to completing your task quickly and correctly.',
'totalTime' => 'PT15M',
'step' => array(
array(
'@type' => 'HowToStep',
'position' => 1,
'name' => 'First Step',
'text' => 'Detailed instructions for the first step go here.',
),
array(
'@type' => 'HowToStep',
'position' => 2,
'name' => 'Second Step',
'text' => 'Detailed instructions for the second step go here.',
),
array(
'@type' => 'HowToStep',
'position' => 3,
'name' => 'Third Step',
'text' => 'Detailed instructions for the third step go here.',
),
),
);
echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT ) . '</script>' . PHP_EOL;
}
}
add_action( 'wp_head', 'my_howto_schema' );Add to your child theme's functions.php. Replace 'your-page-slug' with the actual page slug. Extend the steps array as needed.
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 WordPress.org
- Install 'Yoast SEO' or 'Rank Math SEO' from Plugins → Add New.
- For Yoast: navigate to Yoast SEO → Settings → Site representation. Set type to 'Organization' or 'Person', add your business name, logo, and social profiles.
- For Rank Math: go to Rank Math → Titles & Meta → Local SEO, toggle on 'Local Business', and fill in your complete address, phone, and opening hours.
- For a fully manual approach, install 'Insert Headers and Footers' plugin, then go to Settings → Insert Headers and Footers and paste your JSON-LD block in the 'Scripts in Header' box.
- Save and validate with Google's Rich Results Test.
<?php
function my_localbusiness_schema() {
if ( is_front_page() ) {
echo '<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>';
}
}
add_action( 'wp_head', 'my_localbusiness_schema' );
?>Add to your child theme's functions.php to inject JSON-LD into the <head> on the front page only.
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 WordPress.org
- Install Rank Math SEO (free) from Plugins → Add New, search 'Rank Math'.
- After activation, go to Rank Math → Titles & Meta → Local SEO.
- Set 'Type of Person or Organization' to 'Organization', fill in your Name, Logo, and all social profile URLs.
- Save changes — Rank Math outputs the Organization JSON-LD automatically on every page.
- If you prefer not to use a plugin, go to Appearance → Theme File Editor → open your active child theme's functions.php and add the manual wp_head hook (see code snippet).
- Verify using the Rich Results Test.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Acme Store",
"url": "https://www.acmestore.com",
"logo": {
"@type": "ImageObject",
"url": "https://www.acmestore.com/images/logo.png",
"width": 250,
"height": 250
},
"sameAs": [
"https://www.facebook.com/acmestore",
"https://www.instagram.com/acmestore",
"https://twitter.com/acmestore",
"https://www.linkedin.com/company/acmestore",
"https://www.youtube.com/c/acmestore"
],
"contactPoint": {
"@type": "ContactPoint",
"contactType": "customer service",
"email": "hello@acmestore.com"
}
}
</script>Drop this complete block inside your <head>…</head> section on every page. Replace all placeholder values.
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 WordPress.org
- Install Yoast SEO or RankMath SEO — both output WebPage and Organization schema automatically.
- For Yoast: Yoast SEO → Settings → Site representation → choose 'Organization', fill in name, logo URL, and social profile URLs. WebPage schema is output automatically on every page.
- For RankMath: RankMath → Titles & Meta → set a default Schema Type of 'WebPage'. Configure Organization details under RankMath → General Settings → Local SEO.
- To add schema manually without a plugin, add a wp_head action hook in your child theme's functions.php that outputs the JSON-LD <script> block, using get_the_title() and get_permalink() for dynamic values.
- Validate using Google's Rich Results Test.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "About Us | Acme Store",
"description": "Learn about Acme Store, our mission, and the team behind our products.",
"url": "https://www.example.com/about/",
"inLanguage": "en-US",
"datePublished": "2022-01-15",
"dateModified": "2024-06-01",
"isPartOf": {
"@type": "WebSite",
"name": "Acme Store",
"url": "https://www.example.com/"
},
"breadcrumb": {
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://www.example.com/"
},
{
"@type": "ListItem",
"position": 2,
"name": "About Us",
"item": "https://www.example.com/about/"
}
]
}
}
</script>Paste this inside the <head> element of any HTML page. Replace all placeholder values with real page data.
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 WordPress.org
- If using Yoast SEO or Rank Math, these plugins output ISO-8601 dates automatically — no change needed unless you have overridden schema output.
- For custom schema in your theme's functions.php or a custom plugin, replace get_the_date() with get_the_date('c') and get_the_modified_date() with get_the_modified_date('c') — the 'c' format returns full ISO-8601.
- If schema is in a template file (e.g., single.php), locate the JSON-LD script block and apply the same get_the_date('c') fix.
- Update child theme files via Appearance → Theme File Editor, or via FTP/SFTP for direct file access.
- Clear any active caching plugin (WP Rocket, W3 Total Cache, etc.) after saving.
- Verify with Google's Rich Results Test.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Your Article Headline",
"datePublished": "2025-03-12",
"dateModified": "2025-03-12",
"author": {
"@type": "Person",
"name": "Author Name"
}
}
</script>Minimum viable JSON-LD block to paste into any HTML <head> — replace values with your own.
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 WordPress.org
- If using Yoast SEO: go to Yoast SEO → Social in wp-admin and enter all social profile URLs; Yoast outputs them as sameAs automatically.
- If using Rank Math: go to Rank Math → Titles & Meta → Local SEO and enter social profile URLs.
- For manual control without a plugin: edit your child theme's functions.php and use add_action('wp_head', ...) to echo the JSON-LD block (see code snippet below), OR install 'Insert Headers and Footers' plugin and paste the JSON-LD in the Header section.
- Verify with View Page Source, then validate with Google's Rich Results Test.
<?php
function mytheme_organization_schema() {
if ( ! is_front_page() ) return;
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Organization',
'name' => get_bloginfo( 'name' ),
'url' => home_url( '/' ),
'logo' => get_site_icon_url(),
'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',
],
];
echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ) . '</script>' . "\n";
}
add_action( 'wp_head', 'mytheme_organization_schema' );Add this to your child theme's functions.php. Replace placeholder URLs with your real social profile URLs.
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 WordPress.org
- In WordPress admin, go to the relevant post/page and confirm its status is 'Published' and visibility is 'Public'.
- Check Settings → Reading — ensure 'Discourage search engines from indexing this site' is unchecked.
- Use your SEO plugin (Yoast SEO, Rank Math, or All in One SEO): open the page editor → SEO meta box → Advanced tab → confirm 'Allow search engines to index this page' (no noindex override is set).
- For broken URLs, install the Redirection plugin (Tools → Redirection → Add Redirect) to 301-redirect old broken paths to the correct live page.
- To add structured data, use your SEO plugin's built-in schema features (Yoast SEO Premium or Rank Math's Schema module supports Product, FAQ, Breadcrumb schema) or install 'Schema Pro' for granular control. Add JSON-LD manually by inserting a Custom HTML block in the block editor if needed.
- After publishing, submit the URL via Google Search Console URL Inspection → Request Indexing, then validate with the Rich Results Test.
Accessibility (WCAG) · 13 fixes
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 WordPress.org
- Go to Appearance → Theme File Editor, or edit via FTP/SFTP using a child theme.
- Search all PHP and HTML template files for the disallowed `role` attribute string.
- Edit the offending file: remove the invalid role from interactive elements, or correct it on non-interactive ones.
- If the markup is generated by a block (Gutenberg) or plugin shortcode, check the block's HTML settings in the Block Editor sidebar or the plugin's output template.
- Save changes and confirm the fix with a browser accessibility audit.
<!-- ❌ WRONG: role="textbox" is not permitted on <select> -->
<select role="textbox" name="country">
<option value="us">United States</option>
</select>
<!-- ✅ FIX A: Remove the invalid role — <select> already has the correct native semantics -->
<select name="country">
<option value="us">United States</option>
</select>
<!-- ❌ WRONG: role="textbox" on a <div> with no keyboard/input handling -->
<div role="textbox" class="fake-input">Enter your name</div>
<!-- ✅ FIX B: Replace with a real <input> element -->
<label for="name">Your name</label>
<input type="text" id="name" name="name" placeholder="Enter your name">
<!-- ❌ WRONG: role="textbox" on a <p> element -->
<p role="textbox">Some editable text</p>
<!-- ✅ FIX C: If you need an editable region, use contenteditable + the permitted role -->
<!-- role="textbox" IS permitted on elements with contenteditable, like a <div> -->
<div role="textbox"
contenteditable="true"
aria-multiline="false"
aria-label="Your name">
</div>Common patterns: wrong role on the left, correct fix on the right (as comments).
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 WordPress.org
- In WordPress Admin, go to Appearance → Theme File Editor and open the file rendering the broken component (commonly 'header.php' or a navigation partial).
- Add the required parent ARIA role wrapper around the orphaned child-role elements directly in the template file — always use a child theme so the change survives theme updates.
- If a block theme is used (Full Site Editing), navigate to Appearance → Editor, select the block containing the issue, switch to Code Editor view (three-dot menu), and manually add the `role` attribute to the correct wrapper block.
- Run the axe DevTools browser extension after saving to confirm the fix.
<!-- INCORRECT: menuitem with no required parent -->
<nav>
<a href="/shop" role="menuitem">Shop</a>
<a href="/about" role="menuitem">About</a>
<a href="/contact" role="menuitem">Contact</a>
</nav>
<!-- CORRECT: menuitem children wrapped in role="menubar" -->
<nav>
<ul role="menubar">
<li role="none"><a href="/shop" role="menuitem">Shop</a></li>
<li role="none"><a href="/about" role="menuitem">About</a></li>
<li role="none"><a href="/contact" role="menuitem">Contact</a></li>
</ul>
</nav>
<!-- CORRECT: option children wrapped in role="listbox" -->
<div role="listbox" aria-label="Select a size">
<div role="option" aria-selected="false">Small</div>
<div role="option" aria-selected="true">Medium</div>
<div role="option" aria-selected="false">Large</div>
</div>
<!-- CORRECT: tab children wrapped in role="tablist" -->
<div role="tablist" aria-label="Product details">
<button role="tab" aria-selected="true" aria-controls="panel-desc">Description</button>
<button role="tab" aria-selected="false" aria-controls="panel-reviews">Reviews</button>
</div>
<div id="panel-desc" role="tabpanel">Description content here.</div>
<div id="panel-reviews" role="tabpanel" hidden>Reviews content here.</div>Correct pattern: child roles (menuitem) wrapped in the required parent role (menu/menubar). Apply the same principle to other role pairs.
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 WordPress.org
- Go to Appearance → Customize → Additional CSS and add a targeted CSS override (see snippet below) — this is the safest method and survives theme updates.
- Alternatively, if you use a page builder (Elementor, Divi, Bricks), select the text widget/element → Style tab → Typography → Color and enter the accessible hex value directly.
- For block editor (Gutenberg) sites, check Appearance → Editor → Styles → Colors and update the relevant color role.
/* Fix low-contrast secondary/muted text — paste in Appearance > Customize > Additional CSS */
.wp-block-paragraph,
.entry-content p,
.secondary-text,
.text-muted {
color: #595959; /* was #777777 — now 7.0:1 on white */
}Paste into Appearance → Customize → Additional CSS. Adjust the selector to match your theme's actual class.
DlitemModerate effort
Wrap every `<dt>` (term) and `<dd>` (description) element inside a parent `<dl>` element so description list items have their required container.
On WordPress.org
- Identify the source of the markup: is it your theme, a page builder (Elementor, Divi, Beaver Builder), or a plugin?
- For theme files: go to Appearance → Theme File Editor, or edit via FTP, and search for `<dt` / `<dd` in your child theme's PHP files.
- For Elementor: use the 'HTML' widget or the 'Text Editor' widget's HTML mode to locate and fix the markup; alternatively, use Elementor's custom code blocks.
- For Divi: open the affected module in the Divi Builder, switch to 'Text' mode in the content area, and add `<dl>` wrappers around bare `<dt>`/`<dd>` tags.
- For Gutenberg blocks: if using a custom HTML block, edit the block directly; if using a third-party block plugin that outputs bad HTML, contact the plugin author or replace the block with a Custom HTML block using correct markup.
- After fixing, flush permalinks (Settings → Permalinks → Save) and any caching, then retest.
<!-- CORRECT: dt and dd are direct children of dl -->
<dl>
<dt>Shipping time</dt>
<dd>3–5 business days</dd>
<dt>Return policy</dt>
<dd>30-day free returns</dd>
<dt>Material</dt>
<dd>100% organic cotton</dd>
</dl>
<!-- ALSO CORRECT: grouping pairs with div inside dl (HTML5 spec allows this) -->
<dl>
<div>
<dt>Shipping time</dt>
<dd>3–5 business days</dd>
</div>
<div>
<dt>Return policy</dt>
<dd>30-day free returns</dd>
</div>
</dl>
<!-- INCORRECT — do NOT do this: dt/dd outside a dl -->
<!-- <div>
<dt>Shipping time</dt>
<dd>3–5 business days</dd>
</div> -->The correct structure: every <dt> and <dd> must be a direct child of a <dl> element.
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 WordPress.org
- Go to WP Admin → Settings → General and set 'Site Language' to your correct language.
- In your active (child) theme's `header.php`, ensure the `<html>` tag uses the WordPress template tag: `<html <?php language_attributes(); ?>>`. This outputs the correct `lang` attribute automatically.
- If you don't have a child theme, create one before editing to avoid losing changes on theme updates.
- Save and verify via page source.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Page Title</title>
</head>
<body>
<!-- page content -->
</body>
</html>Place this as the very first tag in your HTML document. Replace 'en' with your actual BCP 47 language code.
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 WordPress.org
- For theme-controlled forms (search, comments, newsletter), open your child theme's template files in Appearance → Theme File Editor or via FTP. Locate `searchform.php`, `comments.php`, or the relevant template part and add a `<label for="s">Search</label>` (or equivalent) before each unlabeled input.
- For plugin-generated forms (Contact Form 7, Gravity Forms, WPForms, etc.): open the form builder in the plugin's admin area, select the field, and enable 'Show label' or fill in the 'Field Label' setting. Never leave the label blank and rely only on placeholder text or a tooltip.
- If a label must be visually hidden for design reasons, use a CSS class like `.screen-reader-text` (built into most WordPress themes) on the `<label>` element rather than removing the label or using `title` alone — this keeps the label accessible to screen readers while hiding it visually.
- Save and verify with the axe browser extension or WP Accessibility plugin.
<!-- CORRECT: Visible label paired to input -->
<div class="form-field">
<label for="customer-email">Email address</label>
<input
type="email"
id="customer-email"
name="email"
autocomplete="email"
/>
</div>
<!-- CORRECT: Visually-hidden label (readable by screen readers, invisible to sighted users) -->
<style>
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
<div class="form-field">
<label for="site-search" class="visually-hidden">Search the store</label>
<input
type="search"
id="site-search"
name="q"
placeholder="Search..."
/>
</div>
<!-- CORRECT: aria-label as alternative when no <label> element is possible -->
<input
type="search"
name="q"
aria-label="Search the store"
placeholder="Search..."
/>
<!-- WRONG: title only — do NOT rely on this -->
<!-- <input type="search" name="q" title="Search the store" /> -->
<!-- WRONG: placeholder only — do NOT rely on this -->
<!-- <input type="email" name="email" placeholder="Email address" /> -->The correct pattern: a visible <label> paired to an input via matching for/id attributes.
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 WordPress.org
- Go to Appearance → Theme File Editor → header.php in your child theme.
- Ensure `<header id="masthead" role="banner">` (or equivalent) opens directly after `<body <?php body_class(); ?>>` and is closed before `<main`.
- If the theme uses a wrapping `<div>` landmark that contains the header, either unwrap that div or remove its landmark role.
- Save and test with axe DevTools or WAVE.
<?php
/**
* The header template for the theme.
* header.php — place <header> as direct child of <body>.
*/
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<a class="skip-link screen-reader-text" href="#primary">Skip to content</a>
<!-- CORRECT: <header> is the first landmark inside <body> -->
<header id="masthead" class="site-header">
<div class="site-branding">
<?php the_custom_logo(); ?>
</div>
<nav id="site-navigation" class="main-navigation" aria-label="Primary">
<?php wp_nav_menu(array('theme_location' => 'primary')); ?>
</nav>
</header><!-- #masthead -->In your child theme's header.php, open <header> directly after <body>.
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 WordPress.org
- To avoid losing changes on theme updates, create a child theme. Copy the offending template file (e.g. 'header.php', 'page.php') into your child theme directory.
- Open the file in a code editor (or Appearance → Theme File Editor) and search for '<ul', '<ol', '<li' to locate invalid nesting.
- Correct the markup: only <li> as direct children of <ul>/<ol>, and every <li> must live inside a list container.
- For menus, WordPress outputs standard <ul>/<li> structure via 'wp_nav_menu()'. If a custom Walker class is used (often in 'functions.php'), find the walker and correct the render_list_item / start_el methods to output proper <li> tags.
- Clear caching (plugin or server), then re-audit with the axe DevTools extension.
<!-- CORRECT: <ul> or <ol> with only <li> children -->
<ul>
<li>First item</li>
<li>Second item</li>
<li>Third item
<!-- CORRECT: nested list lives INSIDE an <li> -->
<ul>
<li>Nested item A</li>
<li>Nested item B</li>
</ul>
</li>
</ul>
<!-- WRONG: <div> as a direct child of <ul> -->
<!-- <ul>
<div>This breaks the list</div>
<li>Item</li>
</ul> -->
<!-- WRONG: <li> with no list parent -->
<!-- <li>Orphaned item</li> -->Correct list structure — all platforms. Use this as a reference for what valid markup looks like.
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 WordPress.org
- Go to Appearance → Theme File Editor and open `header.php` for your active theme. Search for `http-equiv` and delete the meta tag if found.
- If using a child theme, check the child theme's `header.php` and `functions.php` as well.
- Search active plugins: some SEO or caching plugins can inject meta-refresh tags — check each plugin's settings and disable the feature, or deactivate plugins to isolate the source.
- For redirects, use the free 'Redirection' plugin to manage 301/302 redirects without touching code.
- Clear caches (plugin cache, server cache, CDN cache) and re-test.
<!-- REMOVE — self-refresh every 30 seconds (WCAG fail) -->
<!-- <meta http-equiv="refresh" content="30"> -->
<!-- REMOVE — redirect to another URL after 5 seconds (WCAG fail) -->
<!-- <meta http-equiv="refresh" content="5; url=https://example.com/new-page"> -->
<!-- ALLOWED — 0-second instant redirect (no time limit imposed on user) -->
<!-- Only use this as a fallback alongside a server-side redirect -->
<meta http-equiv="refresh" content="0; url=https://example.com/new-page">REMOVE this tag from your page's <head>. Both forms below are problematic (self-refresh and timed redirect).
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 WordPress.org
- Go to Appearance → Theme File Editor in your WordPress Admin.
- Select your active theme (use a child theme to prevent update overwrites) and open 'header.php'.
- Find and edit the <meta name="viewport"> tag to remove maximum-scale=1 or user-scalable=no.
- Save the file. If you don't see a Theme File Editor, use FTP/SFTP to edit the file directly on your server.
<meta name="viewport" content="width=device-width, initial-scale=1">Replace any existing <meta name="viewport"> tag in your <head> with this. Omitting maximum-scale and user-scalable entirely is the safest, most accessible choice.
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 WordPress.org
- In wp-admin go to Appearance → Theme File Editor (or edit files locally via FTP/SSH — recommended for safety).
- Open header.php of your active (child) theme. Verify a skip link exists immediately after <body>: `<a class='skip-link screen-reader-text' href='#primary'>Skip to content</a>`. The exact fragment id varies by theme.
- Find the element in header.php, page.php, or index.php that opens the main content area and confirm it has the matching id (e.g. `<main id='primary' tabindex='-1'>`). Add the id and tabindex if missing.
- If you use a block theme (Full Site Editing), go to Appearance → Editor → Templates → edit the template part that wraps the Query Loop or Page Content block, switch to Code Editor view, and add the id attribute to the <main> tag.
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 WordPress.org
- Access your theme files via Appearance → Theme File Editor in the WordPress admin, or connect via FTP/SFTP to wp-content/themes/[your-theme]/.
- Search all PHP, HTML, and JavaScript files for 'tabindex' — pay special attention to header.php, footer.php, functions.php, and any page-builder output.
- Change any tabindex value greater than 0 to '0' or remove the attribute from natively interactive elements.
- If the issue is in a page builder (Elementor, Divi, Beaver Builder), open the affected element's advanced settings in the page builder UI — look for an 'Accessibility' or 'Custom Attributes' panel where tabindex may be set, and correct it there.
- For plugin-generated markup, check the plugin's settings panel or contact the plugin author.
- After saving, purge your cache (if using a caching plugin) and verify by tabbing through the page.
<!-- ❌ WRONG: positive tabindex breaks natural tab order -->
<button tabindex="3">Submit</button>
<a href="/shop" tabindex="1">Shop Now</a>
<div tabindex="2">Custom widget</div>
<!-- ✅ CORRECT: native elements use no tabindex (natural order) -->
<button>Submit</button>
<a href="/shop">Shop Now</a>
<!-- ✅ CORRECT: non-native element made focusable in natural order -->
<div tabindex="0" role="button" aria-label="Custom widget">Custom widget</div>
<!-- ✅ CORRECT: element only focused by JavaScript (e.g., modal), not in tab flow -->
<div tabindex="-1" id="modal-container" role="dialog" aria-modal="true">...</div>Replace any positive tabindex value with 0 (or remove the attribute). Use -1 only for programmatic focus.
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 WordPress.org
- Same resolution as WooCommerce for the root `lang` attribute: Settings → General → Site Language in the WordPress admin.
- Ensure your active theme's `header.php` uses `<?php language_attributes(); ?>` on the `<html>` tag rather than a hard-coded `lang` value.
- For block themes (Full Site Editing), go to Appearance → Editor → Templates → open the root template and ensure no hard-coded `lang` override is present in any HTML block.
- Verify with the axe browser extension after saving.
<!-- Primary page language on the root element -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Store</title>
</head>
<body>
<p>Welcome to our store. Free shipping on orders over $50.</p>
<!-- Inline passage in a different language: add lang to the wrapping element -->
<p lang="fr">Bienvenue dans notre boutique. Livraison gratuite dès 50 €.</p>
<!-- Inline span for a short foreign-language phrase within an English sentence -->
<p>Our slogan is <span lang="de">Qualität zuerst</span>, which means "Quality first".</p>
</body>
</html>Set the primary language on the <html> tag and mark inline foreign-language passages with their own lang attribute.
Security (OWASP) · 16 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 WordPress.org
- Install a dedicated SMTP plugin (WP Mail SMTP is most common) from Plugins → Add New.
- Connect it to an authenticated SMTP relay (Mailgun, SendGrid, Brevo, Amazon SES, etc.) and complete that relay's DKIM key publication and SPF record update at your DNS host.
- At your DNS provider, edit _dmarc.yourdomain.com TXT record from p=none to p=quarantine, then p=reject after confirming all mail passes.
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 WordPress.org
- Install a security headers plugin such as 'Headers Security Advanced & HSTS WP' or 'Solid Security (iThemes Security)' from WordPress Admin → Plugins → Add New.
- In the plugin's settings panel, find the HSTS or Security Headers section and enable Strict-Transport-Security with max-age set to 31536000 and 'Include Subdomains' enabled.
- Alternatively, add the header directly in your theme's functions.php (child theme recommended): `add_action('send_headers', function(){ header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); });`
- For server-level control, edit your Nginx or Apache config as described in the WooCommerce steps above — this is the most reliable method.
Strict-Transport-Security: max-age=31536000; includeSubDomainsThis is an HTTP response header — it cannot be set in HTML meta tags. It must be set by your server, CDN, or platform. Shown here for reference only.
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 WordPress.org
- WordPress core does not set HSTS — you must set it at the server or plugin level.
- Plugin method: Install 'Headers Security Advanced & HSTS WP' or 'HTTP Headers' from Plugins → Add New. Activate and navigate to the plugin's settings page. Enable Strict-Transport-Security and set max-age to 31536000 with includeSubDomains checked.
- Apache method: In your root .htaccess, inside a <IfModule mod_headers.c> block add: Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
- Nginx method: In your server {} block for port 443: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; — reload Nginx after saving.
- Verify at securityheaders.com.
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.
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 WordPress.org
- The fix is at the web server layer, not inside WordPress core or a theme.
- For Apache: add `Header unset Server` to your `.htaccess` or virtual host config (requires `mod_headers`). For nginx: add `server_tokens off;` to your `nginx.conf` server block.
- Use the 'HTTP Headers' plugin (Settings → HTTP Headers → Response Headers) to remove the `Server` header without editing server config files directly.
- Verify with browser DevTools (F12 → Network → any request → Response Headers) that `Server` is absent or shows no version string.
<!-- 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 WordPress.org
- Same approach as WooCommerce above — install 'HTTP Headers' or 'Security Headers' plugin, or edit .htaccess / Nginx config at the server level.
- To suppress via PHP: add header_remove('X-Powered-By'); in wp-content/mu-plugins/remove-headers.php (create the file if it doesn't exist). This fires before any theme or plugin can set the header.
- For Apache hosts: add 'Header unset X-Powered-By' and 'Header always unset X-Powered-By' to your .htaccess file (requires mod_headers).
- For Nginx hosts: add 'more_clear_headers X-Powered-By;' in the server {} block (requires headers_more module) or manage via your host's control panel.
<?php
/**
* Plugin Name: Remove X-Powered-By Header
* Description: Suppresses the X-Powered-By HTTP response header for security.
* Version: 1.0
*/
// Remove the X-Powered-By header as early as possible
header_remove('X-Powered-By');
?>Create the file wp-content/mu-plugins/remove-x-powered-by.php — it runs automatically on every request before themes and plugins load.
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 WordPress.org
- Add the following code to your theme's functions.php or a site-specific plugin to harden WordPress cookie settings (see PHP code snippet below).
- Install a dedicated security plugin such as Wordfence, iThemes Security, or Solid Security — each has a 'Secure Cookies' or 'Cookie Security' toggle in their settings dashboard.
- With Wordfence: go to WP Admin → Wordfence → All Options → Login Security → enable 'Enforce secure cookies'.
- With Solid Security (iThemes): go to Security → Settings → System Tweaks → enable 'Secure Socket Layers / SSL' and 'Disable File Editing'.
- Ensure HTTPS is active (SSL certificate installed) before setting Secure flag — cookies flagged Secure are silently dropped over HTTP.
- Verify in browser DevTools → Application → Cookies.
<?php
// Harden PHP session cookie flags
add_action('init', function() {
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => COOKIE_DOMAIN ?: '',
'secure' => true, // HTTPS only
'httponly' => true, // No JS access
'samesite' => 'Strict'
]);
});
// Harden WordPress logged-in and auth cookies
add_filter('auth_cookie', function($cookie) {
// WordPress itself sets the cookie; we override the params it uses
return $cookie;
});
// Force secure cookies in wp-config.php instead (preferred):
// define('FORCE_SSL_ADMIN', true);
// WordPress respects COOKIE_SECURE from wp-config too.
// In wp-config.php, also add:
// define('COOKIE_SECURE', true);Add to functions.php in your active theme, or in a site-specific plugin. Hardens PHP session cookies AND WordPress auth cookies.
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 WordPress.org
- Install the 'HTTP Headers' plugin or 'Headers Security Advanced & HSTS WP' from the WordPress plugin repository.
- Configure the Content-Security-Policy header value in the plugin's settings page.
- Alternatively, add directly via functions.php: add_action('send_headers', function(){ header("Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';"); });
- Or set in .htaccess (Apache): Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none';"
- Or in Nginx config: add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none';" always;
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 WordPress.org
- Option A — .htaccess (Apache): Open .htaccess in your WordPress root via FTP, SFTP, or your host's File Manager. Add the block from the Apache code snippet. Save.
- Option B — Plugin: In WordPress admin → Plugins → Add New, search for 'HTTP Headers' by David Gwyer or 'Security Headers'. Install and activate. Navigate to Settings → HTTP Headers → Add Header → Name: 'Permissions-Policy', Value: 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()'. Save.
- Option C — functions.php (child theme): Add the PHP snippet from the code snippets section to your child theme's functions.php.
- Verify in DevTools → Network → select HTML document → Response Headers.
<?php
add_action( 'send_headers', function() {
header( 'Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()' );
} );Add to your child theme's functions.php file.
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 WordPress.org
- Install a security-headers plugin such as *Headers Security Advanced & HSTS WP* or *WP Headers* from WP Admin → Plugins → Add New.
- Activate the plugin, navigate to its settings page, find the Referrer-Policy option, and set it to `strict-origin-when-cross-origin`.
- Alternatively, add the header directly in `.htaccess` (Apache) or your Nginx server block config as described in the WooCommerce steps above.
- Verify with browser DevTools → Network tab → document response headers.
<meta name="referrer" content="strict-origin-when-cross-origin">Add inside the <head> element of every HTML page as a fallback when HTTP header control is unavailable.
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 WordPress.org
- Connect via SFTP or use cPanel File Manager and navigate to your site's document root (the folder containing wp-config.php).
- Create a .well-known directory if absent, then create security.txt inside it.
- Paste your security.txt content (see code snippets) and save.
- Visit https://yourdomain.com/.well-known/security.txt to confirm a 200 response.
Contact: mailto:security@yourdomain.com
Expires: 2026-12-31T23:59:00.000Z
Preferred-Languages: en
Policy: https://yourdomain.com/security-policy
Acknowledgments: https://yourdomain.com/security-acknowledgmentsSave this as security.txt and place it at /.well-known/security.txt on your web server. Update all placeholder values before publishing.
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 WordPress.org
- WordPress.org (self-hosted) — set HSTS at the server or plugin level, not in WordPress PHP.
- Recommended plugin: Install 'HTTP Headers' by WebFactory from the WordPress plugin directory. Admin → HTTP Headers → Security Headers → Strict-Transport-Security → Enable → set max-age=31536000, check includeSubDomains → Save.
- Server-level (Apache): Add `Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"` inside a `<IfModule mod_headers.c>` block in .htaccess.
- Server-level (Nginx): Add `add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;` in the SSL server block.
- Verify in DevTools → Network → document response headers.
<!-- HSTS cannot be set via an HTML meta tag. It MUST be sent as an HTTP response header. Use the server-level examples below. -->
<!-- Correct header value -->
<!-- Strict-Transport-Security: max-age=31536000; includeSubDomains -->Meta tags cannot set HSTS — it must be a real HTTP response header. This is a reminder of the correct header value to use at the server level.
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 WordPress.org
- Same as WooCommerce above — WordPress.org self-hosted sites use Apache or Nginx; use either the `.htaccess`/Nginx method or a security-headers plugin.
- Recommended plugin path: WP Admin → Plugins → Add New → 'HTTP Headers' by David Gwyer → Activate → Settings → HTTP Headers → add `X-Content-Type-Options` with value `nosniff`.
X-Content-Type-Options: nosniffThis is an HTTP response header — it is sent by the server, not placed in HTML markup. The examples below show where and how to set it.
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 WordPress.org
- Go to Dashboard → Updates and apply all available WordPress core updates — core updates ship updated jQuery.
- Go to Dashboard → Plugins → Installed Plugins and update every plugin; go to Appearance → Themes and update your theme.
- Install the free 'Query Monitor' plugin to see which themes/plugins enqueue jQuery and what version.
- If a plugin is loading an old jQuery independently, add the following to your child theme's functions.php to deregister and replace it (see code snippet); test on a staging site first.
- For any plugin that cannot be updated and still loads old jQuery, contact the developer or replace the plugin.
<?php
/**
* Replace bundled WordPress jQuery with the latest CDN version.
* Add this to your CHILD theme's functions.php only.
* Test thoroughly — some plugins rely on wp-includes jQuery.
*/
function replace_jquery_with_latest() {
if ( ! is_admin() ) {
wp_deregister_script( 'jquery' );
wp_deregister_script( 'jquery-core' );
wp_deregister_script( 'jquery-migrate' );
wp_register_script(
'jquery',
'https://code.jquery.com/jquery-3.7.1.min.js',
array(),
'3.7.1',
false // load in <head>
);
wp_enqueue_script( 'jquery' );
}
}
add_action( 'wp_enqueue_scripts', 'replace_jquery_with_latest', 100 );Add to your child theme's functions.php to deregister WordPress's default jQuery and load the CDN version. Test on staging first.
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 WordPress.org
- SPF is set at your DNS/registrar, not in WordPress itself.
- Log in to your hosting control panel (cPanel → 'Zone Editor' or 'Email Deliverability') or your external DNS provider.
- Find the TXT record for @ starting with v=spf1.
- If you use an SMTP plugin (WP Mail SMTP, Fluent SMTP, Easy WP SMTP) connected to SendGrid, Mailgun, Brevo, etc., include that provider's SPF include value before adding -all.
- Save and test with an SPF checker.
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 WordPress.org
- Install the 'HTTP Headers' plugin or 'Solid Security' (formerly iThemes Security) from WordPress Admin → Plugins → Add New.
- Solid Security path: WordPress Admin → Security → Settings → Security Check → or search 'Security Headers' within the plugin — enable `X-Content-Type-Options: nosniff`.
- If your host (e.g. WP Engine, Kinsta, SiteGround) already injects this header at the server level, do NOT also add it via plugin — check your host's dashboard or contact support to confirm.
- Verify with DevTools or securityheaders.com.
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 WordPress.org
- Same as WooCommerce above — WordPress.org self-hosted sites use Apache or Nginx server config or a security/headers plugin.
- For Apache: add to your .htaccess file in the site root — Header always set X-Frame-Options "DENY"
- For Nginx: add to your server block — add_header X-Frame-Options "DENY" always;
- Recommended plugin: 'HTTP Headers' by BestWebSoft (Settings → HTTP Headers → Add Header → X-Frame-Options → DENY) or 'Secure Headers' plugin.
- After saving, purge any caching layer and verify headers via DevTools.
<!-- X-Frame-Options MUST be sent as an HTTP response header, not a meta tag. -->
<!-- Use the server-side or platform snippets below instead. -->
<!-- The modern equivalent you CAN set via meta tag (limited support): -->
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'none';">If you cannot set server-level headers, use this meta-equivalent only as a last resort — note that X-Frame-Options via meta tag is NOT supported by browsers (it must be an HTTP header). This snippet is shown for illustration only; use server-side headers.
Site Lifecycle · 5 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 WordPress.org
- Log in to wp-admin. The Dashboard > Home screen will show a banner if WordPress core is out of date.
- Go to Dashboard > Updates to see pending WordPress core, plugin, and theme updates all in one place.
- Click 'Update Now' under WordPress core. WordPress will download and apply the update automatically.
- On the same Updates screen, select all outdated plugins (including WooCommerce if installed) and click 'Update Plugins', then repeat for themes.
- If you are more than one major version behind (e.g., on 5.x and current is 6.x), read the official upgrade guide at wordpress.org/support/article/upgrading-wordpress before proceeding.
- Use the free 'Health Check & Troubleshooting' plugin or WP Staging to test updates safely before pushing to production.
<?php
/**
* Remove WordPress version number from <head> and RSS feeds.
* Add to: wp-content/themes/YOUR-THEME/functions.php
* or create a must-use plugin in wp-content/mu-plugins/hide-version.php
*/
// Remove from <head>
remove_action( 'wp_head', 'wp_generator' );
// Remove from RSS feeds
add_filter( 'the_generator', '__return_empty_string' );
// Remove version query strings from scripts and styles (optional hardening)
function remove_version_query_strings( $src ) {
if ( strpos( $src, 'ver=' ) ) {
$src = remove_query_arg( 'ver', $src );
}
return $src;
}
add_filter( 'style_loader_src', 'remove_version_query_strings', 9999 );
add_filter( 'script_loader_src', 'remove_version_query_strings', 9999 );Add to your active theme's functions.php (or a site-specific plugin) to hide the WordPress version from the public HTML source — a secondary hardening step alongside keeping WordPress updated.
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 WordPress.org
- WordPress.org (self-hosted) does not manage domain registration. Log in to the registrar where you bought the domain (or ask your hosting provider if the domain was included with hosting) and enable auto-renew.
<!-- 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.
Html langQuick win
Add a lang attribute to your site's <html> tag that matches the primary language of your store's content.
On WordPress.org
- Go to Settings → General → Site Language and choose your language from the dropdown — WordPress will automatically output the correct lang attribute via language_attributes() in any properly coded theme.
- To verify, open Appearance → Theme File Editor → header.php and confirm <html <?php language_attributes(); ?>> is present.
- If your theme's header.php is missing this tag, add it: <html <?php language_attributes(); ?>>.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Store</title>
</head>
<body>
<!-- page content -->
</body>
</html>Place this as the very first tag in your HTML document, before <head>.
Server versionQuick win
Remove or suppress HTTP response headers that advertise your server software name and version to the public internet.
On WordPress.org
- The fix is identical to WooCommerce above — WordPress.org (self-hosted) shares the same Apache/Nginx server layer.
- Via .htaccess (Apache): add `Header unset Server` and `Header unset X-Powered-By` and `ServerSignature Off` to your root .htaccess.
- Via plugin: install 'HTTP Headers' or 'Security Headers' plugin (Dashboard → Plugins → Add New), activate, and configure removal of Server and X-Powered-By headers.
- For PHP specifically: in wp-config.php or a must-use plugin, you can add `header_remove('X-Powered-By');` before any output is sent. To suppress PHP version exposure also set `expose_php = Off` in php.ini if your host allows it.
- Contact your host for server-level (ServerTokens) changes if you are on shared hosting without root access.
<?php
/**
* Remove technology-disclosing HTTP response headers.
* Add to functions.php or a must-use plugin.
*/
add_filter( 'wp_headers', function ( $headers ) {
// Remove PHP/WordPress version disclosure
unset( $headers['X-Powered-By'] );
// Remove WordPress generator tag from REST API
unset( $headers['X-WP-Generator'] );
return $headers;
} );
// Also remove the <meta name="generator"> tag from <head>
remove_action( 'wp_head', 'wp_generator' );Add to your theme's functions.php or a must-use plugin in /wp-content/mu-plugins/remove-headers.php
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 WordPress.org
- SSL is managed at the server/hosting level, not inside WordPress itself.
- Log in to your hosting control panel and enable AutoSSL (cPanel) or the equivalent Let's Encrypt integration.
- Install the 'Really Simple SSL' plugin for WordPress — it checks your certificate status and forces HTTPS site-wide, and its Pro version can alert you before expiry.
- Confirm WordPress Site URL and Home URL both use 'https://' under Settings → General.
<!-- SERVER-SIDE redirect is strongly preferred. Example nginx config snippet below. -->
<!-- nginx: force HTTPS -->
<!-- server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
} -->Verify your HTTPS redirect is in place at the server/app level. This meta-refresh is a last-resort fallback only — implement a proper server-side 301 redirect instead.