brezngeo/README.md
2026-03-28 10:54:01 +00:00

27 KiB
Raw Blame History

BreznGEO

PHP 8.0+ WordPress 6.0+ License: GPL-2.0 Version Tests

🇩🇪 Deutsche Version → README.de.md

Website: brezngeo.com  ·  How To  ·  FAQ  ·  Changelog


BreznGEO is a lightweight SEO & GEO plugin for WordPress. It generates AI-powered meta descriptions, outputs Schema.org structured data, creates GEO content blocks for AI engines, and manages crawler access via robots.txt and llms.txt — all in one plugin, nothing hidden behind a paywall.

It works with or without an AI key. It integrates without conflicts into Rank Math, Yoast, AIOSEO, and SEOPress. No SaaS. No telemetry. No upsells.


Why This Plugin Exists

Most WordPress SEO plugins have evolved in the same direction: bloated feature sets, dashboards full of metrics nobody needed, and a pricing model that locks the useful functionality behind a monthly subscription.

The AI wave made it worse. Plugins started offering "AI-powered" features — but as a proxy service. You pay a monthly fee, your content goes through their servers, they call the AI API on your behalf and add a margin on top.

BreznGEO takes a different approach:

  • Direct API access. You store your own key from OpenAI, Anthropic, Google, or xAI. BreznGEO calls the API directly. No middleman, no margin, no data passing through third-party servers.
  • Clear output, not noise. Meta descriptions, structured data, GEO content blocks, bot management. No readability scores, no keyword density meters, no upsell banners.
  • No subscription. GPL-2.0. Free to use on any number of sites. The only costs are API usage — typically fractions of a cent per post.
  • No telemetry. BreznGEO sends no data home. No usage tracking, no remote logging, no analytics leaving your server.
  • Works without AI. No API key? The fallback extractor generates a usable meta description from post content using sentence boundary detection. Every post gets a description.

Built in Passau, Bavaria — for Donau2Space, a personal AI blog, where exactly this was needed — and nothing more.


Table of Contents


Directory Structure

brezngeo/
├── brezngeo.php      # Plugin header, constants (BREZNGEO_VERSION, BREZNGEO_DIR, BREZNGEO_URL)
├── uninstall.php                 # Cleanup on plugin deletion
├── assets/
│   ├── admin.css                 # Shared admin stylesheet
│   ├── admin.js                  # Provider selector, connection test
│   ├── bulk.js                   # Bulk generator AJAX loop + progress UI
│   ├── editor-meta.js            # Meta editor box: live counter, AI regen button
│   ├── geo-editor.js             # GEO block editor: generate / clear button
│   ├── geo-frontend.css          # Minimal stylesheet for .brezngeo-geo on frontend
│   ├── keyword-analysis.js        # Keyword analysis: AJAX checks, result rendering, AI handlers
│   ├── link-suggest.js           # Internal link suggestions: trigger, UI, apply (Gutenberg + Classic)
│   └── seo-widget.js             # SEO analysis widget: live evaluation in editor
├── includes/
│   ├── Core.php                  # Singleton bootstrap, loads all dependencies
│   ├── Admin/
│   │   ├── AdminMenu.php         # Menu structure + dashboard render
│   │   ├── BulkPage.php          # Bulk generator admin page
│   │   ├── GeoEditorBox.php      # GEO block meta box in post editor
│   │   ├── GeoPage.php           # GEO block settings page
│   │   ├── KeywordMetaBox.php    # Keyword analysis meta box in post editor
│   │   ├── KeywordPage.php       # Keyword analysis settings page
│   │   ├── LinkAnalysis.php      # AJAX handler for link analysis dashboard
│   │   ├── LinkSuggestPage.php   # Internal link suggestions settings page
│   │   ├── MetaEditorBox.php     # Meta description meta box in post editor
│   │   ├── MetaPage.php          # Meta generator settings page
│   │   ├── ProviderPage.php      # AI provider settings page
│   │   ├── SchemaMetaBox.php     # Schema.org per-post meta box
│   │   ├── TxtPage.php           # TXT Files page: llms.txt + robots.txt (tabbed)
│   │   ├── SchemaPage.php        # Schema.org settings page
│   │   ├── SeoWidget.php         # SEO analysis sidebar widget
│   │   ├── SettingsPage.php      # Central getSettings() — merges all option keys
│   │   └── views/                # PHP templates for all admin pages
│   ├── Features/
│   │   ├── CrawlerLog.php        # Log AI bot visits (own DB table)
│   │   ├── GeoBlock.php          # GEO Quick Overview block (frontend output)
│   │   ├── LlmsTxt.php           # /llms.txt endpoint with ETag/cache
│   │   ├── LinkSuggest.php       # Internal link suggestions: matching engine + AJAX handler + meta box
│   │   ├── KeywordAnalysis.php    # Keyword analysis engine: 10 SEO checks
│   │   ├── MetaGenerator.php     # Core logic: AI call, save, bulk, AJAX
│   │   ├── RobotsTxt.php         # robots.txt bot blocking via WP filter
│   │   └── SchemaEnhancer.php    # JSON-LD Schema.org output in wp_head
│   ├── Helpers/
│   │   ├── BulkQueue.php         # Mutex lock for bulk processes (transient-based)
│   │   ├── FallbackMeta.php      # Meta extraction from post content without AI
│   │   ├── KeyVault.php          # API key obfuscation before writing to DB
│   │   ├── KeywordVariants.php   # Locale-aware keyword variant generation (EN/DE)
│   │   └── TokenEstimator.php    # Rough token estimate for cost preview in bulk
│   └── Providers/
│       ├── ProviderInterface.php # Interface: getId, getName, getModels, testConnection, generateText
│       ├── ProviderRegistry.php  # Registry pattern: register and retrieve providers
│       ├── AnthropicProvider.php # Claude API (Messages API)
│       ├── GeminiProvider.php    # Google Gemini (generateContent API)
│       ├── GrokProvider.php      # xAI Grok (OpenAI-compatible endpoint)
│       └── OpenAIProvider.php    # OpenAI GPT (Chat Completions API)
└── vendor/                       # Composer dependencies (production only)

