Skip to Content
PluginsLW SEO

LW SEO

Lightweight SEO plugin for WordPress - minimal footprint, maximum impact.

Overview

Requires WordPress6.6+
Requires PHP8.0+
Tested up to7.1
LicenseGPL-2.0-or-later
GitHublwplugins/lw-seo 

Quick Start

  1. Install and activate the plugin
  2. Navigate to LW Plugins > SEO
  3. Configure title templates in the General tab
  4. Upload a default social image in the Social tab
  5. Enable the Sitemap in the Sitemap tab

To find a setting, use the Quick search field at the top of the settings sidebar (or press Cmd/Ctrl+K) and type its name: the match opens on its tab, scrolled into view and highlighted.

Installation

Via WordPress Admin

  1. Go to Plugins > Add New
  2. Search for “LW SEO”
  3. Click Install Now
  4. Click Activate

Via Composer

bash
composer require lwplugins/lw-seo

The plugin will be installed to wp-content/plugins/lw-seo/.

Manual Installation

  1. Download the plugin from GitHub Releases 
  2. Upload the lw-seo folder to /wp-content/plugins/
  3. Activate via Plugins menu in WordPress admin

After Installation

  1. Navigate to LW Plugins > SEO in the admin menu
  2. Configure your settings across the available tabs
  3. If using permalinks, visit Settings > Permalinks and click Save to flush rewrite rules

Conflict Detection

LW SEO automatically detects and disables its output when the following SEO plugins are active:

  • Yoast SEO
  • Rank Math
  • All in One SEO

This prevents duplicate meta tags and conflicts.

Uninstallation

  1. Deactivate the plugin via Plugins menu
  2. Delete the plugin
  3. Deleting the plugin removes the 404 log table, the internal IndexNow options (key, queue, log), the HTML sitemap cache version and the scheduled events (lw_seo_404_prune, lw_seo_indexnow_flush). On a multisite network this runs for every site.
  4. Your settings (lw_seo_options in the wp_options table) and your redirects are kept on purpose and remain unless you remove them manually

Settings

Configure under LW Plugins > SEO. The settings page uses a tabbed interface:

TabDescription
GeneralTitle templates, separator, site name format
ContentMeta descriptions, canonical URLs, robots meta, image alt and title templates
SocialOpen Graph, Twitter Cards, default social image
SitemapXML sitemap (images, browser view), HTML sitemap, IndexNow
AI/LLMContent Signals, llms.txt, AI crawler blocking, Markdown endpoint
LocalLocalBusiness schema, address, hours
WooCommerceProduct SEO, brand, ProductGroup, shipping and return policy (when WooCommerce is active)
RedirectsURL Redirect Manager (301, 302, 307, 410, 451) and the redirect URL tester
404404 to homepage redirect and the optional 404 log
AdvancedSchema.org, head cleanup, verification codes, redirects
ImportSEO data migration

General Settings

Navigate to LW Plugins > SEO > General to configure basic SEO settings.

Title Separator

Choose the character used between title parts. Default is |.

Available options: |, -, >, >>, *, /

Example: “My Post Title | Site Name”

Title Templates

Configure how page titles are generated using template variables.

Homepage Title

Template for your site’s front page.

Default: %%sitename%% %%sep%% %%sitedesc%%

Example output: “My Website | Your tagline here”

Post Title

Template for single blog posts.

Default: %%title%% %%sep%% %%sitename%%

Example output: “How to Configure SEO | My Website”

Page Title

Template for static pages.

Default: %%title%% %%sep%% %%sitename%%

Archive Title

Template for category, tag, and date archives.

Default: %%term_title%% %%sep%% %%sitename%%

Search Results Title

Template for search result pages.

Default: %%searchphrase%% %%sep%% %%sitename%%

Example output: “wordpress plugins | My Website”

Taxonomy Titles

Under Content → Taxonomies, every public taxonomy has its own title template and noindex switch, not only categories and tags: WooCommerce product categories, product tags and product attributes, and custom taxonomies too (since 1.9.3).

Default: %%term_title%% %%sep%% %%sitename%%

The keys are title_<taxonomy> and noindex_<taxonomy>, so they can also be set from the command line, e.g. wp lw-seo option set title_product_cat "%%term_title%% %%pagenumber%% %%sep%% %%sitename%%". The Rank Math import brings these templates over for every taxonomy.

When an empty variable leaves two separators next to each other, only one is kept: %%term_title%% %%sep%% %%pagenumber%% %%sep%% %%sitename%% gives “Drills - Page 2 of 4 - Shop” on page 2 and “Drills - Shop” on the first page.

Tips

  • Keep titles under 60 characters for best display in search results
  • Include your brand/site name for recognition
  • Use the separator to create visual hierarchy
  • Test how your titles appear using browser dev tools or SEO testing tools

Content SEO Settings

Navigate to LW Plugins > SEO > Content to configure content-related SEO options.

Meta Descriptions

Auto-generate Meta Descriptions

When enabled, the plugin automatically creates meta descriptions from post content when no custom description is set.

  • Extracts the first ~155 characters from post content
  • Strips HTML tags and shortcodes
  • Falls back to excerpt if available

Custom Meta Descriptions

Set custom meta descriptions per post/page using the SEO meta box in the editor:

  1. Edit any post or page
  2. Scroll to the LW SEO meta box
  3. Enter your custom meta description
  4. Save the post

Canonical URLs

Enable Canonical URLs

Adds <link rel="canonical"> tags to prevent duplicate content issues.

  • Automatically generates canonical URL for each page
  • Points search engines to the preferred version of a page
  • Handles pagination correctly

Custom Canonical URLs

Override the automatic canonical URL per post/page:

  1. Edit the post/page
  2. In the LW SEO meta box, find “Canonical URL”
  3. Enter the full URL you want as canonical
  4. Save

Robots Meta

Default Robots Settings

Configure default robots meta tags for different content types.

Per-Post Robots

Override robots settings per post/page:

  1. Edit the post/page
  2. In the LW SEO meta box, find “Robots”
  3. Select desired options:
    • noindex - Don’t show in search results
    • nofollow - Don’t follow links on this page

SEO Meta Box

The LW SEO meta box appears on post/page edit screens and includes:

FieldDescription
SEO TitleCustom title (overrides template)
Meta DescriptionCustom meta description
Canonical URLCustom canonical URL
Robotsnoindex/nofollow settings
OG TitleCustom Open Graph title
OG DescriptionCustom Open Graph description
OG ImageCustom social sharing image

Tips

  • Write unique meta descriptions for important pages
  • Keep meta descriptions between 120-155 characters
  • Include a call-to-action in descriptions
  • Use noindex for thin content, thank you pages, or internal search results

Image Alt and Title Templates

Navigate to LW Plugins > SEO > Content > Images. Off by default.

Images without alt text, or with an empty alt, get one when the page is displayed. Your media library is not modified: the text is filled in at display time only.

SettingOptionDefaultDescription
Alt textimage_alt_enabledOffFill in missing alt text from the template
Alt Text Templateimage_alt_template%%image_title%%Template for the alt text
Title attributeimage_title_enabledOffAdd a title attribute to images that have none
Title Attribute Templateimage_title_template%%image_title%%Template for the title attribute

Image variables

In addition to the usual variables (%%title%%, %%sitename%%, %%sep%%), the templates support:

VariableDescription
%%image_title%%The attachment title
%%image_caption%%The attachment caption
%%image_filename%%The file name

Decorative images

An empty alt is how editors mark decorative images. To keep an image’s alt empty, add role="presentation" or aria-hidden="true" to it, and the template is not applied.


Social Media Settings

Navigate to LW Plugins > SEO > Social to configure Open Graph and Twitter Card settings.

Open Graph

Open Graph tags control how your content appears when shared on Facebook, LinkedIn, and other social platforms.

Enable Open Graph

Toggle Open Graph meta tags on/off. Enabled by default.

Default Social Image

Upload a fallback image used when posts don’t have a featured image.

Recommended specifications:

  • Size: 1200 x 630 pixels
  • Format: JPG or PNG
  • Max file size: 1MB

This image appears when sharing:

  • Posts without featured images
  • Archive pages
  • The homepage

Per-Post Open Graph

Customize Open Graph data per post in the SEO meta box:

FieldDescription
OG TitleCustom title for social sharing
OG DescriptionCustom description for social sharing
OG ImageCustom image for social sharing

Twitter Cards

Twitter Cards provide rich media experiences when your content is shared on Twitter/X.

Enable Twitter Cards

Toggle Twitter Card meta tags on/off. Enabled by default.

Twitter Card Type

Choose the card format:

TypeDescription
SummarySmall square image with title and description
Summary Large ImageLarge image above title and description

Recommendation: Use “Summary Large Image” for better visibility.

Twitter Username

Enter your Twitter/X username (without @) for attribution.

Example: lwplugins

This adds twitter:site and twitter:creator meta tags.

Generated Meta Tags

When enabled, the plugin outputs:

html
<!-- Open Graph --> <meta property="og:title" content="Post Title"> <meta property="og:description" content="Post description..."> <meta property="og:image" content="https://example.com/image.jpg"> <meta property="og:url" content="https://example.com/post/"> <meta property="og:type" content="article"> <meta property="og:site_name" content="Site Name"> <!-- Twitter Cards --> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="Post Title"> <meta name="twitter:description" content="Post description..."> <meta name="twitter:image" content="https://example.com/image.jpg"> <meta name="twitter:site" content="@lwplugins">

Testing Social Media Tags

Validate your Open Graph and Twitter Card implementation:

Tips

  • Always set a default social image
  • Use high-quality images for better engagement
  • Keep OG descriptions under 200 characters
  • Test shares before launching campaigns

Sitemap Settings

Navigate to LW Plugins > SEO > Sitemap to configure XML sitemap generation.

Enable Sitemap

Toggle XML sitemap generation on/off.

When enabled, your sitemap is available at:

text
https://yoursite.com/sitemap.xml

Sitemap URL

After enabling, flush rewrite rules to activate:

  1. Go to Settings > Permalinks
  2. Click Save Changes (no changes needed)

Include Images

Option: sitemap_images (default: on).

