WP Manifestindependent plugin directory
manifest / seo / wp-custom-seo

WP Custom SEO

Modern SEO management for WordPress with an AI assistant.

by Manpreet Singh · github.com/manpreetdev21/wp-custom-seo · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/manpreetdev21/wp-custom-seo/archive/refs/heads/main.zip

Readme

WP Custom SEO

Modern SEO management for WordPress. Developed by Manpreet Singh.

Foundation, on-page SEO, the entity and schema graph layer, the aggregated schema API, sitemaps, breadcrumbs, social metadata, the redirect manager with its 404 monitor, the internal link graph, the bulk editor, Local SEO, the brand entity, WooCommerce, the AI assistant, the site audit, AI internal linking and FAQ generation, CSV import/export, migration from four other SEO plugins, WP-CLI commands, a configurable score model, Search Console, Analytics, email reports, AI crawler controls and the Abilities API.

Nothing here fabricates results it cannot produce. Where an external service is not connected, the feature says so and shows nothing, rather than showing zeroes or a placeholder.

Recommendations are recommendations. Nothing in this plugin is presented as a guaranteed ranking factor, and no score here represents any search engine's algorithm.

Requirements

  • PHP 8.1+ (tested on 8.4)
  • WordPress 6.5+

Install

Drop the folder in wp-content/plugins/ and activate. Composer is optional — without vendor/, a small PSR-4 fallback autoloader takes over.

For development:

composer install
composer test   # php tests/self-check.php
composer cs     # phpcs against WordPress standards
composer i18n   # regenerate languages/wp-custom-seo.pot (needs WP-CLI)

Lifecycle

Activation creates the tables, adds the capability to administrators and editors, and records the version. Deactivation is non-destructive: it deletes nothing, and clears every scheduled event, because an event for a hook nothing listens to any more is a row that sits in the cron option for good.

The list of those events is built from the constants the modules themselves declare rather than repeated as literal strings — it had already drifted once, and a hook that is renamed in one place and left scheduled here is invisible until someone reads the cron option.

Uninstall clears the scheduled events whatever your settings say — they are instructions to run code that is about to stop existing, not data. Everything else is removed only if you ticked Delete data on uninstall: the tables, the prefixed post, term and user meta, the prefixed options and transients, and the capability. Data belonging to other plugins is never touched, including anything this plugin imported from — a Yoast install is left exactly as it was.

Translations

Text domain wp-custom-seo, template at languages/wp-custom-seo.pot, loaded on init — WordPress warns when a domain is loaded earlier, and nothing here needs a translated string before then.

Architecture