Features

AI Meta Generator

Generates SEO-optimized meta descriptions (150160 characters) automatically when a post is published. The prompt is fully customizable; supported placeholders: {title}, {content}, {excerpt}, {language}.

Language detection: The target language is automatically detected from Polylang, WPML, or the WordPress locale and passed in the prompt — no configuration needed.

SEO plugin integration: Generated descriptions are written not only to _bre_meta_description but also directly to the native field of the active SEO plugin:

SEO Plugin Meta Field
Rank Math rank_math_description
Yoast SEO _yoast_wpseo_metadesc
AIOSEO _aioseo_description
SEOPress _seopress_titles_desc
(none active) BreznGEO outputs <meta name="description"> itself

Token mode: Either the full post content is sent (full) or it is trimmed to a configurable token count (1008000) (limit). Trimming is handled by TokenEstimator — a word-based estimate without external libraries, using a ratio of ~0.75 words per token.

Fallback without AI: FallbackMeta::extract() always delivers a usable description — even without an API key or on error. Extraction prefers sentence boundaries (. , ! , ? ), falls back to word boundaries, and only uses a hard cut with as a last resort. Fully multibyte-safe via mb_substr / mb_strrpos.


GEO Block (Quick Overview)

Generates AI-powered content blocks directly in post text for Generative Engine Optimization:

  • Summary — brief overview of the post
  • Key Points — bullet list of the most important points
  • FAQ — question-answer pairs (only above a configurable word threshold, default: 350 words)

Insert position (configurable): after the first paragraph (default), top, bottom.

Display modes:

Mode Behavior
details_collapsible Native HTML <details> — collapsed, no JavaScript needed
open_always Block always visible
store_only_no_frontend Store in DB only, no frontend output (e.g. for FAQPage schema)

All labels (title, summary, key points, FAQ), the accent color, color scheme (auto/light/dark), and custom CSS are configurable via the admin page — no coding required.

Per-post prompt add-on: Authors can enter a custom prompt addition via a meta box in the post editor that is appended to the base prompt. Can be enabled/disabled globally.


Schema.org Enhancer