Featured images and images used in the content (and WooCommerce product gallery images) are listed for each URL with <image:image> entries. Content inside shortcodes and non-media blocks is skipped. Use the lw_seo_sitemap_images and lw_seo_sitemap_content_images filters to change the list (see Sitemap Filters).

Readable View in Browsers (XSL)

Option: sitemap_stylesheet (default: on).

Attaches an XSL stylesheet so the sitemap opens as a readable table in a browser. Browsers that dropped XSLT show the raw XML. Search engines are not affected either way.

Include Content Types

Select which content types to include in your sitemap:

  • Posts - Include blog posts. Enabled by default.
  • Pages - Include static pages. Enabled by default.
  • Custom Post Types - If you have custom post types (e.g., products, portfolio), they appear here. Enable as needed.

Include Taxonomies

Select which taxonomies to include:

  • Categories - Include category archive pages.
  • Tags - Include tag archive pages.
  • Custom Taxonomies - Additional taxonomies appear here if registered.

Exclude Specific Content

Exclude Posts/Pages

Exclude individual items from the sitemap:

  1. Edit the post/page
  2. In the LW SEO meta box, check “Exclude from Sitemap”
  3. Save

Exclude by ID

Enter comma-separated post IDs to exclude:

text
42, 156, 789

WooCommerce Sitemap Integration

When WooCommerce is active, additional options appear:

OptionDescription
ProductsInclude product pages
Product CategoriesInclude product category archives
Product TagsInclude product tag archives

Hidden products (catalog visibility set to Hidden) are left out of the sitemap by default (woo_sitemap_exclude_hidden), and out-of-stock products can be left out too (woo_sitemap_exclude_outofstock, off by default). See WooCommerce Integration.

Sitemap Structure

The generated sitemap follows the sitemaps.org  protocol:

xml
<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>https://yoursite.com/</loc> <lastmod>2024-01-15T10:30:00+00:00</lastmod> <changefreq>daily</changefreq> <priority>1.0</priority> </url> <url> <loc>https://yoursite.com/sample-post/</loc> <lastmod>2024-01-14T15:20:00+00:00</lastmod> <changefreq>monthly</changefreq> <priority>0.8</priority> </url> </urlset>

Submitting to Search Engines

Google Search Console

  1. Go to Search Console 
  2. Select your property
  3. Navigate to Sitemaps
  4. Enter sitemap.xml and click Submit

Bing Webmaster Tools

  1. Go to Bing Webmaster Tools 
  2. Select your site
  3. Navigate to Sitemaps
  4. Submit your sitemap URL

HTML Sitemap

A page for your visitors that links to your content. Add it with the [lw_html_sitemap] shortcode or the HTML Sitemap (LW SEO) block (lw-seo/html-sitemap). The list follows the XML sitemap’s rules: noindex content and excluded items never appear. The markup is cached, and the cache is dropped when content changes.

SettingOptionDefaultDescription
Post typeshtml_sitemap_post_typesnone selected (falls back to Posts and Pages)Post types to list
Taxonomieshtml_sitemap_taxonomiesnone selectedTaxonomies to list
Max items per sectionhtml_sitemap_max_items500Longer lists are cut off and get a “View all” link
Orderhtml_sitemap_ordertitletitle or date (newest first)

Shortcode attributes override the settings:

AttributeDescription
post_typesComma-separated post type names
taxonomiesComma-separated taxonomy names
maxItems per section. It can only lower the setting, never raise it
ordertitle or date
text
[lw_html_sitemap] [lw_html_sitemap post_types="page,post" taxonomies="category" max="100" order="date"]

Unknown post types and taxonomies are ignored. The block takes the same four attributes. Use the lw_seo_html_sitemap_sections and lw_seo_html_sitemap_output filters to change the list or the markup.

IndexNow

Navigate to LW Plugins > SEO > Sitemap > IndexNow. Off by default (indexnow_enabled).

When on, LW SEO tells participating search engines (Bing, Yandex, Seznam, Naver and others) about new, changed and deleted public content right away instead of waiting for the next crawl. Google does not use IndexNow.

  • Key file - a verification key is generated on your site and served at https://yoursite.com/{key}.txt. Search engines fetch it to confirm the site is yours. Use Generate a new key to replace it.
  • Queue - changes are queued (up to 10,000 entries) and sent in a batch by a scheduled event about a minute later. Only public, indexable content is sent.
  • Submit URLs now - paste up to 100 addresses of your site, one per line, and click Submit. Save your settings first.
  • Recent submissions - the last 20 submissions with time, URL count and result.
  • Paused on non-public sites - while Settings > Reading > Discourage search engines is set, or the environment type (WP_ENVIRONMENT_TYPE) is not production, nothing is queued, sent or served, and the screen says why. A copied database (staging, local) keeps the switch and the key, so this stops the copy from notifying search engines. Use the lw_seo_indexnow_active filter to override.

External services: when IndexNow is on, the URLs of public, indexable content are sent to api.indexnow.org when they are published, updated or deleted. Sent: your site’s host name, the page URLs, the verification key and the key file URL. No personal data is sent. See the IndexNow terms .

WP-CLI

bash
# Submit URLs now, or add them to the queue wp lw-seo indexnow submit https://yoursite.com/a/ https://yoursite.com/b/ wp lw-seo indexnow submit https://yoursite.com/a/ --queue # State: enabled, key file, queue and the last submissions wp lw-seo indexnow status [--format=<table|csv|json>] # Key file URL; --regenerate replaces the key wp lw-seo indexnow key [--regenerate]

The commands refuse to run while IndexNow is off (wp lw-seo option update indexnow_enabled 1 turns it on). submit also refuses, and status warns, while IndexNow is paused on a non-public or non-production site.

Tips

  • Keep sitemaps under 50,000 URLs
  • Exclude thin content and duplicate pages
  • Update sitemap after major content changes
  • Monitor indexing status in Search Console

AI/LLM Settings

Navigate to LW Plugins > SEO > AI/LLM to control AI content signals, crawler access, and llms.txt generation.

Content Signals

What are Content Signals?

Content Signals tell AI agents how they may use your content. They are sent as:

  • HTTP header (X-Content-Signals) on every response
  • HTML meta tag (<meta name="ai-content-signals">) in the <head>

Inspired by Cloudflare - Markdown for Agents .

Global Settings

Three checkboxes, all enabled by default:

SettingHeader keyDescription
AI Trainingai-trainAllow AI systems to use content for model training
AI Input (RAG)ai-inputAllow AI systems to use content for generating responses
SearchsearchAllow content to appear in AI search results

Per-Content Override

Each post/page has an AI Content Signals section in the LW SEO meta box (post editor). Three dropdowns:

  • Default - uses the global setting
  • Yes - explicitly allow
  • No - explicitly deny

Output Example

HTTP header (every response):

text
X-Content-Signals: ai-train=yes, ai-input=yes, search=yes

HTML meta tag:

html
<meta name="ai-content-signals" content="ai-train=yes, ai-input=no, search=yes" />

Resolution Order

  1. Per-post meta value (if not “default”)
  2. lw_seo_content_signals filter (if applied)
  3. Global setting from AI/LLM tab

Markdown Endpoint (/md)

What is it?

Every post, page, taxonomy, and WooCommerce product is available as clean markdown. AI agents can consume content without parsing HTML.

Three Ways to Access

1. URL suffix:

text
https://yoursite.com/hello-world/md/ https://yoursite.com/category/news/md/ https://yoursite.com/product/shoes/md/

2. Query parameter:

text
https://yoursite.com/?p=123&format=md

3. Accept header (singular pages only):

bash
curl -H "Accept: text/markdown" https://yoursite.com/hello-world/

Response Format

Headers:

text
Content-Type: text/markdown; charset=UTF-8 X-Content-Signals: ai-train=yes, ai-input=yes, search=yes X-Markdown-Tokens: 1250

Body - YAML frontmatter + markdown:

markdown
--- title: "Hello World" url: "https://yoursite.com/hello-world/" date: "2026-03-21" modified: "2026-03-21" author: "Admin" language: "hu_HU" categories: ["WordPress"] tags: ["example"] featured_image: "https://yoursite.com/image.jpg" --- # Hello World Your post content converted to markdown...

Security

  • Only publish and private (with capability) posts are accessible
  • Password-protected posts return 403
  • Non-public taxonomies return 404
  • Draft, pending, future posts return 404

Flush Rewrite Rules

After activating the plugin, go to Settings > Permalinks > Save Changes to flush rewrite rules. Without this, the /md/ URLs won’t work.

Renderers

Content TypeRendererFrontmatter
Post/PagePostRenderertitle, url, date, modified, author, language, categories, tags, featured_image, excerpt
WC ProductProductRenderertitle, url, language, price, sku, stock_status, categories, featured_image
TaxonomyTaxonomyRenderertitle, url, type, post_count, language, parent

Response Headers

HeaderDescription
Content-Type: text/markdown; charset=UTF-8MIME type
X-Content-Signals: ai-train=yes, ai-input=yes, search=yesContent permission signals
X-Markdown-Tokens: 1250Approximate token count (mb_strlen / 4)

HTML to Markdown Conversion

The HtmlToMarkdown helper converts post_content HTML to markdown:

HTMLMarkdown
<h1>-<h6>#-######
<p>Double newline
<a>[text](url)
<strong>, <b>**text**
<em>, <i>*text*
<ul><li>- item
<ol><li>1. item
<blockquote>> text
<code>`code`
<pre>Fenced code block
<img>![alt](src)
<table>Markdown table
UnsupportedPlain text fallback

llms.txt

What is llms.txt?

The llms.txt file provides structured information about your website to AI systems. Similar to robots.txt but designed for LLMs.

Learn more: llmstxt.org 

When enabled, available at: https://yoursite.com/llms.txt

Content

The generated file includes:

  • Site name and description
  • Content summary (post/page/category counts)
  • Important pages
  • Recent posts
  • Sitemap reference

AI Crawler Control

Block or allow specific AI crawlers via robots.txt rules.

Available Crawlers

CrawlerCompanyPurpose
GPTBotOpenAIChatGPT training data
ChatGPT-UserOpenAIChatGPT browsing
Claude-WebAnthropicClaude training
Google-ExtendedGoogleGemini training
BytespiderByteDanceTikTok AI training
CCBotCommon CrawlOpen dataset
PerplexityBotPerplexityAI search engine
Cohere-AICohereAI training

