LW SEO
Lightweight SEO plugin for WordPress - minimal footprint, maximum impact.
Overview
| Requires WordPress | 6.0+ |
| Requires PHP | 8.2+ |
| Tested up to | 6.7 |
| 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
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
- Plugin settings are stored in
wp_optionstable underlw_seo_optionsand will remain unless manually removed
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 |
| Social | Open Graph, Twitter Cards, default social image |
| Sitemap | Post types and taxonomies to include |
| AI/LLM | Content Signals, llms.txt, AI crawler blocking, Markdown endpoint |
| Local | LocalBusiness schema, address, hours |
| WooCommerce | Product SEO settings (when WooCommerce is active) |
| Advanced | Schema.org, head cleanup, verification codes, redirects |
| Redirects | URL Redirect Manager (301, 302, 307, 410, 451) |
| Import | RankMath 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”
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
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 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 |
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
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.
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) |
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
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:
| 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.
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
WooCommerce Settings Tab
When WooCommerce is active, a dedicated WooCommerce tab appears in settings for product SEO configuration.
Sitemap Inclusion
- Products and product taxonomies can be included in the XML sitemap
- Product categories and tags as separate sitemap entries
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 | Page 2 |
%%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();
}
?>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 );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
// General
'title_separator' // string: |, -, >, >>, *, /
'title_home' // string: Homepage title template
'title_post' // string: Post title template
'title_page' // string: Page title template
'title_archive' // string: Archive title template
'title_search' // string: Search title template
// Social
'og_enabled' // bool: Enable Open Graph
'twitter_enabled' // bool: Enable Twitter Cards
'twitter_card_type' // string: summary, summary_large_image
'social_twitter' // string: Twitter handle
'default_social_image'// int: Attachment ID
// Sitemap
'sitemap_enabled' // bool: Enable sitemap
'sitemap_posts' // bool: Include posts
'sitemap_pages' // bool: Include pages
// Local SEO
'local_enabled' // bool: Enable LocalBusiness schema
'local_business_type' // string: Schema.org type
'local_business_name' // string: Business name
'local_street' // string: Street address
'local_city' // string: City
'local_phone' // string: Phone number
'local_lat' // string: Latitude
'local_lng' // string: Longitude
'local_hours_enabled' // bool: Enable opening hours
'local_hours_{day}_open' // string: Opening time
'local_hours_{day}_close' // string: Closing time
'local_hours_{day}_closed' // bool: Closed on this day
// AI/LLM
'llms_txt_enabled' // bool: Enable llms.txt
'block_gptbot' // bool: Block GPTBot
'block_claude' // bool: Block Claude-Web
// ... other crawlersPost 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
// 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 5 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 |
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 |
lw-seo/set-meta | edit_posts |
lw-seo/get-content-signals | edit_posts |
lw-seo/get-markdown | edit_posts |
lw-seo/get-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
}
}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.
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