Outputs JSON-LD structured data and meta tags in <head>. Settings under BreznGEO → Schema.org. Each type is individually toggleable:

Type Schema.org Type Notes
organization Organization Name, URL, logo, sameAs links (social profiles)
author Person Author name, profile URL, optional Twitter sameAs
speakable WebPage + SpeakableSpecification CSS selectors on H1 and first paragraph
article_about Article Headline, publish/modified, description, publisher
breadcrumb BreadcrumbList Automatically suppressed when Rank Math or Yoast is active
ai_meta_tags <meta name="robots"> + <meta name="googlebot"> with max-snippet:-1
faq_schema FAQPage Automatically populated from GEO block data
blog_posting BlogPosting / Article With embedded author and featured image
image_object ImageObject Featured image with dimensions and caption
video_object VideoObject YouTube/Vimeo automatically detected and embedded
howto HowTo Step-by-step guide — data via meta box in post editor
review Review Rating with ratingValue — data via meta box
recipe Recipe Ingredients, times, nutritional values — data via meta box
event Event Date, location, organizer — data via meta box

llms.txt

Serves /llms.txt and paginated follow-up files (/llms-2.txt, /llms-3.txt …) via a parse_request hook at priority 1 — before WordPress routing, no rewrite rule flush needed.

File structure:

# Site Title
> Description block

## Featured Links
- [Title](URL): Description

## Content
- [Title](URL): Date

---
## More
/llms-2.txt

HTTP caching:

  • ETag: "md5(content)" → HTTP 304 on If-None-Match
  • Last-Modified based on the most recent post_modified_gmt in the database
  • Cache-Control: public, max-age=3600
  • Transient cache is automatically invalidated on every settings change

Rank Math conflict notice: If Rank Math also wants to serve an llms.txt, BreznGEO shows an admin notice — BreznGEO takes precedence automatically due to priority 1.


robots.txt Manager

Appends Disallow blocks via the WordPress filter robots_txt — WordPress's own robots.txt is preserved; BreznGEO only extends it.

Supported AI bots (all individually toggleable):

User-Agent Operator
GPTBot OpenAI
ClaudeBot Anthropic
Google-Extended Google (Bard/Gemini training)
PerplexityBot Perplexity AI
CCBot Common Crawl
Applebot-Extended Apple AI
Bytespider ByteDance
DataForSeoBot DataForSEO
ImagesiftBot Imagesift
omgili Omgili
Diffbot Diffbot
FacebookBot Meta
Amazonbot Amazon

Bulk Generator

Batch processing of all published posts without a meta description. The process runs as a repeated AJAX request in the browser — no WP-Cron, no CLI required.

Technical details:

  • 120 posts per batch (configurable)
  • 6-second delay between batches (rate limiting against API limits)
  • Up to 3 attempts per post
  • Live progress log and running cost estimate in the admin UI
  • Mutex lock via transient (brezngeo_bulk_running, TTL 15 minutes): prevents parallel runs across multiple browser tabs or admin users. The lock is set at start, automatically released after the last batch — or manually via button.

Crawler Log

Logs visits from known AI bots in the dedicated database table {prefix}brezngeo_crawler_log:

Column Type Content
bot_name VARCHAR Name of the bot (e.g. GPTBot)
ip_hash CHAR(64) SHA-256 hash of the visitor IP
url VARCHAR(512) Requested URL
visited_at DATETIME Timestamp

Why SHA-256 instead of plain-text IP? The original IP is never stored. The hash satisfies the GDPR requirement of data minimization: bot patterns are identifiable (same hash = same IP), but tracing back to a person without the plain-text value is practically impossible.

Entries older than 90 days are automatically cleaned up via weekly cron (brezngeo_cleanup_crawler_log). The dashboard shows a 30-day summary per bot.


Keyword Analysis

Meta box in the post editor that analyzes keyword usage across ten on-page SEO checks:

Check What it evaluates
Title Keyword present in the post title
Headings Keyword appears in at least one H2H6
Density Keyword density within target range (default 0.5 %2.5 %)
Image Alt At least one image has the keyword in its alt attribute
Meta Description Keyword present in the meta description
Slug Keyword present in the URL slug
First Paragraph Keyword appears in the opening paragraph
Last Paragraph Keyword appears in the closing paragraph
Image Title/Caption Keyword in at least one image title or <figcaption>
Excerpt Keyword present in the post excerpt