Blocking

Check “Block” to add a Disallow rule to robots.txt:

text
User-agent: GPTBot Disallow: /

Content Signals vs Crawler Blocking

FeatureScopeEnforcement
Content SignalsPer-contentAdvisory (agent decides)
AI Crawler BlockingSite-wideHard block (robots.txt)

Content Signals are advisory - the AI agent decides whether to respect them. Crawler blocking via robots.txt is a hard block (though also technically advisory).

Technical Notes

  • robots.txt blocking doesn’t remove already-crawled content
  • Content Signals follow the emerging web standard for AI content permissions
  • The /md endpoint respects Content Signals but doesn’t block content - it includes the signals in the response headers
  • X-Markdown-Tokens is an approximate token count (mb_strlen / 4)

Local SEO Settings

Navigate to LW Plugins > SEO > Local SEO to configure LocalBusiness schema markup.

Overview

Local SEO helps your business appear in local search results and Google Maps. The plugin generates Schema.org LocalBusiness structured data.

Enable Local SEO

Toggle LocalBusiness schema generation on/off.

Business Information

Business Type

Select the Schema.org type that best describes your business. 100+ options available including:

CategoryExamples
FoodRestaurant, Bakery, CafeOrCoffeeShop, FastFoodRestaurant
RetailStore, ClothingStore, ElectronicsStore, GroceryStore
ServicesAutoRepair, HairSalon, DaySpa, RealEstateAgent
HealthDentist, Pharmacy, MedicalClinic, Optician
EntertainmentMovieTheater, NightClub, SportingGoodsStore
ProfessionalAccountingService, Attorney, FinancialService

Business Name

Your official business name. Leave empty to use the site name.

Business Description

A short description of your business (1-2 sentences).

Price Range

Indicate your price level:

  • $ - Budget
  • $$ - Moderate
  • $$$ - Expensive
  • $$$$ - Luxury

Address

Enter your physical business address:

FieldDescriptionExample
Street AddressPrimary street address123 Main Street
Address Line 2Suite, unit, floorSuite 100
CityCity/localityBudapest
State/RegionState, province, regionBudapest
Postal CodeZIP/postal code1051
CountryISO 3166-1 alpha-2 codeHU

Country codes: US, HU, DE, GB, FR, etc.

Contact Information

Phone

Enter your business phone number in international format:

text
+36 1 234 5678

Email

Business contact email:

Opening Hours

Enable Opening Hours

Toggle opening hours in the schema output.

Configure Hours

For each day of the week:

  1. Closed - Check if closed on this day
  2. Open time - Opening time (e.g., 09:00)
  3. Close time - Closing time (e.g., 17:00)

Use 24-hour format for times.

Coordinates

Latitude & Longitude

Enter your exact location coordinates for map accuracy.

Finding coordinates:

  1. Go to Google Maps 
  2. Right-click on your location
  3. Click the coordinates to copy them
  4. First number is latitude, second is longitude

Example:

  • Latitude: 47.4979
  • Longitude: 19.0402

Generated Schema

The plugin outputs JSON-LD structured data:

json
{ "@context": "https://schema.org", "@type": "Restaurant", "name": "My Business", "description": "A great local business", "address": { "@type": "PostalAddress", "streetAddress": "123 Main Street", "addressLocality": "Budapest", "postalCode": "1051", "addressCountry": "HU" }, "telephone": "+36 1 234 5678", "email": "[email protected]", "priceRange": "$$", "geo": { "@type": "GeoCoordinates", "latitude": 47.4979, "longitude": 19.0402 }, "openingHoursSpecification": [ { "@type": "OpeningHoursSpecification", "dayOfWeek": "Monday", "opens": "09:00", "closes": "17:00" } ] }

Testing Local SEO

Validate your Local SEO markup:

Tips

  • Use the most specific business type available
  • Keep business info consistent across the web (NAP consistency)
  • Add coordinates for accurate map placement
  • Update hours for holidays using your Google Business Profile

Redirect Manager

Navigate to LW Plugins > SEO > Redirects to create and manage URL redirects.

Overview

The Redirect Manager helps you:

  • Prevent 404 errors when URLs change
  • Preserve SEO value when moving content
  • Handle deleted pages gracefully
  • Set up complex redirect patterns with regex

Enable Redirects

Toggle redirect processing on/off. When disabled, no redirect rules are applied.

Adding a Redirect

Source URL

The old URL path that should redirect. Enter just the path without domain:

text
/old-page/ /blog/2023/old-post/ /products/discontinued-item/

Destination URL

The new URL to redirect to. Can be:

  • Relative path: /new-page/
  • Full URL: https://example.com/new-page/
  • External URL: https://other-site.com/page/

Leave empty for 410/451 status codes.

Redirect Types

CodeNameUse Case
301Moved PermanentlyContent permanently moved. Transfers SEO value.
302Found (Temporary)Content temporarily moved. Testing changes.
307Temporary RedirectLike 302 but preserves request method. API endpoints.
410Content DeletedContent intentionally removed. No destination.
451Unavailable For Legal ReasonsLegal takedown. GDPR, DMCA. No destination.

Best Practices

  • Use 301 for permanent moves (most common)
  • Use 302 only for temporary changes
  • Use 410 when content is intentionally deleted
  • Use 451 for legal compliance

Regex Redirects

Enable “Source is a regular expression” for pattern matching.

Example Patterns

Redirect category URLs:

text
Source: ^/category/(.*)$ Destination: /blog/category/$1

Redirect all blog posts:

text
Source: ^/blog/(\d{4})/(\d{2})/(.*)$ Destination: /articles/$1-$2-$3

Redirect product URLs:

text
Source: ^/shop/(.*)$ Destination: /store/$1

Capture Groups

Use $1, $2, etc. in destination to reference captured groups:

PatternInputCaptures
^/old/(.*)$/old/page$1 = page
^/(\d+)/(\w+)$/123/abc$1 = 123, $2 = abc

Managing Redirects

Redirect Table

The table shows all configured redirects:

ColumnDescription
SourceThe URL path to match
DestinationWhere to redirect
TypeHTTP status code
HitsNumber of times triggered
Last AccessedMost recent redirect time

Edit Redirect

Click Edit to modify an existing redirect. The form populates with current values.

Delete Redirect

Click Delete to remove a redirect. Confirm the deletion when prompted.

Delete Several Redirects

Tick the checkboxes of the redirects to remove (the header checkbox selects the whole page), then click Delete in the selection bar and confirm. The selection is kept across pages and filters. From the command line: wp lw-seo redirect delete <id>....

Import / Export

Export to CSV

Click Download CSV to export all redirects. Format:

csv
source,destination,type,regex /old-page/,/new-page/,301,false ^/category/(.*)$,/blog/$1,301,true /deleted-page/,,410,false

Import from CSV

  1. Prepare a CSV file with columns: source, destination, type, regex
  2. Select the file
  3. Click Import CSV

CSV Format:

ColumnRequiredValues
sourceYesURL path or regex pattern
destinationNo*Target URL (empty for 410/451)
typeNo301, 302, 307, 410, 451 (default: 301)
regexNotrue, false, 1, 0, yes, no (default: false)

Test a URL

Under Redirects > Test a URL, enter a path or full URL and click Test to see which redirect it would hit: the matching rule and destination, No redirect matches, or Invalid regex for rules whose pattern is broken. The query string is ignored, and the checked rules are listed.

The same check from the command line:

bash
wp lw-seo redirect test /old-page/

Invalid regular-expression redirects are skipped instead of causing PHP warnings.

How It Works

  1. When a request comes in, the plugin checks for a matching redirect
  2. Exact matches are checked first, then regex patterns
  3. If found, the appropriate HTTP redirect is sent
  4. Hit counter is incremented for analytics

Performance

  • Redirects are stored in WordPress options (no database table)
  • Matching happens early in the request lifecycle
  • Regex patterns are compiled once per request
  • Minimal impact on non-matching URLs

Troubleshooting

Redirect Not Working

  1. Check if “Enable Redirects” is on
  2. Verify the source path is correct (starts with /)
  3. Check for conflicting redirects
  4. Clear any caching plugins
  5. Test in incognito/private browsing

Redirect Loop

A redirect pointing to itself or creating a chain that loops back:

text
/page-a/ -> /page-b/ /page-b/ -> /page-a/ <- Loop!

Remove one redirect to break the loop.

Regex Not Matching

  1. Test your pattern at regex101.com 
  2. Ensure special characters are escaped: \., \?, etc.
  3. Use ^ for start and $ for end anchors
  4. Check the “Invalid regex pattern” error message

Migration from Other Plugins

From Yoast SEO Premium

Export redirects from Yoast as CSV and import into LW SEO. The format is compatible.

From Redirection Plugin

  1. Export from Redirection (Tools > Export > CSV)
  2. Adjust columns to match: source, destination, type, regex
  3. Import into LW SEO

From .htaccess

Convert .htaccess rules to LW SEO redirects:

apache
# .htaccess Redirect 301 /old-page/ /new-page/ RedirectMatch 301 ^/blog/(.*)$ /articles/$1

Becomes:

csv
source,destination,type,regex /old-page/,/new-page/,301,false ^/blog/(.*)$,/articles/$1,301,true

Tips

  • Start with exact matches, use regex only when needed
  • Test redirects immediately after creating them
  • Review hit counts periodically to identify unused redirects
  • Export redirects before major site changes
  • Use 301 for SEO value transfer

404 Log

Navigate to LW Plugins > SEO > 404. Besides the Redirect 404 to Homepage switch (redirect_404_to_home, a 302 redirect, off by default), the tab has an optional log of missing URLs. The log is off by default.

SettingOptionDefaultDescription
Log 404 errorsnot_found_log_enabledOffStores the requested path, a hit count and the referrer without its query string. Static files such as images are skipped
Keep entries for (days)not_found_log_retention_days30Older entries are removed daily
Maximum entriesnot_found_log_max_rows1000When the log is full, the oldest one-off entries make room. Recurring entries stay until the daily cleanup
Store user agentnot_found_log_store_uaOffAlso keeps the visitor’s browser or bot name

