Developer guide
How the plugin is built and how to extend it: meta, robots, analysis, structured data, breadcrumbs, templates and Pro features. Every example is checked against the code.
Architecture
- Namespaces:
Siteryn_SEO_GEO_AEO\(Free) andSiteryn_SEO_GEO_AEO_Pro\(Pro), autoloaded fromincludes/. Public functions are global. - Boot: Free runs on
plugins_loadedand firessiteryn_seo_geo_aeo_loadedwhen ready.Plugin::API_VERSIONis1.0. Optional modules (WooCommerce) are registered throughsiteryn_seo_geo_aeo_modules. - Pro boots on
siteryn_seo_geo_aeo_loadedand starts its features oninit(15) only with an active license. It extends Free only through hooks and public classes. - No REST routes, custom tables or cron in Free. Every endpoint is admin-ajax or admin-post.
Public functions
Loaded only when requirements are met; guard calls with function_exists().
| Function | Returns |
|---|---|
siteryn_seo_geo_aeo_get_seo_field( $object_id, $field, $type = 'post' ) | A saved field: title, description, focus_keyword, canonical, robots_index, robots_follow, social_title, social_description, social_image, twitter_title, twitter_description, twitter_image. $type: post, product or term. |
siteryn_seo_geo_aeo_get_meta( $post_id ) | Resolved title, description, canonical, robots, image, og, twitter. |
siteryn_seo_geo_aeo_analyze( $post_id, $overrides = array() ) | {score, checks}. |
siteryn_seo_geo_aeo_analyze_product( $product_id, $overrides = array() ) | Product analysis (WooCommerce). |
siteryn_seo_geo_aeo_get_head_data( $product_id ) | {title, description, og} for a product. |
siteryn_seo_geo_aeo_breadcrumbs( $args = array() ) | Breadcrumb markup. Args separator, home, echo. |
Titles, placeholders & robots
add_filter( 'siteryn_seo_geo_aeo_title_vars', function ( array $vars, $template ) {
$vars['year'] = wp_date( 'Y' ); // key must match [a-z_]+
return $vars;
}, 10, 2 );
add_filter( 'siteryn_seo_geo_aeo_resolved_meta', function ( array $meta, array $request ) {
if ( 'singular' !== ( $request['type'] ?? '' ) || 'event' !== ( $request['object_type'] ?? '' ) ) {
return $meta;
}
$id = (int) $request['object_id'];
if ( '' === siteryn_seo_geo_aeo_get_seo_field( $id, 'title' ) ) {
$meta['title'] = wp_strip_all_tags( get_the_title( $id ) ) . ' – Events – ' . get_bloginfo( 'name' );
if ( isset( $meta['og']['og:title'] ) ) {
$meta['og']['og:title'] = $meta['title'];
}
}
return $meta;
}, 30, 2 ); // after Pro's Meta Templates (20)
add_filter( 'siteryn_seo_geo_aeo_default_robots', function ( array $robots, array $request ) {
$path = (string) wp_parse_url( (string) ( $request['url'] ?? '' ), PHP_URL_PATH );
if ( '/thank-you/' === trailingslashit( $path ) ) {
$robots['noindex'] = true;
}
return $robots;
}, 30, 2 );
// Use siteryn_seo_geo_aeo_robots to override even saved per-entry values.
$request has type (front_page, home, singular, term, author, date, search, post_type_archive, 404, other), object_id, object_type, url and paged. Robots filters apply while "SEO Title, Description, Canonical & Robots" is on.
Custom checks & analyzers
add_filter( 'siteryn_seo_geo_aeo_post_checks', function ( array $checks, array $context, array $values ) {
$has_h2 = (bool) array_filter( $context['structure']['headings'], function ( $h ) {
return 2 === $h['level'];
} );
$checks[] = \Siteryn_SEO_GEO_AEO\Seo\Analyzer::check(
'my_has_h2', 'content', $has_h2 ? 'good' : 'warning',
'Subheadings',
$has_h2 ? 'The content uses H2 subheadings.' : 'Add H2 subheadings to structure longer content.',
2 // weight
);
return $checks;
}, 10, 3 );
Status is good, warning or problem. Also: term_checks, product_checks, geo_checks, aeo_checks, geo_site_checks, health_checks. $context includes title, slug, url, text, word_count, terms and structure (headings, paragraphs, lists, tables, images, links, sections).
For a whole new analysis with its own score, implement Contracts\AnalyzerInterface (id(), label(), checks( $context, $overrides )) and add it with siteryn_seo_geo_aeo_analyzers. The ids seo, geo and aeo are reserved.
Structured data
add_filter( 'siteryn_seo_geo_aeo_schema_graph', function ( array $graph, array $request ) {
foreach ( $graph as &$piece ) {
if ( isset( $piece['@id'] ) && home_url( '/' ) . '#organization' === $piece['@id'] ) {
$piece['foundingDate'] = '2015'; // only facts the site shows
}
}
unset( $piece );
return $graph;
}, 10, 2 );
add_filter( 'siteryn_seo_geo_aeo_schema_pieces', function ( array $pieces, array $request ) {
require_once __DIR__ . '/class-event-piece.php';
$pieces[] = new My_Event_Piece();
return $pieces;
}, 10, 2 );
class My_Event_Piece implements \Siteryn_SEO_GEO_AEO\Contracts\SchemaPieceInterface {
public function applies( array $request, array $graph ) {
return 'singular' === $request['type'] && 'event' === $request['object_type'];
}
public function generate( array $request, array $graph ) {
$start = get_post_meta( (int) $request['object_id'], 'event_start', true );
if ( ! $start ) { return array(); }
return array( array(
'@type' => 'Event',
'@id' => $request['url'] . '#event',
'name' => get_the_title( (int) $request['object_id'] ),
'startDate' => $start,
'mainEntityOfPage' => array( '@id' => $request['url'] . '#webpage' ),
'organizer' => array( '@id' => home_url( '/' ) . '#organization' ),
) );
}
}
@id conventions: {home}#organization, #website, #logo, {url}#webpage, #breadcrumb, #primaryimage, #faq, #article. A custom Schema Generator variable:
add_filter( 'siteryn_seo_geo_aeo_schema_variable', function ( $value, $name, $param, $post ) {
return 'isbn' === $name ? (string) get_post_meta( $post->ID, 'book_isbn', true ) : $value;
}, 10, 4 );
Breadcrumbs
add_filter( 'siteryn_seo_geo_aeo_breadcrumbs', function ( array $trail, array $request ) {
// Each item: array( 'name' => …, 'url' => … ). Items with an empty name are dropped.
return $trail;
}, 10, 2 );
// Markup only: siteryn_seo_geo_aeo_breadcrumbs_html( $html, $trail, $args )
Sitemaps & settings at runtime
// Core filter; the plugin filters at priority 20, so run later.
add_filter( 'wp_sitemaps_post_types', function ( $types ) {
unset( $types['event'] );
return $types;
}, 30 );
// Or override settings at runtime (never saved):
add_filter( 'siteryn_seo_geo_aeo_settings_get_all', function ( $s ) {
$s['sitemap_users'] = false;
return $s;
} );
Capabilities
Three meta capabilities, nothing added to roles: siteryn_seo_geo_aeo_manage_settings (→ manage_options), siteryn_seo_geo_aeo_edit_post_seo (→ edit_post) and siteryn_seo_geo_aeo_edit_term_seo (→ edit_term).
add_filter( 'siteryn_seo_geo_aeo_map_capability', function ( $caps, $cap ) {
return 'siteryn_seo_geo_aeo_manage_settings' === $cap ? array( 'manage_woocommerce' ) : $caps;
}, 10, 2 );
// Pro owner actions (license, AI, redirects) also need siteryn_seo_geo_aeo_pro_owner_capability.
Templates
| Template | Theme path |
|---|---|
head-meta.php (description, archive canonical, OG, X) | your-theme/siteryn-seo-geo-aeo/head-meta.php |
aeo/definition.php, how-to.php, comparison.php, faq.php Pro | your-theme/siteryn-seo-geo-aeo-pro/aeo/ |
prompts/*.php (AI task prompts) Pro | your-theme/siteryn-seo-geo-aeo-pro/prompts/ |
head-meta.php receives $args with description, canonical, og and twitter. An outdated theme copy triggers an admin notice (Free templates only).
Data
| Where | What |
|---|---|
Post and term meta _siteryn_sga_{title, description, focus_keyword, canonical, robots_index, robots_follow, social_title, social_description, social_image, twitter_title, twitter_description, twitter_image} | The SEO fields. Not registered for REST. |
_siteryn_sga_schemas | Schema Generator items (absent = content type default). |
siteryn_seo_geo_aeo_settings | All settings (autoloaded). |
siteryn_seo_geo_aeo_404_log · _audit | 404 log · last content audit. |
siteryn_seo_geo_aeo_pro_settings · _pro_redirects Pro | Pro settings · Redirect Manager rules. |
_siteryn_sga_pro_index Pro | Per-entry audit row for the insights. |
Pro extension points Pro
add_filter( 'siteryn_seo_geo_aeo_pro_llms_txt_lines', function ( array $lines ) {
$lines[] = '';
$lines[] = '## Docs';
$lines[] = '- [Developer guide](' . esc_url_raw( home_url( '/docs/' ) ) . '): How to extend the site.';
return $lines;
} );
// llms.txt is cached for a day: \Siteryn_SEO_GEO_AEO_Pro\Geo\LlmsTxt::flush();
add_filter( 'siteryn_seo_geo_aeo_pro_ai_providers', function ( $providers ) {
require_once __DIR__ . '/class-together-provider.php';
$p = new My_Together_Provider();
$providers[ $p->get_id() ] = $p; // key must equal get_id()
return $providers;
} );
class My_Together_Provider extends \Siteryn_SEO_GEO_AEO_Pro\AI\Providers\OpenAICompatibleProvider {
public function get_id() { return 'together'; }
public function get_label() { return 'Together AI'; }
public function get_default_model() { return 'meta-llama/Llama-3.3-70B-Instruct-Turbo'; }
public function get_suggested_models() { return array( $this->get_default_model() ); }
protected function base_url( \Siteryn_SEO_GEO_AEO_Pro\AI\ProviderConfig $config ) {
return 'https://api.together.xyz/v1';
}
}
// wp-config.php: define( 'SITERYN_SEO_GEO_AEO_PRO_TOGETHER_API_KEY', '…' );
- Your own Pro feature: implement
Siteryn_SEO_GEO_AEO_Pro\Contracts\FeatureInterface(and optionallyAdminTabsInterfacefor tabs and fields) and callFeatures::register()onsiteryn_seo_geo_aeo_pro_register_features. - Reports: react to
siteryn_seo_geo_aeo_pro_audit_indexed,_seo_report_built,_crawl_completed; change them with_seo_report,_geo_report,_aeo_report,_crawl_report,_catalog_report. - Tuning:
_intent_markers,_ai_crawlers,_brand_taxonomies,_identifier_meta_keys,_address_params.
wp-config constants Pro
| Constant | Purpose |
|---|---|
SITERYN_SEO_GEO_AEO_PRO_{PROVIDER}_API_KEY | AI key for one provider, e.g. …_OPENAI_API_KEY. |
SITERYN_SEO_GEO_AEO_PRO_API_KEY | One key for every fixed-endpoint provider. |
SITERYN_SEO_GEO_AEO_PRO_{PROVIDER}_BASE_URL | Pins the host for a constant key on endpoint providers. |
SITERYN_SEO_GEO_AEO_PRO_LICENSE_ALLOW_HTTP | Development only: allow a non-HTTPS license server. |
See the Hooks reference for every action and filter.