Primary + secondary keywords: The primary keyword is evaluated against all ten checks with stricter thresholds. Secondary keywords support multiple entries and use relaxed minimums.

Update modes: live (debounced while typing), manual (button click), or save (on post save).

Locale-aware variants: KeywordVariants generates compound forms (space ↔ hyphen), trailing-s, and language-specific suffixes (EN: -es, -ing, -ed; DE: -er, -en, -e) to match keyword variations automatically.

Optional AI features (when an API key is configured):

  • Suggest — AI-generated keyword suggestions based on post content
  • Optimize — content optimization tips for the current keyword
  • Semantic — related semantic keywords for broader topic coverage

Meta Editor Box

Meta box in the post editor (Classic and Block Editor):

  • Source badge: AI generated / Fallback / Manual / Not generated
  • Text field with maxlength="160" and live character counter (JavaScript, no save needed)
  • "Regenerate with AI" button (only when API key is configured) — generates inline without page reload

SEO Analysis Widget

Sidebar meta box in the post editor with live evaluation while writing:

  • Title length (target: ≤ 60 characters)
  • Word count and estimated reading time
  • Heading hierarchy (H1H6 tree)
  • Counter for internal and external links
  • Inline warnings: no H2, no internal link, title too long

All stats are updated live via MutationObserver, no manual save needed.


AJAX widget on the plugin dashboard:

  • Posts with no internal links at all
  • Posts with an above-average number of external links
  • Top-5 pillar pages by number of incoming internal links

Results are cached for 1 hour in the transient cache (brezngeo_link_analysis).


Data Storage

WordPress Options (wp_options)

Option Key Content
brezngeo_settings Active provider, API keys (obfuscated), model selection, token costs, ai_enabled flag
brezngeo_meta_settings Meta generator: auto mode, post types, token mode, prompt
brezngeo_schema_settings Schema.org: enabled types, organization sameAs URLs
brezngeo_geo_settings GEO block: mode, position, labels, CSS, prompt, color scheme
brezngeo_robots_settings robots.txt: blocked bots
brezngeo_llms_settings llms.txt: title, description, featured links, footer, page count
brezngeo_keyword_settings Keyword analysis: update mode, target density, min occurrences, post types, debounce
brezngeo_usage_stats Accumulated token usage: tokens_in, tokens_out, count
brezngeo_first_activated Unix timestamp of first activation (used by welcome notice)

Post Meta (wp_postmeta)

Meta Key Content
_bre_meta_description Generated meta description
_bre_meta_source Source: ai / fallback / manual
_bre_bulk_failed Last error during bulk attempt
_bre_geo_summary GEO block summary
_bre_geo_bullets GEO block key points (JSON array)
_bre_geo_faq GEO block FAQ (JSON array)
_brezngeo_keyword_main Primary keyword
_brezngeo_keyword_secondary Secondary keywords (comma-separated)
_brezngeo_keyword_results Cached analysis results (JSON)

Custom Database Table

Table Purpose
{prefix}brezngeo_crawler_log AI bot visits (bot_name, ip_hash, url, visited_at)

Transients

Transient TTL Purpose
brezngeo_llms_cache_{n} 1 hour Cached llms.txt content per page
brezngeo_link_analysis 1 hour Dashboard link analysis result
brezngeo_bulk_running 15 minutes Mutex lock for bulk generator
brezngeo_meta_stats 5 minutes Dashboard meta coverage query result
brezngeo_crawler_summary 5 minutes Dashboard crawler summary (last 30 days)

Uninstall cleanup

uninstall.php removes on plugin deletion:

  • Options brezngeo_settings, brezngeo_keyword_settings
  • Post meta _brezngeo_meta_description, _brezngeo_keyword_main, _brezngeo_keyword_secondary, _brezngeo_keyword_results for all posts

Note: The remaining option keys and the brezngeo_crawler_log table are not automatically removed. For full cleanup, delete these manually.