The log is kept in its own database table, and a daily event applies the retention window and the row cap.

Logged 404 URLs

With logging on, the tab lists the URLs with Hits, Last seen and Referrer, and a search box. Per row:

  • Create redirect - adds a redirect for that URL (see Redirect Manager)
  • Delete - removes the entry

Clear log deletes every logged URL after a confirmation.

WP-CLI

bash
# List logged URLs, most recent first wp lw-seo not-found list [--search=<text>] [--orderby=<last_seen|hits|url>] [--limit=<1-100>] [--format=<table|csv|json|count>] # Empty the log wp lw-seo not-found clear [--yes] # Apply the retention window and row cap now wp lw-seo not-found prune

Advanced Settings

Navigate to LW Plugins > SEO > Advanced to configure advanced SEO options.

Schema.org / JSON-LD

Organization or Person

Choose how your site is represented in schema markup:

TypeUse Case
OrganizationBusiness, company, brand
PersonPersonal blog, portfolio, individual

Organization Name

Your organization’s official name. Defaults to site name.

Upload your logo for schema markup. Recommended: 112x112 pixels minimum, square format.

Head Cleanup

Remove unnecessary elements from the <head> section to clean up your HTML.

Removes the shortlink <link> tag:

html
<!-- Removed: --> <link rel='shortlink' href='https://example.com/?p=123' />

Removes the Really Simple Discovery link (used by XML-RPC clients):

html
<!-- Removed: --> <link rel="EditURI" type="application/rsd+xml" href="https://example.com/xmlrpc.php?rsd" />

Remove Windows Live Writer Manifest

Removes the WLW manifest link:

html
<!-- Removed: --> <link rel="wlwmanifest" type="application/wlwmanifest+xml" href="https://example.com/wp-includes/wlwmanifest.xml" />

Remove WordPress Generator Tag

Removes the WordPress version meta tag:

html
<!-- Removed: --> <meta name="generator" content="WordPress 6.4" />

Security note: Hiding the WordPress version provides minimal security benefit but reduces information disclosure.

Verification Codes

Add search engine verification meta tags without editing theme files.

Google Search Console

  1. Go to Search Console 
  2. Add your property
  3. Choose “HTML tag” verification
  4. Copy the content value from the meta tag
  5. Paste into the “Google Verification” field

Bing Webmaster Tools

  1. Go to Bing Webmaster Tools 
  2. Add your site
  3. Choose “Meta tag” verification
  4. Copy the content value
  5. Paste into the “Bing Verification” field

Generated Output

html
<meta name="google-site-verification" content="your-google-code" /> <meta name="msvalidate.01" content="your-bing-code" />

Redirects

www/non-www Redirect

Ensure consistent URL format:

OptionDescription
No redirectLet server handle (default)
Force wwwRedirect non-www to www
Force non-wwwRedirect www to non-www

Note: This is better handled at server/hosting level for performance.

HTTPS Redirect

Force HTTPS for all pages:

OptionDescription
No redirectAllow both HTTP and HTTPS
Force HTTPSRedirect HTTP to HTTPS

Note: Ensure you have a valid SSL certificate before enabling.

Additional Meta Tags

Add custom meta tags to all pages:

html
<meta name="author" content="Your Name"> <meta name="copyright" content="Your Company">

Enter one meta tag per line. Tags are output in the <head> section.

Performance

Disable Plugin on Specific Pages

Disable LW SEO output on certain pages by ID:

text
42, 156, 789

Useful for landing pages with their own SEO setup.

Debug Mode

Enable Debug Output

When enabled, adds HTML comments showing:

  • Which title template was used
  • Meta description source (auto-generated vs custom)
  • Active schema types
html
<!-- LW SEO Debug: Title template: %%title%% %%sep%% %%sitename%% --> <!-- LW SEO Debug: Meta desc: auto-generated from content -->

Warning: Only enable for debugging. Disable on production sites.

Tips

  • Remove unused head elements to reduce page size
  • Always verify site ownership with search engines
  • Use HTTPS for better SEO and security
  • Test changes in staging before applying to production

WooCommerce Integration

The plugin automatically detects WooCommerce and adds product-specific SEO features. Settings are in the WooCommerce tab.

Product-specific OpenGraph Tags

  • Price, availability, brand, condition
  • Product Open Graph type (og:type: product)

Product Schema.org Markup

  • Product structured data with reviews and offers
  • Automatic price and stock status
  • A product page carries a single Product: when WooCommerce prints its own, LW SEO adds its data (description, images, brand, GTIN, reviews, seller) to that one instead of printing a second Product
  • The GTIN is read from WooCommerce’s GTIN, UPC, EAN or ISBN field, and from the older _gtin meta

WooCommerce Settings Tab

SettingOptionDefaultDescription
WooCommerce SEOwoo_enabledOnMaster switch for the product SEO features
Product Schemawoo_schema_enabledOnProduct structured data
Include Reviewswoo_schema_reviewsOnReviews in the product data
Product variants as ProductGroupwoo_product_groupOffVariable products are output as a ProductGroup, each variation with its own price and availability
Default brandwoo_default_brandemptyUsed as the product brand when a product has no brand attribute or brand term
Hidden products (sitemap)woo_sitemap_exclude_hiddenOnProducts with catalog visibility Hidden are left out of the sitemap
Noindex hidden productswoo_noindex_hiddenOnHidden products get noindex
Out-of-stock products (sitemap)woo_sitemap_exclude_outofstockOffLeave out-of-stock products out of the sitemap

Shipping Details

Adds shipping cost and delivery time to the product offers in the structured data. Off by default.

SettingOptionDefault
Shipping detailswoo_shipping_enabledOff
Countrieswoo_shipping_countriesempty
Shipping cost: Flat rate or Free shippingwoo_shipping_rate_typeflat
Ratewoo_shipping_rateempty
Handling time, from / to (days)woo_handling_min / woo_handling_max0 / 1
Transit time, from / to (days)woo_transit_min / woo_transit_max1 / 5

Return Policy

Adds a merchant return policy to the product offers. Off by default.

SettingOptionDefaultDescription
Return policywoo_returns_enabledOffMaster switch
Countrywoo_return_countryemptyCountry the policy applies to
Policywoo_return_categoryfiniteReturns within a set number of days, unlimited window, or no returns
Return window (days)woo_return_days14For the set-number-of-days policy
Return methodwoo_return_methodby_mailBy mail or in store
Return costswoo_return_feesfreeFree return or customer pays

Use the lw_seo_woo_offer filter for anything else an offer needs (see WooCommerce Filters).

Sitemap Inclusion

  • Products and product taxonomies can be included in the XML sitemap
  • Product categories and tags as separate sitemap entries
  • Gallery images are listed in the image sitemap

Template Variables

Use these variables in title templates to dynamically generate page titles.

Available Variables

VariableDescriptionExample Output
%%sitename%%Site name from Settings > GeneralMy Website
%%sitedesc%%Site tagline from Settings > GeneralJust another WordPress site
%%title%%Post/page titleHow to Configure SEO
%%sep%%Separator character (configured in settings)|
%%excerpt%%Post excerpt (first 155 chars)This post explains…
%%author%%Post author display nameJohn Doe
%%category%%Primary category nameWordPress
%%term_title%%Current taxonomy term titleTutorials
%%searchphrase%%Search query on search pageswordpress seo
%%currentdate%%Current dateJanuary 15, 2024
%%currentyear%%Current year2024
%%currentmonth%%Current month nameJanuary
%%page%%Page number for paginated content2
%%pagenumber%%Page string, empty on the first pagePage 2 of 5
%%pagetotal%%Total pages for paginated content5
%%currentday%%Current day of month15

Usage Examples

Homepage

Template: %%sitename%% %%sep%% %%sitedesc%%

Output: My Website | Your tagline here

Single Post

Template: %%title%% %%sep%% %%sitename%%

Output: How to Configure SEO | My Website

Category Archive

Template: %%term_title%% %%sep%% %%sitename%%

Output: Tutorials | My Website

Search Results

Template: Search: %%searchphrase%% %%sep%% %%sitename%%

Output: Search: wordpress seo | My Website

Author Archive

Template: Posts by %%author%% %%sep%% %%sitename%%

Output: Posts by John Doe | My Website

Paginated Archives

Template: %%term_title%% %%sep%% Page %%page%% of %%pagetotal%% %%sep%% %%sitename%%

Output: Tutorials | Page 2 of 5 | My Website

Date-based Content

Template: %%title%% %%sep%% Published %%currentmonth%% %%currentyear%%

Output: My Post | Published January 2024

Context Availability

Not all variables work on every page type:

VariableHomepagePostsPagesArchivesSearch
%%sitename%%YesYesYesYesYes
%%sitedesc%%YesYesYesYesYes
%%title%%NoYesYesNoNo
%%sep%%YesYesYesYesYes
%%excerpt%%NoYesYesNoNo
%%author%%NoYesYesYes*No
%%category%%NoYesNoNoNo
%%term_title%%NoNoNoYesNo
%%searchphrase%%NoNoNoNoYes
%%currentdate%%YesYesYesYesYes
%%page%%Yes*Yes*Yes*Yes*Yes*

*Only on paginated content

Custom Title Override

Override the template for individual posts/pages:

  1. Edit the post/page
  2. Find the LW SEO meta box
  3. Enter a custom title in the “SEO Title” field
  4. This replaces the template entirely

Tips

  • Google displays ~50-60 characters
  • Important info should be at the beginning
  • Include brand name for recognition

Good patterns:

text
%%title%% %%sep%% %%sitename%% # Standard post %%sitename%% %%sep%% %%sitedesc%% # Homepage %%term_title%% Archives %%sep%% %%sitename%% # Archives

Avoid:

text
%%sitename%% %%sep%% %%title%% %%sep%% %%category%% %%sep%% %%author%% # Too long, keyword stuffing %%sitename%% # Too short, no context

Shortcodes

LW SEO provides shortcodes for displaying SEO-related content in your posts, pages, and widgets.

[lw_breadcrumbs]

Display a breadcrumb navigation trail.

Attributes:

AttributeDefaultDescription
separator>>Character between breadcrumb items
homeHomeText for home link
schematrueInclude Schema.org markup

