LW SEO
Lightweight SEO plugin for WordPress - minimal footprint, maximum impact.
Overview
| Requires WordPress | 6.6+ |
| Requires PHP | 8.0+ |
| Tested up to | 7.1 |
| License | GPL-2.0-or-later |
| GitHub | lwplugins/lw-seo |
Quick Start
- Install and activate the plugin
- Navigate to LW Plugins > SEO
- Configure title templates in the General tab
- Upload a default social image in the Social tab
- Enable the Sitemap in the Sitemap tab
Quick search
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
- Go to Plugins > Add New
- Search for “LW SEO”
- Click Install Now
- Click Activate
Via Composer
composer require lwplugins/lw-seoThe plugin will be installed to wp-content/plugins/lw-seo/.
Manual Installation
- Download the plugin from GitHub Releases
- Upload the
lw-seofolder to/wp-content/plugins/ - Activate via Plugins menu in WordPress admin
After Installation
- Navigate to LW Plugins > SEO in the admin menu
- Configure your settings across the available tabs
- 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
- Deactivate the plugin via Plugins menu
- Delete the plugin
- 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. - Your settings (
lw_seo_optionsin thewp_optionstable) 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:
| Tab | Description |
|---|---|
| General | Title templates, separator, site name format |
| Content | Meta descriptions, canonical URLs, robots meta, image alt and title templates |
| Social | Open Graph, Twitter Cards, default social image |
| Sitemap | XML sitemap (images, browser view), HTML sitemap, IndexNow |
| AI/LLM | Content Signals, llms.txt, AI crawler blocking, Markdown endpoint |
| Local | LocalBusiness schema, address, hours |
| WooCommerce | Product SEO, brand, ProductGroup, shipping and return policy (when WooCommerce is active) |
| Redirects | URL Redirect Manager (301, 302, 307, 410, 451) and the redirect URL tester |
| 404 | 404 to homepage redirect and the optional 404 log |
| Advanced | Schema.org, head cleanup, verification codes, redirects |
| Import | SEO 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:
- Edit any post or page
- Scroll to the LW SEO meta box
- Enter your custom meta description
- 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:
- Edit the post/page
- In the LW SEO meta box, find “Canonical URL”
- Enter the full URL you want as canonical
- Save
Robots Meta
Default Robots Settings
Configure default robots meta tags for different content types.
Per-Post Robots
Override robots settings per post/page:
- Edit the post/page
- In the LW SEO meta box, find “Robots”
- 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:
| Field | Description |
|---|---|
| SEO Title | Custom title (overrides template) |
| Meta Description | Custom meta description |
| Canonical URL | Custom canonical URL |
| Robots | noindex/nofollow settings |
| OG Title | Custom Open Graph title |
| OG Description | Custom Open Graph description |
| OG Image | Custom 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.
| Setting | Option | Default | Description |
|---|---|---|---|
| Alt text | image_alt_enabled | Off | Fill in missing alt text from the template |
| Alt Text Template | image_alt_template | %%image_title%% | Template for the alt text |
| Title attribute | image_title_enabled | Off | Add a title attribute to images that have none |
| Title Attribute Template | image_title_template | %%image_title%% | Template for the title attribute |
Image variables
In addition to the usual variables (%%title%%, %%sitename%%, %%sep%%), the templates support:
| Variable | Description |
|---|---|
%%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:
| Field | Description |
|---|---|
| OG Title | Custom title for social sharing |
| OG Description | Custom description for social sharing |
| OG Image | Custom 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:
| Type | Description |
|---|---|
| Summary | Small square image with title and description |
| Summary Large Image | Large 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:
<!-- 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:
- Facebook: Sharing Debugger
- Twitter: Card Validator
- LinkedIn: Post Inspector
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:
https://yoursite.com/sitemap.xmlSitemap URL
After enabling, flush rewrite rules to activate:
- Go to Settings > Permalinks
- 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:
- Edit the post/page
- In the LW SEO meta box, check “Exclude from Sitemap”
- Save
Exclude by ID
Enter comma-separated post IDs to exclude:
42, 156, 789WooCommerce Sitemap Integration
When WooCommerce is active, additional options appear:
| Option | Description |
|---|---|
| Products | Include product pages |
| Product Categories | Include product category archives |
| Product Tags | Include 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 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
- Go to Search Console
- Select your property
- Navigate to Sitemaps
- Enter
sitemap.xmland click Submit
Bing Webmaster Tools
- Go to Bing Webmaster Tools
- Select your site
- Navigate to Sitemaps
- 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.
| Setting | Option | Default | Description |
|---|---|---|---|
| Post types | html_sitemap_post_types | none selected (falls back to Posts and Pages) | Post types to list |
| Taxonomies | html_sitemap_taxonomies | none selected | Taxonomies to list |
| Max items per section | html_sitemap_max_items | 500 | Longer lists are cut off and get a “View all” link |
| Order | html_sitemap_order | title | title or date (newest first) |
Shortcode attributes override the settings:
| Attribute | Description |
|---|---|
post_types | Comma-separated post type names |
taxonomies | Comma-separated taxonomy names |
max | Items per section. It can only lower the setting, never raise it |
order | title or date |
[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 notproduction, 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 thelw_seo_indexnow_activefilter 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
# 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:
| Setting | Header key | Description |
|---|---|---|
| AI Training | ai-train | Allow AI systems to use content for model training |
| AI Input (RAG) | ai-input | Allow AI systems to use content for generating responses |
| Search | search | Allow 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):
X-Content-Signals: ai-train=yes, ai-input=yes, search=yesHTML meta tag:
<meta name="ai-content-signals" content="ai-train=yes, ai-input=no, search=yes" />Resolution Order
- Per-post meta value (if not “default”)
lw_seo_content_signalsfilter (if applied)- 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:
https://yoursite.com/hello-world/md/
https://yoursite.com/category/news/md/
https://yoursite.com/product/shoes/md/2. Query parameter:
https://yoursite.com/?p=123&format=md3. Accept header (singular pages only):
curl -H "Accept: text/markdown" https://yoursite.com/hello-world/Response Format
Headers:
Content-Type: text/markdown; charset=UTF-8
X-Content-Signals: ai-train=yes, ai-input=yes, search=yes
X-Markdown-Tokens: 1250Body - YAML frontmatter + 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
publishandprivate(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 Type | Renderer | Frontmatter |
|---|---|---|
| Post/Page | PostRenderer | title, url, date, modified, author, language, categories, tags, featured_image, excerpt |
| WC Product | ProductRenderer | title, url, language, price, sku, stock_status, categories, featured_image |
| Taxonomy | TaxonomyRenderer | title, url, type, post_count, language, parent |
Response Headers
| Header | Description |
|---|---|
Content-Type: text/markdown; charset=UTF-8 | MIME type |
X-Content-Signals: ai-train=yes, ai-input=yes, search=yes | Content permission signals |
X-Markdown-Tokens: 1250 | Approximate token count (mb_strlen / 4) |
HTML to Markdown Conversion
The HtmlToMarkdown helper converts post_content HTML to markdown:
| HTML | Markdown |
|---|---|
<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> |  |
<table> | Markdown table |
| Unsupported | Plain 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
| Crawler | Company | Purpose |
|---|---|---|
| GPTBot | OpenAI | ChatGPT training data |
| ChatGPT-User | OpenAI | ChatGPT browsing |
| Claude-Web | Anthropic | Claude training |
| Google-Extended | Gemini training | |
| Bytespider | ByteDance | TikTok AI training |
| CCBot | Common Crawl | Open dataset |
| PerplexityBot | Perplexity | AI search engine |
| Cohere-AI | Cohere | AI training |
Blocking
Check “Block” to add a Disallow rule to robots.txt:
User-agent: GPTBot
Disallow: /Content Signals vs Crawler Blocking
| Feature | Scope | Enforcement |
|---|---|---|
| Content Signals | Per-content | Advisory (agent decides) |
| AI Crawler Blocking | Site-wide | Hard 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
/mdendpoint respects Content Signals but doesn’t block content - it includes the signals in the response headers X-Markdown-Tokensis 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:
| Category | Examples |
|---|---|
| Food | Restaurant, Bakery, CafeOrCoffeeShop, FastFoodRestaurant |
| Retail | Store, ClothingStore, ElectronicsStore, GroceryStore |
| Services | AutoRepair, HairSalon, DaySpa, RealEstateAgent |
| Health | Dentist, Pharmacy, MedicalClinic, Optician |
| Entertainment | MovieTheater, NightClub, SportingGoodsStore |
| Professional | AccountingService, 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:
| Field | Description | Example |
|---|---|---|
| Street Address | Primary street address | 123 Main Street |
| Address Line 2 | Suite, unit, floor | Suite 100 |
| City | City/locality | Budapest |
| State/Region | State, province, region | Budapest |
| Postal Code | ZIP/postal code | 1051 |
| Country | ISO 3166-1 alpha-2 code | HU |
Country codes: US, HU, DE, GB, FR, etc.
Contact Information
Phone
Enter your business phone number in international format:
+36 1 234 5678Business contact email:
Opening Hours
Enable Opening Hours
Toggle opening hours in the schema output.
Configure Hours
For each day of the week:
- Closed - Check if closed on this day
- Open time - Opening time (e.g., 09:00)
- 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:
- Go to Google Maps
- Right-click on your location
- Click the coordinates to copy them
- First number is latitude, second is longitude
Example:
- Latitude:
47.4979 - Longitude:
19.0402
Generated Schema
The plugin outputs JSON-LD structured data:
{
"@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:
/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
| Code | Name | Use Case |
|---|---|---|
| 301 | Moved Permanently | Content permanently moved. Transfers SEO value. |
| 302 | Found (Temporary) | Content temporarily moved. Testing changes. |
| 307 | Temporary Redirect | Like 302 but preserves request method. API endpoints. |
| 410 | Content Deleted | Content intentionally removed. No destination. |
| 451 | Unavailable For Legal Reasons | Legal 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:
Source: ^/category/(.*)$
Destination: /blog/category/$1Redirect all blog posts:
Source: ^/blog/(\d{4})/(\d{2})/(.*)$
Destination: /articles/$1-$2-$3Redirect product URLs:
Source: ^/shop/(.*)$
Destination: /store/$1Capture Groups
Use $1, $2, etc. in destination to reference captured groups:
| Pattern | Input | Captures |
|---|---|---|
^/old/(.*)$ | /old/page | $1 = page |
^/(\d+)/(\w+)$ | /123/abc | $1 = 123, $2 = abc |
Managing Redirects
Redirect Table
The table shows all configured redirects:
| Column | Description |
|---|---|
| Source | The URL path to match |
| Destination | Where to redirect |
| Type | HTTP status code |
| Hits | Number of times triggered |
| Last Accessed | Most 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:
source,destination,type,regex
/old-page/,/new-page/,301,false
^/category/(.*)$,/blog/$1,301,true
/deleted-page/,,410,falseImport from CSV
- Prepare a CSV file with columns:
source,destination,type,regex - Select the file
- Click Import CSV
CSV Format:
| Column | Required | Values |
|---|---|---|
| source | Yes | URL path or regex pattern |
| destination | No* | Target URL (empty for 410/451) |
| type | No | 301, 302, 307, 410, 451 (default: 301) |
| regex | No | true, 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:
wp lw-seo redirect test /old-page/Invalid regular-expression redirects are skipped instead of causing PHP warnings.
How It Works
- When a request comes in, the plugin checks for a matching redirect
- Exact matches are checked first, then regex patterns
- If found, the appropriate HTTP redirect is sent
- 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
- Check if “Enable Redirects” is on
- Verify the source path is correct (starts with
/) - Check for conflicting redirects
- Clear any caching plugins
- Test in incognito/private browsing
Redirect Loop
A redirect pointing to itself or creating a chain that loops back:
/page-a/ -> /page-b/
/page-b/ -> /page-a/ <- Loop!Remove one redirect to break the loop.
Regex Not Matching
- Test your pattern at regex101.com
- Ensure special characters are escaped:
\.,\?, etc. - Use
^for start and$for end anchors - 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
- Export from Redirection (Tools > Export > CSV)
- Adjust columns to match: source, destination, type, regex
- Import into LW SEO
From .htaccess
Convert .htaccess rules to LW SEO redirects:
# .htaccess
Redirect 301 /old-page/ /new-page/
RedirectMatch 301 ^/blog/(.*)$ /articles/$1Becomes:
source,destination,type,regex
/old-page/,/new-page/,301,false
^/blog/(.*)$,/articles/$1,301,trueTips
- 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.
| Setting | Option | Default | Description |
|---|---|---|---|
| Log 404 errors | not_found_log_enabled | Off | Stores 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_days | 30 | Older entries are removed daily |
| Maximum entries | not_found_log_max_rows | 1000 | When the log is full, the oldest one-off entries make room. Recurring entries stay until the daily cleanup |
| Store user agent | not_found_log_store_ua | Off | Also 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
# 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 pruneAdvanced 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:
| Type | Use Case |
|---|---|
| Organization | Business, company, brand |
| Person | Personal blog, portfolio, individual |
Organization Name
Your organization’s official name. Defaults to site name.
Organization Logo
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.
Remove Shortlinks
Removes the shortlink <link> tag:
<!-- Removed: -->
<link rel='shortlink' href='https://example.com/?p=123' />Remove RSD Link
Removes the Really Simple Discovery link (used by XML-RPC clients):
<!-- 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:
<!-- 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:
<!-- 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
- Go to Search Console
- Add your property
- Choose “HTML tag” verification
- Copy the content value from the meta tag
- Paste into the “Google Verification” field
Bing Webmaster Tools
- Go to Bing Webmaster Tools
- Add your site
- Choose “Meta tag” verification
- Copy the content value
- Paste into the “Bing Verification” field
Generated Output
<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:
| Option | Description |
|---|---|
| No redirect | Let server handle (default) |
| Force www | Redirect non-www to www |
| Force non-www | Redirect www to non-www |
Note: This is better handled at server/hosting level for performance.
HTTPS Redirect
Force HTTPS for all pages:
| Option | Description |
|---|---|
| No redirect | Allow both HTTP and HTTPS |
| Force HTTPS | Redirect HTTP to HTTPS |
Note: Ensure you have a valid SSL certificate before enabling.
Additional Meta Tags
Add custom meta tags to all pages:
<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:
42, 156, 789Useful 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
<!-- 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
_gtinmeta
WooCommerce Settings Tab
| Setting | Option | Default | Description |
|---|---|---|---|
| WooCommerce SEO | woo_enabled | On | Master switch for the product SEO features |
| Product Schema | woo_schema_enabled | On | Product structured data |
| Include Reviews | woo_schema_reviews | On | Reviews in the product data |
| Product variants as ProductGroup | woo_product_group | Off | Variable products are output as a ProductGroup, each variation with its own price and availability |
| Default brand | woo_default_brand | empty | Used as the product brand when a product has no brand attribute or brand term |
| Hidden products (sitemap) | woo_sitemap_exclude_hidden | On | Products with catalog visibility Hidden are left out of the sitemap |
| Noindex hidden products | woo_noindex_hidden | On | Hidden products get noindex |
| Out-of-stock products (sitemap) | woo_sitemap_exclude_outofstock | Off | Leave 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.
| Setting | Option | Default |
|---|---|---|
| Shipping details | woo_shipping_enabled | Off |
| Countries | woo_shipping_countries | empty |
| Shipping cost: Flat rate or Free shipping | woo_shipping_rate_type | flat |
| Rate | woo_shipping_rate | empty |
| Handling time, from / to (days) | woo_handling_min / woo_handling_max | 0 / 1 |
| Transit time, from / to (days) | woo_transit_min / woo_transit_max | 1 / 5 |
Return Policy
Adds a merchant return policy to the product offers. Off by default.
| Setting | Option | Default | Description |
|---|---|---|---|
| Return policy | woo_returns_enabled | Off | Master switch |
| Country | woo_return_country | empty | Country the policy applies to |
| Policy | woo_return_category | finite | Returns within a set number of days, unlimited window, or no returns |
| Return window (days) | woo_return_days | 14 | For the set-number-of-days policy |
| Return method | woo_return_method | by_mail | By mail or in store |
| Return costs | woo_return_fees | free | Free 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
| Variable | Description | Example Output |
|---|---|---|
%%sitename%% | Site name from Settings > General | My Website |
%%sitedesc%% | Site tagline from Settings > General | Just another WordPress site |
%%title%% | Post/page title | How to Configure SEO |
%%sep%% | Separator character (configured in settings) | | |
%%excerpt%% | Post excerpt (first 155 chars) | This post explains… |
%%author%% | Post author display name | John Doe |
%%category%% | Primary category name | WordPress |
%%term_title%% | Current taxonomy term title | Tutorials |
%%searchphrase%% | Search query on search pages | wordpress seo |
%%currentdate%% | Current date | January 15, 2024 |
%%currentyear%% | Current year | 2024 |
%%currentmonth%% | Current month name | January |
%%page%% | Page number for paginated content | 2 |
%%pagenumber%% | Page string, empty on the first page | Page 2 of 5 |
%%pagetotal%% | Total pages for paginated content | 5 |
%%currentday%% | Current day of month | 15 |
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:
| Variable | Homepage | Posts | Pages | Archives | Search |
|---|---|---|---|---|---|
%%sitename%% | Yes | Yes | Yes | Yes | Yes |
%%sitedesc%% | Yes | Yes | Yes | Yes | Yes |
%%title%% | No | Yes | Yes | No | No |
%%sep%% | Yes | Yes | Yes | Yes | Yes |
%%excerpt%% | No | Yes | Yes | No | No |
%%author%% | No | Yes | Yes | Yes* | No |
%%category%% | No | Yes | No | No | No |
%%term_title%% | No | No | No | Yes | No |
%%searchphrase%% | No | No | No | No | Yes |
%%currentdate%% | Yes | Yes | Yes | Yes | Yes |
%%page%% | Yes* | Yes* | Yes* | Yes* | Yes* |
*Only on paginated content
Custom Title Override
Override the template for individual posts/pages:
- Edit the post/page
- Find the LW SEO meta box
- Enter a custom title in the “SEO Title” field
- 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:
%%title%% %%sep%% %%sitename%% # Standard post
%%sitename%% %%sep%% %%sitedesc%% # Homepage
%%term_title%% Archives %%sep%% %%sitename%% # ArchivesAvoid:
%%sitename%% %%sep%% %%title%% %%sep%% %%category%% %%sep%% %%author%%
# Too long, keyword stuffing
%%sitename%%
# Too short, no contextShortcodes
LW SEO provides shortcodes for displaying SEO-related content in your posts, pages, and widgets.
Breadcrumbs
[lw_breadcrumbs]
Display a breadcrumb navigation trail.
Attributes:
| Attribute | Default | Description |
|---|---|---|
| separator | >> | Character between breadcrumb items |
| home | Home | Text for home link |
| schema | true | Include Schema.org markup |
Examples:
[lw_breadcrumbs]
[lw_breadcrumbs separator=" / " home="Start"]
[lw_breadcrumbs schema="false"]Output:
<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
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).
[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:
| Attribute | Default | Description |
|---|---|---|
| format | full | full includes country, short excludes it |
| schema | true | Include Schema.org microdata |
Examples:
[lw_address]
[lw_address format="short"]
[lw_address schema="false"]Output:
<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:
| Attribute | Default | Description |
|---|---|---|
| link | true | Wrap in clickable tel: link |
| schema | true | Include Schema.org microdata |
Examples:
[lw_phone]
[lw_phone link="false"]
[lw_phone schema="false"]Output:
<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:
| Attribute | Default | Description |
|---|---|---|
| link | true | Wrap in clickable mailto: link |
| schema | true | Include Schema.org microdata |
Examples:
[lw_email]
[lw_email link="false"]Output:
<span class="lw-seo-email" itemprop="email">
<a href="mailto:[email protected]">[email protected]</a>
</span>[lw_hours]
Display your business opening hours.
Attributes:
| Attribute | Default | Description |
|---|---|---|
| format | table | table or list |
| schema | true | Include Schema.org microdata |
Examples:
[lw_hours]
[lw_hours format="list"]
[lw_hours format="table" schema="false"]Table Output:
<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:
<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:
| Attribute | Default | Description |
|---|---|---|
| width | 100% | Map width (px or %) |
| height | 400 | Map height (px) |
| zoom | 15 | Zoom level (1-20) |
Examples:
[lw_map]
[lw_map height="300" zoom="17"]
[lw_map width="600" height="400"]Output:
<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:
/* 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
/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:
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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Post ID |
Response:
{
"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:
curl "https://example.com/wp-json/lw-seo/v1/meta/123"Errors:
404- Post not found403- 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Term ID |
| taxonomy | string | No | Taxonomy name (default: category) |
Response:
{
"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:
# 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | User/Author ID |
Response:
{
"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:
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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Post ID |
Response:
{
"@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:
curl "https://example.com/wp-json/lw-seo/v1/schema/123"Errors:
404- Post not found403- Post is not accessible
Get Post Breadcrumbs
Returns breadcrumb navigation items for a post.
Endpoint: GET /wp-json/lw-seo/v1/breadcrumbs/{id}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Post ID |
Response:
[
{
"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:
curl "https://example.com/wp-json/lw-seo/v1/breadcrumbs/123"Errors:
404- Post not found403- Post is not accessible
Usage Examples
Next.js / React
// 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
// 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)
// 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:
{
"code": "error_code",
"message": "Human readable error message",
"data": {
"status": 404
}
}| Code | Status | Description |
|---|---|---|
post_not_found | 404 | The requested post does not exist |
post_not_accessible | 403 | Post is draft/private and user lacks permission |
term_not_found | 404 | The requested term does not exist |
author_not_found | 404 | The 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:
namespace LightweightPlugins\SEO;
// Classes are in includes/ directory
// LightweightPlugins\SEO\Plugin -> includes/Plugin.php
// LightweightPlugins\SEO\Admin\Settings -> includes/Admin/Settings.phpHooks & Filters
Title Filters
/**
* 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
/**
* 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
/**
* 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
/**
* 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
/**
* 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 );/**
* 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
/**
* 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
/**
* 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
/**
* 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
/**
* 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 );Breadcrumb Filters
/**
* 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
/**
* 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
/**
* 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
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()).
// 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 purposePost Meta
// 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
Breadcrumbs
/**
* 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
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
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
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
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:
// 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:
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
// 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:
// 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
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
- Fork the repository
- Create a feature branch
- Follow WordPress Coding Standards
- Run
composer phpcsbefore committing - 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
| Ability | Type | Description |
|---|---|---|
lw-seo/get-meta | readonly | Get SEO metadata for a post or term |
lw-seo/set-meta | write | Set SEO metadata |
lw-seo/get-content-signals | readonly | Get resolved AI Content Signals |
lw-seo/get-markdown | readonly | Get content as markdown |
lw-seo/get-options | readonly | Get global SEO settings |
lw-seo/set-options | write | Update global SEO settings (since 1.9.1) |
Authentication
All requests require a WordPress Application Password:
curl -u "[email protected]:XXXX XXXX XXXX XXXX XXXX XXXX" <URL>Permissions
| Ability | Required capability |
|---|---|
lw-seo/get-meta | edit_posts, and read access to the target object |
lw-seo/set-meta | edit_posts, and edit access to the target object |
lw-seo/get-content-signals | edit_posts, and read access to the target object |
lw-seo/get-markdown | edit_posts, and read access to the target object |
lw-seo/get-options | manage_options |
lw-seo/set-options | manage_options |
lw-seo/get-meta
Retrieve SEO meta fields for a post or taxonomy term.
Method: GET
# 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:
{
"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:
| Field | Type | Description |
|---|---|---|
title | string | SEO title (overrides template) |
description | string | Meta description |
noindex | string | ”1” if noindex, "" if not |
og_title | string | Open Graph title |
og_description | string | Open Graph description |
og_image | string | Open Graph image URL |
ai_train | string | AI training signal: “default” / “yes” / “no” |
ai_input | string | AI input signal: “default” / “yes” / “no” |
search | string | AI search signal: “default” / “yes” / “no” |
markdown_content | string | Custom markdown for the /md endpoint |
lw-seo/set-meta
Set SEO meta fields. Only the provided fields are updated, others remain unchanged.
Method: POST
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:
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:
{
"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
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:
{
"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
# 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:
{
"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
curl -u "user:app-password" \
"https://example.com/wp-json/wp-abilities/v1/abilities/lw-seo/get-options/run"Response (excerpt):
{
"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
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):
{
"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
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 ""
doneExport all posts as markdown
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"
doneFAQ
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
- GitHub Issues: github.com/lwplugins/lw-seo/issues
- Website: lwplugins.com