Security

API Key Obfuscation (KeyVault)

Plaintext key  →  XOR(key, sha256(AUTH_KEY . SECURE_AUTH_KEY))  →  base64  →  "bre1:<base64>"

BreznGEO\Helpers\KeyVault obfuscates API keys before writing to wp_options:

  1. A 64-byte salt is derived from the WordPress constants AUTH_KEY and SECURE_AUTH_KEY via hash('sha256', ...).
  2. The plaintext is XOR'd byte-by-byte (salt is repeated as needed).
  3. The result is base64-encoded and prefixed with bre1:.

Why XOR and not AES? No openssl_* or external extension required — the code runs on any PHP 8.0+ installation without configuration. The bre1: prefix allows future migration to stronger encryption without a breaking change.

Security boundary: XOR with a static salt is obfuscation, not cryptographic encryption. An attacker with access to both the database and wp-config.php can reconstruct the key. For maximum security, keys can be defined as wp-config.php constants — these take precedence over the database version:

define( 'BREZNGEO_OPENAI_KEY',    'sk-...' );
define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' );
define( 'BREZNGEO_GEMINI_KEY',    'AI...' );
define( 'BREZNGEO_GROK_KEY',      'xai-...' );

In the admin UI, keys are always displayed masked: ••••••Ab3c9 (only the last 5 characters visible).

CSRF Protection and Capability Checks

Every AJAX handler follows the same pattern — without exception:

check_ajax_referer( 'brezngeo_admin', 'nonce' );          // CSRF
if ( ! current_user_can( 'manage_options' ) ) {      // Authorization
    wp_send_json_error( 'Unauthorized', 403 );
}

The nonce brezngeo_admin is passed to the frontend via wp_localize_script and validated server-side on every request. There are no wp_ajax_nopriv_ handlers — all AJAX endpoints are exclusively accessible to logged-in users with manage_options capability.

Input Validation and Output Escaping

  • All $_POST values are processed via wp_unslash() + specific sanitizers (sanitize_text_field, absint, wp_kses_post depending on context).
  • All output in admin views is escaped (esc_html, esc_attr, esc_url, esc_textarea).
  • SQL queries exclusively via $wpdb->prepare().
  • GEO Block custom CSS is sanitized through Helpers\Css::sanitize_declarations() — strips comments, braces, at-rules (@import, @media, etc.), url(), expression(), and javascript: before injection via wp_add_inline_style().

Privacy (GDPR)

The crawler log stores IP addresses exclusively as SHA-256 hashes. The original value is never persisted. Entries are automatically deleted after 90 days.


AI Providers

BreznGEO supports four providers, all implementing the same ProviderInterface:

Provider Class API Base URL
OpenAI OpenAIProvider https://api.openai.com/v1/chat/completions
Anthropic AnthropicProvider https://api.anthropic.com/v1/messages
Google Gemini GeminiProvider https://generativelanguage.googleapis.com/...
xAI Grok GrokProvider https://api.x.ai/v1/chat/completions

Adding a New Provider

// includes/Providers/YourProvider.php
namespace BreznGEO\Providers;

class YourProvider implements ProviderInterface {
    public function getId(): string    { return 'yourprovider'; }
    public function getName(): string  { return 'Your Provider'; }
    public function getModels(): array { return [ 'model-id' => 'Model Name' ]; }

    public function testConnection( string $api_key ): array {
        // Minimal API call — returns ['success' => bool, 'message' => string]
    }

    public function generateText( string $prompt, string $api_key, string $model, int $max_tokens = 300 ): string {
        // Call API, return plain text — throw \RuntimeException on error
    }
}

Register in includes/Core.phpregister_hooks():

$registry->register( new Providers\YourProvider() );

The provider automatically appears in all admin dropdowns, the provider settings page, and the cost overview of the bulk generator.


Hooks & Extensibility

brezngeo_prompt (Filter)

Allows modifying the final prompt immediately before the API call.

add_filter( 'brezngeo_prompt', function( string $prompt, WP_Post $post ): string {
    $keyword = get_post_meta( $post->ID, 'focus_keyword', true );
    return $keyword ? $prompt . "\nFocus keyword: {$keyword}" : $prompt;
}, 10, 2 );