Examples:

text
[lw_breadcrumbs] [lw_breadcrumbs separator=" / " home="Start"] [lw_breadcrumbs schema="false"]

Output:

html
<nav class="lw-seo-breadcrumbs" aria-label="Breadcrumb"> <ol itemscope itemtype="https://schema.org/BreadcrumbList"> <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"> <a itemprop="item" href="https://example.com/"> <span itemprop="name">Home</span> </a> <meta itemprop="position" content="1" /> </li> <li>>></li> <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"> <span itemprop="name">Current Page</span> <meta itemprop="position" content="2" /> </li> </ol> </nav>

PHP Function:

php
<?php if ( function_exists( 'lw_seo_breadcrumbs' ) ) { lw_seo_breadcrumbs(); } ?>

HTML Sitemap Shortcode

[lw_html_sitemap]

Display a visitor-friendly list of your content. The settings and attributes are described under HTML Sitemap.

Attributes: post_types, taxonomies, max, order (all optional, they override the Sitemap tab settings).

text
[lw_html_sitemap] [lw_html_sitemap post_types="page" order="date"]

The same list is available as the HTML Sitemap (LW SEO) block.

Local SEO Shortcodes

These shortcodes display business information configured in LW Plugins > SEO > Local SEO.

[lw_address]

Display your business address.

Attributes:

AttributeDefaultDescription
formatfullfull includes country, short excludes it
schematrueInclude Schema.org microdata

Examples:

text
[lw_address] [lw_address format="short"] [lw_address schema="false"]

Output:

html
<address class="lw-seo-address" itemscope itemtype="https://schema.org/PostalAddress"> <span itemprop="streetAddress">123 Main Street, Suite 100</span><br> <span itemprop="postalCode">1051</span> <span itemprop="addressLocality">Budapest</span>, <span itemprop="addressRegion">Budapest</span><br> <span itemprop="addressCountry">HU</span> </address>

[lw_phone]

Display your business phone number.

Attributes:

AttributeDefaultDescription
linktrueWrap in clickable tel: link
schematrueInclude Schema.org microdata

Examples:

text
[lw_phone] [lw_phone link="false"] [lw_phone schema="false"]

Output:

html
<span class="lw-seo-phone" itemprop="telephone"> <a href="tel:+3612345678">+36 1 234 5678</a> </span>

[lw_email]

Display your business email address.

Attributes:

AttributeDefaultDescription
linktrueWrap in clickable mailto: link
schematrueInclude Schema.org microdata

Examples:

text
[lw_email] [lw_email link="false"]

Output:

html
<span class="lw-seo-email" itemprop="email"> <a href="mailto:[email protected]">[email protected]</a> </span>

[lw_hours]

Display your business opening hours.

Attributes:

AttributeDefaultDescription
formattabletable or list
schematrueInclude Schema.org microdata

Examples:

text
[lw_hours] [lw_hours format="list"] [lw_hours format="table" schema="false"]

Table Output:

html
<div class="lw-seo-hours"> <table class="lw-seo-hours-table"> <tbody> <tr> <th>Monday</th> <td> <time itemprop="openingHours" datetime="Monday 09:00-17:00"> 09:00 - 17:00 </time> </td> </tr> <tr> <th>Saturday</th> <td>Closed</td> </tr> </tbody> </table> </div>

List Output:

html
<div class="lw-seo-hours"> <ul class="lw-seo-hours-list"> <li><strong>Monday:</strong> 09:00 - 17:00</li> <li><strong>Saturday:</strong> Closed</li> </ul> </div>

[lw_map]

Display a Google Maps embed of your business location.

Requires: Latitude and longitude set in Local SEO settings.

Attributes:

AttributeDefaultDescription
width100%Map width (px or %)
height400Map height (px)
zoom15Zoom level (1-20)

Examples:

text
[lw_map] [lw_map height="300" zoom="17"] [lw_map width="600" height="400"]

Output:

html
<div class="lw-seo-map" style="width:100%;height:400px;"> <iframe src="https://maps.google.com/maps?q=123+Main+Street%2C+Budapest&output=embed&z=15" width="100%" height="100%" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade" title="My Business"> </iframe> </div>

Styling Shortcodes

Add custom CSS to style shortcode output:

