# BreznGEO ![PHP 8.0+](https://img.shields.io/badge/PHP-8.0%2B-blue) ![WordPress 6.0+](https://img.shields.io/badge/WordPress-6.0%2B-21759b) ![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0--or--later-green) ![Version](https://img.shields.io/badge/Version-1.2.2-orange) ![Tests](https://img.shields.io/badge/Tests-158%20passing-brightgreen) ๐Ÿ‡ฉ๐Ÿ‡ช [Deutsche Version โ†’ README.de.md](README.de.md) **Website:** [brezngeo.com](https://brezngeo.com)  ยท  [How To](https://brezngeo.com/howto.html)  ยท  [FAQ](https://brezngeo.com/faq.html)  ยท  [Changelog](https://brezngeo.com/changelog.html) --- 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](https://donau2space.de), a personal AI blog, where exactly this was needed โ€” and nothing more. --- ## Table of Contents - [Why This Plugin Exists](#why-this-plugin-exists) - [Directory Structure](#directory-structure) - [Features](#features) - [Data Storage](#data-storage) - [Security](#security) - [AI Providers](#ai-providers) - [Hooks & Extensibility](#hooks--extensibility) - [AJAX Endpoints](#ajax-endpoints) - [Installation](#installation) - [Tech Stack](#tech-stack) - [License](#license) --- ## 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 (150โ€“160 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 `` itself | **Token mode:** Either the full post content is sent (`full`) or it is trimmed to a configurable token count (100โ€“8000) (`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 `
` โ€” 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 ``. 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` | โ€” | `` + `` 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:** - 1โ€“20 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 H2โ€“H6 | | 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 `
` | | 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 (H1โ€“H6 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. --- ### Link Analysis (Dashboard) 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:" ``` `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: ```php 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: ```php 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 ```php // 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.php` โ†’ `register_hooks()`: ```php $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. ```php 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. ```php 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.php` โ†’ `load_dependencies()`: `require_once BREZNGEO_DIR . 'includes/Features/YourFeature.php';` 3. In `includes/Core.php` โ†’ `register_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](https://github.com/noschmarrn/brezngeo/releases/latest) 2. In WordPress go to *Plugins โ†’ Add New โ†’ Upload Plugin* **Manual (clone):** ```bash 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](https://www.gnu.org/licenses/gpl-2.0.html) Copyright (c) 2025โ€“2026 [Donau2Space](https://donau2space.de)