brezngeo_meta_saved (Action)

Fired after a meta description is successfully saved — both on automatic generation at publish and on manual regen in the editor.

add_action( 'brezngeo_meta_saved', function( int $post_id, string $description ): void {
    // e.g. sync with external system or cache invalidation
    my_cdn_purge( get_permalink( $post_id ) );
}, 10, 2 );

Adding a New Feature

  1. Create includes/Features/YourFeature.php with a register() method that registers WordPress hooks.
  2. In includes/Core.phpload_dependencies(): require_once BREZNGEO_DIR . 'includes/Features/YourFeature.php';
  3. In includes/Core.phpregister_hooks(): ( new Features\YourFeature() )->register();

AJAX Endpoints

All endpoints are exclusively accessible to logged-in users with manage_options (no nopriv).

Action Handler Description
brezngeo_regen_meta MetaEditorBox::ajax_regen Regenerate meta description for a single post
brezngeo_test_connection ProviderPage::ajax_test_connection Test API key and connection
brezngeo_get_default_prompt ProviderPage::ajax_get_default_prompt Reset to default prompt
brezngeo_link_analysis LinkAnalysis::ajax_analyse Run link analysis for the dashboard
brezngeo_link_suggestions LinkSuggest::ajax_suggest Return top-10 internal link suggestions for current post
brezngeo_geo_generate GeoEditorBox::ajax_generate Generate GEO block for a single post
brezngeo_geo_clear GeoEditorBox::ajax_clear Clear GEO block data for a single post
brezngeo_llms_clear_cache TxtPage::ajax_clear_cache Clear llms.txt transient cache
brezngeo_dismiss_llms_notice LlmsTxt::ajax_dismiss_notice Dismiss Rank Math conflict admin notice
brezngeo_dismiss_welcome AdminMenu::ajax_dismiss_welcome Dismiss the welcome notice per user
brezngeo_bulk_generate MetaGenerator::ajaxBulkGenerate Process next batch in bulk generator
brezngeo_bulk_stats MetaGenerator::ajaxBulkStats Retrieve progress and stats of running bulk
brezngeo_bulk_release MetaGenerator::ajaxBulkRelease Manually release bulk mutex lock
brezngeo_bulk_status MetaGenerator::ajaxBulkStatus Check bulk lock status
brezngeo_keyword_analyze KeywordMetaBox::ajax_analyze Run keyword analysis for a post
brezngeo_keyword_ai_suggest KeywordMetaBox::ajax_ai_suggest AI keyword suggestions
brezngeo_keyword_ai_optimize KeywordMetaBox::ajax_ai_optimize AI content optimization tips
brezngeo_keyword_ai_semantic KeywordMetaBox::ajax_ai_semantic AI semantic keyword analysis

Installation

Via GitHub Release (recommended):

  1. Download brezngeo.zip from the latest release
  2. In WordPress go to Plugins → Add New → Upload Plugin

Manual (clone):

cd /path/to/wordpress/wp-content/plugins/
git clone https://github.com/noschmarrn/brezngeo.git brezngeo
wp plugin activate brezngeo

After activation:

  1. Go to BreznGEO → AI Provider, select your provider and enter your API key
  2. Run the connection test
  3. Go to Meta Generator, enable auto mode and select post types

The plugin has no JavaScript build step. All assets under assets/ are direct JS/CSS files without transpiling or bundling.


Tech Stack

Component Technology
Backend PHP 8.0+, WordPress Plugin API
Namespace BreznGEO\
Architecture Singleton core, registry pattern (providers), feature classes with register()
Database WordPress Options API, wpdb (custom table for CrawlerLog)
Caching WordPress transients (llms.txt, link analysis, bulk lock)
Frontend Vanilla JS + jQuery (WordPress-bundled), no build step
i18n .pot file, text domain brezngeo
Tests PHPUnit (158 tests, 301 assertions)
Coding standard WordPress PHPCS
License GPL-2.0-or-later

License

GPL-2.0-or-later — https://www.gnu.org/licenses/gpl-2.0.html

Copyright (c) 20252026 Donau2Space