css
/* Breadcrumbs */ .lw-seo-breadcrumbs { padding: 10px 0; font-size: 14px; } .lw-seo-breadcrumbs ol { list-style: none; display: flex; flex-wrap: wrap; gap: 5px; margin: 0; padding: 0; } /* Address */ .lw-seo-address { font-style: normal; line-height: 1.6; } /* Hours Table */ .lw-seo-hours-table { width: 100%; border-collapse: collapse; } .lw-seo-hours-table th, .lw-seo-hours-table td { padding: 8px; border-bottom: 1px solid #eee; text-align: left; } /* Map */ .lw-seo-map { border-radius: 8px; overflow: hidden; }

Tips

  • Use shortcodes in widgets for sidebar/footer display
  • Combine multiple shortcodes for a complete contact section
  • Enable schema markup for better search visibility
  • Test shortcode output after changing Local SEO settings

REST API

REST API endpoints for headless WordPress implementations. Get SEO meta data, Schema.org JSON-LD, and breadcrumbs in JSON format.

Base URL

text
/wp-json/lw-seo/v1/

Authentication

All endpoints are publicly accessible (read-only). No authentication required for published content.

For draft/private content, use WordPress Application Passwords with Basic Auth:

text
Authorization: Basic base64(username:app_password)

Get Post SEO Meta

Returns SEO meta data for a post, page, or custom post type.

Endpoint: GET /wp-json/lw-seo/v1/meta/{id}

Parameters:

ParameterTypeRequiredDescription
idintegerYesPost ID

Response:

json
{ "title": "Post Title - Site Name", "description": "Meta description for the post...", "canonical": "https://example.com/post-slug/", "robots": { "index": "index", "follow": "follow" }, "og": { "locale": "en_US", "type": "article", "title": "Post Title", "description": "OG description...", "url": "https://example.com/post-slug/", "site_name": "Site Name", "image": "https://example.com/wp-content/uploads/image.jpg", "article:published_time": "2024-01-15T10:30:00+00:00", "article:modified_time": "2024-01-16T14:22:00+00:00", "article:author": "John Doe" }, "twitter": { "card": "summary_large_image", "title": "Post Title", "description": "Twitter description...", "image": "https://example.com/wp-content/uploads/image.jpg" } }

Example:

bash
curl "https://example.com/wp-json/lw-seo/v1/meta/123"

Errors:

  • 404 - Post not found
  • 403 - Post is not accessible (draft/private without auth)

Get Term SEO Meta

Returns SEO meta data for a taxonomy term (category, tag, custom taxonomy).

Endpoint: GET /wp-json/lw-seo/v1/meta/term/{id}

Parameters:

ParameterTypeRequiredDescription
idintegerYesTerm ID
taxonomystringNoTaxonomy name (default: category)

Response:

json
{ "title": "Category Name - Site Name", "description": "Category description...", "canonical": "https://example.com/category/slug/", "robots": { "index": "index", "follow": "follow" }, "og": { "locale": "en_US", "type": "website", "title": "Category Name - Site Name", "description": "Category description...", "url": "https://example.com/category/slug/", "site_name": "Site Name", "image": "https://example.com/wp-content/uploads/default-og.jpg" }, "twitter": { "card": "summary_large_image", "title": "Category Name - Site Name", "description": "Category description...", "image": "https://example.com/wp-content/uploads/default-og.jpg" } }

Examples:

bash
# Get category meta curl "https://example.com/wp-json/lw-seo/v1/meta/term/5" # Get custom taxonomy term meta curl "https://example.com/wp-json/lw-seo/v1/meta/term/10?taxonomy=product_cat"

Errors:

  • 404 - Term not found

Get Author SEO Meta

Returns SEO meta data for an author archive.

Endpoint: GET /wp-json/lw-seo/v1/meta/author/{id}

Parameters:

ParameterTypeRequiredDescription
idintegerYesUser/Author ID

Response:

json
{ "title": "John Doe - Site Name", "description": "Author bio description...", "canonical": "https://example.com/author/johndoe/", "robots": { "index": "index", "follow": "follow" }, "og": { "locale": "en_US", "type": "profile", "title": "John Doe - Site Name", "description": "Author bio description...", "url": "https://example.com/author/johndoe/", "site_name": "Site Name", "image": "https://secure.gravatar.com/avatar/..." }, "twitter": { "card": "summary", "title": "John Doe - Site Name", "description": "Author bio description...", "image": "https://secure.gravatar.com/avatar/..." } }

Example:

bash
curl "https://example.com/wp-json/lw-seo/v1/meta/author/1"

Errors:

  • 404 - Author not found

Get Post Schema

Returns Schema.org JSON-LD structured data for a post.

Endpoint: GET /wp-json/lw-seo/v1/schema/{id}

Parameters:

ParameterTypeRequiredDescription
idintegerYesPost ID

Response:

json
{ "@context": "https://schema.org", "@graph": [ { "@type": "WebSite", "@id": "https://example.com/#website", "url": "https://example.com/", "name": "Site Name", "description": "Site tagline", "inLanguage": "en_US", "potentialAction": { "@type": "SearchAction", "target": { "@type": "EntryPoint", "urlTemplate": "https://example.com/?s={search_term_string}" }, "query-input": "required name=search_term_string" }, "publisher": { "@id": "https://example.com/#organization" } }, { "@type": "Organization", "@id": "https://example.com/#organization", "name": "Company Name", "url": "https://example.com/", "logo": { "@type": "ImageObject", "@id": "https://example.com/#logo", "url": "https://example.com/logo.png" }, "sameAs": [ "https://facebook.com/example", "https://twitter.com/example" ] }, { "@type": "WebPage", "@id": "https://example.com/post-slug/#webpage", "url": "https://example.com/post-slug/", "name": "Post Title", "isPartOf": {"@id": "https://example.com/#website"}, "inLanguage": "en_US", "datePublished": "2024-01-15T10:30:00+00:00", "dateModified": "2024-01-16T14:22:00+00:00", "primaryImageOfPage": {"@id": "https://example.com/#primaryimage"}, "thumbnailUrl": "https://example.com/wp-content/uploads/image.jpg" }, { "@type": "Article", "@id": "https://example.com/post-slug/#article", "headline": "Post Title", "datePublished": "2024-01-15T10:30:00+00:00", "dateModified": "2024-01-16T14:22:00+00:00", "mainEntityOfPage": {"@id": "https://example.com/post-slug/#webpage"}, "wordCount": 1250, "inLanguage": "en_US", "author": { "@type": "Person", "@id": "https://example.com/author/johndoe/#author", "name": "John Doe", "url": "https://example.com/author/johndoe/" }, "publisher": {"@id": "https://example.com/#organization"}, "image": { "@type": "ImageObject", "@id": "https://example.com/#primaryimage", "url": "https://example.com/wp-content/uploads/image.jpg" }, "keywords": "Category 1, Category 2" } ] }

Example:

bash
curl "https://example.com/wp-json/lw-seo/v1/schema/123"

Errors:

  • 404 - Post not found
  • 403 - Post is not accessible

Get Post Breadcrumbs

Returns breadcrumb navigation items for a post.

Endpoint: GET /wp-json/lw-seo/v1/breadcrumbs/{id}

Parameters:

ParameterTypeRequiredDescription
idintegerYesPost ID

Response:

json
[ { "title": "Home", "url": "https://example.com/", "position": 1 }, { "title": "Category Name", "url": "https://example.com/category/slug/", "position": 2 }, { "title": "Post Title", "url": "https://example.com/post-slug/", "position": 3 } ]

Example:

bash
curl "https://example.com/wp-json/lw-seo/v1/breadcrumbs/123"

Errors:

  • 404 - Post not found
  • 403 - Post is not accessible

Usage Examples

Next.js / React

javascript
// Fetch SEO data for a post async function getPostSEO(postId) { const response = await fetch( `https://example.com/wp-json/lw-seo/v1/meta/${postId}` ); return response.json(); } // Use in Next.js page export async function generateMetadata({ params }) { const seo = await getPostSEO(params.id); return { title: seo.title, description: seo.description, alternates: { canonical: seo.canonical, }, openGraph: { title: seo.og.title, description: seo.og.description, url: seo.og.url, siteName: seo.og.site_name, images: seo.og.image ? [seo.og.image] : [], locale: seo.og.locale, type: seo.og.type, }, twitter: { card: seo.twitter.card, title: seo.twitter.title, description: seo.twitter.description, images: seo.twitter.image ? [seo.twitter.image] : [], }, robots: { index: seo.robots.index === 'index', follow: seo.robots.follow === 'follow', }, }; }

Nuxt.js / Vue

javascript
// composables/useSeo.js export async function useSeo(postId) { const { data: seo } = await useFetch( `https://example.com/wp-json/lw-seo/v1/meta/${postId}` ); useHead({ title: seo.value.title, meta: [ { name: 'description', content: seo.value.description }, { property: 'og:title', content: seo.value.og.title }, { property: 'og:description', content: seo.value.og.description }, { property: 'og:image', content: seo.value.og.image }, { name: 'twitter:card', content: seo.value.twitter.card }, ], link: [ { rel: 'canonical', href: seo.value.canonical }, ], }); return seo; }

PHP (Server-side)

php
// Fetch SEO data from another WordPress site $response = wp_remote_get( 'https://example.com/wp-json/lw-seo/v1/meta/123' ); $seo = json_decode( wp_remote_retrieve_body( $response ), true ); // Use the data echo '<title>' . esc_html( $seo['title'] ) . '</title>'; echo '<meta name="description" content="' . esc_attr( $seo['description'] ) . '">';

Error Responses

All error responses follow this format:

json
{ "code": "error_code", "message": "Human readable error message", "data": { "status": 404 } }
CodeStatusDescription
post_not_found404The requested post does not exist
post_not_accessible403Post is draft/private and user lacks permission
term_not_found404The requested term does not exist
author_not_found404The requested author does not exist

For Developers

Technical documentation for developers extending or integrating with LW SEO.

Namespace & Autoloading

The plugin uses PSR-4 autoloading:

php
namespace LightweightPlugins\SEO; // Classes are in includes/ directory // LightweightPlugins\SEO\Plugin -> includes/Plugin.php // LightweightPlugins\SEO\Admin\Settings -> includes/Admin/Settings.php

Hooks & Filters

Title Filters

php
/** * Filter the generated title. * * @param string $title The generated title. * @param int $post_id Post ID (0 for non-singular pages). */ $title = apply_filters( 'lw_seo_title', $title, $post_id ); /** * Filter the title separator. * * @param string $separator The separator character. */ $separator = apply_filters( 'lw_seo_title_separator', $separator );

Meta Description Filters

php
/** * Filter the meta description. * * @param string $description The meta description. * @param int $post_id Post ID. */ $description = apply_filters( 'lw_seo_meta_description', $description, $post_id ); /** * Filter auto-generated description length. * * @param int $length Maximum characters. */ $length = apply_filters( 'lw_seo_description_length', 155 );

Open Graph Filters

php
/** * Filter Open Graph meta tags. * * @param array $og_tags Array of OG tags. * @param int $post_id Post ID. */ $og_tags = apply_filters( 'lw_seo_og_tags', $og_tags, $post_id ); /** * Filter the default social image URL. * * @param string $image_url Image URL. */ $image_url = apply_filters( 'lw_seo_default_social_image', $image_url );

Schema Filters

php
/** * Filter JSON-LD schema output. * * @param array $schema Schema data array. * @param string $type Schema type (Organization, LocalBusiness, etc). */ $schema = apply_filters( 'lw_seo_schema', $schema, $type ); /** * Filter LocalBusiness schema. * * @param array $schema LocalBusiness schema array. */ $schema = apply_filters( 'lw_seo_local_schema', $schema );

Sitemap Filters

php
/** * Filter sitemap URLs before output. * * @param array $urls Array of URL entries. */ $urls = apply_filters( 'lw_seo_sitemap_urls', $urls ); /** * Filter post types included in sitemap. * * @param array $post_types Post type names. */ $post_types = apply_filters( 'lw_seo_sitemap_post_types', $post_types ); /** * Exclude specific post from sitemap. * * @param bool $exclude Whether to exclude. * @param int $post_id Post ID. */ $exclude = apply_filters( 'lw_seo_sitemap_exclude_post', false, $post_id );
php
/** * Filter the image URLs listed for a post in the image sitemap. * * @param array $urls Absolute image URLs. * @param WP_Post $post Post. */ $urls = apply_filters( 'lw_seo_sitemap_images', $urls, $post ); /** * Whether images found in the content are listed (default true). * Return false to list the featured image only. * * @param bool $include Whether to list them. * @param WP_Post $post Post. */ $include = apply_filters( 'lw_seo_sitemap_content_images', true, $post );

HTML Sitemap Filters

php
/** * Filter the sections of the HTML sitemap before they are rendered. * * @param array $sections Sections: label, type, items, archive_url. * @param array $args Resolved arguments: post_types, taxonomies, max, order. */ $sections = apply_filters( 'lw_seo_html_sitemap_sections', $sections, $args ); /** * Filter the HTML sitemap markup. * * @param string $html Markup. * @param array $args Resolved arguments. */ $html = apply_filters( 'lw_seo_html_sitemap_output', $html, $args );

IndexNow Filter

php
/** * Filter whether IndexNow may notify search engines from this site (1.9.2). * Only consulted while IndexNow is switched on. Default: paused while search * engines are discouraged or the environment type is not production. * * @param bool $active Default decision. * @param string $reason 'not_public', 'environment', or '' when active. */ $active = apply_filters( 'lw_seo_indexnow_active', $active, $reason ); // Example: allow IndexNow on a public staging site. add_filter( 'lw_seo_indexnow_active', fn( $active, $reason ) => 'environment' === $reason ? true : $active, 10, 2 );

Robots Filter

php
/** * Filter whether a singular post is noindex. * * @param bool $noindex Whether the post is noindex by its meta or post type setting. * @param WP_Post $post The post. */ $noindex = apply_filters( 'lw_seo_post_noindex', $noindex, $post );

WooCommerce Filters

php
/** * Filter one Offer of the product markup (a variation's offer too). * * @param array $offer The Offer. A non-array return is ignored. * @param WC_Product $product The product (or the variation) it offers. */ $offer = apply_filters( 'lw_seo_woo_offer', $offer, $product ); /** * Which attribute slugs map to which schema.org property in ProductGroup markup. * * @param array $map Attribute slug (without `pa_`) => schema.org property name. */ $map = apply_filters( 'lw_seo_woo_varies_by_map', $map ); /** * Filter the product page's final Product markup, built by WooCommerce or by LW SEO. * Use it to add shipping or return policy details. * * @param array $markup Product markup. * @param WC_Product $product The product. */ $markup = apply_filters( 'lw_seo_woo_product_schema', $markup, $product );
php
/** * Filter breadcrumb items. * * @param array $items Breadcrumb items array. */ $items = apply_filters( 'lw_seo_breadcrumb_items', $items ); /** * Filter breadcrumb separator. * * @param string $separator Separator HTML. */ $separator = apply_filters( 'lw_seo_breadcrumb_separator', '>>' );

Markdown Endpoint Filters

php
/** * Control whether a request should receive markdown output. */ add_filter( 'lw_seo_markdown_is_supported', function ( bool $supported, WP_Query $query ): bool { // Disable markdown for a specific post type. if ( $query->is_singular( 'secret_docs' ) ) { return false; } return $supported; }, 10, 2 ); /** * Add or modify YAML frontmatter fields. */ add_filter( 'lw_seo_markdown_frontmatter', function ( array $data, WP_Post|WP_Term $object ): array { // Add custom field to product frontmatter. if ( $object instanceof WP_Post && 'product' === $object->post_type ) { $data['brand'] = get_post_meta( $object->ID, 'brand', true ); } return $data; }, 10, 2 ); /** * Modify or replace the markdown body content. */ add_filter( 'lw_seo_markdown_body', function ( string $body, WP_Post|WP_Term $object ): string { // Append related posts to the body. if ( $object instanceof WP_Post ) { $body .= "\n## Related\n\n"; // ... add related post links ... } return $body; }, 10, 2 ); /** * Override Content Signals for any content type. */ add_filter( 'lw_seo_content_signals', function ( array $signals, WP_Post|WP_Term|null $object ): array { // Disable AI training for premium posts. if ( $object instanceof WP_Post && has_tag( 'premium', $object ) ) { $signals['ai-train'] = 'no'; } return $signals; }, 10, 2 ); /** * Full output override (escape hatch). */ add_filter( 'lw_seo_markdown_output', function ( string $output, WP_Post|WP_Term $object ): string { // Completely replace output for a custom post type. if ( $object instanceof WP_Post && 'event' === $object->post_type ) { return "---\ntitle: ...\n---\n\nCustom event markdown..."; } return $output; }, 10, 2 );

Action Hooks

php
/** * Fires before SEO meta tags are output. */ do_action( 'lw_seo_before_meta' ); /** * Fires after SEO meta tags are output. */ do_action( 'lw_seo_after_meta' ); /** * Fires when plugin settings are saved. * * @param array $old_options Previous options. * @param array $new_options New options. */ do_action( 'lw_seo_settings_saved', $old_options, $new_options );

Options API

php
use LightweightPlugins\SEO\Options; // Get option with default $value = Options::get( 'option_name', 'default_value' ); // Get all options $all_options = Options::get_all(); // Option name constant $option_key = Options::OPTION_NAME; // 'lw_seo_options'

Available Options

Keys of lw_seo_options with their defaults (from Options::get_defaults()).

php
// General 'separator' // string: '-' 'title_home' // string: '%%sitename%% %%sep%% %%sitedesc%%' 'desc_home' // string: '' 'title_post' // string: '%%title%% %%sep%% %%sitename%%' 'title_page' // string: '%%title%% %%sep%% %%sitename%%' 'title_category' // string: '%%term_title%% %%sep%% %%sitename%%' 'title_post_tag' // string: '%%term_title%% %%sep%% %%sitename%%' 'title_author' // string: '%%author%% %%sep%% %%sitename%%' 'title_date' // string: '%%currentdate%% %%sep%% %%sitename%%' 'title_search' // string: 'Search: %%searchphrase%% %%sep%% %%sitename%%' 'title_404' // string: 'Page not found %%sep%% %%sitename%%' 'noindex_post' // bool: false (also noindex_page, noindex_category, noindex_post_tag, noindex_author) 'noindex_date' // bool: true // Social 'opengraph_enabled' // bool: true 'twitter_enabled' // bool: true 'twitter_card_type' // string: 'summary_large_image' (or 'summary') 'default_og_image' // string: default social image 'social_facebook' // string: profile URL (also social_twitter, social_instagram, // social_linkedin, social_youtube) // Organization / Person 'knowledge_type' // string: 'organization' (or 'person') 'knowledge_name' // string: '' 'knowledge_logo' // string: '' // Sitemap 'sitemap_enabled' // bool: true 'sitemap_posts' // bool: true 'sitemap_pages' // bool: true 'sitemap_categories' // bool: true 'sitemap_tags' // bool: false 'sitemap_post_types' // array: post type => bool 'sitemap_taxonomies' // array: taxonomy => bool 'sitemap_images' // bool: true (image sitemap) 'sitemap_stylesheet' // bool: true (browser view, XSL) 'html_sitemap_post_types' // array: post type => bool 'html_sitemap_taxonomies' // array: taxonomy => bool 'html_sitemap_max_items' // int: 500 'html_sitemap_order' // string: 'title' (or 'date') // IndexNow 'indexnow_enabled' // bool: false // 404 and redirects 'redirects_enabled' // bool: true 'redirect_404_to_home'// bool: false 'not_found_log_enabled' // bool: false 'not_found_log_max_rows' // int: 1000 'not_found_log_retention_days' // int: 30 'not_found_log_store_ua' // bool: false // Images 'image_alt_enabled' // bool: false 'image_alt_template' // string: '%%image_title%%' 'image_title_enabled' // bool: false 'image_title_template' // string: '%%image_title%%' // Advanced 'breadcrumbs_enabled' // bool: true 'schema_enabled' // bool: true 'robots_txt_enabled' // bool: true 'remove_shortlinks' // bool: true 'remove_rsd' // bool: true 'remove_wlw' // bool: true // WooCommerce 'woo_enabled' // bool: true 'title_product' // string: '%%title%% %%sep%% %%sitename%%' 'title_ptarchive_product' // string: '%%title%% %%sep%% %%sitename%%' 'noindex_product' // bool: false 'woo_schema_enabled' // bool: true 'woo_schema_reviews' // bool: true 'woo_product_group' // bool: false 'woo_default_brand' // string: '' 'sitemap_products' // bool: true (also sitemap_product_cat true, sitemap_product_tag false) 'woo_noindex_hidden' // bool: true 'woo_sitemap_exclude_hidden' // bool: true 'woo_sitemap_exclude_outofstock' // bool: false 'woo_shipping_enabled' // bool: false (woo_shipping_countries, woo_shipping_rate_type 'flat', // woo_shipping_rate, woo_handling_min 0, woo_handling_max 1, // woo_transit_min 1, woo_transit_max 5) 'woo_returns_enabled' // bool: false (woo_return_country, woo_return_category 'finite', // woo_return_days 14, woo_return_method 'by_mail', woo_return_fees 'free') 'wc_remove_category_base' // bool: false 'wc_remove_category_parent_slugs' // bool: false 'wc_remove_product_base' // bool: false // Local SEO 'local_enabled' // bool: false 'local_business_type' // string: 'LocalBusiness' 'local_business_name' // string: '' 'local_description' // string: '' 'local_price_range' // string: '' 'local_street' // string: '' (also local_street_2, local_city, local_state, local_zip, local_country) 'local_phone' // string: '' 'local_email' // string: '' 'local_lat' // string: '' 'local_lng' // string: '' 'local_hours_enabled' // bool: false 'local_hours_{day}_open' // string: '' (day: monday ... sunday) 'local_hours_{day}_close' // string: '' 'local_hours_{day}_closed' // bool: false // AI/LLM 'llms_txt_enabled' // bool: true 'llms_txt_max_items' // int: 100 'llms_full_txt_enabled' // bool: false 'content_signals_ai_train' // string: '' (also content_signals_ai_input, content_signals_search) 'block_{crawler}' // bool: false, one per crawler, e.g. block_gptbot, block_claudebot, // block_google_extended, block_ccbot 'block_purpose_{purpose}' // bool: false, blocks every crawler of a purpose

Post Meta

php
// Meta key prefix $prefix = '_lw_seo_'; // Available meta keys '_lw_seo_title' // Custom SEO title '_lw_seo_description' // Custom meta description '_lw_seo_canonical' // Custom canonical URL '_lw_seo_noindex' // bool: noindex this post '_lw_seo_nofollow' // bool: nofollow this post '_lw_seo_og_title' // Custom OG title '_lw_seo_og_description' // Custom OG description '_lw_seo_og_image' // Custom OG image (attachment ID) '_lw_seo_exclude_sitemap' // bool: Exclude from sitemap // Example usage $custom_title = get_post_meta( $post_id, '_lw_seo_title', true );

PHP Functions

php
/** * Output breadcrumbs. * * @param array $args { * @type string $separator Separator between items. Default '>>'. * @type string $home Home link text. Default 'Home'. * @type bool $schema Include schema markup. Default true. * @type bool $echo Echo or return. Default true. * } * @return string|void */ lw_seo_breadcrumbs( $args = [] ); // Example lw_seo_breadcrumbs( [ 'separator' => ' / ', 'home' => 'Start', 'schema' => true, ] );

Extending the Plugin

Add Custom Schema Type

php
add_filter( 'lw_seo_schema', function( $schema, $type ) { if ( is_singular( 'product' ) ) { $schema['@type'] = 'Product'; $schema['name'] = get_the_title(); $schema['description'] = get_the_excerpt(); // Add more product properties... } return $schema; }, 10, 2 );

Add Custom Template Variable

php
add_filter( 'lw_seo_title', function( $title, $post_id ) { // Replace custom variable if ( strpos( $title, '%%custom%%' ) !== false ) { $custom_value = 'My Custom Value'; $title = str_replace( '%%custom%%', $custom_value, $title ); } return $title; }, 10, 2 );

Modify Sitemap Output

php
add_filter( 'lw_seo_sitemap_urls', function( $urls ) { // Add custom URL $urls[] = [ 'loc' => 'https://example.com/custom-page/', 'lastmod' => '2024-01-15', 'changefreq' => 'monthly', 'priority' => '0.5', ]; return $urls; } );

Conditionally Disable Output

php
add_filter( 'lw_seo_title', function( $title, $post_id ) { // Disable on specific pages if ( $post_id === 42 ) { return ''; // Return empty to use default WP title } return $title; }, 10, 2 );

Adding Support for Custom Post Types (Markdown Endpoint)

Custom post types with public => true work automatically via PostRenderer. To customize the output:

php
// Add custom frontmatter for 'event' post type. add_filter( 'lw_seo_markdown_frontmatter', function ( array $data, WP_Post|WP_Term $object ): array { if ( ! $object instanceof WP_Post || 'event' !== $object->post_type ) { return $data; } $data['event_date'] = get_post_meta( $object->ID, 'event_date', true ); $data['location'] = get_post_meta( $object->ID, 'event_location', true ); $data['ticket_url'] = get_post_meta( $object->ID, 'ticket_url', true ); return $data; }, 10, 2 );

Adding Support for Custom Taxonomies (Markdown Endpoint)

Public custom taxonomies work automatically via TaxonomyRenderer. To customize:

php
add_filter( 'lw_seo_markdown_frontmatter', function ( array $data, WP_Post|WP_Term $object ): array { if ( ! $object instanceof WP_Term || 'location' !== $object->taxonomy ) { return $data; } $data['map_url'] = get_term_meta( $object->term_id, 'map_url', true ); return $data; }, 10, 2 );

WooCommerce Integration

The plugin automatically detects WooCommerce and adds:

  • Product schema markup
  • Product Open Graph tags (price, availability)
  • Product sitemap entries
  • Brand, ProductGroup, shipping and return policy data in the product markup
php
// Check if WooCommerce integration is active if ( class_exists( 'LightweightPlugins\SEO\WooCommerce\Integration' ) ) { // WooCommerce features available }

Conflict Detection

The plugin checks for other SEO plugins:

php
// Detected plugins that disable LW SEO output $conflicting_plugins = [ 'wordpress-seo/wp-seo.php', // Yoast SEO 'seo-by-rank-math/rank-math.php', // Rank Math 'all-in-one-seo-pack/all_in_one_seo_pack.php', // AIOSEO ];

File Structure

text
lw-seo/ ├── lw-seo.php # Main plugin file ├── composer.json # Composer config ├── includes/ │ ├── Plugin.php # Main plugin class │ ├── Options.php # Options handler │ ├── Admin/ │ │ ├── Settings.php # Settings page │ │ └── Settings/ │ │ ├── TabGeneral.php │ │ ├── TabContent.php │ │ └── ... │ ├── Frontend/ │ │ ├── Meta.php # Meta tags output │ │ ├── Schema.php # JSON-LD output │ │ └── ... │ ├── Sitemap/ │ │ └── Generator.php │ ├── Local/ │ │ ├── Schema.php │ │ └── Shortcodes.php │ └── WooCommerce/ │ └── Integration.php └── assets/ ├── css/ └── js/

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Follow WordPress Coding Standards
  4. Run composer phpcs before committing
  5. Submit a pull request

Repository: github.com/lwplugins/lw-seo 


AI Abilities (Site Manager Integration)

LW SEO registers 6 abilities with LW Site Manager, enabling AI agents and automation tools to manage SEO settings programmatically.

These abilities are only active when LW Site Manager is also installed and activated. No hard dependency - the integration is a no-op otherwise.

Abilities Overview

AbilityTypeDescription
lw-seo/get-metareadonlyGet SEO metadata for a post or term
lw-seo/set-metawriteSet SEO metadata
lw-seo/get-content-signalsreadonlyGet resolved AI Content Signals
lw-seo/get-markdownreadonlyGet content as markdown
lw-seo/get-optionsreadonlyGet global SEO settings
lw-seo/set-optionswriteUpdate global SEO settings (since 1.9.1)

Authentication

All requests require a WordPress Application Password:

bash
curl -u "[email protected]:XXXX XXXX XXXX XXXX XXXX XXXX" <URL>

Permissions

AbilityRequired capability
lw-seo/get-metaedit_posts, and read access to the target object
lw-seo/set-metaedit_posts, and edit access to the target object
lw-seo/get-content-signalsedit_posts, and read access to the target object
lw-seo/get-markdownedit_posts, and read access to the target object
lw-seo/get-optionsmanage_options
lw-seo/set-optionsmanage_options

lw-seo/get-meta

Retrieve SEO meta fields for a post or taxonomy term.

Method: GET

bash
# Post meta curl -u "user:app-password" \ "https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/get-meta/run?input[post_id]=123" # Term meta curl -u "user:app-password" \ "https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/get-meta/run?input[term_id]=5"

Response:

json
{ "success": true, "type": "post", "id": 123, "meta": { "title": "Custom SEO Title", "description": "Meta description...", "noindex": "", "og_title": "Social Title", "og_description": "Social description...", "og_image": "https://example.com/image.jpg", "ai_train": "default", "ai_input": "no", "search": "default", "markdown_content": "" } }

Available fields:

FieldTypeDescription
titlestringSEO title (overrides template)
descriptionstringMeta description
noindexstring”1” if noindex, "" if not
og_titlestringOpen Graph title
og_descriptionstringOpen Graph description
og_imagestringOpen Graph image URL
ai_trainstringAI training signal: “default” / “yes” / “no”
ai_inputstringAI input signal: “default” / “yes” / “no”
searchstringAI search signal: “default” / “yes” / “no”
markdown_contentstringCustom markdown for the /md endpoint

lw-seo/set-meta

Set SEO meta fields. Only the provided fields are updated, others remain unchanged.

Method: POST

bash
curl -u "user:app-password" \ -X POST -H "Content-Type: application/json" \ -d '{ "input": { "post_id": 123, "meta": { "title": "New SEO Title", "description": "New meta description for search engines.", "og_title": "Share Title", "og_description": "Share description.", "ai_train": "no" } } }' \ "https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/set-meta/run"

Setting term meta:

bash
curl -u "user:app-password" \ -X POST -H "Content-Type: application/json" \ -d '{ "input": { "term_id": 5, "meta": { "title": "Category SEO Title", "description": "Category description for search engines." } } }' \ "https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/set-meta/run"

Response:

json
{ "success": true, "message": "5 SEO fields updated.", "updated": ["title", "description", "og_title", "og_description", "ai_train"] }

lw-seo/get-content-signals

Get the resolved Content Signals for a post or term. Resolution order: per-post/term meta > lw_seo_content_signals filter > global setting.

Method: GET

bash
curl -u "user:app-password" \ "https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/get-content-signals/run?input[post_id]=123"

Response:

json
{ "success": true, "signals": { "ai-train": "yes", "ai-input": "no", "search": "yes" } }

If neither post_id nor term_id is provided, returns the global values.

lw-seo/get-markdown

Get content as markdown with YAML frontmatter. Returns the same output as the /md endpoint.

Method: GET

bash
# Post/page markdown curl -u "user:app-password" \ "https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/get-markdown/run?input[post_id]=123" # Taxonomy term markdown curl -u "user:app-password" \ "https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/get-markdown/run?input[term_id]=5"

Response:

json
{ "success": true, "markdown": "---\ntitle: \"Hello World\"\nurl: \"https://example.com/hello-world/\"\ndate: \"2026-03-22\"\nauthor: \"Admin\"\nlanguage: \"en_US\"\n---\n\n# Hello World\n\nContent here...\n", "tokens": 125 }

If the post/term has a custom markdown_content meta field, it takes precedence over the auto-generated conversion.

lw-seo/get-options

Get all global LW SEO settings.

Method: GET

bash
curl -u "user:app-password" \ "https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/get-options/run"

Response (excerpt):

json
{ "success": true, "options": { "separator": "-", "title_home": "%%sitename%% %%sep%% %%sitedesc%%", "sitemap_enabled": true, "llms_txt_enabled": true, "content_signals_ai_train": true, "content_signals_ai_input": true, "content_signals_search": true, "schema_enabled": true, "breadcrumbs_enabled": true } }

lw-seo/set-options

Update global LW SEO settings (since 1.9.1). Only the submitted keys change; every other setting keeps its stored value. Each value is sanitized exactly like a save on the settings screen, with the same side effects (rewrite flush, llms.txt cache). Keys that are not LW SEO options are not saved and are listed in skipped. Use lw-seo/get-options for the key names and their current values.

Method: POST

bash
curl -u "user:app-password" -X POST \ -H "Content-Type: application/json" \ -d '{"input": {"options": {"separator": "|", "sitemap_images": true}}}' \ "https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/set-options/run"

Response (excerpt):

json
{ "success": true, "updated": ["separator", "sitemap_images"], "skipped": [], "options": { "separator": "|", "sitemap_images": true } }

When no submitted key is an LW SEO option, the ability returns a 400 error (no_options) and saves nothing.

Examples

Bulk SEO meta update

bash
for id in 100 101 102 103; do curl -s -u "user:app-password" \ -X POST -H "Content-Type: application/json" \ -d "{\"input\":{\"post_id\":$id,\"meta\":{\"ai_train\":\"no\",\"ai_input\":\"yes\"}}}" \ "https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/set-meta/run" echo "" done

Export all posts as markdown

bash
ids=$(curl -s -u "user:app-password" \ "https://example.com/wp-json/wp/v2/posts?per_page=100&_fields=id" \ | python3 -c "import sys,json; [print(p['id']) for p in json.load(sys.stdin)]") for id in $ids; do curl -s -u "user:app-password" \ "https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/get-markdown/run?input[post_id]=$id" \ | python3 -c "import sys,json; d=json.load(sys.stdin); open(f'post-{$id}.md','w').write(d.get('markdown',''))" 2>/dev/null echo "Exported post $id" done

FAQ

Does this work with other SEO plugins?

LW SEO detects Yoast SEO, Rank Math, and All in One SEO and automatically disables its output to prevent conflicts.

How do I add breadcrumbs?

Use the shortcode [lw_breadcrumbs] in your content or the PHP function lw_seo_breadcrumbs() in your theme.

What is llms.txt?

The llms.txt file provides information to AI crawlers about your website. See llmstxt.org  for more details.

How do I block AI crawlers like ChatGPT or Claude?

Go to LW Plugins > SEO > AI/LLM tab and enable blocking for the crawlers you want to block. This adds the appropriate rules to your robots.txt.

How do I set a default social image?

Go to LW Plugins > SEO > Social tab and upload a default image. This will be used for Open Graph and Twitter Cards when a post has no featured image.

Where can I find the sitemap?

Your sitemap is available at yoursite.com/sitemap.xml. To regenerate rewrite rules, go to Settings > Permalinks and click Save.

How do I add an HTML sitemap page?

Create a page and add the [lw_html_sitemap] shortcode or the HTML Sitemap (LW SEO) block. Choose the post types and taxonomies under LW Plugins > SEO > Sitemap > HTML sitemap.

Does IndexNow work with Google?

No. Google does not use IndexNow. It notifies the participating search engines (Bing, Yandex, Seznam, Naver and others). It is off by default and sends the URLs of public content to api.indexnow.org once you turn it on.

How do I find broken links on my site?

Turn on the 404 log under LW Plugins > SEO > 404. Missing URLs are listed with hit counts and a Create redirect button, or use wp lw-seo not-found list.

Does the image alt template change my media library?

No. Missing alt text is filled in when the page is displayed only. Images marked with role="presentation" or aria-hidden="true" keep their empty alt.

Where can I find robots.txt and llms.txt?

  • robots.txt: yoursite.com/robots.txt
  • llms.txt: yoursite.com/llms.txt

How do I access the Markdown endpoint?

Append /md/ to any post, page, or taxonomy URL. For example: yoursite.com/hello-world/md/. You can also use the query parameter ?format=md or send an Accept: text/markdown header.

Support