Path Role
wp-custom-seo.php Header, constants, autoloader selection, lifecycle hooks
src/Core/Plugin.php Boots modules on plugins_loaded, applies pending migrations
src/Core/Autoloader.php PSR-4 fallback for WPCustomSeo\ when Composer is absent
src/Core/Activator.php Activation (multisite-aware), non-destructive deactivation
src/Core/Capabilities.php The wpcseo_manage_seo capability
src/Core/Settings.php Schema-driven settings on one option, via the Settings API
src/Database/Migrator.php Versioned, filterable migrations
src/Admin/Menu.php SEO menu, filterable page registry, page-scoped assets
src/Admin/MetaBox.php Editor SEO panel: fields, nonce, save
src/SEO/Meta.php Meta key registration and access
src/SEO/Templates.php %%variable%% expansion and truncation
src/SEO/Frontend.php Title, description, canonical, robots output
src/SEO/Analyzer.php On-page analysis and scoring
src/SEO/Weights.php The score model: what each check is worth
src/Entities/Registry.php Site entities and their stable @ids
src/Entities/Authors.php Author profile fields
src/Schema/Graph/Graph.php Node collection, merge-by-@id
src/Schema/Graph/Pieces.php Request and post graph builders
src/Schema/Validator.php Structural and referential validation
src/Schema/Faq.php Detection of a visible FAQ, and its nodes
src/Schema/Conflicts.php Detection of other schema emitters
src/Schema/Output.php Front-end JSON-LD
src/Schema/Aggregator.php Paginated site-wide schema
src/Schema/Cache.php Version-keyed aggregation cache
src/Admin/SchemaPage.php SEO → Schema
src/Admin/ToolsPage.php SEO → Tools
src/Database/Tables.php The two custom tables
src/Redirects/Redirects.php Rule storage, validation, loop detection
src/Redirects/Engine.php Request matching and dispatch
src/Redirects/NotFound.php 404 logging and pruning
src/Audit/Auditor.php The site audit
src/Audit/Finding.php One finding, with its evidence
src/Audit/Cannibalization.php Pages competing for one keyphrase
src/Audit/Decay.php Pages worth re-reading
src/Admin/AuditPage.php SEO → Site Audit
src/Transfer/Sources.php Other SEO plugins' field maps and dialects
src/Transfer/Import.php Batched copy from another plugin
src/Transfer/Csv.php Spreadsheet export and import
src/CLI/Command.php The wp seo commands
src/Crawlers/AiCrawlers.php AI crawler controls in robots.txt
src/Abilities/Abilities.php The wp-custom-seo/ abilities
src/Reports/Report.php What a periodic report contains
src/Reports/Mailer.php Subject, body and delivery
src/Reports/Schedule.php Cron reconciliation
src/Analytics/Client.php GA4 Data API client
src/Analytics/Engagement.php Organic landing pages and totals
src/SearchConsole/Account.php Service account key validation and storage
src/SearchConsole/Token.php Signed assertion and access tokens
src/SearchConsole/Client.php The API client, cached and error-translated
src/SearchConsole/Performance.php Report shaping
src/Admin/SearchConsolePage.php SEO → Search Performance
src/AI/ProviderInterface.php The provider contract
src/AI/AbstractProvider.php HTTP transport and error translation
src/AI/{Anthropic,OpenAI,Gemini}Provider.php The three shipped providers
src/AI/Credentials.php Encrypted API key storage
src/AI/Manager.php Provider selection, throttling, logging
src/AI/Prompts/ One class per prompt
src/AI/Json.php Tolerant reader for structured model output
src/Admin/BriefPage.php SEO → Content Brief
src/AI/UsageLog.php Metadata-only usage log
src/Admin/AIPage.php SEO → AI
src/API/AIRoutes.php Generation endpoints
src/Local/Locations.php Location post type, fields, shortcode
src/Local/Schema.php LocalBusiness structured data
src/WooCommerce/Integration.php Guard and WooCommerce hooks
src/WooCommerce/Product.php Product structured data
src/Links/Links.php Link graph storage and queries
src/Links/Scanner.php Link extraction and batched rebuild
src/Links/Candidates.php Real pages a post could link to
src/Admin/RedirectsPage.php SEO → Redirects
src/Admin/NotFoundPage.php SEO → 404 Monitor
src/Admin/LinksPage.php SEO → Internal Links
src/Admin/BulkEditorPage.php SEO → Bulk Editor
src/Sitemap/Sitemap.php Shapes the core XML sitemap
src/SEO/Breadcrumbs.php Trail builder, renderer, shortcode, block
src/Social/Social.php Open Graph and X/Twitter tags
src/functions.php Template functions for theme authors
src/API/Routes.php Authenticated REST endpoints
src/API/SchemaRoutes.php Public schema aggregation endpoints
templates/admin/ Admin screen markup

On-page SEO

Every public post type gets a SEO panel on its edit screen: focus keyphrase, SEO title, meta description, a search-result preview, and an Advanced tab with canonical URL and noindex/nofollow. It is a classic meta box, so it renders in the block editor, the classic editor and page builders alike, with no build step.

Meta keys, all registered through register_post_meta and therefore available over the REST API with the same sanitization and capability checks:

_wpcseo_title, _wpcseo_description, _wpcseo_focus_keyword, _wpcseo_canonical, _wpcseo_noindex, _wpcseo_nofollow.

Front-end output goes through native hooks — pre_get_document_title, wp_robots, get_canonical_url and wp_head — so themes and other plugins keep their usual chance to filter it. Everything is gated behind Settings → General → Enable SEO output.

The optimization score

Analyzer scores 16 on-page checks: title length and keyphrase placement, description length and keyphrase, content length, keyphrase distribution and placement in the introduction, subheadings, heading hierarchy, images and their alt text, internal links, external references, and the URL slug.

Each check reports what was detected, why it matters and what to do about it. The score is this plugin's own measure of how completely a page follows established on-page practice. It is not Google's algorithm and does not predict rankings.

The model is editable

Because the score is a checklist and not a measurement, you get to disagree with it. Settings → Score model gives every check a weight from 0 to 5, and Weights::defaults() is the single table those weights and labels come from — they used to sit inline at each check, which made the model a fact about the code rather than a decision anyone could see.

A weight of zero excludes a check from the score without hiding its advice. The check still runs and still says what it found, marked not counted in the editor. Silencing a recommendation and disagreeing with its importance are different things, and a site whose pages are short by design should not be told forever that its content is too short.

The fields are ordinary settings, so they are registered, sanitized, saved and capability-checked by the same code as everything else. A check added through wpcseo_analysis_checks that is not in the model is worth 1 rather than nothing, so a third-party check is never silently ignored; add it to wpcseo_score_model to give it a weight and a settings field of its own.

Entities and the schema graph

The plugin describes a site as entities with stable identifiers, and emits one connected @graph per page rather than isolated JSON-LD blocks. An entity is stated once and referenced by @id everywhere else, so a consumer can resolve author, publisher and page into a single description.

Organization ──publisher── WebSite
     │                        │
     │                     isPartOf
     │                        │
     └──publisher──── Article ──author──► Person
                         │
                    mainEntityOfPage
                         ▼
                      WebPage ──primaryImageOfPage──► ImageObject

Identifiers are derived from the site URL — …/#/schema/organization, …/#/schema/person/12, <permalink>#webpage, <permalink>#article — so they stay stable across requests without a table to keep in sync.

Nothing is invented. Every builder omits a property it cannot establish from real data, and returns nothing at all when the entity does not exist. The organization trust properties (publishingPrinciples, correctionsPolicy, ownershipFundingInfo, actionableFeedbackPolicy, diversityPolicy) are emitted only when you supply a URL for a page that genuinely exists. Invalid URLs in any list are discarded rather than published.

Validation

SEO → Schema validates the graph for a chosen post or the front page: unique @ids, resolvable references, absolute URLs, present @type, and the properties each type needs to be usable. Errors, warnings and notices are distinguished — and a graph with errors is withheld from the front end rather than published, because wrong structured data is worse than none.

The screen also lists other active structured-data emitters (Yoast, Rank Math, AIOSEO, SEOPress, Schema Pro, WooCommerce). Nothing is ever disabled automatically; you choose which source to turn off.

Per-post schema type lives on the editor's Schema tab. HowTo is deliberately absent: it requires the visible page to actually contain that content, and will be offered once the module can verify it.

FAQPage is derived, never chosen

FAQPage is not on the type list either, for the same reason — but it does not need to be, because the plugin can verify it. Content is read for a real question-and-answer structure: a heading ending in a question mark followed by its answer, or a <details>/<summary> disclosure block. Find two or more and the page node gains FAQPage alongside whatever it already was, carrying the questions as mainEntity. Find fewer and nothing is emitted.

A heading that merely starts with an interrogative word ("How we work") is a statement, not a question, so the question mark is required rather than inferred. Generating FAQ text with the AI assistant does not switch this on: the text has to be on the page first.

Read the full README on GitHub →