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.

FreeProAPI 1.0

Architecture

Public functions

Loaded only when requirements are met; guard calls with function_exists().

FunctionReturns
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 );
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

TemplateTheme 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 Proyour-theme/siteryn-seo-geo-aeo-pro/aeo/
prompts/*.php (AI task prompts) Proyour-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

WhereWhat
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_schemasSchema Generator items (absent = content type default).
siteryn_seo_geo_aeo_settingsAll settings (autoloaded).
siteryn_seo_geo_aeo_404_log · _audit404 log · last content audit.
siteryn_seo_geo_aeo_pro_settings · _pro_redirects ProPro settings · Redirect Manager rules.
_siteryn_sga_pro_index ProPer-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', '…' );

wp-config constants Pro

ConstantPurpose
SITERYN_SEO_GEO_AEO_PRO_{PROVIDER}_API_KEYAI key for one provider, e.g. …_OPENAI_API_KEY.
SITERYN_SEO_GEO_AEO_PRO_API_KEYOne key for every fixed-endpoint provider.
SITERYN_SEO_GEO_AEO_PRO_{PROVIDER}_BASE_URLPins the host for a constant key on endpoint providers.
SITERYN_SEO_GEO_AEO_PRO_LICENSE_ALLOW_HTTPDevelopment only: allow a non-HTTPS license server.

See the Hooks reference for every action and filter.